chore: merge release/Quebec into devel

This commit is contained in:
Jakub 2023-04-26 11:30:24 +02:00
commit 3ebd2f53f4
114 changed files with 5580 additions and 3979 deletions

View File

@ -6,9 +6,12 @@
* Go 1.18
* 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/)
* GCC (linux), msvc (windows) or Xcode (macOS)
* Windres (windows)
* libglvnd and libsecret development files (linux)
* GCC (Linux), msvc (Windows) or Xcode (macOS)
* Windres (Windows)
* 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
`DSN_SENTRY` environment variable with the client key of your sentry project before build.

View File

@ -2,13 +2,31 @@
Changelog [format](http://keepachangelog.com/en/1.0.0/)
## [Bridge 3.2.0] Rialto
### Changed
* 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-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
### Changed
* GODT-2582 Dedup recovered messages folder.
## [Bridge 3.1.1] Quebec
### Fixed

View File

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

20
go.mod
View File

@ -5,10 +5,10 @@ go 1.18
require (
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
github.com/Masterminds/semver/v3 v3.2.0
github.com/ProtonMail/gluon v0.15.1-0.20230425092028-a5ce20d22175
github.com/ProtonMail/gluon v0.16.1-0.20230425073628-8ec759b512f1
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/gopenpgp/v2 v2.5.2
github.com/ProtonMail/go-proton-api v0.4.1-0.20230426081144-f77778bae1be
github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton
github.com/PuerkitoBio/goquery v1.8.1
github.com/abiosoft/ishell v2.0.0+incompatible
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37
@ -43,9 +43,9 @@ require (
github.com/vmihailenco/msgpack/v5 v5.3.5
go.uber.org/goleak v1.2.1
golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb
golang.org/x/net v0.7.0
golang.org/x/sys v0.5.0
golang.org/x/text v0.7.0
golang.org/x/net v0.8.0
golang.org/x/sys v0.6.0
golang.org/x/text v0.8.0
google.golang.org/grpc v1.53.0
google.golang.org/protobuf v1.28.1
howett.net/plist v1.0.0
@ -55,8 +55,8 @@ require (
ariga.io/atlas v0.9.1-0.20230119145809-92243f7c55cb // indirect
entgo.io/ent v0.11.8 // 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-mime v0.0.0-20221031134845-8fd9bc37cf08 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20230322105811-d73448b7e800 // indirect
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect
github.com/ProtonMail/go-srp v0.0.5 // indirect
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // 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/zclconf/go-cty v1.12.1 // 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/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
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/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/gluon v0.15.1-0.20230425092028-a5ce20d22175 h1:gVTIzUcXhIyv10rAy1t+6dvMOLdEhYoF+vwXp29Wtrg=
github.com/ProtonMail/gluon v0.15.1-0.20230425092028-a5ce20d22175/go.mod h1:yA4hk6CJw0BMo+YL8Y3ckCYs5L20sysu9xseshwY3QI=
github.com/ProtonMail/gluon v0.16.1-0.20230425073628-8ec759b512f1 h1:6YTRwn95hnTymOz5g+MbikfJXYE4xGJkIgTLm0t/1w8=
github.com/ProtonMail/gluon v0.16.1-0.20230425073628-8ec759b512f1/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/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-20230124153114-0acdc8ae009b/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g=
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 h1:wPbRQzjjwFc0ih8puEVAOFGELsn1zoIIYdxvML7mDxA=
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/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-20230322105811-d73448b7e800/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
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-mime v0.0.0-20221031134845-8fd9bc37cf08 h1:dS7r5z4iGS0qCjM7UwWdsEMzQesUQbGcXdSm2/tWboA=
github.com/ProtonMail/go-mime v0.0.0-20221031134845-8fd9bc37cf08/go.mod h1:qRZgbeASl2a9OwmsV85aWwRqic0NHPh+9ewGAzb4cgM=
github.com/ProtonMail/go-proton-api v0.4.1-0.20230406143739-c7596e170799 h1:slk4Drrkij1EVTnFOlIDyJsfjt69tnw8w2g1NMb253U=
github.com/ProtonMail/go-proton-api v0.4.1-0.20230406143739-c7596e170799/go.mod h1:kis4GD6FHp1ZWnenSBepldt8ai+vYalDPeey9yGwyXk=
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
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.20230426081144-f77778bae1be h1:TNHnEyUQDf97CRGCFWLxg7I5ASSEMO3TN2lbNw2cD6U=
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/go.mod h1:06iYHtLXW8vjLtccWj++x3MKy65sIT8yZd7nrJF49rs=
github.com/ProtonMail/gopenpgp/v2 v2.5.2 h1:97SjlWNAxXl9P22lgwgrZRshQdiEfAht0g3ZoiA1GCw=
github.com/ProtonMail/gopenpgp/v2 v2.5.2/go.mod h1:52qDaCnto6r+CoWbuU50T77XQt99lIs46HtHtvgFO3o=
github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton h1:YS6M20yvjCJPR1r4ADW5TPn6rahs4iAyZaACei86bEc=
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/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
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-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.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
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-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-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-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
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/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-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.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
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-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.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.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-20190226205417-e64efc72b421/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-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-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
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/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-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.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.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-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.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.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
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.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.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
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-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
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.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.3.1-0.20221202221704-aa9f4b2f3d57 h1:/X0t/E4VxbZE7MLS7auvE7YICHeVvbIa9vkOVvYW/24=
golang.org/x/tools v0.3.1-0.20221202221704-aa9f4b2f3d57/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
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-20191011141410-1b5146add898/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"
"net/http"
"net/http/cookiejar"
"net/url"
"os"
"path/filepath"
"runtime"
@ -35,6 +36,7 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/crash"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"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/logging"
"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.
return withCookieJar(v, func(cookieJar http.CookieJar) error {
// Create a new bridge instance.
@ -258,6 +269,9 @@ func run(c *cli.Context) error {
b.PushError(bridge.ErrVaultCorrupt)
}
// Start telemetry heartbeat process
b.StartHeartbeat(b)
// Run the frontend.
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)
}
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.
defer func() {
logrus.Debug("Persisting cookies")
@ -428,3 +446,21 @@ func withCookieJar(vault *vault.Vault, fn func(http.CookieJar) error) error {
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
import (
"crypto/tls"
"net/http"
"os"
@ -36,6 +37,14 @@ func newAPIOptions(
transport http.RoundTripper,
panicHandler async.PanicHandler,
) []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)
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/safe"
"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/vault"
"github.com/bradenaw/juniper/xslices"
@ -78,6 +79,9 @@ type Bridge struct {
updater Updater
installCh chan installJob
// heartbeat is the telemetry heartbeat for metrics.
heartbeat telemetry.Heartbeat
// curVersion is the current version of the bridge,
// newVersion is the version that was installed by the updater.
curVersion *semver.Version
@ -126,6 +130,9 @@ type Bridge struct {
// goUpdate triggers a check/install of updates.
goUpdate func()
// goHeartbeat triggers a check/sending if heartbeat is needed.
goHeartbeat func()
uidValidityGenerator imap.UIDValidityGenerator
}
@ -237,6 +244,8 @@ func newBridge(
return nil, fmt.Errorf("failed to save last version indicator: %w", err)
}
identifier.SetClientString(vault.GetLastUserAgent())
imapServer, err := newIMAPServer(
gluonCacheDir,
gluonDataDir,

View File

@ -49,6 +49,7 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/ProtonMail/proton-bridge/v3/tests"
"github.com/bradenaw/juniper/xslices"
imapid "github.com/emersion/go-imap-id"
"github.com/emersion/go-imap/client"
"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) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
var (
@ -736,6 +823,9 @@ func withBridgeNoMocks(
require.NoError(t, err)
require.Empty(t, bridge.GetErrors())
// Start the Heartbeat process.
bridge.StartHeartbeat(mocks.Heartbeat)
// Wait for bridge to finish loading users.
waitForEvent(t, eventCh, events.AllUsersLoaded{})
// 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
import "github.com/sirupsen/logrus"
func (bridge *Bridge) GetCurrentUserAgent() string {
return bridge.identifier.GetUserAgent()
}
@ -24,3 +26,17 @@ func (bridge *Bridge) GetCurrentUserAgent() string {
func (bridge *Bridge) SetCurrentPlatform(platform string) {
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"
)
const (
defaultClientName = "UnknownClient"
defaultClientVersion = "0.0.1"
)
func (bridge *Bridge) serveIMAP() error {
port, err := func() (int, error) {
if bridge.imapServer == nil {
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)
if err != nil {
@ -249,11 +247,6 @@ func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) {
}).Info("Received mailbox message count")
}
case imapEvents.SessionAdded:
if !bridge.identifier.HasClient() {
bridge.identifier.SetClient(defaultClientName, defaultClientVersion)
}
case imapEvents.IMAPID:
logrus.WithFields(logrus.Fields{
"sessionID": event.SessionID,
@ -262,7 +255,7 @@ func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) {
}).Info("Received IMAP ID")
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:

View File

@ -24,6 +24,7 @@ type Mocks struct {
CrashHandler *mocks.MockPanicHandler
Reporter *mocks.MockReporter
Heartbeat *mocks.MockHeartbeatManager
}
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),
Reporter: mocks.NewMockReporter(ctl),
Heartbeat: mocks.NewMockHeartbeatManager(ctl),
}
// When getting the TLS issue channel, we want to return the test channel.
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()
// this is called at start of heartbeat process.
mocks.Heartbeat.EXPECT().IsTelemetryAvailable().AnyTimes()
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
}
bridge.heartbeat.SetKeyChainPref(helper)
return vault.SetHelper(vaultDir, helper)
}
@ -62,6 +64,8 @@ func (bridge *Bridge) SetIMAPPort(newPort int) error {
return err
}
bridge.heartbeat.SetIMAPPort(newPort)
return bridge.restartIMAP()
}
@ -78,6 +82,8 @@ func (bridge *Bridge) SetIMAPSSL(newSSL bool) error {
return err
}
bridge.heartbeat.SetIMAPConnectionMode(newSSL)
return bridge.restartIMAP()
}
@ -94,6 +100,8 @@ func (bridge *Bridge) SetSMTPPort(newPort int) error {
return err
}
bridge.heartbeat.SetSMTPPort(newPort)
return bridge.restartSMTP()
}
@ -110,6 +118,8 @@ func (bridge *Bridge) SetSMTPSSL(newSSL bool) error {
return err
}
bridge.heartbeat.SetSMTPConnectionMode(newSSL)
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()
if err != nil {
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.heartbeat.SetDoh(allowed)
return bridge.vault.SetProxyAllowed(allowed)
}
@ -220,6 +234,8 @@ func (bridge *Bridge) SetShowAllMail(show bool) error {
user.SetShowAllMail(show)
}
bridge.heartbeat.SetShowAllMail(show)
return bridge.vault.SetShowAllMail(show)
}, bridge.usersLock)
}
@ -233,6 +249,8 @@ func (bridge *Bridge) SetAutostart(autostart bool) error {
if err := bridge.vault.SetAutostart(autostart); err != nil {
return err
}
bridge.heartbeat.SetAutoStart(autostart)
}
var err error
@ -253,6 +271,10 @@ func (bridge *Bridge) SetAutostart(autostart bool) error {
return err
}
func (bridge *Bridge) GetUpdateRollout() float64 {
return bridge.vault.GetUpdateRollout()
}
func (bridge *Bridge) GetAutoUpdate() bool {
return bridge.vault.GetAutoUpdate()
}
@ -266,11 +288,28 @@ func (bridge *Bridge) SetAutoUpdate(autoUpdate bool) error {
return err
}
bridge.heartbeat.SetAutoUpdate(autoUpdate)
bridge.goUpdate()
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 {
return bridge.vault.GetUpdateChannel()
}
@ -284,6 +323,8 @@ func (bridge *Bridge) SetUpdateChannel(channel updater.Channel) error {
return err
}
bridge.heartbeat.SetBeta(channel)
bridge.goUpdate()
return nil

View File

@ -33,7 +33,10 @@ import (
func (bridge *Bridge) serveSMTP() 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)
if err != nil {

View File

@ -23,6 +23,7 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/emersion/go-smtp"
"github.com/sirupsen/logrus"
)
type smtpBackend struct {
@ -85,7 +86,7 @@ func (s *smtpSession) Rcpt(to string) 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]
if !ok {
return ErrNoSuchUser
@ -93,4 +94,10 @@ func (s *smtpSession) Data(r io.Reader) error {
return user.SendMail(s.authID, s.from, s.to, r)
}, 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
SetClient(name, version string)
SetPlatform(platform string)
SetClientString(client string)
GetClientString() string
}
type ProxyController interface {

View File

@ -295,6 +295,15 @@ func (bridge *Bridge) SetAddressMode(ctx context.Context, userID string, mode va
AddressMode: mode,
})
var splitMode = false
for _, user := range bridge.users {
if user.GetAddressMode() == vault.SplitMode {
splitMode = true
break
}
}
bridge.heartbeat.SetSplitMode(splitMode)
return nil
}, bridge.usersLock)
}
@ -399,7 +408,7 @@ func (bridge *Bridge) loadUsers(ctx context.Context) error {
return nil
}
log.Info("Loading connected user")
log.WithField("mode", user.AddressMode()).Info("Loading connected user")
bridge.publish(events.UserLoading{
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.
client.AddPreRequestHook(func(_ *resty.Client, r *resty.Request) error {
if imapID, ok := imap.GetIMAPIDFromContext(r.Context()); ok {
bridge.identifier.SetClient(imapID.Name, imapID.Version)
bridge.setUserAgent(imapID.Name, imapID.Version)
}
return nil
@ -559,8 +568,12 @@ func (bridge *Bridge) addUserWithVault(
// Finally, save the user in the bridge.
safe.Lock(func() {
bridge.users[apiUser.ID] = user
bridge.heartbeat.SetNbAccount(len(bridge.users))
}, bridge.usersLock)
// As we need at least one user to send heartbeat, try to send it.
defer bridge.goHeartbeat()
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")
}
bridge.heartbeat.SetNbAccount(len(bridge.users))
user.Close()
}

View File

@ -451,6 +451,65 @@ func TestBridge_User_DropConn_NoBadEvent(t *testing.T) {
}, 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) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
// 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.
func userLoginAndSync(
ctx context.Context,

View File

@ -17,69 +17,141 @@
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 {
name, err := writeToTempFile(certPEM)
certDER, err := certPEMToDER(certPEM)
if err != nil {
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 {
name, err := writeToTempFile(certPEM)
certDER, err := certPEMToDER(certPEM)
if err != nil {
return err
}
return removeTrustedCert(name)
}
p := C.CBytes(certDER)
defer C.free(unsafe.Pointer(p))
func addTrustedCert(certPath string) error {
return execabs.Command( //nolint:gosec
"/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 errCode := C.removeTrustedCert((*C.char)(p), (C.ulonglong)(len(certDER))); errCode != 0 {
return fmt.Errorf("could not install certificate from keychain (error %v)", errCode)
}
if _, err := f.Write(data); err != nil {
return "", err
}
if err := f.Close(); err != nil {
return "", err
}
return f.Name(), nil
return 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::setIsBetaEnabledReceived, &settingsTab, &SettingsTab::setIsBetaEnabled);
connect(this, &GRPCQtProxy::setIsAllMailVisibleReceived, &settingsTab, &SettingsTab::setIsAllMailVisible);
connect(this, &GRPCQtProxy::setIsTelemetryDisabledReceived, &settingsTab, &SettingsTab::setIsTelemetryDisabled);
connect(this, &GRPCQtProxy::setColorSchemeNameReceived, &settingsTab, &SettingsTab::setColorSchemeName);
connect(this, &GRPCQtProxy::reportBugReceived, &settingsTab, &SettingsTab::setBugReport);
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.
//****************************************************************************************************************************************************

View File

@ -41,6 +41,7 @@ public: // member functions.
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 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 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.
@ -62,6 +63,7 @@ signals:
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 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 reportBugReceived(QString const &osType, QString const &osVersion, QString const &emailClient, QString const &address,
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.
//****************************************************************************************************************************************************
@ -820,3 +842,4 @@ void GRPCService::finishLogin() {
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 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 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 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;

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.
//****************************************************************************************************************************************************

View File

@ -45,6 +45,7 @@ public: // member functions.
bool isAutostartOn() const; ///< Get the value for the 'Autostart' 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 isTelemetryDisabled() const; ///< Get the value for the 'Disable Telemetry' check box.
QString colorSchemeName() const; ///< Get the value of the 'Use Dark Theme' checkbox.
qint32 eventDelayMs() const; ///< Get the delay for sending automatically generated events.
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 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 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 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.

View File

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

View File

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

View File

@ -24,7 +24,6 @@
#include <bridgepp/Log/LogUtils.h>
#include <bridgepp/GRPC/GRPCClient.h>
#include <bridgepp/Worker/Overseer.h>
#include <bridgepp/BridgeUtils.h>
#define HANDLE_EXCEPTION(x) try { x } \
@ -50,6 +49,9 @@ QMLBackend::QMLBackend()
/// \param[in] serviceConfig
//****************************************************************************************************************************************************
void QMLBackend::init(GRPCConfig const &serviceConfig) {
trayIcon_.reset(new TrayIcon());
this->setNormalTrayIcon();
connect(this, &QMLBackend::fatalError, &app(), &AppController::onFatalError);
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)
//****************************************************************************************************************************************************
@ -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.
//****************************************************************************************************************************************************
@ -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
//****************************************************************************************************************************************************
@ -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] smtpPort The SMTP port.
@ -892,7 +968,7 @@ void QMLBackend::onLoginAlreadyLoggedIn(QString const &userID) {
//****************************************************************************************************************************************************
/// \param[in] userID The userID.
//****************************************************************************************************************************************************
void QMLBackend::onUserBadEvent(QString const &userID, QString const& ) {
void QMLBackend::onUserBadEvent(QString const &userID, QString const &) {
HANDLE_EXCEPTION(
if (badEventDisplayQueue_.contains(userID)) {
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
return;
}
if (user->isInIMAPLoginFailureCooldown())
if (user->isInIMAPLoginFailureCooldown()) {
return;
}
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 imapLoginWhileSignedOut(username);

View File

@ -22,6 +22,7 @@
#include "MacOS/DockIcon.h"
#include "BuildConfig.h"
#include "TrayIcon.h"
#include "UserList.h"
#include <bridgepp/GRPC/GRPCClient.h>
#include <bridgepp/GRPC/GRPCUtils.h>
@ -43,6 +44,7 @@ public: // member functions.
QMLBackend &operator=(QMLBackend &&) = delete; ///< Disabled move assignment operator.
void init(GRPCConfig const &serviceConfig); ///< Initialize the backend.
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.
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 isBetaEnabled READ isBetaEnabled NOTIFY isBetaEnabledChanged)
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(QUrl diskCachePath READ diskCachePath NOTIFY diskCachePathChanged)
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 isBetaEnabled() const; ///< Getter for the 'isBetaEnabled' 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.
QUrl diskCachePath() const; ///< Getter for the 'diskCachePath' 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 isBetaEnabledChanged(bool value); ///<Signal for the change of the 'isBetaEnabled' 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 isDoHEnabledChanged(bool value); ///<Signal for the change of the 'isDoHEnabled' 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 toggleBeta(bool active); ///< Slot for the beta toggle.
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 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).
@ -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 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 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
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 showMainWindow(); ///< Signal for the 'showMainWindow' 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 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.
// 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 useSSLForSMTP_ { false }; ///< The cached value for useSSLForSMTP.
QList<QString> badEventDisplayQueue_; ///< THe queue for displaying 'bad event feedback request dialog'.
std::unique_ptr<TrayIcon> trayIcon_;
friend class AppController;
};

View File

@ -5,11 +5,7 @@
<file>qml/AccountView.qml</file>
<file>qml/Banner.qml</file>
<file>qml/Bridge.qml</file>
<file>qml/Bridge_test.qml</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/Configuration.qml</file>
<file>qml/ConfigurationItem.qml</file>
@ -28,6 +24,7 @@
<file>qml/icons/ic-connected.svg</file>
<file>qml/icons/ic-copy.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-exclamation-circle-filled.svg</file>
<file>qml/icons/ic-external-link.svg</file>
@ -104,17 +101,6 @@
<file>qml/ConnectionModeSettings.qml</file>
<file>qml/SplashScreen.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>
</qresource>
</RCC>

View File

@ -0,0 +1,250 @@
// 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.
//****************************************************************************************************************************************************
/// \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());
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
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::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;
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);
this->setIcon(icon);
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,70 @@
// 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
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.
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.
};
#endif //BRIDGE_GUI_NATIVE_TRAY_ICON_H

View File

@ -27,67 +27,55 @@ Item {
property var notifications
property var user
signal showSignIn()
signal showSignIn
signal showSetupGuide(var user, string address)
property int _leftMargin: 64
property int _rightMargin: 64
property int _contentWidth: 640
property int _topMargin: 32
property int _detailsTopMargin: 25
property int _bottomMargin: 12
property int _detailsMargin: 25
property int _spacing: 20
property int _lineWidth: 1
ScrollView {
id: scrollView
clip: true
property int _lineThickness: 1
property bool _connected: root.user ? root.user.state === EUserState.Connected : false
Rectangle {
anchors.fill: parent
Component.onCompleted: contentItem.boundsBehavior = Flickable.StopAtBounds // Disable the springy effect when scroll reaches top/bottom.
color: root.colorScheme.background_weak
Item {
// can't use parent here because parent is not ScrollView (Flickable inside contentItem inside ScrollView)
width: scrollView.availableWidth
height: scrollView.availableHeight
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
ScrollView {
id: scrollView
anchors.fill: parent
Component.onCompleted: contentItem.boundsBehavior = Flickable.StopAtBounds
ColumnLayout {
id: topLevelColumnLayout
anchors.fill: parent
spacing: 0
anchors.fill: parent
Rectangle {
id: topRectangle
id: topArea
color: root.colorScheme.background_norm
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
clip: true
Layout.fillWidth: true
implicitHeight: childrenRect.height
ColumnLayout {
spacing: root._spacing
id: topLayout
width: _contentWidth
anchors.horizontalCenter: parent.horizontalCenter
spacing: _spacing
anchors.fill: parent
anchors.leftMargin: root._leftMargin
anchors.rightMargin: root._rightMargin
anchors.topMargin: root._topMargin
anchors.bottomMargin: root._bottomMargin
RowLayout { // account delegate with action buttons
RowLayout {
// account delegate with action buttons
Layout.fillWidth: true
Layout.topMargin: _topMargin
AccountDelegate {
Layout.fillWidth: true
colorScheme: root.colorScheme
user: root.user
type: AccountDelegate.LargeView
enabled: root.user ? (root.user.state === EUserState.Connected) : false
enabled: _connected
}
Button {
@ -95,10 +83,11 @@ Item {
colorScheme: root.colorScheme
text: qsTr("Sign out")
secondary: true
visible: root.user ? (root.user.state === EUserState.Connected) : false
visible: _connected
onClicked: {
if (!root.user) return
root.user.logout()
if (!root.user)
return;
root.user.logout();
}
}
@ -109,8 +98,9 @@ Item {
secondary: true
visible: root.user ? (root.user.state === EUserState.SignedOut) : false
onClicked: {
if (!root.user) return
root.showSignIn()
if (!root.user)
return;
root.showSignIn();
}
}
@ -120,8 +110,9 @@ Item {
icon.source: "/qml/icons/ic-trash.svg"
secondary: true
onClicked: {
if (!root.user) return
root.notifications.askDeleteAccount(root.user)
if (!root.user)
return;
root.notifications.askDeleteAccount(root.user);
}
visible: root.user ? root.user.state !== EUserState.Locked : false
}
@ -129,7 +120,7 @@ Item {
Rectangle {
Layout.fillWidth: true
height: root._lineWidth
height: root._lineThickness
color: root.colorScheme.border_weak
}
@ -139,12 +130,12 @@ Item {
actionText: qsTr("Configure")
description: qsTr("Using the mailbox details below (re)configure your client.")
type: SettingsItem.Button
enabled: root.user ? root.user.state === EUserState.Connected : false
visible: root.user ? !root.user.splitMode || root.user.addresses.length==1 : false
visible: _connected && (!root.user.splitMode) || (root.user.addresses.length === 1)
showSeparator: splitMode.visible
onClicked: {
if (!root.user) return
root.showSetupGuide(root.user, user.addresses[0])
if (!root.user)
return;
root.showSetupGuide(root.user, user.addresses[0]);
}
Layout.fillWidth: true
@ -157,15 +148,14 @@ Item {
description: qsTr("Setup multiple email addresses individually.")
type: SettingsItem.Toggle
checked: root.user ? root.user.splitMode : false
visible: root.user ? root.user.addresses.length > 1 : false
enabled: root.user ? (root.user.state === EUserState.Connected) : false
visible: _connected && root.user.addresses.length > 1
showSeparator: addressSelector.visible
onClicked: {
if (!splitMode.checked){
root.notifications.askEnableSplitMode(user)
if (!splitMode.checked) {
root.notifications.askEnableSplitMode(user);
} else {
addressSelector.currentIndex = 0
root.user.toggleSplitMode(!splitMode.checked)
addressSelector.currentIndex = 0;
root.user.toggleSplitMode(!splitMode.checked);
}
}
@ -174,8 +164,8 @@ Item {
RowLayout {
Layout.fillWidth: true
enabled: root.user ? (root.user.state === EUserState.Connected) : false
visible: root.user ? root.user.splitMode : false
Layout.bottomMargin: _spacing
visible: _connected && root.user.splitMode
ComboBox {
id: addressSelector
@ -189,60 +179,68 @@ Item {
text: qsTr("Configure")
secondary: true
onClicked: {
if (!root.user) return
root.showSetupGuide(root.user, addressSelector.displayText)
if (!root.user)
return;
root.showSetupGuide(root.user, addressSelector.displayText);
}
}
}
Rectangle {
height: 0
} // just for some extra space before separator
}
}
Rectangle {
id: bottomArea
Layout.fillWidth: true
implicitHeight: bottomLayout.implicitHeight
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 {
id: configuration
anchors.fill: parent
anchors.leftMargin: root._leftMargin
anchors.rightMargin: root._rightMargin
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
id: bottomLayout
width: _contentWidth
anchors.horizontalCenter: parent.horizontalCenter
spacing: _spacing
visible: _connected
Label {
Layout.topMargin: _detailsMargin
colorScheme: root.colorScheme
text: qsTr("Mailbox details")
type: Label.Body_semibold
}
Configuration {
colorScheme: root.colorScheme
title: qsTr("IMAP")
hostname: Backend.hostname
port: Backend.imapPort.toString()
username: configuration.currentAddress
password: root.user ? root.user.password : ""
security : Backend.useSSLForIMAP ? "SSL" : "STARTTLS"
}
RowLayout {
id: configuration
spacing: _spacing
Layout.fillWidth: true
Layout.fillHeight: true
Configuration {
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"
property string currentAddress: addressSelector.displayText
Configuration {
Layout.fillWidth: true
colorScheme: root.colorScheme
title: qsTr("IMAP")
hostname: Backend.hostname
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 {
id: root
function isInInterval(num, lower_limit, upper_limit) {
return lower_limit <= num && num <= upper_limit
}
function bound(num, lower_limit, upper_limit) {
return Math.max(lower_limit, Math.min(upper_limit, num))
function bound(num, lowerLimit, upperLimit) {
return Math.max(lowerLimit, Math.min(upperLimit, num))
}
property var title: Backend.appname
@ -38,10 +35,30 @@ QtObject {
property Notifications _notifications: Notifications {
id: notifications
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 {
id: mainWindow
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: {
if (!Backend) {
@ -266,7 +99,7 @@ QtObject {
var c = Backend.users.count
var u = Backend.users.get(0)
// DEBUG
if (c != 0) {
if (c !== 0) {
console.log("users non zero", c)
console.log("first user", u )
}
@ -290,7 +123,7 @@ QtObject {
}
function setColorScheme() {
if (Backend.colorSchemeName == "light") ProtonStyle.currentStyle = ProtonStyle.lightStyle
if (Backend.colorSchemeName == "dark") ProtonStyle.currentStyle = ProtonStyle.darkStyle
if (Backend.colorSchemeName === "light") ProtonStyle.currentStyle = ProtonStyle.lightStyle
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
}
Item{}
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("Username") ; value: root.username }

View File

@ -169,6 +169,19 @@ SettingsView {
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 {
id: ports
visible: root._isAdvancedShown

View File

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

View File

@ -24,29 +24,20 @@ import QtQuick.Controls
import Proton
import Notifications
import "tests"
ApplicationWindow {
id: root
width: 960
height: 576
colorScheme: ProtonStyle.currentStyle
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
// 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
Connections {
target: Backend.users
@ -86,10 +77,6 @@ ApplicationWindow {
root.showAndRise()
}
function onSelectUser(userID) {
root.selectUser(userID)
}
function onLoginFinished(index, wasSignedOut) {
var user = Backend.users.get(index)
if (user && !wasSignedOut) {
@ -97,6 +84,21 @@ ApplicationWindow {
}
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 {

View File

@ -24,8 +24,6 @@ QtObject {
id: root
property MainWindow frontendMain
property StatusWindow frontendStatus
property SystemTrayIcon frontendTray
signal askEnableBeta()
signal askEnableSplitMode(var user)
@ -140,7 +138,7 @@ QtObject {
property Notification imapPortChangeError: Notification {
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"
type: Notification.NotificationType.Danger
group: Notifications.Group.Connection
@ -156,7 +154,7 @@ QtObject {
property Notification smtpPortChangeError: Notification {
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"
type: Notification.NotificationType.Danger
group: Notifications.Group.Connection
@ -172,7 +170,7 @@ QtObject {
property Notification imapConnectionModeChangeError: Notification {
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"
type: Notification.NotificationType.Danger
group: Notifications.Group.Connection
@ -196,7 +194,7 @@ QtObject {
property Notification smtpConnectionModeChangeError: Notification {
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"
type: Notification.NotificationType.Danger
group: Notifications.Group.Connection
@ -227,7 +225,7 @@ QtObject {
var link = Backend.releaseNotesLink
return `${descr} <a href="${link}">${text}</a>`
}
brief: qsTr("Update available.")
brief: qsTr("Update available")
icon: "./icons/ic-info-circle-filled.svg"
type: Notification.NotificationType.Info
group: Notifications.Group.Update | Notifications.Group.Dialogs
@ -514,7 +512,7 @@ QtObject {
// login
property Notification loginConnectionError: Notification {
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"
type: Notification.NotificationType.Danger
group: Notifications.Group.Configuration
@ -538,7 +536,7 @@ QtObject {
property Notification onlyPaidUsers: Notification {
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"
type: Notification.NotificationType.Danger
group: Notifications.Group.Configuration
@ -562,7 +560,7 @@ QtObject {
property Notification alreadyLoggedIn: Notification {
description: qsTr("This account is already signed in.")
brief: description
brief: qsTr("Already signed in")
icon: "./icons/ic-exclamation-circle-filled.svg"
type: Notification.NotificationType.Info
group: Notifications.Group.Configuration
@ -587,7 +585,7 @@ QtObject {
// Bug reports
property Notification bugReportSendSuccess: Notification {
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"
type: Notification.NotificationType.Success
group: Notifications.Group.Configuration
@ -611,7 +609,7 @@ QtObject {
property Notification bugReportSendError: Notification {
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"
type: Notification.NotificationType.Danger
group: Notifications.Group.Configuration
@ -634,8 +632,8 @@ QtObject {
// Cache
property Notification cacheUnavailable: Notification {
title: qsTr("Cache location is unavailable")
description: qsTr("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.")
description: 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"
type: Notification.NotificationType.Warning
group: Notifications.Group.Configuration | Notifications.Group.Dialogs
@ -725,7 +723,7 @@ QtObject {
// Other
property Notification accountChanged: Notification {
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"
type: Notification.NotificationType.Danger
group: Notifications.Group.Configuration
@ -742,7 +740,7 @@ QtObject {
property Notification diskFull: Notification {
title: qsTr("Your disk is almost full")
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"
type: Notification.NotificationType.Warning
group: Notifications.Group.Configuration | Notifications.Group.Dialogs
@ -948,8 +946,8 @@ QtObject {
property Notification noKeychain: Notification {
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
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"
type: Notification.NotificationType.Danger
group: Notifications.Group.Dialogs | Notifications.Group.Configuration
@ -982,13 +980,13 @@ QtObject {
property Notification rebuildKeychain: Notification {
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
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"
type: Notification.NotificationType.Danger
group: Notifications.Group.Dialogs | Notifications.Group.Configuration
property var supportLink: "https://proton.me/support/mail"
property var supportLink: "https://proton.me/support/bridge"
Connections {
@ -1014,8 +1012,8 @@ QtObject {
property Notification addressChanged: Notification {
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.")
brief: description
icon: "./icons/ic-exclamation-circle-filled.svg"
type: Notification.NotificationType.Warning
group: Notifications.Group.Configuration
@ -1047,11 +1045,11 @@ QtObject {
property Notification apiCertIssue: Notification {
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. " +
"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>.")
brief: title
icon: "./icons/ic-exclamation-circle-filled.svg"
type: Notification.NotificationType.Danger
group: Notifications.Group.Dialogs | Notifications.Group.Connection
@ -1078,6 +1076,7 @@ QtObject {
property Notification noActiveKeyForRecipient: Notification {
title: qsTr("Unable to send \nencrypted message")
brief: title
description: "#PlaceholderText#"
icon: "./icons/ic-exclamation-circle-filled.svg"
type: Notification.NotificationType.Danger
@ -1174,8 +1173,9 @@ QtObject {
}
property Notification genericError: Notification {
title: "#PlaceholderText#"
description: "#PlaceholderText#"
title: ""
brief: title
description: ""
icon: "./icons/ic-exclamation-circle-filled.svg"
type: Notification.NotificationType.Danger
group: Notifications.Group.Dialogs
@ -1201,7 +1201,7 @@ QtObject {
property Notification genericQuestion: Notification {
title: ""
brief: ""
brief: title
description: ""
type: Notification.NotificationType.Warning
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)
#*****************************************************************************************************************************************************
# 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
#include <bridgepp/User/User.h>
#include "User/User.h"
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.
//****************************************************************************************************************************************************
@ -109,4 +117,5 @@ QString Exception::detailedWhat() const {
return result;
}
} // namespace bridgepp

View File

@ -42,6 +42,7 @@ public: // member functions
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
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.
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 "GRPCUtils.h"
#include "GRPCErrors.h"
#include "../BridgeUtils.h"
#include "../Exception/Exception.h"
#include "../ProcessMonitor.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.
/// \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 isAllMailVisible(bool &outIsVisible); ///< Performs the "isAllMailVisible" 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 setColorSchemeName(QString const &name); ///< Performs the "setColorSchemeName' 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/SetIsAllMailVisible",
"/grpc.Bridge/IsAllMailVisible",
"/grpc.Bridge/SetIsTelemetryDisabled",
"/grpc.Bridge/IsTelemetryDisabled",
"/grpc.Bridge/GoOs",
"/grpc.Bridge/TriggerReset",
"/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_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_GoOs_(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_Version_(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_LicensePath_(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_DependencyLicensesLink_(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_SetColorSchemeName_(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_CurrentEmailClient_(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_ExportTLSCertificates_(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_SetMainExecutable_(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_Login2FA_(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_LoginAbort_(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_InstallUpdate_(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_IsAutomaticUpdateOn_(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_SetDiskCachePath_(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_IsDoHEnabled_(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_SetMailServerSettings_(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_IsPortFree_(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_SetCurrentKeychain_(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_GetUserList_(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_SetUserSplitMode_(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_LogoutUser_(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_ConfigureUserAppleMail_(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_StopEventStream_(Bridge_method_names[54], 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_IsTelemetryDisabled_(Bridge_method_names[13], 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_TriggerReset_(Bridge_method_names[15], 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_LogsPath_(Bridge_method_names[17], 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_ReleaseNotesPageLink_(Bridge_method_names[19], 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_LandingPageLink_(Bridge_method_names[21], 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_ColorSchemeName_(Bridge_method_names[23], 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_ReportBug_(Bridge_method_names[25], 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_ForceLauncher_(Bridge_method_names[27], 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_Login_(Bridge_method_names[29], 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_Login2Passwords_(Bridge_method_names[31], 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_CheckUpdate_(Bridge_method_names[33], 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_SetIsAutomaticUpdateOn_(Bridge_method_names[35], 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_DiskCachePath_(Bridge_method_names[37], 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_SetIsDoHEnabled_(Bridge_method_names[39], 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_MailServerSettings_(Bridge_method_names[41], 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_Hostname_(Bridge_method_names[43], 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_AvailableKeychains_(Bridge_method_names[45], 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_CurrentKeychain_(Bridge_method_names[47], 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_GetUser_(Bridge_method_names[49], 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_SendBadEventUserFeedback_(Bridge_method_names[51], 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_RemoveUser_(Bridge_method_names[53], 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) {
@ -419,6 +423,52 @@ void Bridge::Stub::async::IsAllMailVisible(::grpc::ClientContext* context, const
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) {
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(
Bridge_method_names[12],
::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,
::grpc::ServerContext* ctx,
const ::google::protobuf::Empty* req,
::google::protobuf::StringValue* resp) {
return service->GoOs(ctx, req, resp);
const ::google::protobuf::BoolValue* req,
::google::protobuf::Empty* resp) {
return service->SetIsTelemetryDisabled(ctx, req, resp);
}, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[13],
::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,
::grpc::ServerContext* ctx,
const ::google::protobuf::Empty* req,
::google::protobuf::Empty* resp) {
return service->TriggerReset(ctx, req, resp);
::google::protobuf::BoolValue* resp) {
return service->IsTelemetryDisabled(ctx, req, resp);
}, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[14],
@ -1550,17 +1600,17 @@ Bridge::Service::Service() {
::grpc::ServerContext* ctx,
const ::google::protobuf::Empty* req,
::google::protobuf::StringValue* resp) {
return service->Version(ctx, req, resp);
return service->GoOs(ctx, req, resp);
}, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[15],
::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,
::grpc::ServerContext* ctx,
const ::google::protobuf::Empty* req,
::google::protobuf::StringValue* resp) {
return service->LogsPath(ctx, req, resp);
::google::protobuf::Empty* resp) {
return service->TriggerReset(ctx, req, resp);
}, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[16],
@ -1570,7 +1620,7 @@ Bridge::Service::Service() {
::grpc::ServerContext* ctx,
const ::google::protobuf::Empty* req,
::google::protobuf::StringValue* resp) {
return service->LicensePath(ctx, req, resp);
return service->Version(ctx, req, resp);
}, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[17],
@ -1580,7 +1630,7 @@ Bridge::Service::Service() {
::grpc::ServerContext* ctx,
const ::google::protobuf::Empty* req,
::google::protobuf::StringValue* resp) {
return service->ReleaseNotesPageLink(ctx, req, resp);
return service->LogsPath(ctx, req, resp);
}, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[18],
@ -1590,7 +1640,7 @@ Bridge::Service::Service() {
::grpc::ServerContext* ctx,
const ::google::protobuf::Empty* req,
::google::protobuf::StringValue* resp) {
return service->DependencyLicensesLink(ctx, req, resp);
return service->LicensePath(ctx, req, resp);
}, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[19],
@ -1600,17 +1650,17 @@ Bridge::Service::Service() {
::grpc::ServerContext* ctx,
const ::google::protobuf::Empty* req,
::google::protobuf::StringValue* resp) {
return service->LandingPageLink(ctx, req, resp);
return service->ReleaseNotesPageLink(ctx, req, resp);
}, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[20],
::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,
::grpc::ServerContext* ctx,
const ::google::protobuf::StringValue* req,
::google::protobuf::Empty* resp) {
return service->SetColorSchemeName(ctx, req, resp);
const ::google::protobuf::Empty* req,
::google::protobuf::StringValue* resp) {
return service->DependencyLicensesLink(ctx, req, resp);
}, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[21],
@ -1620,11 +1670,31 @@ Bridge::Service::Service() {
::grpc::ServerContext* ctx,
const ::google::protobuf::Empty* req,
::google::protobuf::StringValue* resp) {
return service->ColorSchemeName(ctx, req, resp);
return service->LandingPageLink(ctx, req, resp);
}, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[22],
::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>(
[](Bridge::Service* service,
::grpc::ServerContext* ctx,
@ -1633,7 +1703,7 @@ Bridge::Service::Service() {
return service->CurrentEmailClient(ctx, req, resp);
}, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[23],
Bridge_method_names[25],
::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::grpc::ReportBugRequest, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service,
@ -1643,7 +1713,7 @@ Bridge::Service::Service() {
return service->ReportBug(ctx, req, resp);
}, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[24],
Bridge_method_names[26],
::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,
@ -1653,7 +1723,7 @@ Bridge::Service::Service() {
return service->ExportTLSCertificates(ctx, req, resp);
}, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[25],
Bridge_method_names[27],
::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,
@ -1663,7 +1733,7 @@ Bridge::Service::Service() {
return service->ForceLauncher(ctx, req, resp);
}, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[26],
Bridge_method_names[28],
::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,
@ -1673,7 +1743,7 @@ Bridge::Service::Service() {
return service->SetMainExecutable(ctx, req, resp);
}, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[27],
Bridge_method_names[29],
::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::grpc::LoginRequest, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service,
@ -1683,7 +1753,7 @@ Bridge::Service::Service() {
return service->Login(ctx, req, resp);
}, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[28],
Bridge_method_names[30],
::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::grpc::LoginRequest, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service,
@ -1693,7 +1763,7 @@ Bridge::Service::Service() {
return service->Login2FA(ctx, req, resp);
}, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[29],
Bridge_method_names[31],
::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::grpc::LoginRequest, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service,
@ -1703,7 +1773,7 @@ Bridge::Service::Service() {
return service->Login2Passwords(ctx, req, resp);
}, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[30],
Bridge_method_names[32],
::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::grpc::LoginAbortRequest, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service,
@ -1713,7 +1783,7 @@ Bridge::Service::Service() {
return service->LoginAbort(ctx, req, resp);
}, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[31],
Bridge_method_names[33],
::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::Empty, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service,
@ -1723,7 +1793,7 @@ Bridge::Service::Service() {
return service->CheckUpdate(ctx, req, resp);
}, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[32],
Bridge_method_names[34],
::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::Empty, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service,
@ -1733,7 +1803,7 @@ Bridge::Service::Service() {
return service->InstallUpdate(ctx, req, resp);
}, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[33],
Bridge_method_names[35],
::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::BoolValue, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service,
@ -1743,7 +1813,7 @@ Bridge::Service::Service() {
return service->SetIsAutomaticUpdateOn(ctx, req, resp);
}, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[34],
Bridge_method_names[36],
::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::Empty, ::google::protobuf::BoolValue, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service,
@ -1753,7 +1823,7 @@ Bridge::Service::Service() {
return service->IsAutomaticUpdateOn(ctx, req, resp);
}, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[35],
Bridge_method_names[37],
::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,
@ -1763,7 +1833,7 @@ Bridge::Service::Service() {
return service->DiskCachePath(ctx, req, resp);
}, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[36],
Bridge_method_names[38],
::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,
@ -1773,7 +1843,7 @@ Bridge::Service::Service() {
return service->SetDiskCachePath(ctx, req, resp);
}, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[37],
Bridge_method_names[39],
::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::BoolValue, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service,
@ -1783,7 +1853,7 @@ Bridge::Service::Service() {
return service->SetIsDoHEnabled(ctx, req, resp);
}, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[38],
Bridge_method_names[40],
::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::Empty, ::google::protobuf::BoolValue, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service,
@ -1793,7 +1863,7 @@ Bridge::Service::Service() {
return service->IsDoHEnabled(ctx, req, resp);
}, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[39],
Bridge_method_names[41],
::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::Empty, ::grpc::ImapSmtpSettings, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service,
@ -1803,7 +1873,7 @@ Bridge::Service::Service() {
return service->MailServerSettings(ctx, req, resp);
}, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[40],
Bridge_method_names[42],
::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::grpc::ImapSmtpSettings, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service,
@ -1813,7 +1883,7 @@ Bridge::Service::Service() {
return service->SetMailServerSettings(ctx, req, resp);
}, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[41],
Bridge_method_names[43],
::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,
@ -1823,7 +1893,7 @@ Bridge::Service::Service() {
return service->Hostname(ctx, req, resp);
}, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[42],
Bridge_method_names[44],
::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::Int32Value, ::google::protobuf::BoolValue, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service,
@ -1833,7 +1903,7 @@ Bridge::Service::Service() {
return service->IsPortFree(ctx, req, resp);
}, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[43],
Bridge_method_names[45],
::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::Empty, ::grpc::AvailableKeychainsResponse, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service,
@ -1843,7 +1913,7 @@ Bridge::Service::Service() {
return service->AvailableKeychains(ctx, req, resp);
}, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[44],
Bridge_method_names[46],
::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,
@ -1853,7 +1923,7 @@ Bridge::Service::Service() {
return service->SetCurrentKeychain(ctx, req, resp);
}, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[45],
Bridge_method_names[47],
::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,
@ -1863,7 +1933,7 @@ Bridge::Service::Service() {
return service->CurrentKeychain(ctx, req, resp);
}, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[46],
Bridge_method_names[48],
::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::Empty, ::grpc::UserListResponse, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service,
@ -1873,7 +1943,7 @@ Bridge::Service::Service() {
return service->GetUserList(ctx, req, resp);
}, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[47],
Bridge_method_names[49],
::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::StringValue, ::grpc::User, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service,
@ -1883,7 +1953,7 @@ Bridge::Service::Service() {
return service->GetUser(ctx, req, resp);
}, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[48],
Bridge_method_names[50],
::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::grpc::UserSplitModeRequest, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service,
@ -1893,7 +1963,7 @@ Bridge::Service::Service() {
return service->SetUserSplitMode(ctx, req, resp);
}, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[49],
Bridge_method_names[51],
::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::grpc::UserBadEventFeedbackRequest, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service,
@ -1903,7 +1973,7 @@ Bridge::Service::Service() {
return service->SendBadEventUserFeedback(ctx, req, resp);
}, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[50],
Bridge_method_names[52],
::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,
@ -1913,7 +1983,7 @@ Bridge::Service::Service() {
return service->LogoutUser(ctx, req, resp);
}, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[51],
Bridge_method_names[53],
::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,
@ -1923,7 +1993,7 @@ Bridge::Service::Service() {
return service->RemoveUser(ctx, req, resp);
}, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[52],
Bridge_method_names[54],
::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::grpc::ConfigureAppleMailRequest, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service,
@ -1933,7 +2003,7 @@ Bridge::Service::Service() {
return service->ConfigureUserAppleMail(ctx, req, resp);
}, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[53],
Bridge_method_names[55],
::grpc::internal::RpcMethod::SERVER_STREAMING,
new ::grpc::internal::ServerStreamingHandler< Bridge::Service, ::grpc::EventStreamRequest, ::grpc::StreamEvent>(
[](Bridge::Service* service,
@ -1943,7 +2013,7 @@ Bridge::Service::Service() {
return service->RunEventStream(ctx, req, writer);
}, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod(
Bridge_method_names[54],
Bridge_method_names[56],
::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Bridge::Service, ::google::protobuf::Empty, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Bridge::Service* service,
@ -2041,6 +2111,20 @@ Bridge::Service::~Service() {
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) {
(void) context;
(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"
"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"
"\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"
"ngValue\022\?\n\013AddLogEntry\022\030.grpc.AddLogEntr"
"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."
"BoolValue\032\026.google.protobuf.Empty\022F\n\020IsA"
"llMailVisible\022\026.google.protobuf.Empty\032\032."
"google.protobuf.BoolValue\022<\n\004GoOs\022\026.goog"
"le.protobuf.Empty\032\034.google.protobuf.Stri"
"ngValue\022>\n\014TriggerReset\022\026.google.protobu"
"f.Empty\032\026.google.protobuf.Empty\022\?\n\007Versi"
"on\022\026.google.protobuf.Empty\032\034.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"
"google.protobuf.BoolValue\022L\n\026SetIsTeleme"
"tryDisabled\022\032.google.protobuf.BoolValue\032"
"\026.google.protobuf.Empty\022I\n\023IsTelemetryDi"
"sabled\022\026.google.protobuf.Empty\032\032.google."
"protobuf.BoolValue\022<\n\004GoOs\022\026.google.prot"
"obuf.Empty\032\034.google.protobuf.StringValue"
"\022H\n\020SetDiskCachePath\022\034.google.protobuf.S"
"tringValue\032\026.google.protobuf.Empty\022E\n\017Se"
"tIsDoHEnabled\022\032.google.protobuf.BoolValu"
"e\032\026.google.protobuf.Empty\022B\n\014IsDoHEnable"
"d\022\026.google.protobuf.Empty\032\032.google.proto"
"buf.BoolValue\022D\n\022MailServerSettings\022\026.go"
"ogle.protobuf.Empty\032\026.grpc.ImapSmtpSetti"
"ngs\022G\n\025SetMailServerSettings\022\026.grpc.Imap"
"SmtpSettings\032\026.google.protobuf.Empty\022@\n\010"
"Hostname\022\026.google.protobuf.Empty\032\034.googl"
"e.protobuf.StringValue\022E\n\nIsPortFree\022\033.g"
"oogle.protobuf.Int32Value\032\032.google.proto"
"buf.BoolValue\022N\n\022AvailableKeychains\022\026.go"
"ogle.protobuf.Empty\032 .grpc.AvailableKeyc"
"hainsResponse\022J\n\022SetCurrentKeychain\022\034.go"
"ogle.protobuf.StringValue\032\026.google.proto"
"buf.Empty\022G\n\017CurrentKeychain\022\026.google.pr"
"otobuf.Empty\032\034.google.protobuf.StringVal"
"ue\022=\n\013GetUserList\022\026.google.protobuf.Empt"
"y\032\026.grpc.UserListResponse\0223\n\007GetUser\022\034.g"
"oogle.protobuf.StringValue\032\n.grpc.User\022F"
"\n\020SetUserSplitMode\022\032.grpc.UserSplitModeR"
"equest\032\026.google.protobuf.Empty\022U\n\030SendBa"
"dEventUserFeedback\022!.grpc.UserBadEventFe"
"edbackRequest\032\026.google.protobuf.Empty\022B\n"
"\nLogoutUser\022\034.google.protobuf.StringValu"
"e\032\026.google.protobuf.Empty\022B\n\nRemoveUser\022"
"\034.google.protobuf.StringValue\032\026.google.p"
"rotobuf.Empty\022Q\n\026ConfigureUserAppleMail\022"
"\037.grpc.ConfigureAppleMailRequest\032\026.googl"
"e.protobuf.Empty\022\?\n\016RunEventStream\022\030.grp"
"c.EventStreamRequest\032\021.grpc.StreamEvent0"
"\001\022A\n\017StopEventStream\022\026.google.protobuf.E"
"mpty\032\026.google.protobuf.EmptyB6Z4github.c"
"om/ProtonMail/proton-bridge/v3/internal/"
"grpcb\006proto3"
"\022>\n\014TriggerReset\022\026.google.protobuf.Empty"
"\032\026.google.protobuf.Empty\022\?\n\007Version\022\026.go"
"ogle.protobuf.Empty\032\034.google.protobuf.St"
"ringValue\022@\n\010LogsPath\022\026.google.protobuf."
"Empty\032\034.google.protobuf.StringValue\022C\n\013L"
"icensePath\022\026.google.protobuf.Empty\032\034.goo"
"gle.protobuf.StringValue\022L\n\024ReleaseNotes"
"PageLink\022\026.google.protobuf.Empty\032\034.googl"
"e.protobuf.StringValue\022N\n\026DependencyLice"
"nsesLink\022\026.google.protobuf.Empty\032\034.googl"
"e.protobuf.StringValue\022G\n\017LandingPageLin"
"k\022\026.google.protobuf.Empty\032\034.google.proto"
"buf.StringValue\022J\n\022SetColorSchemeName\022\034."
"google.protobuf.StringValue\032\026.google.pro"
"tobuf.Empty\022G\n\017ColorSchemeName\022\026.google."
"protobuf.Empty\032\034.google.protobuf.StringV"
"alue\022J\n\022CurrentEmailClient\022\026.google.prot"
"obuf.Empty\032\034.google.protobuf.StringValue"
"\022;\n\tReportBug\022\026.grpc.ReportBugRequest\032\026."
"google.protobuf.Empty\022M\n\025ExportTLSCertif"
"icates\022\034.google.protobuf.StringValue\032\026.g"
"oogle.protobuf.Empty\022E\n\rForceLauncher\022\034."
"google.protobuf.StringValue\032\026.google.pro"
"tobuf.Empty\022I\n\021SetMainExecutable\022\034.googl"
"e.protobuf.StringValue\032\026.google.protobuf"
".Empty\0223\n\005Login\022\022.grpc.LoginRequest\032\026.go"
"ogle.protobuf.Empty\0226\n\010Login2FA\022\022.grpc.L"
"oginRequest\032\026.google.protobuf.Empty\022=\n\017L"
"ogin2Passwords\022\022.grpc.LoginRequest\032\026.goo"
"gle.protobuf.Empty\022=\n\nLoginAbort\022\027.grpc."
"LoginAbortRequest\032\026.google.protobuf.Empt"
"y\022=\n\013CheckUpdate\022\026.google.protobuf.Empty"
"\032\026.google.protobuf.Empty\022\?\n\rInstallUpdat"
"e\022\026.google.protobuf.Empty\032\026.google.proto"
"buf.Empty\022L\n\026SetIsAutomaticUpdateOn\022\032.go"
"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] = {
&::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;
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",
&descriptor_table_bridge_2eproto_once, descriptor_table_bridge_2eproto_deps, 2, 64,
schemas, file_default_instances, TableStruct_bridge_2eproto::offsets,

View File

@ -17,8 +17,8 @@
#include "LogUtils.h"
#include <bridgepp/BridgeUtils.h>
#include <bridgepp/Exception/Exception.h>
#include "../BridgeUtils.h"
#include "../Exception/Exception.h"
namespace bridgepp {

View File

@ -289,6 +289,23 @@ func New(
})
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 func() {

View File

@ -40,7 +40,7 @@ func (f *frontendCLI) printLogDir(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) {
@ -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) {
if gluonDir := f.bridge.GetGluonCacheDir(); 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.
// versions:
// protoc-gen-go v1.28.1
// protoc v3.21.12
// protoc-gen-go v1.28.0
// protoc v3.21.3
// source: bridge.proto
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,
0x5f, 0x43, 0x45, 0x52, 0x54, 0x5f, 0x45, 0x58, 0x50, 0x4f, 0x52, 0x54, 0x5f, 0x45, 0x52, 0x52,
0x4f, 0x52, 0x10, 0x01, 0x12, 0x18, 0x0a, 0x14, 0x54, 0x4c, 0x53, 0x5f, 0x4b, 0x45, 0x59, 0x5f,
0x45, 0x58, 0x50, 0x4f, 0x52, 0x54, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x02, 0x32, 0xf0,
0x1d, 0x0a, 0x06, 0x42, 0x72, 0x69, 0x64, 0x67, 0x65, 0x12, 0x49, 0x0a, 0x0b, 0x43, 0x68, 0x65,
0x45, 0x58, 0x50, 0x4f, 0x52, 0x54, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x02, 0x32, 0x89,
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,
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,
@ -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,
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,
0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x4c, 0x0a, 0x16, 0x53, 0x65, 0x74, 0x49, 0x73, 0x54,
0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x44, 0x69, 0x73, 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, 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,
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, 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,
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, 0x4a, 0x0a, 0x12, 0x53, 0x65, 0x74, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x53,
0x63, 0x68, 0x65, 0x6d, 0x65, 0x4e, 0x61, 0x6d, 0x65, 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,
0x47, 0x0a, 0x0f, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x65, 0x4e, 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, 0x4a, 0x0a, 0x12, 0x43, 0x75, 0x72, 0x72,
0x65, 0x6e, 0x74, 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x16,
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, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1c,
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,
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, 0x63, 0x68, 0x65, 0x6d, 0x65, 0x4e, 0x61,
0x6d, 0x65, 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, 0x47, 0x0a, 0x0f, 0x43, 0x6f, 0x6c, 0x6f,
0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x65, 0x4e, 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, 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,
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, 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, 0x49, 0x0a, 0x11, 0x53, 0x65, 0x74, 0x4d, 0x61,
0x69, 0x6e, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65, 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, 0x33, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x12, 0x2e, 0x67, 0x72,
0x12, 0x49, 0x0a, 0x11, 0x53, 0x65, 0x74, 0x4d, 0x61, 0x69, 0x6e, 0x45, 0x78, 0x65, 0x63, 0x75,
0x74, 0x61, 0x62, 0x6c, 0x65, 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, 0x33, 0x0a, 0x05, 0x4c,
0x6f, 0x67, 0x69, 0x6e, 0x12, 0x12, 0x2e, 0x67, 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, 0x36, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x32, 0x46, 0x41, 0x12, 0x12, 0x2e, 0x67,
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,
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,
0x32, 0x46, 0x41, 0x12, 0x12, 0x2e, 0x67, 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, 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, 0x0a, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x41, 0x62, 0x6f, 0x72, 0x74, 0x12, 0x17, 0x2e, 0x67,
0x72, 0x70, 0x63, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x41, 0x62, 0x6f, 0x72, 0x74, 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,
0x0b, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x16, 0x2e, 0x67,
0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x3d, 0x0a, 0x0a, 0x4c, 0x6f, 0x67, 0x69, 0x6e,
0x41, 0x62, 0x6f, 0x72, 0x74, 0x12, 0x17, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x6f, 0x67,
0x69, 0x6e, 0x41, 0x62, 0x6f, 0x72, 0x74, 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, 0x0b, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x55,
0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e,
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x3f, 0x0a, 0x0d, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c,
0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16,
0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 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,
0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x3f, 0x0a, 0x0d,
0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x16, 0x2e,
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 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, 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,
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, 0x20, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e,
0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x63, 0x68, 0x61, 0x69,
0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4a, 0x0a, 0x12, 0x53, 0x65,
0x74, 0x43, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x4b, 0x65, 0x79, 0x63, 0x68, 0x61, 0x69, 0x6e,
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,
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, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x47, 0x0a, 0x0f, 0x43, 0x75, 0x72, 0x72, 0x65, 0x6e,
0x74, 0x4b, 0x65, 0x79, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
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, 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,
0x79, 0x1a, 0x20, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62,
0x6c, 0x65, 0x4b, 0x65, 0x79, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x12, 0x4a, 0x0a, 0x12, 0x53, 0x65, 0x74, 0x43, 0x75, 0x72, 0x72, 0x65, 0x6e,
0x74, 0x4b, 0x65, 0x79, 0x63, 0x68, 0x61, 0x69, 0x6e, 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,
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,
0x47, 0x0a, 0x0f, 0x43, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x4b, 0x65, 0x79, 0x63, 0x68, 0x61,
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,
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, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70,
0x74, 0x79, 0x12, 0x42, 0x0a, 0x0a, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 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, 0x16,
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x42, 0x0a, 0x0a, 0x4c,
0x6f, 0x67, 0x6f, 0x75, 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, 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, 0x0a, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 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, 0x16, 0x2e, 0x67, 0x6f,
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, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x42, 0x0a, 0x0a, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65,
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, 0x16, 0x2e, 0x67, 0x6f, 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, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x3f, 0x0a,
0x0e, 0x52, 0x75, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12,
0x18, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x65,
0x61, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x67, 0x72, 0x70, 0x63,
0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x30, 0x01, 0x12, 0x41,
0x0a, 0x0f, 0x53, 0x74, 0x6f, 0x70, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61,
0x6d, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74,
0x79, 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,
0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x3f, 0x0a, 0x0e, 0x52, 0x75, 0x6e, 0x45, 0x76, 0x65,
0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x18, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e,
0x45, 0x76, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x1a, 0x11, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d,
0x45, 0x76, 0x65, 0x6e, 0x74, 0x30, 0x01, 0x12, 0x41, 0x0a, 0x0f, 0x53, 0x74, 0x6f, 0x70, 0x45,
0x76, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f,
0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70,
0x74, 0x79, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 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 (
@ -5214,106 +5223,110 @@ var file_bridge_proto_depIdxs = []int32{
72, // 69: grpc.Bridge.IsBetaEnabled:input_type -> google.protobuf.Empty
73, // 70: grpc.Bridge.SetIsAllMailVisible:input_type -> google.protobuf.BoolValue
72, // 71: grpc.Bridge.IsAllMailVisible:input_type -> google.protobuf.Empty
72, // 72: grpc.Bridge.GoOs:input_type -> google.protobuf.Empty
72, // 73: grpc.Bridge.TriggerReset:input_type -> google.protobuf.Empty
72, // 74: grpc.Bridge.Version:input_type -> google.protobuf.Empty
72, // 75: grpc.Bridge.LogsPath:input_type -> google.protobuf.Empty
72, // 76: grpc.Bridge.LicensePath:input_type -> google.protobuf.Empty
72, // 77: grpc.Bridge.ReleaseNotesPageLink:input_type -> google.protobuf.Empty
72, // 78: grpc.Bridge.DependencyLicensesLink:input_type -> google.protobuf.Empty
72, // 79: grpc.Bridge.LandingPageLink:input_type -> google.protobuf.Empty
71, // 80: grpc.Bridge.SetColorSchemeName:input_type -> google.protobuf.StringValue
72, // 81: grpc.Bridge.ColorSchemeName:input_type -> google.protobuf.Empty
72, // 82: grpc.Bridge.CurrentEmailClient:input_type -> google.protobuf.Empty
9, // 83: grpc.Bridge.ReportBug:input_type -> grpc.ReportBugRequest
71, // 84: grpc.Bridge.ExportTLSCertificates:input_type -> google.protobuf.StringValue
71, // 85: grpc.Bridge.ForceLauncher:input_type -> google.protobuf.StringValue
71, // 86: grpc.Bridge.SetMainExecutable:input_type -> google.protobuf.StringValue
10, // 87: grpc.Bridge.Login:input_type -> grpc.LoginRequest
10, // 88: grpc.Bridge.Login2FA:input_type -> grpc.LoginRequest
10, // 89: grpc.Bridge.Login2Passwords:input_type -> grpc.LoginRequest
11, // 90: grpc.Bridge.LoginAbort:input_type -> grpc.LoginAbortRequest
72, // 91: grpc.Bridge.CheckUpdate:input_type -> google.protobuf.Empty
72, // 92: grpc.Bridge.InstallUpdate:input_type -> google.protobuf.Empty
73, // 93: grpc.Bridge.SetIsAutomaticUpdateOn:input_type -> google.protobuf.BoolValue
72, // 94: grpc.Bridge.IsAutomaticUpdateOn:input_type -> google.protobuf.Empty
72, // 95: grpc.Bridge.DiskCachePath:input_type -> google.protobuf.Empty
71, // 96: grpc.Bridge.SetDiskCachePath:input_type -> google.protobuf.StringValue
73, // 97: grpc.Bridge.SetIsDoHEnabled:input_type -> google.protobuf.BoolValue
72, // 98: grpc.Bridge.IsDoHEnabled:input_type -> google.protobuf.Empty
72, // 99: grpc.Bridge.MailServerSettings:input_type -> google.protobuf.Empty
12, // 100: grpc.Bridge.SetMailServerSettings:input_type -> grpc.ImapSmtpSettings
72, // 101: grpc.Bridge.Hostname:input_type -> google.protobuf.Empty
74, // 102: grpc.Bridge.IsPortFree:input_type -> google.protobuf.Int32Value
72, // 103: grpc.Bridge.AvailableKeychains:input_type -> google.protobuf.Empty
71, // 104: grpc.Bridge.SetCurrentKeychain:input_type -> google.protobuf.StringValue
72, // 105: grpc.Bridge.CurrentKeychain:input_type -> google.protobuf.Empty
72, // 106: grpc.Bridge.GetUserList:input_type -> google.protobuf.Empty
71, // 107: grpc.Bridge.GetUser:input_type -> google.protobuf.StringValue
15, // 108: grpc.Bridge.SetUserSplitMode:input_type -> grpc.UserSplitModeRequest
16, // 109: grpc.Bridge.SendBadEventUserFeedback:input_type -> grpc.UserBadEventFeedbackRequest
71, // 110: grpc.Bridge.LogoutUser:input_type -> google.protobuf.StringValue
71, // 111: grpc.Bridge.RemoveUser:input_type -> google.protobuf.StringValue
18, // 112: grpc.Bridge.ConfigureUserAppleMail:input_type -> grpc.ConfigureAppleMailRequest
19, // 113: grpc.Bridge.RunEventStream:input_type -> grpc.EventStreamRequest
72, // 114: grpc.Bridge.StopEventStream:input_type -> google.protobuf.Empty
71, // 115: grpc.Bridge.CheckTokens:output_type -> google.protobuf.StringValue
72, // 116: grpc.Bridge.AddLogEntry:output_type -> google.protobuf.Empty
8, // 117: grpc.Bridge.GuiReady:output_type -> grpc.GuiReadyResponse
72, // 118: grpc.Bridge.Quit:output_type -> google.protobuf.Empty
72, // 119: grpc.Bridge.Restart:output_type -> google.protobuf.Empty
73, // 120: grpc.Bridge.ShowOnStartup:output_type -> google.protobuf.BoolValue
72, // 121: grpc.Bridge.SetIsAutostartOn:output_type -> google.protobuf.Empty
73, // 122: grpc.Bridge.IsAutostartOn:output_type -> google.protobuf.BoolValue
72, // 123: grpc.Bridge.SetIsBetaEnabled:output_type -> google.protobuf.Empty
73, // 124: grpc.Bridge.IsBetaEnabled:output_type -> google.protobuf.BoolValue
72, // 125: grpc.Bridge.SetIsAllMailVisible:output_type -> google.protobuf.Empty
73, // 126: grpc.Bridge.IsAllMailVisible:output_type -> google.protobuf.BoolValue
71, // 127: grpc.Bridge.GoOs:output_type -> google.protobuf.StringValue
72, // 128: grpc.Bridge.TriggerReset:output_type -> google.protobuf.Empty
71, // 129: grpc.Bridge.Version:output_type -> google.protobuf.StringValue
71, // 130: grpc.Bridge.LogsPath:output_type -> google.protobuf.StringValue
71, // 131: grpc.Bridge.LicensePath:output_type -> google.protobuf.StringValue
71, // 132: grpc.Bridge.ReleaseNotesPageLink:output_type -> google.protobuf.StringValue
71, // 133: grpc.Bridge.DependencyLicensesLink:output_type -> google.protobuf.StringValue
71, // 134: grpc.Bridge.LandingPageLink:output_type -> google.protobuf.StringValue
72, // 135: grpc.Bridge.SetColorSchemeName:output_type -> google.protobuf.Empty
71, // 136: grpc.Bridge.ColorSchemeName:output_type -> google.protobuf.StringValue
71, // 137: grpc.Bridge.CurrentEmailClient:output_type -> google.protobuf.StringValue
72, // 138: grpc.Bridge.ReportBug:output_type -> google.protobuf.Empty
72, // 139: grpc.Bridge.ExportTLSCertificates:output_type -> google.protobuf.Empty
72, // 140: grpc.Bridge.ForceLauncher:output_type -> google.protobuf.Empty
72, // 141: grpc.Bridge.SetMainExecutable:output_type -> google.protobuf.Empty
72, // 142: grpc.Bridge.Login:output_type -> google.protobuf.Empty
72, // 143: grpc.Bridge.Login2FA:output_type -> google.protobuf.Empty
72, // 144: grpc.Bridge.Login2Passwords:output_type -> google.protobuf.Empty
72, // 145: grpc.Bridge.LoginAbort:output_type -> google.protobuf.Empty
72, // 146: grpc.Bridge.CheckUpdate:output_type -> google.protobuf.Empty
72, // 147: grpc.Bridge.InstallUpdate:output_type -> google.protobuf.Empty
72, // 148: grpc.Bridge.SetIsAutomaticUpdateOn:output_type -> google.protobuf.Empty
73, // 149: grpc.Bridge.IsAutomaticUpdateOn:output_type -> google.protobuf.BoolValue
71, // 150: grpc.Bridge.DiskCachePath:output_type -> google.protobuf.StringValue
72, // 151: grpc.Bridge.SetDiskCachePath:output_type -> google.protobuf.Empty
72, // 152: grpc.Bridge.SetIsDoHEnabled:output_type -> google.protobuf.Empty
73, // 153: grpc.Bridge.IsDoHEnabled:output_type -> google.protobuf.BoolValue
12, // 154: grpc.Bridge.MailServerSettings:output_type -> grpc.ImapSmtpSettings
72, // 155: grpc.Bridge.SetMailServerSettings:output_type -> google.protobuf.Empty
71, // 156: grpc.Bridge.Hostname:output_type -> google.protobuf.StringValue
73, // 157: grpc.Bridge.IsPortFree:output_type -> google.protobuf.BoolValue
13, // 158: grpc.Bridge.AvailableKeychains:output_type -> grpc.AvailableKeychainsResponse
72, // 159: grpc.Bridge.SetCurrentKeychain:output_type -> google.protobuf.Empty
71, // 160: grpc.Bridge.CurrentKeychain:output_type -> google.protobuf.StringValue
17, // 161: grpc.Bridge.GetUserList:output_type -> grpc.UserListResponse
14, // 162: grpc.Bridge.GetUser:output_type -> grpc.User
72, // 163: grpc.Bridge.SetUserSplitMode:output_type -> google.protobuf.Empty
72, // 164: grpc.Bridge.SendBadEventUserFeedback:output_type -> google.protobuf.Empty
72, // 165: grpc.Bridge.LogoutUser:output_type -> google.protobuf.Empty
72, // 166: grpc.Bridge.RemoveUser:output_type -> google.protobuf.Empty
72, // 167: grpc.Bridge.ConfigureUserAppleMail:output_type -> google.protobuf.Empty
20, // 168: grpc.Bridge.RunEventStream:output_type -> grpc.StreamEvent
72, // 169: grpc.Bridge.StopEventStream:output_type -> google.protobuf.Empty
115, // [115:170] is the sub-list for method output_type
60, // [60:115] is the sub-list for method input_type
73, // 72: grpc.Bridge.SetIsTelemetryDisabled:input_type -> google.protobuf.BoolValue
72, // 73: grpc.Bridge.IsTelemetryDisabled:input_type -> google.protobuf.Empty
72, // 74: grpc.Bridge.GoOs:input_type -> google.protobuf.Empty
72, // 75: grpc.Bridge.TriggerReset:input_type -> google.protobuf.Empty
72, // 76: grpc.Bridge.Version:input_type -> google.protobuf.Empty
72, // 77: grpc.Bridge.LogsPath:input_type -> google.protobuf.Empty
72, // 78: grpc.Bridge.LicensePath:input_type -> google.protobuf.Empty
72, // 79: grpc.Bridge.ReleaseNotesPageLink:input_type -> google.protobuf.Empty
72, // 80: grpc.Bridge.DependencyLicensesLink:input_type -> google.protobuf.Empty
72, // 81: grpc.Bridge.LandingPageLink:input_type -> google.protobuf.Empty
71, // 82: grpc.Bridge.SetColorSchemeName:input_type -> google.protobuf.StringValue
72, // 83: grpc.Bridge.ColorSchemeName:input_type -> google.protobuf.Empty
72, // 84: grpc.Bridge.CurrentEmailClient:input_type -> google.protobuf.Empty
9, // 85: grpc.Bridge.ReportBug:input_type -> grpc.ReportBugRequest
71, // 86: grpc.Bridge.ExportTLSCertificates:input_type -> google.protobuf.StringValue
71, // 87: grpc.Bridge.ForceLauncher:input_type -> google.protobuf.StringValue
71, // 88: grpc.Bridge.SetMainExecutable:input_type -> google.protobuf.StringValue
10, // 89: grpc.Bridge.Login:input_type -> grpc.LoginRequest
10, // 90: grpc.Bridge.Login2FA:input_type -> grpc.LoginRequest
10, // 91: grpc.Bridge.Login2Passwords:input_type -> grpc.LoginRequest
11, // 92: grpc.Bridge.LoginAbort:input_type -> grpc.LoginAbortRequest
72, // 93: grpc.Bridge.CheckUpdate:input_type -> google.protobuf.Empty
72, // 94: grpc.Bridge.InstallUpdate:input_type -> google.protobuf.Empty
73, // 95: grpc.Bridge.SetIsAutomaticUpdateOn:input_type -> google.protobuf.BoolValue
72, // 96: grpc.Bridge.IsAutomaticUpdateOn:input_type -> google.protobuf.Empty
72, // 97: grpc.Bridge.DiskCachePath:input_type -> google.protobuf.Empty
71, // 98: grpc.Bridge.SetDiskCachePath:input_type -> google.protobuf.StringValue
73, // 99: grpc.Bridge.SetIsDoHEnabled:input_type -> google.protobuf.BoolValue
72, // 100: grpc.Bridge.IsDoHEnabled:input_type -> google.protobuf.Empty
72, // 101: grpc.Bridge.MailServerSettings:input_type -> google.protobuf.Empty
12, // 102: grpc.Bridge.SetMailServerSettings:input_type -> grpc.ImapSmtpSettings
72, // 103: grpc.Bridge.Hostname:input_type -> google.protobuf.Empty
74, // 104: grpc.Bridge.IsPortFree:input_type -> google.protobuf.Int32Value
72, // 105: grpc.Bridge.AvailableKeychains:input_type -> google.protobuf.Empty
71, // 106: grpc.Bridge.SetCurrentKeychain:input_type -> google.protobuf.StringValue
72, // 107: grpc.Bridge.CurrentKeychain:input_type -> google.protobuf.Empty
72, // 108: grpc.Bridge.GetUserList:input_type -> google.protobuf.Empty
71, // 109: grpc.Bridge.GetUser:input_type -> google.protobuf.StringValue
15, // 110: grpc.Bridge.SetUserSplitMode:input_type -> grpc.UserSplitModeRequest
16, // 111: grpc.Bridge.SendBadEventUserFeedback:input_type -> grpc.UserBadEventFeedbackRequest
71, // 112: grpc.Bridge.LogoutUser:input_type -> google.protobuf.StringValue
71, // 113: grpc.Bridge.RemoveUser:input_type -> google.protobuf.StringValue
18, // 114: grpc.Bridge.ConfigureUserAppleMail:input_type -> grpc.ConfigureAppleMailRequest
19, // 115: grpc.Bridge.RunEventStream:input_type -> grpc.EventStreamRequest
72, // 116: grpc.Bridge.StopEventStream:input_type -> google.protobuf.Empty
71, // 117: grpc.Bridge.CheckTokens:output_type -> google.protobuf.StringValue
72, // 118: grpc.Bridge.AddLogEntry:output_type -> google.protobuf.Empty
8, // 119: grpc.Bridge.GuiReady:output_type -> grpc.GuiReadyResponse
72, // 120: grpc.Bridge.Quit:output_type -> google.protobuf.Empty
72, // 121: grpc.Bridge.Restart:output_type -> google.protobuf.Empty
73, // 122: grpc.Bridge.ShowOnStartup:output_type -> google.protobuf.BoolValue
72, // 123: grpc.Bridge.SetIsAutostartOn:output_type -> google.protobuf.Empty
73, // 124: grpc.Bridge.IsAutostartOn:output_type -> google.protobuf.BoolValue
72, // 125: grpc.Bridge.SetIsBetaEnabled:output_type -> google.protobuf.Empty
73, // 126: grpc.Bridge.IsBetaEnabled:output_type -> google.protobuf.BoolValue
72, // 127: grpc.Bridge.SetIsAllMailVisible:output_type -> google.protobuf.Empty
73, // 128: grpc.Bridge.IsAllMailVisible:output_type -> google.protobuf.BoolValue
72, // 129: grpc.Bridge.SetIsTelemetryDisabled:output_type -> google.protobuf.Empty
73, // 130: grpc.Bridge.IsTelemetryDisabled:output_type -> google.protobuf.BoolValue
71, // 131: grpc.Bridge.GoOs:output_type -> google.protobuf.StringValue
72, // 132: grpc.Bridge.TriggerReset:output_type -> google.protobuf.Empty
71, // 133: grpc.Bridge.Version:output_type -> google.protobuf.StringValue
71, // 134: grpc.Bridge.LogsPath:output_type -> google.protobuf.StringValue
71, // 135: grpc.Bridge.LicensePath:output_type -> google.protobuf.StringValue
71, // 136: grpc.Bridge.ReleaseNotesPageLink:output_type -> google.protobuf.StringValue
71, // 137: grpc.Bridge.DependencyLicensesLink:output_type -> google.protobuf.StringValue
71, // 138: grpc.Bridge.LandingPageLink:output_type -> google.protobuf.StringValue
72, // 139: grpc.Bridge.SetColorSchemeName:output_type -> google.protobuf.Empty
71, // 140: grpc.Bridge.ColorSchemeName:output_type -> google.protobuf.StringValue
71, // 141: grpc.Bridge.CurrentEmailClient:output_type -> google.protobuf.StringValue
72, // 142: grpc.Bridge.ReportBug:output_type -> google.protobuf.Empty
72, // 143: grpc.Bridge.ExportTLSCertificates:output_type -> google.protobuf.Empty
72, // 144: grpc.Bridge.ForceLauncher:output_type -> google.protobuf.Empty
72, // 145: grpc.Bridge.SetMainExecutable:output_type -> google.protobuf.Empty
72, // 146: grpc.Bridge.Login:output_type -> google.protobuf.Empty
72, // 147: grpc.Bridge.Login2FA:output_type -> google.protobuf.Empty
72, // 148: grpc.Bridge.Login2Passwords:output_type -> google.protobuf.Empty
72, // 149: grpc.Bridge.LoginAbort:output_type -> google.protobuf.Empty
72, // 150: grpc.Bridge.CheckUpdate:output_type -> google.protobuf.Empty
72, // 151: grpc.Bridge.InstallUpdate:output_type -> google.protobuf.Empty
72, // 152: grpc.Bridge.SetIsAutomaticUpdateOn:output_type -> google.protobuf.Empty
73, // 153: grpc.Bridge.IsAutomaticUpdateOn:output_type -> google.protobuf.BoolValue
71, // 154: grpc.Bridge.DiskCachePath:output_type -> google.protobuf.StringValue
72, // 155: grpc.Bridge.SetDiskCachePath:output_type -> google.protobuf.Empty
72, // 156: grpc.Bridge.SetIsDoHEnabled:output_type -> google.protobuf.Empty
73, // 157: grpc.Bridge.IsDoHEnabled:output_type -> google.protobuf.BoolValue
12, // 158: grpc.Bridge.MailServerSettings:output_type -> grpc.ImapSmtpSettings
72, // 159: grpc.Bridge.SetMailServerSettings:output_type -> google.protobuf.Empty
71, // 160: grpc.Bridge.Hostname:output_type -> google.protobuf.StringValue
73, // 161: grpc.Bridge.IsPortFree:output_type -> google.protobuf.BoolValue
13, // 162: grpc.Bridge.AvailableKeychains:output_type -> grpc.AvailableKeychainsResponse
72, // 163: grpc.Bridge.SetCurrentKeychain:output_type -> google.protobuf.Empty
71, // 164: grpc.Bridge.CurrentKeychain:output_type -> google.protobuf.StringValue
17, // 165: grpc.Bridge.GetUserList:output_type -> grpc.UserListResponse
14, // 166: grpc.Bridge.GetUser:output_type -> grpc.User
72, // 167: grpc.Bridge.SetUserSplitMode:output_type -> google.protobuf.Empty
72, // 168: grpc.Bridge.SendBadEventUserFeedback:output_type -> google.protobuf.Empty
72, // 169: grpc.Bridge.LogoutUser:output_type -> google.protobuf.Empty
72, // 170: grpc.Bridge.RemoveUser:output_type -> google.protobuf.Empty
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 extendee
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 SetIsAllMailVisible(google.protobuf.BoolValue) returns (google.protobuf.Empty);
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 TriggerReset(google.protobuf.Empty) returns (google.protobuf.Empty);
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
}
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) {
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
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQml.Models
//go:build !windows
// +build !windows
ListModel {
// overriding get method to ignore any role and return directly object itself
function get(row) {
if (row < 0 || row >= count) {
return undefined
}
return data(index(row, 0), Qt.DisplayRole)
}
package sentry
import "os"
func GetSystemLang() string {
lang := os.Getenv("LC_ALL")
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
import (
"crypto/sha256"
"errors"
"fmt"
"log"
@ -29,6 +28,7 @@ import (
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/gluon/reporter"
"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/getsentry/sentry-go"
"github.com/sirupsen/logrus"
@ -50,7 +50,7 @@ func init() { //nolint:gochecknoinits
Release: constants.AppVersion(appVersion),
BeforeSend: EnhanceSentryEvent,
Transport: sentrySyncTransport,
ServerName: getProtectedHostname(),
ServerName: GetProtectedHostname(),
Environment: constants.BuildEnv,
MaxBreadcrumbs: 50,
}
@ -61,7 +61,7 @@ func init() { //nolint:gochecknoinits
sentry.ConfigureScope(func(scope *sentry.Scope) {
scope.SetFingerprint([]string{"{{ default }}"})
scope.SetUser(sentry.User{ID: getProtectedHostname()})
scope.SetUser(sentry.User{ID: GetProtectedHostname()})
})
sentry.Logger = log.New(
@ -81,12 +81,17 @@ type Identifier interface {
GetUserAgent() string
}
func getProtectedHostname() string {
func GetProtectedHostname() string {
hostname, err := os.Hostname()
if err != nil {
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.

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

@ -400,32 +400,8 @@ func (conn *imapConnector) RemoveMessagesFromMailbox(ctx context.Context, messag
}
if mailboxID == proton.TrashLabel || mailboxID == proton.DraftsLabel {
var metadata []proton.MessageMetadata
// 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
if err := conn.client.DeleteMessage(ctx, xslices.Map(messageIDs, func(m imap.MessageID) string {
return string(m)
})...); err != nil {
return err
}
@ -626,7 +602,7 @@ func (conn *imapConnector) createDraft(ctx context.Context, literal []byte, addr
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 {
return proton.Message{}, fmt.Errorf("failed to parse message: %w", err)
}

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).
message, err := message.ParseWithParser(parser)
message, err := message.ParseWithParser(parser, false)
if err != nil {
return fmt.Errorf("failed to parse message: %w", err)
}

View File

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

View File

@ -119,6 +119,12 @@ func New(
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.
user := &User{
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.
// It is assumed that user.apiAddrs and user.updateCh are already locked.
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
server := server.New()
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"))
}
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 {
ua.lock.RLock()
defer ua.lock.RUnlock()

View File

@ -20,6 +20,7 @@ package vault
import (
"math"
"math/rand"
"time"
"github.com/Masterminds/semver/v3"
"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.
func (vault *Vault) GetLastVersion() *semver.Version {
return semver.MustParse(vault.get().Settings.LastVersion)
@ -225,3 +238,34 @@ func (vault *Vault) SetMaxSyncMemory(maxMemory uint64) error {
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())
}
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) {
// create a new test vault.
s := newVault(t)
@ -216,3 +230,11 @@ func TestVault_Settings_MaxSyncMemory(t *testing.T) {
// Check the default first start value.
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 (
"math/rand"
"runtime"
"time"
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
"github.com/ProtonMail/proton-bridge/v3/pkg/ports"
)
type Settings struct {
@ -35,23 +37,29 @@ type Settings struct {
UpdateChannel updater.Channel
UpdateRollout float64
ColorScheme string
ProxyAllowed bool
ShowAllMail bool
Autostart bool
AutoUpdate bool
ColorScheme string
ProxyAllowed bool
ShowAllMail bool
Autostart bool
AutoUpdate bool
TelemetryDisabled bool
LastVersion string
FirstStart bool
MaxSyncMemory uint64
LastUserAgent string
LastHeartbeatSent time.Time
// **WARNING**: These entry can't be removed until they vault has proper migration support.
SyncWorkers int
SyncAttPool int
}
const DefaultMaxSyncMemory = 2 * 1024 * uint64(1024*1024)
const DefaultUserAgent = "UnknownClient/0.0.1"
func GetDefaultSyncWorkerCount() int {
const minSyncWorkers = 16
@ -67,23 +75,26 @@ func GetDefaultSyncWorkerCount() int {
func newDefaultSettings(gluonDir string) Settings {
syncWorkers := GetDefaultSyncWorkerCount()
imapPort := ports.FindFreePortFrom(1143)
smtpPort := ports.FindFreePortFrom(1025, imapPort)
return Settings{
GluonDir: gluonDir,
IMAPPort: 1143,
SMTPPort: 1025,
IMAPPort: imapPort,
SMTPPort: smtpPort,
IMAPSSL: false,
SMTPSSL: false,
UpdateChannel: updater.DefaultUpdateChannel,
UpdateRollout: rand.Float64(), //nolint:gosec
ColorScheme: "",
ProxyAllowed: false,
ShowAllMail: true,
Autostart: true,
AutoUpdate: true,
ColorScheme: "",
ProxyAllowed: false,
ShowAllMail: true,
Autostart: true,
AutoUpdate: true,
TelemetryDisabled: false,
LastVersion: "0.0.0",
FirstStart: true,
@ -91,5 +102,8 @@ func newDefaultSettings(gluonDir string) Settings {
MaxSyncMemory: DefaultMaxSyncMemory,
SyncWorkers: syncWorkers,
SyncAttPool: syncWorkers,
LastUserAgent: DefaultUserAgent,
LastHeartbeatSent: time.Time{},
}
}

View File

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

View File

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

View File

@ -31,7 +31,7 @@ func init() { //nolint:gochecknoinits
Helpers[WindowsCredentials] = newWinCredHelper
// Use WindowsCredentials by default.
defaultHelper = WindowsCredentials
DefaultHelper = WindowsCredentials
}
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 map[string]helperConstructor //nolint:gochecknoglobals
// defaultHelper is the default helper to use if the user hasn't yet set a preference.
defaultHelper string //nolint:gochecknoglobals
// DefaultHelper is the default helper to use if the user hasn't yet set a preference.
DefaultHelper string //nolint:gochecknoglobals
)
// 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 _, ok := Helpers[preferred]; !ok {
preferred = defaultHelper
preferred = DefaultHelper
}
// 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())
}
if len(msg.ReplyTos) > 0 {
if len(msg.ReplyTos) > 0 && !msg.IsDraft() {
if !(len(msg.ReplyTos) == 1 && addressEmpty(msg.ReplyTos[0])) {
hdr.Set("Reply-To", toAddressList(msg.ReplyTos))
}

View File

@ -431,7 +431,7 @@ func TestBuildSignedPlainEncryptedMessageWithPubKey(t *testing.T) {
section(t, res).
expectDate(is(`Wed, 01 Jan 2020 00:00:00 +0000`)).
expectContentType(is(`multipart/signed`)).
expectContentTypeParam(`micalg`, is(`pgp-sha256`)).
expectContentTypeParam(`micalg`, is(`SHA-256`)).
expectContentTypeParam(`protocol`, is(`application/pgp-signature`))
section(t, res, 1).
@ -439,7 +439,7 @@ func TestBuildSignedPlainEncryptedMessageWithPubKey(t *testing.T) {
expectContentTypeParam(`protected-headers`, is(`v1`)).
expectHeader(`Subject`, is(`simple plaintext body`)).
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()))
section(t, res, 1, 1).
@ -477,7 +477,7 @@ func TestBuildSignedHTMLEncryptedMessageWithPubKey(t *testing.T) {
section(t, res).
expectDate(is(`Wed, 01 Jan 2020 00:00:00 +0000`)).
expectContentType(is(`multipart/signed`)).
expectContentTypeParam(`micalg`, is(`pgp-sha256`)).
expectContentTypeParam(`micalg`, is(`SHA-256`)).
expectContentTypeParam(`protocol`, is(`application/pgp-signature`))
section(t, res, 1).
@ -485,7 +485,7 @@ func TestBuildSignedHTMLEncryptedMessageWithPubKey(t *testing.T) {
expectContentTypeParam(`protected-headers`, is(`v1`)).
expectHeader(`Subject`, is(`simple html body`)).
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()))
section(t, res, 1, 1).
@ -524,7 +524,7 @@ func TestBuildSignedMultipartAlternativeEncryptedMessageWithPubKey(t *testing.T)
section(t, res).
expectDate(is(`Wed, 01 Jan 2020 00:00:00 +0000`)).
expectContentType(is(`multipart/signed`)).
expectContentTypeParam(`micalg`, is(`pgp-sha256`)).
expectContentTypeParam(`micalg`, is(`SHA-256`)).
expectContentTypeParam(`protocol`, is(`application/pgp-signature`))
section(t, res, 1).
@ -532,8 +532,8 @@ func TestBuildSignedMultipartAlternativeEncryptedMessageWithPubKey(t *testing.T)
expectContentTypeParam(`protected-headers`, is(`v1`)).
expectHeader(`Subject`, is(`Alternative`)).
expectHeader(`From`, is(`"pm.bridge.qa" <pm.bridge.qa@gmail.com>`)).
expectHeader(`To`, is(`schizofrenic@pm.me`)).
expectSection(verifiesAgainst(section(t, res, 1, 1, 3).pubKey(), section(t, res, 2).signature()))
expectHeader(`To`, is("\"InfernalBridgeTester@proton.me\" <InfernalbridgeTester@proton.me>")).
expectSection(verifiesAgainst(section(t, res, 1, 1, 2).pubKey(), section(t, res, 2).signature()))
section(t, res, 1, 1).
expectContentType(is(`multipart/mixed`))
@ -549,17 +549,11 @@ func TestBuildSignedMultipartAlternativeEncryptedMessageWithPubKey(t *testing.T)
section(t, res, 1, 1, 1, 2).
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(`How does a penguin build its house`))
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`)).
expectContentTypeParam(`name`, is(`OpenPGP_0x161C0875822359F7.asc`)).
expectContentDisposition(is(`attachment`)).
@ -587,16 +581,16 @@ func TestBuildSignedEmbeddedMessageRFC822EncryptedMessageWithPubKey(t *testing.T
section(t, res).
expectDate(is(`Wed, 01 Jan 2020 00:00:00 +0000`)).
expectContentType(is(`multipart/signed`)).
expectContentTypeParam(`micalg`, is(`pgp-sha256`)).
expectContentTypeParam(`micalg`, is(`SHA-256`)).
expectContentTypeParam(`protocol`, is(`application/pgp-signature`))
section(t, res, 1).
expectContentType(is(`multipart/mixed`)).
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(`To`, is(`schizofrenic@pm.me`)).
expectSection(verifiesAgainst(section(t, res, 1, 1, 2).pubKey(), section(t, res, 2).signature()))
expectHeader(`To`, is("\"InfernalBridgeTester@proton.me\" <InfernalbridgeTester@proton.me>")).
expectSection(verifiesAgainst(section(t, res, 1, 1, 3).pubKey(), section(t, res, 2).signature()))
section(t, res, 1, 1).
expectContentType(is(`multipart/mixed`))
@ -605,17 +599,17 @@ func TestBuildSignedEmbeddedMessageRFC822EncryptedMessageWithPubKey(t *testing.T
expectContentType(is(`text/plain`))
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`)).
expectContentTypeParam(`name`, is(`OpenPGP_0x161C0875822359F7.asc`)).
expectContentDisposition(is(`attachment`)).
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).
expectContentType(is(`application/pgp-signature`)).
expectContentTypeParam(`name`, is(`OpenPGP_signature.asc`)).

View File

@ -73,6 +73,15 @@ type Attachment struct {
// Parse parses an RFC822 message.
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() {
if r := recover(); r != nil {
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 parse(p)
return parse(p, allowInvalidAddressLists)
}
// 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() {
if r := recover(); r != nil {
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 {
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")
}
m, err := parseMessageHeader(p.Root().Header)
m, err := parseMessageHeader(p.Root().Header, allowInvalidAddressLists)
if err != nil {
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
for fields := h.Fields(); fields.Next(); {
@ -451,7 +460,11 @@ func parseMessageHeader(h message.Header) (Message, error) {
case "from":
sender, err := rfc5322.ParseAddressList(fields.Value())
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 {
@ -461,7 +474,11 @@ func parseMessageHeader(h message.Header) (Message, error) {
case "to":
toList, err := rfc5322.ParseAddressList(fields.Value())
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
@ -469,7 +486,11 @@ func parseMessageHeader(h message.Header) (Message, error) {
case "reply-to":
replyTos, err := rfc5322.ParseAddressList(fields.Value())
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
@ -477,7 +498,11 @@ func parseMessageHeader(h message.Header) (Message, error) {
case "cc":
ccList, err := rfc5322.ParseAddressList(fields.Value())
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
@ -485,7 +510,11 @@ func parseMessageHeader(h message.Header) (Message, error) {
case "bcc":
bccList, err := rfc5322.ParseAddressList(fields.Value())
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

View File

@ -23,6 +23,7 @@ import (
"io"
"os"
"path/filepath"
"strings"
"testing"
"github.com/ProtonMail/go-proton-api"
@ -444,7 +445,7 @@ func TestParseWithAttachedPublicKey(t *testing.T) {
p, err := parser.New(f)
require.NoError(t, err)
m, err := ParseWithParser(p)
m, err := ParseWithParser(p, false)
require.NoError(t, err)
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")
}
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) {
var err error

View File

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

View File

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

View File

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

View File

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

View File

@ -32,12 +32,12 @@ func TestFreePort(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.True(t, !IsPortFree(testPort), "port should be occupied")
_ = dummyserver.Close()
_ = dummyServer.Close()
}
func TestFindFreePortFromDirectly(t *testing.T) {
@ -46,11 +46,21 @@ func TestFindFreePortFromDirectly(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)
foundPort := FindFreePortFrom(testPort)
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 body in the "([^"]*)" request to "([^"]*)" is:$`, s.theBodyInTheRequestToIs)
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 ====
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)
@ -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 (\d+) messages in "([^"]*)"$`, s.theAddressOfAccountHasMessagesInMailbox)
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 ===
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 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 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 SMTP port to (\d+)$`, s.theUserChangesTheSMTPPortTo)
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 deletes the gluon files$`, s.theUserDeletesTheGluonFiles)
ctx.Step(`^the user deletes the gluon cache$`, s.theUserDeletesTheGluonCache)
ctx.Step(`^the user reports a bug$`, s.theUserReportsABug)
ctx.Step(`^the user hides All Mail$`, s.theUserHidesAllMail)
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 down event$`, s.bridgeSendsAConnectionDownEvent)
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 a forced update event$`, s.bridgeSendsAForcedUpdateEvent)
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 ====
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 not listed$`, s.userIsNotListed)
ctx.Step(`^user "([^"]*)" finishes syncing$`, s.userFinishesSyncing)
ctx.Step(`^user "([^"]*)" has telemetry set to (\d+)$`, s.userHasTelemetrySetTo)
// ==== IMAP ====
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 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 "([^"]*)" does not see header "([^"]*)" in message with subject "([^"]*)" in "([^"]*)"$`, s.imapClientDoesNotSeeHeaderInMessageWithSubject)
// ==== SMTP ====
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 the following message from "([^"]*)" to "([^"]*)":$`, s.smtpClientSendsTheFollowingMessageFromTo)
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{
Format: "pretty",

View File

@ -21,7 +21,9 @@ import (
"context"
"errors"
"fmt"
"net"
"os"
"strconv"
"time"
"github.com/Masterminds/semver/v3"
@ -78,6 +80,10 @@ func (s *scenario) theUserSetsTheAddressModeOfUserTo(user, mode string) error {
}
}
func (s *scenario) theUserChangesTheDefaultKeychainApplication() error {
return s.t.bridge.SetKeychainApp("CustomKeychainApp")
}
func (s *scenario) theUserChangesTheGluonPath() error {
gluonDir, err := os.MkdirTemp(s.t.dir, "gluon")
if err != nil {
@ -116,7 +122,6 @@ func (s *scenario) theUserHasDisabledAutomaticUpdates() error {
started = true
}
if err := s.t.bridge.SetAutoUpdate(false); err != nil {
return err
}
@ -126,10 +131,26 @@ func (s *scenario) theUserHasDisabledAutomaticUpdates() error {
return err
}
}
return nil
}
func (s *scenario) theUserHasDisabledAutomaticStart() error {
return s.t.bridge.SetAutostart(false)
}
func (s *scenario) theUserHasEnabledAlternativeRouting() error {
s.t.expectProxyCtlAllowProxy()
return s.t.bridge.SetProxyAllowed(true)
}
func (s *scenario) theUserSetIMAPModeToSSL() error {
return s.t.bridge.SetIMAPSSL(true)
}
func (s *scenario) theUserSetSMTPModeToSSL() error {
return s.t.bridge.SetSMTPSSL(true)
}
func (s *scenario) theUserReportsABug() error {
return s.t.bridge.ReportBug(context.Background(), "osType", "osVersion", "description", "username", "email", "client", false)
}
@ -284,6 +305,22 @@ func (s *scenario) bridgeReportsMessage(message string) error {
return nil
}
func (s *scenario) bridgeTelemetryFeatureEnabled() error {
return s.checkTelemetry(true)
}
func (s *scenario) bridgeTelemetryFeatureDisabled() error {
return s.checkTelemetry(false)
}
func (s *scenario) checkTelemetry(expect bool) error {
res := s.t.bridge.IsTelemetryAvailable()
if res != expect {
return fmt.Errorf("expected telemetry feature %v but got %v ", expect, res)
}
return nil
}
func (s *scenario) theUserHidesAllMail() error {
return s.t.bridge.SetShowAllMail(false)
}
@ -291,3 +328,45 @@ func (s *scenario) theUserHidesAllMail() error {
func (s *scenario) theUserShowsAllMail() error {
return s.t.bridge.SetShowAllMail(true)
}
func (s *scenario) theUserDisablesTelemetryInBridgeSettings() error {
return s.t.bridge.SetTelemetryDisabled(true)
}
func (s *scenario) theUserEnablesTelemetryInBridgeSettings() error {
return s.t.bridge.SetTelemetryDisabled(false)
}
func (s *scenario) networkPortIsBusy(port int) {
if listener, err := net.Listen("tcp", "127.0.0.1:"+strconv.Itoa(port)); err == nil { // we ignore errors. Most likely port is already busy.
s.t.dummyListeners = append(s.t.dummyListeners, listener)
}
}
func (s *scenario) networkPortRangeIsBusy(startPort, endPort int) {
if startPort > endPort {
startPort, endPort = endPort, startPort
}
for port := startPort; port <= endPort; port++ {
s.networkPortIsBusy(port)
}
}
func (s *scenario) bridgeIMAPPortIs(expectedPort int) error {
actualPort := s.t.bridge.GetIMAPPort()
if actualPort != expectedPort {
return fmt.Errorf("expected IMAP port to be %v but got %v", expectedPort, actualPort)
}
return nil
}
func (s *scenario) bridgeSMTPPortIs(expectedPort int) error {
actualPort := s.t.bridge.GetSMTPPort()
if actualPort != expectedPort {
return fmt.Errorf("expected SMTP port to be %v but got %v", expectedPort, actualPort)
}
return nil
}

View File

@ -174,6 +174,9 @@ func (t *testCtx) initBridge() (<-chan events.Event, error) {
}
t.bridge = bridge
t.heartbeat.setBridge(bridge)
bridge.StartHeartbeat(t.heartbeat)
return t.events.collectFrom(eventCh), nil
}
@ -342,6 +345,9 @@ func (t *testCtx) closeFrontendClient() error {
return nil
}
func (t *testCtx) expectProxyCtlAllowProxy() {
t.mocks.ProxyCtl.EXPECT().AllowProxy()
}
type mockRestarter struct{}

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