chore: FF devel into master

This commit is contained in:
Atanas Janeshliev
2024-12-16 12:22:45 +01:00
91 changed files with 1987 additions and 2572 deletions

View File

@ -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.
<!--- Provide a general summary of the issue in the Title above -->
## Expected Behavior
<!--- Tell us what should happen -->
## Current Behavior
<!--- Tell us what happens instead of the expected behavior -->
## Possible Solution
<!--- Not obligatory, but suggest a fix/reason for the bug, -->
## Steps to Reproduce
<!--- Provide a link to a live example, or an unambiguous set of steps to -->
<!--- reproduce this bug. Include code to reproduce, if relevant -->
1.
2.
3.
4.
## Version Information
<!--- Which version of the app(s) were you using when you experienced this issue? -->
## Context (Environment)
<!--- How has this issue affected you? What are you trying to accomplish? -->
<!--- Providing context helps us come up with a solution that is most useful in the real world -->
## Detailed Description
<!--- Provide a detailed description of the change or addition you are proposing -->
## Possible Implementation
<!--- Not obligatory, but suggest an idea for implementing addition or change -->

1
.gitlab/CODEOWNERS Normal file
View File

@ -0,0 +1 @@
* @go/bridge-ppl/devs

View File

@ -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: By making a contribution to this project:
1. I assign any and all copyright related to the contribution to Proton AG; 1. You assign any and all copyright related to the contribution to Proton AG;
2. I certify that the contribution was created in whole by me; 2. You certify that the contribution was created in whole by you;
3. I understand and agree that this project and the contribution are public 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.
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.

2
go.mod
View File

@ -7,7 +7,7 @@ toolchain go1.21.9
require ( require (
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557 github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
github.com/Masterminds/semver/v3 v3.2.0 github.com/Masterminds/semver/v3 v3.2.0
github.com/ProtonMail/gluon v0.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-autostart v0.0.0-20210130080809-00ed301c8e9a
github.com/ProtonMail/go-proton-api v0.4.1-0.20240918100656-b4860af56d47 github.com/ProtonMail/go-proton-api v0.4.1-0.20240918100656-b4860af56d47
github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton

16
go.sum
View File

@ -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-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 h1:yc9daCCYUefEs69zUkSzubzjBbL+cmOXgnmt9Fyd9ug=
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo= github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
github.com/ProtonMail/gluon v0.17.1-0.20240923151549-d23b4bec3602 h1:EoMjWlC32tg46L/07hWoiZfLkqJyxVMcsq4Cyn+Ofqc= github.com/ProtonMail/gluon v0.17.1-0.20241121121545-aa1cfd19b4b2 h1:iZjKvjb6VkGb52ZaBBiXC1MGYJN4C/S97JfppdzpMHQ=
github.com/ProtonMail/gluon v0.17.1-0.20240923151549-d23b4bec3602/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8= github.com/ProtonMail/gluon v0.17.1-0.20241121121545-aa1cfd19b4b2/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/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4= github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4= github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE= github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=

View File

@ -208,6 +208,7 @@ func New() *cli.App {
// We override the default help value because we want "Show" to be capitalized // We override the default help value because we want "Show" to be capitalized
cli.HelpFlag = &cli.BoolFlag{ cli.HelpFlag = &cli.BoolFlag{
Name: "help", Name: "help",
Aliases: []string{"h"},
Usage: "Show help", Usage: "Show help",
DisableDefaultText: true, DisableDefaultText: true,
} }

View File

@ -636,14 +636,6 @@ func loadTLSConfig(vault *vault.Vault) (*tls.Config, error) {
}, nil }, nil
} }
func min(a, b time.Duration) time.Duration { //nolint:predeclared
if a < b {
return a
}
return b
}
func (bridge *Bridge) HasAPIConnection() bool { func (bridge *Bridge) HasAPIConnection() bool {
return bridge.api.GetStatus() == proton.StatusUp return bridge.api.GetStatus() == proton.StatusUp
} }
@ -723,3 +715,18 @@ func (bridge *Bridge) PushDistinctObservabilityMetrics(errType observability.Dis
func (bridge *Bridge) ModifyObservabilityHeartbeatInterval(duration time.Duration) { func (bridge *Bridge) ModifyObservabilityHeartbeatInterval(duration time.Duration) {
bridge.observabilityService.ModifyHeartbeatInterval(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
}

View File

@ -25,7 +25,6 @@ import (
"github.com/ProtonMail/go-proton-api" "github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/proton-bridge/v3/internal/constants" "github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/logging" "github.com/ProtonMail/proton-bridge/v3/internal/logging"
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/vault" "github.com/ProtonMail/proton-bridge/v3/internal/vault"
) )
@ -80,12 +79,6 @@ func (bridge *Bridge) ReportBug(ctx context.Context, report *ReportBugReq) error
return err 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 // if we have a token we can append more attachment to the bugReport
for i, att := range attachments { for i, att := range attachments {
if i == 0 && report.IncludeLogs { if i == 0 && report.IncludeLogs {

View File

@ -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 <https://www.gnu.org/licenses/>.
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)
}

View File

@ -73,15 +73,15 @@ func (h *heartBeatState) init(bridge *Bridge, manager telemetry.HeartbeatManager
for _, user := range bridge.users { for _, user := range bridge.users {
if user.GetAddressMode() == vault.SplitMode { if user.GetAddressMode() == vault.SplitMode {
splitMode = true splitMode = true
break
} }
h.SetUserPlan(user.GetUserPlanName())
} }
var nbAccount = len(bridge.users) var numberConnectedAccounts = len(bridge.users)
h.SetNbAccount(nbAccount) h.SetNumberConnectedAccounts(numberConnectedAccounts)
h.SetSplitMode(splitMode) h.SetSplitMode(splitMode)
// Do not try to send if there is no user yet. // Do not try to send if there is no user yet.
if nbAccount > 0 { if numberConnectedAccounts > 0 {
defer h.start() defer h.start()
} }
}, bridge.usersLock) }, bridge.usersLock)

View File

@ -17,7 +17,9 @@
package bridge package bridge
import "github.com/sirupsen/logrus" import (
"github.com/sirupsen/logrus"
)
func (bridge *Bridge) GetCurrentUserAgent() string { func (bridge *Bridge) GetCurrentUserAgent() string {
return bridge.identifier.GetUserAgent() return bridge.identifier.GetUserAgent()
@ -30,6 +32,8 @@ func (bridge *Bridge) SetCurrentPlatform(platform string) {
func (bridge *Bridge) setUserAgent(name, version string) { func (bridge *Bridge) setUserAgent(name, version string) {
currentUserAgent := bridge.identifier.GetClientString() currentUserAgent := bridge.identifier.GetClientString()
bridge.heartbeat.SetContactedByAppleNotes(name)
bridge.identifier.SetClient(name, version) bridge.identifier.SetClient(name, version)
newUserAgent := bridge.identifier.GetClientString() newUserAgent := bridge.identifier.GetClientString()
@ -54,6 +58,7 @@ func (b *bridgeUserAgentUpdater) HasClient() bool {
} }
func (b *bridgeUserAgentUpdater) SetClient(name, version string) { func (b *bridgeUserAgentUpdater) SetClient(name, version string) {
b.heartbeat.SetContactedByAppleNotes(name)
b.identifier.SetClient(name, version) b.identifier.SetClient(name, version)
} }

View File

@ -26,6 +26,7 @@ import (
imapEvents "github.com/ProtonMail/gluon/events" imapEvents "github.com/ProtonMail/gluon/events"
"github.com/ProtonMail/proton-bridge/v3/internal/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/services/imapsmtpserver"
"github.com/ProtonMail/proton-bridge/v3/internal/unleash"
"github.com/ProtonMail/proton-bridge/v3/internal/useragent" "github.com/ProtonMail/proton-bridge/v3/internal/useragent"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -93,6 +94,10 @@ func (b *bridgeIMAPSettings) LogServer() bool {
return b.b.logIMAPServer return b.b.logIMAPServer
} }
func (b *bridgeIMAPSettings) DisableIMAPAuthenticate() bool {
return b.b.unleashService.GetFlagValue(unleash.IMAPAuthenticateCommandDisabled)
}
func (b *bridgeIMAPSettings) Port() int { func (b *bridgeIMAPSettings) Port() int {
return b.b.vault.GetIMAPPort() return b.b.vault.GetIMAPPort()
} }

View File

@ -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, // 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. // which we need at next startup to decrypt the vault.
func (bridge *Bridge) FactoryReset(ctx context.Context) { func (bridge *Bridge) FactoryReset(ctx context.Context) {
useTelemetry := !bridge.GetTelemetryDisabled()
// Delete all the users. // Delete all the users.
safe.Lock(func() { safe.Lock(func() {
for _, user := range bridge.users { for _, user := range bridge.users {
bridge.logoutUser(ctx, user, true, true, useTelemetry) bridge.logoutUser(ctx, user, true, true)
} }
}, bridge.usersLock) }, bridge.usersLock)

View File

@ -28,7 +28,6 @@ type Locator interface {
ProvideLogsPath() (string, error) ProvideLogsPath() (string, error)
ProvideGluonCachePath() (string, error) ProvideGluonCachePath() (string, error)
ProvideGluonDataPath() (string, error) ProvideGluonDataPath() (string, error)
ProvideStatsPath() (string, error)
GetLicenseFilePath() string GetLicenseFilePath() string
GetDependencyLicensesLink() string GetDependencyLicensesLink() string
Clear(...string) error Clear(...string) error

View File

@ -33,6 +33,7 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/safe" "github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice" "github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice"
"github.com/ProtonMail/proton-bridge/v3/internal/try" "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/user"
"github.com/ProtonMail/proton-bridge/v3/internal/vault" "github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
@ -255,7 +256,7 @@ func (bridge *Bridge) LogoutUser(ctx context.Context, userID string) error {
return ErrNoSuchUser return ErrNoSuchUser
} }
bridge.logoutUser(ctx, user, true, false, false) bridge.logoutUser(ctx, user, true, false)
bridge.publish(events.UserLoggedOut{ bridge.publish(events.UserLoggedOut{
UserID: userID, UserID: userID,
@ -280,7 +281,7 @@ func (bridge *Bridge) DeleteUser(ctx context.Context, userID string) error {
} }
if user, ok := bridge.users[userID]; ok { 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 { if err := imapservice.DeleteSyncState(syncConfigDir, userID); err != nil {
@ -358,7 +359,7 @@ func (bridge *Bridge) SendBadEventUserFeedback(_ context.Context, userID string,
return user.BadEventFeedbackResync(ctx) return user.BadEventFeedbackResync(ctx)
} }
bridge.logoutUser(ctx, user, true, false, false) bridge.logoutUser(ctx, user, true, false)
bridge.publish(events.UserLoggedOut{ bridge.publish(events.UserLoggedOut{
UserID: userID, UserID: userID,
@ -527,11 +528,6 @@ func (bridge *Bridge) addUserWithVault(
vault *vault.User, vault *vault.User,
isNew bool, isNew bool,
) error { ) error {
statsPath, err := bridge.locator.ProvideStatsPath()
if err != nil {
return fmt.Errorf("failed to get Statistics directory: %w", err)
}
syncSettingsPath, err := bridge.locator.ProvideIMAPSyncConfigPath() syncSettingsPath, err := bridge.locator.ProvideIMAPSyncConfigPath()
if err != nil { if err != nil {
return fmt.Errorf("failed to get IMAP sync config path: %w", err) return fmt.Errorf("failed to get IMAP sync config path: %w", err)
@ -546,7 +542,6 @@ func (bridge *Bridge) addUserWithVault(
bridge.panicHandler, bridge.panicHandler,
bridge.vault.GetShowAllMail(), bridge.vault.GetShowAllMail(),
bridge.vault.GetMaxSyncMemory(), bridge.vault.GetMaxSyncMemory(),
statsPath,
bridge, bridge,
bridge.serverManager, bridge.serverManager,
bridge.serverManager, bridge.serverManager,
@ -589,9 +584,12 @@ func (bridge *Bridge) addUserWithVault(
// Finally, save the user in the bridge. // Finally, save the user in the bridge.
safe.Lock(func() { safe.Lock(func() {
bridge.users[apiUser.ID] = user bridge.users[apiUser.ID] = user
bridge.heartbeat.SetNbAccount(len(bridge.users)) bridge.heartbeat.SetNumberConnectedAccounts(len(bridge.users))
}, bridge.usersLock) }, 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. // As we need at least one user to send heartbeat, try to send it.
bridge.heartbeat.start() bridge.heartbeat.start()
@ -610,26 +608,21 @@ func (bridge *Bridge) newVaultUser(
return bridge.vault.GetOrAddUser(apiUser.ID, apiUser.Name, apiUser.Email, authUID, authRef, saltedKeyPass) return bridge.vault.GetOrAddUser(apiUser.ID, apiUser.Name, apiUser.Email, authUID, authRef, saltedKeyPass)
} }
// logout logs out the given user, optionally logging them out from the API too. // logoutUser logs out the given user, optionally logging them out from the API and deleting user related gluon data.
func (bridge *Bridge) logoutUser(ctx context.Context, user *user.User, withAPI, withData, withTelemetry bool) { func (bridge *Bridge) logoutUser(ctx context.Context, user *user.User, withAPI, withData bool) {
defer delete(bridge.users, user.ID()) defer delete(bridge.users, user.ID())
// if this is actually a remove account
if withData && withAPI {
user.SendConfigStatusAbort(ctx, withTelemetry)
}
logUser.WithFields(logrus.Fields{ logUser.WithFields(logrus.Fields{
"userID": user.ID(), "userID": user.ID(),
"withAPI": withAPI, "withAPI": withAPI,
"withData": withData, "withData": withData,
}).Debug("Logging out user") }).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") logUser.WithError(err).Error("Failed to logout user")
} }
bridge.heartbeat.SetNbAccount(len(bridge.users)) bridge.heartbeat.SetNumberConnectedAccounts(len(bridge.users) - 1)
user.Close() user.Close()
} }

View File

@ -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) { func (bridge *Bridge) handleUserDeauth(ctx context.Context, user *user.User) {
safe.Lock(func() { safe.Lock(func() {
bridge.logoutUser(ctx, user, false, false, false) bridge.logoutUser(ctx, user, false, false)
user.ReportConfigStatusFailure("User deauth.")
}, bridge.usersLock) }, bridge.usersLock)
} }

View File

@ -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 <https://www.gnu.org/licenses/>.
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
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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)
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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(),
},
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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)
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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)
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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,
},
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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)
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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(),
},
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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)
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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
}

View File

@ -30,7 +30,7 @@ using namespace bridgepp;
namespace { namespace {
QString const defaultKeychain = "defaultKeychain"; ///< The default keychain. 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] request The request
/// \param[in] writer The writer /// \param[in] writer The writer

View File

@ -97,9 +97,6 @@ public: // member functions.
grpc::Status IsTLSCertificateInstalled(::grpc::ServerContext *, ::google::protobuf::Empty const*, ::google::protobuf::BoolValue *response) override; 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 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 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 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; 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. bool sendEvent(bridgepp::SPStreamEvent const &event); ///< Queue an event for sending through the event stream.

View File

@ -303,7 +303,6 @@ void QMLBackend::openExternalLink(QString const &url) {
HANDLE_EXCEPTION( HANDLE_EXCEPTION(
QString const u = url.isEmpty() ? bridgeKBUrl : url; QString const u = url.isEmpty() ? bridgeKBUrl : url;
QDesktopServices::openUrl(u); 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);
)
}
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
// //
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************

View File

@ -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 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 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 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 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. void userNotificationDismissed(); ///< Slot to pop the notification from the stack and display the rest.

View File

@ -83,7 +83,6 @@ SettingsView {
onClicked: { onClicked: {
Backend.updateCurrentMailClient(); Backend.updateCurrentMailClient();
Backend.notifyReportBugClicked();
root.parent.showBugReport(); root.parent.showBugReport();
} }
} }

View File

@ -29,6 +29,7 @@ FocusScope {
property alias username: usernameTextField.text property alias username: usernameTextField.text
property var wizard property var wizard
property string hvLinkUrl: "" property string hvLinkUrl: ""
property bool hvLinkClicked: false
signal loginAbort(string username, bool wasSignedOut) signal loginAbort(string username, bool wasSignedOut)
@ -49,6 +50,7 @@ FocusScope {
} }
passwordTextField.hidePassword(); passwordTextField.hidePassword();
secondPasswordTextField.hidePassword(); secondPasswordTextField.hidePassword();
hvLinkClicked = false;
} }
function resetViaHv() { function resetViaHv() {
usernameTextField.enabled = false; usernameTextField.enabled = false;
@ -56,6 +58,7 @@ FocusScope {
signInButton.loading = true; signInButton.loading = true;
secondPasswordButton.loading = false; secondPasswordButton.loading = false;
secondPasswordTextField.enabled = true; secondPasswordTextField.enabled = true;
hvLinkClicked = false;
totpLayout.reset(); totpLayout.reset();
} }
@ -562,6 +565,7 @@ FocusScope {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
Qt.openUrlExternally(hvLinkUrl); Qt.openUrlExternally(hvLinkUrl);
hvLinkClicked = true;
} }
} }
} }
@ -574,7 +578,8 @@ FocusScope {
id: hVContinueButton id: hVContinueButton
Layout.fillWidth: true Layout.fillWidth: true
colorScheme: wizard.colorScheme colorScheme: wizard.colorScheme
text: qsTr("Continue") text: qsTr("Ive completed the verification")
enabled: hvLinkClicked
function checkAndSignInHv() { function checkAndSignInHv() {
console.assert(stackLayout.currentIndex === Login.RootStack.HV || stackLayout.currentIndex === Login.RootStack.MailboxPassword, "Unexpected checkInAndSignInHv") console.assert(stackLayout.currentIndex === Login.RootStack.HV || stackLayout.currentIndex === Login.RootStack.MailboxPassword, "Unexpected checkInAndSignInHv")

View File

@ -1572,32 +1572,6 @@ UPClientContext GRPCClient::clientContext() const {
return ctx; 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__);
}
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
// //

View File

@ -232,11 +232,6 @@ signals:
void syncFinished(QString const &userID); void syncFinished(QString const &userID);
void syncProgress(QString const &userID, double progress, qint64 elapsedMs, qint64 remainingMs); 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 public: // keychain related calls
grpc::Status availableKeychains(QStringList &outKeychains); grpc::Status availableKeychains(QStringList &outKeychains);
grpc::Status currentKeychain(QString &outKeychain); grpc::Status currentKeychain(QString &outKeychain);

View File

@ -159,7 +159,10 @@ func (f *frontendCLI) loginAccount(c *ishell.Context) {
hvDetails, hvErr := hv.VerifyAndExtractHvRequest(err) hvDetails, hvErr := hv.VerifyAndExtractHvRequest(err)
if hvErr != nil || hvDetails != nil { if hvErr != nil || hvDetails != nil {
if hvErr != 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 return
} }
f.promptHvURL(hvDetails) f.promptHvURL(hvDetails)

View File

@ -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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 0x74, 0x79, 0x12, 0x4f, 0x0a, 0x19, 0x49, 0x73, 0x54, 0x4c, 0x53, 0x43, 0x65, 0x72, 0x74, 0x69,
0x6c, 0x69, 0x63, 0x6b, 0x65, 0x64, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x65, 0x64, 0x12,
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,
0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 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, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x18, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61,
0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x6c, 0x75, 0x65, 0x12, 0x47, 0x0a, 0x15, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x54, 0x4c,
0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x53, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x16, 0x2e, 0x67,
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, 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, 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, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x4d, 0x0a, 0x15,
0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x54, 0x4c, 0x53, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69,
0x6e, 0x4d, 0x61, 0x69, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x6e, 0x2d, 0x62, 0x72, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
0x64, 0x67, 0x65, 0x2f, 0x76, 0x33, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61,
0x67, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 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 ( var (
@ -5938,81 +5924,75 @@ var file_bridge_proto_depIdxs = []int32{
80, // 121: grpc.Bridge.LogoutUser:input_type -> google.protobuf.StringValue 80, // 121: grpc.Bridge.LogoutUser:input_type -> google.protobuf.StringValue
80, // 122: grpc.Bridge.RemoveUser:input_type -> google.protobuf.StringValue 80, // 122: grpc.Bridge.RemoveUser:input_type -> google.protobuf.StringValue
18, // 123: grpc.Bridge.ConfigureUserAppleMail:input_type -> grpc.ConfigureAppleMailRequest 18, // 123: grpc.Bridge.ConfigureUserAppleMail:input_type -> grpc.ConfigureAppleMailRequest
81, // 124: grpc.Bridge.ReportBugClicked:input_type -> google.protobuf.Empty 81, // 124: grpc.Bridge.IsTLSCertificateInstalled:input_type -> google.protobuf.Empty
80, // 125: grpc.Bridge.AutoconfigClicked:input_type -> google.protobuf.StringValue 81, // 125: grpc.Bridge.InstallTLSCertificate:input_type -> google.protobuf.Empty
80, // 126: grpc.Bridge.ExternalLinkClicked:input_type -> google.protobuf.StringValue 80, // 126: grpc.Bridge.ExportTLSCertificates:input_type -> google.protobuf.StringValue
81, // 127: grpc.Bridge.IsTLSCertificateInstalled:input_type -> google.protobuf.Empty 19, // 127: grpc.Bridge.RunEventStream:input_type -> grpc.EventStreamRequest
81, // 128: grpc.Bridge.InstallTLSCertificate:input_type -> google.protobuf.Empty 81, // 128: grpc.Bridge.StopEventStream:input_type -> google.protobuf.Empty
80, // 129: grpc.Bridge.ExportTLSCertificates:input_type -> google.protobuf.StringValue 81, // 129: grpc.Bridge.TriggerRepair:input_type -> google.protobuf.Empty
19, // 130: grpc.Bridge.RunEventStream:input_type -> grpc.EventStreamRequest 80, // 130: grpc.Bridge.CheckTokens:output_type -> google.protobuf.StringValue
81, // 131: grpc.Bridge.StopEventStream:input_type -> google.protobuf.Empty 81, // 131: grpc.Bridge.AddLogEntry:output_type -> google.protobuf.Empty
81, // 132: grpc.Bridge.TriggerRepair:input_type -> google.protobuf.Empty 8, // 132: grpc.Bridge.GuiReady:output_type -> grpc.GuiReadyResponse
80, // 133: grpc.Bridge.CheckTokens:output_type -> google.protobuf.StringValue 81, // 133: grpc.Bridge.Quit:output_type -> google.protobuf.Empty
81, // 134: grpc.Bridge.AddLogEntry:output_type -> google.protobuf.Empty 81, // 134: grpc.Bridge.Restart:output_type -> google.protobuf.Empty
8, // 135: grpc.Bridge.GuiReady:output_type -> grpc.GuiReadyResponse 82, // 135: grpc.Bridge.ShowOnStartup:output_type -> google.protobuf.BoolValue
81, // 136: grpc.Bridge.Quit:output_type -> google.protobuf.Empty 81, // 136: grpc.Bridge.SetIsAutostartOn:output_type -> google.protobuf.Empty
81, // 137: grpc.Bridge.Restart:output_type -> google.protobuf.Empty 82, // 137: grpc.Bridge.IsAutostartOn:output_type -> google.protobuf.BoolValue
82, // 138: grpc.Bridge.ShowOnStartup:output_type -> google.protobuf.BoolValue 81, // 138: grpc.Bridge.SetIsBetaEnabled:output_type -> google.protobuf.Empty
81, // 139: grpc.Bridge.SetIsAutostartOn:output_type -> google.protobuf.Empty 82, // 139: grpc.Bridge.IsBetaEnabled:output_type -> google.protobuf.BoolValue
82, // 140: grpc.Bridge.IsAutostartOn:output_type -> google.protobuf.BoolValue 81, // 140: grpc.Bridge.SetIsAllMailVisible:output_type -> google.protobuf.Empty
81, // 141: grpc.Bridge.SetIsBetaEnabled:output_type -> google.protobuf.Empty 82, // 141: grpc.Bridge.IsAllMailVisible:output_type -> google.protobuf.BoolValue
82, // 142: grpc.Bridge.IsBetaEnabled:output_type -> google.protobuf.BoolValue 81, // 142: grpc.Bridge.SetIsTelemetryDisabled:output_type -> google.protobuf.Empty
81, // 143: grpc.Bridge.SetIsAllMailVisible:output_type -> google.protobuf.Empty 82, // 143: grpc.Bridge.IsTelemetryDisabled:output_type -> google.protobuf.BoolValue
82, // 144: grpc.Bridge.IsAllMailVisible:output_type -> google.protobuf.BoolValue 80, // 144: grpc.Bridge.GoOs:output_type -> google.protobuf.StringValue
81, // 145: grpc.Bridge.SetIsTelemetryDisabled:output_type -> google.protobuf.Empty 81, // 145: grpc.Bridge.TriggerReset:output_type -> google.protobuf.Empty
82, // 146: grpc.Bridge.IsTelemetryDisabled:output_type -> google.protobuf.BoolValue 80, // 146: grpc.Bridge.Version:output_type -> google.protobuf.StringValue
80, // 147: grpc.Bridge.GoOs:output_type -> google.protobuf.StringValue 80, // 147: grpc.Bridge.LogsPath:output_type -> google.protobuf.StringValue
81, // 148: grpc.Bridge.TriggerReset:output_type -> google.protobuf.Empty 80, // 148: grpc.Bridge.LicensePath:output_type -> google.protobuf.StringValue
80, // 149: grpc.Bridge.Version:output_type -> google.protobuf.StringValue 80, // 149: grpc.Bridge.ReleaseNotesPageLink:output_type -> google.protobuf.StringValue
80, // 150: grpc.Bridge.LogsPath:output_type -> google.protobuf.StringValue 80, // 150: grpc.Bridge.DependencyLicensesLink:output_type -> google.protobuf.StringValue
80, // 151: grpc.Bridge.LicensePath:output_type -> google.protobuf.StringValue 80, // 151: grpc.Bridge.LandingPageLink:output_type -> google.protobuf.StringValue
80, // 152: grpc.Bridge.ReleaseNotesPageLink:output_type -> google.protobuf.StringValue 81, // 152: grpc.Bridge.SetColorSchemeName:output_type -> google.protobuf.Empty
80, // 153: grpc.Bridge.DependencyLicensesLink:output_type -> google.protobuf.StringValue 80, // 153: grpc.Bridge.ColorSchemeName:output_type -> google.protobuf.StringValue
80, // 154: grpc.Bridge.LandingPageLink:output_type -> google.protobuf.StringValue 80, // 154: grpc.Bridge.CurrentEmailClient:output_type -> google.protobuf.StringValue
81, // 155: grpc.Bridge.SetColorSchemeName:output_type -> google.protobuf.Empty 81, // 155: grpc.Bridge.ReportBug:output_type -> google.protobuf.Empty
80, // 156: grpc.Bridge.ColorSchemeName:output_type -> google.protobuf.StringValue 81, // 156: grpc.Bridge.ForceLauncher:output_type -> google.protobuf.Empty
80, // 157: grpc.Bridge.CurrentEmailClient:output_type -> google.protobuf.StringValue 81, // 157: grpc.Bridge.SetMainExecutable:output_type -> google.protobuf.Empty
81, // 158: grpc.Bridge.ReportBug:output_type -> google.protobuf.Empty 81, // 158: grpc.Bridge.RequestKnowledgeBaseSuggestions:output_type -> google.protobuf.Empty
81, // 159: grpc.Bridge.ForceLauncher:output_type -> google.protobuf.Empty 81, // 159: grpc.Bridge.Login:output_type -> google.protobuf.Empty
81, // 160: grpc.Bridge.SetMainExecutable:output_type -> google.protobuf.Empty 81, // 160: grpc.Bridge.Login2FA:output_type -> google.protobuf.Empty
81, // 161: grpc.Bridge.RequestKnowledgeBaseSuggestions:output_type -> google.protobuf.Empty 81, // 161: grpc.Bridge.Login2Passwords:output_type -> google.protobuf.Empty
81, // 162: grpc.Bridge.Login:output_type -> google.protobuf.Empty 81, // 162: grpc.Bridge.LoginAbort:output_type -> google.protobuf.Empty
81, // 163: grpc.Bridge.Login2FA:output_type -> google.protobuf.Empty 81, // 163: grpc.Bridge.CheckUpdate:output_type -> google.protobuf.Empty
81, // 164: grpc.Bridge.Login2Passwords:output_type -> google.protobuf.Empty 81, // 164: grpc.Bridge.InstallUpdate:output_type -> google.protobuf.Empty
81, // 165: grpc.Bridge.LoginAbort:output_type -> google.protobuf.Empty 81, // 165: grpc.Bridge.SetIsAutomaticUpdateOn:output_type -> google.protobuf.Empty
81, // 166: grpc.Bridge.CheckUpdate:output_type -> google.protobuf.Empty 82, // 166: grpc.Bridge.IsAutomaticUpdateOn:output_type -> google.protobuf.BoolValue
81, // 167: grpc.Bridge.InstallUpdate:output_type -> google.protobuf.Empty 80, // 167: grpc.Bridge.DiskCachePath:output_type -> google.protobuf.StringValue
81, // 168: grpc.Bridge.SetIsAutomaticUpdateOn:output_type -> google.protobuf.Empty 81, // 168: grpc.Bridge.SetDiskCachePath:output_type -> google.protobuf.Empty
82, // 169: grpc.Bridge.IsAutomaticUpdateOn:output_type -> google.protobuf.BoolValue 81, // 169: grpc.Bridge.SetIsDoHEnabled:output_type -> google.protobuf.Empty
80, // 170: grpc.Bridge.DiskCachePath:output_type -> google.protobuf.StringValue 82, // 170: grpc.Bridge.IsDoHEnabled:output_type -> google.protobuf.BoolValue
81, // 171: grpc.Bridge.SetDiskCachePath:output_type -> google.protobuf.Empty 12, // 171: grpc.Bridge.MailServerSettings:output_type -> grpc.ImapSmtpSettings
81, // 172: grpc.Bridge.SetIsDoHEnabled:output_type -> google.protobuf.Empty 81, // 172: grpc.Bridge.SetMailServerSettings:output_type -> google.protobuf.Empty
82, // 173: grpc.Bridge.IsDoHEnabled:output_type -> google.protobuf.BoolValue 80, // 173: grpc.Bridge.Hostname:output_type -> google.protobuf.StringValue
12, // 174: grpc.Bridge.MailServerSettings:output_type -> grpc.ImapSmtpSettings 82, // 174: grpc.Bridge.IsPortFree:output_type -> google.protobuf.BoolValue
81, // 175: grpc.Bridge.SetMailServerSettings:output_type -> google.protobuf.Empty 13, // 175: grpc.Bridge.AvailableKeychains:output_type -> grpc.AvailableKeychainsResponse
80, // 176: grpc.Bridge.Hostname:output_type -> google.protobuf.StringValue 81, // 176: grpc.Bridge.SetCurrentKeychain:output_type -> google.protobuf.Empty
82, // 177: grpc.Bridge.IsPortFree:output_type -> google.protobuf.BoolValue 80, // 177: grpc.Bridge.CurrentKeychain:output_type -> google.protobuf.StringValue
13, // 178: grpc.Bridge.AvailableKeychains:output_type -> grpc.AvailableKeychainsResponse 17, // 178: grpc.Bridge.GetUserList:output_type -> grpc.UserListResponse
81, // 179: grpc.Bridge.SetCurrentKeychain:output_type -> google.protobuf.Empty 14, // 179: grpc.Bridge.GetUser:output_type -> grpc.User
80, // 180: grpc.Bridge.CurrentKeychain:output_type -> google.protobuf.StringValue 81, // 180: grpc.Bridge.SetUserSplitMode:output_type -> google.protobuf.Empty
17, // 181: grpc.Bridge.GetUserList:output_type -> grpc.UserListResponse 81, // 181: grpc.Bridge.SendBadEventUserFeedback:output_type -> google.protobuf.Empty
14, // 182: grpc.Bridge.GetUser:output_type -> grpc.User 81, // 182: grpc.Bridge.LogoutUser:output_type -> google.protobuf.Empty
81, // 183: grpc.Bridge.SetUserSplitMode:output_type -> google.protobuf.Empty 81, // 183: grpc.Bridge.RemoveUser:output_type -> google.protobuf.Empty
81, // 184: grpc.Bridge.SendBadEventUserFeedback:output_type -> google.protobuf.Empty 81, // 184: grpc.Bridge.ConfigureUserAppleMail:output_type -> google.protobuf.Empty
81, // 185: grpc.Bridge.LogoutUser:output_type -> google.protobuf.Empty 82, // 185: grpc.Bridge.IsTLSCertificateInstalled:output_type -> google.protobuf.BoolValue
81, // 186: grpc.Bridge.RemoveUser:output_type -> google.protobuf.Empty 81, // 186: grpc.Bridge.InstallTLSCertificate:output_type -> google.protobuf.Empty
81, // 187: grpc.Bridge.ConfigureUserAppleMail:output_type -> google.protobuf.Empty 81, // 187: grpc.Bridge.ExportTLSCertificates:output_type -> google.protobuf.Empty
81, // 188: grpc.Bridge.ReportBugClicked:output_type -> google.protobuf.Empty 20, // 188: grpc.Bridge.RunEventStream:output_type -> grpc.StreamEvent
81, // 189: grpc.Bridge.AutoconfigClicked:output_type -> google.protobuf.Empty 81, // 189: grpc.Bridge.StopEventStream:output_type -> google.protobuf.Empty
81, // 190: grpc.Bridge.ExternalLinkClicked:output_type -> google.protobuf.Empty 81, // 190: grpc.Bridge.TriggerRepair:output_type -> google.protobuf.Empty
82, // 191: grpc.Bridge.IsTLSCertificateInstalled:output_type -> google.protobuf.BoolValue 130, // [130:191] is the sub-list for method output_type
81, // 192: grpc.Bridge.InstallTLSCertificate:output_type -> google.protobuf.Empty 69, // [69:130] is the sub-list for method input_type
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
69, // [69:69] is the sub-list for extension type_name 69, // [69:69] is the sub-list for extension type_name
69, // [69:69] is the sub-list for extension extendee 69, // [69:69] is the sub-list for extension extendee
0, // [0:69] is the sub-list for field type_name 0, // [0:69] is the sub-list for field type_name

View File

@ -98,11 +98,6 @@ service Bridge {
rpc RemoveUser(google.protobuf.StringValue) returns (google.protobuf.Empty); rpc RemoveUser(google.protobuf.StringValue) returns (google.protobuf.Empty);
rpc ConfigureUserAppleMail(ConfigureAppleMailRequest) 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 // TLS certificate related calls
rpc IsTLSCertificateInstalled(google.protobuf.Empty) returns (google.protobuf.BoolValue); rpc IsTLSCertificateInstalled(google.protobuf.Empty) returns (google.protobuf.BoolValue);
rpc InstallTLSCertificate(google.protobuf.Empty) returns (google.protobuf.Empty); rpc InstallTLSCertificate(google.protobuf.Empty) returns (google.protobuf.Empty);

View File

@ -93,9 +93,6 @@ const (
Bridge_LogoutUser_FullMethodName = "/grpc.Bridge/LogoutUser" Bridge_LogoutUser_FullMethodName = "/grpc.Bridge/LogoutUser"
Bridge_RemoveUser_FullMethodName = "/grpc.Bridge/RemoveUser" Bridge_RemoveUser_FullMethodName = "/grpc.Bridge/RemoveUser"
Bridge_ConfigureUserAppleMail_FullMethodName = "/grpc.Bridge/ConfigureUserAppleMail" 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_IsTLSCertificateInstalled_FullMethodName = "/grpc.Bridge/IsTLSCertificateInstalled"
Bridge_InstallTLSCertificate_FullMethodName = "/grpc.Bridge/InstallTLSCertificate" Bridge_InstallTLSCertificate_FullMethodName = "/grpc.Bridge/InstallTLSCertificate"
Bridge_ExportTLSCertificates_FullMethodName = "/grpc.Bridge/ExportTLSCertificates" 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) 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) 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) 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 // TLS certificate related calls
IsTLSCertificateInstalled(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*wrapperspb.BoolValue, error) 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) 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 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) { func (c *bridgeClient) IsTLSCertificateInstalled(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*wrapperspb.BoolValue, error) {
out := new(wrapperspb.BoolValue) out := new(wrapperspb.BoolValue)
err := c.cc.Invoke(ctx, Bridge_IsTLSCertificateInstalled_FullMethodName, in, out, opts...) 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) LogoutUser(context.Context, *wrapperspb.StringValue) (*emptypb.Empty, error)
RemoveUser(context.Context, *wrapperspb.StringValue) (*emptypb.Empty, error) RemoveUser(context.Context, *wrapperspb.StringValue) (*emptypb.Empty, error)
ConfigureUserAppleMail(context.Context, *ConfigureAppleMailRequest) (*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 // TLS certificate related calls
IsTLSCertificateInstalled(context.Context, *emptypb.Empty) (*wrapperspb.BoolValue, error) IsTLSCertificateInstalled(context.Context, *emptypb.Empty) (*wrapperspb.BoolValue, error)
InstallTLSCertificate(context.Context, *emptypb.Empty) (*emptypb.Empty, 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) { func (UnimplementedBridgeServer) ConfigureUserAppleMail(context.Context, *ConfigureAppleMailRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method ConfigureUserAppleMail not implemented") 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) { func (UnimplementedBridgeServer) IsTLSCertificateInstalled(context.Context, *emptypb.Empty) (*wrapperspb.BoolValue, error) {
return nil, status.Errorf(codes.Unimplemented, "method IsTLSCertificateInstalled not implemented") 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) 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) { func _Bridge_IsTLSCertificateInstalled_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(emptypb.Empty) in := new(emptypb.Empty)
if err := dec(in); err != nil { if err := dec(in); err != nil {
@ -2465,18 +2364,6 @@ var Bridge_ServiceDesc = grpc.ServiceDesc{
MethodName: "ConfigureUserAppleMail", MethodName: "ConfigureUserAppleMail",
Handler: _Bridge_ConfigureUserAppleMail_Handler, Handler: _Bridge_ConfigureUserAppleMail_Handler,
}, },
{
MethodName: "ReportBugClicked",
Handler: _Bridge_ReportBugClicked_Handler,
},
{
MethodName: "AutoconfigClicked",
Handler: _Bridge_AutoconfigClicked_Handler,
},
{
MethodName: "ExternalLinkClicked",
Handler: _Bridge_ExternalLinkClicked_Handler,
},
{ {
MethodName: "IsTLSCertificateInstalled", MethodName: "IsTLSCertificateInstalled",
Handler: _Bridge_IsTLSCertificateInstalled_Handler, Handler: _Bridge_IsTLSCertificateInstalled_Handler,

View File

@ -465,7 +465,7 @@ func (s *Service) finishLogin() {
if apiErr := new(proton.APIError); errors.As(err, &apiErr) && apiErr.Code == proton.HumanValidationInvalidToken { if apiErr := new(proton.APIError); errors.As(err, &apiErr) && apiErr.Code == proton.HumanValidationInvalidToken {
s.hvDetails = nil s.hvDetails = nil
_ = s.SendEvent(NewLoginError(LoginErrorType_HV_ERROR, err.Error())) _ = s.SendEvent(NewLoginError(LoginErrorType_HV_ERROR, hv.VerificationFailedErrorMsg))
return return
} }
@ -643,7 +643,10 @@ func (s *Service) monitorParentPID() {
func (s *Service) handleHvRequest(err error) { func (s *Service) handleHvRequest(err error) {
hvDet, hvErr := hv.VerifyAndExtractHvRequest(err) hvDet, hvErr := hv.VerifyAndExtractHvRequest(err)
if hvErr != nil { 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 return
} }

View File

@ -30,6 +30,7 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/constants" "github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/events" "github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/frontend/theme" "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/kb"
"github.com/ProtonMail/proton-bridge/v3/internal/safe" "github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/service" "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: case proton.HumanValidationInvalidToken:
s.hvDetails = nil s.hvDetails = nil
_ = s.SendEvent(NewLoginError(LoginErrorType_HV_ERROR, err.Error())) _ = s.SendEvent(NewLoginError(LoginErrorType_HV_ERROR, hv.VerificationFailedErrorMsg))
default: default:
_ = s.SendEvent(NewLoginError(LoginErrorType_USERNAME_PASSWORD_ERROR, err.Error())) _ = s.SendEvent(NewLoginError(LoginErrorType_USERNAME_PASSWORD_ERROR, err.Error()))

View File

@ -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 <https://www.gnu.org/licenses/>.
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
}

View File

@ -21,6 +21,11 @@ import (
"github.com/ProtonMail/go-proton-api" "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 // 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) // 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 // 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() { if errors.As(err, &protonErr) && protonErr.IsHVError() {
hvDetails, hvErr := protonErr.GetHVDetails() hvDetails, hvErr := protonErr.GetHVDetails()
if hvErr != nil { if hvErr != nil {
return nil, fmt.Errorf("received HV request, but can't decode HV details") return nil, hvErr
} }
return hvDetails, nil return hvDetails, nil
} }

View File

@ -188,16 +188,6 @@ func (l *Locations) ProvideUpdatesPath() (string, error) {
return l.getUpdatesPath(), nil return l.getUpdatesPath(), nil
} }
// ProvideStatsPath returns a location for statistics files (e.g. ~/.local/share/<company>/<app>/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) { func (l *Locations) ProvideIMAPSyncConfigPath() (string, error) {
if err := os.MkdirAll(l.getIMAPSyncConfigPath(), 0o700); err != nil { if err := os.MkdirAll(l.getIMAPSyncConfigPath(), 0o700); err != nil {
return "", err return "", err
@ -252,10 +242,6 @@ func (l *Locations) getNotificationsCachePath() string {
return filepath.Join(l.userCache, "notifications") 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") } func (l *Locations) getUnleashCachePath() string { return filepath.Join(l.userCache, "unleash_cache") }
// Clear removes everything except the lock and update files. // Clear removes everything except the lock and update files.

View File

@ -31,8 +31,8 @@ type CoolDownProvider interface {
Reset() Reset()
} }
func jitter(max int) time.Duration { //nolint:predeclared func jitter(maxValue int) time.Duration {
return time.Duration(rand.Intn(max)) * time.Second //nolint:gosec return time.Duration(rand.Intn(maxValue)) * time.Second //nolint:gosec
} }
type ExpCoolDown struct { type ExpCoolDown struct {

87
internal/plan/plan.go Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
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
}
}

View File

@ -56,7 +56,6 @@ type Connector struct {
identityState sharedIdentity identityState sharedIdentity
client APIClient client APIClient
telemetry Telemetry
reporter reporter.Reporter reporter reporter.Reporter
panicHandler async.PanicHandler panicHandler async.PanicHandler
sendRecorder *sendrecorder.SendRecorder sendRecorder *sendrecorder.SendRecorder
@ -80,7 +79,6 @@ func NewConnector(
addressMode usertypes.AddressMode, addressMode usertypes.AddressMode,
sendRecorder *sendrecorder.SendRecorder, sendRecorder *sendrecorder.SendRecorder,
panicHandler async.PanicHandler, panicHandler async.PanicHandler,
telemetry Telemetry,
reporter reporter.Reporter, reporter reporter.Reporter,
showAllMail bool, showAllMail bool,
syncState *SyncState, syncState *SyncState,
@ -96,7 +94,6 @@ func NewConnector(
attrs: defaultMailboxAttributes(), attrs: defaultMailboxAttributes(),
client: apiClient, client: apiClient,
telemetry: telemetry,
reporter: reporter, reporter: reporter,
panicHandler: panicHandler, panicHandler: panicHandler,
sendRecorder: sendRecorder, 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) addrID, err := s.identityState.CheckAuth(username, password)
if err != nil { if err != nil {
s.telemetry.ReportConfigStatusFailure("IMAP " + err.Error())
return false return false
} }
@ -180,8 +176,6 @@ func (s *Connector) Authorize(ctx context.Context, username string, password []b
return false return false
} }
s.telemetry.SendConfigStatusSuccess(ctx)
return true return true
} }

View File

@ -47,12 +47,6 @@ type EventProvider interface {
RewindEventID(ctx context.Context, eventID string) error RewindEventID(ctx context.Context, eventID string) error
} }
type Telemetry interface {
useridentity.Telemetry
SendConfigStatusSuccess(ctx context.Context)
ReportConfigStatusFailure(errDetails string)
}
type GluonIDProvider interface { type GluonIDProvider interface {
GetGluonID(addrID string) (string, bool) GetGluonID(addrID string) (string, bool)
GetGluonIDs() map[string]string GetGluonIDs() map[string]string
@ -77,7 +71,6 @@ type Service struct {
serverManager IMAPServerManager serverManager IMAPServerManager
eventPublisher events.EventPublisher eventPublisher events.EventPublisher
telemetry Telemetry
panicHandler async.PanicHandler panicHandler async.PanicHandler
sendRecorder *sendrecorder.SendRecorder sendRecorder *sendrecorder.SendRecorder
reporter reporter.Reporter reporter reporter.Reporter
@ -112,7 +105,6 @@ func NewService(
keyPassProvider useridentity.KeyPassProvider, keyPassProvider useridentity.KeyPassProvider,
panicHandler async.PanicHandler, panicHandler async.PanicHandler,
sendRecorder *sendrecorder.SendRecorder, sendRecorder *sendrecorder.SendRecorder,
telemetry Telemetry,
reporter reporter.Reporter, reporter reporter.Reporter,
addressMode usertypes.AddressMode, addressMode usertypes.AddressMode,
subscription events.Subscription, subscription events.Subscription,
@ -150,7 +142,6 @@ func NewService(
panicHandler: panicHandler, panicHandler: panicHandler,
sendRecorder: sendRecorder, sendRecorder: sendRecorder,
telemetry: telemetry,
reporter: reporter, reporter: reporter,
connectors: make(map[string]*Connector), connectors: make(map[string]*Connector),
@ -242,6 +233,12 @@ func (s *Service) OnLogout(ctx context.Context) error {
return err 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 { func (s *Service) ShowAllMail(ctx context.Context, v bool) error {
_, err := s.cpc.Send(ctx, &showAllMailReq{v: v}) _, 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) err := s.removeConnectorsFromServer(ctx, s.connectors, false)
req.Reply(ctx, nil, err) 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: case *showAllMailReq:
s.log.Debug("Show all mail request") s.log.Debug("Show all mail request")
req.Reply(ctx, nil, nil) req.Reply(ctx, nil, nil)
@ -513,7 +515,6 @@ func (s *Service) buildConnectors() (map[string]*Connector, error) {
s.addressMode, s.addressMode,
s.sendRecorder, s.sendRecorder,
s.panicHandler, s.panicHandler,
s.telemetry,
s.reporter, s.reporter,
s.showAllMail, s.showAllMail,
s.syncStateProvider, s.syncStateProvider,
@ -531,7 +532,6 @@ func (s *Service) buildConnectors() (map[string]*Connector, error) {
s.addressMode, s.addressMode,
s.sendRecorder, s.sendRecorder,
s.panicHandler, s.panicHandler,
s.telemetry,
s.reporter, s.reporter,
s.showAllMail, s.showAllMail,
s.syncStateProvider, s.syncStateProvider,
@ -655,6 +655,8 @@ type onLogoutReq struct{}
type showAllMailReq struct{ v bool } type showAllMailReq struct{ v bool }
type onDeleteReq struct{}
type setAddressModeReq struct { type setAddressModeReq struct {
mode usertypes.AddressMode mode usertypes.AddressMode
} }

View File

@ -154,7 +154,6 @@ func addNewAddressSplitMode(ctx context.Context, s *Service, addrID string) erro
s.addressMode, s.addressMode,
s.sendRecorder, s.sendRecorder,
s.panicHandler, s.panicHandler,
s.telemetry,
s.reporter, s.reporter,
s.showAllMail, s.showAllMail,
s.syncStateProvider, s.syncStateProvider,

View File

@ -65,6 +65,22 @@ func (s syncMessageEventHandler) HandleMessageEvents(ctx context.Context, events
return err 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: case proton.EventDelete:
updates := onMessageDeleted( updates := onMessageDeleted(
logging.WithLogrusField(ctx, "action", "delete message (sync)"), logging.WithLogrusField(ctx, "action", "delete message (sync)"),

View File

@ -49,6 +49,7 @@ type IMAPSettingsProvider interface {
Port() int Port() int
SetPort(int) error SetPort(int) error
UseSSL() bool UseSSL() bool
DisableIMAPAuthenticate() bool
CacheDirectory() string CacheDirectory() string
DataDirectory() (string, error) DataDirectory() (string, error)
SetCacheDirectory(string) error SetCacheDirectory(string) error
@ -74,6 +75,7 @@ func newIMAPServer(
tlsConfig *tls.Config, tlsConfig *tls.Config,
reporter reporter.Reporter, reporter reporter.Reporter,
logClient, logServer bool, logClient, logServer bool,
disableIMAPAuthenticate bool,
eventPublisher IMAPEventPublisher, eventPublisher IMAPEventPublisher,
tasks *async.Group, tasks *async.Group,
uidValidityGenerator imap.UIDValidityGenerator, uidValidityGenerator imap.UIDValidityGenerator,
@ -113,7 +115,7 @@ func newIMAPServer(
imapServerLog = io.Discard imapServerLog = io.Discard
} }
imapServer, err := gluon.New( options := []gluon.Option{
gluon.WithTLS(tlsConfig), gluon.WithTLS(tlsConfig),
gluon.WithDataDir(gluonCacheDir), gluon.WithDataDir(gluonCacheDir),
gluon.WithDatabaseDir(gluonConfigDir), gluon.WithDatabaseDir(gluonConfigDir),
@ -124,7 +126,13 @@ func newIMAPServer(
gluon.WithUIDValidityGenerator(uidValidityGenerator), gluon.WithUIDValidityGenerator(uidValidityGenerator),
gluon.WithPanicHandler(panicHandler), gluon.WithPanicHandler(panicHandler),
gluon.WithObservabilitySender(observability.NewAdapter(observabilitySender), int(observability.GluonImapError), int(observability.GluonMessageError), int(observability.GluonOtherError)), 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 { if err != nil {
return nil, err return nil, err
} }

View File

@ -451,6 +451,7 @@ func (sm *Service) createIMAPServer(ctx context.Context) (*gluon.Server, error)
sm.reporter, sm.reporter,
sm.imapSettings.LogClient(), sm.imapSettings.LogClient(),
sm.imapSettings.LogServer(), sm.imapSettings.LogServer(),
sm.imapSettings.DisableIMAPAuthenticate(),
sm.imapSettings.EventPublisher(), sm.imapSettings.EventPublisher(),
sm.tasks, sm.tasks,
sm.uidValidityGenerator, sm.uidValidityGenerator,

View File

@ -50,7 +50,6 @@ type Service struct {
} }
const bitfieldRegexPattern = `^\\\d+` const bitfieldRegexPattern = `^\\\d+`
const disableNotificationsKillSwitch = "InboxBridgeEventLoopNotificationDisabled"
func NewService(userID string, service userevents.Subscribable, eventPublisher events.EventPublisher, store *Store, func NewService(userID string, service userevents.Subscribable, eventPublisher events.EventPublisher, store *Store,
getFlagFn unleash.GetFlagValueFn, observabilitySender observability.Sender) *Service { 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 { 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.") s.log.Info("Received notification events. Skipping as kill switch is enabled.")
return nil return nil
} }

View File

@ -24,6 +24,7 @@ import (
"github.com/ProtonMail/gluon/async" "github.com/ProtonMail/gluon/async"
"github.com/ProtonMail/go-proton-api" "github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/proton-bridge/v3/internal/plan"
"github.com/ProtonMail/proton-bridge/v3/internal/updater" "github.com/ProtonMail/proton-bridge/v3/internal/updater"
) )
@ -62,7 +63,7 @@ func newDistinctionUtility(ctx context.Context, panicHandler async.PanicHandler,
observabilitySender: observabilitySender, observabilitySender: observabilitySender,
userPlanUnsafe: planUnknown, userPlanUnsafe: plan.Unknown,
heartbeatData: heartbeatData{}, heartbeatData: heartbeatData{},
heartbeatTicker: time.NewTicker(updateInterval), heartbeatTicker: time.NewTicker(updateInterval),

View File

@ -18,7 +18,7 @@
package observability package observability
import ( import (
"fmt" "strconv"
"time" "time"
"github.com/ProtonMail/gluon/async" "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. // generateHeartbeatUserMetric creates the heartbeat user metric and includes the relevant data.
func (d *distinctionUtility) generateHeartbeatUserMetric() proton.ObservabilityMetric { func (d *distinctionUtility) generateHeartbeatUserMetric() proton.ObservabilityMetric {
return generateHeartbeatMetric( return generateHeartbeatMetric(
@ -98,10 +94,10 @@ func (d *distinctionUtility) generateHeartbeatUserMetric() proton.ObservabilityM
d.getEmailClientUserAgent(), d.getEmailClientUserAgent(),
getEnabled(d.settingsGetter.GetProxyAllowed()), getEnabled(d.settingsGetter.GetProxyAllowed()),
getEnabled(d.getBetaAccessEnabled()), getEnabled(d.getBetaAccessEnabled()),
formatBool(d.heartbeatData.receivedOtherError), strconv.FormatBool(d.heartbeatData.receivedOtherError),
formatBool(d.heartbeatData.receivedSyncError), strconv.FormatBool(d.heartbeatData.receivedSyncError),
formatBool(d.heartbeatData.receivedEventLoopError), strconv.FormatBool(d.heartbeatData.receivedEventLoopError),
formatBool(d.heartbeatData.receivedGluonError), strconv.FormatBool(d.heartbeatData.receivedGluonError),
) )
} }

View File

@ -18,76 +18,9 @@
package observability package observability
import ( import (
"context" "github.com/ProtonMail/proton-bridge/v3/internal/plan"
"strings"
"github.com/ProtonMail/gluon/async"
"github.com/ProtonMail/go-proton-api"
) )
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) { func (d *distinctionUtility) setUserPlan(planName string) {
if planName == "" { if planName == "" {
return return
@ -96,24 +29,12 @@ func (d *distinctionUtility) setUserPlan(planName string) {
d.userPlanLock.Lock() d.userPlanLock.Lock()
defer d.userPlanLock.Unlock() defer d.userPlanLock.Unlock()
userPlanMapped := mapUserPlan(planName) userPlanMapped := plan.MapUserPlan(planName)
if isHigherPriority(d.userPlanUnsafe, userPlanMapped) { if plan.IsHigherPriority(d.userPlanUnsafe, userPlanMapped) {
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 { func (d *distinctionUtility) getUserPlanSafe() string {
d.userPlanLock.Lock() d.userPlanLock.Lock()
defer d.userPlanLock.Unlock() defer d.userPlanLock.Unlock()

View File

@ -250,7 +250,7 @@ func (s *Service) addMetricsIfClients(metric ...proton.ObservabilityMetric) {
s.addMetrics(metric...) 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.log.Info("Registering user client, ID:", userID)
s.withUserClientStoreLock(func() { 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; // There may be a case where we already have metric updates stored, so try to flush;
s.sendSignal(s.signalDataArrived) s.sendSignal(s.signalDataArrived)

View File

@ -20,15 +20,16 @@ package observability
import ( import (
gluonMetrics "github.com/ProtonMail/gluon/observability/metrics" gluonMetrics "github.com/ProtonMail/gluon/observability/metrics"
"github.com/ProtonMail/go-proton-api" "github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/proton-bridge/v3/internal/plan"
) )
func GenerateAllUsedDistinctionMetricPermutations() []proton.ObservabilityMetric { func GenerateAllUsedDistinctionMetricPermutations() []proton.ObservabilityMetric {
planValues := []string{ planValues := []string{
planUnknown, plan.Unknown,
planOther, plan.Other,
planBusiness, plan.Business,
planIndividual, plan.Individual,
planGroup} plan.Group}
mailClientValues := []string{ mailClientValues := []string{
emailAgentAppleMail, emailAgentAppleMail,
emailAgentOutlook, emailAgentOutlook,
@ -58,11 +59,11 @@ func GenerateAllUsedDistinctionMetricPermutations() []proton.ObservabilityMetric
func GenerateAllHeartbeatMetricPermutations() []proton.ObservabilityMetric { func GenerateAllHeartbeatMetricPermutations() []proton.ObservabilityMetric {
planValues := []string{ planValues := []string{
planUnknown, plan.Unknown,
planOther, plan.Other,
planBusiness, plan.Business,
planIndividual, plan.Individual,
planGroup} plan.Group}
mailClientValues := []string{ mailClientValues := []string{
emailAgentAppleMail, emailAgentAppleMail,
emailAgentOutlook, emailAgentOutlook,

View File

@ -104,8 +104,3 @@ func TestMatchUserAgent(t *testing.T) {
require.Equal(t, testCase.result, matchUserAgent(testCase.agent)) 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))
}

View File

@ -66,14 +66,9 @@ func (s *Accounts) CheckAuth(user string, password []byte) (string, string, erro
continue continue
} }
account.service.telemetry.ReportSMTPAuthSuccess(context.Background())
return id, addrID, nil return id, addrID, nil
} }
for _, service := range s.accounts {
service.service.telemetry.ReportSMTPAuthFailed(user)
}
return "", "", ErrNoSuchUser return "", "", ErrNoSuchUser
} }

View File

@ -39,12 +39,6 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
type Telemetry interface {
useridentity.Telemetry
ReportSMTPAuthSuccess(context.Context)
ReportSMTPAuthFailed(username string)
}
type Service struct { type Service struct {
userID string userID string
panicHandler async.PanicHandler panicHandler async.PanicHandler
@ -57,7 +51,6 @@ type Service struct {
bridgePassProvider useridentity.BridgePassProvider bridgePassProvider useridentity.BridgePassProvider
keyPassProvider useridentity.KeyPassProvider keyPassProvider useridentity.KeyPassProvider
identityState *useridentity.State identityState *useridentity.State
telemetry Telemetry
eventService userevents.Subscribable eventService userevents.Subscribable
subscription *userevents.EventChanneledSubscriber subscription *userevents.EventChanneledSubscriber
@ -76,7 +69,6 @@ func NewService(
reporter reporter.Reporter, reporter reporter.Reporter,
bridgePassProvider useridentity.BridgePassProvider, bridgePassProvider useridentity.BridgePassProvider,
keyPassProvider useridentity.KeyPassProvider, keyPassProvider useridentity.KeyPassProvider,
telemetry Telemetry,
eventService userevents.Subscribable, eventService userevents.Subscribable,
mode usertypes.AddressMode, mode usertypes.AddressMode,
identityState *useridentity.State, identityState *useridentity.State,
@ -99,7 +91,6 @@ func NewService(
bridgePassProvider: bridgePassProvider, bridgePassProvider: bridgePassProvider,
keyPassProvider: keyPassProvider, keyPassProvider: keyPassProvider,
telemetry: telemetry,
identityState: identityState, identityState: identityState,
eventService: eventService, eventService: eventService,

View File

@ -231,7 +231,7 @@ func downloadAttachment(ctx context.Context, cache *DownloadCache, client APICli
} }
type DownloadRateModifier interface { 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]( func autoDownloadRate[T any, R any](
@ -285,14 +285,14 @@ func autoDownloadRate[T any, R any](
type DefaultDownloadRateModifier struct{} 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 { if !wasSuccess {
return 2 return 2
} }
parallelTasks := current * 2 parallelTasks := currentValue * 2
if parallelTasks > max { if parallelTasks > maxValue {
parallelTasks = max parallelTasks = maxValue
} }
return parallelTasks return parallelTasks

View File

@ -64,38 +64,3 @@ func (mr *MockIdentityProviderMockRecorder) GetUser(arg0 interface{}) *gomock.Ca
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUser", reflect.TypeOf((*MockIdentityProvider)(nil).GetUser), arg0) 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)
}

View File

@ -50,7 +50,6 @@ type Service struct {
subscription *userevents.EventChanneledSubscriber subscription *userevents.EventChanneledSubscriber
bridgePassProvider BridgePassProvider bridgePassProvider BridgePassProvider
telemetry Telemetry
} }
func NewService( func NewService(
@ -58,7 +57,6 @@ func NewService(
eventPublisher events.EventPublisher, eventPublisher events.EventPublisher,
state *State, state *State,
bridgePassProvider BridgePassProvider, bridgePassProvider BridgePassProvider,
telemetry Telemetry,
) *Service { ) *Service {
subscriberName := fmt.Sprintf("identity-%v", state.User.ID) subscriberName := fmt.Sprintf("identity-%v", state.User.ID)
@ -73,7 +71,6 @@ func NewService(
}), }),
subscription: userevents.NewEventSubscriber(subscriberName), subscription: userevents.NewEventSubscriber(subscriberName),
bridgePassProvider: bridgePassProvider, bridgePassProvider: bridgePassProvider,
telemetry: telemetry,
} }
} }

View File

@ -361,10 +361,9 @@ func newTestService(_ *testing.T, mockCtrl *gomock.Controller) (*Service, *mocks
eventPublisher := mocks2.NewMockEventPublisher(mockCtrl) eventPublisher := mocks2.NewMockEventPublisher(mockCtrl)
provider := mocks.NewMockIdentityProvider(mockCtrl) provider := mocks.NewMockIdentityProvider(mockCtrl)
user := newTestUser() user := newTestUser()
telemetry := mocks.NewMockTelemetry(mockCtrl)
bridgePassProvider := NewFixedBridgePassProvider([]byte("hello")) 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 return service, eventPublisher, provider
} }

View File

@ -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 <https://www.gnu.org/licenses/>.
package useridentity
type Telemetry interface {
ReportConfigStatusFailure(errDetails string)
}

View File

@ -19,9 +19,12 @@ package telemetry
import ( import (
"context" "context"
"math"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/ProtonMail/proton-bridge/v3/internal/plan"
"github.com/ProtonMail/proton-bridge/v3/internal/updater" "github.com/ProtonMail/proton-bridge/v3/internal/updater"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -31,71 +34,67 @@ func NewHeartbeat(manager HeartbeatManager, imapPort, smtpPort int, cacheDir, ke
log: logrus.WithField("pkg", "telemetry"), log: logrus.WithField("pkg", "telemetry"),
manager: manager, manager: manager,
metrics: HeartbeatData{ metrics: HeartbeatData{
MeasurementGroup: "bridge.any.usage", MeasurementGroup: "bridge.any.heartbeat",
Event: "bridge_heartbeat", Event: "bridge_heartbeat_new",
Dimensions: NewHeartbeatDimensions(),
}, },
defaultIMAPPort: imapPort, defaultIMAPPort: imapPort,
defaultSMTPPort: smtpPort, defaultSMTPPort: smtpPort,
defaultCache: cacheDir, defaultCache: cacheDir,
defaultKeychain: keychain, defaultKeychain: keychain,
defaultUserPlan: plan.Unknown,
} }
return heartbeat return heartbeat
} }
func (heartbeat *Heartbeat) SetRollout(val float64) { 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) { func (heartbeat *Heartbeat) GetRollout() int {
heartbeat.metrics.Values.NbAccount = val return heartbeat.metrics.Values.Rollout
}
func (heartbeat *Heartbeat) SetNumberConnectedAccounts(val int) {
heartbeat.metrics.Values.NumberConnectedAccounts = val
} }
func (heartbeat *Heartbeat) SetAutoUpdate(val bool) { func (heartbeat *Heartbeat) SetAutoUpdate(val bool) {
if val { heartbeat.metrics.Dimensions.AutoUpdateEnabled = strconv.FormatBool(val)
heartbeat.metrics.Dimensions.AutoUpdate = dimensionON
} else {
heartbeat.metrics.Dimensions.AutoUpdate = dimensionOFF
}
} }
func (heartbeat *Heartbeat) SetAutoStart(val bool) { func (heartbeat *Heartbeat) SetAutoStart(val bool) {
if val { heartbeat.metrics.Dimensions.AutoStartEnabled = strconv.FormatBool(val)
heartbeat.metrics.Dimensions.AutoStart = dimensionON
} else {
heartbeat.metrics.Dimensions.AutoStart = dimensionOFF
}
} }
func (heartbeat *Heartbeat) SetBeta(val updater.Channel) { func (heartbeat *Heartbeat) SetBeta(val updater.Channel) {
if val == updater.EarlyChannel { heartbeat.metrics.Dimensions.BetaEnabled = strconv.FormatBool(val == updater.EarlyChannel)
heartbeat.metrics.Dimensions.Beta = dimensionON
} else {
heartbeat.metrics.Dimensions.Beta = dimensionOFF
}
} }
func (heartbeat *Heartbeat) SetDoh(val bool) { func (heartbeat *Heartbeat) SetDoh(val bool) {
if val { heartbeat.metrics.Dimensions.DohEnabled = strconv.FormatBool(val)
heartbeat.metrics.Dimensions.Doh = dimensionON
} else {
heartbeat.metrics.Dimensions.Doh = dimensionOFF
}
} }
func (heartbeat *Heartbeat) SetSplitMode(val bool) { func (heartbeat *Heartbeat) SetSplitMode(val bool) {
if val { heartbeat.metrics.Dimensions.UseSplitMode = strconv.FormatBool(val)
heartbeat.metrics.Dimensions.SplitMode = dimensionON }
} else {
heartbeat.metrics.Dimensions.SplitMode = dimensionOFF 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) { func (heartbeat *Heartbeat) SetShowAllMail(val bool) {
if val { heartbeat.metrics.Dimensions.ShowAllMail = strconv.FormatBool(val)
heartbeat.metrics.Dimensions.ShowAllMail = dimensionON
} else {
heartbeat.metrics.Dimensions.ShowAllMail = dimensionOFF
}
} }
func (heartbeat *Heartbeat) SetIMAPConnectionMode(val bool) { func (heartbeat *Heartbeat) SetIMAPConnectionMode(val bool) {
@ -115,35 +114,19 @@ func (heartbeat *Heartbeat) SetSMTPConnectionMode(val bool) {
} }
func (heartbeat *Heartbeat) SetIMAPPort(val int) { func (heartbeat *Heartbeat) SetIMAPPort(val int) {
if val == heartbeat.defaultIMAPPort { heartbeat.metrics.Dimensions.UseDefaultIMAPPort = strconv.FormatBool(val == heartbeat.defaultIMAPPort)
heartbeat.metrics.Dimensions.IMAPPort = dimensionDefault
} else {
heartbeat.metrics.Dimensions.IMAPPort = dimensionCustom
}
} }
func (heartbeat *Heartbeat) SetSMTPPort(val int) { func (heartbeat *Heartbeat) SetSMTPPort(val int) {
if val == heartbeat.defaultSMTPPort { heartbeat.metrics.Dimensions.UseDefaultSMTPPort = strconv.FormatBool(val == heartbeat.defaultSMTPPort)
heartbeat.metrics.Dimensions.SMTPPort = dimensionDefault
} else {
heartbeat.metrics.Dimensions.SMTPPort = dimensionCustom
}
} }
func (heartbeat *Heartbeat) SetCacheLocation(val string) { func (heartbeat *Heartbeat) SetCacheLocation(val string) {
if val == heartbeat.defaultCache { heartbeat.metrics.Dimensions.UseDefaultCacheLocation = strconv.FormatBool(val == heartbeat.defaultCache)
heartbeat.metrics.Dimensions.CacheLocation = dimensionDefault
} else {
heartbeat.metrics.Dimensions.CacheLocation = dimensionCustom
}
} }
func (heartbeat *Heartbeat) SetKeyChainPref(val string) { func (heartbeat *Heartbeat) SetKeyChainPref(val string) {
if val == heartbeat.defaultKeychain { heartbeat.metrics.Dimensions.UseDefaultKeychain = strconv.FormatBool(val == heartbeat.defaultKeychain)
heartbeat.metrics.Dimensions.KeychainPref = dimensionDefault
} else {
heartbeat.metrics.Dimensions.KeychainPref = dimensionCustom
}
} }
func (heartbeat *Heartbeat) SetPrevVersion(val string) { func (heartbeat *Heartbeat) SetPrevVersion(val string) {

View File

@ -22,34 +22,38 @@ import (
"testing" "testing"
"time" "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"
"github.com/ProtonMail/proton-bridge/v3/internal/telemetry/mocks" "github.com/ProtonMail/proton-bridge/v3/internal/telemetry/mocks"
"github.com/golang/mock/gomock" "github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
) )
func TestHeartbeat_default_heartbeat(t *testing.T) { func TestHeartbeat_default_heartbeat(t *testing.T) {
withHeartbeat(t, 1143, 1025, "/tmp", "defaultKeychain", func(hb *telemetry.Heartbeat, mock *mocks.MockHeartbeatManager) { withHeartbeat(t, 1143, 1025, "/tmp", "defaultKeychain", func(hb *telemetry.Heartbeat, mock *mocks.MockHeartbeatManager) {
data := telemetry.HeartbeatData{ data := telemetry.HeartbeatData{
MeasurementGroup: "bridge.any.usage", MeasurementGroup: "bridge.any.heartbeat",
Event: "bridge_heartbeat", Event: "bridge_heartbeat_new",
Values: telemetry.HeartbeatValues{ Values: telemetry.HeartbeatValues{
NbAccount: 1, NumberConnectedAccounts: 1,
Rollout: 1,
}, },
Dimensions: telemetry.HeartbeatDimensions{ Dimensions: telemetry.HeartbeatDimensions{
AutoUpdate: "on", AutoUpdateEnabled: "true",
AutoStart: "on", AutoStartEnabled: "true",
Beta: "off", BetaEnabled: "false",
Doh: "off", DohEnabled: "false",
SplitMode: "off", UseSplitMode: "false",
ShowAllMail: "off", ShowAllMail: "false",
IMAPConnectionMode: "ssl", UseDefaultIMAPPort: "true",
SMTPConnectionMode: "ssl", UseDefaultSMTPPort: "true",
IMAPPort: "default", UseDefaultCacheLocation: "true",
SMTPPort: "default", UseDefaultKeychain: "true",
CacheLocation: "default", ContactedByAppleNotes: "false",
KeychainPref: "default", PrevVersion: "1.2.3",
PrevVersion: "1.2.3", IMAPConnectionMode: "ssl",
Rollout: "10", 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 := telemetry.NewHeartbeat(manager, imap, smtp, cache, keychain)
heartbeat.SetRollout(0.1) heartbeat.SetRollout(0.1)
heartbeat.SetNbAccount(1) heartbeat.SetNumberConnectedAccounts(1)
heartbeat.SetSplitMode(false) heartbeat.SetSplitMode(false)
heartbeat.SetAutoStart(true) heartbeat.SetAutoStart(true)
heartbeat.SetAutoUpdate(true) heartbeat.SetAutoUpdate(true)
@ -98,3 +102,29 @@ func withHeartbeat(t *testing.T, imap, smtp int, cache, keychain string, tests f
tests(&heartbeat, manager) 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())
}
}

View File

@ -21,14 +21,11 @@ import (
"context" "context"
"time" "time"
"github.com/ProtonMail/proton-bridge/v3/internal/plan"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
const ( const (
dimensionON = "on"
dimensionOFF = "off"
dimensionDefault = "default"
dimensionCustom = "custom"
dimensionSSL = "ssl" dimensionSSL = "ssl"
dimensionStartTLS = "starttls" dimensionStartTLS = "starttls"
) )
@ -46,24 +43,29 @@ type HeartbeatManager interface {
} }
type HeartbeatValues struct { type HeartbeatValues struct {
NbAccount int `json:"nb_account"` NumberConnectedAccounts int `json:"numberConnectedAccounts"`
Rollout int `json:"rolloutPercentage"`
} }
type HeartbeatDimensions struct { type HeartbeatDimensions struct {
AutoUpdate string `json:"auto_update"` // Fields below correspond to bool
AutoStart string `json:"auto_start"` AutoUpdateEnabled string `json:"isAutoUpdateEnabled"`
Beta string `json:"beta"` AutoStartEnabled string `json:"isAutoStartEnabled"`
Doh string `json:"doh"` BetaEnabled string `json:"isBetaEnabled"`
SplitMode string `json:"split_mode"` DohEnabled string `json:"isDohEnabled"`
ShowAllMail string `json:"show_all_mail"` UseSplitMode string `json:"usesSplitMode"`
IMAPConnectionMode string `json:"imap_connection_mode"` ShowAllMail string `json:"useAllMail"`
SMTPConnectionMode string `json:"smtp_connection_mode"` UseDefaultIMAPPort string `json:"useDefaultImapPort"`
IMAPPort string `json:"imap_port"` UseDefaultSMTPPort string `json:"useDefaultSmtpPort"`
SMTPPort string `json:"smtp_port"` UseDefaultCacheLocation string `json:"useDefaultCacheLocation"`
CacheLocation string `json:"cache_location"` UseDefaultKeychain string `json:"useDefaultKeychain"`
KeychainPref string `json:"keychain_pref"` ContactedByAppleNotes string `json:"isContactedByAppleNotes"`
PrevVersion string `json:"prev_version"`
Rollout string `json:"rollout"` // 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 { type HeartbeatData struct {
@ -82,4 +84,26 @@ type Heartbeat struct {
defaultSMTPPort int defaultSMTPPort int
defaultCache string defaultCache string
defaultKeychain 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,
}
} }

View File

@ -36,6 +36,12 @@ var pollJitter = 2 * time.Minute //nolint:gochecknoglobals
const filename = "unleash_flags" const filename = "unleash_flags"
const (
EventLoopNotificationDisabled = "InboxBridgeEventLoopNotificationDisabled"
IMAPAuthenticateCommandDisabled = "InboxBridgeImapAuthenticateCommandDisabled"
UserRemovalGluonDataCleanupDisabled = "InboxBridgeUserRemovalGluonDataCleanupDisabled"
)
type requestFeaturesFn func(ctx context.Context) (proton.FeatureFlagResult, error) type requestFeaturesFn func(ctx context.Context) (proton.FeatureFlagResult, error)
type GetFlagValueFn func(key string) bool type GetFlagValueFn func(key string) bool

View File

@ -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 <https://www.gnu.org/licenses/>.
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)
}
}

View File

@ -21,13 +21,11 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"path/filepath"
"time" "time"
"github.com/ProtonMail/gluon/async" "github.com/ProtonMail/gluon/async"
"github.com/ProtonMail/gluon/reporter" "github.com/ProtonMail/gluon/reporter"
"github.com/ProtonMail/go-proton-api" "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/events"
"github.com/ProtonMail/proton-bridge/v3/internal/safe" "github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice" "github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice"
@ -65,6 +63,8 @@ type User struct {
id string id string
log *logrus.Entry log *logrus.Entry
userPlan string
vault *vault.User vault *vault.User
client *proton.Client client *proton.Client
reporter reporter.Reporter reporter reporter.Reporter
@ -78,10 +78,7 @@ type User struct {
maxSyncMemory uint64 maxSyncMemory uint64
panicHandler async.PanicHandler panicHandler async.PanicHandler
configStatus *configstatus.ConfigurationStatus
telemetryManager telemetry.Availability telemetryManager telemetry.Availability
// goStatusProgress triggers a check/sending if progress is needed.
goStatusProgress func()
eventService *userevents.Service eventService *userevents.Service
identityService *useridentity.Service identityService *useridentity.Service
@ -104,7 +101,6 @@ func New(
crashHandler async.PanicHandler, crashHandler async.PanicHandler,
showAllMail bool, showAllMail bool,
maxSyncMemory uint64, maxSyncMemory uint64,
statsDir string,
telemetryManager telemetry.Availability, telemetryManager telemetry.Availability,
imapServerManager imapservice.IMAPServerManager, imapServerManager imapservice.IMAPServerManager,
smtpServerManager smtp.ServerManager, smtpServerManager smtp.ServerManager,
@ -125,7 +121,6 @@ func New(
crashHandler, crashHandler,
showAllMail, showAllMail,
maxSyncMemory, maxSyncMemory,
statsDir,
telemetryManager, telemetryManager,
imapServerManager, imapServerManager,
smtpServerManager, smtpServerManager,
@ -159,7 +154,6 @@ func newImpl(
crashHandler async.PanicHandler, crashHandler async.PanicHandler,
showAllMail bool, showAllMail bool,
maxSyncMemory uint64, maxSyncMemory uint64,
statsDir string,
telemetryManager telemetry.Availability, telemetryManager telemetry.Availability,
imapServerManager imapservice.IMAPServerManager, imapServerManager imapservice.IMAPServerManager,
smtpServerManager smtp.ServerManager, smtpServerManager smtp.ServerManager,
@ -184,6 +178,14 @@ func newImpl(
return nil, fmt.Errorf("failed to get addresses: %w", err) 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. // Get the user's API labels.
apiLabels, err := client.GetLabels(ctx, proton.LabelTypeSystem, proton.LabelTypeFolder, proton.LabelTypeLabel) apiLabels, err := client.GetLabels(ctx, proton.LabelTypeSystem, proton.LabelTypeFolder, proton.LabelTypeLabel)
if err != nil { if err != nil {
@ -198,12 +200,6 @@ func newImpl(
"numLabels": len(apiLabels), "numLabels": len(apiLabels),
}).Info("Creating user object") }).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) sendRecorder := sendrecorder.NewSendRecorder(sendrecorder.SendEntryExpiry)
// Create the user object. // Create the user object.
@ -211,6 +207,8 @@ func newImpl(
log: logrus.WithField("userID", apiUser.ID), log: logrus.WithField("userID", apiUser.ID),
id: apiUser.ID, id: apiUser.ID,
userPlan: userPlan,
vault: encVault, vault: encVault,
client: client, client: client,
reporter: reporter, reporter: reporter,
@ -225,7 +223,6 @@ func newImpl(
panicHandler: crashHandler, panicHandler: crashHandler,
configStatus: configStatus,
telemetryManager: telemetryManager, telemetryManager: telemetryManager,
serviceGroup: orderedtasks.NewOrderedCancelGroup(crashHandler), serviceGroup: orderedtasks.NewOrderedCancelGroup(crashHandler),
@ -248,7 +245,7 @@ func newImpl(
addressMode := usertypes.VaultToAddressMode(encVault.AddressMode()) 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) user.telemetryService = telemetryservice.NewService(apiUser.ID, client, user.eventService)
@ -260,7 +257,6 @@ func newImpl(
reporter, reporter,
encVault, encVault,
encVault, encVault,
user,
user.eventService, user.eventService,
addressMode, addressMode,
identityState.Clone(), identityState.Clone(),
@ -279,7 +275,6 @@ func newImpl(
encVault, encVault,
crashHandler, crashHandler,
sendRecorder, sendRecorder,
user,
reporter, reporter,
addressMode, addressMode,
eventSubscription, eventSubscription,
@ -291,12 +286,6 @@ func newImpl(
user.notificationService = notifications.NewService(user.id, user.eventService, user, notificationStore, getFlagValueFn, observabilityService) 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. // When we receive an auth object, we update it in the vault.
// This will be used to authorize the user on the next run. // This will be used to authorize the user on the next run.
user.client.AddAuthHandler(func(auth proton.Auth) { user.client.AddAuthHandler(func(auth proton.Auth) {
@ -340,7 +329,7 @@ func newImpl(
user.identityService.Start(ctx, user.serviceGroup) user.identityService.Start(ctx, user.serviceGroup)
// Add user client to observability service // Add user client to observability service
observabilityService.RegisterUserClient(user.id, client, user.telemetryService) observabilityService.RegisterUserClient(user.id, client, user.telemetryService, userPlan)
// Start Notification service // Start Notification service
user.notificationService.Start(ctx, user.serviceGroup) user.notificationService.Start(ctx, user.serviceGroup)
@ -439,6 +428,11 @@ func (user *User) GetAddressMode() vault.AddressMode {
return user.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. // SetAddressMode sets the user's address mode.
func (user *User) SetAddressMode(ctx context.Context, mode vault.AddressMode) error { func (user *User) SetAddressMode(ctx context.Context, mode vault.AddressMode) error {
user.log.WithField("mode", mode).Info("Setting address mode") 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. // Logout logs the user out from the API.
func (user *User) Logout(ctx context.Context, withAPI bool) error { func (user *User) Logout(ctx context.Context, withAPI, withData, withDataDisabledKillSwitch bool) error {
user.log.WithField("withAPI", withAPI).Info("Logging out user") user.log.WithFields(
logrus.Fields{
"withAPI": withAPI,
"withData": withData,
"withDataDisabledKillSwitch": withDataDisabledKillSwitch,
}).Info("Logging out user")
user.log.Debug("Canceling ongoing tasks") 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) return fmt.Errorf("failed to remove user from smtp server: %w", err)
} }
if err := user.imapService.OnLogout(ctx); err != nil { if withData && !withDataDisabledKillSwitch {
return fmt.Errorf("failed to remove user from imap server: %w", err) 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() user.tasks.CancelAndWait()
@ -698,19 +709,6 @@ func (user *User) SendTelemetry(ctx context.Context, data []byte) error {
return nil 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 { func (user *User) GetSMTPService() *smtp.Service {
return user.smtpService return user.smtpService
} }

View File

@ -160,7 +160,6 @@ func withUser(tb testing.TB, ctx context.Context, _ *server.Server, m *proton.Ma
nil, nil,
true, true,
vault.DefaultMaxSyncMemory, vault.DefaultMaxSyncMemory,
tb.TempDir(),
manager, manager,
nullIMAPServerManager, nullIMAPServerManager,
nullSMTPServerManager, nullSMTPServerManager,

View File

@ -21,6 +21,7 @@ import (
"fmt" "fmt"
"regexp" "regexp"
"runtime" "runtime"
"strings"
"sync" "sync"
) )
@ -42,9 +43,12 @@ func New() *UserAgent {
} }
func (ua *UserAgent) SetClient(name, version string) { func (ua *UserAgent) SetClient(name, version string) {
if strings.EqualFold("Mac OS X Notes", name) {
return
}
ua.lock.Lock() ua.lock.Lock()
defer ua.lock.Unlock() defer ua.lock.Unlock()
ua.client = fmt.Sprintf("%v/%v", name, regexp.MustCompile(`(.*) \((.*)\)`).ReplaceAllString(version, "$1-$2")) ua.client = fmt.Sprintf("%v/%v", name, regexp.MustCompile(`(.*) \((.*)\)`).ReplaceAllString(version, "$1-$2"))
} }

View File

@ -64,6 +64,14 @@ func TestUserAgent(t *testing.T) {
platform: "Windows 10 (10.0)", platform: "Windows 10 (10.0)",
want: "Thunderbird/78.6.1 (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 { for _, test := range tests {

View File

@ -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). // 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-- { for i := len(hdr.Order) - 1; i >= 0; i-- {
key := hdr.Order[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. // 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 // This buffer is used latter on in message writer to construct message and avoid crash
// when key length is more than 76 characters long. // when key length is more than 76 characters long.
res.AddRaw([]byte(key + ": " + val + "\r\n")) res.AddRaw([]byte(key + ": " + values[j] + "\r\n"))
} }
} }

View File

@ -101,6 +101,11 @@ func newTestMessageFromRFC822(t *testing.T, literal []byte) proton.Message {
var parsedHeaders proton.Headers var parsedHeaders proton.Headers
parsedHeaders.Values = make(map[string][]string) parsedHeaders.Values = make(map[string][]string)
h.Entries(func(key, val 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.Values[key] = []string{val}
parsedHeaders.Order = append(parsedHeaders.Order, key) parsedHeaders.Order = append(parsedHeaders.Order, key)
}) })

View File

@ -145,6 +145,9 @@ From: Dummy Recipient <dummy@proton.me>
Date: Tue, 15 Oct 2024 07:54:39 +0000 Date: Tue, 15 Oct 2024 07:54:39 +0000
Mime-Version: 1.0 Mime-Version: 1.0
Content-Type: multipart/mixed;boundary=---------------------a136fc3851075ca3f022f5c3ec6bf8f5 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> Message-Id: <1rYR51zNVZdyCXVvAZ8C9N8OaBg4wO_wg6VlSoLK_Mv-2AaiF5UL-vE_tIZ6FdYP8ylsuV3fpaKUpVwuUcnQ6ql_83aEgZvfC5QcZbind1k=@proton.me>
X-Pm-Spamscore: 0 X-Pm-Spamscore: 0
Received: from mail.protonmail.ch by mail.protonmail.ch; Tue, 15 Oct 2024 07:54:43 +0000 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") lines := strings.Split(str, "\r\n")
// Check we have the expected order // 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 // 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 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, `Subject: header test`, lines[8])
require.Equal(t, `Date: Tue, 15 Oct 2024 07:54:39 +0000`, lines[9]) 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, `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-Attached: image1.jpg`, lines[11])
require.Equal(t, `X-Pm-Spamscore: 0`, lines[12]) require.Equal(t, `X-Attached: image2.jpg`, 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-Attached: image3.jpg`, lines[13])
require.Equal(t, `X-Original-To: test@proton.me`, lines[14]) require.Equal(t, `Message-Id: <1rYR51zNVZdyCXVvAZ8C9N8OaBg4wO_wg6VlSoLK_Mv-2AaiF5UL-vE_tIZ6FdYP8ylsuV3fpaKUpVwuUcnQ6ql_83aEgZvfC5QcZbind1k=@proton.me>`, lines[14])
require.Equal(t, `Return-Path: <dummy@proton.me>`, lines[15]) require.Equal(t, `X-Pm-Spamscore: 0`, lines[15])
require.Equal(t, `Delivered-To: test@proton.me`, lines[16]) 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: <dummy@proton.me>`, lines[18])
require.Equal(t, `Delivered-To: test@proton.me`, lines[19])
} }

View File

@ -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 <https://www.gnu.org/licenses/>.
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
}

View File

@ -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;
}
}
}

View File

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

View File

@ -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<AutomationElement> 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<bool> 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;
}
}
}

View File

@ -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

View File

@ -1,6 +1,8 @@
Feature: Send Telemetry Heartbeat Feature: Send Telemetry Heartbeat
Background: Background:
Given there exists an account with username "[user:user1]" and password "password" 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 Then it succeeds
When bridge starts When bridge starts
Then it succeeds Then it succeeds
@ -12,28 +14,30 @@ Feature: Send Telemetry Heartbeat
When the user logs in with username "[user:user1]" and password "password" When the user logs in with username "[user:user1]" and password "password"
And user "[user:user1]" finishes syncing And user "[user:user1]" finishes syncing
Then bridge eventually sends the following heartbeat: Then bridge eventually sends the following heartbeat:
""" """
{ {
"MeasurementGroup": "bridge.any.usage", "MeasurementGroup": "bridge.any.heartbeat",
"Event": "bridge_heartbeat", "Event": "bridge_heartbeat_new",
"Values": { "Values": {
"nb_account": 1 "NumberConnectedAccounts": 1,
"rolloutPercentage": 1
}, },
"Dimensions": { "Dimensions": {
"auto_update": "on", "isAutoUpdateEnabled": "true",
"auto_start": "on", "isAutoStartEnabled": "true",
"beta": "off", "isBetaEnabled": "false",
"doh": "off", "isDohEnabled": "false",
"split_mode": "off", "usesSplitMode": "false",
"show_all_mail": "on", "useAllMail": "true",
"imap_connection_mode": "starttls", "useDefaultImapPort": "true",
"smtp_connection_mode": "starttls", "useDefaultSmtpPort": "true",
"imap_port": "default", "useDefaultCacheLocation": "true",
"smtp_port": "default", "useDefaultKeychain": "true",
"cache_location": "default", "isContactedByAppleNotes": "false",
"keychain_pref": "default", "imapConnectionMode": "starttls",
"prev_version": "0.0.0", "smtpConnectionMode": "starttls",
"rollout": "42" "prevVersion": "0.0.0",
"bridgePlanGroup": "unknown"
} }
} }
""" """
@ -58,26 +62,28 @@ Feature: Send Telemetry Heartbeat
Then bridge eventually sends the following heartbeat: Then bridge eventually sends the following heartbeat:
""" """
{ {
"MeasurementGroup": "bridge.any.usage", "MeasurementGroup": "bridge.any.heartbeat",
"Event": "bridge_heartbeat", "Event": "bridge_heartbeat_new",
"Values": { "Values": {
"nb_account": 1 "NumberConnectedAccounts": 1,
"rolloutPercentage": 1
}, },
"Dimensions": { "Dimensions": {
"auto_update": "off", "isAutoUpdateEnabled": "false",
"auto_start": "off", "isAutoStartEnabled": "false",
"beta": "off", "isBetaEnabled": "false",
"doh": "on", "isDohEnabled": "true",
"split_mode": "off", "usesSplitMode": "false",
"show_all_mail": "off", "useAllMail": "false",
"imap_connection_mode": "ssl", "useDefaultImapPort": "false",
"smtp_connection_mode": "ssl", "useDefaultSmtpPort": "false",
"imap_port": "custom", "useDefaultCacheLocation": "false",
"smtp_port": "custom", "useDefaultKeychain": "false",
"cache_location": "custom", "isContactedByAppleNotes": "false",
"keychain_pref": "custom", "imapConnectionMode": "ssl",
"prev_version": "0.0.0", "smtpConnectionMode": "ssl",
"rollout": "42" "prevVersion": "0.0.0",
"bridgePlanGroup": "unknown"
} }
} }
""" """
@ -96,26 +102,106 @@ Feature: Send Telemetry Heartbeat
Then bridge eventually sends the following heartbeat: Then bridge eventually sends the following heartbeat:
""" """
{ {
"MeasurementGroup": "bridge.any.usage", "MeasurementGroup": "bridge.any.heartbeat",
"Event": "bridge_heartbeat", "Event": "bridge_heartbeat_new",
"Values": { "Values": {
"nb_account": 1 "NumberConnectedAccounts": 1,
"rolloutPercentage": 1
}, },
"Dimensions": { "Dimensions": {
"auto_update": "on", "isAutoUpdateEnabled": "true",
"auto_start": "on", "isAutoStartEnabled": "true",
"beta": "off", "isBetaEnabled": "false",
"doh": "off", "isDohEnabled": "false",
"split_mode": "on", "usesSplitMode": "true",
"show_all_mail": "on", "useAllMail": "true",
"imap_connection_mode": "starttls", "useDefaultImapPort": "true",
"smtp_connection_mode": "starttls", "useDefaultSmtpPort": "true",
"imap_port": "default", "useDefaultCacheLocation": "true",
"smtp_port": "default", "useDefaultKeychain": "true",
"cache_location": "default", "isContactedByAppleNotes": "false",
"keychain_pref": "default", "imapConnectionMode": "starttls",
"prev_version": "0.0.0", "smtpConnectionMode": "starttls",
"rollout": "42" "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"
} }
} }
""" """

View File

@ -13,26 +13,46 @@ Feature: A user can authenticate an IMAP client
When user "[user:user]" connects IMAP client "1" When user "[user:user]" connects IMAP client "1"
Then IMAP client "1" can authenticate 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 Scenario: IMAP client can authenticate successfully with different case
When user "[user:user]" connects IMAP client "1" When user "[user:user]" connects IMAP client "1"
Then IMAP client "1" can authenticate with address "{toUpper:[user:user]@[domain]}" 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 Scenario: IMAP client can authenticate successfully with secondary address
Given user "[user:user]" connects and authenticates IMAP client "1" with address "[alias:alias]@[domain]" Given user "[user:user]" connects and authenticates IMAP client "1" with address "[alias:alias]@[domain]"
Scenario: IMAP client can authenticate successfully Scenario: IMAP client can authenticate successfully with secondary address using IMAP AUTHENTICATE
When user "[user:user]" connects IMAP client "1" Given user "[user:user]" connects and authenticates IMAP client "1" with address "[alias:alias]@[domain]" using IMAP AUTHENTICATE
Then IMAP client "1" can authenticate
Scenario: IMAP client cannot authenticate with bad username Scenario: IMAP client cannot authenticate with bad username
When user "[user:user]" connects IMAP client "1" When user "[user:user]" connects IMAP client "1"
Then IMAP client "1" cannot authenticate with incorrect username 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 Scenario: IMAP client cannot authenticate with bad password
When user "[user:user]" connects IMAP client "1" When user "[user:user]" connects IMAP client "1"
Then IMAP client "1" cannot authenticate with incorrect password 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 Scenario: IMAP client cannot authenticate for disconnected user
When user "[user:user]" logs out When user "[user:user]" logs out
And user "[user:user]" connects IMAP client "1" And user "[user:user]" connects IMAP client "1"
Then IMAP client "1" cannot authenticate 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

View File

@ -22,4 +22,29 @@ Feature: The IMAP ID is propagated to bridge
When user "[user:user]" connects IMAP client "1" When user "[user:user]" connects IMAP client "1"
And IMAP client "1" announces its ID with name "name" and version "version" And IMAP client "1" announces its ID with name "name" and version "version"
When the user reports a bug 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])" 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])"

View File

@ -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

View File

@ -54,6 +54,10 @@ func (s *scenario) bridgeNeedsToSendHeartbeat() error {
return nil return nil
} }
func (s *scenario) bridgeNeedsToSendExplicitHeartbeat() error {
return s.t.heartbeat.SetLastHeartbeatSent(time.Now().Add(-24 * time.Hour))
}
func (s *scenario) bridgeDoNotNeedToSendHeartbeat() error { func (s *scenario) bridgeDoNotNeedToSendHeartbeat() error {
last := s.t.heartbeat.GetLastHeartbeatSent() last := s.t.heartbeat.GetLastHeartbeatSent()
if isAnotherDay(last, time.Now()) { if isAnotherDay(last, time.Now()) {
@ -73,7 +77,7 @@ func matchHeartbeat(have, want telemetry.HeartbeatData) error {
} }
// Ignore rollout number // Ignore rollout number
want.Dimensions.Rollout = have.Dimensions.Rollout want.Values.Rollout = have.Values.Rollout
if have != want { if have != want {
return fmt.Errorf("missing heartbeat: have %#v, want %#v", have, want) return fmt.Errorf("missing heartbeat: have %#v, want %#v", have, want)

View File

@ -36,10 +36,38 @@ import (
"github.com/emersion/go-imap" "github.com/emersion/go-imap"
id "github.com/emersion/go-imap-id" id "github.com/emersion/go-imap-id"
"github.com/emersion/go-imap/client" "github.com/emersion/go-imap/client"
"github.com/emersion/go-sasl"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"golang.org/x/exp/slices" "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 { func (s *scenario) userConnectsIMAPClient(username, clientID string) error {
return s.t.newIMAPClient(s.t.getUserByName(username).getUserID(), clientID) 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) 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 { func (s *scenario) userConnectsAndCanNotAuthenticateIMAPClientWithAddress(username, clientID, address string) error {
@ -69,7 +107,7 @@ func (s *scenario) userConnectsAndCanNotAuthenticateIMAPClientWithAddress(userna
userID, client := s.t.getIMAPClient(clientID) 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") return fmt.Errorf("expected error, got nil")
} }
@ -79,19 +117,51 @@ func (s *scenario) userConnectsAndCanNotAuthenticateIMAPClientWithAddress(userna
func (s *scenario) imapClientCanAuthenticate(clientID string) error { func (s *scenario) imapClientCanAuthenticate(clientID string) error {
userID, client := s.t.getIMAPClient(clientID) 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 { func (s *scenario) imapClientCanAuthenticateWithAddress(clientID string, address string) error {
userID, client := s.t.getIMAPClient(clientID) 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 { func (s *scenario) imapClientCannotAuthenticate(clientID string) error {
userID, client := s.t.getIMAPClient(clientID) 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") 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 { func (s *scenario) imapClientCannotAuthenticateWithAddress(clientID, address string) error {
userID, client := s.t.getIMAPClient(clientID) 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") 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 { func (s *scenario) imapClientCannotAuthenticateWithIncorrectUsername(clientID string) error {
userID, client := s.t.getIMAPClient(clientID) 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") return fmt.Errorf("expected error, got nil")
} }
@ -121,7 +211,17 @@ func (s *scenario) imapClientCannotAuthenticateWithIncorrectUsername(clientID st
func (s *scenario) imapClientCannotAuthenticateWithIncorrectPassword(clientID string) error { func (s *scenario) imapClientCannotAuthenticateWithIncorrectPassword(clientID string) error {
userID, client := s.t.getIMAPClient(clientID) userID, client := s.t.getIMAPClient(clientID)
badPass := base64.StdEncoding.EncodeToString([]byte("bad_password")) 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") return fmt.Errorf("expected error, got nil")
} }

View File

@ -116,6 +116,7 @@ func (s *scenario) steps(ctx *godog.ScenarioContext) {
ctx.Step(`^user "([^"]*)" has telemetry set to (\d+)$`, s.userHasTelemetrySetTo) 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 changed to "([^"]*)"`, s.bridgePasswordOfUserIsChangedTo)
ctx.Step(`^the bridge password of user "([^"]*)" is equal to "([^"]*)"`, s.bridgePasswordOfUserIsEqualTo) 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 ==== // ==== ACCOUNT SETTINGS ====
ctx.Step(`^the account "([^"]*)" has public key attachment "([^"]*)"`, s.accountHasPublicKeyAttachment) 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 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 "([^"]*)"$`, s.userConnectsAndAuthenticatesIMAPClient)
ctx.Step(`^user "([^"]*)" connects and authenticates IMAP client "([^"]*)" with address "([^"]*)"$`, s.userConnectsAndAuthenticatesIMAPClientWithAddress) 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(`^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$`, 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 "([^"]*)"$`, 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$`, 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 address "([^"]*)"$`, s.imapClientCannotAuthenticateWithAddress)
ctx.Step(`^IMAP client "([^"]*)" cannot authenticate with incorrect username$`, s.imapClientCannotAuthenticateWithIncorrectUsername) 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$`, 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 "([^"]*)" closes$`, s.imapClientCloses)
ctx.Step(`^IMAP client "([^"]*)" announces its ID with name "([^"]*)" and version "([^"]*)"$`, s.imapClientAnnouncesItsIDWithNameAndVersion) ctx.Step(`^IMAP client "([^"]*)" announces its ID with name "([^"]*)" and version "([^"]*)"$`, s.imapClientAnnouncesItsIDWithNameAndVersion)
ctx.Step(`^IMAP client "([^"]*)" creates "([^"]*)"$`, s.imapClientCreatesMailbox) ctx.Step(`^IMAP client "([^"]*)" creates "([^"]*)"$`, s.imapClientCreatesMailbox)
@ -201,15 +208,10 @@ func (s *scenario) steps(ctx *godog.ScenarioContext) {
// ==== TELEMETRY ==== // ==== TELEMETRY ====
ctx.Step(`^bridge eventually sends the following heartbeat:$`, s.bridgeEventuallySendsTheFollowingHeartbeat) ctx.Step(`^bridge eventually sends the following heartbeat:$`, s.bridgeEventuallySendsTheFollowingHeartbeat)
ctx.Step(`^bridge needs to send heartbeat`, s.bridgeNeedsToSendHeartbeat) 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(`^bridge do not need to send heartbeat`, s.bridgeDoNotNeedToSendHeartbeat)
ctx.Step(`^heartbeat is not whitelisted`, s.heartbeatIsNotwhitelisted) 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 ==== // ==== CONTACT ====
ctx.Step(`^user "([^"]*)" has contact "([^"]*)" with name "([^"]*)"$`, s.userHasContactWithName) ctx.Step(`^user "([^"]*)" has contact "([^"]*)" with name "([^"]*)"$`, s.userHasContactWithName)

View File

@ -22,6 +22,8 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/mail" "net/mail"
"os"
"path/filepath"
"strings" "strings"
"time" "time"
@ -388,6 +390,70 @@ func (s *scenario) userIsDeleted(username string) error {
return s.t.bridge.DeleteUser(context.Background(), s.t.getUserByName(username).getUserID()) 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 { func (s *scenario) theAuthOfUserIsRevoked(username string) error {
return s.t.withClient(context.Background(), username, func(ctx context.Context, client *proton.Client) error { return s.t.withClient(context.Background(), username, func(ctx context.Context, client *proton.Client) error {
return client.AuthRevokeAll(ctx) return client.AuthRevokeAll(ctx)