mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-10 12:46:46 +00:00
Compare commits
200 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f7f898cee | |||
| 8d53ee855b | |||
| ec0db47f32 | |||
| 6fcea2ad83 | |||
| 098f294cac | |||
| a6f5cc870c | |||
| 7330406752 | |||
| fd100eecc2 | |||
| a11559fe58 | |||
| 7d8e71c9ea | |||
| 8b80938e49 | |||
| 9bbeabcf50 | |||
| de5fd07a22 | |||
| ec92c918cd | |||
| 9f59e61b14 | |||
| 9756a7b51f | |||
| 579e996d3a | |||
| 7ba523393c | |||
| 0a0bcd7b90 | |||
| ee87e60239 | |||
| babb4412ae | |||
| 2166224e91 | |||
| 60df01eece | |||
| f1989193c0 | |||
| 4e7acd9091 | |||
| 3ca5d0af71 | |||
| 9425e091d8 | |||
| bb770f3871 | |||
| b1ad0ab6dc | |||
| b63b56960e | |||
| 2d6e0c66a5 | |||
| ed05823c78 | |||
| ec351330f1 | |||
| dd7c81ca4b | |||
| f71a89c265 | |||
| 31de358bfd | |||
| 212eac0925 | |||
| e36c2b3d6e | |||
| 8b33d56b59 | |||
| 48274ee178 | |||
| 4273405393 | |||
| 3a85de2f3c | |||
| a3aafabde3 | |||
| 30c1c14505 | |||
| 8164c41728 | |||
| 1820af5021 | |||
| b57ca1506d | |||
| 7eaf1655b2 | |||
| f627151d04 | |||
| 7c232b1331 | |||
| 7be46a4740 | |||
| b57c7abe92 | |||
| c86c428718 | |||
| ed6e17a0ab | |||
| c3454360fc | |||
| 182dab18a6 | |||
| 13c8a98389 | |||
| 05a2c9d254 | |||
| d926dd3806 | |||
| 7cc2f3361d | |||
| c496d6c71c | |||
| 7fc907a874 | |||
| b953468af2 | |||
| 9f4caa4948 | |||
| 86630ce137 | |||
| 2c9477d65c | |||
| 34c002ff68 | |||
| f03688ba72 | |||
| 8c0bb22de3 | |||
| 53c2cbcaee | |||
| 07339aff21 | |||
| 3ca56cfab3 | |||
| 59cf5e890b | |||
| 70950e0048 | |||
| 2781f06549 | |||
| febc994cec | |||
| 21ffde316d | |||
| 2aa4e7c9da | |||
| 9058544f5c | |||
| 227bbf1c03 | |||
| 667998c207 | |||
| 1d426e621c | |||
| 6e4dcdb93b | |||
| 20f35edc83 | |||
| 26cf684fb8 | |||
| 2a4cb6a916 | |||
| caa4a5cbdb | |||
| 91900a7942 | |||
| 04a7a81e27 | |||
| 53d5619c51 | |||
| f793b71569 | |||
| 8aec11a634 | |||
| 30651a531b | |||
| 91ab77dce9 | |||
| 69aa784d32 | |||
| 825031e052 | |||
| ee4a8939d5 | |||
| 89117bbd59 | |||
| 9e4310712c | |||
| 0b35b275d3 | |||
| d6acb0fb19 | |||
| 2db5a04e7a | |||
| d7bfee2414 | |||
| 4a6460f3ed | |||
| c6f1f159f3 | |||
| 82af4e01bc | |||
| 9ad5f74409 | |||
| 10cf153678 | |||
| 2b09796d1c | |||
| 5ba07db7e3 | |||
| ad0d4ebd36 | |||
| 9f3c14ab1e | |||
| 74cf5d422b | |||
| dcf694588c | |||
| a2b9fc3dee | |||
| 761c16d8cd | |||
| 810705ba01 | |||
| c15917aba4 | |||
| 51cbb91513 | |||
| f8bfbaf361 | |||
| 3e878058e7 | |||
| 065dcd4d47 | |||
| 00256fafe8 | |||
| 671db7c516 | |||
| e9f20aee7a | |||
| 8534da98ea | |||
| 265af2d299 | |||
| 89112baf96 | |||
| 5007d451c2 | |||
| 4775fb4b22 | |||
| a75a72a2b9 | |||
| 038eb6d243 | |||
| 2bd8f6938a | |||
| 889f3286b5 | |||
| 9dfdd07f7a | |||
| 0b796f4401 | |||
| a741ffb595 | |||
| cf8284a489 | |||
| 6a9f6a173a | |||
| 54c013012e | |||
| cac0cf35f6 | |||
| 4c24c004db | |||
| 571133f2ff | |||
| 0207fa04f1 | |||
| 98fdf45fa3 | |||
| 21b3a4bca3 | |||
| 7225fc31da | |||
| eca4810f19 | |||
| 249658c05b | |||
| da82d7a107 | |||
| 08dab2d115 | |||
| 13db1b0db8 | |||
| c1921a811b | |||
| 473be3d485 | |||
| 0823d393ed | |||
| 8b9265ad96 | |||
| 5f930c262c | |||
| afa95d4799 | |||
| 2b75fcf773 | |||
| cdff2ef792 | |||
| a740a8f962 | |||
| d1f1c390f6 | |||
| 1c88ce3cc0 | |||
| c4ef1a24c0 | |||
| a79fce907e | |||
| c9d496956c | |||
| 31dce41276 | |||
| c6576dfc4b | |||
| f3c5e300cd | |||
| 29072f0285 | |||
| 40aca0fe73 | |||
| f4a2fb9687 | |||
| ad65bdde9d | |||
| 34cd611a8b | |||
| d82b71de89 | |||
| 8894a982f2 | |||
| a74d1ce9ca | |||
| 2e832520e6 | |||
| 62285a141e | |||
| c3d5a0b8f8 | |||
| a36dbbf422 | |||
| e2c1f38ed3 | |||
| 5ec1da34b4 | |||
| 219400de8d | |||
| 8901d83c94 | |||
| 0c8d4e8dd8 | |||
| a955dcbaa9 | |||
| fbac5134ca | |||
| 45ec6b6e74 | |||
| 79c2523585 | |||
| f14ad8b3fa | |||
| 590fdacba3 | |||
| 342a2a5568 | |||
| e382687168 | |||
| 4577a40b1e | |||
| c0aacb7d62 | |||
| c8065c8092 | |||
| ce03bfbf0f | |||
| 0182e2c0bc | |||
| e464e11ab9 |
143
.gitlab-ci.yml
143
.gitlab-ci.yml
@ -32,7 +32,7 @@ stages:
|
||||
|
||||
.rules-branch-and-MR-always:
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH || $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
- if: $CI_COMMIT_BRANCH || $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
when: always
|
||||
allow_failure: false
|
||||
- when: never
|
||||
@ -54,6 +54,28 @@ stages:
|
||||
allow_failure: true
|
||||
- when: never
|
||||
|
||||
.rules-branch-manual-MR-and-devel-always:
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == "devel" || $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
when: always
|
||||
allow_failure: false
|
||||
- if: $CI_COMMIT_BRANCH
|
||||
when: manual
|
||||
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:
|
||||
@ -65,46 +87,62 @@ lint:
|
||||
tags:
|
||||
- medium
|
||||
|
||||
test-linux:
|
||||
|
||||
.test-base:
|
||||
stage: test
|
||||
extends:
|
||||
- .rules-branch-manual-MR-always
|
||||
script:
|
||||
- make test
|
||||
|
||||
test-linux:
|
||||
extends:
|
||||
- .test-base
|
||||
- .rules-branch-manual-MR-and-devel-always
|
||||
- .after-script-code-coverage
|
||||
tags:
|
||||
- medium
|
||||
|
||||
test-linux-race:
|
||||
stage: test
|
||||
extends:
|
||||
- test-linux
|
||||
- .rules-branch-and-MR-manual
|
||||
script:
|
||||
- make test-race
|
||||
tags:
|
||||
- medium
|
||||
|
||||
test-integration:
|
||||
stage: test
|
||||
extends:
|
||||
- .rules-branch-manual-MR-always
|
||||
- test-linux
|
||||
script:
|
||||
- make test-integration
|
||||
tags:
|
||||
- large
|
||||
|
||||
test-integration-race:
|
||||
stage: test
|
||||
extends:
|
||||
- test-integration
|
||||
- .rules-branch-and-MR-manual
|
||||
script:
|
||||
- make test-integration-race
|
||||
tags:
|
||||
- large
|
||||
|
||||
dependency-updates:
|
||||
|
||||
.windows-base:
|
||||
before_script:
|
||||
- export GOROOT=/c/Go1.18
|
||||
- export PATH=$GOROOT/bin:$PATH
|
||||
- export GOARCH=amd64
|
||||
- export GOPATH=~/go18
|
||||
- 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 updates
|
||||
- make test
|
||||
|
||||
# Stage: BUILD
|
||||
|
||||
@ -120,31 +158,20 @@ dependency-updates:
|
||||
when: manual
|
||||
allow_failure: true
|
||||
- when: never
|
||||
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}
|
||||
script:
|
||||
- make build
|
||||
- git diff && git diff-index --quiet HEAD
|
||||
- make vault-editor
|
||||
artifacts:
|
||||
# Note: The latest artifacts for refs are locked against deletion, and kept
|
||||
# regardless of the expiry time. Introduced in GitLab 13.0 behind a
|
||||
# disabled feature flag, and made the default behavior in GitLab 13.4.
|
||||
expire_in: 1 day
|
||||
when: always
|
||||
name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"
|
||||
paths:
|
||||
- bridge_*.tgz
|
||||
- vault-editor
|
||||
tags:
|
||||
- large
|
||||
|
||||
build-linux:
|
||||
extends: .build-base
|
||||
|
||||
.linux-build-setup:
|
||||
image: gitlab.protontech.ch:4567/go/bridge-internal:qt6
|
||||
variables:
|
||||
VCPKG_DEFAULT_BINARY_CACHE: ${CI_PROJECT_DIR}/.cache
|
||||
@ -153,19 +180,29 @@ build-linux:
|
||||
paths:
|
||||
- .cache
|
||||
when: 'always'
|
||||
artifacts:
|
||||
name: "bridge-linux-$CI_COMMIT_SHORT_SHA"
|
||||
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
|
||||
extends:
|
||||
- build-linux
|
||||
variables:
|
||||
BUILD_TAGS: "build_qa"
|
||||
artifacts:
|
||||
name: "bridge-linux-qa-$CI_COMMIT_SHORT_SHA"
|
||||
|
||||
|
||||
.build-darwin-base:
|
||||
extends: .build-base
|
||||
.darwin-build-setup:
|
||||
before_script:
|
||||
- export PATH=/usr/local/bin:$PATH
|
||||
- export PATH=/usr/local/opt/git/bin:$PATH
|
||||
@ -177,30 +214,22 @@ build-linux-qa:
|
||||
- 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}
|
||||
script:
|
||||
- go version
|
||||
- make build-nogui
|
||||
- git diff && git diff-index --quiet HEAD
|
||||
- make vault-editor
|
||||
cache: {}
|
||||
tags:
|
||||
- macOS
|
||||
|
||||
build-darwin:
|
||||
extends: .build-darwin-base
|
||||
artifacts:
|
||||
name: "bridge-darwin-$CI_COMMIT_SHORT_SHA"
|
||||
extends:
|
||||
- .build-base
|
||||
- .darwin-build-setup
|
||||
|
||||
build-darwin-qa:
|
||||
extends: .build-darwin-base
|
||||
extends:
|
||||
- build-darwin
|
||||
variables:
|
||||
BUILD_TAGS: "build_qa"
|
||||
artifacts:
|
||||
name: "bridge-darwin-qa-$CI_COMMIT_SHORT_SHA"
|
||||
|
||||
|
||||
.build-windows-base:
|
||||
extends: .build-base
|
||||
.windows-build-setup:
|
||||
before_script:
|
||||
- export GOROOT=/c/Go1.18/
|
||||
- export PATH=$GOROOT/bin:$PATH
|
||||
@ -214,23 +243,19 @@ build-darwin-qa:
|
||||
- export PATH="/c/Program Files/Microsoft Visual Studio/2022/Community/Common7/IDE/CommonExtensions/Microsoft/CMake/CMake/bin:$PATH"
|
||||
- $(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}
|
||||
script:
|
||||
- make build-nogui
|
||||
- git diff && git diff-index --quiet HEAD
|
||||
- make vault-editor
|
||||
cache: {}
|
||||
tags:
|
||||
- windows-bridge
|
||||
|
||||
build-windows:
|
||||
extends: .build-windows-base
|
||||
artifacts:
|
||||
name: "bridge-windows-$CI_COMMIT_SHORT_SHA"
|
||||
extends:
|
||||
- .build-base
|
||||
- .windows-build-setup
|
||||
|
||||
build-windows-qa:
|
||||
extends: .build-windows-base
|
||||
extends:
|
||||
- build-windows
|
||||
variables:
|
||||
BUILD_TAGS: "build_qa"
|
||||
artifacts:
|
||||
name: "bridge-windows-qa-$CI_COMMIT_SHORT_SHA"
|
||||
|
||||
# TODO: PUT BACK ALL THE JOBS! JUST DID THIS FOR NOW TO GET CI WORKING AGAIN...
|
||||
|
||||
@ -23,7 +23,6 @@ issues:
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- dupl
|
||||
- funlen
|
||||
- gochecknoglobals
|
||||
- gochecknoinits
|
||||
- gosec
|
||||
@ -32,7 +31,6 @@ issues:
|
||||
- path: test
|
||||
linters:
|
||||
- dupl
|
||||
- funlen
|
||||
- gochecknoglobals
|
||||
- gochecknoinits
|
||||
- gosec
|
||||
@ -64,7 +62,6 @@ linters:
|
||||
- depguard # Go linter that checks if package imports are in a list of acceptable packages [fast: true, auto-fix: false]
|
||||
- dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) [fast: true, auto-fix: false]
|
||||
- dupl # Tool for code clone detection [fast: true, auto-fix: false]
|
||||
- funlen # Tool for detection of long functions [fast: true, auto-fix: false]
|
||||
- gochecknoglobals # Checks that no globals are present in Go code [fast: true, auto-fix: false]
|
||||
- gochecknoinits # Checks that no init functions are present in Go code [fast: true, auto-fix: false]
|
||||
- goconst # Finds repeated strings that could be replaced by a constant [fast: true, auto-fix: false]
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
* libglvnd and libsecret development files (linux)
|
||||
|
||||
To enable the sending of crash reports using Sentry please set the
|
||||
`main.DSNSentry` value with the client key of your sentry project before build.
|
||||
`DSN_SENTRY` environment variable with the client key of your sentry project before build.
|
||||
Otherwise, the sending of crash reports will be disabled.
|
||||
|
||||
## Build
|
||||
|
||||
@ -26,7 +26,6 @@ Proton Mail Bridge includes the following 3rd party software:
|
||||
* [gluon](https://github.com/ProtonMail/gluon) available under [license](https://github.com/ProtonMail/gluon/blob/master/LICENSE)
|
||||
* [go-autostart](https://github.com/ProtonMail/go-autostart) available under [license](https://github.com/ProtonMail/go-autostart/blob/master/LICENSE)
|
||||
* [go-proton-api](https://github.com/ProtonMail/go-proton-api) available under [license](https://github.com/ProtonMail/go-proton-api/blob/master/LICENSE)
|
||||
* [go-rfc5322](https://github.com/ProtonMail/go-rfc5322) available under [license](https://github.com/ProtonMail/go-rfc5322/blob/master/LICENSE)
|
||||
* [gopenpgp](https://github.com/ProtonMail/gopenpgp/v2) available under [license](https://github.com/ProtonMail/gopenpgp/v2/blob/master/LICENSE)
|
||||
* [goquery](https://github.com/PuerkitoBio/goquery) available under [license](https://github.com/PuerkitoBio/goquery/blob/master/LICENSE)
|
||||
* [ishell](https://github.com/abiosoft/ishell) available under [license](https://github.com/abiosoft/ishell/blob/master/LICENSE)
|
||||
@ -53,6 +52,7 @@ Proton Mail Bridge includes the following 3rd party software:
|
||||
* [html2text](https://github.com/jaytaylor/html2text) available under [license](https://github.com/jaytaylor/html2text/blob/master/LICENSE)
|
||||
* [go-keychain](https://github.com/keybase/go-keychain) available under [license](https://github.com/keybase/go-keychain/blob/master/LICENSE)
|
||||
* [dns](https://github.com/miekg/dns) available under [license](https://github.com/miekg/dns/blob/master/LICENSE)
|
||||
* [memory](https://github.com/pbnjay/memory) available under [license](https://github.com/pbnjay/memory/blob/master/LICENSE)
|
||||
* [errors](https://github.com/pkg/errors) available under [license](https://github.com/pkg/errors/blob/master/LICENSE)
|
||||
* [profile](https://github.com/pkg/profile) available under [license](https://github.com/pkg/profile/blob/master/LICENSE)
|
||||
* [logrus](https://github.com/sirupsen/logrus) available under [license](https://github.com/sirupsen/logrus/blob/master/LICENSE)
|
||||
@ -76,8 +76,9 @@ Proton Mail Bridge includes the following 3rd party software:
|
||||
* [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)
|
||||
* [antlr](https://github.com/antlr/antlr4/runtime/Go/antlr) available under [license](https://github.com/antlr/antlr4/runtime/Go/antlr/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)
|
||||
* [circl](https://github.com/cloudflare/circl) available under [license](https://github.com/cloudflare/circl/blob/master/LICENSE)
|
||||
* [go-md2man](https://github.com/cpuguy83/go-md2man/v2) available under [license](https://github.com/cpuguy83/go-md2man/v2/blob/master/LICENSE)
|
||||
@ -88,6 +89,7 @@ Proton Mail Bridge includes the following 3rd party software:
|
||||
* [go-windows](https://github.com/elastic/go-windows) available under [license](https://github.com/elastic/go-windows/blob/master/LICENSE)
|
||||
* [go-textwrapper](https://github.com/emersion/go-textwrapper) available under [license](https://github.com/emersion/go-textwrapper/blob/master/LICENSE)
|
||||
* [go-vcard](https://github.com/emersion/go-vcard) available under [license](https://github.com/emersion/go-vcard/blob/master/LICENSE)
|
||||
* [fgprof](https://github.com/felixge/fgprof) available under [license](https://github.com/felixge/fgprof/blob/master/LICENSE)
|
||||
* [go-shlex](https://github.com/flynn-archive/go-shlex) available under [license](https://github.com/flynn-archive/go-shlex/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)
|
||||
@ -97,6 +99,7 @@ Proton Mail Bridge includes the following 3rd party software:
|
||||
* [validator](https://github.com/go-playground/validator/v10) available under [license](https://github.com/go-playground/validator/v10/blob/master/LICENSE)
|
||||
* [uuid](https://github.com/gofrs/uuid) available under [license](https://github.com/gofrs/uuid/blob/master/LICENSE)
|
||||
* [protobuf](https://github.com/golang/protobuf) available under [license](https://github.com/golang/protobuf/blob/master/LICENSE)
|
||||
* [pprof](https://github.com/google/pprof) available under [license](https://github.com/google/pprof/blob/master/LICENSE)
|
||||
* [errwrap](https://github.com/hashicorp/errwrap) available under [license](https://github.com/hashicorp/errwrap/blob/master/LICENSE)
|
||||
* [go-immutable-radix](https://github.com/hashicorp/go-immutable-radix) available under [license](https://github.com/hashicorp/go-immutable-radix/blob/master/LICENSE)
|
||||
* [go-memdb](https://github.com/hashicorp/go-memdb) available under [license](https://github.com/hashicorp/go-memdb/blob/master/LICENSE)
|
||||
@ -104,6 +107,7 @@ Proton Mail Bridge includes the following 3rd party software:
|
||||
* [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)
|
||||
* [go-urn](https://github.com/leodido/go-urn) available under [license](https://github.com/leodido/go-urn/blob/master/LICENSE)
|
||||
* [go-colorable](https://github.com/mattn/go-colorable) available under [license](https://github.com/mattn/go-colorable/blob/master/LICENSE)
|
||||
* [go-isatty](https://github.com/mattn/go-isatty) available under [license](https://github.com/mattn/go-isatty/blob/master/LICENSE)
|
||||
@ -114,22 +118,24 @@ Proton Mail Bridge includes the following 3rd party software:
|
||||
* [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)
|
||||
* [go-toml](https://github.com/pelletier/go-toml/v2) available under [license](https://github.com/pelletier/go-toml/v2/blob/master/LICENSE)
|
||||
* [lz4](https://github.com/pierrec/lz4/v4) available under [license](https://github.com/pierrec/lz4/v4/blob/master/LICENSE)
|
||||
* [go-difflib](https://github.com/pmezard/go-difflib) available under [license](https://github.com/pmezard/go-difflib/blob/master/LICENSE)
|
||||
* [procfs](https://github.com/prometheus/procfs) available under [license](https://github.com/prometheus/procfs/blob/master/LICENSE)
|
||||
* [uniseg](https://github.com/rivo/uniseg) available under [license](https://github.com/rivo/uniseg/blob/master/LICENSE)
|
||||
* [blackfriday](https://github.com/russross/blackfriday/v2) available under [license](https://github.com/russross/blackfriday/v2/blob/master/LICENSE)
|
||||
* [pflag](https://github.com/spf13/pflag) available under [license](https://github.com/spf13/pflag/blob/master/LICENSE)
|
||||
* [bom](https://github.com/ssor/bom) available under [license](https://github.com/ssor/bom/blob/master/LICENSE)
|
||||
* [golang-asm](https://github.com/twitchyliquid64/golang-asm) available under [license](https://github.com/twitchyliquid64/golang-asm/blob/master/LICENSE)
|
||||
* [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.v2
|
||||
gopkg.in/yaml.v3
|
||||
* [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)
|
||||
|
||||
158
Changelog.md
158
Changelog.md
@ -1,7 +1,163 @@
|
||||
# Proton Mail Bridge and Import-Export app Changelog
|
||||
# Proton Mail Bridge Changelog
|
||||
|
||||
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
|
||||
|
||||
## [Bridge 3.1.2] Quebec
|
||||
|
||||
### Changed
|
||||
* GODT-2582 Dedup recovered messages folder.
|
||||
|
||||
|
||||
## [Bridge 3.1.1] Quebec
|
||||
|
||||
### Fixed
|
||||
* GODT-2500: Fix handler passing.
|
||||
|
||||
|
||||
## [Bridge 3.1.0] Quebec
|
||||
|
||||
### Changed
|
||||
* GODT-2523: Use software QML rendering backend by default on Windows.
|
||||
* GODT-2500: Reorganise async methods.
|
||||
* GODT-2500: Add panic handlers everywhere.
|
||||
* GODT-2511: Add bridge-gui switches to permanently select the QML rendering backend.
|
||||
* GODT-2509: Migrate TLS cert from v1/v2 location during upgrade to v3.
|
||||
* GODT-2487: Add windows test job and worker.
|
||||
* Update GPA to include detailed error messages.
|
||||
* GODT-2479: Ensure messages always have a text body part.
|
||||
* GODT-2482: More attachment to relevant exceptions.
|
||||
* GODT-2224: Refactor bridge sync to use less memory.
|
||||
* GODT-2448: Supported Answered flag.
|
||||
* GODT-2382: Added bridge-gui settings file with 'UseSoftwareRenderer' value.
|
||||
* GODT-2411: Allow qmake executable to be named qmake6.
|
||||
* GODT-2273: Menu with "Close window" and "Quit Bridge" button in main window.
|
||||
* GODT-2261: Sync progress in GUI.
|
||||
* GODT-2385: Gluon cache fallback.
|
||||
* GODT-2366: Handle failed message updates as creates.
|
||||
* GODT-2201: Bump Gluon to use pure Go IMAP parser.
|
||||
* GODT-2374: Import TLS certs via shell.
|
||||
* GODT-2361: Bump GPA to use simple encrypter.
|
||||
* GODT-1264: Constraint on Scheduled mailbox in connector + Integration tests.
|
||||
* GODT-1264: Creation and visibility of the 'Scheduled' system label.
|
||||
* GODT-2283: Limit max import size to 30MB (bump GPA to v0.4.0).
|
||||
* GODT-2352: Only copy resource file when needed.
|
||||
* GODT-2352: Use go-build-finalize macro to build vault-editor for both mac arch.
|
||||
* GODT-2278: Properly override server_name for go.
|
||||
* GODT-2255: Randomize the focus service port.
|
||||
* GODT-2144: Handle IMAP/SMTP server errors via event stream.
|
||||
* GODT-2144: Delay IMAP/SMTP server start until all users are loaded.
|
||||
* GODT-2295: Notifications for IMAP login when signed out.
|
||||
* GODT-2278: Improve sentry logs.
|
||||
* GODT-2289: UIDValidity as Timestamp.
|
||||
|
||||
### Fixed
|
||||
* GODT-2505: Show notification only for cases when user needs to do actions.
|
||||
* GODT-2516: Log error when the vault key cannot be created/loaded from the keychain.
|
||||
* GODT-2526: Fix high memory usage with fetch/search.
|
||||
* GODT-2514: Apply Retry-After to 503.
|
||||
* GODT-2524: Preserve old vault values.
|
||||
* GODT-2508: Handle Address Updated for none-existing address.
|
||||
* GODT-2504: Fix missing attachments in imported message.
|
||||
* GODT-2513: Scanner Crash in Gluon.
|
||||
* GODT-2512: Catch unhandled API errors.
|
||||
* GODT-2507: Memory consumption bug.
|
||||
* GODT-2497: Do not report EOF and network errors.
|
||||
* GODT-2481: Fix DBUS Secert Service.
|
||||
* GODT-2455: Upper limit for number of merged events.
|
||||
* GODT-2480: Do not override X-Original-Date with invalid Date.
|
||||
* GODT-2473: Fix handling of complex mime types.
|
||||
* GODT-2469: Fix sentry revision hash for cmake on windows.
|
||||
* GODT-2424: Sync Builder Message Split.
|
||||
* GODT-2419: Use connector.ErrOperationNotAllowed.
|
||||
* GODT-2418: Ensure child folders are updated when parent is.
|
||||
* GODT-1945: Handle disabled addresses correctly.
|
||||
* GODT-2390: Add reports for uncaught json and net.opErr.
|
||||
* GODT-2393: Improved handling of unrecoverable error.
|
||||
* GODT-2394: Bump Gluon for golang.org/x/text DoS risk.
|
||||
* GODT-2387: Ensure vault can be unlocked after factory reset.
|
||||
* GODT-2389: Close bridge on exception and add max termination wait time.
|
||||
* GODT-2201: Add missing rfc5322.CharsetReader initialization.
|
||||
* GODT-1804: Preserve MIME parameters when uploading attachments.
|
||||
* GODT-2312: Used space is properly updated.
|
||||
* GODT-2319: Seed the math/rand RNG on app startup.
|
||||
* GODT-2272: Use shorter filename for gRPC file socket.
|
||||
* GODT-2318: Remove gluon DB if label sync was incomplete.
|
||||
* GODT-2326: Only run sync after addIMAPUser().
|
||||
* GODT-2323: Fix Expunge not issued for move.
|
||||
* GODT-2224: Properly handle context cancellation during sync.
|
||||
* GODT-2328: Ignore labels that aren't part of user label set.
|
||||
* GODT-2326: Fix potential Win32 API deadlock.
|
||||
* GODT-1804: Only promote content headers if non-empty.
|
||||
* GODT-2327: Remove unnecessary sync when changing address mode.
|
||||
* GODT-2343: Only poll after send if sync is complete.
|
||||
* GODT-2336: Recover from changed address order while bridge is down.
|
||||
* GODT-2347: Prevent updates from being dropped if goroutine doesn't start fast.
|
||||
* GODT-2351: Bump GPA to properly handle net.OpError and add tests.
|
||||
* GODT-2351: Bump GPA to automatically retry on net.OpError.
|
||||
* GODT-2365: Use predictable remote ID for placeholder mailboxes.
|
||||
* GODT-2381: Unset draft flag on sent messages.
|
||||
* GODT-2380: Only set external ID in header if non-empty.
|
||||
|
||||
|
||||
## [Bridge 3.0.21] Perth Narrows
|
||||
|
||||
### Added
|
||||
* GODT-2509: Migrate TLS cert from v1/v2 location during upgrade to v3.
|
||||
|
||||
### Changed
|
||||
* GODT-2516: log error when the vault key cannot be created/loaded from the keychain.
|
||||
|
||||
### Fixed
|
||||
* GODT-2501: Remove additional .desktop file.
|
||||
* GODT-2513: Crash in scanner.
|
||||
* GODT-2481: Fix DBUS Secert Service.
|
||||
* GODT-2512: Catch unhandled API errors.
|
||||
* GODT-2469: Fix sentry revision hash for cmake on windows.
|
||||
|
||||
|
||||
## [Bridge 3.0.20] Perth Narrows
|
||||
|
||||
### Added
|
||||
* GODT-2442: Allow user to re-sync DB without logout.
|
||||
|
||||
### Changed
|
||||
* GODT-2419: Reduce sentry reports.
|
||||
* GODT-2458: Wait for both bridge and bridge-gui to be ended before restarting on crash.
|
||||
* GODT-2457: Include address if GetPublickKeys() error message.
|
||||
* GODT-2446: Attach logs to sentry reports for relevant bridge-gui exceptions.
|
||||
* GODT-2425: Out of sync messages and read status.
|
||||
* GODT-2435: Group report exception by message if exception message looks corrupted.
|
||||
* GODT-2356: Unify sentry release description and add more context to it.
|
||||
* GODT-2357: Hide DSN_SENTRY and use single setting point for DSN_SENTRY.
|
||||
* GODT-2444: Bad event info.
|
||||
* GODT-2447: Don't assume timestamp exists in log filename.
|
||||
* GODT-2333: Do not allow modifications to All Mail label.
|
||||
* GODT-2429: Do not report context cancel to sentry.
|
||||
|
||||
### Fixed
|
||||
* GODT-2467: elide long email adresses in 'bad event' QML notification dialog.
|
||||
* GODT-2449: fix bug in Bridge-GUI's Exception::what().
|
||||
* GODT-2427: Parsing header issues.
|
||||
* GODT-2426: Fix crash on user delete.
|
||||
* GODT-2417: Do not request gluon recovered message from API.
|
||||
|
||||
|
||||
## [Bridge 3.0.19] Perth Narrows
|
||||
|
||||
### Fixed
|
||||
* GODT-2364: Wait and retry once if the gRPC service config file exists but cannot be opened.
|
||||
* GODT-2364: Added optional details to C++ exceptions.
|
||||
* GODT-2413: Use qEnvironmentVariable() instead of qgetenv().
|
||||
* GODT-2412: Don't treat context cancellation as BadEvent.
|
||||
* GODT-2404: Handle unexpected EOF.
|
||||
* GODT-2400: Allow state updates to be applied if command fails.
|
||||
* GODT-2399: Fix immediate message deletion during updates.
|
||||
* GODT-2390: Missing changes from pervious commit.
|
||||
* GODT-2390: Add reports for uncaught json and net.opErr.
|
||||
* GODT-2414: Multiple deletion bug in WriteControlledStore.
|
||||
|
||||
|
||||
## [Bridge 3.0.18] Perth Narrows
|
||||
|
||||
### Fixed
|
||||
|
||||
42
Makefile
42
Makefile
@ -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.0.18+git
|
||||
BRIDGE_APP_VERSION?=3.1.2+git
|
||||
APP_VERSION:=${BRIDGE_APP_VERSION}
|
||||
APP_FULL_NAME:=Proton Mail Bridge
|
||||
APP_VENDOR:=Proton AG
|
||||
@ -23,16 +23,21 @@ REVISION:=$(shell git rev-parse --short=10 HEAD)
|
||||
BUILD_TIME:=$(shell date +%FT%T%z)
|
||||
MACOS_MIN_VERSION_ARM64=11.0
|
||||
MACOS_MIN_VERSION_AMD64=10.15
|
||||
BUILD_ENV?=dev
|
||||
|
||||
BUILD_FLAGS:=-tags='${BUILD_TAGS}'
|
||||
BUILD_FLAGS_LAUNCHER:=${BUILD_FLAGS}
|
||||
BUILD_FLAGS_GUI:=-tags='${BUILD_TAGS} build_qt'
|
||||
GO_LDFLAGS:=$(addprefix -X github.com/ProtonMail/proton-bridge/v3/internal/constants., Version=${APP_VERSION} Revision=${REVISION} BuildTime=${BUILD_TIME})
|
||||
GO_LDFLAGS+=-X "github.com/ProtonMail/proton-bridge/v3/internal/constants.FullAppName=${APP_FULL_NAME}"
|
||||
|
||||
ifneq "${BUILD_LDFLAGS}" ""
|
||||
GO_LDFLAGS+=${BUILD_LDFLAGS}
|
||||
ifneq "${DSN_SENTRY}" ""
|
||||
GO_LDFLAGS+=-X github.com/ProtonMail/proton-bridge/v3/internal/constants.DSNSentry=${DSN_SENTRY}
|
||||
endif
|
||||
|
||||
ifneq "${BUILD_ENV}" ""
|
||||
GO_LDFLAGS+=-X github.com/ProtonMail/proton-bridge/v3/internal/constants.BuildEnv=${BUILD_ENV}
|
||||
endif
|
||||
|
||||
GO_LDFLAGS_LAUNCHER:=${GO_LDFLAGS}
|
||||
ifeq "${TARGET_OS}" "windows"
|
||||
#GO_LDFLAGS+=-H=windowsgui # Disabled so we can inspect trace logs from the bridge for debugging.
|
||||
@ -40,7 +45,6 @@ ifeq "${TARGET_OS}" "windows"
|
||||
endif
|
||||
|
||||
BUILD_FLAGS+=-ldflags '${GO_LDFLAGS}'
|
||||
BUILD_FLAGS_GUI+=-ldflags "${GO_LDFLAGS}"
|
||||
BUILD_FLAGS_LAUNCHER+=-ldflags '${GO_LDFLAGS_LAUNCHER}'
|
||||
DEPLOY_DIR:=cmd/${TARGET_CMD}/deploy
|
||||
DIRNAME:=$(shell basename ${CURDIR})
|
||||
@ -96,9 +100,9 @@ endif
|
||||
|
||||
ifeq "${GOOS}" "windows"
|
||||
go-build-finalize= \
|
||||
powershell Copy-Item ${ROOT_DIR}/${RESOURCE_FILE} ${4} && \
|
||||
$(call go-build,$(1),$(2),$(3)) && \
|
||||
powershell Remove-Item ${4} -Force
|
||||
$(if $(4),powershell Copy-Item ${ROOT_DIR}/${RESOURCE_FILE} ${4} &&,) \
|
||||
$(call go-build,$(1),$(2),$(3)) \
|
||||
$(if $(4), && powershell Remove-Item ${4} -Force,)
|
||||
endif
|
||||
|
||||
${EXE_NAME}: gofiles ${RESOURCE_FILE}
|
||||
@ -112,7 +116,7 @@ versioner:
|
||||
go build ${BUILD_FLAGS} -o versioner utils/versioner/main.go
|
||||
|
||||
vault-editor:
|
||||
go build -tags debug -o vault-editor utils/vault-editor/main.go
|
||||
$(call go-build-finalize,"-tags=debug","vault-editor","./utils/vault-editor/main.go")
|
||||
|
||||
hasher:
|
||||
go build -o hasher utils/hasher/main.go
|
||||
@ -154,8 +158,10 @@ ${EXE_TARGET}: check-build-essentials ${EXE_NAME}
|
||||
BRIDGE_VENDOR="${APP_VENDOR}" \
|
||||
BRIDGE_APP_VERSION=${APP_VERSION} \
|
||||
BRIDGE_REVISION=${REVISION} \
|
||||
BRIDGE_BUILD_TIME=${BUILD_TIME} \
|
||||
BRIDGE_DSN_SENTRY=${DSN_SENTRY} \
|
||||
BRIDGE_BUILD_TIME=${BUILD_TIME} \
|
||||
BRIDGE_GUI_BUILD_CONFIG=Release \
|
||||
BRIDGE_BUILD_ENV=${BUILD_ENV} \
|
||||
BRIDGE_INSTALL_PATH=${ROOT_DIR}/${DEPLOY_DIR}/${GOOS} \
|
||||
./build.sh install
|
||||
mv "${ROOT_DIR}/${BRIDGE_EXE}" "$(ROOT_DIR)/${EXE_TARGET}"
|
||||
@ -222,13 +228,13 @@ change-copyright-year:
|
||||
./utils/missing_license.sh change-year
|
||||
|
||||
test: gofiles
|
||||
go test -v -timeout=5m -p=1 -count=1 -coverprofile=/tmp/coverage.out -run=${TESTRUN} ./internal/... ./pkg/...
|
||||
go test -v -timeout=10m -p=1 -count=1 -coverprofile=/tmp/coverage.out -run=${TESTRUN} ./internal/... ./pkg/...
|
||||
|
||||
test-race: gofiles
|
||||
go test -v -timeout=30m -p=1 -count=1 -race -failfast -run=${TESTRUN} ./internal/... ./pkg/...
|
||||
|
||||
test-integration: gofiles
|
||||
go test -v -timeout=10m -p=1 -count=1 github.com/ProtonMail/proton-bridge/v3/tests
|
||||
go test -v -timeout=20m -p=1 -count=1 github.com/ProtonMail/proton-bridge/v3/tests
|
||||
|
||||
test-integration-debug: gofiles
|
||||
dlv test github.com/ProtonMail/proton-bridge/v3/tests -- -test.v -test.timeout=10m -test.parallel=1 -test.count=1
|
||||
@ -247,7 +253,7 @@ coverage: test
|
||||
mocks:
|
||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/bridge TLSReporter,ProxyController,Autostarter > tmp
|
||||
mv tmp internal/bridge/mocks/mocks.go
|
||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/async PanicHandler > internal/bridge/mocks/async_mocks.go
|
||||
mockgen --package mocks github.com/ProtonMail/gluon/async PanicHandler > internal/bridge/mocks/async_mocks.go
|
||||
mockgen --package mocks github.com/ProtonMail/gluon/reporter Reporter > internal/bridge/mocks/gluon_mocks.go
|
||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/updater Downloader,Installer > internal/updater/mocks/mocks.go
|
||||
|
||||
@ -319,7 +325,11 @@ run-nogui: build-nogui clean-vendor gofiles
|
||||
PROTONMAIL_ENV=dev ./${LAUNCHER_EXE} ${RUN_FLAGS} -c
|
||||
|
||||
run-debug:
|
||||
dlv debug ./cmd/Desktop-Bridge/main.go -- -l=debug
|
||||
dlv debug \
|
||||
--build-flags "-ldflags '-X github.com/ProtonMail/proton-bridge/v3/internal/constants.Version=3.1.0+git'" \
|
||||
./cmd/Desktop-Bridge/main.go \
|
||||
-- \
|
||||
-n -l=trace
|
||||
|
||||
ifeq "${TARGET_OS}" "windows"
|
||||
EXE_SUFFIX=.exe
|
||||
@ -340,7 +350,7 @@ clean-vendor:
|
||||
|
||||
clean-gui:
|
||||
cd internal/frontend/bridge-gui/ && \
|
||||
rm -f Version.h && \
|
||||
rm -f BuildConfig.h && \
|
||||
rm -rf cmake-build-*/
|
||||
|
||||
clean-vcpkg:
|
||||
@ -363,6 +373,6 @@ clean: clean-vendor clean-gui clean-vcpkg
|
||||
.PHONY: generate
|
||||
generate:
|
||||
go generate ./...
|
||||
$(MAKE) add-license
|
||||
$(MAKE) build
|
||||
|
||||
.FORCE:
|
||||
|
||||
34
README.md
34
README.md
@ -1,5 +1,5 @@
|
||||
# Proton Mail Bridge and Import Export app
|
||||
Copyright (c) 2022 Proton AG
|
||||
Copyright (c) 2023 Proton AG
|
||||
|
||||
This repository holds the Proton Mail Bridge and the Proton Mail Import-Export applications.
|
||||
For a detailed build information see [BUILDS](./BUILDS.md).
|
||||
@ -48,9 +48,6 @@ major problems.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
### Bridge application
|
||||
- `BRIDGESTRICTMODE`: tells bridge to turn on `bbolt`'s "strict mode" which checks the database after every `Commit`. Set to `1` to enable.
|
||||
|
||||
### Dev build or run
|
||||
- `APP_VERSION`: set the bridge app version used during testing or building
|
||||
- `PROTONMAIL_ENV`: when set to `dev` it is not using Sentry to report crashes
|
||||
@ -70,25 +67,26 @@ There are now three types of system folders which Bridge recognises:
|
||||
|--------|-------------------------------------|-----------------------------------------------------|-------------------------------------|---------------------------------------|
|
||||
| config | %APPDATA%\protonmail\bridge-v3 | ~/Library/Application Support/protonmail/bridge-v3 | ~/.config/protonmail/bridge-v3 | $XDG_CONFIG_HOME/protonmail/bridge-v3 |
|
||||
| cache | %LOCALAPPDATA%\protonmail\bridge-v3 | ~/Library/Caches/protonmail/bridge-v3 | ~/.cache/protonmail/bridge-v3 | $XDG_CACHE_HOME/protonmail/bridge-v3 |
|
||||
| data | %APPDATA%\protonmail\bridge-v3 | ~/Library/Application Support/protonmail/bridge-v3 | ~/.local/share/protonmail/bridge-v3 | $XDG_DATA_HOME/protonmail/bridge-v3 |
|
||||
| data | %APPDATA%\protonmail\bridge-v3 | ~/Library/Application Support/protonmail/bridge-v3 | ~/.local/share/protonmail/bridge-v3 | $XDG_DATA_HOME/protonmail/bridge-v3 |
|
||||
| temp | %LOCALAPPDATA%\Temp | $TMPDIR if non-empty, else /tmp | $TMPDIR if non-empty, else /tmp | $TMPDIR if non-empty, else /tmp |
|
||||
|
||||
|
||||
|
||||
## Files
|
||||
|
||||
| | Base Dir | Path |
|
||||
|-----------------------|----------|----------------------------|
|
||||
| bridge lock file | cache | bridge.lock |
|
||||
| bridge-gui lock file | cache | bridge-gui.lock |
|
||||
| vault | config | vault.enc |
|
||||
| gRPC server json | config | grpcServerConfig.json |
|
||||
| gRPC client json | config | grpcClientConfig_<id>.json |
|
||||
| Logs | data | logs |
|
||||
| gluon DB | data | gluon/backend/db |
|
||||
| gluon messages | sata | gluon/backend/store |
|
||||
| Update files | data | updates |
|
||||
| sentry cache | data | sentry_cache |
|
||||
| Mac/Linux File Socket | temp | bridge_{RANDOM_UUID}.sock |
|
||||
| | Base Dir | Path |
|
||||
|------------------------|----------|----------------------------|
|
||||
| bridge lock file | cache | bridge.lock |
|
||||
| bridge-gui lock file | cache | bridge-gui.lock |
|
||||
| vault | config | vault.enc |
|
||||
| gRPC server json | config | grpcServerConfig.json |
|
||||
| gRPC client json | config | grpcClientConfig_<id>.json |
|
||||
| gRPC Focus server json | config | grpcFocusServerConfig.json |
|
||||
| Logs | data | logs |
|
||||
| gluon DB | data | gluon/backend/db |
|
||||
| gluon messages | data | gluon/backend/store |
|
||||
| Update files | data | updates |
|
||||
| sentry cache | data | sentry_cache |
|
||||
| Mac/Linux File Socket | temp | bridge{4_DIGITS} |
|
||||
|
||||
|
||||
|
||||
@ -25,6 +25,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/crash"
|
||||
@ -59,10 +60,10 @@ func main() { //nolint:funlen
|
||||
logrus.SetLevel(logrus.DebugLevel)
|
||||
l := logrus.WithField("launcher_version", constants.Version)
|
||||
|
||||
reporter := sentry.NewReporter(appName, constants.Version, useragent.New())
|
||||
reporter := sentry.NewReporter(appName, useragent.New())
|
||||
|
||||
crashHandler := crash.NewHandler(reporter.ReportException)
|
||||
defer crashHandler.HandlePanic()
|
||||
defer async.HandlePanic(crashHandler)
|
||||
|
||||
locationsProvider, err := locations.NewDefaultProvider(filepath.Join(constants.VendorName, constants.ConfigName))
|
||||
if err != nil {
|
||||
@ -127,9 +128,11 @@ func main() { //nolint:funlen
|
||||
|
||||
l = l.WithField("exe_path", exe)
|
||||
|
||||
args, wait, mainExe := findAndStripWait(args)
|
||||
args, wait, mainExes := findAndStripWait(args)
|
||||
if wait {
|
||||
waitForProcessToFinish(mainExe)
|
||||
for _, mainExe := range mainExes {
|
||||
waitForProcessToFinish(mainExe)
|
||||
}
|
||||
}
|
||||
|
||||
cmd := execabs.Command(exe, appendLauncherPath(launcher, args)...) //nolint:gosec
|
||||
@ -186,12 +189,11 @@ func findAndStrip[T comparable](slice []T, v T) (strippedList []T, found bool) {
|
||||
}
|
||||
|
||||
// findAndStripWait Check for waiter flag get its value and clean them both.
|
||||
func findAndStripWait(args []string) ([]string, bool, string) {
|
||||
func findAndStripWait(args []string) ([]string, bool, []string) {
|
||||
res := append([]string{}, args...)
|
||||
|
||||
hasFlag := false
|
||||
var value string
|
||||
|
||||
values := make([]string, 0)
|
||||
for k, v := range res {
|
||||
if v != FlagWait {
|
||||
continue
|
||||
@ -200,14 +202,16 @@ func findAndStripWait(args []string) ([]string, bool, string) {
|
||||
continue
|
||||
}
|
||||
hasFlag = true
|
||||
value = res[k+1]
|
||||
values = append(values, res[k+1])
|
||||
}
|
||||
|
||||
if hasFlag {
|
||||
res, _ = findAndStrip(res, FlagWait)
|
||||
res, _ = findAndStrip(res, value)
|
||||
for _, v := range values {
|
||||
res, _ = findAndStrip(res, v)
|
||||
}
|
||||
}
|
||||
return res, hasFlag, value
|
||||
return res, hasFlag, values
|
||||
}
|
||||
|
||||
func getPathToUpdatedExecutable(
|
||||
|
||||
@ -56,3 +56,25 @@ func TestFindAndStrip(t *testing.T) {
|
||||
assert.False(t, found)
|
||||
assert.True(t, xslices.Equal(result, []string{}))
|
||||
}
|
||||
|
||||
func TestFindAndStripWait(t *testing.T) {
|
||||
result, found, values := findAndStripWait([]string{"a", "b", "c"})
|
||||
assert.False(t, found)
|
||||
assert.True(t, xslices.Equal(result, []string{"a", "b", "c"}))
|
||||
assert.True(t, xslices.Equal(values, []string{}))
|
||||
|
||||
result, found, values = findAndStripWait([]string{"a", "--wait", "b"})
|
||||
assert.True(t, found)
|
||||
assert.True(t, xslices.Equal(result, []string{"a"}))
|
||||
assert.True(t, xslices.Equal(values, []string{"b"}))
|
||||
|
||||
result, found, values = findAndStripWait([]string{"a", "--wait", "b", "--wait", "c"})
|
||||
assert.True(t, found)
|
||||
assert.True(t, xslices.Equal(result, []string{"a"}))
|
||||
assert.True(t, xslices.Equal(values, []string{"b", "c"}))
|
||||
|
||||
result, found, values = findAndStripWait([]string{"a", "--wait", "b", "--wait", "c", "--wait", "d"})
|
||||
assert.True(t, found)
|
||||
assert.True(t, xslices.Equal(result, []string{"a"}))
|
||||
assert.True(t, xslices.Equal(values, []string{"b", "c", "d"}))
|
||||
}
|
||||
|
||||
86
go.mod
86
go.mod
@ -4,21 +4,20 @@ go 1.18
|
||||
|
||||
require (
|
||||
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
|
||||
github.com/Masterminds/semver/v3 v3.1.1
|
||||
github.com/ProtonMail/gluon v0.14.2-0.20230221144759-b277a90ca303
|
||||
github.com/Masterminds/semver/v3 v3.2.0
|
||||
github.com/ProtonMail/gluon v0.15.1-0.20230426114936-a356ef7f2753
|
||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
|
||||
github.com/ProtonMail/go-proton-api v0.3.1-0.20230209110241-fe7894c4931a
|
||||
github.com/ProtonMail/go-rfc5322 v0.11.0
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.4.10
|
||||
github.com/PuerkitoBio/goquery v1.8.0
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20230406143739-c7596e170799
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.5.2
|
||||
github.com/PuerkitoBio/goquery v1.8.1
|
||||
github.com/abiosoft/ishell v2.0.0+incompatible
|
||||
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37
|
||||
github.com/bradenaw/juniper v0.8.0
|
||||
github.com/bradenaw/juniper v0.10.2
|
||||
github.com/cucumber/godog v0.12.5
|
||||
github.com/cucumber/messages-go/v16 v16.0.1
|
||||
github.com/docker/docker-credential-helpers v0.6.3
|
||||
github.com/elastic/go-sysinfo v1.8.1
|
||||
github.com/emersion/go-imap v1.2.1-0.20220429085312-746087b7a317
|
||||
github.com/emersion/go-imap v1.2.1
|
||||
github.com/emersion/go-imap-id v0.0.0-20190926060100-f94a56b9ecde
|
||||
github.com/emersion/go-message v0.16.0
|
||||
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead
|
||||
@ -26,7 +25,7 @@ require (
|
||||
github.com/fatih/color v1.13.0
|
||||
github.com/getsentry/sentry-go v0.15.0
|
||||
github.com/go-resty/resty/v2 v2.7.0
|
||||
github.com/goccy/go-json v0.9.11
|
||||
github.com/goccy/go-json v0.10.0
|
||||
github.com/godbus/dbus v4.1.0+incompatible
|
||||
github.com/golang/mock v1.6.0
|
||||
github.com/google/go-cmp v0.5.9
|
||||
@ -35,36 +34,38 @@ require (
|
||||
github.com/jaytaylor/html2text v0.0.0-20211105163654-bc68cce691ba
|
||||
github.com/keybase/go-keychain v0.0.0
|
||||
github.com/miekg/dns v1.1.50
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pkg/profile v1.6.0
|
||||
github.com/pkg/profile v1.7.0
|
||||
github.com/sirupsen/logrus v1.9.0
|
||||
github.com/stretchr/testify v1.8.0
|
||||
github.com/urfave/cli/v2 v2.20.3
|
||||
github.com/stretchr/testify v1.8.1
|
||||
github.com/urfave/cli/v2 v2.24.4
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.5
|
||||
go.uber.org/goleak v1.2.0
|
||||
golang.org/x/exp v0.0.0-20221023144134-a1e5550cf13e
|
||||
golang.org/x/net v0.1.0
|
||||
golang.org/x/sys v0.1.0
|
||||
golang.org/x/text v0.4.0
|
||||
google.golang.org/grpc v1.50.1
|
||||
go.uber.org/goleak v1.2.1
|
||||
golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb
|
||||
golang.org/x/net v0.7.0
|
||||
golang.org/x/sys v0.5.0
|
||||
golang.org/x/text v0.7.0
|
||||
google.golang.org/grpc v1.53.0
|
||||
google.golang.org/protobuf v1.28.1
|
||||
howett.net/plist v1.0.0
|
||||
)
|
||||
|
||||
require (
|
||||
ariga.io/atlas v0.7.0 // indirect
|
||||
entgo.io/ent v0.11.2 // indirect
|
||||
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-20220824120805-4b6e5c587895 // indirect
|
||||
github.com/ProtonMail/go-mime v0.0.0-20220429130430-2192574d760f // indirect
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect
|
||||
github.com/ProtonMail/go-mime v0.0.0-20221031134845-8fd9bc37cf08 // indirect
|
||||
github.com/ProtonMail/go-srp v0.0.5 // 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.1 // indirect
|
||||
github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 // indirect
|
||||
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
|
||||
github.com/bytedance/sonic v1.8.1 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/chzyer/test v1.0.0 // indirect
|
||||
github.com/cloudflare/circl v1.2.0 // indirect
|
||||
github.com/cloudflare/circl v1.3.2 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
github.com/cronokirby/saferith v0.33.0 // indirect
|
||||
github.com/cucumber/gherkin-go/v19 v19.0.3 // indirect
|
||||
@ -73,48 +74,53 @@ require (
|
||||
github.com/elastic/go-windows v1.0.1 // indirect
|
||||
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect
|
||||
github.com/emersion/go-vcard v0.0.0-20220507122617-d4056df0ec4a // indirect
|
||||
github.com/felixge/fgprof v0.9.3 // indirect
|
||||
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/gin-gonic/gin v1.8.1 // indirect
|
||||
github.com/gin-gonic/gin v1.9.0 // indirect
|
||||
github.com/go-openapi/inflect v0.19.0 // indirect
|
||||
github.com/go-playground/locales v0.14.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.11.1 // 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.11.2 // indirect
|
||||
github.com/gofrs/uuid v4.3.0+incompatible // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
||||
github.com/hashicorp/go-memdb v1.3.3 // indirect
|
||||
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
||||
github.com/hashicorp/hcl/v2 v2.14.0 // 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
|
||||
github.com/leodido/go-urn v1.2.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.15 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.16 // indirect
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 // 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
|
||||
github.com/pelletier/go-toml/v2 v2.0.5 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.17 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/procfs v0.8.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.2 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||
github.com/ugorji/go/codec v1.2.7 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.10 // 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.11.0 // indirect
|
||||
golang.org/x/crypto v0.1.0 // indirect
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
|
||||
golang.org/x/sync v0.0.0-20220907140024-f12130a52804 // indirect
|
||||
golang.org/x/tools v0.1.13-0.20220804200503-81c7dc4e4efa // indirect
|
||||
google.golang.org/genproto v0.0.0-20220921223823-23cae91e6737 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
github.com/zclconf/go-cty v1.12.1 // indirect
|
||||
golang.org/x/arch v0.2.0 // indirect
|
||||
golang.org/x/crypto v0.6.0 // indirect
|
||||
golang.org/x/mod v0.8.0 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/tools v0.3.1-0.20221202221704-aa9f4b2f3d57 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230221151758-ace64dc21148 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
|
||||
223
go.sum
223
go.sum
@ -1,5 +1,5 @@
|
||||
ariga.io/atlas v0.7.0 h1:daEFdUsyNm7EHyzcMfjWwq/fVv48fCfad+dIGyobY1k=
|
||||
ariga.io/atlas v0.7.0/go.mod h1:ft47uSh5hWGDCmQC9DsztZg6Xk+KagM5Ts/mZYKb9JE=
|
||||
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,44 +13,41 @@ 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.2 h1:UM2/BUhF2FfsxPHRxLjQbhqJNaDdVlOwNIAMLs2jyto=
|
||||
entgo.io/ent v0.11.2/go.mod h1:YGHEQnmmIUgtD5b1ICD5vg74dS3npkNnmC5K+0J+IHU=
|
||||
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.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
|
||||
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||
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=
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs69zUkSzubzjBbL+cmOXgnmt9Fyd9ug=
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
|
||||
github.com/ProtonMail/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.14.2-0.20230221144759-b277a90ca303 h1:OTzqa4dcVuo4O4Kkng8rcoKJiKEY9HalQb8Y3OJWNFA=
|
||||
github.com/ProtonMail/gluon v0.14.2-0.20230221144759-b277a90ca303/go.mod h1:z2AxLIiBCT1K+0OBHyaDI7AEaO5qI6/BEC2TE42vs4Q=
|
||||
github.com/ProtonMail/gluon v0.15.1-0.20230426114936-a356ef7f2753 h1:Ae/Ny7Fm+8RS/L+/9CiT6ntoPFSIJLyrLbD5bI77il4=
|
||||
github.com/ProtonMail/gluon v0.15.1-0.20230426114936-a356ef7f2753/go.mod h1:yA4hk6CJw0BMo+YL8Y3ckCYs5L20sysu9xseshwY3QI=
|
||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
|
||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20220822140716-1678d6eb0cbe/go.mod h1:UBYPn8k0D56RtnR8RFQMjmh4KrZzWJ5o7Z9SYjossQ8=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20220824120805-4b6e5c587895 h1:NsReiLpErIPzRrnogAXYwSoU7txA977LjDGrbkewJbg=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20220824120805-4b6e5c587895/go.mod h1:UBYPn8k0D56RtnR8RFQMjmh4KrZzWJ5o7Z9SYjossQ8=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230124153114-0acdc8ae009b/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 h1:wPbRQzjjwFc0ih8puEVAOFGELsn1zoIIYdxvML7mDxA=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g=
|
||||
github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753 h1:I8IsYA297x0QLU80G5I6aLYUu3JYNSpo8j5fkXtFDW0=
|
||||
github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20220302105931-303f85f7fe0f/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20220429130430-2192574d760f h1:4IWzKjHzZxdrW9k4zl/qCwenOVHDbVDADPPHFLjs0Oc=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20220429130430-2192574d760f/go.mod h1:qRZgbeASl2a9OwmsV85aWwRqic0NHPh+9ewGAzb4cgM=
|
||||
github.com/ProtonMail/go-proton-api v0.3.1-0.20230209110241-fe7894c4931a h1:h9KLPt0HTCJjILYHREWCYnZv+1xaYmOVx/rxiT/1dIg=
|
||||
github.com/ProtonMail/go-proton-api v0.3.1-0.20230209110241-fe7894c4931a/go.mod h1:JUo5IQG0hNuPRuDpOUsCOvtee6UjTEHHF1QN2i8RSos=
|
||||
github.com/ProtonMail/go-rfc5322 v0.11.0 h1:o5Obrm4DpmQEffvgsVqG6S4BKwC1Wat+hYwjIp2YcCY=
|
||||
github.com/ProtonMail/go-rfc5322 v0.11.0/go.mod h1:6oOKr0jXvpoE6pwTx/HukigQpX2J9WUf6h0auplrFTw=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20221031134845-8fd9bc37cf08 h1:dS7r5z4iGS0qCjM7UwWdsEMzQesUQbGcXdSm2/tWboA=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20221031134845-8fd9bc37cf08/go.mod h1:qRZgbeASl2a9OwmsV85aWwRqic0NHPh+9ewGAzb4cgM=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20230406143739-c7596e170799 h1:slk4Drrkij1EVTnFOlIDyJsfjt69tnw8w2g1NMb253U=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20230406143739-c7596e170799/go.mod h1:kis4GD6FHp1ZWnenSBepldt8ai+vYalDPeey9yGwyXk=
|
||||
github.com/ProtonMail/go-srp v0.0.5 h1:xhUioxZgDbCnpo9JehyFhwwsn9JLWkUGfB0oiKXgiGg=
|
||||
github.com/ProtonMail/go-srp v0.0.5/go.mod h1:06iYHtLXW8vjLtccWj++x3MKy65sIT8yZd7nrJF49rs=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.4.10 h1:EYgkxzwmQvsa6kxxkgP1AwzkFqKHscF2UINxaSn6rdI=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.4.10/go.mod h1:CTRA7/toc/4DxDy5Du4hPDnIZnJvXSeQ8LsRTOUJoyc=
|
||||
github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U=
|
||||
github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.5.2 h1:97SjlWNAxXl9P22lgwgrZRshQdiEfAht0g3ZoiA1GCw=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.5.2/go.mod h1:52qDaCnto6r+CoWbuU50T77XQt99lIs46HtHtvgFO3o=
|
||||
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
|
||||
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
|
||||
github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw=
|
||||
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=
|
||||
@ -63,9 +60,6 @@ github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37 h1:2
|
||||
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37/go.mod h1:6AXRstqK+32jeFmw89QGL2748+dj34Av4xc/I9oo9BY=
|
||||
github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
|
||||
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
|
||||
github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20220816024939-bc8df83d7b9d/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY=
|
||||
github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 h1:yL7+Jz0jTC6yykIK/Wh74gnTJnrGr5AyrNMXuA0gves=
|
||||
github.com/antlr/antlr4/runtime/Go/antlr v1.4.10/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY=
|
||||
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=
|
||||
@ -75,19 +69,27 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
|
||||
github.com/bradenaw/juniper v0.8.0 h1:sdanLNdJbLjcLj993VYIwUHlUVkLzvgiD/x9O7cvvxk=
|
||||
github.com/bradenaw/juniper v0.8.0/go.mod h1:Z2B7aJlQ7xbfWsnMLROj5t/5FQ94/MkIdKC30J4WvzI=
|
||||
github.com/bradenaw/juniper v0.10.2 h1:EY7r8SJJrigJ7lvWk6ews3K5RD4XTG9z+WSwHJKijP4=
|
||||
github.com/bradenaw/juniper v0.10.2/go.mod h1:Z2B7aJlQ7xbfWsnMLROj5t/5FQ94/MkIdKC30J4WvzI=
|
||||
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||
github.com/bwesterb/go-ristretto v1.2.1/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.8.1 h1:NqAHCaGaTzro0xMmnTCLUyRlbEP6r8MCA1cJUrH3Pu4=
|
||||
github.com/bytedance/sonic v1.8.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
|
||||
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
|
||||
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
|
||||
github.com/cloudflare/circl v1.2.0 h1:NheeISPSUcYftKlfrLuOo4T62FkmD4t4jviLfFFYaec=
|
||||
github.com/cloudflare/circl v1.2.0/go.mod h1:Ch2UgYr6ti2KTtlejELlROl0YIYj7SLjAC8M+INXlMk=
|
||||
github.com/cloudflare/circl v1.3.2 h1:VWp8dY3yH69fdM7lM6A1+NhhVoDu9vqK0jOgmkQHFWk=
|
||||
github.com/cloudflare/circl v1.3.2/go.mod h1:+CauBF6R70Jqcyl8N2hC8pAXYbWkGIezuSbuGLtRhnw=
|
||||
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
@ -96,7 +98,6 @@ github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfc
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/cronokirby/saferith v0.33.0 h1:TgoQlfsD4LIwx71+ChfRcIpjkw+RPOapDEVxa+LhwLo=
|
||||
github.com/cronokirby/saferith v0.33.0/go.mod h1:QKJhjoqUtBsXCAVEjw38mFqoi7DebT7kthcD7UzbnoA=
|
||||
github.com/cucumber/gherkin-go/v19 v19.0.3 h1:mMSKu1077ffLbTJULUfM5HPokgeBcIGboyeNUof1MdE=
|
||||
@ -120,8 +121,8 @@ github.com/elastic/go-sysinfo v1.8.1 h1:4Yhj+HdV6WjbCRgGdZpPJ8lZQlXZLKDAeIkmQ/VR
|
||||
github.com/elastic/go-sysinfo v1.8.1/go.mod h1:JfllUnzoQV/JRYymbH3dO1yggI3mV2oTKSXsDHM+uIM=
|
||||
github.com/elastic/go-windows v1.0.1 h1:AlYZOldA+UJ0/2nBuqWdo90GFCgG9xuyw9SYzGUtJm0=
|
||||
github.com/elastic/go-windows v1.0.1/go.mod h1:FoVvqWSun28vaDQPbj2Elfc0JahhPB7WQEGa3c814Ss=
|
||||
github.com/emersion/go-imap v1.2.1-0.20220429085312-746087b7a317 h1:i0cBrdFLm8A/3hWEjn/BwdXLBplFJoZtu63p7bjrmaI=
|
||||
github.com/emersion/go-imap v1.2.1-0.20220429085312-746087b7a317/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY=
|
||||
github.com/emersion/go-imap v1.2.1 h1:+s9ZjMEjOB8NzZMVTM3cCenz2JrQIGGo5j1df19WjTA=
|
||||
github.com/emersion/go-imap v1.2.1/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY=
|
||||
github.com/emersion/go-imap-id v0.0.0-20190926060100-f94a56b9ecde h1:43mBoVwooyLm1+1YVf5nvn1pSFWhw7rOpcrp1Jg/qk0=
|
||||
github.com/emersion/go-imap-id v0.0.0-20190926060100-f94a56b9ecde/go.mod h1:sPwp0FFboaK/bxsrUz1lNrDMUCsZUsKC5YuM4uRVRVs=
|
||||
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||
@ -136,6 +137,8 @@ github.com/emersion/go-vcard v0.0.0-20220507122617-d4056df0ec4a/go.mod h1:HMJKR5
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
|
||||
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
|
||||
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BMXYYRWTLOJKlh+lOBt6nUQgXAfB7oVIQt5cNreqSLI=
|
||||
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:rZfgFAXFS/z/lEd6LJmf9HVZ1LkgYiHx5pHhV5DR16M=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
@ -144,8 +147,8 @@ github.com/getsentry/sentry-go v0.15.0/go.mod h1:RZPJKSw+adu8PBNygiri/A98FqVr2Ht
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8=
|
||||
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
|
||||
github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8=
|
||||
github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k=
|
||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
@ -153,20 +156,19 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9
|
||||
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.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
|
||||
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
||||
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
|
||||
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
||||
github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ=
|
||||
github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
|
||||
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=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU=
|
||||
github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s=
|
||||
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.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk=
|
||||
github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA=
|
||||
github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4=
|
||||
github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
|
||||
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
@ -198,6 +200,8 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y=
|
||||
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
@ -239,12 +243,13 @@ 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.14.0 h1:jX6+Q38Ly9zaAJlAjnFVyeNSNCKKW8D0wvyg7vij5Wc=
|
||||
github.com/hashicorp/hcl/v2 v2.14.0/go.mod h1:e4z5nxYlWNPdDSNYX+ph14EvWYMFm3eP0zIUqPc2jr0=
|
||||
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=
|
||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/jaytaylor/html2text v0.0.0-20211105163654-bc68cce691ba h1:QFQpJdgbON7I0jr2hYW7Bs+XV0qjc3d5tZoDnRFnqTg=
|
||||
github.com/jaytaylor/html2text v0.0.0-20211105163654-bc68cce691ba/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
|
||||
@ -261,16 +266,17 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
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/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4=
|
||||
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
|
||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||
@ -282,13 +288,14 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
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.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
|
||||
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
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/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=
|
||||
@ -314,17 +321,20 @@ github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg=
|
||||
github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas=
|
||||
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
|
||||
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
|
||||
github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc=
|
||||
github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/profile v1.6.0 h1:hUDfIISABYI59DyeB3OTay/HxSRwTQ8rB/H83k6r5dM=
|
||||
github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18=
|
||||
github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
|
||||
github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
@ -344,9 +354,7 @@ github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8=
|
||||
github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
@ -355,7 +363,6 @@ github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg
|
||||
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.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
|
||||
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
@ -376,24 +383,26 @@ github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cma
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
|
||||
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
|
||||
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
||||
github.com/urfave/cli/v2 v2.20.3 h1:lOgGidH/N5loaigd9HjFsOIhXSTrzl7tBpHswZ428w4=
|
||||
github.com/urfave/cli/v2 v2.20.3/go.mod h1:1CNUng3PtjQMtRzJO4FMXBQvkGtuYRxxiR9xMa7jMwI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.10 h1:eimT6Lsr+2lzmSZxPhLFoOWFmQqwk0fllJJ5hEbTXtQ=
|
||||
github.com/ugorji/go/codec v1.2.10/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/urfave/cli/v2 v2.24.4 h1:0gyJJEBYtCV87zI/x2nZCPyDxD51K6xM8SkwjHFCNEU=
|
||||
github.com/urfave/cli/v2 v2.24.4/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
|
||||
@ -402,16 +411,20 @@ github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
||||
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/zclconf/go-cty v1.11.0 h1:726SxLdi2SDnjY+BStqB9J1hNp4+2WlzyXLuimibIe0=
|
||||
github.com/zclconf/go-cty v1.11.0/go.mod h1:s9IfD1LK5ccNMSWCVFCE2rJfHiZgi7JijgeWIMfhLvA=
|
||||
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=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
|
||||
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
|
||||
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
|
||||
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.2.0 h1:W1sUEHXiJTfjaFJ5SLo0N6lZn+0eO5gWD1MFeTGqQEY=
|
||||
golang.org/x/arch v0.2.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
@ -420,18 +433,16 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
|
||||
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
|
||||
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
|
||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/exp v0.0.0-20221023144134-a1e5550cf13e h1:SkwG94eNiiYJhbeDE018Grw09HIN/KB9NlRmZsrzfWs=
|
||||
golang.org/x/exp v0.0.0-20221023144134-a1e5550cf13e/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
|
||||
golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb h1:PaBZQdo+iSDyHT053FjUCgZQ/9uqVwPOcl7KSWhKn6w=
|
||||
golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
@ -440,18 +451,16 @@ golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTk
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mobile v0.0.0-20200801112145-973feb4309de/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4=
|
||||
golang.org/x/mobile v0.0.0-20221110043201-43a038452099/go.mod h1:aAjjkJNdrh3PMckS4B10TGS2nag27cbKR1y2BpUxsiY=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@ -471,9 +480,9 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
|
||||
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0=
|
||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@ -483,8 +492,10 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220907140024-f12130a52804 h1:0SH2R3f1b1VmIMG7BXbEZCBUu2dKmHschSmjqGUrW8A=
|
||||
golang.org/x/sync v0.0.0-20220907140024-f12130a52804/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@ -494,7 +505,6 @@ golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5h
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@ -508,15 +518,18 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
@ -524,8 +537,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@ -546,11 +559,11 @@ golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtn
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.13-0.20220804200503-81c7dc4e4efa h1:uKcci2q7Qtp6nMTC/AAvfNUAldFtJuHWV9/5QWiypts=
|
||||
golang.org/x/tools v0.1.13-0.20220804200503-81c7dc4e4efa/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.3.1-0.20221202221704-aa9f4b2f3d57 h1:/X0t/E4VxbZE7MLS7auvE7YICHeVvbIa9vkOVvYW/24=
|
||||
golang.org/x/tools v0.3.1-0.20221202221704-aa9f4b2f3d57/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@ -573,13 +586,13 @@ google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20220921223823-23cae91e6737 h1:K1zaaMdYBXRyX+cwFnxj7M6zwDyumLQMZ5xqwGvjreQ=
|
||||
google.golang.org/genproto v0.0.0-20220921223823-23cae91e6737/go.mod h1:2r/26NEF3bFmT3eC3aZreahSal0C3Shl8Gi6vyDYqOQ=
|
||||
google.golang.org/genproto v0.0.0-20230221151758-ace64dc21148 h1:muK+gVBJBfFb4SejshDBlN2/UgxCCOKH9Y34ljqEGOc=
|
||||
google.golang.org/genproto v0.0.0-20230221151758-ace64dc21148/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.50.1 h1:DS/BukOZWp8s6p4Dt/tOaJaTQyPyOoCcrjroHuCeLzY=
|
||||
google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
|
||||
google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc=
|
||||
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
|
||||
@ -598,11 +611,8 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
@ -612,3 +622,4 @@ honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt
|
||||
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
|
||||
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
|
||||
@ -28,6 +28,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/cookies"
|
||||
@ -81,7 +82,7 @@ const (
|
||||
appUsage = "Proton Mail IMAP and SMTP Bridge"
|
||||
)
|
||||
|
||||
func New() *cli.App { //nolint:funlen
|
||||
func New() *cli.App {
|
||||
app := cli.NewApp()
|
||||
|
||||
app.Name = constants.FullAppName
|
||||
@ -156,7 +157,7 @@ func New() *cli.App { //nolint:funlen
|
||||
return app
|
||||
}
|
||||
|
||||
func run(c *cli.Context) error { //nolint:funlen
|
||||
func run(c *cli.Context) error {
|
||||
// Seed the default RNG from the math/rand package.
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
|
||||
@ -170,7 +171,7 @@ func run(c *cli.Context) error { //nolint:funlen
|
||||
identifier := useragent.New()
|
||||
|
||||
// Create a new Sentry client that will be used to report crashes etc.
|
||||
reporter := sentry.NewReporter(constants.FullAppName, constants.Version, identifier)
|
||||
reporter := sentry.NewReporter(constants.FullAppName, identifier)
|
||||
|
||||
// Determine the exe that should be used to restart/autostart the app.
|
||||
// By default, this is the launcher, if used. Otherwise, we try to get
|
||||
@ -185,14 +186,14 @@ func run(c *cli.Context) error { //nolint:funlen
|
||||
exe = os.Args[0]
|
||||
}
|
||||
|
||||
migrationErr := migrateOldVersions()
|
||||
// Restart the app if requested.
|
||||
return withRestarter(exe, func(restarter *restarter.Restarter) error {
|
||||
// Handle crashes with various actions.
|
||||
return withCrashHandler(restarter, reporter, func(crashHandler *crash.Handler, quitCh <-chan struct{}) error {
|
||||
migrationErr := migrateOldVersions()
|
||||
|
||||
// Run with profiling if requested.
|
||||
return withProfiler(c, func() error {
|
||||
// Restart the app if requested.
|
||||
return withRestarter(exe, func(restarter *restarter.Restarter) error {
|
||||
// Handle crashes with various actions.
|
||||
return withCrashHandler(restarter, reporter, func(crashHandler *crash.Handler, quitCh <-chan struct{}) error {
|
||||
// Run with profiling if requested.
|
||||
return withProfiler(c, func() error {
|
||||
// Load the locations where we store our files.
|
||||
return WithLocations(func(locations *locations.Locations) error {
|
||||
// Migrate the keychain helper.
|
||||
@ -208,9 +209,14 @@ func run(c *cli.Context) error { //nolint:funlen
|
||||
}
|
||||
|
||||
// Ensure we are the only instance running.
|
||||
return withSingleInstance(locations, version, func() error {
|
||||
settings, err := locations.ProvideSettingsPath()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Failed to get settings path")
|
||||
}
|
||||
|
||||
return withSingleInstance(settings, locations.GetLockFile(), version, func() error {
|
||||
// Unlock the encrypted vault.
|
||||
return WithVault(locations, func(vault *vault.Vault, insecure, corrupt bool) error {
|
||||
return WithVault(locations, crashHandler, func(v *vault.Vault, insecure, corrupt bool) error {
|
||||
// Report insecure vault.
|
||||
if insecure {
|
||||
_ = reporter.ReportMessageWithContext("Vault is insecure", map[string]interface{}{})
|
||||
@ -221,27 +227,27 @@ func run(c *cli.Context) error { //nolint:funlen
|
||||
_ = reporter.ReportMessageWithContext("Vault is corrupt", map[string]interface{}{})
|
||||
}
|
||||
|
||||
if !vault.Migrated() {
|
||||
if !v.Migrated() {
|
||||
// Migrate old settings into the vault.
|
||||
if err := migrateOldSettings(vault); err != nil {
|
||||
if err := migrateOldSettings(v); err != nil {
|
||||
logrus.WithError(err).Error("Failed to migrate old settings")
|
||||
}
|
||||
|
||||
// Migrate old accounts into the vault.
|
||||
if err := migrateOldAccounts(locations, vault); err != nil {
|
||||
if err := migrateOldAccounts(locations, v); err != nil {
|
||||
logrus.WithError(err).Error("Failed to migrate old accounts")
|
||||
}
|
||||
|
||||
// The vault has been migrated.
|
||||
if err := vault.SetMigrated(); err != nil {
|
||||
if err := v.SetMigrated(); err != nil {
|
||||
logrus.WithError(err).Error("Failed to mark vault as migrated")
|
||||
}
|
||||
}
|
||||
|
||||
// Load the cookies from the vault.
|
||||
return withCookieJar(vault, func(cookieJar http.CookieJar) error {
|
||||
return withCookieJar(v, func(cookieJar http.CookieJar) error {
|
||||
// Create a new bridge instance.
|
||||
return withBridge(c, exe, locations, version, identifier, crashHandler, reporter, vault, cookieJar, func(b *bridge.Bridge, eventCh <-chan events.Event) error {
|
||||
return withBridge(c, exe, locations, version, identifier, crashHandler, reporter, v, cookieJar, func(b *bridge.Bridge, eventCh <-chan events.Event) error {
|
||||
if insecure {
|
||||
logrus.Warn("The vault key could not be retrieved; the vault will not be encrypted")
|
||||
b.PushError(bridge.ErrVaultInsecure)
|
||||
@ -266,15 +272,15 @@ func run(c *cli.Context) error { //nolint:funlen
|
||||
}
|
||||
|
||||
// If there's another instance already running, try to raise it and exit.
|
||||
func withSingleInstance(locations *locations.Locations, version *semver.Version, fn func() error) error {
|
||||
func withSingleInstance(settingPath, lockFile string, version *semver.Version, fn func() error) error {
|
||||
logrus.Debug("Checking for other instances")
|
||||
defer logrus.Debug("Single instance stopped")
|
||||
|
||||
lock, err := checkSingleInstance(locations.GetLockFile(), version)
|
||||
lock, err := checkSingleInstance(settingPath, lockFile, version)
|
||||
if err != nil {
|
||||
logrus.Info("Another instance is already running; raising it")
|
||||
|
||||
if ok := focus.TryRaise(); !ok {
|
||||
if ok := focus.TryRaise(settingPath); !ok {
|
||||
return fmt.Errorf("another instance is already running but it could not be raised")
|
||||
}
|
||||
|
||||
@ -374,7 +380,7 @@ func withCrashHandler(restarter *restarter.Restarter, reporter *sentry.Reporter,
|
||||
defer logrus.Debug("Crash handler stopped")
|
||||
|
||||
crashHandler := crash.NewHandler(crash.ShowErrorNotification(constants.FullAppName))
|
||||
defer crashHandler.HandlePanic()
|
||||
defer async.HandlePanic(crashHandler)
|
||||
|
||||
// On crash, send crash report to Sentry.
|
||||
crashHandler.AddRecoveryAction(reporter.ReportException)
|
||||
|
||||
@ -23,6 +23,7 @@ import (
|
||||
"runtime"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/gluon/imap"
|
||||
"github.com/ProtonMail/go-autostart"
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||
@ -40,13 +41,11 @@ import (
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
const vaultSecretName = "bridge-vault-key"
|
||||
|
||||
// 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.
|
||||
func withBridge( //nolint:funlen
|
||||
func withBridge(
|
||||
c *cli.Context,
|
||||
exe string,
|
||||
locations *locations.Locations,
|
||||
@ -79,7 +78,7 @@ func withBridge( //nolint:funlen
|
||||
)
|
||||
|
||||
// Create a proxy dialer which switches to a proxy if the request fails.
|
||||
proxyDialer := dialer.NewProxyTLSDialer(pinningDialer, constants.APIHost)
|
||||
proxyDialer := dialer.NewProxyTLSDialer(pinningDialer, constants.APIHost, crashHandler)
|
||||
|
||||
// Create the autostarter.
|
||||
autostarter := newAutostarter(exe)
|
||||
@ -110,6 +109,7 @@ func withBridge( //nolint:funlen
|
||||
// Crash and report stuff
|
||||
crashHandler,
|
||||
reporter,
|
||||
imap.DefaultEpochUIDValidityGenerator(),
|
||||
|
||||
// The logging stuff.
|
||||
c.String(flagLogIMAP) == "client" || c.String(flagLogIMAP) == "all",
|
||||
|
||||
@ -46,10 +46,11 @@ func runFrontend(
|
||||
|
||||
switch {
|
||||
case c.Bool(flagCLI):
|
||||
return bridgeCLI.New(bridge, restarter, eventCh).Loop()
|
||||
return bridgeCLI.New(bridge, restarter, eventCh, crashHandler, quitCh).Loop()
|
||||
|
||||
case c.Bool(flagNonInteractive):
|
||||
select {}
|
||||
<-quitCh
|
||||
return nil
|
||||
|
||||
case c.Bool(flagGRPC):
|
||||
service, err := grpc.NewService(crashHandler, restarter, locations, bridge, eventCh, quitCh, !c.Bool(flagNoWindow), parentPID)
|
||||
|
||||
@ -87,6 +87,11 @@ func migrateOldSettings(v *vault.Vault) error {
|
||||
return fmt.Errorf("failed to get user config dir: %w", err)
|
||||
}
|
||||
|
||||
return migrateOldSettingsWithDir(configDir, v)
|
||||
}
|
||||
|
||||
// nolint:gosec
|
||||
func migrateOldSettingsWithDir(configDir string, v *vault.Vault) error {
|
||||
b, err := os.ReadFile(filepath.Join(configDir, "protonmail", "bridge", "prefs.json"))
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return nil
|
||||
@ -94,7 +99,27 @@ func migrateOldSettings(v *vault.Vault) error {
|
||||
return fmt.Errorf("failed to read old prefs file: %w", err)
|
||||
}
|
||||
|
||||
return migratePrefsToVault(v, b)
|
||||
if err := migratePrefsToVault(v, b); err != nil {
|
||||
return fmt.Errorf("failed to migrate prefs to vault: %w", err)
|
||||
}
|
||||
|
||||
logrus.Info("Migrating TLS certificate")
|
||||
|
||||
certPEM, err := os.ReadFile(filepath.Join(configDir, "protonmail", "bridge", "cert.pem"))
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("failed to read old cert file: %w", err)
|
||||
}
|
||||
|
||||
keyPEM, err := os.ReadFile(filepath.Join(configDir, "protonmail", "bridge", "key.pem"))
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("failed to read old key file: %w", err)
|
||||
}
|
||||
|
||||
return v.SetBridgeTLSCertKey(certPEM, keyPEM)
|
||||
}
|
||||
|
||||
func migrateOldAccounts(locations *locations.Locations, v *vault.Vault) error {
|
||||
@ -187,7 +212,6 @@ func migrateOldAccount(userID string, store *credentials.Store, v *vault.Vault)
|
||||
return nil
|
||||
}
|
||||
|
||||
// nolint:funlen
|
||||
func migratePrefsToVault(vault *vault.Vault, b []byte) error {
|
||||
var prefs struct {
|
||||
IMAPPort int `json:"user_port_imap,,string"`
|
||||
@ -265,14 +289,6 @@ func migratePrefsToVault(vault *vault.Vault, b []byte) error {
|
||||
errs = multierror.Append(errs, fmt.Errorf("failed to migrate show all mail: %w", err))
|
||||
}
|
||||
|
||||
if err := vault.SetSyncWorkers(prefs.FetchWorkers); err != nil {
|
||||
errs = multierror.Append(errs, fmt.Errorf("failed to migrate sync workers: %w", err))
|
||||
}
|
||||
|
||||
if err := vault.SetSyncAttPool(prefs.AttachmentWorkers); err != nil {
|
||||
errs = multierror.Append(errs, fmt.Errorf("failed to migrate sync attachment pool: %w", err))
|
||||
}
|
||||
|
||||
if err := vault.SetCookies([]byte(prefs.Cookies)); err != nil {
|
||||
errs = multierror.Append(errs, fmt.Errorf("failed to migrate cookies: %w", err))
|
||||
}
|
||||
|
||||
@ -25,6 +25,7 @@ import (
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/cookies"
|
||||
@ -38,53 +39,49 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMigratePrefsToVault(t *testing.T) {
|
||||
func TestMigratePrefsToVaultWithKeys(t *testing.T) {
|
||||
// Create a new vault.
|
||||
vault, corrupt, err := vault.New(t.TempDir(), t.TempDir(), []byte("my secret key"))
|
||||
vault, corrupt, err := vault.New(t.TempDir(), t.TempDir(), []byte("my secret key"), async.NoopPanicHandler{})
|
||||
require.NoError(t, err)
|
||||
require.False(t, corrupt)
|
||||
|
||||
// load the old prefs file.
|
||||
b, err := os.ReadFile(filepath.Join("testdata", "prefs.json"))
|
||||
require.NoError(t, err)
|
||||
configDir := filepath.Join("testdata", "with_keys")
|
||||
|
||||
// Migrate the old prefs file to the new vault.
|
||||
require.NoError(t, migratePrefsToVault(vault, b))
|
||||
require.NoError(t, migrateOldSettingsWithDir(configDir, vault))
|
||||
|
||||
// Check that the IMAP and SMTP prefs are migrated.
|
||||
require.Equal(t, 2143, vault.GetIMAPPort())
|
||||
require.Equal(t, 2025, vault.GetSMTPPort())
|
||||
require.True(t, vault.GetSMTPSSL())
|
||||
// Check Json Settings
|
||||
validateJSONPrefs(t, vault)
|
||||
|
||||
// Check that the update channel is migrated.
|
||||
require.True(t, vault.GetAutoUpdate())
|
||||
require.Equal(t, updater.EarlyChannel, vault.GetUpdateChannel())
|
||||
require.Equal(t, 0.4849529004202015, vault.GetUpdateRollout())
|
||||
cert, key := vault.GetBridgeTLSCert()
|
||||
// Check the keys were found and collected.
|
||||
require.Equal(t, "-----BEGIN CERTIFICATE-----", string(cert))
|
||||
require.Equal(t, "-----BEGIN RSA PRIVATE KEY-----", string(key))
|
||||
}
|
||||
|
||||
// Check that the app settings have been migrated.
|
||||
require.False(t, vault.GetFirstStart())
|
||||
require.Equal(t, "blablabla", vault.GetColorScheme())
|
||||
require.Equal(t, "2.3.0+git", vault.GetLastVersion().String())
|
||||
require.True(t, vault.GetAutostart())
|
||||
|
||||
// Check that the other app settings have been migrated.
|
||||
require.Equal(t, 16, vault.SyncWorkers())
|
||||
require.Equal(t, 16, vault.SyncAttPool())
|
||||
require.False(t, vault.GetProxyAllowed())
|
||||
require.False(t, vault.GetShowAllMail())
|
||||
|
||||
// Check that the cookies have been migrated.
|
||||
jar, err := cookiejar.New(nil)
|
||||
func TestMigratePrefsToVaultWithoutKeys(t *testing.T) {
|
||||
// Create a new vault.
|
||||
vault, corrupt, err := vault.New(t.TempDir(), t.TempDir(), []byte("my secret key"), async.NoopPanicHandler{})
|
||||
require.NoError(t, err)
|
||||
require.False(t, corrupt)
|
||||
|
||||
cookies, err := cookies.NewCookieJar(jar, vault)
|
||||
require.NoError(t, err)
|
||||
// load the old prefs file.
|
||||
configDir := filepath.Join("testdata", "without_keys")
|
||||
|
||||
url, err := url.Parse("https://api.protonmail.ch")
|
||||
require.NoError(t, err)
|
||||
// Migrate the old prefs file to the new vault.
|
||||
require.NoError(t, migrateOldSettingsWithDir(configDir, vault))
|
||||
|
||||
// There should be a cookie for the API.
|
||||
require.NotEmpty(t, cookies.Cookies(url))
|
||||
// Migrate the old prefs file to the new vault.
|
||||
require.NoError(t, migrateOldSettingsWithDir(configDir, vault))
|
||||
|
||||
// Check Json Settings
|
||||
validateJSONPrefs(t, vault)
|
||||
|
||||
// Check the keys were found and collected.
|
||||
cert, key := vault.GetBridgeTLSCert()
|
||||
require.NotEqual(t, []byte("-----BEGIN CERTIFICATE-----"), cert)
|
||||
require.NotEqual(t, []byte("-----BEGIN RSA PRIVATE KEY-----"), key)
|
||||
}
|
||||
|
||||
func TestKeychainMigration(t *testing.T) {
|
||||
@ -101,7 +98,7 @@ func TestKeychainMigration(t *testing.T) {
|
||||
oldCacheDir := filepath.Join(tmpDir, "protonmail", "bridge")
|
||||
require.NoError(t, os.MkdirAll(oldCacheDir, 0o700))
|
||||
|
||||
oldPrefs, err := os.ReadFile(filepath.Join("testdata", "prefs.json"))
|
||||
oldPrefs, err := os.ReadFile(filepath.Join("testdata", "without_keys", "protonmail", "bridge", "prefs.json"))
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, os.WriteFile(
|
||||
@ -177,7 +174,7 @@ func TestUserMigration(t *testing.T) {
|
||||
token, err := crypto.RandomToken(32)
|
||||
require.NoError(t, err)
|
||||
|
||||
v, corrupt, err := vault.New(settingsFolder, settingsFolder, token)
|
||||
v, corrupt, err := vault.New(settingsFolder, settingsFolder, token, async.NoopPanicHandler{})
|
||||
require.NoError(t, err)
|
||||
require.False(t, corrupt)
|
||||
|
||||
@ -196,3 +193,38 @@ func TestUserMigration(t *testing.T) {
|
||||
require.Equal(t, vault.CombinedMode, u.AddressMode())
|
||||
}))
|
||||
}
|
||||
|
||||
func validateJSONPrefs(t *testing.T, vault *vault.Vault) {
|
||||
// Check that the IMAP and SMTP prefs are migrated.
|
||||
require.Equal(t, 2143, vault.GetIMAPPort())
|
||||
require.Equal(t, 2025, vault.GetSMTPPort())
|
||||
require.True(t, vault.GetSMTPSSL())
|
||||
|
||||
// Check that the update channel is migrated.
|
||||
require.True(t, vault.GetAutoUpdate())
|
||||
require.Equal(t, updater.EarlyChannel, vault.GetUpdateChannel())
|
||||
require.Equal(t, 0.4849529004202015, vault.GetUpdateRollout())
|
||||
|
||||
// Check that the app settings have been migrated.
|
||||
require.False(t, vault.GetFirstStart())
|
||||
require.Equal(t, "blablabla", vault.GetColorScheme())
|
||||
require.Equal(t, "2.3.0+git", vault.GetLastVersion().String())
|
||||
require.True(t, vault.GetAutostart())
|
||||
|
||||
// Check that the other app settings have been migrated.
|
||||
require.False(t, vault.GetProxyAllowed())
|
||||
require.False(t, vault.GetShowAllMail())
|
||||
|
||||
// Check that the cookies have been migrated.
|
||||
jar, err := cookiejar.New(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
cookies, err := cookies.NewCookieJar(jar, vault)
|
||||
require.NoError(t, err)
|
||||
|
||||
url, err := url.Parse("https://api.protonmail.ch")
|
||||
require.NoError(t, err)
|
||||
|
||||
// There should be a cookie for the API.
|
||||
require.NotEmpty(t, cookies.Cookies(url))
|
||||
}
|
||||
|
||||
@ -34,17 +34,17 @@ import (
|
||||
//
|
||||
// For macOS and Linux when already running version is older than this instance
|
||||
// it will kill old and continue with this new bridge (i.e. no error returned).
|
||||
func checkSingleInstance(lockFilePath string, curVersion *semver.Version) (*os.File, error) {
|
||||
func checkSingleInstance(settingPath, lockFilePath string, curVersion *semver.Version) (*os.File, error) {
|
||||
if lock, err := singleinstance.CreateLockFile(lockFilePath); err == nil {
|
||||
logrus.WithField("path", lockFilePath).Debug("Created lock file; no other instance is running")
|
||||
return lock, nil
|
||||
}
|
||||
|
||||
logrus.Debug("Failed to create lock file; another instance is running")
|
||||
logrus.Warn("Failed to create lock file; another instance is running")
|
||||
|
||||
// We couldn't create the lock file, so another instance is probably running.
|
||||
// Check if it's an older version of the app.
|
||||
lastVersion, ok := focus.TryVersion()
|
||||
lastVersion, ok := focus.TryVersion(settingPath)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to determine version of running instance")
|
||||
}
|
||||
|
||||
1
internal/app/testdata/with_keys/protonmail/bridge/cert.pem
vendored
Normal file
1
internal/app/testdata/with_keys/protonmail/bridge/cert.pem
vendored
Normal file
@ -0,0 +1 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
1
internal/app/testdata/with_keys/protonmail/bridge/key.pem
vendored
Normal file
1
internal/app/testdata/with_keys/protonmail/bridge/key.pem
vendored
Normal file
@ -0,0 +1 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
31
internal/app/testdata/without_keys/protonmail/bridge/prefs.json
vendored
Normal file
31
internal/app/testdata/without_keys/protonmail/bridge/prefs.json
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
{
|
||||
"allow_proxy": "false",
|
||||
"attachment_workers": "16",
|
||||
"autostart": "true",
|
||||
"autoupdate": "true",
|
||||
"cache_compression": "true",
|
||||
"cache_concurrent_read": "16",
|
||||
"cache_concurrent_write": "16",
|
||||
"cache_enabled": "true",
|
||||
"cache_location": "/home/user/.config/protonmail/bridge/cache/c11/messages",
|
||||
"cache_min_free_abs": "250000000",
|
||||
"cache_min_free_rat": "",
|
||||
"color_scheme": "blablabla",
|
||||
"cookies": "{\"https://api.protonmail.ch\":[{\"Name\":\"Session-Id\",\"Value\":\"blablablablablablablablabla\",\"Path\":\"/\",\"Domain\":\"protonmail.ch\",\"Expires\":\"2023-02-19T00:20:40.269424437+01:00\",\"RawExpires\":\"\",\"MaxAge\":7776000,\"Secure\":true,\"HttpOnly\":true,\"SameSite\":0,\"Raw\":\"Session-Id=blablablablablablablablabla; Domain=protonmail.ch; Path=/; HttpOnly; Secure; Max-Age=7776000\",\"Unparsed\":null},{\"Name\":\"Tag\",\"Value\":\"default\",\"Path\":\"/\",\"Domain\":\"\",\"Expires\":\"2023-02-19T00:20:40.269428627+01:00\",\"RawExpires\":\"\",\"MaxAge\":7776000,\"Secure\":true,\"HttpOnly\":false,\"SameSite\":0,\"Raw\":\"Tag=default; Path=/; Secure; Max-Age=7776000\",\"Unparsed\":null}],\"https://protonmail.com\":[{\"Name\":\"Session-Id\",\"Value\":\"blablablablablablablablabla\",\"Path\":\"/\",\"Domain\":\"protonmail.com\",\"Expires\":\"2023-02-19T00:20:18.315084712+01:00\",\"RawExpires\":\"\",\"MaxAge\":7776000,\"Secure\":true,\"HttpOnly\":true,\"SameSite\":0,\"Raw\":\"Session-Id=Y3q2Mh-ClvqL6LWeYdfyPgAAABI; Domain=protonmail.com; Path=/; HttpOnly; Secure; Max-Age=7776000\",\"Unparsed\":null},{\"Name\":\"Tag\",\"Value\":\"redirect\",\"Path\":\"/\",\"Domain\":\"\",\"Expires\":\"2023-02-19T00:20:18.315087646+01:00\",\"RawExpires\":\"\",\"MaxAge\":7776000,\"Secure\":true,\"HttpOnly\":false,\"SameSite\":0,\"Raw\":\"Tag=redirect; Path=/; Secure; Max-Age=7776000\",\"Unparsed\":null}]}",
|
||||
"fetch_workers": "16",
|
||||
"first_time_start": "false",
|
||||
"first_time_start_gui": "true",
|
||||
"imap_workers": "16",
|
||||
"is_all_mail_visible": "false",
|
||||
"last_heartbeat": "325",
|
||||
"last_used_version": "2.3.0+git",
|
||||
"preferred_keychain": "secret-service",
|
||||
"rebranding_migrated": "true",
|
||||
"report_outgoing_email_without_encryption": "false",
|
||||
"rollout": "0.4849529004202015",
|
||||
"user_port_api": "1042",
|
||||
"update_channel": "early",
|
||||
"user_port_imap": "2143",
|
||||
"user_port_smtp": "2025",
|
||||
"user_ssl_smtp": "true"
|
||||
}
|
||||
@ -18,26 +18,24 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"path"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/certs"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
func WithVault(locations *locations.Locations, fn func(*vault.Vault, bool, bool) error) error {
|
||||
func WithVault(locations *locations.Locations, panicHandler async.PanicHandler, fn func(*vault.Vault, bool, bool) error) error {
|
||||
logrus.Debug("Creating vault")
|
||||
defer logrus.Debug("Vault stopped")
|
||||
|
||||
// Create the encVault.
|
||||
encVault, insecure, corrupt, err := newVault(locations)
|
||||
encVault, insecure, corrupt, err := newVault(locations, panicHandler)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create vault: %w", err)
|
||||
}
|
||||
@ -51,7 +49,9 @@ func WithVault(locations *locations.Locations, fn func(*vault.Vault, bool, bool)
|
||||
if installed := encVault.GetCertsInstalled(); !installed {
|
||||
logrus.Debug("Installing certificates")
|
||||
|
||||
if err := certs.NewInstaller().InstallCert(encVault.GetBridgeTLSCert()); err != nil {
|
||||
certPEM, _ := encVault.GetBridgeTLSCert()
|
||||
|
||||
if err := certs.NewInstaller().InstallCert(certPEM); err != nil {
|
||||
return fmt.Errorf("failed to install certs: %w", err)
|
||||
}
|
||||
|
||||
@ -67,7 +67,7 @@ func WithVault(locations *locations.Locations, fn func(*vault.Vault, bool, bool)
|
||||
return fn(encVault, insecure, corrupt)
|
||||
}
|
||||
|
||||
func newVault(locations *locations.Locations) (*vault.Vault, bool, bool, error) {
|
||||
func newVault(locations *locations.Locations, panicHandler async.PanicHandler) (*vault.Vault, bool, bool, error) {
|
||||
vaultDir, err := locations.ProvideSettingsPath()
|
||||
if err != nil {
|
||||
return nil, false, false, fmt.Errorf("could not get vault dir: %w", err)
|
||||
@ -80,7 +80,8 @@ func newVault(locations *locations.Locations) (*vault.Vault, bool, bool, error)
|
||||
insecure bool
|
||||
)
|
||||
|
||||
if key, err := getVaultKey(vaultDir); err != nil {
|
||||
if key, err := loadVaultKey(vaultDir); err != nil {
|
||||
logrus.WithError(err).Error("Could not load/create vault key")
|
||||
insecure = true
|
||||
|
||||
// We store the insecure vault in a separate directory
|
||||
@ -94,7 +95,7 @@ func newVault(locations *locations.Locations) (*vault.Vault, bool, bool, error)
|
||||
return nil, false, false, fmt.Errorf("could not provide gluon path: %w", err)
|
||||
}
|
||||
|
||||
vault, corrupt, err := vault.New(vaultDir, gluonCacheDir, vaultKey)
|
||||
vault, corrupt, err := vault.New(vaultDir, gluonCacheDir, vaultKey, panicHandler)
|
||||
if err != nil {
|
||||
return nil, false, false, fmt.Errorf("could not create vault: %w", err)
|
||||
}
|
||||
@ -102,42 +103,25 @@ func newVault(locations *locations.Locations) (*vault.Vault, bool, bool, error)
|
||||
return vault, insecure, corrupt, nil
|
||||
}
|
||||
|
||||
func getVaultKey(vaultDir string) ([]byte, error) {
|
||||
func loadVaultKey(vaultDir string) ([]byte, error) {
|
||||
helper, err := vault.GetHelper(vaultDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get keychain helper: %w", err)
|
||||
}
|
||||
|
||||
keychain, err := keychain.NewKeychain(helper, constants.KeyChainName)
|
||||
kc, err := keychain.NewKeychain(helper, constants.KeyChainName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create keychain: %w", err)
|
||||
}
|
||||
|
||||
secrets, err := keychain.List()
|
||||
has, err := vault.HasVaultKey(kc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not list keychain: %w", err)
|
||||
return nil, fmt.Errorf("could not check for vault key: %w", err)
|
||||
}
|
||||
|
||||
if !slices.Contains(secrets, vaultSecretName) {
|
||||
tok, err := crypto.RandomToken(32)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not generate random token: %w", err)
|
||||
}
|
||||
|
||||
if err := keychain.Put(vaultSecretName, base64.StdEncoding.EncodeToString(tok)); err != nil {
|
||||
return nil, fmt.Errorf("could not put keychain item: %w", err)
|
||||
}
|
||||
if has {
|
||||
return vault.GetVaultKey(kc)
|
||||
}
|
||||
|
||||
_, keyEnc, err := keychain.Get(vaultSecretName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get keychain item: %w", err)
|
||||
}
|
||||
|
||||
keyDec, err := base64.StdEncoding.DecodeString(keyEnc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not decode keychain item: %w", err)
|
||||
}
|
||||
|
||||
return keyDec, nil
|
||||
return vault.NewVaultKey(kc)
|
||||
}
|
||||
|
||||
@ -1,82 +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 async
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Abortable collects groups of functions that can be aborted by calling Abort.
|
||||
type Abortable struct {
|
||||
abortFunc []context.CancelFunc
|
||||
abortLock sync.RWMutex
|
||||
}
|
||||
|
||||
func (a *Abortable) Do(ctx context.Context, fn func(context.Context)) {
|
||||
fn(a.newCancelCtx(ctx))
|
||||
}
|
||||
|
||||
func (a *Abortable) Abort() {
|
||||
a.abortLock.RLock()
|
||||
defer a.abortLock.RUnlock()
|
||||
|
||||
for _, fn := range a.abortFunc {
|
||||
fn()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Abortable) newCancelCtx(ctx context.Context) context.Context {
|
||||
a.abortLock.Lock()
|
||||
defer a.abortLock.Unlock()
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
|
||||
a.abortFunc = append(a.abortFunc, cancel)
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
// RangeContext iterates over the given channel until the context is canceled or the
|
||||
// channel is closed.
|
||||
func RangeContext[T any](ctx context.Context, ch <-chan T, fn func(T)) {
|
||||
for {
|
||||
select {
|
||||
case v, ok := <-ch:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
fn(v)
|
||||
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ForwardContext forwards all values from the src channel to the dst channel until the
|
||||
// context is canceled or the src channel is closed.
|
||||
func ForwardContext[T any](ctx context.Context, dst chan<- T, src <-chan T) {
|
||||
RangeContext(ctx, src, func(v T) {
|
||||
select {
|
||||
case dst <- v:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -1,233 +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 async
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type PanicHandler interface {
|
||||
HandlePanic()
|
||||
}
|
||||
|
||||
// Group is forked and improved version of "github.com/bradenaw/juniper/xsync.Group".
|
||||
//
|
||||
// It manages a group of goroutines. The main change to original is posibility
|
||||
// to wait passed function to finish without canceling it's context and adding
|
||||
// PanicHandler.
|
||||
type Group struct {
|
||||
baseCtx context.Context
|
||||
ctx context.Context
|
||||
jobCtx context.Context
|
||||
cancel context.CancelFunc
|
||||
finish context.CancelFunc
|
||||
wg sync.WaitGroup
|
||||
|
||||
panicHandler PanicHandler
|
||||
}
|
||||
|
||||
// NewGroup returns a Group ready for use. The context passed to any of the f functions will be a
|
||||
// descendant of ctx.
|
||||
func NewGroup(ctx context.Context, panicHandler PanicHandler) *Group {
|
||||
bgCtx, cancel := context.WithCancel(ctx)
|
||||
jobCtx, finish := context.WithCancel(ctx)
|
||||
return &Group{
|
||||
baseCtx: ctx,
|
||||
ctx: bgCtx,
|
||||
jobCtx: jobCtx,
|
||||
cancel: cancel,
|
||||
finish: finish,
|
||||
panicHandler: panicHandler,
|
||||
}
|
||||
}
|
||||
|
||||
// Once calls f once from another goroutine.
|
||||
func (g *Group) Once(f func(ctx context.Context)) {
|
||||
g.wg.Add(1)
|
||||
go func() {
|
||||
defer g.handlePanic()
|
||||
|
||||
f(g.ctx)
|
||||
g.wg.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
// jitterDuration returns a random duration in [d - jitter, d + jitter].
|
||||
func jitterDuration(d time.Duration, jitter time.Duration) time.Duration {
|
||||
return d + time.Duration(float64(jitter)*((rand.Float64()*2)-1)) //nolint:gosec
|
||||
}
|
||||
|
||||
// Periodic spawns a goroutine that calls f once per interval +/- jitter.
|
||||
func (g *Group) Periodic(
|
||||
interval time.Duration,
|
||||
jitter time.Duration,
|
||||
f func(ctx context.Context),
|
||||
) {
|
||||
g.wg.Add(1)
|
||||
go func() {
|
||||
defer g.handlePanic()
|
||||
|
||||
defer g.wg.Done()
|
||||
|
||||
t := time.NewTimer(jitterDuration(interval, jitter))
|
||||
defer t.Stop()
|
||||
|
||||
for {
|
||||
if g.ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case <-g.jobCtx.Done():
|
||||
return
|
||||
case <-t.C:
|
||||
}
|
||||
|
||||
t.Reset(jitterDuration(interval, jitter))
|
||||
f(g.ctx)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Trigger spawns a goroutine which calls f whenever the returned function is called. If f is
|
||||
// already running when triggered, f will run again immediately when it finishes.
|
||||
func (g *Group) Trigger(f func(ctx context.Context)) func() {
|
||||
c := make(chan struct{}, 1)
|
||||
g.wg.Add(1)
|
||||
go func() {
|
||||
defer g.handlePanic()
|
||||
|
||||
defer g.wg.Done()
|
||||
|
||||
for {
|
||||
if g.ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-g.jobCtx.Done():
|
||||
return
|
||||
case <-c:
|
||||
}
|
||||
f(g.ctx)
|
||||
}
|
||||
}()
|
||||
|
||||
return func() {
|
||||
select {
|
||||
case c <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PeriodicOrTrigger spawns a goroutine which calls f whenever the returned function is called. If
|
||||
// f is already running when triggered, f will run again immediately when it finishes. Also calls f
|
||||
// when it has been interval+/-jitter since the last trigger.
|
||||
func (g *Group) PeriodicOrTrigger(
|
||||
interval time.Duration,
|
||||
jitter time.Duration,
|
||||
f func(ctx context.Context),
|
||||
) func() {
|
||||
c := make(chan struct{}, 1)
|
||||
g.wg.Add(1)
|
||||
go func() {
|
||||
defer g.handlePanic()
|
||||
|
||||
defer g.wg.Done()
|
||||
|
||||
t := time.NewTimer(jitterDuration(interval, jitter))
|
||||
defer t.Stop()
|
||||
|
||||
for {
|
||||
if g.ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-g.jobCtx.Done():
|
||||
return
|
||||
case <-t.C:
|
||||
t.Reset(jitterDuration(interval, jitter))
|
||||
case <-c:
|
||||
if !t.Stop() {
|
||||
<-t.C
|
||||
}
|
||||
t.Reset(jitterDuration(interval, jitter))
|
||||
}
|
||||
f(g.ctx)
|
||||
}
|
||||
}()
|
||||
|
||||
return func() {
|
||||
select {
|
||||
case c <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Group) resetCtx() {
|
||||
g.jobCtx, g.finish = context.WithCancel(g.baseCtx)
|
||||
g.ctx, g.cancel = context.WithCancel(g.baseCtx)
|
||||
}
|
||||
|
||||
// Cancel is send to all of the spawn goroutines and ends periodic
|
||||
// or trigger routines.
|
||||
func (g *Group) Cancel() {
|
||||
g.cancel()
|
||||
g.finish()
|
||||
g.resetCtx()
|
||||
}
|
||||
|
||||
// Finish will ends all periodic or polls routines. It will let
|
||||
// currently running functions to finish (cancel is not sent).
|
||||
//
|
||||
// It is not safe to call Wait concurrently with any other method on g.
|
||||
func (g *Group) Finish() {
|
||||
g.finish()
|
||||
g.jobCtx, g.finish = context.WithCancel(g.baseCtx)
|
||||
}
|
||||
|
||||
// CancelAndWait cancels the context passed to any of the spawned goroutines and waits for all spawned
|
||||
// goroutines to exit.
|
||||
//
|
||||
// It is not safe to call Wait concurrently with any other method on g.
|
||||
func (g *Group) CancelAndWait() {
|
||||
g.finish()
|
||||
g.cancel()
|
||||
g.wg.Wait()
|
||||
g.resetCtx()
|
||||
}
|
||||
|
||||
// WaitToFinish will ends all periodic or polls routines. It will wait for
|
||||
// currently running functions to finish (cancel is not sent).
|
||||
//
|
||||
// It is not safe to call Wait concurrently with any other method on g.
|
||||
func (g *Group) WaitToFinish() {
|
||||
g.finish()
|
||||
g.wg.Wait()
|
||||
g.jobCtx, g.finish = context.WithCancel(g.baseCtx)
|
||||
}
|
||||
|
||||
func (g *Group) handlePanic() {
|
||||
if g.panicHandler != nil {
|
||||
g.panicHandler.HandlePanic()
|
||||
}
|
||||
}
|
||||
@ -21,6 +21,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/sirupsen/logrus"
|
||||
@ -32,14 +33,14 @@ func defaultAPIOptions(
|
||||
version *semver.Version,
|
||||
cookieJar http.CookieJar,
|
||||
transport http.RoundTripper,
|
||||
poolSize int,
|
||||
panicHandler async.PanicHandler,
|
||||
) []proton.Option {
|
||||
return []proton.Option{
|
||||
proton.WithHostURL(apiURL),
|
||||
proton.WithAppVersion(constants.AppVersion(version.Original())),
|
||||
proton.WithCookieJar(cookieJar),
|
||||
proton.WithTransport(transport),
|
||||
proton.WithAttPoolSize(poolSize),
|
||||
proton.WithLogger(logrus.StandardLogger()),
|
||||
proton.WithPanicHandler(panicHandler),
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,6 +23,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
)
|
||||
|
||||
@ -32,7 +33,7 @@ func newAPIOptions(
|
||||
version *semver.Version,
|
||||
cookieJar http.CookieJar,
|
||||
transport http.RoundTripper,
|
||||
poolSize int,
|
||||
panicHandler async.PanicHandler,
|
||||
) []proton.Option {
|
||||
return defaultAPIOptions(apiURL, version, cookieJar, transport, poolSize)
|
||||
return defaultAPIOptions(apiURL, version, cookieJar, transport, panicHandler)
|
||||
}
|
||||
|
||||
@ -24,6 +24,7 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
)
|
||||
|
||||
@ -33,9 +34,9 @@ func newAPIOptions(
|
||||
version *semver.Version,
|
||||
cookieJar http.CookieJar,
|
||||
transport http.RoundTripper,
|
||||
poolSize int,
|
||||
panicHandler async.PanicHandler,
|
||||
) []proton.Option {
|
||||
opt := defaultAPIOptions(apiURL, version, cookieJar, transport, poolSize)
|
||||
opt := defaultAPIOptions(apiURL, version, cookieJar, transport, panicHandler)
|
||||
|
||||
if host := os.Getenv("BRIDGE_API_HOST"); host != "" {
|
||||
opt = append(opt, proton.WithHostURL(host))
|
||||
|
||||
@ -30,11 +30,12 @@ import (
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/gluon"
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
imapEvents "github.com/ProtonMail/gluon/events"
|
||||
"github.com/ProtonMail/gluon/imap"
|
||||
"github.com/ProtonMail/gluon/reporter"
|
||||
"github.com/ProtonMail/gluon/watcher"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/async"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/focus"
|
||||
@ -92,8 +93,8 @@ type Bridge struct {
|
||||
// locator is the bridge's locator.
|
||||
locator Locator
|
||||
|
||||
// crashHandler
|
||||
crashHandler async.PanicHandler
|
||||
// panicHandler
|
||||
panicHandler async.PanicHandler
|
||||
|
||||
// reporter
|
||||
reporter reporter.Reporter
|
||||
@ -124,10 +125,12 @@ type Bridge struct {
|
||||
|
||||
// goUpdate triggers a check/install of updates.
|
||||
goUpdate func()
|
||||
|
||||
uidValidityGenerator imap.UIDValidityGenerator
|
||||
}
|
||||
|
||||
// New creates a new bridge.
|
||||
func New( //nolint:funlen
|
||||
func New(
|
||||
locator Locator, // the locator to provide paths to store data
|
||||
vault *vault.Vault, // the bridge's encrypted data store
|
||||
autostarter Autostarter, // the autostarter to manage autostart settings
|
||||
@ -140,17 +143,18 @@ func New( //nolint:funlen
|
||||
tlsReporter TLSReporter, // the TLS reporter to report TLS errors
|
||||
roundTripper http.RoundTripper, // the round tripper to use for API requests
|
||||
proxyCtl ProxyController, // the DoH controller
|
||||
crashHandler async.PanicHandler,
|
||||
panicHandler async.PanicHandler,
|
||||
reporter reporter.Reporter,
|
||||
uidValidityGenerator imap.UIDValidityGenerator,
|
||||
|
||||
logIMAPClient, logIMAPServer bool, // whether to log IMAP client/server activity
|
||||
logSMTP bool, // whether to log SMTP activity
|
||||
) (*Bridge, <-chan events.Event, error) {
|
||||
// api is the user's API manager.
|
||||
api := proton.New(newAPIOptions(apiURL, curVersion, cookieJar, roundTripper, vault.SyncAttPool())...)
|
||||
api := proton.New(newAPIOptions(apiURL, curVersion, cookieJar, roundTripper, panicHandler)...)
|
||||
|
||||
// tasks holds all the bridge's background tasks.
|
||||
tasks := async.NewGroup(context.Background(), crashHandler)
|
||||
tasks := async.NewGroup(context.Background(), panicHandler)
|
||||
|
||||
// imapEventCh forwards IMAP events from gluon instances to the bridge for processing.
|
||||
imapEventCh := make(chan imapEvents.Event)
|
||||
@ -165,12 +169,13 @@ func New( //nolint:funlen
|
||||
autostarter,
|
||||
updater,
|
||||
curVersion,
|
||||
crashHandler,
|
||||
panicHandler,
|
||||
reporter,
|
||||
|
||||
api,
|
||||
identifier,
|
||||
proxyCtl,
|
||||
uidValidityGenerator,
|
||||
logIMAPClient, logIMAPServer, logSMTP,
|
||||
)
|
||||
if err != nil {
|
||||
@ -185,22 +190,9 @@ func New( //nolint:funlen
|
||||
return nil, nil, fmt.Errorf("failed to initialize bridge: %w", err)
|
||||
}
|
||||
|
||||
// Start serving IMAP.
|
||||
if err := bridge.serveIMAP(); err != nil {
|
||||
logrus.WithError(err).Error("IMAP error")
|
||||
bridge.PushError(ErrServeIMAP)
|
||||
}
|
||||
|
||||
// Start serving SMTP.
|
||||
if err := bridge.serveSMTP(); err != nil {
|
||||
logrus.WithError(err).Error("SMTP error")
|
||||
bridge.PushError(ErrServeSMTP)
|
||||
}
|
||||
|
||||
return bridge, eventCh, nil
|
||||
}
|
||||
|
||||
// nolint:funlen
|
||||
func newBridge(
|
||||
tasks *async.Group,
|
||||
imapEventCh chan imapEvents.Event,
|
||||
@ -210,12 +202,13 @@ func newBridge(
|
||||
autostarter Autostarter,
|
||||
updater Updater,
|
||||
curVersion *semver.Version,
|
||||
crashHandler async.PanicHandler,
|
||||
panicHandler async.PanicHandler,
|
||||
reporter reporter.Reporter,
|
||||
|
||||
api *proton.Manager,
|
||||
identifier Identifier,
|
||||
proxyCtl ProxyController,
|
||||
uidValidityGenerator imap.UIDValidityGenerator,
|
||||
|
||||
logIMAPClient, logIMAPServer, logSMTP bool,
|
||||
) (*Bridge, error) {
|
||||
@ -254,12 +247,14 @@ func newBridge(
|
||||
logIMAPServer,
|
||||
imapEventCh,
|
||||
tasks,
|
||||
uidValidityGenerator,
|
||||
panicHandler,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create IMAP server: %w", err)
|
||||
}
|
||||
|
||||
focusService, err := focus.NewService(curVersion)
|
||||
focusService, err := focus.NewService(locator, curVersion, panicHandler)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create focus service: %w", err)
|
||||
}
|
||||
@ -285,7 +280,7 @@ func newBridge(
|
||||
newVersion: curVersion,
|
||||
newVersionLock: safe.NewRWMutex(),
|
||||
|
||||
crashHandler: crashHandler,
|
||||
panicHandler: panicHandler,
|
||||
reporter: reporter,
|
||||
|
||||
focusService: focusService,
|
||||
@ -300,6 +295,8 @@ func newBridge(
|
||||
lastVersion: lastVersion,
|
||||
|
||||
tasks: tasks,
|
||||
|
||||
uidValidityGenerator: uidValidityGenerator,
|
||||
}
|
||||
|
||||
bridge.smtpServer = newSMTPServer(bridge, tlsConfig, logSMTP)
|
||||
@ -307,7 +304,6 @@ func newBridge(
|
||||
return bridge, nil
|
||||
}
|
||||
|
||||
// nolint:funlen
|
||||
func (bridge *Bridge) init(tlsReporter TLSReporter) error {
|
||||
// Enable or disable the proxy at startup.
|
||||
if bridge.vault.GetProxyAllowed() {
|
||||
@ -376,16 +372,32 @@ func (bridge *Bridge) init(tlsReporter TLSReporter) error {
|
||||
})
|
||||
})
|
||||
|
||||
// Attempt to lazy load users when triggered.
|
||||
// We need to load users before we can start the IMAP and SMTP servers.
|
||||
// We must only start the servers once.
|
||||
var once sync.Once
|
||||
|
||||
// Attempt to load users from the vault when triggered.
|
||||
bridge.goLoad = bridge.tasks.Trigger(func(ctx context.Context) {
|
||||
if err := bridge.loadUsers(ctx); err != nil {
|
||||
logrus.WithError(err).Error("Failed to load users")
|
||||
if netErr := new(proton.NetError); !errors.As(err, &netErr) {
|
||||
sentry.ReportError(bridge.reporter, "Failed to load users", err)
|
||||
}
|
||||
} else {
|
||||
bridge.publish(events.AllUsersLoaded{})
|
||||
return
|
||||
}
|
||||
|
||||
bridge.publish(events.AllUsersLoaded{})
|
||||
|
||||
// Once all users have been loaded, start the bridge's IMAP and SMTP servers.
|
||||
once.Do(func() {
|
||||
if err := bridge.serveIMAP(); err != nil {
|
||||
logrus.WithError(err).Error("Failed to start IMAP server")
|
||||
}
|
||||
|
||||
if err := bridge.serveSMTP(); err != nil {
|
||||
logrus.WithError(err).Error("Failed to start SMTP server")
|
||||
}
|
||||
})
|
||||
})
|
||||
defer bridge.goLoad()
|
||||
|
||||
@ -484,7 +496,7 @@ func (bridge *Bridge) addWatcher(ofType ...events.Event) *watcher.Watcher[events
|
||||
bridge.watchersLock.Lock()
|
||||
defer bridge.watchersLock.Unlock()
|
||||
|
||||
watcher := watcher.New(ofType...)
|
||||
watcher := watcher.New(bridge.panicHandler, ofType...)
|
||||
|
||||
bridge.watchers = append(bridge.watchers, watcher)
|
||||
|
||||
@ -545,7 +557,7 @@ func (bridge *Bridge) onStatusDown(ctx context.Context) {
|
||||
}
|
||||
|
||||
func loadTLSConfig(vault *vault.Vault) (*tls.Config, error) {
|
||||
cert, err := tls.X509KeyPair(vault.GetBridgeTLSCert(), vault.GetBridgeTLSKey())
|
||||
cert, err := tls.X509KeyPair(vault.GetBridgeTLSCert())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -21,6 +21,7 @@ import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@ -29,6 +30,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
"github.com/ProtonMail/gluon/imap"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/go-proton-api/server"
|
||||
"github.com/ProtonMail/go-proton-api/server/backend"
|
||||
@ -121,8 +124,11 @@ func TestBridge_Focus(t *testing.T) {
|
||||
raiseCh, done := bridge.GetEvents(events.Raise{})
|
||||
defer done()
|
||||
|
||||
settingsFolder, err := locator.ProvideSettingsPath()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Simulate a focus event.
|
||||
focus.TryRaise()
|
||||
focus.TryRaise(settingsFolder)
|
||||
|
||||
// Wait for the event.
|
||||
require.IsType(t, events.Raise{}, <-raiseCh)
|
||||
@ -496,6 +502,21 @@ func TestBridge_InitGluonDirectory(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_LoginFailed(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
failCh, done := chToType[events.Event, events.IMAPLoginFailed](bridge.GetEvents(events.IMAPLoginFailed{}))
|
||||
defer done()
|
||||
|
||||
imapClient, err := client.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Error(t, imapClient.Login("badUser", "badPass"))
|
||||
require.Equal(t, "badUser", (<-failCh).Username)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_ChangeCacheDirectory(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||
userID, addrID, err := s.CreateUser("imap", password)
|
||||
@ -657,6 +678,9 @@ func withMocks(t *testing.T, tests func(*bridge.Mocks)) {
|
||||
tests(mocks)
|
||||
}
|
||||
|
||||
// Needs to be global to survive bridge shutdown/startup in unit tests as they happen to fast.
|
||||
var testUIDValidityGenerator = imap.DefaultEpochUIDValidityGenerator()
|
||||
|
||||
// withBridge creates a new bridge which points to the given API URL and uses the given keychain, and closes it when done.
|
||||
func withBridgeNoMocks(
|
||||
ctx context.Context,
|
||||
@ -676,7 +700,7 @@ func withBridgeNoMocks(
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create the vault.
|
||||
vault, _, err := vault.New(vaultDir, t.TempDir(), vaultKey)
|
||||
vault, _, err := vault.New(vaultDir, t.TempDir(), vaultKey, async.NoopPanicHandler{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a new cookie jar.
|
||||
@ -702,6 +726,7 @@ func withBridgeNoMocks(
|
||||
mocks.ProxyCtl,
|
||||
mocks.CrashHandler,
|
||||
mocks.Reporter,
|
||||
testUIDValidityGenerator,
|
||||
|
||||
// The logging stuff.
|
||||
os.Getenv("BRIDGE_LOG_IMAP_CLIENT") == "1",
|
||||
@ -713,6 +738,10 @@ func withBridgeNoMocks(
|
||||
|
||||
// Wait for bridge to finish loading users.
|
||||
waitForEvent(t, eventCh, events.AllUsersLoaded{})
|
||||
// Wait for bridge to start the IMAP server.
|
||||
waitForEvent(t, eventCh, events.IMAPServerReady{})
|
||||
// Wait for bridge to start the SMTP server.
|
||||
waitForEvent(t, eventCh, events.SMTPServerReady{})
|
||||
|
||||
// Set random IMAP and SMTP ports for the tests.
|
||||
require.NoError(t, bridge.SetIMAPPort(0))
|
||||
@ -742,7 +771,7 @@ func withBridge(
|
||||
})
|
||||
}
|
||||
|
||||
func waitForEvent[T any](t *testing.T, eventCh <-chan events.Event, wantEvent T) {
|
||||
func waitForEvent[T any](t *testing.T, eventCh <-chan events.Event, _ T) {
|
||||
t.Helper()
|
||||
|
||||
for event := range eventCh {
|
||||
|
||||
@ -37,7 +37,7 @@ const (
|
||||
MaxCompressedFilesCount = 6
|
||||
)
|
||||
|
||||
func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, description, username, email, client string, attachLogs bool) error { //nolint:funlen
|
||||
func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, description, username, email, client string, attachLogs bool) error {
|
||||
var account string
|
||||
|
||||
if info, err := bridge.QueryUserInfo(username); err == nil {
|
||||
|
||||
@ -22,10 +22,7 @@ import "errors"
|
||||
var (
|
||||
ErrVaultInsecure = errors.New("the vault is insecure")
|
||||
ErrVaultCorrupt = errors.New("the vault is corrupt")
|
||||
|
||||
ErrServeIMAP = errors.New("failed to serve IMAP")
|
||||
ErrServeSMTP = errors.New("failed to serve SMTP")
|
||||
ErrWatchUpdates = errors.New("failed to watch for updates")
|
||||
ErrWatchUpdates = errors.New("failed to watch for updates")
|
||||
|
||||
ErrNoSuchUser = errors.New("no such user")
|
||||
ErrUserAlreadyExists = errors.New("user already exists")
|
||||
|
||||
@ -27,11 +27,14 @@ import (
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/gluon"
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
imapEvents "github.com/ProtonMail/gluon/events"
|
||||
"github.com/ProtonMail/gluon/imap"
|
||||
"github.com/ProtonMail/gluon/reporter"
|
||||
"github.com/ProtonMail/gluon/store"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/async"
|
||||
"github.com/ProtonMail/gluon/store/fallback_v0"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
@ -44,26 +47,42 @@ const (
|
||||
)
|
||||
|
||||
func (bridge *Bridge) serveIMAP() error {
|
||||
if bridge.imapServer == nil {
|
||||
return fmt.Errorf("no imap server instance running")
|
||||
}
|
||||
port, err := func() (int, error) {
|
||||
if bridge.imapServer == nil {
|
||||
return 0, fmt.Errorf("no IMAP server instance running")
|
||||
}
|
||||
|
||||
logrus.Info("Starting IMAP server")
|
||||
logrus.Info("Starting IMAP server")
|
||||
|
||||
imapListener, err := newListener(bridge.vault.GetIMAPPort(), bridge.vault.GetIMAPSSL(), bridge.tlsConfig)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to create IMAP listener: %w", err)
|
||||
}
|
||||
|
||||
bridge.imapListener = imapListener
|
||||
|
||||
if err := bridge.imapServer.Serve(context.Background(), bridge.imapListener); err != nil {
|
||||
return 0, fmt.Errorf("failed to serve IMAP: %w", err)
|
||||
}
|
||||
|
||||
if err := bridge.vault.SetIMAPPort(getPort(imapListener.Addr())); err != nil {
|
||||
return 0, fmt.Errorf("failed to store IMAP port in vault: %w", err)
|
||||
}
|
||||
|
||||
return getPort(imapListener.Addr()), nil
|
||||
}()
|
||||
|
||||
imapListener, err := newListener(bridge.vault.GetIMAPPort(), bridge.vault.GetIMAPSSL(), bridge.tlsConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create IMAP listener: %w", err)
|
||||
bridge.publish(events.IMAPServerError{
|
||||
Error: err,
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
bridge.imapListener = imapListener
|
||||
|
||||
if err := bridge.imapServer.Serve(context.Background(), bridge.imapListener); err != nil {
|
||||
return fmt.Errorf("failed to serve IMAP: %w", err)
|
||||
}
|
||||
|
||||
if err := bridge.vault.SetIMAPPort(getPort(imapListener.Addr())); err != nil {
|
||||
return fmt.Errorf("failed to store IMAP port in vault: %w", err)
|
||||
}
|
||||
bridge.publish(events.IMAPServerReady{
|
||||
Port: port,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -75,6 +94,8 @@ func (bridge *Bridge) restartIMAP() error {
|
||||
if err := bridge.imapListener.Close(); err != nil {
|
||||
return fmt.Errorf("failed to close IMAP listener: %w", err)
|
||||
}
|
||||
|
||||
bridge.publish(events.IMAPServerStopped{})
|
||||
}
|
||||
|
||||
return bridge.serveIMAP()
|
||||
@ -87,6 +108,7 @@ func (bridge *Bridge) closeIMAP(ctx context.Context) error {
|
||||
if err := bridge.imapServer.Close(ctx); err != nil {
|
||||
return fmt.Errorf("failed to close IMAP server: %w", err)
|
||||
}
|
||||
|
||||
bridge.imapServer = nil
|
||||
}
|
||||
|
||||
@ -96,12 +118,12 @@ func (bridge *Bridge) closeIMAP(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
bridge.publish(events.IMAPServerStopped{})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// addIMAPUser connects the given user to gluon.
|
||||
//
|
||||
//nolint:funlen
|
||||
func (bridge *Bridge) addIMAPUser(ctx context.Context, user *user.User) error {
|
||||
if bridge.imapServer == nil {
|
||||
return fmt.Errorf("no imap server instance running")
|
||||
@ -242,6 +264,13 @@ func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) {
|
||||
if event.IMAPID.Name != "" && event.IMAPID.Version != "" {
|
||||
bridge.identifier.SetClient(event.IMAPID.Name, event.IMAPID.Version)
|
||||
}
|
||||
|
||||
case imapEvents.LoginFailed:
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"sessionID": event.SessionID,
|
||||
"username": event.Username,
|
||||
}).Info("Received IMAP login failure notification")
|
||||
bridge.publish(events.IMAPLoginFailed{Username: event.Username})
|
||||
}
|
||||
}
|
||||
|
||||
@ -261,7 +290,6 @@ func ApplyGluonConfigPathSuffix(basePath string) string {
|
||||
return filepath.Join(basePath, "backend", "db")
|
||||
}
|
||||
|
||||
// nolint:funlen
|
||||
func newIMAPServer(
|
||||
gluonCacheDir, gluonConfigDir string,
|
||||
version *semver.Version,
|
||||
@ -270,6 +298,8 @@ func newIMAPServer(
|
||||
logClient, logServer bool,
|
||||
eventCh chan<- imapEvents.Event,
|
||||
tasks *async.Group,
|
||||
uidValidityGenerator imap.UIDValidityGenerator,
|
||||
panicHandler async.PanicHandler,
|
||||
) (*gluon.Server, error) {
|
||||
gluonCacheDir = ApplyGluonCachePathSuffix(gluonCacheDir)
|
||||
gluonConfigDir = ApplyGluonConfigPathSuffix(gluonConfigDir)
|
||||
@ -313,6 +343,8 @@ func newIMAPServer(
|
||||
gluon.WithLogger(imapClientLog, imapServerLog),
|
||||
getGluonVersionInfo(version),
|
||||
gluon.WithReporter(reporter),
|
||||
gluon.WithUIDValidityGenerator(uidValidityGenerator),
|
||||
gluon.WithPanicHandler(panicHandler),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -348,7 +380,7 @@ func (*storeBuilder) New(path, userID string, passphrase []byte) (store.Store, e
|
||||
return store.NewOnDiskStore(
|
||||
filepath.Join(path, userID),
|
||||
passphrase,
|
||||
store.WithCompressor(new(store.GZipCompressor)),
|
||||
store.WithFallback(fallback_v0.NewOnDiskStoreV0WithCompressor(&fallback_v0.GZipCompressor{})),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -45,7 +45,7 @@ func NewMocks(tb testing.TB, version, minAuto *semver.Version) *Mocks {
|
||||
mocks.TLSReporter.EXPECT().GetTLSIssueCh().Return(mocks.TLSIssueCh).AnyTimes()
|
||||
|
||||
// This is called at he end of any go-routine:
|
||||
mocks.CrashHandler.EXPECT().HandlePanic().AnyTimes()
|
||||
mocks.CrashHandler.EXPECT().HandlePanic(gomock.Any()).AnyTimes()
|
||||
|
||||
return mocks
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/ProtonMail/proton-bridge/v3/internal/async (interfaces: PanicHandler)
|
||||
// Source: github.com/ProtonMail/gluon/async (interfaces: PanicHandler)
|
||||
|
||||
// Package mocks is a generated GoMock package.
|
||||
package mocks
|
||||
@ -34,13 +34,13 @@ func (m *MockPanicHandler) EXPECT() *MockPanicHandlerMockRecorder {
|
||||
}
|
||||
|
||||
// HandlePanic mocks base method.
|
||||
func (m *MockPanicHandler) HandlePanic() {
|
||||
func (m *MockPanicHandler) HandlePanic(arg0 interface{}) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "HandlePanic")
|
||||
m.ctrl.Call(m, "HandlePanic", arg0)
|
||||
}
|
||||
|
||||
// HandlePanic indicates an expected call of HandlePanic.
|
||||
func (mr *MockPanicHandlerMockRecorder) HandlePanic() *gomock.Call {
|
||||
func (mr *MockPanicHandlerMockRecorder) HandlePanic(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandlePanic", reflect.TypeOf((*MockPanicHandler)(nil).HandlePanic))
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandlePanic", reflect.TypeOf((*MockPanicHandler)(nil).HandlePanic), arg0)
|
||||
}
|
||||
|
||||
@ -57,6 +57,7 @@ func TestBridge_Refresh(t *testing.T) {
|
||||
require.Equal(t, userID, (<-syncCh).UserID)
|
||||
})
|
||||
|
||||
var uidValidities = make(map[string]uint32, len(names))
|
||||
// If we then connect an IMAP client, it should see all the labels with UID validity of 1.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).AnyTimes()
|
||||
@ -73,7 +74,7 @@ func TestBridge_Refresh(t *testing.T) {
|
||||
for _, name := range names {
|
||||
status, err := client.Select("Folders/"+name, false)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint32(1000), status.UidValidity)
|
||||
uidValidities[name] = status.UidValidity
|
||||
}
|
||||
})
|
||||
|
||||
@ -106,7 +107,7 @@ func TestBridge_Refresh(t *testing.T) {
|
||||
for _, name := range names {
|
||||
status, err := client.Select("Folders/"+name, false)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint32(1001), status.UidValidity)
|
||||
require.Greater(t, status.UidValidity, uidValidities[name])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@ -330,3 +330,182 @@ func TestBridge_SendInvite(t *testing.T) {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_SendAddTextBodyPartIfNotExists(t *testing.T) {
|
||||
const messageMultipartWithoutText = `Content-Type: multipart/mixed;
|
||||
boundary="Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84"
|
||||
Subject: A new message
|
||||
Date: Mon, 13 Mar 2023 16:06:16 +0100
|
||||
|
||||
|
||||
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
|
||||
Content-Disposition: inline;
|
||||
filename=Cat_August_2010-4.jpeg
|
||||
Content-Type: image/jpeg;
|
||||
name="Cat_August_2010-4.jpeg"
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
SGVsbG8gd29ybGQ=
|
||||
|
||||
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84--
|
||||
`
|
||||
|
||||
const messageMultipartWithText = `Content-Type: multipart/mixed;
|
||||
boundary="Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84"
|
||||
Subject: A new message Part2
|
||||
Date: Mon, 13 Mar 2023 16:06:16 +0100
|
||||
|
||||
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
|
||||
Content-Disposition: inline;
|
||||
filename=Cat_August_2010-4.jpeg
|
||||
Content-Type: image/jpeg;
|
||||
name="Cat_August_2010-4.jpeg"
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
SGVsbG8gd29ybGQ=
|
||||
|
||||
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
|
||||
Content-Type: text/html;charset=utf8
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
Hello world
|
||||
|
||||
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84--
|
||||
`
|
||||
|
||||
const messageWithTextOnly = `Content-Type: text/plain;charset=utf8
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
Subject: A new message Part3
|
||||
Date: Mon, 13 Mar 2023 16:06:16 +0100
|
||||
|
||||
Hello world
|
||||
|
||||
`
|
||||
|
||||
const messageMultipartWithoutTextWithTextAttachment = `Content-Type: multipart/mixed;
|
||||
boundary="Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84"
|
||||
Subject: A new message Part4
|
||||
Date: Mon, 13 Mar 2023 16:06:16 +0100
|
||||
|
||||
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
|
||||
Content-Type: text/plain; charset=UTF-8; name="text.txt"
|
||||
Content-Disposition: attachment; filename="text.txt"
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
SGVsbG8gd29ybGQK
|
||||
|
||||
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84--
|
||||
`
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
_, _, err := s.CreateUser("recipient", password)
|
||||
require.NoError(t, err)
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
senderUserID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
recipientUserID, err := bridge.LoginFull(ctx, "recipient", password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
senderInfo, err := bridge.GetUserInfo(senderUserID)
|
||||
require.NoError(t, err)
|
||||
|
||||
recipientInfo, err := bridge.GetUserInfo(recipientUserID)
|
||||
require.NoError(t, err)
|
||||
|
||||
messages := []string{
|
||||
messageMultipartWithoutText,
|
||||
messageMultipartWithText,
|
||||
messageWithTextOnly,
|
||||
messageMultipartWithoutTextWithTextAttachment,
|
||||
}
|
||||
|
||||
for _, m := range messages {
|
||||
// Dial the server.
|
||||
client, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort())))
|
||||
require.NoError(t, err)
|
||||
defer client.Close() //nolint:errcheck
|
||||
|
||||
// Upgrade to TLS.
|
||||
require.NoError(t, client.StartTLS(&tls.Config{InsecureSkipVerify: true}))
|
||||
|
||||
// Authorize with SASL LOGIN.
|
||||
require.NoError(t, client.Auth(sasl.NewLoginClient(
|
||||
senderInfo.Addresses[0],
|
||||
string(senderInfo.BridgePass)),
|
||||
))
|
||||
|
||||
// Send the message.
|
||||
require.NoError(t, client.SendMail(
|
||||
senderInfo.Addresses[0],
|
||||
[]string{recipientInfo.Addresses[0]},
|
||||
strings.NewReader(m),
|
||||
))
|
||||
}
|
||||
|
||||
// Connect the sender IMAP client.
|
||||
senderIMAPClient, err := client.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, senderIMAPClient.Login(senderInfo.Addresses[0], string(senderInfo.BridgePass)))
|
||||
defer senderIMAPClient.Logout() //nolint:errcheck
|
||||
|
||||
// Connect the recipient IMAP client.
|
||||
recipientIMAPClient, err := client.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, recipientIMAPClient.Login(recipientInfo.Addresses[0], string(recipientInfo.BridgePass)))
|
||||
defer recipientIMAPClient.Logout() //nolint:errcheck
|
||||
|
||||
require.Eventually(t, func() bool {
|
||||
messages, err := clientFetch(senderIMAPClient, `Sent`, imap.FetchBodyStructure)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 4, len(messages))
|
||||
|
||||
// messages may not be in order
|
||||
for _, message := range messages {
|
||||
switch {
|
||||
case message.Envelope.Subject == "A new message":
|
||||
// The message that was sent should now include an empty text/plain body part since there was none
|
||||
// in the original message.
|
||||
require.Equal(t, 2, len(message.BodyStructure.Parts))
|
||||
|
||||
require.Equal(t, "text", message.BodyStructure.Parts[0].MIMEType)
|
||||
require.Equal(t, "plain", message.BodyStructure.Parts[0].MIMESubType)
|
||||
require.Equal(t, uint32(0), message.BodyStructure.Parts[0].Size)
|
||||
require.Equal(t, "image", message.BodyStructure.Parts[1].MIMEType)
|
||||
require.Equal(t, "jpeg", message.BodyStructure.Parts[1].MIMESubType)
|
||||
|
||||
case message.Envelope.Subject == "A new message Part2":
|
||||
// This message already has a text body, should be unchanged
|
||||
require.Equal(t, 2, len(message.BodyStructure.Parts))
|
||||
|
||||
require.Equal(t, "image", message.BodyStructure.Parts[1].MIMEType)
|
||||
require.Equal(t, "jpeg", message.BodyStructure.Parts[1].MIMESubType)
|
||||
require.Equal(t, "text", message.BodyStructure.Parts[0].MIMEType)
|
||||
require.Equal(t, "html", message.BodyStructure.Parts[0].MIMESubType)
|
||||
|
||||
case message.Envelope.Subject == "A new message Part3":
|
||||
// This message already has a text body, should be unchanged
|
||||
require.Equal(t, 0, len(message.BodyStructure.Parts))
|
||||
|
||||
require.Equal(t, "text", message.BodyStructure.MIMEType)
|
||||
require.Equal(t, "plain", message.BodyStructure.MIMESubType)
|
||||
|
||||
case message.Envelope.Subject == "A new message Part4":
|
||||
// The message that was sent should now include an empty text/plain body part since even though
|
||||
// there was only a text/plain attachment in the original message.
|
||||
require.Equal(t, 2, len(message.BodyStructure.Parts))
|
||||
|
||||
require.Equal(t, "text", message.BodyStructure.Parts[0].MIMEType)
|
||||
require.Equal(t, "plain", message.BodyStructure.Parts[0].MIMESubType)
|
||||
require.Equal(t, uint32(0), message.BodyStructure.Parts[0].Size)
|
||||
require.Equal(t, "text", message.BodyStructure.Parts[1].MIMEType)
|
||||
require.Equal(t, "plain", message.BodyStructure.Parts[1].MIMESubType)
|
||||
require.Equal(t, "attachment", message.BodyStructure.Parts[1].Disposition)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}, 10*time.Second, 100*time.Millisecond)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@ -25,11 +25,9 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@ -131,26 +129,21 @@ func (bridge *Bridge) SetGluonDir(ctx context.Context, newGluonDir string) error
|
||||
return fmt.Errorf("new gluon dir is the same as the old one")
|
||||
}
|
||||
|
||||
if err := bridge.stopEventLoops(); err != nil {
|
||||
return err
|
||||
if err := bridge.closeIMAP(context.Background()); err != nil {
|
||||
return fmt.Errorf("failed to close IMAP: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
err := bridge.startEventLoops(ctx)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := bridge.moveGluonCacheDir(currentGluonDir, newGluonDir); err != nil {
|
||||
logrus.WithError(err).Error("failed to move GluonCacheDir")
|
||||
|
||||
if err := bridge.vault.SetGluonDir(currentGluonDir); err != nil {
|
||||
panic(err)
|
||||
return fmt.Errorf("failed to revert GluonCacheDir: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
gluonDataDir, err := bridge.GetGluonDataDir()
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to get Gluon Database directory: %w", err))
|
||||
return fmt.Errorf("failed to get Gluon Database directory: %w", err)
|
||||
}
|
||||
|
||||
imapServer, err := newIMAPServer(
|
||||
@ -163,13 +156,25 @@ func (bridge *Bridge) SetGluonDir(ctx context.Context, newGluonDir string) error
|
||||
bridge.logIMAPServer,
|
||||
bridge.imapEventCh,
|
||||
bridge.tasks,
|
||||
bridge.uidValidityGenerator,
|
||||
bridge.panicHandler,
|
||||
)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to create new IMAP server: %w", err))
|
||||
return fmt.Errorf("failed to create new IMAP server: %w", err)
|
||||
}
|
||||
|
||||
bridge.imapServer = imapServer
|
||||
|
||||
for _, user := range bridge.users {
|
||||
if err := bridge.addIMAPUser(ctx, user); err != nil {
|
||||
return fmt.Errorf("failed to add users to new IMAP server: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := bridge.serveIMAP(); err != nil {
|
||||
return fmt.Errorf("failed to serve IMAP: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}, bridge.usersLock)
|
||||
}
|
||||
@ -191,34 +196,6 @@ func (bridge *Bridge) moveGluonCacheDir(oldGluonDir, newGluonDir string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bridge *Bridge) stopEventLoops() error {
|
||||
if err := bridge.closeIMAP(context.Background()); err != nil {
|
||||
return fmt.Errorf("failed to close IMAP: %w", err)
|
||||
}
|
||||
|
||||
if err := bridge.closeSMTP(); err != nil {
|
||||
return fmt.Errorf("failed to close SMTP: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bridge *Bridge) startEventLoops(ctx context.Context) error {
|
||||
for _, user := range bridge.users {
|
||||
if err := bridge.addIMAPUser(ctx, user); err != nil {
|
||||
return fmt.Errorf("failed to add users to new IMAP server: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := bridge.serveIMAP(); err != nil {
|
||||
panic(fmt.Errorf("failed to serve IMAP: %w", err))
|
||||
}
|
||||
|
||||
if err := bridge.serveSMTP(); err != nil {
|
||||
panic(fmt.Errorf("failed to serve SMTP: %w", err))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bridge *Bridge) GetProxyAllowed() bool {
|
||||
return bridge.vault.GetProxyAllowed()
|
||||
}
|
||||
@ -332,6 +309,9 @@ func (bridge *Bridge) SetColorScheme(colorScheme string) error {
|
||||
return bridge.vault.SetColorScheme(colorScheme)
|
||||
}
|
||||
|
||||
// FactoryReset deletes all users, wipes the vault, and deletes all files.
|
||||
// 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) {
|
||||
// Delete all the users.
|
||||
safe.Lock(func() {
|
||||
@ -348,22 +328,10 @@ func (bridge *Bridge) FactoryReset(ctx context.Context) {
|
||||
logrus.WithError(err).Error("Failed to reset vault")
|
||||
}
|
||||
|
||||
// Then delete all files.
|
||||
if err := bridge.locator.Clear(); err != nil {
|
||||
// Lastly, delete all files except the vault.
|
||||
if err := bridge.locator.Clear(bridge.vault.Path()); err != nil {
|
||||
logrus.WithError(err).Error("Failed to clear data paths")
|
||||
}
|
||||
|
||||
// Lastly clear the keychain.
|
||||
vaultDir, err := bridge.locator.ProvideSettingsPath()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Failed to get vault dir")
|
||||
} else if helper, err := vault.GetHelper(vaultDir); err != nil {
|
||||
logrus.WithError(err).Error("Failed to get keychain helper")
|
||||
} else if keychain, err := keychain.NewKeychain(helper, constants.KeyChainName); err != nil {
|
||||
logrus.WithError(err).Error("Failed to get keychain")
|
||||
} else if err := keychain.Clear(); err != nil {
|
||||
logrus.WithError(err).Error("Failed to clear keychain")
|
||||
}
|
||||
}
|
||||
|
||||
func getPort(addr net.Addr) int {
|
||||
|
||||
@ -22,6 +22,7 @@ import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
@ -31,25 +32,41 @@ import (
|
||||
)
|
||||
|
||||
func (bridge *Bridge) serveSMTP() error {
|
||||
logrus.Info("Starting SMTP server")
|
||||
port, err := func() (int, error) {
|
||||
logrus.Info("Starting SMTP server")
|
||||
|
||||
smtpListener, err := newListener(bridge.vault.GetSMTPPort(), bridge.vault.GetSMTPSSL(), bridge.tlsConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create SMTP listener: %w", err)
|
||||
}
|
||||
|
||||
bridge.smtpListener = smtpListener
|
||||
|
||||
bridge.tasks.Once(func(context.Context) {
|
||||
if err := bridge.smtpServer.Serve(smtpListener); err != nil {
|
||||
logrus.WithError(err).Info("SMTP server stopped")
|
||||
smtpListener, err := newListener(bridge.vault.GetSMTPPort(), bridge.vault.GetSMTPSSL(), bridge.tlsConfig)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to create SMTP listener: %w", err)
|
||||
}
|
||||
})
|
||||
|
||||
if err := bridge.vault.SetSMTPPort(getPort(smtpListener.Addr())); err != nil {
|
||||
return fmt.Errorf("failed to store SMTP port in vault: %w", err)
|
||||
bridge.smtpListener = smtpListener
|
||||
|
||||
bridge.tasks.Once(func(context.Context) {
|
||||
if err := bridge.smtpServer.Serve(smtpListener); err != nil {
|
||||
logrus.WithError(err).Info("SMTP server stopped")
|
||||
}
|
||||
})
|
||||
|
||||
if err := bridge.vault.SetSMTPPort(getPort(smtpListener.Addr())); err != nil {
|
||||
return 0, fmt.Errorf("failed to store SMTP port in vault: %w", err)
|
||||
}
|
||||
|
||||
return getPort(smtpListener.Addr()), nil
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
bridge.publish(events.SMTPServerError{
|
||||
Error: err,
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
bridge.publish(events.SMTPServerReady{
|
||||
Port: port,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -60,6 +77,8 @@ func (bridge *Bridge) restartSMTP() error {
|
||||
return fmt.Errorf("failed to close SMTP: %w", err)
|
||||
}
|
||||
|
||||
bridge.publish(events.SMTPServerStopped{})
|
||||
|
||||
bridge.smtpServer = newSMTPServer(bridge, bridge.tlsConfig, bridge.logSMTP)
|
||||
|
||||
return bridge.serveSMTP()
|
||||
@ -82,6 +101,8 @@ func (bridge *Bridge) closeSMTP() error {
|
||||
logrus.WithError(err).Debug("Failed to close SMTP server (expected -- we close the listener ourselves)")
|
||||
}
|
||||
|
||||
bridge.publish(events.SMTPServerStopped{})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@ -28,6 +28,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
"github.com/ProtonMail/gluon/rfc822"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/go-proton-api/server"
|
||||
@ -351,7 +352,7 @@ func withClient(ctx context.Context, t *testing.T, s *server.Server, username st
|
||||
fn(ctx, c)
|
||||
}
|
||||
|
||||
func clientFetch(client *client.Client, mailbox string) ([]*imap.Message, error) {
|
||||
func clientFetch(client *client.Client, mailbox string, extraItems ...imap.FetchItem) ([]*imap.Message, error) {
|
||||
status, err := client.Select(mailbox, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -363,10 +364,13 @@ func clientFetch(client *client.Client, mailbox string) ([]*imap.Message, error)
|
||||
|
||||
resCh := make(chan *imap.Message)
|
||||
|
||||
fetchItems := []imap.FetchItem{imap.FetchFlags, imap.FetchEnvelope, imap.FetchUid, imap.FetchBodyStructure, "BODY.PEEK[]"}
|
||||
fetchItems = append(fetchItems, extraItems...)
|
||||
|
||||
go func() {
|
||||
if err := client.Fetch(
|
||||
&imap.SeqSet{Set: []imap.Seq{{Start: 1, Stop: status.Messages}}},
|
||||
[]imap.FetchItem{imap.FetchFlags, imap.FetchEnvelope, imap.FetchUid, "BODY.PEEK[]"},
|
||||
fetchItems,
|
||||
resCh,
|
||||
); err != nil {
|
||||
panic(err)
|
||||
@ -425,13 +429,13 @@ func createMessages(ctx context.Context, t *testing.T, c *proton.Client, addrID,
|
||||
keyPass, err := salt.SaltForKey(password, user.Keys.Primary().ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, addrKRs, err := proton.Unlock(user, addr, keyPass)
|
||||
_, addrKRs, err := proton.Unlock(user, addr, keyPass, async.NoopPanicHandler{})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, ok := addrKRs[addrID]
|
||||
require.True(t, ok)
|
||||
|
||||
res, err := stream.Collect(ctx, c.ImportMessages(
|
||||
str, err := c.ImportMessages(
|
||||
ctx,
|
||||
addrKRs[addrID],
|
||||
runtime.NumCPU(),
|
||||
@ -446,7 +450,10 @@ func createMessages(ctx context.Context, t *testing.T, c *proton.Client, addrID,
|
||||
Message: message,
|
||||
}
|
||||
})...,
|
||||
))
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := stream.Collect(ctx, str)
|
||||
require.NoError(t, err)
|
||||
|
||||
return xslices.Map(res, func(res proton.ImportRes) string {
|
||||
|
||||
@ -18,5 +18,9 @@
|
||||
package bridge
|
||||
|
||||
func (bridge *Bridge) GetBridgeTLSCert() ([]byte, []byte) {
|
||||
return bridge.vault.GetBridgeTLSCert(), bridge.vault.GetBridgeTLSKey()
|
||||
return bridge.vault.GetBridgeTLSCert()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) SetBridgeTLSCertPath(certPath, keyPath string) error {
|
||||
return bridge.vault.SetBridgeTLSCertPath(certPath, keyPath)
|
||||
}
|
||||
|
||||
@ -30,7 +30,7 @@ type Locator interface {
|
||||
ProvideGluonDataPath() (string, error)
|
||||
GetLicenseFilePath() string
|
||||
GetDependencyLicensesLink() string
|
||||
Clear() error
|
||||
Clear(...string) error
|
||||
}
|
||||
|
||||
type Identifier interface {
|
||||
|
||||
@ -32,19 +32,7 @@ func (bridge *Bridge) CheckForUpdates() {
|
||||
}
|
||||
|
||||
func (bridge *Bridge) InstallUpdate(version updater.VersionInfo) {
|
||||
log := logrus.WithFields(logrus.Fields{
|
||||
"version": version.Version,
|
||||
"current": bridge.curVersion,
|
||||
"channel": bridge.vault.GetUpdateChannel(),
|
||||
})
|
||||
|
||||
select {
|
||||
case bridge.installCh <- installJob{version: version, silent: false}:
|
||||
log.Info("The update will be installed manually")
|
||||
|
||||
default:
|
||||
log.Info("An update is already being installed")
|
||||
}
|
||||
bridge.installCh <- installJob{version: version, silent: false}
|
||||
}
|
||||
|
||||
func (bridge *Bridge) handleUpdate(version updater.VersionInfo) {
|
||||
@ -89,17 +77,7 @@ func (bridge *Bridge) handleUpdate(version updater.VersionInfo) {
|
||||
|
||||
default:
|
||||
safe.RLock(func() {
|
||||
if version.Version.GreaterThan(bridge.newVersion) {
|
||||
log.Info("An update is available")
|
||||
|
||||
select {
|
||||
case bridge.installCh <- installJob{version: version, silent: true}:
|
||||
log.Info("The update will be installed silently")
|
||||
|
||||
default:
|
||||
log.Info("An update is already being installed")
|
||||
}
|
||||
}
|
||||
bridge.installCh <- installJob{version: version, silent: true}
|
||||
}, bridge.newVersionLock)
|
||||
}
|
||||
}
|
||||
@ -117,6 +95,12 @@ func (bridge *Bridge) installUpdate(ctx context.Context, job installJob) {
|
||||
"channel": bridge.vault.GetUpdateChannel(),
|
||||
})
|
||||
|
||||
if !job.version.Version.GreaterThan(bridge.newVersion) {
|
||||
return
|
||||
}
|
||||
|
||||
log.WithField("silent", job.silent).Info("An update is available")
|
||||
|
||||
bridge.publish(events.UpdateAvailable{
|
||||
Version: job.version,
|
||||
Compatible: true,
|
||||
@ -142,6 +126,7 @@ func (bridge *Bridge) installUpdate(ctx context.Context, job installJob) {
|
||||
Silent: job.silent,
|
||||
Error: err,
|
||||
})
|
||||
|
||||
default:
|
||||
log.Info("The update was installed successfully")
|
||||
|
||||
|
||||
@ -23,9 +23,10 @@ import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
"github.com/ProtonMail/gluon/imap"
|
||||
"github.com/ProtonMail/gluon/reporter"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/async"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||
@ -298,6 +299,59 @@ func (bridge *Bridge) SetAddressMode(ctx context.Context, userID string, mode va
|
||||
}, bridge.usersLock)
|
||||
}
|
||||
|
||||
// SendBadEventUserFeedback passes the feedback to the given user.
|
||||
func (bridge *Bridge) SendBadEventUserFeedback(_ context.Context, userID string, doResync bool) error {
|
||||
logrus.WithField("userID", userID).WithField("doResync", doResync).Info("Passing bad event feedback to user")
|
||||
|
||||
return safe.LockRet(func() error {
|
||||
ctx := context.Background()
|
||||
|
||||
user, ok := bridge.users[userID]
|
||||
if !ok {
|
||||
if rerr := bridge.reporter.ReportMessageWithContext(
|
||||
"Failed to handle event: feedback failed: no such user",
|
||||
reporter.Context{"user_id": userID},
|
||||
); rerr != nil {
|
||||
logrus.WithError(rerr).Error("Failed to report feedback failure")
|
||||
}
|
||||
|
||||
return ErrNoSuchUser
|
||||
}
|
||||
|
||||
if doResync {
|
||||
if rerr := bridge.reporter.ReportMessageWithContext(
|
||||
"Failed to handle event: feedback resync",
|
||||
reporter.Context{"user_id": userID},
|
||||
); rerr != nil {
|
||||
logrus.WithError(rerr).Error("Failed to report feedback failure")
|
||||
}
|
||||
|
||||
if err := bridge.addIMAPUser(ctx, user); err != nil {
|
||||
return fmt.Errorf("failed to add IMAP user: %w", err)
|
||||
}
|
||||
|
||||
user.BadEventFeedbackResync(ctx)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if rerr := bridge.reporter.ReportMessageWithContext(
|
||||
"Failed to handle event: feedback logout",
|
||||
reporter.Context{"user_id": userID},
|
||||
); rerr != nil {
|
||||
logrus.WithError(rerr).Error("Failed to report feedback failure")
|
||||
}
|
||||
|
||||
bridge.logoutUser(ctx, user, true, false)
|
||||
|
||||
bridge.publish(events.UserLoggedOut{
|
||||
UserID: userID,
|
||||
})
|
||||
|
||||
return nil
|
||||
}, bridge.usersLock)
|
||||
}
|
||||
|
||||
func (bridge *Bridge) loginUser(ctx context.Context, client *proton.Client, authUID, authRef string, keyPass []byte) (string, error) {
|
||||
apiUser, err := client.GetUser(ctx)
|
||||
if err != nil {
|
||||
@ -380,6 +434,7 @@ func (bridge *Bridge) loadUser(ctx context.Context, user *vault.User) error {
|
||||
logrus.WithError(err).Warn("Failed to clear user secrets")
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("failed to create API client: %w", err)
|
||||
}
|
||||
|
||||
@ -461,9 +516,9 @@ func (bridge *Bridge) addUserWithVault(
|
||||
client,
|
||||
bridge.reporter,
|
||||
apiUser,
|
||||
bridge.crashHandler,
|
||||
bridge.vault.SyncWorkers(),
|
||||
bridge.panicHandler,
|
||||
bridge.vault.GetShowAllMail(),
|
||||
bridge.vault.GetMaxSyncMemory(),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create user: %w", err)
|
||||
|
||||
@ -20,18 +20,24 @@ package bridge_test
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/mail"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
"github.com/ProtonMail/gluon/rfc822"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/go-proton-api/server"
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||
"github.com/bradenaw/juniper/stream"
|
||||
"github.com/bradenaw/juniper/xslices"
|
||||
"github.com/emersion/go-imap"
|
||||
"github.com/emersion/go-imap/client"
|
||||
@ -40,7 +46,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestBridge_User_BadMessage_BadEvent(t *testing.T) {
|
||||
func TestBridge_User_RefreshEvent(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
// Create a user.
|
||||
userID, addrID, err := s.CreateUser("user", password)
|
||||
@ -49,54 +55,133 @@ func TestBridge_User_BadMessage_BadEvent(t *testing.T) {
|
||||
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
|
||||
require.NoError(t, err)
|
||||
|
||||
var messageIDs []string
|
||||
|
||||
// Create 10 messages for the user.
|
||||
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||
createNumMessages(ctx, t, c, addrID, labelID, 10)
|
||||
messageIDs = createNumMessages(ctx, t, c, addrID, labelID, 10)
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
userLoginAndSync(ctx, t, bridge, "user", password)
|
||||
})
|
||||
|
||||
var messageIDs []string
|
||||
// Remove a message
|
||||
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||
require.NoError(t, c.DeleteMessage(ctx, messageIDs[0]))
|
||||
})
|
||||
|
||||
// Create 10 more messages for the user, generating events.
|
||||
require.NoError(t, s.RefreshUser(userID, proton.RefreshMail))
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
syncCh, closeCh := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
||||
|
||||
mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).MinTimes(1)
|
||||
|
||||
require.Equal(t, userID, (<-syncCh).UserID)
|
||||
closeCh()
|
||||
|
||||
userContinueEventProcess(ctx, t, s, bridge)
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||
messageIDs = createNumMessages(ctx, t, c, addrID, labelID, 10)
|
||||
createNumMessages(ctx, t, c, addrID, labelID, 10)
|
||||
})
|
||||
|
||||
// If bridge attempts to sync the new messages, it should get a BadRequest error.
|
||||
doBadRequest := true
|
||||
s.AddStatusHook(func(req *http.Request) (int, bool) {
|
||||
if !doBadRequest {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
if xslices.Index(xslices.Map(messageIDs, func(messageID string) string {
|
||||
return "/mail/v4/messages/" + messageID
|
||||
}), req.URL.Path) < 0 {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return http.StatusBadRequest, true
|
||||
})
|
||||
|
||||
userReceiveBadErrorAndLogout(t, bridge, mocks)
|
||||
|
||||
// Remove messages
|
||||
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||
require.NoError(t, c.DeleteMessage(ctx, messageIDs...))
|
||||
})
|
||||
doBadRequest = false
|
||||
|
||||
// Login again
|
||||
_, err := bridge.LoginFull(ctx, "user", password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
userContinueEventProcess(ctx, t, s, bridge)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_User_BadMessage_BadEvent(t *testing.T) {
|
||||
t.Run("Resync", test_badMessage_badEvent(func(t *testing.T, ctx context.Context, bridge *bridge.Bridge, badUserID string) {
|
||||
// User feedback is resync
|
||||
require.NoError(t, bridge.SendBadEventUserFeedback(ctx, badUserID, true))
|
||||
|
||||
// Wait for sync to finish
|
||||
syncCh, closeCh := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
||||
require.Equal(t, badUserID, (<-syncCh).UserID)
|
||||
closeCh()
|
||||
}))
|
||||
|
||||
t.Run("LogoutAndLogin", test_badMessage_badEvent(func(t *testing.T, ctx context.Context, bridge *bridge.Bridge, badUserID string) {
|
||||
logoutCh, closeCh := chToType[events.Event, events.UserLoggedOut](bridge.GetEvents(events.UserLoggedOut{}))
|
||||
|
||||
// User feedback is logout
|
||||
require.NoError(t, bridge.SendBadEventUserFeedback(ctx, badUserID, false))
|
||||
|
||||
require.Equal(t, badUserID, (<-logoutCh).UserID)
|
||||
closeCh()
|
||||
|
||||
// The user will eventually be logged out due to the bad request errors.
|
||||
require.Eventually(t, func() bool {
|
||||
return len(bridge.GetUserIDs()) == 1 && len(getConnectedUserIDs(t, bridge)) == 0
|
||||
}, 100*user.EventPeriod, user.EventPeriod)
|
||||
|
||||
// Login again
|
||||
_, err := bridge.LoginFull(ctx, "user", password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
}))
|
||||
}
|
||||
|
||||
func test_badMessage_badEvent(userFeedback func(t *testing.T, ctx context.Context, bridge *bridge.Bridge, badUserID string)) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
// Create a user.
|
||||
userID, addrID, err := s.CreateUser("user", password)
|
||||
require.NoError(t, err)
|
||||
|
||||
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create 10 messages for the user.
|
||||
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||
createNumMessages(ctx, t, c, addrID, labelID, 10)
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
userLoginAndSync(ctx, t, bridge, "user", password)
|
||||
|
||||
var messageIDs []string
|
||||
|
||||
// Create 10 more messages for the user, generating events.
|
||||
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||
messageIDs = createNumMessages(ctx, t, c, addrID, labelID, 10)
|
||||
})
|
||||
|
||||
// If bridge attempts to sync the new messages, it should get a BadRequest error.
|
||||
doBadRequest := true
|
||||
s.AddStatusHook(func(req *http.Request) (int, bool) {
|
||||
if !doBadRequest {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
if xslices.Index(xslices.Map(messageIDs[0:5], func(messageID string) string {
|
||||
return "/mail/v4/messages/" + messageID
|
||||
}), req.URL.Path) < 0 {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return http.StatusBadRequest, true
|
||||
})
|
||||
|
||||
badUserID := userReceivesBadError(t, bridge, mocks)
|
||||
|
||||
// Remove messages, make response OK again
|
||||
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||
require.NoError(t, c.DeleteMessage(ctx, messageIDs[0:5]...))
|
||||
})
|
||||
doBadRequest = false
|
||||
|
||||
userFeedback(t, ctx, bridge, badUserID)
|
||||
|
||||
userContinueEventProcess(ctx, t, s, bridge)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBridge_User_BadMessage_NoBadEvent(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
// Create a user.
|
||||
@ -113,12 +198,13 @@ func TestBridge_User_BadMessage_NoBadEvent(t *testing.T) {
|
||||
|
||||
var messageIDs []string
|
||||
|
||||
// Create 10 more messages for the user, generating events.
|
||||
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||
messageIDs = createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 10)
|
||||
})
|
||||
|
||||
// If bridge attempts to sync the new messages, it should get a BadRequest error.
|
||||
s.AddStatusHook(func(req *http.Request) (int, bool) {
|
||||
if len(messageIDs) < 3 {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
if strings.Contains(req.URL.Path, "/mail/v4/messages/"+messageIDs[2]) {
|
||||
return http.StatusUnprocessableEntity, true
|
||||
}
|
||||
@ -126,11 +212,6 @@ func TestBridge_User_BadMessage_NoBadEvent(t *testing.T) {
|
||||
return 0, false
|
||||
})
|
||||
|
||||
// Create 10 more messages for the user, generating events.
|
||||
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||
messageIDs = createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 10)
|
||||
})
|
||||
|
||||
// Remove messages
|
||||
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||
require.NoError(t, c.DeleteMessage(ctx, messageIDs...))
|
||||
@ -249,6 +330,24 @@ func TestBridge_User_AddressEvents_NoBadEvent(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_User_AddressEventUpdatedForAddressThatDoesNotExist_NoBadEvent(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
// Create a user.
|
||||
userID, _, err := s.CreateUser("user", password)
|
||||
require.NoError(t, err)
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
userLoginAndSync(ctx, t, bridge, "user", password)
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
_, err := s.CreateAddressAsUpdate(userID, "another@pm.me", password)
|
||||
require.NoError(t, err)
|
||||
userContinueEventProcess(ctx, t, s, bridge)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_User_Network_NoBadEvents(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
retVal := int32(0)
|
||||
@ -295,6 +394,396 @@ func TestBridge_User_Network_NoBadEvents(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_User_DropConn_NoBadEvent(t *testing.T) {
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
|
||||
dropListener := proton.NewListener(l, proton.NewDropConn)
|
||||
defer func() { _ = dropListener.Close() }()
|
||||
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
// Create a user.
|
||||
_, addrID, err := s.CreateUser("user", password)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create 10 messages for the user.
|
||||
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||
createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 10)
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
userLoginAndSync(ctx, t, bridge, "user", password)
|
||||
|
||||
mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).AnyTimes()
|
||||
|
||||
// Create 10 more messages for the user, generating events.
|
||||
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||
createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 10)
|
||||
})
|
||||
|
||||
var count int
|
||||
|
||||
// The first 10 times bridge attempts to sync any of the messages, drop the connection.
|
||||
s.AddStatusHook(func(req *http.Request) (int, bool) {
|
||||
if strings.Contains(req.URL.Path, "/mail/v4/messages") {
|
||||
if count++; count < 10 {
|
||||
dropListener.DropAll()
|
||||
}
|
||||
}
|
||||
|
||||
return 0, false
|
||||
})
|
||||
|
||||
info, err := bridge.QueryUserInfo("user")
|
||||
require.NoError(t, err)
|
||||
|
||||
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
||||
defer func() { _ = client.Logout() }()
|
||||
|
||||
// The IMAP client will eventually see 20 messages.
|
||||
require.Eventually(t, func() bool {
|
||||
status, err := client.Status("INBOX", []imap.StatusItem{imap.StatusMessages})
|
||||
return err == nil && status.Messages == 20
|
||||
}, 10*time.Second, 100*time.Millisecond)
|
||||
})
|
||||
}, server.WithListener(dropListener))
|
||||
}
|
||||
|
||||
func TestBridge_User_UpdateDraftAndCreateOtherMessage(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
// Create a bridge user.
|
||||
_, _, err := s.CreateUser("user", password)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Initially sync the user.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
userLoginAndSync(ctx, t, bridge, "user", password)
|
||||
})
|
||||
|
||||
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||
user, err := c.GetUser(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
addrs, err := c.GetAddresses(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
salts, err := c.GetSalts(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
keyPass, err := salts.SaltForKey(password, user.Keys.Primary().ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, addrKRs, err := proton.Unlock(user, addrs, keyPass, async.NoopPanicHandler{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a draft (generating a "create draft message" event).
|
||||
draft, err := c.CreateDraft(ctx, addrKRs[addrs[0].ID], proton.CreateDraftReq{
|
||||
Message: proton.DraftTemplate{
|
||||
Subject: "subject",
|
||||
Sender: &mail.Address{Name: "sender", Address: addrs[0].Email},
|
||||
Body: "body",
|
||||
MIMEType: rfc822.TextPlain,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Process those events
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
userContinueEventProcess(ctx, t, s, bridge)
|
||||
})
|
||||
|
||||
// Update the draft (generating an "update draft message" event).
|
||||
require.NoError(t, getErr(c.UpdateDraft(ctx, draft.ID, addrKRs[addrs[0].ID], proton.UpdateDraftReq{
|
||||
Message: proton.DraftTemplate{
|
||||
Subject: "subject 2",
|
||||
Sender: &mail.Address{Name: "sender", Address: addrs[0].Email},
|
||||
Body: "body 2",
|
||||
MIMEType: rfc822.TextPlain,
|
||||
},
|
||||
})))
|
||||
|
||||
// Import a message (generating a "create message" event).
|
||||
str, err := c.ImportMessages(ctx, addrKRs[addrs[0].ID], 1, 1, proton.ImportReq{
|
||||
Metadata: proton.ImportMetadata{
|
||||
AddressID: addrs[0].ID,
|
||||
Flags: proton.MessageFlagReceived,
|
||||
},
|
||||
Message: []byte("From: someone@example.com\r\nTo: blabla@example.com\r\n\r\nhello"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := stream.Collect(ctx, str)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Process those events.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
userContinueEventProcess(ctx, t, s, bridge)
|
||||
})
|
||||
|
||||
// Update the imported message (generating an "update message" event).
|
||||
require.NoError(t, c.MarkMessagesUnread(ctx, res[0].MessageID))
|
||||
|
||||
// Process those events.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
userContinueEventProcess(ctx, t, s, bridge)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_User_SendDraftRemoveDraftFlag(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
// Create a bridge user.
|
||||
_, _, err := s.CreateUser("user", password)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Initially sync the user.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
userLoginAndSync(ctx, t, bridge, "user", password)
|
||||
})
|
||||
|
||||
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||
user, err := c.GetUser(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
addrs, err := c.GetAddresses(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
salts, err := c.GetSalts(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
keyPass, err := salts.SaltForKey(password, user.Keys.Primary().ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, addrKRs, err := proton.Unlock(user, addrs, keyPass, async.NoopPanicHandler{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a draft (generating a "create draft message" event).
|
||||
draft, err := c.CreateDraft(ctx, addrKRs[addrs[0].ID], proton.CreateDraftReq{
|
||||
Message: proton.DraftTemplate{
|
||||
Subject: "subject",
|
||||
ToList: []*mail.Address{{Address: addrs[0].Email}},
|
||||
Sender: &mail.Address{Name: "sender", Address: addrs[0].Email},
|
||||
Body: "body",
|
||||
MIMEType: rfc822.TextPlain,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Process those events
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
userContinueEventProcess(ctx, t, s, bridge)
|
||||
|
||||
info, err := bridge.QueryUserInfo("user")
|
||||
require.NoError(t, err)
|
||||
|
||||
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
||||
defer func() { _ = client.Logout() }()
|
||||
|
||||
messages, err := clientFetch(client, "Drafts")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, messages, 1)
|
||||
require.Contains(t, messages[0].Flags, imap.DraftFlag)
|
||||
})
|
||||
|
||||
// Send the draft (generating an "update message" event).
|
||||
{
|
||||
pubKeys, recType, err := c.GetPublicKeys(ctx, addrs[0].Email)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, recType, proton.RecipientTypeInternal)
|
||||
|
||||
var req proton.SendDraftReq
|
||||
|
||||
require.NoError(t, req.AddTextPackage(addrKRs[addrs[0].ID], "body", rfc822.TextPlain, map[string]proton.SendPreferences{
|
||||
addrs[0].Email: {
|
||||
Encrypt: true,
|
||||
PubKey: must(crypto.NewKeyRing(must(crypto.NewKeyFromArmored(pubKeys[0].PublicKey)))),
|
||||
SignatureType: proton.DetachedSignature,
|
||||
EncryptionScheme: proton.InternalScheme,
|
||||
MIMEType: rfc822.TextPlain,
|
||||
},
|
||||
}, nil))
|
||||
|
||||
require.NoError(t, getErr(c.SendDraft(ctx, draft.ID, req)))
|
||||
}
|
||||
|
||||
// Process those events; the draft will move to the sent folder and lose the draft flag.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
userContinueEventProcess(ctx, t, s, bridge)
|
||||
|
||||
info, err := bridge.QueryUserInfo("user")
|
||||
require.NoError(t, err)
|
||||
|
||||
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
||||
defer func() { _ = client.Logout() }()
|
||||
|
||||
messages, err := clientFetch(client, "Sent")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, messages, 1)
|
||||
require.NotContains(t, messages[0].Flags, imap.DraftFlag)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_User_DisableEnableAddress(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
// Create a user.
|
||||
userID, _, err := s.CreateUser("user", password)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create an additional address for the user.
|
||||
aliasID, err := s.CreateAddress(userID, "alias@"+s.GetDomain(), password)
|
||||
require.NoError(t, err)
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
require.NoError(t, getErr(bridge.LoginFull(ctx, "user", password, nil, nil)))
|
||||
|
||||
// Initially we should list the address.
|
||||
info, err := bridge.QueryUserInfo("user")
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, info.Addresses, "alias@"+s.GetDomain())
|
||||
})
|
||||
|
||||
// Disable the address.
|
||||
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||
require.NoError(t, c.DisableAddress(ctx, aliasID))
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Eventually we shouldn't list the address.
|
||||
require.Eventually(t, func() bool {
|
||||
info, err := bridge.QueryUserInfo("user")
|
||||
require.NoError(t, err)
|
||||
|
||||
return xslices.Index(info.Addresses, "alias@"+s.GetDomain()) < 0
|
||||
}, 5*time.Second, 100*time.Millisecond)
|
||||
})
|
||||
|
||||
// Enable the address.
|
||||
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||
require.NoError(t, c.EnableAddress(ctx, aliasID))
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Eventually we should list the address.
|
||||
require.Eventually(t, func() bool {
|
||||
info, err := bridge.QueryUserInfo("user")
|
||||
require.NoError(t, err)
|
||||
|
||||
return xslices.Index(info.Addresses, "alias@"+s.GetDomain()) >= 0
|
||||
}, 5*time.Second, 100*time.Millisecond)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_User_CreateDisabledAddress(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
// Create a user.
|
||||
userID, _, err := s.CreateUser("user", password)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create an additional address for the user.
|
||||
aliasID, err := s.CreateAddress(userID, "alias@"+s.GetDomain(), password)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Immediately disable the address.
|
||||
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||
require.NoError(t, c.DisableAddress(ctx, aliasID))
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
require.NoError(t, getErr(bridge.LoginFull(ctx, "user", password, nil, nil)))
|
||||
|
||||
// Initially we shouldn't list the address.
|
||||
info, err := bridge.QueryUserInfo("user")
|
||||
require.NoError(t, err)
|
||||
require.NotContains(t, info.Addresses, "alias@"+s.GetDomain())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_User_HandleParentLabelRename(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
require.NoError(t, getErr(bridge.LoginFull(ctx, username, password, nil, nil)))
|
||||
|
||||
info, err := bridge.QueryUserInfo(username)
|
||||
require.NoError(t, err)
|
||||
|
||||
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
||||
defer func() { _ = client.Logout() }()
|
||||
|
||||
withClient(ctx, t, s, username, password, func(ctx context.Context, c *proton.Client) {
|
||||
parentName := uuid.NewString()
|
||||
childName := uuid.NewString()
|
||||
|
||||
// Create a folder.
|
||||
parentLabel, err := c.CreateLabel(ctx, proton.CreateLabelReq{
|
||||
Name: parentName,
|
||||
Type: proton.LabelTypeFolder,
|
||||
Color: "#f66",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Wait for the parent folder to be created.
|
||||
require.Eventually(t, func() bool {
|
||||
return xslices.IndexFunc(clientList(client), func(mailbox *imap.MailboxInfo) bool {
|
||||
return mailbox.Name == fmt.Sprintf("Folders/%v", parentName)
|
||||
}) >= 0
|
||||
}, 100*user.EventPeriod, user.EventPeriod)
|
||||
|
||||
// Create a subfolder.
|
||||
childLabel, err := c.CreateLabel(ctx, proton.CreateLabelReq{
|
||||
Name: childName,
|
||||
Type: proton.LabelTypeFolder,
|
||||
Color: "#f66",
|
||||
ParentID: parentLabel.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, parentLabel.ID, childLabel.ParentID)
|
||||
|
||||
// Wait for the parent folder to be created.
|
||||
require.Eventually(t, func() bool {
|
||||
return xslices.IndexFunc(clientList(client), func(mailbox *imap.MailboxInfo) bool {
|
||||
return mailbox.Name == fmt.Sprintf("Folders/%v/%v", parentName, childName)
|
||||
}) >= 0
|
||||
}, 100*user.EventPeriod, user.EventPeriod)
|
||||
|
||||
newParentName := uuid.NewString()
|
||||
|
||||
// Rename the parent folder.
|
||||
require.NoError(t, getErr(c.UpdateLabel(ctx, parentLabel.ID, proton.UpdateLabelReq{
|
||||
Color: "#f66",
|
||||
Name: newParentName,
|
||||
})))
|
||||
|
||||
// Wait for the parent folder to be renamed.
|
||||
require.Eventually(t, func() bool {
|
||||
return xslices.IndexFunc(clientList(client), func(mailbox *imap.MailboxInfo) bool {
|
||||
return mailbox.Name == fmt.Sprintf("Folders/%v", newParentName)
|
||||
}) >= 0
|
||||
}, 100*user.EventPeriod, user.EventPeriod)
|
||||
|
||||
// Wait for the child folder to be renamed.
|
||||
require.Eventually(t, func() bool {
|
||||
return xslices.IndexFunc(clientList(client), func(mailbox *imap.MailboxInfo) bool {
|
||||
return mailbox.Name == fmt.Sprintf("Folders/%v/%v", newParentName, childName)
|
||||
}) >= 0
|
||||
}, 100*user.EventPeriod, user.EventPeriod)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// userLoginAndSync logs in user and waits until user is fully synced.
|
||||
func userLoginAndSync(
|
||||
ctx context.Context,
|
||||
@ -311,21 +800,24 @@ func userLoginAndSync(
|
||||
require.Equal(t, userID, (<-syncCh).UserID)
|
||||
}
|
||||
|
||||
func userReceiveBadErrorAndLogout(
|
||||
func userReceivesBadError(
|
||||
t *testing.T,
|
||||
bridge *bridge.Bridge,
|
||||
mocks *bridge.Mocks,
|
||||
) {
|
||||
) (userID string) {
|
||||
badEventCh, closeCh := bridge.GetEvents(events.UserBadEvent{})
|
||||
|
||||
// The user will continue to process events and will receive bad request errors.
|
||||
mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).MinTimes(1)
|
||||
|
||||
// The user will eventually be logged out due to the bad request errors.
|
||||
require.Eventually(t, func() bool {
|
||||
return len(bridge.GetUserIDs()) == 1 && len(getConnectedUserIDs(t, bridge)) == 0
|
||||
}, 100*user.EventPeriod, user.EventPeriod)
|
||||
badEvent, ok := (<-badEventCh).(events.UserBadEvent)
|
||||
require.True(t, ok)
|
||||
|
||||
closeCh()
|
||||
|
||||
return badEvent.UserID
|
||||
}
|
||||
|
||||
// userContinueEventProcess checks that user will continue to process events and will not receive any bad request errors.
|
||||
func userContinueEventProcess(
|
||||
ctx context.Context,
|
||||
t *testing.T,
|
||||
|
||||
@ -22,6 +22,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/ProtonMail/gluon/reporter"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||
@ -36,9 +37,14 @@ func (bridge *Bridge) handleUserEvent(ctx context.Context, user *user.User, even
|
||||
return fmt.Errorf("failed to handle user address created event: %w", err)
|
||||
}
|
||||
|
||||
case events.UserAddressUpdated:
|
||||
if err := bridge.handleUserAddressUpdated(ctx, user, event); err != nil {
|
||||
return fmt.Errorf("failed to handle user address updated event: %w", err)
|
||||
case events.UserAddressEnabled:
|
||||
if err := bridge.handleUserAddressEnabled(ctx, user, event); err != nil {
|
||||
return fmt.Errorf("failed to handle user address enabled event: %w", err)
|
||||
}
|
||||
|
||||
case events.UserAddressDisabled:
|
||||
if err := bridge.handleUserAddressDisabled(ctx, user, event); err != nil {
|
||||
return fmt.Errorf("failed to handle user address disabled event: %w", err)
|
||||
}
|
||||
|
||||
case events.UserAddressDeleted:
|
||||
@ -47,7 +53,7 @@ func (bridge *Bridge) handleUserEvent(ctx context.Context, user *user.User, even
|
||||
}
|
||||
|
||||
case events.UserRefreshed:
|
||||
if err := bridge.handleUserRefreshed(ctx, user); err != nil {
|
||||
if err := bridge.handleUserRefreshed(ctx, user, event); err != nil {
|
||||
return fmt.Errorf("failed to handle user refreshed event: %w", err)
|
||||
}
|
||||
|
||||
@ -55,69 +61,105 @@ func (bridge *Bridge) handleUserEvent(ctx context.Context, user *user.User, even
|
||||
bridge.handleUserDeauth(ctx, user)
|
||||
|
||||
case events.UserBadEvent:
|
||||
bridge.handleUserBadEvent(ctx, user, event.Error)
|
||||
bridge.handleUserBadEvent(ctx, user, event)
|
||||
|
||||
case events.UncategorizedEventError:
|
||||
bridge.handleUncategorizedErrorEvent(event)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bridge *Bridge) handleUserAddressCreated(ctx context.Context, user *user.User, event events.UserAddressCreated) error {
|
||||
if user.GetAddressMode() == vault.SplitMode {
|
||||
if bridge.imapServer == nil {
|
||||
return fmt.Errorf("no imap server instance running")
|
||||
}
|
||||
if user.GetAddressMode() == vault.CombinedMode {
|
||||
return nil
|
||||
}
|
||||
|
||||
gluonID, err := bridge.imapServer.AddUser(ctx, user.NewIMAPConnector(event.AddressID), user.GluonKey())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add user to IMAP server: %w", err)
|
||||
}
|
||||
if bridge.imapServer == nil {
|
||||
return fmt.Errorf("no imap server instance running")
|
||||
}
|
||||
|
||||
if err := user.SetGluonID(event.AddressID, gluonID); err != nil {
|
||||
return fmt.Errorf("failed to set gluon ID: %w", err)
|
||||
}
|
||||
gluonID, err := bridge.imapServer.AddUser(ctx, user.NewIMAPConnector(event.AddressID), user.GluonKey())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add user to IMAP server: %w", err)
|
||||
}
|
||||
|
||||
if err := user.SetGluonID(event.AddressID, gluonID); err != nil {
|
||||
return fmt.Errorf("failed to set gluon ID: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GODT-1948: Handle addresses that have been disabled!
|
||||
func (bridge *Bridge) handleUserAddressUpdated(_ context.Context, user *user.User, _ events.UserAddressUpdated) error {
|
||||
switch user.GetAddressMode() {
|
||||
case vault.CombinedMode:
|
||||
return fmt.Errorf("not implemented")
|
||||
func (bridge *Bridge) handleUserAddressEnabled(ctx context.Context, user *user.User, event events.UserAddressEnabled) error {
|
||||
if user.GetAddressMode() == vault.CombinedMode {
|
||||
return nil
|
||||
}
|
||||
|
||||
case vault.SplitMode:
|
||||
return fmt.Errorf("not implemented")
|
||||
gluonID, err := bridge.imapServer.AddUser(ctx, user.NewIMAPConnector(event.AddressID), user.GluonKey())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add user to IMAP server: %w", err)
|
||||
}
|
||||
|
||||
if err := user.SetGluonID(event.AddressID, gluonID); err != nil {
|
||||
return fmt.Errorf("failed to set gluon ID: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bridge *Bridge) handleUserAddressDisabled(ctx context.Context, user *user.User, event events.UserAddressDisabled) error {
|
||||
if user.GetAddressMode() == vault.CombinedMode {
|
||||
return nil
|
||||
}
|
||||
|
||||
gluonID, ok := user.GetGluonID(event.AddressID)
|
||||
if !ok {
|
||||
return fmt.Errorf("gluon ID not found for address %s", event.AddressID)
|
||||
}
|
||||
|
||||
if err := bridge.imapServer.RemoveUser(ctx, gluonID, true); err != nil {
|
||||
return fmt.Errorf("failed to remove user from IMAP server: %w", err)
|
||||
}
|
||||
|
||||
if err := user.RemoveGluonID(event.AddressID, gluonID); err != nil {
|
||||
return fmt.Errorf("failed to remove gluon ID for address: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bridge *Bridge) handleUserAddressDeleted(ctx context.Context, user *user.User, event events.UserAddressDeleted) error {
|
||||
if user.GetAddressMode() == vault.SplitMode {
|
||||
if bridge.imapServer == nil {
|
||||
return fmt.Errorf("no imap server instance running")
|
||||
}
|
||||
if user.GetAddressMode() == vault.CombinedMode {
|
||||
return nil
|
||||
}
|
||||
|
||||
gluonID, ok := user.GetGluonID(event.AddressID)
|
||||
if !ok {
|
||||
return fmt.Errorf("gluon ID not found for address %s", event.AddressID)
|
||||
}
|
||||
if bridge.imapServer == nil {
|
||||
return fmt.Errorf("no imap server instance running")
|
||||
}
|
||||
|
||||
if err := bridge.imapServer.RemoveUser(ctx, gluonID, true); err != nil {
|
||||
return fmt.Errorf("failed to remove user from IMAP server: %w", err)
|
||||
}
|
||||
gluonID, ok := user.GetGluonID(event.AddressID)
|
||||
if !ok {
|
||||
return fmt.Errorf("gluon ID not found for address %s", event.AddressID)
|
||||
}
|
||||
|
||||
if err := user.RemoveGluonID(event.AddressID, gluonID); err != nil {
|
||||
return fmt.Errorf("failed to remove gluon ID for address: %w", err)
|
||||
}
|
||||
if err := bridge.imapServer.RemoveUser(ctx, gluonID, true); err != nil {
|
||||
return fmt.Errorf("failed to remove user from IMAP server: %w", err)
|
||||
}
|
||||
|
||||
if err := user.RemoveGluonID(event.AddressID, gluonID); err != nil {
|
||||
return fmt.Errorf("failed to remove gluon ID for address: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bridge *Bridge) handleUserRefreshed(ctx context.Context, user *user.User) error {
|
||||
func (bridge *Bridge) handleUserRefreshed(ctx context.Context, user *user.User, event events.UserRefreshed) error {
|
||||
return safe.RLockRet(func() error {
|
||||
if event.CancelEventPool {
|
||||
user.CancelSyncAndEventPoll()
|
||||
}
|
||||
|
||||
if err := bridge.removeIMAPUser(ctx, user, true); err != nil {
|
||||
return fmt.Errorf("failed to remove IMAP user: %w", err)
|
||||
}
|
||||
@ -136,14 +178,33 @@ func (bridge *Bridge) handleUserDeauth(ctx context.Context, user *user.User) {
|
||||
}, bridge.usersLock)
|
||||
}
|
||||
|
||||
func (bridge *Bridge) handleUserBadEvent(ctx context.Context, user *user.User, err error) {
|
||||
func (bridge *Bridge) handleUserBadEvent(_ context.Context, user *user.User, event events.UserBadEvent) {
|
||||
safe.Lock(func() {
|
||||
if rerr := bridge.reporter.ReportMessageWithContext("Failed to handle event", reporter.Context{
|
||||
"error": err,
|
||||
"user_id": user.ID(),
|
||||
"old_event_id": event.OldEventID,
|
||||
"new_event_id": event.NewEventID,
|
||||
"event_info": event.EventInfo,
|
||||
"error": event.Error,
|
||||
"error_type": internal.ErrCauseType(event.Error),
|
||||
}); rerr != nil {
|
||||
logrus.WithError(rerr).Error("Failed to report failed event handling")
|
||||
}
|
||||
|
||||
bridge.logoutUser(ctx, user, true, false)
|
||||
user.CancelSyncAndEventPoll()
|
||||
|
||||
// Disable IMAP user
|
||||
if err := bridge.removeIMAPUser(context.Background(), user, false); err != nil {
|
||||
logrus.WithError(err).Error("Failed to remove IMAP user")
|
||||
}
|
||||
}, bridge.usersLock)
|
||||
}
|
||||
|
||||
func (bridge *Bridge) handleUncategorizedErrorEvent(event events.UncategorizedEventError) {
|
||||
if rerr := bridge.reporter.ReportMessageWithContext("Failed to handle due to uncategorized error", reporter.Context{
|
||||
"error_type": internal.ErrCauseType(event.Error),
|
||||
"error": event.Error,
|
||||
}); rerr != nil {
|
||||
logrus.WithError(rerr).Error("Failed to report failed event handling")
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,6 +20,8 @@ package bridge_test
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@ -61,6 +63,50 @@ func TestBridge_Login(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_Login_DropConn(t *testing.T) {
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
|
||||
dropListener := proton.NewListener(l, proton.NewDropConn)
|
||||
defer func() { _ = dropListener.Close() }()
|
||||
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Login the user.
|
||||
userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// The user is now connected.
|
||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
|
||||
})
|
||||
|
||||
// Whether to allow the user to be created.
|
||||
var allowUser bool
|
||||
|
||||
s.AddStatusHook(func(req *http.Request) (int, bool) {
|
||||
// Drop any request to the users endpoint.
|
||||
if !allowUser && req.URL.Path == "/core/v4/users" {
|
||||
dropListener.DropAll()
|
||||
}
|
||||
|
||||
// After the ping request, allow the user to be created.
|
||||
if req.URL.Path == "/tests/ping" {
|
||||
allowUser = true
|
||||
}
|
||||
|
||||
return 0, false
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// The user is eventually connected.
|
||||
require.Eventually(t, func() bool {
|
||||
return len(bridge.GetUserIDs()) == 1 && len(getConnectedUserIDs(t, bridge)) == 1
|
||||
}, 5*time.Second, 100*time.Millisecond)
|
||||
})
|
||||
}, server.WithListener(dropListener))
|
||||
}
|
||||
|
||||
func TestBridge_LoginTwice(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
|
||||
@ -98,6 +98,8 @@ func saveConfigTemporarily(mc *mobileconfig.Config) (fname string, err error) {
|
||||
|
||||
// Make sure the temporary file is deleted.
|
||||
go func() {
|
||||
defer recover() //nolint:errcheck
|
||||
|
||||
<-time.After(10 * time.Minute)
|
||||
_ = os.RemoveAll(dir)
|
||||
}()
|
||||
|
||||
@ -44,6 +44,9 @@ var (
|
||||
|
||||
// DSNSentry client keys to be able to report crashes to Sentry.
|
||||
DSNSentry = ""
|
||||
|
||||
// BuildEnv tags used at build time.
|
||||
BuildEnv = ""
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@ -38,14 +38,16 @@ func (h *Handler) AddRecoveryAction(action RecoveryAction) *Handler {
|
||||
return h
|
||||
}
|
||||
|
||||
func (h *Handler) HandlePanic() {
|
||||
func (h *Handler) HandlePanic(r interface{}) {
|
||||
sentry.SkipDuringUnwind()
|
||||
|
||||
if r := recover(); r != nil {
|
||||
for _, action := range h.actions {
|
||||
if err := action(r); err != nil {
|
||||
logrus.WithError(err).Error("Failed to execute recovery action")
|
||||
}
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, action := range h.actions {
|
||||
if err := action(r); err != nil {
|
||||
logrus.WithError(err).Error("Failed to execute recovery action")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,38 +21,41 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestHandler(t *testing.T) {
|
||||
var s string
|
||||
assert.NotPanics(t, func() {
|
||||
var s string
|
||||
|
||||
h := NewHandler(
|
||||
func(r interface{}) error {
|
||||
s += fmt.Sprintf("1: %v\n", r)
|
||||
return nil
|
||||
},
|
||||
func(r interface{}) error {
|
||||
s += fmt.Sprintf("2: %v\n", r)
|
||||
return nil
|
||||
},
|
||||
)
|
||||
h := NewHandler(
|
||||
func(r interface{}) error {
|
||||
s += fmt.Sprintf("1: %v\n", r)
|
||||
return nil
|
||||
},
|
||||
func(r interface{}) error {
|
||||
s += fmt.Sprintf("2: %v\n", r)
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
h.
|
||||
AddRecoveryAction(func(r interface{}) error {
|
||||
s += fmt.Sprintf("3: %v\n", r)
|
||||
return nil
|
||||
}).
|
||||
AddRecoveryAction(func(r interface{}) error {
|
||||
s += fmt.Sprintf("4: %v\n", r)
|
||||
return nil
|
||||
})
|
||||
h.
|
||||
AddRecoveryAction(func(r interface{}) error {
|
||||
s += fmt.Sprintf("3: %v\n", r)
|
||||
return nil
|
||||
}).
|
||||
AddRecoveryAction(func(r interface{}) error {
|
||||
s += fmt.Sprintf("4: %v\n", r)
|
||||
return nil
|
||||
})
|
||||
|
||||
defer func() {
|
||||
assert.Equal(t, "1: thing\n2: thing\n3: thing\n4: thing\n", s)
|
||||
}()
|
||||
defer func() {
|
||||
assert.Equal(t, "1: thing\n2: thing\n3: thing\n4: thing\n", s)
|
||||
}()
|
||||
|
||||
defer h.HandlePanic()
|
||||
defer async.HandlePanic(h)
|
||||
|
||||
panic("thing")
|
||||
panic("thing")
|
||||
})
|
||||
}
|
||||
|
||||
@ -24,6 +24,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
@ -40,17 +41,20 @@ type ProxyTLSDialer struct {
|
||||
allowProxy bool
|
||||
proxyProvider *proxyProvider
|
||||
proxyUseDuration time.Duration
|
||||
|
||||
panicHandler async.PanicHandler
|
||||
}
|
||||
|
||||
// NewProxyTLSDialer constructs a dialer which provides a proxy-managing layer on top of an underlying dialer.
|
||||
func NewProxyTLSDialer(dialer TLSDialer, hostURL string) *ProxyTLSDialer {
|
||||
func NewProxyTLSDialer(dialer TLSDialer, hostURL string, panicHandler async.PanicHandler) *ProxyTLSDialer {
|
||||
return &ProxyTLSDialer{
|
||||
dialer: dialer,
|
||||
locker: sync.RWMutex{},
|
||||
directAddress: formatAsAddress(hostURL),
|
||||
proxyAddress: formatAsAddress(hostURL),
|
||||
proxyProvider: newProxyProvider(dialer, hostURL, DoHProviders),
|
||||
proxyProvider: newProxyProvider(dialer, hostURL, DoHProviders, panicHandler),
|
||||
proxyUseDuration: proxyUseDuration,
|
||||
panicHandler: panicHandler,
|
||||
}
|
||||
}
|
||||
|
||||
@ -129,6 +133,8 @@ func (d *ProxyTLSDialer) switchToReachableServer() error {
|
||||
// This means we want to disable it again in 24 hours.
|
||||
if d.proxyAddress == d.directAddress {
|
||||
go func() {
|
||||
defer async.HandlePanic(d.panicHandler)
|
||||
|
||||
<-time.After(d.proxyUseDuration)
|
||||
|
||||
d.locker.Lock()
|
||||
|
||||
@ -24,6 +24,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/pkg/errors"
|
||||
@ -67,11 +68,13 @@ type proxyProvider struct {
|
||||
canReachTimeout time.Duration
|
||||
|
||||
lastLookup time.Time // The time at which we last attempted to find a proxy.
|
||||
|
||||
panicHandler async.PanicHandler
|
||||
}
|
||||
|
||||
// newProxyProvider creates a new proxyProvider that queries the given DoH providers
|
||||
// to retrieve DNS records for the given query string.
|
||||
func newProxyProvider(dialer TLSDialer, hostURL string, providers []string) (p *proxyProvider) {
|
||||
func newProxyProvider(dialer TLSDialer, hostURL string, providers []string, panicHandler async.PanicHandler) (p *proxyProvider) {
|
||||
p = &proxyProvider{
|
||||
dialer: dialer,
|
||||
hostURL: hostURL,
|
||||
@ -80,6 +83,7 @@ func newProxyProvider(dialer TLSDialer, hostURL string, providers []string) (p *
|
||||
cacheRefreshTimeout: proxyCacheRefreshTimeout,
|
||||
dohTimeout: proxyDoHTimeout,
|
||||
canReachTimeout: proxyCanReachTimeout,
|
||||
panicHandler: panicHandler,
|
||||
}
|
||||
|
||||
// Use the default DNS lookup method; this can be overridden if necessary.
|
||||
@ -109,11 +113,13 @@ func (p *proxyProvider) findReachableServer() (proxy string, err error) {
|
||||
wg.Add(2)
|
||||
|
||||
go func() {
|
||||
defer async.HandlePanic(p.panicHandler)
|
||||
defer wg.Done()
|
||||
apiReachable = p.canReach(p.hostURL)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer async.HandlePanic(p.panicHandler)
|
||||
defer wg.Done()
|
||||
err = p.refreshProxyCache()
|
||||
}()
|
||||
@ -150,6 +156,8 @@ func (p *proxyProvider) refreshProxyCache() error {
|
||||
resultChan := make(chan []string)
|
||||
|
||||
go func() {
|
||||
defer async.HandlePanic(p.panicHandler)
|
||||
|
||||
for _, provider := range p.providers {
|
||||
if proxies, err := p.dohLookup(ctx, p.query, provider); err == nil {
|
||||
resultChan <- proxies
|
||||
@ -203,6 +211,7 @@ func (p *proxyProvider) defaultDoHLookup(ctx context.Context, query, dohProvider
|
||||
dataChan, errChan := make(chan []string), make(chan error)
|
||||
|
||||
go func() {
|
||||
defer async.HandlePanic(p.panicHandler)
|
||||
// Build new DNS request in RFC1035 format.
|
||||
dnsRequest := new(dns.Msg).SetQuestion(dns.Fqdn(query), dns.TypeTXT)
|
||||
|
||||
|
||||
@ -23,6 +23,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
|
||||
r "github.com/stretchr/testify/require"
|
||||
)
|
||||
@ -31,7 +32,7 @@ func TestProxyProvider_FindProxy(t *testing.T) {
|
||||
proxy := getTrustedServer()
|
||||
defer closeServer(proxy)
|
||||
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"not used"})
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"not used"}, async.NoopPanicHandler{})
|
||||
p.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{proxy.URL}, nil }
|
||||
|
||||
url, err := p.findReachableServer()
|
||||
@ -47,7 +48,7 @@ func TestProxyProvider_FindProxy_ChooseReachableProxy(t *testing.T) {
|
||||
unreachableProxy := getTrustedServer()
|
||||
closeServer(unreachableProxy)
|
||||
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"not used"})
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"not used"}, async.NoopPanicHandler{})
|
||||
p.dohLookup = func(ctx context.Context, q, p string) ([]string, error) {
|
||||
return []string{reachableProxy.URL, unreachableProxy.URL}, nil
|
||||
}
|
||||
@ -68,7 +69,7 @@ func TestProxyProvider_FindProxy_ChooseTrustedProxy(t *testing.T) {
|
||||
checker := NewTLSPinChecker(TrustedAPIPins)
|
||||
dialer := NewPinningTLSDialer(NewBasicTLSDialer(""), reporter, checker)
|
||||
|
||||
p := newProxyProvider(dialer, "", []string{"not used"})
|
||||
p := newProxyProvider(dialer, "", []string{"not used"}, async.NoopPanicHandler{})
|
||||
p.dohLookup = func(ctx context.Context, q, p string) ([]string, error) {
|
||||
return []string{untrustedProxy.URL, trustedProxy.URL}, nil
|
||||
}
|
||||
@ -85,7 +86,7 @@ func TestProxyProvider_FindProxy_FailIfNoneReachable(t *testing.T) {
|
||||
unreachableProxy2 := getTrustedServer()
|
||||
closeServer(unreachableProxy2)
|
||||
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"not used"})
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"not used"}, async.NoopPanicHandler{})
|
||||
p.dohLookup = func(ctx context.Context, q, p string) ([]string, error) {
|
||||
return []string{unreachableProxy1.URL, unreachableProxy2.URL}, nil
|
||||
}
|
||||
@ -105,7 +106,7 @@ func TestProxyProvider_FindProxy_FailIfNoneTrusted(t *testing.T) {
|
||||
checker := NewTLSPinChecker(TrustedAPIPins)
|
||||
dialer := NewPinningTLSDialer(NewBasicTLSDialer(""), reporter, checker)
|
||||
|
||||
p := newProxyProvider(dialer, "", []string{"not used"})
|
||||
p := newProxyProvider(dialer, "", []string{"not used"}, async.NoopPanicHandler{})
|
||||
p.dohLookup = func(ctx context.Context, q, p string) ([]string, error) {
|
||||
return []string{untrustedProxy1.URL, untrustedProxy2.URL}, nil
|
||||
}
|
||||
@ -115,7 +116,7 @@ func TestProxyProvider_FindProxy_FailIfNoneTrusted(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestProxyProvider_FindProxy_RefreshCacheTimeout(t *testing.T) {
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"not used"})
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"not used"}, async.NoopPanicHandler{})
|
||||
p.cacheRefreshTimeout = 1 * time.Second
|
||||
p.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { time.Sleep(2 * time.Second); return nil, nil }
|
||||
|
||||
@ -132,7 +133,7 @@ func TestProxyProvider_FindProxy_CanReachTimeout(t *testing.T) {
|
||||
}))
|
||||
defer closeServer(slowProxy)
|
||||
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"not used"})
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"not used"}, async.NoopPanicHandler{})
|
||||
p.canReachTimeout = 1 * time.Second
|
||||
p.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{slowProxy.URL}, nil }
|
||||
|
||||
@ -144,7 +145,7 @@ func TestProxyProvider_FindProxy_CanReachTimeout(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestProxyProvider_DoHLookup_Quad9(t *testing.T) {
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{Quad9Provider, GoogleProvider})
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{Quad9Provider, GoogleProvider}, async.NoopPanicHandler{})
|
||||
|
||||
records, err := p.dohLookup(context.Background(), proxyQuery, Quad9Provider)
|
||||
r.NoError(t, err)
|
||||
@ -155,7 +156,7 @@ func TestProxyProvider_DoHLookup_Quad9(t *testing.T) {
|
||||
// port filter. Basic functionality should be covered by other tests. Keeping
|
||||
// code here to be able to run it locally if needed.
|
||||
func DISABLEDTestProxyProviderDoHLookupQuad9Port(t *testing.T) {
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{Quad9Provider, GoogleProvider})
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{Quad9Provider, GoogleProvider}, async.NoopPanicHandler{})
|
||||
|
||||
records, err := p.dohLookup(context.Background(), proxyQuery, Quad9PortProvider)
|
||||
r.NoError(t, err)
|
||||
@ -163,7 +164,7 @@ func DISABLEDTestProxyProviderDoHLookupQuad9Port(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestProxyProvider_DoHLookup_Google(t *testing.T) {
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{Quad9Provider, GoogleProvider})
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{Quad9Provider, GoogleProvider}, async.NoopPanicHandler{})
|
||||
|
||||
records, err := p.dohLookup(context.Background(), proxyQuery, GoogleProvider)
|
||||
r.NoError(t, err)
|
||||
@ -173,7 +174,7 @@ func TestProxyProvider_DoHLookup_Google(t *testing.T) {
|
||||
func TestProxyProvider_DoHLookup_FindProxy(t *testing.T) {
|
||||
skipIfProxyIsSet(t)
|
||||
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{Quad9Provider, GoogleProvider})
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{Quad9Provider, GoogleProvider}, async.NoopPanicHandler{})
|
||||
|
||||
url, err := p.findReachableServer()
|
||||
r.NoError(t, err)
|
||||
@ -183,7 +184,7 @@ func TestProxyProvider_DoHLookup_FindProxy(t *testing.T) {
|
||||
func TestProxyProvider_DoHLookup_FindProxyFirstProviderUnreachable(t *testing.T) {
|
||||
skipIfProxyIsSet(t)
|
||||
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"https://unreachable", Quad9Provider, GoogleProvider})
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"https://unreachable", Quad9Provider, GoogleProvider}, async.NoopPanicHandler{})
|
||||
|
||||
url, err := p.findReachableServer()
|
||||
r.NoError(t, err)
|
||||
|
||||
@ -25,6 +25,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@ -141,8 +142,8 @@ func TestProxyDialer_UseProxy(t *testing.T) {
|
||||
trustedProxy := getTrustedServer()
|
||||
defer closeServer(trustedProxy)
|
||||
|
||||
provider := newProxyProvider(NewBasicTLSDialer(""), "", DoHProviders)
|
||||
d := NewProxyTLSDialer(NewBasicTLSDialer(""), "")
|
||||
provider := newProxyProvider(NewBasicTLSDialer(""), "", DoHProviders, async.NoopPanicHandler{})
|
||||
d := NewProxyTLSDialer(NewBasicTLSDialer(""), "", async.NoopPanicHandler{})
|
||||
d.proxyProvider = provider
|
||||
provider.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{trustedProxy.URL}, nil }
|
||||
|
||||
@ -159,8 +160,8 @@ func TestProxyDialer_UseProxy_MultipleTimes(t *testing.T) {
|
||||
proxy3 := getTrustedServer()
|
||||
defer closeServer(proxy3)
|
||||
|
||||
provider := newProxyProvider(NewBasicTLSDialer(""), "", DoHProviders)
|
||||
d := NewProxyTLSDialer(NewBasicTLSDialer(""), "")
|
||||
provider := newProxyProvider(NewBasicTLSDialer(""), "", DoHProviders, async.NoopPanicHandler{})
|
||||
d := NewProxyTLSDialer(NewBasicTLSDialer(""), "", async.NoopPanicHandler{})
|
||||
d.proxyProvider = provider
|
||||
provider.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{proxy1.URL}, nil }
|
||||
|
||||
@ -189,8 +190,8 @@ func TestProxyDialer_UseProxy_RevertAfterTime(t *testing.T) {
|
||||
trustedProxy := getTrustedServer()
|
||||
defer closeServer(trustedProxy)
|
||||
|
||||
provider := newProxyProvider(NewBasicTLSDialer(""), "", DoHProviders)
|
||||
d := NewProxyTLSDialer(NewBasicTLSDialer(""), "")
|
||||
provider := newProxyProvider(NewBasicTLSDialer(""), "", DoHProviders, async.NoopPanicHandler{})
|
||||
d := NewProxyTLSDialer(NewBasicTLSDialer(""), "", async.NoopPanicHandler{})
|
||||
d.proxyProvider = provider
|
||||
d.proxyUseDuration = time.Second
|
||||
|
||||
@ -212,8 +213,8 @@ func TestProxyDialer_UseProxy_RevertAfterTime(t *testing.T) {
|
||||
func TestProxyDialer_UseProxy_RevertIfProxyStopsWorkingAndOriginalAPIIsReachable(t *testing.T) {
|
||||
trustedProxy := getTrustedServer()
|
||||
|
||||
provider := newProxyProvider(NewBasicTLSDialer(""), "", DoHProviders)
|
||||
d := NewProxyTLSDialer(NewBasicTLSDialer(""), "")
|
||||
provider := newProxyProvider(NewBasicTLSDialer(""), "", DoHProviders, async.NoopPanicHandler{})
|
||||
d := NewProxyTLSDialer(NewBasicTLSDialer(""), "", async.NoopPanicHandler{})
|
||||
d.proxyProvider = provider
|
||||
provider.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{trustedProxy.URL}, nil }
|
||||
|
||||
@ -242,8 +243,8 @@ func TestProxyDialer_UseProxy_FindSecondAlternativeIfFirstFailsAndAPIIsStillBloc
|
||||
proxy2 := getTrustedServer()
|
||||
defer closeServer(proxy2)
|
||||
|
||||
provider := newProxyProvider(NewBasicTLSDialer(""), "", DoHProviders)
|
||||
d := NewProxyTLSDialer(NewBasicTLSDialer(""), "")
|
||||
provider := newProxyProvider(NewBasicTLSDialer(""), "", DoHProviders, async.NoopPanicHandler{})
|
||||
d := NewProxyTLSDialer(NewBasicTLSDialer(""), "", async.NoopPanicHandler{})
|
||||
d.proxyProvider = provider
|
||||
provider.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{proxy1.URL, proxy2.URL}, nil }
|
||||
|
||||
|
||||
38
internal/errors.go
Normal file
38
internal/errors.go
Normal 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
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package internal
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ErrCause returns the cause of the error, the inner-most error in the wrapped chain.
|
||||
func ErrCause(err error) error {
|
||||
cause := err
|
||||
|
||||
for errors.Unwrap(cause) != nil {
|
||||
cause = errors.Unwrap(cause)
|
||||
}
|
||||
|
||||
return cause
|
||||
}
|
||||
|
||||
func ErrCauseType(err error) string {
|
||||
return fmt.Sprintf("%T", ErrCause(err))
|
||||
}
|
||||
@ -35,6 +35,30 @@ func (event UserAddressCreated) String() string {
|
||||
return fmt.Sprintf("UserAddressCreated: UserID: %s, AddressID: %s, Email: %s", event.UserID, event.AddressID, logging.Sensitive(event.Email))
|
||||
}
|
||||
|
||||
type UserAddressEnabled struct {
|
||||
eventBase
|
||||
|
||||
UserID string
|
||||
AddressID string
|
||||
Email string
|
||||
}
|
||||
|
||||
func (event UserAddressEnabled) String() string {
|
||||
return fmt.Sprintf("UserAddressEnabled: UserID: %s, AddressID: %s, Email: %s", event.UserID, event.AddressID, logging.Sensitive(event.Email))
|
||||
}
|
||||
|
||||
type UserAddressDisabled struct {
|
||||
eventBase
|
||||
|
||||
UserID string
|
||||
AddressID string
|
||||
Email string
|
||||
}
|
||||
|
||||
func (event UserAddressDisabled) String() string {
|
||||
return fmt.Sprintf("UserAddressDisabled: UserID: %s, AddressID: %s, Email: %s", event.UserID, event.AddressID, logging.Sensitive(event.Email))
|
||||
}
|
||||
|
||||
type UserAddressUpdated struct {
|
||||
eventBase
|
||||
|
||||
|
||||
76
internal/events/serve.go
Normal file
76
internal/events/serve.go
Normal file
@ -0,0 +1,76 @@
|
||||
// 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 events
|
||||
|
||||
import "fmt"
|
||||
|
||||
type IMAPServerReady struct {
|
||||
eventBase
|
||||
|
||||
Port int
|
||||
}
|
||||
|
||||
func (event IMAPServerReady) String() string {
|
||||
return fmt.Sprintf("IMAPServerReady: Port %d", event.Port)
|
||||
}
|
||||
|
||||
type IMAPServerStopped struct {
|
||||
eventBase
|
||||
}
|
||||
|
||||
func (event IMAPServerStopped) String() string {
|
||||
return "IMAPServerStopped"
|
||||
}
|
||||
|
||||
type IMAPServerError struct {
|
||||
eventBase
|
||||
|
||||
Error error
|
||||
}
|
||||
|
||||
func (event IMAPServerError) String() string {
|
||||
return fmt.Sprintf("IMAPServerError: %v", event.Error)
|
||||
}
|
||||
|
||||
type SMTPServerReady struct {
|
||||
eventBase
|
||||
|
||||
Port int
|
||||
}
|
||||
|
||||
func (event SMTPServerReady) String() string {
|
||||
return fmt.Sprintf("SMTPServerReady: Port %d", event.Port)
|
||||
}
|
||||
|
||||
type SMTPServerStopped struct {
|
||||
eventBase
|
||||
}
|
||||
|
||||
func (event SMTPServerStopped) String() string {
|
||||
return "SMTPServerStopped"
|
||||
}
|
||||
|
||||
type SMTPServerError struct {
|
||||
eventBase
|
||||
|
||||
Error error
|
||||
}
|
||||
|
||||
func (event SMTPServerError) String() string {
|
||||
return fmt.Sprintf("SMTPServerError: %v", event.Error)
|
||||
}
|
||||
@ -103,12 +103,23 @@ func (event UserDeauth) String() string {
|
||||
type UserBadEvent struct {
|
||||
eventBase
|
||||
|
||||
UserID string
|
||||
Error error
|
||||
UserID string
|
||||
OldEventID string
|
||||
NewEventID string
|
||||
EventInfo string
|
||||
|
||||
Error error
|
||||
}
|
||||
|
||||
func (event UserBadEvent) String() string {
|
||||
return fmt.Sprintf("UserBadEvent: UserID: %s, Error: %s", event.UserID, event.Error)
|
||||
return fmt.Sprintf(
|
||||
"UserBadEvent: UserID: %s, OldEventID: %s, NewEventID: %s, EventInfo: %v, Error: %s",
|
||||
event.UserID,
|
||||
event.OldEventID,
|
||||
event.NewEventID,
|
||||
event.EventInfo,
|
||||
event.Error,
|
||||
)
|
||||
}
|
||||
|
||||
// UserDeleted is emitted when a user has been deleted.
|
||||
@ -137,7 +148,8 @@ func (event UserChanged) String() string {
|
||||
type UserRefreshed struct {
|
||||
eventBase
|
||||
|
||||
UserID string
|
||||
UserID string
|
||||
CancelEventPool bool
|
||||
}
|
||||
|
||||
func (event UserRefreshed) String() string {
|
||||
@ -156,3 +168,37 @@ type AddressModeChanged struct {
|
||||
func (event AddressModeChanged) String() string {
|
||||
return fmt.Sprintf("AddressModeChanged: UserID: %s, AddressMode: %s", event.UserID, event.AddressMode)
|
||||
}
|
||||
|
||||
// UsedSpaceChanged is emitted when the storage space used by the user has changed.
|
||||
type UsedSpaceChanged struct {
|
||||
eventBase
|
||||
|
||||
UserID string
|
||||
|
||||
UsedSpace int
|
||||
}
|
||||
|
||||
func (event UsedSpaceChanged) String() string {
|
||||
return fmt.Sprintf("UsedSpaceChanged: UserID: %s, UsedSpace: %v", event.UserID, event.UsedSpace)
|
||||
}
|
||||
|
||||
type IMAPLoginFailed struct {
|
||||
eventBase
|
||||
|
||||
Username string
|
||||
}
|
||||
|
||||
func (event IMAPLoginFailed) String() string {
|
||||
return fmt.Sprintf("IMAPLoginFailed: Username: %s", event.Username)
|
||||
}
|
||||
|
||||
type UncategorizedEventError struct {
|
||||
eventBase
|
||||
|
||||
UserID string
|
||||
Error error
|
||||
}
|
||||
|
||||
func (event UncategorizedEventError) String() string {
|
||||
return fmt.Sprintf("UncategorizedEventError: UserID: %s, Source:%T, Error: %s", event.UserID, event.Error, event.Error)
|
||||
}
|
||||
|
||||
@ -21,9 +21,11 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/focus/proto"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/service"
|
||||
"github.com/sirupsen/logrus"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
@ -32,10 +34,10 @@ import (
|
||||
|
||||
// TryRaise tries to raise the application by dialing the focus service.
|
||||
// It returns true if the service is running and the application was told to raise.
|
||||
func TryRaise() bool {
|
||||
func TryRaise(settingsPath string) bool {
|
||||
var raised bool
|
||||
|
||||
if err := withClientConn(context.Background(), func(ctx context.Context, client proto.FocusClient) error {
|
||||
if err := withClientConn(context.Background(), settingsPath, func(ctx context.Context, client proto.FocusClient) error {
|
||||
if _, err := client.Raise(ctx, &emptypb.Empty{}); err != nil {
|
||||
return fmt.Errorf("failed to call client.Raise: %w", err)
|
||||
}
|
||||
@ -53,10 +55,10 @@ func TryRaise() bool {
|
||||
|
||||
// TryVersion tries to determine the version of the running application instance.
|
||||
// It returns the version and true if the version could be determined.
|
||||
func TryVersion() (*semver.Version, bool) {
|
||||
func TryVersion(settingsPath string) (*semver.Version, bool) {
|
||||
var version *semver.Version
|
||||
|
||||
if err := withClientConn(context.Background(), func(ctx context.Context, client proto.FocusClient) error {
|
||||
if err := withClientConn(context.Background(), settingsPath, func(ctx context.Context, client proto.FocusClient) error {
|
||||
raw, err := client.Version(ctx, &emptypb.Empty{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to call client.Version: %w", err)
|
||||
@ -78,10 +80,15 @@ func TryVersion() (*semver.Version, bool) {
|
||||
return version, true
|
||||
}
|
||||
|
||||
func withClientConn(ctx context.Context, fn func(context.Context, proto.FocusClient) error) error {
|
||||
func withClientConn(ctx context.Context, settingsPath string, fn func(context.Context, proto.FocusClient) error) error {
|
||||
var config = service.Config{}
|
||||
err := config.Load(filepath.Join(settingsPath, serverConfigFileName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cc, err := grpc.DialContext(
|
||||
ctx,
|
||||
net.JoinHostPort(Host, fmt.Sprint(Port)),
|
||||
net.JoinHostPort(Host, fmt.Sprint(config.Port)),
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
@ -18,19 +18,25 @@
|
||||
package focus
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestFocus_Raise(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
locations := locations.New(newTestLocationsProvider(tmpDir), "config-name")
|
||||
// Start the focus service.
|
||||
service, err := NewService(semver.MustParse("1.2.3"))
|
||||
service, err := NewService(locations, semver.MustParse("1.2.3"), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
settingsFolder, err := locations.ProvideSettingsPath()
|
||||
require.NoError(t, err)
|
||||
// Try to dial it, it should succeed.
|
||||
require.True(t, TryRaise())
|
||||
require.True(t, TryRaise(settingsFolder))
|
||||
|
||||
// The service should report a raise call.
|
||||
<-service.GetRaiseCh()
|
||||
@ -39,16 +45,60 @@ func TestFocus_Raise(t *testing.T) {
|
||||
service.Close()
|
||||
|
||||
// Try to dial it, it should fail.
|
||||
require.False(t, TryRaise())
|
||||
require.False(t, TryRaise(settingsFolder))
|
||||
}
|
||||
|
||||
func TestFocus_Version(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
locations := locations.New(newTestLocationsProvider(tmpDir), "config-name")
|
||||
// Start the focus service.
|
||||
_, err := NewService(semver.MustParse("1.2.3"))
|
||||
_, err := NewService(locations, semver.MustParse("1.2.3"), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
settingsFolder, err := locations.ProvideSettingsPath()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Try to dial it, it should succeed.
|
||||
version, ok := TryVersion()
|
||||
version, ok := TryVersion(settingsFolder)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "1.2.3", version.String())
|
||||
}
|
||||
|
||||
type TestLocationsProvider struct {
|
||||
config, data, cache string
|
||||
}
|
||||
|
||||
func newTestLocationsProvider(dir string) *TestLocationsProvider {
|
||||
config, err := os.MkdirTemp(dir, "config")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
data, err := os.MkdirTemp(dir, "data")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
cache, err := os.MkdirTemp(dir, "cache")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &TestLocationsProvider{
|
||||
config: config,
|
||||
data: data,
|
||||
cache: cache,
|
||||
}
|
||||
}
|
||||
|
||||
func (provider *TestLocationsProvider) UserConfig() string {
|
||||
return provider.config
|
||||
}
|
||||
|
||||
func (provider *TestLocationsProvider) UserData() string {
|
||||
return provider.data
|
||||
}
|
||||
|
||||
func (provider *TestLocationsProvider) UserCache() string {
|
||||
return provider.cache
|
||||
}
|
||||
|
||||
@ -24,17 +24,18 @@ import (
|
||||
"net"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/focus/proto"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/service"
|
||||
"github.com/sirupsen/logrus"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
// Host is the local host to listen on.
|
||||
const Host = "127.0.0.1"
|
||||
|
||||
// Port is the port to listen on.
|
||||
var Port = 1042 // nolint:gochecknoglobals
|
||||
const (
|
||||
Host = "127.0.0.1"
|
||||
serverConfigFileName = "grpcFocusServerConfig.json"
|
||||
)
|
||||
|
||||
// Service is a gRPC service that can be used to raise the application.
|
||||
type Service struct {
|
||||
@ -43,30 +44,48 @@ type Service struct {
|
||||
server *grpc.Server
|
||||
raiseCh chan struct{}
|
||||
version *semver.Version
|
||||
|
||||
panicHandler async.PanicHandler
|
||||
}
|
||||
|
||||
// NewService creates a new focus service.
|
||||
// It listens on the local host and port 1042 (by default).
|
||||
func NewService(version *semver.Version) (*Service, error) {
|
||||
service := &Service{
|
||||
server: grpc.NewServer(),
|
||||
raiseCh: make(chan struct{}, 1),
|
||||
version: version,
|
||||
func NewService(locator service.Locator, version *semver.Version, panicHandler async.PanicHandler) (*Service, error) {
|
||||
serv := &Service{
|
||||
server: grpc.NewServer(),
|
||||
raiseCh: make(chan struct{}, 1),
|
||||
version: version,
|
||||
panicHandler: panicHandler,
|
||||
}
|
||||
|
||||
proto.RegisterFocusServer(service.server, service)
|
||||
proto.RegisterFocusServer(serv.server, serv)
|
||||
|
||||
if listener, err := net.Listen("tcp", net.JoinHostPort(Host, fmt.Sprint(Port))); err != nil {
|
||||
logrus.WithError(err).Warn("Failed to start focus service")
|
||||
if listener, err := net.Listen("tcp", net.JoinHostPort(Host, fmt.Sprint(0))); err != nil {
|
||||
logrus.WithError(err).Warn("Failed to start focus serv")
|
||||
} else {
|
||||
config := service.Config{}
|
||||
// retrieve the port assigned by the system, so that we can put it in the config file.
|
||||
address, ok := listener.Addr().(*net.TCPAddr)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("could not retrieve gRPC service listener address")
|
||||
}
|
||||
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")
|
||||
} else {
|
||||
logrus.WithField("path", path).Info("Successfully saved gRPC Focus service config file")
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err := service.server.Serve(listener); err != nil {
|
||||
defer async.HandlePanic(serv.panicHandler)
|
||||
|
||||
if err := serv.server.Serve(listener); err != nil {
|
||||
fmt.Printf("failed to serve: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return service, nil
|
||||
return serv, nil
|
||||
}
|
||||
|
||||
// Raise implements the gRPC FocusService interface; it raises the application.
|
||||
@ -90,6 +109,8 @@ func (service *Service) GetRaiseCh() <-chan struct{} {
|
||||
// Close closes the service.
|
||||
func (service *Service) Close() {
|
||||
go func() {
|
||||
defer async.HandlePanic(service.panicHandler)
|
||||
|
||||
// we do this in a goroutine, as on Windows, the gRPC shutdown may take minutes if something tries to
|
||||
// interact with it in an invalid way (e.g. HTTP GET request from a Qt QNetworkManager instance).
|
||||
service.server.Stop()
|
||||
|
||||
2
internal/frontend/.gitignore
vendored
2
internal/frontend/.gitignore
vendored
@ -10,5 +10,5 @@ rcc_cgo_*.go
|
||||
*.qmlc
|
||||
|
||||
# Generated file
|
||||
bridge-gui/bridge-gui/Version.h
|
||||
bridge-gui/bridge-gui/BuildConfig.h
|
||||
bridge-gui/bridge-gui/Resources.rc
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
find_program(QMAKE_EXE "qmake")
|
||||
find_program(QMAKE_EXE NAMES "qmake" "qmake6")
|
||||
if (NOT QMAKE_EXE)
|
||||
message(FATAL_ERROR "Could not locate qmake executable, make sur you have Qt 6 installed in that qmake is in your PATH environment variable.")
|
||||
message(FATAL_ERROR "Could not locate qmake executable, make sure you have Qt 6 installed in that qmake is in your PATH environment variable.")
|
||||
endif()
|
||||
message(STATUS "Found qmake at ${QMAKE_EXE}")
|
||||
execute_process(COMMAND "${QMAKE_EXE}" -query QT_INSTALL_PREFIX OUTPUT_VARIABLE QT_DIR OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
|
||||
set(CMAKE_PREFIX_PATH ${QT_DIR} ${CMAKE_PREFIX_PATH})
|
||||
set(CMAKE_PREFIX_PATH ${QT_DIR} ${CMAKE_PREFIX_PATH})
|
||||
|
||||
@ -53,6 +53,7 @@ void GRPCQtProxy::connectSignals() {
|
||||
connect(this, &GRPCQtProxy::logoutUserReceived, &usersTab, &UsersTab::logoutUser);
|
||||
connect(this, &GRPCQtProxy::setUserSplitModeReceived, &usersTab, &UsersTab::setUserSplitMode);
|
||||
connect(this, &GRPCQtProxy::configureUserAppleMailReceived, &usersTab, &UsersTab::configureUserAppleMail);
|
||||
connect(this, &GRPCQtProxy::sendBadEventUserFeedbackReceived, &usersTab, &UsersTab::processBadEventUserFeedback);
|
||||
}
|
||||
|
||||
|
||||
@ -178,6 +179,15 @@ void GRPCQtProxy::setUserSplitMode(QString const &userID, bool makeItActive) {
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] userID The userID.
|
||||
/// \param[in] doResync Did the user request a resync?
|
||||
//****************************************************************************************************************************************************
|
||||
void GRPCQtProxy::sendBadEventUserFeedback(QString const &userID, bool doResync) {
|
||||
emit sendBadEventUserFeedbackReceived(userID, doResync);
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] userID The userID.
|
||||
//****************************************************************************************************************************************************
|
||||
|
||||
@ -52,6 +52,7 @@ public: // member functions.
|
||||
void setDiskCachePath(QString const &path); ///< Forwards a setDiskCachePath call via a Qt signal.
|
||||
void setIsAutomaticUpdateOn(bool on); ///< Forwards a SetIsAutomaticUpdateOn call via a Qt signal.
|
||||
void setUserSplitMode(QString const &userID, bool makeItActive); ///< Forwards a setUserSplitMode call via a Qt signal.
|
||||
void sendBadEventUserFeedback(QString const &userID, bool doResync); ///< Forwards a sendBadEventUserFeedback call via a Qt signal.
|
||||
void logoutUser(QString const &userID); ///< Forwards a logoutUser call via a Qt signal.
|
||||
void removeUser(QString const &userID); ///< Forwards a removeUser call via a Qt signal.
|
||||
void configureUserAppleMail(QString const &userID, QString const &address); ///< Forwards a configureUserAppleMail call via a Qt signal.
|
||||
@ -72,6 +73,7 @@ signals:
|
||||
void setDiskCachePathReceived(QString const &path); ///< Signal for the setDiskCachePath gRPC call.
|
||||
void setIsAutomaticUpdateOnReceived(bool on); ///< Signal for the SetIsAutomaticUpdateOn gRPC call.
|
||||
void setUserSplitModeReceived(QString const &userID, bool makeItActive); ///< Signal for the SetUserSplitModeReceived gRPC call.
|
||||
void sendBadEventUserFeedbackReceived(QString const &userID, bool doResync); ///< Signal for the SendBadEventUserFeedback gRPC call.
|
||||
void logoutUserReceived(QString const &userID); ///< Signal for the LogoutUserReceived gRPC call.
|
||||
void removeUserReceived(QString const &userID); ///< Signal for the RemoveUserReceived gRPC call.
|
||||
void configureUserAppleMailReceived(QString const &userID, QString const &address); ///< Signal for the ConfigureAppleMail gRPC call.
|
||||
|
||||
@ -20,6 +20,7 @@
|
||||
#include "Cert.h"
|
||||
#include "GRPCService.h"
|
||||
#include <bridgepp/Exception/Exception.h>
|
||||
#include <bridgepp/BridgeUtils.h>
|
||||
#include <bridgepp/GRPC/GRPCUtils.h>
|
||||
#include <bridgepp/GRPC/GRPCConfig.h>
|
||||
|
||||
@ -33,7 +34,6 @@ using namespace grpc;
|
||||
//****************************************************************************************************************************************************
|
||||
GRPCServerWorker::GRPCServerWorker(QObject *parent)
|
||||
: Worker(parent) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -81,7 +81,7 @@ void GRPCServerWorker::run() {
|
||||
|
||||
config.port = port;
|
||||
QString err;
|
||||
if (!config.save(grpcServerConfigPath(), &err)) {
|
||||
if (!config.save(grpcServerConfigPath(bridgepp::userConfigDir()), &err)) {
|
||||
throw Exception(QString("Could not save gRPC server config. %1").arg(err));
|
||||
}
|
||||
|
||||
|
||||
@ -192,17 +192,6 @@ Status GRPCService::IsAllMailVisible(ServerContext *, Empty const *request, Bool
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[out] response The response.
|
||||
/// \return The status for the call.
|
||||
//****************************************************************************************************************************************************
|
||||
Status GRPCService::GoOs(ServerContext *, Empty const *, StringValue *response) {
|
||||
app().log().debug(__FUNCTION__);
|
||||
response->set_value(app().mainWindow().settingsTab().os().toStdString());
|
||||
return Status::OK;
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The status for the call.
|
||||
//****************************************************************************************************************************************************
|
||||
@ -694,6 +683,17 @@ Status GRPCService::SetUserSplitMode(ServerContext *, UserSplitModeRequest const
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] request The request.
|
||||
/// \return The status for the call.
|
||||
//****************************************************************************************************************************************************
|
||||
Status GRPCService::SendBadEventUserFeedback(ServerContext *, UserBadEventFeedbackRequest const *request, Empty *) {
|
||||
app().log().debug(__FUNCTION__);
|
||||
qtProxy_.sendBadEventUserFeedback(QString::fromStdString(request->userid()), request->doresync());
|
||||
return Status::OK;
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] request The request.
|
||||
/// \return The status for the call.
|
||||
|
||||
@ -51,7 +51,6 @@ public: // member functions.
|
||||
grpc::Status IsBetaEnabled(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::BoolValue *response) override;
|
||||
grpc::Status SetIsAllMailVisible(::grpc::ServerContext *context, ::google::protobuf::BoolValue const *request, ::google::protobuf::Empty *response) override;
|
||||
grpc::Status IsAllMailVisible(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::google::protobuf::BoolValue *response) override;
|
||||
grpc::Status GoOs(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::StringValue *response) override;
|
||||
grpc::Status TriggerReset(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::Empty *) override;
|
||||
grpc::Status Version(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::StringValue *response) override;
|
||||
grpc::Status LogsPath(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::StringValue *response) override;
|
||||
@ -88,12 +87,12 @@ public: // member functions.
|
||||
grpc::Status GetUserList(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::grpc::UserListResponse *response) override;
|
||||
grpc::Status GetUser(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::grpc::User *response) override;
|
||||
grpc::Status SetUserSplitMode(::grpc::ServerContext *, ::grpc::UserSplitModeRequest const *request, ::google::protobuf::Empty *) override;
|
||||
grpc::Status SendBadEventUserFeedback(::grpc::ServerContext *context, ::grpc::UserBadEventFeedbackRequest const *request, ::google::protobuf::Empty *response) override;
|
||||
grpc::Status LogoutUser(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
|
||||
grpc::Status RemoveUser(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
|
||||
grpc::Status ConfigureUserAppleMail(::grpc::ServerContext *, ::grpc::ConfigureAppleMailRequest const *request, ::google::protobuf::Empty *) override;
|
||||
grpc::Status RunEventStream(::grpc::ServerContext *ctx, ::grpc::EventStreamRequest const *request, ::grpc::ServerWriter<::grpc::StreamEvent> *writer) override;
|
||||
grpc::Status StopEventStream(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::Empty *) override;
|
||||
|
||||
bool sendEvent(bridgepp::SPStreamEvent const &event); ///< Queue an event for sending through the event stream.
|
||||
|
||||
private: // member functions
|
||||
|
||||
@ -52,7 +52,11 @@ UsersTab::UsersTab(QWidget *parent)
|
||||
connect(ui_.tableUserList, &QTableView::doubleClicked, this, &UsersTab::onEditUserButton);
|
||||
connect(ui_.buttonRemoveUser, &QPushButton::clicked, this, &UsersTab::onRemoveUserButton);
|
||||
connect(ui_.buttonUserBadEvent, &QPushButton::clicked, this, &UsersTab::onSendUserBadEvent);
|
||||
connect(ui_.buttonImapLoginFailed, &QPushButton::clicked, this, &UsersTab::onSendIMAPLoginFailedEvent);
|
||||
connect(ui_.buttonUsedBytesChanged, &QPushButton::clicked, this, &UsersTab::onSendUsedBytesChangedEvent);
|
||||
connect(ui_.checkUsernamePasswordError, &QCheckBox::toggled, this, &UsersTab::updateGUIState);
|
||||
connect(ui_.checkSync, &QCheckBox::toggled, this, &UsersTab::onCheckSyncToggled);
|
||||
connect(ui_.sliderSync, &QSlider::valueChanged, this, &UsersTab::onSliderSyncValueChanged);
|
||||
|
||||
users_.append(randomUser());
|
||||
|
||||
@ -144,9 +148,6 @@ void UsersTab::onSendUserBadEvent() {
|
||||
app().log().error(QString("%1 failed. User is already signed out").arg(__FUNCTION__));
|
||||
}
|
||||
|
||||
user->setState(UserState::SignedOut);
|
||||
users_.touch(index);
|
||||
|
||||
GRPCService &grpc = app().grpc();
|
||||
if (grpc.isStreaming()) {
|
||||
QString const userID = user->id();
|
||||
@ -158,16 +159,76 @@ void UsersTab::onSendUserBadEvent() {
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
//
|
||||
//****************************************************************************************************************************************************
|
||||
void UsersTab::onSendUsedBytesChangedEvent() {
|
||||
SPUser const user = selectedUser();
|
||||
int const index = this->selectedIndex();
|
||||
|
||||
if (!user) {
|
||||
app().log().error(QString("%1 failed. Unkown user.").arg(__FUNCTION__));
|
||||
return;
|
||||
}
|
||||
|
||||
if (UserState::Connected != user->state()) {
|
||||
app().log().error(QString("%1 failed. User is not connected").arg(__FUNCTION__));
|
||||
}
|
||||
|
||||
qint64 const usedBytes = qint64(ui_.spinUsedBytes->value());
|
||||
user->setUsedBytes(usedBytes);
|
||||
users_.touch(index);
|
||||
|
||||
GRPCService &grpc = app().grpc();
|
||||
if (grpc.isStreaming()) {
|
||||
QString const userID = user->id();
|
||||
grpc.sendEvent(newUsedBytesChangedEvent(userID, usedBytes));
|
||||
}
|
||||
|
||||
this->updateGUIState();
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
//
|
||||
//****************************************************************************************************************************************************
|
||||
void UsersTab::onSendIMAPLoginFailedEvent() {
|
||||
GRPCService &grpc = app().grpc();
|
||||
if (grpc.isStreaming()) {
|
||||
grpc.sendEvent(newIMAPLoginFailedEvent(ui_.editIMAPLoginFailedUsername->text()));
|
||||
}
|
||||
|
||||
this->updateGUIState();
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
//
|
||||
//****************************************************************************************************************************************************
|
||||
void UsersTab::updateGUIState() {
|
||||
SPUser const user = selectedUser();
|
||||
bool const hasSelectedUser = user.get();
|
||||
UserState const state = user ? user->state() : UserState::SignedOut;
|
||||
|
||||
ui_.buttonEditUser->setEnabled(hasSelectedUser);
|
||||
ui_.buttonRemoveUser->setEnabled(hasSelectedUser);
|
||||
ui_.groupBoxBadEvent->setEnabled(hasSelectedUser && (UserState::SignedOut != user->state()));
|
||||
ui_.groupBoxBadEvent->setEnabled(hasSelectedUser && (UserState::SignedOut != state));
|
||||
ui_.groupBoxUsedSpace->setEnabled(hasSelectedUser && (UserState::Connected == state));
|
||||
ui_.editUsernamePasswordError->setEnabled(ui_.checkUsernamePasswordError->isChecked());
|
||||
ui_.spinUsedBytes->setValue(user ? user->usedBytes() : 0.0);
|
||||
ui_.groupboxSync->setEnabled(user.get());
|
||||
|
||||
if (user)
|
||||
ui_.editIMAPLoginFailedUsername->setText(user->primaryEmailOrUsername());
|
||||
|
||||
QSignalBlocker b(ui_.checkSync);
|
||||
bool const syncing = user && user->isSyncing();
|
||||
ui_.checkSync->setChecked(syncing);
|
||||
b = QSignalBlocker(ui_.sliderSync);
|
||||
ui_.sliderSync->setEnabled(syncing);
|
||||
qint32 const progressPercent = syncing ? qint32(user->syncProgress() * 100.0f) : 0;
|
||||
ui_.sliderSync->setValue(progressPercent);
|
||||
ui_.labelSync->setText(syncing ? QString("%1%").arg(progressPercent) : "" );
|
||||
}
|
||||
|
||||
|
||||
@ -344,5 +405,71 @@ void UsersTab::removeUser(QString const &userID) {
|
||||
//****************************************************************************************************************************************************
|
||||
void UsersTab::configureUserAppleMail(QString const &userID, QString const &address) {
|
||||
app().log().info(QString("Apple mail configuration was requested for user %1, address %2").arg(userID, address));
|
||||
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] userID The userID.
|
||||
/// \param[in] doResync Did the user request a resync?
|
||||
//****************************************************************************************************************************************************
|
||||
void UsersTab::processBadEventUserFeedback(QString const &userID, bool doResync) {
|
||||
app().log().info(QString("Feedback received for bad event: doResync = %1, userID = %2").arg(doResync ? "true" : "false", userID));
|
||||
if (doResync) {
|
||||
return; // we do not do any form of emulation for resync.
|
||||
}
|
||||
|
||||
SPUser user = users_.userWithID(userID);
|
||||
if (!user) {
|
||||
app().log().error(QString("%1(): could not find user with id %1.").arg(__func__, userID));
|
||||
}
|
||||
|
||||
user->setState(UserState::SignedOut);
|
||||
users_.touch(userID);
|
||||
GRPCService &grpc = app().grpc();
|
||||
if (grpc.isStreaming()) {
|
||||
grpc.sendEvent(newUserChangedEvent(userID));
|
||||
}
|
||||
|
||||
this->updateGUIState();
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] checked Is the sync checkbox checked?
|
||||
//****************************************************************************************************************************************************
|
||||
void UsersTab::onCheckSyncToggled(bool checked) {
|
||||
SPUser const user = this->selectedUser();
|
||||
if ((!user) || (user->isSyncing() == checked)) {
|
||||
return;
|
||||
}
|
||||
|
||||
user->setIsSyncing(checked);
|
||||
user->setSyncProgress(0.0);
|
||||
GRPCService &grpc = app().grpc();
|
||||
|
||||
// we do not apply delay for these event.
|
||||
if (checked) {
|
||||
grpc.sendEvent(newSyncStartedEvent(user->id()));
|
||||
grpc.sendEvent(newSyncProgressEvent(user->id(), 0.0, 1, 1));
|
||||
} else {
|
||||
grpc.sendEvent(newSyncFinishedEvent(user->id()));
|
||||
}
|
||||
|
||||
this->updateGUIState();
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] value The value for the slider.
|
||||
//****************************************************************************************************************************************************
|
||||
void UsersTab::onSliderSyncValueChanged(int value) {
|
||||
SPUser const user = this->selectedUser();
|
||||
if ((!user) || (!user->isSyncing()) || user->syncProgress() == value) {
|
||||
return;
|
||||
}
|
||||
|
||||
double const progress = value / 100.0;
|
||||
user->setSyncProgress(progress);
|
||||
app().grpc().sendEvent(newSyncProgressEvent(user->id(), progress, 1, 1)); // we do not simulate elapsed & remaining.
|
||||
this->updateGUIState();
|
||||
}
|
||||
|
||||
@ -54,6 +54,7 @@ public slots:
|
||||
void logoutUser(QString const &userID); ///< slot for the logging out of a user.
|
||||
void removeUser(QString const &userID); ///< Slot for the removal of a user.
|
||||
void configureUserAppleMail(QString const &userID, QString const &address); ///< Slot for the configuration of Apple mail.
|
||||
void processBadEventUserFeedback(QString const& userID, bool doResync); ///< Slot for the reception of a bad event user feedback.
|
||||
|
||||
private slots:
|
||||
void onAddUserButton(); ///< Add a user to the user list.
|
||||
@ -61,6 +62,10 @@ private slots:
|
||||
void onRemoveUserButton(); ///< Remove the currently selected user.
|
||||
void onSelectionChanged(QItemSelection, QItemSelection); ///< Slot for the change of the selection.
|
||||
void onSendUserBadEvent(); ///< Slot for the 'Send Bad Event Error' button.
|
||||
void onSendUsedBytesChangedEvent(); ///< Slot for the 'Send Used Bytes Changed Event' button.
|
||||
void onSendIMAPLoginFailedEvent(); ///< Slot for the 'Send IMAP Login failure Event' button.
|
||||
void onCheckSyncToggled(bool checked); ///< Slot for the 'Synchronizing' check box.
|
||||
void onSliderSyncValueChanged(int value); ///< Slot for the sync 'Progress' slider.
|
||||
void updateGUIState(); ///< Update the GUI state.
|
||||
|
||||
private: // member functions.
|
||||
|
||||
@ -6,8 +6,8 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1225</width>
|
||||
<height>717</height>
|
||||
<width>1221</width>
|
||||
<height>894</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
@ -66,6 +66,52 @@
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupboxSync">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Sync</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_5">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_4" stretch="1,0">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkSync">
|
||||
<property name="text">
|
||||
<string>Synchronizing</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="labelSync">
|
||||
<property name="text">
|
||||
<string>0%</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSlider" name="sliderSync">
|
||||
<property name="maximum">
|
||||
<number>100</number>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="tickInterval">
|
||||
<number>10</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBoxBadEvent">
|
||||
<property name="minimumSize">
|
||||
@ -80,13 +126,6 @@
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<item>
|
||||
<widget class="QLabel" name="labelUserBadEvent">
|
||||
<property name="text">
|
||||
<string>Message: </string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="editUserBadEvent">
|
||||
<property name="minimumSize">
|
||||
@ -96,18 +135,102 @@
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Bad event error.</string>
|
||||
<string/>
|
||||
</property>
|
||||
<property name="placeholderText">
|
||||
<string>error message</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonUserBadEvent">
|
||||
<property name="text">
|
||||
<string>Send</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBoxUsedSpace">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Used Bytes Changed</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonUserBadEvent">
|
||||
<property name="text">
|
||||
<string>Send Bad Event Error</string>
|
||||
</property>
|
||||
</widget>
|
||||
<layout class="QHBoxLayout" name="hBoxUsedBytes" stretch="1,0">
|
||||
<item>
|
||||
<widget class="QDoubleSpinBox" name="spinUsedBytes">
|
||||
<property name="buttonSymbols">
|
||||
<enum>QAbstractSpinBox::NoButtons</enum>
|
||||
</property>
|
||||
<property name="decimals">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>1000000000000000.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonUsedBytesChanged">
|
||||
<property name="text">
|
||||
<string>Send</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBoxIMAPLoginFailed">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>IMAP Login Failure</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_8">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_7">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="editIMAPLoginFailedUsername">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="placeholderText">
|
||||
<string>username or primary email</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonImapLoginFailed">
|
||||
<property name="text">
|
||||
<string>Send</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
|
||||
@ -186,6 +186,14 @@ void UserTable::touch(qint32 index) {
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] userID The userID.
|
||||
//****************************************************************************************************************************************************
|
||||
void UserTable::touch(QString const &userID) {
|
||||
this->touch(this->indexOfUser(userID));
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] index The index of the user in the list.
|
||||
//****************************************************************************************************************************************************
|
||||
|
||||
@ -43,6 +43,7 @@ public: // member functions.
|
||||
bridgepp::SPUser userWithUsername(QString const &username); ///< Return the user with a given username.
|
||||
qint32 indexOfUser(QString const &userID); ///< Return the index of a given User.
|
||||
void touch(qint32 index); ///< touch the user at a given index (indicates it has been modified).
|
||||
void touch(QString const& userID); ///< touch the user with the given userID (indicates it has been modified).
|
||||
void remove(qint32 index); ///< Remove the user at a given index.
|
||||
QList<bridgepp::SPUser> users() const; ///< Return a copy of the user list.
|
||||
|
||||
|
||||
@ -85,7 +85,10 @@ int main(int argc, char **argv) {
|
||||
return exitCode;
|
||||
}
|
||||
catch (Exception const &e) {
|
||||
QTextStream(stderr) << QString("A fatal error occurred: %1\n").arg(e.qwhat());
|
||||
QString message = e.qwhat();
|
||||
if (!e.details().isEmpty())
|
||||
message += "\n\nDetails:\n" + e.details();
|
||||
QTextStream(stderr) << QString("A fatal error occurred: %1\n").arg(message);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,6 +19,7 @@
|
||||
#include "AppController.h"
|
||||
#include "QMLBackend.h"
|
||||
#include "SentryUtils.h"
|
||||
#include "Settings.h"
|
||||
#include <bridgepp/GRPC/GRPCClient.h>
|
||||
#include <bridgepp/Exception/Exception.h>
|
||||
#include <bridgepp/ProcessMonitor.h>
|
||||
@ -32,6 +33,7 @@ namespace {
|
||||
QString const noWindowFlag = "--no-window"; ///< The no-window command-line flag.
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The AppController instance.
|
||||
//****************************************************************************************************************************************************
|
||||
@ -47,10 +49,19 @@ AppController &app() {
|
||||
AppController::AppController()
|
||||
: backend_(std::make_unique<QMLBackend>())
|
||||
, grpc_(std::make_unique<GRPCClient>())
|
||||
, log_(std::make_unique<Log>()) {
|
||||
, log_(std::make_unique<Log>())
|
||||
, settings_(new Settings) {
|
||||
}
|
||||
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
// The following is in the implementation file because of unique pointers with incomplete types in headers.
|
||||
// See https://stackoverflow.com/questions/6012157/is-stdunique-ptrt-required-to-know-the-full-definition-of-t
|
||||
//****************************************************************************************************************************************************
|
||||
AppController::~AppController() = default;
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The bridge worker, which can be null if the application was run in 'attach' mode (-a command-line switch).
|
||||
//****************************************************************************************************************************************************
|
||||
@ -71,32 +82,42 @@ ProcessMonitor *AppController::bridgeMonitor() const {
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] function The function that caught the exception.
|
||||
/// \param[in] message The error message.
|
||||
/// \return A reference to the application settings.
|
||||
//****************************************************************************************************************************************************
|
||||
void AppController::onFatalError(QString const &function, QString const &message) {
|
||||
QString const fullMessage = QString("%1(): %2").arg(function, message);
|
||||
auto uuid = reportSentryException(SENTRY_LEVEL_ERROR, "AppController got notified of a fatal error", "Exception", fullMessage.toLocal8Bit());
|
||||
QMessageBox::critical(nullptr, tr("Error"), message);
|
||||
Settings &AppController::settings() {
|
||||
return *settings_;
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] exception The exception that triggered the fatal error.
|
||||
//****************************************************************************************************************************************************
|
||||
void AppController::onFatalError(Exception const &exception) {
|
||||
sentry_uuid_t uuid = reportSentryException("AppController got notified of a fatal error", exception);
|
||||
|
||||
QMessageBox::critical(nullptr, tr("Error"), exception.what());
|
||||
restart(true);
|
||||
log().fatal(QString("reportID: %1 Captured exception: %2").arg(QByteArray(uuid.bytes, 16).toHex()).arg(fullMessage));
|
||||
log().fatal(QString("reportID: %1 Captured exception: %2").arg(QByteArray(uuid.bytes, 16).toHex(), exception.detailedWhat()));
|
||||
qApp->exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
|
||||
void AppController::restart(bool isCrashing) {
|
||||
if (!launcher_.isEmpty()) {
|
||||
QProcess p;
|
||||
log_->info(QString("Restarting - App : %1 - Args : %2").arg(launcher_,launcherArgs_.join(" ")));
|
||||
log_->info(QString("Restarting - App : %1 - Args : %2").arg(launcher_, launcherArgs_.join(" ")));
|
||||
QStringList args = launcherArgs_;
|
||||
if (isCrashing)
|
||||
if (isCrashing) {
|
||||
args.append(noWindowFlag);
|
||||
}
|
||||
|
||||
p.startDetached(launcher_, args);
|
||||
p.waitForStarted();
|
||||
}
|
||||
}
|
||||
|
||||
void AppController::setLauncherArgs(const QString& launcher, const QStringList& args){
|
||||
|
||||
void AppController::setLauncherArgs(const QString &launcher, const QStringList &args) {
|
||||
launcher_ = launcher;
|
||||
launcherArgs_ = args;
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,21 +20,17 @@
|
||||
#define BRIDGE_GUI_APP_CONTROLLER_H
|
||||
|
||||
|
||||
//@formatter:off
|
||||
class QMLBackend;
|
||||
|
||||
|
||||
class Settings;
|
||||
namespace bridgepp {
|
||||
class Log;
|
||||
|
||||
|
||||
class Overseer;
|
||||
|
||||
|
||||
class GRPCClient;
|
||||
|
||||
|
||||
class ProcessMonitor;
|
||||
class Exception;
|
||||
}
|
||||
//@formatter:on
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
@ -47,7 +43,7 @@ Q_OBJECT
|
||||
public: // member functions.
|
||||
AppController(AppController const &) = delete; ///< Disabled copy-constructor.
|
||||
AppController(AppController &&) = delete; ///< Disabled assignment copy-constructor.
|
||||
~AppController() override = default; ///< Destructor.
|
||||
~AppController() override; ///< Destructor.
|
||||
AppController &operator=(AppController const &) = delete; ///< Disabled assignment operator.
|
||||
AppController &operator=(AppController &&) = delete; ///< Disabled move assignment operator.
|
||||
QMLBackend &backend() { return *backend_; } ///< Return a reference to the backend.
|
||||
@ -55,10 +51,11 @@ public: // member functions.
|
||||
bridgepp::Log &log() { return *log_; } ///< Return a reference to the log.
|
||||
std::unique_ptr<bridgepp::Overseer> &bridgeOverseer() { return bridgeOverseer_; }; ///< Returns a reference the bridge overseer
|
||||
bridgepp::ProcessMonitor *bridgeMonitor() const; ///< Return the bridge worker.
|
||||
void setLauncherArgs(const QString& launcher, const QStringList& args);
|
||||
Settings &settings();; ///< Return the application settings.
|
||||
void setLauncherArgs(const QString &launcher, const QStringList &args);
|
||||
|
||||
public slots:
|
||||
void onFatalError(QString const &function, QString const &message); ///< Handle fatal errors.
|
||||
void onFatalError(bridgepp::Exception const& e); ///< Handle fatal errors.
|
||||
|
||||
private: // member functions
|
||||
AppController(); ///< Default constructor.
|
||||
@ -69,6 +66,7 @@ private: // data members
|
||||
std::unique_ptr<bridgepp::GRPCClient> grpc_; ///< The RPC client.
|
||||
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_;
|
||||
};
|
||||
|
||||
31
internal/frontend/bridge-gui/bridge-gui/BuildConfig.h.in
Normal file
31
internal/frontend/bridge-gui/bridge-gui/BuildConfig.h.in
Normal file
@ -0,0 +1,31 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
#ifndef BRIDGE_GUI_VERSION_H
|
||||
#define BRIDGE_GUI_VERSION_H
|
||||
|
||||
#define PROJECT_FULL_NAME "@BRIDGE_APP_FULL_NAME@"
|
||||
#define PROJECT_VENDOR "@BRIDGE_VENDOR@"
|
||||
#define PROJECT_VER "@BRIDGE_APP_VERSION@"
|
||||
#define PROJECT_REVISION "@BRIDGE_REVISION@"
|
||||
#define PROJECT_BUILD_TIME "@BRIDGE_BUILD_TIME@"
|
||||
#define PROJECT_DSN_SENTRY "@BRIDGE_DSN_SENTRY@"
|
||||
#define PROJECT_BUILD_ENV "@BRIDGE_BUILD_ENV@"
|
||||
#define PROJECT_CRASHPAD_HANDLER_PATH "@BRIDGE_CRASHPAD_HANDLER_PATH@"
|
||||
|
||||
#endif // BRIDGE_GUI_VERSION_H
|
||||
@ -85,20 +85,11 @@ message(STATUS "Using Qt ${Qt6_VERSION}")
|
||||
#*****************************************************************************************************************************************************
|
||||
find_package(sentry CONFIG REQUIRED)
|
||||
|
||||
set(DSN_SENTRY "https://ea31dfe8574849108fb8ba044fec3620@api.protonmail.ch/core/v4/reports/sentry/7")
|
||||
set(SENTRY_CONFIG_GENERATED_FILE_DIR ${CMAKE_CURRENT_BINARY_DIR}/sentry-generated)
|
||||
set(SENTRY_CONFIG_FILE ${SENTRY_CONFIG_GENERATED_FILE_DIR}/project_sentry_config.h)
|
||||
file(GENERATE OUTPUT ${SENTRY_CONFIG_FILE} CONTENT
|
||||
"// AUTO GENERATED FILE, DO NOT MODIFY\n#pragma once\nconst char* SentryDNS=\"${DSN_SENTRY}\";\nconst char* SentryProductID=\"bridge-mail@${BRIDGE_APP_VERSION}\";\n"
|
||||
)
|
||||
|
||||
|
||||
|
||||
#*****************************************************************************************************************************************************
|
||||
# Source files and output
|
||||
#*****************************************************************************************************************************************************
|
||||
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/Version.h.in ${CMAKE_CURRENT_SOURCE_DIR}/Version.h)
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/BuildConfig.h.in ${CMAKE_CURRENT_SOURCE_DIR}/BuildConfig.h)
|
||||
|
||||
if (NOT TARGET bridgepp)
|
||||
add_subdirectory(../bridgepp bridgepp)
|
||||
@ -118,23 +109,23 @@ add_executable(bridge-gui
|
||||
Resources.qrc
|
||||
AppController.cpp AppController.h
|
||||
BridgeApp.cpp BridgeApp.h
|
||||
BuildConfig.h
|
||||
CommandLine.cpp CommandLine.h
|
||||
EventStreamWorker.cpp EventStreamWorker.h
|
||||
LogUtils.cpp LogUtils.h
|
||||
main.cpp
|
||||
Pch.h
|
||||
Version.h
|
||||
QMLBackend.cpp QMLBackend.h
|
||||
UserList.cpp UserList.h
|
||||
SentryUtils.cpp SentryUtils.h
|
||||
Settings.cpp Settings.h
|
||||
${DOCK_ICON_SRC_FILE} MacOS/DockIcon.h
|
||||
)
|
||||
|
||||
|
||||
if (APPLE)
|
||||
target_sources(bridge-gui PRIVATE MacOS/SecondInstance.mm MacOS/SecondInstance.h)
|
||||
endif(APPLE)
|
||||
|
||||
|
||||
if (WIN32) # on Windows, we add a (non-Qt) resource file that contains the application icon and version information.
|
||||
string(TIMESTAMP BRIDGE_BUILD_YEAR "%Y")
|
||||
set(REGEX_NUMBER "[0123456789]") # CMake matches does not support \d.
|
||||
|
||||
@ -18,6 +18,7 @@
|
||||
|
||||
#include "Pch.h"
|
||||
#include "CommandLine.h"
|
||||
#include "Settings.h"
|
||||
|
||||
|
||||
using namespace bridgepp;
|
||||
@ -28,7 +29,10 @@ namespace {
|
||||
|
||||
QString const launcherFlag = "--launcher"; ///< launcher flag parameter used for bridge.
|
||||
QString const noWindowFlag = "--no-window"; ///< The no-window command-line flag.
|
||||
QString const softwareRendererFlag = "--software-renderer"; ///< The 'software-renderer' command-line flag.
|
||||
QString const softwareRendererFlag = "--software-renderer"; ///< The 'software-renderer' command-line flag. enable software rendering for a single execution
|
||||
QString const setSoftwareRendererFlag = "--set-software-renderer"; ///< The 'set-software-renderer' command-line flag. Software rendering will be used for all subsequent executions of the application.
|
||||
QString const setHardwareRendererFlag = "--set-hardware-renderer"; ///< The 'set-hardware-renderer' command-line flag. Hardware rendering will be used for all subsequent executions of the application.
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \brief parse a command-line string argument as expected by go's CLI package.
|
||||
@ -101,6 +105,14 @@ CommandLineOptions parseCommandLine(int argc, char *argv[]) {
|
||||
options.bridgeGuiArgs.append(arg);
|
||||
options.useSoftwareRenderer = true;
|
||||
}
|
||||
if (arg == setSoftwareRendererFlag) {
|
||||
app().settings().setUseSoftwareRenderer(true);
|
||||
continue; // setting is permanent. no need to keep/pass it to bridge for restart.
|
||||
}
|
||||
if (arg == setHardwareRendererFlag) {
|
||||
app().settings().setUseSoftwareRenderer(false);
|
||||
continue; // setting is permanent. no need to keep/pass it to bridge for restart.
|
||||
}
|
||||
if (arg == noWindowFlag) {
|
||||
options.noWindow = true;
|
||||
}
|
||||
|
||||
@ -52,7 +52,7 @@ void EventStreamReader::run() {
|
||||
emit finished();
|
||||
}
|
||||
catch (Exception const &e) {
|
||||
reportSentryException(SENTRY_LEVEL_ERROR, "Error during event stream read", "Exception", e.what());
|
||||
reportSentryException("Error during event stream read", e);
|
||||
emit error(e.qwhat());
|
||||
}
|
||||
}
|
||||
|
||||
58
internal/frontend/bridge-gui/bridge-gui/LogUtils.cpp
Normal file
58
internal/frontend/bridge-gui/bridge-gui/LogUtils.cpp
Normal file
@ -0,0 +1,58 @@
|
||||
// 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 "LogUtils.h"
|
||||
#include "BuildConfig.h"
|
||||
#include <bridgepp/Log/LogUtils.h>
|
||||
#include <bridgepp/BridgeUtils.h>
|
||||
|
||||
|
||||
using namespace bridgepp;
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return A reference to the log.
|
||||
//****************************************************************************************************************************************************
|
||||
Log &initLog() {
|
||||
Log &log = app().log();
|
||||
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)) {
|
||||
log.error(error);
|
||||
}
|
||||
|
||||
log.info("bridge-gui starting");
|
||||
QString const qtCompileTimeVersion = QT_VERSION_STR;
|
||||
QString const qtRuntimeVersion = qVersion();
|
||||
QString msg = QString("Using Qt %1").arg(qtRuntimeVersion);
|
||||
if (qtRuntimeVersion != qtCompileTimeVersion) {
|
||||
msg += QString(" (compiled against %1)").arg(qtCompileTimeVersion);
|
||||
}
|
||||
log.info(msg);
|
||||
|
||||
return log;
|
||||
}
|
||||
@ -16,13 +16,14 @@
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
#ifndef BRIDGE_GUI_VERSION_H
|
||||
#define BRIDGE_GUI_VERSION_H
|
||||
#ifndef BRIDGE_GUI_LOG_UTILS_H
|
||||
#define BRIDGE_GUI_LOG_UTILS_H
|
||||
|
||||
#define PROJECT_FULL_NAME "@BRIDGE_APP_FULL_NAME@"
|
||||
#define PROJECT_VENDOR "@BRIDGE_VENDOR@"
|
||||
#define PROJECT_VER "@BRIDGE_APP_VERSION@"
|
||||
#define PROJECT_REVISION "@BRIDGE_REVISION@"
|
||||
#define PROJECT_BUILD_TIME "@BRIDGE_BUILD_TIME@"
|
||||
|
||||
#endif // BRIDGE_GUI_VERSION_H
|
||||
#include <bridgepp/Log/Log.h>
|
||||
|
||||
|
||||
bridgepp::Log &initLog(); ///< Initialize the application log.
|
||||
|
||||
|
||||
#endif //BRIDGE_GUI_LOG_UTILS_H
|
||||
@ -17,16 +17,19 @@
|
||||
|
||||
|
||||
#include "QMLBackend.h"
|
||||
#include "BuildConfig.h"
|
||||
#include "EventStreamWorker.h"
|
||||
#include "Version.h"
|
||||
#include <bridgepp/GRPC/GRPCClient.h>
|
||||
#include <bridgepp/BridgeUtils.h>
|
||||
#include <bridgepp/Exception/Exception.h>
|
||||
#include <bridgepp/Log/LogUtils.h>
|
||||
#include <bridgepp/GRPC/GRPCClient.h>
|
||||
#include <bridgepp/Worker/Overseer.h>
|
||||
#include <bridgepp/BridgeUtils.h>
|
||||
|
||||
|
||||
#define HANDLE_EXCEPTION(x) try { x } \
|
||||
catch (Exception const &e) { emit fatalError(__func__, e.qwhat()); } \
|
||||
catch (...) { emit fatalError(__func__, QString("An unknown exception occurred")); }
|
||||
catch (Exception const &e) { emit fatalError(e); } \
|
||||
catch (...) { emit fatalError(Exception("An unknown exception occurred", QString(), __func__)); }
|
||||
#define HANDLE_EXCEPTION_RETURN_BOOL(x) HANDLE_EXCEPTION(x) return false;
|
||||
#define HANDLE_EXCEPTION_RETURN_QSTRING(x) HANDLE_EXCEPTION(x) return QString();
|
||||
#define HANDLE_EXCEPTION_RETURN_ZERO(x) HANDLE_EXCEPTION(x) return 0;
|
||||
@ -56,12 +59,8 @@ void QMLBackend::init(GRPCConfig const &serviceConfig) {
|
||||
app().grpc().setLog(&log);
|
||||
this->connectGrpcEvents();
|
||||
|
||||
QString error;
|
||||
if (app().grpc().connectToServer(serviceConfig, app().bridgeMonitor(), error)) {
|
||||
app().log().info("Connected to backend via gRPC service.");
|
||||
} else {
|
||||
throw Exception(QString("Cannot connectToServer to go backend via gRPC: %1").arg(error));
|
||||
}
|
||||
app().grpc().connectToServer(bridgepp::userConfigDir(), serviceConfig, app().bridgeMonitor());
|
||||
app().log().info("Connected to backend via gRPC service.");
|
||||
|
||||
QString bridgeVer;
|
||||
app().grpc().version(bridgeVer);
|
||||
@ -180,16 +179,6 @@ void QMLBackend::setShowSplashScreen(bool show) {
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The value for the 'showSplashScreen' property.
|
||||
//****************************************************************************************************************************************************
|
||||
bool QMLBackend::showSplashScreen() const {
|
||||
HANDLE_EXCEPTION_RETURN_BOOL(
|
||||
return showSplashScreen_;
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The value for the 'GOOS' property.
|
||||
//****************************************************************************************************************************************************
|
||||
@ -200,6 +189,16 @@ QString QMLBackend::goos() const {
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The value for the 'showSplashScreen' property.
|
||||
//****************************************************************************************************************************************************
|
||||
bool QMLBackend::showSplashScreen() const {
|
||||
HANDLE_EXCEPTION_RETURN_BOOL(
|
||||
return showSplashScreen_;
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The value for the 'logsPath' property.
|
||||
//****************************************************************************************************************************************************
|
||||
@ -467,6 +466,7 @@ bool QMLBackend::isDoHEnabled() const {
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The value for the 'isAutomaticUpdateOn' property.
|
||||
//****************************************************************************************************************************************************
|
||||
@ -597,7 +597,8 @@ void QMLBackend::setDiskCachePath(QUrl const &path) const {
|
||||
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");
|
||||
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());
|
||||
}
|
||||
app().grpc().login(username, password);
|
||||
)
|
||||
@ -817,6 +818,26 @@ void QMLBackend::setMailServerSettings(int imapPort, int smtpPort, bool useSSLFo
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] userID The userID.
|
||||
/// \param[in] doResync Did the user request a resync.
|
||||
//****************************************************************************************************************************************************
|
||||
void QMLBackend::sendBadEventUserFeedback(QString const &userID, bool doResync) {
|
||||
HANDLE_EXCEPTION(
|
||||
app().grpc().sendBadEventUserFeedback(userID, doResync);
|
||||
|
||||
// Notification dialog has just been dismissed, we remove the userID from the queue, and if there are other events in the queue, we show
|
||||
// the dialog again.
|
||||
badEventDisplayQueue_.removeOne(userID);
|
||||
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()); });
|
||||
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] imapPort The IMAP port.
|
||||
/// \param[in] smtpPort The SMTP port.
|
||||
@ -870,20 +891,41 @@ void QMLBackend::onLoginAlreadyLoggedIn(QString const &userID) {
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] userID The userID.
|
||||
/// \param[in] errorMessage. Unused
|
||||
//****************************************************************************************************************************************************
|
||||
void QMLBackend::onUserBadEvent(QString const &userID, QString const &errorMessage) {
|
||||
void QMLBackend::onUserBadEvent(QString const &userID, QString const& ) {
|
||||
HANDLE_EXCEPTION(
|
||||
if (badEventDisplayQueue_.contains(userID)) {
|
||||
app().log().error("Received 'bad event' for a user that is already in the queue.");
|
||||
return;
|
||||
}
|
||||
|
||||
SPUser const user = users_->getUserWithID(userID);
|
||||
if (!user)
|
||||
app().log().error(QString("Received bad event for unknown user %1").arg(user->id()));
|
||||
user->setState(UserState::SignedOut);
|
||||
emit userBadEvent(
|
||||
tr("Internal error: %1 was automatically logged out. Please log in again or report this problem if the issue persists.").arg(user->primaryEmailOrUsername()),
|
||||
errorMessage
|
||||
);
|
||||
emit selectUser(userID);
|
||||
emit showMainWindow();
|
||||
if (!user) {
|
||||
app().log().error(QString("Received bad event for unknown user %1."));
|
||||
}
|
||||
|
||||
badEventDisplayQueue_.append(userID);
|
||||
if (badEventDisplayQueue_.size() == 1) { // there was no other item is the queue, we can display the dialog immediately.
|
||||
this->displayBadEventDialog(userID);
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] username The username (or primary email address)
|
||||
//****************************************************************************************************************************************************
|
||||
void QMLBackend::onIMAPLoginFailed(QString const &username) {
|
||||
HANDLE_EXCEPTION(
|
||||
SPUser const user = users_->getUserWithUsernameOrEmail(username);
|
||||
if ((!user) || (user->state() != UserState::SignedOut)) { // We want to pop-up only if a signed-out user has been detected
|
||||
return;
|
||||
}
|
||||
if (user->isInIMAPLoginFailureCooldown())
|
||||
return;
|
||||
user->startImapLoginFailureCooldown(60 * 60 * 1000); // 1 hour cooldown during which we will not display this notification to this user again.
|
||||
emit selectUser(user->id());
|
||||
emit imapLoginWhileSignedOut(username);
|
||||
)
|
||||
}
|
||||
|
||||
@ -996,5 +1038,26 @@ void QMLBackend::connectGrpcEvents() {
|
||||
// user events
|
||||
connect(client, &GRPCClient::userDisconnected, this, &QMLBackend::userDisconnected);
|
||||
connect(client, &GRPCClient::userBadEvent, this, &QMLBackend::onUserBadEvent);
|
||||
connect(client, &GRPCClient::imapLoginFailed, this, &QMLBackend::onIMAPLoginFailed);
|
||||
|
||||
users_->connectGRPCEvents();
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] userID The userID.
|
||||
//****************************************************************************************************************************************************
|
||||
void QMLBackend::displayBadEventDialog(QString const &userID) {
|
||||
HANDLE_EXCEPTION(
|
||||
SPUser const user = users_->getUserWithID(userID);
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
|
||||
emit userBadEvent(userID,
|
||||
tr("Bridge ran into an internal error and it is not able to proceed with the account %1. Synchronize your local database now or logout"
|
||||
" to do it later. Synchronization time depends on the size of your mailbox.").arg(elideLongString(user->primaryEmailOrUsername(), 30)));
|
||||
emit selectUser(userID);
|
||||
emit showMainWindow();
|
||||
)
|
||||
}
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
|
||||
|
||||
#include "MacOS/DockIcon.h"
|
||||
#include "Version.h"
|
||||
#include "BuildConfig.h"
|
||||
#include "UserList.h"
|
||||
#include <bridgepp/GRPC/GRPCClient.h>
|
||||
#include <bridgepp/GRPC/GRPCUtils.h>
|
||||
@ -173,6 +173,7 @@ public slots: // slot for signals received from QML -> To be forwarded to Bridge
|
||||
void onResetFinished(); ///< Slot for the reset finish signal.
|
||||
void onVersionChanged(); ///< Slot for the version change signal.
|
||||
void setMailServerSettings(int imapPort, int smtpPort, bool useSSLForIMAP, bool useSSLForSMTP) const; ///< Forwards a connection mode change request from QML to gRPC
|
||||
void sendBadEventUserFeedback(QString const &userID, bool doResync); ///< Slot the providing user feedback for a bad event.
|
||||
|
||||
public slots: // slot for signals received from gRPC that need transformation instead of simple forwarding
|
||||
void onMailServerSettingsChanged(int imapPort, int smtpPort, bool useSSLForIMAP, bool useSSLForSMTP); ///< Slot for the ConnectionModeChanged gRPC event.
|
||||
@ -180,6 +181,7 @@ public slots: // slot for signals received from gRPC that need transformation in
|
||||
void onLoginFinished(QString const &userID, bool wasSignedOut); ///< Slot for LoginFinished gRPC event.
|
||||
void onLoginAlreadyLoggedIn(QString const &userID); ///< Slot for the LoginAlreadyLoggedIn gRPC event.
|
||||
void onUserBadEvent(QString const& userID, QString const& errorMessage); ///< Slot for the userBadEvent gRPC event.
|
||||
void onIMAPLoginFailed(QString const& username); ///< Slot the the imapLoginFailed event.
|
||||
|
||||
signals: // Signals received from the Go backend, to be forwarded to QML
|
||||
void toggleAutostartFinished(); ///< Signal for the 'toggleAutostartFinished' gRPC stream event.
|
||||
@ -222,7 +224,7 @@ signals: // Signals received from the Go backend, to be forwarded to QML
|
||||
void addressChangedLogout(QString const &address); ///< Signal for the 'addressChangedLogout' gRPC stream event.
|
||||
void apiCertIssue(); ///< Signal for the 'apiCertIssue' gRPC stream event.
|
||||
void userDisconnected(QString const &username); ///< Signal for the 'userDisconnected' gRPC stream event.
|
||||
void userBadEvent(QString const &description, QString const &errorMessage); ///< Signal for the 'userBadEvent' gRPC stream event.
|
||||
void userBadEvent(QString const &userID, QString const &description); ///< Signal for the 'userBadEvent' gRPC stream event.
|
||||
void internetOff(); ///< Signal for the 'internetOff' gRPC stream event.
|
||||
void internetOn(); ///< Signal for the 'internetOn' gRPC stream event.
|
||||
void resetFinished(); ///< Signal for the 'resetFinished' gRPC stream event.
|
||||
@ -233,13 +235,15 @@ signals: // Signals received from the Go backend, to be forwarded to QML
|
||||
void hideMainWindow(); ///< Signal for the 'hideMainWindow' gRPC stream event.
|
||||
void genericError(QString const &title, QString const &description); ///< Signal for the 'genericError' gRPC stream event.
|
||||
void selectUser(QString const); ///< Signal that request the given user account to be displayed.
|
||||
void imapLoginWhileSignedOut(QString const& username); ///< Signal for the notification of IMAP login attempt on a signed out account.
|
||||
|
||||
// This signal is emitted when an exception is intercepted is calls triggered by QML. QML engine would intercept the exception otherwise.
|
||||
void fatalError(QString const &function, QString const &message) const; ///< Signal emitted when an fatal error occurs.
|
||||
void fatalError(bridgepp::Exception const& e) const; ///< Signal emitted when an fatal error occurs.
|
||||
|
||||
private: // member functions
|
||||
void retrieveUserList(); ///< Retrieve the list of users via gRPC.
|
||||
void connectGrpcEvents(); ///< Connect gRPC that need to be forwarded to QML via backend signals
|
||||
void displayBadEventDialog(QString const& userID); ///< Displays the bad event dialog for a user.
|
||||
|
||||
private: // data members
|
||||
UserList *users_ { nullptr }; ///< The user list. Owned by backend.
|
||||
@ -252,6 +256,7 @@ 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.
|
||||
QList<QString> badEventDisplayQueue_; ///< THe queue for displaying 'bad event feedback request dialog'.
|
||||
|
||||
friend class AppController;
|
||||
};
|
||||
|
||||
@ -16,38 +16,166 @@
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "SentryUtils.h"
|
||||
#include "Version.h"
|
||||
#include "BuildConfig.h"
|
||||
#include <bridgepp/BridgeUtils.h>
|
||||
#include <bridgepp/Exception/Exception.h>
|
||||
|
||||
|
||||
using namespace bridgepp;
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QCryptographicHash>
|
||||
#include <QString>
|
||||
#include <QSysInfo>
|
||||
|
||||
static constexpr const char *LoggerName = "bridge-gui";
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The temporary file used for sentry attachment.
|
||||
//****************************************************************************************************************************************************
|
||||
QString sentryAttachmentFilePath() {
|
||||
static QString path;
|
||||
if (!path.isEmpty()) {
|
||||
return path;
|
||||
}
|
||||
while (true) {
|
||||
path = QDir::temp().absoluteFilePath(QUuid::createUuid().toString(QUuid::WithoutBraces) + ".txt"); // Sentry does not offer preview for .log files.
|
||||
if (!QFileInfo::exists(path)) {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \brief Get a hash of the computer's host name
|
||||
//****************************************************************************************************************************************************
|
||||
QByteArray getProtectedHostname() {
|
||||
QByteArray hostname = QCryptographicHash::hash(QSysInfo::machineHostName().toUtf8(), QCryptographicHash::Sha256);
|
||||
return hostname.toHex();
|
||||
}
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The OS String used by sentry
|
||||
//****************************************************************************************************************************************************
|
||||
QString getApiOS() {
|
||||
switch (os()) {
|
||||
case OS::MacOS:
|
||||
return "macos";
|
||||
case OS::Windows:
|
||||
return "windows";
|
||||
case OS::Linux:
|
||||
default:
|
||||
return "linux";
|
||||
}
|
||||
}
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The application version number.
|
||||
//****************************************************************************************************************************************************
|
||||
QString appVersion(const QString& version) {
|
||||
return QString("%1-bridge@%2").arg(getApiOS(), version);
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
//
|
||||
//****************************************************************************************************************************************************
|
||||
void initSentry() {
|
||||
sentry_options_t *sentryOptions = newSentryOptions(PROJECT_DSN_SENTRY, sentryCacheDir().toStdString().c_str());
|
||||
if (!QString(PROJECT_CRASHPAD_HANDLER_PATH).isEmpty())
|
||||
sentry_options_set_handler_path(sentryOptions, PROJECT_CRASHPAD_HANDLER_PATH);
|
||||
|
||||
if (sentry_init(sentryOptions) != 0) {
|
||||
QTextStream(stderr) << "Failed to initialize sentry\n";
|
||||
}
|
||||
setSentryReportScope();
|
||||
}
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
//
|
||||
//****************************************************************************************************************************************************
|
||||
void setSentryReportScope() {
|
||||
sentry_set_tag("OS", bridgepp::goos().toUtf8());
|
||||
sentry_set_tag("Client", PROJECT_FULL_NAME);
|
||||
sentry_set_tag("Version", PROJECT_VER);
|
||||
sentry_set_tag("UserAgent", QString("/ (%1)").arg(bridgepp::goos()).toUtf8());
|
||||
sentry_set_tag("HostArch", QSysInfo::currentCpuArchitecture().toUtf8());
|
||||
sentry_set_tag("server_name", getProtectedHostname());
|
||||
sentry_set_tag("Version", PROJECT_REVISION);
|
||||
sentry_set_tag("HostArch", QSysInfo::currentCpuArchitecture().toUtf8());
|
||||
sentry_set_tag("server_name", getProtectedHostname());
|
||||
sentry_value_t user = sentry_value_new_object();
|
||||
sentry_value_set_by_key(user, "id", sentry_value_new_string(getProtectedHostname()));
|
||||
sentry_set_user(user);
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
//
|
||||
//****************************************************************************************************************************************************
|
||||
sentry_options_t* newSentryOptions(const char *sentryDNS, const char *cacheDir) {
|
||||
sentry_options_t *sentryOptions = sentry_options_new();
|
||||
sentry_options_set_dsn(sentryOptions, sentryDNS);
|
||||
sentry_options_set_database_path(sentryOptions, cacheDir);
|
||||
sentry_options_set_release(sentryOptions, appVersion(PROJECT_VER).toUtf8());
|
||||
sentry_options_set_max_breadcrumbs(sentryOptions, 50);
|
||||
sentry_options_set_environment(sentryOptions, PROJECT_BUILD_ENV);
|
||||
QByteArray const array = sentryAttachmentFilePath().toLocal8Bit();
|
||||
sentry_options_add_attachment(sentryOptions, array.constData());
|
||||
// Enable this for debugging sentry.
|
||||
// sentry_options_set_debug(sentryOptions, 1);
|
||||
|
||||
return sentryOptions;
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
//
|
||||
//****************************************************************************************************************************************************
|
||||
sentry_uuid_t reportSentryEvent(sentry_level_t level, const char *message) {
|
||||
auto event = sentry_value_new_message_event(level, LoggerName, message);
|
||||
return sentry_capture_event(event);
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
//
|
||||
//****************************************************************************************************************************************************
|
||||
sentry_uuid_t reportSentryException(sentry_level_t level, const char *message, const char *exceptionType, const char *exception) {
|
||||
auto event = sentry_value_new_message_event(level, LoggerName, message);
|
||||
sentry_event_add_exception(event, sentry_value_new_exception(exceptionType, exception));
|
||||
|
||||
// reject exception content from the fingerprint if there is not enough content
|
||||
if ( strlen(exception) < 5) {
|
||||
sentry_value_t fingerprint = sentry_value_new_list();
|
||||
sentry_value_append(fingerprint, sentry_value_new_string("level"));
|
||||
sentry_value_append(fingerprint, sentry_value_new_string(message));
|
||||
sentry_value_append(fingerprint, sentry_value_new_string(LoggerName));
|
||||
sentry_value_set_by_key(event, "fingerprint", fingerprint);
|
||||
}
|
||||
|
||||
return sentry_capture_event(event);
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] message The message for the exception.
|
||||
/// \param[in] function The name of the function that triggered the exception.
|
||||
/// \param[in] exception The exception.
|
||||
/// \return The Sentry exception UUID.
|
||||
//****************************************************************************************************************************************************
|
||||
sentry_uuid_t reportSentryException(QString const &message, bridgepp::Exception const exception) {
|
||||
QByteArray const attachment = exception.attachment();
|
||||
QFile file(sentryAttachmentFilePath());
|
||||
bool const hasAttachment = !attachment.isEmpty();
|
||||
if (hasAttachment) {
|
||||
if (file.open(QIODevice::Text | QIODevice::WriteOnly)) {
|
||||
file.write(attachment);
|
||||
file.close();
|
||||
}
|
||||
}
|
||||
|
||||
sentry_uuid_t const uuid = reportSentryException(SENTRY_LEVEL_ERROR, message.toLocal8Bit(), "Exception",
|
||||
exception.detailedWhat().toLocal8Bit());
|
||||
|
||||
if (hasAttachment) {
|
||||
file.remove();
|
||||
}
|
||||
|
||||
return uuid;
|
||||
}
|
||||
|
||||
|
||||
@ -21,8 +21,11 @@
|
||||
|
||||
#include <sentry.h>
|
||||
|
||||
void initSentry();
|
||||
void setSentryReportScope();
|
||||
sentry_options_t* newSentryOptions(const char * sentryDNS, const char * cacheDir);
|
||||
sentry_uuid_t reportSentryEvent(sentry_level_t level, const char *message);
|
||||
sentry_uuid_t reportSentryException(sentry_level_t level, const char *message, const char *exceptionType, const char *exception);
|
||||
sentry_uuid_t reportSentryException(QString const& message, bridgepp::Exception const exception);
|
||||
|
||||
|
||||
#endif //BRIDGE_GUI_SENTRYUTILS_H
|
||||
|
||||
56
internal/frontend/bridge-gui/bridge-gui/Settings.cpp
Normal file
56
internal/frontend/bridge-gui/bridge-gui/Settings.cpp
Normal 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/>.
|
||||
|
||||
|
||||
#include "Settings.h"
|
||||
#include <bridgepp/BridgeUtils.h>
|
||||
|
||||
|
||||
using namespace bridgepp;
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
|
||||
QString const settingsFileName = "bridge-gui.ini"; ///< The name of the settings file.
|
||||
QString const keyUseSoftwareRenderer = "UseSoftwareRenderer"; ///< The key for storing the 'Use software rendering' setting.
|
||||
|
||||
|
||||
}
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
//
|
||||
//****************************************************************************************************************************************************
|
||||
Settings::Settings()
|
||||
: settings_(QDir(userConfigDir()).absoluteFilePath("bridge-gui.ini"), QSettings::Format::IniFormat) {
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The value for the 'Use software renderer' setting.
|
||||
//****************************************************************************************************************************************************
|
||||
bool Settings::useSoftwareRenderer() const {
|
||||
return settings_.value(keyUseSoftwareRenderer, onWindows()).toBool();
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] value The value for the 'Use software renderer' setting.
|
||||
//****************************************************************************************************************************************************
|
||||
void Settings::setUseSoftwareRenderer(bool value) {
|
||||
settings_.setValue(keyUseSoftwareRenderer, value);
|
||||
}
|
||||
47
internal/frontend/bridge-gui/bridge-gui/Settings.h
Normal file
47
internal/frontend/bridge-gui/bridge-gui/Settings.h
Normal file
@ -0,0 +1,47 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
#ifndef BRIDGE_GUI_SETTINGS_H
|
||||
#define BRIDGE_GUI_SETTINGS_H
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \brief Application settings class
|
||||
//****************************************************************************************************************************************************
|
||||
class Settings {
|
||||
public: // member functions.
|
||||
Settings(Settings const&) = delete; ///< Disabled copy-constructor.
|
||||
Settings(Settings&&) = delete; ///< Disabled assignment copy-constructor.
|
||||
~Settings() = default; ///< Destructor.
|
||||
Settings& operator=(Settings const&) = delete; ///< Disabled assignment operator.
|
||||
Settings& operator=(Settings&&) = delete; ///< Disabled move assignment operator.
|
||||
|
||||
bool useSoftwareRenderer() const; ///< Get the 'Use software renderer' settings value.
|
||||
void setUseSoftwareRenderer(bool value); ///< Set the 'Use software renderer' settings value.
|
||||
|
||||
private: // member functions.
|
||||
Settings(); ///< Default constructor.
|
||||
|
||||
private: // data members.
|
||||
QSettings settings_; ///< The settings.
|
||||
|
||||
friend class AppController;
|
||||
};
|
||||
|
||||
|
||||
#endif //BRIDGE_GUI_SETTINGS_H
|
||||
@ -38,6 +38,10 @@ void UserList::connectGRPCEvents() const {
|
||||
GRPCClient &client = app().grpc();
|
||||
connect(&client, &GRPCClient::userChanged, this, &UserList::onUserChanged);
|
||||
connect(&client, &GRPCClient::toggleSplitModeFinished, this, &UserList::onToggleSplitModeFinished);
|
||||
connect(&client, &GRPCClient::usedBytesChanged, this, &UserList::onUsedBytesChanged);
|
||||
connect(&client, &GRPCClient::syncStarted, this, &UserList::onSyncStarted);
|
||||
connect(&client, &GRPCClient::syncFinished, this, &UserList::onSyncFinished);
|
||||
connect(&client, &GRPCClient::syncProgress, this, &UserList::onSyncProgress);
|
||||
}
|
||||
|
||||
|
||||
@ -148,6 +152,19 @@ bridgepp::SPUser UserList::getUserWithID(QString const &userID) const {
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] username The username or email.
|
||||
/// \return The user with the given ID.
|
||||
/// \return A null pointer if the user could not be found.
|
||||
//****************************************************************************************************************************************************
|
||||
bridgepp::SPUser UserList::getUserWithUsernameOrEmail(QString const &username) const {
|
||||
QList<SPUser>::const_iterator it = std::find_if(users_.begin(), users_.end(), [username](SPUser const &user) -> bool {
|
||||
return user && ((username.compare(user->username(), Qt::CaseInsensitive) == 0) || user->addresses().contains(username, Qt::CaseInsensitive));
|
||||
});
|
||||
return (it == users_.end()) ? nullptr : *it;
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] row The row.
|
||||
//****************************************************************************************************************************************************
|
||||
@ -223,3 +240,61 @@ void UserList::onToggleSplitModeFinished(QString const &userID) {
|
||||
int UserList::count() const {
|
||||
return users_.size();
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] userID The userID.
|
||||
/// \param[in] usedBytes The used space, in bytes.
|
||||
//****************************************************************************************************************************************************
|
||||
void UserList::onUsedBytesChanged(QString const &userID, qint64 usedBytes) {
|
||||
int const index = this->rowOfUserID(userID);
|
||||
if (index < 0) {
|
||||
app().log().error(QString("Received usedBytesChanged event for unknown userID %1").arg(userID));
|
||||
return;
|
||||
}
|
||||
users_[index]->setUsedBytes(usedBytes);
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] userID The userID.
|
||||
//****************************************************************************************************************************************************
|
||||
void UserList::onSyncStarted(QString const &userID) {
|
||||
int const index = this->rowOfUserID(userID);
|
||||
if (index < 0) {
|
||||
app().log().error(QString("Received onSyncStarted event for unknown userID %1").arg(userID));
|
||||
return;
|
||||
}
|
||||
users_[index]->setIsSyncing(true);
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] userID The userID.
|
||||
//****************************************************************************************************************************************************
|
||||
void UserList::onSyncFinished(QString const &userID) {
|
||||
int const index = this->rowOfUserID(userID);
|
||||
if (index < 0) {
|
||||
app().log().error(QString("Received onSyncFinished event for unknown userID %1").arg(userID));
|
||||
return;
|
||||
}
|
||||
users_[index]->setIsSyncing(false);
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] userID The userID.
|
||||
/// \param[in] progress The sync progress ratio.
|
||||
/// \param[in] elapsedMs The elapsed sync time in milliseconds.
|
||||
/// \param[in] remainingMs The remaining sync time in milliseconds.
|
||||
//****************************************************************************************************************************************************
|
||||
void UserList::onSyncProgress(QString const &userID, double progress, float elapsedMs, float remainingMs) {
|
||||
Q_UNUSED(elapsedMs)
|
||||
Q_UNUSED(remainingMs)
|
||||
int const index = this->rowOfUserID(userID);
|
||||
if (index < 0) {
|
||||
app().log().error(QString("Received onSyncFinished event for unknown userID %1").arg(userID));
|
||||
return;
|
||||
}
|
||||
users_[index]->setSyncProgress(progress);
|
||||
}
|
||||
|
||||
@ -44,6 +44,7 @@ public: // member functions.
|
||||
void appendUser(bridgepp::SPUser const &user); ///< Add a new user.
|
||||
void updateUserAtRow(int row, bridgepp::User const &user); ///< Update the user at given row.
|
||||
bridgepp::SPUser getUserWithID(QString const &userID) const; ///< Retrieve the user with the given ID.
|
||||
bridgepp::SPUser getUserWithUsernameOrEmail(QString const& username) const; ///< Retrieve the user with the given primary email address or username
|
||||
|
||||
// the userCount property.
|
||||
Q_PROPERTY(int count READ count NOTIFY countChanged)
|
||||
@ -59,7 +60,11 @@ public:
|
||||
public slots: ///< handler for signals coming from the gRPC service
|
||||
void onUserChanged(QString const &userID);
|
||||
void onToggleSplitModeFinished(QString const &userID);
|
||||
|
||||
void onUsedBytesChanged(QString const &userID, qint64 usedBytes); ///< Slot for usedBytesChanged events.
|
||||
void onSyncStarted(QString const &userID); ///< Slot for syncStarted events.
|
||||
void onSyncFinished(QString const &userID); ///< Slot for syncFinished events.
|
||||
void onSyncProgress(QString const &userID, double progress, float elapsedMs, float remainingMs); ///< Slot for syncFinished events.
|
||||
|
||||
private: // data members
|
||||
QList<bridgepp::SPUser> users_; ///< The user list.
|
||||
};
|
||||
|
||||
@ -76,6 +76,15 @@ function check_exit() {
|
||||
Write-host "Running build for version $bridgeVersion - $buildConfig in $buildDir"
|
||||
|
||||
$REVISION_HASH = git rev-parse --short=10 HEAD
|
||||
$bridgeDsnSentry = ($env:BRIDGE_DSN_SENTRY)
|
||||
$bridgeBuidTime = ($env:BRIDGE_BUILD_TIME)
|
||||
|
||||
$bridgeBuildEnv = ($env:BRIDGE_BUILD_ENV)
|
||||
if ($null -eq $bridgeBuildEnv)
|
||||
{
|
||||
$bridgeBuildEnv = "dev"
|
||||
}
|
||||
|
||||
git submodule update --init --recursive $vcpkgRoot
|
||||
. $vcpkgBootstrap -disableMetrics
|
||||
. $vcpkgExe install sentry-native:x64-windows grpc:x64-windows --clean-after-build
|
||||
@ -83,8 +92,11 @@ git submodule update --init --recursive $vcpkgRoot
|
||||
. $cmakeExe -G "Visual Studio 17 2022" -DCMAKE_BUILD_TYPE="$buildConfig" `
|
||||
-DBRIDGE_APP_FULL_NAME="$bridgeFullName" `
|
||||
-DBRIDGE_VENDOR="$bridgeVendor" `
|
||||
-DBRIDGE_REVISION=$REVISION_HASH `
|
||||
-DBRIDGE_REVISION="$REVISION_HASH" `
|
||||
-DBRIDGE_APP_VERSION="$bridgeVersion" `
|
||||
-DBRIDGE_BUILD_TIME="$bridgeBuidTime" `
|
||||
-DBRIDGE_DSN_SENTRY="$bridgeDsnSentry" `
|
||||
-DBRIDGE_BUILD_ENV="$bridgeBuildEnv" `
|
||||
-S . -B $buildDir
|
||||
|
||||
check_exit "CMake failed"
|
||||
|
||||
@ -56,6 +56,9 @@ BUILD_CONFIG=${BRIDGE_GUI_BUILD_CONFIG:-Debug}
|
||||
BUILD_DIR=$(echo "./cmake-build-${BUILD_CONFIG}" | tr '[:upper:]' '[:lower:]')
|
||||
VCPKG_ROOT="${BRIDGE_REPO_ROOT}/extern/vcpkg"
|
||||
BRIDGE_REVISION=$(git rev-parse --short=10 HEAD)
|
||||
BRIDGE_DSN_SENTRY=${BRIDGE_DSN_SENTRY}
|
||||
BRIDGE_BUILD_TIME=${BRIDGE_BUILD_TIME}
|
||||
BRIDGE_BUILD_ENV= ${BRIDGE_BUILD_ENV:-"dev"}
|
||||
git submodule update --init --recursive ${VCPKG_ROOT}
|
||||
check_exit "Failed to initialize vcpkg as a submodule."
|
||||
|
||||
@ -94,6 +97,9 @@ cmake \
|
||||
-DBRIDGE_APP_FULL_NAME="${BRIDGE_APP_FULL_NAME}" \
|
||||
-DBRIDGE_VENDOR="${BRIDGE_VENDOR}" \
|
||||
-DBRIDGE_REVISION="${BRIDGE_REVISION}" \
|
||||
-DBRIDGE_DSN_SENTRY="${BRIDGE_DSN_SENTRY}" \
|
||||
-DBRIDGE_BRIDGE_TIME="${BRIDGE_BRIDGE_TIME}" \
|
||||
-DBRIDGE_BUILD_ENV="${BRIDGE_BUILD_ENV}" \
|
||||
-DBRIDGE_APP_VERSION="${BRIDGE_APP_VERSION}" "${BRIDGE_CMAKE_MACOS_OPTS}" \
|
||||
-G Ninja \
|
||||
-S . \
|
||||
|
||||
@ -16,20 +16,19 @@
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
#include "Pch.h"
|
||||
#include "BridgeApp.h"
|
||||
#include "BuildConfig.h"
|
||||
#include "CommandLine.h"
|
||||
#include "LogUtils.h"
|
||||
#include "QMLBackend.h"
|
||||
#include "SentryUtils.h"
|
||||
#include "Version.h"
|
||||
#include "Settings.h"
|
||||
#include <bridgepp/BridgeUtils.h>
|
||||
#include <bridgepp/Exception/Exception.h>
|
||||
#include <bridgepp/FocusGRPC/FocusGRPCClient.h>
|
||||
#include <bridgepp/Log/Log.h>
|
||||
#include <bridgepp/Log/LogUtils.h>
|
||||
#include <bridgepp/ProcessMonitor.h>
|
||||
#include <sentry.h>
|
||||
#include <SentryUtils.h>
|
||||
#include <project_sentry_config.h>
|
||||
|
||||
|
||||
#ifdef Q_OS_MACOS
|
||||
@ -99,38 +98,6 @@ void initQtApplication() {
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return A reference to the log.
|
||||
//****************************************************************************************************************************************************
|
||||
Log &initLog() {
|
||||
Log &log = app().log();
|
||||
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)) {
|
||||
log.error(error);
|
||||
}
|
||||
|
||||
log.info("bridge-gui starting");
|
||||
QString const qtCompileTimeVersion = QT_VERSION_STR;
|
||||
QString const qtRuntimeVersion = qVersion();
|
||||
QString msg = QString("Using Qt %1").arg(qtRuntimeVersion);
|
||||
if (qtRuntimeVersion != qtCompileTimeVersion) {
|
||||
msg += QString(" (compiled against %1)").arg(qtCompileTimeVersion);
|
||||
}
|
||||
log.info(msg);
|
||||
|
||||
return log;
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] engine The QML component.
|
||||
@ -150,8 +117,9 @@ QQmlComponent *createRootQmlComponent(QQmlApplicationEngine &engine) {
|
||||
|
||||
rootComponent->loadUrl(QUrl(qrcQmlDir + "/Bridge.qml"));
|
||||
if (rootComponent->status() != QQmlComponent::Status::Ready) {
|
||||
app().log().error(rootComponent->errorString());
|
||||
throw Exception("Could not load QML component");
|
||||
QString const &err = rootComponent->errorString();
|
||||
app().log().error(err);
|
||||
throw Exception("Could not load QML component", err);
|
||||
}
|
||||
return rootComponent;
|
||||
}
|
||||
@ -218,7 +186,7 @@ QUrl getApiUrl() {
|
||||
/// \return true if an instance of bridge is already running.
|
||||
//****************************************************************************************************************************************************
|
||||
bool isBridgeRunning() {
|
||||
QLockFile lockFile(QDir(userCacheDir()).absoluteFilePath(bridgeLock));
|
||||
QLockFile lockFile(QDir(bridgepp::userCacheDir()).absoluteFilePath(bridgeLock));
|
||||
return (!lockFile.tryLock()) && (lockFile.error() == QLockFile::LockFailedError);
|
||||
}
|
||||
|
||||
@ -229,9 +197,22 @@ bool isBridgeRunning() {
|
||||
void focusOtherInstance() {
|
||||
try {
|
||||
FocusGRPCClient client;
|
||||
GRPCConfig sc;
|
||||
QString const path = FocusGRPCClient::grpcFocusServerConfigPath(bridgepp::userConfigDir());
|
||||
QFile file(path);
|
||||
if (file.exists()) {
|
||||
if (!sc.load(path)) {
|
||||
throw Exception("The gRPC focus service configuration file is invalid.");
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw Exception("Server did not provide gRPC Focus service configuration.");
|
||||
}
|
||||
|
||||
|
||||
QString error;
|
||||
if (!client.connectToServer(5000, &error)) {
|
||||
throw Exception(QString("Could not connect to bridge focus service for a raise call: %1").arg(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()) {
|
||||
throw Exception(QString("The raise call to the bridge focus service failed."));
|
||||
@ -239,16 +220,17 @@ void focusOtherInstance() {
|
||||
}
|
||||
catch (Exception const &e) {
|
||||
app().log().error(e.qwhat());
|
||||
auto uuid = reportSentryException(SENTRY_LEVEL_ERROR, "Exception occurred during focusOtherInstance()", "Exception", e.what());
|
||||
app().log().fatal(QString("reportID: %1 Captured exception: %2").arg(QByteArray(uuid.bytes, 16).toHex()).arg(e.qwhat()));
|
||||
auto uuid = reportSentryException("Exception occurred during focusOtherInstance()", e);
|
||||
app().log().fatal(QString("reportID: %1 Captured exception: %2").arg(QByteArray(uuid.bytes, 16).toHex(), e.qwhat()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param [in] args list of arguments to pass to bridge.
|
||||
/// \return bridge executable path
|
||||
//****************************************************************************************************************************************************
|
||||
void launchBridge(QStringList const &args) {
|
||||
const QString launchBridge(QStringList const &args) {
|
||||
UPOverseer &overseer = app().bridgeOverseer();
|
||||
overseer.reset();
|
||||
|
||||
@ -265,6 +247,7 @@ void launchBridge(QStringList const &args) {
|
||||
app().log().info(QString("Launching bridge process with command \"%1\" %2").arg(bridgeExePath, params.join(" ")));
|
||||
overseer = std::make_unique<Overseer>(new ProcessMonitor(bridgeExePath, params, nullptr), nullptr);
|
||||
overseer->startWorker(true);
|
||||
return bridgeExePath;
|
||||
}
|
||||
|
||||
|
||||
@ -275,12 +258,8 @@ void closeBridgeApp() {
|
||||
app().grpc().quit(); // this will cause the grpc service and the bridge app to close.
|
||||
|
||||
UPOverseer &overseer = app().bridgeOverseer();
|
||||
if (!overseer) { // The app was run in 'attach' mode and attached to an existing instance of Bridge. We're not monitoring it.
|
||||
return;
|
||||
}
|
||||
|
||||
while (!overseer->isFinished()) {
|
||||
QThread::msleep(20);
|
||||
if (overseer) { // A null overseer means the app was run in 'attach' mode. We're not monitoring it.
|
||||
overseer->wait(Overseer::maxTerminationWaitTimeMs);
|
||||
}
|
||||
}
|
||||
|
||||
@ -291,31 +270,20 @@ void closeBridgeApp() {
|
||||
/// \return The exit code for the application.
|
||||
//****************************************************************************************************************************************************
|
||||
int main(int argc, char *argv[]) {
|
||||
// Init sentry.
|
||||
sentry_options_t *sentryOptions = sentry_options_new();
|
||||
sentry_options_set_dsn(sentryOptions, SentryDNS);
|
||||
{
|
||||
const QString sentryCachePath = sentryCacheDir();
|
||||
sentry_options_set_database_path(sentryOptions, sentryCachePath.toStdString().c_str());
|
||||
}
|
||||
sentry_options_set_release(sentryOptions, QByteArray(PROJECT_REVISION).toHex());
|
||||
// Enable this for debugging sentry.
|
||||
// sentry_options_set_debug(sentryOptions, 1);
|
||||
if (sentry_init(sentryOptions) != 0) {
|
||||
std::cerr << "Failed to initialize sentry" << std::endl;
|
||||
}
|
||||
setSentryReportScope();
|
||||
auto sentryClose = qScopeGuard([] { sentry_close(); });
|
||||
|
||||
// The application instance is needed to display system message boxes. As we may have to do it in the exception handler,
|
||||
// application instance is create outside the try/catch clause.
|
||||
if (QSysInfo::productType() != "windows") {
|
||||
QCoreApplication::setAttribute(Qt::AA_UseSoftwareOpenGL);
|
||||
QCoreApplication::setAttribute(Qt::AA_UseSoftwareOpenGL); // must be called before instantiating the BridgeApp
|
||||
}
|
||||
|
||||
BridgeApp guiApp(argc, argv);
|
||||
initSentry();
|
||||
auto sentryCloser = qScopeGuard([] { sentry_close(); });
|
||||
|
||||
try {
|
||||
QString const& configDir = bridgepp::userConfigDir();
|
||||
|
||||
|
||||
initQtApplication();
|
||||
|
||||
Log &log = initLog();
|
||||
@ -337,21 +305,24 @@ int main(int argc, char *argv[]) {
|
||||
// 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);
|
||||
|
||||
QString bridgeexec;
|
||||
if (!cliOptions.attach) {
|
||||
if (isBridgeRunning()) {
|
||||
throw Exception("An orphan instance of bridge is already running. Please terminate it and relaunch the application.");
|
||||
throw Exception("An orphan instance of bridge is already running. Please terminate it and relaunch the application.",
|
||||
QString(), __FUNCTION__, tailOfLatestBridgeLog());
|
||||
}
|
||||
|
||||
// before launching bridge, we remove any trailing service config file, because we need to make sure we get a newly generated one.
|
||||
GRPCClient::removeServiceConfigFile();
|
||||
launchBridge(cliOptions.bridgeArgs);
|
||||
FocusGRPCClient::removeServiceConfigFile(configDir);
|
||||
GRPCClient::removeServiceConfigFile(configDir);
|
||||
bridgeexec = launchBridge(cliOptions.bridgeArgs);
|
||||
}
|
||||
|
||||
log.info(QString("Retrieving gRPC service configuration from '%1'").arg(QDir::toNativeSeparators(grpcServerConfigPath())));
|
||||
app().backend().init(GRPCClient::waitAndRetrieveServiceConfig(cliOptions.attach ? 0 : grpcServiceConfigWaitDelayMs, app().bridgeMonitor()));
|
||||
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()));
|
||||
if (!cliOptions.attach) {
|
||||
GRPCClient::removeServiceConfigFile();
|
||||
GRPCClient::removeServiceConfigFile(configDir);
|
||||
}
|
||||
|
||||
// gRPC communication is established. From now on, log events will be sent to bridge via gRPC. bridge will write these to file,
|
||||
@ -364,14 +335,15 @@ int main(int argc, char *argv[]) {
|
||||
// The following allows to render QML content in software with a 'Rendering Hardware Interface' (OpenGL, Vulkan, Metal, Direct3D...)
|
||||
// Note that it is different from the Qt::AA_UseSoftwareOpenGL attribute we use on some platforms that instruct Qt that we would like
|
||||
// to use a software-only implementation of OpenGL.
|
||||
QQuickWindow::setSceneGraphBackend(cliOptions.useSoftwareRenderer ? "software" : "rhi");
|
||||
QQuickWindow::setSceneGraphBackend((app().settings().useSoftwareRenderer() || cliOptions.useSoftwareRenderer) ? "software" : "rhi");
|
||||
log.info(QString("Qt Quick renderer: %1").arg(QQuickWindow::sceneGraphBackend()));
|
||||
|
||||
|
||||
QQmlApplicationEngine engine;
|
||||
std::unique_ptr<QQmlComponent> rootComponent(createRootQmlComponent(engine));
|
||||
std::unique_ptr<QObject> rootObject(rootComponent->create(engine.rootContext()));
|
||||
if (!rootObject) {
|
||||
throw Exception("Could not create root object.");
|
||||
throw Exception("Could not create QML root object.");
|
||||
}
|
||||
|
||||
ProcessMonitor *bridgeMonitor = app().bridgeMonitor();
|
||||
@ -406,13 +378,17 @@ int main(int argc, char *argv[]) {
|
||||
QStringList args = cliOptions.bridgeGuiArgs;
|
||||
args.append(waitFlag);
|
||||
args.append(mainexec);
|
||||
if (!bridgeexec.isEmpty()) {
|
||||
args.append(waitFlag);
|
||||
args.append(bridgeexec);
|
||||
}
|
||||
app().setLauncherArgs(cliOptions.launcher, args);
|
||||
result = QGuiApplication::exec();
|
||||
}
|
||||
|
||||
QObject::disconnect(connection);
|
||||
app().grpc().stopEventStreamReader();
|
||||
if (!app().backend().waitForEventStreamReaderToFinish(5000)) {
|
||||
if (!app().backend().waitForEventStreamReaderToFinish(Overseer::maxTerminationWaitTimeMs)) {
|
||||
log.warn("Event stream reader took too long to finish.");
|
||||
}
|
||||
|
||||
@ -428,9 +404,9 @@ int main(int argc, char *argv[]) {
|
||||
return result;
|
||||
}
|
||||
catch (Exception const &e) {
|
||||
auto uuid = reportSentryException(SENTRY_LEVEL_ERROR, "Exception occurred during main", "Exception", e.what());
|
||||
sentry_uuid_s const uuid = reportSentryException("Exception occurred during main", e);
|
||||
QMessageBox::critical(nullptr, "Error", e.qwhat());
|
||||
QTextStream(stderr) << "reportID: " << QByteArray(uuid.bytes, 16).toHex() << "Captured exception :" << e.qwhat() << "\n";
|
||||
QTextStream(stderr) << "reportID: " << QByteArray(uuid.bytes, 16).toHex() << " Captured exception :" << e.detailedWhat() << "\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,14 +29,19 @@ Item {
|
||||
|
||||
property var _spacing: 12 * ProtonStyle.px
|
||||
|
||||
property color usedSpaceColor : {
|
||||
property color progressColor : {
|
||||
if (!root.enabled) return root.colorScheme.text_weak
|
||||
if (root.type == AccountDelegate.SmallView) return root.colorScheme.text_weak
|
||||
if (root.usedFraction < .50) return root.colorScheme.signal_success
|
||||
if (root.usedFraction < .75) return root.colorScheme.signal_warning
|
||||
if (root.user && root.user.isSyncing) return root.colorScheme.text_weak
|
||||
if (root.progressRatio < .50) return root.colorScheme.signal_success
|
||||
if (root.progressRatio < .75) return root.colorScheme.signal_warning
|
||||
return root.colorScheme.signal_danger
|
||||
}
|
||||
property real usedFraction: root.user ? reasonableFraction(root.user.usedBytes, root.user.totalBytes) : 0
|
||||
property real progressRatio: {
|
||||
if (!root.user)
|
||||
return 0
|
||||
return root.user.isSyncing ? root.user.syncProgress : reasonableFraction(root.user.usedBytes, root.user.totalBytes)
|
||||
}
|
||||
property string totalSpace: root.spaceWithUnits(root.user ? root.reasonableBytes(root.user.totalBytes) : 0)
|
||||
property string usedSpace: root.spaceWithUnits(root.user ? root.reasonableBytes(root.user.usedBytes) : 0)
|
||||
|
||||
@ -171,18 +176,21 @@ Item {
|
||||
case EUserState.Locked:
|
||||
return qsTr("Connecting") + dotsTimer.dots
|
||||
case EUserState.Connected:
|
||||
return root.usedSpace
|
||||
if (root.user.isSyncing)
|
||||
return qsTr("Synchronizing (%1%)").arg(Math.floor(root.user.syncProgress * 100)) + dotsTimer.dots
|
||||
else
|
||||
return root.usedSpace
|
||||
}
|
||||
}
|
||||
|
||||
Timer { // dots animation while connecting. 1 sec cycle, roughly similar to the webmail loading page.
|
||||
Timer { // dots animation while connecting & syncing.
|
||||
id:dotsTimer
|
||||
property string dots: ""
|
||||
interval: 250;
|
||||
interval: 500;
|
||||
repeat: true;
|
||||
running: (root.user != null) && (root.user.state === EUserState.Locked)
|
||||
running: (root.user != null) && ((root.user.state === EUserState.Locked) || (root.user.isSyncing))
|
||||
onTriggered: {
|
||||
dots = dots + "."
|
||||
dots += "."
|
||||
if (dots.length > 3)
|
||||
dots = ""
|
||||
}
|
||||
@ -191,7 +199,7 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
color: root.usedSpaceColor
|
||||
color: root.progressColor
|
||||
type: {
|
||||
switch (root.type) {
|
||||
case AccountDelegate.SmallView: return Label.Caption
|
||||
@ -202,7 +210,7 @@ Item {
|
||||
|
||||
Label {
|
||||
colorScheme: root.colorScheme
|
||||
text: root.user && root.user.state == EUserState.Connected ? " / " + root.totalSpace : ""
|
||||
text: root.user && root.user.state == EUserState.Connected && !root.user.isSyncing ? " / " + root.totalSpace : ""
|
||||
color: root.colorScheme.text_weak
|
||||
type: {
|
||||
switch (root.type) {
|
||||
@ -213,26 +221,27 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Item { implicitHeight: root.type == AccountDelegate.LargeView ? 3 * ProtonStyle.px : 0 }
|
||||
|
||||
Rectangle {
|
||||
id: storage_bar
|
||||
id: progress_bar
|
||||
visible: root.user ? root.type == AccountDelegate.LargeView : false
|
||||
width: 140 * ProtonStyle.px
|
||||
height: 4 * ProtonStyle.px
|
||||
radius: ProtonStyle.storage_bar_radius
|
||||
radius: ProtonStyle.progress_bar_radius
|
||||
color: root.colorScheme.border_weak
|
||||
|
||||
Rectangle {
|
||||
id: storage_bar_filled
|
||||
radius: ProtonStyle.storage_bar_radius
|
||||
color: root.usedSpaceColor
|
||||
visible: root.user ? parent.visible && (root.user.state == EUserState.Connected) : false
|
||||
id: progress_bar_filled
|
||||
radius: ProtonStyle.progress_bar_radius
|
||||
color: root.progressColor
|
||||
visible: root.user ? parent.visible && (root.user.state == EUserState.Connected): false
|
||||
anchors {
|
||||
top : parent.top
|
||||
bottom : parent.bottom
|
||||
left : parent.left
|
||||
}
|
||||
width: Math.min(1,Math.max(0.02,root.usedFraction)) * parent.width
|
||||
width: Math.min(1,Math.max(0.02,root.progressRatio)) * parent.width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,6 +29,8 @@ Item {
|
||||
property var notifications
|
||||
|
||||
signal showSetupGuide(var user, string address)
|
||||
signal closeWindow()
|
||||
signal quitBridge()
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
@ -107,7 +109,7 @@ Item {
|
||||
|
||||
Layout.topMargin: 16
|
||||
Layout.bottomMargin: 9
|
||||
Layout.rightMargin: 16
|
||||
Layout.rightMargin: 4
|
||||
|
||||
horizontalPadding: 0
|
||||
|
||||
@ -115,6 +117,55 @@ Item {
|
||||
|
||||
onClicked: rightContent.showGeneralSettings()
|
||||
}
|
||||
|
||||
Button {
|
||||
id: dotMenuButton
|
||||
Layout.bottomMargin: 9
|
||||
Layout.maximumHeight: 36
|
||||
Layout.maximumWidth: 36
|
||||
Layout.minimumHeight: 36
|
||||
Layout.minimumWidth: 36
|
||||
Layout.preferredHeight: 36
|
||||
Layout.preferredWidth: 36
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 16
|
||||
colorScheme: leftBar.colorScheme
|
||||
horizontalPadding: 0
|
||||
icon.source: "/qml/icons/ic-three-dots-vertical.svg"
|
||||
|
||||
onClicked: {
|
||||
dotMenu.open()
|
||||
}
|
||||
|
||||
Menu {
|
||||
id: dotMenu
|
||||
colorScheme: root.colorScheme
|
||||
modal: true
|
||||
y: dotMenuButton.Layout.preferredHeight + dotMenuButton.Layout.bottomMargin
|
||||
|
||||
MenuItem {
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Close window")
|
||||
onClicked: {
|
||||
root.closeWindow()
|
||||
}
|
||||
}
|
||||
MenuItem {
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Quit Bridge")
|
||||
onClicked: {
|
||||
root.quitBridge()
|
||||
}
|
||||
}
|
||||
|
||||
onClosed: {
|
||||
parent.checked = false
|
||||
}
|
||||
onOpened: {
|
||||
parent.checked = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {implicitHeight:10}
|
||||
|
||||
@ -142,6 +142,17 @@ ApplicationWindow {
|
||||
onShowSetupGuide: function(user, address) {
|
||||
root.showSetup(user,address)
|
||||
}
|
||||
|
||||
onCloseWindow: {
|
||||
root.close()
|
||||
}
|
||||
|
||||
onQuitBridge: {
|
||||
// If we ever want to add a confirmation dialog before quitting:
|
||||
//root.notifications.askQuestion("Quit Bridge", "Insert warning message here.", "Quit", "Cancel", Backend.quit, null)
|
||||
root.close()
|
||||
Backend.quit()
|
||||
}
|
||||
}
|
||||
|
||||
WelcomeGuide { // 1
|
||||
|
||||
@ -138,4 +138,9 @@ Item {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.genericError
|
||||
}
|
||||
|
||||
NotificationDialog {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.genericQuestion
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,7 +32,7 @@ QtObject {
|
||||
signal askResetBridge()
|
||||
signal askChangeAllMailVisibility(var isVisibleNow)
|
||||
signal askDeleteAccount(var user)
|
||||
|
||||
signal askQuestion(var title, var description, var option1, var option2, var action1, var action2)
|
||||
enum Group {
|
||||
Connection = 1,
|
||||
Update = 2,
|
||||
@ -81,7 +81,9 @@ QtObject {
|
||||
root.apiCertIssue,
|
||||
root.noActiveKeyForRecipient,
|
||||
root.userBadEvent,
|
||||
root.genericError
|
||||
root.imapLoginWhileSignedOut,
|
||||
root.genericError,
|
||||
root.genericQuestion,
|
||||
]
|
||||
|
||||
// Connection
|
||||
@ -1103,48 +1105,72 @@ QtObject {
|
||||
}
|
||||
|
||||
property Notification userBadEvent: Notification {
|
||||
title: qsTr("Your account was logged out")
|
||||
title: qsTr("Internal error")
|
||||
brief: title
|
||||
description: "#PlaceHolderText"
|
||||
icon: "./icons/ic-exclamation-circle-filled.svg"
|
||||
type: Notification.NotificationType.Danger
|
||||
group: Notifications.Group.Connection | Notifications.Group.Dialogs
|
||||
|
||||
property var bugReportMsg: "Reporting an issue:\n\n\"%1\"\n\nError: %2\n\nThe issue persists even after loggin back in."
|
||||
property var errorMessage: ""
|
||||
property var userID: ""
|
||||
|
||||
Connections {
|
||||
target: Backend
|
||||
function onUserBadEvent(description, errorMessage) {
|
||||
root.userBadEvent.description = description
|
||||
root.userBadEvent.errorMessage = errorMessage
|
||||
function onUserBadEvent(userID, errorMessage) {
|
||||
root.userBadEvent.userID = userID
|
||||
root.userBadEvent.description = errorMessage
|
||||
root.userBadEvent.active = true
|
||||
}
|
||||
}
|
||||
|
||||
action: [
|
||||
Action {
|
||||
text: qsTr("Synchronize")
|
||||
|
||||
onTriggered: {
|
||||
root.userBadEvent.active = false
|
||||
Backend.sendBadEventUserFeedback(root.userBadEvent.userID, true)
|
||||
}
|
||||
},
|
||||
|
||||
Action {
|
||||
text: qsTr("Logout")
|
||||
|
||||
onTriggered: {
|
||||
root.userBadEvent.active = false
|
||||
Backend.sendBadEventUserFeedback(root.userBadEvent.userID, false)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
property Notification imapLoginWhileSignedOut: Notification {
|
||||
title: qsTr("IMAP Login failed")
|
||||
brief: title
|
||||
description: "#PlaceHolderText"
|
||||
icon: "./icons/ic-exclamation-circle-filled.svg"
|
||||
type: Notification.NotificationType.Danger
|
||||
group: Notifications.Group.Connection
|
||||
|
||||
Connections {
|
||||
target: Backend
|
||||
function onImapLoginWhileSignedOut(username) {
|
||||
root.imapLoginWhileSignedOut.description = qsTr("An email client tried to connect to the account %1, but this account is signed " +
|
||||
"out. Please sign-in to continue.").arg(username)
|
||||
root.imapLoginWhileSignedOut.active = true
|
||||
}
|
||||
}
|
||||
|
||||
action: [
|
||||
Action {
|
||||
text: qsTr("OK")
|
||||
|
||||
onTriggered: {
|
||||
root.userBadEvent.active = false
|
||||
}
|
||||
},
|
||||
|
||||
Action {
|
||||
text: qsTr("Report")
|
||||
|
||||
onTriggered: {
|
||||
root.frontendMain.showBugReportAndPrefill(
|
||||
root.userBadEvent.bugReportMsg.
|
||||
arg( root.userBadEvent.description).
|
||||
arg(root.userBadEvent.errorMessage)
|
||||
)
|
||||
root.userBadEvent.active = false
|
||||
root.imapLoginWhileSignedOut.active = false
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
property Notification genericError: Notification {
|
||||
@ -1172,4 +1198,50 @@ QtObject {
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
property Notification genericQuestion: Notification {
|
||||
title: ""
|
||||
brief: ""
|
||||
description: ""
|
||||
type: Notification.NotificationType.Warning
|
||||
group: Notifications.Group.Dialogs
|
||||
property var option1: ""
|
||||
property var option2: ""
|
||||
property variant action1: null
|
||||
property variant action2: null
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onAskQuestion(title, description, option1, option2, action1, action2) {
|
||||
root.genericQuestion.title = title
|
||||
root.genericQuestion.description = description
|
||||
root.genericQuestion.option1 = option1
|
||||
root.genericQuestion.option2 = option2
|
||||
root.genericQuestion.action1 = action1
|
||||
root.genericQuestion.action2 = action2
|
||||
root.genericQuestion.active = true
|
||||
}
|
||||
}
|
||||
|
||||
action: [
|
||||
Action {
|
||||
text: root.genericQuestion.option1
|
||||
|
||||
onTriggered: {
|
||||
root.genericQuestion.active = false
|
||||
if (root.genericQuestion.action1)
|
||||
root.genericQuestion.action1()
|
||||
}
|
||||
},
|
||||
Action {
|
||||
text: root.genericQuestion.option2
|
||||
|
||||
onTriggered: {
|
||||
root.genericQuestion.active = false
|
||||
if (root.genericQuestion.action2)
|
||||
root.genericQuestion.action2()
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,13 +26,12 @@ T.MenuItem {
|
||||
|
||||
property ColorScheme colorScheme
|
||||
|
||||
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
|
||||
implicitContentWidth + leftPadding + rightPadding)
|
||||
width: parent.width // required. Other item overflows to the right of the menu and get clipped.
|
||||
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
|
||||
implicitContentHeight + topPadding + bottomPadding,
|
||||
implicitIndicatorHeight + topPadding + bottomPadding)
|
||||
|
||||
padding: 6
|
||||
padding: 12
|
||||
spacing: 6
|
||||
|
||||
icon.width: 24
|
||||
|
||||
@ -362,7 +362,7 @@ QtObject {
|
||||
property real banner_radius : 12 * root.px // px
|
||||
property real dialog_radius : 12 * root.px // px
|
||||
property real card_radius : 12 * root.px // px
|
||||
property real storage_bar_radius : 3 * root.px // px
|
||||
property real progress_bar_radius : 3 * root.px // px
|
||||
property real tooltip_radius : 8 * root.px // px
|
||||
|
||||
property int heading_font_size: 28
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user