forked from Silverfish/proton-bridge
Compare commits
15 Commits
917cf3fd51
...
v3.16.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 03c9455b0d | |||
| dd2448f35a | |||
| 3f78f4d672 | |||
| 5fbe94c559 | |||
| 80d556343e | |||
| 612d1054db | |||
| acf2fc32c4 | |||
| af01c63298 | |||
| 2e98d64f94 | |||
| cdcdd45bcf | |||
| b3e2a91f56 | |||
| 7d9753e2da | |||
| f1aef383b7 | |||
| 6647231278 | |||
| 531368da86 |
45
Changelog.md
45
Changelog.md
@ -3,6 +3,51 @@
|
|||||||
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||||
|
|
||||||
|
|
||||||
|
## Flavien Bridge 3.16.0
|
||||||
|
|
||||||
|
### Added
|
||||||
|
* BRIDGE-205: Add support for the IMAP AUTHENTICATE command.
|
||||||
|
* BRIDGE-268: Add kill switch feature flag for the IMAP AUTHENTICATE command.
|
||||||
|
* BRIDGE-261: Delete gluon data during user deletion.
|
||||||
|
* BRIDGE-246: Test: Add Settings Menu Bridge UI e2e automation tests.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* BRIDGE-107: Improved human verification UX.
|
||||||
|
* BRIDGE-281: Disable keychain test on macOS.
|
||||||
|
* BRIDGE-266: Heartbeat telemetry update.
|
||||||
|
* BRIDGE-253: Removed unused telemetry (activation and troubleshooting).
|
||||||
|
* BRIDGE-252: Restored the -h shortcut for the CLI --help switch.
|
||||||
|
* BRIDGE-264: Ignore apple notes as UserAgent.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* BRIDGE-256: Fix reversed order of headers with multiple values.
|
||||||
|
* BRIDGE-258: Fixed issue with draft updates and sending during synchronization.
|
||||||
|
|
||||||
|
|
||||||
|
## Erasmus Bridge 3.15.0
|
||||||
|
|
||||||
|
### Added
|
||||||
|
* BRIDGE-238: Added host information to sentry events; new sentry event for keychain issues.
|
||||||
|
* BRIDGE-236: Added SMTP observability metrics.
|
||||||
|
* BRIDGE-217: Added missing parameter to the CLI help command.
|
||||||
|
* BRIDGE-234: Add accessibility name in QML for UI automation.
|
||||||
|
* BRIDGE-232: Test: Add Home Menu Bridge UI e2e automation tests.
|
||||||
|
* BRIDGE-220: Test: Add Bridge E2E UI login/logout tests for Windows.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* BRIDGE-228: Removed sentry events.
|
||||||
|
* BRIDGE-218: Observability adapter; gluon observability metrics and tests.
|
||||||
|
* BRIDGE-215: Tweak wording on macOS profile install page.
|
||||||
|
* BRIDGE-131: Test: Integration tests for messages from Proton <-> Gmail.
|
||||||
|
* BRIDGE-142: Bridge icon can be removed from the menu bar on macOS.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* BRIDGE-240: Fix for running against Qt 6.8 (contribution of GitHub user Cimbali).
|
||||||
|
* BRIDGE-231: Fix reversed header order in messages.
|
||||||
|
* BRIDGE-235: Fix compilation of Bridge GUI Tester on Windows.
|
||||||
|
* BRIDGE-120: Use appropriate address key when importing / saving draft.
|
||||||
|
|
||||||
|
|
||||||
## Dragon Bridge 3.14.0
|
## Dragon Bridge 3.14.0
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
2
Makefile
2
Makefile
@ -12,7 +12,7 @@ ROOT_DIR:=$(realpath .)
|
|||||||
.PHONY: build build-gui build-nogui build-launcher versioner hasher
|
.PHONY: build build-gui build-nogui build-launcher versioner hasher
|
||||||
|
|
||||||
# Keep version hardcoded so app build works also without Git repository.
|
# Keep version hardcoded so app build works also without Git repository.
|
||||||
BRIDGE_APP_VERSION?=3.14.0+git
|
BRIDGE_APP_VERSION?=3.16.0+git
|
||||||
APP_VERSION:=${BRIDGE_APP_VERSION}
|
APP_VERSION:=${BRIDGE_APP_VERSION}
|
||||||
APP_FULL_NAME:=Proton Mail Bridge
|
APP_FULL_NAME:=Proton Mail Bridge
|
||||||
APP_VENDOR:=Proton AG
|
APP_VENDOR:=Proton AG
|
||||||
|
|||||||
2
go.mod
2
go.mod
@ -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
16
go.sum
@ -34,20 +34,8 @@ github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAE
|
|||||||
github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=
|
github.com/ProtonMail/bcrypt v0.0.0-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=
|
||||||
|
|||||||
@ -97,18 +97,21 @@ const (
|
|||||||
appShortName = "bridge"
|
appShortName = "bridge"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// the two flags below have been deprecated by BRIDGE-281. We however keep them so that bridge does not error if they are passed on startup.
|
||||||
var cliFlagEnableKeychainTest = &cli.BoolFlag{ //nolint:gochecknoglobals
|
var cliFlagEnableKeychainTest = &cli.BoolFlag{ //nolint:gochecknoglobals
|
||||||
Name: flagEnableKeychainTest,
|
Name: flagEnableKeychainTest,
|
||||||
Usage: "Enable the keychain test for the current and future executions of the application",
|
Usage: "This flag is deprecated and does nothing",
|
||||||
Value: false,
|
Value: false,
|
||||||
DisableDefaultText: true,
|
DisableDefaultText: true,
|
||||||
} //nolint:gochecknoglobals
|
Hidden: true,
|
||||||
|
}
|
||||||
|
|
||||||
var cliFlagDisableKeychainTest = &cli.BoolFlag{ //nolint:gochecknoglobals
|
var cliFlagDisableKeychainTest = &cli.BoolFlag{ //nolint:gochecknoglobals
|
||||||
Name: flagDisableKeychainTest,
|
Name: flagDisableKeychainTest,
|
||||||
Usage: "Disable the keychain test for the current and future executions of the application",
|
Usage: "This flag is deprecated and does nothing",
|
||||||
Value: false,
|
Value: false,
|
||||||
DisableDefaultText: true,
|
DisableDefaultText: true,
|
||||||
|
Hidden: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
func New() *cli.App {
|
func New() *cli.App {
|
||||||
@ -205,12 +208,14 @@ 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,
|
||||||
}
|
}
|
||||||
|
|
||||||
if onMacOS() {
|
if onMacOS() {
|
||||||
// The two flags below were introduced for BRIDGE-116, and are available only on macOS.
|
// The two flags below were introduced for BRIDGE-116, and are available only on macOS.
|
||||||
|
// They have been later removed fro BRIDGE-281.
|
||||||
app.Flags = append(app.Flags, cliFlagEnableKeychainTest, cliFlagDisableKeychainTest)
|
app.Flags = append(app.Flags, cliFlagEnableKeychainTest, cliFlagDisableKeychainTest)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -282,8 +287,7 @@ func run(c *cli.Context) error {
|
|||||||
|
|
||||||
return withSingleInstance(settings, locations.GetLockFile(), version, func() error {
|
return withSingleInstance(settings, locations.GetLockFile(), version, func() error {
|
||||||
// Look for available keychains
|
// Look for available keychains
|
||||||
skipKeychainTest := checkSkipKeychainTest(c, settings)
|
return WithKeychainList(crashHandler, func(keychains *keychain.List) error {
|
||||||
return WithKeychainList(crashHandler, skipKeychainTest, func(keychains *keychain.List) error {
|
|
||||||
// Unlock the encrypted vault.
|
// Unlock the encrypted vault.
|
||||||
return WithVault(reporter, locations, keychains, crashHandler, func(v *vault.Vault, insecure, corrupt bool) error {
|
return WithVault(reporter, locations, keychains, crashHandler, func(v *vault.Vault, insecure, corrupt bool) error {
|
||||||
if !v.Migrated() {
|
if !v.Migrated() {
|
||||||
@ -547,11 +551,11 @@ func withCookieJar(vault *vault.Vault, fn func(http.CookieJar) error) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WithKeychainList init the list of usable keychains.
|
// WithKeychainList init the list of usable keychains.
|
||||||
func WithKeychainList(panicHandler async.PanicHandler, skipKeychainTest bool, fn func(*keychain.List) error) error {
|
func WithKeychainList(panicHandler async.PanicHandler, fn func(*keychain.List) error) error {
|
||||||
logrus.Debug("Creating keychain list")
|
logrus.Debug("Creating keychain list")
|
||||||
defer logrus.Debug("Keychain list stop")
|
defer logrus.Debug("Keychain list stop")
|
||||||
defer async.HandlePanic(panicHandler)
|
defer async.HandlePanic(panicHandler)
|
||||||
return fn(keychain.NewList(skipKeychainTest))
|
return fn(keychain.NewList())
|
||||||
}
|
}
|
||||||
|
|
||||||
func setDeviceCookies(jar *cookies.Jar) error {
|
func setDeviceCookies(jar *cookies.Jar) error {
|
||||||
@ -572,38 +576,6 @@ func setDeviceCookies(jar *cookies.Jar) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkSkipKeychainTest(c *cli.Context, settingsDir string) bool {
|
|
||||||
if !onMacOS() {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
enable := c.Bool(flagEnableKeychainTest)
|
|
||||||
disable := c.Bool(flagDisableKeychainTest)
|
|
||||||
|
|
||||||
skip, err := vault.GetShouldSkipKeychainTest(settingsDir)
|
|
||||||
if err != nil {
|
|
||||||
logrus.WithError(err).Error("Could not load keychain settings.")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!enable) && (!disable) {
|
|
||||||
return skip
|
|
||||||
}
|
|
||||||
|
|
||||||
// if both switches are passed, 'enable' has priority
|
|
||||||
if disable {
|
|
||||||
skip = true
|
|
||||||
}
|
|
||||||
if enable {
|
|
||||||
skip = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := vault.SetShouldSkipKeychainTest(settingsDir, skip); err != nil {
|
|
||||||
logrus.WithError(err).Error("Could not save keychain settings.")
|
|
||||||
}
|
|
||||||
|
|
||||||
return skip
|
|
||||||
}
|
|
||||||
|
|
||||||
func onMacOS() bool {
|
func onMacOS() bool {
|
||||||
return runtime.GOOS == "darwin"
|
return runtime.GOOS == "darwin"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,64 +0,0 @@
|
|||||||
// Copyright (c) 2024 Proton AG
|
|
||||||
//
|
|
||||||
// This file is part of Proton Mail Bridge.
|
|
||||||
//
|
|
||||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU General Public License
|
|
||||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"github.com/urfave/cli/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestCheckSkipKeychainTest(t *testing.T) {
|
|
||||||
var expectedResult bool
|
|
||||||
dir := t.TempDir()
|
|
||||||
app := cli.App{
|
|
||||||
Flags: []cli.Flag{
|
|
||||||
cliFlagEnableKeychainTest,
|
|
||||||
cliFlagDisableKeychainTest,
|
|
||||||
},
|
|
||||||
Action: func(c *cli.Context) error {
|
|
||||||
require.Equal(t, expectedResult, checkSkipKeychainTest(c, dir))
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
noArgs := []string{"appName"}
|
|
||||||
enableArgs := []string{"appName", "-" + flagEnableKeychainTest}
|
|
||||||
disableArgs := []string{"appName", "-" + flagDisableKeychainTest}
|
|
||||||
bothArgs := []string{"appName", "-" + flagDisableKeychainTest, "-" + flagEnableKeychainTest}
|
|
||||||
|
|
||||||
onMac := onMacOS()
|
|
||||||
|
|
||||||
expectedResult = false
|
|
||||||
require.NoError(t, app.Run(noArgs))
|
|
||||||
|
|
||||||
expectedResult = onMac
|
|
||||||
require.NoError(t, app.Run(disableArgs))
|
|
||||||
require.NoError(t, app.Run(noArgs))
|
|
||||||
|
|
||||||
expectedResult = false
|
|
||||||
require.NoError(t, app.Run(enableArgs))
|
|
||||||
require.NoError(t, app.Run(noArgs))
|
|
||||||
|
|
||||||
expectedResult = onMac
|
|
||||||
require.NoError(t, app.Run(disableArgs))
|
|
||||||
|
|
||||||
expectedResult = false
|
|
||||||
require.NoError(t, app.Run(bothArgs))
|
|
||||||
}
|
|
||||||
@ -723,3 +723,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
|
||||||
|
}
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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)
|
|
||||||
}
|
|
||||||
@ -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)
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
@ -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(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
@ -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,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
@ -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(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
@ -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
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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);
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
//
|
//
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
|
|||||||
@ -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.
|
||||||
|
|
||||||
|
|||||||
@ -83,7 +83,6 @@ SettingsView {
|
|||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
Backend.updateCurrentMailClient();
|
Backend.updateCurrentMailClient();
|
||||||
Backend.notifyReportBugClicked();
|
|
||||||
root.parent.showBugReport();
|
root.parent.showBugReport();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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("I’ve 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")
|
||||||
|
|||||||
@ -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__);
|
|
||||||
}
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
//
|
//
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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()))
|
||||||
|
|||||||
@ -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
|
|
||||||
}
|
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
87
internal/plan/plan.go
Normal file
87
internal/plan/plan.go
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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)"),
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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),
|
||||||
|
|||||||
@ -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),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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))
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
|
||||||
}
|
|
||||||
@ -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) {
|
||||||
|
|||||||
@ -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",
|
||||||
|
UseDefaultIMAPPort: "true",
|
||||||
|
UseDefaultSMTPPort: "true",
|
||||||
|
UseDefaultCacheLocation: "true",
|
||||||
|
UseDefaultKeychain: "true",
|
||||||
|
ContactedByAppleNotes: "false",
|
||||||
|
PrevVersion: "1.2.3",
|
||||||
IMAPConnectionMode: "ssl",
|
IMAPConnectionMode: "ssl",
|
||||||
SMTPConnectionMode: "ssl",
|
SMTPConnectionMode: "ssl",
|
||||||
IMAPPort: "default",
|
UserPlanGroup: plan.Unknown,
|
||||||
SMTPPort: "default",
|
|
||||||
CacheLocation: "default",
|
|
||||||
KeychainPref: "default",
|
|
||||||
PrevVersion: "1.2.3",
|
|
||||||
Rollout: "10",
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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,9 +606,21 @@ 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 withData && !withDataDisabledKillSwitch {
|
||||||
|
if err := user.imapService.OnDelete(ctx); err != nil {
|
||||||
|
if rerr := user.reporter.ReportMessageWithContext("Failed to delete user IMAP data", map[string]any{
|
||||||
|
"error": err.Error(),
|
||||||
|
}); rerr != nil {
|
||||||
|
logrus.WithError(rerr).Info("Failed to report user IMAP deletion issue to Sentry")
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("failed to delete user from imap server: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if err := user.imapService.OnLogout(ctx); err != nil {
|
if err := user.imapService.OnLogout(ctx); err != nil {
|
||||||
return fmt.Errorf("failed to remove user from imap server: %w", err)
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -31,21 +31,12 @@ const (
|
|||||||
MacOSKeychain = "macos-keychain"
|
MacOSKeychain = "macos-keychain"
|
||||||
)
|
)
|
||||||
|
|
||||||
func listHelpers(skipKeychainTest bool) (Helpers, string) {
|
func listHelpers() (Helpers, string) {
|
||||||
helpers := make(Helpers)
|
helpers := make(Helpers)
|
||||||
|
|
||||||
// MacOS always provides a keychain.
|
// MacOS always provides a keychain.
|
||||||
if skipKeychainTest {
|
|
||||||
logrus.WithField("pkg", "keychain").Info("Skipping macOS keychain test")
|
logrus.WithField("pkg", "keychain").Info("Skipping macOS keychain test")
|
||||||
helpers[MacOSKeychain] = newMacOSHelper
|
helpers[MacOSKeychain] = newMacOSHelper
|
||||||
} else {
|
|
||||||
if isUsable(newMacOSHelper("")) {
|
|
||||||
helpers[MacOSKeychain] = newMacOSHelper
|
|
||||||
logrus.WithField("keychain", "MacOSKeychain").Info("Keychain is usable.")
|
|
||||||
} else {
|
|
||||||
logrus.WithField("keychain", "MacOSKeychain").Debug("Keychain is not available.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use MacOSKeychain by default.
|
// Use MacOSKeychain by default.
|
||||||
return helpers, MacOSKeychain
|
return helpers, MacOSKeychain
|
||||||
|
|||||||
@ -31,7 +31,7 @@ const (
|
|||||||
SecretServiceDBus = "secret-service-dbus"
|
SecretServiceDBus = "secret-service-dbus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func listHelpers(_ bool) (Helpers, string) {
|
func listHelpers() (Helpers, string) {
|
||||||
helpers := make(Helpers)
|
helpers := make(Helpers)
|
||||||
|
|
||||||
if isUsable(newDBusHelper("")) {
|
if isUsable(newDBusHelper("")) {
|
||||||
|
|||||||
@ -25,7 +25,7 @@ import (
|
|||||||
|
|
||||||
const WindowsCredentials = "windows-credentials"
|
const WindowsCredentials = "windows-credentials"
|
||||||
|
|
||||||
func listHelpers(_ bool) (Helpers, string) {
|
func listHelpers() (Helpers, string) {
|
||||||
helpers := make(Helpers)
|
helpers := make(Helpers)
|
||||||
// Windows always provides a keychain.
|
// Windows always provides a keychain.
|
||||||
if isUsable(newWinCredHelper("")) {
|
if isUsable(newWinCredHelper("")) {
|
||||||
|
|||||||
@ -62,9 +62,9 @@ type List struct {
|
|||||||
// NewList checks availability of every keychains detected on the User Operating System
|
// NewList checks availability of every keychains detected on the User Operating System
|
||||||
// This will ask the user to unlock keychain(s) to check their usability.
|
// This will ask the user to unlock keychain(s) to check their usability.
|
||||||
// This should only be called once.
|
// This should only be called once.
|
||||||
func NewList(skipKeychainTest bool) *List {
|
func NewList() *List {
|
||||||
var list = List{locker: &sync.Mutex{}}
|
var list = List{locker: &sync.Mutex{}}
|
||||||
list.helpers, list.defaultHelper = listHelpers(skipKeychainTest)
|
list.helpers, list.defaultHelper = listHelpers()
|
||||||
return &list
|
return &list
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -210,7 +210,7 @@ func (kc *Keychain) secretURL(userID string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// isUsable returns whether the credentials helper is usable.
|
// isUsable returns whether the credentials helper is usable.
|
||||||
func isUsable(helper credentials.Helper, err error) bool {
|
func isUsable(helper credentials.Helper, err error) bool { //nolint:unused
|
||||||
l := logrus.WithField("helper", reflect.TypeOf(helper))
|
l := logrus.WithField("helper", reflect.TypeOf(helper))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -240,7 +240,7 @@ func isUsable(helper credentials.Helper, err error) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTestCredentials() *credentials.Credentials {
|
func getTestCredentials() *credentials.Credentials { //nolint:unused
|
||||||
// On macOS, a handful of users experience failures of the test credentials.
|
// On macOS, a handful of users experience failures of the test credentials.
|
||||||
if runtime.GOOS == "darwin" {
|
if runtime.GOOS == "darwin" {
|
||||||
return &credentials.Credentials{
|
return &credentials.Credentials{
|
||||||
@ -257,7 +257,7 @@ func getTestCredentials() *credentials.Credentials {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func retry(condition func() error) error {
|
func retry(condition func() error) error { //nolint:unused
|
||||||
var maxRetry = 5
|
var maxRetry = 5
|
||||||
for r := 0; ; r++ {
|
for r := 0; ; r++ {
|
||||||
err := condition()
|
err := condition()
|
||||||
|
|||||||
@ -117,7 +117,7 @@ func TestInsertReadRemove(t *testing.T) {
|
|||||||
|
|
||||||
func TestIsErrKeychainNoItem(t *testing.T) {
|
func TestIsErrKeychainNoItem(t *testing.T) {
|
||||||
r := require.New(t)
|
r := require.New(t)
|
||||||
helpers := NewList(false).GetHelpers()
|
helpers := NewList().GetHelpers()
|
||||||
|
|
||||||
for helperName := range helpers {
|
for helperName := range helpers {
|
||||||
kc, err := NewKeychain(helperName, "bridge-test", helpers, helperName)
|
kc, err := NewKeychain(helperName, "bridge-test", helpers, helperName)
|
||||||
|
|||||||
@ -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"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
})
|
})
|
||||||
|
|||||||
@ -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])
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
|
||||||
}
|
|
||||||
118
tests/e2e/ui_tests/windows_os/Results/SettingsMenuResults.cs
Normal file
118
tests/e2e/ui_tests/windows_os/Results/SettingsMenuResults.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
332
tests/e2e/ui_tests/windows_os/Tests/SettingsMenuTests.cs
Normal file
332
tests/e2e/ui_tests/windows_os/Tests/SettingsMenuTests.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
523
tests/e2e/ui_tests/windows_os/Windows/SettingsMenuWindow.cs
Normal file
523
tests/e2e/ui_tests/windows_os/Windows/SettingsMenuWindow.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
|
||||||
@ -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
|
||||||
@ -14,26 +16,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": "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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -23,3 +23,28 @@ Feature: The IMAP ID is propagated to bridge
|
|||||||
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])"
|
||||||
|
|
||||||
|
|||||||
25
tests/features/user/delete_imap.feature
Normal file
25
tests/features/user/delete_imap.feature
Normal 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
|
||||||
@ -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)
|
||||||
|
|||||||
@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -61,7 +61,7 @@ func main() {
|
|||||||
|
|
||||||
func getRollout(_ *cli.Context) error {
|
func getRollout(_ *cli.Context) error {
|
||||||
return app.WithLocations(func(locations *locations.Locations) error {
|
return app.WithLocations(func(locations *locations.Locations) error {
|
||||||
return app.WithKeychainList(async.NoopPanicHandler{}, false, func(keychains *keychain.List) error {
|
return app.WithKeychainList(async.NoopPanicHandler{}, func(keychains *keychain.List) error {
|
||||||
return app.WithVault(nil, locations, keychains, async.NoopPanicHandler{}, func(vault *vault.Vault, _, _ bool) error {
|
return app.WithVault(nil, locations, keychains, async.NoopPanicHandler{}, func(vault *vault.Vault, _, _ bool) error {
|
||||||
fmt.Println(vault.GetUpdateRollout())
|
fmt.Println(vault.GetUpdateRollout())
|
||||||
return nil
|
return nil
|
||||||
@ -72,7 +72,7 @@ func getRollout(_ *cli.Context) error {
|
|||||||
|
|
||||||
func setRollout(c *cli.Context) error {
|
func setRollout(c *cli.Context) error {
|
||||||
return app.WithLocations(func(locations *locations.Locations) error {
|
return app.WithLocations(func(locations *locations.Locations) error {
|
||||||
return app.WithKeychainList(async.NoopPanicHandler{}, false, func(keychains *keychain.List) error {
|
return app.WithKeychainList(async.NoopPanicHandler{}, func(keychains *keychain.List) error {
|
||||||
return app.WithVault(nil, locations, keychains, async.NoopPanicHandler{}, func(vault *vault.Vault, _, _ bool) error {
|
return app.WithVault(nil, locations, keychains, async.NoopPanicHandler{}, func(vault *vault.Vault, _, _ bool) error {
|
||||||
clamped := max(0.0, min(1.0, c.Float64("value")))
|
clamped := max(0.0, min(1.0, c.Float64("value")))
|
||||||
if err := vault.SetUpdateRollout(clamped); err != nil {
|
if err := vault.SetUpdateRollout(clamped); err != nil {
|
||||||
|
|||||||
@ -51,7 +51,7 @@ func main() {
|
|||||||
|
|
||||||
func readAction(c *cli.Context) error {
|
func readAction(c *cli.Context) error {
|
||||||
return app.WithLocations(func(locations *locations.Locations) error {
|
return app.WithLocations(func(locations *locations.Locations) error {
|
||||||
return app.WithKeychainList(async.NoopPanicHandler{}, false, func(keychains *keychain.List) error {
|
return app.WithKeychainList(async.NoopPanicHandler{}, func(keychains *keychain.List) error {
|
||||||
return app.WithVault(nil, locations, keychains, async.NoopPanicHandler{}, func(vault *vault.Vault, insecure, corrupt bool) error {
|
return app.WithVault(nil, locations, keychains, async.NoopPanicHandler{}, func(vault *vault.Vault, insecure, corrupt bool) error {
|
||||||
if _, err := os.Stdout.Write(vault.ExportJSON()); err != nil {
|
if _, err := os.Stdout.Write(vault.ExportJSON()); err != nil {
|
||||||
return fmt.Errorf("failed to write vault: %w", err)
|
return fmt.Errorf("failed to write vault: %w", err)
|
||||||
@ -65,7 +65,7 @@ func readAction(c *cli.Context) error {
|
|||||||
|
|
||||||
func writeAction(c *cli.Context) error {
|
func writeAction(c *cli.Context) error {
|
||||||
return app.WithLocations(func(locations *locations.Locations) error {
|
return app.WithLocations(func(locations *locations.Locations) error {
|
||||||
return app.WithKeychainList(async.NoopPanicHandler{}, false, func(keychains *keychain.List) error {
|
return app.WithKeychainList(async.NoopPanicHandler{}, func(keychains *keychain.List) error {
|
||||||
return app.WithVault(nil, locations, keychains, async.NoopPanicHandler{}, func(vault *vault.Vault, insecure, corrupt bool) error {
|
return app.WithVault(nil, locations, keychains, async.NoopPanicHandler{}, func(vault *vault.Vault, insecure, corrupt bool) error {
|
||||||
b, err := io.ReadAll(os.Stdin)
|
b, err := io.ReadAll(os.Stdin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user