Compare commits

..

1 Commits

Author SHA1 Message Date
43cbedafb8 chore: Colorado Bridge 3.13.0 changelog. 2024-08-30 15:35:30 +02:00
136 changed files with 811 additions and 11724 deletions

3
.gitignore vendored
View File

@ -7,7 +7,6 @@
*~
.idea
.vscode
.vs
# Test files
godog.test
@ -36,8 +35,6 @@ cmd/Import-Export/deploy
proton-bridge
cmd/Desktop-Bridge/*.exe
cmd/launcher/*.exe
bin/
obj/
# Jetbrains (CLion, Golang) cmake build dirs
cmake-build-*/

View File

@ -88,7 +88,6 @@ linters:
- durationcheck # check for two durations multiplied together [fast: false, auto-fix: false]
- exhaustive # check exhaustiveness of enum switch statements [fast: false, auto-fix: false]
- exportloopref # checks for pointers to enclosing loop variables [fast: false, auto-fix: false]
- copyloopvar # detects places where loop variables are copied.
- forcetypeassert # finds forced type assertions [fast: true, auto-fix: false]
- godot # Check if comments end in a period [fast: true, auto-fix: true]
- goheader # Checks is file header matches to pattern [fast: true, auto-fix: false]

View File

@ -63,15 +63,11 @@ Proton Mail Bridge includes the following 3rd party software:
* [goleak](https://go.uber.org/goleak) available under [license](https://pkg.go.dev/go.uber.org/goleak?tab=licenses)
* [exp](https://golang.org/x/exp) available under [license](https://cs.opensource.google/go/x/exp/+/master:LICENSE)
* [net](https://golang.org/x/net) available under [license](https://cs.opensource.google/go/x/net/+/master:LICENSE)
* [oauth2](https://golang.org/x/oauth2) available under [license](https://cs.opensource.google/go/x/oauth2/+/master:LICENSE)
* [sys](https://golang.org/x/sys) available under [license](https://cs.opensource.google/go/x/sys/+/master:LICENSE)
* [text](https://golang.org/x/text) available under [license](https://cs.opensource.google/go/x/text/+/master:LICENSE)
* [api](https://google.golang.org/api) available under [license](https://pkg.go.dev/google.golang.org/api?tab=licenses)
* [grpc](https://google.golang.org/grpc) available under [license](https://github.com/grpc/grpc-go/blob/master/LICENSE)
* [protobuf](https://google.golang.org/protobuf) available under [license](https://github.com/protocolbuffers/protobuf/blob/main/LICENSE)
* [plist](https://howett.net/plist) available under [license](https://github.com/DHowett/go-plist/blob/main/LICENSE)
* [compute](https://cloud.google.com/go/compute) available under [license](https://pkg.go.dev/cloud.google.com/go/compute?tab=licenses)
* [metadata](https://cloud.google.com/go/compute/metadata) available under [license](https://pkg.go.dev/cloud.google.com/go/compute/metadata?tab=licenses)
* [bcrypt](https://github.com/ProtonMail/bcrypt) available under [license](https://github.com/ProtonMail/bcrypt/blob/master/LICENSE)
* [go-crypto](https://github.com/ProtonMail/go-crypto) available under [license](https://github.com/ProtonMail/go-crypto/blob/master/LICENSE)
* [go-mime](https://github.com/ProtonMail/go-mime) available under [license](https://github.com/ProtonMail/go-mime/blob/master/LICENSE)
@ -99,11 +95,8 @@ Proton Mail Bridge includes the following 3rd party software:
* [validator](https://github.com/go-playground/validator/v10) available under [license](https://github.com/go-playground/validator/v10/blob/master/LICENSE)
* [go-json](https://github.com/goccy/go-json) available under [license](https://github.com/goccy/go-json/blob/master/LICENSE)
* [uuid](https://github.com/gofrs/uuid) available under [license](https://github.com/gofrs/uuid/blob/master/LICENSE)
* [groupcache](https://github.com/golang/groupcache) available under [license](https://github.com/golang/groupcache/blob/master/LICENSE)
* [protobuf](https://github.com/golang/protobuf) available under [license](https://github.com/golang/protobuf/blob/master/LICENSE)
* [pprof](https://github.com/google/pprof) available under [license](https://github.com/google/pprof/blob/master/LICENSE)
* [enterprise-certificate-proxy](https://github.com/googleapis/enterprise-certificate-proxy) available under [license](https://github.com/googleapis/enterprise-certificate-proxy/blob/master/LICENSE)
* [gax-go](https://github.com/googleapis/gax-go/v2) available under [license](https://github.com/googleapis/gax-go/v2/blob/master/LICENSE)
* [errwrap](https://github.com/hashicorp/errwrap) available under [license](https://github.com/hashicorp/errwrap/blob/master/LICENSE)
* [go-immutable-radix](https://github.com/hashicorp/go-immutable-radix) available under [license](https://github.com/hashicorp/go-immutable-radix/blob/master/LICENSE)
* [go-memdb](https://github.com/hashicorp/go-memdb) available under [license](https://github.com/hashicorp/go-memdb/blob/master/LICENSE)
@ -131,16 +124,14 @@ Proton Mail Bridge includes the following 3rd party software:
* [codec](https://github.com/ugorji/go/codec) available under [license](https://github.com/ugorji/go/codec/blob/master/LICENSE)
* [tagparser](https://github.com/vmihailenco/tagparser/v2) available under [license](https://github.com/vmihailenco/tagparser/v2/blob/master/LICENSE)
* [smetrics](https://github.com/xrash/smetrics) available under [license](https://github.com/xrash/smetrics/blob/master/LICENSE)
* [go-ordered-json](https://gitlab.com/c0b/go-ordered-json) available under [license](https://gitlab.com/c0b/go-ordered-json/blob/master/LICENSE)
* [go.opencensus.io](https://pkg.go.dev/go.opencensus.io?tab=licenses) available under [license](https://pkg.go.dev/go.opencensus.io?tab=licenses)
* [go-ordered-json](https://gitlab.com/c0b/go-ordered-json)
* [arch](https://golang.org/x/arch) available under [license](https://cs.opensource.google/go/x/arch/+/master:LICENSE)
* [crypto](https://golang.org/x/crypto) available under [license](https://cs.opensource.google/go/x/crypto/+/master:LICENSE)
* [mod](https://golang.org/x/mod) available under [license](https://cs.opensource.google/go/x/mod/+/master:LICENSE)
* [sync](https://golang.org/x/sync) available under [license](https://cs.opensource.google/go/x/sync/+/master:LICENSE)
* [tools](https://golang.org/x/tools) available under [license](https://cs.opensource.google/go/x/tools/+/master:LICENSE)
* [appengine](https://google.golang.org/appengine) available under [license](https://pkg.go.dev/google.golang.org/appengine?tab=licenses)
* [genproto](https://google.golang.org/genproto) available under [license](https://pkg.go.dev/google.golang.org/genproto?tab=licenses)
* [yaml](https://gopkg.in/yaml.v3) available under [license](https://github.com/go-yaml/yaml/blob/v3.0.1/LICENSE) available under [license](https://github.com/go-yaml/yaml/blob/v3.0.1/LICENSE)
* [yaml](https://gopkg.in/yaml.v3) available under [license](https://github.com/go-yaml/yaml/blob/v3.0.1/LICENSE)
* [go-message](https://github.com/ProtonMail/go-message) available under [license](https://github.com/ProtonMail/go-message/blob/master/LICENSE)
* [go-smtp](https://github.com/ProtonMail/go-smtp) available under [license](https://github.com/ProtonMail/go-smtp/blob/master/LICENSE)
* [resty](https://github.com/LBeernaertProton/resty/v2) available under [license](https://github.com/LBeernaertProton/resty/v2/blob/master/LICENSE)

View File

@ -3,43 +3,6 @@
Changelog [format](http://keepachangelog.com/en/1.0.0/)
## Erasmus Bridge 3.15.0
### Added
* BRIDGE-238: Added host information to sentry events; new sentry event for keychain issues.
* BRIDGE-236: Added SMTP observability metrics.
* BRIDGE-217: Added missing parameter to the CLI help command.
* BRIDGE-234: Add accessibility name in QML for UI automation.
* BRIDGE-232: Test: Add Home Menu Bridge UI e2e automation tests.
* BRIDGE-220: Test: Add Bridge E2E UI login/logout tests for Windows.
### Changed
* BRIDGE-228: Removed sentry events.
* BRIDGE-218: Observability adapter; gluon observability metrics and tests.
* BRIDGE-215: Tweak wording on macOS profile install page.
* BRIDGE-131: Test: Integration tests for messages from Proton <-> Gmail.
* BRIDGE-142: Bridge icon can be removed from the menu bar on macOS.
### Fixed
* BRIDGE-240: Fix for running against Qt 6.8 (contribution of GitHub user Cimbali).
* BRIDGE-231: Fix reversed header order in messages.
* BRIDGE-235: Fix compilation of Bridge GUI Tester on Windows.
* BRIDGE-120: Use appropriate address key when importing / saving draft.
## Dragon Bridge 3.14.0
### Changed
* BRIDGE-207: Failure to download or verify an update now fails silently.
* BRIDGE-204: Removed redundant Sentry events.
* BRIDGE-150: Observability service modification.
* BRIDGE-210: Reduced log level of cache events so they won't be printed to stdout.
### Fixed
* BRIDGE-106: Fixed import of multipart-related messages.
* BRIDGE-108: Fixed GetInitials when empty username is passed.
## Colorado Bridge 3.13.0
### Added

View File

@ -12,7 +12,7 @@ ROOT_DIR:=$(realpath .)
.PHONY: build build-gui build-nogui build-launcher versioner hasher
# Keep version hardcoded so app build works also without Git repository.
BRIDGE_APP_VERSION?=3.15.0+git
BRIDGE_APP_VERSION?=3.13.0+git
APP_VERSION:=${BRIDGE_APP_VERSION}
APP_FULL_NAME:=Proton Mail Bridge
APP_VENDOR:=Proton AG
@ -189,7 +189,7 @@ ${RESOURCE_FILE}: ./dist/info.rc ./dist/${SRC_ICO} .FORCE
## Dev dependencies
.PHONY: install-devel-tools install-linter install-go-mod-outdated install-git-hooks
LINTVER:="v1.61.0"
LINTVER:="v1.59.1"
LINTSRC:="https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh"
install-dev-dependencies: install-devel-tools install-linter install-go-mod-outdated

View File

@ -1,7 +1,7 @@
# Proton Mail Bridge
# Proton Mail Bridge and Import Export app
Copyright (c) 2024 Proton AG
This repository holds the Proton Mail Bridge application.
This repository holds the Proton Mail Bridge and the Proton Mail Import-Export applications.
For a detailed build information see [BUILDS](./BUILDS.md).
The license can be found in [LICENSE](./LICENSE) file, for more licensing information see [COPYING_NOTES](./COPYING_NOTES.md).
For contribution policy see [CONTRIBUTING](./CONTRIBUTING.md).
@ -13,7 +13,7 @@ Proton Mail Bridge for e-mail clients.
When launched, Bridge will initialize local IMAP/SMTP servers and render
its GUI.
To configure an e-mail client, first log in using your Proton Mail credentials.
To configure an e-mail client, firstly log in using your Proton Mail credentials.
Open your e-mail client and add a new account using the settings which are
located in the Bridge GUI. The client will only be able to sync with
your Proton Mail account when the Bridge is running, thus the option
@ -24,10 +24,10 @@ background.
More details [on the public website](https://proton.me/mail/bridge).
## Launcher
The launcher is a binary used to run the Proton Mail Bridge.
## Launchers
Launchers are binaries used to run the Proton Mail Bridge or Import-Export apps.
The Official distribution of the Proton Mail Bridge application contains
Official distributions of the Proton Mail Bridge and Import-Export apps contain
both a launcher and the app itself. The launcher is installed in a protected
area of the system (i.e. an area accessible only with admin privileges) and is
used to run the app. The launcher ensures that nobody tampered with the app's
@ -37,7 +37,7 @@ feature enables the app to securely update itself automatically without asking
the user for a password.
## Keychain
You need to have a keychain in order to run Proton Mail Bridge. On Mac or
You need to have a keychain in order to run the Proton Mail Bridge. On Mac or
Windows, Bridge uses native credential managers. On Linux, use `secret-service` freedesktop.org API
(e.g. [Gnome keyring](https://wiki.gnome.org/Projects/GnomeKeyring/))
or

15
go.mod
View File

@ -7,9 +7,9 @@ toolchain go1.21.9
require (
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
github.com/Masterminds/semver/v3 v3.2.0
github.com/ProtonMail/gluon v0.17.1-0.20241018144126-31e040c2417e
github.com/ProtonMail/gluon v0.17.1-0.20240514133734-79cdd0fec41c
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
github.com/ProtonMail/go-proton-api v0.4.1-0.20240918100656-b4860af56d47
github.com/ProtonMail/go-proton-api v0.4.1-0.20240829112804-d663a2ef90c2
github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton
github.com/PuerkitoBio/goquery v1.8.1
github.com/abiosoft/ishell v2.0.0+incompatible
@ -47,18 +47,14 @@ require (
go.uber.org/goleak v1.2.1
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
golang.org/x/net v0.24.0
golang.org/x/oauth2 v0.7.0
golang.org/x/sys v0.19.0
golang.org/x/text v0.14.0
google.golang.org/api v0.114.0
google.golang.org/grpc v1.56.3
google.golang.org/protobuf v1.33.0
howett.net/plist v1.0.0
)
require (
cloud.google.com/go/compute v1.19.1 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf // indirect
github.com/ProtonMail/go-crypto v0.0.0-20230717121622-edf196117233 // indirect
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect
@ -86,11 +82,8 @@ require (
github.com/go-playground/validator/v10 v10.14.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/gofrs/uuid v4.3.0+incompatible // indirect
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
github.com/googleapis/gax-go/v2 v2.7.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/go-memdb v1.3.3 // indirect
@ -119,19 +112,17 @@ require (
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a // indirect
go.opencensus.io v0.24.0 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.22.0 // indirect
golang.org/x/mod v0.8.0 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/tools v0.6.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace (
github.com/emersion/go-message => github.com/ProtonMail/go-message v0.13.1-0.20240919135104-3bc88e6a9423
github.com/emersion/go-message => github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7
github.com/emersion/go-smtp => github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865
github.com/go-resty/resty/v2 => github.com/LBeernaertProton/resty/v2 v2.0.0-20231129100320-dddf8030d93a
github.com/keybase/go-keychain => github.com/cuthix/go-keychain v0.0.0-20240103134243-0b6a41580b77

99
go.sum
View File

@ -5,16 +5,9 @@ cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6A
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.110.0 h1:Zc8gqp3+a9/Eyph2KDmcGaPtbKRIoqq4YTlL4NMD0Ys=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/compute v1.19.1 h1:am86mquDUgjGNWxiGn+5PGLbmgiWXlE/yNWpIpNvuXY=
cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE=
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
cloud.google.com/go/longrunning v0.4.1 h1:v+yFJOfKC3yZdY6ZUI933pIYdhyhV8S3NpWrXWmg7jM=
cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
@ -34,31 +27,35 @@ github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAE
github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs69zUkSzubzjBbL+cmOXgnmt9Fyd9ug=
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
github.com/ProtonMail/gluon v0.17.1-0.20240923151549-d23b4bec3602 h1:EoMjWlC32tg46L/07hWoiZfLkqJyxVMcsq4Cyn+Ofqc=
github.com/ProtonMail/gluon v0.17.1-0.20240923151549-d23b4bec3602/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
github.com/ProtonMail/gluon v0.17.1-0.20241002092751-3bbeea9053af h1:iMxTQUg2cB47cXqpMev3cZmQoGBOef3cSUjBbdEl33M=
github.com/ProtonMail/gluon v0.17.1-0.20241002092751-3bbeea9053af/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
github.com/ProtonMail/gluon v0.17.1-0.20241002111651-173859b80060 h1:dcu3tT84GjoXb++n7crv8UJeG8eRwogjTYdkoJ+MjQI=
github.com/ProtonMail/gluon v0.17.1-0.20241002111651-173859b80060/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
github.com/ProtonMail/gluon v0.17.1-0.20241002142736-ef4153d156d8 h1:YxPHSJUA87i1hc6s1YrW89++V7HpcR7LSFQ6XM0TsAE=
github.com/ProtonMail/gluon v0.17.1-0.20241002142736-ef4153d156d8/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
github.com/ProtonMail/gluon v0.17.1-0.20241008123701-ddf4a459d0b4 h1:xE+V17O9HIttMpVymNCORQILk9OKpSekrrPbX7YGnF8=
github.com/ProtonMail/gluon v0.17.1-0.20241008123701-ddf4a459d0b4/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
github.com/ProtonMail/gluon v0.17.1-0.20241014082854-9d93627be032 h1:5bwI+mwF26c460xlq2Dw3/cVF1cU4Xo4kTKX1/pBXko=
github.com/ProtonMail/gluon v0.17.1-0.20241014082854-9d93627be032/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
github.com/ProtonMail/gluon v0.17.1-0.20241018144126-31e040c2417e h1:+UfdKOkF9JEiH9VXWBo+/nlXNVSJcxtuf4+SJTrk9fw=
github.com/ProtonMail/gluon v0.17.1-0.20241018144126-31e040c2417e/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
github.com/ProtonMail/gluon v0.17.1-0.20240514133734-79cdd0fec41c h1:P3SvCACt13Zqdj0IRDB4bgwqI68+oMB2j0uVuPQyoTw=
github.com/ProtonMail/gluon v0.17.1-0.20240514133734-79cdd0fec41c/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
github.com/ProtonMail/go-crypto v0.0.0-20230717121622-edf196117233 h1:bdoKdh0f66/lrgVfYlxw0aqISY/KOqXmFJyGt7rGmnc=
github.com/ProtonMail/go-crypto v0.0.0-20230717121622-edf196117233/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
github.com/ProtonMail/go-message v0.13.1-0.20240919135104-3bc88e6a9423 h1:p8nBDxvRnvDOyrcePKkPpErWGhDoTqpX8a1c54CcSu0=
github.com/ProtonMail/go-message v0.13.1-0.20240919135104-3bc88e6a9423/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7 h1:+j+Kd/DyZ/qGfMT9htAT7HxqIEbZHsatsx+m8AoV6fc=
github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
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.20240918100656-b4860af56d47 h1:a+3dOyIxJEslN5HxyICM8flY9lnCyJupXNcv6fUaivA=
github.com/ProtonMail/go-proton-api v0.4.1-0.20240918100656-b4860af56d47/go.mod h1:3A0cpdo0BIenIPjTG6u8EbzJ8uuJy7rVvM/NaynjCKA=
github.com/ProtonMail/go-proton-api v0.4.1-0.20240612082117-0f92424eed80 h1:cP4+6RFn9vVgYnoDwxBU4EtIAZA+eM4rzOaSZNqZ1xg=
github.com/ProtonMail/go-proton-api v0.4.1-0.20240612082117-0f92424eed80/go.mod h1:3A0cpdo0BIenIPjTG6u8EbzJ8uuJy7rVvM/NaynjCKA=
github.com/ProtonMail/go-proton-api v0.4.1-0.20240808145610-88df257767f6 h1:nERxOYS4ndSgWEr834YYkb1j0bZK/dJAmhoyYB1MtNY=
github.com/ProtonMail/go-proton-api v0.4.1-0.20240808145610-88df257767f6/go.mod h1:3A0cpdo0BIenIPjTG6u8EbzJ8uuJy7rVvM/NaynjCKA=
github.com/ProtonMail/go-proton-api v0.4.1-0.20240819131705-149e50199c5b h1:zifGh4LS5HwQIaVCccSe5/oJGTOjFeVObMRl3QJoJ3k=
github.com/ProtonMail/go-proton-api v0.4.1-0.20240819131705-149e50199c5b/go.mod h1:3A0cpdo0BIenIPjTG6u8EbzJ8uuJy7rVvM/NaynjCKA=
github.com/ProtonMail/go-proton-api v0.4.1-0.20240821081056-dd607af0f917 h1:Ma6PfXFDuw7rYYq28FXNW6ubhYquRUmBuLyZrjJWHUE=
github.com/ProtonMail/go-proton-api v0.4.1-0.20240821081056-dd607af0f917/go.mod h1:3A0cpdo0BIenIPjTG6u8EbzJ8uuJy7rVvM/NaynjCKA=
github.com/ProtonMail/go-proton-api v0.4.1-0.20240822150235-7a6190889179 h1:6Xo0iRYa4GBgZ2HA+IR3KdqiML8Z10h2F9TYe+9n1+M=
github.com/ProtonMail/go-proton-api v0.4.1-0.20240822150235-7a6190889179/go.mod h1:3A0cpdo0BIenIPjTG6u8EbzJ8uuJy7rVvM/NaynjCKA=
github.com/ProtonMail/go-proton-api v0.4.1-0.20240827084449-71096377c391 h1:PW6bE+mhsfAx4+wDCCNjhFrCNiiuMjY6j7RwqRUdPKI=
github.com/ProtonMail/go-proton-api v0.4.1-0.20240827084449-71096377c391/go.mod h1:3A0cpdo0BIenIPjTG6u8EbzJ8uuJy7rVvM/NaynjCKA=
github.com/ProtonMail/go-proton-api v0.4.1-0.20240827122236-ca6bb6449bba h1:QtDxgIbgPqRQg7VT+nIUJlaOyNFAoGyg59oW3Hji/0A=
github.com/ProtonMail/go-proton-api v0.4.1-0.20240827122236-ca6bb6449bba/go.mod h1:3A0cpdo0BIenIPjTG6u8EbzJ8uuJy7rVvM/NaynjCKA=
github.com/ProtonMail/go-proton-api v0.4.1-0.20240827132526-849231fc34a1 h1:gATlMoj4raG32WyGGh8SpipoQeR2AlU7g+8NAMicTcw=
github.com/ProtonMail/go-proton-api v0.4.1-0.20240827132526-849231fc34a1/go.mod h1:3A0cpdo0BIenIPjTG6u8EbzJ8uuJy7rVvM/NaynjCKA=
github.com/ProtonMail/go-proton-api v0.4.1-0.20240829112804-d663a2ef90c2 h1:yx0iejqB5c21HIN5jn9IsbyzUns0dPUUaGfyUHF3TmQ=
github.com/ProtonMail/go-proton-api v0.4.1-0.20240829112804-d663a2ef90c2/go.mod h1:3A0cpdo0BIenIPjTG6u8EbzJ8uuJy7rVvM/NaynjCKA=
github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865 h1:EP1gnxLL5Z7xBSymE9nSTM27nRYINuvssAtDmG0suD8=
github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
@ -93,7 +90,6 @@ github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7N
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
@ -110,7 +106,6 @@ github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtM
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
@ -162,10 +157,6 @@ github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwo
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3 h1:hQ1wTMaKcGfobYRT88RM8NFNyX+IQHvagkm/tqViU98=
github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
@ -218,8 +209,6 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69
github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff/go.mod h1:wfqRWLHRBsRgkp5dmbG56SA0DmVtwrF5N3oPdI8t+Aw=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
@ -228,13 +217,6 @@ github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+Licev
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
@ -242,10 +224,6 @@ github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Z
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
@ -256,15 +234,10 @@ github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OI
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y=
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k=
github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gax-go/v2 v2.7.1 h1:gF4c0zjUP2H/s/hEGyLA3I0fA2ZWjzYiONAD6cvPr8A=
github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
@ -410,7 +383,6 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
@ -492,8 +464,6 @@ gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a/go.mod h1:NREv
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
@ -508,7 +478,6 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
@ -557,7 +526,6 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
@ -575,8 +543,6 @@ golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
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=
golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g=
golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -607,7 +573,6 @@ golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200720211630-cb9d2d5c5666/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -667,7 +632,6 @@ golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
@ -692,14 +656,10 @@ google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.114.0 h1:1xQPji6cO2E2vLiI+C/XiFAnsn1WV3mjaEwGLhi3grE=
google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
@ -709,27 +669,13 @@ google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc=
google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
@ -757,7 +703,6 @@ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=

View File

@ -75,21 +75,17 @@ const (
flagLogIMAP = "log-imap"
flagLogSMTP = "log-smtp"
flagEnableKeychainTest = "enable-keychain-test"
flagDisableKeychainTest = "disable-keychain-test"
flagSoftwareRenderer = "software-renderer"
flagSetSoftwareRenderer = "set-software-renderer"
flagSetHardwareRenderer = "set-hardware-renderer"
)
// Hidden flags.
const (
flagLauncher = "launcher"
flagNoWindow = "no-window"
flagParentPID = "parent-pid"
FlagSessionID = "session-id"
flagLauncher = "launcher"
flagNoWindow = "no-window"
flagParentPID = "parent-pid"
flagSoftwareRenderer = "software-renderer"
flagEnableKeychainTest = "enable-keychain-test"
flagDisableKeychainTest = "disable-keychain-test"
FlagSessionID = "session-id"
)
const (
@ -98,17 +94,17 @@ const (
)
var cliFlagEnableKeychainTest = &cli.BoolFlag{ //nolint:gochecknoglobals
Name: flagEnableKeychainTest,
Usage: "Enable the keychain test for the current and future executions of the application",
Value: false,
DisableDefaultText: true,
Name: flagEnableKeychainTest,
Usage: "Enable the keychain test",
Hidden: true,
Value: false,
} //nolint:gochecknoglobals
var cliFlagDisableKeychainTest = &cli.BoolFlag{ //nolint:gochecknoglobals
Name: flagDisableKeychainTest,
Usage: "Disable the keychain test for the current and future executions of the application",
Value: false,
DisableDefaultText: true,
Name: flagDisableKeychainTest,
Usage: "Disable the keychain test",
Hidden: true,
Value: false,
}
func New() *cli.App {
@ -160,24 +156,6 @@ func New() *cli.App {
Name: flagLogSMTP,
Usage: "Enable logging of SMTP communications (may contain decrypted data!)",
},
&cli.BoolFlag{
Name: flagSoftwareRenderer, // This flag is ignored by bridge, but should be passed to launcher in case of restart, so it need to be accepted by the CLI parser.
Usage: "Use software rendering of the GUI for the current execution of the application",
Value: false,
DisableDefaultText: true,
},
&cli.BoolFlag{
Name: flagSetSoftwareRenderer, // This flag is ignored by bridge, we just want it to be shown in the help (BRIDGE-217).
Usage: "Toggle software rendering of the GUI for the current and future executions of the application",
Value: false,
DisableDefaultText: true,
},
&cli.BoolFlag{
Name: flagSetHardwareRenderer, // This flag is ignored by bridge, we just want it to be shown in the help (BRIDGE-217).
Usage: "Toggle hardware rendering of the GUI for the current and future executions of the application",
Value: false,
DisableDefaultText: true,
},
// Hidden flags
&cli.BoolFlag{
@ -196,22 +174,19 @@ func New() *cli.App {
Hidden: true,
Value: -1,
},
&cli.BoolFlag{
Name: flagSoftwareRenderer, // This flag is ignored by bridge, but should be passed to launcher in case of restart, so it need to be accepted by the CLI parser.
Usage: "GUI is using software renderer",
Hidden: true,
Value: false,
},
&cli.StringFlag{
Name: FlagSessionID,
Hidden: true,
},
}
// We override the default help value because we want "Show" to be capitalized
cli.HelpFlag = &cli.BoolFlag{
Name: "help",
Usage: "Show help",
DisableDefaultText: true,
}
if onMacOS() {
// The two flags below were introduced for BRIDGE-116, and are available only on macOS.
app.Flags = append(app.Flags, cliFlagEnableKeychainTest, cliFlagDisableKeychainTest)
// the two flags below were introduced by BRIDGE-116
cliFlagEnableKeychainTest,
cliFlagDisableKeychainTest,
}
app.Action = run
@ -285,7 +260,7 @@ func run(c *cli.Context) error {
skipKeychainTest := checkSkipKeychainTest(c, settings)
return WithKeychainList(crashHandler, skipKeychainTest, func(keychains *keychain.List) error {
// Unlock the encrypted vault.
return WithVault(reporter, locations, keychains, crashHandler, func(v *vault.Vault, insecure, corrupt bool) error {
return WithVault(locations, keychains, crashHandler, func(v *vault.Vault, insecure, corrupt bool) error {
if !v.Migrated() {
// Migrate old settings into the vault.
if err := migrateOldSettings(v); err != nil {
@ -573,7 +548,7 @@ func setDeviceCookies(jar *cookies.Jar) error {
}
func checkSkipKeychainTest(c *cli.Context, settingsDir string) bool {
if !onMacOS() {
if runtime.GOOS != "darwin" {
return false
}
@ -603,7 +578,3 @@ func checkSkipKeychainTest(c *cli.Context, settingsDir string) bool {
return skip
}
func onMacOS() bool {
return runtime.GOOS == "darwin"
}

View File

@ -18,6 +18,7 @@
package app
import (
"runtime"
"testing"
"github.com/stretchr/testify/require"
@ -43,12 +44,12 @@ func TestCheckSkipKeychainTest(t *testing.T) {
disableArgs := []string{"appName", "-" + flagDisableKeychainTest}
bothArgs := []string{"appName", "-" + flagDisableKeychainTest, "-" + flagEnableKeychainTest}
onMac := onMacOS()
const trueOnlyOnMac = runtime.GOOS == "darwin"
expectedResult = false
require.NoError(t, app.Run(noArgs))
expectedResult = onMac
expectedResult = trueOnlyOnMac
require.NoError(t, app.Run(disableArgs))
require.NoError(t, app.Run(noArgs))
@ -56,7 +57,7 @@ func TestCheckSkipKeychainTest(t *testing.T) {
require.NoError(t, app.Run(enableArgs))
require.NoError(t, app.Run(noArgs))
expectedResult = onMac
expectedResult = trueOnlyOnMac
require.NoError(t, app.Run(disableArgs))
expectedResult = false

View File

@ -25,18 +25,17 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/certs"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
"github.com/ProtonMail/proton-bridge/v3/internal/sentry"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
"github.com/sirupsen/logrus"
)
func WithVault(reporter *sentry.Reporter, locations *locations.Locations, keychains *keychain.List, panicHandler async.PanicHandler, fn func(*vault.Vault, bool, bool) error) error {
func WithVault(locations *locations.Locations, keychains *keychain.List, panicHandler async.PanicHandler, fn func(*vault.Vault, bool, bool) error) error {
logrus.Debug("Creating vault")
defer logrus.Debug("Vault stopped")
// Create the encVault.
encVault, insecure, corrupt, err := newVault(reporter, locations, keychains, panicHandler)
encVault, insecure, corrupt, err := newVault(locations, keychains, panicHandler)
if err != nil {
return fmt.Errorf("could not create vault: %w", err)
}
@ -58,7 +57,7 @@ func WithVault(reporter *sentry.Reporter, locations *locations.Locations, keycha
return fn(encVault, insecure, corrupt != nil)
}
func newVault(reporter *sentry.Reporter, locations *locations.Locations, keychains *keychain.List, panicHandler async.PanicHandler) (*vault.Vault, bool, error, error) {
func newVault(locations *locations.Locations, keychains *keychain.List, panicHandler async.PanicHandler) (*vault.Vault, bool, error, error) {
vaultDir, err := locations.ProvideSettingsPath()
if err != nil {
return nil, false, nil, fmt.Errorf("could not get vault dir: %w", err)
@ -72,16 +71,6 @@ func newVault(reporter *sentry.Reporter, locations *locations.Locations, keychai
)
if key, err := loadVaultKey(vaultDir, keychains); err != nil {
if reporter != nil {
if rerr := reporter.ReportMessageWithContext("Could not load/create vault key", map[string]any{
"keychainDefaultHelper": keychains.GetDefaultHelper(),
"keychainUsableHelpersLength": len(keychains.GetHelpers()),
"error": err.Error(),
}); rerr != nil {
logrus.WithError(err).Info("Failed to report keychain issue to Sentry")
}
}
logrus.WithError(err).Error("Could not load/create vault key")
insecure = true

View File

@ -267,8 +267,6 @@ func newBridge(
unleashService := unleash.NewBridgeService(ctx, api, locator, panicHandler)
observabilityService := observability.NewService(ctx, panicHandler)
bridge := &Bridge{
vault: vault,
@ -308,11 +306,11 @@ func newBridge(
lastVersion: lastVersion,
tasks: tasks,
syncService: syncservice.NewService(panicHandler, observabilityService),
syncService: syncservice.NewService(reporter, panicHandler),
unleashService: unleashService,
observabilityService: observabilityService,
observabilityService: observability.NewService(ctx, panicHandler),
notificationStore: notifications.NewStore(locator.ProvideNotificationsCachePath),
}
@ -325,7 +323,6 @@ func newBridge(
reporter,
uidValidityGenerator,
&bridgeIMAPSMTPTelemetry{b: bridge},
observabilityService,
)
// Check whether username has changed and correct (macOS only)
@ -345,7 +342,7 @@ func newBridge(
bridge.unleashService.Run()
bridge.observabilityService.Run(bridge)
bridge.observabilityService.Run()
return bridge, nil
}
@ -636,7 +633,7 @@ func loadTLSConfig(vault *vault.Vault) (*tls.Config, error) {
}, nil
}
func min(a, b time.Duration) time.Duration { //nolint:predeclared
func min(a, b time.Duration) time.Duration {
if a < b {
return a
}
@ -713,13 +710,5 @@ func (bridge *Bridge) GetFeatureFlagValue(key string) bool {
}
func (bridge *Bridge) PushObservabilityMetric(metric proton.ObservabilityMetric) {
bridge.observabilityService.AddMetrics(metric)
}
func (bridge *Bridge) PushDistinctObservabilityMetrics(errType observability.DistinctionErrorTypeEnum, metrics ...proton.ObservabilityMetric) {
bridge.observabilityService.AddDistinctMetrics(errType, metrics...)
}
func (bridge *Bridge) ModifyObservabilityHeartbeatInterval(duration time.Duration) {
bridge.observabilityService.ModifyHeartbeatInterval(duration)
bridge.observabilityService.AddMetric(metric)
}

View File

@ -1,49 +0,0 @@
package mocks
import (
reflect "reflect"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
"github.com/golang/mock/gomock"
)
type MockObservabilitySender struct {
ctrl *gomock.Controller
recorder *MockObservabilitySenderRecorder
}
type MockObservabilitySenderRecorder struct {
mock *MockObservabilitySender
}
func NewMockObservabilitySender(ctrl *gomock.Controller) *MockObservabilitySender {
mock := &MockObservabilitySender{ctrl: ctrl}
mock.recorder = &MockObservabilitySenderRecorder{mock: mock}
return mock
}
func (m *MockObservabilitySender) EXPECT() *MockObservabilitySenderRecorder { return m.recorder }
func (m *MockObservabilitySender) AddDistinctMetrics(errType observability.DistinctionErrorTypeEnum, _ ...proton.ObservabilityMetric) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "AddDistinctMetrics", errType)
}
func (m *MockObservabilitySender) AddMetrics(metrics ...proton.ObservabilityMetric) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "AddMetrics", metrics)
}
func (mr *MockObservabilitySenderRecorder) AddDistinctMetrics(errType observability.DistinctionErrorTypeEnum, _ ...proton.ObservabilityMetric) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock,
"AddDistinctMetrics",
reflect.TypeOf((*MockObservabilitySender)(nil).AddDistinctMetrics),
errType)
}
func (mr *MockObservabilitySenderRecorder) AddMetrics(metrics ...proton.ObservabilityMetric) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddMetrics", reflect.TypeOf((*MockObservabilitySender)(nil).AddMetrics), metrics)
}

View File

@ -95,70 +95,3 @@ func TestBridge_Observability(t *testing.T) {
})
})
}
func TestBridge_Observability_Heartbeat(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
throttlePeriod := time.Millisecond * 300
observability.ModifyThrottlePeriod(throttlePeriod)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
require.NoError(t, getErr(bridge.LoginFull(ctx, username, password, nil, nil)))
bridge.ModifyObservabilityHeartbeatInterval(throttlePeriod)
require.Equal(t, 0, len(s.GetObservabilityStatistics().Metrics))
time.Sleep(time.Millisecond * 150)
require.Equal(t, 0, len(s.GetObservabilityStatistics().Metrics))
time.Sleep(time.Millisecond * 200)
require.Equal(t, 1, len(s.GetObservabilityStatistics().Metrics))
time.Sleep(time.Millisecond * 350)
require.Equal(t, 2, len(s.GetObservabilityStatistics().Metrics))
time.Sleep(time.Millisecond * 350)
require.Equal(t, 3, len(s.GetObservabilityStatistics().Metrics))
})
})
}
func TestBridge_Observability_UserMetric(t *testing.T) {
testMetric := proton.ObservabilityMetric{
Name: "test1",
Version: 1,
Timestamp: time.Now().Unix(),
Data: nil,
}
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
userMetricPeriod := time.Millisecond * 200
heartbeatPeriod := time.Second * 10
throttlePeriod := time.Millisecond * 100
observability.ModifyUserMetricInterval(userMetricPeriod)
observability.ModifyThrottlePeriod(throttlePeriod)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
require.NoError(t, getErr(bridge.LoginFull(ctx, username, password, nil, nil)))
bridge.ModifyObservabilityHeartbeatInterval(heartbeatPeriod)
time.Sleep(throttlePeriod)
require.Equal(t, 0, len(s.GetObservabilityStatistics().Metrics))
bridge.PushDistinctObservabilityMetrics(observability.SyncError, testMetric)
time.Sleep(throttlePeriod)
// We're expecting two observability metrics to be sent, the actual metric + the user metric.
require.Equal(t, 2, len(s.GetObservabilityStatistics().Metrics))
bridge.PushDistinctObservabilityMetrics(observability.SyncError, testMetric)
time.Sleep(throttlePeriod)
// We're expecting only a single metric to be sent, since the user metric update has been sent already within the predefined period.
require.Equal(t, 3, len(s.GetObservabilityStatistics().Metrics))
bridge.PushDistinctObservabilityMetrics(observability.SyncError, testMetric)
time.Sleep(throttlePeriod)
// Two metric updates should be sent again.
require.Equal(t, 5, len(s.GetObservabilityStatistics().Metrics))
bridge.PushDistinctObservabilityMetrics(observability.SyncError, testMetric)
time.Sleep(throttlePeriod)
// Only a single one should be sent.
require.Equal(t, 6, len(s.GetObservabilityStatistics().Metrics))
})
})
}

View File

@ -29,12 +29,13 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
)
func TestBridge_Report(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
defer done()
@ -55,6 +56,12 @@ func TestBridge_Report(t *testing.T) {
require.NoError(t, err)
defer func() { require.NoError(t, conn.Close()) }()
// Sending garbage to the IMAP port should cause the bridge to report it.
mocks.Reporter.EXPECT().ReportMessageWithContext(
gomock.Eq("Failed to parse IMAP command"),
gomock.Any(),
).Return(nil)
// Read lines from the IMAP port.
lineCh := liner.New(conn).Lines(func() error { return nil })

View File

@ -21,7 +21,6 @@ import (
"context"
"errors"
"github.com/ProtonMail/gluon/reporter"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
@ -116,17 +115,6 @@ func (bridge *Bridge) installUpdate(ctx context.Context, job installJob) {
err := bridge.updater.InstallUpdate(ctx, bridge.api, job.version)
switch {
case errors.Is(err, updater.ErrDownloadVerify):
// BRIDGE-207: if download or verification fails, we do not want to trigger a manual update. We report in the log and to Sentry
// and we fail silently.
log.WithError(err).Error("The update could not be installed, but we will fail silently")
if reporterErr := bridge.reporter.ReportMessageWithContext(
"Cannot download or verify update",
reporter.Context{"error": err},
); reporterErr != nil {
log.WithError(reporterErr).Error("Failed to report update error")
}
case errors.Is(err, updater.ErrUpdateAlreadyInstalled):
log.Info("The update was already installed")

View File

@ -355,9 +355,23 @@ func (bridge *Bridge) SendBadEventUserFeedback(_ context.Context, userID string,
}
if doResync {
if rerr := bridge.reporter.ReportMessageWithContext(
"Failed to handle event: feedback resync",
reporter.Context{"user_id": userID},
); rerr != nil {
logUser.WithError(rerr).Error("Failed to report feedback failure")
}
return user.BadEventFeedbackResync(ctx)
}
if rerr := bridge.reporter.ReportMessageWithContext(
"Failed to handle event: feedback logout",
reporter.Context{"user_id": userID},
); rerr != nil {
logUser.WithError(rerr).Error("Failed to report feedback failure")
}
bridge.logoutUser(ctx, user, true, false, false)
bridge.publish(events.UserLoggedOut{
@ -557,6 +571,7 @@ func (bridge *Bridge) addUserWithVault(
isNew,
bridge.notificationStore,
bridge.unleashService.GetFlagValue,
bridge.observabilityService.AddMetric,
)
if err != nil {
return fmt.Errorf("failed to create user: %w", err)

View File

@ -38,6 +38,9 @@ func (bridge *Bridge) handleUserEvent(ctx context.Context, user *user.User, even
case events.UserLoadedCheckResync:
user.VerifyResyncAndExecute()
case events.UncategorizedEventError:
bridge.handleUncategorizedErrorEvent(event)
}
}
@ -64,3 +67,12 @@ func (bridge *Bridge) handleUserBadEvent(ctx context.Context, user *user.User, e
user.OnBadEvent(ctx)
}, bridge.usersLock)
}
func (bridge *Bridge) handleUncategorizedErrorEvent(event events.UncategorizedEventError) {
if rerr := bridge.reporter.ReportMessageWithContext("Failed to handle due to uncategorized error", reporter.Context{
"error_type": internal.ErrCauseType(event.Error),
"error": event.Error,
}); rerr != nil {
logrus.WithField("pkg", "bridge/event").WithError(rerr).Error("Failed to report failed event handling")
}
}

View File

@ -96,7 +96,7 @@ func (status *ConfigurationStatus) IsPending() bool {
}
func (status *ConfigurationStatus) isPendingSinceMin() int {
if min := int(time.Since(status.Data.DataV1.PendingSince).Minutes()); min > 0 { //nolint:predeclared
if min := int(time.Since(status.Data.DataV1.PendingSince).Minutes()); min > 0 {
return min
}
return 0
@ -211,7 +211,7 @@ func (data *ConfigurationStatusData) clickedLinkToString() string {
var str = ""
var first = true
for i := 0; i < 64; i++ {
if data.hasLinkClicked(uint64(i)) { //nolint:gosec // disable G115
if data.hasLinkClicked(uint64(i)) {
if !first {
str += ","
} else {

View File

@ -364,9 +364,9 @@ grpc::Status GRPCService::RequestKnowledgeBaseSuggestions(ServerContext*, String
QList<bridgepp::KnowledgeBaseSuggestion> suggestions;
for (qsizetype i = 1; i <= 3; ++i) {
suggestions.push_back({
.url = QString("https://proton.me/support/bridge#%1").arg(i),
suggestions.push_back( {
.title = QString("Suggested link %1").arg(i),
.url = QString("https://proton.me/support/bridge#%1").arg(i),
});
}
qtProxy_.sendDelayedEvent(newKnowledgeBaseSuggestionsEvent(app().mainWindow().knowledgeBaseTab().getSuggestions()));

View File

@ -25,7 +25,6 @@
#include <bridgepp/GRPC/GRPCClient.h>
#include <bridgepp/Worker/Overseer.h>
#include "Settings.h"
#define HANDLE_EXCEPTION(x) try { x } \
catch (Exception const &e) { emit fatalError(e); } \
@ -60,19 +59,15 @@ QMLBackend::QMLBackend()
/// \param[in] serviceConfig
//****************************************************************************************************************************************************
void QMLBackend::init(GRPCConfig const &serviceConfig) {
Log &log = app().log();
log.info(QString("Connecting to gRPC service"));
trayIcon_.reset(new TrayIcon());
connect(this, &QMLBackend::trayIconVisibleChanged, trayIcon_.get(), &TrayIcon::setVisible);
log.info(QString("Tray icon is visible: %1").arg(trayIcon_->isVisible() ? "true" : "false"));
this->setNormalTrayIcon();
connect(this, &QMLBackend::fatalError, &app(), &AppController::onFatalError);
users_ = new UserList(this);
Log &log = app().log();
log.info(QString("Connecting to gRPC service"));
app().grpc().setLog(&log);
this->connectGrpcEvents();
@ -736,32 +731,6 @@ void QMLBackend::setDockIconVisible(bool visible) {
}
//****************************************************************************************************************************************************
/// \param[in] visible Should the tray icon be visible.
//****************************************************************************************************************************************************
void QMLBackend::setTrayIconVisible(bool visible) {
HANDLE_EXCEPTION(
AppController& app = ::app();
if (visible == app.settings().trayIconVisible()) {
return;
}
app.settings().setTrayIconVisible(visible);
emit trayIconVisibleChanged(visible);
app.log().info(QString("Changing tray icon visibility to %1").arg(visible ? "true" : "false"));
)
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
bool QMLBackend::trayIconVisible() const {
HANDLE_EXCEPTION_RETURN_BOOL(
return app().settings().trayIconVisible();
)
}
//****************************************************************************************************************************************************
/// \param[in] active Should we activate autostart.
//****************************************************************************************************************************************************

View File

@ -102,7 +102,6 @@ public: // Qt/QML properties. Note that the NOTIFY-er signal is required even fo
Q_PROPERTY(QVariantList bugQuestions READ bugQuestions NOTIFY bugQuestionsChanged)
Q_PROPERTY(UserList *users MEMBER users_ NOTIFY usersChanged)
Q_PROPERTY(bool dockIconVisible READ dockIconVisible WRITE setDockIconVisible NOTIFY dockIconVisibleChanged)
Q_PROPERTY(bool trayIconVisible READ trayIconVisible WRITE setTrayIconVisible NOTIFY trayIconVisibleChanged)
// Qt Property system setters & getters.
bool showOnStartup() const; ///< Getter for the 'showOnStartup' property.
@ -142,9 +141,6 @@ public: // Qt/QML properties. Note that the NOTIFY-er signal is required even fo
QVariantList bugQuestions() const; ///< Getter for the 'bugQuestions' property.
void setDockIconVisible(bool visible); ///< Setter for the 'dockIconVisible' property.
bool dockIconVisible() const;; ///< Getter for the 'dockIconVisible' property.
void setTrayIconVisible(bool visible); ///< Setter for the 'trayIconVisible' property.
bool trayIconVisible() const; ///< Getter for the 'trayIconVisible' property.
signals: // Signal used by the Qt property system. Many of them are unused but required to avoid warning from the QML engine.
void showSplashScreenChanged(bool value); ///<Signal for the change of the 'showSplashScreen' property.
@ -179,7 +175,6 @@ signals: // Signal used by the Qt property system. Many of them are unused but r
void isAutostartOnChanged(bool value); ///<Signal for the change of the 'isAutostartOn' property.
void usersChanged(UserList *users); ///<Signal for the change of the 'users' property.
void dockIconVisibleChanged(bool value); ///<Signal for the change of the 'dockIconVisible' property.
void trayIconVisibleChanged(bool value); ///< Signal for the change of the 'trayIconVisible' property.
void receivedUserNotification(bridgepp::UserNotification const& notification); ///< Signal to display the userNotification modal

View File

@ -28,7 +28,6 @@ namespace {
QString const settingsFileName = "bridge-gui.ini"; ///< The name of the settings file.
QString const keyUseSoftwareRenderer = "UseSoftwareRenderer"; ///< The key for storing the 'Use software rendering' setting.
QString const keyTrayIconVisible = "TrayIconVisible"; ///< The key for storing the 'Tray icon visible' setting.
}
@ -37,7 +36,7 @@ QString const keyTrayIconVisible = "TrayIconVisible"; ///< The key for storing t
//
//****************************************************************************************************************************************************
Settings::Settings()
: settings_(QDir(userConfigDir()).absoluteFilePath(settingsFileName), QSettings::Format::IniFormat) {
: settings_(QDir(userConfigDir()).absoluteFilePath("bridge-gui.ini"), QSettings::Format::IniFormat) {
}
@ -55,17 +54,3 @@ bool Settings::useSoftwareRenderer() const {
void Settings::setUseSoftwareRenderer(bool value) {
settings_.setValue(keyUseSoftwareRenderer, value);
}
//****************************************************************************************************************************************************
/// \param[in] value The value for the 'Tray icon visible' setting.
//****************************************************************************************************************************************************
void Settings::setTrayIconVisible(bool value) {
settings_.setValue(keyTrayIconVisible, value);
}
//****************************************************************************************************************************************************
/// \return The value for the 'Tray icon visible' setting.
//****************************************************************************************************************************************************
bool Settings::trayIconVisible() const {
return settings_.value(keyTrayIconVisible, true).toBool();
}

View File

@ -33,8 +33,6 @@ public: // member functions.
bool useSoftwareRenderer() const; ///< Get the 'Use software renderer' settings value.
void setUseSoftwareRenderer(bool value); ///< Set the 'Use software renderer' settings value.
void setTrayIconVisible(bool value); ///< Get the 'Tray icon visible' setting value.
bool trayIconVisible() const; ///< Set the 'Tray icon visible' setting value.
private: // member functions.
Settings(); ///< Default constructor.

View File

@ -21,7 +21,6 @@
#include <bridgepp/Exception/Exception.h>
#include <bridgepp/BridgeUtils.h>
#include "Settings.h"
using namespace bridgepp;
@ -196,7 +195,7 @@ TrayIcon::TrayIcon()
}
this->setIcon();
this->setVisible(app().settings().trayIconVisible());
this->show();
// TrayIcon does not expose its screen, so we connect relevant screen events to our DPI change handler.
for (QScreen *screen: QGuiApplication::screens()) {
@ -210,6 +209,7 @@ TrayIcon::TrayIcon()
connect(&iconRefreshTimer_, &QTimer::timeout, this, &TrayIcon::onIconRefreshTimer);
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
@ -322,38 +322,21 @@ void TrayIcon::setState(TrayIcon::State state, QString const &stateString, QStri
this->generateStatusIcon(statusIconPath, stateColor(state));
}
//****************************************************************************************************************************************************
/// \brief A helper struct to temporarily force the tray to be visible. Useful for operations that do not work when the tray is not visible,
/// such as showMessage().
//****************************************************************************************************************************************************
struct ScopedTrayVisibility {
explicit ScopedTrayVisibility(TrayIcon& trayIcon) : trayIcon_(trayIcon), wasVisible_(app().settings().trayIconVisible()) {
trayIcon_.setVisible(true);
}
~ScopedTrayVisibility() { trayIcon_.setVisible(wasVisible_); }
private:
TrayIcon &trayIcon_;
bool wasVisible_;
};
//****************************************************************************************************************************************************
/// \param[in] title The title.
/// \param[in] message The message.
//****************************************************************************************************************************************************
void TrayIcon::showErrorPopupNotification(QString const &title, QString const &message) {
ScopedTrayVisibility visible(*this);
this->showMessage(title, message, notificationErrorIcon_);
}
//****************************************************************************************************************************************************
/// Used only by user notifications received from the event loop
/// \param[in] title The title.
/// \param[in] subtitle The subtitle.
//****************************************************************************************************************************************************
void TrayIcon::showUserNotification(QString const &title, QString const &subtitle) {
ScopedTrayVisibility visible(*this);
this->showMessage(title, subtitle, QSystemTrayIcon::NoIcon);
}

View File

@ -43,6 +43,8 @@ public: // data members
void setState(State state, QString const& stateString, QString const &statusIconPath); ///< Set the state of the icon
void showErrorPopupNotification(QString const& title, QString const &message); ///< Display a pop up notification.
void showUserNotification(QString const& title, QString const &subtitle); ///< Display an OS pop up notification (without icon).
signals:
void selectUser(QString const& userID, bool forceShowWindow); ///< Signal for selecting a user with a given userID

View File

@ -54,7 +54,6 @@ QString const bridgeGUILock = "bridge-v3-gui.lock"; ///< The file name used for
QString const exeName = "bridge" + exeSuffix; ///< The bridge executable file name.*
qint64 constexpr grpcServiceConfigWaitDelayMs = 180000; ///< The wait delay for the gRPC config file in milliseconds.
QString const waitFlag = "--wait"; ///< The wait command-line flag.
QString const orphanInstanceException = "An orphan instance of bridge is already running. Please terminate it and relaunch the application.";
} // anonymous namespace
@ -318,7 +317,7 @@ int main(int argc, char *argv[]) {
QString bridgeExe;
if (!cliOptions.attach) {
if (isBridgeRunning()) {
throw Exception(orphanInstanceException,
throw Exception("An orphan instance of bridge is already running. Please terminate it and relaunch the application.",
QString(), __FUNCTION__, tailOfLatestBridgeLog(sessionID));
}
@ -414,18 +413,13 @@ int main(int argc, char *argv[]) {
lock.unlock();
return result;
} catch (Exception const &e) {
sentry_uuid_s const uuid = reportSentryException("Exception occurred during main", e);
QString message = e.qwhat();
if (e.showSupportLink()) {
message += R"(<br/><br/>If the issue persists, please contact our <a href="https://proton.me/support/contact">customer support</a>.)";
}
QMessageBox::critical(nullptr, "Error", message);
if (e.qwhat() != orphanInstanceException) {
sentry_uuid_s const uuid = reportSentryException("Exception occurred during main", e);
QTextStream(stderr) << "reportID: " << QByteArray(uuid.bytes, 16).toHex() << " Captured exception :"
<< e.detailedWhat() << "\n";
}
QTextStream(stderr) << "reportID: " << QByteArray(uuid.bytes, 16).toHex() << " Captured exception :" << e.detailedWhat() << "\n";
return EXIT_FAILURE;
}
}

View File

@ -86,13 +86,11 @@ Item {
}
}
Button {
id: signIn
Layout.alignment: Qt.AlignTop
colorScheme: root.colorScheme
secondary: true
text: qsTr("Sign in")
visible: root.user ? (root.user.state === EUserState.SignedOut) : false
Accessible.name: text
onClicked: {
if (user) {
@ -101,13 +99,11 @@ Item {
}
}
Button {
id: removeAccount
Layout.alignment: Qt.AlignTop
colorScheme: root.colorScheme
icon.source: "/qml/icons/ic-trash.svg"
secondary: true
visible: root.user ? root.user.state !== EUserState.Locked : false
Accessible.name: qsTr("Remove account")
onClicked: {
if (!root.user)
@ -122,7 +118,6 @@ Item {
height: root._lineThickness
}
SettingsItem {
id: configureEmailClient
Layout.fillWidth: true
actionText: qsTr("Configure email client")
colorScheme: root.colorScheme
@ -131,7 +126,6 @@ Item {
text: qsTr("Email clients")
type: SettingsItem.PrimaryButton
visible: _connected && ((!root.user.splitMode) || (root.user.addresses.length === 1))
Accessible.name: actionText
onClicked: {
if (!root.user)
@ -149,7 +143,6 @@ Item {
text: qsTr("Split addresses")
type: SettingsItem.Toggle
visible: _connected && root.user.addresses.length > 1
Accessible.name: text
onClicked: {
if (!splitMode.checked) {

View File

@ -28,7 +28,7 @@ Popup {
implicitWidth: 600 // contentLayout.implicitWidth + contentLayout.anchors.leftMargin + contentLayout.anchors.rightMargin
leftMargin: (mainWindow.width - root.implicitWidth) / 2
modal: false
popupPriority: ApplicationWindow.PopupPriority.Banner
popupType: ApplicationWindow.PopupType.Banner
shouldShow: notification ? (notification.active && !notification.dismissed) : false
topMargin: 37

View File

@ -13,7 +13,6 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import QtQuick.Controls.impl
import Proton
Item {

View File

@ -114,7 +114,6 @@ Item {
colorScheme: leftBar.colorScheme
horizontalPadding: 0
icon.source: "/qml/icons/ic-question-circle.svg"
Accessible.name: qsTr("Help")
onClicked: rightContent.showHelpView()
}
@ -131,7 +130,6 @@ Item {
colorScheme: leftBar.colorScheme
horizontalPadding: 0
icon.source: "/qml/icons/ic-cog-wheel.svg"
Accessible.name: qsTr("Settings")
onClicked: rightContent.showGeneralSettings()
}
@ -149,7 +147,6 @@ Item {
colorScheme: leftBar.colorScheme
horizontalPadding: 0
icon.source: "/qml/icons/ic-three-dots-vertical.svg"
Accessible.name: "..."
onClicked: {
dotMenu.open();
@ -322,7 +319,6 @@ Item {
horizontalPadding: 0
icon.source: "/qml/icons/ic-plus.svg"
width: 36
Accessible.name: qsTr("Add account")
onClicked: {
root.showLogin("");

View File

@ -147,19 +147,6 @@ SettingsView {
onClicked: Backend.changeColorScheme(darkMode.checked ? "light" : "dark")
}
SettingsItem {
id: trayIconVisible
Layout.fillWidth: true
checked: Backend.trayIconVisible
colorScheme: root.colorScheme
description: qsTr("Show the Bridge icon in the menu bar. When the Bridge icon is not visible, launch the " +
"application again to display the main window.")
text: qsTr("Show the Bridge icon in the menu bar")
type: SettingsItem.Toggle
visible: (Backend.goos === "darwin") && root._isAdvancedShown
onClicked: Backend.trayIconVisible = !trayIconVisible.checked
}
SettingsItem {
id: allMail
Layout.fillWidth: true

View File

@ -21,7 +21,7 @@ T.ApplicationWindow {
id: root
// popup priority based on types
enum PopupPriority {
enum PopupType {
Banner,
Dialog
}
@ -78,10 +78,10 @@ T.ApplicationWindow {
topmost = obj;
break;
}
if (topmost && (topmost.popupPriority > obj.popupPriority)) {
if (topmost && (topmost.popupType > obj.popupType)) {
continue;
}
if (topmost && (topmost.popupPriority === obj.popupPriority) && (topmost.occurred > obj.occurred)) {
if (topmost && (topmost.popupType === obj.popupType) && (topmost.occurred > obj.occurred)) {
continue;
}
topmost = obj;

View File

@ -21,7 +21,7 @@ T.Dialog {
property ColorScheme colorScheme
readonly property var occurred: shouldShow ? new Date() : undefined
readonly property int popupPriority: ApplicationWindow.PopupPriority.Dialog
readonly property int popupType: ApplicationWindow.PopupType.Dialog
property bool shouldShow: false
function close() {

View File

@ -16,7 +16,6 @@
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.impl
import QtQuick.Layouts
ColorImage {

View File

@ -12,7 +12,6 @@
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.impl
import QtQuick.Layouts
RowLayout {

View File

@ -21,7 +21,7 @@ T.Popup {
property ColorScheme colorScheme
readonly property var occurred: shouldShow ? new Date() : undefined
property int popupPriority: ApplicationWindow.PopupPriority.Banner
property int popupType: ApplicationWindow.PopupType.Banner
property bool shouldShow: false
function close() {

View File

@ -172,8 +172,6 @@ FocusScope {
implicitHeight: children[0].implicitHeight
implicitWidth: children[0].implicitWidth
Accessible.role: Accessible.Grouping
Accessible.name: label.text
onEditingFinished: {
if (!validateOnEditingFinished) {
@ -276,7 +274,6 @@ FocusScope {
selectionColor: control.palette.highlight
topPadding: 8
verticalAlignment: TextInput.AlignVCenter
Accessible.name: label.text + qsTr(" edit")
background: Item {
implicitHeight: 36
@ -352,7 +349,6 @@ FocusScope {
icon.color: control.color
icon.source: checked ? "../icons/ic-eye-slash.svg" : "../icons/ic-eye.svg"
visible: root.echoMode === TextInput.Password
Accessible.name: label.text + qsTr(" show check")
}
}
}

View File

@ -41,8 +41,6 @@ Item {
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
Accessible.name: text
Accessible.role: Accessible.Grouping
RowLayout {
anchors.fill: parent
@ -79,8 +77,6 @@ Item {
colorScheme: root.colorScheme
loading: root.loading
visible: root.type === SettingsItem.ActionType.Toggle
Accessible.role: Accessible.CheckBox
Accessible.name: root.Accessible.name + " toggle"
onClicked: {
if (!root.loading)
@ -96,8 +92,6 @@ Item {
secondary: root.type !== SettingsItem.PrimaryButton
text: root.actionText
visible: root.type === SettingsItem.Button || root.type === SettingsItem.PrimaryButton
Accessible.role: Accessible.Button
Accessible.name: root.Accessible.name + " button"
onClicked: {
if (!root.loading)

View File

@ -80,7 +80,6 @@ Item {
horizontalPadding: 8
icon.source: "/qml/icons/ic-arrow-left.svg"
secondary: true
Accessible.name: qsTr("Back")
onClicked: root.back()

View File

@ -51,7 +51,7 @@ Item {
color: colorScheme.text_weak
colorScheme: wizard.colorScheme
horizontalAlignment: Text.AlignHCenter
text: qsTr("A series of pop-ups will appear. Follow the instructions to install the profile.")
text: qsTr("A system pop-up will appear. Double click on the entry with your email, and click Install in the dialog that appears.")
type: Label.LabelType.Body
wrapMode: Text.WordWrap
}

View File

@ -15,7 +15,6 @@ import QtQml
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import QtQuick.Controls.impl
Item {
id: root

View File

@ -14,7 +14,6 @@ import QtQml
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import QtQuick.Controls.impl
import ".."
Rectangle {

View File

@ -14,7 +14,6 @@ import QtQml
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import QtQuick.Controls.impl
Rectangle {
id: root
@ -38,8 +37,6 @@ Rectangle {
}
height: 68
radius: ProtonStyle.banner_radius
Accessible.role: Accessible.Button
Accessible.name: root.text
RowLayout {
anchors.fill: parent

View File

@ -33,7 +33,6 @@ Button {
icon.source: "/qml/icons/ic-question-circle.svg"
icon.width: _iconSize
verticalPadding: 0
Accessible.name: qsTr("Help")
onClicked: {
menu.popup(-menu.width + root.width, -menu.height);

View File

@ -60,7 +60,7 @@ Item {
}
function showAppleMailAutoconfigProfileInstall() {
showClientConfigCommon();
descriptionLabel.text = qsTr("The final step before you can start using Apple Mail is to install the Bridge server profile in the system preferences.\n\nAdding a server profile is necessary to ensure that your Mac can receive and send Proton Mail messages.");
descriptionLabel.text = qsTr("The final step before you can start using Apple Mail is to install the Bridge server profile in the system preferences.\n\nAdding a server profile is necessary to ensure that your Mac can receive and send Proton Mails.");
linkLabel1.setCallback(function() { Backend.openExternalLink("https://proton.me/support/macos-certificate-warning"); }, qsTr("Why is there a yellow warning sign?"), true);
linkLabel2.setCallback(wizard.showClientParams, qsTr("Configure Apple Mail manually"), false);
}

View File

@ -14,7 +14,6 @@ import QtQml
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import QtQuick.Controls.impl
FocusScope {
id: root

View File

@ -214,10 +214,7 @@ func NewUserBadEvent(userID string, errorMessage string) *StreamEvent {
}
func NewUsedBytesChangedEvent(userID string, usedBytes uint64) *StreamEvent {
return userEvent(&UserEvent{Event: &UserEvent_UsedBytesChangedEvent{UsedBytesChangedEvent: &UsedBytesChangedEvent{
UserID: userID,
UsedBytes: int64(usedBytes), //nolint:gosec // disable G115
}}})
return userEvent(&UserEvent{Event: &UserEvent_UsedBytesChangedEvent{UsedBytesChangedEvent: &UsedBytesChangedEvent{UserID: userID, UsedBytes: int64(usedBytes)}}})
}
func newIMAPLoginFailedEvent(username string) *StreamEvent {

View File

@ -717,8 +717,8 @@ func (s *Service) MailServerSettings(_ context.Context, _ *emptypb.Empty) (*Imap
state: protoimpl.MessageState{},
sizeCache: 0,
unknownFields: nil,
ImapPort: int32(s.bridge.GetIMAPPort()), //nolint:gosec // disable G115
SmtpPort: int32(s.bridge.GetSMTPPort()), //nolint:gosec // disable G115
ImapPort: int32(s.bridge.GetIMAPPort()),
SmtpPort: int32(s.bridge.GetSMTPPort()),
UseSSLForImap: s.bridge.GetIMAPSSL(),
UseSSLForSmtp: s.bridge.GetSMTPSSL(),
}, nil
@ -864,8 +864,8 @@ func base64Decode(in []byte) ([]byte, error) {
func (s *Service) getMailServerSettings() *ImapSmtpSettings {
return &ImapSmtpSettings{
ImapPort: int32(s.bridge.GetIMAPPort()), //nolint:gosec // disable G115
SmtpPort: int32(s.bridge.GetSMTPPort()), //nolint:gosec // disable G115
ImapPort: int32(s.bridge.GetIMAPPort()),
SmtpPort: int32(s.bridge.GetSMTPPort()),
UseSSLForImap: s.bridge.GetIMAPSSL(),
UseSSLForSmtp: s.bridge.GetSMTPSSL(),
}

View File

@ -34,11 +34,6 @@ var (
// getInitials based on webapp implementation:
// https://github.com/ProtonMail/WebClients/blob/55d96a8b4afaaa4372fc5f1ef34953f2070fd7ec/packages/shared/lib/helpers/string.ts#L145
func getInitials(fullName string) string {
fullName = strings.TrimSpace(fullName)
if fullName == "" {
return "?"
}
words := strings.Split(
reMultiSpaces.ReplaceAllString(fullName, " "),
" ",
@ -71,8 +66,8 @@ func grpcUserFromInfo(user bridge.UserInfo) *User {
AvatarText: getInitials(user.Username),
State: userStateToGrpc(user.State),
SplitMode: user.AddressMode == vault.SplitMode,
UsedBytes: int64(user.UsedSpace), //nolint:gosec // disable G115
TotalBytes: int64(user.MaxSpace), //nolint:gosec // disable G115
UsedBytes: int64(user.UsedSpace),
TotalBytes: int64(user.MaxSpace),
Password: user.BridgePass,
Addresses: user.Addresses,
}

View File

@ -1,45 +0,0 @@
// Copyright (c) 2024 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package grpc
import (
"testing"
"github.com/stretchr/testify/require"
)
func Test_GetInitials(t *testing.T) {
require.Equal(t, "?", getInitials(""))
require.Equal(t, "T", getInitials(" test"))
require.Equal(t, "T", getInitials("test "))
require.Equal(t, "T", getInitials(" test "))
require.Equal(t, "JD", getInitials(" John Doe "))
require.Equal(t, "J", getInitials(" JohnDoe@proton.me "))
require.Equal(t, "JD", getInitials("\t\r\n John Doe \t\r\n "))
require.Equal(t, "T", getInitials("TestTestman"))
require.Equal(t, "TT", getInitials("Test Testman"))
require.Equal(t, "J", getInitials("JamesJoyce"))
require.Equal(t, "J", getInitials("JamesJoyceJeremy"))
require.Equal(t, "J", getInitials("james.joyce"))
require.Equal(t, "JJ", getInitials("James Joyce"))
require.Equal(t, "JM", getInitials("James Joyce Mahabharata"))
require.Equal(t, "JL", getInitials("James Joyce Jeremy Lin"))
require.Equal(t, "JM", getInitials("Jean Michel"))
require.Equal(t, "GC", getInitials("George Michael Carrie"))
}

View File

@ -99,7 +99,7 @@ func GetArticleIndex(url string) (uint64, error) {
if index == -1 {
return 0, ErrArticleNotFound
}
return uint64(index), nil //nolint:gosec // disable G115
return uint64(index), nil
}
func simplifyUserInput(input string) string {

View File

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

View File

@ -21,13 +21,18 @@
package sentry
import (
"github.com/elastic/go-sysinfo/types"
"github.com/elastic/go-sysinfo"
"golang.org/x/sys/unix"
)
const translatedProcDarwin = "sysctl.proc_translated"
func getHostArch(host types.Host) string {
func getHostArch() string {
host, err := sysinfo.Host()
if err != nil {
return "not-detected"
}
// It is not possible to retrieve real hardware architecture once using
// rosetta. But it is possible to detect the process translation if
// rosetta is used.

View File

@ -20,10 +20,12 @@
package sentry
import (
"github.com/elastic/go-sysinfo/types"
)
import "github.com/elastic/go-sysinfo"
func getHostArch(host types.Host) string {
func getHostArch() string {
host, err := sysinfo.Host()
if err != nil {
return "not-detected"
}
return host.Info().Architecture
}

View File

@ -30,13 +30,10 @@ import (
"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/elastic/go-sysinfo"
"github.com/getsentry/sentry-go"
"github.com/sirupsen/logrus"
)
const hostNotDetectedField = "not-detected"
var skippedFunctions = []string{} //nolint:gochecknoglobals
func init() { //nolint:gochecknoinits
@ -73,50 +70,17 @@ func init() { //nolint:gochecknoinits
)
}
type hostInfoData struct {
hostArch string
hostName string
hostVersion string
hostBuild string
}
func newHostInfoData() hostInfoData {
return hostInfoData{
hostArch: hostNotDetectedField,
hostName: hostNotDetectedField,
hostVersion: hostNotDetectedField,
hostBuild: hostNotDetectedField,
}
}
type Reporter struct {
appName string
appVersion string
identifier Identifier
hostInfo hostInfoData
hostArch string
}
type Identifier interface {
GetUserAgent() string
}
func getHostInfo() hostInfoData {
data := newHostInfoData()
host, err := sysinfo.Host()
if err != nil {
return data
}
data.hostArch = getHostArch(host)
osInfo := host.Info().OS
data.hostName = osInfo.Name
data.hostVersion = osInfo.Version
data.hostBuild = osInfo.Build
return data
}
func GetProtectedHostname() string {
hostname, err := os.Hostname()
if err != nil {
@ -136,7 +100,7 @@ func NewReporter(appName string, identifier Identifier) *Reporter {
appName: appName,
appVersion: constants.Revision,
identifier: identifier,
hostInfo: getHostInfo(),
hostArch: getHostArch(),
}
}
@ -188,14 +152,11 @@ func (r *Reporter) scopedReport(context map[string]interface{}, doReport func())
}
tags := map[string]string{
"OS": runtime.GOOS,
"Client": r.appName,
"Version": r.appVersion,
"UserAgent": r.identifier.GetUserAgent(),
"HostArch": r.hostInfo.hostArch,
"HostName": r.hostInfo.hostName,
"HostVersion": r.hostInfo.hostVersion,
"HostBuild": r.hostInfo.hostBuild,
"OS": runtime.GOOS,
"Client": r.appName,
"Version": r.appVersion,
"UserAgent": r.identifier.GetUserAgent(),
"HostArch": r.hostArch,
}
sentry.WithScope(func(scope *sentry.Scope) {

View File

@ -70,8 +70,6 @@ type Connector struct {
syncState *SyncState
}
var errNoSenderAddressMatch = errors.New("no matching sender found in address list")
func NewConnector(
addrID string,
apiClient APIClient,
@ -110,7 +108,6 @@ func NewConnector(
labels: labels,
addressMode: addressMode,
log: logrus.WithFields(logrus.Fields{
"pkg": "imapservice",
"gluon-connector": addressMode,
"addr-id": addrID,
"user-id": userID,
@ -680,18 +677,22 @@ func (s *Connector) importMessage(
) (imap.Message, []byte, error) {
var full proton.FullMessage
// addr is primary for combined mode or active for split mode
addr, ok := s.identityState.GetAddress(s.addrID)
if !ok {
return imap.Message{}, nil, fmt.Errorf("could not find address")
}
p, err2 := parser.New(bytes.NewReader(literal))
if err2 != nil {
return imap.Message{}, nil, fmt.Errorf("failed to parse literal: %w", err2)
}
isDraft := slices.Contains(labelIDs, proton.DraftsLabel)
addr, err := s.getImportAddress(p, isDraft)
if err != nil {
return imap.Message{}, nil, err
}
if err := s.identityState.WithAddrKR(addr.ID, func(_, addrKR *crypto.KeyRing) error {
s.reportGODT3185(isDraft, addr.Email, p, s.addressMode == usertypes.AddressModeCombined)
if err := s.identityState.WithAddrKR(s.addrID, func(_, addrKR *crypto.KeyRing) error {
primaryKey, errKey := addrKR.FirstKey()
if errKey != nil {
return fmt.Errorf("failed to get primary key for import: %w", errKey)
@ -718,7 +719,7 @@ func (s *Connector) importMessage(
}
str, err := s.client.ImportMessages(ctx, primaryKey, 1, 1, []proton.ImportReq{{
Metadata: proton.ImportMetadata{
AddressID: addr.ID,
AddressID: s.addrID,
LabelIDs: labelIDs,
Unread: proton.Bool(unread),
Flags: flags,
@ -877,74 +878,79 @@ func equalAddresses(a, b string) bool {
return strings.EqualFold(stripPlusAlias(a), stripPlusAlias(b))
}
func (s *Connector) getImportAddress(p *parser.Parser, isDraft bool) (proton.Address, error) {
// addr is primary for combined mode or active for split mode
address, ok := s.identityState.GetAddress(s.addrID)
if !ok {
return proton.Address{}, errors.New("could not find account address")
func (s *Connector) reportGODT3185(isDraft bool, defaultAddr string, p *parser.Parser, isCombinedMode bool) {
reportAction := "draft"
if !isDraft {
reportAction = "import"
}
inCombinedMode := s.addressMode == usertypes.AddressModeCombined
if !inCombinedMode {
return address, nil
reportMode := "combined"
if !isCombinedMode {
reportMode = "split"
}
senderAddr, err := s.getSenderProtonAddress(p)
if err != nil {
if !errors.Is(err, errNoSenderAddressMatch) {
s.log.WithError(err).Warn("Could not get import address")
senderAddr := ""
if p != nil && p.Root() != nil && p.Root().Header.Len() != 0 {
addrField := p.Root().Header.Get("From")
if addrField == "" {
addrField = p.Root().Header.Get("Sender")
}
if addrField != "" {
sender, err := rfc5322.ParseAddressList(addrField)
if err == nil && len(sender) > 0 {
senderAddr = sender[0].Address
} else {
s.log.WithError(err).Warn("Invalid sender address in reporter")
}
}
}
if equalAddresses(defaultAddr, senderAddr) {
return
}
isDisabled := false
isUserAddress := false
for _, a := range s.identityState.GetAddresses() {
if !equalAddresses(a.Email, senderAddr) {
continue
}
// We did not find a match, so we use the default address.
return address, nil
isUserAddress = true
isDisabled = !bool(a.Send) || (a.Status != proton.AddressStatusEnabled)
break
}
if senderAddr.ID == address.ID {
return address, nil
if !isUserAddress && senderAddr != "" {
return
}
// GODT-3185 / BRIDGE-120 In combined mode, in certain cases we adapt the address used for encryption.
// - draft with non-default address in combined mode: using sender address
// - import with non-default address in combined mode: using sender address
// - import with non-default disabled address in combined mode: using sender address
reportResult := "using sender address"
isSenderAddressDisabled := (!bool(senderAddr.Send)) || (senderAddr.Status != proton.AddressStatusEnabled)
if isDraft && isSenderAddressDisabled {
return address, nil
if !isCombinedMode {
reportResult = "error address not match"
}
return senderAddr, nil
}
func (s *Connector) getSenderProtonAddress(p *parser.Parser) (proton.Address, error) {
// Step 1: extract sender email address from message
if (p == nil) || (p.Root() == nil) || (p.Root().Header.Len() == 0) {
return proton.Address{}, errors.New("invalid message encountered while trying to extract sender address")
}
addrField := p.Root().Header.Get("From")
if len(addrField) == 0 {
addrField = p.Root().Header.Get("Sender")
}
if len(addrField) == 0 {
return proton.Address{}, errors.New("no sender found in message headers")
}
sender, err := rfc5322.ParseAddressList(addrField)
if (err != nil) || (len(sender) == 0) {
return proton.Address{}, fmt.Errorf("invalid sender address in message: %w", err)
}
addrStr := sender[0].Address
// Step 2: match email with the user address list.
addressList := s.identityState.GetAddresses()
index := slices.IndexFunc(addressList, func(a proton.Address) bool {
return equalAddresses(a.Email, addrStr)
})
if index < 0 {
return proton.Address{}, errNoSenderAddressMatch
}
return addressList[index], nil
reportAddress := ""
if senderAddr == "" {
reportAddress = " invalid"
reportResult = "error import/draft"
}
if isDisabled {
reportAddress = " disabled"
if isDraft {
reportResult = "error draft"
}
}
report := fmt.Sprintf(
"GODT-3185: %s with non-default%s address in %s mode: %s",
reportAction, reportAddress, reportMode, reportResult,
)
s.log.Warn(report)
if s.reporter != nil {
_ = s.reporter.ReportMessage(report)
}
}

View File

@ -1,67 +0,0 @@
// Copyright (c) 2024 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package evtloopmsgevents
import (
"time"
"github.com/ProtonMail/go-proton-api"
)
const (
messageEventErrorCaseSchemaName = "bridge_event_loop_message_event_failures_total"
messageEventErrorCaseSchemaVersion = 1
)
func generateMessageEventFailureObservabilityMetric(eventType string) proton.ObservabilityMetric {
return proton.ObservabilityMetric{
Name: messageEventErrorCaseSchemaName,
Version: messageEventErrorCaseSchemaVersion,
Timestamp: time.Now().Unix(),
Data: map[string]interface{}{
"Value": 1,
"Labels": map[string]string{
"eventType": eventType,
},
},
}
}
func GenerateMessageEventFailureCreateMessageMetric() proton.ObservabilityMetric {
return generateMessageEventFailureObservabilityMetric("createMessageEvent")
}
func GenerateMessageEventFailureDeleteMessageMetric() proton.ObservabilityMetric {
return generateMessageEventFailureObservabilityMetric("deleteMessageEvent")
}
func GenerateMessageEventFailureUpdateMetric() proton.ObservabilityMetric {
return generateMessageEventFailureObservabilityMetric("updateEvent")
}
func GenerateMessageEventFailedToBuildMessage() proton.ObservabilityMetric {
return generateMessageEventFailureObservabilityMetric("failedToBuildMessage")
}
func GenerateMessageEventFailedToBuildDraft() proton.ObservabilityMetric {
return generateMessageEventFailureObservabilityMetric("failedToBuildDraft")
}
func GenerateMessageEventUpdateChannelDoesNotExist() proton.ObservabilityMetric {
return generateMessageEventFailureObservabilityMetric("messageUpdateChannelDoesNotExist")
}

View File

@ -1,51 +0,0 @@
// Copyright (c) 2024 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package syncmsgevents
import (
"time"
"github.com/ProtonMail/go-proton-api"
)
const (
syncEventErrorCaseSchemaName = "bridge_sync_message_event_failures_total"
syncEventErrorCaseSchemaVersion = 1
)
func generateSyncEventFailureObservabilityMetric(eventType string) proton.ObservabilityMetric {
return proton.ObservabilityMetric{
Name: syncEventErrorCaseSchemaName,
Version: syncEventErrorCaseSchemaVersion,
Timestamp: time.Now().Unix(),
Data: map[string]interface{}{
"Value": 1,
"Labels": map[string]string{
"eventType": eventType,
},
},
}
}
func GenerateSyncFailureCreateMessageEventMetric() proton.ObservabilityMetric {
return generateSyncEventFailureObservabilityMetric("createMessageEvent")
}
func GenerateSyncFailureDeleteMessageEventMetric() proton.ObservabilityMetric {
return generateSyncEventFailureObservabilityMetric("deleteMessageEvent")
}

View File

@ -30,7 +30,6 @@ import (
"github.com/ProtonMail/gluon/watcher"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
"github.com/ProtonMail/proton-bridge/v3/internal/services/orderedtasks"
"github.com/ProtonMail/proton-bridge/v3/internal/services/sendrecorder"
"github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice"
@ -97,8 +96,6 @@ type Service struct {
syncConfigPath string
lastHandledEventID string
isSyncing atomic.Bool
observabilitySender observability.Sender
}
func NewService(
@ -119,7 +116,6 @@ func NewService(
syncConfigDir string,
maxSyncMemory uint64,
showAllMail bool,
observabilitySender observability.Sender,
) *Service {
subscriberName := fmt.Sprintf("imap-%v", identityState.User.ID)
@ -164,8 +160,6 @@ func NewService(
syncMessageBuilder: syncMessageBuilder,
syncReporter: syncReporter,
syncConfigPath: GetSyncConfigPath(syncConfigDir, identityState.User.ID),
observabilitySender: observabilitySender,
}
}

View File

@ -26,11 +26,11 @@ import (
"github.com/ProtonMail/gluon"
"github.com/ProtonMail/gluon/imap"
"github.com/ProtonMail/gluon/reporter"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/v3/internal"
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
obsMetrics "github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice/observabilitymetrics/evtloopmsgevents"
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
"github.com/ProtonMail/proton-bridge/v3/internal/usertypes"
"github.com/sirupsen/logrus"
"golang.org/x/exp/maps"
@ -46,7 +46,7 @@ func (s *Service) HandleMessageEvents(ctx context.Context, events []proton.Messa
case proton.EventCreate:
updates, err := onMessageCreated(logging.WithLogrusField(ctx, "action", "create message"), s, event.Message, false)
if err != nil {
s.observabilitySender.AddDistinctMetrics(observability.EventLoopError, obsMetrics.GenerateMessageEventFailureCreateMessageMetric())
reportError(s.reporter, s.log, "Failed to apply create message event", err)
return fmt.Errorf("failed to handle create message event: %w", err)
}
@ -64,7 +64,7 @@ func (s *Service) HandleMessageEvents(ctx context.Context, events []proton.Messa
event,
)
if err != nil {
s.observabilitySender.AddDistinctMetrics(observability.EventLoopError, obsMetrics.GenerateMessageEventFailureUpdateMetric())
reportError(s.reporter, s.log, "Failed to apply update draft message event", err)
return fmt.Errorf("failed to handle update draft event: %w", err)
}
@ -85,7 +85,7 @@ func (s *Service) HandleMessageEvents(ctx context.Context, events []proton.Messa
event.Message,
)
if err != nil {
s.observabilitySender.AddDistinctMetrics(observability.EventLoopError, obsMetrics.GenerateMessageEventFailureUpdateMetric())
reportError(s.reporter, s.log, "Failed to apply update message event", err)
return fmt.Errorf("failed to handle update message event: %w", err)
}
@ -113,7 +113,6 @@ func (s *Service) HandleMessageEvents(ctx context.Context, events []proton.Messa
)
if err := waitOnIMAPUpdates(ctx, updates); err != nil {
s.observabilitySender.AddDistinctMetrics(observability.EventLoopError, obsMetrics.GenerateMessageEventFailureDeleteMessageMetric())
return fmt.Errorf("failed to handle delete message event in gluon: %w", err)
}
}
@ -159,7 +158,8 @@ func onMessageCreated(
s.log.WithError(err).Error("Failed to add failed message ID to vault")
}
s.observabilitySender.AddDistinctMetrics(observability.EventLoopError, obsMetrics.GenerateMessageEventFailedToBuildMessage())
reportErrorAndMessageID(s.reporter, s.log, "Failed to build message (event create)", res.err, res.messageID)
return nil
}
@ -221,7 +221,8 @@ func onMessageUpdateDraftOrSent(ctx context.Context, s *Service, event proton.Me
s.log.WithError(err).Error("Failed to add failed message ID to vault")
}
s.observabilitySender.AddDistinctMetrics(observability.EventLoopError, obsMetrics.GenerateMessageEventFailedToBuildDraft())
reportErrorAndMessageID(s.reporter, s.log, "Failed to build draft message (event update)", res.err, res.messageID)
return nil
}
@ -298,6 +299,24 @@ func onMessageDeleted(ctx context.Context, s *Service, event proton.MessageEvent
return updates
}
func reportError(r reporter.Reporter, entry *logrus.Entry, title string, err error) {
reportErrorNoContextCancel(r, entry, title, err, reporter.Context{})
}
func reportErrorAndMessageID(r reporter.Reporter, entry *logrus.Entry, title string, err error, messgeID string) {
reportErrorNoContextCancel(r, entry, title, err, reporter.Context{"messageID": messgeID})
}
func reportErrorNoContextCancel(r reporter.Reporter, entry *logrus.Entry, title string, err error, reportContext reporter.Context) {
if !errors.Is(err, context.Canceled) {
reportContext["error"] = err
reportContext["error_type"] = internal.ErrCauseType(err)
if rerr := r.ReportMessageWithContext(title, reportContext); rerr != nil {
entry.WithError(err).WithField("title", title).Error("Failed to report message")
}
}
}
// safePublishMessageUpdate handles the rare case where the address' update channel may have been deleted in the same
// event. This rare case can take place if in the same event fetch request there is an update for delete address and
// create/update message.
@ -322,7 +341,7 @@ func safePublishMessageUpdate(ctx context.Context, s *Service, addressID string,
}
logrus.Warnf("Update channel not found for address %v, it may have been already deleted", addressID)
s.observabilitySender.AddDistinctMetrics(observability.EventLoopError, obsMetrics.GenerateMessageEventUpdateChannelDoesNotExist())
_ = s.reporter.ReportMessage("Message Update channel does not exist")
return false, nil
}

View File

@ -23,8 +23,6 @@ import (
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
obsMetrics "github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice/observabilitymetrics/syncmsgevents"
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
"github.com/ProtonMail/proton-bridge/v3/internal/services/userevents"
)
@ -57,7 +55,7 @@ func (s syncMessageEventHandler) HandleMessageEvents(ctx context.Context, events
true,
)
if err != nil {
s.service.observabilitySender.AddDistinctMetrics(observability.SyncError, obsMetrics.GenerateSyncFailureCreateMessageEventMetric())
reportError(s.service.reporter, s.service.log, "Failed to apply create message event", err)
return fmt.Errorf("failed to handle create message event: %w", err)
}
@ -73,7 +71,6 @@ func (s syncMessageEventHandler) HandleMessageEvents(ctx context.Context, events
)
if err := waitOnIMAPUpdates(ctx, updates); err != nil {
s.service.observabilitySender.AddDistinctMetrics(observability.SyncError, obsMetrics.GenerateSyncFailureDeleteMessageEventMetric())
return fmt.Errorf("failed to handle delete message event in gluon: %w", err)
}
default:

View File

@ -36,7 +36,6 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/files"
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
"github.com/sirupsen/logrus"
)
@ -78,7 +77,6 @@ func newIMAPServer(
tasks *async.Group,
uidValidityGenerator imap.UIDValidityGenerator,
panicHandler async.PanicHandler,
observabilitySender observability.Sender,
) (*gluon.Server, error) {
gluonCacheDir = ApplyGluonCachePathSuffix(gluonCacheDir)
gluonConfigDir = ApplyGluonConfigPathSuffix(gluonConfigDir)
@ -123,7 +121,6 @@ func newIMAPServer(
gluon.WithReporter(reporter),
gluon.WithUIDValidityGenerator(uidValidityGenerator),
gluon.WithPanicHandler(panicHandler),
gluon.WithObservabilitySender(observability.NewAdapter(observabilitySender), int(observability.GluonImapError), int(observability.GluonMessageError), int(observability.GluonOtherError)),
)
if err != nil {
return nil, err
@ -156,9 +153,9 @@ func newIMAPServer(
func getGluonVersionInfo(version *semver.Version) gluon.Option {
return gluon.WithVersionInfo(
int(version.Major()), //nolint:gosec // disable G115
int(version.Minor()), //nolint:gosec // disable G115
int(version.Patch()), //nolint:gosec // disable G115
int(version.Major()),
int(version.Minor()),
int(version.Patch()),
constants.FullAppName,
"TODO",
"TODO",

View File

@ -31,7 +31,6 @@ import (
"github.com/ProtonMail/gluon/reporter"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice"
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
bridgesmtp "github.com/ProtonMail/proton-bridge/v3/internal/services/smtp"
"github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice"
"github.com/ProtonMail/proton-bridge/v3/pkg/cpc"
@ -61,8 +60,6 @@ type Service struct {
uidValidityGenerator imap.UIDValidityGenerator
telemetry Telemetry
observabilitySender observability.Sender
}
func NewService(
@ -74,7 +71,6 @@ func NewService(
reporter reporter.Reporter,
uidValidityGenerator imap.UIDValidityGenerator,
telemetry Telemetry,
observabilitySender observability.Sender,
) *Service {
return &Service{
requests: cpc.NewCPC(),
@ -89,8 +85,6 @@ func NewService(
tasks: async.NewGroup(ctx, panicHandler),
uidValidityGenerator: uidValidityGenerator,
telemetry: telemetry,
observabilitySender: observabilitySender,
}
}
@ -455,7 +449,6 @@ func (sm *Service) createIMAPServer(ctx context.Context) (*gluon.Server, error)
sm.tasks,
sm.uidValidityGenerator,
sm.panicHandler,
sm.observabilitySender,
)
if err == nil {
sm.eventPublisher.PublishEvent(ctx, events.IMAPServerCreated{})

View File

@ -44,16 +44,15 @@ type Service struct {
store *Store
getFlagValueFn unleash.GetFlagValueFn
observabilitySender observability.Sender
getFlagValueFn unleash.GetFlagValueFn
pushObservabilityMetricFn observability.PushObsMetricFn
}
const bitfieldRegexPattern = `^\\\d+`
const disableNotificationsKillSwitch = "InboxBridgeEventLoopNotificationDisabled"
func NewService(userID string, service userevents.Subscribable, eventPublisher events.EventPublisher, store *Store,
getFlagFn unleash.GetFlagValueFn, observabilitySender observability.Sender) *Service {
getFlagFn unleash.GetFlagValueFn, pushMetricFn observability.PushObsMetricFn) *Service {
return &Service{
userID: userID,
@ -69,8 +68,8 @@ func NewService(userID string, service userevents.Subscribable, eventPublisher e
store: store,
getFlagValueFn: getFlagFn,
observabilitySender: observabilitySender,
getFlagValueFn: getFlagFn,
pushObservabilityMetricFn: pushMetricFn,
}
}
@ -111,7 +110,7 @@ func (s *Service) HandleNotificationEvents(ctx context.Context, notificationEven
s.log.Debug("Handling notification events")
// Publish observability metrics that we've received notifications
s.observabilitySender.AddMetrics(GenerateReceivedMetric(len(notificationEvents)))
s.pushObservabilityMetricFn(GenerateReceivedMetric(len(notificationEvents)))
for _, event := range notificationEvents {
ctx = logging.WithLogrusField(ctx, "notificationID", event.ID)
@ -134,7 +133,7 @@ func (s *Service) HandleNotificationEvents(ctx context.Context, notificationEven
Subtitle: event.Payload.Subtitle, Body: event.Payload.Body})
// Publish observability metric that we've successfully processed notifications
s.observabilitySender.AddMetrics(GenerateProcessedMetric(1))
s.pushObservabilityMetricFn(GenerateProcessedMetric(1))
}
default:

View File

@ -109,7 +109,7 @@ func (s *Store) readCache() {
file, err := os.Open(s.cacheFilepath)
if err != nil {
s.log.WithError(err).Info("Unable to open cache file")
s.log.WithError(err).Error("Unable to open cache file")
return
}

View File

@ -1,93 +0,0 @@
// Copyright (c) 2024 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package observability
import (
"github.com/ProtonMail/go-proton-api"
)
type Adapter struct {
sender Sender
}
func NewAdapter(sender Sender) *Adapter {
return &Adapter{sender: sender}
}
// VerifyAndParseGenericMetrics parses a metric provided as an interface into a proton.ObservabilityMetric type.
// It's exported as it is also used in integration tests.
func VerifyAndParseGenericMetrics(metric map[string]interface{}) (bool, proton.ObservabilityMetric) {
name, ok := metric["Name"].(string)
if !ok {
return false, proton.ObservabilityMetric{}
}
version, ok := metric["Version"].(int)
if !ok {
return false, proton.ObservabilityMetric{}
}
timestamp, ok := metric["Timestamp"].(int64)
if !ok {
return false, proton.ObservabilityMetric{}
}
data, ok := metric["Data"]
if !ok {
return false, proton.ObservabilityMetric{}
}
return true, proton.ObservabilityMetric{
Name: name,
Version: version,
Timestamp: timestamp,
Data: data,
}
}
func (adapter *Adapter) AddMetrics(metrics ...map[string]interface{}) {
var typedMetrics []proton.ObservabilityMetric
for _, metric := range metrics {
if ok, m := VerifyAndParseGenericMetrics(metric); ok {
typedMetrics = append(typedMetrics, m)
}
}
if len(typedMetrics) > 0 {
adapter.sender.AddMetrics(typedMetrics...)
}
}
func (adapter *Adapter) AddDistinctMetrics(errType interface{}, metrics ...map[string]interface{}) {
errTypeInt, ok := errType.(int)
if !ok {
return
}
var typedMetrics []proton.ObservabilityMetric
for _, metric := range metrics {
if ok, m := VerifyAndParseGenericMetrics(metric); ok {
typedMetrics = append(typedMetrics, m)
}
}
if len(typedMetrics) > 0 {
adapter.sender.AddDistinctMetrics(DistinctionErrorTypeEnum(errTypeInt), typedMetrics...)
}
}

View File

@ -1,58 +0,0 @@
// Copyright (c) 2024 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package observability
import (
"fmt"
"testing"
"time"
"github.com/stretchr/testify/require"
)
func Test_AdapterCustomMetrics(t *testing.T) {
customMetric := map[string]interface{}{
"Name": "name",
"Version": 1,
"Timestamp": time.Now().Unix(),
"Data": map[string]interface{}{
"Value": 1,
"Labels": map[string]string{
"error": "customError",
},
},
}
ok, metric := VerifyAndParseGenericMetrics(customMetric)
require.True(t, ok)
require.Equal(t, metric.Name, customMetric["Name"])
require.Equal(t, metric.Timestamp, customMetric["Timestamp"])
require.Equal(t, metric.Version, customMetric["Version"])
require.Equal(t, metric.Data, customMetric["Data"])
}
func Test_AdapterGluonMetrics(t *testing.T) {
metrics := GenerateAllGluonMetrics()
for _, metric := range metrics {
ok, m := VerifyAndParseGenericMetrics(metric)
fmt.Println(m)
require.True(t, ok)
}
}

View File

@ -1,55 +0,0 @@
// Copyright (c) 2024 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package observability
import "time"
// DistinctionErrorTypeEnum - maps to the specific error schema for which we
// want to send a user update.
type DistinctionErrorTypeEnum int
const (
SyncError DistinctionErrorTypeEnum = iota
GluonImapError
GluonMessageError
GluonOtherError
SMTPError
EventLoopError // EventLoopError - should always be kept last when inserting new keys.
)
// errorSchemaMap - maps between the DistinctionErrorTypeEnum and the relevant schema name.
var errorSchemaMap = map[DistinctionErrorTypeEnum]string{ //nolint:gochecknoglobals
SyncError: "bridge_sync_errors_users_total",
EventLoopError: "bridge_event_loop_events_errors_users_total",
GluonImapError: "bridge_gluon_imap_errors_users_total",
GluonMessageError: "bridge_gluon_message_errors_users_total",
SMTPError: "bridge_smtp_errors_users_total",
GluonOtherError: "bridge_gluon_other_errors_users_total",
}
// createLastSentMap - needs to be updated whenever we make changes to the enum.
func createLastSentMap() map[DistinctionErrorTypeEnum]time.Time {
registerTime := time.Now().Add(-updateInterval)
lastSentMap := make(map[DistinctionErrorTypeEnum]time.Time)
for errType := SyncError; errType <= EventLoopError; errType++ {
lastSentMap[errType] = registerTime
}
return lastSentMap
}

View File

@ -1,170 +0,0 @@
// Copyright (c) 2024 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package observability
import (
"context"
"sync"
"time"
"github.com/ProtonMail/gluon/async"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
)
var updateInterval = time.Minute * 5 //nolint:gochecknoglobals
type observabilitySender interface {
addMetricsIfClients(metric ...proton.ObservabilityMetric)
}
// distinctionUtility - used to discern whether X number of events stem from Y number of users.
type distinctionUtility struct {
ctx context.Context
panicHandler async.PanicHandler
lastSentMap map[DistinctionErrorTypeEnum]time.Time // Ensures we don't step over the limit of one user update every 5 mins.
observabilitySender observabilitySender
settingsGetter settingsGetter
userPlanUnsafe string
userPlanLock sync.Mutex
heartbeatData heartbeatData
heartbeatDataLock sync.Mutex
heartbeatTicker *time.Ticker
}
func newDistinctionUtility(ctx context.Context, panicHandler async.PanicHandler, observabilitySender observabilitySender) *distinctionUtility {
distinctionUtility := &distinctionUtility{
ctx: ctx,
panicHandler: panicHandler,
lastSentMap: createLastSentMap(),
observabilitySender: observabilitySender,
userPlanUnsafe: planUnknown,
heartbeatData: heartbeatData{},
heartbeatTicker: time.NewTicker(updateInterval),
}
return distinctionUtility
}
// sendMetricsWithGuard - schedules the metrics to be sent only if there are authenticated clients.
func (d *distinctionUtility) sendMetricsWithGuard(metrics ...proton.ObservabilityMetric) {
if d.observabilitySender == nil {
return
}
d.observabilitySender.addMetricsIfClients(metrics...)
}
func (d *distinctionUtility) setSettingsGetter(getter settingsGetter) {
d.settingsGetter = getter
}
// checkAndUpdateLastSentMap - checks whether we have sent a relevant user update metric
// within the last 5 minutes.
func (d *distinctionUtility) checkAndUpdateLastSentMap(key DistinctionErrorTypeEnum) bool {
curTime := time.Now()
val, ok := d.lastSentMap[key]
if !ok {
d.lastSentMap[key] = curTime
return true
}
if val.Add(updateInterval).Before(curTime) {
d.lastSentMap[key] = curTime
return true
}
return false
}
// generateUserMetric creates the relevant user update metric based on its type
// and the relevant settings. In the future this will need to be expanded to support multiple
// versions of the metric if we ever decide to change them.
func (d *distinctionUtility) generateUserMetric(
metricType DistinctionErrorTypeEnum,
) proton.ObservabilityMetric {
schemaName, ok := errorSchemaMap[metricType]
if !ok {
return proton.ObservabilityMetric{}
}
return generateUserMetric(schemaName, d.getUserPlanSafe(),
d.getEmailClientUserAgent(),
getEnabled(d.getProxyAllowed()),
getEnabled(d.getBetaAccessEnabled()),
)
}
func generateUserMetric(schemaName, plan, mailClient, dohEnabled, betaAccess string) proton.ObservabilityMetric {
return proton.ObservabilityMetric{
Name: schemaName,
Version: 1,
Timestamp: time.Now().Unix(),
Data: map[string]interface{}{
"Value": 1,
"Labels": map[string]string{
"plan": plan,
"mailClient": mailClient,
"dohEnabled": dohEnabled,
"betaAccessEnabled": betaAccess,
},
},
}
}
func (d *distinctionUtility) generateDistinctMetrics(errType DistinctionErrorTypeEnum, metrics ...proton.ObservabilityMetric) []proton.ObservabilityMetric {
d.updateHeartbeatData(errType)
if d.checkAndUpdateLastSentMap(errType) {
metrics = append(metrics, d.generateUserMetric(errType))
}
return metrics
}
func (d *distinctionUtility) getEmailClientUserAgent() string {
ua := ""
if d.settingsGetter != nil {
ua = d.settingsGetter.GetCurrentUserAgent()
}
return matchUserAgent(ua)
}
func (d *distinctionUtility) getBetaAccessEnabled() bool {
if d.settingsGetter == nil {
return false
}
return d.settingsGetter.GetUpdateChannel() == updater.EarlyChannel
}
func (d *distinctionUtility) getProxyAllowed() bool {
if d.settingsGetter == nil {
return false
}
return d.settingsGetter.GetProxyAllowed()
}

View File

@ -1,127 +0,0 @@
// Copyright (c) 2024 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package observability
import (
"fmt"
"time"
"github.com/ProtonMail/gluon/async"
"github.com/ProtonMail/go-proton-api"
)
const genericHeartbeatSchemaName = "bridge_generic_user_heartbeat_total"
const genericHeartbeatVersion = 2
type heartbeatData struct {
receivedSyncError bool
receivedEventLoopError bool
receivedOtherError bool
receivedGluonError bool
}
func (d *distinctionUtility) resetHeartbeatData() {
d.heartbeatData.receivedSyncError = false
d.heartbeatData.receivedOtherError = false
d.heartbeatData.receivedEventLoopError = false
d.heartbeatData.receivedGluonError = false
}
func (d *distinctionUtility) updateHeartbeatData(errType DistinctionErrorTypeEnum) {
d.withUpdateHeartbeatDataLock(func() {
//nolint:exhaustive
switch errType {
case SyncError:
d.heartbeatData.receivedSyncError = true
case EventLoopError:
d.heartbeatData.receivedEventLoopError = true
case GluonMessageError, GluonImapError, GluonOtherError:
d.heartbeatData.receivedGluonError = true
}
})
}
func (d *distinctionUtility) runHeartbeat() {
go func() {
defer async.HandlePanic(d.panicHandler)
defer d.heartbeatTicker.Stop()
for {
select {
case <-d.ctx.Done():
return
case <-d.heartbeatTicker.C:
d.sendHeartbeat()
}
}
}()
}
func (d *distinctionUtility) withUpdateHeartbeatDataLock(fn func()) {
d.heartbeatDataLock.Lock()
defer d.heartbeatDataLock.Unlock()
fn()
}
// sendHeartbeat - will only send a heartbeat if there is an authenticated client
// otherwise we might end up polluting the cache and therefore our metrics.
func (d *distinctionUtility) sendHeartbeat() {
d.withUpdateHeartbeatDataLock(func() {
d.sendMetricsWithGuard(d.generateHeartbeatUserMetric())
d.resetHeartbeatData()
})
}
func formatBool(value bool) string {
return fmt.Sprintf("%t", value)
}
// generateHeartbeatUserMetric creates the heartbeat user metric and includes the relevant data.
func (d *distinctionUtility) generateHeartbeatUserMetric() proton.ObservabilityMetric {
return generateHeartbeatMetric(
d.getUserPlanSafe(),
d.getEmailClientUserAgent(),
getEnabled(d.settingsGetter.GetProxyAllowed()),
getEnabled(d.getBetaAccessEnabled()),
formatBool(d.heartbeatData.receivedOtherError),
formatBool(d.heartbeatData.receivedSyncError),
formatBool(d.heartbeatData.receivedEventLoopError),
formatBool(d.heartbeatData.receivedGluonError),
)
}
func generateHeartbeatMetric(plan, mailClient, dohEnabled, betaAccess, otherError, syncError, eventLoopError, gluonError string) proton.ObservabilityMetric {
return proton.ObservabilityMetric{
Name: genericHeartbeatSchemaName,
Version: genericHeartbeatVersion,
Timestamp: time.Now().Unix(),
Data: map[string]interface{}{
"Value": 1,
"Labels": map[string]string{
"plan": plan,
"mailClient": mailClient,
"dohEnabled": dohEnabled,
"betaAccessEnabled": betaAccess,
"receivedOtherError": otherError,
"receivedSyncError": syncError,
"receivedEventLoopError": eventLoopError,
"receivedGluonError": gluonError,
},
},
}
}

View File

@ -1,121 +0,0 @@
// Copyright (c) 2024 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package observability
import (
"context"
"strings"
"github.com/ProtonMail/gluon/async"
"github.com/ProtonMail/go-proton-api"
)
const (
planUnknown = "unknown"
planOther = "other"
planBusiness = "business"
planIndividual = "individual"
planGroup = "group"
)
var planHierarchy = map[string]int{ //nolint:gochecknoglobals
planBusiness: 4,
planGroup: 3,
planIndividual: 2,
planOther: 1,
planUnknown: 0,
}
type planGetter interface {
GetOrganizationData(ctx context.Context) (proton.OrganizationResponse, error)
}
func isHigherPriority(currentPlan, newPlan string) bool {
newRank, ok := planHierarchy[newPlan]
if !ok {
return false
}
currentRank, ok2 := planHierarchy[currentPlan]
if !ok2 {
return true // we don't have a valid plan, might as well replace it
}
return newRank > currentRank
}
func mapUserPlan(planName string) string {
if planName == "" {
return planUnknown
}
switch strings.TrimSpace(strings.ToLower(planName)) {
case "mail2022":
return planIndividual
case "bundle2022":
return planIndividual
case "family2022":
return planGroup
case "visionary2022":
return planGroup
case "mailpro2022":
return planBusiness
case "planbiz2024":
return planBusiness
case "bundlepro2022":
return planBusiness
case "bundlepro2024":
return planBusiness
case "duo2024":
return planGroup
default:
return planOther
}
}
func (d *distinctionUtility) setUserPlan(planName string) {
if planName == "" {
return
}
d.userPlanLock.Lock()
defer d.userPlanLock.Unlock()
userPlanMapped := mapUserPlan(planName)
if isHigherPriority(d.userPlanUnsafe, userPlanMapped) {
d.userPlanUnsafe = userPlanMapped
}
}
func (d *distinctionUtility) registerUserPlan(ctx context.Context, getter planGetter, panicHandler async.PanicHandler) {
go func() {
defer async.HandlePanic(panicHandler)
orgRes, err := getter.GetOrganizationData(ctx)
if err != nil {
return
}
d.setUserPlan(orgRes.Organization.PlanName)
}()
}
func (d *distinctionUtility) getUserPlanSafe() string {
d.userPlanLock.Lock()
defer d.userPlanLock.Unlock()
return d.userPlanUnsafe
}

View File

@ -36,18 +36,13 @@ const (
maxBatchSize = 1000
)
type PushObsMetricFn func(metric proton.ObservabilityMetric)
type client struct {
isTelemetryEnabled func(context.Context) bool
sendMetrics func(context.Context, proton.ObservabilityBatch) error
}
// Sender - interface maps to the observability service methods,
// so we can easily pass them down to relevant components.
type Sender interface {
AddMetrics(metrics ...proton.ObservabilityMetric)
AddDistinctMetrics(errType DistinctionErrorTypeEnum, metrics ...proton.ObservabilityMetric)
}
type Service struct {
ctx context.Context
cancel context.CancelFunc
@ -67,8 +62,6 @@ type Service struct {
userClientStore map[string]*client
userClientStoreLock sync.Mutex
distinctionUtility *distinctionUtility
}
func NewService(ctx context.Context, panicHandler async.PanicHandler) *Service {
@ -92,19 +85,11 @@ func NewService(ctx context.Context, panicHandler async.PanicHandler) *Service {
userClientStore: make(map[string]*client),
}
service.distinctionUtility = newDistinctionUtility(ctx, panicHandler, service)
return service
}
// Run starts the observability service goroutine.
// The function also sets some utility functions to a helper struct aimed at differentiating the amount of users sending metric updates.
func (s *Service) Run(settingsGetter settingsGetter) {
func (s *Service) Run() {
s.log.Info("Starting service")
s.distinctionUtility.setSettingsGetter(settingsGetter)
s.distinctionUtility.runHeartbeat()
go func() {
s.start()
}()
@ -215,7 +200,7 @@ func (s *Service) scheduleDispatch() {
}()
}
func (s *Service) addMetrics(metric ...proton.ObservabilityMetric) {
func (s *Service) AddMetric(metric proton.ObservabilityMetric) {
s.withMetricStoreLock(func() {
metricStoreLength := len(s.metricStore)
if metricStoreLength >= maxStorageSize {
@ -224,32 +209,12 @@ func (s *Service) addMetrics(metric ...proton.ObservabilityMetric) {
dropCount := metricStoreLength - maxStorageSize + 1
s.metricStore = s.metricStore[dropCount:]
}
s.metricStore = append(s.metricStore, metric...)
s.metricStore = append(s.metricStore, metric)
})
// If the context has been cancelled i.e. the service has been stopped then we should be free to exit.
if s.ctx.Err() != nil {
return
}
s.sendSignal(s.signalDataArrived)
}
// addMetricsIfClients - will append a metric only if there are authenticated clients
// via which we can reach the endpoint.
func (s *Service) addMetricsIfClients(metric ...proton.ObservabilityMetric) {
hasClients := false
s.withUserClientStoreLock(func() {
hasClients = len(s.userClientStore) > 0
})
if !hasClients {
return
}
s.addMetrics(metric...)
}
func (s *Service) RegisterUserClient(userID string, protonClient *proton.Client, telemetryService *telemetry.Service) {
s.log.Info("Registering user client, ID:", userID)
@ -260,8 +225,6 @@ func (s *Service) RegisterUserClient(userID string, protonClient *proton.Client,
}
})
s.distinctionUtility.registerUserPlan(s.ctx, protonClient, s.panicHandler)
// There may be a case where we already have metric updates stored, so try to flush;
s.sendSignal(s.signalDataArrived)
}
@ -316,25 +279,3 @@ func (s *Service) sendSignal(channel chan struct{}) {
func ModifyThrottlePeriod(duration time.Duration) {
throttleDuration = duration
}
func (s *Service) AddMetrics(metrics ...proton.ObservabilityMetric) {
s.addMetrics(metrics...)
}
// AddDistinctMetrics - sends an additional metric related to the user, so we can determine
// what number of events come from what number of users.
// As the binning interval is what allows us to do this we
// should not send these if there are no logged-in users at that moment.
func (s *Service) AddDistinctMetrics(errType DistinctionErrorTypeEnum, metrics ...proton.ObservabilityMetric) {
metrics = s.distinctionUtility.generateDistinctMetrics(errType, metrics...)
s.addMetricsIfClients(metrics...)
}
// ModifyHeartbeatInterval - should only be used for testing. Resets the heartbeat ticker.
func (s *Service) ModifyHeartbeatInterval(duration time.Duration) {
s.distinctionUtility.heartbeatTicker.Reset(duration)
}
func ModifyUserMetricInterval(duration time.Duration) {
updateInterval = duration
}

View File

@ -1,127 +0,0 @@
// Copyright (c) 2024 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package observability
import (
gluonMetrics "github.com/ProtonMail/gluon/observability/metrics"
"github.com/ProtonMail/go-proton-api"
)
func GenerateAllUsedDistinctionMetricPermutations() []proton.ObservabilityMetric {
planValues := []string{
planUnknown,
planOther,
planBusiness,
planIndividual,
planGroup}
mailClientValues := []string{
emailAgentAppleMail,
emailAgentOutlook,
emailAgentThunderbird,
emailAgentOther,
emailAgentUnknown,
}
enabledValues := []string{
getEnabled(true), getEnabled(false),
}
var metrics []proton.ObservabilityMetric
for _, schemaName := range errorSchemaMap {
for _, plan := range planValues {
for _, mailClient := range mailClientValues {
for _, dohEnabled := range enabledValues {
for _, betaAccess := range enabledValues {
metrics = append(metrics, generateUserMetric(schemaName, plan, mailClient, dohEnabled, betaAccess))
}
}
}
}
}
return metrics
}
func GenerateAllHeartbeatMetricPermutations() []proton.ObservabilityMetric {
planValues := []string{
planUnknown,
planOther,
planBusiness,
planIndividual,
planGroup}
mailClientValues := []string{
emailAgentAppleMail,
emailAgentOutlook,
emailAgentThunderbird,
emailAgentOther,
emailAgentUnknown,
}
enabledValues := []string{
getEnabled(true), getEnabled(false),
}
trueFalseValues := []string{
"true", "false",
}
var metrics []proton.ObservabilityMetric
for _, plan := range planValues {
for _, mailClient := range mailClientValues {
for _, dohEnabled := range enabledValues {
for _, betaAccess := range enabledValues {
for _, receivedOtherError := range trueFalseValues {
for _, receivedSyncError := range trueFalseValues {
for _, receivedEventLoopError := range trueFalseValues {
for _, receivedGluonError := range trueFalseValues {
metrics = append(metrics,
generateHeartbeatMetric(plan,
mailClient,
dohEnabled,
betaAccess,
receivedOtherError,
receivedSyncError,
receivedEventLoopError,
receivedGluonError,
),
)
}
}
}
}
}
}
}
}
return metrics
}
func GenerateAllGluonMetrics() []map[string]interface{} {
var metrics []map[string]interface{}
metrics = append(metrics,
gluonMetrics.GenerateFailedParseIMAPCommandMetric(),
gluonMetrics.GenerateFailedToCreateMailbox(),
gluonMetrics.GenerateFailedToDeleteMailboxMetric(),
gluonMetrics.GenerateFailedToCopyMessagesMetric(),
gluonMetrics.GenerateFailedToMoveMessagesFromMailboxMetric(),
gluonMetrics.GenerateFailedToRemoveDeletedMessagesMetric(),
gluonMetrics.GenerateFailedToCommitDatabaseTransactionMetric(),
gluonMetrics.GenerateAppendToDraftsMustNotReturnExistingRemoteID(),
gluonMetrics.GenerateDatabaseMigrationFailed(),
gluonMetrics.GenerateFailedToStoreFlagsOnMessages(),
)
return metrics
}

View File

@ -1,68 +0,0 @@
// Copyright (c) 2024 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package observability
import (
"strings"
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
)
// settingsGetter - interface that maps to bridge object methods such that we
// can pass the whole object instead of individual function callbacks.
type settingsGetter interface {
GetCurrentUserAgent() string
GetProxyAllowed() bool
GetUpdateChannel() updater.Channel
}
// User agent mapping.
const (
emailAgentAppleMail = "apple_mail"
emailAgentOutlook = "outlook"
emailAgentThunderbird = "thunderbird"
emailAgentOther = "other"
emailAgentUnknown = "unknown"
)
func matchUserAgent(userAgent string) string {
if userAgent == "" {
return emailAgentUnknown
}
userAgent = strings.ToLower(userAgent)
switch {
case strings.Contains(userAgent, "outlook"):
return emailAgentOutlook
case strings.Contains(userAgent, "thunderbird"):
return emailAgentThunderbird
case strings.Contains(userAgent, "mac") && strings.Contains(userAgent, "mail"):
return emailAgentAppleMail
case strings.Contains(userAgent, "mac") && strings.Contains(userAgent, "notes"):
return emailAgentUnknown
default:
return emailAgentOther
}
}
func getEnabled(value bool) string {
if !value {
return "disabled"
}
return "enabled"
}

View File

@ -1,111 +0,0 @@
// Copyright (c) 2024 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package observability
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestMatchUserAgent(t *testing.T) {
type agentParseResult struct {
agent string
result string
}
testCases := []agentParseResult{
{
agent: "Microsoft Outlook/16.0.17928.20114 (Windows 11 Version 23H2)",
result: emailAgentOutlook,
},
{
agent: "Mailbird/3.0.18.0 (Windows 11 Version 23H2)",
result: emailAgentOther,
},
{
agent: "Microsoft Outlook/16.0.17830.20166 (Windows 11 Version 23H2)",
result: emailAgentOutlook,
},
{
agent: "Mac OS X Mail/16.0-3776.700.51 (macOS 14.6)",
result: emailAgentAppleMail,
},
{
agent: "/ (Windows 11 Version 23H2)",
result: emailAgentOther,
},
{
agent: "Microsoft Outlook for Mac/16.88.0-BUILDDAY (macOS 14.6)",
result: emailAgentOutlook,
},
{
agent: "/ (macOS 14.5)",
result: emailAgentOther,
},
{
agent: "/ (Freedesktop SDK 23.08 (Flatpak runtime))",
result: emailAgentOther,
},
{
agent: "Mac OS X Mail/16.0-3774.600.62 (macOS 14.5)",
result: emailAgentAppleMail,
},
{
agent: "Mac OS X Notes/4.11-2817 (macOS 14.6)",
result: emailAgentUnknown,
},
{
agent: "NoClient/0.0.1 (macOS 14.6)",
result: emailAgentOther,
},
{
agent: "Thunderbird/115.15.0 (Ubuntu 20.04.6 LTS)",
result: emailAgentThunderbird,
},
{
agent: "Thunderbird/115.14.0 (macOS 14.6)",
result: emailAgentThunderbird,
},
{
agent: "Thunderbird/115.10.2 (Windows 11 Version 23H2)",
result: emailAgentThunderbird,
},
{
agent: "Mac OS X Notes/4.9-1965 (macOS Monterey (12.0))",
result: emailAgentUnknown,
},
{
agent: " Thunderbird/115.14.0 (macOS 14.6) ",
result: emailAgentThunderbird,
},
{
agent: "",
result: emailAgentUnknown,
},
}
for _, testCase := range testCases {
require.Equal(t, testCase.result, matchUserAgent(testCase.agent))
}
}
func TestFormatBool(t *testing.T) {
require.Equal(t, "false", formatBool(false))
require.Equal(t, "true", formatBool(true))
}

View File

@ -1,90 +0,0 @@
// Copyright (c) 2024 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package observabilitymetrics
import (
"time"
"github.com/ProtonMail/go-proton-api"
)
const (
smtpErrorsSchemaName = "bridge_smtp_errors_total"
smtpErrorsSchemaVersion = 1
smtpSendSuccessSchemaName = "bridge_smtp_send_success_total"
smtpSendSuccessSchemaVersion = 1
)
func generateSMTPErrorObservabilityMetric(errorType string) proton.ObservabilityMetric {
return proton.ObservabilityMetric{
Name: smtpErrorsSchemaName,
Version: smtpErrorsSchemaVersion,
Timestamp: time.Now().Unix(),
Data: map[string]interface{}{
"Value": 1,
"Labels": map[string]string{
"errorType": errorType,
},
},
}
}
func GenerateFailedGetParentID() proton.ObservabilityMetric {
return generateSMTPErrorObservabilityMetric("failedGetParentId")
}
func GenerateUnsupportedMIMEType() proton.ObservabilityMetric {
return generateSMTPErrorObservabilityMetric("unsupportedMIMEType")
}
func GenerateFailedCreateDraft() proton.ObservabilityMetric {
return generateSMTPErrorObservabilityMetric("failedToCreateDraft")
}
func GenerateFailedCreateAttachments() proton.ObservabilityMetric {
return generateSMTPErrorObservabilityMetric("failedCreateAttachments")
}
func GenerateFailedToGetRecipients() proton.ObservabilityMetric {
return generateSMTPErrorObservabilityMetric("failedGetRecipients")
}
func GenerateFailedCreatePackages() proton.ObservabilityMetric {
return generateSMTPErrorObservabilityMetric("failedCreatePackages")
}
func GenerateFailedSendDraft() proton.ObservabilityMetric {
return generateSMTPErrorObservabilityMetric("failedSendDraft")
}
func GenerateFailedDeleteFromDrafts() proton.ObservabilityMetric {
return generateSMTPErrorObservabilityMetric("failedDeleteFromDrafts")
}
func GenerateSMTPSendSuccess() proton.ObservabilityMetric {
return proton.ObservabilityMetric{
Name: smtpSendSuccessSchemaName,
Version: smtpSendSuccessSchemaVersion,
Timestamp: time.Now().Unix(),
Data: map[string]interface{}{
"Value": 1,
"Labels": map[string]string{},
},
}
}

View File

@ -29,7 +29,6 @@ import (
"github.com/ProtonMail/gluon/reporter"
"github.com/ProtonMail/go-proton-api"
bridgelogging "github.com/ProtonMail/proton-bridge/v3/internal/logging"
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
"github.com/ProtonMail/proton-bridge/v3/internal/services/orderedtasks"
"github.com/ProtonMail/proton-bridge/v3/internal/services/sendrecorder"
"github.com/ProtonMail/proton-bridge/v3/internal/services/userevents"
@ -64,8 +63,6 @@ type Service struct {
addressMode usertypes.AddressMode
serverManager ServerManager
observabilitySender observability.Sender
}
func NewService(
@ -81,7 +78,6 @@ func NewService(
mode usertypes.AddressMode,
identityState *useridentity.State,
serverManager ServerManager,
observabilitySender observability.Sender,
) *Service {
subscriberName := fmt.Sprintf("smpt-%v", userID)
@ -107,8 +103,6 @@ func NewService(
addressMode: mode,
serverManager: serverManager,
observabilitySender: observabilitySender,
}
}

View File

@ -30,14 +30,13 @@ import (
"time"
"github.com/ProtonMail/gluon/async"
"github.com/ProtonMail/gluon/reporter"
"github.com/ProtonMail/gluon/rfc5322"
"github.com/ProtonMail/gluon/rfc822"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
"github.com/ProtonMail/proton-bridge/v3/internal/services/sendrecorder"
"github.com/ProtonMail/proton-bridge/v3/internal/services/smtp/observabilitymetrics"
"github.com/ProtonMail/proton-bridge/v3/internal/usertypes"
"github.com/ProtonMail/proton-bridge/v3/pkg/message"
"github.com/ProtonMail/proton-bridge/v3/pkg/message/parser"
@ -168,10 +167,6 @@ func (s *Service) smtpSendMail(ctx context.Context, authID string, from string,
// If the message was successfully sent, we can update the message ID in the record.
s.log.Debug("Message sent successfully, signaling recorder")
// Send SMTP success observability metric
s.observabilitySender.AddMetrics(observabilitymetrics.GenerateSMTPSendSuccess())
s.recorder.SignalMessageSent(hash, srID, sent.ID)
return nil
@ -202,7 +197,13 @@ func (s *Service) sendWithKey(
}
parentID, draftsToDelete, err := getParentID(ctx, s.client, authAddrID, addrMode, references)
if err != nil {
s.observabilitySender.AddDistinctMetrics(observability.SMTPError, observabilitymetrics.GenerateFailedGetParentID())
if err := s.reporter.ReportMessageWithContext("Failed to get parent ID", reporter.Context{
"error": err,
"references": message.References,
}); err != nil {
logrus.WithError(err).Error("Failed to report error")
}
s.log.WithError(err).Warn("Failed to get parent ID")
}
@ -217,7 +218,6 @@ func (s *Service) sendWithKey(
decBody = string(message.PlainBody)
default:
s.observabilitySender.AddDistinctMetrics(observability.SMTPError, observabilitymetrics.GenerateUnsupportedMIMEType())
return proton.Message{}, fmt.Errorf("unsupported MIME type: %v", message.MIMEType)
}
@ -234,38 +234,32 @@ func (s *Service) sendWithKey(
ExternalID: message.ExternalID,
})
if err != nil {
s.observabilitySender.AddDistinctMetrics(observability.SMTPError, observabilitymetrics.GenerateFailedCreateDraft())
return proton.Message{}, fmt.Errorf("failed to create draft: %w", err)
}
attKeys, err := s.createAttachments(ctx, s.client, addrKR, draft.ID, message.Attachments)
if err != nil {
s.observabilitySender.AddDistinctMetrics(observability.SMTPError, observabilitymetrics.GenerateFailedCreateAttachments())
return proton.Message{}, fmt.Errorf("failed to create attachments: %w", err)
}
recipients, err := s.getRecipients(ctx, s.client, userKR, settings, draft)
if err != nil {
s.observabilitySender.AddDistinctMetrics(observability.SMTPError, observabilitymetrics.GenerateFailedToGetRecipients())
return proton.Message{}, fmt.Errorf("failed to get recipients: %w", err)
}
req, err := createSendReq(addrKR, message.MIMEBody, message.RichBody, message.PlainBody, recipients, attKeys)
if err != nil {
s.observabilitySender.AddDistinctMetrics(observability.SMTPError, observabilitymetrics.GenerateFailedCreatePackages())
return proton.Message{}, fmt.Errorf("failed to create packages: %w", err)
}
res, err := s.client.SendDraft(ctx, draft.ID, req)
if err != nil {
s.observabilitySender.AddDistinctMetrics(observability.SMTPError, observabilitymetrics.GenerateFailedSendDraft())
return proton.Message{}, fmt.Errorf("failed to send draft: %w", err)
}
// Only delete the drafts, if any, after message was successfully sent.
if len(draftsToDelete) != 0 {
if err := s.client.DeleteMessage(ctx, draftsToDelete...); err != nil {
s.observabilitySender.AddDistinctMetrics(observability.SMTPError, observabilitymetrics.GenerateFailedDeleteFromDrafts())
s.log.WithField("ids", draftsToDelete).WithError(err).Errorf("Failed to delete requested messages from Drafts")
}
}

View File

@ -160,6 +160,10 @@ func (s *childJob) onError(err error) {
s.job.onError(err)
}
func (s *childJob) userID() string {
return s.job.userID
}
func (s *childJob) chunkDivide(chunks [][]proton.FullMessage) []childJob {
numChunks := len(chunks)

View File

@ -1,67 +0,0 @@
// Copyright (c) 2024 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package observabilitymetrics
import (
"time"
"github.com/ProtonMail/go-proton-api"
)
const (
errorCaseSchemaName = "bridge_sync_message_build_errors_total"
errorCaseSchemaVersion = 1
successCaseSchemaName = "bridge_sync_message_build_success_total"
successCaseSchemaVersion = 1
)
func generateStageBuildFailureObservabilityMetric(errorType string) proton.ObservabilityMetric {
return proton.ObservabilityMetric{
Name: errorCaseSchemaName,
Version: errorCaseSchemaVersion,
Timestamp: time.Now().Unix(),
Data: map[string]interface{}{
"Value": 1,
"Labels": map[string]string{
"errorType": errorType,
},
},
}
}
func GenerateNoUnlockedKeyringMetric() proton.ObservabilityMetric {
return generateStageBuildFailureObservabilityMetric("noUnlockedKeyring")
}
func GenerateFailedToBuildMetric() proton.ObservabilityMetric {
return generateStageBuildFailureObservabilityMetric("failedToBuild")
}
// GenerateMessageBuiltSuccessMetric - Maybe this is incorrect, I'm not sure how metrics with no labels
// should be dealt with. The integration tests will tell us.
func GenerateMessageBuiltSuccessMetric() proton.ObservabilityMetric {
return proton.ObservabilityMetric{
Name: successCaseSchemaName,
Version: successCaseSchemaVersion,
Timestamp: time.Now().Unix(),
Data: map[string]interface{}{
"Value": 1,
"Labels": map[string]string{},
},
}
}

View File

@ -21,7 +21,7 @@ import (
"context"
"github.com/ProtonMail/gluon/async"
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
"github.com/ProtonMail/gluon/reporter"
)
// Service which mediates IMAP syncing in Bridge.
@ -36,9 +36,8 @@ type Service struct {
group *async.Group
}
func NewService(
func NewService(reporter reporter.Reporter,
panicHandler async.PanicHandler,
observabilitySender observability.Sender,
) *Service {
limits := newSyncLimits(2 * Gigabyte)
@ -51,7 +50,7 @@ func NewService(
limits: limits,
metadataStage: NewMetadataStage(metaCh, downloadCh, limits.DownloadRequestMem, panicHandler),
downloadStage: NewDownloadStage(downloadCh, buildCh, limits.MaxParallelDownloads, panicHandler),
buildStage: NewBuildStage(buildCh, applyCh, limits.MessageBuildMem, panicHandler, observabilitySender),
buildStage: NewBuildStage(buildCh, applyCh, limits.MessageBuildMem, panicHandler, reporter),
applyStage: NewApplyStage(applyCh),
metaCh: metaCh,
group: async.NewGroup(context.Background(), panicHandler),

View File

@ -26,10 +26,9 @@ import (
"github.com/ProtonMail/gluon/async"
"github.com/ProtonMail/gluon/logging"
"github.com/ProtonMail/gluon/reporter"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
obsMetrics "github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice/observabilitymetrics"
"github.com/bradenaw/juniper/parallel"
"github.com/bradenaw/juniper/xslices"
"github.com/sirupsen/logrus"
@ -51,10 +50,8 @@ type BuildStage struct {
maxBuildMem uint64
panicHandler async.PanicHandler
reporter reporter.Reporter
log *logrus.Entry
// Observability
observabilitySender observability.Sender
}
func NewBuildStage(
@ -62,15 +59,15 @@ func NewBuildStage(
output BuildStageOutput,
maxBuildMem uint64,
panicHandler async.PanicHandler,
observabilitySender observability.Sender,
reporter reporter.Reporter,
) *BuildStage {
return &BuildStage{
input: input,
output: output,
maxBuildMem: maxBuildMem,
log: logrus.WithField("sync-stage", "build"),
panicHandler: panicHandler,
observabilitySender: observabilitySender,
input: input,
output: output,
maxBuildMem: maxBuildMem,
log: logrus.WithField("sync-stage", "build"),
panicHandler: panicHandler,
reporter: reporter,
}
}
@ -114,7 +111,7 @@ func (b *BuildStage) run(ctx context.Context) {
messages: nil,
}); err != nil {
err = fmt.Errorf("failed to produce output for next stage: %w", err)
logrus.Error(err.Error())
logrus.Errorf(err.Error())
req.job.onError(err)
}
@ -150,24 +147,35 @@ func (b *BuildStage) run(ctx context.Context) {
req.job.log.WithError(err).Error("Failed to add failed message ID")
}
b.observabilitySender.AddDistinctMetrics(observability.SyncError, obsMetrics.GenerateNoUnlockedKeyringMetric())
if err := b.reporter.ReportMessageWithContext("Failed to build message - no unlocked keyring (sync)", reporter.Context{
"messageID": msg.ID,
"userID": req.userID(),
}); err != nil {
req.job.log.WithError(err).Error("Failed to report message build error")
}
return BuildResult{}, nil
}
res, err := req.job.messageBuilder.BuildMessage(req.job.labels, msg, kr, new(bytes.Buffer))
if err != nil {
req.job.log.WithError(err).WithField("msgID", msg.ID).Error("Failed to build message (sync)")
req.job.log.WithError(err).WithField("msgID", msg.ID).Error("Failed to build message (syn)")
if err := req.job.state.AddFailedMessageID(req.getContext(), msg.ID); err != nil {
req.job.log.WithError(err).Error("Failed to add failed message ID")
}
b.observabilitySender.AddDistinctMetrics(observability.SyncError, obsMetrics.GenerateFailedToBuildMetric())
if err := b.reporter.ReportMessageWithContext("Failed to build message (sync)", reporter.Context{
"messageID": msg.ID,
"error": err,
"userID": req.userID(),
}); err != nil {
req.job.log.WithError(err).Error("Failed to report message build error")
}
// We could sync a placeholder message here, but for now we skip it entirely.
return BuildResult{}, nil
}
b.observabilitySender.AddMetrics(obsMetrics.GenerateMessageBuiltSuccessMetric())
return res, nil
})
if err != nil {
@ -217,7 +225,7 @@ func chunkSyncBuilderBatch(batch []proton.FullMessage, maxMemory uint64) [][]pro
for _, v := range batch {
var dataSize uint64
for _, a := range v.Attachments {
dataSize += uint64(a.Size) //nolint:gosec // disable G115
dataSize += uint64(a.Size)
}
// 2x increase for attachment due to extra memory needed for decrypting and writing

View File

@ -24,11 +24,10 @@ import (
"github.com/ProtonMail/gluon/async"
"github.com/ProtonMail/gluon/imap"
"github.com/ProtonMail/gluon/reporter"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/v3/internal/bridge/mocks"
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
obsMetrics "github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice/observabilitymetrics"
"github.com/bradenaw/juniper/xslices"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
@ -68,6 +67,7 @@ func TestBuildStage_SuccessRemovesFailedMessage(t *testing.T) {
input := NewChannelConsumerProducer[BuildRequest]()
output := NewChannelConsumerProducer[ApplyRequest]()
reporter := mocks.NewMockReporter(mockCtrl)
labels := getTestLabels()
@ -105,10 +105,7 @@ func TestBuildStage_SuccessRemovesFailedMessage(t *testing.T) {
tj.messageBuilder.EXPECT().BuildMessage(gomock.Eq(labels), gomock.Eq(msg), gomock.Any(), gomock.Any()).Return(buildResult, nil)
tj.state.EXPECT().RemFailedMessageID(gomock.Any(), gomock.Eq("MSG"))
observabilityService := mocks.NewMockObservabilitySender(mockCtrl)
observabilityService.EXPECT().AddMetrics(obsMetrics.GenerateMessageBuiltSuccessMetric())
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, observabilityService)
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, reporter)
go func() {
stage.run(ctx)
@ -128,7 +125,7 @@ func TestBuildStage_BuildFailureIsReportedButDoesNotCancelJob(t *testing.T) {
input := NewChannelConsumerProducer[BuildRequest]()
output := NewChannelConsumerProducer[ApplyRequest]()
mockObservabilityService := mocks.NewMockObservabilitySender(mockCtrl)
mockReporter := mocks.NewMockReporter(mockCtrl)
labels := getTestLabels()
@ -159,12 +156,15 @@ func TestBuildStage_BuildFailureIsReportedButDoesNotCancelJob(t *testing.T) {
tj.messageBuilder.EXPECT().BuildMessage(gomock.Eq(labels), gomock.Eq(msg), gomock.Any(), gomock.Any()).Return(BuildResult{}, buildError)
tj.state.EXPECT().AddFailedMessageID(gomock.Any(), gomock.Eq([]string{"MSG"}))
mockReporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Eq(reporter.Context{
"userID": "u",
"messageID": "MSG",
"error": buildError,
})).Return(nil)
tj.syncReporter.EXPECT().OnProgress(gomock.Any(), gomock.Eq(int64(10)))
mockObservabilityService.EXPECT().AddDistinctMetrics(observability.SyncError, obsMetrics.GenerateNoUnlockedKeyringMetric())
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, mockObservabilityService)
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, mockReporter)
go func() {
stage.run(ctx)
@ -183,6 +183,7 @@ func TestBuildStage_FailedToLocateKeyRingIsReportedButDoesNotFailBuild(t *testin
input := NewChannelConsumerProducer[BuildRequest]()
output := NewChannelConsumerProducer[ApplyRequest]()
mockReporter := mocks.NewMockReporter(mockCtrl)
labels := getTestLabels()
@ -208,13 +209,14 @@ func TestBuildStage_FailedToLocateKeyRingIsReportedButDoesNotFailBuild(t *testin
tj.job.end()
tj.state.EXPECT().AddFailedMessageID(gomock.Any(), gomock.Eq([]string{"MSG"}))
mockReporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Eq(reporter.Context{
"userID": "u",
"messageID": "MSG",
})).Return(nil)
tj.syncReporter.EXPECT().OnProgress(gomock.Any(), gomock.Eq(int64(10)))
observabilitySender := mocks.NewMockObservabilitySender(mockCtrl)
observabilitySender.EXPECT().AddDistinctMetrics(observability.SyncError)
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, observabilitySender)
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, mockReporter)
go func() {
stage.run(ctx)
@ -233,6 +235,7 @@ func TestBuildStage_OtherErrorsFailJob(t *testing.T) {
input := NewChannelConsumerProducer[BuildRequest]()
output := NewChannelConsumerProducer[ApplyRequest]()
mockReporter := mocks.NewMockReporter(mockCtrl)
labels := getTestLabels()
@ -258,7 +261,7 @@ func TestBuildStage_OtherErrorsFailJob(t *testing.T) {
childJob := tj.job.newChildJob("f", 10)
tj.job.end()
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, mocks.NewMockObservabilitySender(mockCtrl))
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, mockReporter)
go func() {
stage.run(ctx)
@ -280,6 +283,7 @@ func TestBuildStage_CancelledJobIsDiscarded(t *testing.T) {
input := NewChannelConsumerProducer[BuildRequest]()
output := NewChannelConsumerProducer[ApplyRequest]()
mockReporter := mocks.NewMockReporter(mockCtrl)
msg := proton.FullMessage{
Message: proton.Message{
@ -290,7 +294,7 @@ func TestBuildStage_CancelledJobIsDiscarded(t *testing.T) {
},
}
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, mocks.NewMockObservabilitySender(mockCtrl))
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, mockReporter)
ctx, cancel := context.WithCancel(context.Background())
@ -323,6 +327,7 @@ func TestTask_EmptyInputDoesNotCrash(t *testing.T) {
input := NewChannelConsumerProducer[BuildRequest]()
output := NewChannelConsumerProducer[ApplyRequest]()
reporter := mocks.NewMockReporter(mockCtrl)
labels := getTestLabels()
@ -335,7 +340,7 @@ func TestTask_EmptyInputDoesNotCrash(t *testing.T) {
childJob := tj.job.newChildJob("f", 10)
tj.job.end()
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, mocks.NewMockObservabilitySender(mockCtrl))
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, reporter)
go func() {
stage.run(ctx)

View File

@ -231,7 +231,7 @@ func downloadAttachment(ctx context.Context, cache *DownloadCache, client APICli
}
type DownloadRateModifier interface {
Apply(wasSuccess bool, current int, max int) int //nolint:predeclared
Apply(wasSuccess bool, current int, max int) int
}
func autoDownloadRate[T any, R any](
@ -285,7 +285,7 @@ func autoDownloadRate[T any, R any](
type DefaultDownloadRateModifier struct{}
func (d DefaultDownloadRateModifier) Apply(wasSuccess bool, current int, max int) int { //nolint:predeclared
func (d DefaultDownloadRateModifier) Apply(wasSuccess bool, current int, max int) int {
if !wasSuccess {
return 2
}

View File

@ -217,7 +217,7 @@ func (m *metadataIterator) Next(maxDownloadMem uint64, metadataPageSize int, max
}
for idx, meta := range m.remaining {
nextSize := m.expectedSize + uint64(meta.Size) //nolint:gosec // disable G115
nextSize := m.expectedSize + uint64(meta.Size)
if nextSize >= maxDownloadMem || len(m.downloadReqIDs) >= maxMessages {
m.expectedSize = 0
m.remaining = m.remaining[idx:]

View File

@ -105,10 +105,10 @@ func (s *Service) CheckAuth(ctx context.Context, email string, password []byte)
func (s *Service) HandleUsedSpaceEvent(ctx context.Context, newSpace int64) error {
s.log.Info("Handling User Space Changed event")
if s.identity.OnUserSpaceChanged(uint64(newSpace)) { //nolint:gosec // disable G115
if s.identity.OnUserSpaceChanged(uint64(newSpace)) {
s.eventPublisher.PublishEvent(ctx, events.UsedSpaceChanged{
UserID: s.identity.User.ID,
UsedSpace: uint64(newSpace), //nolint:gosec // disable G115
UsedSpace: uint64(newSpace),
})
}

View File

@ -111,7 +111,7 @@ func (s *Service) readCacheFile() {
file, err := os.Open(s.cacheFilepath)
if err != nil {
s.log.WithError(err).Info("Unable to open cache file")
s.log.WithError(err).Error("Unable to open cache file")
return
}

View File

@ -101,7 +101,7 @@ func (u *Updater) InstallUpdate(ctx context.Context, downloader Downloader, upda
update.Package+".sig",
)
if err != nil {
return fmt.Errorf("%w: %w", ErrDownloadVerify, err)
return ErrDownloadVerify
}
if err := u.installer.InstallUpdate(update.Version, bytes.NewReader(b)); err != nil {

View File

@ -115,6 +115,7 @@ func New(
isNew bool,
notificationStore *notifications.Store,
getFlagValFn unleash.GetFlagValueFn,
pushObservabilityMetric observability.PushObsMetricFn,
) (*User, error) {
user, err := newImpl(
ctx,
@ -136,6 +137,7 @@ func New(
isNew,
notificationStore,
getFlagValFn,
pushObservabilityMetric,
)
if err != nil {
// Cleanup any pending resources on error
@ -170,6 +172,7 @@ func newImpl(
isNew bool,
notificationStore *notifications.Store,
getFlagValueFn unleash.GetFlagValueFn,
pushObservabilityMetric observability.PushObsMetricFn,
) (*User, error) {
logrus.WithField("userID", apiUser.ID).Info("Creating new user")
@ -265,7 +268,6 @@ func newImpl(
addressMode,
identityState.Clone(),
smtpServerManager,
observabilityService,
)
user.imapService = imapservice.NewService(
@ -286,10 +288,9 @@ func newImpl(
syncConfigDir,
user.maxSyncMemory,
showAllMail,
observabilityService,
)
user.notificationService = notifications.NewService(user.id, user.eventService, user, notificationStore, getFlagValueFn, observabilityService)
user.notificationService = notifications.NewService(user.id, user.eventService, user, notificationStore, getFlagValueFn, pushObservabilityMetric)
// Check for status_progress when triggered.
user.goStatusProgress = user.tasks.PeriodicOrTrigger(configstatus.ProgressCheckInterval, 0, func(ctx context.Context) {

View File

@ -175,6 +175,7 @@ func withUser(tb testing.TB, ctx context.Context, _ *server.Server, m *proton.Ma
func(_ string) bool {
return false
},
func(_ proton.ObservabilityMetric) {},
)
require.NoError(tb, err)
defer user.Close()

View File

@ -19,7 +19,6 @@ package vault
import (
"fmt"
"strings"
"github.com/bradenaw/juniper/xslices"
"golang.org/x/exp/slices"
@ -38,10 +37,6 @@ func (user *User) Username() string {
return user.vault.getUser(user.userID).Username
}
func (user *User) usernameUnsafe() string {
return user.vault.getUserUnsafe(user.userID).Username
}
// PrimaryEmail returns the user's primary email address.
func (user *User) PrimaryEmail() string {
return user.vault.getUser(user.userID).PrimaryEmail
@ -247,15 +242,3 @@ func (user *User) SetShouldSync(shouldResync bool) error {
func (user *User) GetShouldResync() bool {
return user.vault.getUser(user.userID).ShouldResync
}
// updateUsernameUnsafe - updates the username of the relevant user, provided that the new username is not empty
// and differs from the previous. Writes are not performed if this case is not met.
// Should only be called from contexts where the vault mutex is already locked.
func (user *User) updateUsernameUnsafe(username string) error {
if strings.TrimSpace(username) == "" || user.usernameUnsafe() == username {
return nil
}
return user.vault.modUserUnsafe(user.userID, func(userData *UserData) {
userData.Username = username
})
}

View File

@ -240,10 +240,6 @@ func (vault *Vault) GetOrAddUser(userID, username, primaryEmail, authUID, authRe
return nil, false, err
}
if err := user.updateUsernameUnsafe(username); err != nil {
return nil, false, err
}
return user, false, nil
}
}
@ -454,22 +450,6 @@ func (vault *Vault) getUser(userID string) UserData {
return users[idx]
}
// getUserUnsafe - fetches the relevant UserData.
// Should only be called from contexts in which the vault mutex has been read locked.
func (vault *Vault) getUserUnsafe(userID string) UserData {
users := vault.getUnsafe().Users
idx := xslices.IndexFunc(users, func(user UserData) bool {
return user.UserID == userID
})
if idx < 0 {
panic("Unknown user")
}
return users[idx]
}
func (vault *Vault) modUser(userID string, fn func(userData *UserData)) error {
vault.lock.Lock()
defer vault.lock.Unlock()

View File

@ -369,30 +369,30 @@ func getMessageHeader(msg proton.Message, opts JobOptions) message.Header {
// SetText will RFC2047-encode.
if msg.Subject != "" {
setUTF8EncodedHeaderIfNeeded(&hdr, "Subject", msg.Subject)
hdr.SetText("Subject", msg.Subject)
}
// mail.Address.String() will RFC2047-encode if necessary.
if !addressEmpty(msg.Sender) {
setHeaderIfNeeded(&hdr, "From", msg.Sender.String())
hdr.Set("From", msg.Sender.String())
}
if len(msg.ReplyTos) > 0 && !msg.IsDraft() {
if !(len(msg.ReplyTos) == 1 && addressEmpty(msg.ReplyTos[0])) {
setHeaderIfNeeded(&hdr, "Reply-To", toAddressList(msg.ReplyTos))
hdr.Set("Reply-To", toAddressList(msg.ReplyTos))
}
}
if len(msg.ToList) > 0 {
setHeaderIfNeeded(&hdr, "To", toAddressList(msg.ToList))
hdr.Set("To", toAddressList(msg.ToList))
}
if len(msg.CCList) > 0 {
setHeaderIfNeeded(&hdr, "Cc", toAddressList(msg.CCList))
hdr.Set("Cc", toAddressList(msg.CCList))
}
if len(msg.BCCList) > 0 {
setHeaderIfNeeded(&hdr, "Bcc", toAddressList(msg.BCCList))
hdr.Set("Bcc", toAddressList(msg.BCCList))
}
setMessageIDIfNeeded(msg, &hdr)
@ -401,7 +401,7 @@ func getMessageHeader(msg proton.Message, opts JobOptions) message.Header {
if opts.SanitizeDate {
if date, err := rfc5322.ParseDateTime(hdr.Get("Date")); err != nil || date.Before(time.Unix(0, 0)) {
msgDate := SanitizeMessageDate(msg.Time)
setHeaderIfNeeded(&hdr, "Date", msgDate.In(time.UTC).Format(time.RFC1123Z))
hdr.Set("Date", msgDate.In(time.UTC).Format(time.RFC1123Z))
// We clobbered the date so we save it under X-Original-Date only if no such value exists.
if !hdr.Has("X-Original-Date") {
hdr.Set("X-Original-Date", date.In(time.UTC).Format(time.RFC1123Z))
@ -412,7 +412,7 @@ func getMessageHeader(msg proton.Message, opts JobOptions) message.Header {
// Set our internal ID if requested.
// This is important for us to detect whether APPENDed things are actually "move like outlook".
if opts.AddInternalID {
setHeaderIfNeeded(&hdr, "X-Pm-Internal-Id", msg.ID)
hdr.Set("X-Pm-Internal-Id", msg.ID)
}
// Set our external ID if requested.
@ -426,7 +426,7 @@ func getMessageHeader(msg proton.Message, opts JobOptions) message.Header {
// Set our server date if requested.
// Can be useful to see how long it took for a message to arrive.
if opts.AddMessageDate {
setHeaderIfNeeded(&hdr, "X-Pm-Date", time.Unix(msg.Time, 0).In(time.UTC).Format(time.RFC1123Z))
hdr.Set("X-Pm-Date", time.Unix(msg.Time, 0).In(time.UTC).Format(time.RFC1123Z))
}
// Include the message ID in the references (supposedly this somehow improves outlook support...).
@ -463,25 +463,6 @@ func setMessageIDIfNeeded(msg proton.Message, hdr *message.Header) {
}
}
// setTextHeaderIfNeeded sets a text (UTF-encoded) header entry if its does not exists or if value is changed.
// Not systematically overwriting the value prevents it from being moved to the top (Del + Add) if not changed.
func setUTF8EncodedHeaderIfNeeded(header *message.Header, k, v string) {
encoded := mime.QEncoding.Encode("utf-8", v)
if header.Has(k) && (header.Get(k) == encoded) {
return
}
header.Set(k, encoded)
}
// setHeaderIfNeeded sets a header entry if its does not exists or if value is changed.
// Not systematically overwriting the value prevents it from being moved to the top (Del + Add) if not changed.
func setHeaderIfNeeded(header *message.Header, key, value string) {
if header.Has(key) && (header.Get(key) == value) {
return
}
header.Set(key, value)
}
func getTextPartHeader(hdr message.Header, body []byte, mimeType rfc822.MIMEType) message.Header {
params := make(map[string]string)
@ -528,9 +509,8 @@ func getAttachmentPartHeader(att proton.Attachment) message.Header {
func toMessageHeader(hdr proton.Headers) message.Header {
var res message.Header
// go-message's message.Header are in reversed order (you should only add fields at the top, so storing in reverse order offer faster performances).
for i := len(hdr.Order) - 1; i >= 0; i-- {
key := hdr.Order[i]
for _, key := range hdr.Order {
for _, val := range hdr.Values[key] {
// Using AddRaw instead of Add to save key-value pair as byte buffer within Header.
// This buffer is used latter on in message writer to construct message and avoid crash

View File

@ -21,7 +21,6 @@ import (
"bufio"
"bytes"
"encoding/base64"
"net/mail"
"strings"
"testing"
"time"
@ -93,67 +92,6 @@ func newRawTestMessageWithHeaders(messageID, addressID, mimeType, body string, d
}
}
func newTestMessageFromRFC822(t *testing.T, literal []byte) proton.Message {
// Note attachment are not supported.
p := rfc822.Parse(literal)
h, err := p.ParseHeader()
require.NoError(t, err)
var parsedHeaders proton.Headers
parsedHeaders.Values = make(map[string][]string)
h.Entries(func(key, val string) {
parsedHeaders.Values[key] = []string{val}
parsedHeaders.Order = append(parsedHeaders.Order, key)
})
var mailHeaders = mail.Header(parsedHeaders.Values)
require.True(t, h.Has("Content-Type"))
mime, _, err := rfc822.ParseMIMEType(h.Get("Content-Type"))
require.NoError(t, err)
date, err := mailHeaders.Date()
require.NoError(t, err)
sender, err := mail.ParseAddress(parsedHeaders.Values["From"][0])
require.NoError(t, err)
return proton.Message{
MessageMetadata: proton.MessageMetadata{
ID: "messageID",
AddressID: "addressID",
LabelIDs: []string{},
ExternalID: "",
Subject: parsedHeaders.Values["Subject"][0],
Sender: sender,
ToList: parseAddressList(t, mailHeaders, "To"),
CCList: parseAddressList(t, mailHeaders, "Cc"),
BCCList: parseAddressList(t, mailHeaders, "Bcc"),
ReplyTos: parseAddressList(t, mailHeaders, "Reply-To"),
Flags: 0,
Time: date.Unix(),
Size: 0,
Unread: false,
IsReplied: false,
IsRepliedAll: false,
IsForwarded: false,
NumAttachments: 0,
},
Header: string(h.Raw()),
ParsedHeaders: parsedHeaders,
Body: string(p.Body()),
MIMEType: mime,
Attachments: nil,
}
}
func parseAddressList(t *testing.T, header mail.Header, key string) []*mail.Address {
var result []*mail.Address
if len(header.Get(key)) == 0 {
return nil
}
result, err := header.AddressList(key)
require.NoError(t, err)
return result
}
func addTestAttachment(
t *testing.T,
kr *crypto.KeyRing,

View File

@ -18,14 +18,9 @@
package message
import (
"bytes"
"regexp"
"strings"
"testing"
gomessage "github.com/emersion/go-message"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestHeaderLines(t *testing.T) {
@ -135,69 +130,3 @@ func FuzzReadHeaderBody(f *testing.F) {
_, _, _ = readHeaderBody(b)
})
}
func TestHeaderOrder(t *testing.T) {
literal := []byte(`X-Pm-Content-Encryption: end-to-end
X-Pm-Origin: internal
Subject: header test
To: Test Proton <test@proton.me>
From: Dummy Recipient <dummy@proton.me>
Date: Tue, 15 Oct 2024 07:54:39 +0000
Mime-Version: 1.0
Content-Type: multipart/mixed;boundary=---------------------a136fc3851075ca3f022f5c3ec6bf8f5
Message-Id: <1rYR51zNVZdyCXVvAZ8C9N8OaBg4wO_wg6VlSoLK_Mv-2AaiF5UL-vE_tIZ6FdYP8ylsuV3fpaKUpVwuUcnQ6ql_83aEgZvfC5QcZbind1k=@proton.me>
X-Pm-Spamscore: 0
Received: from mail.protonmail.ch by mail.protonmail.ch; Tue, 15 Oct 2024 07:54:43 +0000
X-Original-To: test@proton.me
Return-Path: <dummy@proton.me>
Delivered-To: test@proton.me
lorem`)
// build a proton message
message := newTestMessageFromRFC822(t, literal)
options := JobOptions{
IgnoreDecryptionErrors: true,
SanitizeDate: true,
AddInternalID: true,
AddExternalID: true,
AddMessageDate: true,
AddMessageIDReference: true,
SanitizeMBOXHeaderLine: true,
}
// Rebuild the headers using bridge's algorithm, sanitizing fields.
hdr := getTextPartHeader(getMessageHeader(message, options), []byte(message.Body), message.MIMEType)
var b bytes.Buffer
w, err := gomessage.CreateWriter(&b, hdr)
require.NoError(t, err)
_ = w.Close()
// split the header
str := string(regexp.MustCompile(`\r\n(\s+)`).ReplaceAll(b.Bytes(), nil)) // join multi
lines := strings.Split(str, "\r\n")
// Check we have the expected order
require.Equal(t, len(lines), 17)
// The fields added or modified are at the top
require.True(t, strings.HasPrefix(lines[0], "Content-Type: multipart/mixed;boundary=")) // we changed the boundary
require.True(t, strings.HasPrefix(lines[1], "References: ")) // Reference was added
require.True(t, strings.HasPrefix(lines[2], "X-Pm-Date: ")) // X-Pm-Date was added
require.True(t, strings.HasPrefix(lines[3], "X-Pm-Internal-Id: ")) // X-Pm-Internal-Id was added
require.Equal(t, `To: "Test Proton" <test@proton.me>`, lines[4]) // Name was double quoted
require.Equal(t, `From: "Dummy Recipient" <dummy@proton.me>`, lines[5]) // Name was double quoted
// all other fields appear in their original order
require.Equal(t, `X-Pm-Content-Encryption: end-to-end`, lines[6])
require.Equal(t, `X-Pm-Origin: internal`, lines[7])
require.Equal(t, `Subject: header test`, lines[8])
require.Equal(t, `Date: Tue, 15 Oct 2024 07:54:39 +0000`, lines[9])
require.Equal(t, `Mime-Version: 1.0`, lines[10])
require.Equal(t, `Message-Id: <1rYR51zNVZdyCXVvAZ8C9N8OaBg4wO_wg6VlSoLK_Mv-2AaiF5UL-vE_tIZ6FdYP8ylsuV3fpaKUpVwuUcnQ6ql_83aEgZvfC5QcZbind1k=@proton.me>`, lines[11])
require.Equal(t, `X-Pm-Spamscore: 0`, lines[12])
require.Equal(t, `Received: from mail.protonmail.ch by mail.protonmail.ch; Tue, 15 Oct 2024 07:54:43 +0000`, lines[13])
require.Equal(t, `X-Original-To: test@proton.me`, lines[14])
require.Equal(t, `Return-Path: <dummy@proton.me>`, lines[15])
require.Equal(t, `Delivered-To: test@proton.me`, lines[16])
}

View File

@ -857,23 +857,6 @@ func getFileReader(filename string) io.Reader {
return f
}
func TestParseInvalidOriginalBoundary(t *testing.T) {
f := getFileReader("incorrect_boundary_w_invalid_character_tuta.eml")
p, err := parser.New(f)
require.NoError(t, err)
require.Equal(t, true, p.Root().Header.Get("Content-Type") == `multipart/related; boundary="------------1234567890@tutanota"`)
m, err := ParseWithParser(p, false)
require.NoError(t, err)
require.Equal(t, true, strings.HasPrefix(string(m.MIMEBody), "Content-Type: multipart/related;\r\n boundary="))
require.Equal(t, false, strings.HasPrefix(string(m.MIMEBody), `Content-Type: multipart/related;\n boundary="------------1234567890@tutanota"`))
require.Equal(t, false, strings.HasPrefix(string(m.MIMEBody), `Content-Type: multipart/related;\n boundary=------------1234567890@tutanota`))
require.Equal(t, false, strings.HasPrefix(string(m.MIMEBody), `Content-Type: multipart/related;\n boundary=1234567890@tutanota`))
}
type panicReader struct{}
func (panicReader) Read(_ []byte) (int, error) {

View File

@ -1,13 +0,0 @@
Date: Mon, 01 Jan 2000 00:00:00 +0000 (UTC)
From: Daniel at Test <daniel@test.com>
Mime-Version: 1.0
Subject: Test incorrect original boundary w. invalid character
To: david@test.com
Content-Type: multipart/related; boundary="------------1234567890@tutanota"
--------------1234567890@tutanota
Content-Type: text/html; charset=UTF-8
Content-transfer-encoding: base64
PGh0bWw+PGgxPkhlbGxvIFdvcmxkITwvaDE+PC9odG1sPg==
--------------1234567890@tutanota--

View File

@ -129,15 +129,13 @@ func getFeatureTags() string {
switch arguments := os.Args; arguments[len(arguments)-1] {
case "nightly":
tags = "~@gmail-integration"
tags = ""
case "smoke": // Currently this is just a placeholder, as there are no scenarios tagged with @smoke
tags = "@smoke"
case "black": // Currently this is just a placeholder, as there are no scenarios tagged with @smoke
tags = "~@skip-black"
case "gmail-integration":
tags = "@gmail-integration"
default:
tags = "~@regression && ~@smoke && ~@gmail-integration" // To exclude more add `&& ~@tag`
tags = "~@regression && ~@smoke" // To exclude more add `&& ~@tag`
}
return tags

View File

@ -1,35 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<PlatformTarget>x64</PlatformTarget>
<Platforms>AnyCPU;x64</Platforms>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="FlaUI.Core" Version="4.0.0" />
<PackageReference Include="FlaUI.UIA3" Version="4.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.0" />
<PackageReference Include="NUnit" Version="4.2.1" />
<PackageReference Include="NUnit.Analyzers" Version="4.3.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
<PackageReference Include="System.Drawing.Common" Version="8.0.8" />
</ItemGroup>
<ItemGroup>
<Using Include="NUnit.Framework" />
</ItemGroup>
</Project>

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