chore: merge tmp/Quebec into devel

This commit is contained in:
Jakub
2023-04-28 14:04:55 +02:00
116 changed files with 5824 additions and 4059 deletions

View File

@ -6,9 +6,12 @@
* Go 1.18 * Go 1.18
* Bash with basic build utils: make, gcc, sed, find, grep, ... * Bash with basic build utils: make, gcc, sed, find, grep, ...
- For Windows, it is recommended to use MinGW 64bit shell from [MSYS2](https://www.msys2.org/) - For Windows, it is recommended to use MinGW 64bit shell from [MSYS2](https://www.msys2.org/)
* GCC (linux), msvc (windows) or Xcode (macOS) * GCC (Linux), msvc (Windows) or Xcode (macOS)
* Windres (windows) * Windres (Windows)
* libglvnd and libsecret development files (linux) * libglvnd and libsecret development files (Linux)
* pkg-config (Linux)
* cmake, ninja-build and Qt 6 are required to build the graphical user interface. On Linux,
the Mesa OpenGL development files are also needed.
To enable the sending of crash reports using Sentry please set the To enable the sending of crash reports using Sentry please set the
`DSN_SENTRY` environment variable with the client key of your sentry project before build. `DSN_SENTRY` environment variable with the client key of your sentry project before build.

View File

@ -2,13 +2,46 @@
Changelog [format](http://keepachangelog.com/en/1.0.0/) Changelog [format](http://keepachangelog.com/en/1.0.0/)
## [Bridge 3.2.0] Rialto
### Added
* GODT-2552, GODT-2553, GODT-2555, GODT-2556: Add telemetry.
* GODT-2575: Add dev info to cookies.
### Changed
* GODT-2496: Bump gopenPGP to 2.7.1-proton.
* GODT-2517: Replace status window with native tray icon context menu.
* GODT-2586: Two-columns layout for account details.
* GODT-2580: Updated link to support website in GUI.
* GODT-2239: Bridgepp worker/overseer unit tests.
* GODT-2538: Implement smart picking of default IMAP/SMTP ports.
* GODT-2502: Improve logs.
* GODT-2551: Store and Recover Last User Agent from Vault.
* GODT-2550: Verify IMAP ID is set properly.
* GODT-2554: Compute telemetry availability from API UserSettings.
* Add missing double quotes in test.
* GODT-2239: Unit tests for BridgeUtils.cpp in bridgepp.
* Replace go-rfc5322 with gluon's rfc5322 parser.
* GODT-2483: Install cert without external tool on macOS.
### Fixed
* GODT-2588: Always perma-delete from Drafts/Trash.
* GODT-2582: Dedup recovered messages folder.
* GODT-2589: Update BUILDS.md.
* GODT-2581: Update outdated link to bridge homepage in CLI 'manual' command.
* GODT-2337: Filter reply-to on draft.
* GODT-2550: Announce IMAP ID Capability.
* GODT-2574: Fix label/unlabel of large amounts of messages.
* GODT-2573: Handle invalid header fields in message.
* GODT-2573: Crash on null update.
* GODT-2407: Replace invalid email addresses with emtpy for new Drafts.
## [Bridge 3.1.2] Quebec ## [Bridge 3.1.2] Quebec
### Changed ### Changed
* GODT-2582 Dedup recovered messages folder. * GODT-2582 Dedup recovered messages folder.
## [Bridge 3.1.1] Quebec ## [Bridge 3.1.1] Quebec
### Fixed ### Fixed

View File

@ -11,7 +11,7 @@ ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
.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.1.2+git BRIDGE_APP_VERSION?=3.2.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
@ -256,6 +256,8 @@ mocks:
mockgen --package mocks github.com/ProtonMail/gluon/async PanicHandler > internal/bridge/mocks/async_mocks.go mockgen --package mocks github.com/ProtonMail/gluon/async PanicHandler > internal/bridge/mocks/async_mocks.go
mockgen --package mocks github.com/ProtonMail/gluon/reporter Reporter > internal/bridge/mocks/gluon_mocks.go mockgen --package mocks github.com/ProtonMail/gluon/reporter Reporter > internal/bridge/mocks/gluon_mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/updater Downloader,Installer > internal/updater/mocks/mocks.go mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/updater Downloader,Installer > internal/updater/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/telemetry HeartbeatManager > internal/telemetry/mocks/mocks.go
cp internal/telemetry/mocks/mocks.go internal/bridge/mocks/telemetry_mocks.go
lint: gofiles lint-golang lint-license lint-dependencies lint-changelog lint: gofiles lint-golang lint-license lint-dependencies lint-changelog

20
go.mod
View File

@ -5,10 +5,10 @@ go 1.18
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.15.1-0.20230426114936-a356ef7f2753 github.com/ProtonMail/gluon v0.16.1-0.20230428090920-2797a1764f16
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.20230406143739-c7596e170799 github.com/ProtonMail/go-proton-api v0.4.1-0.20230426081144-f77778bae1be
github.com/ProtonMail/gopenpgp/v2 v2.5.2 github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton
github.com/PuerkitoBio/goquery v1.8.1 github.com/PuerkitoBio/goquery v1.8.1
github.com/abiosoft/ishell v2.0.0+incompatible github.com/abiosoft/ishell v2.0.0+incompatible
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37 github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37
@ -43,9 +43,9 @@ require (
github.com/vmihailenco/msgpack/v5 v5.3.5 github.com/vmihailenco/msgpack/v5 v5.3.5
go.uber.org/goleak v1.2.1 go.uber.org/goleak v1.2.1
golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb
golang.org/x/net v0.7.0 golang.org/x/net v0.8.0
golang.org/x/sys v0.5.0 golang.org/x/sys v0.6.0
golang.org/x/text v0.7.0 golang.org/x/text v0.8.0
google.golang.org/grpc v1.53.0 google.golang.org/grpc v1.53.0
google.golang.org/protobuf v1.28.1 google.golang.org/protobuf v1.28.1
howett.net/plist v1.0.0 howett.net/plist v1.0.0
@ -55,8 +55,8 @@ require (
ariga.io/atlas v0.9.1-0.20230119145809-92243f7c55cb // indirect ariga.io/atlas v0.9.1-0.20230119145809-92243f7c55cb // indirect
entgo.io/ent v0.11.8 // indirect entgo.io/ent v0.11.8 // indirect
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf // indirect github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf // indirect
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect github.com/ProtonMail/go-crypto v0.0.0-20230322105811-d73448b7e800 // indirect
github.com/ProtonMail/go-mime v0.0.0-20221031134845-8fd9bc37cf08 // indirect github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect
github.com/ProtonMail/go-srp v0.0.5 // indirect github.com/ProtonMail/go-srp v0.0.5 // indirect
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect
github.com/agext/levenshtein v1.2.3 // indirect github.com/agext/levenshtein v1.2.3 // indirect
@ -116,10 +116,10 @@ require (
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
github.com/zclconf/go-cty v1.12.1 // indirect github.com/zclconf/go-cty v1.12.1 // indirect
golang.org/x/arch v0.2.0 // indirect golang.org/x/arch v0.2.0 // indirect
golang.org/x/crypto v0.6.0 // indirect golang.org/x/crypto v0.7.0 // indirect
golang.org/x/mod v0.8.0 // indirect golang.org/x/mod v0.8.0 // indirect
golang.org/x/sync v0.1.0 // indirect golang.org/x/sync v0.1.0 // indirect
golang.org/x/tools v0.3.1-0.20221202221704-aa9f4b2f3d57 // indirect golang.org/x/tools v0.6.0 // indirect
google.golang.org/genproto v0.0.0-20230221151758-ace64dc21148 // indirect google.golang.org/genproto v0.0.0-20230221151758-ace64dc21148 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

43
go.sum
View File

@ -28,24 +28,23 @@ github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo= github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk= github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g= github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
github.com/ProtonMail/gluon v0.15.1-0.20230426114936-a356ef7f2753 h1:Ae/Ny7Fm+8RS/L+/9CiT6ntoPFSIJLyrLbD5bI77il4= github.com/ProtonMail/gluon v0.16.1-0.20230428090920-2797a1764f16 h1:X5kb4PwVrgVDQjBkpiobYrDlqKDMuS1o92Ty+rZ1ptE=
github.com/ProtonMail/gluon v0.15.1-0.20230426114936-a356ef7f2753/go.mod h1:yA4hk6CJw0BMo+YL8Y3ckCYs5L20sysu9xseshwY3QI= github.com/ProtonMail/gluon v0.16.1-0.20230428090920-2797a1764f16/go.mod h1:yA4hk6CJw0BMo+YL8Y3ckCYs5L20sysu9xseshwY3QI=
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-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo= github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
github.com/ProtonMail/go-crypto v0.0.0-20230124153114-0acdc8ae009b/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g= github.com/ProtonMail/go-crypto v0.0.0-20230322105811-d73448b7e800 h1:o8/VQLSiuRkkSAfVOpFCG1GnTsWxFIOPLvJ2O7hJcFg=
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 h1:wPbRQzjjwFc0ih8puEVAOFGELsn1zoIIYdxvML7mDxA= github.com/ProtonMail/go-crypto v0.0.0-20230322105811-d73448b7e800/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g=
github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753 h1:I8IsYA297x0QLU80G5I6aLYUu3JYNSpo8j5fkXtFDW0= github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753 h1:I8IsYA297x0QLU80G5I6aLYUu3JYNSpo8j5fkXtFDW0=
github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4= github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
github.com/ProtonMail/go-mime v0.0.0-20221031134845-8fd9bc37cf08 h1:dS7r5z4iGS0qCjM7UwWdsEMzQesUQbGcXdSm2/tWboA= github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
github.com/ProtonMail/go-mime v0.0.0-20221031134845-8fd9bc37cf08/go.mod h1:qRZgbeASl2a9OwmsV85aWwRqic0NHPh+9ewGAzb4cgM= github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
github.com/ProtonMail/go-proton-api v0.4.1-0.20230406143739-c7596e170799 h1:slk4Drrkij1EVTnFOlIDyJsfjt69tnw8w2g1NMb253U= github.com/ProtonMail/go-proton-api v0.4.1-0.20230426081144-f77778bae1be h1:TNHnEyUQDf97CRGCFWLxg7I5ASSEMO3TN2lbNw2cD6U=
github.com/ProtonMail/go-proton-api v0.4.1-0.20230406143739-c7596e170799/go.mod h1:kis4GD6FHp1ZWnenSBepldt8ai+vYalDPeey9yGwyXk= github.com/ProtonMail/go-proton-api v0.4.1-0.20230426081144-f77778bae1be/go.mod h1:UkrG9gN2o9mzdx/an0XRc6a4s5Haef1A7Eyd2iXlw28=
github.com/ProtonMail/go-srp v0.0.5 h1:xhUioxZgDbCnpo9JehyFhwwsn9JLWkUGfB0oiKXgiGg= github.com/ProtonMail/go-srp v0.0.5 h1:xhUioxZgDbCnpo9JehyFhwwsn9JLWkUGfB0oiKXgiGg=
github.com/ProtonMail/go-srp v0.0.5/go.mod h1:06iYHtLXW8vjLtccWj++x3MKy65sIT8yZd7nrJF49rs= github.com/ProtonMail/go-srp v0.0.5/go.mod h1:06iYHtLXW8vjLtccWj++x3MKy65sIT8yZd7nrJF49rs=
github.com/ProtonMail/gopenpgp/v2 v2.5.2 h1:97SjlWNAxXl9P22lgwgrZRshQdiEfAht0g3ZoiA1GCw= github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton h1:YS6M20yvjCJPR1r4ADW5TPn6rahs4iAyZaACei86bEc=
github.com/ProtonMail/gopenpgp/v2 v2.5.2/go.mod h1:52qDaCnto6r+CoWbuU50T77XQt99lIs46HtHtvgFO3o= github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton/go.mod h1:S1lYsaGHykYpxxh2SnJL6ypcAlANKj5NRSY6HxKryKQ=
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM= github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ= github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw= github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw=
@ -433,12 +432,11 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb h1:PaBZQdo+iSDyHT053FjUCgZQ/9uqVwPOcl7KSWhKn6w= golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb h1:PaBZQdo+iSDyHT053FjUCgZQ/9uqVwPOcl7KSWhKn6w=
@ -454,7 +452,6 @@ golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHl
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mobile v0.0.0-20221110043201-43a038452099/go.mod h1:aAjjkJNdrh3PMckS4B10TGS2nag27cbKR1y2BpUxsiY=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
@ -481,8 +478,10 @@ golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -493,7 +492,6 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -525,11 +523,13 @@ golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
@ -537,8 +537,9 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -562,8 +563,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.3.1-0.20221202221704-aa9f4b2f3d57 h1:/X0t/E4VxbZE7MLS7auvE7YICHeVvbIa9vkOVvYW/24= golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
golang.org/x/tools v0.3.1-0.20221202221704-aa9f4b2f3d57/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@ -22,6 +22,7 @@ import (
"math/rand" "math/rand"
"net/http" "net/http"
"net/http/cookiejar" "net/http/cookiejar"
"net/url"
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
@ -35,6 +36,7 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/crash" "github.com/ProtonMail/proton-bridge/v3/internal/crash"
"github.com/ProtonMail/proton-bridge/v3/internal/events" "github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/focus" "github.com/ProtonMail/proton-bridge/v3/internal/focus"
"github.com/ProtonMail/proton-bridge/v3/internal/frontend/theme"
"github.com/ProtonMail/proton-bridge/v3/internal/locations" "github.com/ProtonMail/proton-bridge/v3/internal/locations"
"github.com/ProtonMail/proton-bridge/v3/internal/logging" "github.com/ProtonMail/proton-bridge/v3/internal/logging"
"github.com/ProtonMail/proton-bridge/v3/internal/sentry" "github.com/ProtonMail/proton-bridge/v3/internal/sentry"
@ -244,6 +246,15 @@ func run(c *cli.Context) error {
} }
} }
logrus.WithFields(logrus.Fields{
"lastVersion": v.GetLastVersion().String(),
"showAllMail": v.GetShowAllMail(),
"updateCh": v.GetUpdateChannel(),
"autoUpdate": v.GetAutoUpdate(),
"rollout": v.GetUpdateRollout(),
"DoH": v.GetProxyAllowed(),
}).Info("Vault loaded")
// Load the cookies from the vault. // Load the cookies from the vault.
return withCookieJar(v, func(cookieJar http.CookieJar) error { return withCookieJar(v, func(cookieJar http.CookieJar) error {
// Create a new bridge instance. // Create a new bridge instance.
@ -258,6 +269,9 @@ func run(c *cli.Context) error {
b.PushError(bridge.ErrVaultCorrupt) b.PushError(bridge.ErrVaultCorrupt)
} }
// Start telemetry heartbeat process
b.StartHeartbeat(b)
// Run the frontend. // Run the frontend.
return runFrontend(c, crashHandler, restarter, locations, b, eventCh, quitCh, c.Int(flagParentPID)) return runFrontend(c, crashHandler, restarter, locations, b, eventCh, quitCh, c.Int(flagParentPID))
}) })
@ -417,6 +431,10 @@ func withCookieJar(vault *vault.Vault, fn func(http.CookieJar) error) error {
return fmt.Errorf("could not create cookie jar: %w", err) return fmt.Errorf("could not create cookie jar: %w", err)
} }
if err := setDeviceCookies(persister); err != nil {
return fmt.Errorf("could not set device cookies: %w", err)
}
// Persist the cookies to the vault when we close. // Persist the cookies to the vault when we close.
defer func() { defer func() {
logrus.Debug("Persisting cookies") logrus.Debug("Persisting cookies")
@ -428,3 +446,21 @@ func withCookieJar(vault *vault.Vault, fn func(http.CookieJar) error) error {
return fn(persister) return fn(persister)
} }
func setDeviceCookies(jar *cookies.Jar) error {
url, err := url.Parse(constants.APIHost)
if err != nil {
return err
}
for name, value := range map[string]string{
"hhn": sentry.GetProtectedHostname(),
"tz": sentry.GetTimeZone(),
"lng": sentry.GetSystemLang(),
"clr": string(theme.DefaultTheme()),
} {
jar.SetCookies(url, []*http.Cookie{{Name: name, Value: value, Secure: true}})
}
return nil
}

View File

@ -20,6 +20,7 @@
package bridge package bridge
import ( import (
"crypto/tls"
"net/http" "net/http"
"os" "os"
@ -36,6 +37,14 @@ func newAPIOptions(
transport http.RoundTripper, transport http.RoundTripper,
panicHandler async.PanicHandler, panicHandler async.PanicHandler,
) []proton.Option { ) []proton.Option {
if allow := os.Getenv("BRIDGE_ALLOW_PROXY"); allow != "" {
transport = &http.Transport{
Proxy: http.ProxyFromEnvironment,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
}
opt := defaultAPIOptions(apiURL, version, cookieJar, transport, panicHandler) opt := defaultAPIOptions(apiURL, version, cookieJar, transport, panicHandler)
if host := os.Getenv("BRIDGE_API_HOST"); host != "" { if host := os.Getenv("BRIDGE_API_HOST"); host != "" {

View File

@ -41,6 +41,7 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/focus" "github.com/ProtonMail/proton-bridge/v3/internal/focus"
"github.com/ProtonMail/proton-bridge/v3/internal/safe" "github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/sentry" "github.com/ProtonMail/proton-bridge/v3/internal/sentry"
"github.com/ProtonMail/proton-bridge/v3/internal/telemetry"
"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/bradenaw/juniper/xslices" "github.com/bradenaw/juniper/xslices"
@ -78,6 +79,9 @@ type Bridge struct {
updater Updater updater Updater
installCh chan installJob installCh chan installJob
// heartbeat is the telemetry heartbeat for metrics.
heartbeat telemetry.Heartbeat
// curVersion is the current version of the bridge, // curVersion is the current version of the bridge,
// newVersion is the version that was installed by the updater. // newVersion is the version that was installed by the updater.
curVersion *semver.Version curVersion *semver.Version
@ -126,6 +130,9 @@ type Bridge struct {
// goUpdate triggers a check/install of updates. // goUpdate triggers a check/install of updates.
goUpdate func() goUpdate func()
// goHeartbeat triggers a check/sending if heartbeat is needed.
goHeartbeat func()
uidValidityGenerator imap.UIDValidityGenerator uidValidityGenerator imap.UIDValidityGenerator
} }
@ -237,6 +244,8 @@ func newBridge(
return nil, fmt.Errorf("failed to save last version indicator: %w", err) return nil, fmt.Errorf("failed to save last version indicator: %w", err)
} }
identifier.SetClientString(vault.GetLastUserAgent())
imapServer, err := newIMAPServer( imapServer, err := newIMAPServer(
gluonCacheDir, gluonCacheDir,
gluonDataDir, gluonDataDir,

View File

@ -49,6 +49,7 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/vault" "github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/ProtonMail/proton-bridge/v3/tests" "github.com/ProtonMail/proton-bridge/v3/tests"
"github.com/bradenaw/juniper/xslices" "github.com/bradenaw/juniper/xslices"
imapid "github.com/emersion/go-imap-id"
"github.com/emersion/go-imap/client" "github.com/emersion/go-imap/client"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -170,6 +171,92 @@ func TestBridge_UserAgent(t *testing.T) {
}) })
} }
func TestBridge_UserAgent_Persistence(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
currentUserAgent := b.GetCurrentUserAgent()
require.Contains(t, currentUserAgent, vault.DefaultUserAgent)
imapClient, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
require.NoError(t, err)
defer func() { _ = imapClient.Logout() }()
idClient := imapid.NewClient(imapClient)
// Set IMAP ID before Login to have the value capture in the Login API Call.
_, err = idClient.ID(imapid.ID{
imapid.FieldName: "MyFancyClient",
imapid.FieldVersion: "0.1.2",
})
require.NoError(t, err)
// Login the user.
_, err = b.LoginFull(context.Background(), username, password, nil, nil)
require.NoError(t, err)
// Assert that the user agent then contains the platform.
require.Contains(t, b.GetCurrentUserAgent(), "MyFancyClient/0.1.2")
})
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
currentUserAgent := bridge.GetCurrentUserAgent()
require.Contains(t, currentUserAgent, "MyFancyClient/0.1.2")
})
})
}
func TestBridge_UserAgentFromIMAPID(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
var (
calls []server.Call
lock sync.Mutex
)
s.AddCallWatcher(func(call server.Call) {
lock.Lock()
defer lock.Unlock()
calls = append(calls, call)
})
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
imapClient, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
require.NoError(t, err)
defer func() { _ = imapClient.Logout() }()
idClient := imapid.NewClient(imapClient)
// Set IMAP ID before Login to have the value capture in the Login API Call.
_, err = idClient.ID(imapid.ID{
imapid.FieldName: "MyFancyClient",
imapid.FieldVersion: "0.1.2",
})
require.NoError(t, err)
// Login the user.
userID, err := b.LoginFull(context.Background(), username, password, nil, nil)
require.NoError(t, err)
info, err := b.GetUserInfo(userID)
require.NoError(t, err)
require.True(t, info.State == bridge.Connected)
require.NoError(t, imapClient.Login(info.Addresses[0], string(info.BridgePass)))
lock.Lock()
defer lock.Unlock()
userAgent := calls[len(calls)-1].RequestHeader.Get("User-Agent")
// Assert that the user agent was sent to the API.
require.Contains(t, userAgent, b.GetCurrentUserAgent())
require.Contains(t, userAgent, "MyFancyClient/0.1.2")
})
})
}
func TestBridge_Cookies(t *testing.T) { func TestBridge_Cookies(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) { withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
var ( var (
@ -736,6 +823,9 @@ func withBridgeNoMocks(
require.NoError(t, err) require.NoError(t, err)
require.Empty(t, bridge.GetErrors()) require.Empty(t, bridge.GetErrors())
// Start the Heartbeat process.
bridge.StartHeartbeat(mocks.Heartbeat)
// Wait for bridge to finish loading users. // Wait for bridge to finish loading users.
waitForEvent(t, eventCh, events.AllUsersLoaded{}) waitForEvent(t, eventCh, events.AllUsersLoaded{})
// Wait for bridge to start the IMAP server. // Wait for bridge to start the IMAP server.

View File

@ -0,0 +1,128 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge
import (
"context"
"encoding/json"
"time"
"github.com/ProtonMail/gluon/reporter"
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/telemetry"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
"github.com/sirupsen/logrus"
)
const HeartbeatCheckInterval = time.Hour
func (bridge *Bridge) IsTelemetryAvailable() bool {
var flag = true
if bridge.GetTelemetryDisabled() {
return false
}
safe.RLock(func() {
for _, user := range bridge.users {
flag = flag && user.IsTelemetryEnabled(context.Background())
}
}, bridge.usersLock)
return flag
}
func (bridge *Bridge) SendHeartbeat(heartbeat *telemetry.HeartbeatData) bool {
data, err := json.Marshal(heartbeat)
if err != nil {
if err := bridge.reporter.ReportMessageWithContext("Cannot parse heartbeat data.", reporter.Context{
"error": err,
}); err != nil {
logrus.WithError(err).Error("Failed to parse heartbeat data.")
}
return false
}
var sent = false
safe.RLock(func() {
for _, user := range bridge.users {
if err := user.SendTelemetry(context.Background(), data); err == nil {
sent = true
break
}
}
}, bridge.usersLock)
return sent
}
func (bridge *Bridge) GetLastHeartbeatSent() time.Time {
return bridge.vault.GetLastHeartbeatSent()
}
func (bridge *Bridge) SetLastHeartbeatSent(timestamp time.Time) error {
return bridge.vault.SetLastHeartbeatSent(timestamp)
}
func (bridge *Bridge) StartHeartbeat(manager telemetry.HeartbeatManager) {
bridge.heartbeat = telemetry.NewHeartbeat(manager, 1143, 1025, bridge.GetGluonCacheDir(), keychain.DefaultHelper)
// Check for heartbeat when triggered.
bridge.goHeartbeat = bridge.tasks.PeriodicOrTrigger(HeartbeatCheckInterval, 0, func(ctx context.Context) {
logrus.Debug("Checking for heartbeat")
bridge.heartbeat.TrySending()
})
bridge.heartbeat.SetRollout(bridge.GetUpdateRollout())
bridge.heartbeat.SetAutoStart(bridge.GetAutostart())
bridge.heartbeat.SetAutoUpdate(bridge.GetAutoUpdate())
bridge.heartbeat.SetBeta(bridge.GetUpdateChannel())
bridge.heartbeat.SetDoh(bridge.GetProxyAllowed())
bridge.heartbeat.SetShowAllMail(bridge.GetShowAllMail())
bridge.heartbeat.SetIMAPConnectionMode(bridge.GetIMAPSSL())
bridge.heartbeat.SetSMTPConnectionMode(bridge.GetSMTPSSL())
bridge.heartbeat.SetIMAPPort(bridge.GetIMAPPort())
bridge.heartbeat.SetSMTPPort(bridge.GetSMTPPort())
bridge.heartbeat.SetCacheLocation(bridge.GetGluonCacheDir())
if val, err := bridge.GetKeychainApp(); err != nil {
bridge.heartbeat.SetKeyChainPref(val)
} else {
bridge.heartbeat.SetKeyChainPref(keychain.DefaultHelper)
}
bridge.heartbeat.SetPrevVersion(bridge.GetLastVersion().String())
safe.RLock(func() {
var splitMode = false
for _, user := range bridge.users {
if user.GetAddressMode() == vault.SplitMode {
splitMode = true
break
}
}
var nbAccount = len(bridge.users)
bridge.heartbeat.SetNbAccount(nbAccount)
bridge.heartbeat.SetSplitMode(splitMode)
// Do not try to send if there is no user yet.
if nbAccount > 0 {
defer bridge.goHeartbeat()
}
}, bridge.usersLock)
}

View File

@ -17,6 +17,8 @@
package bridge package bridge
import "github.com/sirupsen/logrus"
func (bridge *Bridge) GetCurrentUserAgent() string { func (bridge *Bridge) GetCurrentUserAgent() string {
return bridge.identifier.GetUserAgent() return bridge.identifier.GetUserAgent()
} }
@ -24,3 +26,17 @@ func (bridge *Bridge) GetCurrentUserAgent() string {
func (bridge *Bridge) SetCurrentPlatform(platform string) { func (bridge *Bridge) SetCurrentPlatform(platform string) {
bridge.identifier.SetPlatform(platform) bridge.identifier.SetPlatform(platform)
} }
func (bridge *Bridge) setUserAgent(name, version string) {
currentUserAgent := bridge.identifier.GetClientString()
bridge.identifier.SetClient(name, version)
newUserAgent := bridge.identifier.GetClientString()
if currentUserAgent != newUserAgent {
if err := bridge.vault.SetLastUserAgent(newUserAgent); err != nil {
logrus.WithError(err).Error("Failed to write new user agent to vault")
}
}
}

View File

@ -41,18 +41,16 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
const (
defaultClientName = "UnknownClient"
defaultClientVersion = "0.0.1"
)
func (bridge *Bridge) serveIMAP() error { func (bridge *Bridge) serveIMAP() error {
port, err := func() (int, error) { port, err := func() (int, error) {
if bridge.imapServer == nil { if bridge.imapServer == nil {
return 0, fmt.Errorf("no IMAP server instance running") return 0, fmt.Errorf("no IMAP server instance running")
} }
logrus.Info("Starting IMAP server") logrus.WithFields(logrus.Fields{
"port": bridge.vault.GetIMAPPort(),
"ssl": bridge.vault.GetIMAPSSL(),
}).Info("Starting IMAP server")
imapListener, err := newListener(bridge.vault.GetIMAPPort(), bridge.vault.GetIMAPSSL(), bridge.tlsConfig) imapListener, err := newListener(bridge.vault.GetIMAPPort(), bridge.vault.GetIMAPSSL(), bridge.tlsConfig)
if err != nil { if err != nil {
@ -249,11 +247,6 @@ func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) {
}).Info("Received mailbox message count") }).Info("Received mailbox message count")
} }
case imapEvents.SessionAdded:
if !bridge.identifier.HasClient() {
bridge.identifier.SetClient(defaultClientName, defaultClientVersion)
}
case imapEvents.IMAPID: case imapEvents.IMAPID:
logrus.WithFields(logrus.Fields{ logrus.WithFields(logrus.Fields{
"sessionID": event.SessionID, "sessionID": event.SessionID,
@ -262,7 +255,7 @@ func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) {
}).Info("Received IMAP ID") }).Info("Received IMAP ID")
if event.IMAPID.Name != "" && event.IMAPID.Version != "" { if event.IMAPID.Name != "" && event.IMAPID.Version != "" {
bridge.identifier.SetClient(event.IMAPID.Name, event.IMAPID.Version) bridge.setUserAgent(event.IMAPID.Name, event.IMAPID.Version)
} }
case imapEvents.LoginFailed: case imapEvents.LoginFailed:

View File

@ -24,6 +24,7 @@ type Mocks struct {
CrashHandler *mocks.MockPanicHandler CrashHandler *mocks.MockPanicHandler
Reporter *mocks.MockReporter Reporter *mocks.MockReporter
Heartbeat *mocks.MockHeartbeatManager
} }
func NewMocks(tb testing.TB, version, minAuto *semver.Version) *Mocks { func NewMocks(tb testing.TB, version, minAuto *semver.Version) *Mocks {
@ -39,14 +40,18 @@ func NewMocks(tb testing.TB, version, minAuto *semver.Version) *Mocks {
CrashHandler: mocks.NewMockPanicHandler(ctl), CrashHandler: mocks.NewMockPanicHandler(ctl),
Reporter: mocks.NewMockReporter(ctl), Reporter: mocks.NewMockReporter(ctl),
Heartbeat: mocks.NewMockHeartbeatManager(ctl),
} }
// When getting the TLS issue channel, we want to return the test channel. // When getting the TLS issue channel, we want to return the test channel.
mocks.TLSReporter.EXPECT().GetTLSIssueCh().Return(mocks.TLSIssueCh).AnyTimes() mocks.TLSReporter.EXPECT().GetTLSIssueCh().Return(mocks.TLSIssueCh).AnyTimes()
// This is called at he end of any go-routine: // This is called at the end of any go-routine:
mocks.CrashHandler.EXPECT().HandlePanic(gomock.Any()).AnyTimes() mocks.CrashHandler.EXPECT().HandlePanic(gomock.Any()).AnyTimes()
// this is called at start of heartbeat process.
mocks.Heartbeat.EXPECT().IsTelemetryAvailable().AnyTimes()
return mocks return mocks
} }

View File

@ -0,0 +1,92 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/ProtonMail/proton-bridge/v3/internal/telemetry (interfaces: HeartbeatManager)
// Package mocks is a generated GoMock package.
package mocks
import (
reflect "reflect"
time "time"
telemetry "github.com/ProtonMail/proton-bridge/v3/internal/telemetry"
gomock "github.com/golang/mock/gomock"
)
// MockHeartbeatManager is a mock of HeartbeatManager interface.
type MockHeartbeatManager struct {
ctrl *gomock.Controller
recorder *MockHeartbeatManagerMockRecorder
}
// MockHeartbeatManagerMockRecorder is the mock recorder for MockHeartbeatManager.
type MockHeartbeatManagerMockRecorder struct {
mock *MockHeartbeatManager
}
// NewMockHeartbeatManager creates a new mock instance.
func NewMockHeartbeatManager(ctrl *gomock.Controller) *MockHeartbeatManager {
mock := &MockHeartbeatManager{ctrl: ctrl}
mock.recorder = &MockHeartbeatManagerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockHeartbeatManager) EXPECT() *MockHeartbeatManagerMockRecorder {
return m.recorder
}
// GetLastHeartbeatSent mocks base method.
func (m *MockHeartbeatManager) GetLastHeartbeatSent() time.Time {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetLastHeartbeatSent")
ret0, _ := ret[0].(time.Time)
return ret0
}
// GetLastHeartbeatSent indicates an expected call of GetLastHeartbeatSent.
func (mr *MockHeartbeatManagerMockRecorder) GetLastHeartbeatSent() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLastHeartbeatSent", reflect.TypeOf((*MockHeartbeatManager)(nil).GetLastHeartbeatSent))
}
// IsTelemetryAvailable mocks base method.
func (m *MockHeartbeatManager) IsTelemetryAvailable() bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IsTelemetryAvailable")
ret0, _ := ret[0].(bool)
return ret0
}
// IsTelemetryAvailable indicates an expected call of IsTelemetryAvailable.
func (mr *MockHeartbeatManagerMockRecorder) IsTelemetryAvailable() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsTelemetryAvailable", reflect.TypeOf((*MockHeartbeatManager)(nil).IsTelemetryAvailable))
}
// SendHeartbeat mocks base method.
func (m *MockHeartbeatManager) SendHeartbeat(arg0 *telemetry.HeartbeatData) bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SendHeartbeat", arg0)
ret0, _ := ret[0].(bool)
return ret0
}
// SendHeartbeat indicates an expected call of SendHeartbeat.
func (mr *MockHeartbeatManagerMockRecorder) SendHeartbeat(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendHeartbeat", reflect.TypeOf((*MockHeartbeatManager)(nil).SendHeartbeat), arg0)
}
// SetLastHeartbeatSent mocks base method.
func (m *MockHeartbeatManager) SetLastHeartbeatSent(arg0 time.Time) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetLastHeartbeatSent", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// SetLastHeartbeatSent indicates an expected call of SetLastHeartbeatSent.
func (mr *MockHeartbeatManagerMockRecorder) SetLastHeartbeatSent(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLastHeartbeatSent", reflect.TypeOf((*MockHeartbeatManager)(nil).SetLastHeartbeatSent), arg0)
}

View File

@ -46,6 +46,8 @@ func (bridge *Bridge) SetKeychainApp(helper string) error {
return err return err
} }
bridge.heartbeat.SetKeyChainPref(helper)
return vault.SetHelper(vaultDir, helper) return vault.SetHelper(vaultDir, helper)
} }
@ -62,6 +64,8 @@ func (bridge *Bridge) SetIMAPPort(newPort int) error {
return err return err
} }
bridge.heartbeat.SetIMAPPort(newPort)
return bridge.restartIMAP() return bridge.restartIMAP()
} }
@ -78,6 +82,8 @@ func (bridge *Bridge) SetIMAPSSL(newSSL bool) error {
return err return err
} }
bridge.heartbeat.SetIMAPConnectionMode(newSSL)
return bridge.restartIMAP() return bridge.restartIMAP()
} }
@ -94,6 +100,8 @@ func (bridge *Bridge) SetSMTPPort(newPort int) error {
return err return err
} }
bridge.heartbeat.SetSMTPPort(newPort)
return bridge.restartSMTP() return bridge.restartSMTP()
} }
@ -110,6 +118,8 @@ func (bridge *Bridge) SetSMTPSSL(newSSL bool) error {
return err return err
} }
bridge.heartbeat.SetSMTPConnectionMode(newSSL)
return bridge.restartSMTP() return bridge.restartSMTP()
} }
@ -141,6 +151,8 @@ func (bridge *Bridge) SetGluonDir(ctx context.Context, newGluonDir string) error
} }
} }
bridge.heartbeat.SetCacheLocation(newGluonDir)
gluonDataDir, err := bridge.GetGluonDataDir() gluonDataDir, err := bridge.GetGluonDataDir()
if err != nil { if err != nil {
return fmt.Errorf("failed to get Gluon Database directory: %w", err) return fmt.Errorf("failed to get Gluon Database directory: %w", err)
@ -207,6 +219,8 @@ func (bridge *Bridge) SetProxyAllowed(allowed bool) error {
bridge.proxyCtl.DisallowProxy() bridge.proxyCtl.DisallowProxy()
} }
bridge.heartbeat.SetDoh(allowed)
return bridge.vault.SetProxyAllowed(allowed) return bridge.vault.SetProxyAllowed(allowed)
} }
@ -220,6 +234,8 @@ func (bridge *Bridge) SetShowAllMail(show bool) error {
user.SetShowAllMail(show) user.SetShowAllMail(show)
} }
bridge.heartbeat.SetShowAllMail(show)
return bridge.vault.SetShowAllMail(show) return bridge.vault.SetShowAllMail(show)
}, bridge.usersLock) }, bridge.usersLock)
} }
@ -233,6 +249,8 @@ func (bridge *Bridge) SetAutostart(autostart bool) error {
if err := bridge.vault.SetAutostart(autostart); err != nil { if err := bridge.vault.SetAutostart(autostart); err != nil {
return err return err
} }
bridge.heartbeat.SetAutoStart(autostart)
} }
var err error var err error
@ -253,6 +271,10 @@ func (bridge *Bridge) SetAutostart(autostart bool) error {
return err return err
} }
func (bridge *Bridge) GetUpdateRollout() float64 {
return bridge.vault.GetUpdateRollout()
}
func (bridge *Bridge) GetAutoUpdate() bool { func (bridge *Bridge) GetAutoUpdate() bool {
return bridge.vault.GetAutoUpdate() return bridge.vault.GetAutoUpdate()
} }
@ -266,11 +288,28 @@ func (bridge *Bridge) SetAutoUpdate(autoUpdate bool) error {
return err return err
} }
bridge.heartbeat.SetAutoUpdate(autoUpdate)
bridge.goUpdate() bridge.goUpdate()
return nil return nil
} }
func (bridge *Bridge) GetTelemetryDisabled() bool {
return bridge.vault.GetTelemetryDisabled()
}
func (bridge *Bridge) SetTelemetryDisabled(isDisabled bool) error {
if err := bridge.vault.SetTelemetryDisabled(isDisabled); err != nil {
return err
}
// If telemetry is re-enabled locally, try to send the heartbeat.
if !isDisabled {
defer bridge.goHeartbeat()
}
return nil
}
func (bridge *Bridge) GetUpdateChannel() updater.Channel { func (bridge *Bridge) GetUpdateChannel() updater.Channel {
return bridge.vault.GetUpdateChannel() return bridge.vault.GetUpdateChannel()
} }
@ -284,6 +323,8 @@ func (bridge *Bridge) SetUpdateChannel(channel updater.Channel) error {
return err return err
} }
bridge.heartbeat.SetBeta(channel)
bridge.goUpdate() bridge.goUpdate()
return nil return nil

View File

@ -33,7 +33,10 @@ import (
func (bridge *Bridge) serveSMTP() error { func (bridge *Bridge) serveSMTP() error {
port, err := func() (int, error) { port, err := func() (int, error) {
logrus.Info("Starting SMTP server") logrus.WithFields(logrus.Fields{
"port": bridge.vault.GetSMTPPort(),
"ssl": bridge.vault.GetSMTPSSL(),
}).Info("Starting SMTP server")
smtpListener, err := newListener(bridge.vault.GetSMTPPort(), bridge.vault.GetSMTPSSL(), bridge.tlsConfig) smtpListener, err := newListener(bridge.vault.GetSMTPPort(), bridge.vault.GetSMTPSSL(), bridge.tlsConfig)
if err != nil { if err != nil {

View File

@ -23,6 +23,7 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/safe" "github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/emersion/go-smtp" "github.com/emersion/go-smtp"
"github.com/sirupsen/logrus"
) )
type smtpBackend struct { type smtpBackend struct {
@ -85,7 +86,7 @@ func (s *smtpSession) Rcpt(to string) error {
} }
func (s *smtpSession) Data(r io.Reader) error { func (s *smtpSession) Data(r io.Reader) error {
return safe.RLockRet(func() error { err := safe.RLockRet(func() error {
user, ok := s.users[s.userID] user, ok := s.users[s.userID]
if !ok { if !ok {
return ErrNoSuchUser return ErrNoSuchUser
@ -93,4 +94,10 @@ func (s *smtpSession) Data(r io.Reader) error {
return user.SendMail(s.authID, s.from, s.to, r) return user.SendMail(s.authID, s.from, s.to, r)
}, s.usersLock) }, s.usersLock)
if err != nil {
logrus.WithField("pkg", "smtp").WithError(err).Error("Send mail failed.")
}
return err
} }

View File

@ -38,6 +38,8 @@ type Identifier interface {
HasClient() bool HasClient() bool
SetClient(name, version string) SetClient(name, version string)
SetPlatform(platform string) SetPlatform(platform string)
SetClientString(client string)
GetClientString() string
} }
type ProxyController interface { type ProxyController interface {

View File

@ -295,6 +295,15 @@ func (bridge *Bridge) SetAddressMode(ctx context.Context, userID string, mode va
AddressMode: mode, AddressMode: mode,
}) })
var splitMode = false
for _, user := range bridge.users {
if user.GetAddressMode() == vault.SplitMode {
splitMode = true
break
}
}
bridge.heartbeat.SetSplitMode(splitMode)
return nil return nil
}, bridge.usersLock) }, bridge.usersLock)
} }
@ -399,7 +408,7 @@ func (bridge *Bridge) loadUsers(ctx context.Context) error {
return nil return nil
} }
log.Info("Loading connected user") log.WithField("mode", user.AddressMode()).Info("Loading connected user")
bridge.publish(events.UserLoading{ bridge.publish(events.UserLoading{
UserID: user.UserID(), UserID: user.UserID(),
@ -550,7 +559,7 @@ func (bridge *Bridge) addUserWithVault(
// As such, if we find this ID in the context, we should use it to update our user agent. // As such, if we find this ID in the context, we should use it to update our user agent.
client.AddPreRequestHook(func(_ *resty.Client, r *resty.Request) error { client.AddPreRequestHook(func(_ *resty.Client, r *resty.Request) error {
if imapID, ok := imap.GetIMAPIDFromContext(r.Context()); ok { if imapID, ok := imap.GetIMAPIDFromContext(r.Context()); ok {
bridge.identifier.SetClient(imapID.Name, imapID.Version) bridge.setUserAgent(imapID.Name, imapID.Version)
} }
return nil return nil
@ -559,8 +568,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.usersLock) }, bridge.usersLock)
// As we need at least one user to send heartbeat, try to send it.
defer bridge.goHeartbeat()
return nil return nil
} }
@ -614,6 +627,8 @@ func (bridge *Bridge) logoutUser(ctx context.Context, user *user.User, withAPI,
logrus.WithError(err).Error("Failed to logout user") logrus.WithError(err).Error("Failed to logout user")
} }
bridge.heartbeat.SetNbAccount(len(bridge.users))
user.Close() user.Close()
} }

View File

@ -451,6 +451,65 @@ func TestBridge_User_DropConn_NoBadEvent(t *testing.T) {
}, server.WithListener(dropListener)) }, server.WithListener(dropListener))
} }
func TestBridge_User_UpdateDraft(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
// Create a bridge user.
_, _, err := s.CreateUser("user", password)
require.NoError(t, err)
// Initially sync the user.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
userLoginAndSync(ctx, t, bridge, "user", password)
})
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
user, err := c.GetUser(ctx)
require.NoError(t, err)
addrs, err := c.GetAddresses(ctx)
require.NoError(t, err)
salts, err := c.GetSalts(ctx)
require.NoError(t, err)
keyPass, err := salts.SaltForKey(password, user.Keys.Primary().ID)
require.NoError(t, err)
_, addrKRs, err := proton.Unlock(user, addrs, keyPass, async.NoopPanicHandler{})
require.NoError(t, err)
// Create a draft (generating a "create draft message" event).
draft, err := c.CreateDraft(ctx, addrKRs[addrs[0].ID], proton.CreateDraftReq{
Message: proton.DraftTemplate{
Subject: "subject",
Sender: &mail.Address{Name: "sender", Address: addrs[0].Email},
Body: "body",
MIMEType: rfc822.TextPlain,
},
})
require.NoError(t, err)
require.Empty(t, draft.ReplyTos)
// Process those events
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
userContinueEventProcess(ctx, t, s, bridge)
})
// Update the draft (generating an "update draft message" event).
draft2, err := c.UpdateDraft(ctx, draft.ID, addrKRs[addrs[0].ID], proton.UpdateDraftReq{
Message: proton.DraftTemplate{
Subject: "subject 2",
Sender: &mail.Address{Name: "sender", Address: addrs[0].Email},
Body: "body 2",
MIMEType: rfc822.TextPlain,
},
})
require.NoError(t, err)
require.Empty(t, draft2.ReplyTos)
})
})
}
func TestBridge_User_UpdateDraftAndCreateOtherMessage(t *testing.T) { func TestBridge_User_UpdateDraftAndCreateOtherMessage(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) { withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
// Create a bridge user. // Create a bridge user.
@ -784,6 +843,48 @@ func TestBridge_User_HandleParentLabelRename(t *testing.T) {
}) })
} }
// TBD: GODT-2527.
func _TestBridge503DuringEventDoesNotCauseBadEvent(t *testing.T) { //nolint:unused,deadcode
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
// Create a user.
userID, addrID, err := s.CreateUser("user", password)
require.NoError(t, err)
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
require.NoError(t, err)
// Create 10 messages for the user.
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
createNumMessages(ctx, t, c, addrID, labelID, 10)
})
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
userLoginAndSync(ctx, t, bridge, "user", password)
var messageIDs []string
// Create 10 more messages for the user, generating events.
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
messageIDs = createNumMessages(ctx, t, c, addrID, labelID, 10)
})
mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).MinTimes(1)
s.AddStatusHook(func(req *http.Request) (int, bool) {
if xslices.Index(xslices.Map(messageIDs[0:5], func(messageID string) string {
return "/mail/v4/messages/" + messageID
}), req.URL.Path) < 0 {
return 0, false
}
return http.StatusServiceUnavailable, true
})
userContinueEventProcess(ctx, t, s, bridge)
})
})
}
// userLoginAndSync logs in user and waits until user is fully synced. // userLoginAndSync logs in user and waits until user is fully synced.
func userLoginAndSync( func userLoginAndSync(
ctx context.Context, ctx context.Context,

View File

@ -17,69 +17,141 @@
package certs package certs
import ( /*
"os" #cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Foundation -framework Security
#import <Foundation/Foundation.h>
#import <Security/Security.h>
"golang.org/x/sys/execabs"
int installTrustedCert(char const *bytes, unsigned long long length) {
if (length == 0) {
return errSecInvalidData;
}
NSData *der = [NSData dataWithBytes:bytes length:length];
// Step 1. Import the certificate in the keychain.
SecCertificateRef cert = SecCertificateCreateWithData(NULL, (CFDataRef) der);
NSDictionary* addQuery = @{
(id)kSecValueRef: (__bridge id) cert,
(id)kSecClass: (id)kSecClassCertificate,
};
OSStatus status = SecItemAdd((__bridge CFDictionaryRef) addQuery, NULL);
if ((errSecSuccess != status) && (errSecDuplicateItem != status)) {
CFRelease(cert);
return status;
}
// Step 2. Set the trust for the certificate.
SecPolicyRef policy = SecPolicyCreateSSL(true, NULL); // we limit our trust to SSL
NSDictionary *trustSettings = @{
(id)kSecTrustSettingsResult: [NSNumber numberWithInt:kSecTrustSettingsResultTrustRoot],
(id)kSecTrustSettingsPolicy: (__bridge id) policy,
};
status = SecTrustSettingsSetTrustSettings(cert, kSecTrustSettingsDomainAdmin, (__bridge CFTypeRef)(trustSettings));
CFRelease(policy);
CFRelease(cert);
return status;
}
int removeTrustedCert(char const *bytes, unsigned long long length) {
if (0 == length) {
return errSecInvalidData;
}
NSData *der = [NSData dataWithBytes: bytes length: length];
SecCertificateRef cert = SecCertificateCreateWithData(NULL, (CFDataRef) der);
// Step 1. Unset the trust for the certificate.
SecPolicyRef policy = SecPolicyCreateSSL(true, NULL);
NSDictionary * trustSettings = @{
(id)kSecTrustSettingsResult: [NSNumber numberWithInt:kSecTrustSettingsResultUnspecified],
(id)kSecTrustSettingsPolicy: (__bridge id) policy,
};
OSStatus status = SecTrustSettingsSetTrustSettings(cert, kSecTrustSettingsDomainAdmin, (__bridge CFTypeRef)(trustSettings));
CFRelease(policy);
if (errSecSuccess != status) {
CFRelease(cert);
return status;
}
// Step 2. Remove the certificate from the keychain.
NSDictionary *query = @{ (id)kSecClass: (id)kSecClassCertificate,
(id)kSecMatchItemList: @[(__bridge id)cert],
(id)kSecMatchLimit: (id)kSecMatchLimitOne,
};
status = SecItemDelete((__bridge CFDictionaryRef) query);
CFRelease(cert);
return status;
}
*/
import "C"
import (
"encoding/pem"
"errors"
"fmt"
"unsafe"
) )
// some of the error codes returned by Apple's Security framework.
const (
errSecSuccess = 0
errAuthorizationCanceled = -60006
)
// certPEMToDER converts a certificate in PEM format to DER format, which is the format required by Apple's Security framework.
func certPEMToDER(certPEM []byte) ([]byte, error) {
block, left := pem.Decode(certPEM)
if block == nil {
return []byte{}, errors.New("invalid PEM certificate")
}
if len(left) > 0 {
return []byte{}, errors.New("trailing data found at the end of a PEM certificate")
}
return block.Bytes, nil
}
func installCert(certPEM []byte) error { func installCert(certPEM []byte) error {
name, err := writeToTempFile(certPEM) certDER, err := certPEMToDER(certPEM)
if err != nil { if err != nil {
return err return err
} }
return addTrustedCert(name) p := C.CBytes(certDER)
defer C.free(unsafe.Pointer(p))
errCode := C.installTrustedCert((*C.char)(p), (C.ulonglong)(len(certDER)))
switch errCode {
case errSecSuccess:
return nil
case errAuthorizationCanceled:
return fmt.Errorf("the user cancelled the authorization dialog")
default:
return fmt.Errorf("could not install certification into keychain (error %v)", errCode)
}
} }
func uninstallCert(certPEM []byte) error { func uninstallCert(certPEM []byte) error {
name, err := writeToTempFile(certPEM) certDER, err := certPEMToDER(certPEM)
if err != nil { if err != nil {
return err return err
} }
return removeTrustedCert(name) p := C.CBytes(certDER)
} defer C.free(unsafe.Pointer(p))
func addTrustedCert(certPath string) error { if errCode := C.removeTrustedCert((*C.char)(p), (C.ulonglong)(len(certDER))); errCode != 0 {
return execabs.Command( //nolint:gosec return fmt.Errorf("could not install certificate from keychain (error %v)", errCode)
"/usr/bin/security",
"execute-with-privileges",
"/usr/bin/security",
"add-trusted-cert",
"-d",
"-r", "trustRoot",
"-p", "ssl",
"-k", "/Library/Keychains/System.keychain",
certPath,
).Run()
}
func removeTrustedCert(certPath string) error {
return execabs.Command( //nolint:gosec
"/usr/bin/security",
"execute-with-privileges",
"/usr/bin/security",
"remove-trusted-cert",
"-d",
certPath,
).Run()
}
// writeToTempFile writes the given data to a temporary file and returns the path.
func writeToTempFile(data []byte) (string, error) {
f, err := os.CreateTemp("", "tls")
if err != nil {
return "", err
} }
if _, err := f.Write(data); err != nil { return nil
return "", err
}
if err := f.Close(); err != nil {
return "", err
}
return f.Name(), nil
} }

View File

@ -0,0 +1,44 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
//go:build darwin
package certs
import (
"testing"
"github.com/stretchr/testify/require"
)
// This test implies human interactions to enter password and is disabled by default.
func _TestTrustedCertsDarwin(t *testing.T) {
template, err := NewTLSTemplate()
require.NoError(t, err)
certPEM, _, err := GenerateCert(template)
require.NoError(t, err)
require.Error(t, installCert([]byte{0})) // Cannot install an invalid cert.
require.Error(t, uninstallCert(certPEM)) // Cannot uninstall a cert that is not installed.
require.NoError(t, installCert(certPEM)) // Can install a valid cert.
require.NoError(t, installCert(certPEM)) // Can install an already installed cert.
require.NoError(t, uninstallCert(certPEM)) // Can uninstall an installed cert.
require.Error(t, uninstallCert(certPEM)) // Cannot uninstall an already uninstalled cert.
require.NoError(t, installCert(certPEM)) // Can reinstall an uninstalled cert.
require.NoError(t, uninstallCert(certPEM)) // Can uninstall a reinstalled cert.
}

View File

@ -39,6 +39,7 @@ void GRPCQtProxy::connectSignals() {
connect(this, &GRPCQtProxy::setIsAutostartOnReceived, &settingsTab, &SettingsTab::setIsAutostartOn); connect(this, &GRPCQtProxy::setIsAutostartOnReceived, &settingsTab, &SettingsTab::setIsAutostartOn);
connect(this, &GRPCQtProxy::setIsBetaEnabledReceived, &settingsTab, &SettingsTab::setIsBetaEnabled); connect(this, &GRPCQtProxy::setIsBetaEnabledReceived, &settingsTab, &SettingsTab::setIsBetaEnabled);
connect(this, &GRPCQtProxy::setIsAllMailVisibleReceived, &settingsTab, &SettingsTab::setIsAllMailVisible); connect(this, &GRPCQtProxy::setIsAllMailVisibleReceived, &settingsTab, &SettingsTab::setIsAllMailVisible);
connect(this, &GRPCQtProxy::setIsTelemetryDisabledReceived, &settingsTab, &SettingsTab::setIsTelemetryDisabled);
connect(this, &GRPCQtProxy::setColorSchemeNameReceived, &settingsTab, &SettingsTab::setColorSchemeName); connect(this, &GRPCQtProxy::setColorSchemeNameReceived, &settingsTab, &SettingsTab::setColorSchemeName);
connect(this, &GRPCQtProxy::reportBugReceived, &settingsTab, &SettingsTab::setBugReport); connect(this, &GRPCQtProxy::reportBugReceived, &settingsTab, &SettingsTab::setBugReport);
connect(this, &GRPCQtProxy::exportTLSCertificatesReceived, &settingsTab, &SettingsTab::exportTLSCertificates); connect(this, &GRPCQtProxy::exportTLSCertificatesReceived, &settingsTab, &SettingsTab::exportTLSCertificates);
@ -89,6 +90,13 @@ void GRPCQtProxy::setIsAllMailVisible(bool visible) {
} }
//****************************************************************************************************************************************************
/// \param[in] isDisabled Is telemetry disabled?
//****************************************************************************************************************************************************
void GRPCQtProxy::setIsTelemetryDisabled(bool isDisabled) {
emit setIsTelemetryDisabledReceived(isDisabled);
}
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \param[in] name The color scheme. /// \param[in] name The color scheme.
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************

View File

@ -41,6 +41,7 @@ public: // member functions.
void setIsAutostartOn(bool on); ///< Forwards a SetIsAutostartOn call via a Qt signal. void setIsAutostartOn(bool on); ///< Forwards a SetIsAutostartOn call via a Qt signal.
void setIsBetaEnabled(bool enabled); ///< Forwards a SetIsBetaEnabled call via a Qt signal. void setIsBetaEnabled(bool enabled); ///< Forwards a SetIsBetaEnabled call via a Qt signal.
void setIsAllMailVisible(bool visible); ///< Forwards a SetIsAllMailVisible call via a Qt signal. void setIsAllMailVisible(bool visible); ///< Forwards a SetIsAllMailVisible call via a Qt signal.
void setIsTelemetryDisabled(bool isDisabled); ///< Forwards a SetIsTelemetryDisabled call via a Qt signal.
void setColorSchemeName(QString const &name); ///< Forward a SetColorSchemeName call via a Qt Signal void setColorSchemeName(QString const &name); ///< Forward a SetColorSchemeName call via a Qt Signal
void reportBug(QString const &osType, QString const &osVersion, QString const &emailClient, QString const &address, void reportBug(QString const &osType, QString const &osVersion, QString const &emailClient, QString const &address,
QString const &description, bool includeLogs); ///< Forwards a ReportBug call via a Qt signal. QString const &description, bool includeLogs); ///< Forwards a ReportBug call via a Qt signal.
@ -62,6 +63,7 @@ signals:
void setIsAutostartOnReceived(bool on); ///< Forwards a SetIsAutostartOn call via a Qt signal. void setIsAutostartOnReceived(bool on); ///< Forwards a SetIsAutostartOn call via a Qt signal.
void setIsBetaEnabledReceived(bool enabled); ///< Forwards a SetIsBetaEnabled call via a Qt signal. void setIsBetaEnabledReceived(bool enabled); ///< Forwards a SetIsBetaEnabled call via a Qt signal.
void setIsAllMailVisibleReceived(bool enabled); ///< Forwards a SetIsBetaEnabled call via a Qt signal. void setIsAllMailVisibleReceived(bool enabled); ///< Forwards a SetIsBetaEnabled call via a Qt signal.
void setIsTelemetryDisabledReceived(bool isDisabled); ///< Forwards a SetIsTelemetryDisabled call via a Qt signal.
void setColorSchemeNameReceived(QString const &name); ///< Forward a SetColorScheme call via a Qt Signal void setColorSchemeNameReceived(QString const &name); ///< Forward a SetColorScheme call via a Qt Signal
void reportBugReceived(QString const &osType, QString const &osVersion, QString const &emailClient, QString const &address, void reportBugReceived(QString const &osType, QString const &osVersion, QString const &emailClient, QString const &address,
QString const &description, bool includeLogs); ///< Signal for the ReportBug gRPC call QString const &description, bool includeLogs); ///< Signal for the ReportBug gRPC call

View File

@ -192,6 +192,28 @@ Status GRPCService::IsAllMailVisible(ServerContext *, Empty const *request, Bool
} }
//****************************************************************************************************************************************************
/// \param[in] request The request.
/// \return The status for the call.
//****************************************************************************************************************************************************
grpc::Status GRPCService::SetIsTelemetryDisabled(::grpc::ServerContext *, ::google::protobuf::BoolValue const *request, ::google::protobuf::Empty *) {
app().log().debug(__FUNCTION__);
qtProxy_.setIsTelemetryDisabledReceived(request->value());
return Status::OK;
}
//****************************************************************************************************************************************************
/// \param[out] response The response.
/// \return The status for the call.
//****************************************************************************************************************************************************
grpc::Status GRPCService::IsTelemetryDisabled(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::BoolValue *response) {
app().log().debug(__FUNCTION__);
response->set_value(app().mainWindow().settingsTab().isTelemetryDisabled());
return Status::OK;
}
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \return The status for the call. /// \return The status for the call.
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
@ -820,3 +842,4 @@ void GRPCService::finishLogin() {
qtProxy_.sendDelayedEvent(newLoginFinishedEvent(user->id(), alreadyExist)); qtProxy_.sendDelayedEvent(newLoginFinishedEvent(user->id(), alreadyExist));
} }

View File

@ -51,6 +51,8 @@ public: // member functions.
grpc::Status IsBetaEnabled(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::BoolValue *response) override; grpc::Status IsBetaEnabled(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::BoolValue *response) override;
grpc::Status SetIsAllMailVisible(::grpc::ServerContext *context, ::google::protobuf::BoolValue const *request, ::google::protobuf::Empty *response) override; grpc::Status SetIsAllMailVisible(::grpc::ServerContext *context, ::google::protobuf::BoolValue const *request, ::google::protobuf::Empty *response) override;
grpc::Status IsAllMailVisible(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::google::protobuf::BoolValue *response) override; grpc::Status IsAllMailVisible(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::google::protobuf::BoolValue *response) override;
grpc::Status SetIsTelemetryDisabled(::grpc::ServerContext *, ::google::protobuf::BoolValue const *request, ::google::protobuf::Empty *response) override;
grpc::Status IsTelemetryDisabled(::grpc::ServerContext *, ::google::protobuf::Empty const *request, ::google::protobuf::BoolValue *response) override;
grpc::Status TriggerReset(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::Empty *) override; grpc::Status TriggerReset(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::Empty *) override;
grpc::Status Version(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::StringValue *response) override; grpc::Status Version(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::StringValue *response) override;
grpc::Status LogsPath(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::StringValue *response) override; grpc::Status LogsPath(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::StringValue *response) override;

View File

@ -202,6 +202,22 @@ void SettingsTab::setIsAllMailVisible(bool visible) {
} }
//****************************************************************************************************************************************************
/// \return the value for the 'Disabled Telemetry' check.
//****************************************************************************************************************************************************
bool SettingsTab::isTelemetryDisabled() const {
return ui_.checkIsTelemetryDisabled->isChecked();
}
//****************************************************************************************************************************************************
/// \param[in] isDisabled The new value for the 'Disable Telemetry' check box.
//****************************************************************************************************************************************************
void SettingsTab::setIsTelemetryDisabled(bool isDisabled) {
ui_.checkIsTelemetryDisabled->setChecked(isDisabled);
}
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \return The delay to apply before sending automatically generated events. /// \return The delay to apply before sending automatically generated events.
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************

View File

@ -45,6 +45,7 @@ public: // member functions.
bool isAutostartOn() const; ///< Get the value for the 'Autostart' check. bool isAutostartOn() const; ///< Get the value for the 'Autostart' check.
bool isBetaEnabled() const; ///< Get the value for the 'Beta Enabled' check. bool isBetaEnabled() const; ///< Get the value for the 'Beta Enabled' check.
bool isAllMailVisible() const; ///< Get the value for the 'All Mail Visible' check. bool isAllMailVisible() const; ///< Get the value for the 'All Mail Visible' check.
bool isTelemetryDisabled() const; ///< Get the value for the 'Disable Telemetry' check box.
QString colorSchemeName() const; ///< Get the value of the 'Use Dark Theme' checkbox. QString colorSchemeName() const; ///< Get the value of the 'Use Dark Theme' checkbox.
qint32 eventDelayMs() const; ///< Get the delay for sending automatically generated events. qint32 eventDelayMs() const; ///< Get the delay for sending automatically generated events.
QString logsPath() const; ///< Get the content of the 'Logs Path' edit. QString logsPath() const; ///< Get the content of the 'Logs Path' edit.
@ -74,6 +75,7 @@ public slots:
void setIsAutostartOn(bool on); ///< Set the value for the 'Autostart' check box. void setIsAutostartOn(bool on); ///< Set the value for the 'Autostart' check box.
void setIsBetaEnabled(bool enabled); ///< Set the value for the 'Beta Enabled' check box. void setIsBetaEnabled(bool enabled); ///< Set the value for the 'Beta Enabled' check box.
void setIsAllMailVisible(bool visible); ///< Set the value for the 'All Mail Visible' check box. void setIsAllMailVisible(bool visible); ///< Set the value for the 'All Mail Visible' check box.
void setIsTelemetryDisabled(bool isDisabled); ///< Set the value for the 'Disable Telemetry' check box.
void setColorSchemeName(QString const &name); ///< Set the value for the 'Use Dark Theme' check box. void setColorSchemeName(QString const &name); ///< Set the value for the 'Use Dark Theme' check box.
void setBugReport(QString const &osType, QString const &osVersion, QString const &emailClient, QString const &address, QString const &description, void setBugReport(QString const &osType, QString const &osVersion, QString const &emailClient, QString const &address, QString const &description,
bool includeLogs); ///< Set the content of the bug report box. bool includeLogs); ///< Set the content of the bug report box.

View File

@ -170,6 +170,13 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="1">
<widget class="QCheckBox" name="checkIsTelemetryDisabled">
<property name="text">
<string>Disable Telemetry</string>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
</item> </item>

View File

@ -114,6 +114,7 @@ add_executable(bridge-gui
EventStreamWorker.cpp EventStreamWorker.h EventStreamWorker.cpp EventStreamWorker.h
LogUtils.cpp LogUtils.h LogUtils.cpp LogUtils.h
main.cpp main.cpp
TrayIcon.cpp TrayIcon.h
Pch.h Pch.h
QMLBackend.cpp QMLBackend.h QMLBackend.cpp QMLBackend.h
UserList.cpp UserList.h UserList.cpp UserList.h

View File

@ -24,7 +24,6 @@
#include <bridgepp/Log/LogUtils.h> #include <bridgepp/Log/LogUtils.h>
#include <bridgepp/GRPC/GRPCClient.h> #include <bridgepp/GRPC/GRPCClient.h>
#include <bridgepp/Worker/Overseer.h> #include <bridgepp/Worker/Overseer.h>
#include <bridgepp/BridgeUtils.h>
#define HANDLE_EXCEPTION(x) try { x } \ #define HANDLE_EXCEPTION(x) try { x } \
@ -50,6 +49,9 @@ QMLBackend::QMLBackend()
/// \param[in] serviceConfig /// \param[in] serviceConfig
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
void QMLBackend::init(GRPCConfig const &serviceConfig) { void QMLBackend::init(GRPCConfig const &serviceConfig) {
trayIcon_.reset(new TrayIcon());
this->setNormalTrayIcon();
connect(this, &QMLBackend::fatalError, &app(), &AppController::onFatalError); connect(this, &QMLBackend::fatalError, &app(), &AppController::onFatalError);
users_ = new UserList(this); users_ = new UserList(this);
@ -100,6 +102,14 @@ bool QMLBackend::waitForEventStreamReaderToFinish(qint32 timeoutMs) {
} }
//****************************************************************************************************************************************************
/// \return The list of users
//****************************************************************************************************************************************************
UserList const &QMLBackend::users() const {
return *users_;
}
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \return The build year as a string (e.g. 2023) /// \return The build year as a string (e.g. 2023)
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
@ -335,6 +345,18 @@ bool QMLBackend::isAllMailVisible() const {
} }
//****************************************************************************************************************************************************
/// \return The value for the 'isAllMailVisible' property.
//****************************************************************************************************************************************************
bool QMLBackend::isTelemetryDisabled() const {
HANDLE_EXCEPTION_RETURN_BOOL(
bool v;
app().grpc().isTelemetryDisabled(v);
return v;
)
}
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \return The value for the 'colorSchemeName' property. /// \return The value for the 'colorSchemeName' property.
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
@ -569,6 +591,17 @@ void QMLBackend::changeIsAllMailVisible(bool isVisible) {
} }
//****************************************************************************************************************************************************
/// \param[in] isDisabled The new state of the 'Is telemetry disabled property'.
//****************************************************************************************************************************************************
void QMLBackend::toggleIsTelemetryDisabled(bool isDisabled) {
HANDLE_EXCEPTION(
app().grpc().setIsTelemetryDisabled(isDisabled);
emit isTelemetryDisabledChanged(isDisabled);
)
}
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \param[in] scheme the scheme name /// \param[in] scheme the scheme name
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
@ -838,6 +871,49 @@ void QMLBackend::sendBadEventUserFeedback(QString const &userID, bool doResync)
} }
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void QMLBackend::setNormalTrayIcon() {
if (trayIcon_) {
trayIcon_->setState(TrayIcon::State::Normal, tr("Connected"), ":/qml/icons/ic-connected.svg");
}
}
//****************************************************************************************************************************************************
/// \param[in] stateString A string describing the state.
/// \param[in] statusIcon The path of the status icon.
//****************************************************************************************************************************************************
void QMLBackend::setErrorTrayIcon(QString const &stateString, QString const &statusIcon) {
if (trayIcon_) {
trayIcon_->setState(TrayIcon::State::Error, stateString, statusIcon);
}
}
//****************************************************************************************************************************************************
/// \param[in] stateString A string describing the state.
/// \param[in] statusIcon The path of the status icon.
//****************************************************************************************************************************************************
void QMLBackend::setWarnTrayIcon(QString const &stateString, QString const &statusIcon) {
if (trayIcon_) {
trayIcon_->setState(TrayIcon::State::Warn, stateString, statusIcon);
}
}
//****************************************************************************************************************************************************
/// \param[in] stateString A string describing the state.
/// \param[in] statusIcon The path of the status icon.
//****************************************************************************************************************************************************
void QMLBackend::setUpdateTrayIcon(QString const &stateString, QString const &statusIcon) {
if (trayIcon_) {
trayIcon_->setState(TrayIcon::State::Update, stateString, statusIcon);
}
}
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \param[in] imapPort The IMAP port. /// \param[in] imapPort The IMAP port.
/// \param[in] smtpPort The SMTP port. /// \param[in] smtpPort The SMTP port.
@ -892,7 +968,7 @@ void QMLBackend::onLoginAlreadyLoggedIn(QString const &userID) {
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \param[in] userID The userID. /// \param[in] userID The userID.
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
void QMLBackend::onUserBadEvent(QString const &userID, QString const& ) { void QMLBackend::onUserBadEvent(QString const &userID, QString const &) {
HANDLE_EXCEPTION( HANDLE_EXCEPTION(
if (badEventDisplayQueue_.contains(userID)) { if (badEventDisplayQueue_.contains(userID)) {
app().log().error("Received 'bad event' for a user that is already in the queue."); app().log().error("Received 'bad event' for a user that is already in the queue.");
@ -921,8 +997,9 @@ void QMLBackend::onIMAPLoginFailed(QString const &username) {
if ((!user) || (user->state() != UserState::SignedOut)) { // We want to pop-up only if a signed-out user has been detected if ((!user) || (user->state() != UserState::SignedOut)) { // We want to pop-up only if a signed-out user has been detected
return; return;
} }
if (user->isInIMAPLoginFailureCooldown()) if (user->isInIMAPLoginFailureCooldown()) {
return; return;
}
user->startImapLoginFailureCooldown(60 * 60 * 1000); // 1 hour cooldown during which we will not display this notification to this user again. user->startImapLoginFailureCooldown(60 * 60 * 1000); // 1 hour cooldown during which we will not display this notification to this user again.
emit selectUser(user->id()); emit selectUser(user->id());
emit imapLoginWhileSignedOut(username); emit imapLoginWhileSignedOut(username);

View File

@ -22,6 +22,7 @@
#include "MacOS/DockIcon.h" #include "MacOS/DockIcon.h"
#include "BuildConfig.h" #include "BuildConfig.h"
#include "TrayIcon.h"
#include "UserList.h" #include "UserList.h"
#include <bridgepp/GRPC/GRPCClient.h> #include <bridgepp/GRPC/GRPCClient.h>
#include <bridgepp/GRPC/GRPCUtils.h> #include <bridgepp/GRPC/GRPCUtils.h>
@ -43,6 +44,7 @@ public: // member functions.
QMLBackend &operator=(QMLBackend &&) = delete; ///< Disabled move assignment operator. QMLBackend &operator=(QMLBackend &&) = delete; ///< Disabled move assignment operator.
void init(GRPCConfig const &serviceConfig); ///< Initialize the backend. void init(GRPCConfig const &serviceConfig); ///< Initialize the backend.
bool waitForEventStreamReaderToFinish(qint32 timeoutMs); ///< Wait for the event stream reader to finish. bool waitForEventStreamReaderToFinish(qint32 timeoutMs); ///< Wait for the event stream reader to finish.
UserList const& users() const; ///< Return the list of users
// invokable methods can be called from QML. They generally return a value, which slots cannot do. // invokable methods can be called from QML. They generally return a value, which slots cannot do.
Q_INVOKABLE static QString buildYear(); ///< Return the application build year. Q_INVOKABLE static QString buildYear(); ///< Return the application build year.
@ -67,6 +69,7 @@ public: // Qt/QML properties. Note that the NOTIFY-er signal is required even fo
Q_PROPERTY(bool isAutostartOn READ isAutostartOn NOTIFY isAutostartOnChanged) Q_PROPERTY(bool isAutostartOn READ isAutostartOn NOTIFY isAutostartOnChanged)
Q_PROPERTY(bool isBetaEnabled READ isBetaEnabled NOTIFY isBetaEnabledChanged) Q_PROPERTY(bool isBetaEnabled READ isBetaEnabled NOTIFY isBetaEnabledChanged)
Q_PROPERTY(bool isAllMailVisible READ isAllMailVisible NOTIFY isAllMailVisibleChanged) Q_PROPERTY(bool isAllMailVisible READ isAllMailVisible NOTIFY isAllMailVisibleChanged)
Q_PROPERTY(bool isTelemetryDisabled READ isTelemetryDisabled NOTIFY isTelemetryDisabledChanged)
Q_PROPERTY(QString colorSchemeName READ colorSchemeName NOTIFY colorSchemeNameChanged) Q_PROPERTY(QString colorSchemeName READ colorSchemeName NOTIFY colorSchemeNameChanged)
Q_PROPERTY(QUrl diskCachePath READ diskCachePath NOTIFY diskCachePathChanged) Q_PROPERTY(QUrl diskCachePath READ diskCachePath NOTIFY diskCachePathChanged)
Q_PROPERTY(bool useSSLForIMAP READ useSSLForIMAP WRITE setUseSSLForIMAP NOTIFY useSSLForIMAPChanged) Q_PROPERTY(bool useSSLForIMAP READ useSSLForIMAP WRITE setUseSSLForIMAP NOTIFY useSSLForIMAPChanged)
@ -99,6 +102,7 @@ public: // Qt/QML properties. Note that the NOTIFY-er signal is required even fo
bool isAutostartOn() const; ///< Getter for the 'isAutostartOn' property. bool isAutostartOn() const; ///< Getter for the 'isAutostartOn' property.
bool isBetaEnabled() const; ///< Getter for the 'isBetaEnabled' property. bool isBetaEnabled() const; ///< Getter for the 'isBetaEnabled' property.
bool isAllMailVisible() const; ///< Getter for the 'isAllMailVisible' property. bool isAllMailVisible() const; ///< Getter for the 'isAllMailVisible' property.
bool isTelemetryDisabled() const; ///< Getter for the 'isTelemetryDisabled' property.
QString colorSchemeName() const; ///< Getter for the 'colorSchemeName' property. QString colorSchemeName() const; ///< Getter for the 'colorSchemeName' property.
QUrl diskCachePath() const; ///< Getter for the 'diskCachePath' property. QUrl diskCachePath() const; ///< Getter for the 'diskCachePath' property.
void setUseSSLForIMAP(bool value); ///< Setter for the 'useSSLForIMAP' property. void setUseSSLForIMAP(bool value); ///< Setter for the 'useSSLForIMAP' property.
@ -129,6 +133,7 @@ signals: // Signal used by the Qt property system. Many of them are unused but r
void isAutomaticUpdateOnChanged(bool value); ///<Signal for the change of the 'isAutomaticUpdateOn' property. void isAutomaticUpdateOnChanged(bool value); ///<Signal for the change of the 'isAutomaticUpdateOn' property.
void isBetaEnabledChanged(bool value); ///<Signal for the change of the 'isBetaEnabled' property. void isBetaEnabledChanged(bool value); ///<Signal for the change of the 'isBetaEnabled' property.
void isAllMailVisibleChanged(bool value); ///<Signal for the change of the 'isAllMailVisible' property. void isAllMailVisibleChanged(bool value); ///<Signal for the change of the 'isAllMailVisible' property.
void isTelemetryDisabledChanged(bool isDisabled); ///<Signal for the change of the 'isTelemetryDisabled' property.
void colorSchemeNameChanged(QString const &scheme); ///<Signal for the change of the 'colorSchemeName' property. void colorSchemeNameChanged(QString const &scheme); ///<Signal for the change of the 'colorSchemeName' property.
void isDoHEnabledChanged(bool value); ///<Signal for the change of the 'isDoHEnabled' property. void isDoHEnabledChanged(bool value); ///<Signal for the change of the 'isDoHEnabled' property.
void logsPathChanged(QUrl const &path); ///<Signal for the change of the 'logsPath' property. void logsPathChanged(QUrl const &path); ///<Signal for the change of the 'logsPath' property.
@ -151,6 +156,7 @@ public slots: // slot for signals received from QML -> To be forwarded to Bridge
void toggleAutostart(bool active); ///< Slot for the autostart toggle. void toggleAutostart(bool active); ///< Slot for the autostart toggle.
void toggleBeta(bool active); ///< Slot for the beta toggle. void toggleBeta(bool active); ///< Slot for the beta toggle.
void changeIsAllMailVisible(bool isVisible); ///< Slot for the changing of 'All Mail' visibility. void changeIsAllMailVisible(bool isVisible); ///< Slot for the changing of 'All Mail' visibility.
void toggleIsTelemetryDisabled(bool isDisabled); ///< Slot for toggling telemetry on/off.
void changeColorScheme(QString const &scheme); ///< Slot for the change of the theme. void changeColorScheme(QString const &scheme); ///< Slot for the change of the theme.
void setDiskCachePath(QUrl const &path) const; ///< Slot for the change of the disk cache path. void setDiskCachePath(QUrl const &path) const; ///< Slot for the change of the disk cache path.
void login(QString const &username, QString const &password) const; ///< Slot for the login button (initial login). void login(QString const &username, QString const &password) const; ///< Slot for the login button (initial login).
@ -174,6 +180,10 @@ 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 setNormalTrayIcon(); ///< Set the tray icon to normal.
void setErrorTrayIcon(QString const& stateString, QString const &statusIcon); ///< Set the tray icon to 'error' state.
void setWarnTrayIcon(QString const& stateString, QString const &statusIcon); ///< Set the tray icon to 'warn' state.
void setUpdateTrayIcon(QString const& stateString, QString const &statusIcon); ///< Set the tray icon to 'update' state.
public slots: // slot for signals received from gRPC that need transformation instead of simple forwarding public slots: // slot for signals received from gRPC that need transformation instead of simple forwarding
void onMailServerSettingsChanged(int imapPort, int smtpPort, bool useSSLForIMAP, bool useSSLForSMTP); ///< Slot for the ConnectionModeChanged gRPC event. void onMailServerSettingsChanged(int imapPort, int smtpPort, bool useSSLForIMAP, bool useSSLForSMTP); ///< Slot for the ConnectionModeChanged gRPC event.
@ -233,8 +243,10 @@ signals: // Signals received from the Go backend, to be forwarded to QML
void bugReportSendError(); ///< Signal for the 'bugReportSendError' gRPC stream event. void bugReportSendError(); ///< Signal for the 'bugReportSendError' gRPC stream event.
void showMainWindow(); ///< Signal for the 'showMainWindow' gRPC stream event. void showMainWindow(); ///< Signal for the 'showMainWindow' gRPC stream event.
void hideMainWindow(); ///< Signal for the 'hideMainWindow' gRPC stream event. void hideMainWindow(); ///< Signal for the 'hideMainWindow' gRPC stream event.
void showHelp(); ///< Signal for the 'showHelp' event (from the context menu).
void showSettings(); ///< Signal for the 'showHelp' event (from the context menu).
void selectUser(QString const& userID); ///< Signal emitted in order to selected a user with a given ID in the list.
void genericError(QString const &title, QString const &description); ///< Signal for the 'genericError' gRPC stream event. void genericError(QString const &title, QString const &description); ///< Signal for the 'genericError' gRPC stream event.
void selectUser(QString const); ///< Signal that request the given user account to be displayed.
void imapLoginWhileSignedOut(QString const& username); ///< Signal for the notification of IMAP login attempt on a signed out account. void imapLoginWhileSignedOut(QString const& username); ///< Signal for the notification of IMAP login attempt on a signed out account.
// This signal is emitted when an exception is intercepted is calls triggered by QML. QML engine would intercept the exception otherwise. // This signal is emitted when an exception is intercepted is calls triggered by QML. QML engine would intercept the exception otherwise.
@ -257,7 +269,7 @@ private: // data members
bool useSSLForIMAP_ { false }; ///< The cached value for useSSLForIMAP. bool useSSLForIMAP_ { false }; ///< The cached value for useSSLForIMAP.
bool useSSLForSMTP_ { false }; ///< The cached value for useSSLForSMTP. bool useSSLForSMTP_ { false }; ///< The cached value for useSSLForSMTP.
QList<QString> badEventDisplayQueue_; ///< THe queue for displaying 'bad event feedback request dialog'. QList<QString> badEventDisplayQueue_; ///< THe queue for displaying 'bad event feedback request dialog'.
std::unique_ptr<TrayIcon> trayIcon_;
friend class AppController; friend class AppController;
}; };

View File

@ -5,11 +5,7 @@
<file>qml/AccountView.qml</file> <file>qml/AccountView.qml</file>
<file>qml/Banner.qml</file> <file>qml/Banner.qml</file>
<file>qml/Bridge.qml</file> <file>qml/Bridge.qml</file>
<file>qml/Bridge_test.qml</file>
<file>qml/bridgeqml.qmlproject</file> <file>qml/bridgeqml.qmlproject</file>
<file>qml/BridgeTest/UserControl.qml</file>
<file>qml/BridgeTest/UserList.qml</file>
<file>qml/BridgeTest/UserModel.qml</file>
<file>qml/BugReportView.qml</file> <file>qml/BugReportView.qml</file>
<file>qml/Configuration.qml</file> <file>qml/Configuration.qml</file>
<file>qml/ConfigurationItem.qml</file> <file>qml/ConfigurationItem.qml</file>
@ -28,6 +24,7 @@
<file>qml/icons/ic-connected.svg</file> <file>qml/icons/ic-connected.svg</file>
<file>qml/icons/ic-copy.svg</file> <file>qml/icons/ic-copy.svg</file>
<file>qml/icons/ic-cross-close.svg</file> <file>qml/icons/ic-cross-close.svg</file>
<file>qml/icons/ic-dot.svg</file>
<file>qml/icons/ic-drive.svg</file> <file>qml/icons/ic-drive.svg</file>
<file>qml/icons/ic-exclamation-circle-filled.svg</file> <file>qml/icons/ic-exclamation-circle-filled.svg</file>
<file>qml/icons/ic-external-link.svg</file> <file>qml/icons/ic-external-link.svg</file>
@ -104,17 +101,6 @@
<file>qml/ConnectionModeSettings.qml</file> <file>qml/ConnectionModeSettings.qml</file>
<file>qml/SplashScreen.qml</file> <file>qml/SplashScreen.qml</file>
<file>qml/Status.qml</file> <file>qml/Status.qml</file>
<file>qml/StatusWindow.qml</file>
<file>qml/tests/Buttons.qml</file>
<file>qml/tests/ButtonsColumn.qml</file>
<file>qml/tests/CheckBoxes.qml</file>
<file>qml/tests/ComboBoxes.qml</file>
<file>qml/tests/RadioButtons.qml</file>
<file>qml/tests/Switches.qml</file>
<file>qml/tests/Test.qml</file>
<file>qml/tests/TestComponents.qml</file>
<file>qml/tests/TextAreas.qml</file>
<file>qml/tests/TextFields.qml</file>
<file>qml/WelcomeGuide.qml</file> <file>qml/WelcomeGuide.qml</file>
</qresource> </qresource>
</RCC> </RCC>

View File

@ -0,0 +1,296 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
#include "TrayIcon.h"
#include "QMLBackend.h"
#include <bridgepp/Exception/Exception.h>
#include <bridgepp/BridgeUtils.h>
using namespace bridgepp;
namespace {
QColor const normalColor(30, 168, 133); /// The normal state color.
QColor const errorColor(220, 50, 81); ///< The error state color.
QColor const warnColor(255, 153, 0); ///< The warn state color.
QColor const updateColor(35, 158, 206); ///< The warn state color.
QColor const greyColor(112, 109, 107); ///< The grey color.
qint64 const iconRefreshTimerIntervalMs = 1000; ///< The interval for the refresh timer when switching DPI / screen config, in milliseconds.
qint64 const iconRefreshDurationSecs = 10; ///< The total number of seconds during wich we periodically refresh the icon after a DPI change.
//****************************************************************************************************************************************************
/// \brief Create a single resolution icon from an image. Throw an exception in case of failure.
//****************************************************************************************************************************************************
QIcon loadIconFromImage(QString const &path) {
QPixmap const pixmap(path);
if (pixmap.isNull()) {
throw Exception(QString("Could create icon from image '%1'.").arg(path));
}
return QIcon(pixmap);
}
//****************************************************************************************************************************************************
/// \brief Retrieve the color associated with a tray icon state.
///
/// \param[in] state The state.
/// \return The color associated with a given tray icon state.
//****************************************************************************************************************************************************
QColor stateColor(TrayIcon::State state) {
switch (state) {
case TrayIcon::State::Normal:
return normalColor;
case TrayIcon::State::Error:
return errorColor;
case TrayIcon::State::Warn:
return warnColor;
case TrayIcon::State::Update:
return updateColor;
default:
app().log().error(QString("Unknown tray icon state %1.").arg(static_cast<qint32>(state)));
return normalColor;
}
}
//****************************************************************************************************************************************************
/// \brief Return the text identifying a state in resource file names.
///
/// \param[in] state The state.
/// \param[in] The text identifying the state in resource file names.
//****************************************************************************************************************************************************
QString stateText(TrayIcon::State state) {
switch (state) {
case TrayIcon::State::Normal:
return "norm";
case TrayIcon::State::Error:
return "error";
case TrayIcon::State::Warn:
return "warn";
case TrayIcon::State::Update:
return "update";
default:
app().log().error(QString("Unknown tray icon state %1.").arg(static_cast<qint32>(state)));
return "norm";
}
}
} // anonymous namespace
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
TrayIcon::TrayIcon()
: QSystemTrayIcon()
, menu_(new QMenu) {
this->generateDotIcons();
this->setContextMenu(menu_.get());
connect(menu_.get(), &QMenu::aboutToShow, this, &TrayIcon::onMenuAboutToShow);
connect(this, &TrayIcon::selectUser, &app().backend(), &QMLBackend::selectUser);
connect(this, &TrayIcon::activated, this, &TrayIcon::onActivated);
this->show();
this->setState(State::Normal, QString(), QString());
// TrayIcon does not expose its screen, so we connect relevant screen events to our DPI change handler.
for (QScreen *screen: QGuiApplication::screens()) {
connect(screen, &QScreen::logicalDotsPerInchChanged, this, &TrayIcon::handleDPIChange);
}
connect(qApp, &QApplication::screenAdded, [&](QScreen *screen) { connect(screen, &QScreen::logicalDotsPerInchChanged, this, &TrayIcon::handleDPIChange); });
connect(qApp, &QApplication::primaryScreenChanged, [&](QScreen *screen) { connect(screen, &QScreen::logicalDotsPerInchChanged, this, &TrayIcon::handleDPIChange); });
iconRefreshTimer_.setSingleShot(false);
iconRefreshTimer_.setInterval(iconRefreshTimerIntervalMs);
connect(&iconRefreshTimer_, &QTimer::timeout, this, &TrayIcon::onIconRefreshTimer);
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void TrayIcon::onMenuAboutToShow() {
this->refreshContextMenu();
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void TrayIcon::onUserClicked() {
try {
auto action = dynamic_cast<QAction *>(this->sender());
if (!action) {
throw Exception("Could not retrieve context menu action.");
}
QString const &userID = action->data().toString();
if (userID.isNull()) {
throw Exception("Could not retrieve context menu's selected user.");
}
emit selectUser(userID);
} catch (Exception const &e) {
app().log().error(e.qwhat());
}
}
//****************************************************************************************************************************************************
/// \param[in] reason The icon activation reason.
//****************************************************************************************************************************************************
void TrayIcon::onActivated(QSystemTrayIcon::ActivationReason reason) {
if ((QSystemTrayIcon::Trigger == reason) && !onMacOS()) {
app().backend().showMainWindow();
}
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void TrayIcon::handleDPIChange() {
this->setIcon();
// Windows forces us to apply a hack. Tray icon does not redraw by itself, so we use the Qt signal that detects screen and DPI changes.
// But the moment we get the signal the DPI change is not yet in effect. so redrawing now will have no effect, and we don't really
// know when we can safely redraw. So we will redraw the icon every second for some time.
iconRefreshDeadline_ = QDateTime::currentDateTime().addSecs(iconRefreshDurationSecs);
if (!iconRefreshTimer_.isActive()) {
iconRefreshTimer_.start();
}
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void TrayIcon::setIcon() {
QString const style = onMacOS() ? "mono" : "color";
QString const text = stateText(state_);
QIcon icon = loadIconFromImage(QString(":/qml/icons/systray-%1-%2.png").arg(style, text));
icon.setIsMask(true);
QSystemTrayIcon::setIcon(icon);
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void TrayIcon::onIconRefreshTimer() {
this->setIcon();
if (QDateTime::currentDateTime() > iconRefreshDeadline_) {
iconRefreshTimer_.stop();
}
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void TrayIcon::generateDotIcons() {
QPixmap dotSVG(":/qml/icons/ic-dot.svg");
struct IconColor {
QIcon &icon;
QColor color;
};
for (auto pair: QList<IconColor> {{ greenDot_, normalColor }, { greyDot_, greyColor }, { orangeDot_, warnColor }}) {
QPixmap p = dotSVG;
QPainter painter(&p);
painter.setCompositionMode(QPainter::CompositionMode_SourceIn);
painter.fillRect(p.rect(), pair.color);
painter.end();
pair.icon = QIcon(p);
}
}
//****************************************************************************************************************************************************
/// \param[in] state The state.
/// \param[in] stateString A string describing the state.
/// \param[in] statusIconPath The status icon path.
/// \param[in] statusIconColor The color for the status icon in hex.
//****************************************************************************************************************************************************
void TrayIcon::setState(TrayIcon::State state, QString const &stateString, QString const &statusIconPath) {
stateString_ = stateString;
state_ = state;
this->setIcon();
this->generateStatusIcon(statusIconPath, stateColor(state));
}
//****************************************************************************************************************************************************
/// \param[in] svgPath The path of the SVG file for the icon.
/// \param[in] color The color to apply to the icon.
//****************************************************************************************************************************************************
void TrayIcon::generateStatusIcon(QString const &svgPath, QColor const &color) {
// We use the SVG path as pixmap mask and fill it with the appropriate color
QString resourcePath = svgPath;
resourcePath.replace(QRegularExpression(R"(^\.\/)"), ":/qml/"); // QML resource path are a bit different from the Qt resources path.
QPixmap pixmap(resourcePath);
QPainter painter(&pixmap);
painter.setCompositionMode(QPainter::CompositionMode_SourceIn);
painter.fillRect(pixmap.rect(), color);
painter.end();
statusIcon_ = QIcon(pixmap);
}
//**********************************************************************************************************************
//
//**********************************************************************************************************************
void TrayIcon::refreshContextMenu() {
if (!menu_) {
app().log().error("Native tray icon context menu is null.");
return;
}
menu_->clear();
menu_->addAction(statusIcon_, stateString_, &app().backend(), &QMLBackend::showMainWindow);
menu_->addSeparator();
UserList const &users = app().backend().users();
qint32 const userCount = users.count();
for (qint32 i = 0; i < userCount; i++) {
User const &user = *users.get(i);
UserState const state = user.state();
auto action = new QAction(user.primaryEmailOrUsername());
action->setIcon((UserState::Connected == state) ? greenDot_ : (UserState::Locked == state ? orangeDot_ : greyDot_));
action->setData(user.id());
connect(action, &QAction::triggered, this, &TrayIcon::onUserClicked);
if (i < 10) {
action->setShortcut(QKeySequence(QString("Ctrl+%1").arg((i + 1) % 10)));
}
menu_->addAction(action);
}
if (userCount) {
menu_->addSeparator();
}
menu_->addAction(tr("&Open Bridge"), QKeySequence("Ctrl+O"), &app().backend(), &QMLBackend::showMainWindow);
menu_->addAction(tr("&Help"), QKeySequence("Ctrl+F1"), &app().backend(), &QMLBackend::showHelp);
menu_->addAction(tr("&Settings"), QKeySequence("Ctrl+,"), &app().backend(), &QMLBackend::showSettings);
menu_->addSeparator();
menu_->addAction(tr("&Quit Bridge"), QKeySequence("Ctrl+Q"), &app().backend(), &QMLBackend::quit);
}

View File

@ -0,0 +1,77 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
#ifndef BRIDGE_GUI_NATIVE_TRAY_ICON_H
#define BRIDGE_GUI_NATIVE_TRAY_ICON_H
//**********************************************************************************************************************
/// \brief A native tray icon.
//**********************************************************************************************************************
class TrayIcon: public QSystemTrayIcon {
Q_OBJECT
public: // typedef enum
enum class State {
Normal,
Error,
Warn,
Update,
}; ///< Enumeration for the state.
public: // data members
TrayIcon(); ///< Default constructor.
~TrayIcon() override = default; ///< Destructor.
TrayIcon(TrayIcon const&) = delete; ///< Disabled copy-constructor.
TrayIcon(TrayIcon&&) = delete; ///< Disabled assignment copy-constructor.
TrayIcon& operator=(TrayIcon const&) = delete; ///< Disabled assignment operator.
TrayIcon& operator=(TrayIcon&&) = delete; ///< Disabled move assignment operator.
void setState(State state, QString const& stateString, QString const &statusIconPath); ///< Set the state of the icon
void showNotificationPopup(QString const& title, QString const &message, QString const& iconPath); ///< Display a pop up notification.
signals:
void selectUser(QString const& userID); ///< Signal for selecting a user with a given userID
private slots:
void onMenuAboutToShow(); ///< Slot called before the context menu is shown.
void onUserClicked(); ///< Slot triggered when clicking on a user in the context menu.
static void onActivated(QSystemTrayIcon::ActivationReason reason); ///< Slot for the activation of the system tray icon.
void handleDPIChange(); ///< Handles DPI change.
void setIcon(); ///< set the tray icon.
void onIconRefreshTimer(); ///< Timer for icon refresh.
private: // member functions.
void generateDotIcons(); ///< generate the colored dot icons used for user status.
void generateStatusIcon(QString const &svgPath, QColor const& color); ///< Generate the status icon.
void refreshContextMenu(); ///< Refresh the context menu.
private: // data members
State state_ { State::Normal }; ///< The state of the tray icon.
QString stateString_; ///< The current state string.
std::unique_ptr<QMenu> menu_; ///< The context menu for the tray icon. Not owned by the tray icon.
QIcon statusIcon_; ///< The path of the status icon displayed in the context menu.
QIcon greenDot_; ///< The green dot icon.
QIcon greyDot_; ///< The grey dot icon.
QIcon orangeDot_; ///< The orange dot icon.
QTimer iconRefreshTimer_; ///< The timer used to periodically refresh the icon when DPI changes.
QDateTime iconRefreshDeadline_; ///< The deadline for refreshing the icon
};
#endif //BRIDGE_GUI_NATIVE_TRAY_ICON_H

View File

@ -27,67 +27,55 @@ Item {
property var notifications property var notifications
property var user property var user
signal showSignIn() signal showSignIn
signal showSetupGuide(var user, string address) signal showSetupGuide(var user, string address)
property int _leftMargin: 64 property int _contentWidth: 640
property int _rightMargin: 64
property int _topMargin: 32 property int _topMargin: 32
property int _detailsTopMargin: 25 property int _detailsMargin: 25
property int _bottomMargin: 12
property int _spacing: 20 property int _spacing: 20
property int _lineWidth: 1 property int _lineThickness: 1
property bool _connected: root.user ? root.user.state === EUserState.Connected : false
ScrollView {
id: scrollView
clip: true
Rectangle {
anchors.fill: parent anchors.fill: parent
Component.onCompleted: contentItem.boundsBehavior = Flickable.StopAtBounds // Disable the springy effect when scroll reaches top/bottom. color: root.colorScheme.background_weak
Item { ScrollView {
// can't use parent here because parent is not ScrollView (Flickable inside contentItem inside ScrollView) id: scrollView
width: scrollView.availableWidth anchors.fill: parent
height: scrollView.availableHeight Component.onCompleted: contentItem.boundsBehavior = Flickable.StopAtBounds
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
// do not set implicitWidth because implicit width of ColumnLayout will be equal to maximum implicit width of
// internal items. And if one of internal items would be a Text or Label - implicit width of those is always
// equal to non-wrapped text (i.e. one line only). That will lead to enabling horizontal scroll when not needed
implicitWidth: width
ColumnLayout { ColumnLayout {
id: topLevelColumnLayout
anchors.fill: parent
spacing: 0 spacing: 0
anchors.fill: parent
Rectangle { Rectangle {
id: topRectangle id: topArea
color: root.colorScheme.background_norm color: root.colorScheme.background_norm
clip: true
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
Layout.fillWidth: true Layout.fillWidth: true
implicitHeight: childrenRect.height
ColumnLayout { ColumnLayout {
spacing: root._spacing id: topLayout
width: _contentWidth
anchors.horizontalCenter: parent.horizontalCenter
spacing: _spacing
anchors.fill: parent RowLayout {
anchors.leftMargin: root._leftMargin // account delegate with action buttons
anchors.rightMargin: root._rightMargin
anchors.topMargin: root._topMargin
anchors.bottomMargin: root._bottomMargin
RowLayout { // account delegate with action buttons
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: _topMargin
AccountDelegate { AccountDelegate {
Layout.fillWidth: true Layout.fillWidth: true
colorScheme: root.colorScheme colorScheme: root.colorScheme
user: root.user user: root.user
type: AccountDelegate.LargeView type: AccountDelegate.LargeView
enabled: root.user ? (root.user.state === EUserState.Connected) : false enabled: _connected
} }
Button { Button {
@ -95,10 +83,11 @@ Item {
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: qsTr("Sign out") text: qsTr("Sign out")
secondary: true secondary: true
visible: root.user ? (root.user.state === EUserState.Connected) : false visible: _connected
onClicked: { onClicked: {
if (!root.user) return if (!root.user)
root.user.logout() return;
root.user.logout();
} }
} }
@ -109,8 +98,9 @@ Item {
secondary: true secondary: true
visible: root.user ? (root.user.state === EUserState.SignedOut) : false visible: root.user ? (root.user.state === EUserState.SignedOut) : false
onClicked: { onClicked: {
if (!root.user) return if (!root.user)
root.showSignIn() return;
root.showSignIn();
} }
} }
@ -120,8 +110,9 @@ Item {
icon.source: "/qml/icons/ic-trash.svg" icon.source: "/qml/icons/ic-trash.svg"
secondary: true secondary: true
onClicked: { onClicked: {
if (!root.user) return if (!root.user)
root.notifications.askDeleteAccount(root.user) return;
root.notifications.askDeleteAccount(root.user);
} }
visible: root.user ? root.user.state !== EUserState.Locked : false visible: root.user ? root.user.state !== EUserState.Locked : false
} }
@ -129,7 +120,7 @@ Item {
Rectangle { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
height: root._lineWidth height: root._lineThickness
color: root.colorScheme.border_weak color: root.colorScheme.border_weak
} }
@ -139,12 +130,12 @@ Item {
actionText: qsTr("Configure") actionText: qsTr("Configure")
description: qsTr("Using the mailbox details below (re)configure your client.") description: qsTr("Using the mailbox details below (re)configure your client.")
type: SettingsItem.Button type: SettingsItem.Button
enabled: root.user ? root.user.state === EUserState.Connected : false visible: _connected && (!root.user.splitMode) || (root.user.addresses.length === 1)
visible: root.user ? !root.user.splitMode || root.user.addresses.length==1 : false
showSeparator: splitMode.visible showSeparator: splitMode.visible
onClicked: { onClicked: {
if (!root.user) return if (!root.user)
root.showSetupGuide(root.user, user.addresses[0]) return;
root.showSetupGuide(root.user, user.addresses[0]);
} }
Layout.fillWidth: true Layout.fillWidth: true
@ -157,15 +148,14 @@ Item {
description: qsTr("Setup multiple email addresses individually.") description: qsTr("Setup multiple email addresses individually.")
type: SettingsItem.Toggle type: SettingsItem.Toggle
checked: root.user ? root.user.splitMode : false checked: root.user ? root.user.splitMode : false
visible: root.user ? root.user.addresses.length > 1 : false visible: _connected && root.user.addresses.length > 1
enabled: root.user ? (root.user.state === EUserState.Connected) : false
showSeparator: addressSelector.visible showSeparator: addressSelector.visible
onClicked: { onClicked: {
if (!splitMode.checked){ if (!splitMode.checked) {
root.notifications.askEnableSplitMode(user) root.notifications.askEnableSplitMode(user);
} else { } else {
addressSelector.currentIndex = 0 addressSelector.currentIndex = 0;
root.user.toggleSplitMode(!splitMode.checked) root.user.toggleSplitMode(!splitMode.checked);
} }
} }
@ -174,8 +164,8 @@ Item {
RowLayout { RowLayout {
Layout.fillWidth: true Layout.fillWidth: true
enabled: root.user ? (root.user.state === EUserState.Connected) : false Layout.bottomMargin: _spacing
visible: root.user ? root.user.splitMode : false visible: _connected && root.user.splitMode
ComboBox { ComboBox {
id: addressSelector id: addressSelector
@ -189,60 +179,68 @@ Item {
text: qsTr("Configure") text: qsTr("Configure")
secondary: true secondary: true
onClicked: { onClicked: {
if (!root.user) return if (!root.user)
root.showSetupGuide(root.user, addressSelector.displayText) return;
root.showSetupGuide(root.user, addressSelector.displayText);
} }
} }
} }
Rectangle {
height: 0
} // just for some extra space before separator
} }
} }
Rectangle { Rectangle {
id: bottomArea
Layout.fillWidth: true
implicitHeight: bottomLayout.implicitHeight
color: root.colorScheme.background_weak color: root.colorScheme.background_weak
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
Layout.fillWidth: true
ColumnLayout { ColumnLayout {
id: configuration id: bottomLayout
width: _contentWidth
anchors.fill: parent anchors.horizontalCenter: parent.horizontalCenter
anchors.leftMargin: root._leftMargin spacing: _spacing
anchors.rightMargin: root._rightMargin visible: _connected
anchors.topMargin: root._detailsTopMargin
anchors.bottomMargin: root._spacing
spacing: root._spacing
visible: root.user ? (root.user.state === EUserState.Connected) : false
property string currentAddress: addressSelector.displayText
Label { Label {
Layout.topMargin: _detailsMargin
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: qsTr("Mailbox details") text: qsTr("Mailbox details")
type: Label.Body_semibold type: Label.Body_semibold
} }
Configuration { RowLayout {
colorScheme: root.colorScheme id: configuration
title: qsTr("IMAP") spacing: _spacing
hostname: Backend.hostname Layout.fillWidth: true
port: Backend.imapPort.toString() Layout.fillHeight: true
username: configuration.currentAddress
password: root.user ? root.user.password : ""
security : Backend.useSSLForIMAP ? "SSL" : "STARTTLS"
}
Configuration { property string currentAddress: addressSelector.displayText
colorScheme: root.colorScheme
title: qsTr("SMTP") Configuration {
hostname : Backend.hostname Layout.fillWidth: true
port : Backend.smtpPort.toString() colorScheme: root.colorScheme
username : configuration.currentAddress title: qsTr("IMAP")
password : root.user ? root.user.password : "" hostname: Backend.hostname
security : Backend.useSSLForSMTP ? "SSL" : "STARTTLS" port: Backend.imapPort.toString()
username: configuration.currentAddress
password: root.user ? root.user.password : ""
security: Backend.useSSLForIMAP ? "SSL" : "STARTTLS"
}
Configuration {
Layout.fillWidth: true
colorScheme: root.colorScheme
title: qsTr("SMTP")
hostname: Backend.hostname
port: Backend.smtpPort.toString()
username: configuration.currentAddress
password: root.user ? root.user.password : ""
security: Backend.useSSLForSMTP ? "SSL" : "STARTTLS"
}
} }
} }
} }

View File

@ -26,11 +26,8 @@ import Notifications
QtObject { QtObject {
id: root id: root
function isInInterval(num, lower_limit, upper_limit) { function bound(num, lowerLimit, upperLimit) {
return lower_limit <= num && num <= upper_limit return Math.max(lowerLimit, Math.min(upperLimit, num))
}
function bound(num, lower_limit, upper_limit) {
return Math.max(lower_limit, Math.min(upper_limit, num))
} }
property var title: Backend.appname property var title: Backend.appname
@ -38,10 +35,30 @@ QtObject {
property Notifications _notifications: Notifications { property Notifications _notifications: Notifications {
id: notifications id: notifications
frontendMain: mainWindow frontendMain: mainWindow
frontendStatus: statusWindow
frontendTray: trayIcon
} }
property NotificationFilter _trayNotificationFilter: NotificationFilter {
id: trayNotificationFilter
source: root._notifications ? root._notifications.all : undefined
onTopmostChanged: {
if (topmost) {
switch (topmost.type) {
case Notification.NotificationType.Danger:
Backend.setErrorTrayIcon(topmost.brief, topmost.icon)
return
case Notification.NotificationType.Warning:
Backend.setWarnTrayIcon(topmost.brief, topmost.icon)
return
case Notification.NotificationType.Info:
Backend.setUpdateTrayIcon(topmost.brief, topmost.icon)
return
}
}
Backend.setNormalTrayIcon()
}
}
property MainWindow _mainWindow: MainWindow { property MainWindow _mainWindow: MainWindow {
id: mainWindow id: mainWindow
visible: false visible: false
@ -66,190 +83,6 @@ QtObject {
} }
} }
property StatusWindow _statusWindow: StatusWindow {
id: statusWindow
visible: false
title: root.title
notifications: root._notifications
onShowMainWindow: {
mainWindow.showAndRise()
}
onShowHelp: {
mainWindow.showHelp()
mainWindow.showAndRise()
}
onShowSettings: {
mainWindow.showSettings()
mainWindow.showAndRise()
}
onSelectUser: function(userID) {
mainWindow.selectUser(userID)
mainWindow.showAndRise()
}
onQuit: {
mainWindow.hide()
trayIcon.visible = false
Backend.quit()
}
property rect screenRect
property rect iconRect
// use binding from function with width and height as arguments so it will be recalculated every time width and height are changed
property point position: getPosition(width, height)
x: position.x
y: position.y
function getPosition(_width, _height) {
if (screenRect.width === 0 || screenRect.height === 0) {
return Qt.point(0, 0)
}
var _x = 0
var _y = 0
// fit above
_y = iconRect.top - height
if (isInInterval(_y, screenRect.top, screenRect.bottom - height)) {
// position preferably in the horizontal center but bound to the screen rect
_x = bound(iconRect.left + (iconRect.width - width)/2, screenRect.left, screenRect.right - width)
return Qt.point(_x, _y)
}
// fit below
_y = iconRect.bottom
if (isInInterval(_y, screenRect.top, screenRect.bottom - height)) {
// position preferably in the horizontal center but bound to the screen rect
_x = bound(iconRect.left + (iconRect.width - width)/2, screenRect.left, screenRect.right - width)
return Qt.point(_x, _y)
}
// fit to the left
_x = iconRect.left - width
if (isInInterval(_x, screenRect.left, screenRect.right - width)) {
// position preferably in the vertical center but bound to the screen rect
_y = bound(iconRect.top + (iconRect.height - height)/2, screenRect.top, screenRect.bottom - height)
return Qt.point(_x, _y)
}
// fit to the right
_x = iconRect.right
if (isInInterval(_x, screenRect.left, screenRect.right - width)) {
// position preferably in the vertical center but bound to the screen rect
_y = bound(iconRect.top + (iconRect.height - height)/2, screenRect.top, screenRect.bottom - height)
return Qt.point(_x, _y)
}
// Fallback: position status window right above icon and let window manager decide.
console.warn("Can't position status window: screenRect =", screenRect, "iconRect =", iconRect)
_x = bound(iconRect.left + (iconRect.width - width)/2, screenRect.left, screenRect.right - width)
_y = bound(iconRect.top + (iconRect.height - height)/2, screenRect.top, screenRect.bottom - height)
return Qt.point(_x, _y)
}
}
property SystemTrayIcon _trayIcon: SystemTrayIcon {
id: trayIcon
visible: true
icon.source: getTrayIconPath()
icon.mask: true // make sure that systems like macOS will use proper color
tooltip: `${root.title} v${Backend.version}`
onActivated: function(reason) {
function calcStatusWindowPosition() {
// On some platforms (X11 / Plasma) Qt does not provide icon position and geometry info.
// In this case we rely on cursor position
var iconRect = Qt.rect(geometry.x, geometry.y, geometry.width, geometry.height)
if (geometry.width == 0 && geometry.height == 0) {
var mousePos = Backend.getCursorPos()
iconRect.x = mousePos.x
iconRect.y = mousePos.y
iconRect.width = 0
iconRect.height = 0
}
// Find screen
var screen
for (var i in Qt.application.screens) {
var _screen = Qt.application.screens[i]
if (
isInInterval(iconRect.x, _screen.virtualX, _screen.virtualX + _screen.width) &&
isInInterval(iconRect.y, _screen.virtualY, _screen.virtualY + _screen.height)
) {
screen = _screen
break
}
}
if (!screen) {
// Fallback to primary screen
screen = Qt.application.screens[0]
}
// In case we used mouse to detect icon position - we want to make a fake icon rectangle from a point
if (iconRect.width == 0 && iconRect.height == 0) {
iconRect.x = bound(iconRect.x - 16, screen.virtualX, screen.virtualX + screen.width - 32)
iconRect.y = bound(iconRect.y - 16, screen.virtualY, screen.virtualY + screen.height - 32)
iconRect.width = 32
iconRect.height = 32
}
statusWindow.screenRect = Qt.rect(screen.virtualX, screen.virtualY, screen.width, screen.height)
statusWindow.iconRect = iconRect
}
function toggleWindow(win) {
if (win.visible) {
win.close()
} else {
win.showAndRise()
}
}
switch (reason) {
case SystemTrayIcon.Unknown:
break;
case SystemTrayIcon.Context:
case SystemTrayIcon.Trigger:
case SystemTrayIcon.DoubleClick:
case SystemTrayIcon.MiddleClick:
calcStatusWindowPosition()
toggleWindow(statusWindow)
break;
default:
break;
}
}
property NotificationFilter _systrayfilter: NotificationFilter {
source: root._notifications ? root._notifications.all : undefined
}
function getTrayIconPath() {
var color = Backend.goos == "darwin" ? "mono" : "color"
var level = "norm"
if (_systrayfilter.topmost) {
switch (_systrayfilter.topmost.type) {
case Notification.NotificationType.Danger:
level = "error"
break;
case Notification.NotificationType.Warning:
level = "warn"
break;
case Notification.NotificationType.Info:
level = "update"
break;
}
}
return `qrc:/qml/icons/systray-${color}-${level}.png`
}
}
Component.onCompleted: { Component.onCompleted: {
if (!Backend) { if (!Backend) {
@ -266,7 +99,7 @@ QtObject {
var c = Backend.users.count var c = Backend.users.count
var u = Backend.users.get(0) var u = Backend.users.get(0)
// DEBUG // DEBUG
if (c != 0) { if (c !== 0) {
console.log("users non zero", c) console.log("users non zero", c)
console.log("first user", u ) console.log("first user", u )
} }
@ -290,7 +123,7 @@ QtObject {
} }
function setColorScheme() { function setColorScheme() {
if (Backend.colorSchemeName == "light") ProtonStyle.currentStyle = ProtonStyle.lightStyle if (Backend.colorSchemeName === "light") ProtonStyle.currentStyle = ProtonStyle.lightStyle
if (Backend.colorSchemeName == "dark") ProtonStyle.currentStyle = ProtonStyle.darkStyle if (Backend.colorSchemeName === "dark") ProtonStyle.currentStyle = ProtonStyle.darkStyle
} }
} }

View File

@ -1,315 +0,0 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQml
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Proton
ColumnLayout {
id: root
property var user
property var userIndex
spacing : 5
Layout.fillHeight: true
//Layout.fillWidth: true
property ColorScheme colorScheme
TextField {
colorScheme: root.colorScheme
Layout.fillWidth: true
text: user !== undefined ? user.username : ""
onEditingFinished: {
user.username = text
}
}
ColumnLayout {
Layout.fillWidth: true
Switch {
id: userLoginSwitch
colorScheme: root.colorScheme
text: "LoggedIn"
enabled: user !== undefined && user.username.length > 0
checked: user ? root.user.state == EUserState.Connected : false
onCheckedChanged: {
if (!user) {
return
}
if (checked) {
if (user === Backend.loginUser) {
var newUserObject = Backend.userComponent.createObject(Backend, {username: user.username, loggedIn: true, setupGuideSeen: user.setupGuideSeen})
Backend.users.append( { object: newUserObject } )
user.username = ""
user.resetLoginRequests()
return
}
user.state = EUserState.Connected
user.resetLoginRequests()
return
} else {
user.state = EUserState.SignedOut
user.resetLoginRequests()
}
}
}
Switch {
colorScheme: root.colorScheme
text: "Setup guide seen"
enabled: user !== undefined && user.username.length > 0
checked: user ? user.setupGuideSeen : false
onCheckedChanged: {
if (!user) {
return
}
user.setupGuideSeen = checked
}
}
}
RowLayout {
Layout.fillWidth: true
Label {
colorScheme: root.colorScheme
id: loginLabel
text: "Login:"
Layout.preferredWidth: Math.max(loginLabel.implicitWidth, faLabel.implicitWidth, passLabel.implicitWidth)
}
Button {
colorScheme: root.colorScheme
text: "name/pass error"
enabled: user !== undefined //&& user.isLoginRequested && !user.isLogin2FARequested && !user.isLogin2PasswordProvided
onClicked: {
Backend.loginUsernamePasswordError("")
user.resetLoginRequests()
}
}
Button {
colorScheme: root.colorScheme
text: "free user error"
enabled: user !== undefined //&& user.isLoginRequested
onClicked: {
Backend.loginFreeUserError()
user.resetLoginRequests()
}
}
Button {
colorScheme: root.colorScheme
text: "connection error"
enabled: user !== undefined //&& user.isLoginRequested
onClicked: {
Backend.loginConnectionError("")
user.resetLoginRequests()
}
}
}
RowLayout {
Layout.fillWidth: true
Label {
colorScheme: root.colorScheme
id: faLabel
text: "2FA:"
Layout.preferredWidth: Math.max(loginLabel.implicitWidth, faLabel.implicitWidth, passLabel.implicitWidth)
}
Button {
colorScheme: root.colorScheme
text: "request"
enabled: user !== undefined //&& user.isLoginRequested && !user.isLogin2FARequested && !user.isLogin2PasswordRequested
onClicked: {
Backend.login2FARequested(user.username)
user.isLogin2FARequested = true
}
}
Button {
colorScheme: root.colorScheme
text: "error"
enabled: user !== undefined //&& user.isLogin2FAProvided && !(user.isLogin2PasswordRequested && !user.isLogin2PasswordProvided)
onClicked: {
Backend.login2FAError("")
user.isLogin2FAProvided = false
}
}
Button {
colorScheme: root.colorScheme
text: "Abort"
enabled: user !== undefined //&& user.isLogin2FAProvided && !(user.isLogin2PasswordRequested && !user.isLogin2PasswordProvided)
onClicked: {
Backend.login2FAErrorAbort("")
user.resetLoginRequests()
}
}
}
RowLayout {
Layout.fillWidth: true
Label {
colorScheme: root.colorScheme
id: passLabel
text: "2 Password:"
Layout.preferredWidth: Math.max(loginLabel.implicitWidth, faLabel.implicitWidth, passLabel.implicitWidth)
}
Button {
colorScheme: root.colorScheme
text: "request"
enabled: user !== undefined //&& user.isLoginRequested && !user.isLogin2PasswordRequested && !(user.isLogin2FARequested && !user.isLogin2FAProvided)
onClicked: {
Backend.login2PasswordRequested("")
user.isLogin2PasswordRequested = true
}
}
Button {
colorScheme: root.colorScheme
text: "error"
enabled: user !== undefined //&& user.isLogin2PasswordProvided && !(user.isLogin2FARequested && !user.isLogin2FAProvided)
onClicked: {
Backend.login2PasswordError("")
user.isLogin2PasswordProvided = false
}
}
Button {
colorScheme: root.colorScheme
text: "Abort"
enabled: user !== undefined //&& user.isLogin2PasswordProvided && !(user.isLogin2FARequested && !user.isLogin2FAProvided)
onClicked: {
Backend.login2PasswordErrorAbort("")
user.resetLoginRequests()
}
}
}
RowLayout {
Button {
colorScheme: root.colorScheme
text: "Login Finished"
onClicked: {
Backend.loginFinished(0+loginFinishedIndex.text)
user.resetLoginRequests()
}
}
TextField {
id: loginFinishedIndex
colorScheme: root.colorScheme
label: "Index:"
text: root.userIndex
}
}
RowLayout {
Button {
colorScheme: root.colorScheme
text: "Already logged in"
onClicked: {
Backend.loginAlreadyLoggedIn(0+loginAlreadyLoggedInIndex.text)
user.resetLoginRequests()
}
}
TextField {
id: loginAlreadyLoggedInIndex
colorScheme: root.colorScheme
label: "Index:"
text: root.userIndex
}
}
RowLayout {
TextField {
colorScheme: root.colorScheme
label: "used:"
text: user && user.usedBytes ? user.usedBytes : 0
onEditingFinished: {
user.usedBytes = parseFloat(text)
}
implicitWidth: 200
}
TextField {
colorScheme: root.colorScheme
label: "total:"
text: user && user.totalBytes ? user.totalBytes : 0
onEditingFinished: {
user.totalBytes = parseFloat(text)
}
implicitWidth: 200
}
}
RowLayout {
Label {colorScheme: root.colorScheme; text: "Split mode"}
Toggle { colorScheme: root.colorScheme; checked: user ? user.splitMode : false; onClicked: {user.splitMode = !user.splitMode}}
Button { colorScheme: root.colorScheme; text: "Toggle Finished"; onClicked: {user.toggleSplitModeFinished()}}
}
TextArea { // TODO: this is causing binding loop on implicitWidth
colorScheme: root.colorScheme
text: user && user.addresses ? user.addresses.join("\n") : "user@protonmail.com"
Layout.fillWidth: true
onEditingFinished: {
user.addresses = text.split("\n")
}
}
Item {
Layout.fillHeight: true
}
}

View File

@ -1,101 +0,0 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Proton
ColumnLayout {
id: root
property ColorScheme colorScheme
property alias currentIndex: usersListView.currentIndex
ListView {
id: usersListView
Layout.fillHeight: true
Layout.preferredWidth: 200
model: Backend.usersTest
highlightFollowsCurrentItem: true
delegate: Item {
implicitHeight: children[0].implicitHeight + anchors.topMargin + anchors.bottomMargin
implicitWidth: children[0].implicitWidth + anchors.leftMargin + anchors.rightMargin
width: usersListView.width
anchors.margins: 10
Label {
colorScheme: root.colorScheme
text: modelData.username
anchors.margins: 10
anchors.fill: parent
MouseArea {
anchors.fill: parent
onClicked: {
usersListView.currentIndex = index
}
}
}
}
highlight: Rectangle {
color: root.colorScheme.interaction_default_active
}
}
RowLayout {
Layout.fillWidth: true
Button {
colorScheme: root.colorScheme
text: "+"
onClicked: {
var newUserObject = Backend.userComponent.createObject(Backend)
newUserObject.username = Backend.loginUser.username.length > 0 ? Backend.loginUser.username : "test@protonmail.com"
newUserObject.state = EUserState.Connected
newUserObject.setupGuideSeen = true // Backend.loginUser.setupGuideSeen
Backend.loginUser.username = ""
Backend.loginUser.state = EUserState.SignedOut
Backend.loginUser.setupGuideSeen = false
Backend.users.append( { object: newUserObject } )
}
}
Button {
colorScheme: root.colorScheme
text: "-"
enabled: usersListView.currentIndex != 0
onClicked: {
// var userObject = Backend.users.get(usersListView.currentIndex - 1)
Backend.users.remove(usersListView.currentIndex - 1)
// userObject.deleteLater()
}
}
}
}

View File

@ -1,982 +0,0 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQml
import QtQuick
import QtQuick.Window
import QtQuick.Layouts
import QtQuick.Controls
import QtQml.Models
import Qt.labs.platform
import Proton
import "./BridgeTest"
import BridgePreview
import Notifications
Window {
id: root
x: 10
y: 10
width: 800
height: 800
property ColorScheme colorScheme: ProtonStyle.darkStyle
flags : Qt.Window | Qt.Dialog
visible : true
title : "Bridge Test GUI"
// This is needed because on MacOS if first window shown is not transparent -
// all other windows of application will not have transparent background (black
// instead of transparency). In our case that mean that if BridgeTest will be
// shown before StatusWindow - StatusWindow will not have transparent corners.
color: "transparent"
function getCursorPos() {
return BridgePreview.getCursorPos()
}
function restart() {
root.quit()
console.log("Restarting....")
root.openBridge()
}
function openBridge() {
bridge = bridgeComponent.createObject()
var showSetupGuide = false
if (showSetupGuide) {
var newUserObject = root.userComponent.createObject(root)
newUserObject.username = "LerooooyJenkins@protonmail.com"
newUserObject.state = EUserState.Connected
newUserObject.setupGuideSeen = false
root.users.append( { object: newUserObject } )
}
}
function quit() {
if (bridge !== undefined && bridge !== null) {
bridge.destroy()
}
}
function guiReady() {
console.log("Gui Ready")
}
function _log(msg, color) {
logTextArea.text += "<p style='color: " + color + ";'>" + msg + "</p>"
logTextArea.text += "\n"
}
function log(msg) {
console.log(msg)
_log(msg, root.colorScheme.signal_info)
}
function error(msg) {
console.error(msg)
_log(msg, root.colorScheme.signal_danger)
}
// No user object should be put in this list until a successful login
property var users: UserModel {
id: _users
onRowsInserted: {
for (var i = first; i <= last; i++) {
_usersTest.insert(i + 1, { object: get(i) } )
}
}
onRowsRemoved: {
_usersTest.remove(first + 1, first - last + 1)
}
onRowsMoved: {
_usersTest.move(start + 1, row + 1, end - start + 1)
}
onDataChanged: {
for (var i = topLeft.row; i <= bottomRight.row; i++) {
_usersTest.set(i + 1, { object: get(i) } )
}
}
}
// this list is used on test gui: it contains same users list as users above + fake user to represent login request of new user on pos 0
property var usersTest: UserModel {
id: _usersTest
}
property var userComponent: Component {
id: _userComponent
QtObject {
property string username: ""
property bool loggedIn: false
property bool splitMode: false
property bool setupGuideSeen: true
property var usedBytes: 5350*1024*1024
property var totalBytes: 20*1024*1024*1024
property string avatarText: "jd"
property string password: "SMj975NnEYYsqu55GGmlpv"
property var addresses: [
"jaanedoe@protonmail.com",
"jane@pm.me",
"jdoe@pm.me"
]
signal loginUsernamePasswordError()
signal loginFreeUserError()
signal loginConnectionError()
signal login2FARequested()
signal login2FAError()
signal login2FAErrorAbort()
signal login2PasswordRequested()
signal login2PasswordError()
signal login2PasswordErrorAbort()
// Test purpose only:
property bool isFakeUser: this === root.loginUser
function userSignal(msg) {
if (isFakeUser) {
return
}
root.log("<- User (" + username + "): " + msg)
}
function toggleSplitMode(makeActive) {
userSignal("toggle split mode "+makeActive)
}
signal toggleSplitModeFinished()
function configureAppleMail(address){
userSignal("configure apple mail "+address)
}
function logout(){
userSignal("logout")
loggedIn = false
}
function remove(){
console.log("remove this", users.count)
for (var i=0; i<users.count; i++) {
if (users.get(i) === this) {
users.remove(i,1)
return
}
}
}
onLoginUsernamePasswordError: {
userSignal("loginUsernamePasswordError")
}
onLoginFreeUserError: {
userSignal("loginFreeUserError")
}
onLoginConnectionError: {
userSignal("loginConnectionError")
}
onLogin2FARequested: {
userSignal("login2FARequested")
}
onLogin2FAError: {
userSignal("login2FAError")
}
onLogin2FAErrorAbort: {
userSignal("login2FAErrorAbort")
}
onLogin2PasswordRequested: {
userSignal("login2PasswordRequested")
}
onLogin2PasswordError: {
userSignal("login2PasswordError")
}
onLogin2PasswordErrorAbort: {
userSignal("login2PasswordErrorAbort")
}
function resetLoginRequests() {
isLoginRequested = false
isLogin2FARequested = false
isLogin2FAProvided = false
isLogin2PasswordRequested = false
isLogin2PasswordProvided = false
}
property bool isLoginRequested: false
property bool isLogin2FARequested: false
property bool isLogin2FAProvided: false
property bool isLogin2PasswordRequested: false
property bool isLogin2PasswordProvided: false
}
}
// this it fake user used only for representing first login request
property var loginUser
Component.onCompleted: {
var newLoginUser = _userComponent.createObject()
root.loginUser = newLoginUser
root.loginUser.setupGuideSeen = false
_usersTest.append({object: newLoginUser})
newLoginUser.loginUsernamePasswordError.connect(root.loginUsernamePasswordError)
newLoginUser.loginFreeUserError.connect(root.loginFreeUserError)
newLoginUser.loginConnectionError.connect(root.loginConnectionError)
newLoginUser.login2FARequested.connect(root.login2FARequested)
newLoginUser.login2FAError.connect(root.login2FAError)
newLoginUser.login2FAErrorAbort.connect(root.login2FAErrorAbort)
newLoginUser.login2PasswordRequested.connect(root.login2PasswordRequested)
newLoginUser.login2PasswordError.connect(root.login2PasswordError)
newLoginUser.login2PasswordErrorAbort.connect(root.login2PasswordErrorAbort)
// add one user on start
var hasUserOnStart = false
if (hasUserOnStart) {
var newUserObject = root.userComponent.createObject(root)
newUserObject.username = "LerooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooyJenkins@protonmail.com"
newUserObject.loggedIn = EUserState.Connected
newUserObject.setupGuideSeen = true
root.users.append( { object: newUserObject } )
}
}
TabBar {
id: tabBar
anchors.left: parent.left
anchors.right: parent.right
TabButton {
text: "Global settings"
}
TabButton {
text: "User control"
}
TabButton {
text: "Notifications"
}
TabButton {
text: "Log"
}
TabButton {
text: "Settings signals"
}
}
Rectangle {
color: root.colorScheme.background_norm
anchors.top: tabBar.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
implicitHeight: children[0].contentHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
implicitWidth: children[0].contentWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
StackLayout {
anchors.fill: parent
currentIndex: tabBar.currentIndex
anchors.margins: 10
RowLayout {
id: globalTab
spacing : 5
ColumnLayout {
spacing : 5
Label {
colorScheme: root.colorScheme
text: "Global settings"
}
ButtonGroup {
id: styleRadioGroup
}
RadioButton {
colorScheme: root.colorScheme
Layout.fillWidth: true
text: "Light UI"
checked: ProtonStyle.currentStyle === ProtonStyle.lightStyle
ButtonGroup.group: styleRadioGroup
onCheckedChanged: {
if (checked && ProtonStyle.currentStyle !== ProtonStyle.lightStyle) {
root.colorSchemeName = "light"
}
}
}
RadioButton {
colorScheme: root.colorScheme
Layout.fillWidth: true
text: "Dark UI"
checked: ProtonStyle.currentStyle === ProtonStyle.darkStyle
ButtonGroup.group: styleRadioGroup
onCheckedChanged: {
if (checked && ProtonStyle.currentStyle !== ProtonStyle.darkStyle) {
root.colorSchemeName = "dark"
}
}
}
CheckBox {
id: showOnStartupCheckbox
colorScheme: root.colorScheme
text: "Show on startup"
checked: root.showOnStartup
onCheckedChanged: {
root.showOnStartup = checked
}
}
CheckBox {
id: showSplashScreen
colorScheme: root.colorScheme
text: "Show splash screen"
checked: root.showSplashScreen
onCheckedChanged: {
root.showSplashScreen = checked
}
}
Button {
colorScheme: root.colorScheme
//Layout.fillWidth: true
text: "Open Bridge"
enabled: bridge === undefined || bridge === null
onClicked: root.openBridge()
}
Button {
colorScheme: root.colorScheme
//Layout.fillWidth: true
text: "Close Bridge"
enabled: bridge !== undefined && bridge !== null
onClicked: {
bridge.destroy()
}
}
Item {
Layout.fillHeight: true
}
}
ColumnLayout {
spacing : 5
Label {
colorScheme: root.colorScheme
text: "Notifications"
}
Button {
colorScheme: root.colorScheme
text: "Notify: danger"
enabled: bridge !== undefined && bridge !== null
onClicked: {
bridge.mainWindow.notifyOnlyPaidUsers()
}
}
Button {
colorScheme: root.colorScheme
text: "Notify: warning"
enabled: bridge !== undefined && bridge !== null
onClicked: {
bridge.mainWindow.notifyUpdateManually()
}
}
Button {
colorScheme: root.colorScheme
text: "Notify: success"
enabled: bridge !== undefined && bridge !== null
onClicked: {
bridge.mainWindow.notifyUserAdded()
}
}
Item {
Layout.fillHeight: true
}
}
}
RowLayout {
id: usersTab
UserList {
id: usersListView
Layout.fillHeight: true
colorScheme: root.colorScheme
backend: root
}
UserControl {
colorScheme: root.colorScheme
backend: root
user: ((root.usersTest.count > usersListView.currentIndex) && usersListView.currentIndex != -1) ? root.usersTest.get(usersListView.currentIndex) : undefined
userIndex: usersListView.currentIndex - 1 // -1 because 0 index is fake user
}
}
RowLayout {
id: notificationsTab
spacing: 5
ColumnLayout {
spacing: 5
Switch {
text: "Internet connection"
colorScheme: root.colorScheme
checked: true
onCheckedChanged: {
checked ? root.internetOn() : root.internetOff()
}
}
Button {
text: "Update manual ready"
colorScheme: root.colorScheme
onClicked: {
root.updateManualReady("3.14.1592")
}
}
Button {
text: "Update manual done"
colorScheme: root.colorScheme
onClicked: {
root.updateManualRestartNeeded()
}
}
Button {
text: "Update manual error"
colorScheme: root.colorScheme
onClicked: {
root.updateManualError()
}
}
Button {
text: "Update force"
colorScheme: root.colorScheme
onClicked: {
root.updateForce("3.14.1592")
}
}
Button {
text: "Update force error"
colorScheme: root.colorScheme
onClicked: {
root.updateForceError()
}
}
Button {
text: "Update silent done"
colorScheme: root.colorScheme
onClicked: {
root.updateSilentRestartNeeded()
}
}
Button {
text: "Update silent error"
colorScheme: root.colorScheme
onClicked: {
root.updateSilentError()
}
}
Button {
text: "Update is latest version"
colorScheme: root.colorScheme
onClicked: {
root.updateIsLatestVersion()
}
}
Button {
text: "Bug report send OK"
colorScheme: root.colorScheme
onClicked: {
root.reportBugFinished()
root.bugReportSendSuccess()
}
}
}
ColumnLayout {
spacing: 5
Button {
text: "Bug report send error"
colorScheme: root.colorScheme
onClicked: {
root.reportBugFinished()
root.bugReportSendError()
}
}
Button {
text: "Cache anavailable"
colorScheme: root.colorScheme
onClicked: {
root.cacheUnavailable()
}
}
Button {
text: "Cache can't move"
colorScheme: root.colorScheme
onClicked: {
root.cacheCantMove()
}
}
Button {
text: "Cache location change success"
onClicked: {
root.cacheLocationChangeSuccess()
}
colorScheme: root.colorScheme
}
Button {
text: "Disk full"
colorScheme: root.colorScheme
onClicked: {
root.diskFull()
}
}
Button {
text: "No keychain"
colorScheme: root.colorScheme
onClicked: {
root.notifyHasNoKeychain()
}
}
Button {
text: "Rebuild keychain"
colorScheme: root.colorScheme
onClicked: {
root.notifyRebuildKeychain()
}
}
Button {
text: "Address changed"
colorScheme: root.colorScheme
onClicked: {
root.addressChanged("p@v.el")
}
}
Button {
text: "Address changed + Logout"
colorScheme: root.colorScheme
onClicked: {
root.addressChangedLogout("p@v.el")
}
}
}
}
TextArea {
id: logTextArea
colorScheme: root.colorScheme
Layout.fillHeight: true
Layout.fillWidth: true
Layout.preferredWidth: 400
Layout.preferredHeight: 200
textFormat: TextEdit.RichText
//readOnly: true
}
ScrollView {
id: settingsTab
ColumnLayout {
RowLayout {
Label {colorScheme : root.colorScheme ; text : "GOOS : "}
Button {colorScheme : root.colorScheme ; text : "Linux" ; onClicked : root.goos = "linux" ; enabled: root.goos != "linux"}
Button {colorScheme : root.colorScheme ; text : "Windows" ; onClicked : root.goos = "windows" ; enabled: root.goos != "windows"}
Button {colorScheme : root.colorScheme ; text : "macOS" ; onClicked : root.goos = "darwin" ; enabled: root.goos != "darwin"}
}
RowLayout {
Label {colorScheme: root.colorScheme; text: "Automatic updates:"}
Toggle {colorScheme: root.colorScheme; checked: root.isAutomaticUpdateOn; onClicked: root.isAutomaticUpdateOn = !root.isAutomaticUpdateOn}
}
RowLayout {
Label {colorScheme: root.colorScheme; text: "Autostart:"}
Toggle {colorScheme: root.colorScheme; checked: root.isAutostartOn; onClicked: root.isAutostartOn = !root.isAutostartOn}
Button {colorScheme: root.colorScheme; text: "Toggle finished"; onClicked: root.toggleAutostartFinished()}
}
RowLayout {
Label {colorScheme: root.colorScheme; text: "Beta:"}
Toggle {colorScheme: root.colorScheme; checked: root.isBetaEnabled; onClicked: root.isBetaEnabled = !root.isBetaEnabled}
}
RowLayout {
Label {colorScheme: root.colorScheme; text: "DoH:"}
Toggle {colorScheme: root.colorScheme; checked: root.isDoHEnabled; onClicked: root.isDoHEnabled = !root.isDoHEnabled}
}
RowLayout {
Label {colorScheme: root.colorScheme; text: "All Mail disabled:"}
Toggle {colorScheme: root.colorScheme; checked: root.isAllMailVisible; onClicked: root.isAllMailVisible = !root.isAllMailVisible}
}
RowLayout {
Label {colorScheme: root.colorScheme; text: "Ports:"}
TextField {
colorScheme:root.colorScheme
label: "IMAP"
text: root.portIMAP
onEditingFinished: root.portIMAP = this.text*1
validator: IntValidator {bottom: 1; top: 65536}
}
TextField {
colorScheme:root.colorScheme
label: "SMTP"
text: root.portSMTP
onEditingFinished: root.portSMTP = this.text*1
validator: IntValidator {bottom: 1; top: 65536}
}
Button {colorScheme: root.colorScheme; text: "Change finished"; onClicked: root.changePortFinished()}
}
RowLayout {
Label {colorScheme: root.colorScheme; text: "SMTP using SSL:"}
Toggle {colorScheme: root.colorScheme; checked: root.useSSLForSMTP; onClicked: root.useSSLForSMTP = !root.useSSLForSMTP}
}
RowLayout {
Label {colorScheme: root.colorScheme; text: "Local cache:"}
Toggle {colorScheme: root.colorScheme; checked: root.isDiskCacheEnabled; onClicked: root.isDiskCacheEnabled = !root.isDiskCacheEnabled}
TextField {
colorScheme:root.colorScheme
label: "Path"
text: root.diskCachePath.toString().replace("file://", "")
implicitWidth: 160
onEditingFinished: {
root.diskCachePath = Qt.resolvedUrl("file://"+text)
}
}
Button {colorScheme: root.colorScheme; text: "Change finished"; onClicked: root.changeLocalCacheFinished()}
}
RowLayout {
Label {colorScheme: root.colorScheme; text: "Reset:"}
Button {colorScheme: root.colorScheme; text: "Finished"; onClicked: root.resetFinished()}
}
RowLayout {
Label {colorScheme: root.colorScheme; text: "Check update:"}
Button {colorScheme: root.colorScheme; text: "Finished"; onClicked: root.checkUpdatesFinished()}
}
}
}
}
}
property Bridge bridge
property string goos: "darwin"
property bool showOnStartup: true // this actually needs to be false, but since we use Bridge_test for testing purpose - lets default this to true just for convenience
property bool dockIconVisible: false
// this signals are used only when trying to login with new user (i.e. not in users model)
signal loginUsernamePasswordError(string errorMsg)
signal loginFreeUserError()
signal loginConnectionError(string errorMsg)
signal login2FARequested(string username)
signal login2FAError(string errorMsg)
signal login2FAErrorAbort(string errorMsg)
signal login2PasswordRequested()
signal login2PasswordError(string errorMsg)
signal login2PasswordErrorAbort(string errorMsg)
signal loginFinished(int index)
signal loginAlreadyLoggedIn(int index)
signal internetOff()
signal internetOn()
signal updateManualReady(var version)
signal updateManualRestartNeeded()
signal updateManualError()
signal updateForce(var version)
signal updateForceError()
signal updateSilentRestartNeeded()
signal updateSilentError()
signal updateIsLatestVersion()
function checkUpdates(){
console.log("check updates")
}
signal checkUpdatesFinished()
function installUpdate() {
console.log("manuall install update triggered")
}
property bool isDiskCacheEnabled: true
// Qt.resolvedUrl("file:///C:/Users/user/AppData/Roaming/protonmail/bridge-v3/cache/c11/messages")
property url diskCachePath: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
signal cacheUnavailable()
signal cacheCantMove()
signal cacheLocationChangeSuccess()
signal diskFull()
function changeLocalCache(enableDiskCache, diskCachePath) {
console.debug("-> disk cache", enableDiskCache, diskCachePath)
}
signal changeLocalCacheFinished()
// Settings
property bool isAutomaticUpdateOn : true
function toggleAutomaticUpdate(makeItActive) {
console.debug("-> silent updates", makeItActive, root.isAutomaticUpdateOn)
var callback = function () {
root.isAutomaticUpdateOn = makeItActive;
console.debug("-> CHANGED silent updates", makeItActive, root.isAutomaticUpdateOn)
}
atimer.onTriggered.connect(callback)
atimer.restart()
}
Timer {
id: atimer
interval: 2000
running: false
repeat: false
}
property bool isAutostartOn : true // Example of settings with loading state
function toggleAutostart(makeItActive) {
console.debug("-> autostart", makeItActive, root.isAutostartOn)
}
signal toggleAutostartFinished()
property bool isBetaEnabled : false
function toggleBeta(makeItActive){
console.debug("-> beta", makeItActive, root.isBetaEnabled)
root.isBetaEnabled = makeItActive
}
property bool isDoHEnabled : true
function toggleDoH(makeItActive){
console.debug("-> DoH", makeItActive, root.isDoHEnabled)
root.isDoHEnabled = makeItActive
}
property bool isAllMailVisible : true
function changeIsAllMailVisible(isVisible){
console.debug("-> All Mail Visible", isVisible, root.isAllMailVisible)
root.isAllMailVisible = isVisible
}
property bool useSSLForSMTP: false
function toggleUseSSLForSMTP(makeItActive){
console.debug("-> SMTP SSL", makeItActive, root.useSSLForSMTP)
}
signal toggleUseSSLFinished()
property string hostname: "127.0.0.1"
property int portIMAP: 1143
property int portSMTP: 1025
function changePorts(imapPort, smtpPort){
console.debug("-> ports", imapPort, smtpPort)
}
function isPortFree(port){
if (port == portIMAP) return false
if (port == portSMTP) return false
if (port == 12345) return false
return true
}
signal changePortFinished()
signal imapPortStartupError()
signal smtpPortStartupError()
function triggerReset() {
console.debug("-> trigger reset")
}
signal resetFinished()
property string version: "2.0.X-BridePreview"
property url logsPath: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
property url licensePath: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
property url releaseNotesLink: Qt.resolvedUrl("https://proton.me/download/bridge/early_releases.html")
property url dependencyLicensesLink: Qt.resolvedUrl("https://github.com/ProtonMail/proton-bridge/v3/blob/master/COPYING_NOTES.md#dependencies")
property url landingPageLink: Qt.resolvedUrl("https://proton.me/mail/bridge#download")
property string colorSchemeName: "light"
function changeColorScheme(newScheme){
root.colorSchemeName = newScheme
}
property string currentEmailClient: "" // "Apple Mail 14.0"
function updateCurrentMailClient(){
currentEmailClient = "Apple Mail 14.0"
}
function reportBug(description,address,emailClient,includeLogs){
console.log("report bug")
console.log(" description",description)
console.log(" address",address)
console.log(" emailClient",emailClient)
console.log(" includeLogs",includeLogs)
}
signal reportBugFinished()
signal bugReportSendSuccess()
signal bugReportSendError()
property var availableKeychain: ["gnome-keyring", "pass", "macos-keychain", "windows-credentials"]
property string currentKeychain: availableKeychain[0]
function changeKeychain(wantedKeychain){
console.log("Changing keychain from", root.currentKeychain, "to", wantedKeychain)
root.currentKeychain = wantedKeychain
root.changeKeychainFinished()
}
signal changeKeychainFinished()
signal notifyHasNoKeychain()
signal notifyRebuildKeychain()
signal noActiveKeyForRecipient(string email)
signal showMainWindow()
signal addressChanged(string address)
signal addressChangedLogout(string address)
signal userDisconnected(string username)
signal apiCertIssue()
property bool showSplashScreen: false
function login(username, password) {
root.log("-> login(" + username + ", " + password + ")")
loginUser.username = username
loginUser.isLoginRequested = true
}
function login2FA(username, code) {
root.log("-> login2FA(" + username + ", " + code + ")")
loginUser.isLogin2FAProvided = true
}
function login2Password(username, password) {
root.log("-> login2FA(" + username + ", " + password + ")")
loginUser.isLogin2PasswordProvided = true
}
function loginAbort(username) {
root.log("-> loginAbort(" + username + ")")
loginUser.resetLoginRequests()
}
onLoginUsernamePasswordError: {
console.debug("<- loginUsernamePasswordError")
}
onLoginFreeUserError: {
console.debug("<- loginFreeUserError")
}
onLoginConnectionError: {
console.debug("<- loginConnectionError")
}
onLogin2FARequested: {
console.debug("<- login2FARequested", username)
}
onLogin2FAError: {
console.debug("<- login2FAError")
}
onLogin2FAErrorAbort: {
console.debug("<- login2FAErrorAbort")
}
onLogin2PasswordRequested: {
console.debug("<- login2PasswordRequested")
}
onLogin2PasswordError: {
console.debug("<- login2PasswordError")
}
onLogin2PasswordErrorAbort: {
console.debug("<- login2PasswordErrorAbort")
}
onLoginFinished: {
console.debug("<- loginFinished", index)
}
onLoginAlreadyLoggedIn: {
console.debug("<- loginAlreadyLoggedIn", index)
}
onInternetOff: {
console.debug("<- internetOff")
}
onInternetOn: {
console.debug("<- internetOn")
}
Component {
id: bridgeComponent
Bridge {
backend: root
}
}
onClosing: {
Qt.quit()
}
}

View File

@ -61,8 +61,6 @@ Rectangle {
type: Label.Body_semibold type: Label.Body_semibold
} }
Item{}
ConfigurationItem{ colorScheme: root.colorScheme; label: qsTr("Hostname") ; value: root.hostname } ConfigurationItem{ colorScheme: root.colorScheme; label: qsTr("Hostname") ; value: root.hostname }
ConfigurationItem{ colorScheme: root.colorScheme; label: qsTr("Port") ; value: root.port } ConfigurationItem{ colorScheme: root.colorScheme; label: qsTr("Port") ; value: root.port }
ConfigurationItem{ colorScheme: root.colorScheme; label: qsTr("Username") ; value: root.username } ConfigurationItem{ colorScheme: root.colorScheme; label: qsTr("Username") ; value: root.username }

View File

@ -169,6 +169,19 @@ SettingsView {
Layout.fillWidth: true Layout.fillWidth: true
} }
SettingsItem {
id: telemetry
Layout.fillWidth: true
checked: !Backend.isTelemetryDisabled
colorScheme: root.colorScheme
description: qsTr("Help us improve Proton services by sending anonymous usage statistics.")
text: qsTr("Collect usage diagnostics")
type: SettingsItem.Toggle
visible: root._isAdvancedShown
onClicked: Backend.toggleIsTelemetryDisabled(telemetry.checked)
}
SettingsItem { SettingsItem {
id: ports id: ports
visible: root._isAdvancedShown visible: root._isAdvancedShown

View File

@ -41,7 +41,7 @@ SettingsView {
actionIcon: "/qml/icons/ic-external-link.svg" actionIcon: "/qml/icons/ic-external-link.svg"
description: qsTr("Get help setting up your client with our instructions and FAQs.") description: qsTr("Get help setting up your client with our instructions and FAQs.")
type: SettingsItem.PrimaryButton type: SettingsItem.PrimaryButton
onClicked: {Qt.openUrlExternally("https://proton.me/support/mail")} onClicked: {Qt.openUrlExternally("https://proton.me/support/bridge")}
Layout.fillWidth: true Layout.fillWidth: true
} }

View File

@ -24,29 +24,20 @@ import QtQuick.Controls
import Proton import Proton
import Notifications import Notifications
import "tests"
ApplicationWindow { ApplicationWindow {
id: root id: root
colorScheme: ProtonStyle.currentStyle
width: 960
height: 576
visible: true visible: true
minimumHeight: contentLayout.implicitHeight
minimumWidth: contentLayout.implicitWidth
colorScheme: ProtonStyle.currentStyle property int _defaultWidth: 1080
property int _defaultHeight: 780
width: _defaultWidth
height: _defaultHeight
minimumWidth: _defaultWidth
property var notifications property var notifications
// This is needed because on MacOS if first window shown is not transparent -
// all other windows of application will not have transparent background (black
// instead of transparency). In our case that mean that if MainWindow will be
// shown before StatusWindow - StatusWindow will not have transparent corners.
color: "transparent"
// show Setup Guide on every new user // show Setup Guide on every new user
Connections { Connections {
target: Backend.users target: Backend.users
@ -86,10 +77,6 @@ ApplicationWindow {
root.showAndRise() root.showAndRise()
} }
function onSelectUser(userID) {
root.selectUser(userID)
}
function onLoginFinished(index, wasSignedOut) { function onLoginFinished(index, wasSignedOut) {
var user = Backend.users.get(index) var user = Backend.users.get(index)
if (user && !wasSignedOut) { if (user && !wasSignedOut) {
@ -97,6 +84,21 @@ ApplicationWindow {
} }
console.debug("Login finished", index) console.debug("Login finished", index)
} }
function onShowHelp() {
root.showHelp()
root.showAndRise()
}
function onShowSettings() {
root.showSettings()
root.showAndRise()
}
function onSelectUser(userID) {
contentWrapper.selectUser(userID)
root.showAndRise()
}
} }
StackLayout { StackLayout {

View File

@ -24,8 +24,6 @@ QtObject {
id: root id: root
property MainWindow frontendMain property MainWindow frontendMain
property StatusWindow frontendStatus
property SystemTrayIcon frontendTray
signal askEnableBeta() signal askEnableBeta()
signal askEnableSplitMode(var user) signal askEnableSplitMode(var user)
@ -140,7 +138,7 @@ QtObject {
property Notification imapPortChangeError: Notification { property Notification imapPortChangeError: Notification {
description: qsTr("The IMAP port could not be changed.") description: qsTr("The IMAP port could not be changed.")
brief: qsTr("IMAP port change error") brief: qsTr("IMAP port error")
icon: "./icons/ic-alert.svg" icon: "./icons/ic-alert.svg"
type: Notification.NotificationType.Danger type: Notification.NotificationType.Danger
group: Notifications.Group.Connection group: Notifications.Group.Connection
@ -156,7 +154,7 @@ QtObject {
property Notification smtpPortChangeError: Notification { property Notification smtpPortChangeError: Notification {
description: qsTr("The SMTP port could not be changed.") description: qsTr("The SMTP port could not be changed.")
brief: qsTr("SMTP port change error") brief: qsTr("SMTP port error")
icon: "./icons/ic-alert.svg" icon: "./icons/ic-alert.svg"
type: Notification.NotificationType.Danger type: Notification.NotificationType.Danger
group: Notifications.Group.Connection group: Notifications.Group.Connection
@ -172,7 +170,7 @@ QtObject {
property Notification imapConnectionModeChangeError: Notification { property Notification imapConnectionModeChangeError: Notification {
description: qsTr("The IMAP connection mode could not be changed.") description: qsTr("The IMAP connection mode could not be changed.")
brief: qsTr("IMAP Connection mode change error") brief: qsTr("IMAP Connection mode error")
icon: "./icons/ic-alert.svg" icon: "./icons/ic-alert.svg"
type: Notification.NotificationType.Danger type: Notification.NotificationType.Danger
group: Notifications.Group.Connection group: Notifications.Group.Connection
@ -196,7 +194,7 @@ QtObject {
property Notification smtpConnectionModeChangeError: Notification { property Notification smtpConnectionModeChangeError: Notification {
description: qsTr("The SMTP connection mode could not be changed.") description: qsTr("The SMTP connection mode could not be changed.")
brief: qsTr("SMTP Connection mode change error") brief: qsTr("SMTP Connection mode error")
icon: "./icons/ic-alert.svg" icon: "./icons/ic-alert.svg"
type: Notification.NotificationType.Danger type: Notification.NotificationType.Danger
group: Notifications.Group.Connection group: Notifications.Group.Connection
@ -227,7 +225,7 @@ QtObject {
var link = Backend.releaseNotesLink var link = Backend.releaseNotesLink
return `${descr} <a href="${link}">${text}</a>` return `${descr} <a href="${link}">${text}</a>`
} }
brief: qsTr("Update available.") brief: qsTr("Update available")
icon: "./icons/ic-info-circle-filled.svg" icon: "./icons/ic-info-circle-filled.svg"
type: Notification.NotificationType.Info type: Notification.NotificationType.Info
group: Notifications.Group.Update | Notifications.Group.Dialogs group: Notifications.Group.Update | Notifications.Group.Dialogs
@ -514,7 +512,7 @@ QtObject {
// login // login
property Notification loginConnectionError: Notification { property Notification loginConnectionError: Notification {
description: qsTr("Bridge is not able to contact the server, please check your internet connection.") description: qsTr("Bridge is not able to contact the server, please check your internet connection.")
brief: description brief: qsTr("Connection error")
icon: "./icons/ic-exclamation-circle-filled.svg" icon: "./icons/ic-exclamation-circle-filled.svg"
type: Notification.NotificationType.Danger type: Notification.NotificationType.Danger
group: Notifications.Group.Configuration group: Notifications.Group.Configuration
@ -538,7 +536,7 @@ QtObject {
property Notification onlyPaidUsers: Notification { property Notification onlyPaidUsers: Notification {
description: qsTr("Bridge is exclusive to our paid plans. Upgrade your account to use Bridge.") description: qsTr("Bridge is exclusive to our paid plans. Upgrade your account to use Bridge.")
brief: description brief: qsTr("Upgrade your account")
icon: "./icons/ic-exclamation-circle-filled.svg" icon: "./icons/ic-exclamation-circle-filled.svg"
type: Notification.NotificationType.Danger type: Notification.NotificationType.Danger
group: Notifications.Group.Configuration group: Notifications.Group.Configuration
@ -562,7 +560,7 @@ QtObject {
property Notification alreadyLoggedIn: Notification { property Notification alreadyLoggedIn: Notification {
description: qsTr("This account is already signed in.") description: qsTr("This account is already signed in.")
brief: description brief: qsTr("Already signed in")
icon: "./icons/ic-exclamation-circle-filled.svg" icon: "./icons/ic-exclamation-circle-filled.svg"
type: Notification.NotificationType.Info type: Notification.NotificationType.Info
group: Notifications.Group.Configuration group: Notifications.Group.Configuration
@ -587,7 +585,7 @@ QtObject {
// Bug reports // Bug reports
property Notification bugReportSendSuccess: Notification { property Notification bugReportSendSuccess: Notification {
description: qsTr("Thank you for the report. We'll get back to you as soon as we can.") description: qsTr("Thank you for the report. We'll get back to you as soon as we can.")
brief: description brief: qsTr("Report sent")
icon: "./icons/ic-info-circle-filled.svg" icon: "./icons/ic-info-circle-filled.svg"
type: Notification.NotificationType.Success type: Notification.NotificationType.Success
group: Notifications.Group.Configuration group: Notifications.Group.Configuration
@ -611,7 +609,7 @@ QtObject {
property Notification bugReportSendError: Notification { property Notification bugReportSendError: Notification {
description: qsTr("Report could not be sent. Try again or email us directly.") description: qsTr("Report could not be sent. Try again or email us directly.")
brief: description brief: qsTr("Error sending report")
icon: "./icons/ic-exclamation-circle-filled.svg" icon: "./icons/ic-exclamation-circle-filled.svg"
type: Notification.NotificationType.Danger type: Notification.NotificationType.Danger
group: Notifications.Group.Configuration group: Notifications.Group.Configuration
@ -634,8 +632,8 @@ QtObject {
// Cache // Cache
property Notification cacheUnavailable: Notification { property Notification cacheUnavailable: Notification {
title: qsTr("Cache location is unavailable") title: qsTr("Cache location is unavailable")
description: qsTr("Check the directory or change it in your settings.") description: qsTr("The current cache location is unavailable. Check the directory or change it in your settings.")
brief: qsTr("The current cache location is unavailable. Check the directory or change it in your settings.") brief: title
icon: "./icons/ic-exclamation-circle-filled.svg" icon: "./icons/ic-exclamation-circle-filled.svg"
type: Notification.NotificationType.Warning type: Notification.NotificationType.Warning
group: Notifications.Group.Configuration | Notifications.Group.Dialogs group: Notifications.Group.Configuration | Notifications.Group.Dialogs
@ -725,7 +723,7 @@ QtObject {
// Other // Other
property Notification accountChanged: Notification { property Notification accountChanged: Notification {
description: qsTr("The address list for .... account has changed. You need to reconfigure your email client.") description: qsTr("The address list for .... account has changed. You need to reconfigure your email client.")
brief: qsTr("The address list for your account has changed. Reconfigure your email client.") brief: qsTr("Address list changed")
icon: "./icons/ic-exclamation-circle-filled.svg" icon: "./icons/ic-exclamation-circle-filled.svg"
type: Notification.NotificationType.Danger type: Notification.NotificationType.Danger
group: Notifications.Group.Configuration group: Notifications.Group.Configuration
@ -742,7 +740,7 @@ QtObject {
property Notification diskFull: Notification { property Notification diskFull: Notification {
title: qsTr("Your disk is almost full") title: qsTr("Your disk is almost full")
description: qsTr("Quit Bridge and free disk space or disable the local cache (not recommended).") description: qsTr("Quit Bridge and free disk space or disable the local cache (not recommended).")
brief: qsTr("Your disk is almost full. Free disk space or disable the local cache.") brief: title
icon: "./icons/ic-exclamation-circle-filled.svg" icon: "./icons/ic-exclamation-circle-filled.svg"
type: Notification.NotificationType.Warning type: Notification.NotificationType.Warning
group: Notifications.Group.Configuration | Notifications.Group.Dialogs group: Notifications.Group.Configuration | Notifications.Group.Dialogs
@ -948,8 +946,8 @@ QtObject {
property Notification noKeychain: Notification { property Notification noKeychain: Notification {
title: qsTr("No keychain available") title: qsTr("No keychain available")
description: qsTr("Bridge is not able to detect a supported password manager (pass or secret-service). Please install and setup supported password manager and restart the application.")
brief: title brief: title
description: qsTr("Bridge is not able to detect a supported password manager (pass or secret-service). Please install and setup supported password manager and restart the application.")
icon: "./icons/ic-exclamation-circle-filled.svg" icon: "./icons/ic-exclamation-circle-filled.svg"
type: Notification.NotificationType.Danger type: Notification.NotificationType.Danger
group: Notifications.Group.Dialogs | Notifications.Group.Configuration group: Notifications.Group.Dialogs | Notifications.Group.Configuration
@ -982,13 +980,13 @@ QtObject {
property Notification rebuildKeychain: Notification { property Notification rebuildKeychain: Notification {
title: qsTr("Your macOS keychain might be corrupted") title: qsTr("Your macOS keychain might be corrupted")
description: qsTr("Bridge is not able to access your macOS keychain. Please consult the instructions on our support page.")
brief: title brief: title
description: qsTr("Bridge is not able to access your macOS keychain. Please consult the instructions on our support page.")
icon: "./icons/ic-exclamation-circle-filled.svg" icon: "./icons/ic-exclamation-circle-filled.svg"
type: Notification.NotificationType.Danger type: Notification.NotificationType.Danger
group: Notifications.Group.Dialogs | Notifications.Group.Configuration group: Notifications.Group.Dialogs | Notifications.Group.Configuration
property var supportLink: "https://proton.me/support/mail" property var supportLink: "https://proton.me/support/bridge"
Connections { Connections {
@ -1014,8 +1012,8 @@ QtObject {
property Notification addressChanged: Notification { property Notification addressChanged: Notification {
title: qsTr("Address list changes") title: qsTr("Address list changes")
brief: title
description: qsTr("The address list for your account has changed. You might need to reconfigure your email client.") description: qsTr("The address list for your account has changed. You might need to reconfigure your email client.")
brief: description
icon: "./icons/ic-exclamation-circle-filled.svg" icon: "./icons/ic-exclamation-circle-filled.svg"
type: Notification.NotificationType.Warning type: Notification.NotificationType.Warning
group: Notifications.Group.Configuration group: Notifications.Group.Configuration
@ -1047,11 +1045,11 @@ QtObject {
property Notification apiCertIssue: Notification { property Notification apiCertIssue: Notification {
title: qsTr("Unable to establish a \nsecure connection to \nProton servers") title: qsTr("Unable to establish a \nsecure connection to \nProton servers")
brief: qsTr("Cannot establish secure connection")
description: qsTr("Bridge cannot verify the authenticity of Proton servers on your current network due to a TLS certificate error. " + description: qsTr("Bridge cannot verify the authenticity of Proton servers on your current network due to a TLS certificate error. " +
"Start Bridge again after ensuring your connection is secure and/or connecting to a VPN. Learn more about TLS pinning " + "Start Bridge again after ensuring your connection is secure and/or connecting to a VPN. Learn more about TLS pinning " +
"<a href=\"https://proton.me/blog/tls-ssl-certificate#Extra-security-precautions-taken-by-ProtonMail\">here</a>.") "<a href=\"https://proton.me/blog/tls-ssl-certificate#Extra-security-precautions-taken-by-ProtonMail\">here</a>.")
brief: title
icon: "./icons/ic-exclamation-circle-filled.svg" icon: "./icons/ic-exclamation-circle-filled.svg"
type: Notification.NotificationType.Danger type: Notification.NotificationType.Danger
group: Notifications.Group.Dialogs | Notifications.Group.Connection group: Notifications.Group.Dialogs | Notifications.Group.Connection
@ -1078,6 +1076,7 @@ QtObject {
property Notification noActiveKeyForRecipient: Notification { property Notification noActiveKeyForRecipient: Notification {
title: qsTr("Unable to send \nencrypted message") title: qsTr("Unable to send \nencrypted message")
brief: title
description: "#PlaceholderText#" description: "#PlaceholderText#"
icon: "./icons/ic-exclamation-circle-filled.svg" icon: "./icons/ic-exclamation-circle-filled.svg"
type: Notification.NotificationType.Danger type: Notification.NotificationType.Danger
@ -1174,8 +1173,9 @@ QtObject {
} }
property Notification genericError: Notification { property Notification genericError: Notification {
title: "#PlaceholderText#" title: ""
description: "#PlaceholderText#" brief: title
description: ""
icon: "./icons/ic-exclamation-circle-filled.svg" icon: "./icons/ic-exclamation-circle-filled.svg"
type: Notification.NotificationType.Danger type: Notification.NotificationType.Danger
group: Notifications.Group.Dialogs group: Notifications.Group.Dialogs
@ -1201,7 +1201,7 @@ QtObject {
property Notification genericQuestion: Notification { property Notification genericQuestion: Notification {
title: "" title: ""
brief: "" brief: title
description: "" description: ""
type: Notification.NotificationType.Warning type: Notification.NotificationType.Warning
group: Notifications.Group.Dialogs group: Notifications.Group.Dialogs

View File

@ -1,352 +0,0 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQml
import QtQuick
import QtQuick.Window
import QtQuick.Layouts
import QtQuick.Controls
import Proton
import Notifications
Window {
id: root
height: contentLayout.implicitHeight
width: contentLayout.implicitWidth
flags: (Qt.platform.os === "linux" ? Qt.Tool : 0) | Qt.FramelessWindowHint | Qt.NoDropShadowWindowHint | Qt.WindowStaysOnTopHint | Qt.WA_TranslucentBackground
color: "transparent"
property ColorScheme colorScheme: ProtonStyle.currentStyle
property var notifications
signal showMainWindow()
signal showHelp()
signal showSettings()
signal selectUser(string userID)
signal quit()
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
}
function enableHoverOnOpenBridgeButton() {
openBridgeButton.hoverEnabled = true
mouseArea.positionChanged.disconnect(enableHoverOnOpenBridgeButton)
}
onVisibleChanged: {
if (visible) { // GODT-1479 To avoid a visual glitch where the 'Open bridge button' would appear hovered when the status windows opens,
// we've disabled hover on it when it was last closed. Re-enabling hover here will not work on all platforms. so we temporarily connect
// mouse move event over the window's mouseArea to a function that will re-enable hover on the open bridge button.
openBridgeButton.focus = false
mouseArea.positionChanged.connect(enableHoverOnOpenBridgeButton)
} else {
menu.close()
}
}
ColumnLayout {
id: contentLayout
Layout.minimumHeight: 201
anchors.fill: parent
spacing: 0
ColumnLayout {
Layout.minimumWidth: 448
Layout.fillWidth: true
spacing: 0
Item {
implicitHeight: 12
Layout.fillWidth: true
clip: true
Rectangle {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
height: parent.height * 2
radius: ProtonStyle.dialog_radius
color: {
if (!statusItem.activeNotification) {
return root.colorScheme.signal_success
}
switch (statusItem.activeNotification.type) {
case Notification.NotificationType.Danger:
return root.colorScheme.signal_danger
case Notification.NotificationType.Warning:
return root.colorScheme.signal_warning
case Notification.NotificationType.Success:
return root.colorScheme.signal_success
case Notification.NotificationType.Info:
return root.colorScheme.signal_info
}
}
}
}
Rectangle {
Layout.fillWidth: true
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
color: colorScheme.background_norm
RowLayout {
anchors.fill: parent
anchors.topMargin: 8
anchors.bottomMargin: 8
anchors.leftMargin: 24
anchors.rightMargin: 24
spacing: 8
Status {
id: statusItem
Layout.fillWidth: true
Layout.topMargin: 12
Layout.bottomMargin: 12
colorScheme: root.colorScheme
notifications: root.notifications
notificationWhitelist: Notifications.Group.Connection | Notifications.Group.Update | Notifications.Group.Configuration
}
Button {
colorScheme: root.colorScheme
secondary: true
Layout.topMargin: 12
Layout.bottomMargin: 12
visible: statusItem.activeNotification && statusItem.activeNotification.action.length > 0
action: statusItem.activeNotification && statusItem.activeNotification.action.length > 0 ? statusItem.activeNotification.action[0] : null
}
}
}
Rectangle {
Layout.fillWidth: true
height: 1
color: root.colorScheme.background_norm
Rectangle {
anchors.fill: parent
anchors.leftMargin: 24
anchors.rightMargin: 24
color: root.colorScheme.border_norm
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.maximumHeight: accountListView.count ?
accountListView.contentHeight / accountListView.count * 3 + accountListView.anchors.topMargin + accountListView.anchors.bottomMargin :
Number.POSITIVE_INFINITY
color: root.colorScheme.background_norm
clip: true
implicitHeight: children[0].contentHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
implicitWidth: children[0].contentWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
ListView {
id: accountListView
model: Backend.users
anchors.fill: parent
anchors.topMargin: 8
anchors.bottomMargin: 8
anchors.leftMargin: 24
anchors.rightMargin: 24
interactive: contentHeight > parent.height
snapMode: ListView.SnapToItem
boundsBehavior: Flickable.StopAtBounds
spacing: 4
delegate: Item {
id: viewItem
width: ListView.view.width
implicitHeight: children[0].implicitHeight
implicitWidth: children[0].implicitWidth
property var user: Backend.users.get(index)
RowLayout {
spacing: 0
anchors.fill: parent
AccountDelegate {
Layout.fillWidth: true
Layout.topMargin: 12
Layout.bottomMargin: 12
user: viewItem.user
colorScheme: root.colorScheme
}
Button {
Layout.topMargin: 12
Layout.bottomMargin: 12
colorScheme: root.colorScheme
visible: viewItem.user ? (viewItem.user.state === EUserState.SignedOut) : false
text: qsTr("Sign in")
onClicked: {
root.selectUser(viewItem.user.id) // selectUser will show login screen if user is in SignedOut state.
root.close()
}
}
}
}
}
}
Item {
Layout.fillWidth: true
implicitHeight: children[1].implicitHeight + children[1].anchors.topMargin + children[1].anchors.bottomMargin
implicitWidth: children[1].implicitWidth + children[1].anchors.leftMargin + children[1].anchors.rightMargin
// background:
clip: true
Rectangle {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
height: parent.height * 2
radius: ProtonStyle.dialog_radius
color: root.colorScheme.background_weak
}
RowLayout {
anchors.fill: parent
anchors.margins: 8
spacing: 0
Button {
id: openBridgeButton
colorScheme: root.colorScheme
secondary: true
text: qsTr("Open Bridge")
borderless: true
labelType: Label.LabelType.Caption_semibold
onClicked: {
// GODT-1479: we disable hover for the button to avoid a visual glitch where the button is
// wrongly hovered when re-opening the status window after clicking
hoverEnabled = false;
root.showMainWindow()
root.close()
}
}
Item {
Layout.fillWidth: true
}
Button {
colorScheme: root.colorScheme
secondary: true
icon.source: "/qml/icons/ic-three-dots-vertical.svg"
borderless: true
checkable: true
onClicked: {
menu.open()
}
Menu {
id: menu
colorScheme: root.colorScheme
modal: true
y: 0 - height
MenuItem {
colorScheme: root.colorScheme
text: qsTr("Help")
onClicked: {
root.showHelp()
root.close()
}
}
MenuItem {
colorScheme: root.colorScheme
text: qsTr("Settings")
onClicked: {
root.showSettings()
root.close()
}
}
MenuItem {
colorScheme: root.colorScheme
text: qsTr("Quit Bridge")
onClicked: {
root.close()
root.quit()
}
}
onClosed: {
parent.checked = false
}
onOpened: {
parent.checked = true
}
}
}
}
}
}
onActiveChanged: {
if (!active) root.close()
}
function showAndRise() {
root.show()
root.raise()
if (!root.active) {
root.requestActivate()
}
}
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.533333,0,0,0.533333,238.933,238.933)">
<circle cx="512" cy="512" r="480" style="fill:white;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 593 B

View File

@ -160,3 +160,41 @@ target_link_libraries(bridgepp
) )
target_precompile_headers(bridgepp PRIVATE Pch.h) target_precompile_headers(bridgepp PRIVATE Pch.h)
#*****************************************************************************************************************************************************
# GoogleTest
#*****************************************************************************************************************************************************
if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.24.0")
cmake_policy(SET CMP0135 NEW) # avoid warning DOWNLOAD_EXTRACT_TIMESTAMP
endif()
include(FetchContent)
FetchContent_Declare(
googletest
URL https://github.com/google/googletest/archive/b796f7d44681514f58a683a3a71ff17c94edb0c1.zip
)
# For Windows: Prevent overriding the parent project's compiler/linker settings
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)
enable_testing()
#*****************************************************************************************************************************************************
# Tests
#*****************************************************************************************************************************************************
add_executable(bridgepp-test
Test/TestBridgeUtils.cpp
Test/TestException.cpp
Test/TestWorker.cpp Test/TestWorker.h)
add_dependencies(bridgepp-test bridgepp)
target_precompile_headers(bridgepp-test PRIVATE Pch.h)
target_link_libraries(bridgepp-test
GTest::gtest_main
bridgepp
)
include(GoogleTest)
gtest_discover_tests(bridgepp-test)

View File

@ -0,0 +1,111 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
#include <gtest/gtest.h>
#include <bridgepp/BridgeUtils.h>
using namespace bridgepp;
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
TEST(BridgeUtils, OS) {
#ifdef Q_OS_MACOS
EXPECT_EQ(os(), OS::MacOS);
EXPECT_FALSE(onLinux());
EXPECT_TRUE(onMacOS());
EXPECT_FALSE(onWindows());
EXPECT_EQ(goos(), "darwin");
return;
#endif
#ifdef Q_OS_WIN
EXPECT_EQ(os(), OS::Windows);
EXPECT_FALSE(onLinux());
EXPECT_FALSE(onMacOS());
EXPECT_TRUE(onWindows());
EXPECT_EQ(goos(), "windows");
return;
#endif
#ifdef Q_OS_LINUX
EXPECT_EQ(os(), OS::Linux);
EXPECT_TRUE(onLinux());
EXPECT_FALSE(onMacOS());
EXPECT_FALSE(onWindows());
EXPECT_EQ(goos(), "linux");
return;
#endif
EXPECT_TRUE(false); // should be unreachable.
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
TEST(BridgeUtils, UserFolders) {
typedef QString (*dirFunction)();
QList<dirFunction> functions = { userConfigDir, userCacheDir, userDataDir, sentryCacheDir };
QString path;
for (dirFunction f: functions) {
EXPECT_NO_THROW(path = f());
EXPECT_FALSE(path.isEmpty());
EXPECT_TRUE(QDir(path).exists());
}
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
TEST(BridgeUtils, Random) {
qint32 repeatCount = 1000;
qint32 const maxValue = 5;
for (qint32 i = 0; i < repeatCount; ++i) {
qint64 n = 0;
EXPECT_NO_THROW(n = randN(maxValue));
EXPECT_TRUE((n >= 0) && (n < maxValue));
QString name;
EXPECT_NO_THROW(name = randomFirstName());
EXPECT_FALSE(name.isEmpty());
EXPECT_NO_THROW(name = randomLastName());
EXPECT_FALSE(name.isEmpty());
EXPECT_NO_THROW(randomUser());
}
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
TEST(BridgeUtils, ElideLongString) {
std::function const test = [](QString const &input, qint32 maxLength, QString const &expected) -> bool {
QString output;
EXPECT_NO_THROW(output = elideLongString(input, maxLength));
return output == expected;
};
EXPECT_TRUE(test( "", 0, ""));
EXPECT_TRUE(test("1234", 4, "1234"));
EXPECT_TRUE(test("123", 2, "..."));
EXPECT_TRUE(test("1234567890", 8, "12...90"));
EXPECT_TRUE(test("1234567890", 10, "1234567890"));
EXPECT_TRUE(test("1234567890", 100, "1234567890"));
}

View File

@ -0,0 +1,95 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
#include <bridgepp/Exception/Exception.h>
#include <gtest/gtest.h>
using namespace bridgepp;
namespace {
QString const testQWhat = "What";
QString const testDetails = "Some details";
QString const testFunction = "function";
QByteArray const testAttachment = QString("Some data").toLocal8Bit();
Exception const testException(testQWhat, testDetails, testFunction, testAttachment);
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
TEST(Exceptions, ExceptionConstructor) {
// Default exception
Exception const emptyException;
EXPECT_TRUE(emptyException.qwhat().isEmpty());
EXPECT_EQ(strlen(emptyException.what()), 0);
EXPECT_EQ(emptyException.attachment().size(), 0);
EXPECT_TRUE(emptyException.details().isEmpty());
EXPECT_TRUE(emptyException.detailedWhat().isEmpty());
// Fully detailed exception
EXPECT_EQ(testException.qwhat(), testQWhat);
EXPECT_EQ(QString::fromLocal8Bit(testException.what()), testQWhat);
EXPECT_EQ(testException.details(), testDetails);
EXPECT_EQ(testException.attachment(), testAttachment);
QString const detailed = testException.detailedWhat();
EXPECT_TRUE(detailed.contains(testQWhat));
EXPECT_TRUE(detailed.contains(testFunction));
EXPECT_TRUE(detailed.contains(testDetails));
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
TEST(Exceptions, ExceptionCopyMoveConstructors) {
Exception const e(testQWhat, testDetails, testFunction, testAttachment);
// Check copy-constructor
Exception eCopied(e);
EXPECT_EQ(eCopied.qwhat(), testQWhat);
EXPECT_EQ(eCopied.details(), testDetails);
EXPECT_EQ(eCopied.function(), testFunction);
EXPECT_EQ(eCopied.attachment(), testAttachment);
// Check move-constructor
Exception eMoved(std::move(eCopied));
EXPECT_EQ(eMoved.qwhat(), testQWhat);
EXPECT_EQ(eMoved.details(), testDetails);
EXPECT_EQ(eMoved.function(), testFunction);
EXPECT_EQ(eMoved.attachment(), testAttachment);
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
TEST(Exceptions, ExceptionThrow) {
std::function t = []() { throw testException; };
EXPECT_THROW(t(), Exception);
EXPECT_THROW(t(), std::exception);
bool caught = false;
try {
t();
} catch (Exception const &e) {
caught = true;
EXPECT_EQ(e.detailedWhat(), testException.detailedWhat());
}
EXPECT_TRUE(caught);
}

View File

@ -0,0 +1,215 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
// clazy:excludeall=lambda-in-connect
#include "TestWorker.h"
#include <bridgepp/Worker/Overseer.h>
#include <bridgepp/Exception/Exception.h>
using namespace bridgepp;
namespace {
qint32 dummyArgc = 1; ///< A dummy int value because QCoreApplication constructor requires a reference to it.
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
Workers::Workers()
: testing::Test()
, app_(dummyArgc, nullptr) {
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void Workers::SetUp() {
Test::SetUp();
EXPECT_NO_THROW(worker_ = new TestWorker);
QObject::connect(worker_, &TestWorker::started, [&]() { results_.started = true; });
QObject::connect(worker_, &TestWorker::finished, [&]() { results_.finished = true; });
QObject::connect(worker_, &TestWorker::finished, &loop_, &QEventLoop::quit);
QObject::connect(worker_, &TestWorker::error, [&] { results_.error = true; });
QObject::connect(worker_, &TestWorker::error, &loop_, &QEventLoop::quit);
QObject::connect(worker_, &TestWorker::error, [&] { results_.error = true; });
QObject::connect(worker_, &TestWorker::error, &loop_, &QEventLoop::quit);
QObject::connect(worker_, &TestWorker::cancelled, [&] { results_.cancelled = true; });
QObject::connect(worker_, &TestWorker::cancelled, &loop_, &QEventLoop::quit);
overseer_ = std::make_unique<Overseer>(worker_, nullptr);
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void Workers::TearDown() {
EXPECT_NO_FATAL_FAILURE(overseer_.reset());
Test::TearDown();
}
//****************************************************************************************************************************************************
/// \param[in] lifetimeMs The lifetime of the worker in milliseconds.
/// \param[in] willSucceed Will the worker succeed (emit finished) or fail (emit error).
//****************************************************************************************************************************************************
TestWorker::TestWorker()
: Worker(nullptr) {
}
//****************************************************************************************************************************************************
/// \param[in] lifetimeMs The lifetime of the worker in milliseconds.
//****************************************************************************************************************************************************
void TestWorker::setLifetime(qint64 lifetimeMs) {
lifetimeMs_ = lifetimeMs;
}
//****************************************************************************************************************************************************
/// \param[in] willSucceed Will the worker succeed?
//****************************************************************************************************************************************************
void TestWorker::setWillSucceed(bool willSucceed) {
willSucceed_ = willSucceed;
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void TestWorker::run() {
emit started();
QElapsedTimer timer;
timer.start();
while (true) {
if (cancelled_.loadRelaxed()) {
emit cancelled();
return;
}
if (timer.elapsed() >= lifetimeMs_) {
break;
}
}
if (willSucceed_) {
emit finished();
} else {
emit error(QString());
}
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void TestWorker::cancel() {
cancelled_.storeRelaxed(1);
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
TEST_F(Workers, SuccessfulWorker) {
worker_->setLifetime(10);
worker_->setWillSucceed(true);
EXPECT_NO_THROW(overseer_->startWorker(false));
EXPECT_NO_THROW(loop_.exec());
EXPECT_TRUE(results_.started);
EXPECT_TRUE(results_.finished);
EXPECT_FALSE(results_.error);
EXPECT_FALSE(results_.cancelled);
EXPECT_TRUE(overseer_->worker() != nullptr); // overseer started without autorelease.
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
TEST_F(Workers, ErrorWorker) {
worker_->setLifetime(10);
worker_->setWillSucceed(false);
EXPECT_NO_THROW(overseer_->startWorker(true));
EXPECT_NO_THROW(loop_.exec());
EXPECT_TRUE(results_.started);
EXPECT_FALSE(results_.finished);
EXPECT_TRUE(results_.error);
EXPECT_FALSE(results_.cancelled);
EXPECT_TRUE(overseer_->worker() == nullptr); // overseer started with autorelease.
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
TEST_F(Workers, CancelledWorker) {
worker_->setLifetime(10000);
worker_->setWillSucceed(true);
EXPECT_NO_THROW(overseer_->startWorker(false));
EXPECT_NO_THROW(QTimer::singleShot(10, [&]() { worker_->cancel(); }));
EXPECT_NO_THROW(loop_.exec());
EXPECT_TRUE(results_.started);
EXPECT_FALSE(results_.finished);
EXPECT_FALSE(results_.error);
EXPECT_TRUE(results_.cancelled);
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
TEST_F(Workers, Wait) {
worker_->setLifetime(10000);
worker_->setWillSucceed(true);
overseer_->startWorker(true);
bool isFinished = false;
EXPECT_NO_THROW(isFinished = overseer_->isFinished());
EXPECT_FALSE(isFinished);
EXPECT_NO_THROW(isFinished = overseer_->wait(10));
EXPECT_FALSE(isFinished);
worker_->cancel();
EXPECT_NO_THROW(isFinished = overseer_->wait(10000));
EXPECT_TRUE(isFinished);
EXPECT_NO_THROW(isFinished = overseer_->isFinished());
EXPECT_TRUE(isFinished);
}

View File

@ -0,0 +1,88 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
#ifndef BRIDGE_GUI_TEST_WORKER_H
#define BRIDGE_GUI_TEST_WORKER_H
#include <bridgepp/Worker/Overseer.h>
#include <gtest/gtest.h>
//****************************************************************************************************************************************************
/// \brief Test worker class.
///
/// This worker simply waits:
/// - For a specified amount of time and will succeed (emit finished()) or fail (emit error()) based on its parameters.
/// - to be cancelled (and will emit cancelled in that case).
//****************************************************************************************************************************************************
class TestWorker : public bridgepp::Worker {
Q_OBJECT
public: // member functions.
TestWorker(); ///< Default constructor.
TestWorker(TestWorker const &) = delete; ///< Disabled copy-constructor.
TestWorker(TestWorker &&) = delete; ///< Disabled assignment copy-constructor.
~TestWorker() override = default; ///< Destructor.
TestWorker &operator=(TestWorker const &) = delete; ///< Disabled assignment operator.
TestWorker &operator=(TestWorker &&) = delete; ///< Disabled move assignment operator.
void setLifetime(qint64 lifetimeMs); ///< Set the lifetime of the worker.
void setWillSucceed(bool willSucceed); ///< Set if the worker will succeed.
void run() override; ///< Run the worker.
void cancel(); ///< Cancel the worker.
private: // data members
qint64 lifetimeMs_ { 10 }; ///< The lifetime of the worker in milliseconds.
bool willSucceed_ { true }; ///< Will the worker succeed?
QAtomicInteger<char> cancelled_; ///< Has the worker been cancelled.
};
//****************************************************************************************************************************************************
/// \brief Fixture class for worker tests.
//****************************************************************************************************************************************************
class Workers : public testing::Test {
public: // member functions.
Workers(); ///< Default constructor.
Workers(Workers const &) = delete; ///< Disabled copy-constructor.
Workers(Workers &&) = delete; ///< Disabled assignment copy-constructor.
~Workers() = default; ///< Destructor.
Workers &operator=(Workers const &) = delete; ///< Disabled assignment operator.
Workers &operator=(Workers &&) = delete; ///< Disabled move assignment operator.
protected: // member functions.
void SetUp() override; ///< Setup the fixture.
void TearDown() override; ///< Tear down the fixture.
protected: // data type
struct Results {
bool started { false };
bool finished { false };
bool error { false };
bool cancelled { false };
}; ///< Test results data type
protected: // data members
QCoreApplication app_; ///< The Qt application required for event loop.
bridgepp::UPOverseer overseer_; ///< The overseer for the worker.
TestWorker *worker_ { nullptr }; ///< The worker.
QEventLoop loop_; ///< The event loop.
Results results_; ///< The test results.
};
#endif //BRIDGE_GUI_TEST_WORKER_H

View File

@ -20,7 +20,7 @@
#define BRIDGE_PP_TESTER_BRIDGE_UTILS_H #define BRIDGE_PP_TESTER_BRIDGE_UTILS_H
#include <bridgepp/User/User.h> #include "User/User.h"
namespace bridgepp { namespace bridgepp {

View File

@ -87,6 +87,14 @@ QString Exception::details() const noexcept {
} }
//****************************************************************************************************************************************************
/// \return The function that threw the exception.
//****************************************************************************************************************************************************
QString Exception::function() const noexcept {
return function_;
}
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \return The attachment for the exception. /// \return The attachment for the exception.
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
@ -109,4 +117,5 @@ QString Exception::detailedWhat() const {
return result; return result;
} }
} // namespace bridgepp } // namespace bridgepp

View File

@ -42,6 +42,7 @@ public: // member functions
QString qwhat() const noexcept; ///< Return the description of the exception as a QString QString qwhat() const noexcept; ///< Return the description of the exception as a QString
const char *what() const noexcept override; ///< Return the description of the exception as C style string const char *what() const noexcept override; ///< Return the description of the exception as C style string
QString details() const noexcept; ///< Return the details for the exception QString details() const noexcept; ///< Return the details for the exception
QString function() const noexcept; ///< Return the function that threw the exception.
QByteArray attachment() const noexcept; ///< Return the attachment for the exception. QByteArray attachment() const noexcept; ///< Return the attachment for the exception.
QString detailedWhat() const; ///< Return the detailed description of the message (i.e. including the function name and the details). QString detailedWhat() const; ///< Return the detailed description of the message (i.e. including the function name and the details).

View File

@ -19,7 +19,6 @@
#include "GRPCClient.h" #include "GRPCClient.h"
#include "GRPCUtils.h" #include "GRPCUtils.h"
#include "GRPCErrors.h" #include "GRPCErrors.h"
#include "../BridgeUtils.h"
#include "../Exception/Exception.h" #include "../Exception/Exception.h"
#include "../ProcessMonitor.h" #include "../ProcessMonitor.h"
#include "../Log/LogUtils.h" #include "../Log/LogUtils.h"
@ -295,6 +294,24 @@ grpc::Status GRPCClient::isAllMailVisible(bool &outIsVisible) {
} }
//****************************************************************************************************************************************************
/// \param[out] outIsDisabled The value for the property
/// \return The status for the gRPC call.
//****************************************************************************************************************************************************
grpc::Status GRPCClient::isTelemetryDisabled(bool &outIsDisabled) {
return this->logGRPCCallStatus(this->getBool(&Bridge::Stub::IsTelemetryDisabled, outIsDisabled), __FUNCTION__);
}
//****************************************************************************************************************************************************
/// \param[out] isDisabled The new value for the property
/// \return The status for the gRPC call.
//****************************************************************************************************************************************************
grpc::Status GRPCClient::setIsTelemetryDisabled(bool isDisabled) {
return this->logGRPCCallStatus(this->setBool(&Bridge::Stub::SetIsTelemetryDisabled, isDisabled), __FUNCTION__);
}
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \param[in] isVisible The new value for the property. /// \param[in] isVisible The new value for the property.
/// \return The status for the gRPC call. /// \return The status for the gRPC call.

View File

@ -71,6 +71,8 @@ public: // member functions.
grpc::Status setIsBetaEnabled(bool enabled); ///< Performs the 'setIsBetaEnabled' gRPC call. grpc::Status setIsBetaEnabled(bool enabled); ///< Performs the 'setIsBetaEnabled' gRPC call.
grpc::Status isAllMailVisible(bool &outIsVisible); ///< Performs the "isAllMailVisible" gRPC call. grpc::Status isAllMailVisible(bool &outIsVisible); ///< Performs the "isAllMailVisible" gRPC call.
grpc::Status setIsAllMailVisible(bool isVisible); ///< Performs the 'setIsAllMailVisible' gRPC call. grpc::Status setIsAllMailVisible(bool isVisible); ///< Performs the 'setIsAllMailVisible' gRPC call.
grpc::Status isTelemetryDisabled(bool &outIsDisabled); ///< Performs the 'setIsTelemetryDisabled' gRPC call.
grpc::Status setIsTelemetryDisabled(bool isDisabled); ///< Performs the 'isTelemetryDisabled' gRPC call.
grpc::Status colorSchemeName(QString &outName); ///< Performs the "colorSchemeName' gRPC call. grpc::Status colorSchemeName(QString &outName); ///< Performs the "colorSchemeName' gRPC call.
grpc::Status setColorSchemeName(QString const &name); ///< Performs the "setColorSchemeName' gRPC call. grpc::Status setColorSchemeName(QString const &name); ///< Performs the "setColorSchemeName' gRPC call.
grpc::Status currentEmailClient(QString &outName); ///< Performs the 'currentEmailClient' gRPC call. grpc::Status currentEmailClient(QString &outName); ///< Performs the 'currentEmailClient' gRPC call.

View File

@ -34,6 +34,8 @@ static const char* Bridge_method_names[] = {
"/grpc.Bridge/IsBetaEnabled", "/grpc.Bridge/IsBetaEnabled",
"/grpc.Bridge/SetIsAllMailVisible", "/grpc.Bridge/SetIsAllMailVisible",
"/grpc.Bridge/IsAllMailVisible", "/grpc.Bridge/IsAllMailVisible",
"/grpc.Bridge/SetIsTelemetryDisabled",
"/grpc.Bridge/IsTelemetryDisabled",
"/grpc.Bridge/GoOs", "/grpc.Bridge/GoOs",
"/grpc.Bridge/TriggerReset", "/grpc.Bridge/TriggerReset",
"/grpc.Bridge/Version", "/grpc.Bridge/Version",
@ -98,49 +100,51 @@ Bridge::Stub::Stub(const std::shared_ptr< ::grpc::ChannelInterface>& channel, co
, rpcmethod_IsBetaEnabled_(Bridge_method_names[9], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel) , rpcmethod_IsBetaEnabled_(Bridge_method_names[9], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel)
, rpcmethod_SetIsAllMailVisible_(Bridge_method_names[10], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel) , rpcmethod_SetIsAllMailVisible_(Bridge_method_names[10], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel)
, rpcmethod_IsAllMailVisible_(Bridge_method_names[11], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel) , rpcmethod_IsAllMailVisible_(Bridge_method_names[11], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel)
, rpcmethod_GoOs_(Bridge_method_names[12], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel) , rpcmethod_SetIsTelemetryDisabled_(Bridge_method_names[12], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel)
, rpcmethod_TriggerReset_(Bridge_method_names[13], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel) , rpcmethod_IsTelemetryDisabled_(Bridge_method_names[13], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel)
, rpcmethod_Version_(Bridge_method_names[14], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel) , rpcmethod_GoOs_(Bridge_method_names[14], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel)
, rpcmethod_LogsPath_(Bridge_method_names[15], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel) , rpcmethod_TriggerReset_(Bridge_method_names[15], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel)
, rpcmethod_LicensePath_(Bridge_method_names[16], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel) , rpcmethod_Version_(Bridge_method_names[16], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel)
, rpcmethod_ReleaseNotesPageLink_(Bridge_method_names[17], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel) , rpcmethod_LogsPath_(Bridge_method_names[17], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel)
, rpcmethod_DependencyLicensesLink_(Bridge_method_names[18], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel) , rpcmethod_LicensePath_(Bridge_method_names[18], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel)
, rpcmethod_LandingPageLink_(Bridge_method_names[19], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel) , rpcmethod_ReleaseNotesPageLink_(Bridge_method_names[19], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel)
, rpcmethod_SetColorSchemeName_(Bridge_method_names[20], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel) , rpcmethod_DependencyLicensesLink_(Bridge_method_names[20], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel)
, rpcmethod_ColorSchemeName_(Bridge_method_names[21], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel) , rpcmethod_LandingPageLink_(Bridge_method_names[21], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel)
, rpcmethod_CurrentEmailClient_(Bridge_method_names[22], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel) , rpcmethod_SetColorSchemeName_(Bridge_method_names[22], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel)
, rpcmethod_ReportBug_(Bridge_method_names[23], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel) , rpcmethod_ColorSchemeName_(Bridge_method_names[23], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel)
, rpcmethod_ExportTLSCertificates_(Bridge_method_names[24], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel) , rpcmethod_CurrentEmailClient_(Bridge_method_names[24], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel)
, rpcmethod_ForceLauncher_(Bridge_method_names[25], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel) , rpcmethod_ReportBug_(Bridge_method_names[25], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel)
, rpcmethod_SetMainExecutable_(Bridge_method_names[26], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel) , rpcmethod_ExportTLSCertificates_(Bridge_method_names[26], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel)
, rpcmethod_Login_(Bridge_method_names[27], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel) , rpcmethod_ForceLauncher_(Bridge_method_names[27], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel)
, rpcmethod_Login2FA_(Bridge_method_names[28], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel) , rpcmethod_SetMainExecutable_(Bridge_method_names[28], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel)
, rpcmethod_Login2Passwords_(Bridge_method_names[29], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel) , rpcmethod_Login_(Bridge_method_names[29], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel)
, rpcmethod_LoginAbort_(Bridge_method_names[30], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel) , rpcmethod_Login2FA_(Bridge_method_names[30], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel)
, rpcmethod_CheckUpdate_(Bridge_method_names[31], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel) , rpcmethod_Login2Passwords_(Bridge_method_names[31], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel)
, rpcmethod_InstallUpdate_(Bridge_method_names[32], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel) , rpcmethod_LoginAbort_(Bridge_method_names[32], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel)
, rpcmethod_SetIsAutomaticUpdateOn_(Bridge_method_names[33], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel) , rpcmethod_CheckUpdate_(Bridge_method_names[33], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel)
, rpcmethod_IsAutomaticUpdateOn_(Bridge_method_names[34], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel) , rpcmethod_InstallUpdate_(Bridge_method_names[34], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel)
, rpcmethod_DiskCachePath_(Bridge_method_names[35], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel) , rpcmethod_SetIsAutomaticUpdateOn_(Bridge_method_names[35], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel)
, rpcmethod_SetDiskCachePath_(Bridge_method_names[36], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel) , rpcmethod_IsAutomaticUpdateOn_(Bridge_method_names[36], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel)
, rpcmethod_SetIsDoHEnabled_(Bridge_method_names[37], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel) , rpcmethod_DiskCachePath_(Bridge_method_names[37], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel)
, rpcmethod_IsDoHEnabled_(Bridge_method_names[38], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel) , rpcmethod_SetDiskCachePath_(Bridge_method_names[38], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel)
, rpcmethod_MailServerSettings_(Bridge_method_names[39], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel) , rpcmethod_SetIsDoHEnabled_(Bridge_method_names[39], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel)
, rpcmethod_SetMailServerSettings_(Bridge_method_names[40], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel) , rpcmethod_IsDoHEnabled_(Bridge_method_names[40], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel)
, rpcmethod_Hostname_(Bridge_method_names[41], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel) , rpcmethod_MailServerSettings_(Bridge_method_names[41], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel)
, rpcmethod_IsPortFree_(Bridge_method_names[42], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel) , rpcmethod_SetMailServerSettings_(Bridge_method_names[42], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel)
, rpcmethod_AvailableKeychains_(Bridge_method_names[43], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel) , rpcmethod_Hostname_(Bridge_method_names[43], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel)
, rpcmethod_SetCurrentKeychain_(Bridge_method_names[44], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel) , rpcmethod_IsPortFree_(Bridge_method_names[44], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel)
, rpcmethod_CurrentKeychain_(Bridge_method_names[45], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel) , rpcmethod_AvailableKeychains_(Bridge_method_names[45], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel)
, rpcmethod_GetUserList_(Bridge_method_names[46], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel) , rpcmethod_SetCurrentKeychain_(Bridge_method_names[46], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel)
, rpcmethod_GetUser_(Bridge_method_names[47], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel) , rpcmethod_CurrentKeychain_(Bridge_method_names[47], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel)
, rpcmethod_SetUserSplitMode_(Bridge_method_names[48], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel) , rpcmethod_GetUserList_(Bridge_method_names[48], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel)
, rpcmethod_SendBadEventUserFeedback_(Bridge_method_names[49], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel) , rpcmethod_GetUser_(Bridge_method_names[49], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel)
, rpcmethod_LogoutUser_(Bridge_method_names[50], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel) , rpcmethod_SetUserSplitMode_(Bridge_method_names[50], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel)
, rpcmethod_RemoveUser_(Bridge_method_names[51], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel) , rpcmethod_SendBadEventUserFeedback_(Bridge_method_names[51], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel)
, rpcmethod_ConfigureUserAppleMail_(Bridge_method_names[52], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel) , rpcmethod_LogoutUser_(Bridge_method_names[52], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel)
, rpcmethod_RunEventStream_(Bridge_method_names[53], options.suffix_for_stats(),::grpc::internal::RpcMethod::SERVER_STREAMING, channel) , rpcmethod_RemoveUser_(Bridge_method_names[53], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel)
, rpcmethod_StopEventStream_(Bridge_method_names[54], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel) , rpcmethod_ConfigureUserAppleMail_(Bridge_method_names[54], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel)
, rpcmethod_RunEventStream_(Bridge_method_names[55], options.suffix_for_stats(),::grpc::internal::RpcMethod::SERVER_STREAMING, channel)
, rpcmethod_StopEventStream_(Bridge_method_names[56], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel)
{} {}
::grpc::Status Bridge::Stub::CheckTokens(::grpc::ClientContext* context, const ::google::protobuf::StringValue& request, ::google::protobuf::StringValue* response) { ::grpc::Status Bridge::Stub::CheckTokens(::grpc::ClientContext* context, const ::google::protobuf::StringValue& request, ::google::protobuf::StringValue* response) {
@ -419,6 +423,52 @@ void Bridge::Stub::async::IsAllMailVisible(::grpc::ClientContext* context, const
return result; return result;
} }
::grpc::Status Bridge::Stub::SetIsTelemetryDisabled(::grpc::ClientContext* context, const ::google::protobuf::BoolValue& request, ::google::protobuf::Empty* response) {
return ::grpc::internal::BlockingUnaryCall< ::google::protobuf::BoolValue, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(channel_.get(), rpcmethod_SetIsTelemetryDisabled_, context, request, response);
}
void Bridge::Stub::async::SetIsTelemetryDisabled(::grpc::ClientContext* context, const ::google::protobuf::BoolValue* request, ::google::protobuf::Empty* response, std::function<void(::grpc::Status)> f) {
::grpc::internal::CallbackUnaryCall< ::google::protobuf::BoolValue, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(stub_->channel_.get(), stub_->rpcmethod_SetIsTelemetryDisabled_, context, request, response, std::move(f));
}
void Bridge::Stub::async::SetIsTelemetryDisabled(::grpc::ClientContext* context, const ::google::protobuf::BoolValue* request, ::google::protobuf::Empty* response, ::grpc::ClientUnaryReactor* reactor) {
::grpc::internal::ClientCallbackUnaryFactory::Create< ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(stub_->channel_.get(), stub_->rpcmethod_SetIsTelemetryDisabled_, context, request, response, reactor);
}
::grpc::ClientAsyncResponseReader< ::google::protobuf::Empty>* Bridge::Stub::PrepareAsyncSetIsTelemetryDisabledRaw(::grpc::ClientContext* context, const ::google::protobuf::BoolValue& request, ::grpc::CompletionQueue* cq) {
return ::grpc::internal::ClientAsyncResponseReaderHelper::Create< ::google::protobuf::Empty, ::google::protobuf::BoolValue, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(channel_.get(), cq, rpcmethod_SetIsTelemetryDisabled_, context, request);
}
::grpc::ClientAsyncResponseReader< ::google::protobuf::Empty>* Bridge::Stub::AsyncSetIsTelemetryDisabledRaw(::grpc::ClientContext* context, const ::google::protobuf::BoolValue& request, ::grpc::CompletionQueue* cq) {
auto* result =
this->PrepareAsyncSetIsTelemetryDisabledRaw(context, request, cq);
result->StartCall();
return result;
}
::grpc::Status Bridge::Stub::IsTelemetryDisabled(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::google::protobuf::BoolValue* response) {
return ::grpc::internal::BlockingUnaryCall< ::google::protobuf::Empty, ::google::protobuf::BoolValue, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(channel_.get(), rpcmethod_IsTelemetryDisabled_, context, request, response);
}
void Bridge::Stub::async::IsTelemetryDisabled(::grpc::ClientContext* context, const ::google::protobuf::Empty* request, ::google::protobuf::BoolValue* response, std::function<void(::grpc::Status)> f) {
::grpc::internal::CallbackUnaryCall< ::google::protobuf::Empty, ::google::protobuf::BoolValue, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(stub_->channel_.get(), stub_->rpcmethod_IsTelemetryDisabled_, context, request, response, std::move(f));
}
void Bridge::Stub::async::IsTelemetryDisabled(::grpc::ClientContext* context, const ::google::protobuf::Empty* request, ::google::protobuf::BoolValue* response, ::grpc::ClientUnaryReactor* reactor) {
::grpc::internal::ClientCallbackUnaryFactory::Create< ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(stub_->channel_.get(), stub_->rpcmethod_IsTelemetryDisabled_, context, request, response, reactor);
}
::grpc::ClientAsyncResponseReader< ::google::protobuf::BoolValue>* Bridge::Stub::PrepareAsyncIsTelemetryDisabledRaw(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::grpc::CompletionQueue* cq) {
return ::grpc::internal::ClientAsyncResponseReaderHelper::Create< ::google::protobuf::BoolValue, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(channel_.get(), cq, rpcmethod_IsTelemetryDisabled_, context, request);
}
::grpc::ClientAsyncResponseReader< ::google::protobuf::BoolValue>* Bridge::Stub::AsyncIsTelemetryDisabledRaw(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::grpc::CompletionQueue* cq) {
auto* result =
this->PrepareAsyncIsTelemetryDisabledRaw(context, request, cq);
result->StartCall();
return result;
}
::grpc::Status Bridge::Stub::GoOs(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::google::protobuf::StringValue* response) { ::grpc::Status Bridge::Stub::GoOs(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::google::protobuf::StringValue* response) {
return ::grpc::internal::BlockingUnaryCall< ::google::protobuf::Empty, ::google::protobuf::StringValue, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(channel_.get(), rpcmethod_GoOs_, context, request, response); return ::grpc::internal::BlockingUnaryCall< ::google::protobuf::Empty, ::google::protobuf::StringValue, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(channel_.get(), rpcmethod_GoOs_, context, request, response);
} }
@ -1525,22 +1575,22 @@ Bridge::Service::Service() {
AddMethod(new ::grpc::internal::RpcServiceMethod( AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[12], Bridge_method_names[12],
::grpc::internal::RpcMethod::NORMAL_RPC, ::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::Empty, ::google::protobuf::StringValue, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>( new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::BoolValue, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service, [](Bridge::Service* service,
::grpc::ServerContext* ctx, ::grpc::ServerContext* ctx,
const ::google::protobuf::Empty* req, const ::google::protobuf::BoolValue* req,
::google::protobuf::StringValue* resp) { ::google::protobuf::Empty* resp) {
return service->GoOs(ctx, req, resp); return service->SetIsTelemetryDisabled(ctx, req, resp);
}, this))); }, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod( AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[13], Bridge_method_names[13],
::grpc::internal::RpcMethod::NORMAL_RPC, ::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::Empty, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>( new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::Empty, ::google::protobuf::BoolValue, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service, [](Bridge::Service* service,
::grpc::ServerContext* ctx, ::grpc::ServerContext* ctx,
const ::google::protobuf::Empty* req, const ::google::protobuf::Empty* req,
::google::protobuf::Empty* resp) { ::google::protobuf::BoolValue* resp) {
return service->TriggerReset(ctx, req, resp); return service->IsTelemetryDisabled(ctx, req, resp);
}, this))); }, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod( AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[14], Bridge_method_names[14],
@ -1550,17 +1600,17 @@ Bridge::Service::Service() {
::grpc::ServerContext* ctx, ::grpc::ServerContext* ctx,
const ::google::protobuf::Empty* req, const ::google::protobuf::Empty* req,
::google::protobuf::StringValue* resp) { ::google::protobuf::StringValue* resp) {
return service->Version(ctx, req, resp); return service->GoOs(ctx, req, resp);
}, this))); }, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod( AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[15], Bridge_method_names[15],
::grpc::internal::RpcMethod::NORMAL_RPC, ::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::Empty, ::google::protobuf::StringValue, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>( new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::Empty, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service, [](Bridge::Service* service,
::grpc::ServerContext* ctx, ::grpc::ServerContext* ctx,
const ::google::protobuf::Empty* req, const ::google::protobuf::Empty* req,
::google::protobuf::StringValue* resp) { ::google::protobuf::Empty* resp) {
return service->LogsPath(ctx, req, resp); return service->TriggerReset(ctx, req, resp);
}, this))); }, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod( AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[16], Bridge_method_names[16],
@ -1570,7 +1620,7 @@ Bridge::Service::Service() {
::grpc::ServerContext* ctx, ::grpc::ServerContext* ctx,
const ::google::protobuf::Empty* req, const ::google::protobuf::Empty* req,
::google::protobuf::StringValue* resp) { ::google::protobuf::StringValue* resp) {
return service->LicensePath(ctx, req, resp); return service->Version(ctx, req, resp);
}, this))); }, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod( AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[17], Bridge_method_names[17],
@ -1580,7 +1630,7 @@ Bridge::Service::Service() {
::grpc::ServerContext* ctx, ::grpc::ServerContext* ctx,
const ::google::protobuf::Empty* req, const ::google::protobuf::Empty* req,
::google::protobuf::StringValue* resp) { ::google::protobuf::StringValue* resp) {
return service->ReleaseNotesPageLink(ctx, req, resp); return service->LogsPath(ctx, req, resp);
}, this))); }, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod( AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[18], Bridge_method_names[18],
@ -1590,7 +1640,7 @@ Bridge::Service::Service() {
::grpc::ServerContext* ctx, ::grpc::ServerContext* ctx,
const ::google::protobuf::Empty* req, const ::google::protobuf::Empty* req,
::google::protobuf::StringValue* resp) { ::google::protobuf::StringValue* resp) {
return service->DependencyLicensesLink(ctx, req, resp); return service->LicensePath(ctx, req, resp);
}, this))); }, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod( AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[19], Bridge_method_names[19],
@ -1600,17 +1650,17 @@ Bridge::Service::Service() {
::grpc::ServerContext* ctx, ::grpc::ServerContext* ctx,
const ::google::protobuf::Empty* req, const ::google::protobuf::Empty* req,
::google::protobuf::StringValue* resp) { ::google::protobuf::StringValue* resp) {
return service->LandingPageLink(ctx, req, resp); return service->ReleaseNotesPageLink(ctx, req, resp);
}, this))); }, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod( AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[20], Bridge_method_names[20],
::grpc::internal::RpcMethod::NORMAL_RPC, ::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::StringValue, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>( new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::Empty, ::google::protobuf::StringValue, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service, [](Bridge::Service* service,
::grpc::ServerContext* ctx, ::grpc::ServerContext* ctx,
const ::google::protobuf::StringValue* req, const ::google::protobuf::Empty* req,
::google::protobuf::Empty* resp) { ::google::protobuf::StringValue* resp) {
return service->SetColorSchemeName(ctx, req, resp); return service->DependencyLicensesLink(ctx, req, resp);
}, this))); }, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod( AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[21], Bridge_method_names[21],
@ -1620,11 +1670,31 @@ Bridge::Service::Service() {
::grpc::ServerContext* ctx, ::grpc::ServerContext* ctx,
const ::google::protobuf::Empty* req, const ::google::protobuf::Empty* req,
::google::protobuf::StringValue* resp) { ::google::protobuf::StringValue* resp) {
return service->ColorSchemeName(ctx, req, resp); return service->LandingPageLink(ctx, req, resp);
}, this))); }, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod( AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[22], Bridge_method_names[22],
::grpc::internal::RpcMethod::NORMAL_RPC, ::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::StringValue, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service,
::grpc::ServerContext* ctx,
const ::google::protobuf::StringValue* req,
::google::protobuf::Empty* resp) {
return service->SetColorSchemeName(ctx, req, resp);
}, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[23],
::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::Empty, ::google::protobuf::StringValue, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service,
::grpc::ServerContext* ctx,
const ::google::protobuf::Empty* req,
::google::protobuf::StringValue* resp) {
return service->ColorSchemeName(ctx, req, resp);
}, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[24],
::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::Empty, ::google::protobuf::StringValue, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>( new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::Empty, ::google::protobuf::StringValue, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service, [](Bridge::Service* service,
::grpc::ServerContext* ctx, ::grpc::ServerContext* ctx,
@ -1633,7 +1703,7 @@ Bridge::Service::Service() {
return service->CurrentEmailClient(ctx, req, resp); return service->CurrentEmailClient(ctx, req, resp);
}, this))); }, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod( AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[23], Bridge_method_names[25],
::grpc::internal::RpcMethod::NORMAL_RPC, ::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::grpc::ReportBugRequest, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>( new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::grpc::ReportBugRequest, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service, [](Bridge::Service* service,
@ -1643,7 +1713,7 @@ Bridge::Service::Service() {
return service->ReportBug(ctx, req, resp); return service->ReportBug(ctx, req, resp);
}, this))); }, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod( AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[24], Bridge_method_names[26],
::grpc::internal::RpcMethod::NORMAL_RPC, ::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::StringValue, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>( new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::StringValue, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service, [](Bridge::Service* service,
@ -1653,7 +1723,7 @@ Bridge::Service::Service() {
return service->ExportTLSCertificates(ctx, req, resp); return service->ExportTLSCertificates(ctx, req, resp);
}, this))); }, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod( AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[25], Bridge_method_names[27],
::grpc::internal::RpcMethod::NORMAL_RPC, ::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::StringValue, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>( new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::StringValue, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service, [](Bridge::Service* service,
@ -1663,7 +1733,7 @@ Bridge::Service::Service() {
return service->ForceLauncher(ctx, req, resp); return service->ForceLauncher(ctx, req, resp);
}, this))); }, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod( AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[26], Bridge_method_names[28],
::grpc::internal::RpcMethod::NORMAL_RPC, ::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::StringValue, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>( new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::StringValue, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service, [](Bridge::Service* service,
@ -1673,7 +1743,7 @@ Bridge::Service::Service() {
return service->SetMainExecutable(ctx, req, resp); return service->SetMainExecutable(ctx, req, resp);
}, this))); }, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod( AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[27], Bridge_method_names[29],
::grpc::internal::RpcMethod::NORMAL_RPC, ::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::grpc::LoginRequest, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>( new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::grpc::LoginRequest, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service, [](Bridge::Service* service,
@ -1683,7 +1753,7 @@ Bridge::Service::Service() {
return service->Login(ctx, req, resp); return service->Login(ctx, req, resp);
}, this))); }, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod( AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[28], Bridge_method_names[30],
::grpc::internal::RpcMethod::NORMAL_RPC, ::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::grpc::LoginRequest, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>( new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::grpc::LoginRequest, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service, [](Bridge::Service* service,
@ -1693,7 +1763,7 @@ Bridge::Service::Service() {
return service->Login2FA(ctx, req, resp); return service->Login2FA(ctx, req, resp);
}, this))); }, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod( AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[29], Bridge_method_names[31],
::grpc::internal::RpcMethod::NORMAL_RPC, ::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::grpc::LoginRequest, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>( new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::grpc::LoginRequest, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service, [](Bridge::Service* service,
@ -1703,7 +1773,7 @@ Bridge::Service::Service() {
return service->Login2Passwords(ctx, req, resp); return service->Login2Passwords(ctx, req, resp);
}, this))); }, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod( AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[30], Bridge_method_names[32],
::grpc::internal::RpcMethod::NORMAL_RPC, ::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::grpc::LoginAbortRequest, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>( new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::grpc::LoginAbortRequest, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service, [](Bridge::Service* service,
@ -1713,7 +1783,7 @@ Bridge::Service::Service() {
return service->LoginAbort(ctx, req, resp); return service->LoginAbort(ctx, req, resp);
}, this))); }, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod( AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[31], Bridge_method_names[33],
::grpc::internal::RpcMethod::NORMAL_RPC, ::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::Empty, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>( new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::Empty, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service, [](Bridge::Service* service,
@ -1723,7 +1793,7 @@ Bridge::Service::Service() {
return service->CheckUpdate(ctx, req, resp); return service->CheckUpdate(ctx, req, resp);
}, this))); }, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod( AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[32], Bridge_method_names[34],
::grpc::internal::RpcMethod::NORMAL_RPC, ::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::Empty, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>( new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::Empty, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service, [](Bridge::Service* service,
@ -1733,7 +1803,7 @@ Bridge::Service::Service() {
return service->InstallUpdate(ctx, req, resp); return service->InstallUpdate(ctx, req, resp);
}, this))); }, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod( AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[33], Bridge_method_names[35],
::grpc::internal::RpcMethod::NORMAL_RPC, ::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::BoolValue, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>( new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::BoolValue, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service, [](Bridge::Service* service,
@ -1743,7 +1813,7 @@ Bridge::Service::Service() {
return service->SetIsAutomaticUpdateOn(ctx, req, resp); return service->SetIsAutomaticUpdateOn(ctx, req, resp);
}, this))); }, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod( AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[34], Bridge_method_names[36],
::grpc::internal::RpcMethod::NORMAL_RPC, ::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::Empty, ::google::protobuf::BoolValue, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>( new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::Empty, ::google::protobuf::BoolValue, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service, [](Bridge::Service* service,
@ -1753,7 +1823,7 @@ Bridge::Service::Service() {
return service->IsAutomaticUpdateOn(ctx, req, resp); return service->IsAutomaticUpdateOn(ctx, req, resp);
}, this))); }, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod( AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[35], Bridge_method_names[37],
::grpc::internal::RpcMethod::NORMAL_RPC, ::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::Empty, ::google::protobuf::StringValue, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>( new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::Empty, ::google::protobuf::StringValue, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service, [](Bridge::Service* service,
@ -1763,7 +1833,7 @@ Bridge::Service::Service() {
return service->DiskCachePath(ctx, req, resp); return service->DiskCachePath(ctx, req, resp);
}, this))); }, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod( AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[36], Bridge_method_names[38],
::grpc::internal::RpcMethod::NORMAL_RPC, ::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::StringValue, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>( new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::StringValue, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service, [](Bridge::Service* service,
@ -1773,7 +1843,7 @@ Bridge::Service::Service() {
return service->SetDiskCachePath(ctx, req, resp); return service->SetDiskCachePath(ctx, req, resp);
}, this))); }, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod( AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[37], Bridge_method_names[39],
::grpc::internal::RpcMethod::NORMAL_RPC, ::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::BoolValue, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>( new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::BoolValue, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service, [](Bridge::Service* service,
@ -1783,7 +1853,7 @@ Bridge::Service::Service() {
return service->SetIsDoHEnabled(ctx, req, resp); return service->SetIsDoHEnabled(ctx, req, resp);
}, this))); }, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod( AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[38], Bridge_method_names[40],
::grpc::internal::RpcMethod::NORMAL_RPC, ::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::Empty, ::google::protobuf::BoolValue, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>( new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::Empty, ::google::protobuf::BoolValue, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service, [](Bridge::Service* service,
@ -1793,7 +1863,7 @@ Bridge::Service::Service() {
return service->IsDoHEnabled(ctx, req, resp); return service->IsDoHEnabled(ctx, req, resp);
}, this))); }, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod( AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[39], Bridge_method_names[41],
::grpc::internal::RpcMethod::NORMAL_RPC, ::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::Empty, ::grpc::ImapSmtpSettings, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>( new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::Empty, ::grpc::ImapSmtpSettings, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service, [](Bridge::Service* service,
@ -1803,7 +1873,7 @@ Bridge::Service::Service() {
return service->MailServerSettings(ctx, req, resp); return service->MailServerSettings(ctx, req, resp);
}, this))); }, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod( AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[40], Bridge_method_names[42],
::grpc::internal::RpcMethod::NORMAL_RPC, ::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::grpc::ImapSmtpSettings, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>( new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::grpc::ImapSmtpSettings, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service, [](Bridge::Service* service,
@ -1813,7 +1883,7 @@ Bridge::Service::Service() {
return service->SetMailServerSettings(ctx, req, resp); return service->SetMailServerSettings(ctx, req, resp);
}, this))); }, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod( AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[41], Bridge_method_names[43],
::grpc::internal::RpcMethod::NORMAL_RPC, ::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::Empty, ::google::protobuf::StringValue, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>( new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::Empty, ::google::protobuf::StringValue, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service, [](Bridge::Service* service,
@ -1823,7 +1893,7 @@ Bridge::Service::Service() {
return service->Hostname(ctx, req, resp); return service->Hostname(ctx, req, resp);
}, this))); }, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod( AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[42], Bridge_method_names[44],
::grpc::internal::RpcMethod::NORMAL_RPC, ::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::Int32Value, ::google::protobuf::BoolValue, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>( new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::Int32Value, ::google::protobuf::BoolValue, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service, [](Bridge::Service* service,
@ -1833,7 +1903,7 @@ Bridge::Service::Service() {
return service->IsPortFree(ctx, req, resp); return service->IsPortFree(ctx, req, resp);
}, this))); }, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod( AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[43], Bridge_method_names[45],
::grpc::internal::RpcMethod::NORMAL_RPC, ::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::Empty, ::grpc::AvailableKeychainsResponse, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>( new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::Empty, ::grpc::AvailableKeychainsResponse, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service, [](Bridge::Service* service,
@ -1843,7 +1913,7 @@ Bridge::Service::Service() {
return service->AvailableKeychains(ctx, req, resp); return service->AvailableKeychains(ctx, req, resp);
}, this))); }, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod( AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[44], Bridge_method_names[46],
::grpc::internal::RpcMethod::NORMAL_RPC, ::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::StringValue, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>( new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::StringValue, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service, [](Bridge::Service* service,
@ -1853,7 +1923,7 @@ Bridge::Service::Service() {
return service->SetCurrentKeychain(ctx, req, resp); return service->SetCurrentKeychain(ctx, req, resp);
}, this))); }, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod( AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[45], Bridge_method_names[47],
::grpc::internal::RpcMethod::NORMAL_RPC, ::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::Empty, ::google::protobuf::StringValue, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>( new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::Empty, ::google::protobuf::StringValue, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service, [](Bridge::Service* service,
@ -1863,7 +1933,7 @@ Bridge::Service::Service() {
return service->CurrentKeychain(ctx, req, resp); return service->CurrentKeychain(ctx, req, resp);
}, this))); }, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod( AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[46], Bridge_method_names[48],
::grpc::internal::RpcMethod::NORMAL_RPC, ::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::Empty, ::grpc::UserListResponse, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>( new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::Empty, ::grpc::UserListResponse, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service, [](Bridge::Service* service,
@ -1873,7 +1943,7 @@ Bridge::Service::Service() {
return service->GetUserList(ctx, req, resp); return service->GetUserList(ctx, req, resp);
}, this))); }, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod( AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[47], Bridge_method_names[49],
::grpc::internal::RpcMethod::NORMAL_RPC, ::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::StringValue, ::grpc::User, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>( new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::StringValue, ::grpc::User, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service, [](Bridge::Service* service,
@ -1883,7 +1953,7 @@ Bridge::Service::Service() {
return service->GetUser(ctx, req, resp); return service->GetUser(ctx, req, resp);
}, this))); }, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod( AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[48], Bridge_method_names[50],
::grpc::internal::RpcMethod::NORMAL_RPC, ::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::grpc::UserSplitModeRequest, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>( new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::grpc::UserSplitModeRequest, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service, [](Bridge::Service* service,
@ -1893,7 +1963,7 @@ Bridge::Service::Service() {
return service->SetUserSplitMode(ctx, req, resp); return service->SetUserSplitMode(ctx, req, resp);
}, this))); }, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod( AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[49], Bridge_method_names[51],
::grpc::internal::RpcMethod::NORMAL_RPC, ::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::grpc::UserBadEventFeedbackRequest, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>( new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::grpc::UserBadEventFeedbackRequest, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service, [](Bridge::Service* service,
@ -1903,7 +1973,7 @@ Bridge::Service::Service() {
return service->SendBadEventUserFeedback(ctx, req, resp); return service->SendBadEventUserFeedback(ctx, req, resp);
}, this))); }, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod( AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[50], Bridge_method_names[52],
::grpc::internal::RpcMethod::NORMAL_RPC, ::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::StringValue, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>( new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::StringValue, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service, [](Bridge::Service* service,
@ -1913,7 +1983,7 @@ Bridge::Service::Service() {
return service->LogoutUser(ctx, req, resp); return service->LogoutUser(ctx, req, resp);
}, this))); }, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod( AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[51], Bridge_method_names[53],
::grpc::internal::RpcMethod::NORMAL_RPC, ::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::StringValue, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>( new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::StringValue, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service, [](Bridge::Service* service,
@ -1923,7 +1993,7 @@ Bridge::Service::Service() {
return service->RemoveUser(ctx, req, resp); return service->RemoveUser(ctx, req, resp);
}, this))); }, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod( AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[52], Bridge_method_names[54],
::grpc::internal::RpcMethod::NORMAL_RPC, ::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::grpc::ConfigureAppleMailRequest, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>( new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::grpc::ConfigureAppleMailRequest, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service, [](Bridge::Service* service,
@ -1933,7 +2003,7 @@ Bridge::Service::Service() {
return service->ConfigureUserAppleMail(ctx, req, resp); return service->ConfigureUserAppleMail(ctx, req, resp);
}, this))); }, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod( AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[53], Bridge_method_names[55],
::grpc::internal::RpcMethod::SERVER_STREAMING, ::grpc::internal::RpcMethod::SERVER_STREAMING,
new ::grpc::internal::ServerStreamingHandler< Bridge::Service, ::grpc::EventStreamRequest, ::grpc::StreamEvent>( new ::grpc::internal::ServerStreamingHandler< Bridge::Service, ::grpc::EventStreamRequest, ::grpc::StreamEvent>(
[](Bridge::Service* service, [](Bridge::Service* service,
@ -1943,7 +2013,7 @@ Bridge::Service::Service() {
return service->RunEventStream(ctx, req, writer); return service->RunEventStream(ctx, req, writer);
}, this))); }, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod( AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[54], Bridge_method_names[56],
::grpc::internal::RpcMethod::NORMAL_RPC, ::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::Empty, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>( new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::Empty, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service, [](Bridge::Service* service,
@ -2041,6 +2111,20 @@ Bridge::Service::~Service() {
return ::grpc::Status(::grpc::StatusCode::UNIMPLEMENTED, ""); return ::grpc::Status(::grpc::StatusCode::UNIMPLEMENTED, "");
} }
::grpc::Status Bridge::Service::SetIsTelemetryDisabled(::grpc::ServerContext* context, const ::google::protobuf::BoolValue* request, ::google::protobuf::Empty* response) {
(void) context;
(void) request;
(void) response;
return ::grpc::Status(::grpc::StatusCode::UNIMPLEMENTED, "");
}
::grpc::Status Bridge::Service::IsTelemetryDisabled(::grpc::ServerContext* context, const ::google::protobuf::Empty* request, ::google::protobuf::BoolValue* response) {
(void) context;
(void) request;
(void) response;
return ::grpc::Status(::grpc::StatusCode::UNIMPLEMENTED, "");
}
::grpc::Status Bridge::Service::GoOs(::grpc::ServerContext* context, const ::google::protobuf::Empty* request, ::google::protobuf::StringValue* response) { ::grpc::Status Bridge::Service::GoOs(::grpc::ServerContext* context, const ::google::protobuf::Empty* request, ::google::protobuf::StringValue* response) {
(void) context; (void) context;
(void) request; (void) request;

View File

@ -1673,7 +1673,7 @@ const char descriptor_table_protodef_bridge_2eproto[] PROTOBUF_SECTION_VARIABLE(
"!SMTP_CONNECTION_MODE_CHANGE_ERROR\020\005*S\n\t" "!SMTP_CONNECTION_MODE_CHANGE_ERROR\020\005*S\n\t"
"ErrorCode\022\021\n\rUNKNOWN_ERROR\020\000\022\031\n\025TLS_CERT" "ErrorCode\022\021\n\rUNKNOWN_ERROR\020\000\022\031\n\025TLS_CERT"
"_EXPORT_ERROR\020\001\022\030\n\024TLS_KEY_EXPORT_ERROR\020" "_EXPORT_ERROR\020\001\022\030\n\024TLS_KEY_EXPORT_ERROR\020"
"\0022\360\035\n\006Bridge\022I\n\013CheckTokens\022\034.google.pro" "\0022\211\037\n\006Bridge\022I\n\013CheckTokens\022\034.google.pro"
"tobuf.StringValue\032\034.google.protobuf.Stri" "tobuf.StringValue\032\034.google.protobuf.Stri"
"ngValue\022\?\n\013AddLogEntry\022\030.grpc.AddLogEntr" "ngValue\022\?\n\013AddLogEntry\022\030.grpc.AddLogEntr"
"yRequest\032\026.google.protobuf.Empty\022:\n\010GuiR" "yRequest\032\026.google.protobuf.Empty\022:\n\010GuiR"
@ -1693,84 +1693,88 @@ const char descriptor_table_protodef_bridge_2eproto[] PROTOBUF_SECTION_VARIABLE(
"\n\023SetIsAllMailVisible\022\032.google.protobuf." "\n\023SetIsAllMailVisible\022\032.google.protobuf."
"BoolValue\032\026.google.protobuf.Empty\022F\n\020IsA" "BoolValue\032\026.google.protobuf.Empty\022F\n\020IsA"
"llMailVisible\022\026.google.protobuf.Empty\032\032." "llMailVisible\022\026.google.protobuf.Empty\032\032."
"google.protobuf.BoolValue\022<\n\004GoOs\022\026.goog" "google.protobuf.BoolValue\022L\n\026SetIsTeleme"
"le.protobuf.Empty\032\034.google.protobuf.Stri" "tryDisabled\022\032.google.protobuf.BoolValue\032"
"ngValue\022>\n\014TriggerReset\022\026.google.protobu" "\026.google.protobuf.Empty\022I\n\023IsTelemetryDi"
"f.Empty\032\026.google.protobuf.Empty\022\?\n\007Versi" "sabled\022\026.google.protobuf.Empty\032\032.google."
"on\022\026.google.protobuf.Empty\032\034.google.prot" "protobuf.BoolValue\022<\n\004GoOs\022\026.google.prot"
"obuf.StringValue\022@\n\010LogsPath\022\026.google.pr"
"otobuf.Empty\032\034.google.protobuf.StringVal"
"ue\022C\n\013LicensePath\022\026.google.protobuf.Empt"
"y\032\034.google.protobuf.StringValue\022L\n\024Relea"
"seNotesPageLink\022\026.google.protobuf.Empty\032"
"\034.google.protobuf.StringValue\022N\n\026Depende"
"ncyLicensesLink\022\026.google.protobuf.Empty\032"
"\034.google.protobuf.StringValue\022G\n\017Landing"
"PageLink\022\026.google.protobuf.Empty\032\034.googl"
"e.protobuf.StringValue\022J\n\022SetColorScheme"
"Name\022\034.google.protobuf.StringValue\032\026.goo"
"gle.protobuf.Empty\022G\n\017ColorSchemeName\022\026."
"google.protobuf.Empty\032\034.google.protobuf."
"StringValue\022J\n\022CurrentEmailClient\022\026.goog"
"le.protobuf.Empty\032\034.google.protobuf.Stri"
"ngValue\022;\n\tReportBug\022\026.grpc.ReportBugReq"
"uest\032\026.google.protobuf.Empty\022M\n\025ExportTL"
"SCertificates\022\034.google.protobuf.StringVa"
"lue\032\026.google.protobuf.Empty\022E\n\rForceLaun"
"cher\022\034.google.protobuf.StringValue\032\026.goo"
"gle.protobuf.Empty\022I\n\021SetMainExecutable\022"
"\034.google.protobuf.StringValue\032\026.google.p"
"rotobuf.Empty\0223\n\005Login\022\022.grpc.LoginReque"
"st\032\026.google.protobuf.Empty\0226\n\010Login2FA\022\022"
".grpc.LoginRequest\032\026.google.protobuf.Emp"
"ty\022=\n\017Login2Passwords\022\022.grpc.LoginReques"
"t\032\026.google.protobuf.Empty\022=\n\nLoginAbort\022"
"\027.grpc.LoginAbortRequest\032\026.google.protob"
"uf.Empty\022=\n\013CheckUpdate\022\026.google.protobu"
"f.Empty\032\026.google.protobuf.Empty\022\?\n\rInsta"
"llUpdate\022\026.google.protobuf.Empty\032\026.googl"
"e.protobuf.Empty\022L\n\026SetIsAutomaticUpdate"
"On\022\032.google.protobuf.BoolValue\032\026.google."
"protobuf.Empty\022I\n\023IsAutomaticUpdateOn\022\026."
"google.protobuf.Empty\032\032.google.protobuf."
"BoolValue\022E\n\rDiskCachePath\022\026.google.prot"
"obuf.Empty\032\034.google.protobuf.StringValue" "obuf.Empty\032\034.google.protobuf.StringValue"
"\022H\n\020SetDiskCachePath\022\034.google.protobuf.S" "\022>\n\014TriggerReset\022\026.google.protobuf.Empty"
"tringValue\032\026.google.protobuf.Empty\022E\n\017Se" "\032\026.google.protobuf.Empty\022\?\n\007Version\022\026.go"
"tIsDoHEnabled\022\032.google.protobuf.BoolValu" "ogle.protobuf.Empty\032\034.google.protobuf.St"
"e\032\026.google.protobuf.Empty\022B\n\014IsDoHEnable" "ringValue\022@\n\010LogsPath\022\026.google.protobuf."
"d\022\026.google.protobuf.Empty\032\032.google.proto" "Empty\032\034.google.protobuf.StringValue\022C\n\013L"
"buf.BoolValue\022D\n\022MailServerSettings\022\026.go" "icensePath\022\026.google.protobuf.Empty\032\034.goo"
"ogle.protobuf.Empty\032\026.grpc.ImapSmtpSetti" "gle.protobuf.StringValue\022L\n\024ReleaseNotes"
"ngs\022G\n\025SetMailServerSettings\022\026.grpc.Imap" "PageLink\022\026.google.protobuf.Empty\032\034.googl"
"SmtpSettings\032\026.google.protobuf.Empty\022@\n\010" "e.protobuf.StringValue\022N\n\026DependencyLice"
"Hostname\022\026.google.protobuf.Empty\032\034.googl" "nsesLink\022\026.google.protobuf.Empty\032\034.googl"
"e.protobuf.StringValue\022E\n\nIsPortFree\022\033.g" "e.protobuf.StringValue\022G\n\017LandingPageLin"
"oogle.protobuf.Int32Value\032\032.google.proto" "k\022\026.google.protobuf.Empty\032\034.google.proto"
"buf.BoolValue\022N\n\022AvailableKeychains\022\026.go" "buf.StringValue\022J\n\022SetColorSchemeName\022\034."
"ogle.protobuf.Empty\032 .grpc.AvailableKeyc" "google.protobuf.StringValue\032\026.google.pro"
"hainsResponse\022J\n\022SetCurrentKeychain\022\034.go" "tobuf.Empty\022G\n\017ColorSchemeName\022\026.google."
"ogle.protobuf.StringValue\032\026.google.proto" "protobuf.Empty\032\034.google.protobuf.StringV"
"buf.Empty\022G\n\017CurrentKeychain\022\026.google.pr" "alue\022J\n\022CurrentEmailClient\022\026.google.prot"
"otobuf.Empty\032\034.google.protobuf.StringVal" "obuf.Empty\032\034.google.protobuf.StringValue"
"ue\022=\n\013GetUserList\022\026.google.protobuf.Empt" "\022;\n\tReportBug\022\026.grpc.ReportBugRequest\032\026."
"y\032\026.grpc.UserListResponse\0223\n\007GetUser\022\034.g" "google.protobuf.Empty\022M\n\025ExportTLSCertif"
"oogle.protobuf.StringValue\032\n.grpc.User\022F" "icates\022\034.google.protobuf.StringValue\032\026.g"
"\n\020SetUserSplitMode\022\032.grpc.UserSplitModeR" "oogle.protobuf.Empty\022E\n\rForceLauncher\022\034."
"equest\032\026.google.protobuf.Empty\022U\n\030SendBa" "google.protobuf.StringValue\032\026.google.pro"
"dEventUserFeedback\022!.grpc.UserBadEventFe" "tobuf.Empty\022I\n\021SetMainExecutable\022\034.googl"
"edbackRequest\032\026.google.protobuf.Empty\022B\n" "e.protobuf.StringValue\032\026.google.protobuf"
"\nLogoutUser\022\034.google.protobuf.StringValu" ".Empty\0223\n\005Login\022\022.grpc.LoginRequest\032\026.go"
"e\032\026.google.protobuf.Empty\022B\n\nRemoveUser\022" "ogle.protobuf.Empty\0226\n\010Login2FA\022\022.grpc.L"
"\034.google.protobuf.StringValue\032\026.google.p" "oginRequest\032\026.google.protobuf.Empty\022=\n\017L"
"rotobuf.Empty\022Q\n\026ConfigureUserAppleMail\022" "ogin2Passwords\022\022.grpc.LoginRequest\032\026.goo"
"\037.grpc.ConfigureAppleMailRequest\032\026.googl" "gle.protobuf.Empty\022=\n\nLoginAbort\022\027.grpc."
"e.protobuf.Empty\022\?\n\016RunEventStream\022\030.grp" "LoginAbortRequest\032\026.google.protobuf.Empt"
"c.EventStreamRequest\032\021.grpc.StreamEvent0" "y\022=\n\013CheckUpdate\022\026.google.protobuf.Empty"
"\001\022A\n\017StopEventStream\022\026.google.protobuf.E" "\032\026.google.protobuf.Empty\022\?\n\rInstallUpdat"
"mpty\032\026.google.protobuf.EmptyB6Z4github.c" "e\022\026.google.protobuf.Empty\032\026.google.proto"
"om/ProtonMail/proton-bridge/v3/internal/" "buf.Empty\022L\n\026SetIsAutomaticUpdateOn\022\032.go"
"grpcb\006proto3" "ogle.protobuf.BoolValue\032\026.google.protobu"
"f.Empty\022I\n\023IsAutomaticUpdateOn\022\026.google."
"protobuf.Empty\032\032.google.protobuf.BoolVal"
"ue\022E\n\rDiskCachePath\022\026.google.protobuf.Em"
"pty\032\034.google.protobuf.StringValue\022H\n\020Set"
"DiskCachePath\022\034.google.protobuf.StringVa"
"lue\032\026.google.protobuf.Empty\022E\n\017SetIsDoHE"
"nabled\022\032.google.protobuf.BoolValue\032\026.goo"
"gle.protobuf.Empty\022B\n\014IsDoHEnabled\022\026.goo"
"gle.protobuf.Empty\032\032.google.protobuf.Boo"
"lValue\022D\n\022MailServerSettings\022\026.google.pr"
"otobuf.Empty\032\026.grpc.ImapSmtpSettings\022G\n\025"
"SetMailServerSettings\022\026.grpc.ImapSmtpSet"
"tings\032\026.google.protobuf.Empty\022@\n\010Hostnam"
"e\022\026.google.protobuf.Empty\032\034.google.proto"
"buf.StringValue\022E\n\nIsPortFree\022\033.google.p"
"rotobuf.Int32Value\032\032.google.protobuf.Boo"
"lValue\022N\n\022AvailableKeychains\022\026.google.pr"
"otobuf.Empty\032 .grpc.AvailableKeychainsRe"
"sponse\022J\n\022SetCurrentKeychain\022\034.google.pr"
"otobuf.StringValue\032\026.google.protobuf.Emp"
"ty\022G\n\017CurrentKeychain\022\026.google.protobuf."
"Empty\032\034.google.protobuf.StringValue\022=\n\013G"
"etUserList\022\026.google.protobuf.Empty\032\026.grp"
"c.UserListResponse\0223\n\007GetUser\022\034.google.p"
"rotobuf.StringValue\032\n.grpc.User\022F\n\020SetUs"
"erSplitMode\022\032.grpc.UserSplitModeRequest\032"
"\026.google.protobuf.Empty\022U\n\030SendBadEventU"
"serFeedback\022!.grpc.UserBadEventFeedbackR"
"equest\032\026.google.protobuf.Empty\022B\n\nLogout"
"User\022\034.google.protobuf.StringValue\032\026.goo"
"gle.protobuf.Empty\022B\n\nRemoveUser\022\034.googl"
"e.protobuf.StringValue\032\026.google.protobuf"
".Empty\022Q\n\026ConfigureUserAppleMail\022\037.grpc."
"ConfigureAppleMailRequest\032\026.google.proto"
"buf.Empty\022\?\n\016RunEventStream\022\030.grpc.Event"
"StreamRequest\032\021.grpc.StreamEvent0\001\022A\n\017St"
"opEventStream\022\026.google.protobuf.Empty\032\026."
"google.protobuf.EmptyB6Z4github.com/Prot"
"onMail/proton-bridge/v3/internal/grpcb\006p"
"roto3"
; ;
static const ::_pbi::DescriptorTable* const descriptor_table_bridge_2eproto_deps[2] = { static const ::_pbi::DescriptorTable* const descriptor_table_bridge_2eproto_deps[2] = {
&::descriptor_table_google_2fprotobuf_2fempty_2eproto, &::descriptor_table_google_2fprotobuf_2fempty_2eproto,
@ -1778,7 +1782,7 @@ static const ::_pbi::DescriptorTable* const descriptor_table_bridge_2eproto_deps
}; };
static ::_pbi::once_flag descriptor_table_bridge_2eproto_once; static ::_pbi::once_flag descriptor_table_bridge_2eproto_once;
const ::_pbi::DescriptorTable descriptor_table_bridge_2eproto = { const ::_pbi::DescriptorTable descriptor_table_bridge_2eproto = {
false, false, 10532, descriptor_table_protodef_bridge_2eproto, false, false, 10685, descriptor_table_protodef_bridge_2eproto,
"bridge.proto", "bridge.proto",
&descriptor_table_bridge_2eproto_once, descriptor_table_bridge_2eproto_deps, 2, 64, &descriptor_table_bridge_2eproto_once, descriptor_table_bridge_2eproto_deps, 2, 64,
schemas, file_default_instances, TableStruct_bridge_2eproto::offsets, schemas, file_default_instances, TableStruct_bridge_2eproto::offsets,

View File

@ -17,8 +17,8 @@
#include "LogUtils.h" #include "LogUtils.h"
#include <bridgepp/BridgeUtils.h> #include "../BridgeUtils.h"
#include <bridgepp/Exception/Exception.h> #include "../Exception/Exception.h"
namespace bridgepp { namespace bridgepp {
@ -43,7 +43,12 @@ QString latestBridgeLogPath() {
if (logsDir.isEmpty()) { if (logsDir.isEmpty()) {
return QString(); return QString();
} }
QFileInfoList files = logsDir.entryInfoList({ "v*.log" }, QDir::Files); // could do sorting, but only by last modification time. we want to sort by creation time. QFileInfoList files = logsDir.entryInfoList({ "v*.log" }, QDir::Files); // could do sorting, but only by last modification time. we want to sort by creation time.
if (files.isEmpty()) {
return QString();
}
std::sort(files.begin(), files.end(), [](QFileInfo const &lhs, QFileInfo const &rhs) -> bool { std::sort(files.begin(), files.end(), [](QFileInfo const &lhs, QFileInfo const &rhs) -> bool {
return lhs.birthTime() < rhs.birthTime(); return lhs.birthTime() < rhs.birthTime();
}); });

View File

@ -289,6 +289,23 @@ func New(
}) })
fe.AddCmd(badEventCmd) fe.AddCmd(badEventCmd)
// Telemetry commands
telemetryCmd := &ishell.Cmd{
Name: "telemetry",
Help: "choose whether usage diagnostics are collected or not",
}
telemetryCmd.AddCmd(&ishell.Cmd{
Name: "enable",
Help: "Usage diagnostics collection will be enabled",
Func: fe.enableTelemetry,
})
telemetryCmd.AddCmd(&ishell.Cmd{
Name: "disable",
Help: "Usage diagnostics collection will be disabled",
Func: fe.disableTelemetry,
})
fe.AddCmd(telemetryCmd)
go fe.watchEvents(eventCh) go fe.watchEvents(eventCh)
go func() { go func() {

View File

@ -40,7 +40,7 @@ func (f *frontendCLI) printLogDir(c *ishell.Context) {
} }
func (f *frontendCLI) printManual(c *ishell.Context) { func (f *frontendCLI) printManual(c *ishell.Context) {
f.Println("More instructions about the Bridge can be found at\n\n https://protonmail.com/bridge") f.Println("More instructions about the Bridge can be found at\n\n https://proton.me/mail/bridge")
} }
func (f *frontendCLI) printCredits(c *ishell.Context) { func (f *frontendCLI) printCredits(c *ishell.Context) {
@ -195,6 +195,38 @@ func (f *frontendCLI) showAllMail(c *ishell.Context) {
} }
} }
func (f *frontendCLI) enableTelemetry(_ *ishell.Context) {
if !f.bridge.GetTelemetryDisabled() {
f.Println("Usage diagnostics collection is enabled.")
return
}
f.Println("Usage diagnostics collection is disabled right now.")
if f.yesNoQuestion("Do you want to enable usage diagnostics collection") {
if err := f.bridge.SetTelemetryDisabled(false); err != nil {
f.printAndLogError(err)
return
}
}
}
func (f *frontendCLI) disableTelemetry(_ *ishell.Context) {
if f.bridge.GetTelemetryDisabled() {
f.Println("Usage diagnostics collection is disabled.")
return
}
f.Println("Usage diagnostics collection is enabled right now.")
if f.yesNoQuestion("Do you want to disable usage diagnostics collection") {
if err := f.bridge.SetTelemetryDisabled(true); err != nil {
f.printAndLogError(err)
return
}
}
}
func (f *frontendCLI) setGluonLocation(c *ishell.Context) { func (f *frontendCLI) setGluonLocation(c *ishell.Context) {
if gluonDir := f.bridge.GetGluonCacheDir(); gluonDir != "" { if gluonDir := f.bridge.GetGluonCacheDir(); gluonDir != "" {
f.Println("The current message cache location is:", gluonDir) f.Println("The current message cache location is:", gluonDir)

View File

@ -17,8 +17,8 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.28.1 // protoc-gen-go v1.28.0
// protoc v3.21.12 // protoc v3.21.3
// source: bridge.proto // source: bridge.proto
package grpc package grpc
@ -4803,8 +4803,8 @@ var file_bridge_proto_rawDesc = []byte{
0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x00, 0x12, 0x19, 0x0a, 0x15, 0x54, 0x4c, 0x53, 0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x00, 0x12, 0x19, 0x0a, 0x15, 0x54, 0x4c, 0x53,
0x5f, 0x43, 0x45, 0x52, 0x54, 0x5f, 0x45, 0x58, 0x50, 0x4f, 0x52, 0x54, 0x5f, 0x45, 0x52, 0x52, 0x5f, 0x43, 0x45, 0x52, 0x54, 0x5f, 0x45, 0x58, 0x50, 0x4f, 0x52, 0x54, 0x5f, 0x45, 0x52, 0x52,
0x4f, 0x52, 0x10, 0x01, 0x12, 0x18, 0x0a, 0x14, 0x54, 0x4c, 0x53, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x4f, 0x52, 0x10, 0x01, 0x12, 0x18, 0x0a, 0x14, 0x54, 0x4c, 0x53, 0x5f, 0x4b, 0x45, 0x59, 0x5f,
0x45, 0x58, 0x50, 0x4f, 0x52, 0x54, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x02, 0x32, 0xf0, 0x45, 0x58, 0x50, 0x4f, 0x52, 0x54, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x02, 0x32, 0x89,
0x1d, 0x0a, 0x06, 0x42, 0x72, 0x69, 0x64, 0x67, 0x65, 0x12, 0x49, 0x0a, 0x0b, 0x43, 0x68, 0x65, 0x1f, 0x0a, 0x06, 0x42, 0x72, 0x69, 0x64, 0x67, 0x65, 0x12, 0x49, 0x0a, 0x0b, 0x43, 0x68, 0x65,
0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 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, 0x65, 0x2e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
@ -4855,199 +4855,208 @@ var file_bridge_proto_rawDesc = []byte{
0x6c, 0x65, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6c, 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, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 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, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f,
0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x3c, 0x0a, 0x04, 0x47, 0x6f, 0x4f, 0x73, 0x12, 0x16, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x4c, 0x0a, 0x16, 0x53, 0x65, 0x74, 0x49, 0x73, 0x54,
0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64,
0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x12, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67,
0x61, 0x6c, 0x75, 0x65, 0x12, 0x3e, 0x0a, 0x0c, 0x54, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x52,
0x65, 0x73, 0x65, 0x74, 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, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45,
0x6d, 0x70, 0x74, 0x79, 0x12, 0x3f, 0x0a, 0x07, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x49, 0x0a, 0x13, 0x49, 0x73, 0x54, 0x65, 0x6c, 0x65, 0x6d, 0x65,
0x74, 0x72, 0x79, 0x44, 0x69, 0x73, 0x61, 0x62, 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,
0x3c, 0x0a, 0x04, 0x47, 0x6f, 0x4f, 0x73, 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,
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, 0x12, 0x3e, 0x0a,
0x0c, 0x54, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x52, 0x65, 0x73, 0x65, 0x74, 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,
0x07, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 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, 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, 0x12, 0x40,
0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x73, 0x50, 0x61, 0x74, 0x68, 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, 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,
0x12, 0x43, 0x0a, 0x0b, 0x4c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x50, 0x61, 0x74, 0x68, 0x12,
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, 0x1a, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67,
0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x40, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x73, 0x50, 0x61, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x4c, 0x0a, 0x14, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65,
0x68, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x4e, 0x6f, 0x74, 0x65, 0x73, 0x50, 0x61, 0x67, 0x65, 0x4c, 0x69, 0x6e, 0x6b, 0x12, 0x16, 0x2e,
0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 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, 0x12, 0x43, 0x0a, 0x0b, 0x4c, 0x69, 0x63, 0x65, 0x6e,
0x73, 0x65, 0x50, 0x61, 0x74, 0x68, 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, 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, 0x12, 0x4c, 0x0a, 0x14,
0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x4e, 0x6f, 0x74, 0x65, 0x73, 0x50, 0x61, 0x67, 0x65,
0x4c, 0x69, 0x6e, 0x6b, 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, 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, 0x12, 0x4e, 0x0a, 0x16, 0x44, 0x65,
0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x79, 0x4c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x73,
0x4c, 0x69, 0x6e, 0x6b, 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, 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, 0x12, 0x47, 0x0a, 0x0f, 0x4c, 0x61,
0x6e, 0x64, 0x69, 0x6e, 0x67, 0x50, 0x61, 0x67, 0x65, 0x4c, 0x69, 0x6e, 0x6b, 0x12, 0x16, 0x2e,
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 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, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61,
0x6c, 0x75, 0x65, 0x12, 0x4a, 0x0a, 0x12, 0x53, 0x65, 0x74, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x53, 0x6c, 0x75, 0x65, 0x12, 0x4e, 0x0a, 0x16, 0x44, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63,
0x63, 0x68, 0x65, 0x6d, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x79, 0x4c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x73, 0x4c, 0x69, 0x6e, 0x6b, 0x12, 0x16, 0x2e,
0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61,
0x47, 0x0a, 0x0f, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x65, 0x4e, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x47, 0x0a, 0x0f, 0x4c, 0x61, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x50, 0x61,
0x6d, 0x65, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x67, 0x65, 0x4c, 0x69, 0x6e, 0x6b, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1c,
0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72,
0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x4a, 0x0a, 0x12, 0x43, 0x75, 0x72, 0x72,
0x65, 0x6e, 0x74, 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x16,
0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x4a, 0x0a, 0x12,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x53, 0x65, 0x74, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x65, 0x4e, 0x61,
0x61, 0x6c, 0x75, 0x65, 0x12, 0x3b, 0x0a, 0x09, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x42, 0x75, 0x6d, 0x65, 0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
0x67, 0x12, 0x16, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x42, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65,
0x75, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x47, 0x0a, 0x0f, 0x43, 0x6f, 0x6c, 0x6f,
0x79, 0x12, 0x4d, 0x0a, 0x15, 0x45, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x54, 0x4c, 0x53, 0x43, 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x2e, 0x67, 0x6f,
0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d,
0x70, 0x74, 0x79, 0x1a, 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, 0x12, 0x4a, 0x0a, 0x12, 0x43, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x45, 0x6d, 0x61, 0x69,
0x6c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 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,
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, 0x12, 0x3b, 0x0a,
0x09, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x42, 0x75, 0x67, 0x12, 0x16, 0x2e, 0x67, 0x72, 0x70,
0x63, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x42, 0x75, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x4d, 0x0a, 0x15, 0x45, 0x78,
0x70, 0x6f, 0x72, 0x74, 0x54, 0x4c, 0x53, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61,
0x74, 0x65, 0x73, 0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75,
0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x45, 0x0a, 0x0d, 0x46, 0x6f, 0x72,
0x63, 0x65, 0x4c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x65, 0x72, 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, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 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, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79,
0x12, 0x45, 0x0a, 0x0d, 0x46, 0x6f, 0x72, 0x63, 0x65, 0x4c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x65, 0x12, 0x49, 0x0a, 0x11, 0x53, 0x65, 0x74, 0x4d, 0x61, 0x69, 0x6e, 0x45, 0x78, 0x65, 0x63, 0x75,
0x72, 0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61,
0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x49, 0x0a, 0x11, 0x53, 0x65, 0x74, 0x4d, 0x61, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x33, 0x0a, 0x05, 0x4c,
0x69, 0x6e, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x12, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x6f, 0x67, 0x69,
0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79,
0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x12, 0x36, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x32, 0x46, 0x41, 0x12, 0x12, 0x2e, 0x67,
0x74, 0x79, 0x12, 0x33, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x12, 0x2e, 0x67, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x3d, 0x0a, 0x0f, 0x4c, 0x6f, 0x67, 0x69,
0x6e, 0x32, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x73, 0x12, 0x12, 0x2e, 0x67, 0x72,
0x70, 0x63, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x70, 0x63, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 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, 0x36, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x3d, 0x0a, 0x0a, 0x4c, 0x6f, 0x67, 0x69, 0x6e,
0x32, 0x46, 0x41, 0x12, 0x12, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x41, 0x62, 0x6f, 0x72, 0x74, 0x12, 0x17, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x6f, 0x67,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x69, 0x6e, 0x41, 0x62, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
0x3d, 0x0a, 0x0f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x32, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x3d, 0x0a, 0x0b, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x55,
0x64, 0x73, 0x12, 0x12, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x3d, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
0x0a, 0x0a, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x41, 0x62, 0x6f, 0x72, 0x74, 0x12, 0x17, 0x2e, 0x67, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x3f, 0x0a, 0x0d, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c,
0x72, 0x70, 0x63, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x41, 0x62, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16,
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x3d, 0x0a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
0x0b, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x16, 0x2e, 0x67, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x4c, 0x0a, 0x16, 0x53, 0x65, 0x74, 0x49, 0x73, 0x41,
0x75, 0x74, 0x6f, 0x6d, 0x61, 0x74, 0x69, 0x63, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4f, 0x6e,
0x12, 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, 0x1a, 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, 0x12, 0x49, 0x0a, 0x13, 0x49, 0x73, 0x41, 0x75, 0x74, 0x6f, 0x6d, 0x61,
0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x3f, 0x0a, 0x0d, 0x74, 0x69, 0x63, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4f, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f,
0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x16, 0x2e, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d,
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x70, 0x74, 0x79, 0x1a, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12,
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x4c, 0x0a, 0x45, 0x0a, 0x0d, 0x44, 0x69, 0x73, 0x6b, 0x43, 0x61, 0x63, 0x68, 0x65, 0x50, 0x61, 0x74, 0x68,
0x16, 0x53, 0x65, 0x74, 0x49, 0x73, 0x41, 0x75, 0x74, 0x6f, 0x6d, 0x61, 0x74, 0x69, 0x63, 0x55,
0x70, 0x64, 0x61, 0x74, 0x65, 0x4f, 0x6e, 0x12, 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, 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, 0x13, 0x49,
0x73, 0x41, 0x75, 0x74, 0x6f, 0x6d, 0x61, 0x74, 0x69, 0x63, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65,
0x4f, 0x6e, 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, 0x45, 0x0a, 0x0d, 0x44, 0x69, 0x73, 0x6b, 0x43, 0x61,
0x63, 0x68, 0x65, 0x50, 0x61, 0x74, 0x68, 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,
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, 0x12, 0x48, 0x0a,
0x10, 0x53, 0x65, 0x74, 0x44, 0x69, 0x73, 0x6b, 0x43, 0x61, 0x63, 0x68, 0x65, 0x50, 0x61, 0x74,
0x68, 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, 0x45, 0x0a, 0x0f, 0x53, 0x65, 0x74, 0x49, 0x73,
0x44, 0x6f, 0x48, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 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, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x42,
0x0a, 0x0c, 0x49, 0x73, 0x44, 0x6f, 0x48, 0x45, 0x6e, 0x61, 0x62, 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, 0x44, 0x0a, 0x12, 0x4d, 0x61, 0x69, 0x6c, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72,
0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 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, 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6d, 0x61, 0x70, 0x53, 0x6d, 0x74, 0x70,
0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x47, 0x0a, 0x15, 0x53, 0x65, 0x74, 0x4d,
0x61, 0x69, 0x6c, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67,
0x73, 0x12, 0x16, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6d, 0x61, 0x70, 0x53, 0x6d, 0x74,
0x70, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 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, 0x40, 0x0a, 0x08, 0x48, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 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, 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, 0x12, 0x45, 0x0a, 0x0a, 0x49, 0x73, 0x50, 0x6f, 0x72, 0x74, 0x46, 0x72, 0x65,
0x65, 0x12, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x62, 0x75, 0x66, 0x2e, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, 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, 0x4e, 0x0a, 0x12, 0x41, 0x76,
0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73,
0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 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, 0x20, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x63, 0x68, 0x61, 0x69, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e,
0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4a, 0x0a, 0x12, 0x53, 0x65, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x48, 0x0a, 0x10, 0x53, 0x65, 0x74, 0x44, 0x69, 0x73,
0x74, 0x43, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x4b, 0x65, 0x79, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x6b, 0x43, 0x61, 0x63, 0x68, 0x65, 0x50, 0x61, 0x74, 0x68, 0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x6f,
0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72,
0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 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, 0x45, 0x0a, 0x0f, 0x53, 0x65, 0x74, 0x49, 0x73, 0x44, 0x6f, 0x48, 0x45, 0x6e, 0x61, 0x62,
0x6c, 0x65, 0x64, 0x12, 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, 0x1a,
0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x42, 0x0a, 0x0c, 0x49, 0x73, 0x44, 0x6f, 0x48,
0x45, 0x6e, 0x61, 0x62, 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, 0x44, 0x0a, 0x12, 0x4d,
0x61, 0x69, 0x6c, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67,
0x73, 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, 0x72, 0x70, 0x63,
0x2e, 0x49, 0x6d, 0x61, 0x70, 0x53, 0x6d, 0x74, 0x70, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67,
0x73, 0x12, 0x47, 0x0a, 0x15, 0x53, 0x65, 0x74, 0x4d, 0x61, 0x69, 0x6c, 0x53, 0x65, 0x72, 0x76,
0x65, 0x72, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x72, 0x70,
0x63, 0x2e, 0x49, 0x6d, 0x61, 0x70, 0x53, 0x6d, 0x74, 0x70, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e,
0x67, 0x73, 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, 0x40, 0x0a, 0x08, 0x48, 0x6f,
0x73, 0x74, 0x6e, 0x61, 0x6d, 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, 0x1c,
0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x47, 0x0a, 0x0f, 0x43, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x45, 0x0a, 0x0a,
0x74, 0x4b, 0x65, 0x79, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x49, 0x73, 0x50, 0x6f, 0x72, 0x74, 0x46, 0x72, 0x65, 0x65, 0x12, 0x1b, 0x2e, 0x67, 0x6f, 0x6f,
0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x49, 0x6e, 0x74,
0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, 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, 0x4e, 0x0a, 0x12, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65,
0x4b, 0x65, 0x79, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74,
0x79, 0x1a, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x79, 0x1a, 0x20, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62,
0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x3d, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x16, 0x6e, 0x73, 0x65, 0x12, 0x4a, 0x0a, 0x12, 0x53, 0x65, 0x74, 0x43, 0x75, 0x72, 0x72, 0x65, 0x6e,
0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x74, 0x4b, 0x65, 0x79, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x55, 0x73,
0x65, 0x72, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33,
0x0a, 0x07, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69,
0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x0a, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x55, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
0x73, 0x65, 0x72, 0x12, 0x46, 0x0a, 0x10, 0x53, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x53, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12,
0x6c, 0x69, 0x74, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x1a, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x55, 0x47, 0x0a, 0x0f, 0x43, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x4b, 0x65, 0x79, 0x63, 0x68, 0x61,
0x73, 0x65, 0x72, 0x53, 0x70, 0x6c, 0x69, 0x74, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x69, 0x6e, 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, 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, 0x12, 0x3d, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x55,
0x73, 0x65, 0x72, 0x4c, 0x69, 0x73, 0x74, 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, 0x72, 0x70, 0x63, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x4c, 0x69, 0x73, 0x74, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x55, 0x73,
0x65, 0x72, 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, 0x0a, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x12, 0x46, 0x0a, 0x10,
0x53, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x53, 0x70, 0x6c, 0x69, 0x74, 0x4d, 0x6f, 0x64, 0x65,
0x12, 0x1a, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x53, 0x70, 0x6c, 0x69,
0x74, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67,
0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45,
0x6d, 0x70, 0x74, 0x79, 0x12, 0x55, 0x0a, 0x18, 0x53, 0x65, 0x6e, 0x64, 0x42, 0x61, 0x64, 0x45,
0x76, 0x65, 0x6e, 0x74, 0x55, 0x73, 0x65, 0x72, 0x46, 0x65, 0x65, 0x64, 0x62, 0x61, 0x63, 0x6b,
0x12, 0x21, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x42, 0x61, 0x64, 0x45,
0x76, 0x65, 0x6e, 0x74, 0x46, 0x65, 0x65, 0x64, 0x62, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x55, 0x0a, 0x18, 0x53, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x42, 0x0a, 0x0a, 0x4c,
0x65, 0x6e, 0x64, 0x42, 0x61, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x55, 0x73, 0x65, 0x72, 0x46, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x55, 0x73, 0x65, 0x72, 0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
0x65, 0x65, 0x64, 0x62, 0x61, 0x63, 0x6b, 0x12, 0x21, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x55, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69,
0x73, 0x65, 0x72, 0x42, 0x61, 0x64, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x46, 0x65, 0x65, 0x64, 0x62, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
0x61, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12,
0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x42, 0x0a, 0x0a, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x55, 0x73, 0x65, 0x72, 0x12, 0x1c, 0x2e,
0x74, 0x79, 0x12, 0x42, 0x0a, 0x0a, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x55, 0x73, 0x65, 0x72, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f,
0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d,
0x70, 0x74, 0x79, 0x12, 0x51, 0x0a, 0x16, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x65,
0x55, 0x73, 0x65, 0x72, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x4d, 0x61, 0x69, 0x6c, 0x12, 0x1f, 0x2e,
0x67, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x65, 0x41, 0x70,
0x70, 0x6c, 0x65, 0x4d, 0x61, 0x69, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16,
0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x42, 0x0a, 0x0a, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x3f, 0x0a, 0x0e, 0x52, 0x75, 0x6e, 0x45, 0x76, 0x65,
0x55, 0x73, 0x65, 0x72, 0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x18, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e,
0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65,
0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d,
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x51, 0x0a, 0x16, 0x43, 0x6f, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x30, 0x01, 0x12, 0x41, 0x0a, 0x0f, 0x53, 0x74, 0x6f, 0x70, 0x45,
0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x65, 0x55, 0x73, 0x65, 0x72, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f,
0x4d, 0x61, 0x69, 0x6c, 0x12, 0x1f, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70,
0x69, 0x67, 0x75, 0x72, 0x65, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x4d, 0x61, 0x69, 0x6c, 0x52, 0x65, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, 0x36, 0x5a, 0x34, 0x67, 0x69,
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x3f, 0x0a, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x6e, 0x4d,
0x0e, 0x52, 0x75, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x61, 0x69, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x6e, 0x2d, 0x62, 0x72, 0x69, 0x64, 0x67,
0x18, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, 0x65, 0x2f, 0x76, 0x33, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x72,
0x61, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
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, 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 (
@ -5214,106 +5223,110 @@ var file_bridge_proto_depIdxs = []int32{
72, // 69: grpc.Bridge.IsBetaEnabled:input_type -> google.protobuf.Empty 72, // 69: grpc.Bridge.IsBetaEnabled:input_type -> google.protobuf.Empty
73, // 70: grpc.Bridge.SetIsAllMailVisible:input_type -> google.protobuf.BoolValue 73, // 70: grpc.Bridge.SetIsAllMailVisible:input_type -> google.protobuf.BoolValue
72, // 71: grpc.Bridge.IsAllMailVisible:input_type -> google.protobuf.Empty 72, // 71: grpc.Bridge.IsAllMailVisible:input_type -> google.protobuf.Empty
72, // 72: grpc.Bridge.GoOs:input_type -> google.protobuf.Empty 73, // 72: grpc.Bridge.SetIsTelemetryDisabled:input_type -> google.protobuf.BoolValue
72, // 73: grpc.Bridge.TriggerReset:input_type -> google.protobuf.Empty 72, // 73: grpc.Bridge.IsTelemetryDisabled:input_type -> google.protobuf.Empty
72, // 74: grpc.Bridge.Version:input_type -> google.protobuf.Empty 72, // 74: grpc.Bridge.GoOs:input_type -> google.protobuf.Empty
72, // 75: grpc.Bridge.LogsPath:input_type -> google.protobuf.Empty 72, // 75: grpc.Bridge.TriggerReset:input_type -> google.protobuf.Empty
72, // 76: grpc.Bridge.LicensePath:input_type -> google.protobuf.Empty 72, // 76: grpc.Bridge.Version:input_type -> google.protobuf.Empty
72, // 77: grpc.Bridge.ReleaseNotesPageLink:input_type -> google.protobuf.Empty 72, // 77: grpc.Bridge.LogsPath:input_type -> google.protobuf.Empty
72, // 78: grpc.Bridge.DependencyLicensesLink:input_type -> google.protobuf.Empty 72, // 78: grpc.Bridge.LicensePath:input_type -> google.protobuf.Empty
72, // 79: grpc.Bridge.LandingPageLink:input_type -> google.protobuf.Empty 72, // 79: grpc.Bridge.ReleaseNotesPageLink:input_type -> google.protobuf.Empty
71, // 80: grpc.Bridge.SetColorSchemeName:input_type -> google.protobuf.StringValue 72, // 80: grpc.Bridge.DependencyLicensesLink:input_type -> google.protobuf.Empty
72, // 81: grpc.Bridge.ColorSchemeName:input_type -> google.protobuf.Empty 72, // 81: grpc.Bridge.LandingPageLink:input_type -> google.protobuf.Empty
72, // 82: grpc.Bridge.CurrentEmailClient:input_type -> google.protobuf.Empty 71, // 82: grpc.Bridge.SetColorSchemeName:input_type -> google.protobuf.StringValue
9, // 83: grpc.Bridge.ReportBug:input_type -> grpc.ReportBugRequest 72, // 83: grpc.Bridge.ColorSchemeName:input_type -> google.protobuf.Empty
71, // 84: grpc.Bridge.ExportTLSCertificates:input_type -> google.protobuf.StringValue 72, // 84: grpc.Bridge.CurrentEmailClient:input_type -> google.protobuf.Empty
71, // 85: grpc.Bridge.ForceLauncher:input_type -> google.protobuf.StringValue 9, // 85: grpc.Bridge.ReportBug:input_type -> grpc.ReportBugRequest
71, // 86: grpc.Bridge.SetMainExecutable:input_type -> google.protobuf.StringValue 71, // 86: grpc.Bridge.ExportTLSCertificates:input_type -> google.protobuf.StringValue
10, // 87: grpc.Bridge.Login:input_type -> grpc.LoginRequest 71, // 87: grpc.Bridge.ForceLauncher:input_type -> google.protobuf.StringValue
10, // 88: grpc.Bridge.Login2FA:input_type -> grpc.LoginRequest 71, // 88: grpc.Bridge.SetMainExecutable:input_type -> google.protobuf.StringValue
10, // 89: grpc.Bridge.Login2Passwords:input_type -> grpc.LoginRequest 10, // 89: grpc.Bridge.Login:input_type -> grpc.LoginRequest
11, // 90: grpc.Bridge.LoginAbort:input_type -> grpc.LoginAbortRequest 10, // 90: grpc.Bridge.Login2FA:input_type -> grpc.LoginRequest
72, // 91: grpc.Bridge.CheckUpdate:input_type -> google.protobuf.Empty 10, // 91: grpc.Bridge.Login2Passwords:input_type -> grpc.LoginRequest
72, // 92: grpc.Bridge.InstallUpdate:input_type -> google.protobuf.Empty 11, // 92: grpc.Bridge.LoginAbort:input_type -> grpc.LoginAbortRequest
73, // 93: grpc.Bridge.SetIsAutomaticUpdateOn:input_type -> google.protobuf.BoolValue 72, // 93: grpc.Bridge.CheckUpdate:input_type -> google.protobuf.Empty
72, // 94: grpc.Bridge.IsAutomaticUpdateOn:input_type -> google.protobuf.Empty 72, // 94: grpc.Bridge.InstallUpdate:input_type -> google.protobuf.Empty
72, // 95: grpc.Bridge.DiskCachePath:input_type -> google.protobuf.Empty 73, // 95: grpc.Bridge.SetIsAutomaticUpdateOn:input_type -> google.protobuf.BoolValue
71, // 96: grpc.Bridge.SetDiskCachePath:input_type -> google.protobuf.StringValue 72, // 96: grpc.Bridge.IsAutomaticUpdateOn:input_type -> google.protobuf.Empty
73, // 97: grpc.Bridge.SetIsDoHEnabled:input_type -> google.protobuf.BoolValue 72, // 97: grpc.Bridge.DiskCachePath:input_type -> google.protobuf.Empty
72, // 98: grpc.Bridge.IsDoHEnabled:input_type -> google.protobuf.Empty 71, // 98: grpc.Bridge.SetDiskCachePath:input_type -> google.protobuf.StringValue
72, // 99: grpc.Bridge.MailServerSettings:input_type -> google.protobuf.Empty 73, // 99: grpc.Bridge.SetIsDoHEnabled:input_type -> google.protobuf.BoolValue
12, // 100: grpc.Bridge.SetMailServerSettings:input_type -> grpc.ImapSmtpSettings 72, // 100: grpc.Bridge.IsDoHEnabled:input_type -> google.protobuf.Empty
72, // 101: grpc.Bridge.Hostname:input_type -> google.protobuf.Empty 72, // 101: grpc.Bridge.MailServerSettings:input_type -> google.protobuf.Empty
74, // 102: grpc.Bridge.IsPortFree:input_type -> google.protobuf.Int32Value 12, // 102: grpc.Bridge.SetMailServerSettings:input_type -> grpc.ImapSmtpSettings
72, // 103: grpc.Bridge.AvailableKeychains:input_type -> google.protobuf.Empty 72, // 103: grpc.Bridge.Hostname:input_type -> google.protobuf.Empty
71, // 104: grpc.Bridge.SetCurrentKeychain:input_type -> google.protobuf.StringValue 74, // 104: grpc.Bridge.IsPortFree:input_type -> google.protobuf.Int32Value
72, // 105: grpc.Bridge.CurrentKeychain:input_type -> google.protobuf.Empty 72, // 105: grpc.Bridge.AvailableKeychains:input_type -> google.protobuf.Empty
72, // 106: grpc.Bridge.GetUserList:input_type -> google.protobuf.Empty 71, // 106: grpc.Bridge.SetCurrentKeychain:input_type -> google.protobuf.StringValue
71, // 107: grpc.Bridge.GetUser:input_type -> google.protobuf.StringValue 72, // 107: grpc.Bridge.CurrentKeychain:input_type -> google.protobuf.Empty
15, // 108: grpc.Bridge.SetUserSplitMode:input_type -> grpc.UserSplitModeRequest 72, // 108: grpc.Bridge.GetUserList:input_type -> google.protobuf.Empty
16, // 109: grpc.Bridge.SendBadEventUserFeedback:input_type -> grpc.UserBadEventFeedbackRequest 71, // 109: grpc.Bridge.GetUser:input_type -> google.protobuf.StringValue
71, // 110: grpc.Bridge.LogoutUser:input_type -> google.protobuf.StringValue 15, // 110: grpc.Bridge.SetUserSplitMode:input_type -> grpc.UserSplitModeRequest
71, // 111: grpc.Bridge.RemoveUser:input_type -> google.protobuf.StringValue 16, // 111: grpc.Bridge.SendBadEventUserFeedback:input_type -> grpc.UserBadEventFeedbackRequest
18, // 112: grpc.Bridge.ConfigureUserAppleMail:input_type -> grpc.ConfigureAppleMailRequest 71, // 112: grpc.Bridge.LogoutUser:input_type -> google.protobuf.StringValue
19, // 113: grpc.Bridge.RunEventStream:input_type -> grpc.EventStreamRequest 71, // 113: grpc.Bridge.RemoveUser:input_type -> google.protobuf.StringValue
72, // 114: grpc.Bridge.StopEventStream:input_type -> google.protobuf.Empty 18, // 114: grpc.Bridge.ConfigureUserAppleMail:input_type -> grpc.ConfigureAppleMailRequest
71, // 115: grpc.Bridge.CheckTokens:output_type -> google.protobuf.StringValue 19, // 115: grpc.Bridge.RunEventStream:input_type -> grpc.EventStreamRequest
72, // 116: grpc.Bridge.AddLogEntry:output_type -> google.protobuf.Empty 72, // 116: grpc.Bridge.StopEventStream:input_type -> google.protobuf.Empty
8, // 117: grpc.Bridge.GuiReady:output_type -> grpc.GuiReadyResponse 71, // 117: grpc.Bridge.CheckTokens:output_type -> google.protobuf.StringValue
72, // 118: grpc.Bridge.Quit:output_type -> google.protobuf.Empty 72, // 118: grpc.Bridge.AddLogEntry:output_type -> google.protobuf.Empty
72, // 119: grpc.Bridge.Restart:output_type -> google.protobuf.Empty 8, // 119: grpc.Bridge.GuiReady:output_type -> grpc.GuiReadyResponse
73, // 120: grpc.Bridge.ShowOnStartup:output_type -> google.protobuf.BoolValue 72, // 120: grpc.Bridge.Quit:output_type -> google.protobuf.Empty
72, // 121: grpc.Bridge.SetIsAutostartOn:output_type -> google.protobuf.Empty 72, // 121: grpc.Bridge.Restart:output_type -> google.protobuf.Empty
73, // 122: grpc.Bridge.IsAutostartOn:output_type -> google.protobuf.BoolValue 73, // 122: grpc.Bridge.ShowOnStartup:output_type -> google.protobuf.BoolValue
72, // 123: grpc.Bridge.SetIsBetaEnabled:output_type -> google.protobuf.Empty 72, // 123: grpc.Bridge.SetIsAutostartOn:output_type -> google.protobuf.Empty
73, // 124: grpc.Bridge.IsBetaEnabled:output_type -> google.protobuf.BoolValue 73, // 124: grpc.Bridge.IsAutostartOn:output_type -> google.protobuf.BoolValue
72, // 125: grpc.Bridge.SetIsAllMailVisible:output_type -> google.protobuf.Empty 72, // 125: grpc.Bridge.SetIsBetaEnabled:output_type -> google.protobuf.Empty
73, // 126: grpc.Bridge.IsAllMailVisible:output_type -> google.protobuf.BoolValue 73, // 126: grpc.Bridge.IsBetaEnabled:output_type -> google.protobuf.BoolValue
71, // 127: grpc.Bridge.GoOs:output_type -> google.protobuf.StringValue 72, // 127: grpc.Bridge.SetIsAllMailVisible:output_type -> google.protobuf.Empty
72, // 128: grpc.Bridge.TriggerReset:output_type -> google.protobuf.Empty 73, // 128: grpc.Bridge.IsAllMailVisible:output_type -> google.protobuf.BoolValue
71, // 129: grpc.Bridge.Version:output_type -> google.protobuf.StringValue 72, // 129: grpc.Bridge.SetIsTelemetryDisabled:output_type -> google.protobuf.Empty
71, // 130: grpc.Bridge.LogsPath:output_type -> google.protobuf.StringValue 73, // 130: grpc.Bridge.IsTelemetryDisabled:output_type -> google.protobuf.BoolValue
71, // 131: grpc.Bridge.LicensePath:output_type -> google.protobuf.StringValue 71, // 131: grpc.Bridge.GoOs:output_type -> google.protobuf.StringValue
71, // 132: grpc.Bridge.ReleaseNotesPageLink:output_type -> google.protobuf.StringValue 72, // 132: grpc.Bridge.TriggerReset:output_type -> google.protobuf.Empty
71, // 133: grpc.Bridge.DependencyLicensesLink:output_type -> google.protobuf.StringValue 71, // 133: grpc.Bridge.Version:output_type -> google.protobuf.StringValue
71, // 134: grpc.Bridge.LandingPageLink:output_type -> google.protobuf.StringValue 71, // 134: grpc.Bridge.LogsPath:output_type -> google.protobuf.StringValue
72, // 135: grpc.Bridge.SetColorSchemeName:output_type -> google.protobuf.Empty 71, // 135: grpc.Bridge.LicensePath:output_type -> google.protobuf.StringValue
71, // 136: grpc.Bridge.ColorSchemeName:output_type -> google.protobuf.StringValue 71, // 136: grpc.Bridge.ReleaseNotesPageLink:output_type -> google.protobuf.StringValue
71, // 137: grpc.Bridge.CurrentEmailClient:output_type -> google.protobuf.StringValue 71, // 137: grpc.Bridge.DependencyLicensesLink:output_type -> google.protobuf.StringValue
72, // 138: grpc.Bridge.ReportBug:output_type -> google.protobuf.Empty 71, // 138: grpc.Bridge.LandingPageLink:output_type -> google.protobuf.StringValue
72, // 139: grpc.Bridge.ExportTLSCertificates:output_type -> google.protobuf.Empty 72, // 139: grpc.Bridge.SetColorSchemeName:output_type -> google.protobuf.Empty
72, // 140: grpc.Bridge.ForceLauncher:output_type -> google.protobuf.Empty 71, // 140: grpc.Bridge.ColorSchemeName:output_type -> google.protobuf.StringValue
72, // 141: grpc.Bridge.SetMainExecutable:output_type -> google.protobuf.Empty 71, // 141: grpc.Bridge.CurrentEmailClient:output_type -> google.protobuf.StringValue
72, // 142: grpc.Bridge.Login:output_type -> google.protobuf.Empty 72, // 142: grpc.Bridge.ReportBug:output_type -> google.protobuf.Empty
72, // 143: grpc.Bridge.Login2FA:output_type -> google.protobuf.Empty 72, // 143: grpc.Bridge.ExportTLSCertificates:output_type -> google.protobuf.Empty
72, // 144: grpc.Bridge.Login2Passwords:output_type -> google.protobuf.Empty 72, // 144: grpc.Bridge.ForceLauncher:output_type -> google.protobuf.Empty
72, // 145: grpc.Bridge.LoginAbort:output_type -> google.protobuf.Empty 72, // 145: grpc.Bridge.SetMainExecutable:output_type -> google.protobuf.Empty
72, // 146: grpc.Bridge.CheckUpdate:output_type -> google.protobuf.Empty 72, // 146: grpc.Bridge.Login:output_type -> google.protobuf.Empty
72, // 147: grpc.Bridge.InstallUpdate:output_type -> google.protobuf.Empty 72, // 147: grpc.Bridge.Login2FA:output_type -> google.protobuf.Empty
72, // 148: grpc.Bridge.SetIsAutomaticUpdateOn:output_type -> google.protobuf.Empty 72, // 148: grpc.Bridge.Login2Passwords:output_type -> google.protobuf.Empty
73, // 149: grpc.Bridge.IsAutomaticUpdateOn:output_type -> google.protobuf.BoolValue 72, // 149: grpc.Bridge.LoginAbort:output_type -> google.protobuf.Empty
71, // 150: grpc.Bridge.DiskCachePath:output_type -> google.protobuf.StringValue 72, // 150: grpc.Bridge.CheckUpdate:output_type -> google.protobuf.Empty
72, // 151: grpc.Bridge.SetDiskCachePath:output_type -> google.protobuf.Empty 72, // 151: grpc.Bridge.InstallUpdate:output_type -> google.protobuf.Empty
72, // 152: grpc.Bridge.SetIsDoHEnabled:output_type -> google.protobuf.Empty 72, // 152: grpc.Bridge.SetIsAutomaticUpdateOn:output_type -> google.protobuf.Empty
73, // 153: grpc.Bridge.IsDoHEnabled:output_type -> google.protobuf.BoolValue 73, // 153: grpc.Bridge.IsAutomaticUpdateOn:output_type -> google.protobuf.BoolValue
12, // 154: grpc.Bridge.MailServerSettings:output_type -> grpc.ImapSmtpSettings 71, // 154: grpc.Bridge.DiskCachePath:output_type -> google.protobuf.StringValue
72, // 155: grpc.Bridge.SetMailServerSettings:output_type -> google.protobuf.Empty 72, // 155: grpc.Bridge.SetDiskCachePath:output_type -> google.protobuf.Empty
71, // 156: grpc.Bridge.Hostname:output_type -> google.protobuf.StringValue 72, // 156: grpc.Bridge.SetIsDoHEnabled:output_type -> google.protobuf.Empty
73, // 157: grpc.Bridge.IsPortFree:output_type -> google.protobuf.BoolValue 73, // 157: grpc.Bridge.IsDoHEnabled:output_type -> google.protobuf.BoolValue
13, // 158: grpc.Bridge.AvailableKeychains:output_type -> grpc.AvailableKeychainsResponse 12, // 158: grpc.Bridge.MailServerSettings:output_type -> grpc.ImapSmtpSettings
72, // 159: grpc.Bridge.SetCurrentKeychain:output_type -> google.protobuf.Empty 72, // 159: grpc.Bridge.SetMailServerSettings:output_type -> google.protobuf.Empty
71, // 160: grpc.Bridge.CurrentKeychain:output_type -> google.protobuf.StringValue 71, // 160: grpc.Bridge.Hostname:output_type -> google.protobuf.StringValue
17, // 161: grpc.Bridge.GetUserList:output_type -> grpc.UserListResponse 73, // 161: grpc.Bridge.IsPortFree:output_type -> google.protobuf.BoolValue
14, // 162: grpc.Bridge.GetUser:output_type -> grpc.User 13, // 162: grpc.Bridge.AvailableKeychains:output_type -> grpc.AvailableKeychainsResponse
72, // 163: grpc.Bridge.SetUserSplitMode:output_type -> google.protobuf.Empty 72, // 163: grpc.Bridge.SetCurrentKeychain:output_type -> google.protobuf.Empty
72, // 164: grpc.Bridge.SendBadEventUserFeedback:output_type -> google.protobuf.Empty 71, // 164: grpc.Bridge.CurrentKeychain:output_type -> google.protobuf.StringValue
72, // 165: grpc.Bridge.LogoutUser:output_type -> google.protobuf.Empty 17, // 165: grpc.Bridge.GetUserList:output_type -> grpc.UserListResponse
72, // 166: grpc.Bridge.RemoveUser:output_type -> google.protobuf.Empty 14, // 166: grpc.Bridge.GetUser:output_type -> grpc.User
72, // 167: grpc.Bridge.ConfigureUserAppleMail:output_type -> google.protobuf.Empty 72, // 167: grpc.Bridge.SetUserSplitMode:output_type -> google.protobuf.Empty
20, // 168: grpc.Bridge.RunEventStream:output_type -> grpc.StreamEvent 72, // 168: grpc.Bridge.SendBadEventUserFeedback:output_type -> google.protobuf.Empty
72, // 169: grpc.Bridge.StopEventStream:output_type -> google.protobuf.Empty 72, // 169: grpc.Bridge.LogoutUser:output_type -> google.protobuf.Empty
115, // [115:170] is the sub-list for method output_type 72, // 170: grpc.Bridge.RemoveUser:output_type -> google.protobuf.Empty
60, // [60:115] is the sub-list for method input_type 72, // 171: grpc.Bridge.ConfigureUserAppleMail:output_type -> google.protobuf.Empty
20, // 172: grpc.Bridge.RunEventStream:output_type -> grpc.StreamEvent
72, // 173: grpc.Bridge.StopEventStream:output_type -> google.protobuf.Empty
117, // [117:174] is the sub-list for method output_type
60, // [60:117] is the sub-list for method input_type
60, // [60:60] is the sub-list for extension type_name 60, // [60:60] is the sub-list for extension type_name
60, // [60:60] is the sub-list for extension extendee 60, // [60:60] is the sub-list for extension extendee
0, // [0:60] is the sub-list for field type_name 0, // [0:60] is the sub-list for field type_name

View File

@ -42,6 +42,8 @@ service Bridge {
rpc IsBetaEnabled(google.protobuf.Empty) returns (google.protobuf.BoolValue); rpc IsBetaEnabled(google.protobuf.Empty) returns (google.protobuf.BoolValue);
rpc SetIsAllMailVisible(google.protobuf.BoolValue) returns (google.protobuf.Empty); rpc SetIsAllMailVisible(google.protobuf.BoolValue) returns (google.protobuf.Empty);
rpc IsAllMailVisible(google.protobuf.Empty) returns (google.protobuf.BoolValue); rpc IsAllMailVisible(google.protobuf.Empty) returns (google.protobuf.BoolValue);
rpc SetIsTelemetryDisabled(google.protobuf.BoolValue) returns (google.protobuf.Empty);
rpc IsTelemetryDisabled(google.protobuf.Empty) returns (google.protobuf.BoolValue);
rpc GoOs(google.protobuf.Empty) returns (google.protobuf.StringValue); rpc GoOs(google.protobuf.Empty) returns (google.protobuf.StringValue);
rpc TriggerReset(google.protobuf.Empty) returns (google.protobuf.Empty); rpc TriggerReset(google.protobuf.Empty) returns (google.protobuf.Empty);
rpc Version(google.protobuf.Empty) returns (google.protobuf.StringValue); rpc Version(google.protobuf.Empty) returns (google.protobuf.StringValue);

File diff suppressed because it is too large Load Diff

View File

@ -214,6 +214,23 @@ func (s *Service) IsAllMailVisible(ctx context.Context, _ *emptypb.Empty) (*wrap
return wrapperspb.Bool(s.bridge.GetShowAllMail()), nil return wrapperspb.Bool(s.bridge.GetShowAllMail()), nil
} }
func (s *Service) SetIsTelemetryDisabled(_ context.Context, isDisabled *wrapperspb.BoolValue) (*emptypb.Empty, error) {
s.log.WithField("isEnabled", isDisabled.Value).Debug("SetIsTelemetryDisabled")
if err := s.bridge.SetTelemetryDisabled(isDisabled.Value); err != nil {
s.log.WithError(err).Error("Failed to set telemetry status")
return nil, status.Errorf(codes.Internal, "failed to set telemetry status: %v", err)
}
return &emptypb.Empty{}, nil
}
func (s *Service) IsTelemetryDisabled(_ context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) {
s.log.Debug("IsTelemetryDisabled")
return wrapperspb.Bool(s.bridge.GetTelemetryDisabled()), nil
}
func (s *Service) GoOs(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) { func (s *Service) GoOs(ctx context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
s.log.Debug("GoOs") // TO-DO We can probably get rid of this and use QSysInfo::product name s.log.Debug("GoOs") // TO-DO We can probably get rid of this and use QSysInfo::product name

View File

@ -15,14 +15,18 @@
// You should have received a copy of the GNU General Public License // 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/>. // along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQml.Models //go:build !windows
// +build !windows
ListModel { package sentry
// overriding get method to ignore any role and return directly object itself
function get(row) { import "os"
if (row < 0 || row >= count) {
return undefined func GetSystemLang() string {
} lang := os.Getenv("LC_ALL")
return data(index(row, 0), Qt.DisplayRole) if lang == "" {
} lang = os.Getenv("LANG")
}
return lang
} }

View File

@ -0,0 +1,67 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
//go:build windows
// +build windows
package sentry
import (
"syscall"
"unsafe"
)
const (
defaultLocaleUser = "GetUserDefaultLocaleName" // https://learn.microsoft.com/en-us/windows/win32/api/winnls/nf-winnls-getuserdefaultlocalename
defaultLocaleSystem = "GetSystemDefaultLocaleName" // https://learn.microsoft.com/en-us/windows/win32/api/winnls/nf-winnls-getsystemdefaultlocalename
localeNameMaxLength = 85 // https://learn.microsoft.com/en-us/windows/win32/intl/locale-name-constants
)
func getLocale(dll *syscall.DLL, procName string) (string, error) {
proc, err := dll.FindProc(procName)
if err != nil {
return "errProc", err
}
b := make([]uint16, localeNameMaxLength)
r, _, err := proc.Call(uintptr(unsafe.Pointer(&b[0])), uintptr(localeNameMaxLength))
if r == 0 || err != nil {
return "errCall", err
}
return syscall.UTF16ToString(b), nil
}
func GetSystemLang() string {
dll, err := syscall.LoadDLL("kernel32")
if err != nil {
return "errDll"
}
defer func() {
_ = dll.Release()
}()
if lang, err := getLocale(dll, defaultLocaleUser); err == nil {
return lang
}
lang, _ := getLocale(dll, defaultLocaleSystem)
return lang
}

View File

@ -18,7 +18,6 @@
package sentry package sentry
import ( import (
"crypto/sha256"
"errors" "errors"
"fmt" "fmt"
"log" "log"
@ -29,6 +28,7 @@ import (
"github.com/Masterminds/semver/v3" "github.com/Masterminds/semver/v3"
"github.com/ProtonMail/gluon/reporter" "github.com/ProtonMail/gluon/reporter"
"github.com/ProtonMail/proton-bridge/v3/internal/constants" "github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/pkg/algo"
"github.com/ProtonMail/proton-bridge/v3/pkg/restarter" "github.com/ProtonMail/proton-bridge/v3/pkg/restarter"
"github.com/getsentry/sentry-go" "github.com/getsentry/sentry-go"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -50,7 +50,7 @@ func init() { //nolint:gochecknoinits
Release: constants.AppVersion(appVersion), Release: constants.AppVersion(appVersion),
BeforeSend: EnhanceSentryEvent, BeforeSend: EnhanceSentryEvent,
Transport: sentrySyncTransport, Transport: sentrySyncTransport,
ServerName: getProtectedHostname(), ServerName: GetProtectedHostname(),
Environment: constants.BuildEnv, Environment: constants.BuildEnv,
MaxBreadcrumbs: 50, MaxBreadcrumbs: 50,
} }
@ -61,7 +61,7 @@ func init() { //nolint:gochecknoinits
sentry.ConfigureScope(func(scope *sentry.Scope) { sentry.ConfigureScope(func(scope *sentry.Scope) {
scope.SetFingerprint([]string{"{{ default }}"}) scope.SetFingerprint([]string{"{{ default }}"})
scope.SetUser(sentry.User{ID: getProtectedHostname()}) scope.SetUser(sentry.User{ID: GetProtectedHostname()})
}) })
sentry.Logger = log.New( sentry.Logger = log.New(
@ -81,12 +81,17 @@ type Identifier interface {
GetUserAgent() string GetUserAgent() string
} }
func getProtectedHostname() string { func GetProtectedHostname() string {
hostname, err := os.Hostname() hostname, err := os.Hostname()
if err != nil { if err != nil {
return "Unknown" return "Unknown"
} }
return fmt.Sprintf("%x", sha256.Sum256([]byte(hostname))) return algo.HashBase64SHA256(hostname)
}
func GetTimeZone() string {
zone, offset := time.Now().Zone()
return fmt.Sprintf("%s%+d", zone, offset/3600)
} }
// NewReporter creates new sentry reporter with appName and appVersion to report. // NewReporter creates new sentry reporter with appName and appVersion to report.

View File

@ -0,0 +1,172 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package telemetry
import (
"strconv"
"time"
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
"github.com/sirupsen/logrus"
)
func NewHeartbeat(manager HeartbeatManager, imapPort, smtpPort int, cacheDir, keychain string) Heartbeat {
heartbeat := Heartbeat{
log: logrus.WithField("pkg", "telemetry"),
manager: manager,
metrics: HeartbeatData{
MeasurementGroup: "bridge.any.usage",
Event: "bridge_heartbeat",
},
defaultIMAPPort: imapPort,
defaultSMTPPort: smtpPort,
defaultCache: cacheDir,
defaultKeychain: keychain,
}
return heartbeat
}
func (heartbeat *Heartbeat) SetRollout(val float64) {
heartbeat.metrics.Dimensions.Rollout = strconv.Itoa(int(val * 100))
}
func (heartbeat *Heartbeat) SetNbAccount(val int) {
heartbeat.metrics.Values.NbAccount = val
}
func (heartbeat *Heartbeat) SetAutoUpdate(val bool) {
if val {
heartbeat.metrics.Dimensions.AutoUpdate = dimensionON
} else {
heartbeat.metrics.Dimensions.AutoUpdate = dimensionOFF
}
}
func (heartbeat *Heartbeat) SetAutoStart(val bool) {
if val {
heartbeat.metrics.Dimensions.AutoStart = dimensionON
} else {
heartbeat.metrics.Dimensions.AutoStart = dimensionOFF
}
}
func (heartbeat *Heartbeat) SetBeta(val updater.Channel) {
if val == updater.EarlyChannel {
heartbeat.metrics.Dimensions.Beta = dimensionON
} else {
heartbeat.metrics.Dimensions.Beta = dimensionOFF
}
}
func (heartbeat *Heartbeat) SetDoh(val bool) {
if val {
heartbeat.metrics.Dimensions.Doh = dimensionON
} else {
heartbeat.metrics.Dimensions.Doh = dimensionOFF
}
}
func (heartbeat *Heartbeat) SetSplitMode(val bool) {
if val {
heartbeat.metrics.Dimensions.SplitMode = dimensionON
} else {
heartbeat.metrics.Dimensions.SplitMode = dimensionOFF
}
}
func (heartbeat *Heartbeat) SetShowAllMail(val bool) {
if val {
heartbeat.metrics.Dimensions.ShowAllMail = dimensionON
} else {
heartbeat.metrics.Dimensions.ShowAllMail = dimensionOFF
}
}
func (heartbeat *Heartbeat) SetIMAPConnectionMode(val bool) {
if val {
heartbeat.metrics.Dimensions.IMAPConnectionMode = dimensionSSL
} else {
heartbeat.metrics.Dimensions.IMAPConnectionMode = dimensionStartTLS
}
}
func (heartbeat *Heartbeat) SetSMTPConnectionMode(val bool) {
if val {
heartbeat.metrics.Dimensions.SMTPConnectionMode = dimensionSSL
} else {
heartbeat.metrics.Dimensions.SMTPConnectionMode = dimensionStartTLS
}
}
func (heartbeat *Heartbeat) SetIMAPPort(val int) {
if val == heartbeat.defaultIMAPPort {
heartbeat.metrics.Dimensions.IMAPPort = dimensionDefault
} else {
heartbeat.metrics.Dimensions.IMAPPort = dimensionCustom
}
}
func (heartbeat *Heartbeat) SetSMTPPort(val int) {
if val == heartbeat.defaultSMTPPort {
heartbeat.metrics.Dimensions.SMTPPort = dimensionDefault
} else {
heartbeat.metrics.Dimensions.SMTPPort = dimensionCustom
}
}
func (heartbeat *Heartbeat) SetCacheLocation(val string) {
if val == heartbeat.defaultCache {
heartbeat.metrics.Dimensions.CacheLocation = dimensionDefault
} else {
heartbeat.metrics.Dimensions.CacheLocation = dimensionCustom
}
}
func (heartbeat *Heartbeat) SetKeyChainPref(val string) {
if val == heartbeat.defaultKeychain {
heartbeat.metrics.Dimensions.KeychainPref = dimensionDefault
} else {
heartbeat.metrics.Dimensions.KeychainPref = dimensionCustom
}
}
func (heartbeat *Heartbeat) SetPrevVersion(val string) {
heartbeat.metrics.Dimensions.PrevVersion = val
}
func (heartbeat *Heartbeat) TrySending() {
if heartbeat.manager.IsTelemetryAvailable() {
lastSent := heartbeat.manager.GetLastHeartbeatSent()
now := time.Now()
if now.Year() > lastSent.Year() || (now.Year() == lastSent.Year() && now.YearDay() > lastSent.YearDay()) {
if !heartbeat.manager.SendHeartbeat(&heartbeat.metrics) {
heartbeat.log.WithFields(logrus.Fields{
"metrics": heartbeat.metrics,
}).Error("Failed to send heartbeat")
return
}
heartbeat.log.WithFields(logrus.Fields{
"metrics": heartbeat.metrics,
}).Info("Heartbeat sent")
if err := heartbeat.manager.SetLastHeartbeatSent(now); err != nil {
heartbeat.log.WithError(err).Warn("Cannot save last heartbeat sent to the vault.")
}
}
}
}

View File

@ -0,0 +1,97 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package telemetry_test
import (
"testing"
"time"
"github.com/ProtonMail/proton-bridge/v3/internal/telemetry"
"github.com/ProtonMail/proton-bridge/v3/internal/telemetry/mocks"
"github.com/golang/mock/gomock"
)
func TestHeartbeat_default_heartbeat(t *testing.T) {
withHeartbeat(t, 1143, 1025, "/tmp", "defaultKeychain", func(hb *telemetry.Heartbeat, mock *mocks.MockHeartbeatManager) {
data := telemetry.HeartbeatData{
MeasurementGroup: "bridge.any.usage",
Event: "bridge_heartbeat",
Values: telemetry.HeartbeatValues{
NbAccount: 1,
},
Dimensions: telemetry.HeartbeatDimensions{
AutoUpdate: "on",
AutoStart: "on",
Beta: "off",
Doh: "off",
SplitMode: "off",
ShowAllMail: "off",
IMAPConnectionMode: "ssl",
SMTPConnectionMode: "ssl",
IMAPPort: "default",
SMTPPort: "default",
CacheLocation: "default",
KeychainPref: "default",
PrevVersion: "1.2.3",
Rollout: "10",
},
}
mock.EXPECT().IsTelemetryAvailable().Return(true)
mock.EXPECT().GetLastHeartbeatSent().Return(time.Date(2022, 6, 4, 0, 0, 0, 0, time.UTC))
mock.EXPECT().SendHeartbeat(&data).Return(true)
mock.EXPECT().SetLastHeartbeatSent(gomock.Any()).Return(nil)
hb.TrySending()
})
}
func TestHeartbeat_already_sent_heartbeat(t *testing.T) {
withHeartbeat(t, 1143, 1025, "/tmp", "defaultKeychain", func(hb *telemetry.Heartbeat, mock *mocks.MockHeartbeatManager) {
mock.EXPECT().IsTelemetryAvailable().Return(true)
mock.EXPECT().GetLastHeartbeatSent().Return(time.Now().Truncate(24 * time.Hour))
hb.TrySending()
})
}
func withHeartbeat(t *testing.T, imap, smtp int, cache, keychain string, tests func(hb *telemetry.Heartbeat, mock *mocks.MockHeartbeatManager)) {
ctl := gomock.NewController(t)
defer ctl.Finish()
manager := mocks.NewMockHeartbeatManager(ctl)
heartbeat := telemetry.NewHeartbeat(manager, imap, smtp, cache, keychain)
heartbeat.SetRollout(0.1)
heartbeat.SetNbAccount(1)
heartbeat.SetSplitMode(false)
heartbeat.SetAutoStart(true)
heartbeat.SetAutoUpdate(true)
heartbeat.SetBeta("stable")
heartbeat.SetDoh(false)
heartbeat.SetShowAllMail(false)
heartbeat.SetIMAPConnectionMode(true)
heartbeat.SetSMTPConnectionMode(true)
heartbeat.SetIMAPPort(1143)
heartbeat.SetSMTPPort(1025)
heartbeat.SetCacheLocation("/tmp")
heartbeat.SetKeyChainPref("defaultKeychain")
heartbeat.SetPrevVersion("1.2.3")
tests(&heartbeat, manager)
}

View File

@ -0,0 +1,92 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/ProtonMail/proton-bridge/v3/internal/telemetry (interfaces: HeartbeatManager)
// Package mocks is a generated GoMock package.
package mocks
import (
reflect "reflect"
time "time"
telemetry "github.com/ProtonMail/proton-bridge/v3/internal/telemetry"
gomock "github.com/golang/mock/gomock"
)
// MockHeartbeatManager is a mock of HeartbeatManager interface.
type MockHeartbeatManager struct {
ctrl *gomock.Controller
recorder *MockHeartbeatManagerMockRecorder
}
// MockHeartbeatManagerMockRecorder is the mock recorder for MockHeartbeatManager.
type MockHeartbeatManagerMockRecorder struct {
mock *MockHeartbeatManager
}
// NewMockHeartbeatManager creates a new mock instance.
func NewMockHeartbeatManager(ctrl *gomock.Controller) *MockHeartbeatManager {
mock := &MockHeartbeatManager{ctrl: ctrl}
mock.recorder = &MockHeartbeatManagerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockHeartbeatManager) EXPECT() *MockHeartbeatManagerMockRecorder {
return m.recorder
}
// GetLastHeartbeatSent mocks base method.
func (m *MockHeartbeatManager) GetLastHeartbeatSent() time.Time {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetLastHeartbeatSent")
ret0, _ := ret[0].(time.Time)
return ret0
}
// GetLastHeartbeatSent indicates an expected call of GetLastHeartbeatSent.
func (mr *MockHeartbeatManagerMockRecorder) GetLastHeartbeatSent() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLastHeartbeatSent", reflect.TypeOf((*MockHeartbeatManager)(nil).GetLastHeartbeatSent))
}
// IsTelemetryAvailable mocks base method.
func (m *MockHeartbeatManager) IsTelemetryAvailable() bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IsTelemetryAvailable")
ret0, _ := ret[0].(bool)
return ret0
}
// IsTelemetryAvailable indicates an expected call of IsTelemetryAvailable.
func (mr *MockHeartbeatManagerMockRecorder) IsTelemetryAvailable() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsTelemetryAvailable", reflect.TypeOf((*MockHeartbeatManager)(nil).IsTelemetryAvailable))
}
// SendHeartbeat mocks base method.
func (m *MockHeartbeatManager) SendHeartbeat(arg0 *telemetry.HeartbeatData) bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SendHeartbeat", arg0)
ret0, _ := ret[0].(bool)
return ret0
}
// SendHeartbeat indicates an expected call of SendHeartbeat.
func (mr *MockHeartbeatManagerMockRecorder) SendHeartbeat(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendHeartbeat", reflect.TypeOf((*MockHeartbeatManager)(nil).SendHeartbeat), arg0)
}
// SetLastHeartbeatSent mocks base method.
func (m *MockHeartbeatManager) SetLastHeartbeatSent(arg0 time.Time) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetLastHeartbeatSent", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// SetLastHeartbeatSent indicates an expected call of SetLastHeartbeatSent.
func (mr *MockHeartbeatManagerMockRecorder) SetLastHeartbeatSent(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLastHeartbeatSent", reflect.TypeOf((*MockHeartbeatManager)(nil).SetLastHeartbeatSent), arg0)
}

View File

@ -0,0 +1,79 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package telemetry
import (
"time"
"github.com/sirupsen/logrus"
)
const (
dimensionON = "on"
dimensionOFF = "off"
dimensionDefault = "default"
dimensionCustom = "custom"
dimensionSSL = "ssl"
dimensionStartTLS = "starttls"
)
type HeartbeatManager interface {
IsTelemetryAvailable() bool
SendHeartbeat(heartbeat *HeartbeatData) bool
GetLastHeartbeatSent() time.Time
SetLastHeartbeatSent(time.Time) error
}
type HeartbeatValues struct {
NbAccount int `json:"nb_account"`
}
type HeartbeatDimensions struct {
AutoUpdate string `json:"auto_update"`
AutoStart string `json:"auto_start"`
Beta string `json:"beta"`
Doh string `json:"doh"`
SplitMode string `json:"split_mode"`
ShowAllMail string `json:"show_all_mail"`
IMAPConnectionMode string `json:"imap_connection_mode"`
SMTPConnectionMode string `json:"smtp_connection_mode"`
IMAPPort string `json:"imap_port"`
SMTPPort string `json:"smtp_port"`
CacheLocation string `json:"cache_location"`
KeychainPref string `json:"keychain_pref"`
PrevVersion string `json:"prev_version"`
Rollout string `json:"rollout"`
}
type HeartbeatData struct {
MeasurementGroup string
Event string
Values HeartbeatValues
Dimensions HeartbeatDimensions
}
type Heartbeat struct {
log *logrus.Entry
manager HeartbeatManager
metrics HeartbeatData
defaultIMAPPort int
defaultSMTPPort int
defaultCache string
defaultKeychain string
}

View File

@ -650,15 +650,28 @@ func (user *User) handleUpdateMessageEvent(ctx context.Context, message proton.M
"subject": logging.Sensitive(message.Subject), "subject": logging.Sensitive(message.Subject),
}).Info("Handling message updated event") }).Info("Handling message updated event")
flags := imap.NewFlagSet()
if message.Seen() {
flags.AddToSelf(imap.FlagSeen)
}
if message.Starred() {
flags.AddToSelf(imap.FlagFlagged)
}
if message.IsDraft() {
flags.AddToSelf(imap.FlagDraft)
}
if message.IsRepliedAll == true || message.IsReplied == true { //nolint: gosimple
flags.AddToSelf(imap.FlagAnswered)
}
update := imap.NewMessageMailboxesUpdated( update := imap.NewMessageMailboxesUpdated(
imap.MessageID(message.ID), imap.MessageID(message.ID),
mapTo[string, imap.MailboxID](wantLabels(user.apiLabels, message.LabelIDs)), mapTo[string, imap.MailboxID](wantLabels(user.apiLabels, message.LabelIDs)),
imap.MessageCustomFlags{ flags,
Seen: message.Seen(),
Flagged: message.Starred(),
Draft: message.IsDraft(),
Answered: message.IsRepliedAll == true || message.IsReplied == true, //nolint: gosimple
},
) )
user.updateCh[message.AddressID].Enqueue(update) user.updateCh[message.AddressID].Enqueue(update)

View File

@ -20,6 +20,7 @@ package user
import ( import (
"bytes" "bytes"
"context" "context"
"errors"
"fmt" "fmt"
"net/mail" "net/mail"
"sync/atomic" "sync/atomic"
@ -350,7 +351,13 @@ func (conn *imapConnector) CreateMessage(
wantFlags = wantFlags.Add(proton.MessageFlagReplied) wantFlags = wantFlags.Add(proton.MessageFlagReplied)
} }
return conn.importMessage(ctx, literal, wantLabelIDs, wantFlags, unread) msg, literal, err := conn.importMessage(ctx, literal, wantLabelIDs, wantFlags, unread)
if err != nil && errors.Is(err, proton.ErrImportSizeExceeded) {
// Remap error so that Gluon does not put this message in the recovery mailbox.
err = fmt.Errorf("%v: %w", err, connector.ErrMessageSizeExceedsLimits)
}
return msg, literal, err
} }
func (conn *imapConnector) GetMessageLiteral(ctx context.Context, id imap.MessageID) ([]byte, error) { func (conn *imapConnector) GetMessageLiteral(ctx context.Context, id imap.MessageID) ([]byte, error) {
@ -400,32 +407,8 @@ func (conn *imapConnector) RemoveMessagesFromMailbox(ctx context.Context, messag
} }
if mailboxID == proton.TrashLabel || mailboxID == proton.DraftsLabel { if mailboxID == proton.TrashLabel || mailboxID == proton.DraftsLabel {
var metadata []proton.MessageMetadata if err := conn.client.DeleteMessage(ctx, xslices.Map(messageIDs, func(m imap.MessageID) string {
return string(m)
// There's currently no limit on how many IDs we can filter on,
// but to be nice to API, let's chunk it by 150.
for _, messageIDs := range xslices.Chunk(messageIDs, 150) {
m, err := conn.client.GetMessageMetadata(ctx, proton.MessageFilter{
ID: mapTo[imap.MessageID, string](messageIDs),
})
if err != nil {
return err
}
// If a message is not preset in any other label other than AllMail, AllDrafts and AllSent, it can be
// permanently deleted.
m = xslices.Filter(m, func(m proton.MessageMetadata) bool {
labelsThatMatter := xslices.Filter(m.LabelIDs, func(id string) bool {
return id != proton.AllDraftsLabel && id != proton.AllMailLabel && id != proton.AllSentLabel
})
return len(labelsThatMatter) == 0
})
metadata = append(metadata, m...)
}
if err := conn.client.DeleteMessage(ctx, xslices.Map(metadata, func(m proton.MessageMetadata) string {
return m.ID
})...); err != nil { })...); err != nil {
return err return err
} }
@ -626,7 +609,7 @@ func (conn *imapConnector) createDraft(ctx context.Context, literal []byte, addr
return proton.Message{}, fmt.Errorf("failed to create parser: %w", err) return proton.Message{}, fmt.Errorf("failed to create parser: %w", err)
} }
message, err := message.ParseWithParser(parser) message, err := message.ParseWithParser(parser, true)
if err != nil { if err != nil {
return proton.Message{}, fmt.Errorf("failed to parse message: %w", err) return proton.Message{}, fmt.Errorf("failed to parse message: %w", err)
} }

View File

@ -18,10 +18,7 @@
package user package user
import ( import (
"bytes"
"context" "context"
"crypto/sha256"
"encoding/base64"
"errors" "errors"
"fmt" "fmt"
"sync" "sync"
@ -219,73 +216,7 @@ func (h *sendRecorder) getWaitCh(hash string) (<-chan struct{}, bool) {
// - the Content-Disposition header of each (leaf) part, // - the Content-Disposition header of each (leaf) part,
// - the (decoded) body of each part. // - the (decoded) body of each part.
func getMessageHash(b []byte) (string, error) { func getMessageHash(b []byte) (string, error) {
section := rfc822.Parse(b) return rfc822.GetMessageHash(b)
header, err := section.ParseHeader()
if err != nil {
return "", err
}
h := sha256.New()
if _, err := h.Write([]byte(header.Get("Subject"))); err != nil {
return "", err
}
if _, err := h.Write([]byte(header.Get("From"))); err != nil {
return "", err
}
if _, err := h.Write([]byte(header.Get("To"))); err != nil {
return "", err
}
if _, err := h.Write([]byte(header.Get("Cc"))); err != nil {
return "", err
}
if _, err := h.Write([]byte(header.Get("Reply-To"))); err != nil {
return "", err
}
if _, err := h.Write([]byte(header.Get("In-Reply-To"))); err != nil {
return "", err
}
if err := section.Walk(func(section *rfc822.Section) error {
children, err := section.Children()
if err != nil {
return err
} else if len(children) > 0 {
return nil
}
header, err := section.ParseHeader()
if err != nil {
return err
}
if _, err := h.Write([]byte(header.Get("Content-Type"))); err != nil {
return err
}
if _, err := h.Write([]byte(header.Get("Content-Disposition"))); err != nil {
return err
}
body := section.Body()
body = bytes.ReplaceAll(body, []byte{'\r'}, nil)
body = bytes.TrimSpace(body)
if _, err := h.Write(body); err != nil {
return err
}
return nil
}); err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(h.Sum(nil)), nil
} }
func matchToList(a, b []string) bool { func matchToList(a, b []string) bool {

View File

@ -140,7 +140,7 @@ func (user *User) sendMail(authID string, from string, to []string, r io.Reader)
} }
// Parse the message we want to send (after we have attached the public key). // Parse the message we want to send (after we have attached the public key).
message, err := message.ParseWithParser(parser) message, err := message.ParseWithParser(parser, false)
if err != nil { if err != nil {
return fmt.Errorf("failed to parse message: %w", err) return fmt.Errorf("failed to parse message: %w", err)
} }
@ -300,7 +300,9 @@ func getParentID(
} }
// If no parent was found, try to find it in the last external reference. // If no parent was found, try to find it in the last external reference.
// There can be multiple messages with the same external ID; in this case, we don't pick any parent. // There can be multiple messages with the same external ID; in this case, we first look if
// there is a single one sent by this account (with the `MessageFlagSent` flag set), if yes,
// then pick that, otherwise don't pick any parent.
if parentID == "" && len(external) > 0 { if parentID == "" && len(external) > 0 {
var addrID string var addrID string
@ -316,8 +318,21 @@ func getParentID(
return "", fmt.Errorf("failed to get message metadata: %w", err) return "", fmt.Errorf("failed to get message metadata: %w", err)
} }
if len(metadata) == 1 { switch len(metadata) {
case 1:
// found exactly one parent
parentID = metadata[0].ID parentID = metadata[0].ID
case 0:
// found no parents
default:
// found multiple parents, search through metadata to try to find a singular parent that
// was sent by this account.
for _, metadata := range metadata {
if metadata.Flags.Has(proton.MessageFlagSent) {
parentID = metadata.ID
break
}
}
} }
} }

View File

@ -262,7 +262,7 @@ func (user *User) syncMessages(
syncStartTime := time.Now() syncStartTime := time.Now()
defer func() { logrus.WithField("duration", time.Since(syncStartTime)).Info("Message sync completed") }() defer func() { logrus.WithField("duration", time.Since(syncStartTime)).Info("Message sync completed") }()
logrus.WithFields(logrus.Fields{ user.log.WithFields(logrus.Fields{
"messages": len(messageIDs), "messages": len(messageIDs),
"numCPU": runtime.NumCPU(), "numCPU": runtime.NumCPU(),
}).Info("Starting message sync") }).Info("Starting message sync")

View File

@ -119,6 +119,12 @@ func New(
return nil, fmt.Errorf("failed to get labels: %w", err) return nil, fmt.Errorf("failed to get labels: %w", err)
} }
logrus.WithFields(logrus.Fields{
"userID": apiUser.ID,
"numAddr": len(apiAddrs),
"numLabels": len(apiLabels),
}).Info("Creating user object")
// Create the user object. // Create the user object.
user := &User{ user := &User{
log: logrus.WithField("userID", apiUser.ID), log: logrus.WithField("userID", apiUser.ID),
@ -591,6 +597,36 @@ func (user *User) Close() {
} }
} }
// IsTelemetryEnabled check if the telemetry is enabled or disabled for this user.
func (user *User) IsTelemetryEnabled(ctx context.Context) bool {
settings, err := user.client.GetUserSettings(ctx)
if err != nil {
user.log.WithError(err).Error("Failed to retrieve API user Settings")
return false
}
return settings.Telemetry == proton.SettingEnabled
}
// SendTelemetry send telemetry request.
func (user *User) SendTelemetry(ctx context.Context, data []byte) error {
var req proton.SendStatsReq
if err := json.Unmarshal(data, &req); err != nil {
user.log.WithError(err).Error("Failed to build telemetry request.")
if err := user.reporter.ReportMessageWithContext("Failed to build telemetry request.", reporter.Context{
"error": err,
}); err != nil {
logrus.WithError(err).Error("Failed to report telemetry request build error")
}
return err
}
err := user.client.SendDataEvent(ctx, req)
if err != nil {
user.log.WithError(err).Error("Failed to send telemetry.")
return err
}
return nil
}
// initUpdateCh initializes the user's update channels in the given address mode. // initUpdateCh initializes the user's update channels in the given address mode.
// It is assumed that user.apiAddrs and user.updateCh are already locked. // It is assumed that user.apiAddrs and user.updateCh are already locked.
func (user *User) initUpdateCh(mode vault.AddressMode) { func (user *User) initUpdateCh(mode vault.AddressMode) {

View File

@ -80,6 +80,23 @@ func TestUser_AddressMode(t *testing.T) {
}) })
} }
func TestUser_Telemetry(t *testing.T) {
withAPI(t, context.Background(), func(ctx context.Context, s *server.Server, m *proton.Manager) {
withAccount(t, s, "username", "password", []string{}, func(string, []string) {
withUser(t, ctx, s, m, "username", "password", func(user *User) {
// By default, user should have Telemetry enabled.
telemetry := user.IsTelemetryEnabled(ctx)
require.Equal(t, true, telemetry)
user.client.Close()
// If telemetry cannot be retrieved it is disabled.
telemetry = user.IsTelemetryEnabled(ctx)
require.Equal(t, false, telemetry)
})
})
})
}
func withAPI(_ testing.TB, ctx context.Context, fn func(context.Context, *server.Server, *proton.Manager)) { //nolint:revive func withAPI(_ testing.TB, ctx context.Context, fn func(context.Context, *server.Server, *proton.Manager)) { //nolint:revive
server := server.New() server := server.New()
defer server.Close() defer server.Close()

View File

@ -44,6 +44,20 @@ func (ua *UserAgent) SetClient(name, version string) {
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"))
} }
func (ua *UserAgent) SetClientString(client string) {
ua.lock.Lock()
defer ua.lock.Unlock()
ua.client = client
}
func (ua *UserAgent) GetClientString() string {
ua.lock.RLock()
defer ua.lock.RUnlock()
return ua.client
}
func (ua *UserAgent) HasClient() bool { func (ua *UserAgent) HasClient() bool {
ua.lock.RLock() ua.lock.RLock()
defer ua.lock.RUnlock() defer ua.lock.RUnlock()

View File

@ -20,6 +20,7 @@ package vault
import ( import (
"math" "math"
"math/rand" "math/rand"
"time"
"github.com/Masterminds/semver/v3" "github.com/Masterminds/semver/v3"
"github.com/ProtonMail/proton-bridge/v3/internal/updater" "github.com/ProtonMail/proton-bridge/v3/internal/updater"
@ -184,6 +185,18 @@ func (vault *Vault) SetAutoUpdate(autoUpdate bool) error {
}) })
} }
// GetTelemetryDisabled checks whether telemetry is disabled.
func (vault *Vault) GetTelemetryDisabled() bool {
return vault.get().Settings.TelemetryDisabled
}
// SetTelemetryDisabled sets whether telemetry is disabled.
func (vault *Vault) SetTelemetryDisabled(telemetryDisabled bool) error {
return vault.mod(func(data *Data) {
data.Settings.TelemetryDisabled = telemetryDisabled
})
}
// GetLastVersion returns the last version of the bridge that was run. // GetLastVersion returns the last version of the bridge that was run.
func (vault *Vault) GetLastVersion() *semver.Version { func (vault *Vault) GetLastVersion() *semver.Version {
return semver.MustParse(vault.get().Settings.LastVersion) return semver.MustParse(vault.get().Settings.LastVersion)
@ -225,3 +238,34 @@ func (vault *Vault) SetMaxSyncMemory(maxMemory uint64) error {
data.Settings.MaxSyncMemory = maxMemory data.Settings.MaxSyncMemory = maxMemory
}) })
} }
// GetLastUserAgent returns the last user agent recorded by bridge.
func (vault *Vault) GetLastUserAgent() string {
v := vault.get().Settings.LastUserAgent
// Handle case where there may be no value.
if len(v) == 0 {
v = DefaultUserAgent
}
return v
}
// SetLastUserAgent store the last user agent recorded by bridge.
func (vault *Vault) SetLastUserAgent(userAgent string) error {
return vault.mod(func(data *Data) {
data.Settings.LastUserAgent = userAgent
})
}
// GetLastHeartbeatSent returns the last time heartbeat was sent.
func (vault *Vault) GetLastHeartbeatSent() time.Time {
return vault.get().Settings.LastHeartbeatSent
}
// SetLastHeartbeatSent store the last time heartbeat was sent.
func (vault *Vault) SetLastHeartbeatSent(timestamp time.Time) error {
return vault.mod(func(data *Data) {
data.Settings.LastHeartbeatSent = timestamp
})
}

View File

@ -153,6 +153,20 @@ func TestVault_Settings_ShowAllMail(t *testing.T) {
require.Equal(t, false, s.GetShowAllMail()) require.Equal(t, false, s.GetShowAllMail())
} }
func TestVault_Settings_TelemetryDisabled(t *testing.T) {
// create a new test vault.
s := newVault(t)
// Check the default show all mail setting.
require.Equal(t, false, s.GetTelemetryDisabled())
// Modify the show all mail setting.
require.NoError(t, s.SetTelemetryDisabled(true))
// Check the new show all mail setting.
require.Equal(t, true, s.GetTelemetryDisabled())
}
func TestVault_Settings_Autostart(t *testing.T) { func TestVault_Settings_Autostart(t *testing.T) {
// create a new test vault. // create a new test vault.
s := newVault(t) s := newVault(t)
@ -216,3 +230,11 @@ func TestVault_Settings_MaxSyncMemory(t *testing.T) {
// Check the default first start value. // Check the default first start value.
require.Equal(t, vault.DefaultMaxSyncMemory, s.GetMaxSyncMemory()) require.Equal(t, vault.DefaultMaxSyncMemory, s.GetMaxSyncMemory())
} }
func TestVault_Settings_LastUserAgent(t *testing.T) {
// create a new test vault.
s := newVault(t)
// Check the default first start value.
require.Equal(t, vault.DefaultUserAgent, s.GetLastUserAgent())
}

View File

@ -20,8 +20,10 @@ package vault
import ( import (
"math/rand" "math/rand"
"runtime" "runtime"
"time"
"github.com/ProtonMail/proton-bridge/v3/internal/updater" "github.com/ProtonMail/proton-bridge/v3/internal/updater"
"github.com/ProtonMail/proton-bridge/v3/pkg/ports"
) )
type Settings struct { type Settings struct {
@ -35,23 +37,29 @@ type Settings struct {
UpdateChannel updater.Channel UpdateChannel updater.Channel
UpdateRollout float64 UpdateRollout float64
ColorScheme string ColorScheme string
ProxyAllowed bool ProxyAllowed bool
ShowAllMail bool ShowAllMail bool
Autostart bool Autostart bool
AutoUpdate bool AutoUpdate bool
TelemetryDisabled bool
LastVersion string LastVersion string
FirstStart bool FirstStart bool
MaxSyncMemory uint64 MaxSyncMemory uint64
LastUserAgent string
LastHeartbeatSent time.Time
// **WARNING**: These entry can't be removed until they vault has proper migration support. // **WARNING**: These entry can't be removed until they vault has proper migration support.
SyncWorkers int SyncWorkers int
SyncAttPool int SyncAttPool int
} }
const DefaultMaxSyncMemory = 2 * 1024 * uint64(1024*1024) const DefaultMaxSyncMemory = 2 * 1024 * uint64(1024*1024)
const DefaultUserAgent = "UnknownClient/0.0.1"
func GetDefaultSyncWorkerCount() int { func GetDefaultSyncWorkerCount() int {
const minSyncWorkers = 16 const minSyncWorkers = 16
@ -67,23 +75,26 @@ func GetDefaultSyncWorkerCount() int {
func newDefaultSettings(gluonDir string) Settings { func newDefaultSettings(gluonDir string) Settings {
syncWorkers := GetDefaultSyncWorkerCount() syncWorkers := GetDefaultSyncWorkerCount()
imapPort := ports.FindFreePortFrom(1143)
smtpPort := ports.FindFreePortFrom(1025, imapPort)
return Settings{ return Settings{
GluonDir: gluonDir, GluonDir: gluonDir,
IMAPPort: 1143, IMAPPort: imapPort,
SMTPPort: 1025, SMTPPort: smtpPort,
IMAPSSL: false, IMAPSSL: false,
SMTPSSL: false, SMTPSSL: false,
UpdateChannel: updater.DefaultUpdateChannel, UpdateChannel: updater.DefaultUpdateChannel,
UpdateRollout: rand.Float64(), //nolint:gosec UpdateRollout: rand.Float64(), //nolint:gosec
ColorScheme: "", ColorScheme: "",
ProxyAllowed: false, ProxyAllowed: false,
ShowAllMail: true, ShowAllMail: true,
Autostart: true, Autostart: true,
AutoUpdate: true, AutoUpdate: true,
TelemetryDisabled: false,
LastVersion: "0.0.0", LastVersion: "0.0.0",
FirstStart: true, FirstStart: true,
@ -91,5 +102,8 @@ func newDefaultSettings(gluonDir string) Settings {
MaxSyncMemory: DefaultMaxSyncMemory, MaxSyncMemory: DefaultMaxSyncMemory,
SyncWorkers: syncWorkers, SyncWorkers: syncWorkers,
SyncAttPool: syncWorkers, SyncAttPool: syncWorkers,
LastUserAgent: DefaultUserAgent,
LastHeartbeatSent: time.Time{},
} }
} }

View File

@ -38,7 +38,7 @@ func init() { //nolint:gochecknoinits
Helpers[MacOSKeychain] = newMacOSHelper Helpers[MacOSKeychain] = newMacOSHelper
// Use MacOSKeychain by default. // Use MacOSKeychain by default.
defaultHelper = MacOSKeychain DefaultHelper = MacOSKeychain
} }
func parseError(original error) error { func parseError(original error) error {

View File

@ -48,14 +48,14 @@ func init() { //nolint:gochecknoinits
Helpers[Pass] = newPassHelper Helpers[Pass] = newPassHelper
} }
defaultHelper = SecretServiceDBus DefaultHelper = SecretServiceDBus
// If Pass is available, use it by default. // If Pass is available, use it by default.
// Otherwise, if SecretService is available, use it by default. // Otherwise, if SecretService is available, use it by default.
if _, ok := Helpers[Pass]; ok { if _, ok := Helpers[Pass]; ok {
defaultHelper = Pass DefaultHelper = Pass
} else if _, ok := Helpers[SecretService]; ok { } else if _, ok := Helpers[SecretService]; ok {
defaultHelper = SecretService DefaultHelper = SecretService
} }
} }

View File

@ -31,7 +31,7 @@ func init() { //nolint:gochecknoinits
Helpers[WindowsCredentials] = newWinCredHelper Helpers[WindowsCredentials] = newWinCredHelper
// Use WindowsCredentials by default. // Use WindowsCredentials by default.
defaultHelper = WindowsCredentials DefaultHelper = WindowsCredentials
} }
func newWinCredHelper(string) (credentials.Helper, error) { func newWinCredHelper(string) (credentials.Helper, error) {

View File

@ -42,8 +42,8 @@ var (
// Helpers holds all discovered keychain helpers. It is populated in init(). // Helpers holds all discovered keychain helpers. It is populated in init().
Helpers map[string]helperConstructor //nolint:gochecknoglobals Helpers map[string]helperConstructor //nolint:gochecknoglobals
// defaultHelper is the default helper to use if the user hasn't yet set a preference. // DefaultHelper is the default helper to use if the user hasn't yet set a preference.
defaultHelper string //nolint:gochecknoglobals DefaultHelper string //nolint:gochecknoglobals
) )
// NewKeychain creates a new native keychain. // NewKeychain creates a new native keychain.
@ -55,7 +55,7 @@ func NewKeychain(preferred, keychainName string) (*Keychain, error) {
// If the preferred keychain is unsupported, fallback to the default one. // If the preferred keychain is unsupported, fallback to the default one.
if _, ok := Helpers[preferred]; !ok { if _, ok := Helpers[preferred]; !ok {
preferred = defaultHelper preferred = DefaultHelper
} }
// Load the user's preferred keychain helper. // Load the user's preferred keychain helper.

View File

@ -441,7 +441,7 @@ func getMessageHeader(msg proton.Message, opts JobOptions) message.Header {
hdr.Set("From", msg.Sender.String()) hdr.Set("From", msg.Sender.String())
} }
if len(msg.ReplyTos) > 0 { if len(msg.ReplyTos) > 0 && !msg.IsDraft() {
if !(len(msg.ReplyTos) == 1 && addressEmpty(msg.ReplyTos[0])) { if !(len(msg.ReplyTos) == 1 && addressEmpty(msg.ReplyTos[0])) {
hdr.Set("Reply-To", toAddressList(msg.ReplyTos)) hdr.Set("Reply-To", toAddressList(msg.ReplyTos))
} }

View File

@ -431,7 +431,7 @@ func TestBuildSignedPlainEncryptedMessageWithPubKey(t *testing.T) {
section(t, res). section(t, res).
expectDate(is(`Wed, 01 Jan 2020 00:00:00 +0000`)). expectDate(is(`Wed, 01 Jan 2020 00:00:00 +0000`)).
expectContentType(is(`multipart/signed`)). expectContentType(is(`multipart/signed`)).
expectContentTypeParam(`micalg`, is(`pgp-sha256`)). expectContentTypeParam(`micalg`, is(`SHA-256`)).
expectContentTypeParam(`protocol`, is(`application/pgp-signature`)) expectContentTypeParam(`protocol`, is(`application/pgp-signature`))
section(t, res, 1). section(t, res, 1).
@ -439,7 +439,7 @@ func TestBuildSignedPlainEncryptedMessageWithPubKey(t *testing.T) {
expectContentTypeParam(`protected-headers`, is(`v1`)). expectContentTypeParam(`protected-headers`, is(`v1`)).
expectHeader(`Subject`, is(`simple plaintext body`)). expectHeader(`Subject`, is(`simple plaintext body`)).
expectHeader(`From`, is(`"pm.bridge.qa" <pm.bridge.qa@gmail.com>`)). expectHeader(`From`, is(`"pm.bridge.qa" <pm.bridge.qa@gmail.com>`)).
expectHeader(`To`, is(`schizofrenic@pm.me`)). expectHeader(`To`, is("\"InfernalBridgeTester@proton.me\" <InfernalbridgeTester@proton.me>")).
expectSection(verifiesAgainst(section(t, res, 1, 1, 2).pubKey(), section(t, res, 2).signature())) expectSection(verifiesAgainst(section(t, res, 1, 1, 2).pubKey(), section(t, res, 2).signature()))
section(t, res, 1, 1). section(t, res, 1, 1).
@ -477,7 +477,7 @@ func TestBuildSignedHTMLEncryptedMessageWithPubKey(t *testing.T) {
section(t, res). section(t, res).
expectDate(is(`Wed, 01 Jan 2020 00:00:00 +0000`)). expectDate(is(`Wed, 01 Jan 2020 00:00:00 +0000`)).
expectContentType(is(`multipart/signed`)). expectContentType(is(`multipart/signed`)).
expectContentTypeParam(`micalg`, is(`pgp-sha256`)). expectContentTypeParam(`micalg`, is(`SHA-256`)).
expectContentTypeParam(`protocol`, is(`application/pgp-signature`)) expectContentTypeParam(`protocol`, is(`application/pgp-signature`))
section(t, res, 1). section(t, res, 1).
@ -485,7 +485,7 @@ func TestBuildSignedHTMLEncryptedMessageWithPubKey(t *testing.T) {
expectContentTypeParam(`protected-headers`, is(`v1`)). expectContentTypeParam(`protected-headers`, is(`v1`)).
expectHeader(`Subject`, is(`simple html body`)). expectHeader(`Subject`, is(`simple html body`)).
expectHeader(`From`, is(`"pm.bridge.qa" <pm.bridge.qa@gmail.com>`)). expectHeader(`From`, is(`"pm.bridge.qa" <pm.bridge.qa@gmail.com>`)).
expectHeader(`To`, is(`schizofrenic@pm.me`)). expectHeader(`To`, is("\"InfernalBridgeTester@proton.me\" <InfernalbridgeTester@proton.me>")).
expectSection(verifiesAgainst(section(t, res, 1, 1, 2).pubKey(), section(t, res, 2).signature())) expectSection(verifiesAgainst(section(t, res, 1, 1, 2).pubKey(), section(t, res, 2).signature()))
section(t, res, 1, 1). section(t, res, 1, 1).
@ -524,7 +524,7 @@ func TestBuildSignedMultipartAlternativeEncryptedMessageWithPubKey(t *testing.T)
section(t, res). section(t, res).
expectDate(is(`Wed, 01 Jan 2020 00:00:00 +0000`)). expectDate(is(`Wed, 01 Jan 2020 00:00:00 +0000`)).
expectContentType(is(`multipart/signed`)). expectContentType(is(`multipart/signed`)).
expectContentTypeParam(`micalg`, is(`pgp-sha256`)). expectContentTypeParam(`micalg`, is(`SHA-256`)).
expectContentTypeParam(`protocol`, is(`application/pgp-signature`)) expectContentTypeParam(`protocol`, is(`application/pgp-signature`))
section(t, res, 1). section(t, res, 1).
@ -532,8 +532,8 @@ func TestBuildSignedMultipartAlternativeEncryptedMessageWithPubKey(t *testing.T)
expectContentTypeParam(`protected-headers`, is(`v1`)). expectContentTypeParam(`protected-headers`, is(`v1`)).
expectHeader(`Subject`, is(`Alternative`)). expectHeader(`Subject`, is(`Alternative`)).
expectHeader(`From`, is(`"pm.bridge.qa" <pm.bridge.qa@gmail.com>`)). expectHeader(`From`, is(`"pm.bridge.qa" <pm.bridge.qa@gmail.com>`)).
expectHeader(`To`, is(`schizofrenic@pm.me`)). expectHeader(`To`, is("\"InfernalBridgeTester@proton.me\" <InfernalbridgeTester@proton.me>")).
expectSection(verifiesAgainst(section(t, res, 1, 1, 3).pubKey(), section(t, res, 2).signature())) expectSection(verifiesAgainst(section(t, res, 1, 1, 2).pubKey(), section(t, res, 2).signature()))
section(t, res, 1, 1). section(t, res, 1, 1).
expectContentType(is(`multipart/mixed`)) expectContentType(is(`multipart/mixed`))
@ -549,17 +549,11 @@ func TestBuildSignedMultipartAlternativeEncryptedMessageWithPubKey(t *testing.T)
section(t, res, 1, 1, 1, 2). section(t, res, 1, 1, 1, 2).
expectContentType(is(`text/html`)). expectContentType(is(`text/html`)).
expectBody(contains(`This <font color="#ee24cc">Rich</font> formated text`)). expectBody(contains(`This Rich formated text`)).
expectBody(contains(`What kind of shoes do ninjas wear`)). expectBody(contains(`What kind of shoes do ninjas wear`)).
expectBody(contains(`How does a penguin build its house`)) expectBody(contains(`How does a penguin build its house`))
section(t, res, 1, 1, 2). section(t, res, 1, 1, 2).
expectContentType(is(`application/pdf`)).
expectTransferEncoding(is(`base64`)).
expectContentTypeParam(`name`, is(`minimal.pdf`)).
expectContentDispositionParam(`filename`, is(`minimal.pdf`))
section(t, res, 1, 1, 3).
expectContentType(is(`application/pgp-keys`)). expectContentType(is(`application/pgp-keys`)).
expectContentTypeParam(`name`, is(`OpenPGP_0x161C0875822359F7.asc`)). expectContentTypeParam(`name`, is(`OpenPGP_0x161C0875822359F7.asc`)).
expectContentDisposition(is(`attachment`)). expectContentDisposition(is(`attachment`)).
@ -587,16 +581,16 @@ func TestBuildSignedEmbeddedMessageRFC822EncryptedMessageWithPubKey(t *testing.T
section(t, res). section(t, res).
expectDate(is(`Wed, 01 Jan 2020 00:00:00 +0000`)). expectDate(is(`Wed, 01 Jan 2020 00:00:00 +0000`)).
expectContentType(is(`multipart/signed`)). expectContentType(is(`multipart/signed`)).
expectContentTypeParam(`micalg`, is(`pgp-sha256`)). expectContentTypeParam(`micalg`, is(`SHA-256`)).
expectContentTypeParam(`protocol`, is(`application/pgp-signature`)) expectContentTypeParam(`protocol`, is(`application/pgp-signature`))
section(t, res, 1). section(t, res, 1).
expectContentType(is(`multipart/mixed`)). expectContentType(is(`multipart/mixed`)).
expectContentTypeParam(`protected-headers`, is(`v1`)). expectContentTypeParam(`protected-headers`, is(`v1`)).
expectHeader(`Subject`, is(`Fwd: HTML with attachment external PGP`)). expectHeader(`Subject`, is(`Fwd: simple html body`)).
expectHeader(`From`, is(`"pm.bridge.qa" <pm.bridge.qa@gmail.com>`)). expectHeader(`From`, is(`"pm.bridge.qa" <pm.bridge.qa@gmail.com>`)).
expectHeader(`To`, is(`schizofrenic@pm.me`)). expectHeader(`To`, is("\"InfernalBridgeTester@proton.me\" <InfernalbridgeTester@proton.me>")).
expectSection(verifiesAgainst(section(t, res, 1, 1, 2).pubKey(), section(t, res, 2).signature())) expectSection(verifiesAgainst(section(t, res, 1, 1, 3).pubKey(), section(t, res, 2).signature()))
section(t, res, 1, 1). section(t, res, 1, 1).
expectContentType(is(`multipart/mixed`)) expectContentType(is(`multipart/mixed`))
@ -605,17 +599,17 @@ func TestBuildSignedEmbeddedMessageRFC822EncryptedMessageWithPubKey(t *testing.T
expectContentType(is(`text/plain`)) expectContentType(is(`text/plain`))
section(t, res, 1, 1, 2). section(t, res, 1, 1, 2).
expectContentType(is(`message/rfc822`)).
expectContentTypeParam(`name`, is(`simple html body.eml`)).
expectContentDisposition(is(`attachment`)).
expectContentDispositionParam(`filename`, is(`simple html body.eml`))
section(t, res, 1, 1, 3).
expectContentType(is(`application/pgp-keys`)). expectContentType(is(`application/pgp-keys`)).
expectContentTypeParam(`name`, is(`OpenPGP_0x161C0875822359F7.asc`)). expectContentTypeParam(`name`, is(`OpenPGP_0x161C0875822359F7.asc`)).
expectContentDisposition(is(`attachment`)). expectContentDisposition(is(`attachment`)).
expectContentDispositionParam(`filename`, is(`OpenPGP_0x161C0875822359F7.asc`)) expectContentDispositionParam(`filename`, is(`OpenPGP_0x161C0875822359F7.asc`))
section(t, res, 1, 1, 3).
expectContentType(is(`message/rfc822`)).
expectContentTypeParam(`name`, is(`HTML with attachment external PGP.eml`)).
expectContentDisposition(is(`attachment`)).
expectContentDispositionParam(`filename`, is(`HTML with attachment external PGP.eml`))
section(t, res, 2). section(t, res, 2).
expectContentType(is(`application/pgp-signature`)). expectContentType(is(`application/pgp-signature`)).
expectContentTypeParam(`name`, is(`OpenPGP_signature.asc`)). expectContentTypeParam(`name`, is(`OpenPGP_signature.asc`)).

View File

@ -73,6 +73,15 @@ type Attachment struct {
// Parse parses an RFC822 message. // Parse parses an RFC822 message.
func Parse(r io.Reader) (m Message, err error) { func Parse(r io.Reader) (m Message, err error) {
return parseIOReaderImpl(r, false)
}
// ParseAndAllowInvalidAddressLists parses an RFC822 message and allows email address lists to be invalid.
func ParseAndAllowInvalidAddressLists(r io.Reader) (m Message, err error) {
return parseIOReaderImpl(r, true)
}
func parseIOReaderImpl(r io.Reader, allowInvalidAddressLists bool) (m Message, err error) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
err = fmt.Errorf("panic while parsing message: %v", r) err = fmt.Errorf("panic while parsing message: %v", r)
@ -84,21 +93,21 @@ func Parse(r io.Reader) (m Message, err error) {
return Message{}, errors.Wrap(err, "failed to create new parser") return Message{}, errors.Wrap(err, "failed to create new parser")
} }
return parse(p) return parse(p, allowInvalidAddressLists)
} }
// ParseWithParser parses an RFC822 message using an existing parser. // ParseWithParser parses an RFC822 message using an existing parser.
func ParseWithParser(p *parser.Parser) (m Message, err error) { func ParseWithParser(p *parser.Parser, allowInvalidAddressLists bool) (m Message, err error) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
err = fmt.Errorf("panic while parsing message: %v", r) err = fmt.Errorf("panic while parsing message: %v", r)
} }
}() }()
return parse(p) return parse(p, allowInvalidAddressLists)
} }
func parse(p *parser.Parser) (Message, error) { func parse(p *parser.Parser, allowInvalidAddressLists bool) (Message, error) {
if err := convertEncodedTransferEncoding(p); err != nil { if err := convertEncodedTransferEncoding(p); err != nil {
return Message{}, errors.Wrap(err, "failed to convert encoded transfer encoding") return Message{}, errors.Wrap(err, "failed to convert encoded transfer encoding")
} }
@ -107,7 +116,7 @@ func parse(p *parser.Parser) (Message, error) {
return Message{}, errors.Wrap(err, "failed to convert foreign encodings") return Message{}, errors.Wrap(err, "failed to convert foreign encodings")
} }
m, err := parseMessageHeader(p.Root().Header) m, err := parseMessageHeader(p.Root().Header, allowInvalidAddressLists)
if err != nil { if err != nil {
return Message{}, errors.Wrap(err, "failed to parse message header") return Message{}, errors.Wrap(err, "failed to parse message header")
} }
@ -433,7 +442,7 @@ func getPlainBody(part *parser.Part) []byte {
} }
} }
func parseMessageHeader(h message.Header) (Message, error) { func parseMessageHeader(h message.Header, allowInvalidAddressLists bool) (Message, error) {
var m Message var m Message
for fields := h.Fields(); fields.Next(); { for fields := h.Fields(); fields.Next(); {
@ -451,7 +460,11 @@ func parseMessageHeader(h message.Header) (Message, error) {
case "from": case "from":
sender, err := rfc5322.ParseAddressList(fields.Value()) sender, err := rfc5322.ParseAddressList(fields.Value())
if err != nil { if err != nil {
return Message{}, errors.Wrap(err, "failed to parse from") if !allowInvalidAddressLists {
return Message{}, errors.Wrap(err, "failed to parse from")
}
logrus.WithError(err).Warn("failed to parse from")
} }
if len(sender) > 0 { if len(sender) > 0 {
@ -461,7 +474,11 @@ func parseMessageHeader(h message.Header) (Message, error) {
case "to": case "to":
toList, err := rfc5322.ParseAddressList(fields.Value()) toList, err := rfc5322.ParseAddressList(fields.Value())
if err != nil { if err != nil {
return Message{}, errors.Wrap(err, "failed to parse to") if !allowInvalidAddressLists {
return Message{}, errors.Wrap(err, "failed to parse to")
}
logrus.WithError(err).Warn("failed to parse to")
} }
m.ToList = toList m.ToList = toList
@ -469,7 +486,11 @@ func parseMessageHeader(h message.Header) (Message, error) {
case "reply-to": case "reply-to":
replyTos, err := rfc5322.ParseAddressList(fields.Value()) replyTos, err := rfc5322.ParseAddressList(fields.Value())
if err != nil { if err != nil {
return Message{}, errors.Wrap(err, "failed to parse reply-to") if !allowInvalidAddressLists {
return Message{}, errors.Wrap(err, "failed to parse reply-to")
}
logrus.WithError(err).Warn("failed to parse reply-to")
} }
m.ReplyTos = replyTos m.ReplyTos = replyTos
@ -477,7 +498,11 @@ func parseMessageHeader(h message.Header) (Message, error) {
case "cc": case "cc":
ccList, err := rfc5322.ParseAddressList(fields.Value()) ccList, err := rfc5322.ParseAddressList(fields.Value())
if err != nil { if err != nil {
return Message{}, errors.Wrap(err, "failed to parse cc") if !allowInvalidAddressLists {
return Message{}, errors.Wrap(err, "failed to parse cc")
}
logrus.WithError(err).Warn("failed to parse cc")
} }
m.CCList = ccList m.CCList = ccList
@ -485,7 +510,11 @@ func parseMessageHeader(h message.Header) (Message, error) {
case "bcc": case "bcc":
bccList, err := rfc5322.ParseAddressList(fields.Value()) bccList, err := rfc5322.ParseAddressList(fields.Value())
if err != nil { if err != nil {
return Message{}, errors.Wrap(err, "failed to parse bcc") if !allowInvalidAddressLists {
return Message{}, errors.Wrap(err, "failed to parse bcc")
}
logrus.WithError(err).Warn("failed to parse bcc")
} }
m.BCCList = bccList m.BCCList = bccList

View File

@ -23,6 +23,7 @@ import (
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"testing" "testing"
"github.com/ProtonMail/go-proton-api" "github.com/ProtonMail/go-proton-api"
@ -444,7 +445,7 @@ func TestParseWithAttachedPublicKey(t *testing.T) {
p, err := parser.New(f) p, err := parser.New(f)
require.NoError(t, err) require.NoError(t, err)
m, err := ParseWithParser(p) m, err := ParseWithParser(p, false)
require.NoError(t, err) require.NoError(t, err)
p.AttachPublicKey("publickey", "publickeyname") p.AttachPublicKey("publickey", "publickeyname")
@ -636,6 +637,32 @@ func TestParseIcsAttachment(t *testing.T) {
assert.Equal(t, string(m.Attachments[0].Data), "This is an ics calendar invite") assert.Equal(t, string(m.Attachments[0].Data), "This is an ics calendar invite")
} }
func TestParseAllowInvalidAddress(t *testing.T) {
const literal = `To: foo
From: bar
BCC: fff
CC: FFF
Reply-To: AAA
Subject: Test
`
// This will fail as the addresses are not valid.
{
_, err := Parse(strings.NewReader(literal))
require.Error(t, err)
}
// This will work as invalid addresses will be ignored.
m, err := ParseAndAllowInvalidAddressLists(strings.NewReader(literal))
require.NoError(t, err)
assert.Empty(t, m.ToList)
assert.Empty(t, m.Sender)
assert.Empty(t, m.CCList)
assert.Empty(t, m.BCCList)
assert.Empty(t, m.ReplyTos)
}
func TestParsePanic(t *testing.T) { func TestParsePanic(t *testing.T) {
var err error var err error

View File

@ -1,212 +1,252 @@
Content-Type: multipart/signed; micalg=pgp-sha256; Content-Type: multipart/signed;
protocol="application/pgp-signature"; boundary=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855;
boundary="Rrmlds5vN3IeeCVjbnepHmuVgyROSBjsS" micalg=SHA-256; protocol="application/pgp-signature"
This is an OpenPGP/MIME signed message (RFC 4880 and 3156) --e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
--Rrmlds5vN3IeeCVjbnepHmuVgyROSBjsS Content-Type: multipart/mixed; boundary="------------6sNRIgaKOJHTghuLxPUzlxn1";
Content-Type: multipart/mixed; boundary="avFkF0LAPYPXcFHcnsgGmACbGIPeVDdYc"; protected-headers="v1"
protected-headers="v1" From: "pm.bridge.qa" <pm.bridge.qa@gmail.com>
Subject: Fwd: HTML with attachment external PGP To: "InfernalBridgeTester@proton.me" <InfernalbridgeTester@proton.me>
From: "pm.bridge.qa" <pm.bridge.qa@gmail.com> Message-ID: <3f60022f-1ff2-9792-926b-a556cc39195d@gmail.com>
To: schizofrenic@pm.me Subject: Fwd: simple html body
Message-ID: <7c04869b-c470-116f-b8e5-8b4fd5e1195d@gmail.com> References: <1655e652-1abc-575f-6e03-5bbf119afbfa@gmail.com>
References: <LCxIUvb0rqBufdwR4JNxMg4ZDMI8x7pRG0vEHGHYwA@cp7-web-042.plabs.ch> In-Reply-To: <1655e652-1abc-575f-6e03-5bbf119afbfa@gmail.com>
In-Reply-To: <LCxIUvb0rqBufdwR4JNxMg4ZDMI8x7pRG0vEHGHYwA@cp7-web-042.plabs.ch>
--------------6sNRIgaKOJHTghuLxPUzlxn1
--avFkF0LAPYPXcFHcnsgGmACbGIPeVDdYc Content-Type: multipart/mixed; boundary="------------Z0a0IncIqoVlOIpOfcALEK5P"
Content-Type: multipart/mixed;
boundary="------------2F19EE9A8A1A6F779F5D14AF" --------------Z0a0IncIqoVlOIpOfcALEK5P
Content-Language: en-US Content-Type: text/plain; charset=UTF-8; format=flowed
Content-Transfer-Encoding: base64
This is a multi-part message in MIME format.
--------------2F19EE9A8A1A6F779F5D14AF DQo=
Content-Type: text/plain; charset=utf-8; format=flowed --------------Z0a0IncIqoVlOIpOfcALEK5P
Content-Transfer-Encoding: quoted-printable Content-Type: message/rfc822; name="simple html body.eml"
Content-Disposition: attachment; filename="simple html body.eml"
Content-Transfer-Encoding: 7bit
--------------2F19EE9A8A1A6F779F5D14AF Message-ID: <1655e652-1abc-575f-6e03-5bbf119afbfa@gmail.com>
Content-Type: application/pgp-keys; Date: Tue, 25 Apr 2023 17:12:15 +0200
name="OpenPGP_0x161C0875822359F7.asc" MIME-Version: 1.0
Content-Transfer-Encoding: quoted-printable User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101
Content-Disposition: attachment; Thunderbird/102.10.0
filename="OpenPGP_0x161C0875822359F7.asc" Content-Language: en-US
To: "InfernalBridgeTester@proton.me" <InfernalbridgeTester@proton.me>
-----BEGIN PGP PUBLIC KEY BLOCK----- From: "pm.bridge.qa" <pm.bridge.qa@gmail.com>
Subject: simple html body
xsBNBFxlUPwBCACx954Ey4SD88f8DSKFw9BaZNXrNwYxNYSgqaqOGHQ0WllF3mstEhTfuxxCZ= Autocrypt: addr=pm.bridge.qa@gmail.com; keydata=
pDh xsBNBFxlUPwBCACx954Ey4SD88f8DSKFw9BaZNXrNwYxNYSgqaqOGHQ0WllF3mstEhTfuxxC
I5IhWCXUNxanzsFkn88mRDwFRVl2sf2aAG4/P/p1381oh2kd0UElMRQaQGzoCadQMaQOL9WYT= ZpDhI5IhWCXUNxanzsFkn88mRDwFRVl2sf2aAG4/P/p1381oh2kd0UElMRQaQGzoCadQMaQO
f4S L9WYTf4SPWSCzjrPyKgjq5FbqjbF/ndu376na9L+tnsEXyL6RrI6aZhjWG73xlqxS65dzTIY
PWSCzjrPyKgjq5FbqjbF/ndu376na9L+tnsEXyL6RrI6aZhjWG73xlqxS65dzTIYzsyM/P97x= zsyM/P97xSndNvlvWtGvLlpFkzxfAEGpVzfOYVYFKoc8rGmUDwrDWYfk5JczRDDogJnY+BNM
Snd Zf9pjSqk6rTyBOfNH5fpU8r7A5Q7l+HVakvMUQ9DzDWJtg2ru1Y8hexnJOF68avO4+a1ABEB
NvlvWtGvLlpFkzxfAEGpVzfOYVYFKoc8rGmUDwrDWYfk5JczRDDogJnY+BNMZf9pjSqk6rTyB= AAHNKEJyaWRnZSBLeXUtRWh5aiA8cG0uYnJpZGdlLnFhQGdtYWlsLmNvbT7CwI4EEwEIADgC
OfN GwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AWIQRc5gl5cC8oW/Mo+bEWHAh1giNZ9wUCZEfU
H5fpU8r7A5Q7l+HVakvMUQ9DzDWJtg2ru1Y8hexnJOF68avO4+a1ABEBAAHNKEJyaWRnZSBLe= zQAKCRAWHAh1giNZ9872B/4mM6cXErSSYK6/apGUVebg9QiP1RhFlLE/kg7BW3FaSP/yTUZN
XUt ZdX7WPnEVyaa5Dk4xRZiB47Yc5myspwc+JEJ3YDAHq+4n/D74TF1kUCzP+QVsWcn40UqX9gH
RWh5aiA8cG0uYnJpZGdlLnFhQGdtYWlsLmNvbT7CwJQEEwEIAD4CGwMFCwkIBwIGFQoJCAsCB= bO01O/DYtoxMOljEgkfQjEZcRoHuUhCUzldFf8aV+uZKiOXhrPYCwsilnh0RAmDV7fLoOfKX
BYC MLiKXE8wM/5Bax+dk2AmEM4bOTIo58GGDDqseIg03ocrW7vPegmxiLUwmsHIIDwTq6qZ0CVx
AwECHgECF4AWIQRc5gl5cC8oW/Mo+bEWHAh1giNZ9wUCYC32ygUJB4sMzgAKCRAWHAh1giNZ9= bt2uv4cCyNz/0pmRzG7p8192Evdu8JOuLSj3pI1X00Ub326yay3BBUnsL4PJIGoly8hnLb5N
/K8 3cyNzsBNBFxlUPwBCADh2HsX23yVnJt9fxFz3D07kCBNvu4HQfps6h1rgNxGhE32VmpESHeb
B/4qs84Ii/zKH+q+C8vwO4jUJkOM73qD0pgB7zBs651zWbpgopyol1YUKNpFaHlx/Qch7RDI7= vIB5xjL6xKbIqqRa3x/7KDVBNJvca0gUsqEt5kzYF88Fyf2NBcejpIbcP7BS/g+C6KOowYj+
Vcz Et26T6GdwFXExUcl80JvoX8yHQOfvJpdiBRbjyB8UqfCaknm3c7dNuXmhflz/w3aBj32q9Zy
1+60/KZJSJR19/N2EDVbCUdh8ueioUp9X/218YWV2TRJNxTnljd4FAn7smZnXuP1TsLjQ6sKO= GqA1NpHCpLyVAlvSNQ/pat/rGUCPZ9duw4KhUUqEmatQPVFPkutTouEZQbMK+i+chOH3AsKC
V0U uNDfvCDwirnsSqIJmAgl1lC4de+bsWYCMqN9ei99hOCRUyhZ3g3sr8RBowVAdcvjZxeIDKAL
u6JoiG6LZFXqDgxYpA++58Rkl6xaY6R71VkmVQlbEKtubX9AjHydq97Y+Jvn11XzWZaKhv4L7= ABEBAAHCwHYEGAEIACACGwwWIQRc5gl5cC8oW/Mo+bEWHAh1giNZ9wUCZEfUugAKCRAWHAh1
6Pa giNZ9ycfB/97iOFFeYSFB5CqOxsyo9TjHve0BOzfGyLh632+sUQEw3qqwVreAqrjVa/wnLFW
4tMKXvvrKh1oywMmh6mZJo+5ZA/ABTkr45cwlTPYqGTS9+uvOHt+PH/oYwwJB4ls2cIAUldSj= 67Pbf4GBn6ZnaIMIq4rv6d4wnDZ4b18yQkITErWvU+3thpeZS/NUeWlLZlXFauDhCdPpAq5T
TVQ nw7+5lq0pe2BEcNBF4IRoBRs7UQVhr/QpN+xawbnQUM1oIOMljTWuQZtPo75OsltQn59+OTn
IsseYz3LlbcCfKJiiCFxeHOQXA5J6zNLKOT58TsczsBNBFxlUPwBCADh2HsX23yVnJt9fxFz3= 7ZYQD4Q3Sn0vPAzFbPBa4fostzbx8ktTGWfhBctQrpwS06VqoSowr8RFY9S+ssjCK+Hlq2t8
D07 ZC0CBL1aU1AAPqcAOfrw9rin0rN1dH07g1uDeZ9SnyLuee5QN7r3VY8b9tiOJV9m
kCBNvu4HQfps6h1rgNxGhE32VmpESHebvIB5xjL6xKbIqqRa3x/7KDVBNJvca0gUsqEt5kzYF= Content-Type: multipart/signed; micalg=pgp-sha256;
88F protocol="application/pgp-signature";
yf2NBcejpIbcP7BS/g+C6KOowYj+Et26T6GdwFXExUcl80JvoX8yHQOfvJpdiBRbjyB8UqfCa= boundary="------------Kh8bXWpcGJrKDmQHlv9QWtgJ"
knm
3c7dNuXmhflz/w3aBj32q9ZyGqA1NpHCpLyVAlvSNQ/pat/rGUCPZ9duw4KhUUqEmatQPVFPk= This is an OpenPGP/MIME signed message (RFC 4880 and 3156)
utT --------------Kh8bXWpcGJrKDmQHlv9QWtgJ
ouEZQbMK+i+chOH3AsKCuNDfvCDwirnsSqIJmAgl1lC4de+bsWYCMqN9ei99hOCRUyhZ3g3sr= Content-Type: multipart/mixed; boundary="------------iFRY0nd000IWAcfqrg6ZdUuF";
8RB protected-headers="v1"
owVAdcvjZxeIDKALABEBAAHCwHwEGAEIACYCGwwWIQRc5gl5cC8oW/Mo+bEWHAh1giNZ9wUCY= From: "pm.bridge.qa" <pm.bridge.qa@gmail.com>
C32 To: "InfernalBridgeTester@proton.me" <InfernalbridgeTester@proton.me>
lAUJB4sMmAAKCRAWHAh1giNZ9+Y2B/9rTKZaKviae+ummXNumXcrKvbkAAvfuLpKUn53FlQLm= Message-ID: <1655e652-1abc-575f-6e03-5bbf119afbfa@gmail.com>
L6H Subject: simple html body
jB++lJnPWvVSzdZxdv8FiPP3d632XHKUrkQRQM/9byRDXDommi7Qttx7YCkhd4JLVYqJqpnAQ=
xI5 --------------iFRY0nd000IWAcfqrg6ZdUuF
RMkXiZNWyr1lz8JOM1XvDk1M7sJwPMWews8VOIE03E1nt7AsQGnvHtadgEnQaufrYNX3hFA8S= Content-Type: multipart/mixed; boundary="------------0tRoF00iqRv70TBczVNfagKk"
osO
HSnedcys6yrzCSIGCqCD9VHbnMtS4DOv0XJGh2hwc8omzH0KZA517dyKBorJRwadcVauGXDKx= --------------0tRoF00iqRv70TBczVNfagKk
Etv Content-Type: text/html; charset=UTF-8
Im4rl94PR/3An1Mj6HeeVVpLqDQ5Jb9J90BahWeQ53FzRa4EQzYCw0nLnxcsT1ZEEP5u Content-Transfer-Encoding: quoted-printable
=3Dv/1p
-----END PGP PUBLIC KEY BLOCK----- <html>
<head>
--------------2F19EE9A8A1A6F779F5D14AF
Content-Type: message/rfc822; <meta http-equiv=3D"content-type" content=3D"text/html; charset=3DUTF=
name="HTML with attachment external PGP.eml" -8">
Content-Transfer-Encoding: 7bit </head>
Content-Disposition: attachment; <body>
filename="HTML with attachment external PGP.eml" <p> </p>
<p> And this is HTML<br>
Delivered-To: pm.bridge.qa@gmail.com </p>
Received: by 2002:a17:906:a051:0:0:0:0 with SMTP id bg17csp66709ejb; <ul>
Wed, 24 Mar 2021 22:03:32 -0700 (PDT) <li><b>Do I enjoy making courthouse puns?</b>
X-Google-Smtp-Source: ABdhPJxllBuHnnJzKWy77R291tZbVFVk0iahkLm1TQsluEYTvyAXdOWB/zp1y10e60UlGGZYH3YF Guilty.=3D3DE2=3D3D80=3D3D9=3D
X-Received: by 2002:a05:6000:118c:: with SMTP id g12mr6758087wrx.353.1616648612550; 4 <i>@=3D3D baddadjokes</i></li>
Wed, 24 Mar 2021 22:03:32 -0700 (PDT) <li><b>Can February March?</b> No, but April May. =3D3DE2=3D3D80=3D=
ARC-Seal: i=1; a=rsa-sha256; t=1616648612; cv=none; 3D94<i=3D>@Bear=3D3D
d=google.com; s=arc-20160816; dedMOGuy</i=3D></li>
b=Jf4vmKEoeJQ3rIDMbI2twiDkfn50ejNnqIbs2nkaFruITcw6XhvhbcfV9HLC80Yt8E </ul>
tfN7TV9qoBneSWzfSJ+Sqw31hBKKtKpMhuqZT9GPzBN5gdMJKj5ISAQ8Lgm9zvR3Zbjn <p> <br>
N0nOzCu/oT1amMMm+48hpKj8VL2tydjvNG+g/a5lk1Aw7JdqIKV6t1XhsyyYaa1O+NFC </p>
rQThdalcQj2NjoZWba1mjZSzI7B7hJdZg5d+jado2TPMQXe2kz2wGmr3+/JcKvPJjrSA <p>
S+jzhpjcd7ZnctkzTfpsdlBJAGKoDBnSvQc3eMJ/AgRHFc+5ks5nRDt/1DowSjQ7i7rp <meta http-equiv=3D"3D&quot;" content-type"=3D""
4a+g== content=3D"3D&quot;text/html;" charset=3D"3DUTF=3D" -8"=3D"">
ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20160816; </p>
h=mime-version:message-id:subject:reply-to:from:to:dkim-signature </body>
:date; </html>
bh=vmJ0JT+IfeO4idMYP7zPvldBkdONjKTXWTp7ly/B9qk=; --------------0tRoF00iqRv70TBczVNfagKk
b=f8VY+ajsE/XNYrqD666FM0WCtNEQtUyU/Zh3pFCI9sFrMnAui4Qp9Gs1fe/8HLxt2v Content-Type: application/pgp-keys; name="OpenPGP_0x161C0875822359F7.asc"
/C4l4eHELvPBv4vX0KtUvOlRZYPZbLZCNdtTcFtiuZEKUHWx370p7yyMWcmSMdlUbq4J Content-Disposition: attachment; filename="OpenPGP_0x161C0875822359F7.asc"
NrKMPGfaYiZe5Rt3MyD5RKm4RJpqvep34VCHMYtoFQP/0Po4/1JMDw0Fy6SXUJ54rBRw Content-Description: OpenPGP public key
bmzqNNBkonda3YghhK3WNrxTxzZ8I7KW9YdpENNS9ewJLeVtFQKdiLZwz5EpMZxOxG0I Content-Transfer-Encoding: quoted-printable
LW0jRtDlmZnqRe7bvTAo51IuLf9okHRI8PRiK0UHl+4Vr5Igq4mub7Ee8pC/Nz3Yj29G
KODw== -----BEGIN PGP PUBLIC KEY BLOCK-----
ARC-Authentication-Results: i=1; mx.google.com;
dkim=pass header.i=@protonmail.com header.s=protonmail header.b=EX07e46H; xsBNBFxlUPwBCACx954Ey4SD88f8DSKFw9BaZNXrNwYxNYSgqaqOGHQ0WllF3mst
spf=pass (google.com: domain of bridge-test-user@protonmail.com designates 185.70.40.22 as permitted sender) smtp.mailfrom=bridge-test-user@protonmail.com; EhTfuxxCZpDhI5IhWCXUNxanzsFkn88mRDwFRVl2sf2aAG4/P/p1381oh2kd0UEl
dmarc=pass (p=QUARANTINE sp=QUARANTINE dis=NONE) header.from=protonmail.com MRQaQGzoCadQMaQOL9WYTf4SPWSCzjrPyKgjq5FbqjbF/ndu376na9L+tnsEXyL6
Return-Path: <bridge-test-user@protonmail.com> RrI6aZhjWG73xlqxS65dzTIYzsyM/P97xSndNvlvWtGvLlpFkzxfAEGpVzfOYVYF
Received: from mail2.protonmail.ch (mail2.protonmail.ch. [185.70.40.22]) Koc8rGmUDwrDWYfk5JczRDDogJnY+BNMZf9pjSqk6rTyBOfNH5fpU8r7A5Q7l+HV
by mx.google.com with ESMTPS id g6si2999785wrr.110.2021.03.24.22.03.32 akvMUQ9DzDWJtg2ru1Y8hexnJOF68avO4+a1ABEBAAHNKEJyaWRnZSBLeXUtRWh5
for <pm.bridge.qa@gmail.com> aiA8cG0uYnJpZGdlLnFhQGdtYWlsLmNvbT7CwI4EEwEIADgCGwMFCwkIBwIGFQoJ
(version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); CAsCBBYCAwECHgECF4AWIQRc5gl5cC8oW/Mo+bEWHAh1giNZ9wUCZEfUzQAKCRAW
Wed, 24 Mar 2021 22:03:32 -0700 (PDT) HAh1giNZ9872B/4mM6cXErSSYK6/apGUVebg9QiP1RhFlLE/kg7BW3FaSP/yTUZN
Received-SPF: pass (google.com: domain of bridge-test-user@protonmail.com designates 185.70.40.22 as permitted sender) client-ip=185.70.40.22; ZdX7WPnEVyaa5Dk4xRZiB47Yc5myspwc+JEJ3YDAHq+4n/D74TF1kUCzP+QVsWcn
Authentication-Results: mx.google.com; 40UqX9gHbO01O/DYtoxMOljEgkfQjEZcRoHuUhCUzldFf8aV+uZKiOXhrPYCwsil
dkim=pass header.i=@protonmail.com header.s=protonmail header.b=EX07e46H; nh0RAmDV7fLoOfKXMLiKXE8wM/5Bax+dk2AmEM4bOTIo58GGDDqseIg03ocrW7vP
spf=pass (google.com: domain of bridge-test-user@protonmail.com designates 185.70.40.22 as permitted sender) smtp.mailfrom=bridge-test-user@protonmail.com; egmxiLUwmsHIIDwTq6qZ0CVxbt2uv4cCyNz/0pmRzG7p8192Evdu8JOuLSj3pI1X
dmarc=pass (p=QUARANTINE sp=QUARANTINE dis=NONE) header.from=protonmail.com 00Ub326yay3BBUnsL4PJIGoly8hnLb5N3cyNwsCUBBMBCAA+FiEEXOYJeXAvKFvz
Date: Thu, 25 Mar 2021 05:03:27 +0000 KPmxFhwIdYIjWfcFAlxlUPwCGwMFCQPCZwAFCwkIBwIGFQoJCAsCBBYCAwECHgEC
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=protonmail.com; F4AACgkQFhwIdYIjWfeQzgf+NWseR+UvrJ7I1CFd6M2+RmyIC1QTQ+uyJIhIBQfC
s=protonmail; t=1616648611; vBhueU9GUPWR6a6GWsb2DGCMkBM2aHoTCcf46fq87VspYb3+b0JsKfRFxBa2cuNH
bh=vmJ0JT+IfeO4idMYP7zPvldBkdONjKTXWTp7ly/B9qk=; Ey6PK3xHBDnYFtSZaRB7QMQMfYVin5oyHq8mOd4mImOKfpGhuuhq1hrT8sOhVxRY
h=Date:To:From:Reply-To:Subject:From; Nl/2Hanya7tEJlVyAAEwtN4QVCqiRjjD7kBQ+mgxdDFo62X+sl4Zz2BFlZks+c1+
b=EX07e46H5/HmotAWZ69I4qa5jCVRao/p3KEM3eQn/AQ8s+cLMaR5b2ozdHrPCsTw5 LRWwaQZvGgf2tm2NqZhC04CKc2Gg5j7wNJBPVh/FVluxY27D2hV6v9/cTvXDGo8J
i5b1DLUHZHBf+6Ven47WJfKNwLUfkAGD2P0aI/dAk/h/h0Bg4Ni85pv+uPpRHLNQKv pnZ28CnQqiiaKEU83BGwcUlcr4G5YApHyIPujaxffE2R887ATQRcZVD8AQgA4dh7
T3VnDP9MSwl6IUJu5zoM2EC70MLoiHS07lxhM2pw= F9t8lZybfX8Rc9w9O5AgTb7uB0H6bOoda4DcRoRN9lZqREh3m7yAecYy+sSmyKqk
To: External Bridge <pm.bridge.qa@gmail.com> Wt8f+yg1QTSb3GtIFLKhLeZM2BfPBcn9jQXHo6SG3D+wUv4PguijqMGI/hLduk+h
From: Bridge Test <bridge-test-user@protonmail.com> ncBVxMVHJfNCb6F/Mh0Dn7yaXYgUW48gfFKnwmpJ5t3O3Tbl5oX5c/8N2gY99qvW
Reply-To: Bridge Test <bridge-test-user@protonmail.com> chqgNTaRwqS8lQJb0jUP6Wrf6xlAj2fXbsOCoVFKhJmrUD1RT5LrU6LhGUGzCvov
Subject: HTML with attachment external PGP nITh9wLCgrjQ37wg8Iq57EqiCZgIJdZQuHXvm7FmAjKjfXovfYTgkVMoWd4N7K/E
Message-ID: <LCxIUvb0rqBufdwR4JNxMg4ZDMI8x7pRG0vEHGHYwA@cp7-web-042.plabs.ch> QaMFQHXL42cXiAygCwARAQABwsB2BBgBCAAgAhsMFiEEXOYJeXAvKFvzKPmxFhwI
MIME-Version: 1.0 dYIjWfcFAmRH1LoACgkQFhwIdYIjWfcnHwf/e4jhRXmEhQeQqjsbMqPU4x73tATs
Content-Type: multipart/mixed; 3xsi4et9vrFEBMN6qsFa3gKq41Wv8JyxVuuz23+BgZ+mZ2iDCKuK7+neMJw2eG9f
boundary="b1_LCxIUvb0rqBufdwR4JNxMg4ZDMI8x7pRG0vEHGHYwA" MkJCExK1r1Pt7YaXmUvzVHlpS2ZVxWrg4QnT6QKuU58O/uZatKXtgRHDQReCEaAU
X-Spam-Status: No, score=-1.2 required=10.0 tests=ALL_TRUSTED,DKIM_SIGNED, bO1EFYa/0KTfsWsG50FDNaCDjJY01rkGbT6O+TrJbUJ+ffjk5+2WEA+EN0p9LzwM
DKIM_VALID,DKIM_VALID_AU,DKIM_VALID_EF,FREEMAIL_FROM,HTML_MESSAGE xWzwWuH6LLc28fJLUxln4QXLUK6cEtOlaqEqMK/ERWPUvrLIwivh5atrfGQtAgS9
shortcircuit=no autolearn=disabled version=3.4.4 WlNQAD6nADn68Pa4p9KzdXR9O4Nbg3mfUp8i7nnuUDe691WPG/bYjiVfZsLAfAQY
X-Spam-Checker-Version: SpamAssassin 3.4.4 (2020-01-24) on AQgAJhYhBFzmCXlwLyhb8yj5sRYcCHWCI1n3BQJcZVD8AhsMBQkDwmcAAAoJEBYc
mailout.protonmail.ch CHWCI1n3rhcH/iCB0ZV861H0RKJ2F7bXEyCLR2ncBFUCnFo3muSrN9NXTojz2vwv
zexRBpZzaRJoksBkvH+ofuZ1iK7ycZO23dnukvPwGQsz3QiITjVeB6ZR0250MG1q
This is a multi-part message in MIME format. A5yZRlZCsCbGJb4/2e8Ey8BbblHn49Zta4l2Ex5NpNNQ8FYoqXhXu5Bd2F/wX/Bp
5gkZegfE3H9Dw4QjP82Mt0RZSBg9RMGCk6nNfEuze1Up+IOdtqzf3/Z8J5XxLzN2
--b1_LCxIUvb0rqBufdwR4JNxMg4ZDMI8x7pRG0vEHGHYwA s8WPmDwJDwvxJRtto8U+ulv4ElcwlA+wYiKAq7cRCKGM/si5ClkUNgb319grUrBU
Content-Type: multipart/alternative; 6h8SuYtgnD84965xRiVAgtH4wCPN544N8CE=3D
boundary="b2_LCxIUvb0rqBufdwR4JNxMg4ZDMI8x7pRG0vEHGHYwA" =3DNmqc
-----END PGP PUBLIC KEY BLOCK-----
--b2_LCxIUvb0rqBufdwR4JNxMg4ZDMI8x7pRG0vEHGHYwA
Content-Type: text/plain; charset=utf-8 --------------0tRoF00iqRv70TBczVNfagKk--
Content-Transfer-Encoding: base64
--------------iFRY0nd000IWAcfqrg6ZdUuF--
VGhpcyBpcyBib2R5IG9mIEhUTUwgbWFpbCB3aXRoIGF0dGFjaG1lbnQ=
--------------Kh8bXWpcGJrKDmQHlv9QWtgJ
--b2_LCxIUvb0rqBufdwR4JNxMg4ZDMI8x7pRG0vEHGHYwA Content-Type: application/pgp-signature; name="OpenPGP_signature.asc"
Content-Type: text/html; charset=utf-8 Content-Description: OpenPGP digital signature
Content-Transfer-Encoding: base64 Content-Disposition: attachment; filename="OpenPGP_signature"
PGh0bWw+PGhlYWQ+PC9oZWFkPjxib2R5PlRoaXMgaXMgYm9keSBvZiA8Yj5IVE1MIG1haWw8L2I+ -----BEGIN PGP SIGNATURE-----
IHdpdGggYXR0YWNobWVudA0KPC9ib2R5PjwvaHRtbD4=
wsB5BAABCAAjFiEEXOYJeXAvKFvzKPmxFhwIdYIjWfcFAmRH7c8FAwAAAAAACgkQFhwIdYIjWffG
uQgAlK68JzT/9NLOg4SZPUbVaEdlbru6ebX1Lwaj93FZEKfmiEZ4heuYfq4yVw1JI/CGhHa1wxUJ
--b2_LCxIUvb0rqBufdwR4JNxMg4ZDMI8x7pRG0vEHGHYwA-- M6+Tg+0lqIslG1rTposgIziugMBhEWXYWXBwDdvu+9T2eqo0se3h+oXueVlRihowfuPcP9jvt6p3
DR+pu+hJ/D42DarJrn8v0CMm3PyLNHodyKLmEtZrzP4ok9Wal850xZ3Tmu5dL3v300wPGJaxrn8o
--b1_LCxIUvb0rqBufdwR4JNxMg4ZDMI8x7pRG0vEHGHYwA 4OeHRUodE43UijUrFKNoEfhqzY8HsMKIdF8Bi6+XiA0sHwcaUqJsaczBqgmbJbPqG8N4CokiUnUu
Content-Type: image/png; name=outline-light-instagram-48.png 4QDmClWa0rG4I5rrx4PrTbzAt7cGIb8N3ACNI7vI/Q==
Content-Transfer-Encoding: base64 =Llod
Content-Disposition: attachment; filename=outline-light-instagram-48.png -----END PGP SIGNATURE-----
iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAALVBMVEUAAAD///////////////// --------------Kh8bXWpcGJrKDmQHlv9QWtgJ--
//////////////////////////////////////+hSKubAAAADnRSTlMAgO8QQM+/IJ9gj1AwcIQd --------------Z0a0IncIqoVlOIpOfcALEK5P
OXUAAAGdSURBVDjLXJC9SgNBFIVPXDURTYhgIQghINgowyLYCAYtRFAIgtYhpAjYhC0srCRW6YIg Content-Type: application/pgp-keys; name="OpenPGP_0x161C0875822359F7.asc"
WNpoHVSsg/gEii+Qnfxq4DyDc3cyMfrBwl2+O+fOHTi8p7LS5RUf/9gpMKL7iT9sK47Q95ggpkzv Content-Disposition: attachment; filename="OpenPGP_0x161C0875822359F7.asc"
1cvRcsGYNMYsmP+zKN27NR2vcDyTNVdfkOuuniNPMWafvIbljt+YoMEvW8y7lt+ARwhvrgPjhA0I Content-Description: OpenPGP public key
BTng7S1GLPlypBvtIBPidY4YBDJFdtnkscQ5JGaGqxC9i7jSDwcwnB8qHWBaQjw1ABI8wYgtVoG6 Content-Transfer-Encoding: quoted-printable
9pFkH8iZIiJeulFt4JLvJq8I5N2GMWYbHWDWzM3JZTMdeSWla0kW86FcuI0mfStiNKQ/AhEeh8h0
YUTffFwrMTT5oSwdojIQ0UKcocgAKRH1HiqhFQmmJa5qRaYHNbRiSsOgslY0NdixItUTUWlZkedP -----BEGIN PGP PUBLIC KEY BLOCK-----
HXVyAgAIA1F0wP5btQZPIyTwvAqa/Fl4oacuP+e4XHAjSYpkQkxSiMX+T7FPoZJToSStzED70HCy
KE3NGCg4jJrC6Ti7AFwZLhnW0gMbzFZc0RmmeAAAAABJRU5ErkJggg== xsBNBFxlUPwBCACx954Ey4SD88f8DSKFw9BaZNXrNwYxNYSgqaqOGHQ0WllF3mst
EhTfuxxCZpDhI5IhWCXUNxanzsFkn88mRDwFRVl2sf2aAG4/P/p1381oh2kd0UEl
--b1_LCxIUvb0rqBufdwR4JNxMg4ZDMI8x7pRG0vEHGHYwA-- MRQaQGzoCadQMaQOL9WYTf4SPWSCzjrPyKgjq5FbqjbF/ndu376na9L+tnsEXyL6
RrI6aZhjWG73xlqxS65dzTIYzsyM/P97xSndNvlvWtGvLlpFkzxfAEGpVzfOYVYF
Koc8rGmUDwrDWYfk5JczRDDogJnY+BNMZf9pjSqk6rTyBOfNH5fpU8r7A5Q7l+HV
--------------2F19EE9A8A1A6F779F5D14AF-- akvMUQ9DzDWJtg2ru1Y8hexnJOF68avO4+a1ABEBAAHNKEJyaWRnZSBLeXUtRWh5
aiA8cG0uYnJpZGdlLnFhQGdtYWlsLmNvbT7CwI4EEwEIADgCGwMFCwkIBwIGFQoJ
--avFkF0LAPYPXcFHcnsgGmACbGIPeVDdYc-- CAsCBBYCAwECHgECF4AWIQRc5gl5cC8oW/Mo+bEWHAh1giNZ9wUCZEfUzQAKCRAW
HAh1giNZ9872B/4mM6cXErSSYK6/apGUVebg9QiP1RhFlLE/kg7BW3FaSP/yTUZN
--Rrmlds5vN3IeeCVjbnepHmuVgyROSBjsS ZdX7WPnEVyaa5Dk4xRZiB47Yc5myspwc+JEJ3YDAHq+4n/D74TF1kUCzP+QVsWcn
Content-Type: application/pgp-signature; name="OpenPGP_signature.asc" 40UqX9gHbO01O/DYtoxMOljEgkfQjEZcRoHuUhCUzldFf8aV+uZKiOXhrPYCwsil
Content-Description: OpenPGP digital signature nh0RAmDV7fLoOfKXMLiKXE8wM/5Bax+dk2AmEM4bOTIo58GGDDqseIg03ocrW7vP
Content-Disposition: attachment; filename="OpenPGP_signature" egmxiLUwmsHIIDwTq6qZ0CVxbt2uv4cCyNz/0pmRzG7p8192Evdu8JOuLSj3pI1X
00Ub326yay3BBUnsL4PJIGoly8hnLb5N3cyNwsCUBBMBCAA+FiEEXOYJeXAvKFvz
-----BEGIN PGP SIGNATURE----- KPmxFhwIdYIjWfcFAlxlUPwCGwMFCQPCZwAFCwkIBwIGFQoJCAsCBBYCAwECHgEC
F4AACgkQFhwIdYIjWfeQzgf+NWseR+UvrJ7I1CFd6M2+RmyIC1QTQ+uyJIhIBQfC
wsB5BAABCAAjFiEEXOYJeXAvKFvzKPmxFhwIdYIjWfcFAmBciIAFAwAAAAAACgkQFhwIdYIjWfcN vBhueU9GUPWR6a6GWsb2DGCMkBM2aHoTCcf46fq87VspYb3+b0JsKfRFxBa2cuNH
ZQf+NzAoEJRTSW5JFNgSGkwLsH89wAbw3wEt4PYuZaa+35xBuU8Sojm1oLOyuPkIasQf98Iu5P1o Ey6PK3xHBDnYFtSZaRB7QMQMfYVin5oyHq8mOd4mImOKfpGhuuhq1hrT8sOhVxRY
8cokViEa6wm+ZZpcFMi6T2/3+UNlSm81Epm7GrFyjAFTWrdTPLb4k4x47sz77RoTp/UEwm/7fVI5 Nl/2Hanya7tEJlVyAAEwtN4QVCqiRjjD7kBQ+mgxdDFo62X+sl4Zz2BFlZks+c1+
gMYhQyIYaocXHmDk61UshWE9q/Po6qjHBnnWS8YBnhUS9lK8uimpfRO9UQ9bIUjIYDGDPAtBoYnb LRWwaQZvGgf2tm2NqZhC04CKc2Gg5j7wNJBPVh/FVluxY27D2hV6v9/cTvXDGo8J
X9V4SjBvbbdNrgoVaDxPw6HYCb3RhzRXunr5Icdnjfbc2H40/FayVi/p7GzFh+8zv/TzRxMkHo72 pnZ28CnQqiiaKEU83BGwcUlcr4G5YApHyIPujaxffE2R887ATQRcZVD8AQgA4dh7
DBsONaC7r8bxQ9BwJvpmWufqL7ZXHfVXQ6z+M43e1Q== F9t8lZybfX8Rc9w9O5AgTb7uB0H6bOoda4DcRoRN9lZqREh3m7yAecYy+sSmyKqk
=Stx+ Wt8f+yg1QTSb3GtIFLKhLeZM2BfPBcn9jQXHo6SG3D+wUv4PguijqMGI/hLduk+h
-----END PGP SIGNATURE----- ncBVxMVHJfNCb6F/Mh0Dn7yaXYgUW48gfFKnwmpJ5t3O3Tbl5oX5c/8N2gY99qvW
chqgNTaRwqS8lQJb0jUP6Wrf6xlAj2fXbsOCoVFKhJmrUD1RT5LrU6LhGUGzCvov
--Rrmlds5vN3IeeCVjbnepHmuVgyROSBjsS-- nITh9wLCgrjQ37wg8Iq57EqiCZgIJdZQuHXvm7FmAjKjfXovfYTgkVMoWd4N7K/E
QaMFQHXL42cXiAygCwARAQABwsB2BBgBCAAgAhsMFiEEXOYJeXAvKFvzKPmxFhwI
dYIjWfcFAmRH1LoACgkQFhwIdYIjWfcnHwf/e4jhRXmEhQeQqjsbMqPU4x73tATs
3xsi4et9vrFEBMN6qsFa3gKq41Wv8JyxVuuz23+BgZ+mZ2iDCKuK7+neMJw2eG9f
MkJCExK1r1Pt7YaXmUvzVHlpS2ZVxWrg4QnT6QKuU58O/uZatKXtgRHDQReCEaAU
bO1EFYa/0KTfsWsG50FDNaCDjJY01rkGbT6O+TrJbUJ+ffjk5+2WEA+EN0p9LzwM
xWzwWuH6LLc28fJLUxln4QXLUK6cEtOlaqEqMK/ERWPUvrLIwivh5atrfGQtAgS9
WlNQAD6nADn68Pa4p9KzdXR9O4Nbg3mfUp8i7nnuUDe691WPG/bYjiVfZsLAfAQY
AQgAJhYhBFzmCXlwLyhb8yj5sRYcCHWCI1n3BQJcZVD8AhsMBQkDwmcAAAoJEBYc
CHWCI1n3rhcH/iCB0ZV861H0RKJ2F7bXEyCLR2ncBFUCnFo3muSrN9NXTojz2vwv
zexRBpZzaRJoksBkvH+ofuZ1iK7ycZO23dnukvPwGQsz3QiITjVeB6ZR0250MG1q
A5yZRlZCsCbGJb4/2e8Ey8BbblHn49Zta4l2Ex5NpNNQ8FYoqXhXu5Bd2F/wX/Bp
5gkZegfE3H9Dw4QjP82Mt0RZSBg9RMGCk6nNfEuze1Up+IOdtqzf3/Z8J5XxLzN2
s8WPmDwJDwvxJRtto8U+ulv4ElcwlA+wYiKAq7cRCKGM/si5ClkUNgb319grUrBU
6h8SuYtgnD84965xRiVAgtH4wCPN544N8CE=3D
=3DNmqc
-----END PGP PUBLIC KEY BLOCK-----
--------------Z0a0IncIqoVlOIpOfcALEK5P--
--------------6sNRIgaKOJHTghuLxPUzlxn1--
--e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
Content-Description: OpenPGP digital signature
Content-Disposition: attachment; filename=OpenPGP_signature
Content-Type: application/pgp-signature; name=OpenPGP_signature.asc
-----BEGIN PGP SIGNATURE-----
Version: GopenPGP 2.7.1
Comment: https://gopenpgp.org
wsB5BAABCAAjFiEEXOYJeXAvKFvzKPmxFhwIdYIjWfcFAmRIzW4FAwAAAAAACgkQ
FhwIdYIjWfe84QgAj1mf/g/jvobkik9RRvyUCN3InyHBqcwMOgyVDmd4U5uh/DVZ
sZ8kFU+/koEfr/8+Y7ataenEG5pY2f4krZNzc85VzjwniK1xcjxDW86UFa/ud/2Q
OD7eqJUQO1UpuQ/xe4gbfy3BKvTp5u09UjcBDnZ/lHASSREpfGBhk37Y2lkb5iIP
4KR7DOi1S1MFrdDPgKp59B9mk2ZMSC1Njzido9Zm/2tdYldAI66uMQiMgB2iv7yn
FMGxKzewt2OxZWKVmNaAzzvrQw7AYfIr70NbwnK75Gmh1CkwLadNpzSRMCqW5BP6
TsypxeGfXuQi0upWT2E3YpIvtkOrHRGXY9jXNw==
=5Yce
-----END PGP SIGNATURE-----
--e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855--

View File

@ -1,116 +1,119 @@
Content-Type: multipart/signed; micalg=pgp-sha256; Content-Type: multipart/signed;
protocol="application/pgp-signature"; boundary=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855;
boundary="pavrbLYh8Q4RWBboYnVxY3mNBBzan1Zz4" micalg=SHA-256; protocol="application/pgp-signature"
This is an OpenPGP/MIME signed message (RFC 4880 and 3156) --e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
--pavrbLYh8Q4RWBboYnVxY3mNBBzan1Zz4 Content-Type: multipart/mixed; boundary="------------iFRY0nd000IWAcfqrg6ZdUuF";
Content-Type: multipart/mixed; boundary="avFoFILZo8SdHM1Pc1OUviN4UKQh16HyR"; protected-headers="v1"
protected-headers="v1" From: "pm.bridge.qa" <pm.bridge.qa@gmail.com>
Subject: simple html body To: "InfernalBridgeTester@proton.me" <InfernalbridgeTester@proton.me>
From: "pm.bridge.qa" <pm.bridge.qa@gmail.com> Message-ID: <1655e652-1abc-575f-6e03-5bbf119afbfa@gmail.com>
To: schizofrenic@pm.me Subject: simple html body
Message-ID: <d9c99685-4e1c-8f95-8b68-c6b0fcfd62ef@gmail.com>
--------------iFRY0nd000IWAcfqrg6ZdUuF
--avFoFILZo8SdHM1Pc1OUviN4UKQh16HyR Content-Type: multipart/mixed; boundary="------------0tRoF00iqRv70TBczVNfagKk"
Content-Type: multipart/mixed;
boundary="------------9EAE2E1A715ACB9849E5C4E3" --------------0tRoF00iqRv70TBczVNfagKk
Content-Language: en-US Content-Type: text/html; charset=UTF-8
Content-Transfer-Encoding: quoted-printable
This is a multi-part message in MIME format.
--------------9EAE2E1A715ACB9849E5C4E3 <html>
Content-Type: text/html; charset=utf-8 <head>
Content-Transfer-Encoding: quoted-printable
<meta http-equiv=3D"content-type" content=3D"text/html; charset=3DUTF=
<html> -8">
<head> </head>
<body>
<meta http-equiv=3D"content-type" content=3D"text/html; charset=3DUTF= <p> </p>
-8"> <p> And this is HTML<br>
</head> </p>
<body> <ul>
And this is HTML<br> <li><b>Do I enjoy making courthouse puns?</b>
<ul> Guilty.=3D3DE2=3D3D80=3D3D9=3D
<li><b>Do I enjoy making courthouse puns?</b> Guilty.=E2=80=94 <i>@= 4 <i>@=3D3D baddadjokes</i></li>
baddadjokes</i></li> <li><b>Can February March?</b> No, but April May. =3D3DE2=3D3D80=3D=
<li><b>Can February March?</b> No, but April May. =E2=80=94<i>@Bear= 3D94<i=3D>@Bear=3D3D
dedMOGuy</i></li> dedMOGuy</i=3D></li>
</ul> </ul>
</body> <p> <br>
</html> </p>
<p>
--------------9EAE2E1A715ACB9849E5C4E3 <meta http-equiv=3D"3D&quot;" content-type"=3D""
Content-Type: application/pgp-keys; content=3D"3D&quot;text/html;" charset=3D"3DUTF=3D" -8"=3D"">
name="OpenPGP_0x161C0875822359F7.asc" </p>
Content-Transfer-Encoding: quoted-printable </body>
Content-Disposition: attachment; </html>
filename="OpenPGP_0x161C0875822359F7.asc" --------------0tRoF00iqRv70TBczVNfagKk
Content-Type: application/pgp-keys; name="OpenPGP_0x161C0875822359F7.asc"
-----BEGIN PGP PUBLIC KEY BLOCK----- Content-Disposition: attachment; filename="OpenPGP_0x161C0875822359F7.asc"
Content-Description: OpenPGP public key
xsBNBFxlUPwBCACx954Ey4SD88f8DSKFw9BaZNXrNwYxNYSgqaqOGHQ0WllF3mstEhTfuxxCZ= Content-Transfer-Encoding: quoted-printable
pDh
I5IhWCXUNxanzsFkn88mRDwFRVl2sf2aAG4/P/p1381oh2kd0UElMRQaQGzoCadQMaQOL9WYT= -----BEGIN PGP PUBLIC KEY BLOCK-----
f4S
PWSCzjrPyKgjq5FbqjbF/ndu376na9L+tnsEXyL6RrI6aZhjWG73xlqxS65dzTIYzsyM/P97x= xsBNBFxlUPwBCACx954Ey4SD88f8DSKFw9BaZNXrNwYxNYSgqaqOGHQ0WllF3mst
Snd EhTfuxxCZpDhI5IhWCXUNxanzsFkn88mRDwFRVl2sf2aAG4/P/p1381oh2kd0UEl
NvlvWtGvLlpFkzxfAEGpVzfOYVYFKoc8rGmUDwrDWYfk5JczRDDogJnY+BNMZf9pjSqk6rTyB= MRQaQGzoCadQMaQOL9WYTf4SPWSCzjrPyKgjq5FbqjbF/ndu376na9L+tnsEXyL6
OfN RrI6aZhjWG73xlqxS65dzTIYzsyM/P97xSndNvlvWtGvLlpFkzxfAEGpVzfOYVYF
H5fpU8r7A5Q7l+HVakvMUQ9DzDWJtg2ru1Y8hexnJOF68avO4+a1ABEBAAHNKEJyaWRnZSBLe= Koc8rGmUDwrDWYfk5JczRDDogJnY+BNMZf9pjSqk6rTyBOfNH5fpU8r7A5Q7l+HV
XUt akvMUQ9DzDWJtg2ru1Y8hexnJOF68avO4+a1ABEBAAHNKEJyaWRnZSBLeXUtRWh5
RWh5aiA8cG0uYnJpZGdlLnFhQGdtYWlsLmNvbT7CwJQEEwEIAD4CGwMFCwkIBwIGFQoJCAsCB= aiA8cG0uYnJpZGdlLnFhQGdtYWlsLmNvbT7CwI4EEwEIADgCGwMFCwkIBwIGFQoJ
BYC CAsCBBYCAwECHgECF4AWIQRc5gl5cC8oW/Mo+bEWHAh1giNZ9wUCZEfUzQAKCRAW
AwECHgECF4AWIQRc5gl5cC8oW/Mo+bEWHAh1giNZ9wUCYC32ygUJB4sMzgAKCRAWHAh1giNZ9= HAh1giNZ9872B/4mM6cXErSSYK6/apGUVebg9QiP1RhFlLE/kg7BW3FaSP/yTUZN
/K8 ZdX7WPnEVyaa5Dk4xRZiB47Yc5myspwc+JEJ3YDAHq+4n/D74TF1kUCzP+QVsWcn
B/4qs84Ii/zKH+q+C8vwO4jUJkOM73qD0pgB7zBs651zWbpgopyol1YUKNpFaHlx/Qch7RDI7= 40UqX9gHbO01O/DYtoxMOljEgkfQjEZcRoHuUhCUzldFf8aV+uZKiOXhrPYCwsil
Vcz nh0RAmDV7fLoOfKXMLiKXE8wM/5Bax+dk2AmEM4bOTIo58GGDDqseIg03ocrW7vP
1+60/KZJSJR19/N2EDVbCUdh8ueioUp9X/218YWV2TRJNxTnljd4FAn7smZnXuP1TsLjQ6sKO= egmxiLUwmsHIIDwTq6qZ0CVxbt2uv4cCyNz/0pmRzG7p8192Evdu8JOuLSj3pI1X
V0U 00Ub326yay3BBUnsL4PJIGoly8hnLb5N3cyNwsCUBBMBCAA+FiEEXOYJeXAvKFvz
u6JoiG6LZFXqDgxYpA++58Rkl6xaY6R71VkmVQlbEKtubX9AjHydq97Y+Jvn11XzWZaKhv4L7= KPmxFhwIdYIjWfcFAlxlUPwCGwMFCQPCZwAFCwkIBwIGFQoJCAsCBBYCAwECHgEC
6Pa F4AACgkQFhwIdYIjWfeQzgf+NWseR+UvrJ7I1CFd6M2+RmyIC1QTQ+uyJIhIBQfC
4tMKXvvrKh1oywMmh6mZJo+5ZA/ABTkr45cwlTPYqGTS9+uvOHt+PH/oYwwJB4ls2cIAUldSj= vBhueU9GUPWR6a6GWsb2DGCMkBM2aHoTCcf46fq87VspYb3+b0JsKfRFxBa2cuNH
TVQ Ey6PK3xHBDnYFtSZaRB7QMQMfYVin5oyHq8mOd4mImOKfpGhuuhq1hrT8sOhVxRY
IsseYz3LlbcCfKJiiCFxeHOQXA5J6zNLKOT58TsczsBNBFxlUPwBCADh2HsX23yVnJt9fxFz3= Nl/2Hanya7tEJlVyAAEwtN4QVCqiRjjD7kBQ+mgxdDFo62X+sl4Zz2BFlZks+c1+
D07 LRWwaQZvGgf2tm2NqZhC04CKc2Gg5j7wNJBPVh/FVluxY27D2hV6v9/cTvXDGo8J
kCBNvu4HQfps6h1rgNxGhE32VmpESHebvIB5xjL6xKbIqqRa3x/7KDVBNJvca0gUsqEt5kzYF= pnZ28CnQqiiaKEU83BGwcUlcr4G5YApHyIPujaxffE2R887ATQRcZVD8AQgA4dh7
88F F9t8lZybfX8Rc9w9O5AgTb7uB0H6bOoda4DcRoRN9lZqREh3m7yAecYy+sSmyKqk
yf2NBcejpIbcP7BS/g+C6KOowYj+Et26T6GdwFXExUcl80JvoX8yHQOfvJpdiBRbjyB8UqfCa= Wt8f+yg1QTSb3GtIFLKhLeZM2BfPBcn9jQXHo6SG3D+wUv4PguijqMGI/hLduk+h
knm ncBVxMVHJfNCb6F/Mh0Dn7yaXYgUW48gfFKnwmpJ5t3O3Tbl5oX5c/8N2gY99qvW
3c7dNuXmhflz/w3aBj32q9ZyGqA1NpHCpLyVAlvSNQ/pat/rGUCPZ9duw4KhUUqEmatQPVFPk= chqgNTaRwqS8lQJb0jUP6Wrf6xlAj2fXbsOCoVFKhJmrUD1RT5LrU6LhGUGzCvov
utT nITh9wLCgrjQ37wg8Iq57EqiCZgIJdZQuHXvm7FmAjKjfXovfYTgkVMoWd4N7K/E
ouEZQbMK+i+chOH3AsKCuNDfvCDwirnsSqIJmAgl1lC4de+bsWYCMqN9ei99hOCRUyhZ3g3sr= QaMFQHXL42cXiAygCwARAQABwsB2BBgBCAAgAhsMFiEEXOYJeXAvKFvzKPmxFhwI
8RB dYIjWfcFAmRH1LoACgkQFhwIdYIjWfcnHwf/e4jhRXmEhQeQqjsbMqPU4x73tATs
owVAdcvjZxeIDKALABEBAAHCwHwEGAEIACYCGwwWIQRc5gl5cC8oW/Mo+bEWHAh1giNZ9wUCY= 3xsi4et9vrFEBMN6qsFa3gKq41Wv8JyxVuuz23+BgZ+mZ2iDCKuK7+neMJw2eG9f
C32 MkJCExK1r1Pt7YaXmUvzVHlpS2ZVxWrg4QnT6QKuU58O/uZatKXtgRHDQReCEaAU
lAUJB4sMmAAKCRAWHAh1giNZ9+Y2B/9rTKZaKviae+ummXNumXcrKvbkAAvfuLpKUn53FlQLm= bO1EFYa/0KTfsWsG50FDNaCDjJY01rkGbT6O+TrJbUJ+ffjk5+2WEA+EN0p9LzwM
L6H xWzwWuH6LLc28fJLUxln4QXLUK6cEtOlaqEqMK/ERWPUvrLIwivh5atrfGQtAgS9
jB++lJnPWvVSzdZxdv8FiPP3d632XHKUrkQRQM/9byRDXDommi7Qttx7YCkhd4JLVYqJqpnAQ= WlNQAD6nADn68Pa4p9KzdXR9O4Nbg3mfUp8i7nnuUDe691WPG/bYjiVfZsLAfAQY
xI5 AQgAJhYhBFzmCXlwLyhb8yj5sRYcCHWCI1n3BQJcZVD8AhsMBQkDwmcAAAoJEBYc
RMkXiZNWyr1lz8JOM1XvDk1M7sJwPMWews8VOIE03E1nt7AsQGnvHtadgEnQaufrYNX3hFA8S= CHWCI1n3rhcH/iCB0ZV861H0RKJ2F7bXEyCLR2ncBFUCnFo3muSrN9NXTojz2vwv
osO zexRBpZzaRJoksBkvH+ofuZ1iK7ycZO23dnukvPwGQsz3QiITjVeB6ZR0250MG1q
HSnedcys6yrzCSIGCqCD9VHbnMtS4DOv0XJGh2hwc8omzH0KZA517dyKBorJRwadcVauGXDKx= A5yZRlZCsCbGJb4/2e8Ey8BbblHn49Zta4l2Ex5NpNNQ8FYoqXhXu5Bd2F/wX/Bp
Etv 5gkZegfE3H9Dw4QjP82Mt0RZSBg9RMGCk6nNfEuze1Up+IOdtqzf3/Z8J5XxLzN2
Im4rl94PR/3An1Mj6HeeVVpLqDQ5Jb9J90BahWeQ53FzRa4EQzYCw0nLnxcsT1ZEEP5u s8WPmDwJDwvxJRtto8U+ulv4ElcwlA+wYiKAq7cRCKGM/si5ClkUNgb319grUrBU
=3Dv/1p 6h8SuYtgnD84965xRiVAgtH4wCPN544N8CE=3D
-----END PGP PUBLIC KEY BLOCK----- =3DNmqc
-----END PGP PUBLIC KEY BLOCK-----
--------------9EAE2E1A715ACB9849E5C4E3--
--------------0tRoF00iqRv70TBczVNfagKk--
--avFoFILZo8SdHM1Pc1OUviN4UKQh16HyR--
--------------iFRY0nd000IWAcfqrg6ZdUuF--
--pavrbLYh8Q4RWBboYnVxY3mNBBzan1Zz4
Content-Type: application/pgp-signature; name="OpenPGP_signature.asc" --e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
Content-Description: OpenPGP digital signature Content-Description: OpenPGP digital signature
Content-Disposition: attachment; filename="OpenPGP_signature" Content-Disposition: attachment; filename=OpenPGP_signature
Content-Type: application/pgp-signature; name=OpenPGP_signature.asc
-----BEGIN PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
wsB5BAABCAAjFiEEXOYJeXAvKFvzKPmxFhwIdYIjWfcFAmBa9hAFAwAAAAAACgkQFhwIdYIjWffL Version: GopenPGP 2.7.1
1AgApF18AVOPEm9y5R+d0NQmxqhSwAtvaqCwqQpG3mArIYK3Y0zrDkPQZZl/3emW8LWht7ZyYCAb Comment: https://gopenpgp.org
NZo7HoYxjLy3yxAOPUl/Pc0nJpEqk/wAZT58yOnzv8DU5Q9o+444FfTMJpcrcH/M5cXYyqRtVhas
k5wu5u2DEgSO3Kj/5l7lThb+CUgRC6wSiOuUkqGEWLiAguCdd88XDkLMbwrDnOu3PbhcA8o1msns wsB5BAABCAAjFiEEXOYJeXAvKFvzKPmxFhwIdYIjWfcFAmRH7c8FAwAAAAAACgkQ
PfkBdq3mFjp4M8M4ha+D2MxmV6tBv1E7snWf/spBVb9fHIa7zI4ZS6shpzGHCnJarO0Jco0Qh3IZ FhwIdYIjWffGuQgAlK68JzT/9NLOg4SZPUbVaEdlbru6ebX1Lwaj93FZEKfmiEZ4
ZVfwhtJeFsmdqSm6DLvCmQWAYk2fDOZDMVKqe9IbUA== heuYfq4yVw1JI/CGhHa1wxUJM6+Tg+0lqIslG1rTposgIziugMBhEWXYWXBwDdvu
=pkS0 +9T2eqo0se3h+oXueVlRihowfuPcP9jvt6p3DR+pu+hJ/D42DarJrn8v0CMm3PyL
-----END PGP SIGNATURE----- NHodyKLmEtZrzP4ok9Wal850xZ3Tmu5dL3v300wPGJaxrn8o4OeHRUodE43UijUr
FKNoEfhqzY8HsMKIdF8Bi6+XiA0sHwcaUqJsaczBqgmbJbPqG8N4CokiUnUu4QDm
--pavrbLYh8Q4RWBboYnVxY3mNBBzan1Zz4-- ClWa0rG4I5rrx4PrTbzAt7cGIb8N3ACNI7vI/Q==
=Llod
-----END PGP SIGNATURE-----
--e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855--

View File

@ -1,161 +1,143 @@
Content-Type: multipart/signed; micalg=pgp-sha256; Content-Type: multipart/signed;
protocol="application/pgp-signature"; boundary=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855;
boundary="MHEDFShwcX18dyE3X7RXujo5fjpgdjHNM" micalg=SHA-256; protocol="application/pgp-signature"
This is an OpenPGP/MIME signed message (RFC 4880 and 3156) --e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
--MHEDFShwcX18dyE3X7RXujo5fjpgdjHNM Content-Type: multipart/mixed; boundary="------------8730PPHC3FpcshavBTup7Fxz";
Content-Type: multipart/mixed; boundary="FBBl2LNv76z8UkvHhSkT9vLwVwxqV8378"; protected-headers="v1"
protected-headers="v1" From: "pm.bridge.qa" <pm.bridge.qa@gmail.com>
Subject: Alternative To: "InfernalBridgeTester@proton.me" <InfernalbridgeTester@proton.me>
From: "pm.bridge.qa" <pm.bridge.qa@gmail.com> Message-ID: <5dce90f2-f1e5-eecb-672e-f39def728f44@gmail.com>
To: schizofrenic@pm.me Subject: Alternative
Message-ID: <753d0314-0286-2c88-2abb-f8080ac7a4cb@gmail.com>
--------------8730PPHC3FpcshavBTup7Fxz
--FBBl2LNv76z8UkvHhSkT9vLwVwxqV8378 Content-Type: multipart/mixed; boundary="------------CCuN7f3eLVIM0a1fSlNjk0pq"
Content-Type: multipart/mixed;
boundary="------------F97C8ED4878E94675762AE43" --------------CCuN7f3eLVIM0a1fSlNjk0pq
Content-Language: en-US Content-Type: multipart/alternative;
boundary="------------5kz0EAFr3j63ikmxfQPRt0bq"
This is a multi-part message in MIME format.
--------------F97C8ED4878E94675762AE43 --------------5kz0EAFr3j63ikmxfQPRt0bq
Content-Type: multipart/alternative; Content-Type: text/plain; charset=UTF-8; format=flowed
boundary="------------041318B15DD3FA540FED32C6" Content-Transfer-Encoding: base64
VGhpcyBSaWNoIGZvcm1hdGVkIHRleHQNCg0KICAqIFdoYXQga2luZCBvZiBzaG9lcyBkbyBu
--------------041318B15DD3FA540FED32C6 aW5qYXMgd2Vhcj8gKlNuZWFrZXJzISoNCiAgKiBIb3cgZG9lcyBhIHBlbmd1aW4gYnVpbGQg
Content-Type: text/plain; charset=utf-8; format=flowed aXRzIGhvdXNlPyAqSWdsb29zIGl0IHRvZ2V0aGVyKg0KDQpUaGlzIFJpY2ggZm9ybWF0ZWQg
Content-Transfer-Encoding: quoted-printable dGV4dA0KDQogICogL1doYXQga2luZCBvZiBzaG9lcyBkbyBuaW5qYXMgd2Vhcj8gLypTbmVh
a2VycyEqDQogICogL0hvdyBkb2VzIGEgcGVuZ3VpbiBidWlsZCBpdHMgaG91c2U/LyoqXy8q
This Rich formated text SWdsb29zIGl0IHRvZ2V0aGVyLiovXw0KDQoNCg0K
--------------5kz0EAFr3j63ikmxfQPRt0bq
* /What kind of shoes do ninjas wear? /*Sneakers!* Content-Type: text/html; charset=UTF-8
* /How does a penguin build its house?/**_/*Igloos it together.*/_ Content-Transfer-Encoding: quoted-printable
<html>
<head>
--------------041318B15DD3FA540FED32C6 <meta http-equiv=3D"content-type" content=3D"text/html; charset=3DUTF=
Content-Type: text/html; charset=utf-8 -8">
Content-Transfer-Encoding: quoted-printable </head>
<body>
<html> <p>This Rich formated text<br>
<head> </p>
<ul>
<meta http-equiv=3D"content-type" content=3D"text/html; charset=3DUTF= <li>What kind of shoes do ninjas wear? <b>Sneakers!</b></li>
-8"> <li>How does a penguin build its house? <b>Igloos it together</b></=
</head> li>
<body> </ul>
<p>This <font color=3D"#ee24cc">Rich</font> formated text</p> <p> </p>
<ul> <p>This <font color=3D"3D&quot;#ee24cc&quot;">Rich</font> formated
<li><i>What kind of shoes do ninjas wear? </i><b>Sneakers!</b></li>= text</p>
<ul>
<li><i>How does a penguin build its house?</i><b> </b><u><i><b>Iglo= <li><i>What kind of shoes do ninjas wear? </i><b>Sneakers!</b></li>=
os
it together.</b></i></u></li> <li><i>How does a penguin build its house?</i><b> </b><u><i><b>Iglo=
</ul> os
<p><br> it together.</b></i></u></li>
</p> </ul>
<p><br> <p><br>
</p> </p>
</body> <p><br>
</html> </p>
<p>
--------------041318B15DD3FA540FED32C6-- <meta http-equiv=3D"3D&quot;content-type&quot;"
content=3D"3D&quot;text/html;" charset=3D"3DUTF-8&quot;">
--------------F97C8ED4878E94675762AE43 </p>
Content-Type: application/pdf; </body>
name="minimal.pdf" </html>
Content-Transfer-Encoding: base64
Content-Disposition: attachment; --------------5kz0EAFr3j63ikmxfQPRt0bq--
filename="minimal.pdf" --------------CCuN7f3eLVIM0a1fSlNjk0pq
Content-Type: application/pgp-keys; name="OpenPGP_0x161C0875822359F7.asc"
JVBERi0xLjEKJcKlwrHDqwoKMSAwIG9iagogIDw8IC9UeXBlIC9DYXRhbG9nCiAgICAgL1Bh Content-Disposition: attachment; filename="OpenPGP_0x161C0875822359F7.asc"
Z2VzIDIgMCBSCiAgPj4KZW5kb2JqCgoyIDAgb2JqCiAgPDwgL1R5cGUgL1BhZ2VzCiAgICAg Content-Description: OpenPGP public key
L0tpZHMgWzMgMCBSXQogICAgIC9Db3VudCAxCiAgICAgL01lZGlhQm94IFswIDAgMzAwIDE0 Content-Transfer-Encoding: quoted-printable
NF0KICA+PgplbmRvYmoKCjMgMCBvYmoKICA8PCAgL1R5cGUgL1BhZ2UKICAgICAgL1BhcmVu
dCAyIDAgUgogICAgICAvUmVzb3VyY2VzCiAgICAgICA8PCAvRm9udAogICAgICAgICAgIDw8 -----BEGIN PGP PUBLIC KEY BLOCK-----
IC9GMQogICAgICAgICAgICAgICA8PCAvVHlwZSAvRm9udAogICAgICAgICAgICAgICAgICAv
U3VidHlwZSAvVHlwZTEKICAgICAgICAgICAgICAgICAgL0Jhc2VGb250IC9UaW1lcy1Sb21h xsBNBFxlUPwBCACx954Ey4SD88f8DSKFw9BaZNXrNwYxNYSgqaqOGHQ0WllF3mst
bgogICAgICAgICAgICAgICA+PgogICAgICAgICAgID4+CiAgICAgICA+PgogICAgICAvQ29u EhTfuxxCZpDhI5IhWCXUNxanzsFkn88mRDwFRVl2sf2aAG4/P/p1381oh2kd0UEl
dGVudHMgNCAwIFIKICA+PgplbmRvYmoKCjQgMCBvYmoKICA8PCAvTGVuZ3RoIDU1ID4+CnN0 MRQaQGzoCadQMaQOL9WYTf4SPWSCzjrPyKgjq5FbqjbF/ndu376na9L+tnsEXyL6
cmVhbQogIEJUCiAgICAvRjEgMTggVGYKICAgIDAgMCBUZAogICAgKEhlbGxvIFdvcmxkKSBU RrI6aZhjWG73xlqxS65dzTIYzsyM/P97xSndNvlvWtGvLlpFkzxfAEGpVzfOYVYF
agogIEVUCmVuZHN0cmVhbQplbmRvYmoKCnhyZWYKMCA1CjAwMDAwMDAwMDAgNjU1MzUgZiAK Koc8rGmUDwrDWYfk5JczRDDogJnY+BNMZf9pjSqk6rTyBOfNH5fpU8r7A5Q7l+HV
MDAwMDAwMDAxOCAwMDAwMCBuIAowMDAwMDAwMDc3IDAwMDAwIG4gCjAwMDAwMDAxNzggMDAw akvMUQ9DzDWJtg2ru1Y8hexnJOF68avO4+a1ABEBAAHNKEJyaWRnZSBLeXUtRWh5
MDAgbiAKMDAwMDAwMDQ1NyAwMDAwMCBuIAp0cmFpbGVyCiAgPDwgIC9Sb290IDEgMCBSCiAg aiA8cG0uYnJpZGdlLnFhQGdtYWlsLmNvbT7CwI4EEwEIADgCGwMFCwkIBwIGFQoJ
ICAgIC9TaXplIDUKICA+PgpzdGFydHhyZWYKNTY1CiUlRU9GCg== CAsCBBYCAwECHgECF4AWIQRc5gl5cC8oW/Mo+bEWHAh1giNZ9wUCZEfUzQAKCRAW
--------------F97C8ED4878E94675762AE43 HAh1giNZ9872B/4mM6cXErSSYK6/apGUVebg9QiP1RhFlLE/kg7BW3FaSP/yTUZN
Content-Type: application/pgp-keys; ZdX7WPnEVyaa5Dk4xRZiB47Yc5myspwc+JEJ3YDAHq+4n/D74TF1kUCzP+QVsWcn
name="OpenPGP_0x161C0875822359F7.asc" 40UqX9gHbO01O/DYtoxMOljEgkfQjEZcRoHuUhCUzldFf8aV+uZKiOXhrPYCwsil
Content-Transfer-Encoding: quoted-printable nh0RAmDV7fLoOfKXMLiKXE8wM/5Bax+dk2AmEM4bOTIo58GGDDqseIg03ocrW7vP
Content-Disposition: attachment; egmxiLUwmsHIIDwTq6qZ0CVxbt2uv4cCyNz/0pmRzG7p8192Evdu8JOuLSj3pI1X
filename="OpenPGP_0x161C0875822359F7.asc" 00Ub326yay3BBUnsL4PJIGoly8hnLb5N3cyNwsCUBBMBCAA+FiEEXOYJeXAvKFvz
KPmxFhwIdYIjWfcFAlxlUPwCGwMFCQPCZwAFCwkIBwIGFQoJCAsCBBYCAwECHgEC
-----BEGIN PGP PUBLIC KEY BLOCK----- F4AACgkQFhwIdYIjWfeQzgf+NWseR+UvrJ7I1CFd6M2+RmyIC1QTQ+uyJIhIBQfC
vBhueU9GUPWR6a6GWsb2DGCMkBM2aHoTCcf46fq87VspYb3+b0JsKfRFxBa2cuNH
xsBNBFxlUPwBCACx954Ey4SD88f8DSKFw9BaZNXrNwYxNYSgqaqOGHQ0WllF3mstEhTfuxxCZ= Ey6PK3xHBDnYFtSZaRB7QMQMfYVin5oyHq8mOd4mImOKfpGhuuhq1hrT8sOhVxRY
pDh Nl/2Hanya7tEJlVyAAEwtN4QVCqiRjjD7kBQ+mgxdDFo62X+sl4Zz2BFlZks+c1+
I5IhWCXUNxanzsFkn88mRDwFRVl2sf2aAG4/P/p1381oh2kd0UElMRQaQGzoCadQMaQOL9WYT= LRWwaQZvGgf2tm2NqZhC04CKc2Gg5j7wNJBPVh/FVluxY27D2hV6v9/cTvXDGo8J
f4S pnZ28CnQqiiaKEU83BGwcUlcr4G5YApHyIPujaxffE2R887ATQRcZVD8AQgA4dh7
PWSCzjrPyKgjq5FbqjbF/ndu376na9L+tnsEXyL6RrI6aZhjWG73xlqxS65dzTIYzsyM/P97x= F9t8lZybfX8Rc9w9O5AgTb7uB0H6bOoda4DcRoRN9lZqREh3m7yAecYy+sSmyKqk
Snd Wt8f+yg1QTSb3GtIFLKhLeZM2BfPBcn9jQXHo6SG3D+wUv4PguijqMGI/hLduk+h
NvlvWtGvLlpFkzxfAEGpVzfOYVYFKoc8rGmUDwrDWYfk5JczRDDogJnY+BNMZf9pjSqk6rTyB= ncBVxMVHJfNCb6F/Mh0Dn7yaXYgUW48gfFKnwmpJ5t3O3Tbl5oX5c/8N2gY99qvW
OfN chqgNTaRwqS8lQJb0jUP6Wrf6xlAj2fXbsOCoVFKhJmrUD1RT5LrU6LhGUGzCvov
H5fpU8r7A5Q7l+HVakvMUQ9DzDWJtg2ru1Y8hexnJOF68avO4+a1ABEBAAHNKEJyaWRnZSBLe= nITh9wLCgrjQ37wg8Iq57EqiCZgIJdZQuHXvm7FmAjKjfXovfYTgkVMoWd4N7K/E
XUt QaMFQHXL42cXiAygCwARAQABwsB2BBgBCAAgAhsMFiEEXOYJeXAvKFvzKPmxFhwI
RWh5aiA8cG0uYnJpZGdlLnFhQGdtYWlsLmNvbT7CwJQEEwEIAD4CGwMFCwkIBwIGFQoJCAsCB= dYIjWfcFAmRH1LoACgkQFhwIdYIjWfcnHwf/e4jhRXmEhQeQqjsbMqPU4x73tATs
BYC 3xsi4et9vrFEBMN6qsFa3gKq41Wv8JyxVuuz23+BgZ+mZ2iDCKuK7+neMJw2eG9f
AwECHgECF4AWIQRc5gl5cC8oW/Mo+bEWHAh1giNZ9wUCYC32ygUJB4sMzgAKCRAWHAh1giNZ9= MkJCExK1r1Pt7YaXmUvzVHlpS2ZVxWrg4QnT6QKuU58O/uZatKXtgRHDQReCEaAU
/K8 bO1EFYa/0KTfsWsG50FDNaCDjJY01rkGbT6O+TrJbUJ+ffjk5+2WEA+EN0p9LzwM
B/4qs84Ii/zKH+q+C8vwO4jUJkOM73qD0pgB7zBs651zWbpgopyol1YUKNpFaHlx/Qch7RDI7= xWzwWuH6LLc28fJLUxln4QXLUK6cEtOlaqEqMK/ERWPUvrLIwivh5atrfGQtAgS9
Vcz WlNQAD6nADn68Pa4p9KzdXR9O4Nbg3mfUp8i7nnuUDe691WPG/bYjiVfZsLAfAQY
1+60/KZJSJR19/N2EDVbCUdh8ueioUp9X/218YWV2TRJNxTnljd4FAn7smZnXuP1TsLjQ6sKO= AQgAJhYhBFzmCXlwLyhb8yj5sRYcCHWCI1n3BQJcZVD8AhsMBQkDwmcAAAoJEBYc
V0U CHWCI1n3rhcH/iCB0ZV861H0RKJ2F7bXEyCLR2ncBFUCnFo3muSrN9NXTojz2vwv
u6JoiG6LZFXqDgxYpA++58Rkl6xaY6R71VkmVQlbEKtubX9AjHydq97Y+Jvn11XzWZaKhv4L7= zexRBpZzaRJoksBkvH+ofuZ1iK7ycZO23dnukvPwGQsz3QiITjVeB6ZR0250MG1q
6Pa A5yZRlZCsCbGJb4/2e8Ey8BbblHn49Zta4l2Ex5NpNNQ8FYoqXhXu5Bd2F/wX/Bp
4tMKXvvrKh1oywMmh6mZJo+5ZA/ABTkr45cwlTPYqGTS9+uvOHt+PH/oYwwJB4ls2cIAUldSj= 5gkZegfE3H9Dw4QjP82Mt0RZSBg9RMGCk6nNfEuze1Up+IOdtqzf3/Z8J5XxLzN2
TVQ s8WPmDwJDwvxJRtto8U+ulv4ElcwlA+wYiKAq7cRCKGM/si5ClkUNgb319grUrBU
IsseYz3LlbcCfKJiiCFxeHOQXA5J6zNLKOT58TsczsBNBFxlUPwBCADh2HsX23yVnJt9fxFz3= 6h8SuYtgnD84965xRiVAgtH4wCPN544N8CE=3D
D07 =3DNmqc
kCBNvu4HQfps6h1rgNxGhE32VmpESHebvIB5xjL6xKbIqqRa3x/7KDVBNJvca0gUsqEt5kzYF= -----END PGP PUBLIC KEY BLOCK-----
88F
yf2NBcejpIbcP7BS/g+C6KOowYj+Et26T6GdwFXExUcl80JvoX8yHQOfvJpdiBRbjyB8UqfCa= --------------CCuN7f3eLVIM0a1fSlNjk0pq--
knm
3c7dNuXmhflz/w3aBj32q9ZyGqA1NpHCpLyVAlvSNQ/pat/rGUCPZ9duw4KhUUqEmatQPVFPk= --------------8730PPHC3FpcshavBTup7Fxz--
utT
ouEZQbMK+i+chOH3AsKCuNDfvCDwirnsSqIJmAgl1lC4de+bsWYCMqN9ei99hOCRUyhZ3g3sr= --e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
8RB Content-Description: OpenPGP digital signature
owVAdcvjZxeIDKALABEBAAHCwHwEGAEIACYCGwwWIQRc5gl5cC8oW/Mo+bEWHAh1giNZ9wUCY= Content-Disposition: attachment; filename=OpenPGP_signature
C32 Content-Type: application/pgp-signature; name=OpenPGP_signature.asc
lAUJB4sMmAAKCRAWHAh1giNZ9+Y2B/9rTKZaKviae+ummXNumXcrKvbkAAvfuLpKUn53FlQLm=
L6H -----BEGIN PGP SIGNATURE-----
jB++lJnPWvVSzdZxdv8FiPP3d632XHKUrkQRQM/9byRDXDommi7Qttx7YCkhd4JLVYqJqpnAQ= Version: GopenPGP 2.7.1
xI5 Comment: https://gopenpgp.org
RMkXiZNWyr1lz8JOM1XvDk1M7sJwPMWews8VOIE03E1nt7AsQGnvHtadgEnQaufrYNX3hFA8S=
osO wsB5BAABCAAjFiEEXOYJeXAvKFvzKPmxFhwIdYIjWfcFAmRH94EFAwAAAAAACgkQ
HSnedcys6yrzCSIGCqCD9VHbnMtS4DOv0XJGh2hwc8omzH0KZA517dyKBorJRwadcVauGXDKx= FhwIdYIjWfcFfgf/XHYGr9+DcntW0TLe6dO82xhLuO6HFmQnnChnfNHKA5U6pOeU
Etv 5wJoEFL2O4WEU1hbuf5K0wpgPi8ZF+r966bCUTt+tYT/p7sKV0OXYJjtPYxiL+ju
Im4rl94PR/3An1Mj6HeeVVpLqDQ5Jb9J90BahWeQ53FzRa4EQzYCw0nLnxcsT1ZEEP5u RaNZgK3rIDyi2DNjbRXSV1Y7H2S3pMLAwPio7ovNpe3OgfkAgFDdiG+NnXl8CzqN
=3Dv/1p suhMwqJbLJRWlEaX7UdbcLimpNtPIU7wtr4YV0NIsqt3EV7AeHnVI9cHJMLvF5tB
-----END PGP PUBLIC KEY BLOCK----- VrnkxXbdO3xEylUB+MNmX5NDIrCg5Iwwq4NMk6qaMK80J9XSxERln56SJVN/+tIu
TZFgwBWM/n48prGXrEo8TPk5UkrVYhLXl/ndTg==
--------------F97C8ED4878E94675762AE43-- =FIoh
-----END PGP SIGNATURE-----
--FBBl2LNv76z8UkvHhSkT9vLwVwxqV8378-- --e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855--
--MHEDFShwcX18dyE3X7RXujo5fjpgdjHNM
Content-Type: application/pgp-signature; name="OpenPGP_signature.asc"
Content-Description: OpenPGP digital signature
Content-Disposition: attachment; filename="OpenPGP_signature"
-----BEGIN PGP SIGNATURE-----
wsB5BAABCAAjFiEEXOYJeXAvKFvzKPmxFhwIdYIjWfcFAmBciUoFAwAAAAAACgkQFhwIdYIjWfez
rgf+NZCibnCUTovpWRVRiiPQtBPGeHUPEwz2xq2zz4AaqrHC2v4mYUIPe6am7INk8fkBLsa8Dj/A
UN/28Qh7tNb7JsXtHDT4PIoXszukQ8VIRbe09mSkkP6jR4WzNR166d6n3rSxzHpviOyQldjjpOMr
Zl7LxmgGr4ojsgCf6pvurWwCCOGJqbSusrD6JVv6DsmPmmQeBmnlTK/0oG9pnlNkugpNB1WS2K5d
RY6+kWkSrxbq95HrgILpHip8Y/+ITWvQocm14PBIAAdW8Hr7iFQLETFJ/KDA+VP19Bt8n4Kitdi8
DPqMsV0oOhATqBjnD63AePJ0VWg8R1z6GEK5A+WOpg==
=Bc6p
-----END PGP SIGNATURE-----
--MHEDFShwcX18dyE3X7RXujo5fjpgdjHNM--

View File

@ -1,103 +1,95 @@
Content-Type: multipart/signed; micalg=pgp-sha256; Content-Type: multipart/signed;
protocol="application/pgp-signature"; boundary=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855;
boundary="x4FrOFG2PnNJvlbzxe80NPwxzh2yUHABp" micalg=SHA-256; protocol="application/pgp-signature"
This is an OpenPGP/MIME signed message (RFC 4880 and 3156) --e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
--x4FrOFG2PnNJvlbzxe80NPwxzh2yUHABp Content-Type: multipart/mixed; boundary="------------bhHlRtbq4tEzp2NgVmAJ4AbV";
Content-Type: multipart/mixed; boundary="bBln6dwDJTLkin5LPHkHBQudqRLwIzTUH"; protected-headers="v1"
protected-headers="v1" From: "pm.bridge.qa" <pm.bridge.qa@gmail.com>
Subject: simple plaintext body To: "InfernalBridgeTester@proton.me" <InfernalbridgeTester@proton.me>
From: "pm.bridge.qa" <pm.bridge.qa@gmail.com> Message-ID: <9bb09a2a-1442-9439-f53f-490df7e46331@gmail.com>
To: schizofrenic@pm.me Subject: simple plaintext body
Message-ID: <adb5ac5d-b8f6-c9a3-5cc0-0fb2e9677512@gmail.com>
--------------bhHlRtbq4tEzp2NgVmAJ4AbV
--bBln6dwDJTLkin5LPHkHBQudqRLwIzTUH Content-Type: multipart/mixed; boundary="------------FHzf8jBNv06d9SowGc7KOjXr"
Content-Type: multipart/mixed;
boundary="------------1B34C666A4C2FB03E0324F1A" --------------FHzf8jBNv06d9SowGc7KOjXr
Content-Language: en-US Content-Type: text/plain; charset=UTF-8; format=flowed
Content-Transfer-Encoding: base64
This is a multi-part message in MIME format.
--------------1B34C666A4C2FB03E0324F1A V2h5IGRvbid0IGNyYWJzIGdpdmUgdG8gY2hhcml0eT8gQmVjYXVzZSB0aGV5J3JlIHNoZWxs
Content-Type: text/plain; charset=utf-8; format=flowed ZmlzaC4NCg0K
Content-Transfer-Encoding: quoted-printable --------------FHzf8jBNv06d9SowGc7KOjXr
Content-Type: application/pgp-keys; name="OpenPGP_0x161C0875822359F7.asc"
Why don't crabs give to charity? Because they're shellfish. Content-Disposition: attachment; filename="OpenPGP_0x161C0875822359F7.asc"
Content-Description: OpenPGP public key
Content-Transfer-Encoding: quoted-printable
--------------1B34C666A4C2FB03E0324F1A -----BEGIN PGP PUBLIC KEY BLOCK-----
Content-Type: application/pgp-keys;
name="OpenPGP_0x161C0875822359F7.asc" xsBNBFxlUPwBCACx954Ey4SD88f8DSKFw9BaZNXrNwYxNYSgqaqOGHQ0WllF3mst
Content-Transfer-Encoding: quoted-printable EhTfuxxCZpDhI5IhWCXUNxanzsFkn88mRDwFRVl2sf2aAG4/P/p1381oh2kd0UEl
Content-Disposition: attachment; MRQaQGzoCadQMaQOL9WYTf4SPWSCzjrPyKgjq5FbqjbF/ndu376na9L+tnsEXyL6
filename="OpenPGP_0x161C0875822359F7.asc" RrI6aZhjWG73xlqxS65dzTIYzsyM/P97xSndNvlvWtGvLlpFkzxfAEGpVzfOYVYF
Koc8rGmUDwrDWYfk5JczRDDogJnY+BNMZf9pjSqk6rTyBOfNH5fpU8r7A5Q7l+HV
-----BEGIN PGP PUBLIC KEY BLOCK----- akvMUQ9DzDWJtg2ru1Y8hexnJOF68avO4+a1ABEBAAHNKEJyaWRnZSBLeXUtRWh5
aiA8cG0uYnJpZGdlLnFhQGdtYWlsLmNvbT7CwI4EEwEIADgCGwMFCwkIBwIGFQoJ
xsBNBFxlUPwBCACx954Ey4SD88f8DSKFw9BaZNXrNwYxNYSgqaqOGHQ0WllF3mstEhTfuxxCZ= CAsCBBYCAwECHgECF4AWIQRc5gl5cC8oW/Mo+bEWHAh1giNZ9wUCZEfUzQAKCRAW
pDh HAh1giNZ9872B/4mM6cXErSSYK6/apGUVebg9QiP1RhFlLE/kg7BW3FaSP/yTUZN
I5IhWCXUNxanzsFkn88mRDwFRVl2sf2aAG4/P/p1381oh2kd0UElMRQaQGzoCadQMaQOL9WYT= ZdX7WPnEVyaa5Dk4xRZiB47Yc5myspwc+JEJ3YDAHq+4n/D74TF1kUCzP+QVsWcn
f4S 40UqX9gHbO01O/DYtoxMOljEgkfQjEZcRoHuUhCUzldFf8aV+uZKiOXhrPYCwsil
PWSCzjrPyKgjq5FbqjbF/ndu376na9L+tnsEXyL6RrI6aZhjWG73xlqxS65dzTIYzsyM/P97x= nh0RAmDV7fLoOfKXMLiKXE8wM/5Bax+dk2AmEM4bOTIo58GGDDqseIg03ocrW7vP
Snd egmxiLUwmsHIIDwTq6qZ0CVxbt2uv4cCyNz/0pmRzG7p8192Evdu8JOuLSj3pI1X
NvlvWtGvLlpFkzxfAEGpVzfOYVYFKoc8rGmUDwrDWYfk5JczRDDogJnY+BNMZf9pjSqk6rTyB= 00Ub326yay3BBUnsL4PJIGoly8hnLb5N3cyNwsCUBBMBCAA+FiEEXOYJeXAvKFvz
OfN KPmxFhwIdYIjWfcFAlxlUPwCGwMFCQPCZwAFCwkIBwIGFQoJCAsCBBYCAwECHgEC
H5fpU8r7A5Q7l+HVakvMUQ9DzDWJtg2ru1Y8hexnJOF68avO4+a1ABEBAAHNKEJyaWRnZSBLe= F4AACgkQFhwIdYIjWfeQzgf+NWseR+UvrJ7I1CFd6M2+RmyIC1QTQ+uyJIhIBQfC
XUt vBhueU9GUPWR6a6GWsb2DGCMkBM2aHoTCcf46fq87VspYb3+b0JsKfRFxBa2cuNH
RWh5aiA8cG0uYnJpZGdlLnFhQGdtYWlsLmNvbT7CwJQEEwEIAD4CGwMFCwkIBwIGFQoJCAsCB= Ey6PK3xHBDnYFtSZaRB7QMQMfYVin5oyHq8mOd4mImOKfpGhuuhq1hrT8sOhVxRY
BYC Nl/2Hanya7tEJlVyAAEwtN4QVCqiRjjD7kBQ+mgxdDFo62X+sl4Zz2BFlZks+c1+
AwECHgECF4AWIQRc5gl5cC8oW/Mo+bEWHAh1giNZ9wUCYC32ygUJB4sMzgAKCRAWHAh1giNZ9= LRWwaQZvGgf2tm2NqZhC04CKc2Gg5j7wNJBPVh/FVluxY27D2hV6v9/cTvXDGo8J
/K8 pnZ28CnQqiiaKEU83BGwcUlcr4G5YApHyIPujaxffE2R887ATQRcZVD8AQgA4dh7
B/4qs84Ii/zKH+q+C8vwO4jUJkOM73qD0pgB7zBs651zWbpgopyol1YUKNpFaHlx/Qch7RDI7= F9t8lZybfX8Rc9w9O5AgTb7uB0H6bOoda4DcRoRN9lZqREh3m7yAecYy+sSmyKqk
Vcz Wt8f+yg1QTSb3GtIFLKhLeZM2BfPBcn9jQXHo6SG3D+wUv4PguijqMGI/hLduk+h
1+60/KZJSJR19/N2EDVbCUdh8ueioUp9X/218YWV2TRJNxTnljd4FAn7smZnXuP1TsLjQ6sKO= ncBVxMVHJfNCb6F/Mh0Dn7yaXYgUW48gfFKnwmpJ5t3O3Tbl5oX5c/8N2gY99qvW
V0U chqgNTaRwqS8lQJb0jUP6Wrf6xlAj2fXbsOCoVFKhJmrUD1RT5LrU6LhGUGzCvov
u6JoiG6LZFXqDgxYpA++58Rkl6xaY6R71VkmVQlbEKtubX9AjHydq97Y+Jvn11XzWZaKhv4L7= nITh9wLCgrjQ37wg8Iq57EqiCZgIJdZQuHXvm7FmAjKjfXovfYTgkVMoWd4N7K/E
6Pa QaMFQHXL42cXiAygCwARAQABwsB2BBgBCAAgAhsMFiEEXOYJeXAvKFvzKPmxFhwI
4tMKXvvrKh1oywMmh6mZJo+5ZA/ABTkr45cwlTPYqGTS9+uvOHt+PH/oYwwJB4ls2cIAUldSj= dYIjWfcFAmRH1LoACgkQFhwIdYIjWfcnHwf/e4jhRXmEhQeQqjsbMqPU4x73tATs
TVQ 3xsi4et9vrFEBMN6qsFa3gKq41Wv8JyxVuuz23+BgZ+mZ2iDCKuK7+neMJw2eG9f
IsseYz3LlbcCfKJiiCFxeHOQXA5J6zNLKOT58TsczsBNBFxlUPwBCADh2HsX23yVnJt9fxFz3= MkJCExK1r1Pt7YaXmUvzVHlpS2ZVxWrg4QnT6QKuU58O/uZatKXtgRHDQReCEaAU
D07 bO1EFYa/0KTfsWsG50FDNaCDjJY01rkGbT6O+TrJbUJ+ffjk5+2WEA+EN0p9LzwM
kCBNvu4HQfps6h1rgNxGhE32VmpESHebvIB5xjL6xKbIqqRa3x/7KDVBNJvca0gUsqEt5kzYF= xWzwWuH6LLc28fJLUxln4QXLUK6cEtOlaqEqMK/ERWPUvrLIwivh5atrfGQtAgS9
88F WlNQAD6nADn68Pa4p9KzdXR9O4Nbg3mfUp8i7nnuUDe691WPG/bYjiVfZsLAfAQY
yf2NBcejpIbcP7BS/g+C6KOowYj+Et26T6GdwFXExUcl80JvoX8yHQOfvJpdiBRbjyB8UqfCa= AQgAJhYhBFzmCXlwLyhb8yj5sRYcCHWCI1n3BQJcZVD8AhsMBQkDwmcAAAoJEBYc
knm CHWCI1n3rhcH/iCB0ZV861H0RKJ2F7bXEyCLR2ncBFUCnFo3muSrN9NXTojz2vwv
3c7dNuXmhflz/w3aBj32q9ZyGqA1NpHCpLyVAlvSNQ/pat/rGUCPZ9duw4KhUUqEmatQPVFPk= zexRBpZzaRJoksBkvH+ofuZ1iK7ycZO23dnukvPwGQsz3QiITjVeB6ZR0250MG1q
utT A5yZRlZCsCbGJb4/2e8Ey8BbblHn49Zta4l2Ex5NpNNQ8FYoqXhXu5Bd2F/wX/Bp
ouEZQbMK+i+chOH3AsKCuNDfvCDwirnsSqIJmAgl1lC4de+bsWYCMqN9ei99hOCRUyhZ3g3sr= 5gkZegfE3H9Dw4QjP82Mt0RZSBg9RMGCk6nNfEuze1Up+IOdtqzf3/Z8J5XxLzN2
8RB s8WPmDwJDwvxJRtto8U+ulv4ElcwlA+wYiKAq7cRCKGM/si5ClkUNgb319grUrBU
owVAdcvjZxeIDKALABEBAAHCwHwEGAEIACYCGwwWIQRc5gl5cC8oW/Mo+bEWHAh1giNZ9wUCY= 6h8SuYtgnD84965xRiVAgtH4wCPN544N8CE=3D
C32 =3DNmqc
lAUJB4sMmAAKCRAWHAh1giNZ9+Y2B/9rTKZaKviae+ummXNumXcrKvbkAAvfuLpKUn53FlQLm= -----END PGP PUBLIC KEY BLOCK-----
L6H
jB++lJnPWvVSzdZxdv8FiPP3d632XHKUrkQRQM/9byRDXDommi7Qttx7YCkhd4JLVYqJqpnAQ= --------------FHzf8jBNv06d9SowGc7KOjXr--
xI5
RMkXiZNWyr1lz8JOM1XvDk1M7sJwPMWews8VOIE03E1nt7AsQGnvHtadgEnQaufrYNX3hFA8S= --------------bhHlRtbq4tEzp2NgVmAJ4AbV--
osO
HSnedcys6yrzCSIGCqCD9VHbnMtS4DOv0XJGh2hwc8omzH0KZA517dyKBorJRwadcVauGXDKx= --e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
Etv Content-Description: OpenPGP digital signature
Im4rl94PR/3An1Mj6HeeVVpLqDQ5Jb9J90BahWeQ53FzRa4EQzYCw0nLnxcsT1ZEEP5u Content-Disposition: attachment; filename=OpenPGP_signature
=3Dv/1p Content-Type: application/pgp-signature; name=OpenPGP_signature.asc
-----END PGP PUBLIC KEY BLOCK-----
-----BEGIN PGP SIGNATURE-----
--------------1B34C666A4C2FB03E0324F1A-- Comment: https://gopenpgp.org
Version: GopenPGP 2.7.1
--bBln6dwDJTLkin5LPHkHBQudqRLwIzTUH--
wsB5BAABCAAjFiEEXOYJeXAvKFvzKPmxFhwIdYIjWfcFAmRH6MwFAwAAAAAACgkQ
--x4FrOFG2PnNJvlbzxe80NPwxzh2yUHABp FhwIdYIjWfdX5Qf/W2hFY5PiCrRTvcXGASc2RBLXmCh/mn0tTNUsGtyq/MhcNKfR
Content-Type: application/pgp-signature; name="OpenPGP_signature.asc" 9bSFCb3xz9q26MAJDHtO/Vm0lUjre42rLMkEIDIdJT960HIClELzmgglwFbVgdqy
Content-Description: OpenPGP digital signature T0Psma8ySQpZ2LxZ1oleCXeXaxm4DOwQP+COfb5+FmLTA2z1djLA3HjFPNKglcUr
Content-Disposition: attachment; filename="OpenPGP_signature" atzCTvlt2yqwrx6aeqTxcFezPkl1o+kdqjMCMP0LFFuImuor0vaCFUz76hgNC3kk
CflcYGPJIVH7D06UXkLKC5vnJZ+Pidn4K5sMkF3nXBlourmSU2cZFTNUUdHNAxi8
-----BEGIN PGP SIGNATURE----- s9XTavfQxm4fyLPyGcgNpdM6PhZW9r+lZkr66w==
=se49
wsB5BAABCAAjFiEEXOYJeXAvKFvzKPmxFhwIdYIjWfcFAmBa9YIFAwAAAAAACgkQFhwIdYIjWfem -----END PGP SIGNATURE-----
vQgAjUMAaxL7D6fRtFBqLjdQGr7PkDBigeQD9ax17CJFld7Zfo2dAYUzYJRi0HP0Kn1YCSBppF0w --e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855--
5/P8458H2sqfPC32ptbDCZ/seL0Rpt/gRx6yufbz7wQC0iUZxqxBq2Ox9PGZYSCrTO837lAVYxUo
aMnDL/K9ohAGIyTZVv31z+r3LLWQsFpfpB5hJFqsjQXA9IGKSQIkWbaeE+0wveJSwqxdTwYvsHs2
xjBw+s8tRHO/whP4pvzL185fGsHAb8x9a9oyoDVcszhw5xBpiWW37mI58qkQ6g+4wTarreuXGTp3
RKgPupoYOMJja90yh3TWovcmuZz6QOgne5Rbn3s+Vg==
=hUb8
-----END PGP SIGNATURE-----
--x4FrOFG2PnNJvlbzxe80NPwxzh2yUHABp--

View File

@ -22,6 +22,7 @@ import (
"net" "net"
"github.com/ProtonMail/proton-bridge/v3/internal/constants" "github.com/ProtonMail/proton-bridge/v3/internal/constants"
"golang.org/x/exp/slices"
) )
const ( const (
@ -43,19 +44,19 @@ func IsPortFree(port int) bool {
func isOccupied(port string) bool { func isOccupied(port string) bool {
// Try to create server at port. // Try to create server at port.
dummyserver, err := net.Listen("tcp", port) dummyServer, err := net.Listen("tcp", port)
if err != nil { if err != nil {
return true return true
} }
_ = dummyserver.Close() _ = dummyServer.Close()
return false return false
} }
// FindFreePortFrom finds first empty port, starting with `startPort`. // FindFreePortFrom finds first empty port, starting with `startPort`, and excluding ports listed in exclude.
func FindFreePortFrom(startPort int) int { func FindFreePortFrom(startPort int, exclude ...int) int {
loopedOnce := false loopedOnce := false
freePort := startPort freePort := startPort
for !IsPortFree(freePort) { for slices.Contains(exclude, freePort) || !IsPortFree(freePort) {
freePort++ freePort++
if freePort >= maxPortNumber { if freePort >= maxPortNumber {
freePort = 1 freePort = 1

View File

@ -32,12 +32,12 @@ func TestFreePort(t *testing.T) {
} }
func TestOccupiedPort(t *testing.T) { func TestOccupiedPort(t *testing.T) {
dummyserver, err := net.Listen("tcp", ":"+strconv.Itoa(testPort)) dummyServer, err := net.Listen("tcp", ":"+strconv.Itoa(testPort))
require.NoError(t, err) require.NoError(t, err)
require.True(t, !IsPortFree(testPort), "port should be occupied") require.True(t, !IsPortFree(testPort), "port should be occupied")
_ = dummyserver.Close() _ = dummyServer.Close()
} }
func TestFindFreePortFromDirectly(t *testing.T) { func TestFindFreePortFromDirectly(t *testing.T) {
@ -46,11 +46,21 @@ func TestFindFreePortFromDirectly(t *testing.T) {
} }
func TestFindFreePortFromNextOne(t *testing.T) { func TestFindFreePortFromNextOne(t *testing.T) {
dummyserver, err := net.Listen("tcp", ":"+strconv.Itoa(testPort)) dummyServer, err := net.Listen("tcp", ":"+strconv.Itoa(testPort))
require.NoError(t, err) require.NoError(t, err)
foundPort := FindFreePortFrom(testPort) foundPort := FindFreePortFrom(testPort)
require.Equal(t, testPort+1, foundPort) require.Equal(t, testPort+1, foundPort)
_ = dummyserver.Close() _ = dummyServer.Close()
}
func TestFindFreePortExcluding(t *testing.T) {
dummyServer, err := net.Listen("tcp", ":"+strconv.Itoa(testPort))
require.NoError(t, err)
foundPort := FindFreePortFrom(testPort, testPort+1, testPort+2)
require.Equal(t, testPort+3, foundPort)
_ = dummyServer.Close()
} }

View File

@ -107,7 +107,10 @@ func TestFeatures(testingT *testing.T) {
ctx.Step(`^the header in the "([^"]*)" request to "([^"]*)" has "([^"]*)" set to "([^"]*)"$`, s.theHeaderInTheRequestToHasSetTo) ctx.Step(`^the header in the "([^"]*)" request to "([^"]*)" has "([^"]*)" set to "([^"]*)"$`, s.theHeaderInTheRequestToHasSetTo)
ctx.Step(`^the body in the "([^"]*)" request to "([^"]*)" is:$`, s.theBodyInTheRequestToIs) ctx.Step(`^the body in the "([^"]*)" request to "([^"]*)" is:$`, s.theBodyInTheRequestToIs)
ctx.Step(`^the API requires bridge version at least "([^"]*)"$`, s.theAPIRequiresBridgeVersion) ctx.Step(`^the API requires bridge version at least "([^"]*)"$`, s.theAPIRequiresBridgeVersion)
ctx.Step(`^the network port (\d+) is busy$`, s.networkPortIsBusy)
ctx.Step(`^the network port range (\d+)-(\d+) is busy$`, s.networkPortRangeIsBusy)
ctx.Step(`^bridge IMAP port is (\d+)`, s.bridgeIMAPPortIs)
ctx.Step(`^bridge SMTP port is (\d+)`, s.bridgeSMTPPortIs)
// ==== SETUP ==== // ==== SETUP ====
ctx.Step(`^there exists an account with username "([^"]*)" and password "([^"]*)"$`, s.thereExistsAnAccountWithUsernameAndPassword) ctx.Step(`^there exists an account with username "([^"]*)" and password "([^"]*)"$`, s.thereExistsAnAccountWithUsernameAndPassword)
ctx.Step(`^there exists a disabled account with username "([^"]*)" and password "([^"]*)"$`, s.thereExistsAnAccountWithUsernameAndPasswordWithDisablePrimary) ctx.Step(`^there exists a disabled account with username "([^"]*)" and password "([^"]*)"$`, s.thereExistsAnAccountWithUsernameAndPasswordWithDisablePrimary)
@ -121,7 +124,7 @@ func TestFeatures(testingT *testing.T) {
ctx.Step(`^the address "([^"]*)" of account "([^"]*)" has the following messages in "([^"]*)":$`, s.theAddressOfAccountHasTheFollowingMessagesInMailbox) ctx.Step(`^the address "([^"]*)" of account "([^"]*)" has the following messages in "([^"]*)":$`, s.theAddressOfAccountHasTheFollowingMessagesInMailbox)
ctx.Step(`^the address "([^"]*)" of account "([^"]*)" has (\d+) messages in "([^"]*)"$`, s.theAddressOfAccountHasMessagesInMailbox) ctx.Step(`^the address "([^"]*)" of account "([^"]*)" has (\d+) messages in "([^"]*)"$`, s.theAddressOfAccountHasMessagesInMailbox)
ctx.Step(`^the following fields were changed in draft (\d+) for address "([^"]*)" of account "([^"]*)":$`, s.theFollowingFieldsWereChangedInDraftForAddressOfAccount) ctx.Step(`^the following fields were changed in draft (\d+) for address "([^"]*)" of account "([^"]*)":$`, s.theFollowingFieldsWereChangedInDraftForAddressOfAccount)
ctx.Step(`^draft (\d+) for address "([^"]*)" of account "([^"]*) was moved to trash$`, s.drafAtIndexWasMovedToTrashForAddressOfAccount) ctx.Step(`^draft (\d+) for address "([^"]*)" of account "([^"]*)" was moved to trash$`, s.drafAtIndexWasMovedToTrashForAddressOfAccount)
// === REPORTER === // === REPORTER ===
ctx.Step(`^test skips reporter checks$`, s.skipReporterChecks) ctx.Step(`^test skips reporter checks$`, s.skipReporterChecks)
@ -132,15 +135,22 @@ func TestFeatures(testingT *testing.T) {
ctx.Step(`^bridge stops$`, s.bridgeStops) ctx.Step(`^bridge stops$`, s.bridgeStops)
ctx.Step(`^bridge is version "([^"]*)" and the latest available version is "([^"]*)" reachable from "([^"]*)"$`, s.bridgeVersionIsAndTheLatestAvailableVersionIsReachableFrom) ctx.Step(`^bridge is version "([^"]*)" and the latest available version is "([^"]*)" reachable from "([^"]*)"$`, s.bridgeVersionIsAndTheLatestAvailableVersionIsReachableFrom)
ctx.Step(`^the user has disabled automatic updates$`, s.theUserHasDisabledAutomaticUpdates) ctx.Step(`^the user has disabled automatic updates$`, s.theUserHasDisabledAutomaticUpdates)
ctx.Step(`^the user has disabled automatic start`, s.theUserHasDisabledAutomaticStart)
ctx.Step(`^the user has enabled alternative routing`, s.theUserHasEnabledAlternativeRouting)
ctx.Step(`^the user set IMAP mode to SSL`, s.theUserSetIMAPModeToSSL)
ctx.Step(`^the user set SMTP mode to SSL`, s.theUserSetSMTPModeToSSL)
ctx.Step(`^the user changes the IMAP port to (\d+)$`, s.theUserChangesTheIMAPPortTo) ctx.Step(`^the user changes the IMAP port to (\d+)$`, s.theUserChangesTheIMAPPortTo)
ctx.Step(`^the user changes the SMTP port to (\d+)$`, s.theUserChangesTheSMTPPortTo) ctx.Step(`^the user changes the SMTP port to (\d+)$`, s.theUserChangesTheSMTPPortTo)
ctx.Step(`^the user sets the address mode of user "([^"]*)" to "([^"]*)"$`, s.theUserSetsTheAddressModeOfUserTo) ctx.Step(`^the user sets the address mode of user "([^"]*)" to "([^"]*)"$`, s.theUserSetsTheAddressModeOfUserTo)
ctx.Step(`^the user changes the default keychain application`, s.theUserChangesTheDefaultKeychainApplication)
ctx.Step(`^the user changes the gluon path$`, s.theUserChangesTheGluonPath) ctx.Step(`^the user changes the gluon path$`, s.theUserChangesTheGluonPath)
ctx.Step(`^the user deletes the gluon files$`, s.theUserDeletesTheGluonFiles) ctx.Step(`^the user deletes the gluon files$`, s.theUserDeletesTheGluonFiles)
ctx.Step(`^the user deletes the gluon cache$`, s.theUserDeletesTheGluonCache) ctx.Step(`^the user deletes the gluon cache$`, s.theUserDeletesTheGluonCache)
ctx.Step(`^the user reports a bug$`, s.theUserReportsABug) ctx.Step(`^the user reports a bug$`, s.theUserReportsABug)
ctx.Step(`^the user hides All Mail$`, s.theUserHidesAllMail) ctx.Step(`^the user hides All Mail$`, s.theUserHidesAllMail)
ctx.Step(`^the user shows All Mail$`, s.theUserShowsAllMail) ctx.Step(`^the user shows All Mail$`, s.theUserShowsAllMail)
ctx.Step(`^the user disables telemetry in bridge settings$`, s.theUserDisablesTelemetryInBridgeSettings)
ctx.Step(`^the user enables telemetry in bridge settings$`, s.theUserEnablesTelemetryInBridgeSettings)
ctx.Step(`^bridge sends a connection up event$`, s.bridgeSendsAConnectionUpEvent) ctx.Step(`^bridge sends a connection up event$`, s.bridgeSendsAConnectionUpEvent)
ctx.Step(`^bridge sends a connection down event$`, s.bridgeSendsAConnectionDownEvent) ctx.Step(`^bridge sends a connection down event$`, s.bridgeSendsAConnectionDownEvent)
ctx.Step(`^bridge sends a deauth event for user "([^"]*)"$`, s.bridgeSendsADeauthEventForUser) ctx.Step(`^bridge sends a deauth event for user "([^"]*)"$`, s.bridgeSendsADeauthEventForUser)
@ -153,6 +163,8 @@ func TestFeatures(testingT *testing.T) {
ctx.Step(`^bridge sends an update not available event$`, s.bridgeSendsAnUpdateNotAvailableEvent) ctx.Step(`^bridge sends an update not available event$`, s.bridgeSendsAnUpdateNotAvailableEvent)
ctx.Step(`^bridge sends a forced update event$`, s.bridgeSendsAForcedUpdateEvent) ctx.Step(`^bridge sends a forced update event$`, s.bridgeSendsAForcedUpdateEvent)
ctx.Step(`^bridge reports a message with "([^"]*)"$`, s.bridgeReportsMessage) ctx.Step(`^bridge reports a message with "([^"]*)"$`, s.bridgeReportsMessage)
ctx.Step(`^bridge telemetry feature is enabled$`, s.bridgeTelemetryFeatureEnabled)
ctx.Step(`^bridge telemetry feature is disabled$`, s.bridgeTelemetryFeatureDisabled)
// ==== FRONTEND ==== // ==== FRONTEND ====
ctx.Step(`^frontend sees that bridge is version "([^"]*)"$`, s.frontendSeesThatBridgeIsVersion) ctx.Step(`^frontend sees that bridge is version "([^"]*)"$`, s.frontendSeesThatBridgeIsVersion)
@ -166,6 +178,7 @@ func TestFeatures(testingT *testing.T) {
ctx.Step(`^user "([^"]*)" is listed but not connected$`, s.userIsListedButNotConnected) ctx.Step(`^user "([^"]*)" is listed but not connected$`, s.userIsListedButNotConnected)
ctx.Step(`^user "([^"]*)" is not listed$`, s.userIsNotListed) ctx.Step(`^user "([^"]*)" is not listed$`, s.userIsNotListed)
ctx.Step(`^user "([^"]*)" finishes syncing$`, s.userFinishesSyncing) ctx.Step(`^user "([^"]*)" finishes syncing$`, s.userFinishesSyncing)
ctx.Step(`^user "([^"]*)" has telemetry set to (\d+)$`, s.userHasTelemetrySetTo)
// ==== IMAP ==== // ==== IMAP ====
ctx.Step(`^user "([^"]*)" connects IMAP client "([^"]*)"$`, s.userConnectsIMAPClient) ctx.Step(`^user "([^"]*)" connects IMAP client "([^"]*)"$`, s.userConnectsIMAPClient)
@ -206,6 +219,7 @@ func TestFeatures(testingT *testing.T) {
ctx.Step(`^IMAP client "([^"]*)" appends "([^"]*)" to "([^"]*)"$`, s.imapClientAppendsToMailbox) ctx.Step(`^IMAP client "([^"]*)" appends "([^"]*)" to "([^"]*)"$`, s.imapClientAppendsToMailbox)
ctx.Step(`^IMAP clients "([^"]*)" and "([^"]*)" move message with subject "([^"]*)" of "([^"]*)" to "([^"]*)" by ([^"]*) ([^"]*) ([^"]*)`, s.imapClientsMoveMessageWithSubjectUserFromToByOrderedOperations) ctx.Step(`^IMAP clients "([^"]*)" and "([^"]*)" move message with subject "([^"]*)" of "([^"]*)" to "([^"]*)" by ([^"]*) ([^"]*) ([^"]*)`, s.imapClientsMoveMessageWithSubjectUserFromToByOrderedOperations)
ctx.Step(`^IMAP client "([^"]*)" sees header "([^"]*)" in message with subject "([^"]*)" in "([^"]*)"$`, s.imapClientSeesHeaderInMessageWithSubject) ctx.Step(`^IMAP client "([^"]*)" sees header "([^"]*)" in message with subject "([^"]*)" in "([^"]*)"$`, s.imapClientSeesHeaderInMessageWithSubject)
ctx.Step(`^IMAP client "([^"]*)" does not see header "([^"]*)" in message with subject "([^"]*)" in "([^"]*)"$`, s.imapClientDoesNotSeeHeaderInMessageWithSubject)
// ==== SMTP ==== // ==== SMTP ====
ctx.Step(`^user "([^"]*)" connects SMTP client "([^"]*)"$`, s.userConnectsSMTPClient) ctx.Step(`^user "([^"]*)" connects SMTP client "([^"]*)"$`, s.userConnectsSMTPClient)
@ -222,6 +236,12 @@ func TestFeatures(testingT *testing.T) {
ctx.Step(`^SMTP client "([^"]*)" sends RSET$`, s.smtpClientSendsReset) ctx.Step(`^SMTP client "([^"]*)" sends RSET$`, s.smtpClientSendsReset)
ctx.Step(`^SMTP client "([^"]*)" sends the following message from "([^"]*)" to "([^"]*)":$`, s.smtpClientSendsTheFollowingMessageFromTo) ctx.Step(`^SMTP client "([^"]*)" sends the following message from "([^"]*)" to "([^"]*)":$`, s.smtpClientSendsTheFollowingMessageFromTo)
ctx.Step(`^SMTP client "([^"]*)" logs out$`, s.smtpClientLogsOut) ctx.Step(`^SMTP client "([^"]*)" logs out$`, s.smtpClientLogsOut)
// ==== TELEMETRY ====
ctx.Step(`^bridge eventually sends the following heartbeat:$`, s.bridgeEventuallySendsTheFollowingHeartbeat)
ctx.Step(`^bridge needs to send heartbeat`, s.bridgeNeedsToSendHeartbeat)
ctx.Step(`^bridge do not need to send heartbeat`, s.bridgeDoNotNeedToSendHeartbeat)
ctx.Step(`^heartbeat is not whitelisted`, s.heartbeatIsNotwhitelisted)
}, },
Options: &godog.Options{ Options: &godog.Options{
Format: "pretty", Format: "pretty",

Some files were not shown because too many files have changed in this diff Show More