Compare commits

..

118 Commits

Author SHA1 Message Date
84adbbc461 chore: Trift Bridge 3.4.0 changelog. 2023-08-01 07:54:57 +02:00
75811d22e8 fix(GODT-2822): rename funcs 2023-07-31 15:21:53 +02:00
e26c7683d2 fix(GODT-2822): event loop behaviour on 429. 2023-07-31 13:52:39 +02:00
d7b71aceda fix(GODT-2822): retry 429 for metadata and exponential cooldown GODT-2823. 2023-07-28 07:58:05 +02:00
25a787529b fix(GODT-2822): Retry on 429 during Message ID fetch 2023-07-27 17:59:13 +02:00
f82965b825 fix(GODT-2812): Fix rare sync deadlock
Copy data rather than hold onto the locks while sync is ongoing. The
data in question does not change while the sync is ongoing and holding
on to the locks during a very long sync can create a deadlock with
due to some IMAP operation that needs to acquire one of those locks with
write access.
2023-07-27 17:48:55 +02:00
f1cf4ee194 fix(GODT-2822): Sync Cache
When the sync fail, store the previously downloaded data in memory so
that on next retry we don't have to re-download everything.
2023-07-27 17:24:20 +02:00
5136919c36 fix(GODT-2822): Handle 429 during message download
When we run into 429 during a message download, do not cancel the whole
batch and switch to a sequential downloader to avoid API overload.
2023-07-27 16:55:18 +02:00
4f8ecd598f chore: Trift Bridge 3.4.0 changelog. 2023-07-13 08:14:44 +02:00
65365281eb chore: merge release/stone with release/trift. 2023-07-13 07:51:19 +02:00
c05dfb36d3 chore: Stone Bridge 3.3.2 changelog. 2023-07-11 17:02:38 +02:00
b2830b39e0 fix(GODT-2782): Filter all labels when doing perma delete check
Previously we were not filtering out labels we ignored from the
perma-delete check. The introduction of new system label types could
break this check leading to user never being able to perma-delete
messages.
2023-07-11 15:25:46 +02:00
7997ad2b93 fix(GODT-2782): Filter all labels when doing perma delete check
Previously we were not filtering out labels we ignored from the
perma-delete check. The introduction of new system label types could
break this check leading to user never being able to perma-delete
messages.
2023-07-11 15:17:25 +02:00
80d743afec chore: Trift Bridge 3.4.0 changelog. 2023-07-06 16:15:55 +02:00
ac9857a965 chore: Merge remote-tracking branch 'origin/devel' into release/trift 2023-07-06 16:13:25 +02:00
0e9fd46a5c chore: Stone Bridge 3.3.1 changelog. 2023-07-06 15:50:18 +02:00
305d180a5f fix(GODT-2774): only check telemetry availability for the current user. 2023-07-06 15:28:24 +02:00
c4f80103b6 fix(GODT-2774): Add external context to telemetry tasks
This ensures they get cancelled if the parent context becomes invalid
2023-07-06 15:28:05 +02:00
0d57e3645a test: Add require.Eventually to TestBridge_UserAgentFromSMTPClient 2023-07-06 14:40:17 +02:00
0afdc31f96 test: Add smtp-send utility
Small program to simulate external SMTP send to Bridge.
2023-07-06 14:06:28 +02:00
91de6e001e fix(GODT-2763): Missing Answered flag on Sync and Message Create
Ensure we are using the same flag conversion code for all IMAP updates.
2023-07-06 14:02:05 +02:00
908ed3e723 feat(GODT-2759): Check for oprhan messages
Orphan messages are messages which are only in AllMail, AllDrafts or
AllSent.
2023-07-06 14:02:05 +02:00
7411073c08 feat(GODT-2759): Add prompt to download missing messages for analysis
This will download the missing messages into a temporary directory and
decrypt them along with the metadata so we can attempt analyze them once
submitted to see what is going wrong.
2023-07-06 14:02:05 +02:00
7d838375bb feat(GODT-2759): CLI debug commands
Add debug commands to CLI to diagnose potential bride problems.
Currently we only have a command which validates whether the state of
all the mailboxes reported by IMAP matches what is currently available
on the proton servers.
2023-07-06 14:02:05 +02:00
f545f30ec0 fix(GODT-2774): only check telemetry availability for the current user. 2023-07-06 13:38:06 +02:00
6579cdfc7f fix(GODT-2774): Add context to Authorize in gluon.Connector
Backported from: https://github.com/ProtonMail/gluon/pull/379
2023-07-06 13:28:58 +02:00
40c48ba804 fix(GODT-2774): Add external context to telemetry tasks
This ensures they get cancelled if the parent context becomes invalid
2023-07-06 13:09:35 +02:00
1c2cb4f439 chore: changelog fix. 2023-07-06 08:45:35 +02:00
650dac37a7 chore: changelog fix. 2023-07-05 12:14:49 +02:00
55275b23ee chore: Trift Bridge 3.4.0 changelog. 2023-07-05 12:13:20 +02:00
bce69e1a1b chore: Merge remote-tracking branch 'origin/devel' into release/trift 2023-07-05 12:07:37 +02:00
f1917ad0de fix(GODT-2758): Fix panic in SetFlagsOnMessages
https://github.com/ProtonMail/gluon/pull/377
2023-07-05 10:57:53 +02:00
6ea6d54af6 chore: Trift Bridge 3.4.0 changelog. 2023-07-04 13:31:16 +02:00
c1b486a7eb chore: Merge remote-tracking branch 'origin/devel' into release/trift 2023-07-04 13:26:46 +02:00
454c9e1534 chore: Stone Bridge 3.3.1 changelog. 2023-07-04 13:09:51 +02:00
eaa673c4e4 fix(GODT-2708): fix dimensions event format + handling of ReportClicked event. 2023-07-04 13:04:30 +02:00
9c389e3007 fix(GODT-2708): fix dimensions event format + handling of ReportClicked event. 2023-07-04 09:59:43 +00:00
7e9a5934c5 feat(GODT-2707): set bridge-gui default log level to 'debug'.
(cherry picked from commit cc1d0e803b)
2023-07-04 11:14:40 +02:00
cc17366c1c fix(GODT-2578): Refresh literals appended to Sent folder
Whenever a message gets moved to the sent folder we should retrieve the
new literal in order to guarantee that, if another client modifies and
sends the message, we always see the latest version of the message and
not a previous state stored in the Gluon cache.

Includes the following Gluon MRs:
* https://github.com/ProtonMail/gluon/pull/374
* https://github.com/ProtonMail/gluon/pull/376

Includes the followin gpa MR:
https://github.com/ProtonMail/go-proton-api/pull/88
2023-07-04 10:39:07 +02:00
62f6db35db chore: Merge remote-tracking branch 'origin/devel' into release/trift. 2023-07-04 08:26:08 +02:00
be422001e8 fix(GODT-2756): fix for 'Settings' context menu opening the 'Help' page.
(cherry picked from commit b2eb35592f)
2023-07-03 16:36:12 +02:00
b2eb35592f fix(GODT-2756): fix for 'Settings' context menu opening the 'Help' page. 2023-07-03 16:35:42 +02:00
a3b26431ce chore: Trift Bridge 3.4.0 changelog. 2023-07-03 12:03:05 +02:00
f460323cc5 chore: Stone Bridge 3.3.1 changelog. 2023-07-03 10:58:12 +02:00
26694d3bd8 feat(GODT-2674): Add more logs during update failed.
(cherry picked from commit 3afd94c61d)

feat(GODT-2674): add more logs to failed update.

(cherry picked from commit 4902898880)
2023-07-03 10:02:59 +02:00
e96713a998 chore: remove gRPC auto-generated C++ source files.
CMake will regenerate the files when needed, they do not need to be under source control.
2023-07-03 09:47:54 +02:00
6359b8639e feat(GODT-2750): disable raise on main window when a notification is clicked on Linux.
(cherry picked from commit 4e080b59d3)
2023-07-03 07:33:39 +00:00
6c9d5ccd4a fix(GODT-2753): vault test now check that value auto-assigned is first available port. 2023-07-03 09:21:01 +02:00
234554b459 feat(GODT-2709): Remove the config status file when user is removed. 2023-07-01 07:21:27 +02:00
6df5a82364 feat(GODT-2749): manual test-windows again. 2023-06-30 15:10:35 +02:00
ac75410657 feat(GODT-2709): Remove the config status file when user is removed. 2023-06-30 14:48:42 +02:00
238929c3ec feat(GODT-2712): Feed config_status with user action while pending. 2023-06-30 09:43:26 +00:00
1f79e3b0a7 feat(GODT-2715): Add Unitary test for configStatus event. 2023-06-30 09:43:26 +00:00
f5af2afce5 feat(GODT-2715): Add Functional test for configStatus telemetry event. 2023-06-30 09:43:26 +00:00
9482bea8af feat(GODT-2714): Apply PR comments. 2023-06-30 09:43:26 +00:00
a55572e5b3 feat(GODT-2714): Set Configuration Status to Failure and send Recovery event when issue is solved. 2023-06-30 09:43:26 +00:00
098eb7cb7a feat(GODT-2713): Send config_progress event once a day if the configuration is stucked in pending for more than a day. 2023-06-30 09:43:26 +00:00
68334e3bb8 feat(GODT-2711): Send config_abort event on User removal. 2023-06-30 09:43:26 +00:00
124231c3c7 feat(GODT-2710): Send config success on IMAP/SMTP connection.. 2023-06-30 09:43:26 +00:00
f591af2cbd feat(GODT-2716): Make Configuration Statistics persistent. 2023-06-30 09:43:26 +00:00
ff11d20d9c feat(GODT-2709): Init Configuration status. 2023-06-30 09:43:26 +00:00
72911235c5 feat(GODT-2748): log calls that cause main window to show, with reason.
(cherry picked from commit a91d9762db)

# Conflicts:
#	internal/frontend/bridge-gui/bridge-gui/QMLBackend.h
2023-06-30 10:37:33 +02:00
60de00c73f feat(GODT-2705): added log entries for focus service on client and server sides.
(cherry picked from commit ae65385c38)
2023-06-30 08:18:23 +00:00
4e080b59d3 feat(GODT-2750): disable raise on main window when a notification is clicked on Linux. 2023-06-30 10:05:07 +02:00
bac4b90c1d feat(GODT-2712): Feed config_status with user action while pending. 2023-06-30 07:51:53 +00:00
ea47c9aa1c feat(GODT-2728): remove the sentry report for gRPC event stream interruptions in bridge-gui.
(cherry picked from commit 58b45d8458)
2023-06-30 09:26:03 +02:00
a91d9762db feat(GODT-2748): log calls that cause main window to show, with reason. 2023-06-29 16:56:47 +02:00
6fb11d69f9 test: Force all unit test to use minimum sync spec 2023-06-29 13:31:46 +02:00
1eab3296d1 Revert "feat(GODT-2749): manual windows-test."
This reverts commit 650158ea8a.
2023-06-29 13:31:21 +02:00
4352154b84 test: Force sync limits to minimum with env variable
Set `BRIDGE_SYNC_FORCE_MINIMUM_SPEC` as environment variable to force
all the sync limits to minimum spec.

This is enabled for windows builds.
2023-06-29 13:31:03 +02:00
03cf601921 feat(GODT-2715): Add Unitary test for configStatus event. 2023-06-29 13:26:08 +02:00
9f13301613 feat(GODT-2715): Add Functional test for configStatus telemetry event. 2023-06-29 13:15:50 +02:00
650158ea8a feat(GODT-2749): manual windows-test. 2023-06-29 11:37:57 +02:00
552fc2700f chore: Disable windows runner 2023-06-29 11:28:43 +02:00
582afa1451 fix(GODT-2726): Fix Parsing of Details field in GPA error message
https://github.com/ProtonMail/go-proton-api/pull/87
2023-06-29 11:24:55 +02:00
c43739a7ef feat(GODT-2714): Apply PR comments. 2023-06-29 09:54:42 +02:00
720f662afe feat(GODT-2714): Set Configuration Status to Failure and send Recovery event when issue is solved. 2023-06-29 09:10:28 +02:00
e9488d12ee fix(GODT-2522): Handle migration with unreferenced db values
https://github.com/ProtonMail/gluon/pull/373
2023-06-29 06:49:54 +00:00
9a87b155ba fix(GODT-2693): Allow missing whitespace after header field colon
https://github.com/ProtonMail/gluon/pull/372/
2023-06-29 06:49:54 +00:00
f6a1cd9b64 fix(GODT-2726): Fix Parsing of Details field in GPA error message
https://github.com/ProtonMail/go-proton-api/pull/87
2023-06-29 06:49:54 +00:00
7b7c9093ce feat(GODT-2691): close logrus output file on exit. 2023-06-28 16:11:40 +02:00
fa4c0ec823 feat(GODT-2713): Send config_progress event once a day if the configuration is stucked in pending for more than a day. 2023-06-28 14:44:20 +02:00
08af1da966 feat(GODT-2711): Send config_abort event on User removal. 2023-06-28 14:40:51 +02:00
ed8475dacf feat(GODT-2710): Send config success on IMAP/SMTP connection.. 2023-06-28 14:39:49 +02:00
e91cdca6f3 feat(GODT-2716): Make Configuration Statistics persistent. 2023-06-28 10:34:01 +00:00
c267168cb7 feat(GODT-2522): New Gluon database layout
All changes were made in Gluon.
2023-06-28 10:49:13 +02:00
58b45d8458 feat(GODT-2728): remove the sentry report for gRPC event stream interruptions in bridge-gui. 2023-06-27 17:08:09 +02:00
b77512dfd9 feat(GODT-2709): Init Configuration status. 2023-06-27 11:24:28 +02:00
bdc6542970 feat(GODT-2678): When internet is off, do not display status dot icon for the user in the context menu. 2023-06-26 11:03:49 +02:00
c942a44f6a feat(GODT-2686): Change the orientation of the expand/collapse arrow for Advanced settings. 2023-06-23 09:01:40 +00:00
55081fa59b test(GODT-2636): Add step for sending from EML 2023-06-23 06:45:31 +00:00
cc1d0e803b feat(GODT-2707): set bridge-gui default log level to 'debug'. 2023-06-22 11:30:40 +00:00
b7a2371220 chore: Log failed message ids during sync 2023-06-22 11:05:36 +00:00
ae65385c38 feat(GODT-2705): added log entries for focus service on client and server sides. 2023-06-22 10:03:15 +00:00
ab70e85f1c fix(GODT-2653): Only log when err is not nil 2023-06-22 10:12:03 +02:00
ff57eb2b43 feat(GODT-2510): Remove Ent
Update Gluon to use new direct SQLite3 implementation.
2023-06-22 06:13:16 +00:00
e78cb8089b test(GODT-2600): Changing state (read/unread, starred/unstarred) of a message in integration tests 2023-06-22 05:13:53 +00:00
09eef64514 feat(GODT-2703): got rid of account details dialog with Apple Mail autoconf. 2023-06-21 10:21:19 +02:00
a2c2710760 feat(GODT-2685): update to bug report log attachment logic. 2023-06-19 16:30:36 +02:00
38a0cdb4ab feat(GODT-2690): update sentry reporting in GUI for new log file naming. 2023-06-14 14:28:38 +02:00
c587dfc0dc feat(GODT-2668): implemented new log retention policy. 2023-06-14 08:44:37 +02:00
7a090ffcc9 test(GODT-2683): Save Draft without "Date" & "From" in headers 2023-06-08 11:24:14 +00:00
6ab49367ba chore: merge branch release/stone to devel 2023-06-08 08:40:31 +02:00
12f9fb03c3 chore: Log errors on failed message Downloads 2023-06-07 16:38:57 +02:00
ac00ef1b64 feat(GODT-2666): feat(GODT-2667): introduce sessionID in bridge. 2023-06-07 09:00:33 +02:00
1e9a77c7b2 fix(GODT-2680): fix for C++ debugger not working on ARM64 because of OpenSSL 3.1 2023-06-05 13:37:41 +00:00
a8dd52800e chore: Fix linter errors 2023-06-05 14:07:39 +02:00
fab063f194 feat(GODT-2653): Log API error details on Message import and send 2023-06-05 14:06:04 +02:00
4902898880 feat(GODT-2674): add more logs to failed update. 2023-06-05 05:31:14 +00:00
c46b3245b8 feat(GODT-2660): calculate bridge coverage and refactor CI yaml file. 2023-06-02 11:56:10 +00:00
3afd94c61d feat(GODT-2674): Add more logs during update failed. 2023-06-02 11:21:27 +00:00
89632b7acd chore: Fix dependency_license script to handle dot formated version. 2023-06-02 11:38:36 +02:00
b8cf193911 chore: Add error logs when messages fail to build during sync 2023-06-02 10:22:12 +02:00
fb4d34ef5b fix(GODT-2675): Update GPA to applye togin-gonic/gin patch + update COPYING_NOTES. 2023-06-02 09:48:42 +02:00
7f7e360cd7 feat(GODT-2673): Use NoClient as UserAgent without any client connected and... 2023-06-01 15:27:56 +00:00
fc06665d2b feat(GODT-2655): display internal build time tag in log and GUI. 2023-06-01 05:24:51 +00:00
bfaf9765ae fix(GODT-2672): fix context cancelled when IMAP/SMTP parameters change is in progress.
(cherry picked from commit 0eab1c0c2b)
2023-05-31 10:59:01 +00:00
a702e19dff fix(GODT-2669): Display sentry ID in bridge init log. 2023-05-31 08:40:15 +02:00
145 changed files with 7230 additions and 47947 deletions

4
.gitignore vendored
View File

@ -41,3 +41,7 @@ cmake-build-*/
# Doxygen doc files
_doc/
# gRPC auto-generated C++ source files
*.pb.cc
*.pb.h

View File

@ -30,13 +30,6 @@ stages:
- test
- build
.rules-branch-and-MR-always:
rules:
- if: $CI_COMMIT_BRANCH || $CI_PIPELINE_SOURCE == "merge_request_event"
when: always
allow_failure: false
- when: never
.rules-branch-and-MR-manual:
rules:
- if: $CI_COMMIT_BRANCH || $CI_PIPELINE_SOURCE == "merge_request_event"
@ -44,16 +37,6 @@ stages:
allow_failure: true
- when: never
.rules-branch-manual-MR-always:
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
when: always
allow_failure: false
- if: $CI_COMMIT_BRANCH
when: manual
allow_failure: true
- when: never
.rules-branch-manual-MR-and-devel-always:
rules:
- if: $CI_COMMIT_BRANCH == "devel" || $CI_PIPELINE_SOURCE == "merge_request_event"
@ -64,173 +47,10 @@ stages:
allow_failure: true
- when: never
.after-script-code-coverage:
after_script:
- go get github.com/boumenot/gocover-cobertura
- go run github.com/boumenot/gocover-cobertura < /tmp/coverage.out > coverage.xml
- "go tool cover -func=/tmp/coverage.out | grep total:"
coverage: '/total:.*\(statements\).*\d+\.\d+%/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
# Stage: TEST
lint:
stage: test
extends:
- .rules-branch-and-MR-always
script:
- make lint
tags:
- medium
.test-base:
stage: test
script:
- make test
test-linux:
extends:
- .test-base
- .rules-branch-manual-MR-and-devel-always
- .after-script-code-coverage
tags:
- large
test-linux-race:
extends:
- test-linux
- .rules-branch-and-MR-manual
script:
- make test-race
test-integration:
extends:
- test-linux
script:
- make test-integration
tags:
- large
test-integration-race:
extends:
- test-integration
- .rules-branch-and-MR-manual
script:
- make test-integration-race
.windows-base:
before_script:
- export GOROOT=/c/Go1.20
- export PATH=$GOROOT/bin:$PATH
- export GOARCH=amd64
- export GOPATH=~/go1.20
- export GO111MODULE=on
- export PATH=$GOPATH/bin:$PATH
- export MSYSTEM=
tags:
- windows-bridge
test-windows:
extends:
- .rules-branch-manual-MR-always
- .windows-base
stage: test
script:
- make test
# Stage: BUILD
.build-base:
stage: build
needs: ["lint"]
rules:
# GODT-1833: use `=~ /qa/` after mac and windows runners are fixed
- if: $CI_JOB_NAME =~ /build-linux-qa/ && $CI_PIPELINE_SOURCE == "merge_request_event"
when: always
allow_failure: false
- if: $CI_COMMIT_BRANCH || $CI_PIPELINE_SOURCE == "merge_request_event"
when: manual
allow_failure: true
- when: never
script:
- make build
- git diff && git diff-index --quiet HEAD
- make vault-editor
artifacts:
expire_in: 1 day
when: always
name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"
paths:
- bridge_*.tgz
- vault-editor
.linux-build-setup:
image: gitlab.protontech.ch:4567/go/bridge-internal:build-go1.20-qt6.3.2
variables:
VCPKG_DEFAULT_BINARY_CACHE: ${CI_PROJECT_DIR}/.cache
cache:
key: linux-vcpkg
paths:
- .cache
when: 'always'
before_script:
- mkdir -p .cache/bin
- export PATH=$(pwd)/.cache/bin:$PATH
- export GOPATH="$CI_PROJECT_DIR/.cache"
- export PATH=$PATH:$QT6DIR/bin
- $(git config --global -l | grep -o 'url.*gitlab.protontech.ch.*insteadof' | xargs -L 1 git config --global --unset &> /dev/null) || echo "nothing to remove"
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
tags:
- large
build-linux:
extends:
- .build-base
- .linux-build-setup
build-linux-qa:
extends:
- build-linux
variables:
BUILD_TAGS: "build_qa"
.darwin-build-setup:
before_script:
- export PATH=/usr/local/bin:$PATH
- export PATH=/usr/local/opt/git/bin:$PATH
- export PATH=/usr/local/opt/make/libexec/gnubin:$PATH
- export PATH=/usr/local/opt/go@1.13/bin:$PATH
- export PATH=/usr/local/opt/gnu-sed/libexec/gnubin:$PATH
- export GOPATH=~/go1.20
- export PATH=$GOPATH/bin:$PATH
- export CGO_CPPFLAGS='-Wno-error -Wno-nullability-completeness -Wno-expansion-to-defined -Wno-builtin-requires-header'
- $(git config --global -l | grep -o 'url.*gitlab.protontech.ch.*insteadof' | xargs -L 1 git config --global --unset &> /dev/null) || echo "nothing to remove"
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
cache: {}
tags:
- macOS
build-darwin:
extends:
- .build-base
- .darwin-build-setup
build-darwin-qa:
extends:
- build-darwin
variables:
BUILD_TAGS: "build_qa"
.windows-build-setup:
# ENV
.env-windows:
before_script:
- export BRIDGE_SYNC_FORCE_MINIMUM_SPEC=1
- export GOROOT=/c/Go1.20/
- export PATH=$GOROOT/bin:$PATH
- export GOARCH=amd64
@ -249,10 +69,172 @@ build-darwin-qa:
tags:
- windows-bridge
.env-darwin:
before_script:
- export BRIDGE_SYNC_FORCE_MINIMUM_SPEC=1
- export PATH=/usr/local/bin:$PATH
- export PATH=/usr/local/opt/git/bin:$PATH
- export PATH=/usr/local/opt/make/libexec/gnubin:$PATH
- export PATH=/usr/local/opt/gnu-sed/libexec/gnubin:$PATH
- export GOROOT=~/local/opt/go@1.20
- export PATH="${GOROOT}/bin:$PATH"
- export GOPATH=~/go1.20
- export PATH="${GOPATH}/bin:$PATH"
- export QT6DIR=/opt/Qt/6.3.2/macos
- export PATH="${QT6DIR}/bin:$PATH"
- uname -a
cache: {}
tags:
- macos-m1-bridge
.env-linux-build:
image: gitlab.protontech.ch:4567/go/bridge-internal:build-go1.20-qt6.3.2
variables:
VCPKG_DEFAULT_BINARY_CACHE: ${CI_PROJECT_DIR}/.cache
cache:
key: linux-vcpkg
paths:
- .cache
when: 'always'
before_script:
- mkdir -p .cache/bin
- export BRIDGE_SYNC_FORCE_MINIMUM_SPEC=1
- export PATH=$(pwd)/.cache/bin:$PATH
- export GOPATH="$CI_PROJECT_DIR/.cache"
- export PATH=$PATH:$QT6DIR/bin
- $(git config --global -l | grep -o 'url.*gitlab.protontech.ch.*insteadof' | xargs -L 1 git config --global --unset &> /dev/null) || echo "nothing to remove"
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
tags:
- large
# Stage: TEST
lint:
stage: test
extends:
- .rules-branch-manual-MR-and-devel-always
script:
- make lint
tags:
- medium
.script-test:
stage: test
extends:
- .rules-branch-manual-MR-and-devel-always
script:
- make test
artifacts:
paths:
- coverage/**
test-linux:
extends:
- .script-test
tags:
- large
test-linux-race:
extends:
- test-linux
- .rules-branch-and-MR-manual
script:
- make test-race
test-integration:
extends:
- test-linux
script:
- make test-integration
test-integration-race:
extends:
- test-integration
- .rules-branch-and-MR-manual
script:
- make test-integration-race
test-windows:
extends:
- .env-windows
- .script-test
- .rules-branch-and-MR-manual
test-darwin:
extends:
- .env-darwin
- .script-test
test-coverage:
stage: test
extends:
- .rules-branch-manual-MR-and-devel-always
script:
- ./utils/coverage.sh
coverage: '/total:.*\(statements\).*\d+\.\d+%/'
needs:
- test-linux
#- test-windows
- test-darwin
- test-integration
tags:
- small
artifacts:
paths:
- coverage*
- coverage/**
when: 'always'
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
# Stage: BUILD
.script-build:
stage: build
needs: ["lint"]
extends:
- .rules-branch-and-MR-manual
script:
- make build
- git diff && git diff-index --quiet HEAD
- make vault-editor
artifacts:
expire_in: 1 day
when: always
name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"
paths:
- bridge_*.tgz
- vault-editor
build-linux:
extends:
- .script-build
- .env-linux-build
build-linux-qa:
extends:
- build-linux
- .rules-branch-manual-MR-and-devel-always
variables:
BUILD_TAGS: "build_qa"
build-darwin:
extends:
- .script-build
- .env-darwin
build-darwin-qa:
extends:
- build-darwin
variables:
BUILD_TAGS: "build_qa"
build-windows:
extends:
- .build-base
- .windows-build-setup
- .script-build
- .env-windows
build-windows-qa:
extends:

View File

@ -36,6 +36,14 @@ issues:
- gosec
- goconst
- dogsled
- path: utils/smtp-send
linters:
- dupl
- gochecknoglobals
- gochecknoinits
- gosec
- goconst
- dogsled
linters-settings:
godox:

View File

@ -58,7 +58,7 @@ Proton Mail Bridge includes the following 3rd party software:
* [testify](https://github.com/stretchr/testify) available under [license](https://github.com/stretchr/testify/blob/master/LICENSE)
* [cli](https://github.com/urfave/cli/v2) available under [license](https://github.com/urfave/cli/v2/blob/master/LICENSE)
* [msgpack](https://github.com/vmihailenco/msgpack/v5) available under [license](https://github.com/vmihailenco/msgpack/v5/blob/master/LICENSE)
* [goleak](https://go.uber.org/goleak)
* [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)
* [sys](https://golang.org/x/sys) available under [license](https://cs.opensource.google/go/x/sys/+/master:LICENSE)
@ -66,16 +66,12 @@ Proton Mail Bridge includes the following 3rd party software:
* [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)
* [atlas](https://ariga.io/atlas)
* [ent](https://entgo.io/ent)
* [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)
* [go-srp](https://github.com/ProtonMail/go-srp) available under [license](https://github.com/ProtonMail/go-srp/blob/master/LICENSE)
* [readline](https://github.com/abiosoft/readline) available under [license](https://github.com/abiosoft/readline/blob/master/LICENSE)
* [levenshtein](https://github.com/agext/levenshtein) available under [license](https://github.com/agext/levenshtein/blob/master/LICENSE)
* [cascadia](https://github.com/andybalholm/cascadia) available under [license](https://github.com/andybalholm/cascadia/blob/master/LICENSE)
* [go-textseg](https://github.com/apparentlymart/go-textseg/v13) available under [license](https://github.com/apparentlymart/go-textseg/v13/blob/master/LICENSE)
* [sonic](https://github.com/bytedance/sonic) available under [license](https://github.com/bytedance/sonic/blob/master/LICENSE)
* [base64x](https://github.com/chenzhuoyu/base64x) available under [license](https://github.com/chenzhuoyu/base64x/blob/master/LICENSE)
* [test](https://github.com/chzyer/test) available under [license](https://github.com/chzyer/test/blob/master/LICENSE)
@ -93,7 +89,6 @@ Proton Mail Bridge includes the following 3rd party software:
* [mimetype](https://github.com/gabriel-vasile/mimetype) available under [license](https://github.com/gabriel-vasile/mimetype/blob/master/LICENSE)
* [sse](https://github.com/gin-contrib/sse) available under [license](https://github.com/gin-contrib/sse/blob/master/LICENSE)
* [gin](https://github.com/gin-gonic/gin) available under [license](https://github.com/gin-gonic/gin/blob/master/LICENSE)
* [inflect](https://github.com/go-openapi/inflect) available under [license](https://github.com/go-openapi/inflect/blob/master/LICENSE)
* [locales](https://github.com/go-playground/locales) available under [license](https://github.com/go-playground/locales/blob/master/LICENSE)
* [universal-translator](https://github.com/go-playground/universal-translator) available under [license](https://github.com/go-playground/universal-translator/blob/master/LICENSE)
* [validator](https://github.com/go-playground/validator/v10) available under [license](https://github.com/go-playground/validator/v10/blob/master/LICENSE)
@ -105,7 +100,6 @@ Proton Mail Bridge includes the following 3rd party software:
* [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)
* [golang-lru](https://github.com/hashicorp/golang-lru) available under [license](https://github.com/hashicorp/golang-lru/blob/master/LICENSE)
* [hcl](https://github.com/hashicorp/hcl/v2) available under [license](https://github.com/hashicorp/hcl/v2/blob/master/LICENSE)
* [multierror](https://github.com/joeshaw/multierror) available under [license](https://github.com/joeshaw/multierror/blob/master/LICENSE)
* [go](https://github.com/json-iterator/go) available under [license](https://github.com/json-iterator/go/blob/master/LICENSE)
* [cpuid](https://github.com/klauspost/cpuid/v2) available under [license](https://github.com/klauspost/cpuid/v2/blob/master/LICENSE)
@ -114,7 +108,6 @@ Proton Mail Bridge includes the following 3rd party software:
* [go-isatty](https://github.com/mattn/go-isatty) available under [license](https://github.com/mattn/go-isatty/blob/master/LICENSE)
* [go-runewidth](https://github.com/mattn/go-runewidth) available under [license](https://github.com/mattn/go-runewidth/blob/master/LICENSE)
* [go-sqlite3](https://github.com/mattn/go-sqlite3) available under [license](https://github.com/mattn/go-sqlite3/blob/master/LICENSE)
* [go-wordwrap](https://github.com/mitchellh/go-wordwrap) available under [license](https://github.com/mitchellh/go-wordwrap/blob/master/LICENSE)
* [concurrent](https://github.com/modern-go/concurrent) available under [license](https://github.com/modern-go/concurrent/blob/master/LICENSE)
* [reflect2](https://github.com/modern-go/reflect2) available under [license](https://github.com/modern-go/reflect2/blob/master/LICENSE)
* [tablewriter](https://github.com/olekukonko/tablewriter) available under [license](https://github.com/olekukonko/tablewriter/blob/master/LICENSE)
@ -130,14 +123,13 @@ 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-cty](https://github.com/zclconf/go-cty) available under [license](https://github.com/zclconf/go-cty/blob/master/LICENSE)
* [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)
* [genproto](https://google.golang.org/genproto)
gopkg.in/yaml.v3
* [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)
* [docker-credential-helpers](https://github.com/ProtonMail/docker-credential-helpers) available under [license](https://github.com/ProtonMail/docker-credential-helpers/blob/master/LICENSE)
* [go-message](https://github.com/ProtonMail/go-message) available under [license](https://github.com/ProtonMail/go-message/blob/master/LICENSE)
* [go-keychain](https://github.com/cuthix/go-keychain) available under [license](https://github.com/cuthix/go-keychain/blob/master/LICENSE)

View File

@ -3,6 +3,88 @@
Changelog [format](http://keepachangelog.com/en/1.0.0/)
## Trift Bridge 3.4.0
### Added
### Changed
* Test: Add require.Eventually to TestBridge_UserAgentFromSMTPClient.
* Test: Add smtp-send utility.
* GODT-2759: Check for oprhan messages.
* GODT-2759: Add prompt to download missing messages for analysis.
* GODT-2759: CLI debug commands.
* Remove gRPC auto-generated C++ source files.
* Test: Force all unit test to use minimum sync spec.
* Test: Force sync limits to minimum with env variable.
* GODT-2691: Close logrus output file on exit.
* GODT-2522: New Gluon database layout.
* GODT-2678: When internet is off, do not display status dot icon for the user in the context menu.
* GODT-2686: Change the orientation of the expand/collapse arrow for Advanced settings.
* Test(GODT-2636): Add step for sending from EML.
* Log failed message ids during sync.
* GODT-2510: Remove Ent.
* Test(GODT-2600): Changing state (read/unread, starred/unstarred) of a message in integration tests.
* GODT-2703: Got rid of account details dialog with Apple Mail autoconf.
* GODT-2685: Update to bug report log attachment logic.
* GODT-2690: Update sentry reporting in GUI for new log file naming.
* GODT-2668: Implemented new log retention policy.
* Test(GODT-2683): Save Draft without "Date" & "From" in headers.
* GODT-2666: Feat(GODT-2667): introduce sessionID in bridge.
* GODT-2660: Calculate bridge coverage and refactor CI yaml file.
* Fix dependency_license script to handle dot formated version.
### Fixed
* GODT-2812: Fix rare sync deadlock.
* GODT-2822: Better handling 429 during sync and event loop.
* GODT-2763: Missing Answered flag on Sync and Message Create.
* GODT-2758: Fix panic in SetFlagsOnMessages.
* GODT-2578: Refresh literals appended to Sent folder.
* GODT-2753: Vault test now check that value auto-assigned is first available port.
* GODT-2522: Handle migration with unreferenced db values.
* GODT-2693: Allow missing whitespace after header field colon.
* GODT-2653: Only log when err is not nil.
* GODT-2680: Fix for C++ debugger not working on ARM64 because of OpenSSL 3.1.
* GODT-2675: Update GPA to applye togin-gonic/gin patch + update COPYING_NOTES.
## Stone Bridge 3.3.2
### Fixed
* GODT-2782: Filter all labels when doing perma delete check.
## Stone Bridge 3.3.1
### Changed
* GODT-2707: Set bridge-gui default log level to 'debug'.
* GODT-2674: Add more logs during update failed.
* GODT-2750: Disable raise on main window when a notification is clicked on Linux.
* GODT-2709: Remove the config status file when user is removed.
* GODT-2748: Log calls that cause main window to show, with reason.
* GODT-2705: Added log entries for focus service on client and server sides.
* GODT-2712: Feed config_status with user action while pending.
* GODT-2728: Remove the sentry report for gRPC event stream interruptions in bridge-gui.
* GODT-2715: Add Unitary test for configStatus event.
* GODT-2715: Add Functional test for configStatus telemetry event.
* Disable windows runner.
* GODT-2714: Apply PR comments.
* GODT-2714: Set Configuration Status to Failure and send Recovery event when issue is solved.
* GODT-2713: Send config_progress event once a day if the configuration is stucked in pending for more than a day.
* GODT-2711: Send config_abort event on User removal.
* GODT-2710: Send config success on IMAP/SMTP connection..
* GODT-2716: Make Configuration Statistics persistent.
* GODT-2709: Init Configuration status.
* Log errors on failed message Downloads.
### Fixed
* GODT-2774: Only check telemetry availability for the current user.
* GODT-2774: Add external context to telemetry tasks.
* GODT-2774: Add context to Authorize in `gluon.Connector`.
* GODT-2726: Fix Parsing of Details field in GPA error message.
* GODT-2708: Fix dimensions event format + handling of ReportClicked event.
* GODT-2756: Fix for 'Settings' context menu opening the 'Help' page.
## Stone Bridge 3.3.0
### Changed

View File

@ -11,7 +11,7 @@ ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
.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.3.0+git
BRIDGE_APP_VERSION?=3.4.0+git
APP_VERSION:=${BRIDGE_APP_VERSION}
APP_FULL_NAME:=Proton Mail Bridge
APP_VENDOR:=Proton AG
@ -229,14 +229,28 @@ add-license:
change-copyright-year:
./utils/missing_license.sh change-year
GOCOVERAGE=-covermode=count -coverpkg=github.com/ProtonMail/proton-bridge/v3/internal/...,github.com/ProtonMail/proton-bridge/v3/pkg/...,
GOCOVERDIR=-args -test.gocoverdir=$$PWD/coverage
test: gofiles
go test -v -timeout=20m -p=1 -count=1 -coverprofile=/tmp/coverage.out -run=${TESTRUN} ./internal/... ./pkg/...
mkdir -p coverage/unit-${GOOS}
go test \
-v -timeout=20m -p=1 -count=1 \
${GOCOVERAGE} \
-run=${TESTRUN} ./internal/... ./pkg/... \
${GOCOVERDIR}/unit-${GOOS}
test-race: gofiles
go test -v -timeout=40m -p=1 -count=1 -race -failfast -run=${TESTRUN} ./internal/... ./pkg/...
test-integration: gofiles
go test -v -timeout=60m -p=1 -count=1 github.com/ProtonMail/proton-bridge/v3/tests
mkdir -p coverage/integration
go test \
-v -timeout=60m -p=1 -count=1 \
${GOCOVERAGE} \
github.com/ProtonMail/proton-bridge/v3/tests \
${GOCOVERDIR}/integration
test-integration-debug: gofiles
dlv test github.com/ProtonMail/proton-bridge/v3/tests -- -test.v -test.timeout=10m -test.parallel=1 -test.count=1
@ -260,6 +274,7 @@ mocks:
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/updater Downloader,Installer > internal/updater/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/telemetry HeartbeatManager > internal/telemetry/mocks/mocks.go
cp internal/telemetry/mocks/mocks.go internal/bridge/mocks/telemetry_mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/user MessageDownloader > internal/user/mocks/mocks.go
lint: gofiles lint-golang lint-license lint-dependencies lint-changelog

View File

@ -18,6 +18,7 @@
package main
import (
"io"
"os"
"path/filepath"
"runtime"
@ -43,9 +44,10 @@ import (
)
const (
appName = "Proton Mail Launcher"
exeName = "bridge"
guiName = "bridge-gui"
appName = "Proton Mail Launcher"
exeName = "bridge"
guiName = "bridge-gui"
launcherName = "launcher"
FlagCLI = "cli"
FlagCLIShort = "c"
@ -53,6 +55,7 @@ const (
FlagNonInteractiveShort = "n"
FlagLauncher = "--launcher"
FlagWait = "--wait"
FlagSessionID = "--session-id"
)
func main() { //nolint:funlen
@ -75,12 +78,26 @@ func main() { //nolint:funlen
if err != nil {
l.WithError(err).Fatal("Failed to get logs path")
}
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath))
if err := logging.Init(logsPath, os.Getenv("VERBOSITY")); err != nil {
sessionID := logging.NewSessionID()
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath, sessionID, launcherName))
var closer io.Closer
if closer, err = logging.Init(
logsPath,
sessionID,
logging.LauncherShortAppName,
logging.DefaultMaxLogFileSize,
logging.NoPruning,
os.Getenv("VERBOSITY"),
); err != nil {
l.WithError(err).Fatal("Failed to setup logging")
}
defer func() {
_ = logging.Close(closer)
}()
updatesPath, err := locations.ProvideUpdatesPath()
if err != nil {
l.WithError(err).Fatal("Failed to get updates path")
@ -134,7 +151,7 @@ func main() { //nolint:funlen
}
}
cmd := execabs.Command(exe, appendLauncherPath(launcher, args)...) //nolint:gosec
cmd := execabs.Command(exe, appendLauncherPath(launcher, append(args, FlagSessionID, string(sessionID)))...) //nolint:gosec
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout

14
go.mod
View File

@ -5,9 +5,9 @@ go 1.20
require (
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
github.com/Masterminds/semver/v3 v3.2.0
github.com/ProtonMail/gluon v0.16.1-0.20230607122549-dbdb8e1cc0c3
github.com/ProtonMail/gluon v0.16.1-0.20230706110757-a9327fb18611
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
github.com/ProtonMail/go-proton-api v0.4.1-0.20230605082423-67859aec0317
github.com/ProtonMail/go-proton-api v0.4.1-0.20230727082922-9115b4750ec7
github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton
github.com/PuerkitoBio/goquery v1.8.1
github.com/abiosoft/ishell v2.0.0+incompatible
@ -51,16 +51,12 @@ require (
)
require (
ariga.io/atlas v0.9.1-0.20230119145809-92243f7c55cb // indirect
entgo.io/ent v0.11.8 // indirect
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf // indirect
github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903 // indirect
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect
github.com/ProtonMail/go-srp v0.0.7 // indirect
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect
github.com/agext/levenshtein v1.2.3 // indirect
github.com/andybalholm/cascadia v1.3.2 // indirect
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
github.com/bytedance/sonic v1.9.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/chzyer/test v1.0.0 // indirect
@ -78,7 +74,6 @@ require (
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-gonic/gin v1.9.1 // indirect
github.com/go-openapi/inflect v0.19.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.0 // indirect
@ -90,7 +85,6 @@ require (
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/go-memdb v1.3.3 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/hashicorp/hcl/v2 v2.16.1 // indirect
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
@ -98,8 +92,7 @@ require (
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/mattn/go-sqlite3 v1.14.16 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/mattn/go-sqlite3 v1.14.17 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
@ -115,7 +108,6 @@ require (
github.com/ugorji/go/codec v1.2.11 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
github.com/zclconf/go-cty v1.12.1 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.9.0 // indirect
golang.org/x/mod v0.8.0 // indirect

34
go.sum
View File

@ -1,5 +1,3 @@
ariga.io/atlas v0.9.1-0.20230119145809-92243f7c55cb h1:mbsFtavDqGdYwdDpP50LGOOZ2hgyGoJcZeOpbgKMyu4=
ariga.io/atlas v0.9.1-0.20230119145809-92243f7c55cb/go.mod h1:T230JFcENj4ZZzMkZrXFDSkv+2kXkUgpJ5FQQ5hMcKU=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
@ -13,13 +11,10 @@ cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqCl
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=
entgo.io/ent v0.11.8 h1:M/M0QL1CYCUSdqGRXUrXhFYSDRJPsOOrr+RLEej/gyQ=
entgo.io/ent v0.11.8/go.mod h1:ericBi6Q8l3wBH1wEIDfKxw7rcQEuRPyBfbIzjtxJ18=
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557 h1:l6surSnJ3RP4qA1qmKJ+hQn3UjytosdoG27WGjrDlVs=
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557/go.mod h1:sTrmvD/TxuypdOERsDOS7SndZg0rzzcCi1b6wQMXUYM=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
@ -28,10 +23,8 @@ github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
github.com/ProtonMail/gluon v0.16.1-0.20230607083802-83f92429ca8d h1:+1BKm++zxmfGwj81q3jFkiDpgVwg529qznGbI//uXpk=
github.com/ProtonMail/gluon v0.16.1-0.20230607083802-83f92429ca8d/go.mod h1:ERZikuN+2i/oTeSwS5fq7J0Fms76uUcBlTAwT4KaEAk=
github.com/ProtonMail/gluon v0.16.1-0.20230607122549-dbdb8e1cc0c3 h1:VMbbJD3dcGPPIgbdQTS5Z4nX0QU/SsVZWdmsMVVBBsI=
github.com/ProtonMail/gluon v0.16.1-0.20230607122549-dbdb8e1cc0c3/go.mod h1:ERZikuN+2i/oTeSwS5fq7J0Fms76uUcBlTAwT4KaEAk=
github.com/ProtonMail/gluon v0.16.1-0.20230706110757-a9327fb18611 h1:QVydPr/+pgz5xihc2ujNNV+qnq3oTidIXvF0PgkcY6U=
github.com/ProtonMail/gluon v0.16.1-0.20230706110757-a9327fb18611/go.mod h1:Og5/Dz1MiGpCJn51XujZwxiLG7WzvvjE5PRpZBQmAHo=
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=
@ -42,8 +35,8 @@ github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7 h1:+j+Kd/
github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
github.com/ProtonMail/go-proton-api v0.4.1-0.20230605082423-67859aec0317 h1:Mo293UO7CfX1mL+jrBoSrftvhEyq6hyRDia2d6+Kx9U=
github.com/ProtonMail/go-proton-api v0.4.1-0.20230605082423-67859aec0317/go.mod h1:+aTJoYu8bqzGECXL2DOdiZTZ64bGn3w0NC8VcFpJrFM=
github.com/ProtonMail/go-proton-api v0.4.1-0.20230727082922-9115b4750ec7 h1:Rmg3TPK6vFGNWR4hxmPoBhV75Sl716iB46wEi2U4Q+c=
github.com/ProtonMail/go-proton-api v0.4.1-0.20230727082922-9115b4750ec7/go.mod h1:+aTJoYu8bqzGECXL2DOdiZTZ64bGn3w0NC8VcFpJrFM=
github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk=
github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton h1:YS6M20yvjCJPR1r4ADW5TPn6rahs4iAyZaACei86bEc=
@ -54,8 +47,6 @@ github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma
github.com/abiosoft/ishell v2.0.0+incompatible/go.mod h1:HQR9AqF2R3P4XXpMpI0NAzgHf/aS6+zVXRj14cVk9qg=
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db h1:CjPUSXOiYptLbTdr1RceuZgSFDQ7U15ITERUGrUORx8=
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db/go.mod h1:rB3B4rKii8V21ydCbIzH5hZiCQE7f5E9SzUb/ZZx530=
github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37 h1:28uU3TtuvQ6KRndxg9TrC868jBWmSKgh0GTXkACCXmA=
@ -63,8 +54,6 @@ github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37/go.m
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw=
github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
@ -159,8 +148,6 @@ github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4=
github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
@ -171,7 +158,6 @@ github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QX
github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4=
@ -248,8 +234,6 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/hcl/v2 v2.16.1 h1:BwuxEMD/tsYgbhIW7UuI3crjovf3MzuFWiVgiv57iHg=
github.com/hashicorp/hcl/v2 v2.16.1/go.mod h1:JRmR89jycNkrrqnMmvPDMd56n1rQJ2Q6KocSLCMCXng=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
@ -282,7 +266,6 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
@ -299,8 +282,8 @@ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
@ -309,8 +292,6 @@ github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceT
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
@ -365,7 +346,6 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y=
@ -419,8 +399,6 @@ github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRT
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zclconf/go-cty v1.12.1 h1:PcupnljUm9EIvbgSHQnHhUr3fO6oFmkOrvs2BAFNXXY=
github.com/zclconf/go-cty v1.12.1/go.mod h1:s9IfD1LK5ccNMSWCVFCE2rJfHiZgi7JijgeWIMfhLvA=
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=

View File

@ -19,6 +19,7 @@ package app
import (
"fmt"
"io"
"net/http"
"net/http/cookiejar"
"net/url"
@ -76,10 +77,12 @@ const (
flagNoWindow = "no-window"
flagParentPID = "parent-pid"
flagSoftwareRenderer = "software-renderer"
flagSessionID = "session-id"
)
const (
appUsage = "Proton Mail IMAP and SMTP Bridge"
appUsage = "Proton Mail IMAP and SMTP Bridge"
appShortName = "bridge"
)
func New() *cli.App {
@ -150,6 +153,10 @@ func New() *cli.App {
Hidden: true,
Value: false,
},
&cli.StringFlag{
Name: flagSessionID,
Hidden: true,
},
}
app.Action = run
@ -183,6 +190,11 @@ func run(c *cli.Context) error {
exe = os.Args[0]
}
var logCloser io.Closer
defer func() {
_ = logging.Close(logCloser)
}()
// Restart the app if requested.
return withRestarter(exe, func(restarter *restarter.Restarter) error {
// Handle crashes with various actions.
@ -199,7 +211,9 @@ func run(c *cli.Context) error {
}
// Initialize logging.
return withLogging(c, crashHandler, locations, func() error {
return withLogging(c, crashHandler, locations, func(closer io.Closer) error {
logCloser = closer
// If there was an error during migration, log it now.
if migrationErr != nil {
logrus.WithError(migrationErr).Error("Failed to migrate old app data")
@ -298,7 +312,7 @@ func withSingleInstance(settingPath, lockFile string, version *semver.Version, f
}
// Initialize our logging system.
func withLogging(c *cli.Context, crashHandler *crash.Handler, locations *locations.Locations, fn func() error) error {
func withLogging(c *cli.Context, crashHandler *crash.Handler, locations *locations.Locations, fn func(closer io.Closer) error) error {
logrus.Debug("Initializing logging")
defer logrus.Debug("Logging stopped")
@ -311,12 +325,21 @@ func withLogging(c *cli.Context, crashHandler *crash.Handler, locations *locatio
logrus.WithField("path", logsPath).Debug("Received logs path")
// Initialize logging.
if err := logging.Init(logsPath, c.String(flagLogLevel)); err != nil {
sessionID := logging.NewSessionIDFromString(c.String(flagSessionID))
var closer io.Closer
if closer, err = logging.Init(
logsPath,
sessionID,
logging.BridgeShortAppName,
logging.DefaultMaxLogFileSize,
logging.DefaultPruningSize,
c.String(flagLogLevel),
); err != nil {
return fmt.Errorf("could not initialize logging: %w", err)
}
// Ensure we dump a stack trace if we crash.
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath))
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath, sessionID, appShortName))
logrus.
WithField("appName", constants.FullAppName).
@ -329,7 +352,7 @@ func withLogging(c *cli.Context, crashHandler *crash.Handler, locations *locatio
WithField("SentryID", sentry.GetProtectedHostname()).
Info("Run app")
return fn()
return fn(closer)
}
// WithLocations provides access to locations where we store our files.

View File

@ -44,7 +44,7 @@ import (
// deleteOldGoIMAPFiles Set with `-ldflags -X app.deleteOldGoIMAPFiles=true` to enable cleanup of old imap cache data.
var deleteOldGoIMAPFiles bool //nolint:gochecknoglobals
// withBridge creates creates and tears down the bridge.
// withBridge creates and tears down the bridge.
func withBridge(
c *cli.Context,
exe string,

View File

@ -25,6 +25,7 @@ import (
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"time"
@ -300,8 +301,11 @@ func TestBridge_UserAgentFromSMTPClient(t *testing.T) {
string(info.BridgePass)),
))
currentUserAgent = b.GetCurrentUserAgent()
require.Contains(t, currentUserAgent, "UnknownClient/0.0.1")
require.Eventually(t, func() bool {
currentUserAgent = b.GetCurrentUserAgent()
return strings.Contains(currentUserAgent, "UnknownClient/0.0.1")
}, time.Minute, 5*time.Second)
})
})
}

View File

@ -18,23 +18,19 @@
package bridge
import (
"archive/zip"
"bytes"
"context"
"io"
"os"
"path/filepath"
"sort"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
)
const (
MaxTotalAttachmentSize = 7 * (1 << 20)
MaxCompressedFilesCount = 6
DefaultMaxBugReportZipSize = 7 * 1024 * 1024
DefaultMaxSessionCountForBugReport = 10
)
func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, description, username, email, client string, attachLogs bool) error {
@ -50,54 +46,25 @@ func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, descript
}
}
var atts []proton.ReportBugAttachment
var attachment []proton.ReportBugAttachment
if attachLogs {
logs, err := getMatchingLogs(bridge.locator, func(filename string) bool {
return logging.MatchLogName(filename) && !logging.MatchStackTraceName(filename)
})
logsPath, err := bridge.locator.ProvideLogsPath()
if err != nil {
return err
}
crashes, err := getMatchingLogs(bridge.locator, func(filename string) bool {
return logging.MatchLogName(filename) && logging.MatchStackTraceName(filename)
})
buffer, err := logging.ZipLogsForBugReport(logsPath, DefaultMaxSessionCountForBugReport, DefaultMaxBugReportZipSize)
if err != nil {
return err
}
guiLogs, err := getMatchingLogs(bridge.locator, func(filename string) bool {
return logging.MatchGUILogName(filename) && !logging.MatchStackTraceName(filename)
})
body, err := io.ReadAll(buffer)
if err != nil {
return err
}
var matchFiles []string
// Include bridge logs, up to a maximum amount.
matchFiles = append(matchFiles, logs[max(0, len(logs)-(MaxCompressedFilesCount/2)):]...)
// Include crash logs, up to a maximum amount.
matchFiles = append(matchFiles, crashes[max(0, len(crashes)-(MaxCompressedFilesCount/2)):]...)
// bridge-gui keeps just one small (~ 1kb) log file; we always include it.
if len(guiLogs) > 0 {
matchFiles = append(matchFiles, guiLogs[len(guiLogs)-1])
}
archive, err := zipFiles(matchFiles)
if err != nil {
return err
}
body, err := io.ReadAll(archive)
if err != nil {
return err
}
atts = append(atts, proton.ReportBugAttachment{
attachment = append(attachment, proton.ReportBugAttachment{
Name: "logs.zip",
Filename: "logs.zip",
MIMEType: "application/zip",
@ -105,6 +72,12 @@ func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, descript
})
}
safe.Lock(func() {
for _, user := range bridge.users {
user.ReportBugSent()
}
}, bridge.usersLock)
return bridge.api.ReportBug(ctx, proton.ReportBugReq{
OS: osType,
OSVersion: osVersion,
@ -118,116 +91,5 @@ func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, descript
Username: account,
Email: email,
}, atts...)
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
func getMatchingLogs(locator Locator, filenameMatchFunc func(string) bool) (filenames []string, err error) {
logsPath, err := locator.ProvideLogsPath()
if err != nil {
return nil, err
}
files, err := os.ReadDir(logsPath)
if err != nil {
return nil, err
}
var matchFiles []string
for _, file := range files {
if filenameMatchFunc(file.Name()) {
matchFiles = append(matchFiles, filepath.Join(logsPath, file.Name()))
}
}
sort.Strings(matchFiles) // Sorted by timestamp: oldest first.
return matchFiles, nil
}
type limitedBuffer struct {
capacity int
buf *bytes.Buffer
}
func newLimitedBuffer(capacity int) *limitedBuffer {
return &limitedBuffer{
capacity: capacity,
buf: bytes.NewBuffer(make([]byte, 0, capacity)),
}
}
func (b *limitedBuffer) Write(p []byte) (n int, err error) {
if len(p)+b.buf.Len() > b.capacity {
return 0, ErrSizeTooLarge
}
return b.buf.Write(p)
}
func (b *limitedBuffer) Read(p []byte) (n int, err error) {
return b.buf.Read(p)
}
func zipFiles(filenames []string) (io.Reader, error) {
if len(filenames) == 0 {
return nil, nil
}
buf := newLimitedBuffer(MaxTotalAttachmentSize)
w := zip.NewWriter(buf)
defer w.Close() //nolint:errcheck
for _, file := range filenames {
if err := addFileToZip(file, w); err != nil {
return nil, err
}
}
if err := w.Close(); err != nil {
return nil, err
}
return buf, nil
}
func addFileToZip(filename string, writer *zip.Writer) error {
fileReader, err := os.Open(filepath.Clean(filename))
if err != nil {
return err
}
defer fileReader.Close() //nolint:errcheck,gosec
fileInfo, err := fileReader.Stat()
if err != nil {
return err
}
header, err := zip.FileInfoHeader(fileInfo)
if err != nil {
return err
}
header.Method = zip.Deflate
header.Name = filepath.Base(filename)
fileWriter, err := writer.CreateHeader(header)
if err != nil {
return err
}
if _, err := io.Copy(fileWriter, fileReader); err != nil {
return err
}
return fileReader.Close()
}, attachment...)
}

View File

@ -0,0 +1,46 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge
import (
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
)
func (bridge *Bridge) ReportBugClicked() {
safe.Lock(func() {
for _, user := range bridge.users {
user.ReportBugClicked()
}
}, bridge.usersLock)
}
func (bridge *Bridge) AutoconfigUsed(client string) {
safe.Lock(func() {
for _, user := range bridge.users {
user.AutoconfigUsed(client)
}
}, bridge.usersLock)
}
func (bridge *Bridge) KBArticleOpened(article string) {
safe.Lock(func() {
for _, user := range bridge.users {
user.KBArticleOpened(article)
}
}, bridge.usersLock)
}

297
internal/bridge/debug.go Normal file
View File

@ -0,0 +1,297 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge
import (
"context"
"fmt"
"io"
"os"
"path/filepath"
"github.com/ProtonMail/gluon/imap"
"github.com/ProtonMail/gluon/rfc822"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/proton-bridge/v3/internal/user"
"github.com/bradenaw/juniper/iterator"
"github.com/bradenaw/juniper/xslices"
goimap "github.com/emersion/go-imap"
goimapclient "github.com/emersion/go-imap/client"
"github.com/sirupsen/logrus"
"golang.org/x/exp/maps"
)
type CheckClientStateResult struct {
MissingMessages map[string]map[string]user.DiagMailboxMessage
}
func (c *CheckClientStateResult) AddMissingMessage(userID string, message user.DiagMailboxMessage) {
v, ok := c.MissingMessages[userID]
if !ok {
c.MissingMessages[userID] = map[string]user.DiagMailboxMessage{message.ID: message}
} else {
v[message.ID] = message
}
}
// CheckClientState checks the current IMAP client reported state against the proton server state and reports
// anything that is out of place.
func (bridge *Bridge) CheckClientState(ctx context.Context, checkFlags bool, progressCB func(string)) (CheckClientStateResult, error) {
bridge.usersLock.RLock()
defer bridge.usersLock.RUnlock()
users := maps.Values(bridge.users)
result := CheckClientStateResult{
MissingMessages: make(map[string]map[string]user.DiagMailboxMessage),
}
for _, usr := range users {
if progressCB != nil {
progressCB(fmt.Sprintf("Checking state for user %v", usr.Name()))
}
log := logrus.WithField("user", usr.Name()).WithField("diag", "state-check")
log.Debug("Retrieving all server metadata")
meta, err := usr.GetDiagnosticMetadata(ctx)
if err != nil {
return result, err
}
success := true
if len(meta.Metadata) != len(meta.MessageIDs) {
log.Errorf("Metadata (%v) and message(%v) list sizes do not match", len(meta.Metadata), len(meta.MessageIDs))
}
log.Debug("Building state")
state, err := meta.BuildMailboxToMessageMap(usr)
if err != nil {
log.WithError(err).Error("Failed to build state")
return result, err
}
info, err := bridge.GetUserInfo(usr.ID())
if err != nil {
log.WithError(err).Error("Failed to get user info")
return result, err
}
addr := fmt.Sprintf("127.0.0.1:%v", bridge.GetIMAPPort())
for account, mboxMap := range state {
if progressCB != nil {
progressCB(fmt.Sprintf("Checking state for user %v's account '%v'", usr.Name(), account))
}
if err := func(account string, mboxMap user.AccountMailboxMap) error {
client, err := goimapclient.Dial(addr)
if err != nil {
log.WithError(err).Error("Failed to connect to imap client")
return err
}
defer func() {
_ = client.Logout()
}()
if err := client.Login(account, string(info.BridgePass)); err != nil {
return fmt.Errorf("failed to login for user %v:%w", usr.Name(), err)
}
log := log.WithField("account", account)
for mboxName, messageList := range mboxMap {
log := log.WithField("mbox", mboxName)
status, err := client.Select(mboxName, true)
if err != nil {
log.WithError(err).Errorf("Failed to select mailbox %v", messageList)
return fmt.Errorf("failed to select '%v':%w", mboxName, err)
}
log.Debug("Checking message count")
if int(status.Messages) != len(messageList) {
success = false
log.Errorf("Message count doesn't match, got '%v' expected '%v'", status.Messages, len(messageList))
}
ids, err := clientGetMessageIDs(client, mboxName)
if err != nil {
return fmt.Errorf("failed to get message ids for mbox '%v': %w", mboxName, err)
}
for _, msg := range messageList {
imapFlags, ok := ids[msg.ID]
if !ok {
if meta.FailedMessageIDs.Contains(msg.ID) {
log.Warningf("Missing message '%v', but it is part of failed message set", msg.ID)
} else {
log.Errorf("Missing message '%v'", msg.ID)
}
result.AddMissingMessage(msg.UserID, msg)
continue
}
if checkFlags {
if !imapFlags.Equals(msg.Flags) {
log.Errorf("Message '%v' flags do mot match, got=%v, expected=%v",
msg.ID,
imapFlags.ToSlice(),
msg.Flags.ToSlice(),
)
}
}
}
}
if !success {
log.Errorf("State does not match")
} else {
log.Info("State matches")
}
return nil
}(account, mboxMap); err != nil {
return result, err
}
}
// Check for orphaned messages (only present in All Mail)
if progressCB != nil {
progressCB(fmt.Sprintf("Checking user %v for orphans", usr.Name()))
}
log.Debugf("Checking for orphans")
for _, m := range meta.Metadata {
filteredLabels := xslices.Filter(m.LabelIDs, func(t string) bool {
switch t {
case proton.AllMailLabel:
return false
case proton.AllSentLabel:
return false
case proton.AllDraftsLabel:
return false
case proton.OutboxLabel:
return false
default:
return true
}
})
if len(filteredLabels) == 0 {
log.Warnf("Message %v is only present in All Mail (Subject=%v)", m.ID, m.Subject)
}
}
}
return result, nil
}
func (bridge *Bridge) DebugDownloadFailedMessages(
ctx context.Context,
result CheckClientStateResult,
exportPath string,
progressCB func(string, int, int),
) error {
bridge.usersLock.RLock()
defer bridge.usersLock.RUnlock()
for userID, messages := range result.MissingMessages {
usr, ok := bridge.users[userID]
if !ok {
return fmt.Errorf("failed to find user with id %v", userID)
}
userDir := filepath.Join(exportPath, userID)
if err := os.MkdirAll(userDir, 0o700); err != nil {
return fmt.Errorf("failed to create directory '%v': %w", userDir, err)
}
if err := usr.DebugDownloadMessages(ctx, userDir, messages, progressCB); err != nil {
return err
}
}
return nil
}
func clientGetMessageIDs(client *goimapclient.Client, mailbox string) (map[string]imap.FlagSet, error) {
status, err := client.Select(mailbox, false)
if err != nil {
return nil, err
}
if status.Messages == 0 {
return nil, nil
}
resCh := make(chan *goimap.Message)
section, err := goimap.ParseBodySectionName("BODY[HEADER]")
if err != nil {
return nil, err
}
fetchItems := []goimap.FetchItem{"BODY[HEADER]", goimap.FetchFlags}
seq, err := goimap.ParseSeqSet("1:*")
if err != nil {
return nil, err
}
go func() {
if err := client.Fetch(
seq,
fetchItems,
resCh,
); err != nil {
panic(err)
}
}()
messages := iterator.Collect(iterator.Chan(resCh))
ids := make(map[string]imap.FlagSet, len(messages))
for i, m := range messages {
literal, err := io.ReadAll(m.GetBody(section))
if err != nil {
return nil, err
}
header, err := rfc822.NewHeader(literal)
if err != nil {
return nil, fmt.Errorf("failed to parse header for msg %v: %w", i, err)
}
internalID, ok := header.GetChecked("X-Pm-Internal-Id")
if !ok {
logrus.Errorf("Message %v does not have internal id", internalID)
continue
}
messageFlags := imap.NewFlagSet(m.Flags...)
// Recent and Deleted are not part of the proton flag set.
messageFlags.RemoveFromSelf("\\Recent")
messageFlags.RemoveFromSelf("\\Deleted")
ids[internalID] = messageFlags
}
return ids, nil
}

View File

@ -0,0 +1,175 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge_test
import (
"bytes"
"context"
"crypto/tls"
"fmt"
"io"
"net"
"strings"
"testing"
"time"
"github.com/ProtonMail/gluon/rfc822"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/go-proton-api/server"
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
go_imap "github.com/emersion/go-imap"
"github.com/emersion/go-sasl"
"github.com/emersion/go-smtp"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"
)
func TestBridge_HandleDraftsSendFromOtherClient(t *testing.T) {
getGluonHeaderID := func(literal []byte) (string, string) {
h, err := rfc822.NewHeader(literal)
require.NoError(t, err)
gluonID, ok := h.GetChecked("X-Pm-Gluon-Id")
require.True(t, ok)
externalID, ok := h.GetChecked("Message-Id")
require.True(t, ok)
return gluonID, externalID
}
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
_, _, err := s.CreateUser("imap", password)
require.NoError(t, err)
_, _, err = s.CreateUser("bar", password)
require.NoError(t, err)
// The initial user should be fully synced.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
waiter := waitForIMAPServerReady(b)
defer waiter.Done()
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
defer done()
userID, err := b.LoginFull(ctx, "imap", password, nil, nil)
require.NoError(t, err)
require.Equal(t, userID, (<-syncCh).UserID)
waiter.Wait()
info, err := b.GetUserInfo(userID)
require.NoError(t, err)
require.True(t, info.State == bridge.Connected)
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = client.Logout() }()
// Create first draft in client.
literal := fmt.Sprintf(`From: %v
To: %v
Date: Fri, 3 Feb 2023 01:04:32 +0100
Subject: Foo
Hello
`, info.Addresses[0], "bar@proton.local")
require.NoError(t, client.Append("Drafts", nil, time.Now(), strings.NewReader(literal)))
// Verify the draft is available in client.
require.Eventually(t, func() bool {
status, err := client.Status("Drafts", []go_imap.StatusItem{go_imap.StatusMessages})
require.NoError(t, err)
return status.Messages == 1
}, 2*time.Second, time.Second)
// Retrieve the new literal so we can have the Proton Message ID.
messages, err := clientFetch(client, "Drafts")
require.NoError(t, err)
require.Equal(t, 1, len(messages))
newLiteral, err := io.ReadAll(messages[0].GetBody(must(go_imap.ParseBodySectionName("BODY[]"))))
require.NoError(t, err)
logrus.Info(string(newLiteral))
newLiteralID, newLiteralExternID := getGluonHeaderID(newLiteral)
// Modify new literal.
newLiteralModified := append(newLiteral, []byte(" world from client2")...) //nolint:gocritic
func() {
smtpClient, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(b.GetSMTPPort())))
require.NoError(t, err)
defer func() { _ = smtpClient.Close() }()
// Upgrade to TLS.
require.NoError(t, smtpClient.StartTLS(&tls.Config{InsecureSkipVerify: true}))
// Authorize with SASL PLAIN.
require.NoError(t, smtpClient.Auth(sasl.NewPlainClient(
info.Addresses[0],
info.Addresses[0],
string(info.BridgePass)),
))
// Send the message.
require.NoError(t, smtpClient.SendMail(
info.Addresses[0],
[]string{"bar@proton.local"},
bytes.NewReader(newLiteralModified),
))
}()
// Append message to Sent as the imap client would.
require.NoError(t, client.Append("Sent", nil, time.Now(), strings.NewReader(literal)))
// Verify the sent message gets updated with the new literal.
require.Eventually(t, func() bool {
// Check if sent message matches the latest draft.
messagesClient1, err := clientFetch(client, "Sent", "BODY[TEXT]", "BODY[]")
require.NoError(t, err)
if len(messagesClient1) != 1 {
return false
}
sentLiteral, err := io.ReadAll(messagesClient1[0].GetBody(must(go_imap.ParseBodySectionName("BODY[]"))))
require.NoError(t, err)
sentLiteralID, sentLiteralExternID := getGluonHeaderID(sentLiteral)
sentLiteralText, err := io.ReadAll(messagesClient1[0].GetBody(must(go_imap.ParseBodySectionName("BODY[TEXT]"))))
require.NoError(t, err)
sentLiteralStr := string(sentLiteralText)
literalMatches := sentLiteralStr == "Hello\r\n world from client2\r\n"
idIsDifferent := sentLiteralID != newLiteralID
externIDMatches := sentLiteralExternID == newLiteralExternID
return literalMatches && idIsDifferent && externIDMatches
}, 2*time.Second, time.Second)
})
}, server.WithMessageDedup())
}

View File

@ -32,7 +32,7 @@ import (
const HeartbeatCheckInterval = time.Hour
func (bridge *Bridge) IsTelemetryAvailable() bool {
func (bridge *Bridge) IsTelemetryAvailable(ctx context.Context) bool {
var flag = true
if bridge.GetTelemetryDisabled() {
return false
@ -40,14 +40,14 @@ func (bridge *Bridge) IsTelemetryAvailable() bool {
safe.RLock(func() {
for _, user := range bridge.users {
flag = flag && user.IsTelemetryEnabled(context.Background())
flag = flag && user.IsTelemetryEnabled(ctx)
}
}, bridge.usersLock)
return flag
}
func (bridge *Bridge) SendHeartbeat(heartbeat *telemetry.HeartbeatData) bool {
func (bridge *Bridge) SendHeartbeat(ctx context.Context, heartbeat *telemetry.HeartbeatData) bool {
data, err := json.Marshal(heartbeat)
if err != nil {
if err := bridge.reporter.ReportMessageWithContext("Cannot parse heartbeat data.", reporter.Context{
@ -62,7 +62,7 @@ func (bridge *Bridge) SendHeartbeat(heartbeat *telemetry.HeartbeatData) bool {
safe.RLock(func() {
for _, user := range bridge.users {
if err := user.SendTelemetry(context.Background(), data); err == nil {
if err := user.SendTelemetry(ctx, data); err == nil {
sent = true
break
}
@ -87,7 +87,7 @@ func (bridge *Bridge) StartHeartbeat(manager telemetry.HeartbeatManager) {
bridge.goHeartbeat = bridge.tasks.PeriodicOrTrigger(HeartbeatCheckInterval, 0, func(ctx context.Context) {
logrus.Debug("Checking for heartbeat")
bridge.heartbeat.TrySending()
bridge.heartbeat.TrySending(ctx)
})
bridge.heartbeat.SetRollout(bridge.GetUpdateRollout())

View File

@ -50,7 +50,7 @@ func NewMocks(tb testing.TB, version, minAuto *semver.Version) *Mocks {
mocks.CrashHandler.EXPECT().HandlePanic(gomock.Any()).AnyTimes()
// this is called at start of heartbeat process.
mocks.Heartbeat.EXPECT().IsTelemetryAvailable().AnyTimes()
mocks.Heartbeat.EXPECT().IsTelemetryAvailable(gomock.Any()).AnyTimes()
return mocks
}

View File

@ -5,6 +5,7 @@
package mocks
import (
context "context"
reflect "reflect"
time "time"
@ -50,31 +51,31 @@ func (mr *MockHeartbeatManagerMockRecorder) GetLastHeartbeatSent() *gomock.Call
}
// IsTelemetryAvailable mocks base method.
func (m *MockHeartbeatManager) IsTelemetryAvailable() bool {
func (m *MockHeartbeatManager) IsTelemetryAvailable(arg0 context.Context) bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IsTelemetryAvailable")
ret := m.ctrl.Call(m, "IsTelemetryAvailable", arg0)
ret0, _ := ret[0].(bool)
return ret0
}
// IsTelemetryAvailable indicates an expected call of IsTelemetryAvailable.
func (mr *MockHeartbeatManagerMockRecorder) IsTelemetryAvailable() *gomock.Call {
func (mr *MockHeartbeatManagerMockRecorder) IsTelemetryAvailable(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsTelemetryAvailable", reflect.TypeOf((*MockHeartbeatManager)(nil).IsTelemetryAvailable))
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsTelemetryAvailable", reflect.TypeOf((*MockHeartbeatManager)(nil).IsTelemetryAvailable), arg0)
}
// SendHeartbeat mocks base method.
func (m *MockHeartbeatManager) SendHeartbeat(arg0 *telemetry.HeartbeatData) bool {
func (m *MockHeartbeatManager) SendHeartbeat(arg0 context.Context, arg1 *telemetry.HeartbeatData) bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SendHeartbeat", arg0)
ret := m.ctrl.Call(m, "SendHeartbeat", arg0, arg1)
ret0, _ := ret[0].(bool)
return ret0
}
// SendHeartbeat indicates an expected call of SendHeartbeat.
func (mr *MockHeartbeatManagerMockRecorder) SendHeartbeat(arg0 interface{}) *gomock.Call {
func (mr *MockHeartbeatManagerMockRecorder) SendHeartbeat(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendHeartbeat", reflect.TypeOf((*MockHeartbeatManager)(nil).SendHeartbeat), arg0)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendHeartbeat", reflect.TypeOf((*MockHeartbeatManager)(nil).SendHeartbeat), arg0, arg1)
}
// SetLastHeartbeatSent mocks base method.

View File

@ -297,10 +297,11 @@ func (bridge *Bridge) SetColorScheme(colorScheme string) error {
// Note: it does not clear the keychain. The only entry in the keychain is the vault password,
// which we need at next startup to decrypt the vault.
func (bridge *Bridge) FactoryReset(ctx context.Context) {
useTelemetry := !bridge.GetTelemetryDisabled()
// Delete all the users.
safe.Lock(func() {
for _, user := range bridge.users {
bridge.logoutUser(ctx, user, true, true)
bridge.logoutUser(ctx, user, true, true, useTelemetry)
}
}, bridge.usersLock)

View File

@ -18,6 +18,7 @@
package bridge
import (
"context"
"fmt"
"io"
"strings"
@ -60,6 +61,9 @@ func (s *smtpSession) AuthPlain(username, password string) error {
if strings.Contains(s.Bridge.GetCurrentUserAgent(), useragent.DefaultUserAgent) {
s.Bridge.setUserAgent(useragent.UnknownClient, useragent.DefaultVersion)
}
user.SendConfigStatusSuccess(context.Background())
return nil
}
@ -67,8 +71,16 @@ func (s *smtpSession) AuthPlain(username, password string) error {
"username": username,
"pkg": "smtp",
}).Error("Incorrect login credentials.")
return fmt.Errorf("invalid username or password")
err := fmt.Errorf("invalid username or password")
for _, user := range s.users {
for _, mail := range user.Emails() {
if mail == username {
user.ReportConfigStatusFailure(err.Error())
return err
}
}
}
return err
}, s.usersLock)
}

View File

@ -399,6 +399,10 @@ func createNumMessages(ctx context.Context, t *testing.T, c *proton.Client, addr
}
func createMessages(ctx context.Context, t *testing.T, c *proton.Client, addrID, labelID string, messages ...[]byte) []string {
return createMessagesWithFlags(ctx, t, c, addrID, labelID, 0, messages...)
}
func createMessagesWithFlags(ctx context.Context, t *testing.T, c *proton.Client, addrID, labelID string, flags proton.MessageFlag, messages ...[]byte) []string {
user, err := c.GetUser(ctx)
require.NoError(t, err)
@ -417,6 +421,13 @@ func createMessages(ctx context.Context, t *testing.T, c *proton.Client, addrID,
_, ok := addrKRs[addrID]
require.True(t, ok)
var msgFlags proton.MessageFlag
if flags == 0 {
msgFlags = proton.MessageFlagReceived
} else {
msgFlags = flags
}
str, err := c.ImportMessages(
ctx,
addrKRs[addrID],
@ -427,7 +438,7 @@ func createMessages(ctx context.Context, t *testing.T, c *proton.Client, addrID,
Metadata: proton.ImportMetadata{
AddressID: addrID,
LabelIDs: []string{labelID},
Flags: proton.MessageFlagReceived,
Flags: msgFlags,
},
Message: message,
}

View File

@ -28,6 +28,7 @@ type Locator interface {
ProvideLogsPath() (string, error)
ProvideGluonCachePath() (string, error)
ProvideGluonDataPath() (string, error)
ProvideStatsPath() (string, error)
GetLicenseFilePath() string
GetDependencyLicensesLink() string
Clear(...string) error

View File

@ -229,7 +229,7 @@ func (bridge *Bridge) LogoutUser(ctx context.Context, userID string) error {
return ErrNoSuchUser
}
bridge.logoutUser(ctx, user, true, false)
bridge.logoutUser(ctx, user, true, false, false)
bridge.publish(events.UserLoggedOut{
UserID: userID,
@ -249,7 +249,7 @@ func (bridge *Bridge) DeleteUser(ctx context.Context, userID string) error {
}
if user, ok := bridge.users[userID]; ok {
bridge.logoutUser(ctx, user, true, true)
bridge.logoutUser(ctx, user, true, true, !bridge.GetTelemetryDisabled())
}
if err := bridge.vault.DeleteUser(userID); err != nil {
@ -351,7 +351,7 @@ func (bridge *Bridge) SendBadEventUserFeedback(_ context.Context, userID string,
logrus.WithError(rerr).Error("Failed to report feedback failure")
}
bridge.logoutUser(ctx, user, true, false)
bridge.logoutUser(ctx, user, true, false, false)
bridge.publish(events.UserLoggedOut{
UserID: userID,
@ -519,6 +519,11 @@ func (bridge *Bridge) addUserWithVault(
apiUser proton.User,
vault *vault.User,
) error {
statsPath, err := bridge.locator.ProvideStatsPath()
if err != nil {
return fmt.Errorf("failed to get Statistics directory: %w", err)
}
user, err := user.New(
ctx,
vault,
@ -528,6 +533,8 @@ func (bridge *Bridge) addUserWithVault(
bridge.panicHandler,
bridge.vault.GetShowAllMail(),
bridge.vault.GetMaxSyncMemory(),
statsPath,
bridge,
)
if err != nil {
return fmt.Errorf("failed to create user: %w", err)
@ -588,9 +595,14 @@ func (bridge *Bridge) newVaultUser(
}
// logout logs out the given user, optionally logging them out from the API too.
func (bridge *Bridge) logoutUser(ctx context.Context, user *user.User, withAPI, withData bool) {
func (bridge *Bridge) logoutUser(ctx context.Context, user *user.User, withAPI, withData, withTelemetry bool) {
defer delete(bridge.users, user.ID())
// if this is actually a remove account
if withData && withAPI {
user.SendConfigStatusAbort(ctx, withTelemetry)
}
logrus.WithFields(logrus.Fields{
"userID": user.ID(),
"withAPI": withAPI,

View File

@ -166,7 +166,8 @@ func (bridge *Bridge) handleUserRefreshed(ctx context.Context, user *user.User,
func (bridge *Bridge) handleUserDeauth(ctx context.Context, user *user.User) {
safe.Lock(func() {
bridge.logoutUser(ctx, user, false, false)
bridge.logoutUser(ctx, user, false, false, false)
user.ReportConfigStatusFailure("User deauth.")
}, bridge.usersLock)
}

View File

@ -70,9 +70,11 @@ func prepareMobileConfig(
password []byte,
) *mobileconfig.Config {
return &mobileconfig.Config{
DisplayName: username,
EmailAddress: addresses,
Identifier: "protonmail " + username + strconv.FormatInt(time.Now().Unix(), 10),
DisplayName: username,
EmailAddress: addresses,
AccountName: username,
AccountDescription: username,
Identifier: "protonmail " + username + strconv.FormatInt(time.Now().Unix(), 10),
IMAP: &mobileconfig.IMAP{
Hostname: hostname,
Port: imapPort,

View File

@ -0,0 +1,221 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package configstatus
import (
"encoding/json"
"fmt"
"os"
"strconv"
"time"
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/sirupsen/logrus"
)
const version = "1.0.0"
func LoadConfigurationStatus(filepath string) (*ConfigurationStatus, error) {
status := ConfigurationStatus{
FilePath: filepath,
DataLock: safe.NewRWMutex(),
Data: &ConfigurationStatusData{},
}
if _, err := os.Stat(filepath); err == nil {
if err := status.Load(); err == nil {
return &status, nil
}
logrus.WithError(err).Warn("Cannot load configuration status file. Reset it.")
}
status.Data.init()
if err := status.Save(); err != nil {
return &status, err
}
return &status, nil
}
func (status *ConfigurationStatus) Load() error {
bytes, err := os.ReadFile(status.FilePath)
if err != nil {
return err
}
var metadata MetadataOnly
if err := json.Unmarshal(bytes, &metadata); err != nil {
return err
}
if metadata.Metadata.Version != version {
return fmt.Errorf("unsupported configstatus file version %s", metadata.Metadata.Version)
}
return json.Unmarshal(bytes, status.Data)
}
func (status *ConfigurationStatus) Save() error {
temp := status.FilePath + "_temp"
f, err := os.Create(temp) //nolint:gosec
if err != nil {
return err
}
enc := json.NewEncoder(f)
enc.SetIndent("", " ")
err = enc.Encode(status.Data)
if err := f.Close(); err != nil {
logrus.WithError(err).Error("Error while closing configstatus file.")
}
if err != nil {
return err
}
return os.Rename(temp, status.FilePath)
}
func (status *ConfigurationStatus) IsPending() bool {
status.DataLock.RLock()
defer status.DataLock.RUnlock()
return !status.Data.DataV1.PendingSince.IsZero()
}
func (status *ConfigurationStatus) IsFromFailure() bool {
status.DataLock.RLock()
defer status.DataLock.RUnlock()
return status.Data.DataV1.FailureDetails != ""
}
func (status *ConfigurationStatus) ApplySuccess() error {
status.DataLock.Lock()
defer status.DataLock.Unlock()
status.Data.init()
status.Data.DataV1.PendingSince = time.Time{}
return status.Save()
}
func (status *ConfigurationStatus) ApplyFailure(err string) error {
status.DataLock.Lock()
defer status.DataLock.Unlock()
status.Data.init()
status.Data.DataV1.FailureDetails = err
return status.Save()
}
func (status *ConfigurationStatus) ApplyProgress() error {
status.DataLock.Lock()
defer status.DataLock.Unlock()
status.Data.DataV1.LastProgress = time.Now()
return status.Save()
}
func (status *ConfigurationStatus) RecordLinkClicked(link uint) error {
status.DataLock.Lock()
defer status.DataLock.Unlock()
if !status.Data.hasLinkClicked(link) {
status.Data.setClickedLink(link)
return status.Save()
}
return nil
}
func (status *ConfigurationStatus) ReportClicked() error {
status.DataLock.Lock()
defer status.DataLock.Unlock()
if !status.Data.DataV1.ReportClick {
status.Data.DataV1.ReportClick = true
return status.Save()
}
return nil
}
func (status *ConfigurationStatus) ReportSent() error {
status.DataLock.Lock()
defer status.DataLock.Unlock()
if !status.Data.DataV1.ReportSent {
status.Data.DataV1.ReportSent = true
return status.Save()
}
return nil
}
func (status *ConfigurationStatus) AutoconfigUsed(client string) error {
status.DataLock.Lock()
defer status.DataLock.Unlock()
if client != status.Data.DataV1.Autoconf {
status.Data.DataV1.Autoconf = client
return status.Save()
}
return nil
}
func (status *ConfigurationStatus) Remove() error {
status.DataLock.Lock()
defer status.DataLock.Unlock()
return os.Remove(status.FilePath)
}
func (data *ConfigurationStatusData) init() {
data.Metadata = Metadata{
Version: version,
}
data.DataV1.PendingSince = time.Now()
data.DataV1.LastProgress = time.Time{}
data.DataV1.Autoconf = ""
data.DataV1.ClickedLink = 0
data.DataV1.ReportSent = false
data.DataV1.ReportClick = false
data.DataV1.FailureDetails = ""
}
func (data *ConfigurationStatusData) setClickedLink(pos uint) {
data.DataV1.ClickedLink |= 1 << pos
}
func (data *ConfigurationStatusData) hasLinkClicked(pos uint) bool {
val := data.DataV1.ClickedLink & (1 << pos)
return val > 0
}
func (data *ConfigurationStatusData) clickedLinkToString() string {
var str = ""
var first = true
for i := 0; i < 64; i++ {
if data.hasLinkClicked(uint(i)) {
if !first {
str += ","
} else {
first = false
str += "["
}
str += strconv.Itoa(i)
}
}
if str != "" {
str += "]"
}
return str
}

View File

@ -0,0 +1,252 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package configstatus_test
import (
"encoding/json"
"os"
"path/filepath"
"testing"
"time"
"github.com/ProtonMail/proton-bridge/v3/internal/configstatus"
"github.com/stretchr/testify/require"
)
func TestConfigStatus_init_virgin(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
require.Equal(t, "1.0.0", config.Data.Metadata.Version)
require.Equal(t, false, config.Data.DataV1.PendingSince.IsZero())
require.Equal(t, true, config.Data.DataV1.LastProgress.IsZero())
require.Equal(t, "", config.Data.DataV1.Autoconf)
require.Equal(t, uint64(0), config.Data.DataV1.ClickedLink)
require.Equal(t, false, config.Data.DataV1.ReportSent)
require.Equal(t, false, config.Data.DataV1.ReportClick)
require.Equal(t, "", config.Data.DataV1.FailureDetails)
}
func TestConfigStatus_init_existing(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
var data = configstatus.ConfigurationStatusData{
Metadata: configstatus.Metadata{Version: "1.0.0"},
DataV1: configstatus.DataV1{Autoconf: "Mr TBird"},
}
require.NoError(t, dumpConfigStatusInFile(&data, file))
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
require.Equal(t, "1.0.0", config.Data.Metadata.Version)
require.Equal(t, "Mr TBird", config.Data.DataV1.Autoconf)
}
func TestConfigStatus_init_bad_version(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
var data = configstatus.ConfigurationStatusData{
Metadata: configstatus.Metadata{Version: "2.0.0"},
DataV1: configstatus.DataV1{Autoconf: "Mr TBird"},
}
require.NoError(t, dumpConfigStatusInFile(&data, file))
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
require.Equal(t, "1.0.0", config.Data.Metadata.Version)
require.Equal(t, "", config.Data.DataV1.Autoconf)
}
func TestConfigStatus_IsPending(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
require.Equal(t, true, config.IsPending())
config.Data.DataV1.PendingSince = time.Time{}
require.Equal(t, false, config.IsPending())
}
func TestConfigStatus_IsFromFailure(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
require.Equal(t, false, config.IsFromFailure())
config.Data.DataV1.FailureDetails = "test"
require.Equal(t, true, config.IsFromFailure())
}
func TestConfigStatus_ApplySuccess(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
require.Equal(t, true, config.IsPending())
require.NoError(t, config.ApplySuccess())
require.Equal(t, false, config.IsPending())
config2, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
require.Equal(t, "1.0.0", config2.Data.Metadata.Version)
require.Equal(t, true, config2.Data.DataV1.PendingSince.IsZero())
require.Equal(t, true, config2.Data.DataV1.LastProgress.IsZero())
require.Equal(t, "", config2.Data.DataV1.Autoconf)
require.Equal(t, uint64(0), config2.Data.DataV1.ClickedLink)
require.Equal(t, false, config2.Data.DataV1.ReportSent)
require.Equal(t, false, config2.Data.DataV1.ReportClick)
require.Equal(t, "", config2.Data.DataV1.FailureDetails)
}
func TestConfigStatus_ApplyFailure(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
require.NoError(t, config.ApplySuccess())
require.NoError(t, config.ApplyFailure("Big Failure"))
require.Equal(t, true, config.IsFromFailure())
require.Equal(t, true, config.IsPending())
config2, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
require.Equal(t, "1.0.0", config2.Data.Metadata.Version)
require.Equal(t, false, config2.Data.DataV1.PendingSince.IsZero())
require.Equal(t, true, config2.Data.DataV1.LastProgress.IsZero())
require.Equal(t, "", config2.Data.DataV1.Autoconf)
require.Equal(t, uint64(0), config2.Data.DataV1.ClickedLink)
require.Equal(t, false, config2.Data.DataV1.ReportSent)
require.Equal(t, false, config2.Data.DataV1.ReportClick)
require.Equal(t, "Big Failure", config2.Data.DataV1.FailureDetails)
}
func TestConfigStatus_ApplyProgress(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
require.Equal(t, true, config.IsPending())
require.Equal(t, true, config.Data.DataV1.LastProgress.IsZero())
require.NoError(t, config.ApplyProgress())
config2, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
require.Equal(t, "1.0.0", config2.Data.Metadata.Version)
require.Equal(t, false, config2.Data.DataV1.PendingSince.IsZero())
require.Equal(t, false, config2.Data.DataV1.LastProgress.IsZero())
require.Equal(t, "", config2.Data.DataV1.Autoconf)
require.Equal(t, uint64(0), config2.Data.DataV1.ClickedLink)
require.Equal(t, false, config2.Data.DataV1.ReportSent)
require.Equal(t, false, config2.Data.DataV1.ReportClick)
require.Equal(t, "", config2.Data.DataV1.FailureDetails)
}
func TestConfigStatus_RecordLinkClicked(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
require.Equal(t, uint64(0), config.Data.DataV1.ClickedLink)
require.NoError(t, config.RecordLinkClicked(0))
require.Equal(t, uint64(1), config.Data.DataV1.ClickedLink)
require.NoError(t, config.RecordLinkClicked(1))
require.Equal(t, uint64(3), config.Data.DataV1.ClickedLink)
config2, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
require.Equal(t, "1.0.0", config2.Data.Metadata.Version)
require.Equal(t, false, config2.Data.DataV1.PendingSince.IsZero())
require.Equal(t, true, config2.Data.DataV1.LastProgress.IsZero())
require.Equal(t, "", config2.Data.DataV1.Autoconf)
require.Equal(t, uint64(3), config2.Data.DataV1.ClickedLink)
require.Equal(t, false, config2.Data.DataV1.ReportSent)
require.Equal(t, false, config2.Data.DataV1.ReportClick)
require.Equal(t, "", config2.Data.DataV1.FailureDetails)
}
func TestConfigStatus_ReportClicked(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
require.Equal(t, false, config.Data.DataV1.ReportClick)
require.NoError(t, config.ReportClicked())
require.Equal(t, true, config.Data.DataV1.ReportClick)
config2, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
require.Equal(t, "1.0.0", config2.Data.Metadata.Version)
require.Equal(t, false, config2.Data.DataV1.PendingSince.IsZero())
require.Equal(t, true, config2.Data.DataV1.LastProgress.IsZero())
require.Equal(t, "", config2.Data.DataV1.Autoconf)
require.Equal(t, uint64(0), config2.Data.DataV1.ClickedLink)
require.Equal(t, false, config2.Data.DataV1.ReportSent)
require.Equal(t, true, config2.Data.DataV1.ReportClick)
require.Equal(t, "", config2.Data.DataV1.FailureDetails)
}
func TestConfigStatus_ReportSent(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
require.Equal(t, false, config.Data.DataV1.ReportSent)
require.NoError(t, config.ReportSent())
require.Equal(t, true, config.Data.DataV1.ReportSent)
config2, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
require.Equal(t, "1.0.0", config2.Data.Metadata.Version)
require.Equal(t, false, config2.Data.DataV1.PendingSince.IsZero())
require.Equal(t, true, config2.Data.DataV1.LastProgress.IsZero())
require.Equal(t, "", config2.Data.DataV1.Autoconf)
require.Equal(t, uint64(0), config2.Data.DataV1.ClickedLink)
require.Equal(t, true, config2.Data.DataV1.ReportSent)
require.Equal(t, false, config2.Data.DataV1.ReportClick)
require.Equal(t, "", config2.Data.DataV1.FailureDetails)
}
func dumpConfigStatusInFile(data *configstatus.ConfigurationStatusData, file string) error {
f, err := os.Create(file)
if err != nil {
return err
}
defer func() { _ = f.Close() }()
return json.NewEncoder(f).Encode(data)
}

View File

@ -0,0 +1,57 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package configstatus
import (
"strconv"
"time"
)
type ConfigAbortValues struct {
Duration int `json:"duration"`
}
type ConfigAbortDimensions struct {
ReportClick string `json:"report_click"`
ReportSent string `json:"report_sent"`
ClickedLink string `json:"clicked_link"`
}
type ConfigAbortData struct {
MeasurementGroup string
Event string
Values ConfigSuccessValues
Dimensions ConfigSuccessDimensions
}
type ConfigAbortBuilder struct{}
func (*ConfigAbortBuilder) New(data *ConfigurationStatusData) ConfigAbortData {
return ConfigAbortData{
MeasurementGroup: "bridge.any.configuration",
Event: "bridge_config_abort",
Values: ConfigSuccessValues{
Duration: int(time.Since(data.DataV1.PendingSince).Minutes()),
},
Dimensions: ConfigSuccessDimensions{
ReportClick: strconv.FormatBool(data.DataV1.ReportClick),
ReportSent: strconv.FormatBool(data.DataV1.ReportSent),
ClickedLink: data.clickedLinkToString(),
},
}
}

View File

@ -0,0 +1,75 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package configstatus_test
import (
"path/filepath"
"testing"
"time"
"github.com/ProtonMail/proton-bridge/v3/internal/configstatus"
"github.com/stretchr/testify/require"
)
func TestConfigurationAbort_default(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
var builder = configstatus.ConfigAbortBuilder{}
req := builder.New(config.Data)
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_abort", req.Event)
require.Equal(t, 0, req.Values.Duration)
require.Equal(t, "false", req.Dimensions.ReportClick)
require.Equal(t, "false", req.Dimensions.ReportSent)
require.Equal(t, "", req.Dimensions.ClickedLink)
}
func TestConfigurationAbort_fed(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
var data = configstatus.ConfigurationStatusData{
Metadata: configstatus.Metadata{Version: "1.0.0"},
DataV1: configstatus.DataV1{
PendingSince: time.Now().Add(-10 * time.Minute),
LastProgress: time.Time{},
Autoconf: "Mr TBird",
ClickedLink: 42,
ReportSent: false,
ReportClick: true,
FailureDetails: "Not an error",
},
}
require.NoError(t, dumpConfigStatusInFile(&data, file))
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
var builder = configstatus.ConfigAbortBuilder{}
req := builder.New(config.Data)
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_abort", req.Event)
require.Equal(t, 10, req.Values.Duration)
require.Equal(t, "true", req.Dimensions.ReportClick)
require.Equal(t, "false", req.Dimensions.ReportSent)
require.Equal(t, "[1,3,5]", req.Dimensions.ClickedLink)
}

View File

@ -0,0 +1,60 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package configstatus
import "time"
type ConfigProgressValues struct {
NbDay int `json:"nb_day"`
NbDaySinceLast int `json:"nb_day_since_last"`
}
type ConfigProgressData struct {
MeasurementGroup string
Event string
Values ConfigProgressValues
Dimensions struct{}
}
type ConfigProgressBuilder struct{}
func (*ConfigProgressBuilder) New(data *ConfigurationStatusData) ConfigProgressData {
return ConfigProgressData{
MeasurementGroup: "bridge.any.configuration",
Event: "bridge_config_progress",
Values: ConfigProgressValues{
NbDay: numberOfDay(time.Now(), data.DataV1.PendingSince),
NbDaySinceLast: numberOfDay(time.Now(), data.DataV1.LastProgress),
},
}
}
func numberOfDay(now, prev time.Time) int {
if now.IsZero() || prev.IsZero() {
return 1
}
if now.Year() > prev.Year() {
if now.YearDay() > prev.YearDay() {
return 365 + (now.YearDay() - prev.YearDay())
}
return (prev.YearDay() + now.YearDay()) - 365
} else if now.YearDay() > prev.YearDay() {
return now.YearDay() - prev.YearDay()
}
return 0
}

View File

@ -0,0 +1,71 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package configstatus_test
import (
"path/filepath"
"testing"
"time"
"github.com/ProtonMail/proton-bridge/v3/internal/configstatus"
"github.com/stretchr/testify/require"
)
func TestConfigurationProgress_default(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
var builder = configstatus.ConfigProgressBuilder{}
req := builder.New(config.Data)
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_progress", req.Event)
require.Equal(t, 0, req.Values.NbDay)
require.Equal(t, 1, req.Values.NbDaySinceLast)
}
func TestConfigurationProgress_fed(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
var data = configstatus.ConfigurationStatusData{
Metadata: configstatus.Metadata{Version: "1.0.0"},
DataV1: configstatus.DataV1{
PendingSince: time.Now().AddDate(0, 0, -5),
LastProgress: time.Now().AddDate(0, 0, -2),
Autoconf: "Mr TBird",
ClickedLink: 42,
ReportSent: false,
ReportClick: true,
FailureDetails: "Not an error",
},
}
require.NoError(t, dumpConfigStatusInFile(&data, file))
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
var builder = configstatus.ConfigProgressBuilder{}
req := builder.New(config.Data)
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_progress", req.Event)
require.Equal(t, 5, req.Values.NbDay)
require.Equal(t, 2, req.Values.NbDaySinceLast)
}

View File

@ -0,0 +1,61 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package configstatus
import (
"strconv"
"time"
)
type ConfigRecoveryValues struct {
Duration int `json:"duration"`
}
type ConfigRecoveryDimensions struct {
Autoconf string `json:"autoconf"`
ReportClick string `json:"report_click"`
ReportSent string `json:"report_sent"`
ClickedLink string `json:"clicked_link"`
FailureDetails string `json:"failure_details"`
}
type ConfigRecoveryData struct {
MeasurementGroup string
Event string
Values ConfigRecoveryValues
Dimensions ConfigRecoveryDimensions
}
type ConfigRecoveryBuilder struct{}
func (*ConfigRecoveryBuilder) New(data *ConfigurationStatusData) ConfigRecoveryData {
return ConfigRecoveryData{
MeasurementGroup: "bridge.any.configuration",
Event: "bridge_config_recovery",
Values: ConfigRecoveryValues{
Duration: int(time.Since(data.DataV1.PendingSince).Minutes()),
},
Dimensions: ConfigRecoveryDimensions{
Autoconf: data.DataV1.Autoconf,
ReportClick: strconv.FormatBool(data.DataV1.ReportClick),
ReportSent: strconv.FormatBool(data.DataV1.ReportSent),
ClickedLink: data.clickedLinkToString(),
FailureDetails: data.DataV1.FailureDetails,
},
}
}

View File

@ -0,0 +1,79 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package configstatus_test
import (
"path/filepath"
"testing"
"time"
"github.com/ProtonMail/proton-bridge/v3/internal/configstatus"
"github.com/stretchr/testify/require"
)
func TestConfigurationRecovery_default(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
var builder = configstatus.ConfigRecoveryBuilder{}
req := builder.New(config.Data)
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_recovery", req.Event)
require.Equal(t, 0, req.Values.Duration)
require.Equal(t, "", req.Dimensions.Autoconf)
require.Equal(t, "false", req.Dimensions.ReportClick)
require.Equal(t, "false", req.Dimensions.ReportSent)
require.Equal(t, "", req.Dimensions.ClickedLink)
require.Equal(t, "", req.Dimensions.FailureDetails)
}
func TestConfigurationRecovery_fed(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
var data = configstatus.ConfigurationStatusData{
Metadata: configstatus.Metadata{Version: "1.0.0"},
DataV1: configstatus.DataV1{
PendingSince: time.Now().Add(-10 * time.Minute),
LastProgress: time.Time{},
Autoconf: "Mr TBird",
ClickedLink: 42,
ReportSent: false,
ReportClick: true,
FailureDetails: "Not an error",
},
}
require.NoError(t, dumpConfigStatusInFile(&data, file))
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
var builder = configstatus.ConfigRecoveryBuilder{}
req := builder.New(config.Data)
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_recovery", req.Event)
require.Equal(t, 10, req.Values.Duration)
require.Equal(t, "Mr TBird", req.Dimensions.Autoconf)
require.Equal(t, "true", req.Dimensions.ReportClick)
require.Equal(t, "false", req.Dimensions.ReportSent)
require.Equal(t, "[1,3,5]", req.Dimensions.ClickedLink)
require.Equal(t, "Not an error", req.Dimensions.FailureDetails)
}

View File

@ -0,0 +1,59 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package configstatus
import (
"strconv"
"time"
)
type ConfigSuccessValues struct {
Duration int `json:"duration"`
}
type ConfigSuccessDimensions struct {
Autoconf string `json:"autoconf"`
ReportClick string `json:"report_click"`
ReportSent string `json:"report_sent"`
ClickedLink string `json:"clicked_link"`
}
type ConfigSuccessData struct {
MeasurementGroup string
Event string
Values ConfigSuccessValues
Dimensions ConfigSuccessDimensions
}
type ConfigSuccessBuilder struct{}
func (*ConfigSuccessBuilder) New(data *ConfigurationStatusData) ConfigSuccessData {
return ConfigSuccessData{
MeasurementGroup: "bridge.any.configuration",
Event: "bridge_config_success",
Values: ConfigSuccessValues{
Duration: int(time.Since(data.DataV1.PendingSince).Minutes()),
},
Dimensions: ConfigSuccessDimensions{
Autoconf: data.DataV1.Autoconf,
ReportClick: strconv.FormatBool(data.DataV1.ReportClick),
ReportSent: strconv.FormatBool(data.DataV1.ReportSent),
ClickedLink: data.clickedLinkToString(),
},
}
}

View File

@ -0,0 +1,77 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package configstatus_test
import (
"path/filepath"
"testing"
"time"
"github.com/ProtonMail/proton-bridge/v3/internal/configstatus"
"github.com/stretchr/testify/require"
)
func TestConfigurationSuccess_default(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
var builder = configstatus.ConfigSuccessBuilder{}
req := builder.New(config.Data)
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_success", req.Event)
require.Equal(t, 0, req.Values.Duration)
require.Equal(t, "", req.Dimensions.Autoconf)
require.Equal(t, "false", req.Dimensions.ReportClick)
require.Equal(t, "false", req.Dimensions.ReportSent)
require.Equal(t, "", req.Dimensions.ClickedLink)
}
func TestConfigurationSuccess_fed(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
var data = configstatus.ConfigurationStatusData{
Metadata: configstatus.Metadata{Version: "1.0.0"},
DataV1: configstatus.DataV1{
PendingSince: time.Now().Add(-10 * time.Minute),
LastProgress: time.Time{},
Autoconf: "Mr TBird",
ClickedLink: 42,
ReportSent: false,
ReportClick: true,
FailureDetails: "Not an error",
},
}
require.NoError(t, dumpConfigStatusInFile(&data, file))
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
var builder = configstatus.ConfigSuccessBuilder{}
req := builder.New(config.Data)
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_success", req.Event)
require.Equal(t, 10, req.Values.Duration)
require.Equal(t, "Mr TBird", req.Dimensions.Autoconf)
require.Equal(t, "true", req.Dimensions.ReportClick)
require.Equal(t, "false", req.Dimensions.ReportSent)
require.Equal(t, "[1,3,5]", req.Dimensions.ClickedLink)
}

View File

@ -0,0 +1,56 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package configstatus
import (
"time"
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
)
const ProgressCheckInterval = time.Hour
type Metadata struct {
Version string `json:"version"`
}
type MetadataOnly struct {
Metadata Metadata `json:"metadata"`
}
type DataV1 struct {
PendingSince time.Time `json:"pending_since"`
LastProgress time.Time `json:"last_progress"`
Autoconf string `json:"auto_conf"`
ClickedLink uint64 `json:"clicked_link"`
ReportSent bool `json:"report_sent"`
ReportClick bool `json:"report_click"`
FailureDetails string `json:"failure_details"`
}
type ConfigurationStatusData struct {
Metadata Metadata `json:"metadata"`
DataV1 DataV1 `json:"dataV1"`
}
type ConfigurationStatus struct {
FilePath string
DataLock safe.RWMutex
Data *ConfigurationStatusData
}

View File

@ -30,6 +30,7 @@ import (
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/protobuf/types/known/emptypb"
"google.golang.org/protobuf/types/known/wrapperspb"
)
// TryRaise tries to raise the application by dialing the focus service.
@ -38,7 +39,7 @@ func TryRaise(settingsPath string) bool {
var raised bool
if err := withClientConn(context.Background(), settingsPath, func(ctx context.Context, client proto.FocusClient) error {
if _, err := client.Raise(ctx, &emptypb.Empty{}); err != nil {
if _, err := client.Raise(ctx, &wrapperspb.StringValue{Value: "TryRaise"}); err != nil {
return fmt.Errorf("failed to call client.Raise: %w", err)
}

View File

@ -18,7 +18,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.0
// protoc v3.21.3
// protoc v3.21.12
// source: focus.proto
package proto
@ -27,6 +27,7 @@ import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
emptypb "google.golang.org/protobuf/types/known/emptypb"
wrapperspb "google.golang.org/protobuf/types/known/wrapperspb"
reflect "reflect"
sync "sync"
)
@ -91,22 +92,24 @@ var file_focus_proto_rawDesc = []byte{
0x0a, 0x0b, 0x66, 0x6f, 0x63, 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x66,
0x6f, 0x63, 0x75, 0x73, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
0x75, 0x66, 0x2f, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x22, 0x2b, 0x0a, 0x0f, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18,
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x32, 0x7b,
0x0a, 0x05, 0x46, 0x6f, 0x63, 0x75, 0x73, 0x12, 0x37, 0x0a, 0x05, 0x52, 0x61, 0x69, 0x73, 0x65,
0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79,
0x12, 0x39, 0x0a, 0x07, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f,
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d,
0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x66, 0x6f, 0x63, 0x75, 0x73, 0x2e, 0x56, 0x65, 0x72, 0x73,
0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x3d, 0x5a, 0x3b, 0x67,
0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x6e,
0x4d, 0x61, 0x69, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x6e, 0x2d, 0x62, 0x72, 0x69, 0x64,
0x67, 0x65, 0x2f, 0x76, 0x33, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x66,
0x6f, 0x63, 0x75, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x33,
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x32, 0x81,
0x01, 0x0a, 0x05, 0x46, 0x6f, 0x63, 0x75, 0x73, 0x12, 0x3d, 0x0a, 0x05, 0x52, 0x61, 0x69, 0x73,
0x65, 0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a,
0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x39, 0x0a, 0x07, 0x56, 0x65, 0x72, 0x73, 0x69,
0x6f, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x66, 0x6f, 0x63,
0x75, 0x73, 0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x42, 0x3d, 0x5a, 0x3b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
0x2f, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x6e, 0x4d, 0x61, 0x69, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x6e, 0x2d, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x2f, 0x76, 0x33, 0x2f, 0x69, 0x6e, 0x74,
0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x66, 0x6f, 0x63, 0x75, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@ -123,13 +126,14 @@ func file_focus_proto_rawDescGZIP() []byte {
var file_focus_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_focus_proto_goTypes = []interface{}{
(*VersionResponse)(nil), // 0: focus.VersionResponse
(*emptypb.Empty)(nil), // 1: google.protobuf.Empty
(*VersionResponse)(nil), // 0: focus.VersionResponse
(*wrapperspb.StringValue)(nil), // 1: google.protobuf.StringValue
(*emptypb.Empty)(nil), // 2: google.protobuf.Empty
}
var file_focus_proto_depIdxs = []int32{
1, // 0: focus.Focus.Raise:input_type -> google.protobuf.Empty
1, // 1: focus.Focus.Version:input_type -> google.protobuf.Empty
1, // 2: focus.Focus.Raise:output_type -> google.protobuf.Empty
1, // 0: focus.Focus.Raise:input_type -> google.protobuf.StringValue
2, // 1: focus.Focus.Version:input_type -> google.protobuf.Empty
2, // 2: focus.Focus.Raise:output_type -> google.protobuf.Empty
0, // 3: focus.Focus.Version:output_type -> focus.VersionResponse
2, // [2:4] is the sub-list for method output_type
0, // [0:2] is the sub-list for method input_type

View File

@ -18,6 +18,7 @@
syntax = "proto3";
import "google/protobuf/empty.proto";
import "google/protobuf/wrappers.proto";
option go_package = "github.com/ProtonMail/proton-bridge/v3/internal/focus/proto";
@ -27,7 +28,7 @@ package focus; // ignored by Go, used as namespace name in C++.
// Service Declaration
//**********************************************************************************************************************≠––
service Focus {
rpc Raise(google.protobuf.Empty) returns (google.protobuf.Empty);
rpc Raise(google.protobuf.StringValue) returns (google.protobuf.Empty);
rpc Version(google.protobuf.Empty) returns (VersionResponse);
}

View File

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.2.0
// - protoc v3.21.3
// - protoc v3.21.12
// source: focus.proto
package proto
@ -12,6 +12,7 @@ import (
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
emptypb "google.golang.org/protobuf/types/known/emptypb"
wrapperspb "google.golang.org/protobuf/types/known/wrapperspb"
)
// This is a compile-time assertion to ensure that this generated file
@ -23,7 +24,7 @@ const _ = grpc.SupportPackageIsVersion7
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type FocusClient interface {
Raise(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
Raise(ctx context.Context, in *wrapperspb.StringValue, opts ...grpc.CallOption) (*emptypb.Empty, error)
Version(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*VersionResponse, error)
}
@ -35,7 +36,7 @@ func NewFocusClient(cc grpc.ClientConnInterface) FocusClient {
return &focusClient{cc}
}
func (c *focusClient) Raise(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) {
func (c *focusClient) Raise(ctx context.Context, in *wrapperspb.StringValue, opts ...grpc.CallOption) (*emptypb.Empty, error) {
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, "/focus.Focus/Raise", in, out, opts...)
if err != nil {
@ -57,7 +58,7 @@ func (c *focusClient) Version(ctx context.Context, in *emptypb.Empty, opts ...gr
// All implementations must embed UnimplementedFocusServer
// for forward compatibility
type FocusServer interface {
Raise(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
Raise(context.Context, *wrapperspb.StringValue) (*emptypb.Empty, error)
Version(context.Context, *emptypb.Empty) (*VersionResponse, error)
mustEmbedUnimplementedFocusServer()
}
@ -66,7 +67,7 @@ type FocusServer interface {
type UnimplementedFocusServer struct {
}
func (UnimplementedFocusServer) Raise(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
func (UnimplementedFocusServer) Raise(context.Context, *wrapperspb.StringValue) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method Raise not implemented")
}
func (UnimplementedFocusServer) Version(context.Context, *emptypb.Empty) (*VersionResponse, error) {
@ -86,7 +87,7 @@ func RegisterFocusServer(s grpc.ServiceRegistrar, srv FocusServer) {
}
func _Focus_Raise_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(emptypb.Empty)
in := new(wrapperspb.StringValue)
if err := dec(in); err != nil {
return nil, err
}
@ -98,7 +99,7 @@ func _Focus_Raise_Handler(srv interface{}, ctx context.Context, dec func(interfa
FullMethod: "/focus.Focus/Raise",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(FocusServer).Raise(ctx, req.(*emptypb.Empty))
return srv.(FocusServer).Raise(ctx, req.(*wrapperspb.StringValue))
}
return interceptor(ctx, in, info, handler)
}

View File

@ -30,6 +30,7 @@ import (
"github.com/sirupsen/logrus"
"google.golang.org/grpc"
"google.golang.org/protobuf/types/known/emptypb"
"google.golang.org/protobuf/types/known/wrapperspb"
)
const (
@ -45,6 +46,7 @@ type Service struct {
raiseCh chan struct{}
version *semver.Version
log *logrus.Entry
panicHandler async.PanicHandler
}
@ -55,13 +57,14 @@ func NewService(locator service.Locator, version *semver.Version, panicHandler a
server: grpc.NewServer(),
raiseCh: make(chan struct{}, 1),
version: version,
log: logrus.WithField("pkg", "focus/service"),
panicHandler: panicHandler,
}
proto.RegisterFocusServer(serv.server, serv)
if listener, err := net.Listen("tcp", net.JoinHostPort(Host, fmt.Sprint(0))); err != nil {
logrus.WithError(err).Warn("Failed to start focus serv")
serv.log.WithError(err).Warn("Failed to start focus service")
} else {
config := service.Config{}
// retrieve the port assigned by the system, so that we can put it in the config file.
@ -71,9 +74,9 @@ func NewService(locator service.Locator, version *semver.Version, panicHandler a
}
config.Port = address.Port
if path, err := service.SaveGRPCServerConfigFile(locator, &config, serverConfigFileName); err != nil {
logrus.WithError(err).WithField("path", path).Warn("Could not write focus gRPC service config file")
serv.log.WithError(err).WithField("path", path).Warn("Could not write focus gRPC service config file")
} else {
logrus.WithField("path", path).Info("Successfully saved gRPC Focus service config file")
serv.log.WithField("path", path).Info("Successfully saved gRPC Focus service config file")
}
go func() {
@ -89,13 +92,15 @@ func NewService(locator service.Locator, version *semver.Version, panicHandler a
}
// Raise implements the gRPC FocusService interface; it raises the application.
func (service *Service) Raise(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
func (service *Service) Raise(_ context.Context, reason *wrapperspb.StringValue) (*emptypb.Empty, error) {
service.log.WithField("Reason", reason.Value).Debug("Raise")
service.raiseCh <- struct{}{}
return &emptypb.Empty{}, nil
}
// Version implements the gRPC FocusService interface; it returns the version of the service.
func (service *Service) Version(context.Context, *emptypb.Empty) (*proto.VersionResponse, error) {
service.log.Debug("Version")
return &proto.VersionResponse{
Version: service.version.Original(),
}, nil

View File

@ -0,0 +1,4 @@
# The following fix an issue happening using LLDB with OpenSSL 3.1 on ARM64 architecture. (GODT-2680)
# WARNING: this file is ignored if you do not enable reading lldb config from cwd in ~/.lldbinit (`settings set target.load-cwd-lldbinit true`)
settings set platform.plugin.darwin.ignored-exceptions EXC_BAD_INSTRUCTION
process handle SIGILL -n false -p true -s false

View File

@ -0,0 +1,4 @@
# The following fix an issue happening using LLDB with OpenSSL 3.1 on ARM64 architecture. (GODT-2680)
# WARNING: this file is ignored if you do not enable reading lldb config from cwd in ~/.lldbinit (`settings set target.load-cwd-lldbinit true`)
settings set platform.plugin.darwin.ignored-exceptions EXC_BAD_INSTRUCTION
process handle SIGILL -n false -p true -s false

View File

@ -117,7 +117,27 @@ void AppController::restart(bool isCrashing) {
}
//****************************************************************************************************************************************************
/// \param[in] launcher The launcher.
/// \param[in] args The launcher arguments.
//****************************************************************************************************************************************************
void AppController::setLauncherArgs(const QString &launcher, const QStringList &args) {
launcher_ = launcher;
launcherArgs_ = args;
}
//****************************************************************************************************************************************************
/// \param[in] sessionID The sessionID.
//****************************************************************************************************************************************************
void AppController::setSessionID(const QString &sessionID) {
sessionID_ = sessionID;
}
//****************************************************************************************************************************************************
/// \return The sessionID.
//****************************************************************************************************************************************************
QString AppController::sessionID() {
return sessionID_;
}

View File

@ -37,7 +37,7 @@ class Exception;
/// \brief App controller class.
//****************************************************************************************************************************************************
class AppController : public QObject {
Q_OBJECT
Q_OBJECT
friend AppController &app();
public: // member functions.
@ -52,10 +52,12 @@ public: // member functions.
std::unique_ptr<bridgepp::Overseer> &bridgeOverseer() { return bridgeOverseer_; }; ///< Returns a reference the bridge overseer
bridgepp::ProcessMonitor *bridgeMonitor() const; ///< Return the bridge worker.
Settings &settings();; ///< Return the application settings.
void setLauncherArgs(const QString &launcher, const QStringList &args);
void setLauncherArgs(const QString &launcher, const QStringList &args); ///< Set the launcher arguments.
void setSessionID(QString const &sessionID); ///< Set the sessionID.
QString sessionID(); ///< Get the sessionID.
public slots:
void onFatalError(bridgepp::Exception const& e); ///< Handle fatal errors.
void onFatalError(bridgepp::Exception const &e); ///< Handle fatal errors.
private: // member functions
AppController(); ///< Default constructor.
@ -67,8 +69,9 @@ private: // data members
std::unique_ptr<bridgepp::Log> log_; ///< The log.
std::unique_ptr<bridgepp::Overseer> bridgeOverseer_; ///< The overseer for the bridge monitor worker.
std::unique_ptr<Settings> settings_; ///< The application settings.
QString launcher_;
QStringList launcherArgs_;
QString launcher_; ///< The launcher.
QStringList launcherArgs_; ///< The launcher arguments.
QString sessionID_; ///< The sessionID.
};

View File

@ -19,6 +19,7 @@
#include "Pch.h"
#include "CommandLine.h"
#include "Settings.h"
#include <bridgepp/SessionID/SessionID.h>
using namespace bridgepp;
@ -142,5 +143,14 @@ CommandLineOptions parseCommandLine(int argc, char *argv[]) {
options.logLevel = parseLogLevel(argc, argv);
QString sessionID = parseGoCLIStringArgument(argc, argv, { "session-id" });
if (sessionID.isEmpty()) {
// The session ID was not passed to us on the command-line -> create one and add to the command-line for bridge
sessionID = newSessionID();
options.bridgeArgs.append("--session-id");
options.bridgeArgs.append(sessionID);
}
app().setSessionID(sessionID);
return options;
}

View File

@ -43,12 +43,9 @@ EventStreamReader::EventStreamReader(QObject *parent)
void EventStreamReader::run() {
try {
emit started();
grpc::Status const status = app().grpc().runEventStreamReader();
if (!status.ok()) {
throw Exception(QString::fromStdString(status.error_message()));
}
// Status code for the call below is ignored. The event stream may have interrupted by system shutdown or OS user sign-out, and we do not
// want this to generate a sentry report.
app().grpc().runEventStreamReader();
emit finished();
}
catch (Exception const &e) {

View File

@ -19,7 +19,6 @@
#include "LogUtils.h"
#include "BuildConfig.h"
#include <bridgepp/Log/LogUtils.h>
#include <bridgepp/BridgeUtils.h>
using namespace bridgepp;
@ -33,15 +32,10 @@ Log &initLog() {
log.registerAsQtMessageHandler();
log.setEchoInConsole(true);
// remove old gui log files
QDir const logsDir(userLogsDir());
for (QFileInfo const fileInfo: logsDir.entryInfoList({ "gui_v*.log" }, QDir::Filter::Files)) { // entryInfolist apparently only support wildcards, not regex.
QFile(fileInfo.absoluteFilePath()).remove();
}
// create new GUI log file
QString error;
if (!log.startWritingToFile(logsDir.absoluteFilePath(QString("gui_v%1_%2.log").arg(PROJECT_VER).arg(QDateTime::currentSecsSinceEpoch())), &error)) {
if (!log.startWritingToFile(QDir(userLogsDir()).absoluteFilePath(QString("%1_gui_000_v%2_%3.log").arg(app().sessionID(),
PROJECT_VER, PROJECT_TAG)), &error)) {
log.error(error);
}

View File

@ -33,7 +33,7 @@ using namespace bridgepp;
/// \brief handle notification of attempt to re-open the application.
//****************************************************************************************************************************************************
void applicationShouldHandleReopen(id, SEL) {
app().backend().showMainWindow();
app().backend().showMainWindow("macOS applicationShouldHandleReopen notification");
}

View File

@ -61,7 +61,7 @@ void QMLBackend::init(GRPCConfig const &serviceConfig) {
app().grpc().setLog(&log);
this->connectGrpcEvents();
app().grpc().connectToServer(bridgepp::userConfigDir(), serviceConfig, app().bridgeMonitor());
app().grpc().connectToServer(app().sessionID(), bridgepp::userConfigDir(), serviceConfig, app().bridgeMonitor());
app().log().info("Connected to backend via gRPC service.");
QString bridgeVer;
@ -109,6 +109,52 @@ UserList const &QMLBackend::users() const {
return *users_;
}
//****************************************************************************************************************************************************
/// \return the if bridge considers internet is on.
//****************************************************************************************************************************************************
bool QMLBackend::isInternetOn() const {
return isInternetOn_;
}
//****************************************************************************************************************************************************
/// \param[in] reason The reason for the request.
//****************************************************************************************************************************************************
void QMLBackend::showMainWindow(QString const&reason) {
app().log().debug(QString("main window show requested: %1").arg(reason));
emit showMainWindow();
}
//****************************************************************************************************************************************************
/// \param[in] reason The reason for the request.
//****************************************************************************************************************************************************
void QMLBackend::showHelp(QString const&reason) {
app().log().debug(QString("main window show requested (help page): %1").arg(reason));
emit showHelp();
}
//****************************************************************************************************************************************************
/// \param[in] reason The reason for the request.
//****************************************************************************************************************************************************
void QMLBackend::showSettings(QString const&reason) {
app().log().debug(QString("main window show requested (settings page): %1").arg(reason));
emit showSettings();
}
//****************************************************************************************************************************************************
/// \param[in] userID The userID.
/// \param[in] forceShowWindow Should the window be force to display.
/// \param[in] reason The reason for the request.
//****************************************************************************************************************************************************
void QMLBackend::selectUser(QString const &userID, bool forceShowWindow, QString const &reason) {
if (forceShowWindow) {
app().log().debug(QString("main window show requested (user page): %1").arg(reason));
}
emit selectUser(userID, forceShowWindow);
}
//****************************************************************************************************************************************************
/// \return The build year as a string (e.g. 2023)
@ -640,7 +686,7 @@ void QMLBackend::login(QString const &username, QString const &password) const {
HANDLE_EXCEPTION(
if (username.compare("coco@bandicoot", Qt::CaseInsensitive) == 0) {
throw Exception("User requested bridge-gui to crash by trying to log as coco@bandicoot",
"This error exists for test purposes and should be ignored.", __func__, tailOfLatestBridgeLog());
"This error exists for test purposes and should be ignored.", __func__, tailOfLatestBridgeLog(app().sessionID()));
}
app().grpc().login(username, password);
)
@ -874,11 +920,36 @@ void QMLBackend::sendBadEventUserFeedback(QString const &userID, bool doResync)
if (!badEventDisplayQueue_.isEmpty()) {
// we introduce a small delay here, so that the user notices the dialog disappear and pops up again.
QTimer::singleShot(500, [&]() { this->displayBadEventDialog(badEventDisplayQueue_.front()); });
}
)
}
//****************************************************************************************************************************************************
///
//****************************************************************************************************************************************************
void QMLBackend::notifyReportBugClicked() const {
HANDLE_EXCEPTION(
app().grpc().reportBugClicked();
)
}
//****************************************************************************************************************************************************
/// \param[in] client The selected Mail client for autoconfig.
//****************************************************************************************************************************************************
void QMLBackend::notifyAutoconfigClicked(QString const &client) const {
HANDLE_EXCEPTION(
app().grpc().autoconfigClicked(client);
)
}
//****************************************************************************************************************************************************
/// \param[in] article The url of the KB article.
//****************************************************************************************************************************************************
void QMLBackend::notifyKBArticleClicked(QString const &article) const {
HANDLE_EXCEPTION(
app().grpc().KBArticleClicked(article);
)
}
//****************************************************************************************************************************************************
//
@ -923,6 +994,25 @@ void QMLBackend::setUpdateTrayIcon(QString const &stateString, QString const &st
}
//****************************************************************************************************************************************************
/// \param[in] isOn Does bridge consider internet as on.
//****************************************************************************************************************************************************
void QMLBackend::internetStatusChanged(bool isOn) {
HANDLE_EXCEPTION(
if (isInternetOn_ == isOn) {
return;
}
isInternetOn_ = isOn;
if (isOn) {
emit internetOn();
} else {
emit internetOff();
}
)
}
//****************************************************************************************************************************************************
/// \param[in] imapPort The IMAP port.
/// \param[in] smtpPort The SMTP port.
@ -1086,13 +1176,13 @@ void QMLBackend::connectGrpcEvents() {
GRPCClient *client = &app().grpc();
// app events
connect(client, &GRPCClient::internetStatus, this, [&](bool isOn) { if (isOn) { emit internetOn(); } else { emit internetOff(); }});
connect(client, &GRPCClient::internetStatus, this, &QMLBackend::internetStatusChanged);
connect(client, &GRPCClient::toggleAutostartFinished, this, &QMLBackend::toggleAutostartFinished);
connect(client, &GRPCClient::resetFinished, this, &QMLBackend::onResetFinished);
connect(client, &GRPCClient::reportBugFinished, this, &QMLBackend::reportBugFinished);
connect(client, &GRPCClient::reportBugSuccess, this, &QMLBackend::bugReportSendSuccess);
connect(client, &GRPCClient::reportBugError, this, &QMLBackend::bugReportSendError);
connect(client, &GRPCClient::showMainWindow, this, &QMLBackend::showMainWindow);
connect(client, &GRPCClient::showMainWindow, [&]() { this->showMainWindow("gRPC showMainWindow event"); });
// cache events
connect(client, &GRPCClient::diskCacheUnavailable, this, &QMLBackend::diskCacheUnavailable);

View File

@ -45,6 +45,11 @@ public: // member functions.
void init(GRPCConfig const &serviceConfig); ///< Initialize the backend.
bool waitForEventStreamReaderToFinish(qint32 timeoutMs); ///< Wait for the event stream reader to finish.
UserList const& users() const; ///< Return the list of users
bool isInternetOn() const; ///< Check if bridge considers internet as on.
void showMainWindow(QString const &reason); ///< Show the main window.
void showHelp(QString const &reason); ///< Show the help page.
void showSettings(QString const &reason); ///< Show the settings page.
void selectUser(QString const &userID, bool forceShowWindow, QString const &reason); ///< Select the user and display its account details (or login screen).
// invokable methods can be called from QML. They generally return a value, which slots cannot do.
Q_INVOKABLE static QString buildYear(); ///< Return the application build year.
@ -85,7 +90,6 @@ public: // Qt/QML properties. Note that the NOTIFY-er signal is required even fo
Q_PROPERTY(UserList *users MEMBER users_ NOTIFY usersChanged)
Q_PROPERTY(bool dockIconVisible READ dockIconVisible WRITE setDockIconVisible NOTIFY dockIconVisibleChanged)
// Qt Property system setters & getters.
bool showOnStartup() const; ///< Getter for the 'showOnStartup' property.
void setShowSplashScreen(bool show); ///< Setter for the 'showSplashScreen' property.
@ -183,6 +187,9 @@ public slots: // slot for signals received from QML -> To be forwarded to Bridge
void onVersionChanged(); ///< Slot for the version change signal.
void setMailServerSettings(int imapPort, int smtpPort, bool useSSLForIMAP, bool useSSLForSMTP) const; ///< Forwards a connection mode change request from QML to gRPC
void sendBadEventUserFeedback(QString const &userID, bool doResync); ///< Slot the providing user feedback for a bad event.
void notifyReportBugClicked() const; ///< Slot for the ReportBugClicked gRPC event.
void notifyAutoconfigClicked(QString const &client) const; ///< Slot for gAutoconfigClicked gRPC event.
void notifyKBArticleClicked(QString const &article) const; ///< Slot for KBArticleClicked gRPC event.
public slots: // slots for functions that need to be processed locally.
void setNormalTrayIcon(); ///< Set the tray icon to normal.
@ -191,6 +198,7 @@ public slots: // slots for functions that need to be processed locally.
void setUpdateTrayIcon(QString const& stateString, QString const &statusIcon); ///< Set the tray icon to 'update' state.
public slots: // slot for signals received from gRPC that need transformation instead of simple forwarding
void internetStatusChanged(bool isOn); ///< Check if bridge considers internet as on.
void onMailServerSettingsChanged(int imapPort, int smtpPort, bool useSSLForIMAP, bool useSSLForSMTP); ///< Slot for the ConnectionModeChanged gRPC event.
void onGenericError(bridgepp::ErrorInfo const &info); ///< Slot for generic errors received from the gRPC service.
void onLoginFinished(QString const &userID, bool wasSignedOut); ///< Slot for LoginFinished gRPC event.
@ -273,8 +281,9 @@ private: // data members
int smtpPort_ { 0 }; ///< The cached value for the SMTP port.
bool useSSLForIMAP_ { false }; ///< The cached value for useSSLForIMAP.
bool useSSLForSMTP_ { false }; ///< The cached value for useSSLForSMTP.
bool isInternetOn_ { true }; ///< Does bridge consider internet as on?
QList<QString> badEventDisplayQueue_; ///< THe queue for displaying 'bad event feedback request dialog'.
std::unique_ptr<TrayIcon> trayIcon_;
std::unique_ptr<TrayIcon> trayIcon_; ///< The tray icon for the application.
friend class AppController;
};

View File

@ -19,6 +19,7 @@
<file>qml/icons/ic-card-identity.svg</file>
<file>qml/icons/ic-check.svg</file>
<file>qml/icons/ic-chevron-down.svg</file>
<file>qml/icons/ic-chevron-right.svg</file>
<file>qml/icons/ic-chevron-up.svg</file>
<file>qml/icons/ic-cog-wheel.svg</file>
<file>qml/icons/ic-connected.svg</file>

View File

@ -34,7 +34,7 @@ QColor const warnColor(255, 153, 0); ///< The warn state color.
QColor const updateColor(35, 158, 206); ///< The warn state color.
QColor const greyColor(112, 109, 107); ///< The grey color.
qint64 const iconRefreshTimerIntervalMs = 1000; ///< The interval for the refresh timer when switching DPI / screen config, in milliseconds.
qint64 const iconRefreshDurationSecs = 10; ///< The total number of seconds during wich we periodically refresh the icon after a DPI change.
qint64 const iconRefreshDurationSecs = 10; ///< The total number of seconds during which we periodically refresh the icon after a DPI change.
//****************************************************************************************************************************************************
@ -184,10 +184,14 @@ TrayIcon::TrayIcon()
this->setContextMenu(menu_.get());
connect(menu_.get(), &QMenu::aboutToShow, this, &TrayIcon::onMenuAboutToShow);
connect(this, &TrayIcon::selectUser, &app().backend(), &QMLBackend::selectUser);
connect(this, &TrayIcon::selectUser, &app().backend(), [](QString const& userID, bool forceShowWindow) {
app().backend().selectUser(userID, forceShowWindow);
});
connect(this, &TrayIcon::activated, this, &TrayIcon::onActivated);
// some OSes/Desktop managers will automatically show main window when clicked, but not all, so we do it manually.
connect(this, &TrayIcon::messageClicked, &app().backend(), &QMLBackend::showMainWindow);
if (!onLinux()) { // we disable this on linux because of a Qt bug that causes the signal to be emitted for other apps (GODT-2750)
connect(this, &TrayIcon::messageClicked, []() { app().backend().showMainWindow("tray icon popup notification clicked"); });
}
this->show();
// TrayIcon does not expose its screen, so we connect relevant screen events to our DPI change handler.
@ -226,7 +230,7 @@ void TrayIcon::onUserClicked() {
throw Exception("Could not retrieve context menu's selected user.");
}
emit selectUser(userID, true);
app().backend().selectUser(userID, true, "tray menu user clicked");
} catch (Exception const &e) {
app().log().error(e.qwhat());
}
@ -238,7 +242,7 @@ void TrayIcon::onUserClicked() {
//****************************************************************************************************************************************************
void TrayIcon::onActivated(QSystemTrayIcon::ActivationReason reason) {
if ((QSystemTrayIcon::Trigger == reason) && !onMacOS()) {
app().backend().showMainWindow();
app().backend().showMainWindow("tray icon activated");
}
}
@ -344,8 +348,9 @@ void TrayIcon::refreshContextMenu() {
return;
}
bool const internetOn = app().backend().isInternetOn();
menu_->clear();
menu_->addAction(statusIcon_, stateString_, &app().backend(), &QMLBackend::showMainWindow);
menu_->addAction(statusIcon_, stateString_, []() {app().backend().showMainWindow("tray menu status clicked");});
menu_->addSeparator();
QKeySequence noShortcut;
UserList const &users = app().backend().users();
@ -355,7 +360,9 @@ void TrayIcon::refreshContextMenu() {
User const &user = *users.get(i);
UserState const state = user.state();
auto action = new QAction(user.primaryEmailOrUsername());
action->setIcon((UserState::Connected == state) ? greenDot_ : (UserState::Locked == state ? orangeDot_ : greyDot_));
if (internetOn) {
action->setIcon((UserState::Connected == state) ? greenDot_ : (UserState::Locked == state ? orangeDot_ : greyDot_));
}
action->setData(user.id());
connect(action, &QAction::triggered, this, &TrayIcon::onUserClicked);
if ((i < 10) && onMac) {
@ -367,11 +374,15 @@ void TrayIcon::refreshContextMenu() {
menu_->addSeparator();
}
menu_->addAction(tr("&Open Bridge"), onMac ? QKeySequence("Ctrl+O") : noShortcut, &app().backend(), &QMLBackend::showMainWindow);
menu_->addAction(tr("&Help"), onMac ? QKeySequence("Ctrl+F1") : noShortcut, &app().backend(), &QMLBackend::showHelp);
menu_->addAction(tr("&Settings"), onMac ? QKeySequence("Ctrl+,") : noShortcut, &app().backend(), &QMLBackend::showSettings);
menu_->addAction(tr("&Open Bridge"), onMac ? QKeySequence("Ctrl+O") : noShortcut, []() {
app().backend().showMainWindow("tray menu 'open bridge' clicked");
});
menu_->addAction(tr("&Help"), onMac ? QKeySequence("Ctrl+F1") : noShortcut, []() {
app().backend().showHelp("tray menu 'Help' clicked");
});
menu_->addAction(tr("&Settings"), onMac ? QKeySequence("Ctrl+,") : noShortcut, []() {
app().backend().showSettings("tray menu 'Settings' clicked");
});
menu_->addSeparator();
menu_->addAction(tr("&Quit Bridge"), onMac ? QKeySequence("Ctrl+Q") : noShortcut, &app().backend(), &QMLBackend::quit);
}

View File

@ -196,7 +196,7 @@ bool isBridgeRunning() {
//****************************************************************************************************************************************************
void focusOtherInstance() {
try {
FocusGRPCClient client;
FocusGRPCClient client(app().log());
GRPCConfig sc;
QString const path = FocusGRPCClient::grpcFocusServerConfigPath(bridgepp::userConfigDir());
QFile file(path);
@ -209,12 +209,11 @@ void focusOtherInstance() {
throw Exception("Server did not provide gRPC Focus service configuration.");
}
QString error;
if (!client.connectToServer(5000, sc.port, &error)) {
throw Exception("Could not connect to bridge focus service for a raise call.", error);
}
if (!client.raise().ok()) {
if (!client.raise("focusOtherInstance").ok()) {
throw Exception(QString("The raise call to the bridge focus service failed."));
}
}
@ -286,7 +285,9 @@ int main(int argc, char *argv[]) {
initQtApplication();
CommandLineOptions const cliOptions = parseCommandLine(argc, argv);
Log &log = initLog();
log.setLevel(cliOptions.logLevel);
QLockFile lock(bridgepp::userCacheDir() + "/" + bridgeGUILock);
if (!checkSingleInstance(lock)) {
@ -294,8 +295,6 @@ int main(int argc, char *argv[]) {
return EXIT_FAILURE;
}
CommandLineOptions const cliOptions = parseCommandLine(argc, argv);
#ifdef Q_OS_MACOS
registerSecondInstanceHandler();
setDockIconVisibleState(!cliOptions.noWindow);
@ -304,25 +303,25 @@ int main(int argc, char *argv[]) {
// In attached mode, we do not intercept stderr and stdout of bridge, as we did not launch it ourselves, so we output the log to the console.
// When not in attached mode, log entries are forwarded to bridge, which output it on stdout/stderr. bridge-gui's process monitor intercept
// these outputs and output them on the command-line.
log.setLevel(cliOptions.logLevel);
log.info(QString("New Sentry reporter - id: %1.").arg(getProtectedHostname()));
QString bridgeexec;
QString const &sessionID = app().sessionID();
QString bridgeExe;
if (!cliOptions.attach) {
if (isBridgeRunning()) {
throw Exception("An orphan instance of bridge is already running. Please terminate it and relaunch the application.",
QString(), __FUNCTION__, tailOfLatestBridgeLog());
QString(), __FUNCTION__, tailOfLatestBridgeLog(sessionID));
}
// before launching bridge, we remove any trailing service config file, because we need to make sure we get a newly generated one.
FocusGRPCClient::removeServiceConfigFile(configDir);
GRPCClient::removeServiceConfigFile(configDir);
bridgeexec = launchBridge(cliOptions.bridgeArgs);
bridgeExe = launchBridge(cliOptions.bridgeArgs);
}
log.info(QString("Retrieving gRPC service configuration from '%1'").arg(QDir::toNativeSeparators(grpcServerConfigPath(configDir))));
app().backend().init(GRPCClient::waitAndRetrieveServiceConfig(configDir, cliOptions.attach ? 0 : grpcServiceConfigWaitDelayMs,
app().bridgeMonitor()));
app().backend().init(GRPCClient::waitAndRetrieveServiceConfig(sessionID, configDir,
cliOptions.attach ? 0 : grpcServiceConfigWaitDelayMs, app().bridgeMonitor()));
if (!cliOptions.attach) {
GRPCClient::removeServiceConfigFile(configDir);
}
@ -380,9 +379,9 @@ int main(int argc, char *argv[]) {
QStringList args = cliOptions.bridgeGuiArgs;
args.append(waitFlag);
args.append(mainexec);
if (!bridgeexec.isEmpty()) {
if (!bridgeExe.isEmpty()) {
args.append(waitFlag);
args.append(bridgeexec);
args.append(bridgeExe);
}
app().setLauncherArgs(cliOptions.launcher, args);
result = QGuiApplication::exec();

View File

@ -90,9 +90,9 @@ SettingsView {
RowLayout {
ColorImage {
Layout.alignment: Qt.AlignTop
Layout.alignment: Qt.AlignCenter
source: root._isAdvancedShown ? "/qml/icons/ic-chevron-up.svg" : "/qml/icons/ic-chevron-down.svg"
source: root._isAdvancedShown ? "/qml/icons/ic-chevron-down.svg" : "/qml/icons/ic-chevron-right.svg"
color: root.colorScheme.interaction_norm
height: root.colorScheme.body_font_size
sourceSize.height: root.colorScheme.body_font_size

View File

@ -41,7 +41,9 @@ SettingsView {
actionIcon: "/qml/icons/ic-external-link.svg"
description: qsTr("Get help setting up your client with our instructions and FAQs.")
type: SettingsItem.PrimaryButton
onClicked: {Qt.openUrlExternally("https://proton.me/support/bridge")}
onClicked: {
Backend.notifyKBArticleClicked("https://proton.me/support/bridge");
Qt.openUrlExternally("https://proton.me/support/bridge")}
Layout.fillWidth: true
}
@ -87,6 +89,7 @@ SettingsView {
type: SettingsItem.Button
onClicked: {
Backend.updateCurrentMailClient()
Backend.notifyReportBugClicked()
root.parent.showBugReport()
}

View File

@ -891,7 +891,7 @@ QtObject {
action: [
Action {
id: allMail_change
text: root.changeAllMailVisibility.isVisibleNow ?
text: root.changeAllMailVisibility.isVisibleNow ?
qsTr("Hide All Mail folder") :
qsTr("Show All Mail folder")
onTriggered: {
@ -1005,6 +1005,7 @@ QtObject {
text: qsTr("Open the support page")
onTriggered: {
Backend.notifyKBArticleClicked(root.rebuildKeychain.supportLink);
Qt.openUrlExternally(root.rebuildKeychain.supportLink)
Backend.quit()
}

View File

@ -288,7 +288,8 @@ Item {
if (user) {
switch (clientID) {
case 0:
root.user.configureAppleMail(root.address)
root.user.configureAppleMail(root.address)
Backend.notifyAutoconfigClicked("AppleMail");
break;
}
}
@ -298,6 +299,7 @@ Item {
var clientObj = clients.get(clientID)
if (clientObj != undefined && clientObj.link != "" ) {
Qt.openUrlExternally(clientObj.link)
Backend.notifyKBArticleClicked(clientObj.link);
} else {
console.log("unexpected client index", actionID, clientID)
}

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(6.12323e-17,-1,1,6.12323e-17,-0.800005,16.8)">
<g id="ic-chevron-down">
<path id="icon" d="M2.3,6.3L8,12L13.7,6.3L13,5.6L8,10.58L3,5.6L2.3,6.3Z" style="fill:rgb(23,24,28);"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 692 B

View File

@ -32,9 +32,9 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
if (NOT DEFINED BRIDGE_APP_VERSION)
message(FATAL_ERROR "BRIDGE_APP_VERSION is not defined.")
else()
else ()
message(STATUS "Bridge version is ${BRIDGE_APP_VERSION}")
endif()
endif ()
#****************************************************************************************************************************************************
@ -148,6 +148,7 @@ add_library(bridgepp
bridgepp/Log/Log.h bridgepp/Log/Log.cpp
bridgepp/Log/LogUtils.h bridgepp/Log/LogUtils.cpp
bridgepp/ProcessMonitor.cpp bridgepp/ProcessMonitor.h
bridgepp/SessionID/SessionID.cpp bridgepp/SessionID/SessionID.h
bridgepp/User/User.cpp bridgepp/User/User.h
bridgepp/Worker/Worker.h bridgepp/Worker/Overseer.h bridgepp/Worker/Overseer.cpp)
@ -167,7 +168,7 @@ target_precompile_headers(bridgepp PRIVATE Pch.h)
if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.24.0")
cmake_policy(SET CMP0135 NEW) # avoid warning DOWNLOAD_EXTRACT_TIMESTAMP
endif()
endif ()
include(FetchContent)
FetchContent_Declare(
@ -188,7 +189,9 @@ enable_testing()
add_executable(bridgepp-test EXCLUDE_FROM_ALL
Test/TestBridgeUtils.cpp
Test/TestException.cpp
Test/TestWorker.cpp Test/TestWorker.h)
Test/TestSessionID.cpp
Test/TestWorker.cpp Test/TestWorker.h
)
add_dependencies(bridgepp-test bridgepp)
target_precompile_headers(bridgepp-test PRIVATE Pch.h)
target_link_libraries(bridgepp-test

View File

@ -0,0 +1,36 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
#include "QtCore/qdatetime.h"
#include <gtest/gtest.h>
#include <bridgepp/SessionID/SessionID.h>
using namespace bridgepp;
TEST(SessionID, SessionID) {
QString const sessionID = newSessionID();
EXPECT_TRUE(sessionID.size() > 0);
EXPECT_FALSE(sessionIDToDateTime("invalidSessionID").isValid());
QDateTime const dateTime = sessionIDToDateTime(sessionID);
EXPECT_TRUE(dateTime.isValid());
EXPECT_TRUE(qAbs(dateTime.secsTo(QDateTime::currentDateTime())) < 5);
}

View File

@ -46,6 +46,15 @@ QString grpcFocusServerConfigFilename() {
}
//****************************************************************************************************************************************************
/// \param[in] log The log.
//****************************************************************************************************************************************************
FocusGRPCClient::FocusGRPCClient(Log& log)
:log_(log) {
}
//****************************************************************************************************************************************************
/// \return The absolute path of the focus service config path.
//****************************************************************************************************************************************************
@ -91,6 +100,7 @@ bool FocusGRPCClient::connectToServer(qint64 timeoutMs, quint16 port, QString *o
throw Exception("Connexion check with focus service failed.");
}
log_.debug(QString("Successfully connected to focus gRPC service."));
return true;
}
catch (Exception const &e) {
@ -103,11 +113,15 @@ bool FocusGRPCClient::connectToServer(qint64 timeoutMs, quint16 port, QString *o
//****************************************************************************************************************************************************
/// \param[in] reason The reason behind the raise call.
/// \return The status for the call.
//****************************************************************************************************************************************************
grpc::Status FocusGRPCClient::raise() {
grpc::Status FocusGRPCClient::raise(QString const &reason) {
log_.debug("FocusGRPCService::raise()");
ClientContext ctx;
return stub_->Raise(&ctx, empty, &empty);
StringValue s;
s.set_value(reason.toStdString());
return stub_->Raise(&ctx, s, &empty);
}
@ -116,6 +130,7 @@ grpc::Status FocusGRPCClient::raise() {
/// \return The status for the call.
//****************************************************************************************************************************************************
grpc::Status FocusGRPCClient::version(QString &outVersion) {
log_.debug("FocusGRPCService::version()");
ClientContext ctx;
VersionResponse response;
Status status = stub_->Version(&ctx, empty, &response);

View File

@ -22,6 +22,7 @@
#include "grpc++/grpc++.h"
#include "focus.grpc.pb.h"
#include "../Log/Log.h"
namespace bridgepp {
@ -36,7 +37,7 @@ public: // static member functions
static QString grpcFocusServerConfigPath(QString const &configDir); ///< Return the path of the gRPC Focus server config file.
public: // member functions.
FocusGRPCClient() = default; ///< Default constructor.
FocusGRPCClient(Log& log); ///< Default constructor.
FocusGRPCClient(FocusGRPCClient const &) = delete; ///< Disabled copy-constructor.
FocusGRPCClient(FocusGRPCClient &&) = delete; ///< Disabled assignment copy-constructor.
~FocusGRPCClient() = default; ///< Destructor.
@ -44,10 +45,11 @@ public: // member functions.
FocusGRPCClient &operator=(FocusGRPCClient &&) = delete; ///< Disabled move assignment operator.
bool connectToServer(qint64 timeoutMs, quint16 port, QString *outError = nullptr); ///< Connect to the focus server
grpc::Status raise(); ///< Performs the 'raise' call.
grpc::Status raise(QString const &reason); ///< Performs the 'raise' call.
grpc::Status version(QString &outVersion); ///< Performs the 'version' call.
private:
Log &log_; ///< The log to use for logging calls
std::shared_ptr<grpc::Channel> channel_ { nullptr }; ///< The gRPC channel.
std::shared_ptr<focus::Focus::Stub> stub_ { nullptr }; ///< The gRPC stub (a.k.a. client).
};

View File

@ -1,128 +0,0 @@
// Generated by the gRPC C++ plugin.
// If you make any local change, they will be lost.
// source: focus.proto
#include "focus.pb.h"
#include "focus.grpc.pb.h"
#include <functional>
#include <grpcpp/support/async_stream.h>
#include <grpcpp/support/async_unary_call.h>
#include <grpcpp/impl/channel_interface.h>
#include <grpcpp/impl/client_unary_call.h>
#include <grpcpp/support/client_callback.h>
#include <grpcpp/support/message_allocator.h>
#include <grpcpp/support/method_handler.h>
#include <grpcpp/impl/rpc_service_method.h>
#include <grpcpp/support/server_callback.h>
#include <grpcpp/impl/codegen/server_callback_handlers.h>
#include <grpcpp/server_context.h>
#include <grpcpp/impl/service_type.h>
#include <grpcpp/support/sync_stream.h>
namespace focus {
static const char* Focus_method_names[] = {
"/focus.Focus/Raise",
"/focus.Focus/Version",
};
std::unique_ptr< Focus::Stub> Focus::NewStub(const std::shared_ptr< ::grpc::ChannelInterface>& channel, const ::grpc::StubOptions& options) {
(void)options;
std::unique_ptr< Focus::Stub> stub(new Focus::Stub(channel, options));
return stub;
}
Focus::Stub::Stub(const std::shared_ptr< ::grpc::ChannelInterface>& channel, const ::grpc::StubOptions& options)
: channel_(channel), rpcmethod_Raise_(Focus_method_names[0], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel)
, rpcmethod_Version_(Focus_method_names[1], options.suffix_for_stats(),::grpc::internal::RpcMethod::NORMAL_RPC, channel)
{}
::grpc::Status Focus::Stub::Raise(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::google::protobuf::Empty* response) {
return ::grpc::internal::BlockingUnaryCall< ::google::protobuf::Empty, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(channel_.get(), rpcmethod_Raise_, context, request, response);
}
void Focus::Stub::async::Raise(::grpc::ClientContext* context, const ::google::protobuf::Empty* request, ::google::protobuf::Empty* response, std::function<void(::grpc::Status)> f) {
::grpc::internal::CallbackUnaryCall< ::google::protobuf::Empty, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(stub_->channel_.get(), stub_->rpcmethod_Raise_, context, request, response, std::move(f));
}
void Focus::Stub::async::Raise(::grpc::ClientContext* context, const ::google::protobuf::Empty* request, ::google::protobuf::Empty* response, ::grpc::ClientUnaryReactor* reactor) {
::grpc::internal::ClientCallbackUnaryFactory::Create< ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(stub_->channel_.get(), stub_->rpcmethod_Raise_, context, request, response, reactor);
}
::grpc::ClientAsyncResponseReader< ::google::protobuf::Empty>* Focus::Stub::PrepareAsyncRaiseRaw(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::grpc::CompletionQueue* cq) {
return ::grpc::internal::ClientAsyncResponseReaderHelper::Create< ::google::protobuf::Empty, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(channel_.get(), cq, rpcmethod_Raise_, context, request);
}
::grpc::ClientAsyncResponseReader< ::google::protobuf::Empty>* Focus::Stub::AsyncRaiseRaw(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::grpc::CompletionQueue* cq) {
auto* result =
this->PrepareAsyncRaiseRaw(context, request, cq);
result->StartCall();
return result;
}
::grpc::Status Focus::Stub::Version(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::focus::VersionResponse* response) {
return ::grpc::internal::BlockingUnaryCall< ::google::protobuf::Empty, ::focus::VersionResponse, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(channel_.get(), rpcmethod_Version_, context, request, response);
}
void Focus::Stub::async::Version(::grpc::ClientContext* context, const ::google::protobuf::Empty* request, ::focus::VersionResponse* response, std::function<void(::grpc::Status)> f) {
::grpc::internal::CallbackUnaryCall< ::google::protobuf::Empty, ::focus::VersionResponse, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(stub_->channel_.get(), stub_->rpcmethod_Version_, context, request, response, std::move(f));
}
void Focus::Stub::async::Version(::grpc::ClientContext* context, const ::google::protobuf::Empty* request, ::focus::VersionResponse* response, ::grpc::ClientUnaryReactor* reactor) {
::grpc::internal::ClientCallbackUnaryFactory::Create< ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(stub_->channel_.get(), stub_->rpcmethod_Version_, context, request, response, reactor);
}
::grpc::ClientAsyncResponseReader< ::focus::VersionResponse>* Focus::Stub::PrepareAsyncVersionRaw(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::grpc::CompletionQueue* cq) {
return ::grpc::internal::ClientAsyncResponseReaderHelper::Create< ::focus::VersionResponse, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(channel_.get(), cq, rpcmethod_Version_, context, request);
}
::grpc::ClientAsyncResponseReader< ::focus::VersionResponse>* Focus::Stub::AsyncVersionRaw(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::grpc::CompletionQueue* cq) {
auto* result =
this->PrepareAsyncVersionRaw(context, request, cq);
result->StartCall();
return result;
}
Focus::Service::Service() {
AddMethod(new ::grpc::internal::RpcServiceMethod(
Focus_method_names[0],
::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Focus::Service, ::google::protobuf::Empty, ::google::protobuf::Empty, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Focus::Service* service,
::grpc::ServerContext* ctx,
const ::google::protobuf::Empty* req,
::google::protobuf::Empty* resp) {
return service->Raise(ctx, req, resp);
}, this)));
AddMethod(new ::grpc::internal::RpcServiceMethod(
Focus_method_names[1],
::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Focus::Service, ::google::protobuf::Empty, ::focus::VersionResponse, ::grpc::protobuf::MessageLite, ::grpc::protobuf::MessageLite>(
[](Focus::Service* service,
::grpc::ServerContext* ctx,
const ::google::protobuf::Empty* req,
::focus::VersionResponse* resp) {
return service->Version(ctx, req, resp);
}, this)));
}
Focus::Service::~Service() {
}
::grpc::Status Focus::Service::Raise(::grpc::ServerContext* context, const ::google::protobuf::Empty* request, ::google::protobuf::Empty* response) {
(void) context;
(void) request;
(void) response;
return ::grpc::Status(::grpc::StatusCode::UNIMPLEMENTED, "");
}
::grpc::Status Focus::Service::Version(::grpc::ServerContext* context, const ::google::protobuf::Empty* request, ::focus::VersionResponse* response) {
(void) context;
(void) request;
(void) response;
return ::grpc::Status(::grpc::StatusCode::UNIMPLEMENTED, "");
}
} // namespace focus

View File

@ -1,418 +0,0 @@
// Generated by the gRPC C++ plugin.
// If you make any local change, they will be lost.
// source: focus.proto
// Original file comments:
// Copyright (c) 2022 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail 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.
//
// ProtonMail 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 ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
//
#ifndef GRPC_focus_2eproto__INCLUDED
#define GRPC_focus_2eproto__INCLUDED
#include "focus.pb.h"
#include <functional>
#include <grpcpp/generic/async_generic_service.h>
#include <grpcpp/support/async_stream.h>
#include <grpcpp/support/async_unary_call.h>
#include <grpcpp/support/client_callback.h>
#include <grpcpp/client_context.h>
#include <grpcpp/completion_queue.h>
#include <grpcpp/support/message_allocator.h>
#include <grpcpp/support/method_handler.h>
#include <grpcpp/impl/codegen/proto_utils.h>
#include <grpcpp/impl/rpc_method.h>
#include <grpcpp/support/server_callback.h>
#include <grpcpp/impl/codegen/server_callback_handlers.h>
#include <grpcpp/server_context.h>
#include <grpcpp/impl/service_type.h>
#include <grpcpp/impl/codegen/status.h>
#include <grpcpp/support/stub_options.h>
#include <grpcpp/support/sync_stream.h>
namespace focus {
// **********************************************************************************************************************
// Service Declaration
// **********************************************************************************************************************≠––
class Focus final {
public:
static constexpr char const* service_full_name() {
return "focus.Focus";
}
class StubInterface {
public:
virtual ~StubInterface() {}
virtual ::grpc::Status Raise(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::google::protobuf::Empty* response) = 0;
std::unique_ptr< ::grpc::ClientAsyncResponseReaderInterface< ::google::protobuf::Empty>> AsyncRaise(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::grpc::CompletionQueue* cq) {
return std::unique_ptr< ::grpc::ClientAsyncResponseReaderInterface< ::google::protobuf::Empty>>(AsyncRaiseRaw(context, request, cq));
}
std::unique_ptr< ::grpc::ClientAsyncResponseReaderInterface< ::google::protobuf::Empty>> PrepareAsyncRaise(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::grpc::CompletionQueue* cq) {
return std::unique_ptr< ::grpc::ClientAsyncResponseReaderInterface< ::google::protobuf::Empty>>(PrepareAsyncRaiseRaw(context, request, cq));
}
virtual ::grpc::Status Version(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::focus::VersionResponse* response) = 0;
std::unique_ptr< ::grpc::ClientAsyncResponseReaderInterface< ::focus::VersionResponse>> AsyncVersion(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::grpc::CompletionQueue* cq) {
return std::unique_ptr< ::grpc::ClientAsyncResponseReaderInterface< ::focus::VersionResponse>>(AsyncVersionRaw(context, request, cq));
}
std::unique_ptr< ::grpc::ClientAsyncResponseReaderInterface< ::focus::VersionResponse>> PrepareAsyncVersion(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::grpc::CompletionQueue* cq) {
return std::unique_ptr< ::grpc::ClientAsyncResponseReaderInterface< ::focus::VersionResponse>>(PrepareAsyncVersionRaw(context, request, cq));
}
class async_interface {
public:
virtual ~async_interface() {}
virtual void Raise(::grpc::ClientContext* context, const ::google::protobuf::Empty* request, ::google::protobuf::Empty* response, std::function<void(::grpc::Status)>) = 0;
virtual void Raise(::grpc::ClientContext* context, const ::google::protobuf::Empty* request, ::google::protobuf::Empty* response, ::grpc::ClientUnaryReactor* reactor) = 0;
virtual void Version(::grpc::ClientContext* context, const ::google::protobuf::Empty* request, ::focus::VersionResponse* response, std::function<void(::grpc::Status)>) = 0;
virtual void Version(::grpc::ClientContext* context, const ::google::protobuf::Empty* request, ::focus::VersionResponse* response, ::grpc::ClientUnaryReactor* reactor) = 0;
};
typedef class async_interface experimental_async_interface;
virtual class async_interface* async() { return nullptr; }
class async_interface* experimental_async() { return async(); }
private:
virtual ::grpc::ClientAsyncResponseReaderInterface< ::google::protobuf::Empty>* AsyncRaiseRaw(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::grpc::CompletionQueue* cq) = 0;
virtual ::grpc::ClientAsyncResponseReaderInterface< ::google::protobuf::Empty>* PrepareAsyncRaiseRaw(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::grpc::CompletionQueue* cq) = 0;
virtual ::grpc::ClientAsyncResponseReaderInterface< ::focus::VersionResponse>* AsyncVersionRaw(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::grpc::CompletionQueue* cq) = 0;
virtual ::grpc::ClientAsyncResponseReaderInterface< ::focus::VersionResponse>* PrepareAsyncVersionRaw(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::grpc::CompletionQueue* cq) = 0;
};
class Stub final : public StubInterface {
public:
Stub(const std::shared_ptr< ::grpc::ChannelInterface>& channel, const ::grpc::StubOptions& options = ::grpc::StubOptions());
::grpc::Status Raise(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::google::protobuf::Empty* response) override;
std::unique_ptr< ::grpc::ClientAsyncResponseReader< ::google::protobuf::Empty>> AsyncRaise(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::grpc::CompletionQueue* cq) {
return std::unique_ptr< ::grpc::ClientAsyncResponseReader< ::google::protobuf::Empty>>(AsyncRaiseRaw(context, request, cq));
}
std::unique_ptr< ::grpc::ClientAsyncResponseReader< ::google::protobuf::Empty>> PrepareAsyncRaise(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::grpc::CompletionQueue* cq) {
return std::unique_ptr< ::grpc::ClientAsyncResponseReader< ::google::protobuf::Empty>>(PrepareAsyncRaiseRaw(context, request, cq));
}
::grpc::Status Version(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::focus::VersionResponse* response) override;
std::unique_ptr< ::grpc::ClientAsyncResponseReader< ::focus::VersionResponse>> AsyncVersion(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::grpc::CompletionQueue* cq) {
return std::unique_ptr< ::grpc::ClientAsyncResponseReader< ::focus::VersionResponse>>(AsyncVersionRaw(context, request, cq));
}
std::unique_ptr< ::grpc::ClientAsyncResponseReader< ::focus::VersionResponse>> PrepareAsyncVersion(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::grpc::CompletionQueue* cq) {
return std::unique_ptr< ::grpc::ClientAsyncResponseReader< ::focus::VersionResponse>>(PrepareAsyncVersionRaw(context, request, cq));
}
class async final :
public StubInterface::async_interface {
public:
void Raise(::grpc::ClientContext* context, const ::google::protobuf::Empty* request, ::google::protobuf::Empty* response, std::function<void(::grpc::Status)>) override;
void Raise(::grpc::ClientContext* context, const ::google::protobuf::Empty* request, ::google::protobuf::Empty* response, ::grpc::ClientUnaryReactor* reactor) override;
void Version(::grpc::ClientContext* context, const ::google::protobuf::Empty* request, ::focus::VersionResponse* response, std::function<void(::grpc::Status)>) override;
void Version(::grpc::ClientContext* context, const ::google::protobuf::Empty* request, ::focus::VersionResponse* response, ::grpc::ClientUnaryReactor* reactor) override;
private:
friend class Stub;
explicit async(Stub* stub): stub_(stub) { }
Stub* stub() { return stub_; }
Stub* stub_;
};
class async* async() override { return &async_stub_; }
private:
std::shared_ptr< ::grpc::ChannelInterface> channel_;
class async async_stub_{this};
::grpc::ClientAsyncResponseReader< ::google::protobuf::Empty>* AsyncRaiseRaw(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::grpc::CompletionQueue* cq) override;
::grpc::ClientAsyncResponseReader< ::google::protobuf::Empty>* PrepareAsyncRaiseRaw(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::grpc::CompletionQueue* cq) override;
::grpc::ClientAsyncResponseReader< ::focus::VersionResponse>* AsyncVersionRaw(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::grpc::CompletionQueue* cq) override;
::grpc::ClientAsyncResponseReader< ::focus::VersionResponse>* PrepareAsyncVersionRaw(::grpc::ClientContext* context, const ::google::protobuf::Empty& request, ::grpc::CompletionQueue* cq) override;
const ::grpc::internal::RpcMethod rpcmethod_Raise_;
const ::grpc::internal::RpcMethod rpcmethod_Version_;
};
static std::unique_ptr<Stub> NewStub(const std::shared_ptr< ::grpc::ChannelInterface>& channel, const ::grpc::StubOptions& options = ::grpc::StubOptions());
class Service : public ::grpc::Service {
public:
Service();
virtual ~Service();
virtual ::grpc::Status Raise(::grpc::ServerContext* context, const ::google::protobuf::Empty* request, ::google::protobuf::Empty* response);
virtual ::grpc::Status Version(::grpc::ServerContext* context, const ::google::protobuf::Empty* request, ::focus::VersionResponse* response);
};
template <class BaseClass>
class WithAsyncMethod_Raise : public BaseClass {
private:
void BaseClassMustBeDerivedFromService(const Service* /*service*/) {}
public:
WithAsyncMethod_Raise() {
::grpc::Service::MarkMethodAsync(0);
}
~WithAsyncMethod_Raise() override {
BaseClassMustBeDerivedFromService(this);
}
// disable synchronous version of this method
::grpc::Status Raise(::grpc::ServerContext* /*context*/, const ::google::protobuf::Empty* /*request*/, ::google::protobuf::Empty* /*response*/) override {
abort();
return ::grpc::Status(::grpc::StatusCode::UNIMPLEMENTED, "");
}
void RequestRaise(::grpc::ServerContext* context, ::google::protobuf::Empty* request, ::grpc::ServerAsyncResponseWriter< ::google::protobuf::Empty>* response, ::grpc::CompletionQueue* new_call_cq, ::grpc::ServerCompletionQueue* notification_cq, void *tag) {
::grpc::Service::RequestAsyncUnary(0, context, request, response, new_call_cq, notification_cq, tag);
}
};
template <class BaseClass>
class WithAsyncMethod_Version : public BaseClass {
private:
void BaseClassMustBeDerivedFromService(const Service* /*service*/) {}
public:
WithAsyncMethod_Version() {
::grpc::Service::MarkMethodAsync(1);
}
~WithAsyncMethod_Version() override {
BaseClassMustBeDerivedFromService(this);
}
// disable synchronous version of this method
::grpc::Status Version(::grpc::ServerContext* /*context*/, const ::google::protobuf::Empty* /*request*/, ::focus::VersionResponse* /*response*/) override {
abort();
return ::grpc::Status(::grpc::StatusCode::UNIMPLEMENTED, "");
}
void RequestVersion(::grpc::ServerContext* context, ::google::protobuf::Empty* request, ::grpc::ServerAsyncResponseWriter< ::focus::VersionResponse>* response, ::grpc::CompletionQueue* new_call_cq, ::grpc::ServerCompletionQueue* notification_cq, void *tag) {
::grpc::Service::RequestAsyncUnary(1, context, request, response, new_call_cq, notification_cq, tag);
}
};
typedef WithAsyncMethod_Raise<WithAsyncMethod_Version<Service > > AsyncService;
template <class BaseClass>
class WithCallbackMethod_Raise : public BaseClass {
private:
void BaseClassMustBeDerivedFromService(const Service* /*service*/) {}
public:
WithCallbackMethod_Raise() {
::grpc::Service::MarkMethodCallback(0,
new ::grpc::internal::CallbackUnaryHandler< ::google::protobuf::Empty, ::google::protobuf::Empty>(
[this](
::grpc::CallbackServerContext* context, const ::google::protobuf::Empty* request, ::google::protobuf::Empty* response) { return this->Raise(context, request, response); }));}
void SetMessageAllocatorFor_Raise(
::grpc::MessageAllocator< ::google::protobuf::Empty, ::google::protobuf::Empty>* allocator) {
::grpc::internal::MethodHandler* const handler = ::grpc::Service::GetHandler(0);
static_cast<::grpc::internal::CallbackUnaryHandler< ::google::protobuf::Empty, ::google::protobuf::Empty>*>(handler)
->SetMessageAllocator(allocator);
}
~WithCallbackMethod_Raise() override {
BaseClassMustBeDerivedFromService(this);
}
// disable synchronous version of this method
::grpc::Status Raise(::grpc::ServerContext* /*context*/, const ::google::protobuf::Empty* /*request*/, ::google::protobuf::Empty* /*response*/) override {
abort();
return ::grpc::Status(::grpc::StatusCode::UNIMPLEMENTED, "");
}
virtual ::grpc::ServerUnaryReactor* Raise(
::grpc::CallbackServerContext* /*context*/, const ::google::protobuf::Empty* /*request*/, ::google::protobuf::Empty* /*response*/) { return nullptr; }
};
template <class BaseClass>
class WithCallbackMethod_Version : public BaseClass {
private:
void BaseClassMustBeDerivedFromService(const Service* /*service*/) {}
public:
WithCallbackMethod_Version() {
::grpc::Service::MarkMethodCallback(1,
new ::grpc::internal::CallbackUnaryHandler< ::google::protobuf::Empty, ::focus::VersionResponse>(
[this](
::grpc::CallbackServerContext* context, const ::google::protobuf::Empty* request, ::focus::VersionResponse* response) { return this->Version(context, request, response); }));}
void SetMessageAllocatorFor_Version(
::grpc::MessageAllocator< ::google::protobuf::Empty, ::focus::VersionResponse>* allocator) {
::grpc::internal::MethodHandler* const handler = ::grpc::Service::GetHandler(1);
static_cast<::grpc::internal::CallbackUnaryHandler< ::google::protobuf::Empty, ::focus::VersionResponse>*>(handler)
->SetMessageAllocator(allocator);
}
~WithCallbackMethod_Version() override {
BaseClassMustBeDerivedFromService(this);
}
// disable synchronous version of this method
::grpc::Status Version(::grpc::ServerContext* /*context*/, const ::google::protobuf::Empty* /*request*/, ::focus::VersionResponse* /*response*/) override {
abort();
return ::grpc::Status(::grpc::StatusCode::UNIMPLEMENTED, "");
}
virtual ::grpc::ServerUnaryReactor* Version(
::grpc::CallbackServerContext* /*context*/, const ::google::protobuf::Empty* /*request*/, ::focus::VersionResponse* /*response*/) { return nullptr; }
};
typedef WithCallbackMethod_Raise<WithCallbackMethod_Version<Service > > CallbackService;
typedef CallbackService ExperimentalCallbackService;
template <class BaseClass>
class WithGenericMethod_Raise : public BaseClass {
private:
void BaseClassMustBeDerivedFromService(const Service* /*service*/) {}
public:
WithGenericMethod_Raise() {
::grpc::Service::MarkMethodGeneric(0);
}
~WithGenericMethod_Raise() override {
BaseClassMustBeDerivedFromService(this);
}
// disable synchronous version of this method
::grpc::Status Raise(::grpc::ServerContext* /*context*/, const ::google::protobuf::Empty* /*request*/, ::google::protobuf::Empty* /*response*/) override {
abort();
return ::grpc::Status(::grpc::StatusCode::UNIMPLEMENTED, "");
}
};
template <class BaseClass>
class WithGenericMethod_Version : public BaseClass {
private:
void BaseClassMustBeDerivedFromService(const Service* /*service*/) {}
public:
WithGenericMethod_Version() {
::grpc::Service::MarkMethodGeneric(1);
}
~WithGenericMethod_Version() override {
BaseClassMustBeDerivedFromService(this);
}
// disable synchronous version of this method
::grpc::Status Version(::grpc::ServerContext* /*context*/, const ::google::protobuf::Empty* /*request*/, ::focus::VersionResponse* /*response*/) override {
abort();
return ::grpc::Status(::grpc::StatusCode::UNIMPLEMENTED, "");
}
};
template <class BaseClass>
class WithRawMethod_Raise : public BaseClass {
private:
void BaseClassMustBeDerivedFromService(const Service* /*service*/) {}
public:
WithRawMethod_Raise() {
::grpc::Service::MarkMethodRaw(0);
}
~WithRawMethod_Raise() override {
BaseClassMustBeDerivedFromService(this);
}
// disable synchronous version of this method
::grpc::Status Raise(::grpc::ServerContext* /*context*/, const ::google::protobuf::Empty* /*request*/, ::google::protobuf::Empty* /*response*/) override {
abort();
return ::grpc::Status(::grpc::StatusCode::UNIMPLEMENTED, "");
}
void RequestRaise(::grpc::ServerContext* context, ::grpc::ByteBuffer* request, ::grpc::ServerAsyncResponseWriter< ::grpc::ByteBuffer>* response, ::grpc::CompletionQueue* new_call_cq, ::grpc::ServerCompletionQueue* notification_cq, void *tag) {
::grpc::Service::RequestAsyncUnary(0, context, request, response, new_call_cq, notification_cq, tag);
}
};
template <class BaseClass>
class WithRawMethod_Version : public BaseClass {
private:
void BaseClassMustBeDerivedFromService(const Service* /*service*/) {}
public:
WithRawMethod_Version() {
::grpc::Service::MarkMethodRaw(1);
}
~WithRawMethod_Version() override {
BaseClassMustBeDerivedFromService(this);
}
// disable synchronous version of this method
::grpc::Status Version(::grpc::ServerContext* /*context*/, const ::google::protobuf::Empty* /*request*/, ::focus::VersionResponse* /*response*/) override {
abort();
return ::grpc::Status(::grpc::StatusCode::UNIMPLEMENTED, "");
}
void RequestVersion(::grpc::ServerContext* context, ::grpc::ByteBuffer* request, ::grpc::ServerAsyncResponseWriter< ::grpc::ByteBuffer>* response, ::grpc::CompletionQueue* new_call_cq, ::grpc::ServerCompletionQueue* notification_cq, void *tag) {
::grpc::Service::RequestAsyncUnary(1, context, request, response, new_call_cq, notification_cq, tag);
}
};
template <class BaseClass>
class WithRawCallbackMethod_Raise : public BaseClass {
private:
void BaseClassMustBeDerivedFromService(const Service* /*service*/) {}
public:
WithRawCallbackMethod_Raise() {
::grpc::Service::MarkMethodRawCallback(0,
new ::grpc::internal::CallbackUnaryHandler< ::grpc::ByteBuffer, ::grpc::ByteBuffer>(
[this](
::grpc::CallbackServerContext* context, const ::grpc::ByteBuffer* request, ::grpc::ByteBuffer* response) { return this->Raise(context, request, response); }));
}
~WithRawCallbackMethod_Raise() override {
BaseClassMustBeDerivedFromService(this);
}
// disable synchronous version of this method
::grpc::Status Raise(::grpc::ServerContext* /*context*/, const ::google::protobuf::Empty* /*request*/, ::google::protobuf::Empty* /*response*/) override {
abort();
return ::grpc::Status(::grpc::StatusCode::UNIMPLEMENTED, "");
}
virtual ::grpc::ServerUnaryReactor* Raise(
::grpc::CallbackServerContext* /*context*/, const ::grpc::ByteBuffer* /*request*/, ::grpc::ByteBuffer* /*response*/) { return nullptr; }
};
template <class BaseClass>
class WithRawCallbackMethod_Version : public BaseClass {
private:
void BaseClassMustBeDerivedFromService(const Service* /*service*/) {}
public:
WithRawCallbackMethod_Version() {
::grpc::Service::MarkMethodRawCallback(1,
new ::grpc::internal::CallbackUnaryHandler< ::grpc::ByteBuffer, ::grpc::ByteBuffer>(
[this](
::grpc::CallbackServerContext* context, const ::grpc::ByteBuffer* request, ::grpc::ByteBuffer* response) { return this->Version(context, request, response); }));
}
~WithRawCallbackMethod_Version() override {
BaseClassMustBeDerivedFromService(this);
}
// disable synchronous version of this method
::grpc::Status Version(::grpc::ServerContext* /*context*/, const ::google::protobuf::Empty* /*request*/, ::focus::VersionResponse* /*response*/) override {
abort();
return ::grpc::Status(::grpc::StatusCode::UNIMPLEMENTED, "");
}
virtual ::grpc::ServerUnaryReactor* Version(
::grpc::CallbackServerContext* /*context*/, const ::grpc::ByteBuffer* /*request*/, ::grpc::ByteBuffer* /*response*/) { return nullptr; }
};
template <class BaseClass>
class WithStreamedUnaryMethod_Raise : public BaseClass {
private:
void BaseClassMustBeDerivedFromService(const Service* /*service*/) {}
public:
WithStreamedUnaryMethod_Raise() {
::grpc::Service::MarkMethodStreamed(0,
new ::grpc::internal::StreamedUnaryHandler<
::google::protobuf::Empty, ::google::protobuf::Empty>(
[this](::grpc::ServerContext* context,
::grpc::ServerUnaryStreamer<
::google::protobuf::Empty, ::google::protobuf::Empty>* streamer) {
return this->StreamedRaise(context,
streamer);
}));
}
~WithStreamedUnaryMethod_Raise() override {
BaseClassMustBeDerivedFromService(this);
}
// disable regular version of this method
::grpc::Status Raise(::grpc::ServerContext* /*context*/, const ::google::protobuf::Empty* /*request*/, ::google::protobuf::Empty* /*response*/) override {
abort();
return ::grpc::Status(::grpc::StatusCode::UNIMPLEMENTED, "");
}
// replace default version of method with streamed unary
virtual ::grpc::Status StreamedRaise(::grpc::ServerContext* context, ::grpc::ServerUnaryStreamer< ::google::protobuf::Empty,::google::protobuf::Empty>* server_unary_streamer) = 0;
};
template <class BaseClass>
class WithStreamedUnaryMethod_Version : public BaseClass {
private:
void BaseClassMustBeDerivedFromService(const Service* /*service*/) {}
public:
WithStreamedUnaryMethod_Version() {
::grpc::Service::MarkMethodStreamed(1,
new ::grpc::internal::StreamedUnaryHandler<
::google::protobuf::Empty, ::focus::VersionResponse>(
[this](::grpc::ServerContext* context,
::grpc::ServerUnaryStreamer<
::google::protobuf::Empty, ::focus::VersionResponse>* streamer) {
return this->StreamedVersion(context,
streamer);
}));
}
~WithStreamedUnaryMethod_Version() override {
BaseClassMustBeDerivedFromService(this);
}
// disable regular version of this method
::grpc::Status Version(::grpc::ServerContext* /*context*/, const ::google::protobuf::Empty* /*request*/, ::focus::VersionResponse* /*response*/) override {
abort();
return ::grpc::Status(::grpc::StatusCode::UNIMPLEMENTED, "");
}
// replace default version of method with streamed unary
virtual ::grpc::Status StreamedVersion(::grpc::ServerContext* context, ::grpc::ServerUnaryStreamer< ::google::protobuf::Empty,::focus::VersionResponse>* server_unary_streamer) = 0;
};
typedef WithStreamedUnaryMethod_Raise<WithStreamedUnaryMethod_Version<Service > > StreamedUnaryService;
typedef Service SplitStreamedService;
typedef WithStreamedUnaryMethod_Raise<WithStreamedUnaryMethod_Version<Service > > StreamedService;
};
} // namespace focus
#endif // GRPC_focus_2eproto__INCLUDED

View File

@ -1,302 +0,0 @@
// Generated by the protocol buffer compiler. DO NOT EDIT!
// source: focus.proto
#include "focus.pb.h"
#include <algorithm>
#include <google/protobuf/io/coded_stream.h>
#include <google/protobuf/extension_set.h>
#include <google/protobuf/wire_format_lite.h>
#include <google/protobuf/descriptor.h>
#include <google/protobuf/generated_message_reflection.h>
#include <google/protobuf/reflection_ops.h>
#include <google/protobuf/wire_format.h>
// @@protoc_insertion_point(includes)
#include <google/protobuf/port_def.inc>
PROTOBUF_PRAGMA_INIT_SEG
namespace _pb = ::PROTOBUF_NAMESPACE_ID;
namespace _pbi = _pb::internal;
namespace focus {
PROTOBUF_CONSTEXPR VersionResponse::VersionResponse(
::_pbi::ConstantInitialized): _impl_{
/*decltype(_impl_.version_)*/{&::_pbi::fixed_address_empty_string, ::_pbi::ConstantInitialized{}}
, /*decltype(_impl_._cached_size_)*/{}} {}
struct VersionResponseDefaultTypeInternal {
PROTOBUF_CONSTEXPR VersionResponseDefaultTypeInternal()
: _instance(::_pbi::ConstantInitialized{}) {}
~VersionResponseDefaultTypeInternal() {}
union {
VersionResponse _instance;
};
};
PROTOBUF_ATTRIBUTE_NO_DESTROY PROTOBUF_CONSTINIT PROTOBUF_ATTRIBUTE_INIT_PRIORITY1 VersionResponseDefaultTypeInternal _VersionResponse_default_instance_;
} // namespace focus
static ::_pb::Metadata file_level_metadata_focus_2eproto[1];
static constexpr ::_pb::EnumDescriptor const** file_level_enum_descriptors_focus_2eproto = nullptr;
static constexpr ::_pb::ServiceDescriptor const** file_level_service_descriptors_focus_2eproto = nullptr;
const uint32_t TableStruct_focus_2eproto::offsets[] PROTOBUF_SECTION_VARIABLE(protodesc_cold) = {
~0u, // no _has_bits_
PROTOBUF_FIELD_OFFSET(::focus::VersionResponse, _internal_metadata_),
~0u, // no _extensions_
~0u, // no _oneof_case_
~0u, // no _weak_field_map_
~0u, // no _inlined_string_donated_
PROTOBUF_FIELD_OFFSET(::focus::VersionResponse, _impl_.version_),
};
static const ::_pbi::MigrationSchema schemas[] PROTOBUF_SECTION_VARIABLE(protodesc_cold) = {
{ 0, -1, -1, sizeof(::focus::VersionResponse)},
};
static const ::_pb::Message* const file_default_instances[] = {
&::focus::_VersionResponse_default_instance_._instance,
};
const char descriptor_table_protodef_focus_2eproto[] PROTOBUF_SECTION_VARIABLE(protodesc_cold) =
"\n\013focus.proto\022\005focus\032\033google/protobuf/em"
"pty.proto\"\"\n\017VersionResponse\022\017\n\007version\030"
"\001 \001(\t2{\n\005Focus\0227\n\005Raise\022\026.google.protobu"
"f.Empty\032\026.google.protobuf.Empty\0229\n\007Versi"
"on\022\026.google.protobuf.Empty\032\026.focus.Versi"
"onResponseB=Z;github.com/ProtonMail/prot"
"on-bridge/v3/internal/focus/protob\006proto"
"3"
;
static const ::_pbi::DescriptorTable* const descriptor_table_focus_2eproto_deps[1] = {
&::descriptor_table_google_2fprotobuf_2fempty_2eproto,
};
static ::_pbi::once_flag descriptor_table_focus_2eproto_once;
const ::_pbi::DescriptorTable descriptor_table_focus_2eproto = {
false, false, 281, descriptor_table_protodef_focus_2eproto,
"focus.proto",
&descriptor_table_focus_2eproto_once, descriptor_table_focus_2eproto_deps, 1, 1,
schemas, file_default_instances, TableStruct_focus_2eproto::offsets,
file_level_metadata_focus_2eproto, file_level_enum_descriptors_focus_2eproto,
file_level_service_descriptors_focus_2eproto,
};
PROTOBUF_ATTRIBUTE_WEAK const ::_pbi::DescriptorTable* descriptor_table_focus_2eproto_getter() {
return &descriptor_table_focus_2eproto;
}
// Force running AddDescriptors() at dynamic initialization time.
PROTOBUF_ATTRIBUTE_INIT_PRIORITY2 static ::_pbi::AddDescriptorsRunner dynamic_init_dummy_focus_2eproto(&descriptor_table_focus_2eproto);
namespace focus {
// ===================================================================
class VersionResponse::_Internal {
public:
};
VersionResponse::VersionResponse(::PROTOBUF_NAMESPACE_ID::Arena* arena,
bool is_message_owned)
: ::PROTOBUF_NAMESPACE_ID::Message(arena, is_message_owned) {
SharedCtor(arena, is_message_owned);
// @@protoc_insertion_point(arena_constructor:focus.VersionResponse)
}
VersionResponse::VersionResponse(const VersionResponse& from)
: ::PROTOBUF_NAMESPACE_ID::Message() {
VersionResponse* const _this = this; (void)_this;
new (&_impl_) Impl_{
decltype(_impl_.version_){}
, /*decltype(_impl_._cached_size_)*/{}};
_internal_metadata_.MergeFrom<::PROTOBUF_NAMESPACE_ID::UnknownFieldSet>(from._internal_metadata_);
_impl_.version_.InitDefault();
#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING
_impl_.version_.Set("", GetArenaForAllocation());
#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING
if (!from._internal_version().empty()) {
_this->_impl_.version_.Set(from._internal_version(),
_this->GetArenaForAllocation());
}
// @@protoc_insertion_point(copy_constructor:focus.VersionResponse)
}
inline void VersionResponse::SharedCtor(
::_pb::Arena* arena, bool is_message_owned) {
(void)arena;
(void)is_message_owned;
new (&_impl_) Impl_{
decltype(_impl_.version_){}
, /*decltype(_impl_._cached_size_)*/{}
};
_impl_.version_.InitDefault();
#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING
_impl_.version_.Set("", GetArenaForAllocation());
#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING
}
VersionResponse::~VersionResponse() {
// @@protoc_insertion_point(destructor:focus.VersionResponse)
if (auto *arena = _internal_metadata_.DeleteReturnArena<::PROTOBUF_NAMESPACE_ID::UnknownFieldSet>()) {
(void)arena;
return;
}
SharedDtor();
}
inline void VersionResponse::SharedDtor() {
GOOGLE_DCHECK(GetArenaForAllocation() == nullptr);
_impl_.version_.Destroy();
}
void VersionResponse::SetCachedSize(int size) const {
_impl_._cached_size_.Set(size);
}
void VersionResponse::Clear() {
// @@protoc_insertion_point(message_clear_start:focus.VersionResponse)
uint32_t cached_has_bits = 0;
// Prevent compiler warnings about cached_has_bits being unused
(void) cached_has_bits;
_impl_.version_.ClearToEmpty();
_internal_metadata_.Clear<::PROTOBUF_NAMESPACE_ID::UnknownFieldSet>();
}
const char* VersionResponse::_InternalParse(const char* ptr, ::_pbi::ParseContext* ctx) {
#define CHK_(x) if (PROTOBUF_PREDICT_FALSE(!(x))) goto failure
while (!ctx->Done(&ptr)) {
uint32_t tag;
ptr = ::_pbi::ReadTag(ptr, &tag);
switch (tag >> 3) {
// string version = 1;
case 1:
if (PROTOBUF_PREDICT_TRUE(static_cast<uint8_t>(tag) == 10)) {
auto str = _internal_mutable_version();
ptr = ::_pbi::InlineGreedyStringParser(str, ptr, ctx);
CHK_(ptr);
CHK_(::_pbi::VerifyUTF8(str, "focus.VersionResponse.version"));
} else
goto handle_unusual;
continue;
default:
goto handle_unusual;
} // switch
handle_unusual:
if ((tag == 0) || ((tag & 7) == 4)) {
CHK_(ptr);
ctx->SetLastTag(tag);
goto message_done;
}
ptr = UnknownFieldParse(
tag,
_internal_metadata_.mutable_unknown_fields<::PROTOBUF_NAMESPACE_ID::UnknownFieldSet>(),
ptr, ctx);
CHK_(ptr != nullptr);
} // while
message_done:
return ptr;
failure:
ptr = nullptr;
goto message_done;
#undef CHK_
}
uint8_t* VersionResponse::_InternalSerialize(
uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const {
// @@protoc_insertion_point(serialize_to_array_start:focus.VersionResponse)
uint32_t cached_has_bits = 0;
(void) cached_has_bits;
// string version = 1;
if (!this->_internal_version().empty()) {
::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::VerifyUtf8String(
this->_internal_version().data(), static_cast<int>(this->_internal_version().length()),
::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::SERIALIZE,
"focus.VersionResponse.version");
target = stream->WriteStringMaybeAliased(
1, this->_internal_version(), target);
}
if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) {
target = ::_pbi::WireFormat::InternalSerializeUnknownFieldsToArray(
_internal_metadata_.unknown_fields<::PROTOBUF_NAMESPACE_ID::UnknownFieldSet>(::PROTOBUF_NAMESPACE_ID::UnknownFieldSet::default_instance), target, stream);
}
// @@protoc_insertion_point(serialize_to_array_end:focus.VersionResponse)
return target;
}
size_t VersionResponse::ByteSizeLong() const {
// @@protoc_insertion_point(message_byte_size_start:focus.VersionResponse)
size_t total_size = 0;
uint32_t cached_has_bits = 0;
// Prevent compiler warnings about cached_has_bits being unused
(void) cached_has_bits;
// string version = 1;
if (!this->_internal_version().empty()) {
total_size += 1 +
::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::StringSize(
this->_internal_version());
}
return MaybeComputeUnknownFieldsSize(total_size, &_impl_._cached_size_);
}
const ::PROTOBUF_NAMESPACE_ID::Message::ClassData VersionResponse::_class_data_ = {
::PROTOBUF_NAMESPACE_ID::Message::CopyWithSourceCheck,
VersionResponse::MergeImpl
};
const ::PROTOBUF_NAMESPACE_ID::Message::ClassData*VersionResponse::GetClassData() const { return &_class_data_; }
void VersionResponse::MergeImpl(::PROTOBUF_NAMESPACE_ID::Message& to_msg, const ::PROTOBUF_NAMESPACE_ID::Message& from_msg) {
auto* const _this = static_cast<VersionResponse*>(&to_msg);
auto& from = static_cast<const VersionResponse&>(from_msg);
// @@protoc_insertion_point(class_specific_merge_from_start:focus.VersionResponse)
GOOGLE_DCHECK_NE(&from, _this);
uint32_t cached_has_bits = 0;
(void) cached_has_bits;
if (!from._internal_version().empty()) {
_this->_internal_set_version(from._internal_version());
}
_this->_internal_metadata_.MergeFrom<::PROTOBUF_NAMESPACE_ID::UnknownFieldSet>(from._internal_metadata_);
}
void VersionResponse::CopyFrom(const VersionResponse& from) {
// @@protoc_insertion_point(class_specific_copy_from_start:focus.VersionResponse)
if (&from == this) return;
Clear();
MergeFrom(from);
}
bool VersionResponse::IsInitialized() const {
return true;
}
void VersionResponse::InternalSwap(VersionResponse* other) {
using std::swap;
auto* lhs_arena = GetArenaForAllocation();
auto* rhs_arena = other->GetArenaForAllocation();
_internal_metadata_.InternalSwap(&other->_internal_metadata_);
::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr::InternalSwap(
&_impl_.version_, lhs_arena,
&other->_impl_.version_, rhs_arena
);
}
::PROTOBUF_NAMESPACE_ID::Metadata VersionResponse::GetMetadata() const {
return ::_pbi::AssignDescriptors(
&descriptor_table_focus_2eproto_getter, &descriptor_table_focus_2eproto_once,
file_level_metadata_focus_2eproto[0]);
}
// @@protoc_insertion_point(namespace_scope)
} // namespace focus
PROTOBUF_NAMESPACE_OPEN
template<> PROTOBUF_NOINLINE ::focus::VersionResponse*
Arena::CreateMaybeMessage< ::focus::VersionResponse >(Arena* arena) {
return Arena::CreateMessageInternal< ::focus::VersionResponse >(arena);
}
PROTOBUF_NAMESPACE_CLOSE
// @@protoc_insertion_point(global_scope)
#include <google/protobuf/port_undef.inc>

View File

@ -1,283 +0,0 @@
// Generated by the protocol buffer compiler. DO NOT EDIT!
// source: focus.proto
#ifndef GOOGLE_PROTOBUF_INCLUDED_focus_2eproto
#define GOOGLE_PROTOBUF_INCLUDED_focus_2eproto
#include <limits>
#include <string>
#include <google/protobuf/port_def.inc>
#if PROTOBUF_VERSION < 3021000
#error This file was generated by a newer version of protoc which is
#error incompatible with your Protocol Buffer headers. Please update
#error your headers.
#endif
#if 3021012 < PROTOBUF_MIN_PROTOC_VERSION
#error This file was generated by an older version of protoc which is
#error incompatible with your Protocol Buffer headers. Please
#error regenerate this file with a newer version of protoc.
#endif
#include <google/protobuf/port_undef.inc>
#include <google/protobuf/io/coded_stream.h>
#include <google/protobuf/arena.h>
#include <google/protobuf/arenastring.h>
#include <google/protobuf/generated_message_util.h>
#include <google/protobuf/metadata_lite.h>
#include <google/protobuf/generated_message_reflection.h>
#include <google/protobuf/message.h>
#include <google/protobuf/repeated_field.h> // IWYU pragma: export
#include <google/protobuf/extension_set.h> // IWYU pragma: export
#include <google/protobuf/unknown_field_set.h>
#include <google/protobuf/empty.pb.h>
// @@protoc_insertion_point(includes)
#include <google/protobuf/port_def.inc>
#define PROTOBUF_INTERNAL_EXPORT_focus_2eproto
PROTOBUF_NAMESPACE_OPEN
namespace internal {
class AnyMetadata;
} // namespace internal
PROTOBUF_NAMESPACE_CLOSE
// Internal implementation detail -- do not use these members.
struct TableStruct_focus_2eproto {
static const uint32_t offsets[];
};
extern const ::PROTOBUF_NAMESPACE_ID::internal::DescriptorTable descriptor_table_focus_2eproto;
namespace focus {
class VersionResponse;
struct VersionResponseDefaultTypeInternal;
extern VersionResponseDefaultTypeInternal _VersionResponse_default_instance_;
} // namespace focus
PROTOBUF_NAMESPACE_OPEN
template<> ::focus::VersionResponse* Arena::CreateMaybeMessage<::focus::VersionResponse>(Arena*);
PROTOBUF_NAMESPACE_CLOSE
namespace focus {
// ===================================================================
class VersionResponse final :
public ::PROTOBUF_NAMESPACE_ID::Message /* @@protoc_insertion_point(class_definition:focus.VersionResponse) */ {
public:
inline VersionResponse() : VersionResponse(nullptr) {}
~VersionResponse() override;
explicit PROTOBUF_CONSTEXPR VersionResponse(::PROTOBUF_NAMESPACE_ID::internal::ConstantInitialized);
VersionResponse(const VersionResponse& from);
VersionResponse(VersionResponse&& from) noexcept
: VersionResponse() {
*this = ::std::move(from);
}
inline VersionResponse& operator=(const VersionResponse& from) {
CopyFrom(from);
return *this;
}
inline VersionResponse& operator=(VersionResponse&& from) noexcept {
if (this == &from) return *this;
if (GetOwningArena() == from.GetOwningArena()
#ifdef PROTOBUF_FORCE_COPY_IN_MOVE
&& GetOwningArena() != nullptr
#endif // !PROTOBUF_FORCE_COPY_IN_MOVE
) {
InternalSwap(&from);
} else {
CopyFrom(from);
}
return *this;
}
static const ::PROTOBUF_NAMESPACE_ID::Descriptor* descriptor() {
return GetDescriptor();
}
static const ::PROTOBUF_NAMESPACE_ID::Descriptor* GetDescriptor() {
return default_instance().GetMetadata().descriptor;
}
static const ::PROTOBUF_NAMESPACE_ID::Reflection* GetReflection() {
return default_instance().GetMetadata().reflection;
}
static const VersionResponse& default_instance() {
return *internal_default_instance();
}
static inline const VersionResponse* internal_default_instance() {
return reinterpret_cast<const VersionResponse*>(
&_VersionResponse_default_instance_);
}
static constexpr int kIndexInFileMessages =
0;
friend void swap(VersionResponse& a, VersionResponse& b) {
a.Swap(&b);
}
inline void Swap(VersionResponse* other) {
if (other == this) return;
#ifdef PROTOBUF_FORCE_COPY_IN_SWAP
if (GetOwningArena() != nullptr &&
GetOwningArena() == other->GetOwningArena()) {
#else // PROTOBUF_FORCE_COPY_IN_SWAP
if (GetOwningArena() == other->GetOwningArena()) {
#endif // !PROTOBUF_FORCE_COPY_IN_SWAP
InternalSwap(other);
} else {
::PROTOBUF_NAMESPACE_ID::internal::GenericSwap(this, other);
}
}
void UnsafeArenaSwap(VersionResponse* other) {
if (other == this) return;
GOOGLE_DCHECK(GetOwningArena() == other->GetOwningArena());
InternalSwap(other);
}
// implements Message ----------------------------------------------
VersionResponse* New(::PROTOBUF_NAMESPACE_ID::Arena* arena = nullptr) const final {
return CreateMaybeMessage<VersionResponse>(arena);
}
using ::PROTOBUF_NAMESPACE_ID::Message::CopyFrom;
void CopyFrom(const VersionResponse& from);
using ::PROTOBUF_NAMESPACE_ID::Message::MergeFrom;
void MergeFrom( const VersionResponse& from) {
VersionResponse::MergeImpl(*this, from);
}
private:
static void MergeImpl(::PROTOBUF_NAMESPACE_ID::Message& to_msg, const ::PROTOBUF_NAMESPACE_ID::Message& from_msg);
public:
PROTOBUF_ATTRIBUTE_REINITIALIZES void Clear() final;
bool IsInitialized() const final;
size_t ByteSizeLong() const final;
const char* _InternalParse(const char* ptr, ::PROTOBUF_NAMESPACE_ID::internal::ParseContext* ctx) final;
uint8_t* _InternalSerialize(
uint8_t* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const final;
int GetCachedSize() const final { return _impl_._cached_size_.Get(); }
private:
void SharedCtor(::PROTOBUF_NAMESPACE_ID::Arena* arena, bool is_message_owned);
void SharedDtor();
void SetCachedSize(int size) const final;
void InternalSwap(VersionResponse* other);
private:
friend class ::PROTOBUF_NAMESPACE_ID::internal::AnyMetadata;
static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() {
return "focus.VersionResponse";
}
protected:
explicit VersionResponse(::PROTOBUF_NAMESPACE_ID::Arena* arena,
bool is_message_owned = false);
public:
static const ClassData _class_data_;
const ::PROTOBUF_NAMESPACE_ID::Message::ClassData*GetClassData() const final;
::PROTOBUF_NAMESPACE_ID::Metadata GetMetadata() const final;
// nested types ----------------------------------------------------
// accessors -------------------------------------------------------
enum : int {
kVersionFieldNumber = 1,
};
// string version = 1;
void clear_version();
const std::string& version() const;
template <typename ArgT0 = const std::string&, typename... ArgT>
void set_version(ArgT0&& arg0, ArgT... args);
std::string* mutable_version();
PROTOBUF_NODISCARD std::string* release_version();
void set_allocated_version(std::string* version);
private:
const std::string& _internal_version() const;
inline PROTOBUF_ALWAYS_INLINE void _internal_set_version(const std::string& value);
std::string* _internal_mutable_version();
public:
// @@protoc_insertion_point(class_scope:focus.VersionResponse)
private:
class _Internal;
template <typename T> friend class ::PROTOBUF_NAMESPACE_ID::Arena::InternalHelper;
typedef void InternalArenaConstructable_;
typedef void DestructorSkippable_;
struct Impl_ {
::PROTOBUF_NAMESPACE_ID::internal::ArenaStringPtr version_;
mutable ::PROTOBUF_NAMESPACE_ID::internal::CachedSize _cached_size_;
};
union { Impl_ _impl_; };
friend struct ::TableStruct_focus_2eproto;
};
// ===================================================================
// ===================================================================
#ifdef __GNUC__
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wstrict-aliasing"
#endif // __GNUC__
// VersionResponse
// string version = 1;
inline void VersionResponse::clear_version() {
_impl_.version_.ClearToEmpty();
}
inline const std::string& VersionResponse::version() const {
// @@protoc_insertion_point(field_get:focus.VersionResponse.version)
return _internal_version();
}
template <typename ArgT0, typename... ArgT>
inline PROTOBUF_ALWAYS_INLINE
void VersionResponse::set_version(ArgT0&& arg0, ArgT... args) {
_impl_.version_.Set(static_cast<ArgT0 &&>(arg0), args..., GetArenaForAllocation());
// @@protoc_insertion_point(field_set:focus.VersionResponse.version)
}
inline std::string* VersionResponse::mutable_version() {
std::string* _s = _internal_mutable_version();
// @@protoc_insertion_point(field_mutable:focus.VersionResponse.version)
return _s;
}
inline const std::string& VersionResponse::_internal_version() const {
return _impl_.version_.Get();
}
inline void VersionResponse::_internal_set_version(const std::string& value) {
_impl_.version_.Set(value, GetArenaForAllocation());
}
inline std::string* VersionResponse::_internal_mutable_version() {
return _impl_.version_.Mutable(GetArenaForAllocation());
}
inline std::string* VersionResponse::release_version() {
// @@protoc_insertion_point(field_release:focus.VersionResponse.version)
return _impl_.version_.Release();
}
inline void VersionResponse::set_allocated_version(std::string* version) {
if (version != nullptr) {
} else {
}
_impl_.version_.SetAllocated(version, GetArenaForAllocation());
#ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING
if (_impl_.version_.IsDefault()) {
_impl_.version_.Set("", GetArenaForAllocation());
}
#endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING
// @@protoc_insertion_point(field_set_allocated:focus.VersionResponse.version)
}
#ifdef __GNUC__
#pragma GCC diagnostic pop
#endif // __GNUC__
// @@protoc_insertion_point(namespace_scope)
} // namespace focus
// @@protoc_insertion_point(global_scope)
#include <google/protobuf/port_undef.inc>
#endif // GOOGLE_PROTOBUF_INCLUDED_GOOGLE_PROTOBUF_INCLUDED_focus_2eproto

View File

@ -57,11 +57,13 @@ void GRPCClient::removeServiceConfigFile(QString const &configDir) {
//****************************************************************************************************************************************************
/// \param[in] sessionID The sessionID.
/// \param[in] timeoutMs The timeout in milliseconds
/// \param[in] serverProcess An optional server process to monitor. If the process it, no need and retry, as connexion cannot be established. Ignored if null.
/// \return The service config.
//****************************************************************************************************************************************************
GRPCConfig GRPCClient::waitAndRetrieveServiceConfig(QString const &configDir, qint64 timeoutMs, ProcessMonitor *serverProcess) {
GRPCConfig GRPCClient::waitAndRetrieveServiceConfig(QString const & sessionID, QString const &configDir, qint64 timeoutMs,
ProcessMonitor *serverProcess) {
QString const path = grpcServerConfigPath(configDir);
QFile file(path);
@ -71,7 +73,7 @@ GRPCConfig GRPCClient::waitAndRetrieveServiceConfig(QString const &configDir, qi
while (true) {
if (serverProcess && serverProcess->getStatus().ended) {
throw Exception("Bridge application exited before providing a gRPC service configuration file.", QString(), __FUNCTION__,
tailOfLatestBridgeLog());
tailOfLatestBridgeLog(sessionID));
}
if (file.exists()) {
@ -85,7 +87,7 @@ GRPCConfig GRPCClient::waitAndRetrieveServiceConfig(QString const &configDir, qi
}
if (!found) {
throw Exception("Server did not provide gRPC service configuration in time.", QString(), __FUNCTION__, tailOfLatestBridgeLog());
throw Exception("Server did not provide gRPC service configuration in time.", QString(), __FUNCTION__, tailOfLatestBridgeLog(sessionID));
}
GRPCConfig sc;
@ -114,10 +116,12 @@ void GRPCClient::setLog(Log *log) {
//****************************************************************************************************************************************************
/// \param[in] sessionID The sessionID.
/// \param[in] configDir The configuration directory
/// \param[in] serverProcess An optional server process to monitor. If the process it, no need and retry, as connexion cannot be established. Ignored if null.
/// \return true iff the connection was successful.
//****************************************************************************************************************************************************
void GRPCClient::connectToServer(QString const &configDir, GRPCConfig const &config, ProcessMonitor *serverProcess) {
void GRPCClient::connectToServer(QString const &sessionID, QString const &configDir, GRPCConfig const &config, ProcessMonitor *serverProcess) {
try {
serverToken_ = config.token.toStdString();
QString address;
@ -147,7 +151,7 @@ void GRPCClient::connectToServer(QString const &configDir, GRPCConfig const &con
while (true) {
if (serverProcess && serverProcess->getStatus().ended) {
throw Exception("Bridge application ended before gRPC connexion could be established.", QString(), __FUNCTION__,
tailOfLatestBridgeLog());
tailOfLatestBridgeLog(sessionID));
}
this->logInfo(QString("Connection to gRPC server at %1. attempt #%2").arg(address).arg(++i));
@ -158,7 +162,7 @@ void GRPCClient::connectToServer(QString const &configDir, GRPCConfig const &con
if (QDateTime::currentDateTime() > giveUpTime) {
throw Exception("Connection to the gRPC server failed because of a timeout.", QString(), __FUNCTION__,
tailOfLatestBridgeLog());
tailOfLatestBridgeLog(sessionID));
}
}
@ -1486,5 +1490,25 @@ UPClientContext GRPCClient::clientContext() const {
return ctx;
}
//****************************************************************************************************************************************************
/// \param[in] userID The user ID.
/// \param[in] address The email address.
/// \return the status for the gRPC call.
//****************************************************************************************************************************************************
grpc::Status GRPCClient::reportBugClicked() {
return this->logGRPCCallStatus(stub_->ReportBugClicked(this->clientContext().get(), empty, &empty), __FUNCTION__);
}
grpc::Status GRPCClient::autoconfigClicked(QString const &client) {
StringValue s;
s.set_value(client.toStdString());
return this->logGRPCCallStatus(stub_->AutoconfigClicked(this->clientContext().get(), s, &empty), __FUNCTION__);
}
grpc::Status GRPCClient::KBArticleClicked(QString const &article) {
StringValue s;
s.set_value(article.toStdString());
return this->logGRPCCallStatus(stub_->KBArticleClicked(this->clientContext().get(), s, &empty), __FUNCTION__);
}
} // namespace bridgepp

View File

@ -49,7 +49,8 @@ class GRPCClient : public QObject {
Q_OBJECT
public: // static member functions
static void removeServiceConfigFile(QString const &configDir); ///< Delete the service config file.
static GRPCConfig waitAndRetrieveServiceConfig(QString const &configDir, qint64 timeoutMs, class ProcessMonitor *serverProcess); ///< Wait and retrieve the service configuration.
static GRPCConfig waitAndRetrieveServiceConfig(QString const &sessionID, QString const &configDir, qint64 timeoutMs,
class ProcessMonitor *serverProcess); ///< Wait and retrieve the service configuration.
public: // member functions.
GRPCClient() = default; ///< Default constructor.
@ -59,7 +60,7 @@ public: // member functions.
GRPCClient &operator=(GRPCClient const &) = delete; ///< Disabled assignment operator.
GRPCClient &operator=(GRPCClient &&) = delete; ///< Disabled move assignment operator.
void setLog(Log *log); ///< Set the log for the client.
void connectToServer(QString const &configDir, GRPCConfig const &config, class ProcessMonitor *serverProcess); ///< Establish connection to the gRPC server.
void connectToServer(QString const &sessionID, QString const &configDir, GRPCConfig const &config, class ProcessMonitor *serverProcess); ///< Establish connection to the gRPC server.
bool isConnected() const; ///< Check whether the gRPC client is connected to the server.
grpc::Status checkTokens(QString const &clientConfigPath, QString &outReturnedClientToken); ///< Performs a token check.
@ -189,6 +190,11 @@ signals:
void syncFinished(QString const &userID);
void syncProgress(QString const &userID, double progress, qint64 elapsedMs, qint64 remainingMs);
public: // telemetry related calls
grpc::Status reportBugClicked(); ///< Performs the 'reportBugClicked' call.
grpc::Status autoconfigClicked(QString const &userID); ///< Performs the 'AutoconfigClicked' call.
grpc::Status KBArticleClicked(QString const &userID); ///< Performs the 'KBArticleClicked' call.
public: // keychain related calls
grpc::Status availableKeychains(QStringList &outKeychains);
grpc::Status currentKeychain(QString &outKeychain);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -46,7 +46,7 @@ public: // static member functions.
static bool stringToLevel(QString const &str, Log::Level &outLevel); ///< parse a level from a string.
public: // static data member.
static const Level defaultLevel { Level::Info }; ///< The default log level (the same as logrus).
static const Level defaultLevel { Level::Debug }; ///< The default log level in Bridge.
public: // member functions.
Log(); ///< Default constructor.

View File

@ -35,34 +35,29 @@ QString userLogsDir() {
//****************************************************************************************************************************************************
/// \brief Return the path of the latest bridge log.
///
/// \param[in] sessionID The sessionID.
/// \return The path of the latest bridge log file.
/// \return An empty string if no bridge log file was found.
//****************************************************************************************************************************************************
QString latestBridgeLogPath() {
QString latestBridgeLogPath(QString const &sessionID) {
QDir const logsDir(userLogsDir());
if (logsDir.isEmpty()) {
return QString();
}
QFileInfoList files = logsDir.entryInfoList({ "v*.log" }, QDir::Files); // could do sorting, but only by last modification time. we want to sort by creation time.
if (files.isEmpty()) {
return QString();
}
std::sort(files.begin(), files.end(), [](QFileInfo const &lhs, QFileInfo const &rhs) -> bool {
return lhs.birthTime() < rhs.birthTime();
});
return files.back().absoluteFilePath();
QFileInfoList const files = logsDir.entryInfoList({ sessionID + "_bri_*.log" }, QDir::Files, QDir::Name);
return files.isEmpty() ? QString() : files.back().absoluteFilePath();
}
//****************************************************************************************************************************************************
/// Return the maxSize last bytes of the latest bridge log.
//****************************************************************************************************************************************************
QByteArray tailOfLatestBridgeLog() {
QString path = latestBridgeLogPath();
QByteArray tailOfLatestBridgeLog(QString const &sessionID) {
QString path = latestBridgeLogPath(sessionID);
if (path.isEmpty()) {
return QByteArray();
return QString("We could not find a bridge log file for the current session.").toLocal8Bit();
}
QFile file(path);

View File

@ -24,7 +24,7 @@ namespace bridgepp {
QString userLogsDir(); ///< Return the path of the user logs dir.
QByteArray tailOfLatestBridgeLog(); ///< Return the last bytes of the last bridge log.
QByteArray tailOfLatestBridgeLog(QString const &sessionID); ///< Return the last bytes of the last bridge log.
} // namespace bridgepp

View File

@ -0,0 +1,53 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
#include "SessionID.h"
#include "QtCore/qdatetime.h"
namespace {
QString const dateTimeFormat = "yyyyMMdd_hhmmsszzz"; ///< The format string for date/time used by the sessionID.
}
namespace bridgepp {
//****************************************************************************************************************************************************
/// \return a new session ID based on the current local date/time
//****************************************************************************************************************************************************
QString newSessionID() {
return QDateTime::currentDateTime().toString(dateTimeFormat);
}
//****************************************************************************************************************************************************
/// \param[in] sessionID The sessionID.
/// \return The date/time corresponding to the sessionID.
/// \return An invalid date/time if an error occurs.
//****************************************************************************************************************************************************
QDateTime sessionIDToDateTime(QString const &sessionID) {
return QDateTime::fromString(sessionID, dateTimeFormat);
}
} // namespace

View File

@ -0,0 +1,32 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
#ifndef BRIDGE_PP_SESSION_ID_H
#define BRIDGE_PP_SESSION_ID_H
namespace bridgepp {
QString newSessionID(); ///< Create a new sessions
QDateTime sessionIDToDateTime(QString const &sessionID); ///< Parse the date/time from a sessionID.
} // namespace
#endif //BRIDGE_PP_SESSION_ID_H

View File

@ -0,0 +1,74 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package cli
import (
"context"
"os"
"github.com/abiosoft/ishell"
)
func (f *frontendCLI) debugMailboxState(c *ishell.Context) {
f.ShowPrompt(false)
defer f.ShowPrompt(true)
checkFlags := f.yesNoQuestion("Also check message flags")
c.Println("Starting state check. Note that depending on your message count this may take a while.")
result, err := f.bridge.CheckClientState(context.Background(), checkFlags, func(s string) {
c.Println(s)
})
if err != nil {
c.Printf("State check failed : %v", err)
return
}
c.Println("State check finished, see log for more details.")
if len(result.MissingMessages) == 0 {
return
}
f.Println("\n\nSome missing messages were detected. Bridge can download these messages for you")
f.Println("in a directory which you can later send to the developers for analysis.\n")
f.Println(bold("Note that the Messages will be stored unencrypted on disk.") + " If you do not wish")
f.Println("to continue, input no in the prompt below.\n")
if !f.yesNoQuestion("Would you like to proceed") {
return
}
location, err := os.MkdirTemp("", "debug-state-check-*")
if err != nil {
f.Printf("Failed to create temporary directory: %v\n", err)
return
}
c.Printf("Messages will be downloaded to: %v\n\n", bold(location))
if err := f.bridge.DebugDownloadFailedMessages(context.Background(), result, location, func(s string, i int, i2 int) {
f.Printf("[%v] Retrieving message %v of %v\n", s, i, i2)
}); err != nil {
f.Println(err)
return
}
c.Printf("\nMessage download finished. Data is available at %v\n", bold(location))
}

View File

@ -312,6 +312,19 @@ func New(
})
fe.AddCmd(telemetryCmd)
dbgCmd := &ishell.Cmd{
Name: "debug",
Help: "Debug diagnostics ",
}
dbgCmd.AddCmd(&ishell.Cmd{
Name: "mailbox-state",
Help: "Verify local mailbox state against proton server state",
Func: fe.debugMailboxState,
})
fe.AddCmd(dbgCmd)
go fe.watchEvents(eventCh)
go func() {

View File

@ -18,7 +18,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.0
// protoc v3.21.3
// protoc v3.21.12
// source: bridge.proto
package grpc
@ -4803,8 +4803,8 @@ var file_bridge_proto_rawDesc = []byte{
0x4e, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x00, 0x12, 0x19, 0x0a, 0x15, 0x54, 0x4c, 0x53,
0x5f, 0x43, 0x45, 0x52, 0x54, 0x5f, 0x45, 0x58, 0x50, 0x4f, 0x52, 0x54, 0x5f, 0x45, 0x52, 0x52,
0x4f, 0x52, 0x10, 0x01, 0x12, 0x18, 0x0a, 0x14, 0x54, 0x4c, 0x53, 0x5f, 0x4b, 0x45, 0x59, 0x5f,
0x45, 0x58, 0x50, 0x4f, 0x52, 0x54, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x02, 0x32, 0x89,
0x1f, 0x0a, 0x06, 0x42, 0x72, 0x69, 0x64, 0x67, 0x65, 0x12, 0x49, 0x0a, 0x0b, 0x43, 0x68, 0x65,
0x45, 0x58, 0x50, 0x4f, 0x52, 0x54, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x02, 0x32, 0xe2,
0x20, 0x0a, 0x06, 0x42, 0x72, 0x69, 0x64, 0x67, 0x65, 0x12, 0x49, 0x0a, 0x0b, 0x43, 0x68, 0x65,
0x63, 0x6b, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e,
0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
@ -5044,19 +5044,33 @@ var file_bridge_proto_rawDesc = []byte{
0x67, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x65, 0x41, 0x70,
0x70, 0x6c, 0x65, 0x4d, 0x61, 0x69, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16,
0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x3f, 0x0a, 0x0e, 0x52, 0x75, 0x6e, 0x45, 0x76, 0x65,
0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x18, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e,
0x45, 0x76, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x1a, 0x11, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d,
0x45, 0x76, 0x65, 0x6e, 0x74, 0x30, 0x01, 0x12, 0x41, 0x0a, 0x0f, 0x53, 0x74, 0x6f, 0x70, 0x45,
0x76, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f,
0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x42, 0x0a, 0x10, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74,
0x42, 0x75, 0x67, 0x43, 0x6c, 0x69, 0x63, 0x6b, 0x65, 0x64, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f,
0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70,
0x74, 0x79, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, 0x36, 0x5a, 0x34, 0x67, 0x69,
0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x6e, 0x4d,
0x61, 0x69, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x6e, 0x2d, 0x62, 0x72, 0x69, 0x64, 0x67,
0x65, 0x2f, 0x76, 0x33, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x72,
0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x49, 0x0a, 0x11, 0x41, 0x75,
0x74, 0x6f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x43, 0x6c, 0x69, 0x63, 0x6b, 0x65, 0x64, 0x12,
0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e,
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x48, 0x0a, 0x10, 0x4b, 0x42, 0x41, 0x72, 0x74, 0x69, 0x63,
0x6c, 0x65, 0x43, 0x6c, 0x69, 0x63, 0x6b, 0x65, 0x64, 0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69,
0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12,
0x3f, 0x0a, 0x0e, 0x52, 0x75, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61,
0x6d, 0x12, 0x18, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x53, 0x74,
0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x67, 0x72,
0x70, 0x63, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x30, 0x01,
0x12, 0x41, 0x0a, 0x0f, 0x53, 0x74, 0x6f, 0x70, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72,
0x65, 0x61, 0x6d, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x67, 0x6f,
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d,
0x70, 0x74, 0x79, 0x42, 0x36, 0x5a, 0x34, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f,
0x6d, 0x2f, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x6e, 0x4d, 0x61, 0x69, 0x6c, 0x2f, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x6e, 0x2d, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x2f, 0x76, 0x33, 0x2f, 0x69, 0x6e,
0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x33,
}
var (
@ -5266,67 +5280,73 @@ var file_bridge_proto_depIdxs = []int32{
71, // 112: grpc.Bridge.LogoutUser:input_type -> google.protobuf.StringValue
71, // 113: grpc.Bridge.RemoveUser:input_type -> google.protobuf.StringValue
18, // 114: grpc.Bridge.ConfigureUserAppleMail:input_type -> grpc.ConfigureAppleMailRequest
19, // 115: grpc.Bridge.RunEventStream:input_type -> grpc.EventStreamRequest
72, // 116: grpc.Bridge.StopEventStream:input_type -> google.protobuf.Empty
71, // 117: grpc.Bridge.CheckTokens:output_type -> google.protobuf.StringValue
72, // 118: grpc.Bridge.AddLogEntry:output_type -> google.protobuf.Empty
8, // 119: grpc.Bridge.GuiReady:output_type -> grpc.GuiReadyResponse
72, // 120: grpc.Bridge.Quit:output_type -> google.protobuf.Empty
72, // 121: grpc.Bridge.Restart:output_type -> google.protobuf.Empty
73, // 122: grpc.Bridge.ShowOnStartup:output_type -> google.protobuf.BoolValue
72, // 123: grpc.Bridge.SetIsAutostartOn:output_type -> google.protobuf.Empty
73, // 124: grpc.Bridge.IsAutostartOn:output_type -> google.protobuf.BoolValue
72, // 125: grpc.Bridge.SetIsBetaEnabled:output_type -> google.protobuf.Empty
73, // 126: grpc.Bridge.IsBetaEnabled:output_type -> google.protobuf.BoolValue
72, // 127: grpc.Bridge.SetIsAllMailVisible:output_type -> google.protobuf.Empty
73, // 128: grpc.Bridge.IsAllMailVisible:output_type -> google.protobuf.BoolValue
72, // 129: grpc.Bridge.SetIsTelemetryDisabled:output_type -> google.protobuf.Empty
73, // 130: grpc.Bridge.IsTelemetryDisabled:output_type -> google.protobuf.BoolValue
71, // 131: grpc.Bridge.GoOs:output_type -> google.protobuf.StringValue
72, // 132: grpc.Bridge.TriggerReset:output_type -> google.protobuf.Empty
71, // 133: grpc.Bridge.Version:output_type -> google.protobuf.StringValue
71, // 134: grpc.Bridge.LogsPath:output_type -> google.protobuf.StringValue
71, // 135: grpc.Bridge.LicensePath:output_type -> google.protobuf.StringValue
71, // 136: grpc.Bridge.ReleaseNotesPageLink:output_type -> google.protobuf.StringValue
71, // 137: grpc.Bridge.DependencyLicensesLink:output_type -> google.protobuf.StringValue
71, // 138: grpc.Bridge.LandingPageLink:output_type -> google.protobuf.StringValue
72, // 139: grpc.Bridge.SetColorSchemeName:output_type -> google.protobuf.Empty
71, // 140: grpc.Bridge.ColorSchemeName:output_type -> google.protobuf.StringValue
71, // 141: grpc.Bridge.CurrentEmailClient:output_type -> google.protobuf.StringValue
72, // 142: grpc.Bridge.ReportBug:output_type -> google.protobuf.Empty
72, // 143: grpc.Bridge.ExportTLSCertificates:output_type -> google.protobuf.Empty
72, // 144: grpc.Bridge.ForceLauncher:output_type -> google.protobuf.Empty
72, // 145: grpc.Bridge.SetMainExecutable:output_type -> google.protobuf.Empty
72, // 146: grpc.Bridge.Login:output_type -> google.protobuf.Empty
72, // 147: grpc.Bridge.Login2FA:output_type -> google.protobuf.Empty
72, // 148: grpc.Bridge.Login2Passwords:output_type -> google.protobuf.Empty
72, // 149: grpc.Bridge.LoginAbort:output_type -> google.protobuf.Empty
72, // 150: grpc.Bridge.CheckUpdate:output_type -> google.protobuf.Empty
72, // 151: grpc.Bridge.InstallUpdate:output_type -> google.protobuf.Empty
72, // 152: grpc.Bridge.SetIsAutomaticUpdateOn:output_type -> google.protobuf.Empty
73, // 153: grpc.Bridge.IsAutomaticUpdateOn:output_type -> google.protobuf.BoolValue
71, // 154: grpc.Bridge.DiskCachePath:output_type -> google.protobuf.StringValue
72, // 155: grpc.Bridge.SetDiskCachePath:output_type -> google.protobuf.Empty
72, // 156: grpc.Bridge.SetIsDoHEnabled:output_type -> google.protobuf.Empty
73, // 157: grpc.Bridge.IsDoHEnabled:output_type -> google.protobuf.BoolValue
12, // 158: grpc.Bridge.MailServerSettings:output_type -> grpc.ImapSmtpSettings
72, // 159: grpc.Bridge.SetMailServerSettings:output_type -> google.protobuf.Empty
71, // 160: grpc.Bridge.Hostname:output_type -> google.protobuf.StringValue
73, // 161: grpc.Bridge.IsPortFree:output_type -> google.protobuf.BoolValue
13, // 162: grpc.Bridge.AvailableKeychains:output_type -> grpc.AvailableKeychainsResponse
72, // 163: grpc.Bridge.SetCurrentKeychain:output_type -> google.protobuf.Empty
71, // 164: grpc.Bridge.CurrentKeychain:output_type -> google.protobuf.StringValue
17, // 165: grpc.Bridge.GetUserList:output_type -> grpc.UserListResponse
14, // 166: grpc.Bridge.GetUser:output_type -> grpc.User
72, // 167: grpc.Bridge.SetUserSplitMode:output_type -> google.protobuf.Empty
72, // 168: grpc.Bridge.SendBadEventUserFeedback:output_type -> google.protobuf.Empty
72, // 169: grpc.Bridge.LogoutUser:output_type -> google.protobuf.Empty
72, // 170: grpc.Bridge.RemoveUser:output_type -> google.protobuf.Empty
72, // 171: grpc.Bridge.ConfigureUserAppleMail:output_type -> google.protobuf.Empty
20, // 172: grpc.Bridge.RunEventStream:output_type -> grpc.StreamEvent
72, // 173: grpc.Bridge.StopEventStream:output_type -> google.protobuf.Empty
117, // [117:174] is the sub-list for method output_type
60, // [60:117] is the sub-list for method input_type
72, // 115: grpc.Bridge.ReportBugClicked:input_type -> google.protobuf.Empty
71, // 116: grpc.Bridge.AutoconfigClicked:input_type -> google.protobuf.StringValue
71, // 117: grpc.Bridge.KBArticleClicked:input_type -> google.protobuf.StringValue
19, // 118: grpc.Bridge.RunEventStream:input_type -> grpc.EventStreamRequest
72, // 119: grpc.Bridge.StopEventStream:input_type -> google.protobuf.Empty
71, // 120: grpc.Bridge.CheckTokens:output_type -> google.protobuf.StringValue
72, // 121: grpc.Bridge.AddLogEntry:output_type -> google.protobuf.Empty
8, // 122: grpc.Bridge.GuiReady:output_type -> grpc.GuiReadyResponse
72, // 123: grpc.Bridge.Quit:output_type -> google.protobuf.Empty
72, // 124: grpc.Bridge.Restart:output_type -> google.protobuf.Empty
73, // 125: grpc.Bridge.ShowOnStartup:output_type -> google.protobuf.BoolValue
72, // 126: grpc.Bridge.SetIsAutostartOn:output_type -> google.protobuf.Empty
73, // 127: grpc.Bridge.IsAutostartOn:output_type -> google.protobuf.BoolValue
72, // 128: grpc.Bridge.SetIsBetaEnabled:output_type -> google.protobuf.Empty
73, // 129: grpc.Bridge.IsBetaEnabled:output_type -> google.protobuf.BoolValue
72, // 130: grpc.Bridge.SetIsAllMailVisible:output_type -> google.protobuf.Empty
73, // 131: grpc.Bridge.IsAllMailVisible:output_type -> google.protobuf.BoolValue
72, // 132: grpc.Bridge.SetIsTelemetryDisabled:output_type -> google.protobuf.Empty
73, // 133: grpc.Bridge.IsTelemetryDisabled:output_type -> google.protobuf.BoolValue
71, // 134: grpc.Bridge.GoOs:output_type -> google.protobuf.StringValue
72, // 135: grpc.Bridge.TriggerReset:output_type -> google.protobuf.Empty
71, // 136: grpc.Bridge.Version:output_type -> google.protobuf.StringValue
71, // 137: grpc.Bridge.LogsPath:output_type -> google.protobuf.StringValue
71, // 138: grpc.Bridge.LicensePath:output_type -> google.protobuf.StringValue
71, // 139: grpc.Bridge.ReleaseNotesPageLink:output_type -> google.protobuf.StringValue
71, // 140: grpc.Bridge.DependencyLicensesLink:output_type -> google.protobuf.StringValue
71, // 141: grpc.Bridge.LandingPageLink:output_type -> google.protobuf.StringValue
72, // 142: grpc.Bridge.SetColorSchemeName:output_type -> google.protobuf.Empty
71, // 143: grpc.Bridge.ColorSchemeName:output_type -> google.protobuf.StringValue
71, // 144: grpc.Bridge.CurrentEmailClient:output_type -> google.protobuf.StringValue
72, // 145: grpc.Bridge.ReportBug:output_type -> google.protobuf.Empty
72, // 146: grpc.Bridge.ExportTLSCertificates:output_type -> google.protobuf.Empty
72, // 147: grpc.Bridge.ForceLauncher:output_type -> google.protobuf.Empty
72, // 148: grpc.Bridge.SetMainExecutable:output_type -> google.protobuf.Empty
72, // 149: grpc.Bridge.Login:output_type -> google.protobuf.Empty
72, // 150: grpc.Bridge.Login2FA:output_type -> google.protobuf.Empty
72, // 151: grpc.Bridge.Login2Passwords:output_type -> google.protobuf.Empty
72, // 152: grpc.Bridge.LoginAbort:output_type -> google.protobuf.Empty
72, // 153: grpc.Bridge.CheckUpdate:output_type -> google.protobuf.Empty
72, // 154: grpc.Bridge.InstallUpdate:output_type -> google.protobuf.Empty
72, // 155: grpc.Bridge.SetIsAutomaticUpdateOn:output_type -> google.protobuf.Empty
73, // 156: grpc.Bridge.IsAutomaticUpdateOn:output_type -> google.protobuf.BoolValue
71, // 157: grpc.Bridge.DiskCachePath:output_type -> google.protobuf.StringValue
72, // 158: grpc.Bridge.SetDiskCachePath:output_type -> google.protobuf.Empty
72, // 159: grpc.Bridge.SetIsDoHEnabled:output_type -> google.protobuf.Empty
73, // 160: grpc.Bridge.IsDoHEnabled:output_type -> google.protobuf.BoolValue
12, // 161: grpc.Bridge.MailServerSettings:output_type -> grpc.ImapSmtpSettings
72, // 162: grpc.Bridge.SetMailServerSettings:output_type -> google.protobuf.Empty
71, // 163: grpc.Bridge.Hostname:output_type -> google.protobuf.StringValue
73, // 164: grpc.Bridge.IsPortFree:output_type -> google.protobuf.BoolValue
13, // 165: grpc.Bridge.AvailableKeychains:output_type -> grpc.AvailableKeychainsResponse
72, // 166: grpc.Bridge.SetCurrentKeychain:output_type -> google.protobuf.Empty
71, // 167: grpc.Bridge.CurrentKeychain:output_type -> google.protobuf.StringValue
17, // 168: grpc.Bridge.GetUserList:output_type -> grpc.UserListResponse
14, // 169: grpc.Bridge.GetUser:output_type -> grpc.User
72, // 170: grpc.Bridge.SetUserSplitMode:output_type -> google.protobuf.Empty
72, // 171: grpc.Bridge.SendBadEventUserFeedback:output_type -> google.protobuf.Empty
72, // 172: grpc.Bridge.LogoutUser:output_type -> google.protobuf.Empty
72, // 173: grpc.Bridge.RemoveUser:output_type -> google.protobuf.Empty
72, // 174: grpc.Bridge.ConfigureUserAppleMail:output_type -> google.protobuf.Empty
72, // 175: grpc.Bridge.ReportBugClicked:output_type -> google.protobuf.Empty
72, // 176: grpc.Bridge.AutoconfigClicked:output_type -> google.protobuf.Empty
72, // 177: grpc.Bridge.KBArticleClicked:output_type -> google.protobuf.Empty
20, // 178: grpc.Bridge.RunEventStream:output_type -> grpc.StreamEvent
72, // 179: grpc.Bridge.StopEventStream:output_type -> google.protobuf.Empty
120, // [120:180] is the sub-list for method output_type
60, // [60:120] is the sub-list for method input_type
60, // [60:60] is the sub-list for extension type_name
60, // [60:60] is the sub-list for extension extendee
0, // [0:60] is the sub-list for field type_name

View File

@ -98,6 +98,11 @@ service Bridge {
rpc RemoveUser(google.protobuf.StringValue) returns (google.protobuf.Empty);
rpc ConfigureUserAppleMail(ConfigureAppleMailRequest) returns (google.protobuf.Empty);
// Telemetry
rpc ReportBugClicked(google.protobuf.Empty) returns (google.protobuf.Empty);
rpc AutoconfigClicked(google.protobuf.StringValue) returns (google.protobuf.Empty);
rpc KBArticleClicked(google.protobuf.StringValue) returns (google.protobuf.Empty);
// Server -> Client event stream
rpc RunEventStream(EventStreamRequest) returns (stream StreamEvent); // Keep streaming until StopEventStream is called.
rpc StopEventStream(google.protobuf.Empty) returns (google.protobuf.Empty);

View File

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.2.0
// - protoc v3.21.3
// - protoc v3.21.12
// source: bridge.proto
package grpc
@ -86,6 +86,10 @@ type BridgeClient interface {
LogoutUser(ctx context.Context, in *wrapperspb.StringValue, opts ...grpc.CallOption) (*emptypb.Empty, error)
RemoveUser(ctx context.Context, in *wrapperspb.StringValue, opts ...grpc.CallOption) (*emptypb.Empty, error)
ConfigureUserAppleMail(ctx context.Context, in *ConfigureAppleMailRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
// Telemetry
ReportBugClicked(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
AutoconfigClicked(ctx context.Context, in *wrapperspb.StringValue, opts ...grpc.CallOption) (*emptypb.Empty, error)
KBArticleClicked(ctx context.Context, in *wrapperspb.StringValue, opts ...grpc.CallOption) (*emptypb.Empty, error)
// Server -> Client event stream
RunEventStream(ctx context.Context, in *EventStreamRequest, opts ...grpc.CallOption) (Bridge_RunEventStreamClient, error)
StopEventStream(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
@ -594,6 +598,33 @@ func (c *bridgeClient) ConfigureUserAppleMail(ctx context.Context, in *Configure
return out, nil
}
func (c *bridgeClient) ReportBugClicked(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) {
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, "/grpc.Bridge/ReportBugClicked", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *bridgeClient) AutoconfigClicked(ctx context.Context, in *wrapperspb.StringValue, opts ...grpc.CallOption) (*emptypb.Empty, error) {
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, "/grpc.Bridge/AutoconfigClicked", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *bridgeClient) KBArticleClicked(ctx context.Context, in *wrapperspb.StringValue, opts ...grpc.CallOption) (*emptypb.Empty, error) {
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, "/grpc.Bridge/KBArticleClicked", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *bridgeClient) RunEventStream(ctx context.Context, in *EventStreamRequest, opts ...grpc.CallOption) (Bridge_RunEventStreamClient, error) {
stream, err := c.cc.NewStream(ctx, &Bridge_ServiceDesc.Streams[0], "/grpc.Bridge/RunEventStream", opts...)
if err != nil {
@ -701,6 +732,10 @@ type BridgeServer interface {
LogoutUser(context.Context, *wrapperspb.StringValue) (*emptypb.Empty, error)
RemoveUser(context.Context, *wrapperspb.StringValue) (*emptypb.Empty, error)
ConfigureUserAppleMail(context.Context, *ConfigureAppleMailRequest) (*emptypb.Empty, error)
// Telemetry
ReportBugClicked(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
AutoconfigClicked(context.Context, *wrapperspb.StringValue) (*emptypb.Empty, error)
KBArticleClicked(context.Context, *wrapperspb.StringValue) (*emptypb.Empty, error)
// Server -> Client event stream
RunEventStream(*EventStreamRequest, Bridge_RunEventStreamServer) error
StopEventStream(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
@ -876,6 +911,15 @@ func (UnimplementedBridgeServer) RemoveUser(context.Context, *wrapperspb.StringV
func (UnimplementedBridgeServer) ConfigureUserAppleMail(context.Context, *ConfigureAppleMailRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method ConfigureUserAppleMail not implemented")
}
func (UnimplementedBridgeServer) ReportBugClicked(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method ReportBugClicked not implemented")
}
func (UnimplementedBridgeServer) AutoconfigClicked(context.Context, *wrapperspb.StringValue) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method AutoconfigClicked not implemented")
}
func (UnimplementedBridgeServer) KBArticleClicked(context.Context, *wrapperspb.StringValue) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method KBArticleClicked not implemented")
}
func (UnimplementedBridgeServer) RunEventStream(*EventStreamRequest, Bridge_RunEventStreamServer) error {
return status.Errorf(codes.Unimplemented, "method RunEventStream not implemented")
}
@ -1885,6 +1929,60 @@ func _Bridge_ConfigureUserAppleMail_Handler(srv interface{}, ctx context.Context
return interceptor(ctx, in, info, handler)
}
func _Bridge_ReportBugClicked_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(emptypb.Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(BridgeServer).ReportBugClicked(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/grpc.Bridge/ReportBugClicked",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(BridgeServer).ReportBugClicked(ctx, req.(*emptypb.Empty))
}
return interceptor(ctx, in, info, handler)
}
func _Bridge_AutoconfigClicked_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(wrapperspb.StringValue)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(BridgeServer).AutoconfigClicked(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/grpc.Bridge/AutoconfigClicked",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(BridgeServer).AutoconfigClicked(ctx, req.(*wrapperspb.StringValue))
}
return interceptor(ctx, in, info, handler)
}
func _Bridge_KBArticleClicked_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(wrapperspb.StringValue)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(BridgeServer).KBArticleClicked(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/grpc.Bridge/KBArticleClicked",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(BridgeServer).KBArticleClicked(ctx, req.(*wrapperspb.StringValue))
}
return interceptor(ctx, in, info, handler)
}
func _Bridge_RunEventStream_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(EventStreamRequest)
if err := stream.RecvMsg(m); err != nil {
@ -2151,6 +2249,18 @@ var Bridge_ServiceDesc = grpc.ServiceDesc{
MethodName: "ConfigureUserAppleMail",
Handler: _Bridge_ConfigureUserAppleMail_Handler,
},
{
MethodName: "ReportBugClicked",
Handler: _Bridge_ReportBugClicked_Handler,
},
{
MethodName: "AutoconfigClicked",
Handler: _Bridge_AutoconfigClicked_Handler,
},
{
MethodName: "KBArticleClicked",
Handler: _Bridge_KBArticleClicked_Handler,
},
{
MethodName: "StopEventStream",
Handler: _Bridge_StopEventStream_Handler,

View File

@ -0,0 +1,40 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package grpc
import (
"context"
"google.golang.org/protobuf/types/known/emptypb"
"google.golang.org/protobuf/types/known/wrapperspb"
)
func (s *Service) ReportBugClicked(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
s.bridge.ReportBugClicked()
return &emptypb.Empty{}, nil
}
func (s *Service) AutoconfigClicked(_ context.Context, client *wrapperspb.StringValue) (*emptypb.Empty, error) {
s.bridge.AutoconfigUsed(client.Value)
return &emptypb.Empty{}, nil
}
func (s *Service) KBArticleClicked(_ context.Context, article *wrapperspb.StringValue) (*emptypb.Empty, error) {
s.bridge.KBArticleOpened(article.Value)
return &emptypb.Empty{}, nil
}

View File

@ -188,6 +188,16 @@ func (l *Locations) ProvideUpdatesPath() (string, error) {
return l.getUpdatesPath(), nil
}
// ProvideStatsPath returns a location for statistics files (e.g. ~/.local/share/<company>/<app>/stats).
// It creates it if it doesn't already exist.
func (l *Locations) ProvideStatsPath() (string, error) {
if err := os.MkdirAll(l.getStatsPath(), 0o700); err != nil {
return "", err
}
return l.getStatsPath(), nil
}
func (l *Locations) getGluonCachePath() string {
return filepath.Join(l.userData, "gluon")
}
@ -216,6 +226,10 @@ func (l *Locations) getUpdatesPath() string {
return filepath.Join(l.userData, "updates")
}
func (l *Locations) getStatsPath() string {
return filepath.Join(l.userData, "stats")
}
// Clear removes everything except the lock and update files.
func (l *Locations) Clear(except ...string) error {
return files.Remove(

View File

@ -1,69 +0,0 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package logging
import (
"fmt"
"io/fs"
"os"
"path/filepath"
"github.com/bradenaw/juniper/xslices"
"github.com/sirupsen/logrus"
"golang.org/x/exp/slices"
)
func clearLogs(logDir string, maxLogs int, maxCrashes int) error {
files, err := os.ReadDir(logDir)
if err != nil {
return fmt.Errorf("failed to read log directory: %w", err)
}
names := xslices.Map(files, func(file fs.DirEntry) string {
return file.Name()
})
// Remove old logs.
removeOldLogs(logDir, xslices.Filter(names, func(name string) bool {
return MatchLogName(name) && !MatchStackTraceName(name)
}), maxLogs)
// Remove old stack traces.
removeOldLogs(logDir, xslices.Filter(names, func(name string) bool {
return MatchLogName(name) && MatchStackTraceName(name)
}), maxCrashes)
return nil
}
func removeOldLogs(dir string, names []string, max int) {
if count := len(names); count <= max {
return
}
// Sort by timestamp, oldest first.
slices.SortFunc(names, func(a, b string) bool {
return getLogTime(a) < getLogTime(b)
})
for _, path := range xslices.Map(names[:len(names)-max], func(name string) string { return filepath.Join(dir, name) }) {
if err := os.Remove(path); err != nil {
logrus.WithError(err).WithField("path", path).Warn("Failed to remove old log file")
}
}
}

View File

@ -0,0 +1,170 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package logging
import (
"archive/zip"
"bytes"
"errors"
"io"
"os"
"path/filepath"
)
var (
errNoInputFile = errors.New("no file was provided to put in the archive")
errCannotFitAnyFile = errors.New("no file can fit in the archive")
)
// zipFilesWithMaxSize compress the maximum number of files from the given list that can fit a ZIP archive file whose size does not exceed
// maxSize. Input files are taken in order and the function returns as soon as the next file cannot fit, even if another file further in the list
// may fit. The function return the number of files that were included in the archive. The files included are filePath[:fileCount].
func zipFilesWithMaxSize(filePaths []string, maxSize int64) (buffer *bytes.Buffer, fileCount int, err error) {
if len(filePaths) == 0 {
return nil, 0, errNoInputFile
}
buffer, err = createZipFromFile(filePaths[0])
if err != nil {
return nil, 0, err
}
if int64(buffer.Len()) > maxSize {
return nil, 0, errCannotFitAnyFile
}
fileCount = 1
var previousBuffer *bytes.Buffer
for _, filePath := range filePaths[1:] {
previousBuffer = cloneBuffer(buffer)
zipReader, err := zip.NewReader(bytes.NewReader(buffer.Bytes()), int64(len(buffer.Bytes())))
if err != nil {
return nil, 0, err
}
buffer, err = addFileToArchive(zipReader, filePath)
if err != nil {
return nil, 0, err
}
if int64(buffer.Len()) > maxSize {
return previousBuffer, fileCount, nil
}
fileCount++
}
return buffer, fileCount, nil
}
// cloneBuffer clones a buffer.
func cloneBuffer(buffer *bytes.Buffer) *bytes.Buffer {
return bytes.NewBuffer(bytes.Clone(buffer.Bytes()))
}
// createZip creates a zip archive containing a single file.
func createZipFromFile(filePath string) (*bytes.Buffer, error) {
file, err := os.Open(filePath) //nolint:gosec
if err != nil {
return nil, err
}
defer func() { _ = file.Close() }()
return createZip(file, filepath.Base(filePath))
}
// createZip creates a zip file containing a file names filename with content read from reader.
func createZip(reader io.Reader, filename string) (*bytes.Buffer, error) {
b := bytes.NewBuffer(make([]byte, 0))
zipWriter := zip.NewWriter(b)
f, err := zipWriter.Create(filename)
if err != nil {
return nil, err
}
if _, err = io.Copy(f, reader); err != nil {
return nil, err
}
if err = zipWriter.Close(); err != nil {
return nil, err
}
return b, nil
}
// addToArchive adds a file to an archive. Because go zip package does not support adding a file to existing (closed) archive file, the way to do it
// is to create a new archive copy the raw content of the archive to the new one and add the new file before closing the archive.
func addFileToArchive(zipReader *zip.Reader, filePath string) (*bytes.Buffer, error) {
file, err := os.Open(filePath) //nolint:gosec
if err != nil {
return nil, err
}
defer func() { _ = file.Close() }()
return addToArchive(zipReader, file, filepath.Base(filePath))
}
// addToArchive adds data from a reader to a file in an archive.
func addToArchive(zipReader *zip.Reader, reader io.Reader, filename string) (*bytes.Buffer, error) {
buffer := bytes.NewBuffer([]byte{})
zipWriter := zip.NewWriter(buffer)
if err := copyZipContent(zipReader, zipWriter); err != nil {
return nil, err
}
f, err := zipWriter.Create(filename)
if err != nil {
return nil, err
}
if _, err := io.Copy(f, reader); err != nil {
return nil, err
}
if err := zipWriter.Close(); err != nil {
return nil, err
}
return buffer, nil
}
// copyZipContent copies the content of a zip to another without recompression.
func copyZipContent(zipReader *zip.Reader, zipWriter *zip.Writer) error {
for _, zipItem := range zipReader.File {
itemReader, err := zipItem.OpenRaw()
if err != nil {
return err
}
header := zipItem.FileHeader
targetItem, err := zipWriter.CreateRaw(&header)
if err != nil {
return err
}
if _, err := io.Copy(targetItem, itemReader); err != nil {
return err
}
}
return nil
}

View File

@ -0,0 +1,134 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package logging
import (
"archive/zip"
"bytes"
"crypto/rand"
"crypto/sha256"
"io"
"os"
"path/filepath"
"testing"
"github.com/bradenaw/juniper/xslices"
"github.com/stretchr/testify/require"
)
func TestLogging_LogCompression(t *testing.T) {
dir := t.TempDir()
files := []fileInfo{
{filepath.Join(dir, "1.log"), 100000},
{filepath.Join(dir, "2.log"), 200000},
{filepath.Join(dir, "3.log"), 300000},
}
// Files will have a content and size (relative to the zip format overhead) that ensure a compression ratio of roughly 2:1.
createRandomFiles(t, files)
paths := xslices.Map(files, func(fileInfo fileInfo) string { return fileInfo.filename })
// Case 1: no input file.
_, _, err := zipFilesWithMaxSize([]string{}, 10)
require.ErrorIs(t, err, errNoInputFile)
// Case 2: limit to low, no file can be included.
_, _, err = zipFilesWithMaxSize(paths, 100)
require.ErrorIs(t, err, errCannotFitAnyFile)
// case 3: 1 file fits.
buffer, fileCount, err := zipFilesWithMaxSize(paths, 100000)
require.NoError(t, err)
require.Equal(t, 1, fileCount)
checkZipFileContent(t, buffer, paths[0:1])
// case 4: 2 files fit.
buffer, fileCount, err = zipFilesWithMaxSize(paths, 200000)
require.NoError(t, err)
require.Equal(t, 2, fileCount)
checkZipFileContent(t, buffer, paths[0:2])
// case 5: 3 files fit.
buffer, fileCount, err = zipFilesWithMaxSize(paths, 500000)
require.NoError(t, err)
require.Equal(t, 3, fileCount)
checkZipFileContent(t, buffer, paths)
}
func createRandomFiles(t *testing.T, files []fileInfo) {
// The file is crafted to have a compression ratio of roughly 2:1 by filling the first half with random data, and the second with zeroes.
for _, file := range files {
randomData := make([]byte, file.size)
_, err := rand.Read(randomData[:file.size/2])
require.NoError(t, err)
require.NoError(t, os.WriteFile(file.filename, randomData, 0660))
}
}
func checkZipFileContent(t *testing.T, buffer *bytes.Buffer, expectedFilePaths []string) {
dir := t.TempDir()
count := unzipFile(t, buffer, dir)
require.Equal(t, len(expectedFilePaths), count)
for _, file := range expectedFilePaths {
checkFilesAreIdentical(t, file, filepath.Join(dir, filepath.Base(file)))
}
}
func unzipFile(t *testing.T, buffer *bytes.Buffer, dir string) int {
reader, err := zip.NewReader(bytes.NewReader(buffer.Bytes()), int64(len(buffer.Bytes())))
require.NoError(t, err)
for _, f := range reader.File {
info := f.FileInfo()
require.False(t, info.IsDir())
require.Equal(t, filepath.Base(info.Name()), info.Name()) // no sub-folder
extractFileFromZip(t, f, filepath.Join(dir, f.Name))
}
return len(reader.File)
}
func extractFileFromZip(t *testing.T, zip *zip.File, path string) {
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, zip.Mode())
require.NoError(t, err)
defer func() { _ = file.Close() }()
reader, err := zip.Open()
require.NoError(t, err)
defer func() { _ = reader.Close() }()
_, err = io.Copy(file, reader)
require.NoError(t, err)
}
func checkFilesAreIdentical(t *testing.T, path1, path2 string) {
require.EqualValues(t, sha256Sum(t, path1), sha256Sum(t, path2))
}
func sha256Sum(t *testing.T, path string) []byte {
f, err := os.Open(path)
require.NoError(t, err)
defer func() { _ = f.Close() }()
hash := sha256.New()
_, err = io.Copy(hash, f)
require.NoError(t, err)
return hash.Sum(nil)
}

View File

@ -23,16 +23,15 @@ import (
"path/filepath"
"regexp"
"runtime/pprof"
"time"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/crash"
"github.com/sirupsen/logrus"
)
func DumpStackTrace(logsPath string) crash.RecoveryAction {
func DumpStackTrace(logsPath string, sessionID SessionID, appName AppName) crash.RecoveryAction {
return func(r interface{}) error {
file := filepath.Join(logsPath, getStackTraceName(constants.Version, constants.Revision))
file := filepath.Join(logsPath, getStackTraceName(sessionID, appName, constants.Version, constants.Tag))
f, err := os.OpenFile(filepath.Clean(file), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0o600)
if err != nil {
@ -53,10 +52,10 @@ func DumpStackTrace(logsPath string) crash.RecoveryAction {
}
}
func getStackTraceName(version, revision string) string {
return fmt.Sprintf("v%v_%v_crash_%v.log", version, revision, time.Now().Unix())
func getStackTraceName(sessionID SessionID, appName AppName, version, tag string) string {
return fmt.Sprintf("%v_%v_000_v%v_%v_crash.log", sessionID, appName, version, tag)
}
func MatchStackTraceName(name string) bool {
return regexp.MustCompile(`^v.*_crash_.*\.log$`).MatchString(name)
return regexp.MustCompile(`^\d{8}_\d{9}_.*_000_.*_crash\.log$`).MatchString(name)
}

View File

@ -0,0 +1,32 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package logging
import (
"testing"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/stretchr/testify/require"
)
func TestLogging_MatchStackTraceName(t *testing.T) {
filename := getStackTraceName(NewSessionID(), constants.AppName, constants.Version, constants.Tag)
require.True(t, len(filename) > 0)
require.True(t, MatchStackTraceName(filename))
require.False(t, MatchStackTraceName("Invalid.log"))
}

View File

@ -18,32 +18,37 @@
package logging
import (
"bytes"
"context"
"fmt"
"errors"
"io"
"os"
"path/filepath"
"regexp"
"strconv"
"time"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/bradenaw/juniper/xslices"
"github.com/sirupsen/logrus"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
)
const (
// MaxLogSize defines the maximum log size we should permit: 5 MB
// DefaultMaxLogFileSize defines the maximum log size we should permit: 5 MB
//
// The Zendesk limit for an attachement is 50MB and this is what will
// The Zendesk limit for an attachment is 50MB and this is what will
// be allowed via the API. However, if that fails for some reason, the
// fallback is sending the report via email, which has a limit of 10mb
// total or 7MB per file. Since we can produce up to 6 logs, and we
// compress all the files (avarage compression - 80%), we need to have
// a limit of 30MB total before compression, hence 5MB per log file.
MaxLogSize = 5 * 1024 * 1024
// total or 7MB per file.
DefaultMaxLogFileSize = 5 * 1024 * 1024
)
// MaxLogs defines how many log files should be kept.
MaxLogs = 10
type AppName string
const (
BridgeShortAppName AppName = "bri"
LauncherShortAppName AppName = "lau"
GUIShortAppName AppName = "gui"
)
type coloredStdOutHook struct {
@ -82,7 +87,9 @@ func (cs *coloredStdOutHook) Fire(entry *logrus.Entry) error {
return nil
}
func Init(logsPath, level string) error {
// Init Initialize logging. Log files are rotated when their size exceeds rotationSize. if pruningSize >= 0, pruning occurs using
// the default pruning algorithm.
func Init(logsPath string, sessionID SessionID, appName AppName, rotationSize, pruningSize int64, level string) (io.Closer, error) {
logrus.SetFormatter(&logrus.TextFormatter{
DisableColors: true,
FullTimestamp: true,
@ -91,20 +98,80 @@ func Init(logsPath, level string) error {
logrus.AddHook(newColoredStdOutHook())
rotator, err := NewRotator(MaxLogSize, func() (io.WriteCloser, error) {
if err := clearLogs(logsPath, MaxLogs, MaxLogs); err != nil {
return nil, err
}
return os.Create(filepath.Join(logsPath, getLogName(constants.Version, constants.Revision))) //nolint:gosec // G304
})
rotator, err := NewDefaultRotator(logsPath, sessionID, appName, rotationSize, pruningSize)
if err != nil {
return err
return nil, err
}
logrus.SetOutput(rotator)
return setLevel(level)
return rotator, setLevel(level)
}
// Close closes the log file. if closer is nil, no error is reported.
func Close(closer io.Closer) error {
if closer == nil {
return nil
}
logrus.SetOutput(os.Stdout)
return closer.Close()
}
// ZipLogsForBugReport returns an archive containing the logs for bug report.
func ZipLogsForBugReport(logsPath string, maxSessionCount int, maxZipSize int64) (*bytes.Buffer, error) {
paths, err := getOrderedLogFileListForBugReport(logsPath, maxSessionCount)
if err != nil {
return nil, err
}
buffer, _, err := zipFilesWithMaxSize(paths, maxZipSize)
return buffer, err
}
// getOrderedLogFileListForBugReport returns the ordered list of log file paths to include in the user triggered bug reports. Only the last
// maxSessionCount sessions are included. Priorities:
// - session in chronologically descending order.
// - for each session: last 2 bridge logs, first bridge log, gui logs, launcher logs, all other bridge logs.
func getOrderedLogFileListForBugReport(logsPath string, maxSessionCount int) ([]string, error) {
sessionInfoList, err := buildSessionInfoList(logsPath)
if err != nil {
return nil, err
}
sortedSessions := maps.Values(sessionInfoList)
slices.SortFunc(sortedSessions, func(lhs, rhs *sessionInfo) bool { return lhs.sessionID > rhs.sessionID })
count := len(sortedSessions)
if count > maxSessionCount {
sortedSessions = sortedSessions[:maxSessionCount]
}
filePathFunc := func(logFileInfo logFileInfo) string { return filepath.Join(logsPath, logFileInfo.filename) }
var result []string
for _, session := range sortedSessions {
bridgeLogCount := len(session.bridgeLogs)
if bridgeLogCount > 0 {
result = append(result, filepath.Join(logsPath, session.bridgeLogs[bridgeLogCount-1].filename))
}
if bridgeLogCount > 1 {
result = append(result, filepath.Join(logsPath, session.bridgeLogs[bridgeLogCount-2].filename))
}
if bridgeLogCount > 2 {
result = append(result, filepath.Join(logsPath, session.bridgeLogs[0].filename))
}
if len(session.guiLogs) > 0 {
result = append(result, xslices.Map(session.guiLogs, filePathFunc)...)
}
if len(session.launcherLogs) > 0 {
result = append(result, xslices.Map(session.launcherLogs, filePathFunc)...)
}
if bridgeLogCount > 3 {
result = append(result, xslices.Map(session.bridgeLogs[1:bridgeLogCount-2], filePathFunc)...)
}
}
return result, nil
}
// setLevel will change the level of logging and in case of Debug or Trace
@ -137,34 +204,51 @@ func setLevel(level string) error {
return nil
}
func getLogName(version, revision string) string {
return fmt.Sprintf("v%v_%v_%v.log", version, revision, time.Now().Unix())
}
func getLogSessionID(filename string) (SessionID, error) {
re := regexp.MustCompile(`^(?P<sessionID>\d{8}_\d{9})_.*\.log$`)
func getLogTime(name string) int {
re := regexp.MustCompile(`^v.*_.*_(?P<timestamp>\d+).log$`)
match := re.FindStringSubmatch(name)
match := re.FindStringSubmatch(filename)
errInvalidFileName := errors.New("log file name is invalid")
if len(match) == 0 {
logrus.Warn("Could not parse log name: ", name)
return 0
logrus.WithField("filename", filename).Warn("Could not parse log filename")
return "", errInvalidFileName
}
timestamp, err := strconv.Atoi(match[re.SubexpIndex("timestamp")])
index := re.SubexpIndex("sessionID")
if index < 0 {
logrus.WithField("filename", filename).Warn("Could not parse log filename")
return "", errInvalidFileName
}
return SessionID(match[index]), nil
}
func getLogTime(filename string) time.Time {
sessionID, err := getLogSessionID(filename)
if err != nil {
return 0
return time.Time{}
}
return timestamp
return sessionID.toTime()
}
func MatchLogName(name string) bool {
return regexp.MustCompile(`^v.*\.log$`).MatchString(name)
// MatchBridgeLogName return true iff filename is a bridge log filename.
func MatchBridgeLogName(filename string) bool {
return matchLogName(filename, BridgeShortAppName)
}
func MatchGUILogName(name string) bool {
return regexp.MustCompile(`^gui_v.*\.log$`).MatchString(name)
// MatchGUILogName return true iff filename is a bridge-gui log filename.
func MatchGUILogName(filename string) bool {
return matchLogName(filename, GUIShortAppName)
}
// MatchLauncherLogName return true iff filename is a launcher log filename.
func MatchLauncherLogName(filename string) bool {
return matchLogName(filename, LauncherShortAppName)
}
func matchLogName(logName string, appName AppName) bool {
return regexp.MustCompile(`^\d{8}_\d{9}_\Q` + string(appName) + `\E_\d{3}_.*\.log$`).MatchString(logName)
}
type logKey string
@ -180,12 +264,3 @@ func WithLogrusField(ctx context.Context, key string, value interface{}) context
fields[key] = value
return context.WithValue(ctx, logrusFields, fields)
}
func LogFromContext(ctx context.Context) *logrus.Entry {
fields, ok := ctx.Value(logrusFields).(logrus.Fields)
if !ok || fields == nil {
return logrus.WithField("ctx", "empty")
}
return logrus.WithFields(fields)
}

View File

@ -22,62 +22,84 @@ import (
"path/filepath"
"testing"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"
)
// TestClearLogs tests that cearLogs removes only bridge old log files keeping last three of them.
func TestClearLogs(t *testing.T) {
func TestLogging_GetLogTime(t *testing.T) {
sessionID := NewSessionID()
fp := defaultFileProvider(os.TempDir(), sessionID, "bridge-test")
wc, err := fp(0)
require.NoError(t, err)
file, ok := wc.(*os.File)
require.True(t, ok)
sessionIDTime := sessionID.toTime()
require.False(t, sessionIDTime.IsZero())
logTime := getLogTime(filepath.Base(file.Name()))
require.False(t, logTime.IsZero())
require.Equal(t, sessionIDTime, logTime)
}
func TestLogging_MatchLogName(t *testing.T) {
bridgeLog := "20230602_094633102_bri_000_v3.0.99+git_5b650b1be3.log"
crashLog := "20230602_094633102_bri_000_v3.0.99+git_5b650b1be3_crash.log"
guiLog := "20230602_094633102_gui_000_v3.0.99+git_5b650b1be3.log"
launcherLog := "20230602_094633102_lau_000_v3.0.99+git_5b650b1be3.log"
require.True(t, MatchBridgeLogName(bridgeLog))
require.False(t, MatchGUILogName(bridgeLog))
require.False(t, MatchLauncherLogName(bridgeLog))
require.True(t, MatchBridgeLogName(crashLog))
require.False(t, MatchGUILogName(crashLog))
require.False(t, MatchLauncherLogName(crashLog))
require.False(t, MatchBridgeLogName(guiLog))
require.True(t, MatchGUILogName(guiLog))
require.False(t, MatchLauncherLogName(guiLog))
require.False(t, MatchBridgeLogName(launcherLog))
require.False(t, MatchGUILogName(launcherLog))
require.True(t, MatchLauncherLogName(launcherLog))
}
func TestLogging_GetOrderedLogFileListForBugReport(t *testing.T) {
dir := t.TempDir()
// Create some old log files.
require.NoError(t, os.WriteFile(filepath.Join(dir, "other.log"), []byte("Hello"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.4.7_debe87f2f5_0000000001.log"), []byte("Hello"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.4.8_debe87f2f5_0000000002.log"), []byte("Hello"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.4.9_debe87f2f5_0000000003.log"), []byte("Hello"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.0_debe87f2f5_0000000004.log"), []byte("Hello"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.1_debe87f2f5_0000000005.log"), []byte("Hello"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.2_debe87f2f5_0000000006.log"), []byte("Hello"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.3_debe87f2f5_0000000007.log"), []byte("Hello"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.4_debe87f2f5_0000000008.log"), []byte("Hello"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.5_debe87f2f5_0000000009.log"), []byte("Hello"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.6_debe87f2f5_0000000010.log"), []byte("Hello"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.7_debe87f2f5_0000000011.log"), []byte("Hello"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.8_debe87f2f5_0000000012.log"), []byte("Hello"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.12_debe87f2f5_0000000013.log"), []byte("Hello"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.9_debe87f2f5_0000000014.log"), []byte("Hello"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.10_debe87f2f5_0000000015.log"), []byte("Hello"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(dir, "v2.5.11_debe87f2f5_0000000016.log"), []byte("Hello"), 0o755))
// Clear the logs.
require.NoError(t, clearLogs(dir, 3, 0))
// We should only clear matching files, and keep the 3 most recent ones.
checkFileNames(t, dir, []string{
"other.log",
"v2.5.9_debe87f2f5_0000000014.log",
"v2.5.10_debe87f2f5_0000000015.log",
"v2.5.11_debe87f2f5_0000000016.log",
})
}
func checkFileNames(t *testing.T, dir string, expectedFileNames []string) {
require.ElementsMatch(t, expectedFileNames, getFileNames(t, dir))
}
func getFileNames(t *testing.T, dir string) []string {
files, err := os.ReadDir(dir)
filePaths, err := getOrderedLogFileListForBugReport(dir, 3)
require.NoError(t, err)
require.True(t, len(filePaths) == 0)
fileNames := []string{}
for _, file := range files {
fileNames = append(fileNames, file.Name())
if file.IsDir() {
subDir := filepath.Join(dir, file.Name())
subFileNames := getFileNames(t, subDir)
for _, subFileName := range subFileNames {
fileNames = append(fileNames, file.Name()+"/"+subFileName)
}
}
}
return fileNames
require.NoError(t, os.WriteFile(filepath.Join(dir, "invalid.log"), []byte("proton"), 0660))
_ = createDummySession(t, dir, 1000, 250, 500, 3000)
sessionID1 := createDummySession(t, dir, 1000, 250, 500, 500)
sessionID2 := createDummySession(t, dir, 1000, 250, 500, 500)
sessionID3 := createDummySession(t, dir, 1000, 250, 500, 4500)
filePaths, err = getOrderedLogFileListForBugReport(dir, 3)
fileSuffix := "_v" + constants.Version + "_" + constants.Tag + ".log"
require.NoError(t, err)
require.EqualValues(t, []string{
filepath.Join(dir, string(sessionID3)+"_bri_004"+fileSuffix),
filepath.Join(dir, string(sessionID3)+"_bri_003"+fileSuffix),
filepath.Join(dir, string(sessionID3)+"_bri_000"+fileSuffix),
filepath.Join(dir, string(sessionID3)+"_gui_000"+fileSuffix),
filepath.Join(dir, string(sessionID3)+"_lau_000"+fileSuffix),
filepath.Join(dir, string(sessionID3)+"_bri_001"+fileSuffix),
filepath.Join(dir, string(sessionID3)+"_bri_002"+fileSuffix),
filepath.Join(dir, string(sessionID2)+"_bri_000"+fileSuffix),
filepath.Join(dir, string(sessionID2)+"_gui_000"+fileSuffix),
filepath.Join(dir, string(sessionID2)+"_lau_000"+fileSuffix),
filepath.Join(dir, string(sessionID1)+"_bri_000"+fileSuffix),
filepath.Join(dir, string(sessionID1)+"_gui_000"+fileSuffix),
filepath.Join(dir, string(sessionID1)+"_lau_000"+fileSuffix),
}, filePaths)
}
func TestLogging_Close(t *testing.T) {
d := t.TempDir()
closer, err := Init(d, NewSessionID(), constants.AppName, 1, DefaultPruningSize, "debug")
require.NoError(t, err)
logrus.Debug("Test") // because we set max log file size to 1, this will force a rotation of the log file.
require.NotNil(t, closer)
require.NoError(t, closer.Close())
}

230
internal/logging/pruning.go Normal file
View File

@ -0,0 +1,230 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package logging
import (
"os"
"path/filepath"
"regexp"
"strings"
"github.com/bradenaw/juniper/xslices"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
)
const (
DefaultPruningSize = 1024 * 1024 * 200
NoPruning = -1
)
type Pruner func() (failureCount int, err error)
type logFileInfo struct {
filename string
size int64
}
type sessionInfo struct {
dir string
sessionID SessionID
launcherLogs []logFileInfo
guiLogs []logFileInfo
bridgeLogs []logFileInfo
}
func defaultPruner(logsDir string, currentSessionID SessionID, pruningSize int64) func() (failureCount int, err error) {
return func() (int, error) {
return pruneLogs(logsDir, currentSessionID, pruningSize)
}
}
func nullPruner() (failureCount int, err error) {
return 0, nil
}
// DefaultPruner gets rid of the older log files according to the following policy:
// - We will limit the total size of the log files to roughly pruningSize.
// The current session is included in this quota, in order not to grow indefinitely on setups where bridge can run uninterrupted for months.
// - If the current session's log files total size is above the pruning size, we delete all other sessions log. For the current we keep
// launcher and gui log (they're limited to a few kb at most by nature), and we have n bridge log files,
// We keep the first and list log file (startup information contains relevant information, notably the SentryID), and start deleting the other
// starting with the oldest until the total size drops below the pruning size.
// - Otherwise: If the total size of log files for all sessions exceeds pruningSize, sessions gets deleted starting with the oldest, until the size
// drops below the pruning size. Sessions are treated atomically. Current session is left untouched in that case.
func pruneLogs(logDir string, currentSessionID SessionID, pruningSize int64) (failureCount int, err error) {
sessionInfoList, err := buildSessionInfoList(logDir)
if err != nil {
return 0, err
}
// we want total size to include the current session.
totalSize := xslices.Reduce(maps.Values(sessionInfoList), int64(0), func(sum int64, info *sessionInfo) int64 { return sum + info.size() })
if totalSize <= pruningSize {
return 0, nil
}
currentSessionInfo, ok := sessionInfoList[currentSessionID]
if ok {
delete(sessionInfoList, currentSessionID)
if currentSessionInfo.size() > pruningSize {
// current session is already too big. We delete all other sessions and prune the current session.
for _, session := range sessionInfoList {
failureCount += session.deleteFiles()
}
failureCount += currentSessionInfo.pruneAsCurrentSession(pruningSize)
return failureCount, nil
}
}
// current session size if below max size, so we erase older session starting with the eldest until we go below maxFileSize
sortedSessions := maps.Values(sessionInfoList)
slices.SortFunc(sortedSessions, func(lhs, rhs *sessionInfo) bool { return lhs.sessionID < rhs.sessionID })
for _, sessionInfo := range sortedSessions {
totalSize -= sessionInfo.size()
failureCount += sessionInfo.deleteFiles()
if totalSize <= pruningSize {
return failureCount, nil
}
}
return failureCount, nil
}
func newSessionInfo(dir string, sessionID SessionID) (*sessionInfo, error) {
paths, err := filepath.Glob(filepath.Join(dir, string(sessionID)+"_*.log"))
if err != nil {
return nil, err
}
rx := regexp.MustCompile(`^\Q` + string(sessionID) + `\E_([^_]*)_\d+_.*\.log$`)
result := sessionInfo{sessionID: sessionID, dir: dir}
for _, path := range paths {
filename := filepath.Base(path)
match := rx.FindStringSubmatch(filename)
if len(match) != 2 {
continue
}
stats, err := os.Stat(path)
if err != nil {
continue
}
fileInfo := logFileInfo{
filename: filename,
size: stats.Size(),
}
switch AppName(match[1]) {
case LauncherShortAppName:
result.launcherLogs = append(result.launcherLogs, fileInfo)
case GUIShortAppName:
result.guiLogs = append(result.guiLogs, fileInfo)
case BridgeShortAppName:
result.bridgeLogs = append(result.bridgeLogs, fileInfo)
}
}
lessFunc := func(lhs, rhs logFileInfo) bool { return strings.Compare(lhs.filename, rhs.filename) < 0 }
slices.SortFunc(result.launcherLogs, lessFunc)
slices.SortFunc(result.guiLogs, lessFunc)
slices.SortFunc(result.bridgeLogs, lessFunc)
return &result, nil
}
func (s *sessionInfo) size() int64 {
summer := func(accum int64, logInfo logFileInfo) int64 { return accum + logInfo.size }
size := xslices.Reduce(s.launcherLogs, 0, summer)
size = xslices.Reduce(s.guiLogs, size, summer)
size = xslices.Reduce(s.bridgeLogs, size, summer)
return size
}
func (s *sessionInfo) deleteFiles() (failureCount int) {
var allLogs []logFileInfo
allLogs = append(allLogs, s.launcherLogs...)
allLogs = append(allLogs, s.guiLogs...)
allLogs = append(allLogs, s.bridgeLogs...)
for _, log := range allLogs {
if err := os.Remove(filepath.Join(s.dir, log.filename)); err != nil {
failureCount++
}
}
return failureCount
}
func (s *sessionInfo) pruneAsCurrentSession(pruningSize int64) (failureCount int) {
// when pruning the current session, we keep the launcher and GUI logs, the first and last bridge log file
// and we delete intermediate bridge logs until the size constraint is satisfied (or there nothing left to delete).
if len(s.bridgeLogs) < 3 {
return 0
}
size := s.size()
if size <= pruningSize {
return 0
}
for _, fileInfo := range s.bridgeLogs[1 : len(s.bridgeLogs)-1] {
if err := os.Remove(filepath.Join(s.dir, fileInfo.filename)); err != nil {
failureCount++
}
size -= fileInfo.size
if size <= pruningSize {
return failureCount
}
}
return failureCount
}
func buildSessionInfoList(dir string) (map[SessionID]*sessionInfo, error) {
result := make(map[SessionID]*sessionInfo)
entries, err := os.ReadDir(dir)
if err != nil {
return nil, err
}
for _, entry := range entries {
if entry.IsDir() {
continue
}
rx := regexp.MustCompile(`^(\d{8}_\d{9})_.*\.log$`)
match := rx.FindStringSubmatch(entry.Name())
if match == nil || len(match) < 2 {
continue
}
sessionID := SessionID(match[1])
if _, ok := result[sessionID]; !ok {
sessionInfo, err := newSessionInfo(dir, sessionID)
if err != nil {
return nil, err
}
result[sessionID] = sessionInfo
}
}
return result, nil
}

View File

@ -0,0 +1,236 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package logging
import (
"os"
"path/filepath"
"testing"
"time"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/stretchr/testify/require"
)
type fileInfo struct {
filename string
size int64
}
var logFileSuffix = "_v" + constants.Version + "_" + constants.Tag + ".log"
func TestLogging_Pruning(t *testing.T) {
dir := t.TempDir()
const maxLogSize = 100
sessionID1 := createDummySession(t, dir, maxLogSize, 50, 50, 250)
sessionID2 := createDummySession(t, dir, maxLogSize, 100, 100, 350)
sessionID3 := createDummySession(t, dir, maxLogSize, 150, 100, 350)
// Expected files per session
session1Files := []fileInfo{
{filename: string(sessionID1) + "_lau_000" + logFileSuffix, size: 50},
{filename: string(sessionID1) + "_gui_000" + logFileSuffix, size: 50},
{filename: string(sessionID1) + "_bri_000" + logFileSuffix, size: 100},
{filename: string(sessionID1) + "_bri_001" + logFileSuffix, size: 100},
{filename: string(sessionID1) + "_bri_002" + logFileSuffix, size: 50},
}
session2Files := []fileInfo{
{filename: string(sessionID2) + "_lau_000" + logFileSuffix, size: 100},
{filename: string(sessionID2) + "_gui_000" + logFileSuffix, size: 100},
{filename: string(sessionID2) + "_bri_000" + logFileSuffix, size: 100},
{filename: string(sessionID2) + "_bri_001" + logFileSuffix, size: 100},
{filename: string(sessionID2) + "_bri_002" + logFileSuffix, size: 100},
{filename: string(sessionID2) + "_bri_003" + logFileSuffix, size: 50},
}
session3Files := []fileInfo{
{filename: string(sessionID3) + "_lau_000" + logFileSuffix, size: 100},
{filename: string(sessionID3) + "_lau_001" + logFileSuffix, size: 50},
{filename: string(sessionID3) + "_gui_000" + logFileSuffix, size: 100},
{filename: string(sessionID3) + "_bri_000" + logFileSuffix, size: 100},
{filename: string(sessionID3) + "_bri_001" + logFileSuffix, size: 100},
{filename: string(sessionID3) + "_bri_002" + logFileSuffix, size: 100},
{filename: string(sessionID3) + "_bri_003" + logFileSuffix, size: 50},
}
allSessions := session1Files
allSessions = append(allSessions, append(session2Files, session3Files...)...)
checkFolderContent(t, dir, allSessions...)
failureCount, err := pruneLogs(dir, sessionID3, 2000) // nothing to prune
require.Equal(t, failureCount, 0)
require.NoError(t, err)
checkFolderContent(t, dir, allSessions...)
failureCount, err = pruneLogs(dir, sessionID3, 1200) // session 1 is pruned
require.Equal(t, failureCount, 0)
require.NoError(t, err)
checkFolderContent(t, dir, append(session2Files, session3Files...)...)
failureCount, err = pruneLogs(dir, sessionID3, 1000) // session 2 is pruned
require.Equal(t, failureCount, 0)
require.NoError(t, err)
checkFolderContent(t, dir, session3Files...)
}
func TestLogging_PruningBigCurrentSession(t *testing.T) {
dir := t.TempDir()
const maxLogFileSize = 1000
sessionID1 := createDummySession(t, dir, maxLogFileSize, 500, 500, 2500)
sessionID2 := createDummySession(t, dir, maxLogFileSize, 1000, 1000, 3500)
sessionID3 := createDummySession(t, dir, maxLogFileSize, 500, 500, 10500)
// Expected files per session
session1Files := []fileInfo{
{filename: string(sessionID1) + "_lau_000" + logFileSuffix, size: 500},
{filename: string(sessionID1) + "_gui_000" + logFileSuffix, size: 500},
{filename: string(sessionID1) + "_bri_000" + logFileSuffix, size: 1000},
{filename: string(sessionID1) + "_bri_001" + logFileSuffix, size: 1000},
{filename: string(sessionID1) + "_bri_002" + logFileSuffix, size: 500},
}
session2Files := []fileInfo{
{filename: string(sessionID2) + "_lau_000" + logFileSuffix, size: 1000},
{filename: string(sessionID2) + "_gui_000" + logFileSuffix, size: 1000},
{filename: string(sessionID2) + "_bri_000" + logFileSuffix, size: 1000},
{filename: string(sessionID2) + "_bri_001" + logFileSuffix, size: 1000},
{filename: string(sessionID2) + "_bri_002" + logFileSuffix, size: 1000},
{filename: string(sessionID2) + "_bri_003" + logFileSuffix, size: 500},
}
session3Files := []fileInfo{
{filename: string(sessionID3) + "_lau_000" + logFileSuffix, size: 500},
{filename: string(sessionID3) + "_gui_000" + logFileSuffix, size: 500},
{filename: string(sessionID3) + "_bri_000" + logFileSuffix, size: 1000},
{filename: string(sessionID3) + "_bri_001" + logFileSuffix, size: 1000},
{filename: string(sessionID3) + "_bri_002" + logFileSuffix, size: 1000},
{filename: string(sessionID3) + "_bri_003" + logFileSuffix, size: 1000},
{filename: string(sessionID3) + "_bri_004" + logFileSuffix, size: 1000},
{filename: string(sessionID3) + "_bri_005" + logFileSuffix, size: 1000},
{filename: string(sessionID3) + "_bri_006" + logFileSuffix, size: 1000},
{filename: string(sessionID3) + "_bri_007" + logFileSuffix, size: 1000},
{filename: string(sessionID3) + "_bri_008" + logFileSuffix, size: 1000},
{filename: string(sessionID3) + "_bri_009" + logFileSuffix, size: 1000},
{filename: string(sessionID3) + "_bri_010" + logFileSuffix, size: 500},
}
allSessions := session1Files
allSessions = append(allSessions, append(session2Files, session3Files...)...)
checkFolderContent(t, dir, allSessions...)
// current session is bigger than maxFileSize. We keep launcher and gui logs, the first and last bridge log
// and only the last bridge log that keep the total file size under the limit.
failureCount, err := pruneLogs(dir, sessionID3, 8000)
require.Equal(t, failureCount, 0)
require.NoError(t, err)
checkFolderContent(t, dir, []fileInfo{
{filename: string(sessionID3) + "_lau_000" + logFileSuffix, size: 500},
{filename: string(sessionID3) + "_gui_000" + logFileSuffix, size: 500},
{filename: string(sessionID3) + "_bri_000" + logFileSuffix, size: 1000},
{filename: string(sessionID3) + "_bri_005" + logFileSuffix, size: 1000},
{filename: string(sessionID3) + "_bri_006" + logFileSuffix, size: 1000},
{filename: string(sessionID3) + "_bri_007" + logFileSuffix, size: 1000},
{filename: string(sessionID3) + "_bri_008" + logFileSuffix, size: 1000},
{filename: string(sessionID3) + "_bri_009" + logFileSuffix, size: 1000},
{filename: string(sessionID3) + "_bri_010" + logFileSuffix, size: 500},
}...)
failureCount, err = pruneLogs(dir, sessionID3, 5000)
require.Equal(t, failureCount, 0)
require.NoError(t, err)
checkFolderContent(t, dir, []fileInfo{
{filename: string(sessionID3) + "_lau_000" + logFileSuffix, size: 500},
{filename: string(sessionID3) + "_gui_000" + logFileSuffix, size: 500},
{filename: string(sessionID3) + "_bri_000" + logFileSuffix, size: 1000},
{filename: string(sessionID3) + "_bri_008" + logFileSuffix, size: 1000},
{filename: string(sessionID3) + "_bri_009" + logFileSuffix, size: 1000},
{filename: string(sessionID3) + "_bri_010" + logFileSuffix, size: 500},
}...)
// whatever maxFileSize is, we will always keep the following files
minimalFiles := []fileInfo{
{filename: string(sessionID3) + "_lau_000" + logFileSuffix, size: 500},
{filename: string(sessionID3) + "_gui_000" + logFileSuffix, size: 500},
{filename: string(sessionID3) + "_bri_000" + logFileSuffix, size: 1000},
{filename: string(sessionID3) + "_bri_010" + logFileSuffix, size: 500},
}
failureCount, err = pruneLogs(dir, sessionID3, 2000)
require.Equal(t, failureCount, 0)
require.NoError(t, err)
checkFolderContent(t, dir, minimalFiles...)
failureCount, err = pruneLogs(dir, sessionID3, 0)
require.Equal(t, failureCount, 0)
require.NoError(t, err)
checkFolderContent(t, dir, minimalFiles...)
}
func createDummySession(t *testing.T, dir string, maxLogFileSize int64, launcherLogSize, guiLogSize, bridgeLogSize int64) SessionID {
time.Sleep(2 * time.Millisecond) // ensure our sessionID is unused.
sessionID := NewSessionID()
if launcherLogSize > 0 {
createDummyRotatedLogFile(t, dir, sessionID, LauncherShortAppName, launcherLogSize, maxLogFileSize)
}
if guiLogSize > 0 {
createDummyRotatedLogFile(t, dir, sessionID, GUIShortAppName, guiLogSize, maxLogFileSize)
}
if bridgeLogSize > 0 {
createDummyRotatedLogFile(t, dir, sessionID, BridgeShortAppName, bridgeLogSize, maxLogFileSize)
}
return sessionID
}
func createDummyRotatedLogFile(t *testing.T, dir string, sessionID SessionID, appName AppName, totalSize, maxLogFileSize int64) {
rotator, err := NewDefaultRotator(dir, sessionID, appName, maxLogFileSize, NoPruning)
require.NoError(t, err)
for i := int64(0); i < totalSize/maxLogFileSize; i++ {
count, err := rotator.Write(make([]byte, maxLogFileSize))
require.NoError(t, err)
require.Equal(t, int64(count), maxLogFileSize)
}
remainder := totalSize % maxLogFileSize
if remainder > 0 {
count, err := rotator.Write(make([]byte, remainder))
require.NoError(t, err)
require.Equal(t, int64(count), remainder)
}
require.NoError(t, rotator.wc.Close())
}
func checkFolderContent(t *testing.T, dir string, fileInfos ...fileInfo) {
for _, fi := range fileInfos {
checkFileExistsWithSize(t, dir, fi)
}
entries, err := os.ReadDir(dir)
require.NoError(t, err)
require.Equal(t, len(fileInfos), len(entries))
}
func checkFileExistsWithSize(t *testing.T, dir string, info fileInfo) {
stat, err := os.Stat(filepath.Join(dir, info.filename))
require.NoError(t, err)
require.Equal(t, stat.Size(), info.size)
}

View File

@ -17,21 +17,39 @@
package logging
import "io"
import (
"fmt"
"io"
"os"
"path/filepath"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
)
type Rotator struct {
getFile FileProvider
wc io.WriteCloser
size int
maxSize int
getFile FileProvider
prune Pruner
wc io.WriteCloser
size int64
maxFileSize int64
nextIndex int
}
type FileProvider func() (io.WriteCloser, error)
type FileProvider func(index int) (io.WriteCloser, error)
func NewRotator(maxSize int, getFile FileProvider) (*Rotator, error) {
func defaultFileProvider(logsPath string, sessionID SessionID, appName AppName) FileProvider {
return func(index int) (io.WriteCloser, error) {
return os.Create(filepath.Join(logsPath, //nolint:gosec // G304
fmt.Sprintf("%v_%v_%03d_v%v_%v.log", sessionID, appName, index, constants.Version, constants.Tag),
))
}
}
func NewRotator(maxFileSize int64, getFile FileProvider, prune Pruner) (*Rotator, error) {
r := &Rotator{
getFile: getFile,
maxSize: maxSize,
getFile: getFile,
prune: prune,
maxFileSize: maxFileSize,
}
if err := r.rotate(); err != nil {
@ -41,8 +59,19 @@ func NewRotator(maxSize int, getFile FileProvider) (*Rotator, error) {
return r, nil
}
func NewDefaultRotator(logsPath string, sessionID SessionID, appName AppName, maxLogFileSize, pruningSize int64) (*Rotator, error) {
var pruner Pruner
if pruningSize < 0 {
pruner = nullPruner
} else {
pruner = defaultPruner(logsPath, sessionID, pruningSize)
}
return NewRotator(maxLogFileSize, defaultFileProvider(logsPath, sessionID, appName), pruner)
}
func (r *Rotator) Write(p []byte) (int, error) {
if r.size+len(p) > r.maxSize {
if r.size+int64(len(p)) > r.maxFileSize {
if err := r.rotate(); err != nil {
return 0, err
}
@ -53,21 +82,33 @@ func (r *Rotator) Write(p []byte) (int, error) {
return n, err
}
r.size += n
r.size += int64(n)
return n, nil
}
func (r *Rotator) Close() error {
if r.wc != nil {
return r.wc.Close()
}
return nil
}
func (r *Rotator) rotate() error {
if r.wc != nil {
_ = r.wc.Close()
}
wc, err := r.getFile()
if _, err := r.prune(); err != nil {
return err
}
wc, err := r.getFile(r.nextIndex)
if err != nil {
return err
}
r.nextIndex++
r.wc = wc
r.size = 0

View File

@ -19,8 +19,10 @@ package logging
import (
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
@ -35,15 +37,15 @@ func (c *WriteCloser) Close() error {
return nil
}
func TestRotator(t *testing.T) {
func TestLogging_Rotator(t *testing.T) {
n := 0
getFile := func() (io.WriteCloser, error) {
getFile := func(_ int) (io.WriteCloser, error) {
n++
return &WriteCloser{}, nil
}
r, err := NewRotator(10, getFile)
r, err := NewRotator(10, getFile, nullPruner)
require.NoError(t, err)
_, err = r.Write([]byte("12345"))
@ -75,12 +77,89 @@ func TestRotator(t *testing.T) {
assert.Equal(t, 4, n)
}
func BenchmarkRotate(b *testing.B) {
benchRotate(b, MaxLogSize, getTestFile(b, b.TempDir(), MaxLogSize-1))
func TestLogging_DefaultRotator(t *testing.T) {
fiveBytes := []byte("00000")
tmpDir := os.TempDir()
sessionID := NewSessionID()
basePath := filepath.Join(tmpDir, string(sessionID))
r, err := NewDefaultRotator(tmpDir, sessionID, "bri", 10, NoPruning)
require.NoError(t, err)
require.Equal(t, 1, countFilesMatching(basePath+"_bri_000_*.log"))
require.Equal(t, 1, countFilesMatching(basePath+"*.log"))
_, err = r.Write(fiveBytes)
require.NoError(t, err)
require.Equal(t, 1, countFilesMatching(basePath+"*.log"))
_, err = r.Write(fiveBytes)
require.NoError(t, err)
require.Equal(t, 1, countFilesMatching(basePath+"*.log"))
_, err = r.Write(fiveBytes)
require.NoError(t, err)
require.Equal(t, 2, countFilesMatching(basePath+"*.log"))
require.Equal(t, 1, countFilesMatching(basePath+"_bri_001_*.log"))
for i := 0; i < 4; i++ {
_, err = r.Write(fiveBytes)
require.NoError(t, err)
}
require.NoError(t, r.wc.Close())
// total written: 35 bytes, i.e. 4 log files
logFileCount := countFilesMatching(basePath + "*.log")
require.Equal(t, 4, logFileCount)
for i := 0; i < logFileCount; i++ {
require.Equal(t, 1, countFilesMatching(basePath+fmt.Sprintf("_bri_%03d_*.log", i)))
}
cleanupLogs(t, sessionID)
}
func benchRotate(b *testing.B, logSize int, getFile func() (io.WriteCloser, error)) {
r, err := NewRotator(logSize, getFile)
func TestLogging_DefaultRotatorWithPruning(t *testing.T) {
tenBytes := []byte("0000000000")
tmpDir := t.TempDir()
sessionID := NewSessionID()
basePath := filepath.Join(tmpDir, string(sessionID))
// fill the log dir while below the pruning quota
r, err := NewDefaultRotator(tmpDir, sessionID, "bri", 10, 40)
require.NoError(t, err)
for i := 0; i < 4; i++ {
_, err = r.Write(tenBytes)
require.NoError(t, err)
}
// from now on at every rotation, (i.e. every write in this case), we will prune, then create a new file.
// we should always have 4 files, remaining after prune, plus the newly rotated file with the last written bytes.
for i := 0; i < 10; i++ {
_, err := r.Write(tenBytes)
require.NoError(t, err)
require.Equal(t, 5, countFilesMatching(basePath+"_bri_*.log"))
}
require.NoError(t, r.wc.Close())
// Final check. 000, 010, 011, 012 are what's left after the last pruning, 013 never got to pass through pruning.
checkFolderContent(t, tmpDir, []fileInfo{
{filename: string(sessionID) + "_bri_000" + logFileSuffix, size: 10},
{filename: string(sessionID) + "_bri_010" + logFileSuffix, size: 10},
{filename: string(sessionID) + "_bri_011" + logFileSuffix, size: 10},
{filename: string(sessionID) + "_bri_012" + logFileSuffix, size: 10},
{filename: string(sessionID) + "_bri_013" + logFileSuffix, size: 10},
}...)
}
func BenchmarkRotate(b *testing.B) {
benchRotate(b, DefaultMaxLogFileSize, getTestFile(b, b.TempDir(), DefaultMaxLogFileSize-1))
}
func benchRotate(b *testing.B, logSize int64, getFile func(index int) (io.WriteCloser, error)) {
r, err := NewRotator(logSize, getFile, nullPruner)
require.NoError(b, err)
for n := 0; n < b.N; n++ {
@ -92,8 +171,8 @@ func benchRotate(b *testing.B, logSize int, getFile func() (io.WriteCloser, erro
}
}
func getTestFile(b *testing.B, dir string, length int) func() (io.WriteCloser, error) {
return func() (io.WriteCloser, error) {
func getTestFile(b *testing.B, dir string, length int) func(int) (io.WriteCloser, error) {
return func(index int) (io.WriteCloser, error) {
b.StopTimer()
defer b.StartTimer()
@ -113,3 +192,20 @@ func getTestFile(b *testing.B, dir string, length int) func() (io.WriteCloser, e
return f, nil
}
}
func countFilesMatching(pattern string) int {
files, err := filepath.Glob(pattern)
if err != nil {
return -1
}
return len(files)
}
func cleanupLogs(t *testing.T, sessionID SessionID) {
paths, err := filepath.Glob(filepath.Join(os.TempDir(), string(sessionID)+"*.log"))
require.NoError(t, err)
for _, path := range paths {
require.NoError(t, os.Remove(path))
}
}

View File

@ -0,0 +1,64 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
package logging
import (
"fmt"
"strconv"
"time"
)
type SessionID string
const (
timeFormat = "20060102_150405" // time format in Go does not support milliseconds without dot, so we'll process it manually.
)
// NewSessionID creates a sessionID based on the current time.
func NewSessionID() SessionID {
now := time.Now()
return SessionID(now.Format(timeFormat) + fmt.Sprintf("%03d", now.Nanosecond()/1000000))
}
// NewSessionIDFromString Return a new sessionID from string. If the str is empty a new time based sessionID is returned, otherwise the string
// is used as the sessionID.
func NewSessionIDFromString(str string) SessionID {
if (len(str)) > 0 {
return SessionID(str)
}
return NewSessionID()
}
// toTime converts a sessionID to a date/Time, considering the time zone is local.
func (s SessionID) toTime() time.Time {
if len(s) < 3 {
return time.Time{}
}
t, err := time.ParseInLocation(timeFormat, string(s)[:len(s)-3], time.Local)
if err != nil {
return time.Time{}
}
var ms int
if ms, err = strconv.Atoi(string(s)[len(s)-3:]); err != nil {
return time.Time{}
}
return t.Add(time.Duration(ms) * time.Millisecond)
}

View File

@ -0,0 +1,38 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
package logging
import (
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestLogging_SessionID(t *testing.T) {
now := time.Now()
sessionID := NewSessionID()
sessionTime := sessionID.toTime()
require.False(t, sessionTime.IsZero())
require.WithinRange(t, sessionTime, now.Add(-1*time.Millisecond), now.Add(1*time.Millisecond))
fromString := NewSessionIDFromString("")
require.True(t, len(fromString) > 0)
fromString = NewSessionIDFromString(string(sessionID))
require.True(t, len(fromString) > 0)
require.Equal(t, sessionID, fromString)
}

View File

@ -18,6 +18,7 @@
package telemetry
import (
"context"
"strconv"
"time"
@ -149,12 +150,12 @@ func (heartbeat *Heartbeat) SetPrevVersion(val string) {
heartbeat.metrics.Dimensions.PrevVersion = val
}
func (heartbeat *Heartbeat) TrySending() {
if heartbeat.manager.IsTelemetryAvailable() {
func (heartbeat *Heartbeat) TrySending(ctx context.Context) {
if heartbeat.manager.IsTelemetryAvailable(ctx) {
lastSent := heartbeat.manager.GetLastHeartbeatSent()
now := time.Now()
if now.Year() > lastSent.Year() || (now.Year() == lastSent.Year() && now.YearDay() > lastSent.YearDay()) {
if !heartbeat.manager.SendHeartbeat(&heartbeat.metrics) {
if !heartbeat.manager.SendHeartbeat(ctx, &heartbeat.metrics) {
heartbeat.log.WithFields(logrus.Fields{
"metrics": heartbeat.metrics,
}).Error("Failed to send heartbeat")

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