Compare commits

..

1 Commits

Author SHA1 Message Date
6105f32c75 chore: Dragon Bridge 3.14.0 changelog. 2024-09-25 10:47:40 +02:00
97 changed files with 219 additions and 9363 deletions

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,30 +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

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.14.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

11
go.mod
View File

@ -7,7 +7,7 @@ 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.20240923151549-d23b4bec3602
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/gopenpgp/v2 v2.7.4-proton
@ -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,13 +112,11 @@ 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
)

105
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,29 +27,51 @@ 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.20240514133734-79cdd0fec41c h1:P3SvCACt13Zqdj0IRDB4bgwqI68+oMB2j0uVuPQyoTw=
github.com/ProtonMail/gluon v0.17.1-0.20240514133734-79cdd0fec41c/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
github.com/ProtonMail/gluon v0.17.1-0.20240918150504-3b2e7f40d961 h1:kCaz78X7OKETvK6AGHeyggHKxDBcqX7EWHf7spJ+D3g=
github.com/ProtonMail/gluon v0.17.1-0.20240918150504-3b2e7f40d961/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
github.com/ProtonMail/gluon v0.17.1-0.20240923094038-e319bf6047c5 h1:LzaUpUj6M2PEBArFCkaimViNpGXDgwHVrdhvYwHLoJQ=
github.com/ProtonMail/gluon v0.17.1-0.20240923094038-e319bf6047c5/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
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/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.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-message v0.13.1-0.20240906141354-38c596f2f5a8 h1:+eE7FGX+4Hu8RZaRmSebrDVXyLuowKSaO7ZhQ6ca4+E=
github.com/ProtonMail/go-message v0.13.1-0.20240906141354-38c596f2f5a8/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
github.com/ProtonMail/go-message v0.13.1-0.20240906144417-4083506a9542 h1:5DqSycYnKfUdHiu0yOdiYW5R2hVxoE0Mk4PLSYwqGyg=
github.com/ProtonMail/go-message v0.13.1-0.20240906144417-4083506a9542/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
github.com/ProtonMail/go-message v0.13.1-0.20240910093530-2ada52e7dffb h1:uOKp93u6JFYlBoJJvOhzmHZURcvWmXiqhihGWtT3HtY=
github.com/ProtonMail/go-message v0.13.1-0.20240910093530-2ada52e7dffb/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
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-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.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-proton-api v0.4.1-0.20240916123336-3ac75d8041dc h1:SWVPwO1M2jCI1bJHBji/JVU01FpWP/6nzh8NBIjo+Fg=
github.com/ProtonMail/go-proton-api v0.4.1-0.20240916123336-3ac75d8041dc/go.mod h1:3A0cpdo0BIenIPjTG6u8EbzJ8uuJy7rVvM/NaynjCKA=
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-smtp v0.0.0-20231109081432-2b3d50599865 h1:EP1gnxLL5Z7xBSymE9nSTM27nRYINuvssAtDmG0suD8=
@ -93,7 +108,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 +124,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 +175,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 +227,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 +235,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 +242,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 +252,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 +401,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 +482,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 +496,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 +544,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 +561,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 +591,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 +650,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 +674,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 +687,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 +721,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

@ -325,7 +325,6 @@ func newBridge(
reporter,
uidValidityGenerator,
&bridgeIMAPSMTPTelemetry{b: bridge},
observabilityService,
)
// Check whether username has changed and correct (macOS only)
@ -636,7 +635,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
}

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{

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

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

@ -71,8 +71,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

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

@ -31,7 +31,6 @@ import (
"github.com/ProtonMail/gluon/connector"
"github.com/ProtonMail/gluon/imap"
"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"
@ -70,8 +69,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 +107,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 +676,20 @@ 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 {
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 +716,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,
@ -876,75 +874,3 @@ func stripPlusAlias(a string) string {
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")
}
inCombinedMode := s.addressMode == usertypes.AddressModeCombined
if !inCombinedMode {
return address, nil
}
senderAddr, err := s.getSenderProtonAddress(p)
if err != nil {
if !errors.Is(err, errNoSenderAddressMatch) {
s.log.WithError(err).Warn("Could not get import address")
}
// We did not find a match, so we use the default address.
return address, nil
}
if senderAddr.ID == address.ID {
return address, nil
}
// 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
isSenderAddressDisabled := (!bool(senderAddr.Send)) || (senderAddr.Status != proton.AddressStatusEnabled)
if isDraft && isSenderAddressDisabled {
return address, nil
}
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
}

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

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

@ -25,21 +25,13 @@ type DistinctionErrorTypeEnum int
const (
SyncError DistinctionErrorTypeEnum = iota
GluonImapError
GluonMessageError
GluonOtherError
SMTPError
EventLoopError // EventLoopError - should always be kept last when inserting new keys.
EventLoopError
)
// 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",
SyncError: "bridge_sync_errors_users_total",
EventLoopError: "bridge_event_loop_events_errors_users_total",
}
// createLastSentMap - needs to be updated whenever we make changes to the enum.

View File

@ -26,32 +26,26 @@ import (
)
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
}
})
}
@ -101,14 +95,13 @@ func (d *distinctionUtility) generateHeartbeatUserMetric() proton.ObservabilityM
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 {
func generateHeartbeatMetric(plan, mailClient, dohEnabled, betaAccess, otherError, syncError, eventLoopError string) proton.ObservabilityMetric {
return proton.ObservabilityMetric{
Name: genericHeartbeatSchemaName,
Version: genericHeartbeatVersion,
Version: 1,
Timestamp: time.Now().Unix(),
Data: map[string]interface{}{
"Value": 1,
@ -120,7 +113,6 @@ func generateHeartbeatMetric(plan, mailClient, dohEnabled, betaAccess, otherErro
"receivedOtherError": otherError,
"receivedSyncError": syncError,
"receivedEventLoopError": eventLoopError,
"receivedGluonError": gluonError,
},
},
}

View File

@ -18,7 +18,6 @@
package observability
import (
gluonMetrics "github.com/ProtonMail/gluon/observability/metrics"
"github.com/ProtonMail/go-proton-api"
)
@ -86,19 +85,16 @@ func GenerateAllHeartbeatMetricPermutations() []proton.ObservabilityMetric {
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,
),
)
}
metrics = append(metrics,
generateHeartbeatMetric(plan,
mailClient,
dohEnabled,
betaAccess,
receivedOtherError,
receivedSyncError,
receivedEventLoopError,
),
)
}
}
}
@ -108,20 +104,3 @@ func GenerateAllHeartbeatMetricPermutations() []proton.ObservabilityMetric {
}
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,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

@ -35,9 +35,7 @@ import (
"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 +166,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 +196,7 @@ func (s *Service) sendWithKey(
}
parentID, draftsToDelete, err := getParentID(ctx, s.client, authAddrID, addrMode, references)
if err != nil {
s.observabilitySender.AddDistinctMetrics(observability.SMTPError, observabilitymetrics.GenerateFailedGetParentID())
// Sentry event has been removed; should be replaced with observability - BRIDGE-206.
s.log.WithError(err).Warn("Failed to get parent ID")
}
@ -217,7 +211,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 +227,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

@ -114,7 +114,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)
}
@ -217,7 +217,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

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

@ -265,7 +265,6 @@ func newImpl(
addressMode,
identityState.Clone(),
smtpServerManager,
observabilityService,
)
user.imapService = imapservice.NewService(

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

@ -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,64 +0,0 @@
using FlaUI.Core.AutomationElements;
using FlaUI.Core.Definitions;
using ProtonMailBridge.UI.Tests.TestsHelper;
using FlaUI.Core.Input;
using System.DirectoryServices;
using System.Net;
namespace ProtonMailBridge.UI.Tests.Results
{
public class HelpMenuResult : UIActions
{
private AutomationElement NotificationWindow => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Window));
private AutomationElement[] TextFields => Window.FindAllDescendants(cf => cf.ByControlType(ControlType.Text));
private TextBox HelpText => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Text).And(cf.ByName("Help"))).AsTextBox();
private TextBox BridgeIsUpToDate => NotificationWindow.FindFirstDescendant(cf => cf.ByControlType(ControlType.Text)).AsTextBox();
private AutomationElement ChromeTab => ChromeWindow.FindFirstDescendant(cf => cf.ByControlType(ControlType.Document));
private TextBox ChromeText => ChromeTab.FindFirstDescendant(cf => cf.ByControlType(ControlType.Text).And(cf.ByName("We can help you with every step of using Proton Mail Bridge."))).AsTextBox();
//private AutomationElement AdressBar => FileExplorerWindow.FindFirstDescendant(cf => cf.ByControlType(ControlType.Group).And(cf.ByAutomationId("PART_BreadcrumbBar")));
private AutomationElement AdressPane => FileExplorerWindow.FindFirstDescendant(cf => cf.ByControlType(ControlType.Pane).And(cf.ByClassName("Microsoft.UI.Content.DesktopChildSiteBridge")));
private AutomationElement AdressBar => AdressPane.FindFirstDescendant(cf =>cf.ByControlType(ControlType.Group).And(cf.ByAutomationId("PART_BreadcrumbBar")));
private AutomationElement[] Folders => AdressBar.FindAllDescendants(cf => cf.ByControlType(ControlType.SplitButton));
private TextBox SendReportConfirmation => NotificationWindow.FindFirstDescendant(cf => cf.ByControlType(ControlType.Text).And(cf.ByName("Thank you for the report. We'll get back to you as soon as we can."))).AsTextBox();
public HelpMenuResult CheckIfUserOpenedHelpMenu()
{
Assert.That(HelpText.IsAvailable, Is.True);
return this;
}
public HelpMenuResult CheckBridgeIsUpToDateNotification()
{
Assert.That(BridgeIsUpToDate.IsAvailable, Is.True);
return this;
}
public HelpMenuResult CheckHelpLinkIsOpen()
{
Assert.That(ChromeText.IsAvailable, Is.True);
return this;
}
public HelpMenuResult CheckBridgeLogsAreOpen()
{
var adressName = "";
foreach (var folder in Folders)
{
var folderName = folder.Name;
adressName = System.IO.Path.Combine(adressName, folderName);
}
var expectedPath = "\\AppData\\Roaming\\protonmail\\bridge-v3\\logs";
Assert.That(adressName.Contains(expectedPath), Is.True);
return this;
}
public HelpMenuResult CheckIfProblemIsSuccReported()
{
Assert.That(SendReportConfirmation.IsAvailable, Is.True);
return this;
}
}
}

View File

@ -1,8 +1,5 @@
using FlaUI.Core.AutomationElements;
using FlaUI.Core.Definitions;
using ProtonMailBridge.UI.Tests.TestsHelper;
using FlaUI.Core.Input;
using System.DirectoryServices;
namespace ProtonMailBridge.UI.Tests.Results
{
@ -12,32 +9,12 @@ namespace ProtonMailBridge.UI.Tests.Results
private AutomationElement NotificationWindow => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Window));
private TextBox FreeAccountErrorText => NotificationWindow.FindFirstDescendant(cf => cf.ByControlType(ControlType.Text)).AsTextBox();
private TextBox SignedOutAccount => AccountView.FindFirstDescendant(cf => cf.ByControlType(ControlType.Text)).AsTextBox();
private TextBox AlreadySignedInText => NotificationWindow.FindFirstDescendant(cf => cf.ByControlType(ControlType.Text)).AsTextBox();
private Button OkToAcknowledgeAccountAlreadySignedIn => NotificationWindow.FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("OK"))).AsButton();
private AutomationElement[] TextFields => Window.FindAllDescendants(cf => cf.ByControlType(ControlType.Text));
private TextBox SynchronizingField => TextFields[4].AsTextBox();
private TextBox AccountDisabledErrorText => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Text).And(cf.ByName("failed to create new API client: 422 POST https://mail-api.proton.me/auth/v4: This account has been suspended due to a potential policy violation. If you believe this is in error, please contact us at https://proton.me/support/appeal-abuse (Code=10003, Status=422)"))).AsTextBox();
private TextBox AccountDelinquentErrorText => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Text).And(cf.ByName("failed to create new API client: 422 POST https://mail-api.proton.me/auth/v4: Use of this client requires permissions not available to your account (Code=2011, Status=422)"))).AsTextBox();
private TextBox IncorrectLoginCredentialsErrorText => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Text).And(cf.ByName("Incorrect login credentials"))).AsTextBox();
private TextBox EnterEmailOrUsernameErrorText => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Text).And(cf.ByName("Enter email or username"))).AsTextBox();
private TextBox EnterPasswordErrorText => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Text).And(cf.ByName("Enter password"))).AsTextBox();
private TextBox ConnectedStateText => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Text).And(cf.ByName("Connected"))).AsTextBox();
public HomeResult CheckConnectedState()
{
Assert.That(ConnectedStateText.IsAvailable, Is.True);
return this;
}
public HomeResult CheckIfLoggedIn()
{
Assert.That(SignOutButton.IsAvailable, Is.True);
return this;
}
public HomeResult CheckIfSynchronizingBarIsShown()
{
Assert.That(SynchronizingField.IsAvailable && SynchronizingField.Name.StartsWith("Synchronizing"), Is.True);
return this;
}
public HomeResult CheckIfFreeAccountErrorIsDisplayed(string ErrorText)
{
Assert.That(FreeAccountErrorText.Name == ErrorText, Is.True);
@ -48,46 +25,5 @@ namespace ProtonMailBridge.UI.Tests.Results
Assert.That(SignedOutAccount.IsAvailable, Is.True);
return this;
}
public HomeResult CheckIfAccountAlreadySignedInIsDisplayed()
{
Assert.That(AlreadySignedInText.IsAvailable, Is.True);
return this;
}
public HomeResult ClickOkToAcknowledgeAccountAlreadySignedIn ()
{
OkToAcknowledgeAccountAlreadySignedIn.Click();
return this;
}
public HomeResult CheckIfIncorrectCredentialsErrorIsDisplayed()
{
Assert.That(IncorrectLoginCredentialsErrorText.IsAvailable, Is.True);
return this;
}
public HomeResult CheckIfEnterUsernameAndEnterPasswordErrorMsgsAreDisplayed()
{
Assert.That(EnterEmailOrUsernameErrorText.IsAvailable && EnterPasswordErrorText.IsAvailable, Is.True);
return this;
}
public HomeResult CheckIfDsabledAccountErrorIsDisplayed()
{
Assert.That(AccountDisabledErrorText.IsAvailable, Is.True);
return this;
}
public HomeResult CheckIfDelinquentAccountErrorIsDisplayed()
{
Assert.That(AccountDelinquentErrorText.IsAvailable, Is.True);
return this;
}
public HomeResult CheckIfNotificationTextIsShown()
{
Assert.That(AlreadySignedInText.IsAvailable, Is.True);
return this;
}
}
}

View File

@ -5,7 +5,6 @@ using FlaUI.Core;
using FlaUI.UIA3;
using ProtonMailBridge.UI.Tests.TestsHelper;
using FlaUI.Core.Input;
using System.Diagnostics;
namespace ProtonMailBridge.UI.Tests
{
@ -15,58 +14,15 @@ namespace ProtonMailBridge.UI.Tests
public static Application App;
protected static Application Service;
protected static Window Window;
protected static Window ChromeWindow;
protected static Window FileExplorerWindow;
protected static void ClientCleanup()
{
App.Kill();
App.Dispose();
// Give some time to properly exit the app
Thread.Sleep(10000);
Thread.Sleep(2000);
}
public static void switchToFileExplorerWindow()
{
var _automation = new UIA3Automation();
var desktop = _automation.GetDesktop();
var _explorerWindow = desktop.FindFirstDescendant(cf => cf.ByClassName("CabinetWClass"));
// If the File Explorer window is not found, fail the test
if (_explorerWindow == null)
{
throw new Exception("File Explorer window not found.");
}
// Cast the found element to a Window object
FileExplorerWindow = _explorerWindow.AsWindow();
// Focus on the File Explorer window
FileExplorerWindow.Focus();
}
public static void switchToChromeWindow()
{
var _automation = new UIA3Automation();
var desktop = _automation.GetDesktop();
var _chromeWindow = desktop.FindFirstDescendant(cf => cf.ByClassName("Chrome_WidgetWin_1"));
// If the Chrome window is not found, fail the test
if (_chromeWindow == null)
{
throw new Exception("Google Chrome window not found.");
}
// Cast the found element to a Window object
ChromeWindow = _chromeWindow.AsWindow();
// Focus on the Chrome window
ChromeWindow.Focus();
}
public static void LaunchApp()
{
string appExecutable = TestData.AppExecutable;

View File

@ -1,141 +0,0 @@
using NUnit.Framework;
using ProtonMailBridge.UI.Tests.TestsHelper;
using ProtonMailBridge.UI.Tests.Windows;
using ProtonMailBridge.UI.Tests.Results;
using FlaUI.Core.Input;
using FlaUI.Core.AutomationElements;
using FlaUI.UIA3;
namespace ProtonMailBridge.UI.Tests.Tests
{
[TestFixture]
public class HelpMenuTests : TestSession
{
private readonly LoginWindow _loginWindow = new();
private readonly HomeWindow _mainWindow = new();
private readonly HelpMenuResult _helpMenuResult = new();
private readonly HelpMenuWindow _helpMenuWindow = new();
private readonly HomeResult _homeResult = new();
[SetUp]
public void TestInitialize()
{
LaunchApp();
}
[Test]
public void OpenHelpMenuAndSwitchBackToAccountView()
{
_loginWindow.SignIn(TestUserData.GetPaidUser());
_helpMenuWindow.ClickHelpButton();
_helpMenuWindow.ClickBackFromHelpMenu();
Thread.Sleep(2000);
_homeResult.CheckIfLoggedIn();
}
[Test]
public void OpenGoToHelpTopics()
{
_loginWindow.SignIn(TestUserData.GetPaidUser());
_helpMenuWindow.ClickHelpButton();
_helpMenuWindow.ClickGoToHelpTopics();
Wait.UntilInputIsProcessed(TimeSpan.FromSeconds(3));
switchToChromeWindow();
_helpMenuResult.CheckHelpLinkIsOpen();
Window.Focus();
_helpMenuWindow.ClickBackFromHelpMenu();
}
[Test]
public void CheckForUpdates()
{
_loginWindow.SignIn(TestUserData.GetPaidUser());
_helpMenuWindow.ClickHelpButton();
_helpMenuWindow.ClickCheckNowButton();
Wait.UntilInputIsProcessed(TimeSpan.FromSeconds(3));
_helpMenuResult.CheckBridgeIsUpToDateNotification();
_helpMenuWindow.ConfirmNotification();
Wait.UntilInputIsProcessed(TimeSpan.FromSeconds(1));
_helpMenuWindow.ClickBackFromHelpMenu();
}
[Test]
public void OpenLogs()
{
_loginWindow.SignIn(TestUserData.GetPaidUser());
_helpMenuWindow.ClickHelpButton();
_helpMenuWindow.ClickLogsButton();
Wait.UntilInputIsProcessed(TimeSpan.FromSeconds(3));
switchToFileExplorerWindow();
_helpMenuResult.CheckBridgeLogsAreOpen();
Window.Focus();
_helpMenuWindow.ClickBackFromHelpMenu();
}
[Test]
public void OpenMissingEmailsReportProblem()
{
_loginWindow.SignIn(TestUserData.GetPaidUser());
_helpMenuWindow.ClickHelpButton();
_helpMenuWindow.ClickReportProblemButton();
_helpMenuWindow.ClickICannotFindEmailsInEmailClient();
_helpMenuWindow.EnterMissingEmailsProblemDetails();
_helpMenuResult.CheckIfProblemIsSuccReported();
_helpMenuWindow.ConfirmNotification();
}
[Test]
public void OpenNotAbleToSendEmailsReportProblem()
{
_loginWindow.SignIn(TestUserData.GetPaidUser());
_helpMenuWindow.ClickHelpButton();
_helpMenuWindow.ClickReportProblemButton();
_helpMenuWindow.ClickNotAbleToSendEmails();
_helpMenuWindow.EnterNotAbleToSendEmailProblemDetails();
_helpMenuResult.CheckIfProblemIsSuccReported();
_helpMenuWindow.ConfirmNotification();
}
[Test]
public void OpenBridgeIsNotStartingCorrectlyReportProblem()
{
_loginWindow.SignIn(TestUserData.GetPaidUser());
_helpMenuWindow.ClickHelpButton();
_helpMenuWindow.ClickReportProblemButton();
_helpMenuWindow.ClickBridgeIsNotStartingCorrectly();
_helpMenuWindow.EnterBridgeIsNotStartingCorrectlyProblemDetails();
_helpMenuResult.CheckIfProblemIsSuccReported();
_helpMenuWindow.ConfirmNotification();
}
[Test]
public void OpenBridgeIsRunningSlowReportProblem()
{
_loginWindow.SignIn(TestUserData.GetPaidUser());
_helpMenuWindow.ClickHelpButton();
_helpMenuWindow.ClickReportProblemButton();
_helpMenuWindow.ClickBridgeIsRunningSlow();
_helpMenuWindow.EnterBridgeIsRunningSlowProblemDetails();
_helpMenuResult.CheckIfProblemIsSuccReported();
_helpMenuWindow.ConfirmNotification();
}
[Test]
public void OpenSomethingElseReportProblem()
{
_loginWindow.SignIn(TestUserData.GetPaidUser());
_helpMenuWindow.ClickHelpButton();
_helpMenuWindow.ClickReportProblemButton();
_helpMenuWindow.ClickSomethingElse();
_helpMenuWindow.EnterSomethingElseProblemDetails();
_helpMenuResult.CheckIfProblemIsSuccReported();
_helpMenuWindow.ConfirmNotification();
}
[TearDown]
public void TestCleanup()
{
_mainWindow.RemoveAccount();
ClientCleanup();
}
}
}

View File

@ -2,7 +2,6 @@
using ProtonMailBridge.UI.Tests.TestsHelper;
using ProtonMailBridge.UI.Tests.Windows;
using ProtonMailBridge.UI.Tests.Results;
using FlaUI.Core.Input;
namespace ProtonMailBridge.UI.Tests.Tests
{
@ -14,13 +13,6 @@ namespace ProtonMailBridge.UI.Tests.Tests
private readonly HomeResult _homeResult = new();
private readonly string FreeAccountErrorText = "Bridge is exclusive to our mail paid plans. Upgrade your account to use Bridge.";
[Test]
public void LoginAsFreeUser()
{
_loginWindow.SignIn(TestUserData.GetFreeUser());
_homeResult.CheckIfFreeAccountErrorIsDisplayed(FreeAccountErrorText);
}
[Test]
public void LoginAsPaidUser()
{
@ -29,121 +21,20 @@ namespace ProtonMailBridge.UI.Tests.Tests
}
[Test]
public void VerifyConnectedState()
public void LoginAsFreeUser()
{
_loginWindow.SignIn(TestUserData.GetPaidUser());
_homeResult.CheckIfLoggedIn();
_homeResult.CheckConnectedState();
_loginWindow.SignIn(TestUserData.GetFreeUser());
_homeResult.CheckIfFreeAccountErrorIsDisplayed(FreeAccountErrorText);
}
[Test]
public void VerifyAccountSynchronizingBar()
public void SuccessfullLogout()
{
_loginWindow.SignIn(TestUserData.GetPaidUser());
_homeResult.CheckIfSynchronizingBarIsShown();
}
[Test]
public void AddAliasAddress()
{
_loginWindow.SignIn(TestUserData.GetPaidUser());
_homeResult.CheckIfLoggedIn();
_mainWindow.AddNewAccount();
_loginWindow.SignIn(TestUserData.GetAliasUser());
_homeResult.CheckIfAccountAlreadySignedInIsDisplayed();
_homeResult.ClickOkToAcknowledgeAccountAlreadySignedIn();
_loginWindow.ClickCancelToSignIn();
}
[Test]
public void LoginWithMailboxPassword()
{
_loginWindow.SignInMailbox(TestUserData.GetMailboxUser());
_homeResult.CheckIfLoggedIn();
_mainWindow.SignOutAccount();
_homeResult.CheckIfAccountIsSignedOut();
}
[Test]
public void AddSameAccountTwice()
{
_loginWindow.SignIn(TestUserData.GetPaidUser());
_homeResult.CheckIfLoggedIn();
_mainWindow.AddNewAccount();
_loginWindow.SignIn(TestUserData.GetPaidUser());
_homeResult.CheckIfAccountAlreadySignedInIsDisplayed();
_homeResult.ClickOkToAcknowledgeAccountAlreadySignedIn();
_loginWindow.ClickCancelToSignIn();
_homeResult.CheckIfLoggedIn();
}
[Test]
public void AddAccountWithWrongCredentials()
{
_loginWindow.SignIn(TestUserData.GetIncorrectCredentialsUser());
_homeResult.CheckIfIncorrectCredentialsErrorIsDisplayed();
_loginWindow.ClickCancelToSignIn();
}
[Test, Order (1)]
public void AddAccountWithEmptyCredentials()
{
_loginWindow.SignIn(TestUserData.GetEmptyCredentialsUser());
_homeResult.CheckIfEnterUsernameAndEnterPasswordErrorMsgsAreDisplayed();
_loginWindow.ClickCancelToSignIn();
_loginWindow.SignIn(TestUserData.GetPaidUser());
_homeResult.CheckIfLoggedIn();
}
[Test]
public void AddSameAccountAfterBeingSignedOut()
{
_loginWindow.SignIn(TestUserData.GetPaidUser());
_homeResult.CheckIfLoggedIn();
_mainWindow.SignOutAccount();
Wait.UntilInputIsProcessed(TimeSpan.FromSeconds(3));
_mainWindow.ClickSignInMainWindow();
_loginWindow.SignIn(TestUserData.GetPaidUser());
_homeResult.CheckIfLoggedIn();
_mainWindow.SignOutAccount();
}
/*
[Test]
public void AddSecondAccount()
{
_loginWindow.SignIn(TestUserData.GetPaidUser());
_homeResult.CheckIfLoggedIn();
_mainWindow.AddNewAccount();
_loginWindow.SignInMailbox(TestUserData.GetMailboxUser());
_homeResult.CheckIfLoggedIn();
}
*/
[Test]
public void AddDisabledAccount()
{
_loginWindow.SignIn(TestUserData.GetDisabledUser());
_homeResult.CheckIfDsabledAccountErrorIsDisplayed();
_loginWindow.ClickCancelToSignIn();
}
[Test]
public void AddDeliquentAccount()
{
_loginWindow.SignIn(TestUserData.GetDeliquentUser());
_homeResult.CheckIfDelinquentAccountErrorIsDisplayed();
_loginWindow.ClickCancelToSignIn();
}
//[Test]
//public void SuccessfullLogout()
//{
// _loginWindow.SignIn(TestUserData.GetPaidUser());
// _mainWindow.SignOutAccount();
// _homeResult.CheckIfAccountIsSignedOut();
//}
[SetUp]
public void TestInitialize()
{
@ -157,4 +48,4 @@ namespace ProtonMailBridge.UI.Tests.Tests
ClientCleanup();
}
}
}
}

View File

@ -11,6 +11,6 @@ namespace ProtonMailBridge.UI.Tests.TestsHelper
public static TimeSpan ThirtySecondsTimeout => TimeSpan.FromSeconds(30);
public static TimeSpan OneMinuteTimeout => TimeSpan.FromSeconds(60);
public static TimeSpan RetryInterval => TimeSpan.FromMilliseconds(1000);
public static string AppExecutable => "C:\\Program Files\\Proton AG\\Proton Mail Bridge\\proton-bridge.exe";
public static string AppExecutable => "C:\\Program Files\\Proton AG\\Proton Mail Bridge\\bridge-gui.exe";
}
}

View File

@ -1,5 +1,4 @@
using System;
using System.Diagnostics;
namespace ProtonMailBridge.UI.Tests.TestsHelper
{
@ -7,60 +6,31 @@ namespace ProtonMailBridge.UI.Tests.TestsHelper
{
public string Username { get; set; }
public string Password { get; set; }
public string MailboxPassword { get; set; }
public TestUserData(string username, string password, string mailboxPassword ="")
public TestUserData(string username, string password)
{
Username = username;
Password = password;
MailboxPassword = mailboxPassword;
}
public static TestUserData GetFreeUser()
{
(string username, string password) = GetUsernameAndPassword("BRIDGE_FLAUI_FREE_USER");
(string username, string password) = GetusernameAndPassword("BRIDGE_FLAUI_FREE_USER");
return new TestUserData(username, password);
}
public static TestUserData GetPaidUser()
{
(string username, string password) = GetUsernameAndPassword("BRIDGE_FLAUI_PAID_USER");
(string username, string password) = GetusernameAndPassword("BRIDGE_FLAUI_PAID_USER");
return new TestUserData(username, password);
}
public static TestUserData GetMailboxUser()
{
(string username, string password, string mailboxPassword) = GetUsernameAndPasswordAndMailbox("BRIDGE_FLAUI_MAILBOX_USER");
return new TestUserData(username, password, mailboxPassword);
}
public static TestUserData GetDisabledUser()
{
(string username, string password) = GetUsernameAndPassword("BRIDGE_FLAUI_DISABLED_USER");
return new TestUserData(username, password);
}
public static TestUserData GetDeliquentUser()
{
(string username, string password) = GetUsernameAndPassword("BRIDGE_FLAUI_DELIQUENT_USER");
return new TestUserData(username, password);
}
public static TestUserData GetIncorrectCredentialsUser()
{
return new TestUserData("IncorrectUsername", "IncorrectPass");
}
public static TestUserData GetEmptyCredentialsUser()
{
return new TestUserData("", "");
}
public static TestUserData GetAliasUser()
{
(string username, string password) = GetUsernameAndPassword("BRIDGE_FLAUI_ALIAS_USER");
return new TestUserData(username, password);
}
private static (string, string) GetUsernameAndPassword(string userType)
private static (string, string) GetusernameAndPassword(string userType)
{
// Get the environment variable for the user and check if missing
// When changing or adding an environment variable, you must restart Visual Studio
@ -68,7 +38,7 @@ namespace ProtonMailBridge.UI.Tests.TestsHelper
string? str = Environment.GetEnvironmentVariable(userType);
if (string.IsNullOrEmpty(str))
{
throw new Exception($"Environment variable {userType} must contain one ':' and it must be between username and password!");
throw new Exception($"Missing environment variable: {userType}");
}
// Check if the environment variable contains only one ':'
@ -84,34 +54,5 @@ namespace ProtonMailBridge.UI.Tests.TestsHelper
string[] split = str.Split(':');
return (split[0], split[1]);
}
private static (string, string, string) GetUsernameAndPasswordAndMailbox(string userType)
{
// Get the environment variable for the user type and check if missing
string? str = Environment.GetEnvironmentVariable(userType);
if (string.IsNullOrEmpty(str))
{
throw new Exception($"Missing environment variable: {userType}");
}
// Check if the environment variable contains exactly two ':'
// The first part is the username, second part is the password, third is the mailbox
string separator = ":";
string[] parts = str.Split(separator);
if (parts.Length != 3)
{
throw new Exception(
$"Environment variable {userType} must contain exactly two ':' characters, separating username, password, and mailbox!"
);
}
string username = parts[0];
string password = parts[1];
string mailbox = parts[2];
// Return the username, password, and mailbox as a tuple
return (username, password, mailbox);
}
}
}

View File

@ -1,260 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using FlaUI.Core.AutomationElements;
using FlaUI.Core.AutomationElements.Scrolling;
using FlaUI.Core.Definitions;
using FlaUI.Core.Input;
using FlaUI.Core.WindowsAPI;
using Microsoft.VisualBasic.Devices;
using NUnit.Framework.Legacy;
using ProtonMailBridge.UI.Tests.Results;
using ProtonMailBridge.UI.Tests.TestsHelper;
using Keyboard = FlaUI.Core.Input.Keyboard;
using Mouse = FlaUI.Core.Input.Mouse;
namespace ProtonMailBridge.UI.Tests.Windows
{
public class HelpMenuWindow : UIActions
{
private AutomationElement[] InputFields => Window.FindAllDescendants(cf => cf.ByControlType(ControlType.Edit));
private AutomationElement[] HomeButtons => Window.FindAllDescendants(cf => cf.ByControlType(ControlType.Button));
private AutomationElement NotificationWindow => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Window));
private AutomationElement[] ReportProblemPane => Window.FindAllDescendants(cf => cf.ByControlType(ControlType.Pane));
private Button HelpButton => HomeButtons[3].AsButton();
private Button BackToAccountViewButton => HomeButtons[11].AsButton();
private Button GoToHelpTopics => HomeButtons[7].AsButton();
private Button CheckNow => HomeButtons[8].AsButton();
private Button ConfirmNotificationButton => NotificationWindow.FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("OK"))).AsButton();
private Button LogsButton => HomeButtons[9].AsButton();
private Button ReportProblemButton => HomeButtons[10].AsButton();
private Button ICannotFindEmailInClient => HomeButtons[7].AsButton();
private TextBox DescriptionOnWhatHappened => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Edit)).AsTextBox();
private RadioButton MissingEmails => ReportProblemPane[0].FindFirstDescendant(cf => cf.ByControlType(ControlType.RadioButton).And(cf.ByName("Old emails are missing"))).AsRadioButton();
private RadioButton FindEmails => ReportProblemPane[0].FindFirstDescendant(cf => cf.ByControlType(ControlType.RadioButton).And(cf.ByName("Yes"))).AsRadioButton();
private CheckBox VPNSoftware => ReportProblemPane[0].FindFirstDescendant(cf => cf.ByControlType(ControlType.CheckBox).And(cf.ByName("VPN"))).AsCheckBox();
private CheckBox FirewallSoftware => ReportProblemPane[0].FindFirstDescendant(cf => cf.ByControlType(ControlType.CheckBox).And(cf.ByName("Firewall"))).AsCheckBox();
private Button ContinueToReportButton => ReportProblemPane[0].FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("Continue"))).AsButton();
private AutomationElement ScrollBarFirstPane => ReportProblemPane[0].FindFirstDescendant(cf => cf.ByControlType(ControlType.ScrollBar));
private Button SendReportButton => ReportProblemPane[0].FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("Send"))).AsButton();
private Button NotAbleToSendEmailsButton => HomeButtons[8].AsButton();
private Button BridgeIsNotStartingCorrectlyButton => HomeButtons[9].AsButton();
private TextBox StepByStepActionsForBridgeIsNotStartingCorrectly => ReportProblemPane[2].AsTextBox();
private TextBox IssuesLastOccurence => ReportProblemPane[3].AsTextBox();
private TextBox QuestionFocusWhenWasLastOccurence => ReportProblemPane[0].FindFirstDescendant(cf => cf.ByControlType(ControlType.Text).And(cf.ByName("When did the issue last occur? Is it repeating?"))).AsTextBox();
private TextBox QuestionFocusOnStepByStepActions => ReportProblemPane[0].FindFirstDescendant(cf => cf.ByControlType(ControlType.Text).And(cf.ByName("What were the step-by-step actions you took that led to this happening?"))).AsTextBox();
private Button BridgeIsRunningSlowButton => HomeButtons[10].AsButton();
private TextBox StepByStepActionsForBridgeIsSlow => ReportProblemPane[2].FindFirstDescendant(cf => cf.ByControlType(ControlType.Edit)).AsTextBox();
private CheckBox ExperiencingIssues => ReportProblemPane[0].FindFirstDescendant(cf => cf.ByControlType(ControlType.CheckBox).And(cf.ByName("Emails arrive with a delay"))).AsCheckBox();
private Button SomethingElseButton => HomeButtons[11].AsButton();
private TextBox StepByStepActionsForSomethingElse => ReportProblemPane[3].AsTextBox();
private TextBox IssuesLastOccurenceInSomethingElseProblemSection => ReportProblemPane[4].AsTextBox();
private TextBox OverviewOfReportProblemDetails => ReportProblemPane[1].FindFirstDescendant(cf => cf.ByControlType(ControlType.Edit)).AsTextBox();
private TextBox ContactEmailInReportProblem => ReportProblemPane[0].FindAllDescendants(cf => cf.ByControlType(ControlType.Edit)).ToList()[1].AsTextBox();
public CheckBox IncludeLogs => ReportProblemPane[0].FindFirstDescendant(cf => cf.ByControlType(ControlType.CheckBox)).AsCheckBox();
public HelpMenuWindow ClickHelpButton()
{
HelpButton.Click();
return this;
}
public HelpMenuWindow ClickBackFromHelpMenu()
{
BackToAccountViewButton.Click();
return this;
}
public HelpMenuWindow ClickGoToHelpTopics()
{
GoToHelpTopics.Click();
return this;
}
public HelpMenuWindow ClickCheckNowButton()
{
CheckNow.Click();
return this;
}
public HelpMenuWindow ConfirmNotification()
{
ConfirmNotificationButton.Click();
return this;
}
public HelpMenuWindow ClickLogsButton()
{
LogsButton.Click();
return this;
}
public HelpMenuWindow ClickReportProblemButton()
{
ReportProblemButton.Click();
return this;
}
public HelpMenuWindow ClickICannotFindEmailsInEmailClient()
{
ICannotFindEmailInClient.Click();
return this;
}
public HelpMenuWindow ClickToFillQuestionDescription()
{
DescriptionOnWhatHappened.Click();
return this;
}
public HelpMenuWindow ClickNotAbleToSendEmails()
{
NotAbleToSendEmailsButton.Click();
return this;
}
public HelpMenuWindow ClickBridgeIsNotStartingCorrectly()
{
BridgeIsNotStartingCorrectlyButton.Click();
return this;
}
public HelpMenuWindow ClickBridgeIsRunningSlow()
{
BridgeIsRunningSlowButton.Click();
return this;
}
public HelpMenuWindow ClickSomethingElse()
{
SomethingElseButton.Click();
return this;
}
public HelpMenuWindow EnterMissingEmailsProblemDetails()
{
DescriptionOnWhatHappened.Enter("I am missing emails in my email client.");
MissingEmails.Click();
FindEmails.Click();
VPNSoftware.IsChecked = true;
FirewallSoftware.IsChecked = true;
Mouse.Scroll(-20);
Wait.UntilInputIsProcessed(TimeSpan.FromSeconds(1));
ContinueToReportButton.Click();
VerifyOverviewOfMissingEmailsDetails();
VerifyContactEmail();
VerifyIncludeLogsIsChecked();
SendReportButton.Click();
Wait.UntilInputIsProcessed(TimeSpan.FromSeconds(5));
return this;
}
public HelpMenuWindow EnterNotAbleToSendEmailProblemDetails()
{
DescriptionOnWhatHappened.Enter("I am not able to send emails.");
StepByStepActionsForBridgeIsNotStartingCorrectly.Enter("I compose a message, I click Send and I get an error that the message cannot be sent.");
IssuesLastOccurence.Enter("It happened this morning for the first time.");
QuestionFocusWhenWasLastOccurence.Click();
VPNSoftware.IsChecked = true;
FirewallSoftware.IsChecked = true;
Mouse.Scroll(-20);
Wait.UntilInputIsProcessed(TimeSpan.FromSeconds(1));
ContinueToReportButton.Click();
VerifyOverviewOfNotAbleToSendEmailsDetails();
VerifyContactEmail();
VerifyIncludeLogsIsChecked();
SendReportButton.Click();
Wait.UntilInputIsProcessed(TimeSpan.FromSeconds(5));
return this;
}
public HelpMenuWindow EnterBridgeIsNotStartingCorrectlyProblemDetails()
{
DescriptionOnWhatHappened.Enter("Bridge is not starting correctly.");
StepByStepActionsForBridgeIsNotStartingCorrectly.Enter("I turned on my device, and Bridge couldn't launch, I received an error.");
IssuesLastOccurence.Enter("It occured today for the first time and I cannot fix it.");
QuestionFocusWhenWasLastOccurence.Click();
VPNSoftware.IsChecked = true;
FirewallSoftware.IsChecked = true;
Mouse.Scroll(-20);
Wait.UntilInputIsProcessed(TimeSpan.FromSeconds(1));
ContinueToReportButton.Click();
VerifyOverviewOfBridgeNotStartingCorrectlyDetails();
VerifyContactEmail();
VerifyIncludeLogsIsChecked();
SendReportButton.Click();
Wait.UntilInputIsProcessed(TimeSpan.FromSeconds(5));
return this;
}
public HelpMenuWindow EnterBridgeIsRunningSlowProblemDetails()
{
DescriptionOnWhatHappened.Enter("Bridge is really slow.");
StepByStepActionsForBridgeIsSlow.Enter("I started Bridge, added an account and the sync takes forever.");
ExperiencingIssues.IsChecked = true;
VPNSoftware.IsChecked = true;
FirewallSoftware.IsChecked = true;
Mouse.Scroll(-20);
Wait.UntilInputIsProcessed(TimeSpan.FromSeconds(1));
ContinueToReportButton.Click();
VerifyOverviewOfBridgeIsRunningSlowDetails();
VerifyContactEmail();
VerifyIncludeLogsIsChecked();
SendReportButton.Click();
Wait.UntilInputIsProcessed(TimeSpan.FromSeconds(5));
return this;
}
public HelpMenuWindow EnterSomethingElseProblemDetails()
{
DescriptionOnWhatHappened.Enter("I don't receive emails.");
StepByStepActionsForBridgeIsSlow.Enter("I am expecting an email that is sent, but it hasn't arrived in my Inbox.");
StepByStepActionsForSomethingElse.Enter("I click Get messages, but the emails that are sent to me do not arrive.");
QuestionFocusOnStepByStepActions.Click();
Mouse.Scroll(-20);
IssuesLastOccurenceInSomethingElseProblemSection.Enter("Issue started happening today.");
QuestionFocusWhenWasLastOccurence.Click();
ContinueToReportButton.Click();
VerifyOverviewOfSomethingElseDetails();
VerifyContactEmail();
VerifyIncludeLogsIsChecked();
SendReportButton.Click();
Wait.UntilInputIsProcessed(TimeSpan.FromSeconds(5));
return this;
}
public HelpMenuWindow VerifyOverviewOfMissingEmailsDetails()
{
Assert.That(OverviewOfReportProblemDetails.Text, Does.Contain("Please describe what happened and include any error messages.\nI am missing emails in my email client.\nAre you missing emails from the email client or not receiving new ones?\nOld emails are missing\nCan you find the emails in the web/mobile application?\nYes\nAre you running any of these software? Select all that apply.\nVPN, Firewall"));
return this;
}
public HelpMenuWindow VerifyOverviewOfNotAbleToSendEmailsDetails()
{
Assert.That(OverviewOfReportProblemDetails.Text, Does.Contain("Please describe what happened and include any error messages.\nI am not able to send emails.\nWhat were the step-by-step actions you took that led to this happening?\nI compose a message, I click Send and I get an error that the message cannot be sent.\nWhen did the issue last occur? Is it repeating?\nIt happened this morning for the first time.\nAre you running any of these software? Select all that apply.\nVPN, Firewall"));
return this;
}
public HelpMenuWindow VerifyOverviewOfBridgeNotStartingCorrectlyDetails()
{
Assert.That(OverviewOfReportProblemDetails.Text, Does.Contain("Please describe what happened and include any error messages.\nBridge is not starting correctly.\nWhat were the step-by-step actions you took that led to this happening?\nI turned on my device, and Bridge couldn't launch, I received an error.\nWhen did the issue last occur? Is it repeating?\nIt occured today for the first time and I cannot fix it.\nAre you running any of these software? Select all that apply.\nVPN, Firewall"));
return this;
}
public HelpMenuWindow VerifyOverviewOfBridgeIsRunningSlowDetails()
{
Assert.That(OverviewOfReportProblemDetails.Text, Does.Contain("Please describe what happened and include any error messages.\nBridge is really slow.\nWhat were the step-by-step actions you took that led to this happening?\nI started Bridge, added an account and the sync takes forever.\nWhich of these issues are you experiencing?\nEmails arrive with a delay\nAre you running any of these software? Select all that apply.\nVPN, Firewall"));
return this;
}
public HelpMenuWindow VerifyOverviewOfSomethingElseDetails()
{
Assert.That(OverviewOfReportProblemDetails.Text, Does.Contain("Please describe what happened and include any error messages.\nI don't receive emails.\nWhat did you want or expect to happen?\nI am expecting an email that is sent, but it hasn't arrived in my Inbox.\nWhat were the step-by-step actions you took that led to this happening?\nI click Get messages, but the emails that are sent to me do not arrive.\nWhen did the issue last occur? Is it repeating?\nIssue started happening today."));
return this;
}
public HelpMenuWindow VerifyContactEmail()
{
Assert.That(ContactEmailInReportProblem.Text, Is.EqualTo(TestUserData.GetPaidUser().Username));
return this;
}
public HelpMenuWindow VerifyIncludeLogsIsChecked()
{
Assert.That(IncludeLogs.IsChecked, Is.True);
return this;
}
}
}

View File

@ -1,8 +1,5 @@
using FlaUI.Core.AutomationElements;
using FlaUI.Core.Definitions;
using FlaUI.Core.Conditions;
using FlaUI.Core.Input;
using ProtonMailBridge.UI.Tests.TestsHelper;
using System;
@ -11,14 +8,10 @@ namespace ProtonMailBridge.UI.Tests.Windows
public class HomeWindow : UIActions
{
private AutomationElement[] AccountViewButtons => AccountView.FindAllChildren(cf => cf.ByControlType(ControlType.Button));
private AutomationElement[] HomeButtons => Window.FindAllDescendants(cf => cf.ByControlType(ControlType.Button));
private Button AddNewAccountButton => HomeButtons[6].AsButton();
private Button RemoveAccountButton => AccountViewButtons[1].AsButton();
private AutomationElement RemoveAccountConfirmModal => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Window));
private Button ConfirmRemoveAccountButton => RemoveAccountConfirmModal.FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("Remove this account"))).AsButton();
private Button SignOutButton => AccountView.FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("Sign out"))).AsButton();
private Button SignInButton => AccountView.FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("Sign in"))).AsButton();
public HomeWindow RemoveAccount()
{
try
@ -32,23 +25,10 @@ namespace ProtonMailBridge.UI.Tests.Windows
}
return this;
}
public HomeWindow AddNewAccount ()
{
AddNewAccountButton.Click();
return this;
}
public HomeWindow SignOutAccount()
{
SignOutButton.Click();
return this;
}
public HomeWindow ClickSignInMainWindow()
{
SignInButton.Click();
return this;
}
}
}

View File

@ -2,8 +2,6 @@
using FlaUI.Core.Input;
using FlaUI.Core.Definitions;
using ProtonMailBridge.UI.Tests.TestsHelper;
using ProtonMailBridge.UI.Tests.Results;
using System.Diagnostics;
namespace ProtonMailBridge.UI.Tests.Windows
{
@ -13,32 +11,14 @@ namespace ProtonMailBridge.UI.Tests.Windows
private TextBox UsernameInput => InputFields[0].AsTextBox();
private TextBox PasswordInput => InputFields[1].AsTextBox();
private Button SignInButton => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("Sign in"))).AsButton();
private Button SigningInButton => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("Signing in"))).AsButton();
private Button StartSetupButton => Window.FindFirstDescendant(cf => cf.ByName("Start setup")).AsButton();
private Button SetUpLater => Window.FindFirstDescendant(cf => cf.ByName("Setup later")).AsButton();
private TextBox MailboxPasswordInput => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Edit)).AsTextBox();
private Button UnlockButton => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("Unlock"))).AsButton();
private Button CancelSignIn => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("Cancel"))).AsButton();
public LoginWindow SignIn(TestUserData user)
{
ClickStartSetupButton();
EnterCredentials(user);
WaitForAuthorizationToComplete(60);
SetUpLater?.Click();
return this;
}
public LoginWindow SignInMailbox(TestUserData user)
{
ClickStartSetupButton();
EnterCredentials(user);
Wait.UntilInputIsProcessed(TestData.TenSecondsTimeout);
EnterMailboxPassword(user);
Wait.UntilInputIsProcessed(TestData.TenSecondsTimeout);
SetUpLater?.Click();
return this;
@ -50,6 +30,7 @@ namespace ProtonMailBridge.UI.Tests.Windows
SignIn(user);
return this;
}
public LoginWindow ClickStartSetupButton()
{
StartSetupButton?.Click();
@ -64,38 +45,5 @@ namespace ProtonMailBridge.UI.Tests.Windows
SignInButton.Click();
return this;
}
public LoginWindow EnterMailboxPassword(TestUserData user)
{
MailboxPasswordInput.Text = user.MailboxPassword;
UnlockButton.Click();
return this;
}
public LoginWindow ClickCancelToSignIn ()
{
CancelSignIn.Click();
return this;
}
private void WaitForAuthorizationToComplete(int numOfSeconds)
{
TimeSpan timeout = TimeSpan.FromSeconds(numOfSeconds);
Stopwatch stopwatch = Stopwatch.StartNew();
while (stopwatch.Elapsed < timeout)
{
//if Signing in button is not visible authorization process is finished
if (SigningInButton == null)
{
return;
}
Wait.UntilInputIsProcessed();
Thread.Sleep(500);
}
}
}
}

View File

@ -20,15 +20,11 @@ package tests
import (
"bufio"
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/cucumber/godog"
)
@ -120,7 +116,7 @@ func (s *scenario) checkParsedMultipartFormForFile(method, path, file string, ha
}
if _, ok := req.MultipartForm.File[file]; hasFile != ok {
return fmt.Errorf("multipart file in bug report is %t, want it to be %t", ok, hasFile)
return fmt.Errorf("Multipart file in bug report is %t, want it to be %t", ok, hasFile)
}
return nil
@ -244,57 +240,3 @@ func (s *scenario) theMessageUsedKeyForSending(address string) error {
return nil
}
func (s *scenario) theKeyForAddressWasUsedToImport(address string) error {
// Response does not include the address ID, only the messageID, so we extract the messageID from the response body
call, err := s.t.getLastCallExcludingHTTPOverride("POST", "/mail/v4/messages/import")
if err != nil {
return err
}
var resp struct {
Responses []struct{ Response struct{ MessageID string } }
}
if err := json.Unmarshal(call.ResponseBody, &resp); err != nil {
return err
}
return s.checkMessageIsEncryptedForAddress(resp.Responses[0].Response.MessageID, address)
}
func (s *scenario) theKeyForAddressWasUsedToCreateDraft(address string) error {
call, err := s.t.getLastCallExcludingHTTPOverride("POST", "/mail/v4/messages")
if err != nil {
return err
}
var resp struct{ Message struct{ ID string } }
if err := json.Unmarshal(call.ResponseBody, &resp); err != nil {
return err
}
return s.checkMessageIsEncryptedForAddress(resp.Message.ID, address)
}
func (s *scenario) checkMessageIsEncryptedForAddress(messageID string, address string) error {
user := s.t.getUserByAddress(address)
addrID := user.getAddrID(address)
return s.t.withClient(context.Background(), user.getName(), func(ctx context.Context, client *proton.Client) error {
message, err := client.GetMessage(ctx, messageID)
if err != nil {
return err
}
// Check 1: the address associated with the message is the one we expect.
if message.AddressID != addrID {
return errors.New("the message is not encrypted with the specified address")
}
// Check 2: we indeed encrypted the message with this address' key ring.
return s.t.withAddrKR(ctx, client, user.name, addrID, func(_ context.Context, kr *crypto.KeyRing) error {
_, err := message.Decrypt(kr)
return err
})
})
}

View File

@ -1,118 +0,0 @@
// Copyright (c) 2024 Proton AG
//
// This file is part of Proton Mail Bridge.Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package tests
import (
"encoding/json"
"fmt"
"mime"
"net/mail"
"strings"
"github.com/ProtonMail/gluon/rfc822"
"github.com/ProtonMail/proton-bridge/v3/pkg/message"
"github.com/ProtonMail/proton-bridge/v3/pkg/message/parser"
GmailService "github.com/ProtonMail/proton-bridge/v3/tests/utils/gmail"
"github.com/cucumber/godog"
)
func (s *scenario) externalClientSendsTheFollowingMessageFromTo(from, to string, message *godog.DocString) error {
return GmailService.ExternalSendEmail(from, to, message)
}
func (s *scenario) externalClientFetchesTheFollowingMessage(subject, sender, state string) error {
err := eventually(func() error {
_, err := GmailService.FetchMessageBySubjectAndSender(subject, sender, state)
return err
})
return err
}
func (s *scenario) externalClientSeesMessageWithStructure(subject, sender, state string, message *godog.DocString) error {
err := eventually(func() error {
gmailMessage, err := GmailService.FetchMessageBySubjectAndSender(subject, sender, state)
if err != nil {
return err
}
var msgStruct MessageStruct
if err := json.Unmarshal([]byte(message.Content), &msgStruct); err != nil {
return err
}
parsedMessage, err := GmailService.GetRawMessage(gmailMessage)
if err != nil {
return err
}
var structs []MessageStruct
messageStruct := parseGmail(parsedMessage)
structs = append(structs, messageStruct)
return matchStructureRecursive(structs, msgStruct)
})
return err
}
func (s *scenario) externalClientDeletesAllMessages() {
GmailService.DeleteAllMessages()
}
func parseGmail(rawMsg string) MessageStruct {
msg, err := mail.ReadMessage(strings.NewReader(rawMsg))
if err != nil {
panic(err)
}
var dec mime.WordDecoder
decodedSubject, err := dec.DecodeHeader(msg.Header.Get("Subject"))
if err != nil {
decodedSubject = msg.Header.Get("Subject")
}
parser, err := parser.New(strings.NewReader(rawMsg))
if err != nil {
panic(fmt.Errorf("parser error: %e", err))
}
m, err := message.ParseWithParser(parser, true)
if err != nil {
panic(fmt.Errorf("parser with parser: %e", err))
}
var body string
switch {
case m.MIMEType == rfc822.TextPlain:
body = strings.TrimSpace(string(m.PlainBody))
case m.MIMEType == rfc822.MultipartMixed:
_, body, _ = strings.Cut(string(m.MIMEBody), "\r\n\r\n")
default:
body = strings.TrimSpace(string(m.RichBody))
}
// There might be an issue with the dates if we end up using them
return MessageStruct{
From: msg.Header.Get("From"),
To: msg.Header.Get("To"),
CC: msg.Header.Get("CC"),
BCC: msg.Header.Get("BCC"),
Subject: decodedSubject,
Date: msg.Header.Get("Date"),
Content: parseMessageSection([]byte(strings.TrimSpace(rawMsg)), strings.TrimSpace(body)),
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,283 +0,0 @@
@gmail-integration
Feature: External sender to Proton recipient sending a plain text message
Background:
Given there exists an account with username "[user:user]" and password "password"
Then it succeeds
When bridge starts
And the user logs in with username "[user:user]" and password "password"
Then it succeeds
Scenario: Plain text message sent from External to Internal
Given external client sends the following message from "auto.bridge.qa@gmail.com" to "[user:user]@[domain]":
"""
From: <auto.bridge.qa@gmail.com>
To: <[user:user]@[domain]>
Content-Type: text/plain; charset=UTF-8; format=flowed
Content-Transfer-Encoding: 8bit
Subject: Hello World!
hello
"""
Then it succeeds
When user "[user:user]" connects and authenticates IMAP client "1"
Then IMAP client "1" eventually sees the following messages in "Inbox":
| from | to | subject | body |
| auto.bridge.qa@gmail.com | [user:user]@[domain] | Hello World! | hello |
And IMAP client "1" eventually sees the following message in "Inbox" with this structure:
"""
{
"from": "auto.bridge.qa@gmail.com",
"to": "[user:user]@[domain]",
"subject": "Hello World!",
"content": {
"content-type": "text/plain",
"content-type-charset": "utf-8",
"transfer-encoding": "quoted-printable",
"body-is": "hello"
}
}
"""
Scenario: Plain message with Foreign/Nonascii chars in Subject and Body from External to Internal
Given external client sends the following message from "auto.bridge.qa@gmail.com" to "[user:user]@[domain]":
"""
To: <[user:user]@[domain]>
From: Bridge Automation <auto.bridge.qa@gmail.com>
Subject: =?UTF-8?B?U3Vias61zq3Pgs+EIMK2IMOEIMOI?=
Content-Type: text/plain; charset=UTF-8; format=flowed
Content-Transfer-Encoding: 8bit
Subjεέςτ Ä È
"""
Then it succeeds
When user "[user:user]" connects and authenticates IMAP client "1"
Then IMAP client "1" eventually sees the following messages in "Inbox":
| from | to | subject | body |
| auto.bridge.qa@gmail.com | [user:user]@[domain] | Subjεέςτ Ä È | Subjεέςτ Ä È |
And IMAP client "1" eventually sees the following message in "Inbox" with this structure:
"""
{
"from": "auto.bridge.qa@gmail.com",
"to": "[user:user]@[domain]",
"subject": "Subjεέςτ Ä È",
"content": {
"content-type": "text/plain",
"content-type-charset": "utf-8",
"transfer-encoding": "quoted-printable",
"body-is": "Subjεέςτ Ä È"
}
}
"""
Scenario: Plain message with numbering/ordering in Body from External to Internal
Given external client sends the following message from "auto.bridge.qa@gmail.com" to "[user:user]@[domain]":
"""
To: <[user:user]@[domain]>
From: Bridge Automation <auto.bridge.qa@gmail.com>
Subject: Message with Numbering/Ordering in Body
Content-Type: text/plain; charset=UTF-8; format=flowed
Content-Transfer-Encoding: 7bit
**Ordering
* *Bullet*1
o Bullet 1.1
* Bullet 2
o Bullet 2.1
o *Bullet 2.2*
+ /Bullet 2.2.1/
o Bullet 2.3
* */Bullet 3/*
Numbering
1. *Number 1*
1. */Number/**1.1*
2. Number 2
1. */Number 2.1/*
2. Number 2.2
1. Number 2.2.1
3. Number 2.3
3. /Number 3/
End
"""
Then it succeeds
When user "[user:user]" connects and authenticates IMAP client "1"
Then IMAP client "1" eventually sees the following messages in "Inbox":
| from | to | subject |
| auto.bridge.qa@gmail.com | [user:user]@[domain] | Message with Numbering/Ordering in Body |
And IMAP client "1" eventually sees the following message in "Inbox" with this structure:
"""
{
"from": "auto.bridge.qa@gmail.com",
"to": "[user:user]@[domain]",
"subject": "Message with Numbering/Ordering in Body",
"content": {
"content-type": "text/plain",
"content-type-charset": "utf-8",
"transfer-encoding": "quoted-printable",
"body-is": "**Ordering\r\n\r\n* *Bullet*1\r\n o Bullet 1.1\r\n* Bullet 2\r\n o Bullet 2.1\r\n o *Bullet 2.2*\r\n + /Bullet 2.2.1/\r\n o Bullet 2.3\r\n* */Bullet 3/*\r\n\r\nNumbering\r\n\r\n1. *Number 1*\r\n 1. */Number/**1.1*\r\n2. Number 2\r\n\r\n 1. */Number 2.1/*\r\n 2. Number 2.2\r\n 1. Number 2.2.1\r\n 3. Number 2.3\r\n\r\n3. /Number 3/\r\n\r\nEnd"
}
}
"""
Scenario: Plain text message with multiple attachments from External to Internal
Given external client sends the following message from "auto.bridge.qa@gmail.com" to "[user:user]@[domain]":
"""
Content-Type: multipart/mixed; boundary="------------WI90RPIYF20K6dGXjs7dm2mi"
Subject: Plain message with different attachments
This is a multi-part message in MIME format.
--------------WI90RPIYF20K6dGXjs7dm2mi
Content-Type: text/plain; charset=UTF-8;
Content-Transfer-Encoding: 7bit
Hello, this is a Plain message with different attachments.
--------------WI90RPIYF20K6dGXjs7dm2mi
Content-Type: text/html; charset=UTF-8; name="index.html"
Content-Disposition: attachment; filename="index.html"
Content-Transfer-Encoding: base64
PCFET0NUWVBFIGh0bWw+
--------------WI90RPIYF20K6dGXjs7dm2mi
Content-Type: text/xml; charset=UTF-8; name="testxml.xml"
Content-Disposition: attachment; filename="testxml.xml"
Content-Transfer-Encoding: base64
PD94bWwgdmVyc2lvbj0iMS4xIj8+PCFET0NUWVBFIF9bPCFFTEVNRU5UIF8gRU1QVFk+XT48
Xy8+
--------------WI90RPIYF20K6dGXjs7dm2mi
Content-Type: text/plain; charset=UTF-8; name="text file.txt"
Content-Disposition: attachment; filename="text file.txt"
Content-Transfer-Encoding: base64
dGV4dCBmaWxl
--------------WI90RPIYF20K6dGXjs7dm2mi
"""
Then it succeeds
When user "[user:user]" connects and authenticates IMAP client "1"
Then IMAP client "1" eventually sees the following messages in "Inbox":
| from | to | subject |
| auto.bridge.qa@gmail.com | [user:user]@[domain] | Plain message with different attachments |
And IMAP client "1" eventually sees the following message in "Inbox" with this structure:
"""
{
"from": "auto.bridge.qa@gmail.com",
"to": "[user:user]@[domain]",
"subject": "Plain message with different attachments",
"content": {
"content-type": "multipart/mixed",
"sections": [
{
"content-type": "text/plain",
"content-type-charset": "utf-8",
"transfer-encoding": "quoted-printable",
"body-is": "Hello, this is a Plain message with different attachments."
},
{
"content-type": "text/plain",
"content-type-name": "text file.txt",
"content-disposition": "attachment",
"content-disposition-filename": "text file.txt",
"transfer-encoding": "base64",
"body-is": "dGV4dCBmaWxl"
},
{
"content-type": "text/html",
"content-type-name": "index.html",
"content-disposition": "attachment",
"content-disposition-filename": "index.html",
"transfer-encoding": "base64",
"body-is": "PCFET0NUWVBFIGh0bWw+"
},
{
"content-type": "text/xml",
"content-type-name": "testxml.xml",
"content-disposition": "attachment",
"content-disposition-filename": "testxml.xml",
"transfer-encoding": "base64",
"body-is": "PD94bWwgdmVyc2lvbj0iMS4xIj8+PCFET0NUWVBFIF9bPCFFTEVNRU5UIF8gRU1QVFk+XT48Xy8+"
}
]
}
}
"""
Scenario: Plain message with multiple inline images from External to Internal
Given external client sends the following message from "auto.bridge.qa@gmail.com" to "[user:user]@[domain]":
"""
To: <[user:user]@[domain]>
From: <auto.bridge.qa@gmail.com>
Subject: Plain message with multiple inline images to Internal
Content-Type: text/plain; charset=UTF-8; format=flowed
Content-Transfer-Encoding: 7bit
Plain message with image 1 multiple image 2 inline image 3 images.
"""
Then it succeeds
When user "[user:user]" connects and authenticates IMAP client "1"
Then IMAP client "1" eventually sees the following messages in "Inbox":
| from | to | subject |
| auto.bridge.qa@gmail.com | [user:user]@[domain] | Plain message with multiple inline images to Internal |
And IMAP client "1" eventually sees the following message in "Inbox" with this structure:
"""
{
"from": "auto.bridge.qa@gmail.com",
"to": "[user:user]@[domain]",
"subject": "Plain message with multiple inline images to Internal",
"content": {
"content-type": "text/plain",
"content-type-charset": "utf-8",
"transfer-encoding": "quoted-printable",
"body-is": "Plain message with image 1 multiple image 2 inline image 3 images.",
"body-contains": "",
"sections": []
}
}
"""
Scenario: Plain text message with a large attachment from External to Internal
Given external client sends the following message from "auto.bridge.qa@gmail.com" to "[user:user]@[domain]":
"""
Content-Type: multipart/mixed; boundary="------------k0Z3FJiZsGaSFqdJGsr0Oml6"
To: <[user:user]@[domain]>
From: Bridge Automation <auto.bridge.qa@gmail.com>
Subject: Plain message with a large attachment
This is a multi-part message in MIME format.
--------------k0Z3FJiZsGaSFqdJGsr0Oml6
Content-Type: text/plain; charset=UTF-8; format=flowed
Content-Transfer-Encoding: 7bit
Hello, this is a plain message with a large attachment.
--------------k0Z3FJiZsGaSFqdJGsr0Oml6
Content-Type: application/msword; name="testDoc.doc"
Content-Disposition: attachment; filename="testDoc.doc"
Content-Transfer-Encoding: base64
--------------k0Z3FJiZsGaSFqdJGsr0Oml6--
"""
Then it succeeds
When user "[user:user]" connects and authenticates IMAP client "1"
Then IMAP client "1" eventually sees the following messages in "Inbox":
| from | to | subject |
| auto.bridge.qa@gmail.com | [user:user]@[domain] | Plain message with a large attachment |
And IMAP client "1" eventually sees the following message in "Inbox" with this structure:
"""
{
"from": "auto.bridge.qa@gmail.com",
"to": "[user:user]@[domain]",
"subject": "Plain message with a large attachment",
"content": {
"content-type": "text/plain",
"content-type-charset": "utf-8"
}
}
"""

File diff suppressed because one or more lines are too long

View File

@ -1,136 +0,0 @@
Feature: IMAP import messages
Background:
Given there exists an account with username "[user:user]" and password "password"
And the account "[user:user]" has additional address "[alias:secondary]@[domain]"
And the account "[user:user]" has additional disabled address "[alias:disabled]@[domain]"
Then it succeeds
When bridge starts
And the user logs in with username "[user:user]" and password "password"
And user "[user:user]" finishes syncing
And user "[user:user]" connects and authenticates IMAP client "1"
Then it succeeds
@skip-black
Scenario: Messages imported with default address as sender are encrypted with the default address key
When IMAP client "1" appends the following message to "INBOX":
"""
From: Bridge Test <[user:user]@[domain]>
Date: 01 Jan 1980 00:00:00 +0000
To: Internal Bridge <bridgetest@example.com>
Received: by 2002:0:0:0:0:0:0:0 with SMTP id 0123456789abcdef; Wed, 30 Dec 2020 01:23:45 0000
Subject: Basic text/plain message
Content-Type: text/plain
Hello
"""
Then it succeeds
And the key for address "[user:user]@[domain]" was used to import
@skip-black
Scenario: Messages imported with alias as sender are encrypted with secondary address key
When IMAP client "1" appends the following message to "INBOX":
"""
From: Bridge Test <[alias:secondary]@[domain]>
Date: 01 Jan 1980 00:00:00 +0000
To: Internal Bridge <bridgetest@example.com>
Received: by 2002:0:0:0:0:0:0:0 with SMTP id 0123456789abcdef; Wed, 30 Dec 2020 01:23:45 0000
Subject: Basic text/plain message
Content-Type: text/plain
Hello
"""
Then it succeeds
And the key for address "[alias:secondary]@[domain]" was used to import
@skip-black
Scenario: Messages imported with a disabled alias as sender are encrypted with the disabled address key
When IMAP client "1" appends the following message to "INBOX":
"""
From: Bridge Test <[alias:disabled]@[domain]>
Date: 01 Jan 1980 00:00:00 +0000
To: Internal Bridge <bridgetest@example.com>
Received: by 2002:0:0:0:0:0:0:0 with SMTP id 0123456789abcdef; Wed, 30 Dec 2020 01:23:45 0000
Subject: Basic text/plain message
Content-Type: text/plain
Hello
"""
Then it succeeds
And the key for address "[alias:disabled]@[domain]" was used to import
@skip-black
Scenario: Messages imported with an unknown address as sender are encrypted with primary address key
When IMAP client "1" appends the following message to "INBOX":
"""
From: Bridge Test <bridgeqa@example.com>
Date: 01 Jan 1980 00:00:00 +0000
To: Internal Bridge <bridgetest@example.com>
Received: by 2002:0:0:0:0:0:0:0 with SMTP id 0123456789abcdef; Wed, 30 Dec 2020 01:23:45 0000
Subject: Basic text/plain message
Content-Type: text/plain
Hello
"""
Then it succeeds
And the key for address "[user:user]@[domain]" was used to import
@skip-black
Scenario: Drafts imported with default address as sender are encrypted with the default address key
When IMAP client "1" appends the following message to "Drafts":
"""
From: Bridge Test <[user:user]@[domain]>
Date: 01 Jan 1980 00:00:00 +0000
To: Internal Bridge <bridgetest@example.com>
Subject: Basic text/plain message
Content-Type: text/plain
Hello
"""
Then it succeeds
And the key for address "[user:user]@[domain]" was used to create draft
@skip-black
Scenario: Drafts imported with alias as sender are encrypted with secondary key
When IMAP client "1" appends the following message to "Drafts":
"""
From: Bridge Test <[alias:secondary]@[domain]>
Date: 01 Jan 1980 00:00:00 +0000
To: Internal Bridge <bridgetest@example.com>
Subject: Basic text/plain message
Content-Type: text/plain
Hello
"""
Then it succeeds
And the key for address "[alias:secondary]@[domain]" was used to create draft
@skip-black
Scenario: Drafts imported with a disabled alias as sender are encrypted with the disabled address key
When IMAP client "1" appends the following message to "Drafts":
"""
From: Bridge Test <[alias:disabled]@[domain]>
Date: 01 Jan 1980 00:00:00 +0000
To: Internal Bridge <bridgetest@example.com>
Subject: Basic text/plain message
Content-Type: text/plain
Hello
"""
Then it succeeds
And the key for address "[user:user]@[domain]" was used to create drafts
@skip-black
Scenario: Drafts imported with an unknown address as sender are encrypted with primary address key
When IMAP client "1" appends the following message to "Drafts":
"""
From: Bridge Test <bridgeqa@example.com>
Date: 01 Jan 1980 00:00:00 +0000
To: Internal Bridge <bridgetest@example.com>
Subject: Basic text/plain message
Content-Type: text/plain
Hello
"""
Then it succeeds
And the key for address "[user:user]@[domain]" was used to create draft

View File

@ -35,15 +35,4 @@ Feature: Bridge send remote notification observability metrics
And the user with username "[user:user1]" sends all possible sync message building success observability metrics
Then it succeeds
Scenario: Test all possible SMTP error observability metrics
When the user logs in with username "[user:user1]" and password "password"
And the user with username "[user:user1]" sends all possible SMTP error observability metrics
Then it succeeds
Scenario: Test SMTP send success observability metrics
When the user logs in with username "[user:user1]" and password "password"
And the user with username "[user:user1]" sends SMTP send success observability metric
Then it succeeds

View File

@ -1,11 +0,0 @@
Feature: Bridge send remote notification observability metrics
Background:
Given there exists an account with username "[user:user1]" and password "password"
Then it succeeds
When bridge starts
Then it succeeds
Scenario: Test all possible gluon error observability metrics
When the user logs in with username "[user:user1]" and password "password"
And the user with username "[user:user1]" sends all possible gluon error observability metrics
Then it succeeds

View File

@ -21,7 +21,6 @@ import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"os"
@ -659,7 +658,7 @@ func (s *scenario) imapClientsMoveMessageWithSubjectUserFromToByOrderedOperation
case "EXPUNGE":
expungeErr = sourceClient.Expunge(nil)
default:
return errors.New("unknown IMAP operation " + op)
return fmt.Errorf("unknown IMAP operation " + op)
}
time.Sleep(100 * time.Millisecond)
}

View File

@ -19,46 +19,26 @@ package tests
import (
"context"
"fmt"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice/observabilitymetrics/evtloopmsgevents"
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice/observabilitymetrics/syncmsgevents"
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
smtpMetrics "github.com/ProtonMail/proton-bridge/v3/internal/services/smtp/observabilitymetrics"
"github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice/observabilitymetrics"
)
// userHeartbeatPermutationsObservability corresponds to bridge_generic_user_heartbeat_total_v1.schema.json.
// userHeartbeatPermutationsObservability - corresponds to bridge_generic_user_heartbeat_total_v1.schema.json.
func (s *scenario) userHeartbeatPermutationsObservability(username string) error {
const batchSize = 1000
metrics := observability.GenerateAllHeartbeatMetricPermutations()
metricLen := len(metrics)
return s.t.withClientPass(context.Background(), username, s.t.getUserByName(username).userPass, func(ctx context.Context, c *proton.Client) error {
for i := 0; i < len(metrics); i += batchSize {
end := i + batchSize
if end > metricLen {
end = metricLen
}
batch := proton.ObservabilityBatch{Metrics: metrics[i:end]}
if err := c.SendObservabilityBatch(ctx, batch); err != nil {
return err
}
}
return nil
batch := proton.ObservabilityBatch{Metrics: metrics}
return c.SendObservabilityBatch(ctx, batch)
})
}
// userDistinctionMetricsPermutationsObservability corresponds to:
// - bridge_sync_errors_users_total_v1.schema.json
// - bridge_gluon_imap_errors_users_total_v1.schema.json
// - bridge_gluon_message_errors_users_total_v1.schema.json
// - bridge_gluon_other_errors_users_total_v1.schema.json
// - bridge_event_loop_events_errors_users_total_v1.schema.json.
// - bridge_smtp_errors_users_total_v1.schema.json
// userDistinctionMetricsPermutationsObservability - corresponds to:
// bridge_sync_errors_users_total_v1.schema.json
// bridge_event_loop_events_errors_users_total_v1.schema.json.
func (s *scenario) userDistinctionMetricsPermutationsObservability(username string) error {
batch := proton.ObservabilityBatch{
Metrics: observability.GenerateAllUsedDistinctionMetricPermutations()}
@ -68,7 +48,7 @@ func (s *scenario) userDistinctionMetricsPermutationsObservability(username stri
})
}
// syncFailureMessageEventsObservability corresponds to bridge_sync_message_event_failures_total_v1.schema.json.
// syncFailureMessageEventsObservability - corresponds to bridge_sync_message_event_failures_total_v1.schema.json.
func (s *scenario) syncFailureMessageEventsObservability(username string) error {
batch := proton.ObservabilityBatch{
Metrics: []proton.ObservabilityMetric{
@ -82,7 +62,7 @@ func (s *scenario) syncFailureMessageEventsObservability(username string) error
})
}
// eventLoopFailureMessageEventsObservability corresponds to bridge_event_loop_message_event_failures_total_v1.schema.json.
// eventLoopFailureMessageEventsObservability - corresponds to bridge_event_loop_message_event_failures_total_v1.schema.json.
func (s *scenario) eventLoopFailureMessageEventsObservability(username string) error {
batch := proton.ObservabilityBatch{
Metrics: []proton.ObservabilityMetric{
@ -101,7 +81,7 @@ func (s *scenario) eventLoopFailureMessageEventsObservability(username string) e
})
}
// syncFailureMessageBuiltObservability corresponds to bridge_sync_message_event_failures_total_v1.schema.json.
// syncFailureMessageBuiltObservability - corresponds to bridge_sync_message_event_failures_total_v1.schema.json.
func (s *scenario) syncFailureMessageBuiltObservability(username string) error {
batch := proton.ObservabilityBatch{
Metrics: []proton.ObservabilityMetric{
@ -116,7 +96,7 @@ func (s *scenario) syncFailureMessageBuiltObservability(username string) error {
})
}
// syncSuccessMessageBuiltObservability corresponds to bridge_sync_message_build_success_total_v1.schema.json.
// syncSuccessMessageBuiltObservability - corresponds to bridge_sync_message_build_success_total_v1.schema.json.
func (s *scenario) syncSuccessMessageBuiltObservability(username string) error {
batch := proton.ObservabilityBatch{
Metrics: []proton.ObservabilityMetric{
@ -129,58 +109,3 @@ func (s *scenario) syncSuccessMessageBuiltObservability(username string) error {
return err
})
}
// testGluonErrorObservabilityMetrics corresponds to bridge_gluon_errors_total_v1.schema.json.
func (s *scenario) testGluonErrorObservabilityMetrics(username string) error {
allMetrics := observability.GenerateAllGluonMetrics()
parsedMetrics := []proton.ObservabilityMetric{}
for _, el := range allMetrics {
ok, parsedMetric := observability.VerifyAndParseGenericMetrics(el)
if !ok {
return fmt.Errorf("failed to parse generic gluon metric")
}
parsedMetrics = append(parsedMetrics, parsedMetric)
}
batch := proton.ObservabilityBatch{Metrics: parsedMetrics}
return s.t.withClientPass(context.Background(), username, s.t.getUserByName(username).userPass, func(ctx context.Context, c *proton.Client) error {
err := c.SendObservabilityBatch(ctx, batch)
return err
})
}
// SMTPErrorObservabilityMetrics corresponds to bridge_smtp_errors_total_v1.schema.json.
func (s *scenario) SMTPErrorObservabilityMetrics(username string) error {
batch := proton.ObservabilityBatch{
Metrics: []proton.ObservabilityMetric{
smtpMetrics.GenerateFailedGetParentID(),
smtpMetrics.GenerateUnsupportedMIMEType(),
smtpMetrics.GenerateFailedCreateDraft(),
smtpMetrics.GenerateFailedCreateAttachments(),
smtpMetrics.GenerateFailedCreatePackages(),
smtpMetrics.GenerateFailedToGetRecipients(),
smtpMetrics.GenerateFailedSendDraft(),
smtpMetrics.GenerateFailedDeleteFromDrafts(),
},
}
return s.t.withClientPass(context.Background(), username, s.t.getUserByName(username).userPass, func(ctx context.Context, c *proton.Client) error {
err := c.SendObservabilityBatch(ctx, batch)
return err
})
}
func (s *scenario) SMTPSendSuccessObservabilityMetric(username string) error {
batch := proton.ObservabilityBatch{
Metrics: []proton.ObservabilityMetric{
smtpMetrics.GenerateSMTPSendSuccess(),
},
}
return s.t.withClientPass(context.Background(), username, s.t.getUserByName(username).userPass, func(ctx context.Context, c *proton.Client) error {
err := c.SendObservabilityBatch(ctx, batch)
return err
})
}

View File

@ -39,8 +39,6 @@ func (s *scenario) steps(ctx *godog.ScenarioContext) {
ctx.Step(`^bridge IMAP port is (\d+)`, s.bridgeIMAPPortIs)
ctx.Step(`^bridge SMTP port is (\d+)`, s.bridgeSMTPPortIs)
ctx.Step(`^the message used "([^"]*)" key for sending$`, s.theMessageUsedKeyForSending)
ctx.Step(`^the key for address "([^"]*)" was used to import`, s.theKeyForAddressWasUsedToImport)
ctx.Step(`^the key for address "([^"]*)" was used to create draft`, s.theKeyForAddressWasUsedToCreateDraft)
// ==== SETUP ====
ctx.Step(`^there exists an account with username "([^"]*)" and password "([^"]*)"$`, s.thereExistsAnAccountWithUsernameAndPassword)
@ -192,12 +190,6 @@ func (s *scenario) steps(ctx *godog.ScenarioContext) {
ctx.Step(`^SMTP client "([^"]*)" sends the following EML "([^"]*)" from "([^"]*)" to "([^"]*)"$`, s.smtpClientSendsTheFollowingEmlFromTo)
ctx.Step(`^SMTP client "([^"]*)" logs out$`, s.smtpClientLogsOut)
// ==== EXTERNAL ====
ctx.Step(`^external client deletes all messages`, s.externalClientDeletesAllMessages)
ctx.Step(`^external client sends the following message from "([^"]*)" to "([^"]*)":$`, s.externalClientSendsTheFollowingMessageFromTo)
ctx.Step(`^external client fetches the following message with subject "([^"]*)" and sender "([^"]*)" and state "([^"]*)"$`, s.externalClientFetchesTheFollowingMessage)
ctx.Step(`^external client fetches the following message with subject "([^"]*)" and sender "([^"]*)" and state "([^"]*)" with this structure:$`, s.externalClientSeesMessageWithStructure)
// ==== TELEMETRY ====
ctx.Step(`^bridge eventually sends the following heartbeat:$`, s.bridgeEventuallySendsTheFollowingHeartbeat)
ctx.Step(`^bridge needs to send heartbeat`, s.bridgeNeedsToSendHeartbeat)
@ -234,10 +226,4 @@ func (s *scenario) steps(ctx *godog.ScenarioContext) {
ctx.Step(`^the user with username "([^"]*)" sends all possible event loop message events observability metrics$`, s.eventLoopFailureMessageEventsObservability)
ctx.Step(`^the user with username "([^"]*)" sends all possible sync message building failure observability metrics$`, s.syncFailureMessageBuiltObservability)
ctx.Step(`^the user with username "([^"]*)" sends all possible sync message building success observability metrics$`, s.syncSuccessMessageBuiltObservability)
// SMTP metrics
ctx.Step(`^the user with username "([^"]*)" sends all possible SMTP error observability metrics$`, s.SMTPErrorObservabilityMetrics)
ctx.Step(`^the user with username "([^"]*)" sends SMTP send success observability metric$`, s.SMTPSendSuccessObservabilityMetric)
// Gluon related metrics
ctx.Step(`^the user with username "([^"]*)" sends all possible gluon error observability metrics$`, s.testGluonErrorObservabilityMetrics)
}

View File

@ -411,89 +411,6 @@ func matchContent(have MessageSection, want MessageSection) (bool, string) {
return true, ""
}
func matchStructureRecursive(have []MessageStruct, want MessageStruct) error {
mismatches := make([]string, 0)
for _, msg := range have {
if want.From != "" && msg.From != want.From {
mismatches = append(mismatches, "From")
continue
}
if want.To != "" && msg.To != want.To {
mismatches = append(mismatches, "To")
continue
}
if want.BCC != "" && msg.BCC != want.BCC {
mismatches = append(mismatches, "BCC")
continue
}
if want.CC != "" && msg.CC != want.CC {
mismatches = append(mismatches, "CC")
continue
}
if want.Subject != "" && msg.Subject != want.Subject {
mismatches = append(mismatches, "Subject")
continue
}
if want.Date != "" && want.Date != msg.Date {
mismatches = append(mismatches, "Date")
continue
}
if ok, mismatch := matchContentRecursive(msg.Content, want.Content); !ok {
mismatches = append(mismatches, "Content: "+mismatch)
continue
}
return nil
}
return fmt.Errorf("missing messages: have %#v, want %#v with mismatch list %#v", have, want, mismatches)
}
func matchContentRecursive(have MessageSection, want MessageSection) (bool, string) {
if want.ContentType != "" && !strings.EqualFold(want.ContentType, have.ContentType) {
return false, "ContentType"
}
if want.ContentTypeBoundary != "" && !strings.EqualFold(want.ContentTypeBoundary, have.ContentTypeBoundary) {
return false, "ContentTypeBoundary"
}
if want.ContentTypeCharset != "" && !strings.EqualFold(want.ContentTypeCharset, have.ContentTypeCharset) {
return false, "ContentTypeCharset"
}
if want.ContentTypeName != "" && !strings.EqualFold(want.ContentTypeName, have.ContentTypeName) {
return false, "ContentTypeName"
}
if want.ContentDisposition != "" && !strings.EqualFold(want.ContentDisposition, have.ContentDisposition) {
return false, "ContentDisposition"
}
if want.ContentDispositionFilename != "" && !strings.EqualFold(want.ContentDispositionFilename, have.ContentDispositionFilename) {
return false, "ContentDispositionFilename"
}
if want.TransferEncoding != "" && !strings.EqualFold(want.TransferEncoding, have.TransferEncoding) {
return false, "TransferEncoding"
}
if want.BodyContains != "" && !strings.Contains(strings.TrimSpace(have.BodyContains), strings.TrimSpace(want.BodyContains)) {
return false, "BodyContains"
}
if want.BodyIs != "" && strings.TrimSpace(have.BodyIs) != strings.TrimSpace(want.BodyIs) {
return false, "BodyIs"
}
for _, wantSection := range want.Sections {
didPass := false
for _, haveSection := range have.Sections {
ok, _ := matchContentRecursive(haveSection, wantSection)
if ok {
didPass = true
break
}
}
if !didPass {
return false, "recursive mismatch found"
}
}
return true, ""
}
type Mailbox struct {
Name string `bdd:"name"`
Total int `bdd:"total"`

View File

@ -1,149 +0,0 @@
// Copyright (c) 2024 Proton AG
//
// This file is part of Proton Mail Bridge.Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package tests
import (
"context"
"encoding/base64"
"fmt"
"log"
"github.com/ProtonMail/proton-bridge/v3/tests/utils/gmail/tokenservice"
"github.com/cucumber/godog"
"google.golang.org/api/gmail/v1"
"google.golang.org/api/option"
)
const DEBUG = false
const GmailUserID = "me"
func getGmailService() *gmail.Service {
ctx := context.Background()
gmailClient, err := tokenservice.LoadGmailClient(ctx)
if err != nil {
log.Fatalf("unable to retrieve gmail http client: %v", err)
}
gmailService, err := gmail.NewService(ctx, option.WithHTTPClient(gmailClient))
if err != nil {
log.Fatalf("Unable to retrieve Gmail client: %v", err)
}
return gmailService
}
func ExternalSendEmail(from, to string, message *godog.DocString) error {
srv := getGmailService()
var msg gmail.Message
msgStr := []byte(
"From: " + from + " \n" +
"To: " + to + " \n" +
message.Content)
msg.Raw = base64.URLEncoding.EncodeToString(msgStr)
_, err := srv.Users.Messages.Send(GmailUserID, &msg).Do()
if err != nil {
return err
}
return nil
}
func FetchMessageBySubjectAndSender(subject, sender, state string) (*gmail.Message, error) {
srv := getGmailService()
var q string
switch state {
case "read":
q = fmt.Sprintf("(is:read in:inbox OR in:spam) subject:%q from:%q newer:1", subject, sender)
case "unread":
q = fmt.Sprintf("(is:unread in:inbox OR in:spam) subject:%q from:%q newer:1", subject, sender)
default:
return nil, fmt.Errorf("invalid state argument, must be 'read' or 'unread'")
}
if DEBUG {
fmt.Println("Gmail API Query:", q)
}
r, err := srv.Users.Messages.List(GmailUserID).Q(q).Do()
if err != nil {
return nil, fmt.Errorf("unable to retrieve %s messages with subject: %q and sender: %q with error: %v", state, subject, sender, err)
}
if len(r.Messages) == 0 {
return nil, fmt.Errorf("no %s messages found with subject: %q and sender: %q", state, subject, sender)
}
newestMessageID := r.Messages[0].Id
newestMessage, err := srv.Users.Messages.Get(GmailUserID, newestMessageID).Do()
if err != nil {
return nil, fmt.Errorf("unable to retrieve details of the newest message: %v", err)
}
if DEBUG {
fmt.Println("Email Subject:", getEmailHeader(newestMessage, "Subject"))
fmt.Println("Email Sender:", getEmailHeader(newestMessage, "From"))
}
return newestMessage, nil
}
func getEmailHeader(message *gmail.Message, headerName string) string {
if message != nil && message.Payload != nil && message.Payload.Headers != nil {
for _, header := range message.Payload.Headers {
if header.Name == headerName {
return header.Value
}
}
}
return "Header not found"
}
func GetRawMessage(message *gmail.Message) (string, error) {
srv := getGmailService()
msg, err := srv.Users.Messages.Get(GmailUserID, message.Id).Format("raw").Do()
if err != nil {
return "", err
}
decodedMsg, err := base64.URLEncoding.DecodeString(msg.Raw)
return string(decodedMsg), err
}
func DeleteAllMessages() {
srv := getGmailService()
labels := []string{"INBOX", "SENT", "DRAFT", "SPAM", "TRASH"}
for _, label := range labels {
msgs, err := srv.Users.Messages.List(GmailUserID).LabelIds(label).Do()
if err != nil {
continue
}
for _, m := range msgs.Messages {
_ = srv.Users.Messages.Delete(GmailUserID, m.Id).Do()
}
}
}

View File

@ -1,183 +0,0 @@
// Copyright (c) 2024 Proton AG
//
// This file is part of Proton Mail Bridge.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 tokenservice
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"time"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
"google.golang.org/api/gmail/v1"
)
const (
NexusTestingPwd = "NEXUS_TESTING_PWD"
NexusTestingUser = "NEXUS_TESTING_USER"
)
var nexusAccessVerificationURL = os.Getenv("NEXUS_ACCESS_VERIFICATION_URL")
var nexusCredentialsURL = os.Getenv("NEXUS_GMAIL_CREDENTIALS_URL")
var nexusTokenURL = os.Getenv("NEXUS_GMAIL_TOKEN_URL")
var gmailScopes = []string{gmail.GmailComposeScope, gmail.GmailInsertScope, gmail.GmailLabelsScope, gmail.MailGoogleComScope, gmail.GmailMetadataScope, gmail.GmailModifyScope, gmail.GmailSendScope}
func fetchBytes(url string) ([]byte, error) {
res, err := http.Get(url)
if err != nil {
return nil, fmt.Errorf("unable to fetch data from url %v: %v", url, err)
}
defer func() { _ = res.Body.Close() }()
body, err := io.ReadAll(res.Body)
if err != nil {
return nil, fmt.Errorf("unable to read response body for url %v: %v", url, err)
}
return body, nil
}
func fetchConfig() (*oauth2.Config, error) {
data, err := fetchBytes(nexusCredentialsURL)
if err != nil {
return nil, fmt.Errorf("unable to fetch credentials: %v", err)
}
config, err := google.ConfigFromJSON(data, gmailScopes...)
if err != nil {
return nil, fmt.Errorf("unable to parse credentials to gmail config: %v", err)
}
return config, err
}
func fetchToken() (*oauth2.Token, error) {
data, err := fetchBytes(nexusTokenURL)
if err != nil {
return nil, fmt.Errorf("unable to fetch token: %v", err)
}
token := &oauth2.Token{}
if err = json.Unmarshal(data, token); err != nil {
return nil, fmt.Errorf("error when unmarshaling token: %v", err)
}
return token, nil
}
func refreshToken(config *oauth2.Config, token *oauth2.Token) (*oauth2.Token, error) {
ctx := context.Background()
tokenSource := config.TokenSource(ctx, token)
newToken, err := tokenSource.Token()
if err != nil {
return nil, fmt.Errorf("error when refreshing access token: %v", err)
}
return newToken, nil
}
func getNexusCreds() string {
credentials := os.Getenv(NexusTestingUser) + ":" + os.Getenv(NexusTestingPwd)
return base64.StdEncoding.EncodeToString([]byte(credentials))
}
func pushToNexus(url string, data []byte) error {
req, err := http.NewRequest("PUT", url, bytes.NewBuffer(data))
if err != nil {
return fmt.Errorf("error creating put request to nexus: %v", err)
}
req.Header.Set("Content-Type", "application/json")
encodedCredentials := getNexusCreds()
req.Header.Set("Authorization", "Basic "+encodedCredentials)
client := http.Client{}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("error making put request to nexus: %v", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("unexpected status code when making put request to nexus: %v", resp.StatusCode)
}
return nil
}
func uploadTokenToNexus(token *oauth2.Token) error {
jsonData, err := json.Marshal(token)
if err != nil {
return fmt.Errorf("error when encoding access token to json: %v", err)
}
return pushToNexus(nexusTokenURL, jsonData)
}
func verifyNexusAccess() error {
return pushToNexus(nexusAccessVerificationURL, nil)
}
func checkTokenValidityAndRefresh(config *oauth2.Config, token *oauth2.Token) (*oauth2.Token, error) {
// Validate token (check if it has expired, or is 5 minutes from expiring) and refresh
timeTillExpiry := time.Until(token.Expiry)
if !token.Valid() || timeTillExpiry < 5*time.Minute {
token, err := refreshToken(config, token)
if err != nil {
return nil, err
}
if err = uploadTokenToNexus(token); err != nil {
return nil, fmt.Errorf("unable to upload token to nexus: %v", err)
}
}
return token, nil
}
func LoadGmailClient(ctx context.Context) (*http.Client, error) {
err := verifyNexusAccess()
if err != nil {
log.Fatalf("error occurred when verifying nexus access, check your credentials: %v", err)
}
config, err := fetchConfig()
if err != nil {
return nil, fmt.Errorf("issue obtaining oauth config: %v", err)
}
token, err := fetchToken()
if err != nil {
return nil, fmt.Errorf("issue obtaining oauth token: %v", err)
}
token, err = checkTokenValidityAndRefresh(config, token)
if err != nil {
return nil, fmt.Errorf("error checking token validity: %v", err)
}
client := config.Client(ctx, token)
return client, nil
}

View File

@ -62,7 +62,7 @@ func main() {
func getRollout(_ *cli.Context) error {
return app.WithLocations(func(locations *locations.Locations) error {
return app.WithKeychainList(async.NoopPanicHandler{}, false, func(keychains *keychain.List) error {
return app.WithVault(nil, locations, keychains, async.NoopPanicHandler{}, func(vault *vault.Vault, _, _ bool) error {
return app.WithVault(locations, keychains, async.NoopPanicHandler{}, func(vault *vault.Vault, _, _ bool) error {
fmt.Println(vault.GetUpdateRollout())
return nil
})
@ -73,7 +73,7 @@ func getRollout(_ *cli.Context) error {
func setRollout(c *cli.Context) error {
return app.WithLocations(func(locations *locations.Locations) error {
return app.WithKeychainList(async.NoopPanicHandler{}, false, func(keychains *keychain.List) error {
return app.WithVault(nil, locations, keychains, async.NoopPanicHandler{}, func(vault *vault.Vault, _, _ bool) error {
return app.WithVault(locations, keychains, async.NoopPanicHandler{}, func(vault *vault.Vault, _, _ bool) error {
clamped := max(0.0, min(1.0, c.Float64("value")))
if err := vault.SetUpdateRollout(clamped); err != nil {
return err

View File

@ -54,7 +54,6 @@ generate_dep_licenses(){
## add license file to github links, and others
sed -i -r '/github.com/s|^(.*(https://[^)]+).*)$|\1 available under [license](\2/blob/master/LICENSE) |g' "$tmpDepLicenses"
sed -i -r '/gitlab.com/s|^(.*(https://[^)]+).*)$|\1 available under [license](\2/blob/master/LICENSE) |g' "$tmpDepLicenses"
sed -i -r '/go.etcd.io\/bbolt/s|^(.*)$|\1 available under [license](https://github.com/etcd-io/bbolt/blob/master/LICENSE) |g' "$tmpDepLicenses"
sed -i -r '/howett.net\/plist/s|^(.*)$|\1 available under [license](https://github.com/DHowett/go-plist/blob/main/LICENSE) |g' "$tmpDepLicenses"
sed -i -r '/golang.org\/x/s|^(.*golang.org/x/([^)]+).*)$|\1 available under [license](https://cs.opensource.google/go/x/\2/+/master:LICENSE) |g' "$tmpDepLicenses"
@ -65,12 +64,6 @@ generate_dep_licenses(){
sed -i -r '/entgo.io\/ent/s|^(.*)$|\1 available under [license](https://pkg.go.dev/entgo.io/ent?tab=licenses) |g' "$tmpDepLicenses"
sed -i -r '/google.golang.org\/genproto/s|^(.*)$|\1 available under [license](https://pkg.go.dev/google.golang.org/genproto?tab=licenses) |g' "$tmpDepLicenses"
sed -i -r '/gopkg.in\/yaml\.v3/s|^(.*)$|\1 available under [license](https://github.com/go-yaml/yaml/blob/v3.0.1/LICENSE) |g' "$tmpDepLicenses"
sed -i -r '/google.golang.org\/appengine/s|^(.*)$|\1 available under [license](https://pkg.go.dev/google.golang.org/appengine?tab=licenses) |g' "$tmpDepLicenses"
sed -i -r '/go.opencensus.io/s|^(.*)$|* [go.opencensus.io](https://pkg.go.dev/go.opencensus.io?tab=licenses) available under [license](https://pkg.go.dev/go.opencensus.io?tab=licenses) |g' "$tmpDepLicenses"
sed -i -r '/google.golang.org\/api/s|^(.*)$|\1 available under [license](https://pkg.go.dev/google.golang.org/api?tab=licenses) |g' "$tmpDepLicenses"
sed -i -r '/cloud.google.com\/go\/compute\/metadata/s|^(.*)$|\1 available under [license](https://pkg.go.dev/cloud.google.com/go/compute/metadata?tab=licenses) |g' "$tmpDepLicenses"
sed -i -r '/cloud.google.com\/go\/compute\/metadata/!{/cloud.google.com\/go\/compute/s|^(.*)$|\1 available under [license](https://pkg.go.dev/cloud.google.com/go/compute?tab=licenses) |g}' "$tmpDepLicenses"
sed -i -r '/gopkg.in\/yaml\.v3/s|^(.*)$|\1 available under [license](https://github.com/go-yaml/yaml/blob/v3.0.1/LICENSE) |g' "$tmpDepLicenses"
}

View File

@ -31,7 +31,6 @@ main(){
ignore GO-2024-2887 "BRIDGE-95 net/http vulnerability"
ignore GO-2024-2888 "BRIDGE-95 archive/zip vulnerability"
ignore GO-2024-2963 "BRIDGE-95 net/http vulnerability"
ignore GO-2024-3106 "BRIDGE-209 encoding/gob vulnerability"
has_vulns
echo

View File

@ -52,7 +52,7 @@ func main() {
func readAction(c *cli.Context) error {
return app.WithLocations(func(locations *locations.Locations) error {
return app.WithKeychainList(async.NoopPanicHandler{}, false, func(keychains *keychain.List) error {
return app.WithVault(nil, locations, keychains, async.NoopPanicHandler{}, func(vault *vault.Vault, insecure, corrupt bool) error {
return app.WithVault(locations, keychains, async.NoopPanicHandler{}, func(vault *vault.Vault, insecure, corrupt bool) error {
if _, err := os.Stdout.Write(vault.ExportJSON()); err != nil {
return fmt.Errorf("failed to write vault: %w", err)
}
@ -66,7 +66,7 @@ func readAction(c *cli.Context) error {
func writeAction(c *cli.Context) error {
return app.WithLocations(func(locations *locations.Locations) error {
return app.WithKeychainList(async.NoopPanicHandler{}, false, func(keychains *keychain.List) error {
return app.WithVault(nil, locations, keychains, async.NoopPanicHandler{}, func(vault *vault.Vault, insecure, corrupt bool) error {
return app.WithVault(locations, keychains, async.NoopPanicHandler{}, func(vault *vault.Vault, insecure, corrupt bool) error {
b, err := io.ReadAll(os.Stdin)
if err != nil {
return fmt.Errorf("failed to read vault: %w", err)