mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-10 12:46:46 +00:00
Compare commits
274 Commits
v3.1.2
...
release/tr
| Author | SHA1 | Date | |
|---|---|---|---|
| a80fd92018 | |||
| 71063ac5ee | |||
| 0a555bf767 | |||
| d72980e443 | |||
| b24937b666 | |||
| 5ca9ec6674 | |||
| e6ab874308 | |||
| c35344d6f1 | |||
| a9865976a3 | |||
| 84adbbc461 | |||
| 75811d22e8 | |||
| e26c7683d2 | |||
| d7b71aceda | |||
| 25a787529b | |||
| f82965b825 | |||
| f1cf4ee194 | |||
| 5136919c36 | |||
| 4f8ecd598f | |||
| 65365281eb | |||
| c05dfb36d3 | |||
| b2830b39e0 | |||
| 7997ad2b93 | |||
| 80d743afec | |||
| ac9857a965 | |||
| 0e9fd46a5c | |||
| 305d180a5f | |||
| c4f80103b6 | |||
| 0d57e3645a | |||
| 0afdc31f96 | |||
| 91de6e001e | |||
| 908ed3e723 | |||
| 7411073c08 | |||
| 7d838375bb | |||
| f545f30ec0 | |||
| 6579cdfc7f | |||
| 40c48ba804 | |||
| 1c2cb4f439 | |||
| 650dac37a7 | |||
| 55275b23ee | |||
| bce69e1a1b | |||
| f1917ad0de | |||
| 6ea6d54af6 | |||
| c1b486a7eb | |||
| 454c9e1534 | |||
| eaa673c4e4 | |||
| 9c389e3007 | |||
| 7e9a5934c5 | |||
| cc17366c1c | |||
| 62f6db35db | |||
| be422001e8 | |||
| b2eb35592f | |||
| a3b26431ce | |||
| f460323cc5 | |||
| 26694d3bd8 | |||
| e96713a998 | |||
| 6359b8639e | |||
| 6c9d5ccd4a | |||
| 234554b459 | |||
| 6df5a82364 | |||
| ac75410657 | |||
| 238929c3ec | |||
| 1f79e3b0a7 | |||
| f5af2afce5 | |||
| 9482bea8af | |||
| a55572e5b3 | |||
| 098eb7cb7a | |||
| 68334e3bb8 | |||
| 124231c3c7 | |||
| f591af2cbd | |||
| ff11d20d9c | |||
| 72911235c5 | |||
| 60de00c73f | |||
| 4e080b59d3 | |||
| bac4b90c1d | |||
| ea47c9aa1c | |||
| a91d9762db | |||
| 6fb11d69f9 | |||
| 1eab3296d1 | |||
| 4352154b84 | |||
| 03cf601921 | |||
| 9f13301613 | |||
| 650158ea8a | |||
| 552fc2700f | |||
| 582afa1451 | |||
| c43739a7ef | |||
| 720f662afe | |||
| e9488d12ee | |||
| 9a87b155ba | |||
| f6a1cd9b64 | |||
| 7b7c9093ce | |||
| fa4c0ec823 | |||
| 08af1da966 | |||
| ed8475dacf | |||
| e91cdca6f3 | |||
| c267168cb7 | |||
| 58b45d8458 | |||
| b77512dfd9 | |||
| bdc6542970 | |||
| c942a44f6a | |||
| 55081fa59b | |||
| cc1d0e803b | |||
| b7a2371220 | |||
| ae65385c38 | |||
| ab70e85f1c | |||
| ff57eb2b43 | |||
| e78cb8089b | |||
| 09eef64514 | |||
| a2c2710760 | |||
| 38a0cdb4ab | |||
| c587dfc0dc | |||
| 7a090ffcc9 | |||
| 6ab49367ba | |||
| 12f9fb03c3 | |||
| 419a4427d7 | |||
| e434a4fd0c | |||
| d68b163c20 | |||
| 71a559ee4c | |||
| ac00ef1b64 | |||
| 1e9a77c7b2 | |||
| 4aaa555c9b | |||
| a8dd52800e | |||
| fab063f194 | |||
| 5c606aee73 | |||
| 51315d5d2b | |||
| cde9c19f71 | |||
| 4902898880 | |||
| c46b3245b8 | |||
| 3afd94c61d | |||
| ade2fd9403 | |||
| 89632b7acd | |||
| b8cf193911 | |||
| fb4d34ef5b | |||
| 7f7e360cd7 | |||
| 802f7dbc67 | |||
| fc06665d2b | |||
| c4dc829e6d | |||
| bfaf9765ae | |||
| a702e19dff | |||
| 0eab1c0c2b | |||
| 11f55b59a9 | |||
| 172b59c756 | |||
| 8564d3a483 | |||
| 0cef181432 | |||
| 262c4c5d95 | |||
| e343d1f1af | |||
| 84a771d9fe | |||
| f6741a9b58 | |||
| bc5de2b884 | |||
| 7d54e6907d | |||
| 4b52d998db | |||
| aa72fd641d | |||
| ebe45d5abe | |||
| b6eb5a1b13 | |||
| 9c25f56fe6 | |||
| bb99695e68 | |||
| 35f0e081a5 | |||
| 40dc17aea5 | |||
| 5fee2f707b | |||
| d6304b087a | |||
| 21f833ea11 | |||
| 900caec09e | |||
| 9fc9f5ad9f | |||
| d8ccc6c05d | |||
| 4e3ad4f7fa | |||
| 8c958cdc2f | |||
| 0ef9c9c9c9 | |||
| 73bb0ed03e | |||
| faf3780eee | |||
| 98031d296e | |||
| d08b3fcca4 | |||
| 6adb440b84 | |||
| a3e07428b5 | |||
| dfd85f7ed3 | |||
| 4b5edd62d0 | |||
| fb4a0e77af | |||
| edac2419f9 | |||
| 6ba8052a1e | |||
| 51288791c0 | |||
| 51aafe3266 | |||
| 8fd09547fa | |||
| aba94a9353 | |||
| e6dfb814fd | |||
| bfa53b1619 | |||
| b9c54e9276 | |||
| 71d7deb3b1 | |||
| e606d98664 | |||
| 36342299c7 | |||
| a05f93debd | |||
| c438704648 | |||
| 0417e495ae | |||
| bda158d6c6 | |||
| ad02c71ad6 | |||
| eee2c73a61 | |||
| 5630b7d2e6 | |||
| 2ee4893325 | |||
| 01aa19edff | |||
| a0db1645f2 | |||
| 324593596a | |||
| b51d85e768 | |||
| bd47303074 | |||
| fdae8cb729 | |||
| 333daa05c5 | |||
| 6a6ead8e6d | |||
| 543c35041d | |||
| 50709acc5f | |||
| 994cec196c | |||
| 910060a14c | |||
| 38b13d710c | |||
| 06f710a9b1 | |||
| fbbd0245de | |||
| 7002806999 | |||
| c49b42060e | |||
| 21b20ac420 | |||
| d8fa2fb3e3 | |||
| ae7621ed6a | |||
| f3fb10fb2d | |||
| 9241e5317f | |||
| 3ebd2f53f4 | |||
| 59a944a06c | |||
| f9a0c35daa | |||
| e9629aca47 | |||
| 7383d65cb2 | |||
| 3ef3ab72ed | |||
| 00adb8bc22 | |||
| d3fc9a50f6 | |||
| 4adc6d60b9 | |||
| d88bee68c6 | |||
| 67b5e7f96a | |||
| b250d49af8 | |||
| 0f621d0aad | |||
| 6ddaa94bc0 | |||
| fed503501d | |||
| ce5a559926 | |||
| 3b297fa37b | |||
| 95741c6d63 | |||
| ad4e853a8a | |||
| f4631c4bc9 | |||
| c000ee8a3c | |||
| 3ddd88e127 | |||
| 54b209f9e1 | |||
| 4e7a669260 | |||
| 8093bbf5f6 | |||
| 7bb925b6d7 | |||
| d6760d6f50 | |||
| 2191dc70dc | |||
| ff44a3d6df | |||
| b0c3faa228 | |||
| 3928ed08f6 | |||
| c7ae239350 | |||
| 7d51e9123d | |||
| a8cb0012e1 | |||
| c84919faae | |||
| 3735d4b327 | |||
| 1323229362 | |||
| 0a53dc1da7 | |||
| 86fd7961a1 | |||
| aa957f3314 | |||
| 7124e7c9a0 | |||
| 421da99cd8 | |||
| ef1940d227 | |||
| f1fb525dc8 | |||
| 74203e07a4 | |||
| 58df3312c1 | |||
| 4c94606e20 | |||
| ee5e87fe85 | |||
| dbd156a272 | |||
| 03c3e95b34 | |||
| a8250ff65e | |||
| d8bf2cc25e | |||
| 4f49c87bc6 | |||
| 02ca6428b5 | |||
| b3d0dfc60b | |||
| 622b76b57b | |||
| e1dff67c10 |
4
.gitignore
vendored
4
.gitignore
vendored
@ -41,3 +41,7 @@ cmake-build-*/
|
||||
|
||||
# Doxygen doc files
|
||||
_doc/
|
||||
|
||||
# gRPC auto-generated C++ source files
|
||||
*.pb.cc
|
||||
*.pb.h
|
||||
|
||||
234
.gitlab-ci.yml
234
.gitlab-ci.yml
@ -16,7 +16,7 @@
|
||||
# along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
---
|
||||
image: harbor.protontech.ch/docker.io/library/golang:1.18
|
||||
image: gitlab.protontech.ch:4567/go/bridge-internal:test-go1.20
|
||||
|
||||
variables:
|
||||
GOPRIVATE: gitlab.protontech.ch
|
||||
@ -30,13 +30,6 @@ stages:
|
||||
- test
|
||||
- build
|
||||
|
||||
.rules-branch-and-MR-always:
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH || $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
when: always
|
||||
allow_failure: false
|
||||
- when: never
|
||||
|
||||
.rules-branch-and-MR-manual:
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH || $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
@ -44,16 +37,6 @@ stages:
|
||||
allow_failure: true
|
||||
- when: never
|
||||
|
||||
.rules-branch-manual-MR-always:
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
when: always
|
||||
allow_failure: false
|
||||
- if: $CI_COMMIT_BRANCH
|
||||
when: manual
|
||||
allow_failure: true
|
||||
- when: never
|
||||
|
||||
.rules-branch-manual-MR-and-devel-always:
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == "devel" || $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
@ -64,42 +47,92 @@ stages:
|
||||
allow_failure: true
|
||||
- when: never
|
||||
|
||||
.after-script-code-coverage:
|
||||
after_script:
|
||||
- go get github.com/boumenot/gocover-cobertura
|
||||
- go run github.com/boumenot/gocover-cobertura < /tmp/coverage.out > coverage.xml
|
||||
- "go tool cover -func=/tmp/coverage.out | grep total:"
|
||||
coverage: '/total:.*\(statements\).*\d+\.\d+%/'
|
||||
artifacts:
|
||||
reports:
|
||||
coverage_report:
|
||||
coverage_format: cobertura
|
||||
path: coverage.xml
|
||||
# ENV
|
||||
.env-windows:
|
||||
before_script:
|
||||
- export BRIDGE_SYNC_FORCE_MINIMUM_SPEC=1
|
||||
- export GOROOT=/c/Go1.20/
|
||||
- export PATH=$GOROOT/bin:$PATH
|
||||
- export GOARCH=amd64
|
||||
- export GOPATH=~/go1.20
|
||||
- export GO111MODULE=on
|
||||
- export PATH="${GOPATH}/bin:${PATH}"
|
||||
- export MSYSTEM=
|
||||
- export QT6DIR=/c/grrrQt/6.3.2/msvc2019_64
|
||||
- export PATH=$PATH:${QT6DIR}/bin
|
||||
- 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}
|
||||
- git config --global safe.directory '*'
|
||||
- git status --porcelain
|
||||
cache: {}
|
||||
tags:
|
||||
- windows-bridge
|
||||
|
||||
.env-darwin:
|
||||
before_script:
|
||||
- export BRIDGE_SYNC_FORCE_MINIMUM_SPEC=1
|
||||
- export PATH=/usr/local/bin:$PATH
|
||||
- export PATH=/usr/local/opt/git/bin:$PATH
|
||||
- export PATH=/usr/local/opt/make/libexec/gnubin:$PATH
|
||||
- export PATH=/usr/local/opt/gnu-sed/libexec/gnubin:$PATH
|
||||
- export GOROOT=~/local/opt/go@1.20
|
||||
- export PATH="${GOROOT}/bin:$PATH"
|
||||
- export GOPATH=~/go1.20
|
||||
- export PATH="${GOPATH}/bin:$PATH"
|
||||
- export QT6DIR=/opt/Qt/6.3.2/macos
|
||||
- export PATH="${QT6DIR}/bin:$PATH"
|
||||
- uname -a
|
||||
cache: {}
|
||||
tags:
|
||||
- macos-m1-bridge
|
||||
|
||||
.env-linux-build:
|
||||
image: gitlab.protontech.ch:4567/go/bridge-internal:build-go1.20-qt6.3.2
|
||||
variables:
|
||||
VCPKG_DEFAULT_BINARY_CACHE: ${CI_PROJECT_DIR}/.cache
|
||||
cache:
|
||||
key: linux-vcpkg
|
||||
paths:
|
||||
- .cache
|
||||
when: 'always'
|
||||
before_script:
|
||||
- mkdir -p .cache/bin
|
||||
- export BRIDGE_SYNC_FORCE_MINIMUM_SPEC=1
|
||||
- export PATH=$(pwd)/.cache/bin:$PATH
|
||||
- export GOPATH="$CI_PROJECT_DIR/.cache"
|
||||
- export PATH=$PATH:$QT6DIR/bin
|
||||
- $(git config --global -l | grep -o 'url.*gitlab.protontech.ch.*insteadof' | xargs -L 1 git config --global --unset &> /dev/null) || echo "nothing to remove"
|
||||
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
|
||||
tags:
|
||||
- large
|
||||
|
||||
# Stage: TEST
|
||||
|
||||
lint:
|
||||
stage: test
|
||||
extends:
|
||||
- .rules-branch-and-MR-always
|
||||
- .rules-branch-manual-MR-and-devel-always
|
||||
script:
|
||||
- make lint
|
||||
tags:
|
||||
- medium
|
||||
|
||||
|
||||
.test-base:
|
||||
.script-test:
|
||||
stage: test
|
||||
extends:
|
||||
- .rules-branch-manual-MR-and-devel-always
|
||||
script:
|
||||
- make test
|
||||
artifacts:
|
||||
paths:
|
||||
- coverage/**
|
||||
|
||||
test-linux:
|
||||
extends:
|
||||
- .test-base
|
||||
- .rules-branch-manual-MR-and-devel-always
|
||||
- .after-script-code-coverage
|
||||
- .script-test
|
||||
tags:
|
||||
- medium
|
||||
- large
|
||||
|
||||
test-linux-race:
|
||||
extends:
|
||||
@ -113,8 +146,6 @@ test-integration:
|
||||
- test-linux
|
||||
script:
|
||||
- make test-integration
|
||||
tags:
|
||||
- large
|
||||
|
||||
test-integration-race:
|
||||
extends:
|
||||
@ -123,41 +154,48 @@ test-integration-race:
|
||||
script:
|
||||
- make test-integration-race
|
||||
|
||||
|
||||
.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
|
||||
- .env-windows
|
||||
- .script-test
|
||||
- .rules-branch-and-MR-manual
|
||||
|
||||
test-darwin:
|
||||
extends:
|
||||
- .env-darwin
|
||||
- .script-test
|
||||
|
||||
test-coverage:
|
||||
stage: test
|
||||
extends:
|
||||
- .rules-branch-manual-MR-and-devel-always
|
||||
script:
|
||||
- make test
|
||||
- ./utils/coverage.sh
|
||||
coverage: '/total:.*\(statements\).*\d+\.\d+%/'
|
||||
needs:
|
||||
- test-linux
|
||||
#- test-windows
|
||||
- test-darwin
|
||||
- test-integration
|
||||
tags:
|
||||
- small
|
||||
artifacts:
|
||||
paths:
|
||||
- coverage*
|
||||
- coverage/**
|
||||
when: 'always'
|
||||
reports:
|
||||
coverage_report:
|
||||
coverage_format: cobertura
|
||||
path: coverage.xml
|
||||
|
||||
# Stage: BUILD
|
||||
|
||||
.build-base:
|
||||
.script-build:
|
||||
stage: build
|
||||
needs: ["lint"]
|
||||
rules:
|
||||
# GODT-1833: use `=~ /qa/` after mac and windows runners are fixed
|
||||
- if: $CI_JOB_NAME =~ /build-linux-qa/ && $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
when: always
|
||||
allow_failure: false
|
||||
- if: $CI_COMMIT_BRANCH || $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
when: manual
|
||||
allow_failure: true
|
||||
- when: never
|
||||
extends:
|
||||
- .rules-branch-and-MR-manual
|
||||
script:
|
||||
- make build
|
||||
- git diff && git diff-index --quiet HEAD
|
||||
@ -170,58 +208,22 @@ test-windows:
|
||||
- bridge_*.tgz
|
||||
- vault-editor
|
||||
|
||||
|
||||
.linux-build-setup:
|
||||
image: gitlab.protontech.ch:4567/go/bridge-internal:qt6
|
||||
variables:
|
||||
VCPKG_DEFAULT_BINARY_CACHE: ${CI_PROJECT_DIR}/.cache
|
||||
cache:
|
||||
key: linux-vcpkg
|
||||
paths:
|
||||
- .cache
|
||||
when: 'always'
|
||||
before_script:
|
||||
- mkdir -p .cache/bin
|
||||
- export PATH=$(pwd)/.cache/bin:$PATH
|
||||
- export GOPATH="$CI_PROJECT_DIR/.cache"
|
||||
- export PATH=$PATH:$QT6DIR/bin
|
||||
- $(git config --global -l | grep -o 'url.*gitlab.protontech.ch.*insteadof' | xargs -L 1 git config --global --unset &> /dev/null) || echo "nothing to remove"
|
||||
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
|
||||
tags:
|
||||
- large
|
||||
|
||||
build-linux:
|
||||
extends:
|
||||
- .build-base
|
||||
- .linux-build-setup
|
||||
- .script-build
|
||||
- .env-linux-build
|
||||
|
||||
build-linux-qa:
|
||||
extends:
|
||||
- build-linux
|
||||
- .rules-branch-manual-MR-and-devel-always
|
||||
variables:
|
||||
BUILD_TAGS: "build_qa"
|
||||
|
||||
|
||||
.darwin-build-setup:
|
||||
before_script:
|
||||
- export PATH=/usr/local/bin:$PATH
|
||||
- export PATH=/usr/local/opt/git/bin:$PATH
|
||||
- export PATH=/usr/local/opt/make/libexec/gnubin:$PATH
|
||||
- export PATH=/usr/local/opt/go@1.13/bin:$PATH
|
||||
- export PATH=/usr/local/opt/gnu-sed/libexec/gnubin:$PATH
|
||||
- export GOPATH=~/go
|
||||
- export PATH=$GOPATH/bin:$PATH
|
||||
- export CGO_CPPFLAGS='-Wno-error -Wno-nullability-completeness -Wno-expansion-to-defined -Wno-builtin-requires-header'
|
||||
- $(git config --global -l | grep -o 'url.*gitlab.protontech.ch.*insteadof' | xargs -L 1 git config --global --unset &> /dev/null) || echo "nothing to remove"
|
||||
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
|
||||
cache: {}
|
||||
tags:
|
||||
- macOS
|
||||
|
||||
build-darwin:
|
||||
extends:
|
||||
- .build-base
|
||||
- .darwin-build-setup
|
||||
- .script-build
|
||||
- .env-darwin
|
||||
|
||||
build-darwin-qa:
|
||||
extends:
|
||||
@ -229,28 +231,10 @@ build-darwin-qa:
|
||||
variables:
|
||||
BUILD_TAGS: "build_qa"
|
||||
|
||||
.windows-build-setup:
|
||||
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=
|
||||
- export QT6DIR=/c/grrrQt/6.3.1/msvc2019_64
|
||||
- export PATH=$PATH:${QT6DIR}/bin
|
||||
- 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}
|
||||
cache: {}
|
||||
tags:
|
||||
- windows-bridge
|
||||
|
||||
build-windows:
|
||||
extends:
|
||||
- .build-base
|
||||
- .windows-build-setup
|
||||
- .script-build
|
||||
- .env-windows
|
||||
|
||||
build-windows-qa:
|
||||
extends:
|
||||
|
||||
@ -36,6 +36,14 @@ issues:
|
||||
- gosec
|
||||
- goconst
|
||||
- dogsled
|
||||
- path: utils/smtp-send
|
||||
linters:
|
||||
- dupl
|
||||
- gochecknoglobals
|
||||
- gochecknoinits
|
||||
- gosec
|
||||
- goconst
|
||||
- dogsled
|
||||
|
||||
linters-settings:
|
||||
godox:
|
||||
@ -48,18 +56,15 @@ linters:
|
||||
disable-all: true
|
||||
|
||||
enable:
|
||||
- deadcode # Finds unused code [fast: true, auto-fix: false]
|
||||
- errcheck # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases [fast: true, auto-fix: false]
|
||||
- gosimple # Linter for Go source code that specializes in simplifying a code [fast: true, auto-fix: false]
|
||||
- govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string [fast: true, auto-fix: false]
|
||||
- ineffassign # Detects when assignments to existing variables are not used [fast: true, auto-fix: false]
|
||||
- staticcheck # Staticcheck is a go vet on steroids, applying a ton of static analysis checks [fast: true, auto-fix: false]
|
||||
- structcheck # Finds unused struct fields [fast: true, auto-fix: false]
|
||||
- typecheck # Like the front-end of a Go compiler, parses and type-checks Go code [fast: true, auto-fix: false]
|
||||
- unused # Checks Go code for unused constants, variables, functions and types [fast: false, auto-fix: false]
|
||||
- varcheck # Finds unused global variables and constants [fast: true, auto-fix: false]
|
||||
- bodyclose # checks whether HTTP response body is closed successfully [fast: true, auto-fix: false]
|
||||
- depguard # Go linter that checks if package imports are in a list of acceptable packages [fast: true, auto-fix: false]
|
||||
#- 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]
|
||||
- gochecknoglobals # Checks that no globals are present in Go code [fast: true, auto-fix: false]
|
||||
@ -119,3 +124,8 @@ linters:
|
||||
# - testpackage # linter that makes you use a separate _test package [fast: true, auto-fix: false]
|
||||
# - thelper # thelper detects golang test helpers without t.Helper() call and checks the consistency of test helpers [fast: false, auto-fix: false]
|
||||
# - wrapcheck # Checks that errors returned from external packages are wrapped [fast: false, auto-fix: false]
|
||||
|
||||
# Deprecated:
|
||||
# - structcheck # Finds unused struct fields [fast: true, auto-fix: false] deprecated (since v1.49.0) due to: The owner seems to have abandoned the linter. Replaced by unused.
|
||||
# - deadcode # Finds unused code [fast: true, auto-fix: false] deprecated (since v1.49.0) due to: The owner seems to have abandoned the linter. Replaced by unused.
|
||||
# - varcheck # Finds unused global variables and constants [fast: true, auto-fix: false] deprecated (since v1.49.0) due to: The owner seems to have abandoned the linter. Replaced by unused.
|
||||
|
||||
11
BUILDS.md
11
BUILDS.md
@ -3,12 +3,15 @@
|
||||
## Prerequisites
|
||||
* 64-bit OS:
|
||||
- the go-rfc5322 module cannot currently be compiled for 32-bit OSes
|
||||
* Go 1.18
|
||||
* Go 1.20
|
||||
* Bash with basic build utils: make, gcc, sed, find, grep, ...
|
||||
- For Windows, it is recommended to use MinGW 64bit shell from [MSYS2](https://www.msys2.org/)
|
||||
* GCC (linux), msvc (windows) or Xcode (macOS)
|
||||
* Windres (windows)
|
||||
* libglvnd and libsecret development files (linux)
|
||||
* GCC (Linux), msvc (Windows) or Xcode (macOS)
|
||||
* Windres (Windows)
|
||||
* libglvnd and libsecret development files (Linux)
|
||||
* pkg-config (Linux)
|
||||
* cmake, ninja-build and Qt 6 are required to build the graphical user interface. On Linux,
|
||||
the Mesa OpenGL development files are also needed.
|
||||
|
||||
To enable the sending of crash reports using Sentry please set the
|
||||
`DSN_SENTRY` environment variable with the client key of your sentry project before build.
|
||||
|
||||
@ -43,7 +43,6 @@ Proton Mail Bridge includes the following 3rd party software:
|
||||
* [color](https://github.com/fatih/color) available under [license](https://github.com/fatih/color/blob/master/LICENSE)
|
||||
* [sentry-go](https://github.com/getsentry/sentry-go) available under [license](https://github.com/getsentry/sentry-go/blob/master/LICENSE)
|
||||
* [resty](https://github.com/go-resty/resty/v2) available under [license](https://github.com/go-resty/resty/v2/blob/master/LICENSE)
|
||||
* [go-json](https://github.com/goccy/go-json) available under [license](https://github.com/goccy/go-json/blob/master/LICENSE)
|
||||
* [dbus](https://github.com/godbus/dbus) available under [license](https://github.com/godbus/dbus/blob/master/LICENSE)
|
||||
* [mock](https://github.com/golang/mock) available under [license](https://github.com/golang/mock/blob/master/LICENSE)
|
||||
* [go-cmp](https://github.com/google/go-cmp) available under [license](https://github.com/google/go-cmp/blob/master/LICENSE)
|
||||
@ -59,7 +58,7 @@ Proton Mail Bridge includes the following 3rd party software:
|
||||
* [testify](https://github.com/stretchr/testify) available under [license](https://github.com/stretchr/testify/blob/master/LICENSE)
|
||||
* [cli](https://github.com/urfave/cli/v2) available under [license](https://github.com/urfave/cli/v2/blob/master/LICENSE)
|
||||
* [msgpack](https://github.com/vmihailenco/msgpack/v5) available under [license](https://github.com/vmihailenco/msgpack/v5/blob/master/LICENSE)
|
||||
* [goleak](https://go.uber.org/goleak)
|
||||
* [goleak](https://go.uber.org/goleak) available under [license](https://pkg.go.dev/go.uber.org/goleak?tab=licenses)
|
||||
* [exp](https://golang.org/x/exp) available under [license](https://cs.opensource.google/go/x/exp/+/master:LICENSE)
|
||||
* [net](https://golang.org/x/net) available under [license](https://cs.opensource.google/go/x/net/+/master:LICENSE)
|
||||
* [sys](https://golang.org/x/sys) available under [license](https://cs.opensource.google/go/x/sys/+/master:LICENSE)
|
||||
@ -67,16 +66,12 @@ Proton Mail Bridge includes the following 3rd party software:
|
||||
* [grpc](https://google.golang.org/grpc) available under [license](https://github.com/grpc/grpc-go/blob/master/LICENSE)
|
||||
* [protobuf](https://google.golang.org/protobuf) available under [license](https://github.com/protocolbuffers/protobuf/blob/main/LICENSE)
|
||||
* [plist](https://howett.net/plist) available under [license](https://github.com/DHowett/go-plist/blob/main/LICENSE)
|
||||
* [atlas](https://ariga.io/atlas)
|
||||
* [ent](https://entgo.io/ent)
|
||||
* [bcrypt](https://github.com/ProtonMail/bcrypt) available under [license](https://github.com/ProtonMail/bcrypt/blob/master/LICENSE)
|
||||
* [go-crypto](https://github.com/ProtonMail/go-crypto) available under [license](https://github.com/ProtonMail/go-crypto/blob/master/LICENSE)
|
||||
* [go-mime](https://github.com/ProtonMail/go-mime) available under [license](https://github.com/ProtonMail/go-mime/blob/master/LICENSE)
|
||||
* [go-srp](https://github.com/ProtonMail/go-srp) available under [license](https://github.com/ProtonMail/go-srp/blob/master/LICENSE)
|
||||
* [readline](https://github.com/abiosoft/readline) available under [license](https://github.com/abiosoft/readline/blob/master/LICENSE)
|
||||
* [levenshtein](https://github.com/agext/levenshtein) available under [license](https://github.com/agext/levenshtein/blob/master/LICENSE)
|
||||
* [cascadia](https://github.com/andybalholm/cascadia) available under [license](https://github.com/andybalholm/cascadia/blob/master/LICENSE)
|
||||
* [go-textseg](https://github.com/apparentlymart/go-textseg/v13) available under [license](https://github.com/apparentlymart/go-textseg/v13/blob/master/LICENSE)
|
||||
* [sonic](https://github.com/bytedance/sonic) available under [license](https://github.com/bytedance/sonic/blob/master/LICENSE)
|
||||
* [base64x](https://github.com/chenzhuoyu/base64x) available under [license](https://github.com/chenzhuoyu/base64x/blob/master/LICENSE)
|
||||
* [test](https://github.com/chzyer/test) available under [license](https://github.com/chzyer/test/blob/master/LICENSE)
|
||||
@ -91,12 +86,13 @@ Proton Mail Bridge includes the following 3rd party software:
|
||||
* [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)
|
||||
* [mimetype](https://github.com/gabriel-vasile/mimetype) available under [license](https://github.com/gabriel-vasile/mimetype/blob/master/LICENSE)
|
||||
* [sse](https://github.com/gin-contrib/sse) available under [license](https://github.com/gin-contrib/sse/blob/master/LICENSE)
|
||||
* [gin](https://github.com/gin-gonic/gin) available under [license](https://github.com/gin-gonic/gin/blob/master/LICENSE)
|
||||
* [inflect](https://github.com/go-openapi/inflect) available under [license](https://github.com/go-openapi/inflect/blob/master/LICENSE)
|
||||
* [locales](https://github.com/go-playground/locales) available under [license](https://github.com/go-playground/locales/blob/master/LICENSE)
|
||||
* [universal-translator](https://github.com/go-playground/universal-translator) available under [license](https://github.com/go-playground/universal-translator/blob/master/LICENSE)
|
||||
* [validator](https://github.com/go-playground/validator/v10) available under [license](https://github.com/go-playground/validator/v10/blob/master/LICENSE)
|
||||
* [go-json](https://github.com/goccy/go-json) available under [license](https://github.com/goccy/go-json/blob/master/LICENSE)
|
||||
* [uuid](https://github.com/gofrs/uuid) available under [license](https://github.com/gofrs/uuid/blob/master/LICENSE)
|
||||
* [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)
|
||||
@ -104,7 +100,6 @@ Proton Mail Bridge includes the following 3rd party software:
|
||||
* [go-immutable-radix](https://github.com/hashicorp/go-immutable-radix) available under [license](https://github.com/hashicorp/go-immutable-radix/blob/master/LICENSE)
|
||||
* [go-memdb](https://github.com/hashicorp/go-memdb) available under [license](https://github.com/hashicorp/go-memdb/blob/master/LICENSE)
|
||||
* [golang-lru](https://github.com/hashicorp/golang-lru) available under [license](https://github.com/hashicorp/golang-lru/blob/master/LICENSE)
|
||||
* [hcl](https://github.com/hashicorp/hcl/v2) available under [license](https://github.com/hashicorp/hcl/v2/blob/master/LICENSE)
|
||||
* [multierror](https://github.com/joeshaw/multierror) available under [license](https://github.com/joeshaw/multierror/blob/master/LICENSE)
|
||||
* [go](https://github.com/json-iterator/go) available under [license](https://github.com/json-iterator/go/blob/master/LICENSE)
|
||||
* [cpuid](https://github.com/klauspost/cpuid/v2) available under [license](https://github.com/klauspost/cpuid/v2/blob/master/LICENSE)
|
||||
@ -113,7 +108,6 @@ Proton Mail Bridge includes the following 3rd party software:
|
||||
* [go-isatty](https://github.com/mattn/go-isatty) available under [license](https://github.com/mattn/go-isatty/blob/master/LICENSE)
|
||||
* [go-runewidth](https://github.com/mattn/go-runewidth) available under [license](https://github.com/mattn/go-runewidth/blob/master/LICENSE)
|
||||
* [go-sqlite3](https://github.com/mattn/go-sqlite3) available under [license](https://github.com/mattn/go-sqlite3/blob/master/LICENSE)
|
||||
* [go-wordwrap](https://github.com/mitchellh/go-wordwrap) available under [license](https://github.com/mitchellh/go-wordwrap/blob/master/LICENSE)
|
||||
* [concurrent](https://github.com/modern-go/concurrent) available under [license](https://github.com/modern-go/concurrent/blob/master/LICENSE)
|
||||
* [reflect2](https://github.com/modern-go/reflect2) available under [license](https://github.com/modern-go/reflect2/blob/master/LICENSE)
|
||||
* [tablewriter](https://github.com/olekukonko/tablewriter) available under [license](https://github.com/olekukonko/tablewriter/blob/master/LICENSE)
|
||||
@ -129,14 +123,13 @@ Proton Mail Bridge includes the following 3rd party software:
|
||||
* [codec](https://github.com/ugorji/go/codec) available under [license](https://github.com/ugorji/go/codec/blob/master/LICENSE)
|
||||
* [tagparser](https://github.com/vmihailenco/tagparser/v2) available under [license](https://github.com/vmihailenco/tagparser/v2/blob/master/LICENSE)
|
||||
* [smetrics](https://github.com/xrash/smetrics) available under [license](https://github.com/xrash/smetrics/blob/master/LICENSE)
|
||||
* [go-cty](https://github.com/zclconf/go-cty) available under [license](https://github.com/zclconf/go-cty/blob/master/LICENSE)
|
||||
* [arch](https://golang.org/x/arch) available under [license](https://cs.opensource.google/go/x/arch/+/master:LICENSE)
|
||||
* [crypto](https://golang.org/x/crypto) available under [license](https://cs.opensource.google/go/x/crypto/+/master:LICENSE)
|
||||
* [mod](https://golang.org/x/mod) available under [license](https://cs.opensource.google/go/x/mod/+/master:LICENSE)
|
||||
* [sync](https://golang.org/x/sync) available under [license](https://cs.opensource.google/go/x/sync/+/master:LICENSE)
|
||||
* [tools](https://golang.org/x/tools) available under [license](https://cs.opensource.google/go/x/tools/+/master:LICENSE)
|
||||
* [genproto](https://google.golang.org/genproto)
|
||||
gopkg.in/yaml.v3
|
||||
* [genproto](https://google.golang.org/genproto) available under [license](https://pkg.go.dev/google.golang.org/genproto?tab=licenses)
|
||||
* [yaml](https://gopkg.in/yaml.v3) available under [license](https://github.com/go-yaml/yaml/blob/v3.0.1/LICENSE)
|
||||
* [docker-credential-helpers](https://github.com/ProtonMail/docker-credential-helpers) available under [license](https://github.com/ProtonMail/docker-credential-helpers/blob/master/LICENSE)
|
||||
* [go-message](https://github.com/ProtonMail/go-message) available under [license](https://github.com/ProtonMail/go-message/blob/master/LICENSE)
|
||||
* [go-keychain](https://github.com/cuthix/go-keychain) available under [license](https://github.com/cuthix/go-keychain/blob/master/LICENSE)
|
||||
|
||||
207
Changelog.md
207
Changelog.md
@ -3,12 +3,217 @@
|
||||
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
|
||||
|
||||
## Trift Bridge 3.4.2
|
||||
|
||||
### Fixed
|
||||
* GODT-2902: Do not check for changed values. Related to GODT-2857.
|
||||
|
||||
|
||||
## Trift Bridge 3.4.1
|
||||
|
||||
### Fixed
|
||||
* GODT-2859: Trigger user resync while updating from 3.4.0 to 3.4.1.
|
||||
* GODT-2833: Fix migration of message flags.
|
||||
* GODT-2759: Use examine rather than select for fetching.
|
||||
|
||||
|
||||
## Trift Bridge 3.4.0
|
||||
|
||||
### Added
|
||||
|
||||
### Changed
|
||||
* Test: Add require.Eventually to TestBridge_UserAgentFromSMTPClient.
|
||||
* Test: Add smtp-send utility.
|
||||
* GODT-2759: Check for oprhan messages.
|
||||
* GODT-2759: Add prompt to download missing messages for analysis.
|
||||
* GODT-2759: CLI debug commands.
|
||||
* Remove gRPC auto-generated C++ source files.
|
||||
* Test: Force all unit test to use minimum sync spec.
|
||||
* Test: Force sync limits to minimum with env variable.
|
||||
* GODT-2691: Close logrus output file on exit.
|
||||
* GODT-2522: New Gluon database layout.
|
||||
* GODT-2678: When internet is off, do not display status dot icon for the user in the context menu.
|
||||
* GODT-2686: Change the orientation of the expand/collapse arrow for Advanced settings.
|
||||
* Test(GODT-2636): Add step for sending from EML.
|
||||
* Log failed message ids during sync.
|
||||
* GODT-2510: Remove Ent.
|
||||
* Test(GODT-2600): Changing state (read/unread, starred/unstarred) of a message in integration tests.
|
||||
* GODT-2703: Got rid of account details dialog with Apple Mail autoconf.
|
||||
* GODT-2685: Update to bug report log attachment logic.
|
||||
* GODT-2690: Update sentry reporting in GUI for new log file naming.
|
||||
* GODT-2668: Implemented new log retention policy.
|
||||
* Test(GODT-2683): Save Draft without "Date" & "From" in headers.
|
||||
* GODT-2666: Feat(GODT-2667): introduce sessionID in bridge.
|
||||
* GODT-2660: Calculate bridge coverage and refactor CI yaml file.
|
||||
* Fix dependency_license script to handle dot formated version.
|
||||
|
||||
### Fixed
|
||||
* GODT-2812: Fix rare sync deadlock.
|
||||
* GODT-2822: Better handling 429 during sync and event loop.
|
||||
* GODT-2763: Missing Answered flag on Sync and Message Create.
|
||||
* GODT-2758: Fix panic in SetFlagsOnMessages.
|
||||
* GODT-2578: Refresh literals appended to Sent folder.
|
||||
* GODT-2753: Vault test now check that value auto-assigned is first available port.
|
||||
* GODT-2522: Handle migration with unreferenced db values.
|
||||
* GODT-2693: Allow missing whitespace after header field colon.
|
||||
* GODT-2653: Only log when err is not nil.
|
||||
* GODT-2680: Fix for C++ debugger not working on ARM64 because of OpenSSL 3.1.
|
||||
* GODT-2675: Update GPA to applye togin-gonic/gin patch + update COPYING_NOTES.
|
||||
|
||||
|
||||
## Stone Bridge 3.3.2
|
||||
|
||||
### Fixed
|
||||
* GODT-2782: Filter all labels when doing perma delete check.
|
||||
|
||||
|
||||
## Stone Bridge 3.3.1
|
||||
|
||||
### Changed
|
||||
* GODT-2707: Set bridge-gui default log level to 'debug'.
|
||||
* GODT-2674: Add more logs during update failed.
|
||||
* GODT-2750: Disable raise on main window when a notification is clicked on Linux.
|
||||
* GODT-2709: Remove the config status file when user is removed.
|
||||
* GODT-2748: Log calls that cause main window to show, with reason.
|
||||
* GODT-2705: Added log entries for focus service on client and server sides.
|
||||
* GODT-2712: Feed config_status with user action while pending.
|
||||
* GODT-2728: Remove the sentry report for gRPC event stream interruptions in bridge-gui.
|
||||
* GODT-2715: Add Unitary test for configStatus event.
|
||||
* GODT-2715: Add Functional test for configStatus telemetry event.
|
||||
* Disable windows runner.
|
||||
* GODT-2714: Apply PR comments.
|
||||
* GODT-2714: Set Configuration Status to Failure and send Recovery event when issue is solved.
|
||||
* GODT-2713: Send config_progress event once a day if the configuration is stucked in pending for more than a day.
|
||||
* GODT-2711: Send config_abort event on User removal.
|
||||
* GODT-2710: Send config success on IMAP/SMTP connection..
|
||||
* GODT-2716: Make Configuration Statistics persistent.
|
||||
* GODT-2709: Init Configuration status.
|
||||
* Log errors on failed message Downloads.
|
||||
|
||||
### Fixed
|
||||
* GODT-2774: Only check telemetry availability for the current user.
|
||||
* GODT-2774: Add external context to telemetry tasks.
|
||||
* GODT-2774: Add context to Authorize in `gluon.Connector`.
|
||||
* GODT-2726: Fix Parsing of Details field in GPA error message.
|
||||
* GODT-2708: Fix dimensions event format + handling of ReportClicked event.
|
||||
* GODT-2756: Fix for 'Settings' context menu opening the 'Help' page.
|
||||
|
||||
|
||||
## Stone Bridge 3.3.0
|
||||
|
||||
### Changed
|
||||
* GODT-2653: Log API error details on Message import and send.
|
||||
* GODT-2655: Display internal build time tag in log and GUI.
|
||||
* Add error logs when messages fail to build during sync.
|
||||
* GODT-2673: Use NoClient as UserAgent without any client connected and...
|
||||
* GODT-2648: Make win build work on AWS machine.
|
||||
* Disable building of bridgepp-test app in build script.
|
||||
* GODT-2631: Bump go to 1.20.
|
||||
* GODT-2639: Enhance sentry init log.
|
||||
* GODT-2161: Auto-submit 2FA.
|
||||
* Bump Gluon for GODT-2595, GODT-2634 and GODT-2619.
|
||||
* Test: Fix TestBridge_Report.
|
||||
* Extend the timeout for integration test form 20m to 30.
|
||||
* Improve CPC code.
|
||||
* GODT-2585: Only Start IMAP/SMTP once one user is loaded.
|
||||
* GODT-2585: Server Manager.
|
||||
* GODT-2585: Add CPC utility.
|
||||
* GODT-2621: Display pop up warning when IMAP login fails because user is locked (connecting).
|
||||
* Set default log level to Debug.
|
||||
* GODT-2520: Update error message for free users.
|
||||
* Test: Disable sync open files test.
|
||||
* GODT-2346: Treat external address as disabled one.
|
||||
* GODT-2610: Re-use previous password when removing and adding back account.
|
||||
* GODT-2611: Bridge CLI exits on the first SIGINT / Ctrl+C.
|
||||
* GODT-2540: Make icon loading failure behavior consistent.
|
||||
* GODT-2540: Pop-up notification error icon is loaded on startup.
|
||||
* GODT-2540: Notify user of wrong IMAP password.
|
||||
|
||||
### Fixed
|
||||
* GODT-2683: Only validate messages that are not appended to Drafts.
|
||||
* GODT-2683: Reduce message checks when appending into Drafts.
|
||||
* Fix linter errors.
|
||||
* GODT-2669: Display sentry ID in bridge init log.
|
||||
* GODT-2672: Fix context cancelled when IMAP/SMTP parameters change is in progress.
|
||||
* GODT-2650: Fix crash during header serialization.
|
||||
* GODT-2437: Fix lint, test + bump Gluon with silenced report.
|
||||
* GODT-2437: Silence harmless report to sentry.
|
||||
* GODT-2649: Clean up cache files after failed connector create (Gluon).
|
||||
* GODT-2638: Validate messages before import.
|
||||
* GODT-2646: Bump GPA and Gluon dependecy after CIRCL upgrade.
|
||||
* GODT-2454: Only Send status update if transaction succeeded.
|
||||
* Test: fix flaky tests.
|
||||
* GODT-2628: Attempt to fix closed channel panic on logout.
|
||||
* GODT-2627: Properly handle recording of message with Bcc fields.
|
||||
* GODT-2627: Fix crash on closed channel.
|
||||
* GODT-2307: Removed deprecated macOS security framework function.
|
||||
* GODT-2637: Fix address parser error due to trailing separator.
|
||||
* GODT-2635: Ensure Bridge can be compiled with GCC 13.
|
||||
* GODT-2626: Handle rare crash due to missing address update ch.
|
||||
* GODT-2626: Server Events should not be merged.
|
||||
* GODT-2606: Improve Vault concurrency scopes.
|
||||
* GODT-2623: Log IMAP/SMTP login failure as error.
|
||||
* GODT-2527: Cleanup 503 test since handled by GPA.
|
||||
* GODT-2613: Install the TLS certificate in the user keychain.
|
||||
* GODT-2618: Crash when address does not have unlocked keyring.
|
||||
* GODT-2616: Silence out of Order UID report.
|
||||
* Update Gluon for async.Group.Do() fix.
|
||||
* Upgraded golangci-lint v1.52.2 and fixed all issues.
|
||||
* GODT-2464: Filter attachment name from content-type parameter to not send it twice to the API.
|
||||
|
||||
|
||||
## [Bridge 3.2.0] Rialto
|
||||
|
||||
### Added
|
||||
* GODT-2552, GODT-2553, GODT-2555, GODT-2556: Add telemetry.
|
||||
* GODT-2575: Add dev info to cookies.
|
||||
|
||||
### Changed
|
||||
* GODT-2598: Map Message Size Error to Gluon Error.
|
||||
* GODT-2569: Support multiple externalID matching if we send one of it when looking for parentID.
|
||||
* GODT-2576: Connector can send any flags to Gluon.
|
||||
* GODT-2496: Bump gopenPGP to 2.7.1-proton.
|
||||
* GODT-2517: Replace status window with native tray icon context menu.
|
||||
* GODT-2586: Two-columns layout for account details.
|
||||
* GODT-2580: Updated link to support website in GUI.
|
||||
* GODT-2239: Bridgepp worker/overseer unit tests.
|
||||
* GODT-2538: Implement smart picking of default IMAP/SMTP ports.
|
||||
* GODT-2502: Improve logs.
|
||||
* GODT-2551: Store and Recover Last User Agent from Vault.
|
||||
* GODT-2550: Verify IMAP ID is set properly.
|
||||
* GODT-2554: Compute telemetry availability from API UserSettings.
|
||||
* Add missing double quotes in test.
|
||||
* GODT-2239: Unit tests for BridgeUtils.cpp in bridgepp.
|
||||
* Replace go-rfc5322 with gluon's rfc5322 parser.
|
||||
* GODT-2483: Install cert without external tool on macOS.
|
||||
|
||||
### Fixed
|
||||
* GODT-2625: Update Bridge pubkey for updates.
|
||||
* GODT-2620: Avoid stalls in case of panic in gluon.
|
||||
* GODT-2615: Remove keyboard shortcut for tray icon context menu on Windows and Linux.
|
||||
* GODT-2596: Fix bug when trying to generate Sentry report and there is not log.
|
||||
* GODT-1374: Fix tray icon DPI change handling.
|
||||
* GODT-2589: Update BUILDS.md.
|
||||
* GODT-2581: Update outdated link to bridge homepage in CLI 'manual' command.
|
||||
* GODT-2337: Filter reply-to on draft.
|
||||
* GODT-2550: Announce IMAP ID Capability.
|
||||
* GODT-2574: Fix label/unlabel of large amounts of messages.
|
||||
* GODT-2573: Handle invalid header fields in message.
|
||||
* GODT-2573: Crash on null update.
|
||||
* GODT-2407: Replace invalid email addresses with emtpy for new Drafts.
|
||||
|
||||
## [Bridge 3.1.3] Quebec
|
||||
|
||||
### Changed
|
||||
* GODT-2616: Silence UID of order report.
|
||||
* GODT-2614: Handle failed update during sync.
|
||||
|
||||
|
||||
## [Bridge 3.1.2] Quebec
|
||||
|
||||
### Changed
|
||||
* GODT-2582 Dedup recovered messages folder.
|
||||
|
||||
|
||||
## [Bridge 3.1.1] Quebec
|
||||
|
||||
### Fixed
|
||||
|
||||
33
Makefile
33
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.1.2+git
|
||||
BRIDGE_APP_VERSION?=3.4.2+git
|
||||
APP_VERSION:=${BRIDGE_APP_VERSION}
|
||||
APP_FULL_NAME:=Proton Mail Bridge
|
||||
APP_VENDOR:=Proton AG
|
||||
@ -19,7 +19,8 @@ SRC_ICO:=bridge.ico
|
||||
SRC_ICNS:=Bridge.icns
|
||||
SRC_SVG:=bridge.svg
|
||||
EXE_NAME:=proton-bridge
|
||||
REVISION:=$(shell git rev-parse --short=10 HEAD)
|
||||
REVISION:=$(shell ./utils/get_revision.sh)
|
||||
TAG:=$(shell ./utils/get_revision.sh tag)
|
||||
BUILD_TIME:=$(shell date +%FT%T%z)
|
||||
MACOS_MIN_VERSION_ARM64=11.0
|
||||
MACOS_MIN_VERSION_AMD64=10.15
|
||||
@ -27,7 +28,7 @@ BUILD_ENV?=dev
|
||||
|
||||
BUILD_FLAGS:=-tags='${BUILD_TAGS}'
|
||||
BUILD_FLAGS_LAUNCHER:=${BUILD_FLAGS}
|
||||
GO_LDFLAGS:=$(addprefix -X github.com/ProtonMail/proton-bridge/v3/internal/constants., Version=${APP_VERSION} Revision=${REVISION} BuildTime=${BUILD_TIME})
|
||||
GO_LDFLAGS:=$(addprefix -X github.com/ProtonMail/proton-bridge/v3/internal/constants., Version=${APP_VERSION} Revision=${REVISION} Tag=${TAG} BuildTime=${BUILD_TIME})
|
||||
GO_LDFLAGS+=-X "github.com/ProtonMail/proton-bridge/v3/internal/constants.FullAppName=${APP_FULL_NAME}"
|
||||
|
||||
ifneq "${DSN_SENTRY}" ""
|
||||
@ -158,6 +159,7 @@ ${EXE_TARGET}: check-build-essentials ${EXE_NAME}
|
||||
BRIDGE_VENDOR="${APP_VENDOR}" \
|
||||
BRIDGE_APP_VERSION=${APP_VERSION} \
|
||||
BRIDGE_REVISION=${REVISION} \
|
||||
BRIDGE_TAG=${TAG} \
|
||||
BRIDGE_DSN_SENTRY=${DSN_SENTRY} \
|
||||
BRIDGE_BUILD_TIME=${BUILD_TIME} \
|
||||
BRIDGE_GUI_BUILD_CONFIG=Release \
|
||||
@ -183,7 +185,7 @@ ${RESOURCE_FILE}: ./dist/info.rc ./dist/${SRC_ICO} .FORCE
|
||||
|
||||
## Dev dependencies
|
||||
.PHONY: install-devel-tools install-linter install-go-mod-outdated install-git-hooks
|
||||
LINTVER:="v1.50.0"
|
||||
LINTVER:="v1.52.2"
|
||||
LINTSRC:="https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh"
|
||||
|
||||
install-dev-dependencies: install-devel-tools install-linter install-go-mod-outdated
|
||||
@ -227,14 +229,28 @@ add-license:
|
||||
change-copyright-year:
|
||||
./utils/missing_license.sh change-year
|
||||
|
||||
GOCOVERAGE=-covermode=count -coverpkg=github.com/ProtonMail/proton-bridge/v3/internal/...,github.com/ProtonMail/proton-bridge/v3/pkg/...,
|
||||
GOCOVERDIR=-args -test.gocoverdir=$$PWD/coverage
|
||||
|
||||
test: gofiles
|
||||
go test -v -timeout=10m -p=1 -count=1 -coverprofile=/tmp/coverage.out -run=${TESTRUN} ./internal/... ./pkg/...
|
||||
mkdir -p coverage/unit-${GOOS}
|
||||
go test \
|
||||
-v -timeout=20m -p=1 -count=1 \
|
||||
${GOCOVERAGE} \
|
||||
-run=${TESTRUN} ./internal/... ./pkg/... \
|
||||
${GOCOVERDIR}/unit-${GOOS}
|
||||
|
||||
test-race: gofiles
|
||||
go test -v -timeout=30m -p=1 -count=1 -race -failfast -run=${TESTRUN} ./internal/... ./pkg/...
|
||||
go test -v -timeout=40m -p=1 -count=1 -race -failfast -run=${TESTRUN} ./internal/... ./pkg/...
|
||||
|
||||
test-integration: gofiles
|
||||
go test -v -timeout=20m -p=1 -count=1 github.com/ProtonMail/proton-bridge/v3/tests
|
||||
mkdir -p coverage/integration
|
||||
go test \
|
||||
-v -timeout=60m -p=1 -count=1 \
|
||||
${GOCOVERAGE} \
|
||||
github.com/ProtonMail/proton-bridge/v3/tests \
|
||||
${GOCOVERDIR}/integration
|
||||
|
||||
|
||||
test-integration-debug: gofiles
|
||||
dlv test github.com/ProtonMail/proton-bridge/v3/tests -- -test.v -test.timeout=10m -test.parallel=1 -test.count=1
|
||||
@ -256,6 +272,9 @@ mocks:
|
||||
mockgen --package mocks github.com/ProtonMail/gluon/async PanicHandler > internal/bridge/mocks/async_mocks.go
|
||||
mockgen --package mocks github.com/ProtonMail/gluon/reporter Reporter > internal/bridge/mocks/gluon_mocks.go
|
||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/updater Downloader,Installer > internal/updater/mocks/mocks.go
|
||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/telemetry HeartbeatManager > internal/telemetry/mocks/mocks.go
|
||||
cp internal/telemetry/mocks/mocks.go internal/bridge/mocks/telemetry_mocks.go
|
||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/user MessageDownloader > internal/user/mocks/mocks.go
|
||||
|
||||
lint: gofiles lint-golang lint-license lint-dependencies lint-changelog
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
@ -47,6 +47,7 @@ const (
|
||||
appName = "Proton Mail Launcher"
|
||||
exeName = "bridge"
|
||||
guiName = "bridge-gui"
|
||||
launcherName = "launcher"
|
||||
|
||||
FlagCLI = "cli"
|
||||
FlagCLIShort = "c"
|
||||
@ -54,6 +55,7 @@ const (
|
||||
FlagNonInteractiveShort = "n"
|
||||
FlagLauncher = "--launcher"
|
||||
FlagWait = "--wait"
|
||||
FlagSessionID = "--session-id"
|
||||
)
|
||||
|
||||
func main() { //nolint:funlen
|
||||
@ -76,12 +78,26 @@ func main() { //nolint:funlen
|
||||
if err != nil {
|
||||
l.WithError(err).Fatal("Failed to get logs path")
|
||||
}
|
||||
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath))
|
||||
|
||||
if err := logging.Init(logsPath, os.Getenv("VERBOSITY")); err != nil {
|
||||
sessionID := logging.NewSessionID()
|
||||
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath, sessionID, launcherName))
|
||||
|
||||
var closer io.Closer
|
||||
if closer, err = logging.Init(
|
||||
logsPath,
|
||||
sessionID,
|
||||
logging.LauncherShortAppName,
|
||||
logging.DefaultMaxLogFileSize,
|
||||
logging.NoPruning,
|
||||
os.Getenv("VERBOSITY"),
|
||||
); err != nil {
|
||||
l.WithError(err).Fatal("Failed to setup logging")
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = logging.Close(closer)
|
||||
}()
|
||||
|
||||
updatesPath, err := locations.ProvideUpdatesPath()
|
||||
if err != nil {
|
||||
l.WithError(err).Fatal("Failed to get updates path")
|
||||
@ -108,7 +124,7 @@ func main() { //nolint:funlen
|
||||
|
||||
args := os.Args[1:]
|
||||
|
||||
exe, err := getPathToUpdatedExecutable(filepath.Base(launcher), versioner, kr, reporter)
|
||||
exe, err := getPathToUpdatedExecutable(filepath.Base(launcher), versioner, kr)
|
||||
if err != nil {
|
||||
exeToLaunch := guiName
|
||||
if inCLIMode(args) {
|
||||
@ -135,7 +151,7 @@ func main() { //nolint:funlen
|
||||
}
|
||||
}
|
||||
|
||||
cmd := execabs.Command(exe, appendLauncherPath(launcher, args)...) //nolint:gosec
|
||||
cmd := execabs.Command(exe, appendLauncherPath(launcher, append(args, FlagSessionID, string(sessionID)))...) //nolint:gosec
|
||||
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
@ -218,7 +234,6 @@ func getPathToUpdatedExecutable(
|
||||
name string,
|
||||
ver *versioner.Versioner,
|
||||
kr *crypto.KeyRing,
|
||||
reporter *sentry.Reporter,
|
||||
) (string, error) {
|
||||
versions, err := ver.ListVersions()
|
||||
if err != nil {
|
||||
@ -240,10 +255,6 @@ func getPathToUpdatedExecutable(
|
||||
if err := version.VerifyFiles(kr); err != nil {
|
||||
vlog.WithError(err).Error("Files failed verification and will be removed")
|
||||
|
||||
if err := reporter.ReportMessage(fmt.Sprintf("version %v failed verification: %v", version, err)); err != nil {
|
||||
vlog.WithError(err).Error("Failed to report corrupt update files")
|
||||
}
|
||||
|
||||
if err := version.Remove(); err != nil {
|
||||
vlog.WithError(err).Error("Failed to remove files")
|
||||
}
|
||||
|
||||
2
extern/vcpkg
vendored
2
extern/vcpkg
vendored
Submodule extern/vcpkg updated: f93ba152d5...d4d39d71b3
75
go.mod
75
go.mod
@ -1,18 +1,18 @@
|
||||
module github.com/ProtonMail/proton-bridge/v3
|
||||
|
||||
go 1.18
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
|
||||
github.com/Masterminds/semver/v3 v3.2.0
|
||||
github.com/ProtonMail/gluon v0.15.1-0.20230426114936-a356ef7f2753
|
||||
github.com/ProtonMail/gluon v0.16.1-0.20230901124123-075229a92cc4
|
||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20230406143739-c7596e170799
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.5.2
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20230727082922-9115b4750ec7
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton
|
||||
github.com/PuerkitoBio/goquery v1.8.1
|
||||
github.com/abiosoft/ishell v2.0.0+incompatible
|
||||
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37
|
||||
github.com/bradenaw/juniper v0.10.2
|
||||
github.com/bradenaw/juniper v0.12.0
|
||||
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
|
||||
@ -25,7 +25,6 @@ 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.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
|
||||
@ -37,35 +36,31 @@ require (
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pkg/profile v1.7.0
|
||||
github.com/sirupsen/logrus v1.9.0
|
||||
github.com/stretchr/testify v1.8.1
|
||||
github.com/sirupsen/logrus v1.9.2
|
||||
github.com/stretchr/testify v1.8.3
|
||||
github.com/urfave/cli/v2 v2.24.4
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.5
|
||||
go.uber.org/goleak v1.2.1
|
||||
golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb
|
||||
golang.org/x/net v0.7.0
|
||||
golang.org/x/sys v0.5.0
|
||||
golang.org/x/text v0.7.0
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
|
||||
golang.org/x/net v0.10.0
|
||||
golang.org/x/sys v0.8.0
|
||||
golang.org/x/text v0.9.0
|
||||
google.golang.org/grpc v1.53.0
|
||||
google.golang.org/protobuf v1.28.1
|
||||
google.golang.org/protobuf v1.30.0
|
||||
howett.net/plist v1.0.0
|
||||
)
|
||||
|
||||
require (
|
||||
ariga.io/atlas v0.9.1-0.20230119145809-92243f7c55cb // indirect
|
||||
entgo.io/ent v0.11.8 // indirect
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf // indirect
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect
|
||||
github.com/ProtonMail/go-mime v0.0.0-20221031134845-8fd9bc37cf08 // indirect
|
||||
github.com/ProtonMail/go-srp v0.0.5 // indirect
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903 // indirect
|
||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect
|
||||
github.com/ProtonMail/go-srp v0.0.7 // indirect
|
||||
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect
|
||||
github.com/agext/levenshtein v1.2.3 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.1 // indirect
|
||||
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
|
||||
github.com/bytedance/sonic v1.8.1 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.2 // indirect
|
||||
github.com/bytedance/sonic v1.9.1 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/chzyer/test v1.0.0 // indirect
|
||||
github.com/cloudflare/circl v1.3.2 // indirect
|
||||
github.com/cloudflare/circl v1.3.3 // 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,15 +68,16 @@ require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
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/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3 // indirect
|
||||
github.com/felixge/fgprof v0.9.3 // indirect
|
||||
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/gin-gonic/gin v1.9.0 // indirect
|
||||
github.com/go-openapi/inflect v0.19.0 // indirect
|
||||
github.com/gin-gonic/gin v1.9.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/go-playground/validator/v10 v10.14.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/gofrs/uuid v4.3.0+incompatible // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect
|
||||
@ -89,20 +85,18 @@ require (
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
||||
github.com/hashicorp/go-memdb v1.3.3 // indirect
|
||||
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
||||
github.com/hashicorp/hcl/v2 v2.16.1 // indirect
|
||||
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
||||
github.com/leodido/go-urn v1.2.1 // indirect
|
||||
github.com/leodido/go-urn v1.2.4 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.16 // indirect
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.17 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 // 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
|
||||
@ -111,21 +105,20 @@ require (
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.10 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||
github.com/zclconf/go-cty v1.12.1 // indirect
|
||||
golang.org/x/arch v0.2.0 // indirect
|
||||
golang.org/x/crypto v0.6.0 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/crypto v0.9.0 // indirect
|
||||
golang.org/x/mod v0.8.0 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/tools v0.3.1-0.20221202221704-aa9f4b2f3d57 // indirect
|
||||
golang.org/x/sync v0.2.0 // indirect
|
||||
golang.org/x/tools v0.6.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230221151758-ace64dc21148 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
replace (
|
||||
github.com/docker/docker-credential-helpers => github.com/ProtonMail/docker-credential-helpers v1.1.0
|
||||
github.com/emersion/go-message => github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753
|
||||
github.com/keybase/go-keychain => github.com/cuthix/go-keychain v0.0.0-20220405075754-31e7cee908fe
|
||||
github.com/emersion/go-message => github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7
|
||||
github.com/keybase/go-keychain => github.com/cuthix/go-keychain v0.0.0-20230517073537-fc1740a83768
|
||||
)
|
||||
|
||||
164
go.sum
164
go.sum
@ -1,5 +1,3 @@
|
||||
ariga.io/atlas v0.9.1-0.20230119145809-92243f7c55cb h1:mbsFtavDqGdYwdDpP50LGOOZ2hgyGoJcZeOpbgKMyu4=
|
||||
ariga.io/atlas v0.9.1-0.20230119145809-92243f7c55cb/go.mod h1:T230JFcENj4ZZzMkZrXFDSkv+2kXkUgpJ5FQQ5hMcKU=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
@ -13,13 +11,10 @@ cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqCl
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
entgo.io/ent v0.11.8 h1:M/M0QL1CYCUSdqGRXUrXhFYSDRJPsOOrr+RLEej/gyQ=
|
||||
entgo.io/ent v0.11.8/go.mod h1:ericBi6Q8l3wBH1wEIDfKxw7rcQEuRPyBfbIzjtxJ18=
|
||||
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557 h1:l6surSnJ3RP4qA1qmKJ+hQn3UjytosdoG27WGjrDlVs=
|
||||
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557/go.mod h1:sTrmvD/TxuypdOERsDOS7SndZg0rzzcCi1b6wQMXUYM=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
|
||||
github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
|
||||
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
@ -28,40 +23,37 @@ github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
|
||||
github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
|
||||
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
|
||||
github.com/ProtonMail/gluon v0.15.1-0.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/gluon v0.16.1-0.20230901124123-075229a92cc4 h1:Uq2v2NYEtlTaK2WTh9BMph2Kv51JxMgvTkd7CjGPYc8=
|
||||
github.com/ProtonMail/gluon v0.16.1-0.20230901124123-075229a92cc4/go.mod h1:Og5/Dz1MiGpCJn51XujZwxiLG7WzvvjE5PRpZBQmAHo=
|
||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
|
||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230124153114-0acdc8ae009b/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 h1:wPbRQzjjwFc0ih8puEVAOFGELsn1zoIIYdxvML7mDxA=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g=
|
||||
github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753 h1:I8IsYA297x0QLU80G5I6aLYUu3JYNSpo8j5fkXtFDW0=
|
||||
github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20221031134845-8fd9bc37cf08 h1:dS7r5z4iGS0qCjM7UwWdsEMzQesUQbGcXdSm2/tWboA=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20221031134845-8fd9bc37cf08/go.mod h1:qRZgbeASl2a9OwmsV85aWwRqic0NHPh+9ewGAzb4cgM=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20230406143739-c7596e170799 h1:slk4Drrkij1EVTnFOlIDyJsfjt69tnw8w2g1NMb253U=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20230406143739-c7596e170799/go.mod h1:kis4GD6FHp1ZWnenSBepldt8ai+vYalDPeey9yGwyXk=
|
||||
github.com/ProtonMail/go-srp v0.0.5 h1:xhUioxZgDbCnpo9JehyFhwwsn9JLWkUGfB0oiKXgiGg=
|
||||
github.com/ProtonMail/go-srp v0.0.5/go.mod h1:06iYHtLXW8vjLtccWj++x3MKy65sIT8yZd7nrJF49rs=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.5.2 h1:97SjlWNAxXl9P22lgwgrZRshQdiEfAht0g3ZoiA1GCw=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.5.2/go.mod h1:52qDaCnto6r+CoWbuU50T77XQt99lIs46HtHtvgFO3o=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230322105811-d73448b7e800/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903 h1:ZK3C5DtzV2nVAQTx5S5jQvMeDqWtD1By5mOoyY/xJek=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
|
||||
github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7 h1:+j+Kd/DyZ/qGfMT9htAT7HxqIEbZHsatsx+m8AoV6fc=
|
||||
github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20230727082922-9115b4750ec7 h1:Rmg3TPK6vFGNWR4hxmPoBhV75Sl716iB46wEi2U4Q+c=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20230727082922-9115b4750ec7/go.mod h1:+aTJoYu8bqzGECXL2DOdiZTZ64bGn3w0NC8VcFpJrFM=
|
||||
github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
|
||||
github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton h1:YS6M20yvjCJPR1r4ADW5TPn6rahs4iAyZaACei86bEc=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton/go.mod h1:S1lYsaGHykYpxxh2SnJL6ypcAlANKj5NRSY6HxKryKQ=
|
||||
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
|
||||
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
|
||||
github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw=
|
||||
github.com/abiosoft/ishell v2.0.0+incompatible/go.mod h1:HQR9AqF2R3P4XXpMpI0NAzgHf/aS6+zVXRj14cVk9qg=
|
||||
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db h1:CjPUSXOiYptLbTdr1RceuZgSFDQ7U15ITERUGrUORx8=
|
||||
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db/go.mod h1:rB3B4rKii8V21ydCbIzH5hZiCQE7f5E9SzUb/ZZx530=
|
||||
github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
|
||||
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37 h1:28uU3TtuvQ6KRndxg9TrC868jBWmSKgh0GTXkACCXmA=
|
||||
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/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/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
|
||||
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
@ -69,12 +61,12 @@ 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.10.2 h1:EY7r8SJJrigJ7lvWk6ews3K5RD4XTG9z+WSwHJKijP4=
|
||||
github.com/bradenaw/juniper v0.10.2/go.mod h1:Z2B7aJlQ7xbfWsnMLROj5t/5FQ94/MkIdKC30J4WvzI=
|
||||
github.com/bradenaw/juniper v0.12.0 h1:Q/7icpPQD1nH/La5DobQfNEtwyrBSiSu47jOQx7lJEM=
|
||||
github.com/bradenaw/juniper v0.12.0/go.mod h1:Z2B7aJlQ7xbfWsnMLROj5t/5FQ94/MkIdKC30J4WvzI=
|
||||
github.com/bwesterb/go-ristretto v1.2.0/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/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
||||
github.com/bytedance/sonic v1.9.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=
|
||||
@ -88,8 +80,8 @@ 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.3.2 h1:VWp8dY3yH69fdM7lM6A1+NhhVoDu9vqK0jOgmkQHFWk=
|
||||
github.com/cloudflare/circl v1.3.2/go.mod h1:+CauBF6R70Jqcyl8N2hC8pAXYbWkGIezuSbuGLtRhnw=
|
||||
github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs=
|
||||
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
||||
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=
|
||||
@ -107,8 +99,8 @@ github.com/cucumber/godog v0.12.5/go.mod h1:u6SD7IXC49dLpPN35kal0oYEjsXZWee4pW6T
|
||||
github.com/cucumber/messages-go/v16 v16.0.0/go.mod h1:EJcyR5Mm5ZuDsKJnT2N9KRnBK30BGjtYotDKpwQ0v6g=
|
||||
github.com/cucumber/messages-go/v16 v16.0.1 h1:fvkpwsLgnIm0qugftrw2YwNlio+ABe2Iu94Ap8GMYIY=
|
||||
github.com/cucumber/messages-go/v16 v16.0.1/go.mod h1:EJcyR5Mm5ZuDsKJnT2N9KRnBK30BGjtYotDKpwQ0v6g=
|
||||
github.com/cuthix/go-keychain v0.0.0-20220405075754-31e7cee908fe h1:KRj3wdvA9yE92prNmOjS7x5DOqoyjxqdE30qnrmTasc=
|
||||
github.com/cuthix/go-keychain v0.0.0-20220405075754-31e7cee908fe/go.mod h1:ZoZU1fnBy3mOLWr3Pg+Y2+nTKtu6ypDte2kZg9HvSwY=
|
||||
github.com/cuthix/go-keychain v0.0.0-20230517073537-fc1740a83768 h1:Jrcoxtrk4qpuzKIYPlEkjIK0M+bABs0oW2QzrOuwlzk=
|
||||
github.com/cuthix/go-keychain v0.0.0-20230517073537-fc1740a83768/go.mod h1:ZoZU1fnBy3mOLWr3Pg+Y2+nTKtu6ypDte2kZg9HvSwY=
|
||||
github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg=
|
||||
github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0=
|
||||
github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0=
|
||||
@ -132,8 +124,8 @@ github.com/emersion/go-smtp v0.15.1-0.20221021114529-49b17434419d h1:hFRM6zCBSc+
|
||||
github.com/emersion/go-smtp v0.15.1-0.20221021114529-49b17434419d/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
|
||||
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY=
|
||||
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
|
||||
github.com/emersion/go-vcard v0.0.0-20220507122617-d4056df0ec4a h1:cltZpe6s0SJtqK5c/5y2VrIYi8BAtDM6qjmiGYqfTik=
|
||||
github.com/emersion/go-vcard v0.0.0-20220507122617-d4056df0ec4a/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
|
||||
github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3 h1:hQ1wTMaKcGfobYRT88RM8NFNyX+IQHvagkm/tqViU98=
|
||||
github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
|
||||
github.com/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=
|
||||
@ -142,33 +134,32 @@ github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNu
|
||||
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=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||
github.com/getsentry/sentry-go v0.15.0 h1:CP9bmA7pralrVUedYZsmIHWpq/pBtXTSew7xvVpfLaA=
|
||||
github.com/getsentry/sentry-go v0.15.0/go.mod h1:RZPJKSw+adu8PBNygiri/A98FqVr2HtRckJk9XVxJ9I=
|
||||
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.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8=
|
||||
github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k=
|
||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||
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=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4=
|
||||
github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
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-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
|
||||
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
|
||||
github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
|
||||
github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA=
|
||||
github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4=
|
||||
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=
|
||||
@ -243,8 +234,6 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
|
||||
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
|
||||
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/hcl/v2 v2.16.1 h1:BwuxEMD/tsYgbhIW7UuI3crjovf3MzuFWiVgiv57iHg=
|
||||
github.com/hashicorp/hcl/v2 v2.16.1/go.mod h1:JRmR89jycNkrrqnMmvPDMd56n1rQJ2Q6KocSLCMCXng=
|
||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
||||
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
||||
@ -277,9 +266,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4=
|
||||
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
|
||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
@ -289,13 +277,13 @@ github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNx
|
||||
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/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-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
||||
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
||||
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
|
||||
@ -304,8 +292,6 @@ github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceT
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
|
||||
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
|
||||
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
@ -324,8 +310,8 @@ github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FI
|
||||
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.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
|
||||
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
||||
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=
|
||||
@ -354,17 +340,16 @@ 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.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
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=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
|
||||
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y=
|
||||
github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||
@ -393,14 +378,16 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
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/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/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
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/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/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/ugorji/go/codec v1.2.11/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=
|
||||
@ -412,8 +399,6 @@ github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRT
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/zclconf/go-cty v1.12.1 h1:PcupnljUm9EIvbgSHQnHhUr3fO6oFmkOrvs2BAFNXXY=
|
||||
github.com/zclconf/go-cty v1.12.1/go.mod h1:s9IfD1LK5ccNMSWCVFCE2rJfHiZgi7JijgeWIMfhLvA=
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
@ -423,26 +408,25 @@ 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/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||
golang.org/x/arch v0.3.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=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
|
||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
|
||||
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb h1:PaBZQdo+iSDyHT053FjUCgZQ/9uqVwPOcl7KSWhKn6w=
|
||||
golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
||||
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=
|
||||
@ -454,7 +438,6 @@ golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHl
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mobile v0.0.0-20221110043201-43a038452099/go.mod h1:aAjjkJNdrh3PMckS4B10TGS2nag27cbKR1y2BpUxsiY=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
@ -481,8 +464,12 @@ golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qx
|
||||
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@ -493,9 +480,9 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
|
||||
golang.org/x/sync v0.2.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=
|
||||
@ -525,11 +512,16 @@ golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
@ -537,8 +529,10 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@ -562,8 +556,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.3.1-0.20221202221704-aa9f4b2f3d57 h1:/X0t/E4VxbZE7MLS7auvE7YICHeVvbIa9vkOVvYW/24=
|
||||
golang.org/x/tools v0.3.1-0.20221202221704-aa9f4b2f3d57/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
|
||||
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@ -595,8 +589,8 @@ 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=
|
||||
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
@ -19,13 +19,13 @@ package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
@ -35,6 +35,7 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/crash"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/focus"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/frontend/theme"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/sentry"
|
||||
@ -76,10 +77,12 @@ const (
|
||||
flagNoWindow = "no-window"
|
||||
flagParentPID = "parent-pid"
|
||||
flagSoftwareRenderer = "software-renderer"
|
||||
flagSessionID = "session-id"
|
||||
)
|
||||
|
||||
const (
|
||||
appUsage = "Proton Mail IMAP and SMTP Bridge"
|
||||
appShortName = "bridge"
|
||||
)
|
||||
|
||||
func New() *cli.App {
|
||||
@ -150,6 +153,10 @@ func New() *cli.App {
|
||||
Hidden: true,
|
||||
Value: false,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flagSessionID,
|
||||
Hidden: true,
|
||||
},
|
||||
}
|
||||
|
||||
app.Action = run
|
||||
@ -158,9 +165,6 @@ func New() *cli.App {
|
||||
}
|
||||
|
||||
func run(c *cli.Context) error {
|
||||
// Seed the default RNG from the math/rand package.
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
|
||||
// Get the current bridge version.
|
||||
version, err := semver.NewVersion(constants.Version)
|
||||
if err != nil {
|
||||
@ -186,6 +190,11 @@ func run(c *cli.Context) error {
|
||||
exe = os.Args[0]
|
||||
}
|
||||
|
||||
var logCloser io.Closer
|
||||
defer func() {
|
||||
_ = logging.Close(logCloser)
|
||||
}()
|
||||
|
||||
// Restart the app if requested.
|
||||
return withRestarter(exe, func(restarter *restarter.Restarter) error {
|
||||
// Handle crashes with various actions.
|
||||
@ -202,7 +211,9 @@ func run(c *cli.Context) error {
|
||||
}
|
||||
|
||||
// Initialize logging.
|
||||
return withLogging(c, crashHandler, locations, func() error {
|
||||
return withLogging(c, crashHandler, locations, func(closer io.Closer) error {
|
||||
logCloser = closer
|
||||
|
||||
// If there was an error during migration, log it now.
|
||||
if migrationErr != nil {
|
||||
logrus.WithError(migrationErr).Error("Failed to migrate old app data")
|
||||
@ -217,16 +228,6 @@ func run(c *cli.Context) error {
|
||||
return withSingleInstance(settings, locations.GetLockFile(), version, func() error {
|
||||
// Unlock the encrypted vault.
|
||||
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{}{})
|
||||
}
|
||||
|
||||
// Report corrupt vault.
|
||||
if corrupt {
|
||||
_ = reporter.ReportMessageWithContext("Vault is corrupt", map[string]interface{}{})
|
||||
}
|
||||
|
||||
if !v.Migrated() {
|
||||
// Migrate old settings into the vault.
|
||||
if err := migrateOldSettings(v); err != nil {
|
||||
@ -244,6 +245,15 @@ func run(c *cli.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"lastVersion": v.GetLastVersion().String(),
|
||||
"showAllMail": v.GetShowAllMail(),
|
||||
"updateCh": v.GetUpdateChannel(),
|
||||
"autoUpdate": v.GetAutoUpdate(),
|
||||
"rollout": v.GetUpdateRollout(),
|
||||
"DoH": v.GetProxyAllowed(),
|
||||
}).Info("Vault loaded")
|
||||
|
||||
// Load the cookies from the vault.
|
||||
return withCookieJar(v, func(cookieJar http.CookieJar) error {
|
||||
// Create a new bridge instance.
|
||||
@ -258,6 +268,9 @@ func run(c *cli.Context) error {
|
||||
b.PushError(bridge.ErrVaultCorrupt)
|
||||
}
|
||||
|
||||
// Start telemetry heartbeat process
|
||||
b.StartHeartbeat(b)
|
||||
|
||||
// Run the frontend.
|
||||
return runFrontend(c, crashHandler, restarter, locations, b, eventCh, quitCh, c.Int(flagParentPID))
|
||||
})
|
||||
@ -299,7 +312,7 @@ func withSingleInstance(settingPath, lockFile string, version *semver.Version, f
|
||||
}
|
||||
|
||||
// Initialize our logging system.
|
||||
func withLogging(c *cli.Context, crashHandler *crash.Handler, locations *locations.Locations, fn func() error) error {
|
||||
func withLogging(c *cli.Context, crashHandler *crash.Handler, locations *locations.Locations, fn func(closer io.Closer) error) error {
|
||||
logrus.Debug("Initializing logging")
|
||||
defer logrus.Debug("Logging stopped")
|
||||
|
||||
@ -312,23 +325,34 @@ func withLogging(c *cli.Context, crashHandler *crash.Handler, locations *locatio
|
||||
logrus.WithField("path", logsPath).Debug("Received logs path")
|
||||
|
||||
// Initialize logging.
|
||||
if err := logging.Init(logsPath, c.String(flagLogLevel)); err != nil {
|
||||
sessionID := logging.NewSessionIDFromString(c.String(flagSessionID))
|
||||
var closer io.Closer
|
||||
if closer, err = logging.Init(
|
||||
logsPath,
|
||||
sessionID,
|
||||
logging.BridgeShortAppName,
|
||||
logging.DefaultMaxLogFileSize,
|
||||
logging.DefaultPruningSize,
|
||||
c.String(flagLogLevel),
|
||||
); err != nil {
|
||||
return fmt.Errorf("could not initialize logging: %w", err)
|
||||
}
|
||||
|
||||
// Ensure we dump a stack trace if we crash.
|
||||
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath))
|
||||
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath, sessionID, appShortName))
|
||||
|
||||
logrus.
|
||||
WithField("appName", constants.FullAppName).
|
||||
WithField("version", constants.Version).
|
||||
WithField("revision", constants.Revision).
|
||||
WithField("tag", constants.Tag).
|
||||
WithField("build", constants.BuildTime).
|
||||
WithField("runtime", runtime.GOOS).
|
||||
WithField("args", os.Args).
|
||||
WithField("SentryID", sentry.GetProtectedHostname()).
|
||||
Info("Run app")
|
||||
|
||||
return fn()
|
||||
return fn(closer)
|
||||
}
|
||||
|
||||
// WithLocations provides access to locations where we store our files.
|
||||
@ -417,6 +441,10 @@ func withCookieJar(vault *vault.Vault, fn func(http.CookieJar) error) error {
|
||||
return fmt.Errorf("could not create cookie jar: %w", err)
|
||||
}
|
||||
|
||||
if err := setDeviceCookies(persister); err != nil {
|
||||
return fmt.Errorf("could not set device cookies: %w", err)
|
||||
}
|
||||
|
||||
// Persist the cookies to the vault when we close.
|
||||
defer func() {
|
||||
logrus.Debug("Persisting cookies")
|
||||
@ -428,3 +456,21 @@ func withCookieJar(vault *vault.Vault, fn func(http.CookieJar) error) error {
|
||||
|
||||
return fn(persister)
|
||||
}
|
||||
|
||||
func setDeviceCookies(jar *cookies.Jar) error {
|
||||
url, err := url.Parse(constants.APIHost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for name, value := range map[string]string{
|
||||
"hhn": sentry.GetProtectedHostname(),
|
||||
"tz": sentry.GetTimeZone(),
|
||||
"lng": sentry.GetSystemLang(),
|
||||
"clr": string(theme.DefaultTheme()),
|
||||
} {
|
||||
jar.SetCookies(url, []*http.Cookie{{Name: name, Value: value, Secure: true}})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -44,7 +44,7 @@ import (
|
||||
// deleteOldGoIMAPFiles Set with `-ldflags -X app.deleteOldGoIMAPFiles=true` to enable cleanup of old imap cache data.
|
||||
var deleteOldGoIMAPFiles bool //nolint:gochecknoglobals
|
||||
|
||||
// withBridge creates creates and tears down the bridge.
|
||||
// withBridge creates and tears down the bridge.
|
||||
func withBridge(
|
||||
c *cli.Context,
|
||||
exe string,
|
||||
|
||||
@ -20,6 +20,7 @@
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
@ -36,6 +37,14 @@ func newAPIOptions(
|
||||
transport http.RoundTripper,
|
||||
panicHandler async.PanicHandler,
|
||||
) []proton.Option {
|
||||
|
||||
if allow := os.Getenv("BRIDGE_ALLOW_PROXY"); allow != "" {
|
||||
transport = &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
}
|
||||
|
||||
opt := defaultAPIOptions(apiURL, version, cookieJar, transport, panicHandler)
|
||||
|
||||
if host := os.Getenv("BRIDGE_API_HOST"); host != "" {
|
||||
|
||||
@ -29,7 +29,6 @@ import (
|
||||
"time"
|
||||
|
||||
"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"
|
||||
@ -41,10 +40,10 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/focus"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/sentry"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/telemetry"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
"github.com/bradenaw/juniper/xslices"
|
||||
"github.com/emersion/go-smtp"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
@ -66,18 +65,15 @@ type Bridge struct {
|
||||
tlsConfig *tls.Config
|
||||
|
||||
// imapServer is the bridge's IMAP server.
|
||||
imapServer *gluon.Server
|
||||
imapListener net.Listener
|
||||
imapEventCh chan imapEvents.Event
|
||||
|
||||
// smtpServer is the bridge's SMTP server.
|
||||
smtpServer *smtp.Server
|
||||
smtpListener net.Listener
|
||||
|
||||
// updater is the bridge's updater.
|
||||
updater Updater
|
||||
installCh chan installJob
|
||||
|
||||
// heartbeat is the telemetry heartbeat for metrics.
|
||||
heartbeat telemetry.Heartbeat
|
||||
|
||||
// curVersion is the current version of the bridge,
|
||||
// newVersion is the version that was installed by the updater.
|
||||
curVersion *semver.Version
|
||||
@ -126,7 +122,12 @@ type Bridge struct {
|
||||
// goUpdate triggers a check/install of updates.
|
||||
goUpdate func()
|
||||
|
||||
// goHeartbeat triggers a check/sending if heartbeat is needed.
|
||||
goHeartbeat func()
|
||||
|
||||
uidValidityGenerator imap.UIDValidityGenerator
|
||||
|
||||
serverManager *ServerManager
|
||||
}
|
||||
|
||||
// New creates a new bridge.
|
||||
@ -217,16 +218,6 @@ func newBridge(
|
||||
return nil, fmt.Errorf("failed to load TLS config: %w", err)
|
||||
}
|
||||
|
||||
gluonCacheDir, err := getGluonDir(vault)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get Gluon directory: %w", err)
|
||||
}
|
||||
|
||||
gluonDataDir, err := locator.ProvideGluonDataPath()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get Gluon Database directory: %w", err)
|
||||
}
|
||||
|
||||
firstStart := vault.GetFirstStart()
|
||||
if err := vault.SetFirstStart(false); err != nil {
|
||||
return nil, fmt.Errorf("failed to save first start indicator: %w", err)
|
||||
@ -237,22 +228,7 @@ func newBridge(
|
||||
return nil, fmt.Errorf("failed to save last version indicator: %w", err)
|
||||
}
|
||||
|
||||
imapServer, err := newIMAPServer(
|
||||
gluonCacheDir,
|
||||
gluonDataDir,
|
||||
curVersion,
|
||||
tlsConfig,
|
||||
reporter,
|
||||
logIMAPClient,
|
||||
logIMAPServer,
|
||||
imapEventCh,
|
||||
tasks,
|
||||
uidValidityGenerator,
|
||||
panicHandler,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create IMAP server: %w", err)
|
||||
}
|
||||
identifier.SetClientString(vault.GetLastUserAgent())
|
||||
|
||||
focusService, err := focus.NewService(locator, curVersion, panicHandler)
|
||||
if err != nil {
|
||||
@ -270,7 +246,6 @@ func newBridge(
|
||||
identifier: identifier,
|
||||
|
||||
tlsConfig: tlsConfig,
|
||||
imapServer: imapServer,
|
||||
imapEventCh: imapEventCh,
|
||||
|
||||
updater: updater,
|
||||
@ -297,9 +272,13 @@ func newBridge(
|
||||
tasks: tasks,
|
||||
|
||||
uidValidityGenerator: uidValidityGenerator,
|
||||
|
||||
serverManager: newServerManager(),
|
||||
}
|
||||
|
||||
bridge.smtpServer = newSMTPServer(bridge, tlsConfig, logSMTP)
|
||||
if err := bridge.serverManager.Init(bridge); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return bridge, nil
|
||||
}
|
||||
@ -372,10 +351,6 @@ func (bridge *Bridge) init(tlsReporter TLSReporter) error {
|
||||
})
|
||||
})
|
||||
|
||||
// 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 {
|
||||
@ -387,17 +362,6 @@ func (bridge *Bridge) init(tlsReporter TLSReporter) error {
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
@ -443,18 +407,13 @@ func (bridge *Bridge) GetErrors() []error {
|
||||
func (bridge *Bridge) Close(ctx context.Context) {
|
||||
logrus.Info("Closing bridge")
|
||||
|
||||
// Close the IMAP server.
|
||||
if err := bridge.closeIMAP(ctx); err != nil {
|
||||
logrus.WithError(err).Error("Failed to close IMAP server")
|
||||
}
|
||||
|
||||
// Close the SMTP server.
|
||||
if err := bridge.closeSMTP(); err != nil {
|
||||
logrus.WithError(err).Error("Failed to close SMTP server")
|
||||
// Close the servers
|
||||
if err := bridge.serverManager.CloseServers(ctx); err != nil {
|
||||
logrus.WithError(err).Error("Failed to close servers")
|
||||
}
|
||||
|
||||
// Close all users.
|
||||
safe.RLock(func() {
|
||||
safe.Lock(func() {
|
||||
for _, user := range bridge.users {
|
||||
user.Close()
|
||||
}
|
||||
|
||||
@ -25,6 +25,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
@ -49,7 +50,9 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
"github.com/ProtonMail/proton-bridge/v3/tests"
|
||||
"github.com/bradenaw/juniper/xslices"
|
||||
"github.com/emersion/go-imap/client"
|
||||
imapid "github.com/emersion/go-imap-id"
|
||||
"github.com/emersion/go-sasl"
|
||||
"github.com/emersion/go-smtp"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@ -170,6 +173,210 @@ func TestBridge_UserAgent(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_UserAgent_Persistence(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||
otherPassword := []byte("bar")
|
||||
otherUser := "foo"
|
||||
_, _, err := s.CreateUser(otherUser, otherPassword)
|
||||
require.NoError(t, err)
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
imapWaiter := waitForIMAPServerReady(b)
|
||||
defer imapWaiter.Done()
|
||||
|
||||
smtpWaiter := waitForSMTPServerReady(b)
|
||||
defer smtpWaiter.Done()
|
||||
|
||||
currentUserAgent := b.GetCurrentUserAgent()
|
||||
require.Contains(t, currentUserAgent, useragent.DefaultUserAgent)
|
||||
|
||||
require.NoError(t, getErr(b.LoginFull(ctx, otherUser, otherPassword, nil, nil)))
|
||||
|
||||
imapWaiter.Wait()
|
||||
smtpWaiter.Wait()
|
||||
|
||||
imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = imapClient.Logout() }()
|
||||
|
||||
idClient := imapid.NewClient(imapClient)
|
||||
|
||||
// Set IMAP ID before Login to have the value capture in the Login API Call.
|
||||
_, err = idClient.ID(imapid.ID{
|
||||
imapid.FieldName: "MyFancyClient",
|
||||
imapid.FieldVersion: "0.1.2",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
// Login the user.
|
||||
_, err = b.LoginFull(context.Background(), username, password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Assert that the user agent then contains the platform.
|
||||
require.Contains(t, b.GetCurrentUserAgent(), "MyFancyClient/0.1.2")
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
currentUserAgent := bridge.GetCurrentUserAgent()
|
||||
require.Contains(t, currentUserAgent, "MyFancyClient/0.1.2")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_UserAgentFromUnknownClient(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||
otherPassword := []byte("bar")
|
||||
otherUser := "foo"
|
||||
_, _, err := s.CreateUser(otherUser, otherPassword)
|
||||
require.NoError(t, err)
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
imapWaiter := waitForIMAPServerReady(b)
|
||||
defer imapWaiter.Done()
|
||||
|
||||
smtpWaiter := waitForSMTPServerReady(b)
|
||||
defer smtpWaiter.Done()
|
||||
|
||||
currentUserAgent := b.GetCurrentUserAgent()
|
||||
require.Contains(t, currentUserAgent, useragent.DefaultUserAgent)
|
||||
|
||||
userID, err := b.LoginFull(context.Background(), username, password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
imapWaiter.Wait()
|
||||
smtpWaiter.Wait()
|
||||
|
||||
imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = imapClient.Logout() }()
|
||||
|
||||
info, err := b.GetUserInfo(userID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, info.State == bridge.Connected)
|
||||
|
||||
require.NoError(t, imapClient.Login(info.Addresses[0], string(info.BridgePass)))
|
||||
|
||||
currentUserAgent = b.GetCurrentUserAgent()
|
||||
require.Contains(t, currentUserAgent, "UnknownClient/0.0.1")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_UserAgentFromSMTPClient(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||
otherPassword := []byte("bar")
|
||||
otherUser := "foo"
|
||||
_, _, err := s.CreateUser(otherUser, otherPassword)
|
||||
require.NoError(t, err)
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
imapWaiter := waitForIMAPServerReady(b)
|
||||
defer imapWaiter.Done()
|
||||
|
||||
smtpWaiter := waitForSMTPServerReady(b)
|
||||
defer smtpWaiter.Done()
|
||||
|
||||
currentUserAgent := b.GetCurrentUserAgent()
|
||||
require.Contains(t, currentUserAgent, useragent.DefaultUserAgent)
|
||||
|
||||
userID, err := b.LoginFull(context.Background(), username, password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
imapWaiter.Wait()
|
||||
smtpWaiter.Wait()
|
||||
|
||||
client, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(b.GetSMTPPort())))
|
||||
require.NoError(t, err)
|
||||
defer client.Close() //nolint:errcheck
|
||||
|
||||
info, err := b.GetUserInfo(userID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, info.State == bridge.Connected)
|
||||
|
||||
// Upgrade to TLS.
|
||||
require.NoError(t, client.StartTLS(&tls.Config{InsecureSkipVerify: true}))
|
||||
require.NoError(t, client.Auth(sasl.NewLoginClient(
|
||||
info.Addresses[0],
|
||||
string(info.BridgePass)),
|
||||
))
|
||||
|
||||
require.Eventually(t, func() bool {
|
||||
currentUserAgent = b.GetCurrentUserAgent()
|
||||
|
||||
return strings.Contains(currentUserAgent, "UnknownClient/0.0.1")
|
||||
}, time.Minute, 5*time.Second)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_UserAgentFromIMAPID(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||
var (
|
||||
calls []server.Call
|
||||
lock sync.Mutex
|
||||
)
|
||||
|
||||
s.AddCallWatcher(func(call server.Call) {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
calls = append(calls, call)
|
||||
})
|
||||
|
||||
otherPassword := []byte("bar")
|
||||
otherUser := "foo"
|
||||
_, _, err := s.CreateUser(otherUser, otherPassword)
|
||||
require.NoError(t, err)
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
imapWaiter := waitForIMAPServerReady(b)
|
||||
defer imapWaiter.Done()
|
||||
|
||||
smtpWaiter := waitForSMTPServerReady(b)
|
||||
defer smtpWaiter.Done()
|
||||
|
||||
require.NoError(t, getErr(b.LoginFull(ctx, otherUser, otherPassword, nil, nil)))
|
||||
|
||||
imapWaiter.Wait()
|
||||
smtpWaiter.Wait()
|
||||
|
||||
imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = imapClient.Logout() }()
|
||||
|
||||
idClient := imapid.NewClient(imapClient)
|
||||
|
||||
// Set IMAP ID before Login to have the value capture in the Login API Call.
|
||||
_, err = idClient.ID(imapid.ID{
|
||||
imapid.FieldName: "MyFancyClient",
|
||||
imapid.FieldVersion: "0.1.2",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
// Login the user.
|
||||
userID, err := b.LoginFull(context.Background(), username, password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
info, err := b.GetUserInfo(userID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, info.State == bridge.Connected)
|
||||
|
||||
require.NoError(t, imapClient.Login(info.Addresses[0], string(info.BridgePass)))
|
||||
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
userAgent := calls[len(calls)-1].RequestHeader.Get("User-Agent")
|
||||
|
||||
// Assert that the user agent was sent to the API.
|
||||
require.Contains(t, userAgent, b.GetCurrentUserAgent())
|
||||
require.Contains(t, userAgent, "MyFancyClient/0.1.2")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_Cookies(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||
var (
|
||||
@ -505,10 +712,22 @@ 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) {
|
||||
imapWaiter := waitForIMAPServerReady(bridge)
|
||||
defer imapWaiter.Done()
|
||||
|
||||
smtpWaiter := waitForSMTPServerReady(bridge)
|
||||
defer smtpWaiter.Done()
|
||||
|
||||
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())))
|
||||
_, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
imapWaiter.Wait()
|
||||
smtpWaiter.Wait()
|
||||
|
||||
imapClient, err := eventuallyDial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Error(t, imapClient.Login("badUser", "badPass"))
|
||||
@ -535,6 +754,12 @@ func TestBridge_ChangeCacheDirectory(t *testing.T) {
|
||||
configDir, err := b.GetGluonDataDir()
|
||||
require.NoError(t, err)
|
||||
|
||||
imapWaiter := waitForIMAPServerReady(b)
|
||||
defer imapWaiter.Done()
|
||||
|
||||
smtpWaiter := waitForSMTPServerReady(b)
|
||||
defer smtpWaiter.Done()
|
||||
|
||||
// Login the user.
|
||||
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
|
||||
defer done()
|
||||
@ -568,7 +793,10 @@ func TestBridge_ChangeCacheDirectory(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.True(t, info.State == bridge.Connected)
|
||||
|
||||
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||
imapWaiter.Wait()
|
||||
smtpWaiter.Wait()
|
||||
|
||||
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
||||
defer func() { _ = client.Logout() }()
|
||||
@ -608,7 +836,7 @@ func TestBridge_ChangeAddressOrder(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.True(t, info.State == bridge.Connected)
|
||||
|
||||
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
||||
defer func() { _ = client.Logout() }()
|
||||
@ -629,7 +857,7 @@ func TestBridge_ChangeAddressOrder(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.True(t, info.State == bridge.Connected)
|
||||
|
||||
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
||||
defer func() { _ = client.Logout() }()
|
||||
@ -691,6 +919,7 @@ func withBridgeNoMocks(
|
||||
locator bridge.Locator,
|
||||
vaultKey []byte,
|
||||
tests func(*bridge.Bridge),
|
||||
waitOnServers bool,
|
||||
) {
|
||||
// Bridge will disable the proxy by default at startup.
|
||||
mocks.ProxyCtl.EXPECT().DisallowProxy()
|
||||
@ -736,16 +965,22 @@ func withBridgeNoMocks(
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, bridge.GetErrors())
|
||||
|
||||
// Start the Heartbeat process.
|
||||
bridge.StartHeartbeat(mocks.Heartbeat)
|
||||
|
||||
// Wait for bridge to finish loading users.
|
||||
waitForEvent(t, eventCh, events.AllUsersLoaded{})
|
||||
|
||||
// Set random IMAP and SMTP ports for the tests.
|
||||
require.NoError(t, bridge.SetIMAPPort(ctx, 0))
|
||||
require.NoError(t, bridge.SetSMTPPort(ctx, 0))
|
||||
|
||||
if waitOnServers {
|
||||
// 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))
|
||||
require.NoError(t, bridge.SetSMTPPort(0))
|
||||
}
|
||||
|
||||
// Close the bridge when done.
|
||||
defer bridge.Close(ctx)
|
||||
@ -767,7 +1002,24 @@ func withBridge(
|
||||
withMocks(t, func(mocks *bridge.Mocks) {
|
||||
withBridgeNoMocks(ctx, t, mocks, apiURL, netCtl, locator, vaultKey, func(bridge *bridge.Bridge) {
|
||||
tests(bridge, mocks)
|
||||
}, false)
|
||||
})
|
||||
}
|
||||
|
||||
// withBridgeWaitForServers is the same as withBridge, but it will wait until IMAP & SMTP servers are ready.
|
||||
func withBridgeWaitForServers(
|
||||
ctx context.Context,
|
||||
t *testing.T,
|
||||
apiURL string,
|
||||
netCtl *proton.NetCtl,
|
||||
locator bridge.Locator,
|
||||
vaultKey []byte,
|
||||
tests func(*bridge.Bridge, *bridge.Mocks),
|
||||
) {
|
||||
withMocks(t, func(mocks *bridge.Mocks) {
|
||||
withBridgeNoMocks(ctx, t, mocks, apiURL, netCtl, locator, vaultKey, func(bridge *bridge.Bridge) {
|
||||
tests(bridge, mocks)
|
||||
}, true)
|
||||
})
|
||||
}
|
||||
|
||||
@ -820,3 +1072,48 @@ func chToType[In, Out any](inCh <-chan In, done func()) (<-chan Out, func()) {
|
||||
|
||||
return outCh, done
|
||||
}
|
||||
|
||||
type eventWaiter struct {
|
||||
evtCh <-chan events.Event
|
||||
cancel func()
|
||||
}
|
||||
|
||||
func (e *eventWaiter) Done() {
|
||||
e.cancel()
|
||||
}
|
||||
|
||||
func (e *eventWaiter) Wait() {
|
||||
<-e.evtCh
|
||||
}
|
||||
|
||||
func waitForSMTPServerReady(b *bridge.Bridge) *eventWaiter {
|
||||
evtCh, cancel := b.GetEvents(events.SMTPServerReady{})
|
||||
return &eventWaiter{
|
||||
evtCh: evtCh,
|
||||
cancel: cancel,
|
||||
}
|
||||
}
|
||||
|
||||
func waitForSMTPServerStopped(b *bridge.Bridge) *eventWaiter {
|
||||
evtCh, cancel := b.GetEvents(events.SMTPServerStopped{})
|
||||
return &eventWaiter{
|
||||
evtCh: evtCh,
|
||||
cancel: cancel,
|
||||
}
|
||||
}
|
||||
|
||||
func waitForIMAPServerReady(b *bridge.Bridge) *eventWaiter {
|
||||
evtCh, cancel := b.GetEvents(events.IMAPServerReady{})
|
||||
return &eventWaiter{
|
||||
evtCh: evtCh,
|
||||
cancel: cancel,
|
||||
}
|
||||
}
|
||||
|
||||
func waitForIMAPServerStopped(b *bridge.Bridge) *eventWaiter {
|
||||
evtCh, cancel := b.GetEvents(events.IMAPServerStopped{})
|
||||
return &eventWaiter{
|
||||
evtCh: evtCh,
|
||||
cancel: cancel,
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,23 +18,19 @@
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
)
|
||||
|
||||
const (
|
||||
MaxTotalAttachmentSize = 7 * (1 << 20)
|
||||
MaxCompressedFilesCount = 6
|
||||
DefaultMaxBugReportZipSize = 7 * 1024 * 1024
|
||||
DefaultMaxSessionCountForBugReport = 10
|
||||
)
|
||||
|
||||
func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, description, username, email, client string, attachLogs bool) error {
|
||||
@ -50,54 +46,25 @@ func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, descript
|
||||
}
|
||||
}
|
||||
|
||||
var atts []proton.ReportBugAttachment
|
||||
var attachment []proton.ReportBugAttachment
|
||||
|
||||
if attachLogs {
|
||||
logs, err := getMatchingLogs(bridge.locator, func(filename string) bool {
|
||||
return logging.MatchLogName(filename) && !logging.MatchStackTraceName(filename)
|
||||
})
|
||||
logsPath, err := bridge.locator.ProvideLogsPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
crashes, err := getMatchingLogs(bridge.locator, func(filename string) bool {
|
||||
return logging.MatchLogName(filename) && logging.MatchStackTraceName(filename)
|
||||
})
|
||||
buffer, err := logging.ZipLogsForBugReport(logsPath, DefaultMaxSessionCountForBugReport, DefaultMaxBugReportZipSize)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
guiLogs, err := getMatchingLogs(bridge.locator, func(filename string) bool {
|
||||
return logging.MatchGUILogName(filename) && !logging.MatchStackTraceName(filename)
|
||||
})
|
||||
body, err := io.ReadAll(buffer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var matchFiles []string
|
||||
|
||||
// Include bridge logs, up to a maximum amount.
|
||||
matchFiles = append(matchFiles, logs[max(0, len(logs)-(MaxCompressedFilesCount/2)):]...)
|
||||
|
||||
// Include crash logs, up to a maximum amount.
|
||||
matchFiles = append(matchFiles, crashes[max(0, len(crashes)-(MaxCompressedFilesCount/2)):]...)
|
||||
|
||||
// bridge-gui keeps just one small (~ 1kb) log file; we always include it.
|
||||
if len(guiLogs) > 0 {
|
||||
matchFiles = append(matchFiles, guiLogs[len(guiLogs)-1])
|
||||
}
|
||||
|
||||
archive, err := zipFiles(matchFiles)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(archive)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
atts = append(atts, proton.ReportBugAttachment{
|
||||
attachment = append(attachment, proton.ReportBugAttachment{
|
||||
Name: "logs.zip",
|
||||
Filename: "logs.zip",
|
||||
MIMEType: "application/zip",
|
||||
@ -105,6 +72,12 @@ func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, descript
|
||||
})
|
||||
}
|
||||
|
||||
safe.Lock(func() {
|
||||
for _, user := range bridge.users {
|
||||
user.ReportBugSent()
|
||||
}
|
||||
}, bridge.usersLock)
|
||||
|
||||
return bridge.api.ReportBug(ctx, proton.ReportBugReq{
|
||||
OS: osType,
|
||||
OSVersion: osVersion,
|
||||
@ -118,116 +91,5 @@ func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, descript
|
||||
|
||||
Username: account,
|
||||
Email: email,
|
||||
}, atts...)
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func getMatchingLogs(locator Locator, filenameMatchFunc func(string) bool) (filenames []string, err error) {
|
||||
logsPath, err := locator.ProvideLogsPath()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
files, err := os.ReadDir(logsPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var matchFiles []string
|
||||
|
||||
for _, file := range files {
|
||||
if filenameMatchFunc(file.Name()) {
|
||||
matchFiles = append(matchFiles, filepath.Join(logsPath, file.Name()))
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(matchFiles) // Sorted by timestamp: oldest first.
|
||||
|
||||
return matchFiles, nil
|
||||
}
|
||||
|
||||
type limitedBuffer struct {
|
||||
capacity int
|
||||
buf *bytes.Buffer
|
||||
}
|
||||
|
||||
func newLimitedBuffer(capacity int) *limitedBuffer {
|
||||
return &limitedBuffer{
|
||||
capacity: capacity,
|
||||
buf: bytes.NewBuffer(make([]byte, 0, capacity)),
|
||||
}
|
||||
}
|
||||
|
||||
func (b *limitedBuffer) Write(p []byte) (n int, err error) {
|
||||
if len(p)+b.buf.Len() > b.capacity {
|
||||
return 0, ErrSizeTooLarge
|
||||
}
|
||||
|
||||
return b.buf.Write(p)
|
||||
}
|
||||
|
||||
func (b *limitedBuffer) Read(p []byte) (n int, err error) {
|
||||
return b.buf.Read(p)
|
||||
}
|
||||
|
||||
func zipFiles(filenames []string) (io.Reader, error) {
|
||||
if len(filenames) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
buf := newLimitedBuffer(MaxTotalAttachmentSize)
|
||||
|
||||
w := zip.NewWriter(buf)
|
||||
defer w.Close() //nolint:errcheck
|
||||
|
||||
for _, file := range filenames {
|
||||
if err := addFileToZip(file, w); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := w.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
func addFileToZip(filename string, writer *zip.Writer) error {
|
||||
fileReader, err := os.Open(filepath.Clean(filename))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fileReader.Close() //nolint:errcheck,gosec
|
||||
|
||||
fileInfo, err := fileReader.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
header, err := zip.FileInfoHeader(fileInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
header.Method = zip.Deflate
|
||||
header.Name = filepath.Base(filename)
|
||||
|
||||
fileWriter, err := writer.CreateHeader(header)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := io.Copy(fileWriter, fileReader); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return fileReader.Close()
|
||||
}, attachment...)
|
||||
}
|
||||
|
||||
46
internal/bridge/config_status.go
Normal file
46
internal/bridge/config_status.go
Normal file
@ -0,0 +1,46 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||
)
|
||||
|
||||
func (bridge *Bridge) ReportBugClicked() {
|
||||
safe.Lock(func() {
|
||||
for _, user := range bridge.users {
|
||||
user.ReportBugClicked()
|
||||
}
|
||||
}, bridge.usersLock)
|
||||
}
|
||||
|
||||
func (bridge *Bridge) AutoconfigUsed(client string) {
|
||||
safe.Lock(func() {
|
||||
for _, user := range bridge.users {
|
||||
user.AutoconfigUsed(client)
|
||||
}
|
||||
}, bridge.usersLock)
|
||||
}
|
||||
|
||||
func (bridge *Bridge) KBArticleOpened(article string) {
|
||||
safe.Lock(func() {
|
||||
for _, user := range bridge.users {
|
||||
user.KBArticleOpened(article)
|
||||
}
|
||||
}, bridge.usersLock)
|
||||
}
|
||||
@ -18,6 +18,7 @@
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/clientconfig"
|
||||
@ -31,7 +32,7 @@ import (
|
||||
|
||||
// ConfigureAppleMail configures apple mail for the given userID and address.
|
||||
// If configuring apple mail for Catalina or newer, it ensures Bridge is using SSL.
|
||||
func (bridge *Bridge) ConfigureAppleMail(userID, address string) error {
|
||||
func (bridge *Bridge) ConfigureAppleMail(ctx context.Context, userID, address string) error {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"userID": userID,
|
||||
"address": logging.Sensitive(address),
|
||||
@ -56,7 +57,7 @@ func (bridge *Bridge) ConfigureAppleMail(userID, address string) error {
|
||||
}
|
||||
|
||||
if useragent.IsCatalinaOrNewer() && !bridge.vault.GetSMTPSSL() {
|
||||
if err := bridge.SetSMTPSSL(true); err != nil {
|
||||
if err := bridge.SetSMTPSSL(ctx, true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
297
internal/bridge/debug.go
Normal file
297
internal/bridge/debug.go
Normal file
@ -0,0 +1,297 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/ProtonMail/gluon/imap"
|
||||
"github.com/ProtonMail/gluon/rfc822"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||
"github.com/bradenaw/juniper/iterator"
|
||||
"github.com/bradenaw/juniper/xslices"
|
||||
goimap "github.com/emersion/go-imap"
|
||||
goimapclient "github.com/emersion/go-imap/client"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/exp/maps"
|
||||
)
|
||||
|
||||
type CheckClientStateResult struct {
|
||||
MissingMessages map[string]map[string]user.DiagMailboxMessage
|
||||
}
|
||||
|
||||
func (c *CheckClientStateResult) AddMissingMessage(userID string, message user.DiagMailboxMessage) {
|
||||
v, ok := c.MissingMessages[userID]
|
||||
if !ok {
|
||||
c.MissingMessages[userID] = map[string]user.DiagMailboxMessage{message.ID: message}
|
||||
} else {
|
||||
v[message.ID] = message
|
||||
}
|
||||
}
|
||||
|
||||
// CheckClientState checks the current IMAP client reported state against the proton server state and reports
|
||||
// anything that is out of place.
|
||||
func (bridge *Bridge) CheckClientState(ctx context.Context, checkFlags bool, progressCB func(string)) (CheckClientStateResult, error) {
|
||||
bridge.usersLock.RLock()
|
||||
defer bridge.usersLock.RUnlock()
|
||||
|
||||
users := maps.Values(bridge.users)
|
||||
|
||||
result := CheckClientStateResult{
|
||||
MissingMessages: make(map[string]map[string]user.DiagMailboxMessage),
|
||||
}
|
||||
|
||||
for _, usr := range users {
|
||||
if progressCB != nil {
|
||||
progressCB(fmt.Sprintf("Checking state for user %v", usr.Name()))
|
||||
}
|
||||
log := logrus.WithField("user", usr.Name()).WithField("diag", "state-check")
|
||||
log.Debug("Retrieving all server metadata")
|
||||
meta, err := usr.GetDiagnosticMetadata(ctx)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
success := true
|
||||
|
||||
if len(meta.Metadata) != len(meta.MessageIDs) {
|
||||
log.Errorf("Metadata (%v) and message(%v) list sizes do not match", len(meta.Metadata), len(meta.MessageIDs))
|
||||
}
|
||||
|
||||
log.Debug("Building state")
|
||||
state, err := meta.BuildMailboxToMessageMap(usr)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Failed to build state")
|
||||
return result, err
|
||||
}
|
||||
|
||||
info, err := bridge.GetUserInfo(usr.ID())
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Failed to get user info")
|
||||
return result, err
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf("127.0.0.1:%v", bridge.GetIMAPPort())
|
||||
|
||||
for account, mboxMap := range state {
|
||||
if progressCB != nil {
|
||||
progressCB(fmt.Sprintf("Checking state for user %v's account '%v'", usr.Name(), account))
|
||||
}
|
||||
if err := func(account string, mboxMap user.AccountMailboxMap) error {
|
||||
client, err := goimapclient.Dial(addr)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Failed to connect to imap client")
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = client.Logout()
|
||||
}()
|
||||
|
||||
if err := client.Login(account, string(info.BridgePass)); err != nil {
|
||||
return fmt.Errorf("failed to login for user %v:%w", usr.Name(), err)
|
||||
}
|
||||
|
||||
log := log.WithField("account", account)
|
||||
for mboxName, messageList := range mboxMap {
|
||||
log := log.WithField("mbox", mboxName)
|
||||
status, err := client.Select(mboxName, true)
|
||||
if err != nil {
|
||||
log.WithError(err).Errorf("Failed to select mailbox %v", messageList)
|
||||
return fmt.Errorf("failed to select '%v':%w", mboxName, err)
|
||||
}
|
||||
|
||||
log.Debug("Checking message count")
|
||||
|
||||
if int(status.Messages) != len(messageList) {
|
||||
success = false
|
||||
log.Errorf("Message count doesn't match, got '%v' expected '%v'", status.Messages, len(messageList))
|
||||
}
|
||||
|
||||
ids, err := clientGetMessageIDs(client, mboxName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get message ids for mbox '%v': %w", mboxName, err)
|
||||
}
|
||||
|
||||
for _, msg := range messageList {
|
||||
imapFlags, ok := ids[msg.ID]
|
||||
if !ok {
|
||||
if meta.FailedMessageIDs.Contains(msg.ID) {
|
||||
log.Warningf("Missing message '%v', but it is part of failed message set", msg.ID)
|
||||
} else {
|
||||
log.Errorf("Missing message '%v'", msg.ID)
|
||||
}
|
||||
|
||||
result.AddMissingMessage(msg.UserID, msg)
|
||||
continue
|
||||
}
|
||||
|
||||
if checkFlags {
|
||||
if !imapFlags.Equals(msg.Flags) {
|
||||
log.Errorf("Message '%v' flags do mot match, got=%v, expected=%v",
|
||||
msg.ID,
|
||||
imapFlags.ToSlice(),
|
||||
msg.Flags.ToSlice(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !success {
|
||||
log.Errorf("State does not match")
|
||||
} else {
|
||||
log.Info("State matches")
|
||||
}
|
||||
|
||||
return nil
|
||||
}(account, mboxMap); err != nil {
|
||||
return result, err
|
||||
}
|
||||
}
|
||||
|
||||
// Check for orphaned messages (only present in All Mail)
|
||||
if progressCB != nil {
|
||||
progressCB(fmt.Sprintf("Checking user %v for orphans", usr.Name()))
|
||||
}
|
||||
log.Debugf("Checking for orphans")
|
||||
|
||||
for _, m := range meta.Metadata {
|
||||
filteredLabels := xslices.Filter(m.LabelIDs, func(t string) bool {
|
||||
switch t {
|
||||
case proton.AllMailLabel:
|
||||
return false
|
||||
case proton.AllSentLabel:
|
||||
return false
|
||||
case proton.AllDraftsLabel:
|
||||
return false
|
||||
case proton.OutboxLabel:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
if len(filteredLabels) == 0 {
|
||||
log.Warnf("Message %v is only present in All Mail (Subject=%v)", m.ID, m.Subject)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (bridge *Bridge) DebugDownloadFailedMessages(
|
||||
ctx context.Context,
|
||||
result CheckClientStateResult,
|
||||
exportPath string,
|
||||
progressCB func(string, int, int),
|
||||
) error {
|
||||
bridge.usersLock.RLock()
|
||||
defer bridge.usersLock.RUnlock()
|
||||
|
||||
for userID, messages := range result.MissingMessages {
|
||||
usr, ok := bridge.users[userID]
|
||||
if !ok {
|
||||
return fmt.Errorf("failed to find user with id %v", userID)
|
||||
}
|
||||
|
||||
userDir := filepath.Join(exportPath, userID)
|
||||
if err := os.MkdirAll(userDir, 0o700); err != nil {
|
||||
return fmt.Errorf("failed to create directory '%v': %w", userDir, err)
|
||||
}
|
||||
|
||||
if err := usr.DebugDownloadMessages(ctx, userDir, messages, progressCB); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func clientGetMessageIDs(client *goimapclient.Client, mailbox string) (map[string]imap.FlagSet, error) {
|
||||
status, err := client.Select(mailbox, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if status.Messages == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
resCh := make(chan *goimap.Message)
|
||||
|
||||
section, err := goimap.ParseBodySectionName("BODY[HEADER]")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fetchItems := []goimap.FetchItem{"BODY[HEADER]", goimap.FetchFlags}
|
||||
|
||||
seq, err := goimap.ParseSeqSet("1:*")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err := client.Fetch(
|
||||
seq,
|
||||
fetchItems,
|
||||
resCh,
|
||||
); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
messages := iterator.Collect(iterator.Chan(resCh))
|
||||
|
||||
ids := make(map[string]imap.FlagSet, len(messages))
|
||||
|
||||
for i, m := range messages {
|
||||
literal, err := io.ReadAll(m.GetBody(section))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
header, err := rfc822.NewHeader(literal)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse header for msg %v: %w", i, err)
|
||||
}
|
||||
|
||||
internalID, ok := header.GetChecked("X-Pm-Internal-Id")
|
||||
if !ok {
|
||||
logrus.Errorf("Message %v does not have internal id", internalID)
|
||||
continue
|
||||
}
|
||||
|
||||
messageFlags := imap.NewFlagSet(m.Flags...)
|
||||
|
||||
// Recent and Deleted are not part of the proton flag set.
|
||||
messageFlags.RemoveFromSelf("\\Recent")
|
||||
messageFlags.RemoveFromSelf("\\Deleted")
|
||||
|
||||
ids[internalID] = messageFlags
|
||||
}
|
||||
|
||||
return ids, nil
|
||||
}
|
||||
175
internal/bridge/draft_test.go
Normal file
175
internal/bridge/draft_test.go
Normal file
@ -0,0 +1,175 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package bridge_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/gluon/rfc822"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/go-proton-api/server"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
go_imap "github.com/emersion/go-imap"
|
||||
"github.com/emersion/go-sasl"
|
||||
"github.com/emersion/go-smtp"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestBridge_HandleDraftsSendFromOtherClient(t *testing.T) {
|
||||
getGluonHeaderID := func(literal []byte) (string, string) {
|
||||
h, err := rfc822.NewHeader(literal)
|
||||
require.NoError(t, err)
|
||||
|
||||
gluonID, ok := h.GetChecked("X-Pm-Gluon-Id")
|
||||
require.True(t, ok)
|
||||
|
||||
externalID, ok := h.GetChecked("Message-Id")
|
||||
require.True(t, ok)
|
||||
|
||||
return gluonID, externalID
|
||||
}
|
||||
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
_, _, err := s.CreateUser("imap", password)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, _, err = s.CreateUser("bar", password)
|
||||
require.NoError(t, err)
|
||||
|
||||
// The initial user should be fully synced.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
|
||||
waiter := waitForIMAPServerReady(b)
|
||||
defer waiter.Done()
|
||||
|
||||
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
|
||||
defer done()
|
||||
|
||||
userID, err := b.LoginFull(ctx, "imap", password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, userID, (<-syncCh).UserID)
|
||||
waiter.Wait()
|
||||
|
||||
info, err := b.GetUserInfo(userID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, info.State == bridge.Connected)
|
||||
|
||||
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
||||
defer func() { _ = client.Logout() }()
|
||||
|
||||
// Create first draft in client.
|
||||
literal := fmt.Sprintf(`From: %v
|
||||
To: %v
|
||||
Date: Fri, 3 Feb 2023 01:04:32 +0100
|
||||
Subject: Foo
|
||||
|
||||
Hello
|
||||
`, info.Addresses[0], "bar@proton.local")
|
||||
|
||||
require.NoError(t, client.Append("Drafts", nil, time.Now(), strings.NewReader(literal)))
|
||||
// Verify the draft is available in client.
|
||||
require.Eventually(t, func() bool {
|
||||
status, err := client.Status("Drafts", []go_imap.StatusItem{go_imap.StatusMessages})
|
||||
require.NoError(t, err)
|
||||
return status.Messages == 1
|
||||
}, 2*time.Second, time.Second)
|
||||
|
||||
// Retrieve the new literal so we can have the Proton Message ID.
|
||||
messages, err := clientFetch(client, "Drafts")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(messages))
|
||||
|
||||
newLiteral, err := io.ReadAll(messages[0].GetBody(must(go_imap.ParseBodySectionName("BODY[]"))))
|
||||
require.NoError(t, err)
|
||||
logrus.Info(string(newLiteral))
|
||||
|
||||
newLiteralID, newLiteralExternID := getGluonHeaderID(newLiteral)
|
||||
|
||||
// Modify new literal.
|
||||
newLiteralModified := append(newLiteral, []byte(" world from client2")...) //nolint:gocritic
|
||||
|
||||
func() {
|
||||
smtpClient, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(b.GetSMTPPort())))
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = smtpClient.Close() }()
|
||||
|
||||
// Upgrade to TLS.
|
||||
require.NoError(t, smtpClient.StartTLS(&tls.Config{InsecureSkipVerify: true}))
|
||||
|
||||
// Authorize with SASL PLAIN.
|
||||
require.NoError(t, smtpClient.Auth(sasl.NewPlainClient(
|
||||
info.Addresses[0],
|
||||
info.Addresses[0],
|
||||
string(info.BridgePass)),
|
||||
))
|
||||
|
||||
// Send the message.
|
||||
require.NoError(t, smtpClient.SendMail(
|
||||
info.Addresses[0],
|
||||
[]string{"bar@proton.local"},
|
||||
bytes.NewReader(newLiteralModified),
|
||||
))
|
||||
}()
|
||||
|
||||
// Append message to Sent as the imap client would.
|
||||
require.NoError(t, client.Append("Sent", nil, time.Now(), strings.NewReader(literal)))
|
||||
|
||||
// Verify the sent message gets updated with the new literal.
|
||||
require.Eventually(t, func() bool {
|
||||
// Check if sent message matches the latest draft.
|
||||
messagesClient1, err := clientFetch(client, "Sent", "BODY[TEXT]", "BODY[]")
|
||||
require.NoError(t, err)
|
||||
|
||||
if len(messagesClient1) != 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
sentLiteral, err := io.ReadAll(messagesClient1[0].GetBody(must(go_imap.ParseBodySectionName("BODY[]"))))
|
||||
require.NoError(t, err)
|
||||
|
||||
sentLiteralID, sentLiteralExternID := getGluonHeaderID(sentLiteral)
|
||||
|
||||
sentLiteralText, err := io.ReadAll(messagesClient1[0].GetBody(must(go_imap.ParseBodySectionName("BODY[TEXT]"))))
|
||||
require.NoError(t, err)
|
||||
|
||||
sentLiteralStr := string(sentLiteralText)
|
||||
|
||||
literalMatches := sentLiteralStr == "Hello\r\n world from client2\r\n"
|
||||
|
||||
idIsDifferent := sentLiteralID != newLiteralID
|
||||
|
||||
externIDMatches := sentLiteralExternID == newLiteralExternID
|
||||
|
||||
return literalMatches && idIsDifferent && externIDMatches
|
||||
}, 2*time.Second, time.Second)
|
||||
})
|
||||
}, server.WithMessageDedup())
|
||||
}
|
||||
@ -58,11 +58,7 @@ func moveFile(from, to string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.Rename(from, to); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return os.Rename(from, to)
|
||||
}
|
||||
|
||||
func copyDir(from, to string) error {
|
||||
|
||||
128
internal/bridge/heartbeat.go
Normal file
128
internal/bridge/heartbeat.go
Normal file
@ -0,0 +1,128 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/gluon/reporter"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/telemetry"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const HeartbeatCheckInterval = time.Hour
|
||||
|
||||
func (bridge *Bridge) IsTelemetryAvailable(ctx context.Context) bool {
|
||||
var flag = true
|
||||
if bridge.GetTelemetryDisabled() {
|
||||
return false
|
||||
}
|
||||
|
||||
safe.RLock(func() {
|
||||
for _, user := range bridge.users {
|
||||
flag = flag && user.IsTelemetryEnabled(ctx)
|
||||
}
|
||||
}, bridge.usersLock)
|
||||
|
||||
return flag
|
||||
}
|
||||
|
||||
func (bridge *Bridge) SendHeartbeat(ctx context.Context, heartbeat *telemetry.HeartbeatData) bool {
|
||||
data, err := json.Marshal(heartbeat)
|
||||
if err != nil {
|
||||
if err := bridge.reporter.ReportMessageWithContext("Cannot parse heartbeat data.", reporter.Context{
|
||||
"error": err,
|
||||
}); err != nil {
|
||||
logrus.WithError(err).Error("Failed to parse heartbeat data.")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var sent = false
|
||||
|
||||
safe.RLock(func() {
|
||||
for _, user := range bridge.users {
|
||||
if err := user.SendTelemetry(ctx, data); err == nil {
|
||||
sent = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}, bridge.usersLock)
|
||||
|
||||
return sent
|
||||
}
|
||||
|
||||
func (bridge *Bridge) GetLastHeartbeatSent() time.Time {
|
||||
return bridge.vault.GetLastHeartbeatSent()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) SetLastHeartbeatSent(timestamp time.Time) error {
|
||||
return bridge.vault.SetLastHeartbeatSent(timestamp)
|
||||
}
|
||||
|
||||
func (bridge *Bridge) StartHeartbeat(manager telemetry.HeartbeatManager) {
|
||||
bridge.heartbeat = telemetry.NewHeartbeat(manager, 1143, 1025, bridge.GetGluonCacheDir(), keychain.DefaultHelper)
|
||||
|
||||
// Check for heartbeat when triggered.
|
||||
bridge.goHeartbeat = bridge.tasks.PeriodicOrTrigger(HeartbeatCheckInterval, 0, func(ctx context.Context) {
|
||||
logrus.Debug("Checking for heartbeat")
|
||||
|
||||
bridge.heartbeat.TrySending(ctx)
|
||||
})
|
||||
|
||||
bridge.heartbeat.SetRollout(bridge.GetUpdateRollout())
|
||||
bridge.heartbeat.SetAutoStart(bridge.GetAutostart())
|
||||
bridge.heartbeat.SetAutoUpdate(bridge.GetAutoUpdate())
|
||||
bridge.heartbeat.SetBeta(bridge.GetUpdateChannel())
|
||||
bridge.heartbeat.SetDoh(bridge.GetProxyAllowed())
|
||||
bridge.heartbeat.SetShowAllMail(bridge.GetShowAllMail())
|
||||
bridge.heartbeat.SetIMAPConnectionMode(bridge.GetIMAPSSL())
|
||||
bridge.heartbeat.SetSMTPConnectionMode(bridge.GetSMTPSSL())
|
||||
bridge.heartbeat.SetIMAPPort(bridge.GetIMAPPort())
|
||||
bridge.heartbeat.SetSMTPPort(bridge.GetSMTPPort())
|
||||
bridge.heartbeat.SetCacheLocation(bridge.GetGluonCacheDir())
|
||||
if val, err := bridge.GetKeychainApp(); err != nil {
|
||||
bridge.heartbeat.SetKeyChainPref(val)
|
||||
} else {
|
||||
bridge.heartbeat.SetKeyChainPref(keychain.DefaultHelper)
|
||||
}
|
||||
bridge.heartbeat.SetPrevVersion(bridge.GetLastVersion().String())
|
||||
|
||||
safe.RLock(func() {
|
||||
var splitMode = false
|
||||
for _, user := range bridge.users {
|
||||
if user.GetAddressMode() == vault.SplitMode {
|
||||
splitMode = true
|
||||
break
|
||||
}
|
||||
}
|
||||
var nbAccount = len(bridge.users)
|
||||
bridge.heartbeat.SetNbAccount(nbAccount)
|
||||
bridge.heartbeat.SetSplitMode(splitMode)
|
||||
|
||||
// Do not try to send if there is no user yet.
|
||||
if nbAccount > 0 {
|
||||
defer bridge.goHeartbeat()
|
||||
}
|
||||
}, bridge.usersLock)
|
||||
}
|
||||
@ -17,6 +17,8 @@
|
||||
|
||||
package bridge
|
||||
|
||||
import "github.com/sirupsen/logrus"
|
||||
|
||||
func (bridge *Bridge) GetCurrentUserAgent() string {
|
||||
return bridge.identifier.GetUserAgent()
|
||||
}
|
||||
@ -24,3 +26,17 @@ func (bridge *Bridge) GetCurrentUserAgent() string {
|
||||
func (bridge *Bridge) SetCurrentPlatform(platform string) {
|
||||
bridge.identifier.SetPlatform(platform)
|
||||
}
|
||||
|
||||
func (bridge *Bridge) setUserAgent(name, version string) {
|
||||
currentUserAgent := bridge.identifier.GetClientString()
|
||||
|
||||
bridge.identifier.SetClient(name, version)
|
||||
|
||||
newUserAgent := bridge.identifier.GetClientString()
|
||||
|
||||
if currentUserAgent != newUserAgent {
|
||||
if err := bridge.vault.SetLastUserAgent(newUserAgent); err != nil {
|
||||
logrus.WithError(err).Error("Failed to write new user agent to vault")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,10 +20,10 @@ package bridge
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/gluon"
|
||||
@ -37,205 +37,22 @@ import (
|
||||
"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"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultClientName = "UnknownClient"
|
||||
defaultClientVersion = "0.0.1"
|
||||
)
|
||||
|
||||
func (bridge *Bridge) serveIMAP() error {
|
||||
port, err := func() (int, error) {
|
||||
if bridge.imapServer == nil {
|
||||
return 0, fmt.Errorf("no IMAP server instance running")
|
||||
}
|
||||
|
||||
logrus.Info("Starting IMAP server")
|
||||
|
||||
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
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
bridge.publish(events.IMAPServerError{
|
||||
Error: err,
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
bridge.publish(events.IMAPServerReady{
|
||||
Port: port,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bridge *Bridge) restartIMAP() error {
|
||||
logrus.Info("Restarting IMAP server")
|
||||
|
||||
if bridge.imapListener != nil {
|
||||
if err := bridge.imapListener.Close(); err != nil {
|
||||
return fmt.Errorf("failed to close IMAP listener: %w", err)
|
||||
}
|
||||
|
||||
bridge.publish(events.IMAPServerStopped{})
|
||||
}
|
||||
|
||||
return bridge.serveIMAP()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) closeIMAP(ctx context.Context) error {
|
||||
logrus.Info("Closing IMAP server")
|
||||
|
||||
if bridge.imapServer != nil {
|
||||
if err := bridge.imapServer.Close(ctx); err != nil {
|
||||
return fmt.Errorf("failed to close IMAP server: %w", err)
|
||||
}
|
||||
|
||||
bridge.imapServer = nil
|
||||
}
|
||||
|
||||
if bridge.imapListener != nil {
|
||||
if err := bridge.imapListener.Close(); err != nil {
|
||||
return fmt.Errorf("failed to close IMAP listener: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
bridge.publish(events.IMAPServerStopped{})
|
||||
|
||||
return nil
|
||||
func (bridge *Bridge) restartIMAP(ctx context.Context) error {
|
||||
return bridge.serverManager.RestartIMAP(ctx)
|
||||
}
|
||||
|
||||
// addIMAPUser connects the given user to gluon.
|
||||
func (bridge *Bridge) addIMAPUser(ctx context.Context, user *user.User) error {
|
||||
if bridge.imapServer == nil {
|
||||
return fmt.Errorf("no imap server instance running")
|
||||
}
|
||||
|
||||
imapConn, err := user.NewIMAPConnectors()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create IMAP connectors: %w", err)
|
||||
}
|
||||
|
||||
for addrID, imapConn := range imapConn {
|
||||
log := logrus.WithFields(logrus.Fields{
|
||||
"userID": user.ID(),
|
||||
"addrID": addrID,
|
||||
})
|
||||
|
||||
if gluonID, ok := user.GetGluonID(addrID); ok {
|
||||
log.WithField("gluonID", gluonID).Info("Loading existing IMAP user")
|
||||
|
||||
// Load the user, checking whether the DB was newly created.
|
||||
isNew, err := bridge.imapServer.LoadUser(ctx, imapConn, gluonID, user.GluonKey())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load IMAP user: %w", err)
|
||||
}
|
||||
|
||||
if isNew {
|
||||
// If the DB was newly created, clear the sync status; gluon's DB was not found.
|
||||
logrus.Warn("IMAP user DB was newly created, clearing sync status")
|
||||
|
||||
// Remove the user from IMAP so we can clear the sync status.
|
||||
if err := bridge.imapServer.RemoveUser(ctx, gluonID, false); err != nil {
|
||||
return fmt.Errorf("failed to remove IMAP user: %w", err)
|
||||
}
|
||||
|
||||
// Clear the sync status -- we need to resync all messages.
|
||||
if err := user.ClearSyncStatus(); err != nil {
|
||||
return fmt.Errorf("failed to clear sync status: %w", err)
|
||||
}
|
||||
|
||||
// Add the user back to the IMAP server.
|
||||
if isNew, err := bridge.imapServer.LoadUser(ctx, imapConn, gluonID, user.GluonKey()); err != nil {
|
||||
return fmt.Errorf("failed to add IMAP user: %w", err)
|
||||
} else if isNew {
|
||||
panic("IMAP user should already have a database")
|
||||
}
|
||||
} else if status := user.GetSyncStatus(); !status.HasLabels {
|
||||
// Otherwise, the DB already exists -- if the labels are not yet synced, we need to re-create the DB.
|
||||
if err := bridge.imapServer.RemoveUser(ctx, gluonID, true); err != nil {
|
||||
return fmt.Errorf("failed to remove old IMAP user: %w", err)
|
||||
}
|
||||
|
||||
if err := user.RemoveGluonID(addrID, gluonID); err != nil {
|
||||
return fmt.Errorf("failed to remove old IMAP user ID: %w", err)
|
||||
}
|
||||
|
||||
gluonID, err := bridge.imapServer.AddUser(ctx, imapConn, user.GluonKey())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add IMAP user: %w", err)
|
||||
}
|
||||
|
||||
if err := user.SetGluonID(addrID, gluonID); err != nil {
|
||||
return fmt.Errorf("failed to set IMAP user ID: %w", err)
|
||||
}
|
||||
|
||||
log.WithField("gluonID", gluonID).Info("Re-created IMAP user")
|
||||
}
|
||||
} else {
|
||||
log.Info("Creating new IMAP user")
|
||||
|
||||
gluonID, err := bridge.imapServer.AddUser(ctx, imapConn, user.GluonKey())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add IMAP user: %w", err)
|
||||
}
|
||||
|
||||
if err := user.SetGluonID(addrID, gluonID); err != nil {
|
||||
return fmt.Errorf("failed to set IMAP user ID: %w", err)
|
||||
}
|
||||
|
||||
log.WithField("gluonID", gluonID).Info("Created new IMAP user")
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger a sync for the user, if needed.
|
||||
user.TriggerSync()
|
||||
|
||||
return nil
|
||||
return bridge.serverManager.AddIMAPUser(ctx, user)
|
||||
}
|
||||
|
||||
// removeIMAPUser disconnects the given user from gluon, optionally also removing its files.
|
||||
func (bridge *Bridge) removeIMAPUser(ctx context.Context, user *user.User, withData bool) error {
|
||||
if bridge.imapServer == nil {
|
||||
return fmt.Errorf("no imap server instance running")
|
||||
}
|
||||
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"userID": user.ID(),
|
||||
"withData": withData,
|
||||
}).Debug("Removing IMAP user")
|
||||
|
||||
for addrID, gluonID := range user.GetGluonIDs() {
|
||||
if err := bridge.imapServer.RemoveUser(ctx, gluonID, withData); err != nil {
|
||||
return fmt.Errorf("failed to remove IMAP user: %w", err)
|
||||
}
|
||||
|
||||
if withData {
|
||||
if err := user.RemoveGluonID(addrID, gluonID); err != nil {
|
||||
return fmt.Errorf("failed to remove IMAP user ID: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return bridge.serverManager.RemoveIMAPUser(ctx, user, withData)
|
||||
}
|
||||
|
||||
func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) {
|
||||
@ -249,11 +66,6 @@ func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) {
|
||||
}).Info("Received mailbox message count")
|
||||
}
|
||||
|
||||
case imapEvents.SessionAdded:
|
||||
if !bridge.identifier.HasClient() {
|
||||
bridge.identifier.SetClient(defaultClientName, defaultClientVersion)
|
||||
}
|
||||
|
||||
case imapEvents.IMAPID:
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"sessionID": event.SessionID,
|
||||
@ -262,24 +74,22 @@ func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) {
|
||||
}).Info("Received IMAP ID")
|
||||
|
||||
if event.IMAPID.Name != "" && event.IMAPID.Version != "" {
|
||||
bridge.identifier.SetClient(event.IMAPID.Name, event.IMAPID.Version)
|
||||
bridge.setUserAgent(event.IMAPID.Name, event.IMAPID.Version)
|
||||
}
|
||||
|
||||
case imapEvents.LoginFailed:
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"sessionID": event.SessionID,
|
||||
"username": event.Username,
|
||||
}).Info("Received IMAP login failure notification")
|
||||
"pkg": "imap",
|
||||
}).Error("Incorrect login credentials.")
|
||||
bridge.publish(events.IMAPLoginFailed{Username: event.Username})
|
||||
}
|
||||
}
|
||||
|
||||
func getGluonDir(encVault *vault.Vault) (string, error) {
|
||||
if err := os.MkdirAll(encVault.GetGluonCacheDir(), 0o700); err != nil {
|
||||
return "", fmt.Errorf("failed to create gluon dir: %w", err)
|
||||
case imapEvents.Login:
|
||||
if strings.Contains(bridge.GetCurrentUserAgent(), useragent.DefaultUserAgent) {
|
||||
bridge.setUserAgent(useragent.UnknownClient, useragent.DefaultVersion)
|
||||
}
|
||||
}
|
||||
|
||||
return encVault.GetGluonCacheDir(), nil
|
||||
}
|
||||
|
||||
func ApplyGluonCachePathSuffix(basePath string) string {
|
||||
|
||||
55
internal/bridge/migration.go
Normal file
55
internal/bridge/migration.go
Normal file
@ -0,0 +1,55 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/gluon/reporter"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (bridge *Bridge) databaseResyncNeeded() bool {
|
||||
if strings.HasPrefix(bridge.lastVersion.String(), "3.4.0") &&
|
||||
strings.HasPrefix(bridge.curVersion.String(), "3.4.1") {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"lastVersion": bridge.lastVersion.String(),
|
||||
"currVersion": bridge.curVersion.String(),
|
||||
}).Warning("Database re-synchronisation needed")
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (bridge *Bridge) migrateUser(vault *vault.User) {
|
||||
if bridge.databaseResyncNeeded() {
|
||||
if err := bridge.reporter.ReportMessage("Database need to be re-sync for migration."); err != nil {
|
||||
logrus.WithError(err).Error("Failed to report database re-sync for migration.")
|
||||
}
|
||||
if err := vault.ClearSyncStatus(); err != nil {
|
||||
logrus.WithError(err).Error("Failed reset to SyncStatus.")
|
||||
if err2 := bridge.reporter.ReportMessageWithContext("Failed to reset SyncStatus for Database migration.",
|
||||
reporter.Context{
|
||||
"error": err,
|
||||
}); err2 != nil {
|
||||
logrus.WithError(err2).Error("Failed to report reset SyncStatus error.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -24,6 +24,7 @@ type Mocks struct {
|
||||
|
||||
CrashHandler *mocks.MockPanicHandler
|
||||
Reporter *mocks.MockReporter
|
||||
Heartbeat *mocks.MockHeartbeatManager
|
||||
}
|
||||
|
||||
func NewMocks(tb testing.TB, version, minAuto *semver.Version) *Mocks {
|
||||
@ -39,14 +40,18 @@ func NewMocks(tb testing.TB, version, minAuto *semver.Version) *Mocks {
|
||||
|
||||
CrashHandler: mocks.NewMockPanicHandler(ctl),
|
||||
Reporter: mocks.NewMockReporter(ctl),
|
||||
Heartbeat: mocks.NewMockHeartbeatManager(ctl),
|
||||
}
|
||||
|
||||
// When getting the TLS issue channel, we want to return the test channel.
|
||||
mocks.TLSReporter.EXPECT().GetTLSIssueCh().Return(mocks.TLSIssueCh).AnyTimes()
|
||||
|
||||
// This is called at he end of any go-routine:
|
||||
// This is called at the end of any go-routine:
|
||||
mocks.CrashHandler.EXPECT().HandlePanic(gomock.Any()).AnyTimes()
|
||||
|
||||
// this is called at start of heartbeat process.
|
||||
mocks.Heartbeat.EXPECT().IsTelemetryAvailable(gomock.Any()).AnyTimes()
|
||||
|
||||
return mocks
|
||||
}
|
||||
|
||||
@ -139,13 +144,13 @@ func (testUpdater *TestUpdater) SetLatestVersion(version, minAuto *semver.Versio
|
||||
}
|
||||
}
|
||||
|
||||
func (testUpdater *TestUpdater) GetVersionInfo(ctx context.Context, downloader updater.Downloader, channel updater.Channel) (updater.VersionInfo, error) {
|
||||
func (testUpdater *TestUpdater) GetVersionInfo(_ context.Context, _ updater.Downloader, _ updater.Channel) (updater.VersionInfo, error) {
|
||||
testUpdater.lock.RLock()
|
||||
defer testUpdater.lock.RUnlock()
|
||||
|
||||
return testUpdater.latest, nil
|
||||
}
|
||||
|
||||
func (testUpdater *TestUpdater) InstallUpdate(ctx context.Context, downloader updater.Downloader, update updater.VersionInfo) error {
|
||||
func (testUpdater *TestUpdater) InstallUpdate(_ context.Context, _ updater.Downloader, _ updater.VersionInfo) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
93
internal/bridge/mocks/telemetry_mocks.go
Normal file
93
internal/bridge/mocks/telemetry_mocks.go
Normal file
@ -0,0 +1,93 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/ProtonMail/proton-bridge/v3/internal/telemetry (interfaces: HeartbeatManager)
|
||||
|
||||
// Package mocks is a generated GoMock package.
|
||||
package mocks
|
||||
|
||||
import (
|
||||
context "context"
|
||||
reflect "reflect"
|
||||
time "time"
|
||||
|
||||
telemetry "github.com/ProtonMail/proton-bridge/v3/internal/telemetry"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
// MockHeartbeatManager is a mock of HeartbeatManager interface.
|
||||
type MockHeartbeatManager struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockHeartbeatManagerMockRecorder
|
||||
}
|
||||
|
||||
// MockHeartbeatManagerMockRecorder is the mock recorder for MockHeartbeatManager.
|
||||
type MockHeartbeatManagerMockRecorder struct {
|
||||
mock *MockHeartbeatManager
|
||||
}
|
||||
|
||||
// NewMockHeartbeatManager creates a new mock instance.
|
||||
func NewMockHeartbeatManager(ctrl *gomock.Controller) *MockHeartbeatManager {
|
||||
mock := &MockHeartbeatManager{ctrl: ctrl}
|
||||
mock.recorder = &MockHeartbeatManagerMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockHeartbeatManager) EXPECT() *MockHeartbeatManagerMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// GetLastHeartbeatSent mocks base method.
|
||||
func (m *MockHeartbeatManager) GetLastHeartbeatSent() time.Time {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetLastHeartbeatSent")
|
||||
ret0, _ := ret[0].(time.Time)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetLastHeartbeatSent indicates an expected call of GetLastHeartbeatSent.
|
||||
func (mr *MockHeartbeatManagerMockRecorder) GetLastHeartbeatSent() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLastHeartbeatSent", reflect.TypeOf((*MockHeartbeatManager)(nil).GetLastHeartbeatSent))
|
||||
}
|
||||
|
||||
// IsTelemetryAvailable mocks base method.
|
||||
func (m *MockHeartbeatManager) IsTelemetryAvailable(arg0 context.Context) bool {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "IsTelemetryAvailable", arg0)
|
||||
ret0, _ := ret[0].(bool)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// IsTelemetryAvailable indicates an expected call of IsTelemetryAvailable.
|
||||
func (mr *MockHeartbeatManagerMockRecorder) IsTelemetryAvailable(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsTelemetryAvailable", reflect.TypeOf((*MockHeartbeatManager)(nil).IsTelemetryAvailable), arg0)
|
||||
}
|
||||
|
||||
// SendHeartbeat mocks base method.
|
||||
func (m *MockHeartbeatManager) SendHeartbeat(arg0 context.Context, arg1 *telemetry.HeartbeatData) bool {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SendHeartbeat", arg0, arg1)
|
||||
ret0, _ := ret[0].(bool)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// SendHeartbeat indicates an expected call of SendHeartbeat.
|
||||
func (mr *MockHeartbeatManagerMockRecorder) SendHeartbeat(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendHeartbeat", reflect.TypeOf((*MockHeartbeatManager)(nil).SendHeartbeat), arg0, arg1)
|
||||
}
|
||||
|
||||
// SetLastHeartbeatSent mocks base method.
|
||||
func (m *MockHeartbeatManager) SetLastHeartbeatSent(arg0 time.Time) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SetLastHeartbeatSent", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// SetLastHeartbeatSent indicates an expected call of SetLastHeartbeatSent.
|
||||
func (mr *MockHeartbeatManagerMockRecorder) SetLastHeartbeatSent(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLastHeartbeatSent", reflect.TypeOf((*MockHeartbeatManager)(nil).SetLastHeartbeatSent), arg0)
|
||||
}
|
||||
@ -28,7 +28,6 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"github.com/bradenaw/juniper/iterator"
|
||||
"github.com/emersion/go-imap/client"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@ -66,7 +65,7 @@ func TestBridge_Refresh(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.True(t, info.State == bridge.Connected)
|
||||
|
||||
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
||||
defer func() { _ = client.Logout() }()
|
||||
@ -99,7 +98,7 @@ func TestBridge_Refresh(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.True(t, info.State == bridge.Connected)
|
||||
|
||||
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
||||
defer func() { _ = client.Logout() }()
|
||||
|
||||
@ -34,7 +34,6 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"github.com/emersion/go-imap"
|
||||
"github.com/emersion/go-imap/client"
|
||||
"github.com/emersion/go-sasl"
|
||||
"github.com/emersion/go-smtp"
|
||||
"github.com/stretchr/testify/require"
|
||||
@ -46,12 +45,17 @@ func TestBridge_Send(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
smtpWaiter := waitForSMTPServerReady(bridge)
|
||||
defer smtpWaiter.Done()
|
||||
|
||||
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)
|
||||
|
||||
smtpWaiter.Wait()
|
||||
|
||||
senderInfo, err := bridge.GetUserInfo(senderUserID)
|
||||
require.NoError(t, err)
|
||||
|
||||
@ -91,13 +95,13 @@ func TestBridge_Send(t *testing.T) {
|
||||
}
|
||||
|
||||
// Connect the sender IMAP client.
|
||||
senderIMAPClient, err := client.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
|
||||
senderIMAPClient, err := eventuallyDial(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())))
|
||||
recipientIMAPClient, err := eventuallyDial(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
|
||||
@ -135,19 +139,19 @@ func TestBridge_SendDraftFlags(t *testing.T) {
|
||||
})
|
||||
|
||||
// Start the bridge.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
withBridgeWaitForServers(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
// Get the sender user info.
|
||||
userInfo, err := bridge.QueryUserInfo(username)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Connect the sender IMAP client.
|
||||
imapClient, err := client.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
|
||||
imapClient, err := eventuallyDial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, imapClient.Login(userInfo.Addresses[0], string(userInfo.BridgePass)))
|
||||
defer imapClient.Logout() //nolint:errcheck
|
||||
|
||||
// The message to send.
|
||||
const message = `Subject: Test\r\n\r\nHello world!`
|
||||
message := fmt.Sprintf("From: %v\r\nDate: 01 Jan 1980 00:00:00 +0000\r\nSubject: Test\r\n\r\nHello world!", userInfo.Addresses[0])
|
||||
|
||||
// Save a draft.
|
||||
require.NoError(t, imapClient.Append("Drafts", []string{imap.DraftFlag}, time.Now(), strings.NewReader(message)))
|
||||
@ -245,13 +249,13 @@ func TestBridge_SendInvite(t *testing.T) {
|
||||
})
|
||||
|
||||
// Start the bridge.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
withBridgeWaitForServers(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
// Get the sender user info.
|
||||
userInfo, err := bridge.QueryUserInfo(username)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Connect the sender IMAP client.
|
||||
imapClient, err := client.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
|
||||
imapClient, err := eventuallyDial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, imapClient.Login(userInfo.Addresses[0], string(userInfo.BridgePass)))
|
||||
defer imapClient.Logout() //nolint:errcheck
|
||||
@ -401,6 +405,9 @@ SGVsbG8gd29ybGQK
|
||||
require.NoError(t, err)
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
smtpWaiter := waitForSMTPServerReady(bridge)
|
||||
defer smtpWaiter.Done()
|
||||
|
||||
senderUserID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
@ -420,6 +427,8 @@ SGVsbG8gd29ybGQK
|
||||
messageMultipartWithoutTextWithTextAttachment,
|
||||
}
|
||||
|
||||
smtpWaiter.Wait()
|
||||
|
||||
for _, m := range messages {
|
||||
// Dial the server.
|
||||
client, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort())))
|
||||
@ -444,13 +453,13 @@ SGVsbG8gd29ybGQK
|
||||
}
|
||||
|
||||
// Connect the sender IMAP client.
|
||||
senderIMAPClient, err := client.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
|
||||
senderIMAPClient, err := eventuallyDial(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())))
|
||||
recipientIMAPClient, err := eventuallyDial(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
|
||||
|
||||
@ -36,6 +36,9 @@ import (
|
||||
func TestBridge_Report(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
imapWaiter := waitForIMAPServerReady(b)
|
||||
defer imapWaiter.Done()
|
||||
|
||||
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
|
||||
defer done()
|
||||
|
||||
@ -51,6 +54,8 @@ func TestBridge_Report(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.True(t, info.State == bridge.Connected)
|
||||
|
||||
imapWaiter.Wait()
|
||||
|
||||
// Dial the IMAP port.
|
||||
conn, err := net.Dial("tcp", fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||
require.NoError(t, err)
|
||||
|
||||
696
internal/bridge/server_manager.go
Normal file
696
internal/bridge/server_manager.go
Normal file
@ -0,0 +1,696 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/ProtonMail/gluon"
|
||||
"github.com/ProtonMail/gluon/connector"
|
||||
"github.com/ProtonMail/gluon/logging"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||
"github.com/ProtonMail/proton-bridge/v3/pkg/cpc"
|
||||
"github.com/emersion/go-smtp"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// ServerManager manages the IMAP & SMTP servers and their listeners.
|
||||
type ServerManager struct {
|
||||
requests *cpc.CPC
|
||||
|
||||
imapServer *gluon.Server
|
||||
imapListener net.Listener
|
||||
|
||||
smtpServer *smtp.Server
|
||||
smtpListener net.Listener
|
||||
|
||||
loadedUserCount int
|
||||
}
|
||||
|
||||
func newServerManager() *ServerManager {
|
||||
return &ServerManager{
|
||||
requests: cpc.NewCPC(),
|
||||
}
|
||||
}
|
||||
|
||||
func (sm *ServerManager) Init(bridge *Bridge) error {
|
||||
imapServer, err := createIMAPServer(bridge)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
smtpServer := createSMTPServer(bridge)
|
||||
|
||||
sm.imapServer = imapServer
|
||||
sm.smtpServer = smtpServer
|
||||
|
||||
bridge.tasks.Once(func(ctx context.Context) {
|
||||
logging.DoAnnotated(ctx, func(ctx context.Context) {
|
||||
sm.run(ctx, bridge)
|
||||
}, logging.Labels{
|
||||
"service": "server-manager",
|
||||
})
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sm *ServerManager) CloseServers(ctx context.Context) error {
|
||||
defer sm.requests.Close()
|
||||
_, err := sm.requests.Send(ctx, &smRequestClose{})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (sm *ServerManager) RestartIMAP(ctx context.Context) error {
|
||||
_, err := sm.requests.Send(ctx, &smRequestRestartIMAP{})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (sm *ServerManager) RestartSMTP(ctx context.Context) error {
|
||||
_, err := sm.requests.Send(ctx, &smRequestRestartSMTP{})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (sm *ServerManager) AddIMAPUser(ctx context.Context, user *user.User) error {
|
||||
_, err := sm.requests.Send(ctx, &smRequestAddIMAPUser{user: user})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (sm *ServerManager) RemoveIMAPUser(ctx context.Context, user *user.User, withData bool) error {
|
||||
_, err := sm.requests.Send(ctx, &smRequestRemoveIMAPUser{
|
||||
user: user,
|
||||
withData: withData,
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (sm *ServerManager) SetGluonDir(ctx context.Context, gluonDir string) error {
|
||||
_, err := sm.requests.Send(ctx, &smRequestSetGluonDir{
|
||||
dir: gluonDir,
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (sm *ServerManager) AddGluonUser(ctx context.Context, conn connector.Connector, passphrase []byte) (string, error) {
|
||||
reply, err := cpc.SendTyped[string](ctx, sm.requests, &smRequestAddGluonUser{
|
||||
conn: conn,
|
||||
passphrase: passphrase,
|
||||
})
|
||||
|
||||
return reply, err
|
||||
}
|
||||
|
||||
func (sm *ServerManager) RemoveGluonUser(ctx context.Context, gluonID string) error {
|
||||
_, err := sm.requests.Send(ctx, &smRequestRemoveGluonUser{
|
||||
userID: gluonID,
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (sm *ServerManager) run(ctx context.Context, bridge *Bridge) {
|
||||
eventCh, cancel := bridge.GetEvents()
|
||||
defer cancel()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
sm.handleClose(ctx, bridge)
|
||||
return
|
||||
|
||||
case evt := <-eventCh:
|
||||
switch evt.(type) {
|
||||
case events.ConnStatusDown:
|
||||
logrus.Info("Server Manager, network down stopping listeners")
|
||||
if err := sm.closeSMTPServer(bridge); err != nil {
|
||||
logrus.WithError(err).Error("Failed to close SMTP server")
|
||||
}
|
||||
|
||||
if err := sm.stopIMAPListener(bridge); err != nil {
|
||||
logrus.WithError(err)
|
||||
}
|
||||
case events.ConnStatusUp:
|
||||
logrus.Info("Server Manager, network up starting listeners")
|
||||
sm.handleLoadedUserCountChange(ctx, bridge)
|
||||
}
|
||||
|
||||
case request, ok := <-sm.requests.ReceiveCh():
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
switch r := request.Value().(type) {
|
||||
case *smRequestClose:
|
||||
sm.handleClose(ctx, bridge)
|
||||
request.Reply(ctx, nil, nil)
|
||||
return
|
||||
|
||||
case *smRequestRestartSMTP:
|
||||
err := sm.restartSMTP(bridge)
|
||||
request.Reply(ctx, nil, err)
|
||||
|
||||
case *smRequestRestartIMAP:
|
||||
err := sm.restartIMAP(ctx, bridge)
|
||||
request.Reply(ctx, nil, err)
|
||||
|
||||
case *smRequestAddIMAPUser:
|
||||
err := sm.handleAddIMAPUser(ctx, r.user)
|
||||
request.Reply(ctx, nil, err)
|
||||
if err == nil {
|
||||
sm.loadedUserCount++
|
||||
sm.handleLoadedUserCountChange(ctx, bridge)
|
||||
}
|
||||
|
||||
case *smRequestRemoveIMAPUser:
|
||||
err := sm.handleRemoveIMAPUser(ctx, r.user, r.withData)
|
||||
request.Reply(ctx, nil, err)
|
||||
if err == nil {
|
||||
sm.loadedUserCount--
|
||||
sm.handleLoadedUserCountChange(ctx, bridge)
|
||||
}
|
||||
|
||||
case *smRequestSetGluonDir:
|
||||
err := sm.handleSetGluonDir(ctx, bridge, r.dir)
|
||||
request.Reply(ctx, nil, err)
|
||||
|
||||
case *smRequestAddGluonUser:
|
||||
id, err := sm.handleAddGluonUser(ctx, r.conn, r.passphrase)
|
||||
request.Reply(ctx, id, err)
|
||||
|
||||
case *smRequestRemoveGluonUser:
|
||||
err := sm.handleRemoveGluonUser(ctx, r.userID)
|
||||
request.Reply(ctx, nil, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (sm *ServerManager) handleLoadedUserCountChange(ctx context.Context, bridge *Bridge) {
|
||||
logrus.Infof("Validating Listener State %v", sm.loadedUserCount)
|
||||
if sm.shouldStartServers() {
|
||||
if sm.imapListener == nil {
|
||||
if err := sm.serveIMAP(ctx, bridge); err != nil {
|
||||
logrus.WithError(err).Error("Failed to start IMAP server")
|
||||
}
|
||||
}
|
||||
|
||||
if sm.smtpListener == nil {
|
||||
if err := sm.restartSMTP(bridge); err != nil {
|
||||
logrus.WithError(err).Error("Failed to start SMTP server")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if sm.imapListener != nil {
|
||||
if err := sm.stopIMAPListener(bridge); err != nil {
|
||||
logrus.WithError(err).Error("Failed to stop IMAP server")
|
||||
}
|
||||
}
|
||||
|
||||
if sm.smtpListener != nil {
|
||||
if err := sm.closeSMTPServer(bridge); err != nil {
|
||||
logrus.WithError(err).Error("Failed to stop SMTP server")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (sm *ServerManager) handleClose(ctx context.Context, bridge *Bridge) {
|
||||
// Close the IMAP server.
|
||||
if err := sm.closeIMAPServer(ctx, bridge); err != nil {
|
||||
logrus.WithError(err).Error("Failed to close IMAP server")
|
||||
}
|
||||
|
||||
// Close the SMTP server.
|
||||
if err := sm.closeSMTPServer(bridge); err != nil {
|
||||
logrus.WithError(err).Error("Failed to close SMTP server")
|
||||
}
|
||||
}
|
||||
|
||||
func (sm *ServerManager) handleAddIMAPUser(ctx context.Context, user *user.User) error {
|
||||
if sm.imapServer == nil {
|
||||
return fmt.Errorf("no imap server instance running")
|
||||
}
|
||||
|
||||
imapConn, err := user.NewIMAPConnectors()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create IMAP connectors: %w", err)
|
||||
}
|
||||
|
||||
for addrID, imapConn := range imapConn {
|
||||
log := logrus.WithFields(logrus.Fields{
|
||||
"userID": user.ID(),
|
||||
"addrID": addrID,
|
||||
})
|
||||
|
||||
if gluonID, ok := user.GetGluonID(addrID); ok {
|
||||
log.WithField("gluonID", gluonID).Info("Loading existing IMAP user")
|
||||
|
||||
// Load the user, checking whether the DB was newly created.
|
||||
isNew, err := sm.imapServer.LoadUser(ctx, imapConn, gluonID, user.GluonKey())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load IMAP user: %w", err)
|
||||
}
|
||||
|
||||
if isNew {
|
||||
// If the DB was newly created, clear the sync status; gluon's DB was not found.
|
||||
logrus.Warn("IMAP user DB was newly created, clearing sync status")
|
||||
|
||||
// Remove the user from IMAP so we can clear the sync status.
|
||||
if err := sm.imapServer.RemoveUser(ctx, gluonID, false); err != nil {
|
||||
return fmt.Errorf("failed to remove IMAP user: %w", err)
|
||||
}
|
||||
|
||||
// Clear the sync status -- we need to resync all messages.
|
||||
if err := user.ClearSyncStatus(); err != nil {
|
||||
return fmt.Errorf("failed to clear sync status: %w", err)
|
||||
}
|
||||
|
||||
// Add the user back to the IMAP server.
|
||||
if isNew, err := sm.imapServer.LoadUser(ctx, imapConn, gluonID, user.GluonKey()); err != nil {
|
||||
return fmt.Errorf("failed to add IMAP user: %w", err)
|
||||
} else if isNew {
|
||||
panic("IMAP user should already have a database")
|
||||
}
|
||||
} else if status := user.GetSyncStatus(); !status.HasLabels {
|
||||
// Otherwise, the DB already exists -- if the labels are not yet synced, we need to re-create the DB.
|
||||
if err := sm.imapServer.RemoveUser(ctx, gluonID, true); err != nil {
|
||||
return fmt.Errorf("failed to remove old IMAP user: %w", err)
|
||||
}
|
||||
|
||||
if err := user.RemoveGluonID(addrID, gluonID); err != nil {
|
||||
return fmt.Errorf("failed to remove old IMAP user ID: %w", err)
|
||||
}
|
||||
|
||||
gluonID, err := sm.imapServer.AddUser(ctx, imapConn, user.GluonKey())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add IMAP user: %w", err)
|
||||
}
|
||||
|
||||
if err := user.SetGluonID(addrID, gluonID); err != nil {
|
||||
return fmt.Errorf("failed to set IMAP user ID: %w", err)
|
||||
}
|
||||
|
||||
log.WithField("gluonID", gluonID).Info("Re-created IMAP user")
|
||||
}
|
||||
} else {
|
||||
log.Info("Creating new IMAP user")
|
||||
|
||||
gluonID, err := sm.imapServer.AddUser(ctx, imapConn, user.GluonKey())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add IMAP user: %w", err)
|
||||
}
|
||||
|
||||
if err := user.SetGluonID(addrID, gluonID); err != nil {
|
||||
return fmt.Errorf("failed to set IMAP user ID: %w", err)
|
||||
}
|
||||
|
||||
log.WithField("gluonID", gluonID).Info("Created new IMAP user")
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger a sync for the user, if needed.
|
||||
user.TriggerSync()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sm *ServerManager) handleRemoveIMAPUser(ctx context.Context, user *user.User, withData bool) error {
|
||||
if sm.imapServer == nil {
|
||||
return fmt.Errorf("no imap server instance running")
|
||||
}
|
||||
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"userID": user.ID(),
|
||||
"withData": withData,
|
||||
}).Debug("Removing IMAP user")
|
||||
|
||||
for addrID, gluonID := range user.GetGluonIDs() {
|
||||
if err := sm.imapServer.RemoveUser(ctx, gluonID, withData); err != nil {
|
||||
return fmt.Errorf("failed to remove IMAP user: %w", err)
|
||||
}
|
||||
|
||||
if withData {
|
||||
if err := user.RemoveGluonID(addrID, gluonID); err != nil {
|
||||
return fmt.Errorf("failed to remove IMAP user ID: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createIMAPServer(bridge *Bridge) (*gluon.Server, error) {
|
||||
gluonDataDir, err := bridge.GetGluonDataDir()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get Gluon Database directory: %w", err)
|
||||
}
|
||||
|
||||
return newIMAPServer(
|
||||
bridge.vault.GetGluonCacheDir(),
|
||||
gluonDataDir,
|
||||
bridge.curVersion,
|
||||
bridge.tlsConfig,
|
||||
bridge.reporter,
|
||||
bridge.logIMAPClient,
|
||||
bridge.logIMAPServer,
|
||||
bridge.imapEventCh,
|
||||
bridge.tasks,
|
||||
bridge.uidValidityGenerator,
|
||||
bridge.panicHandler,
|
||||
)
|
||||
}
|
||||
|
||||
func createSMTPServer(bridge *Bridge) *smtp.Server {
|
||||
return newSMTPServer(bridge, bridge.tlsConfig, bridge.logSMTP)
|
||||
}
|
||||
|
||||
func (sm *ServerManager) closeSMTPServer(bridge *Bridge) error {
|
||||
// We close the listener ourselves even though it's also closed by smtpServer.Close().
|
||||
// This is because smtpServer.Serve() is called in a separate goroutine and might be executed
|
||||
// after we've already closed the server. However, go-smtp has a bug; it blocks on the listener
|
||||
// even after the server has been closed. So we close the listener ourselves to unblock it.
|
||||
|
||||
if sm.smtpListener != nil {
|
||||
logrus.Info("Closing SMTP Listener")
|
||||
if err := sm.smtpListener.Close(); err != nil {
|
||||
return fmt.Errorf("failed to close SMTP listener: %w", err)
|
||||
}
|
||||
|
||||
sm.smtpListener = nil
|
||||
}
|
||||
|
||||
if sm.smtpServer != nil {
|
||||
logrus.Info("Closing SMTP server")
|
||||
if err := sm.smtpServer.Close(); err != nil {
|
||||
logrus.WithError(err).Debug("Failed to close SMTP server (expected -- we close the listener ourselves)")
|
||||
}
|
||||
|
||||
sm.smtpServer = nil
|
||||
|
||||
bridge.publish(events.SMTPServerStopped{})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sm *ServerManager) closeIMAPServer(ctx context.Context, bridge *Bridge) error {
|
||||
if sm.imapListener != nil {
|
||||
logrus.Info("Closing IMAP Listener")
|
||||
|
||||
if err := sm.imapListener.Close(); err != nil {
|
||||
return fmt.Errorf("failed to close IMAP listener: %w", err)
|
||||
}
|
||||
|
||||
sm.imapListener = nil
|
||||
|
||||
bridge.publish(events.IMAPServerStopped{})
|
||||
}
|
||||
|
||||
if sm.imapServer != nil {
|
||||
logrus.Info("Closing IMAP server")
|
||||
if err := sm.imapServer.Close(ctx); err != nil {
|
||||
return fmt.Errorf("failed to close IMAP server: %w", err)
|
||||
}
|
||||
|
||||
sm.imapServer = nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sm *ServerManager) restartIMAP(ctx context.Context, bridge *Bridge) error {
|
||||
logrus.Info("Restarting IMAP server")
|
||||
|
||||
if sm.imapListener != nil {
|
||||
if err := sm.imapListener.Close(); err != nil {
|
||||
return fmt.Errorf("failed to close IMAP listener: %w", err)
|
||||
}
|
||||
|
||||
sm.imapListener = nil
|
||||
|
||||
bridge.publish(events.IMAPServerStopped{})
|
||||
}
|
||||
|
||||
if sm.shouldStartServers() {
|
||||
return sm.serveIMAP(ctx, bridge)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sm *ServerManager) restartSMTP(bridge *Bridge) error {
|
||||
logrus.Info("Restarting SMTP server")
|
||||
|
||||
if err := sm.closeSMTPServer(bridge); err != nil {
|
||||
return fmt.Errorf("failed to close SMTP: %w", err)
|
||||
}
|
||||
|
||||
bridge.publish(events.SMTPServerStopped{})
|
||||
|
||||
sm.smtpServer = newSMTPServer(bridge, bridge.tlsConfig, bridge.logSMTP)
|
||||
|
||||
if sm.shouldStartServers() {
|
||||
return sm.serveSMTP(bridge)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sm *ServerManager) serveSMTP(bridge *Bridge) error {
|
||||
port, err := func() (int, error) {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"port": bridge.vault.GetSMTPPort(),
|
||||
"ssl": bridge.vault.GetSMTPSSL(),
|
||||
}).Info("Starting SMTP server")
|
||||
|
||||
smtpListener, err := newListener(bridge.vault.GetSMTPPort(), bridge.vault.GetSMTPSSL(), bridge.tlsConfig)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to create SMTP listener: %w", err)
|
||||
}
|
||||
|
||||
sm.smtpListener = smtpListener
|
||||
|
||||
bridge.tasks.Once(func(context.Context) {
|
||||
if err := sm.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
|
||||
}
|
||||
|
||||
func (sm *ServerManager) serveIMAP(ctx context.Context, bridge *Bridge) error {
|
||||
port, err := func() (int, error) {
|
||||
if sm.imapServer == nil {
|
||||
return 0, fmt.Errorf("no IMAP server instance running")
|
||||
}
|
||||
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"port": bridge.vault.GetIMAPPort(),
|
||||
"ssl": bridge.vault.GetIMAPSSL(),
|
||||
}).Info("Starting IMAP server")
|
||||
|
||||
imapListener, err := newListener(bridge.vault.GetIMAPPort(), bridge.vault.GetIMAPSSL(), bridge.tlsConfig)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to create IMAP listener: %w", err)
|
||||
}
|
||||
|
||||
sm.imapListener = imapListener
|
||||
|
||||
if err := sm.imapServer.Serve(ctx, sm.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
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
bridge.publish(events.IMAPServerError{
|
||||
Error: err,
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
bridge.publish(events.IMAPServerReady{
|
||||
Port: port,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sm *ServerManager) stopIMAPListener(bridge *Bridge) error {
|
||||
logrus.Info("Stopping IMAP listener")
|
||||
if sm.imapListener != nil {
|
||||
if err := sm.imapListener.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sm.imapListener = nil
|
||||
|
||||
bridge.publish(events.IMAPServerStopped{})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sm *ServerManager) handleSetGluonDir(ctx context.Context, bridge *Bridge, newGluonDir string) error {
|
||||
return safe.RLockRet(func() error {
|
||||
currentGluonDir := bridge.GetGluonCacheDir()
|
||||
newGluonDir = filepath.Join(newGluonDir, "gluon")
|
||||
if newGluonDir == currentGluonDir {
|
||||
return fmt.Errorf("new gluon dir is the same as the old one")
|
||||
}
|
||||
|
||||
if err := sm.closeIMAPServer(context.Background(), bridge); err != nil {
|
||||
return fmt.Errorf("failed to close IMAP: %w", err)
|
||||
}
|
||||
|
||||
sm.loadedUserCount = 0
|
||||
|
||||
if err := bridge.moveGluonCacheDir(currentGluonDir, newGluonDir); err != nil {
|
||||
logrus.WithError(err).Error("failed to move GluonCacheDir")
|
||||
|
||||
if err := bridge.vault.SetGluonDir(currentGluonDir); err != nil {
|
||||
return fmt.Errorf("failed to revert GluonCacheDir: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
bridge.heartbeat.SetCacheLocation(newGluonDir)
|
||||
|
||||
gluonDataDir, err := bridge.GetGluonDataDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get Gluon Database directory: %w", err)
|
||||
}
|
||||
|
||||
imapServer, err := newIMAPServer(
|
||||
bridge.vault.GetGluonCacheDir(),
|
||||
gluonDataDir,
|
||||
bridge.curVersion,
|
||||
bridge.tlsConfig,
|
||||
bridge.reporter,
|
||||
bridge.logIMAPClient,
|
||||
bridge.logIMAPServer,
|
||||
bridge.imapEventCh,
|
||||
bridge.tasks,
|
||||
bridge.uidValidityGenerator,
|
||||
bridge.panicHandler,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create new IMAP server: %w", err)
|
||||
}
|
||||
|
||||
sm.imapServer = imapServer
|
||||
for _, bridgeUser := range bridge.users {
|
||||
if err := sm.handleAddIMAPUser(ctx, bridgeUser); err != nil {
|
||||
return fmt.Errorf("failed to add users to new IMAP server: %w", err)
|
||||
}
|
||||
sm.loadedUserCount++
|
||||
}
|
||||
|
||||
if sm.shouldStartServers() {
|
||||
if err := sm.serveIMAP(ctx, bridge); err != nil {
|
||||
return fmt.Errorf("failed to serve IMAP: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}, bridge.usersLock)
|
||||
}
|
||||
|
||||
func (sm *ServerManager) handleAddGluonUser(ctx context.Context, conn connector.Connector, passphrase []byte) (string, error) {
|
||||
if sm.imapServer == nil {
|
||||
return "", fmt.Errorf("no imap server instance running")
|
||||
}
|
||||
|
||||
return sm.imapServer.AddUser(ctx, conn, passphrase)
|
||||
}
|
||||
|
||||
func (sm *ServerManager) handleRemoveGluonUser(ctx context.Context, userID string) error {
|
||||
if sm.imapServer == nil {
|
||||
return fmt.Errorf("no imap server instance running")
|
||||
}
|
||||
|
||||
return sm.imapServer.RemoveUser(ctx, userID, true)
|
||||
}
|
||||
|
||||
func (sm *ServerManager) shouldStartServers() bool {
|
||||
return sm.loadedUserCount >= 1
|
||||
}
|
||||
|
||||
type smRequestClose struct{}
|
||||
|
||||
type smRequestRestartIMAP struct{}
|
||||
|
||||
type smRequestRestartSMTP struct{}
|
||||
|
||||
type smRequestAddIMAPUser struct {
|
||||
user *user.User
|
||||
}
|
||||
|
||||
type smRequestRemoveIMAPUser struct {
|
||||
user *user.User
|
||||
withData bool
|
||||
}
|
||||
|
||||
type smRequestSetGluonDir struct {
|
||||
dir string
|
||||
}
|
||||
|
||||
type smRequestAddGluonUser struct {
|
||||
conn connector.Connector
|
||||
passphrase []byte
|
||||
}
|
||||
|
||||
type smRequestRemoveGluonUser struct {
|
||||
userID string
|
||||
}
|
||||
179
internal/bridge/server_manager_test.go
Normal file
179
internal/bridge/server_manager_test.go
Normal file
@ -0,0 +1,179 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package bridge_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/go-proton-api/server"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestServerManager_NoLoadedUsersNoServers(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) {
|
||||
_, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
|
||||
require.Error(t, err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestServerManager_ServersStartAfterFirstConnectedUser(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) {
|
||||
imapWaiter := waitForIMAPServerReady(bridge)
|
||||
defer imapWaiter.Done()
|
||||
|
||||
smtpWaiter := waitForSMTPServerReady(bridge)
|
||||
defer smtpWaiter.Done()
|
||||
|
||||
_, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
imapWaiter.Wait()
|
||||
smtpWaiter.Wait()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestServerManager_ServersStopsAfterUserLogsOut(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) {
|
||||
imapWaiter := waitForIMAPServerReady(bridge)
|
||||
defer imapWaiter.Done()
|
||||
|
||||
smtpWaiter := waitForSMTPServerReady(bridge)
|
||||
defer smtpWaiter.Done()
|
||||
|
||||
userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
imapWaiter.Wait()
|
||||
smtpWaiter.Wait()
|
||||
|
||||
imapWaiterStopped := waitForIMAPServerStopped(bridge)
|
||||
defer imapWaiterStopped.Done()
|
||||
|
||||
require.NoError(t, bridge.LogoutUser(ctx, userID))
|
||||
|
||||
imapWaiterStopped.Wait()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestServerManager_ServersDoNotStopWhenThereIsStillOneActiveUser(t *testing.T) {
|
||||
otherPassword := []byte("bar")
|
||||
otherUser := "foo"
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
_, _, err := s.CreateUser(otherUser, otherPassword)
|
||||
require.NoError(t, err)
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
imapWaiter := waitForIMAPServerReady(bridge)
|
||||
defer imapWaiter.Done()
|
||||
|
||||
smtpWaiter := waitForSMTPServerReady(bridge)
|
||||
defer smtpWaiter.Done()
|
||||
|
||||
_, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
userIDOther, err := bridge.LoginFull(ctx, otherUser, otherPassword, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
imapWaiter.Wait()
|
||||
smtpWaiter.Wait()
|
||||
|
||||
evtCh, cancel := bridge.GetEvents(events.UserDeauth{})
|
||||
defer cancel()
|
||||
|
||||
require.NoError(t, s.RevokeUser(userIDOther))
|
||||
|
||||
waitForEvent(t, evtCh, events.UserDeauth{})
|
||||
|
||||
imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, imapClient.Logout())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestServerManager_ServersStartIfAtLeastOneUserIsLoggedIn(t *testing.T) {
|
||||
otherPassword := []byte("bar")
|
||||
otherUser := "foo"
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
userIDOther, _, err := s.CreateUser(otherUser, otherPassword)
|
||||
require.NoError(t, err)
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
_, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = bridge.LoginFull(ctx, otherUser, otherPassword, nil, nil)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
require.NoError(t, s.RevokeUser(userIDOther))
|
||||
|
||||
withBridgeWaitForServers(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, imapClient.Logout())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestServerManager_NetworkLossStopsServers(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) {
|
||||
imapWaiter := waitForIMAPServerReady(bridge)
|
||||
defer imapWaiter.Done()
|
||||
|
||||
smtpWaiter := waitForSMTPServerReady(bridge)
|
||||
defer smtpWaiter.Done()
|
||||
|
||||
imapWaiterStop := waitForIMAPServerStopped(bridge)
|
||||
defer imapWaiterStop.Done()
|
||||
|
||||
smtpWaiterStop := waitForSMTPServerStopped(bridge)
|
||||
defer smtpWaiterStop.Done()
|
||||
|
||||
_, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
imapWaiter.Wait()
|
||||
smtpWaiter.Wait()
|
||||
|
||||
netCtl.Disable()
|
||||
|
||||
imapWaiterStop.Wait()
|
||||
smtpWaiterStop.Wait()
|
||||
|
||||
netCtl.Enable()
|
||||
|
||||
imapWaiter.Wait()
|
||||
smtpWaiter.Wait()
|
||||
})
|
||||
})
|
||||
}
|
||||
@ -22,7 +22,6 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||
@ -46,6 +45,8 @@ func (bridge *Bridge) SetKeychainApp(helper string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
bridge.heartbeat.SetKeyChainPref(helper)
|
||||
|
||||
return vault.SetHelper(vaultDir, helper)
|
||||
}
|
||||
|
||||
@ -53,7 +54,7 @@ func (bridge *Bridge) GetIMAPPort() int {
|
||||
return bridge.vault.GetIMAPPort()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) SetIMAPPort(newPort int) error {
|
||||
func (bridge *Bridge) SetIMAPPort(ctx context.Context, newPort int) error {
|
||||
if newPort == bridge.vault.GetIMAPPort() {
|
||||
return nil
|
||||
}
|
||||
@ -62,14 +63,16 @@ func (bridge *Bridge) SetIMAPPort(newPort int) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return bridge.restartIMAP()
|
||||
bridge.heartbeat.SetIMAPPort(newPort)
|
||||
|
||||
return bridge.restartIMAP(ctx)
|
||||
}
|
||||
|
||||
func (bridge *Bridge) GetIMAPSSL() bool {
|
||||
return bridge.vault.GetIMAPSSL()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) SetIMAPSSL(newSSL bool) error {
|
||||
func (bridge *Bridge) SetIMAPSSL(ctx context.Context, newSSL bool) error {
|
||||
if newSSL == bridge.vault.GetIMAPSSL() {
|
||||
return nil
|
||||
}
|
||||
@ -78,14 +81,16 @@ func (bridge *Bridge) SetIMAPSSL(newSSL bool) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return bridge.restartIMAP()
|
||||
bridge.heartbeat.SetIMAPConnectionMode(newSSL)
|
||||
|
||||
return bridge.restartIMAP(ctx)
|
||||
}
|
||||
|
||||
func (bridge *Bridge) GetSMTPPort() int {
|
||||
return bridge.vault.GetSMTPPort()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) SetSMTPPort(newPort int) error {
|
||||
func (bridge *Bridge) SetSMTPPort(ctx context.Context, newPort int) error {
|
||||
if newPort == bridge.vault.GetSMTPPort() {
|
||||
return nil
|
||||
}
|
||||
@ -94,14 +99,16 @@ func (bridge *Bridge) SetSMTPPort(newPort int) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return bridge.restartSMTP()
|
||||
bridge.heartbeat.SetSMTPPort(newPort)
|
||||
|
||||
return bridge.restartSMTP(ctx)
|
||||
}
|
||||
|
||||
func (bridge *Bridge) GetSMTPSSL() bool {
|
||||
return bridge.vault.GetSMTPSSL()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) SetSMTPSSL(newSSL bool) error {
|
||||
func (bridge *Bridge) SetSMTPSSL(ctx context.Context, newSSL bool) error {
|
||||
if newSSL == bridge.vault.GetSMTPSSL() {
|
||||
return nil
|
||||
}
|
||||
@ -110,7 +117,9 @@ func (bridge *Bridge) SetSMTPSSL(newSSL bool) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return bridge.restartSMTP()
|
||||
bridge.heartbeat.SetSMTPConnectionMode(newSSL)
|
||||
|
||||
return bridge.restartSMTP(ctx)
|
||||
}
|
||||
|
||||
func (bridge *Bridge) GetGluonCacheDir() string {
|
||||
@ -122,61 +131,7 @@ func (bridge *Bridge) GetGluonDataDir() (string, error) {
|
||||
}
|
||||
|
||||
func (bridge *Bridge) SetGluonDir(ctx context.Context, newGluonDir string) error {
|
||||
return safe.RLockRet(func() error {
|
||||
currentGluonDir := bridge.GetGluonCacheDir()
|
||||
newGluonDir = filepath.Join(newGluonDir, "gluon")
|
||||
if newGluonDir == currentGluonDir {
|
||||
return fmt.Errorf("new gluon dir is the same as the old one")
|
||||
}
|
||||
|
||||
if err := bridge.closeIMAP(context.Background()); err != nil {
|
||||
return fmt.Errorf("failed to close IMAP: %w", 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 {
|
||||
return fmt.Errorf("failed to revert GluonCacheDir: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
gluonDataDir, err := bridge.GetGluonDataDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get Gluon Database directory: %w", err)
|
||||
}
|
||||
|
||||
imapServer, err := newIMAPServer(
|
||||
bridge.vault.GetGluonCacheDir(),
|
||||
gluonDataDir,
|
||||
bridge.curVersion,
|
||||
bridge.tlsConfig,
|
||||
bridge.reporter,
|
||||
bridge.logIMAPClient,
|
||||
bridge.logIMAPServer,
|
||||
bridge.imapEventCh,
|
||||
bridge.tasks,
|
||||
bridge.uidValidityGenerator,
|
||||
bridge.panicHandler,
|
||||
)
|
||||
if err != nil {
|
||||
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)
|
||||
return bridge.serverManager.SetGluonDir(ctx, newGluonDir)
|
||||
}
|
||||
|
||||
func (bridge *Bridge) moveGluonCacheDir(oldGluonDir, newGluonDir string) error {
|
||||
@ -207,6 +162,8 @@ func (bridge *Bridge) SetProxyAllowed(allowed bool) error {
|
||||
bridge.proxyCtl.DisallowProxy()
|
||||
}
|
||||
|
||||
bridge.heartbeat.SetDoh(allowed)
|
||||
|
||||
return bridge.vault.SetProxyAllowed(allowed)
|
||||
}
|
||||
|
||||
@ -220,6 +177,8 @@ func (bridge *Bridge) SetShowAllMail(show bool) error {
|
||||
user.SetShowAllMail(show)
|
||||
}
|
||||
|
||||
bridge.heartbeat.SetShowAllMail(show)
|
||||
|
||||
return bridge.vault.SetShowAllMail(show)
|
||||
}, bridge.usersLock)
|
||||
}
|
||||
@ -233,6 +192,8 @@ func (bridge *Bridge) SetAutostart(autostart bool) error {
|
||||
if err := bridge.vault.SetAutostart(autostart); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bridge.heartbeat.SetAutoStart(autostart)
|
||||
}
|
||||
|
||||
var err error
|
||||
@ -253,6 +214,10 @@ func (bridge *Bridge) SetAutostart(autostart bool) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (bridge *Bridge) GetUpdateRollout() float64 {
|
||||
return bridge.vault.GetUpdateRollout()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) GetAutoUpdate() bool {
|
||||
return bridge.vault.GetAutoUpdate()
|
||||
}
|
||||
@ -266,11 +231,28 @@ func (bridge *Bridge) SetAutoUpdate(autoUpdate bool) error {
|
||||
return err
|
||||
}
|
||||
|
||||
bridge.heartbeat.SetAutoUpdate(autoUpdate)
|
||||
|
||||
bridge.goUpdate()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bridge *Bridge) GetTelemetryDisabled() bool {
|
||||
return bridge.vault.GetTelemetryDisabled()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) SetTelemetryDisabled(isDisabled bool) error {
|
||||
if err := bridge.vault.SetTelemetryDisabled(isDisabled); err != nil {
|
||||
return err
|
||||
}
|
||||
// If telemetry is re-enabled locally, try to send the heartbeat.
|
||||
if !isDisabled {
|
||||
defer bridge.goHeartbeat()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bridge *Bridge) GetUpdateChannel() updater.Channel {
|
||||
return bridge.vault.GetUpdateChannel()
|
||||
}
|
||||
@ -284,6 +266,8 @@ func (bridge *Bridge) SetUpdateChannel(channel updater.Channel) error {
|
||||
return err
|
||||
}
|
||||
|
||||
bridge.heartbeat.SetBeta(channel)
|
||||
|
||||
bridge.goUpdate()
|
||||
|
||||
return nil
|
||||
@ -313,10 +297,11 @@ func (bridge *Bridge) SetColorScheme(colorScheme string) error {
|
||||
// Note: it does not clear the keychain. The only entry in the keychain is the vault password,
|
||||
// which we need at next startup to decrypt the vault.
|
||||
func (bridge *Bridge) FactoryReset(ctx context.Context) {
|
||||
useTelemetry := !bridge.GetTelemetryDisabled()
|
||||
// Delete all the users.
|
||||
safe.Lock(func() {
|
||||
for _, user := range bridge.users {
|
||||
bridge.logoutUser(ctx, user, true, true)
|
||||
bridge.logoutUser(ctx, user, true, true, useTelemetry)
|
||||
}
|
||||
}, bridge.usersLock)
|
||||
|
||||
|
||||
@ -57,7 +57,7 @@ func TestBridge_Settings_IMAPPort(t *testing.T) {
|
||||
curPort := bridge.GetIMAPPort()
|
||||
|
||||
// Set the port to 1144.
|
||||
require.NoError(t, bridge.SetIMAPPort(1144))
|
||||
require.NoError(t, bridge.SetIMAPPort(ctx, 1144))
|
||||
|
||||
// Get the new setting.
|
||||
require.Equal(t, 1144, bridge.GetIMAPPort())
|
||||
@ -75,7 +75,7 @@ func TestBridge_Settings_IMAPSSL(t *testing.T) {
|
||||
require.False(t, bridge.GetIMAPSSL())
|
||||
|
||||
// Enable IMAP SSL.
|
||||
require.NoError(t, bridge.SetIMAPSSL(true))
|
||||
require.NoError(t, bridge.SetIMAPSSL(ctx, true))
|
||||
|
||||
// Get the new setting.
|
||||
require.True(t, bridge.GetIMAPSSL())
|
||||
@ -89,7 +89,7 @@ func TestBridge_Settings_SMTPPort(t *testing.T) {
|
||||
curPort := bridge.GetSMTPPort()
|
||||
|
||||
// Set the port to 1024.
|
||||
require.NoError(t, bridge.SetSMTPPort(1024))
|
||||
require.NoError(t, bridge.SetSMTPPort(ctx, 1024))
|
||||
|
||||
// Get the new setting.
|
||||
require.Equal(t, 1024, bridge.GetSMTPPort())
|
||||
@ -107,7 +107,7 @@ func TestBridge_Settings_SMTPSSL(t *testing.T) {
|
||||
require.False(t, bridge.GetSMTPSSL())
|
||||
|
||||
// Enable SMTP SSL.
|
||||
require.NoError(t, bridge.SetSMTPSSL(true))
|
||||
require.NoError(t, bridge.SetSMTPSSL(ctx, true))
|
||||
|
||||
// Get the new setting.
|
||||
require.True(t, bridge.GetSMTPSSL())
|
||||
|
||||
@ -20,90 +20,16 @@ package bridge
|
||||
import (
|
||||
"context"
|
||||
"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"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
||||
"github.com/emersion/go-sasl"
|
||||
"github.com/emersion/go-smtp"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (bridge *Bridge) serveSMTP() error {
|
||||
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 0, 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")
|
||||
}
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (bridge *Bridge) restartSMTP() error {
|
||||
logrus.Info("Restarting SMTP server")
|
||||
|
||||
if err := bridge.closeSMTP(); err != nil {
|
||||
return fmt.Errorf("failed to close SMTP: %w", err)
|
||||
}
|
||||
|
||||
bridge.publish(events.SMTPServerStopped{})
|
||||
|
||||
bridge.smtpServer = newSMTPServer(bridge, bridge.tlsConfig, bridge.logSMTP)
|
||||
|
||||
return bridge.serveSMTP()
|
||||
}
|
||||
|
||||
// We close the listener ourselves even though it's also closed by smtpServer.Close().
|
||||
// This is because smtpServer.Serve() is called in a separate goroutine and might be executed
|
||||
// after we've already closed the server. However, go-smtp has a bug; it blocks on the listener
|
||||
// even after the server has been closed. So we close the listener ourselves to unblock it.
|
||||
func (bridge *Bridge) closeSMTP() error {
|
||||
logrus.Info("Closing SMTP server")
|
||||
|
||||
if bridge.smtpListener != nil {
|
||||
if err := bridge.smtpListener.Close(); err != nil {
|
||||
return fmt.Errorf("failed to close SMTP listener: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := bridge.smtpServer.Close(); err != nil {
|
||||
logrus.WithError(err).Debug("Failed to close SMTP server (expected -- we close the listener ourselves)")
|
||||
}
|
||||
|
||||
bridge.publish(events.SMTPServerStopped{})
|
||||
|
||||
return nil
|
||||
func (bridge *Bridge) restartSMTP(ctx context.Context) error {
|
||||
return bridge.serverManager.RestartSMTP(ctx)
|
||||
}
|
||||
|
||||
func newSMTPServer(bridge *Bridge, tlsConfig *tls.Config, logSMTP bool) *smtp.Server {
|
||||
|
||||
@ -18,11 +18,15 @@
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
|
||||
"github.com/emersion/go-smtp"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type smtpBackend struct {
|
||||
@ -54,10 +58,29 @@ func (s *smtpSession) AuthPlain(username, password string) error {
|
||||
s.userID = user.ID()
|
||||
s.authID = addrID
|
||||
|
||||
if strings.Contains(s.Bridge.GetCurrentUserAgent(), useragent.DefaultUserAgent) {
|
||||
s.Bridge.setUserAgent(useragent.UnknownClient, useragent.DefaultVersion)
|
||||
}
|
||||
|
||||
user.SendConfigStatusSuccess(context.Background())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("invalid username or password")
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"username": username,
|
||||
"pkg": "smtp",
|
||||
}).Error("Incorrect login credentials.")
|
||||
err := fmt.Errorf("invalid username or password")
|
||||
for _, user := range s.users {
|
||||
for _, mail := range user.Emails() {
|
||||
if mail == username {
|
||||
user.ReportConfigStatusFailure(err.Error())
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return err
|
||||
}, s.usersLock)
|
||||
}
|
||||
|
||||
@ -71,7 +94,7 @@ func (s *smtpSession) Logout() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *smtpSession) Mail(from string, opts *smtp.MailOptions) error {
|
||||
func (s *smtpSession) Mail(from string, _ *smtp.MailOptions) error {
|
||||
s.from = from
|
||||
return nil
|
||||
}
|
||||
@ -85,7 +108,7 @@ func (s *smtpSession) Rcpt(to string) error {
|
||||
}
|
||||
|
||||
func (s *smtpSession) Data(r io.Reader) error {
|
||||
return safe.RLockRet(func() error {
|
||||
err := safe.RLockRet(func() error {
|
||||
user, ok := s.users[s.userID]
|
||||
if !ok {
|
||||
return ErrNoSuchUser
|
||||
@ -93,4 +116,10 @@ func (s *smtpSession) Data(r io.Reader) error {
|
||||
|
||||
return user.SendMail(s.authID, s.from, s.to, r)
|
||||
}, s.usersLock)
|
||||
|
||||
if err != nil {
|
||||
logrus.WithField("pkg", "smtp").WithError(err).Error("Send mail failed.")
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@ -80,7 +80,7 @@ func TestBridge_Sync(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.True(t, info.State == bridge.Connected)
|
||||
|
||||
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
||||
defer func() { _ = client.Logout() }()
|
||||
@ -112,15 +112,6 @@ func TestBridge_Sync(t *testing.T) {
|
||||
info, err := b.GetUserInfo(userID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, info.State == bridge.Connected)
|
||||
|
||||
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
||||
defer func() { _ = client.Logout() }()
|
||||
|
||||
status, err := client.Select(`Folders/folder`, false)
|
||||
require.NoError(t, err)
|
||||
require.Less(t, status.Messages, uint32(numMsg))
|
||||
}
|
||||
|
||||
// Remove the network limit, allowing the sync to finish.
|
||||
@ -136,7 +127,7 @@ func TestBridge_Sync(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.True(t, info.State == bridge.Connected)
|
||||
|
||||
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
||||
defer func() { _ = client.Logout() }()
|
||||
@ -187,7 +178,7 @@ func _TestBridge_Sync_BadMessage(t *testing.T) { //nolint:unused,deadcode
|
||||
require.NoError(t, err)
|
||||
require.True(t, info.State == bridge.Connected)
|
||||
|
||||
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
||||
defer func() { _ = client.Logout() }()
|
||||
@ -273,15 +264,6 @@ func TestBridge_SyncWithOngoingEvents(t *testing.T) {
|
||||
info, err := b.GetUserInfo(userID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, info.State == bridge.Connected)
|
||||
|
||||
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
||||
defer func() { _ = client.Logout() }()
|
||||
|
||||
status, err := client.Select(`Folders/folder`, false)
|
||||
require.NoError(t, err)
|
||||
require.Less(t, status.Messages, uint32(numMsg))
|
||||
}
|
||||
|
||||
// Create a new mailbox and move that last 1/3 of the messages into it to simulate user
|
||||
@ -311,7 +293,7 @@ func TestBridge_SyncWithOngoingEvents(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.True(t, info.State == bridge.Connected)
|
||||
|
||||
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
||||
defer func() { _ = client.Logout() }()
|
||||
@ -417,6 +399,10 @@ func createNumMessages(ctx context.Context, t *testing.T, c *proton.Client, addr
|
||||
}
|
||||
|
||||
func createMessages(ctx context.Context, t *testing.T, c *proton.Client, addrID, labelID string, messages ...[]byte) []string {
|
||||
return createMessagesWithFlags(ctx, t, c, addrID, labelID, 0, messages...)
|
||||
}
|
||||
|
||||
func createMessagesWithFlags(ctx context.Context, t *testing.T, c *proton.Client, addrID, labelID string, flags proton.MessageFlag, messages ...[]byte) []string {
|
||||
user, err := c.GetUser(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
@ -435,6 +421,13 @@ func createMessages(ctx context.Context, t *testing.T, c *proton.Client, addrID,
|
||||
_, ok := addrKRs[addrID]
|
||||
require.True(t, ok)
|
||||
|
||||
var msgFlags proton.MessageFlag
|
||||
if flags == 0 {
|
||||
msgFlags = proton.MessageFlagReceived
|
||||
} else {
|
||||
msgFlags = flags
|
||||
}
|
||||
|
||||
str, err := c.ImportMessages(
|
||||
ctx,
|
||||
addrKRs[addrID],
|
||||
@ -445,7 +438,7 @@ func createMessages(ctx context.Context, t *testing.T, c *proton.Client, addrID,
|
||||
Metadata: proton.ImportMetadata{
|
||||
AddressID: addrID,
|
||||
LabelIDs: []string{labelID},
|
||||
Flags: proton.MessageFlagReceived,
|
||||
Flags: msgFlags,
|
||||
},
|
||||
Message: message,
|
||||
}
|
||||
|
||||
82
internal/bridge/sync_unix_test.go
Normal file
82
internal/bridge/sync_unix_test.go
Normal file
@ -0,0 +1,82 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//go:build !windows
|
||||
|
||||
package bridge_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/go-proton-api/server"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Disabled due to flakyness.
|
||||
func _TestBridge_SyncExistsWithErrorWhenTooManyFilesAreOpen(t *testing.T) { //nolint:unused
|
||||
var rlimitCurrent syscall.Rlimit
|
||||
|
||||
require.NoError(t, syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rlimitCurrent))
|
||||
|
||||
// Restore RLimit for Process at the end of this test
|
||||
defer func() {
|
||||
require.NoError(t, syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rlimitCurrent))
|
||||
}()
|
||||
|
||||
rlimit := syscall.Rlimit{
|
||||
Max: 100,
|
||||
Cur: 100,
|
||||
}
|
||||
|
||||
require.NoError(t, syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rlimit))
|
||||
|
||||
numMsg := 1 << 8
|
||||
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
userID, addrID, err := s.CreateUser("imap", password)
|
||||
require.NoError(t, err)
|
||||
|
||||
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
|
||||
require.NoError(t, err)
|
||||
|
||||
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
|
||||
createNumMessages(ctx, t, c, addrID, labelID, numMsg)
|
||||
})
|
||||
|
||||
// The initial user should be fully synced.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
syncCh, done := bridge.GetEvents(events.SyncFailed{})
|
||||
defer done()
|
||||
|
||||
userID, err := bridge.LoginFull(ctx, "imap", password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
evt := <-syncCh
|
||||
switch e := evt.(type) {
|
||||
case events.SyncFailed:
|
||||
require.Equal(t, userID, e.UserID)
|
||||
default:
|
||||
require.Fail(t, "Expected events.SyncFailed{}")
|
||||
}
|
||||
})
|
||||
}, server.WithTLS(false))
|
||||
}
|
||||
@ -28,6 +28,7 @@ type Locator interface {
|
||||
ProvideLogsPath() (string, error)
|
||||
ProvideGluonCachePath() (string, error)
|
||||
ProvideGluonDataPath() (string, error)
|
||||
ProvideStatsPath() (string, error)
|
||||
GetLicenseFilePath() string
|
||||
GetDependencyLicensesLink() string
|
||||
Clear(...string) error
|
||||
@ -38,6 +39,8 @@ type Identifier interface {
|
||||
HasClient() bool
|
||||
SetClient(name, version string)
|
||||
SetPlatform(platform string)
|
||||
SetClientString(client string)
|
||||
GetClientString() string
|
||||
}
|
||||
|
||||
type ProxyController interface {
|
||||
|
||||
@ -229,7 +229,7 @@ func (bridge *Bridge) LogoutUser(ctx context.Context, userID string) error {
|
||||
return ErrNoSuchUser
|
||||
}
|
||||
|
||||
bridge.logoutUser(ctx, user, true, false)
|
||||
bridge.logoutUser(ctx, user, true, false, false)
|
||||
|
||||
bridge.publish(events.UserLoggedOut{
|
||||
UserID: userID,
|
||||
@ -249,7 +249,7 @@ func (bridge *Bridge) DeleteUser(ctx context.Context, userID string) error {
|
||||
}
|
||||
|
||||
if user, ok := bridge.users[userID]; ok {
|
||||
bridge.logoutUser(ctx, user, true, true)
|
||||
bridge.logoutUser(ctx, user, true, true, !bridge.GetTelemetryDisabled())
|
||||
}
|
||||
|
||||
if err := bridge.vault.DeleteUser(userID); err != nil {
|
||||
@ -295,6 +295,15 @@ func (bridge *Bridge) SetAddressMode(ctx context.Context, userID string, mode va
|
||||
AddressMode: mode,
|
||||
})
|
||||
|
||||
var splitMode = false
|
||||
for _, user := range bridge.users {
|
||||
if user.GetAddressMode() == vault.SplitMode {
|
||||
splitMode = true
|
||||
break
|
||||
}
|
||||
}
|
||||
bridge.heartbeat.SetSplitMode(splitMode)
|
||||
|
||||
return nil
|
||||
}, bridge.usersLock)
|
||||
}
|
||||
@ -342,7 +351,7 @@ func (bridge *Bridge) SendBadEventUserFeedback(_ context.Context, userID string,
|
||||
logrus.WithError(rerr).Error("Failed to report feedback failure")
|
||||
}
|
||||
|
||||
bridge.logoutUser(ctx, user, true, false)
|
||||
bridge.logoutUser(ctx, user, true, false, false)
|
||||
|
||||
bridge.publish(events.UserLoggedOut{
|
||||
UserID: userID,
|
||||
@ -399,7 +408,7 @@ func (bridge *Bridge) loadUsers(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Info("Loading connected user")
|
||||
log.WithField("mode", user.AddressMode()).Info("Loading connected user")
|
||||
|
||||
bridge.publish(events.UserLoading{
|
||||
UserID: user.UserID(),
|
||||
@ -510,6 +519,14 @@ func (bridge *Bridge) addUserWithVault(
|
||||
apiUser proton.User,
|
||||
vault *vault.User,
|
||||
) error {
|
||||
statsPath, err := bridge.locator.ProvideStatsPath()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get Statistics directory: %w", err)
|
||||
}
|
||||
|
||||
// re-set SyncStatus if database need to be re-synced for migration.
|
||||
bridge.migrateUser(vault)
|
||||
|
||||
user, err := user.New(
|
||||
ctx,
|
||||
vault,
|
||||
@ -519,6 +536,8 @@ func (bridge *Bridge) addUserWithVault(
|
||||
bridge.panicHandler,
|
||||
bridge.vault.GetShowAllMail(),
|
||||
bridge.vault.GetMaxSyncMemory(),
|
||||
statsPath,
|
||||
bridge,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create user: %w", err)
|
||||
@ -550,7 +569,7 @@ func (bridge *Bridge) addUserWithVault(
|
||||
// As such, if we find this ID in the context, we should use it to update our user agent.
|
||||
client.AddPreRequestHook(func(_ *resty.Client, r *resty.Request) error {
|
||||
if imapID, ok := imap.GetIMAPIDFromContext(r.Context()); ok {
|
||||
bridge.identifier.SetClient(imapID.Name, imapID.Version)
|
||||
bridge.setUserAgent(imapID.Name, imapID.Version)
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -559,8 +578,12 @@ func (bridge *Bridge) addUserWithVault(
|
||||
// Finally, save the user in the bridge.
|
||||
safe.Lock(func() {
|
||||
bridge.users[apiUser.ID] = user
|
||||
bridge.heartbeat.SetNbAccount(len(bridge.users))
|
||||
}, bridge.usersLock)
|
||||
|
||||
// As we need at least one user to send heartbeat, try to send it.
|
||||
defer bridge.goHeartbeat()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -571,35 +594,18 @@ func (bridge *Bridge) newVaultUser(
|
||||
authUID, authRef string,
|
||||
saltedKeyPass []byte,
|
||||
) (*vault.User, bool, error) {
|
||||
if !bridge.vault.HasUser(apiUser.ID) {
|
||||
user, err := bridge.vault.AddUser(apiUser.ID, apiUser.Name, apiUser.Email, authUID, authRef, saltedKeyPass)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("failed to add user to vault: %w", err)
|
||||
}
|
||||
|
||||
return user, true, nil
|
||||
}
|
||||
|
||||
user, err := bridge.vault.NewUser(apiUser.ID)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
if err := user.SetAuth(authUID, authRef); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
if err := user.SetKeyPass(saltedKeyPass); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
return user, false, nil
|
||||
return bridge.vault.GetOrAddUser(apiUser.ID, apiUser.Name, apiUser.Email, authUID, authRef, saltedKeyPass)
|
||||
}
|
||||
|
||||
// logout logs out the given user, optionally logging them out from the API too.
|
||||
func (bridge *Bridge) logoutUser(ctx context.Context, user *user.User, withAPI, withData bool) {
|
||||
func (bridge *Bridge) logoutUser(ctx context.Context, user *user.User, withAPI, withData, withTelemetry bool) {
|
||||
defer delete(bridge.users, user.ID())
|
||||
|
||||
// if this is actually a remove account
|
||||
if withData && withAPI {
|
||||
user.SendConfigStatusAbort(ctx, withTelemetry)
|
||||
}
|
||||
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"userID": user.ID(),
|
||||
"withAPI": withAPI,
|
||||
@ -614,6 +620,8 @@ func (bridge *Bridge) logoutUser(ctx context.Context, user *user.User, withAPI,
|
||||
logrus.WithError(err).Error("Failed to logout user")
|
||||
}
|
||||
|
||||
bridge.heartbeat.SetNbAccount(len(bridge.users))
|
||||
|
||||
user.Close()
|
||||
}
|
||||
|
||||
|
||||
@ -76,8 +76,6 @@ func TestBridge_User_RefreshEvent(t *testing.T) {
|
||||
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()
|
||||
|
||||
@ -141,6 +139,9 @@ func test_badMessage_badEvent(userFeedback func(t *testing.T, ctx context.Contex
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
smtpWaiter := waitForSMTPServerReady(bridge)
|
||||
defer smtpWaiter.Done()
|
||||
|
||||
userLoginAndSync(ctx, t, bridge, "user", password)
|
||||
|
||||
var messageIDs []string
|
||||
@ -176,6 +177,8 @@ func test_badMessage_badEvent(userFeedback func(t *testing.T, ctx context.Contex
|
||||
|
||||
userFeedback(t, ctx, bridge, badUserID)
|
||||
|
||||
smtpWaiter.Wait()
|
||||
|
||||
userContinueEventProcess(ctx, t, s, bridge)
|
||||
})
|
||||
})
|
||||
@ -194,6 +197,9 @@ func TestBridge_User_BadMessage_NoBadEvent(t *testing.T) {
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
smtpWaiter := waitForSMTPServerReady(bridge)
|
||||
defer smtpWaiter.Done()
|
||||
|
||||
userLoginAndSync(ctx, t, bridge, "user", password)
|
||||
|
||||
var messageIDs []string
|
||||
@ -217,6 +223,7 @@ func TestBridge_User_BadMessage_NoBadEvent(t *testing.T) {
|
||||
require.NoError(t, c.DeleteMessage(ctx, messageIDs...))
|
||||
})
|
||||
|
||||
smtpWaiter.Wait()
|
||||
userContinueEventProcess(ctx, t, s, bridge)
|
||||
})
|
||||
})
|
||||
@ -412,6 +419,17 @@ func TestBridge_User_DropConn_NoBadEvent(t *testing.T) {
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
var count int32
|
||||
// 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 atomic.AddInt32(&count, 1) < 10 {
|
||||
dropListener.DropAll()
|
||||
}
|
||||
}
|
||||
|
||||
return 0, false
|
||||
})
|
||||
userLoginAndSync(ctx, t, bridge, "user", password)
|
||||
|
||||
mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).AnyTimes()
|
||||
@ -421,36 +439,82 @@ func TestBridge_User_DropConn_NoBadEvent(t *testing.T) {
|
||||
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()))
|
||||
cli, err := eventuallyDial(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() }()
|
||||
require.NoError(t, cli.Login(info.Addresses[0], string(info.BridgePass)))
|
||||
defer func() { _ = cli.Logout() }()
|
||||
|
||||
// The IMAP client will eventually see 20 messages.
|
||||
require.Eventually(t, func() bool {
|
||||
status, err := client.Status("INBOX", []imap.StatusItem{imap.StatusMessages})
|
||||
status, err := cli.Status("INBOX", []imap.StatusItem{imap.StatusMessages})
|
||||
return err == nil && status.Messages == 20
|
||||
}, 10*time.Second, 100*time.Millisecond)
|
||||
})
|
||||
}, server.WithListener(dropListener))
|
||||
}
|
||||
|
||||
func TestBridge_User_UpdateDraft(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
// Create a bridge user.
|
||||
_, _, err := s.CreateUser("user", password)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Initially sync the user.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
userLoginAndSync(ctx, t, bridge, "user", password)
|
||||
})
|
||||
|
||||
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||
user, err := c.GetUser(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
addrs, err := c.GetAddresses(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
salts, err := c.GetSalts(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
keyPass, err := salts.SaltForKey(password, user.Keys.Primary().ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, addrKRs, err := proton.Unlock(user, addrs, keyPass, async.NoopPanicHandler{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a draft (generating a "create draft message" event).
|
||||
draft, err := c.CreateDraft(ctx, addrKRs[addrs[0].ID], proton.CreateDraftReq{
|
||||
Message: proton.DraftTemplate{
|
||||
Subject: "subject",
|
||||
Sender: &mail.Address{Name: "sender", Address: addrs[0].Email},
|
||||
Body: "body",
|
||||
MIMEType: rfc822.TextPlain,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, draft.ReplyTos)
|
||||
|
||||
// Process those events
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
userContinueEventProcess(ctx, t, s, bridge)
|
||||
})
|
||||
|
||||
// Update the draft (generating an "update draft message" event).
|
||||
draft2, err := c.UpdateDraft(ctx, draft.ID, addrKRs[addrs[0].ID], proton.UpdateDraftReq{
|
||||
Message: proton.DraftTemplate{
|
||||
Subject: "subject 2",
|
||||
Sender: &mail.Address{Name: "sender", Address: addrs[0].Email},
|
||||
Body: "body 2",
|
||||
MIMEType: rfc822.TextPlain,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, draft2.ReplyTos)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_User_UpdateDraftAndCreateOtherMessage(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
// Create a bridge user.
|
||||
@ -579,12 +643,12 @@ func TestBridge_User_SendDraftRemoveDraftFlag(t *testing.T) {
|
||||
info, err := bridge.QueryUserInfo("user")
|
||||
require.NoError(t, err)
|
||||
|
||||
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
|
||||
cli, err := eventuallyDial(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() }()
|
||||
require.NoError(t, cli.Login(info.Addresses[0], string(info.BridgePass)))
|
||||
defer func() { _ = cli.Logout() }()
|
||||
|
||||
messages, err := clientFetch(client, "Drafts")
|
||||
messages, err := clientFetch(cli, "Drafts")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, messages, 1)
|
||||
require.Contains(t, messages[0].Flags, imap.DraftFlag)
|
||||
@ -618,12 +682,12 @@ func TestBridge_User_SendDraftRemoveDraftFlag(t *testing.T) {
|
||||
info, err := bridge.QueryUserInfo("user")
|
||||
require.NoError(t, err)
|
||||
|
||||
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
|
||||
cli, err := eventuallyDial(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() }()
|
||||
require.NoError(t, cli.Login(info.Addresses[0], string(info.BridgePass)))
|
||||
defer func() { _ = cli.Logout() }()
|
||||
|
||||
messages, err := clientFetch(client, "Sent")
|
||||
messages, err := clientFetch(cli, "Sent")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, messages, 1)
|
||||
require.NotContains(t, messages[0].Flags, imap.DraftFlag)
|
||||
@ -712,15 +776,24 @@ func TestBridge_User_CreateDisabledAddress(t *testing.T) {
|
||||
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) {
|
||||
imapWaiter := waitForIMAPServerReady(bridge)
|
||||
defer imapWaiter.Done()
|
||||
|
||||
smtpWaiter := waitForSMTPServerReady(bridge)
|
||||
defer smtpWaiter.Done()
|
||||
|
||||
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()))
|
||||
imapWaiter.Wait()
|
||||
smtpWaiter.Wait()
|
||||
|
||||
cli, err := eventuallyDial(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() }()
|
||||
require.NoError(t, cli.Login(info.Addresses[0], string(info.BridgePass)))
|
||||
defer func() { _ = cli.Logout() }()
|
||||
|
||||
withClient(ctx, t, s, username, password, func(ctx context.Context, c *proton.Client) {
|
||||
parentName := uuid.NewString()
|
||||
@ -736,7 +809,7 @@ func TestBridge_User_HandleParentLabelRename(t *testing.T) {
|
||||
|
||||
// Wait for the parent folder to be created.
|
||||
require.Eventually(t, func() bool {
|
||||
return xslices.IndexFunc(clientList(client), func(mailbox *imap.MailboxInfo) bool {
|
||||
return xslices.IndexFunc(clientList(cli), func(mailbox *imap.MailboxInfo) bool {
|
||||
return mailbox.Name == fmt.Sprintf("Folders/%v", parentName)
|
||||
}) >= 0
|
||||
}, 100*user.EventPeriod, user.EventPeriod)
|
||||
@ -753,7 +826,7 @@ func TestBridge_User_HandleParentLabelRename(t *testing.T) {
|
||||
|
||||
// Wait for the parent folder to be created.
|
||||
require.Eventually(t, func() bool {
|
||||
return xslices.IndexFunc(clientList(client), func(mailbox *imap.MailboxInfo) bool {
|
||||
return xslices.IndexFunc(clientList(cli), func(mailbox *imap.MailboxInfo) bool {
|
||||
return mailbox.Name == fmt.Sprintf("Folders/%v/%v", parentName, childName)
|
||||
}) >= 0
|
||||
}, 100*user.EventPeriod, user.EventPeriod)
|
||||
@ -768,14 +841,14 @@ func TestBridge_User_HandleParentLabelRename(t *testing.T) {
|
||||
|
||||
// Wait for the parent folder to be renamed.
|
||||
require.Eventually(t, func() bool {
|
||||
return xslices.IndexFunc(clientList(client), func(mailbox *imap.MailboxInfo) bool {
|
||||
return xslices.IndexFunc(clientList(cli), 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 xslices.IndexFunc(clientList(cli), func(mailbox *imap.MailboxInfo) bool {
|
||||
return mailbox.Name == fmt.Sprintf("Folders/%v/%v", newParentName, childName)
|
||||
}) >= 0
|
||||
}, 100*user.EventPeriod, user.EventPeriod)
|
||||
@ -827,10 +900,10 @@ func userContinueEventProcess(
|
||||
info, err := bridge.QueryUserInfo("user")
|
||||
require.NoError(t, err)
|
||||
|
||||
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
|
||||
cli, err := eventuallyDial(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() }()
|
||||
require.NoError(t, cli.Login(info.Addresses[0], string(info.BridgePass)))
|
||||
defer func() { _ = cli.Logout() }()
|
||||
|
||||
randomLabel := uuid.NewString()
|
||||
|
||||
@ -845,8 +918,21 @@ func userContinueEventProcess(
|
||||
|
||||
// Wait for the label to be created.
|
||||
require.Eventually(t, func() bool {
|
||||
return xslices.IndexFunc(clientList(client), func(mailbox *imap.MailboxInfo) bool {
|
||||
return xslices.IndexFunc(clientList(cli), func(mailbox *imap.MailboxInfo) bool {
|
||||
return mailbox.Name == "Labels/"+randomLabel
|
||||
}) >= 0
|
||||
}, 100*user.EventPeriod, user.EventPeriod)
|
||||
}
|
||||
|
||||
func eventuallyDial(addr string) (cli *client.Client, err error) {
|
||||
var sleep = 1 * time.Second
|
||||
for i := 0; i < 5; i++ {
|
||||
cli, err := client.Dial(addr)
|
||||
if err == nil {
|
||||
return cli, nil
|
||||
}
|
||||
time.Sleep(sleep)
|
||||
sleep *= 2
|
||||
}
|
||||
return nil, fmt.Errorf("after 5 attempts, last error: %s", err)
|
||||
}
|
||||
|
||||
@ -75,11 +75,7 @@ func (bridge *Bridge) handleUserAddressCreated(ctx context.Context, user *user.U
|
||||
return nil
|
||||
}
|
||||
|
||||
if bridge.imapServer == nil {
|
||||
return fmt.Errorf("no imap server instance running")
|
||||
}
|
||||
|
||||
gluonID, err := bridge.imapServer.AddUser(ctx, user.NewIMAPConnector(event.AddressID), user.GluonKey())
|
||||
gluonID, err := bridge.serverManager.AddGluonUser(ctx, user.NewIMAPConnector(event.AddressID), user.GluonKey())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add user to IMAP server: %w", err)
|
||||
}
|
||||
@ -96,7 +92,7 @@ func (bridge *Bridge) handleUserAddressEnabled(ctx context.Context, user *user.U
|
||||
return nil
|
||||
}
|
||||
|
||||
gluonID, err := bridge.imapServer.AddUser(ctx, user.NewIMAPConnector(event.AddressID), user.GluonKey())
|
||||
gluonID, err := bridge.serverManager.AddGluonUser(ctx, user.NewIMAPConnector(event.AddressID), user.GluonKey())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add user to IMAP server: %w", err)
|
||||
}
|
||||
@ -118,7 +114,7 @@ func (bridge *Bridge) handleUserAddressDisabled(ctx context.Context, user *user.
|
||||
return fmt.Errorf("gluon ID not found for address %s", event.AddressID)
|
||||
}
|
||||
|
||||
if err := bridge.imapServer.RemoveUser(ctx, gluonID, true); err != nil {
|
||||
if err := bridge.serverManager.RemoveGluonUser(ctx, gluonID); err != nil {
|
||||
return fmt.Errorf("failed to remove user from IMAP server: %w", err)
|
||||
}
|
||||
|
||||
@ -134,16 +130,12 @@ func (bridge *Bridge) handleUserAddressDeleted(ctx context.Context, user *user.U
|
||||
return nil
|
||||
}
|
||||
|
||||
if bridge.imapServer == nil {
|
||||
return fmt.Errorf("no imap server instance running")
|
||||
}
|
||||
|
||||
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 {
|
||||
if err := bridge.serverManager.handleRemoveGluonUser(ctx, gluonID); err != nil {
|
||||
return fmt.Errorf("failed to remove user from IMAP server: %w", err)
|
||||
}
|
||||
|
||||
@ -174,7 +166,8 @@ func (bridge *Bridge) handleUserRefreshed(ctx context.Context, user *user.User,
|
||||
|
||||
func (bridge *Bridge) handleUserDeauth(ctx context.Context, user *user.User) {
|
||||
safe.Lock(func() {
|
||||
bridge.logoutUser(ctx, user, false, false)
|
||||
bridge.logoutUser(ctx, user, false, false, false)
|
||||
user.ReportConfigStatusFailure("User deauth.")
|
||||
}, bridge.usersLock)
|
||||
}
|
||||
|
||||
|
||||
@ -28,10 +28,8 @@ import (
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/go-proton-api/server"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||
mocksPkg "github.com/ProtonMail/proton-bridge/v3/internal/bridge/mocks"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@ -678,11 +676,6 @@ func TestBridge_UserInfo_Alias(t *testing.T) {
|
||||
func TestBridge_User_Refresh(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) {
|
||||
mocks.Reporter.EXPECT().ReportMessageWithContext(
|
||||
gomock.Eq("Warning: refresh occurred"),
|
||||
mocksPkg.NewRefreshContextMatcher(proton.RefreshAll),
|
||||
).Return(nil)
|
||||
|
||||
// Get a channel of sync started events.
|
||||
syncStartCh, done := chToType[events.Event, events.SyncStarted](bridge.GetEvents(events.SyncStarted{}))
|
||||
defer done()
|
||||
@ -708,7 +701,26 @@ func TestBridge_User_Refresh(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_User_GetAddresses(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)
|
||||
addrID2, err := s.CreateAddress(userID, "user@external.com", []byte("password"))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, s.ChangeAddressType(userID, addrID2, proton.AddressTypeExternal))
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
userLoginAndSync(ctx, t, bridge, "user", password)
|
||||
info, err := bridge.GetUserInfo(userID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(info.Addresses))
|
||||
require.Equal(t, info.Addresses[0], "user@proton.local")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// getErr returns the error that was passed to it.
|
||||
func getErr[T any](val T, err error) error {
|
||||
func getErr[T any](_ T, err error) error {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -17,69 +17,140 @@
|
||||
|
||||
package certs
|
||||
|
||||
import (
|
||||
"os"
|
||||
/*
|
||||
#cgo CFLAGS: -x objective-c
|
||||
#cgo LDFLAGS: -framework Foundation -framework Security
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <Security/Security.h>
|
||||
|
||||
"golang.org/x/sys/execabs"
|
||||
|
||||
int installTrustedCert(char const *bytes, unsigned long long length) {
|
||||
if (length == 0) {
|
||||
return errSecInvalidData;
|
||||
}
|
||||
|
||||
NSData *der = [NSData dataWithBytes:bytes length:length];
|
||||
|
||||
// Step 1. Import the certificate in the keychain.
|
||||
SecCertificateRef cert = SecCertificateCreateWithData(NULL, (CFDataRef) der);
|
||||
NSDictionary* addQuery = @{
|
||||
(id)kSecValueRef: (__bridge id) cert,
|
||||
(id)kSecClass: (id)kSecClassCertificate,
|
||||
};
|
||||
|
||||
OSStatus status = SecItemAdd((__bridge CFDictionaryRef) addQuery, NULL);
|
||||
if ((errSecSuccess != status) && (errSecDuplicateItem != status)) {
|
||||
CFRelease(cert);
|
||||
return status;
|
||||
}
|
||||
|
||||
// Step 2. Set the trust for the certificate.
|
||||
SecPolicyRef policy = SecPolicyCreateSSL(true, NULL); // we limit our trust to SSL
|
||||
NSDictionary *trustSettings = @{
|
||||
(id)kSecTrustSettingsResult: [NSNumber numberWithInt:kSecTrustSettingsResultTrustRoot],
|
||||
(id)kSecTrustSettingsPolicy: (__bridge id) policy,
|
||||
};
|
||||
status = SecTrustSettingsSetTrustSettings(cert, kSecTrustSettingsDomainUser, (__bridge CFTypeRef)(trustSettings));
|
||||
CFRelease(policy);
|
||||
CFRelease(cert);
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
|
||||
int removeTrustedCert(char const *bytes, unsigned long long length) {
|
||||
if (0 == length) {
|
||||
return errSecInvalidData;
|
||||
}
|
||||
|
||||
NSData *der = [NSData dataWithBytes: bytes length: length];
|
||||
SecCertificateRef cert = SecCertificateCreateWithData(NULL, (CFDataRef) der);
|
||||
|
||||
// Step 1. Unset the trust for the certificate.
|
||||
SecPolicyRef policy = SecPolicyCreateSSL(true, NULL);
|
||||
NSDictionary * trustSettings = @{
|
||||
(id)kSecTrustSettingsResult: [NSNumber numberWithInt:kSecTrustSettingsResultUnspecified],
|
||||
(id)kSecTrustSettingsPolicy: (__bridge id) policy,
|
||||
};
|
||||
OSStatus status = SecTrustSettingsSetTrustSettings(cert, kSecTrustSettingsDomainUser, (__bridge CFTypeRef)(trustSettings));
|
||||
CFRelease(policy);
|
||||
if (errSecSuccess != status) {
|
||||
CFRelease(cert);
|
||||
return status;
|
||||
}
|
||||
|
||||
// Step 2. Remove the certificate from the keychain.
|
||||
NSDictionary *query = @{ (id)kSecClass: (id)kSecClassCertificate,
|
||||
(id)kSecMatchItemList: @[(__bridge id)cert],
|
||||
(id)kSecMatchLimit: (id)kSecMatchLimitOne,
|
||||
};
|
||||
status = SecItemDelete((__bridge CFDictionaryRef) query);
|
||||
|
||||
CFRelease(cert);
|
||||
return status;
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// some of the error codes returned by Apple's Security framework.
|
||||
const (
|
||||
errSecSuccess = 0
|
||||
errAuthorizationCanceled = -60006
|
||||
)
|
||||
|
||||
// certPEMToDER converts a certificate in PEM format to DER format, which is the format required by Apple's Security framework.
|
||||
func certPEMToDER(certPEM []byte) ([]byte, error) {
|
||||
block, left := pem.Decode(certPEM)
|
||||
if block == nil {
|
||||
return []byte{}, errors.New("invalid PEM certificate")
|
||||
}
|
||||
|
||||
if len(left) > 0 {
|
||||
return []byte{}, errors.New("trailing data found at the end of a PEM certificate")
|
||||
}
|
||||
|
||||
return block.Bytes, nil
|
||||
}
|
||||
|
||||
func installCert(certPEM []byte) error {
|
||||
name, err := writeToTempFile(certPEM)
|
||||
certDER, err := certPEMToDER(certPEM)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return addTrustedCert(name)
|
||||
p := C.CBytes(certDER)
|
||||
defer C.free(unsafe.Pointer(p)) //nolint:unconvert
|
||||
|
||||
errCode := C.installTrustedCert((*C.char)(p), (C.ulonglong)(len(certDER)))
|
||||
switch errCode {
|
||||
case errSecSuccess:
|
||||
return nil
|
||||
case errAuthorizationCanceled:
|
||||
return fmt.Errorf("the user cancelled the authorization dialog")
|
||||
default:
|
||||
return fmt.Errorf("could not install certification into keychain (error %v)", errCode)
|
||||
}
|
||||
}
|
||||
|
||||
func uninstallCert(certPEM []byte) error {
|
||||
name, err := writeToTempFile(certPEM)
|
||||
certDER, err := certPEMToDER(certPEM)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return removeTrustedCert(name)
|
||||
p := C.CBytes(certDER)
|
||||
defer C.free(unsafe.Pointer(p)) //nolint:unconvert
|
||||
|
||||
if errCode := C.removeTrustedCert((*C.char)(p), (C.ulonglong)(len(certDER))); errCode != 0 {
|
||||
return fmt.Errorf("could not install certificate from keychain (error %v)", errCode)
|
||||
}
|
||||
|
||||
func addTrustedCert(certPath string) error {
|
||||
return execabs.Command( //nolint:gosec
|
||||
"/usr/bin/security",
|
||||
"execute-with-privileges",
|
||||
"/usr/bin/security",
|
||||
"add-trusted-cert",
|
||||
"-d",
|
||||
"-r", "trustRoot",
|
||||
"-p", "ssl",
|
||||
"-k", "/Library/Keychains/System.keychain",
|
||||
certPath,
|
||||
).Run()
|
||||
}
|
||||
|
||||
func removeTrustedCert(certPath string) error {
|
||||
return execabs.Command( //nolint:gosec
|
||||
"/usr/bin/security",
|
||||
"execute-with-privileges",
|
||||
"/usr/bin/security",
|
||||
"remove-trusted-cert",
|
||||
"-d",
|
||||
certPath,
|
||||
).Run()
|
||||
}
|
||||
|
||||
// writeToTempFile writes the given data to a temporary file and returns the path.
|
||||
func writeToTempFile(data []byte) (string, error) {
|
||||
f, err := os.CreateTemp("", "tls")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if _, err := f.Write(data); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := f.Close(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return f.Name(), nil
|
||||
return nil
|
||||
}
|
||||
|
||||
44
internal/certs/cert_store_darwin_test.go
Normal file
44
internal/certs/cert_store_darwin_test.go
Normal file
@ -0,0 +1,44 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//go:build darwin
|
||||
|
||||
package certs
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// This test implies human interactions to enter password and is disabled by default.
|
||||
func _TestTrustedCertsDarwin(t *testing.T) { //nolint:unused
|
||||
template, err := NewTLSTemplate()
|
||||
require.NoError(t, err)
|
||||
|
||||
certPEM, _, err := GenerateCert(template)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Error(t, installCert([]byte{0})) // Cannot install an invalid cert.
|
||||
require.Error(t, uninstallCert(certPEM)) // Cannot uninstall a cert that is not installed.
|
||||
require.NoError(t, installCert(certPEM)) // Can install a valid cert.
|
||||
require.NoError(t, installCert(certPEM)) // Can install an already installed cert.
|
||||
require.NoError(t, uninstallCert(certPEM)) // Can uninstall an installed cert.
|
||||
require.Error(t, uninstallCert(certPEM)) // Cannot uninstall an already uninstalled cert.
|
||||
require.NoError(t, installCert(certPEM)) // Can reinstall an uninstalled cert.
|
||||
require.NoError(t, uninstallCert(certPEM)) // Can uninstall a reinstalled cert.
|
||||
}
|
||||
@ -72,6 +72,8 @@ func prepareMobileConfig(
|
||||
return &mobileconfig.Config{
|
||||
DisplayName: username,
|
||||
EmailAddress: addresses,
|
||||
AccountName: username,
|
||||
AccountDescription: username,
|
||||
Identifier: "protonmail " + username + strconv.FormatInt(time.Now().Unix(), 10),
|
||||
IMAP: &mobileconfig.IMAP{
|
||||
Hostname: hostname,
|
||||
|
||||
221
internal/configstatus/config_status.go
Normal file
221
internal/configstatus/config_status.go
Normal file
@ -0,0 +1,221 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package configstatus
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const version = "1.0.0"
|
||||
|
||||
func LoadConfigurationStatus(filepath string) (*ConfigurationStatus, error) {
|
||||
status := ConfigurationStatus{
|
||||
FilePath: filepath,
|
||||
DataLock: safe.NewRWMutex(),
|
||||
Data: &ConfigurationStatusData{},
|
||||
}
|
||||
|
||||
if _, err := os.Stat(filepath); err == nil {
|
||||
if err := status.Load(); err == nil {
|
||||
return &status, nil
|
||||
}
|
||||
logrus.WithError(err).Warn("Cannot load configuration status file. Reset it.")
|
||||
}
|
||||
|
||||
status.Data.init()
|
||||
if err := status.Save(); err != nil {
|
||||
return &status, err
|
||||
}
|
||||
return &status, nil
|
||||
}
|
||||
|
||||
func (status *ConfigurationStatus) Load() error {
|
||||
bytes, err := os.ReadFile(status.FilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var metadata MetadataOnly
|
||||
if err := json.Unmarshal(bytes, &metadata); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if metadata.Metadata.Version != version {
|
||||
return fmt.Errorf("unsupported configstatus file version %s", metadata.Metadata.Version)
|
||||
}
|
||||
|
||||
return json.Unmarshal(bytes, status.Data)
|
||||
}
|
||||
|
||||
func (status *ConfigurationStatus) Save() error {
|
||||
temp := status.FilePath + "_temp"
|
||||
f, err := os.Create(temp) //nolint:gosec
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
enc := json.NewEncoder(f)
|
||||
enc.SetIndent("", " ")
|
||||
err = enc.Encode(status.Data)
|
||||
if err := f.Close(); err != nil {
|
||||
logrus.WithError(err).Error("Error while closing configstatus file.")
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.Rename(temp, status.FilePath)
|
||||
}
|
||||
|
||||
func (status *ConfigurationStatus) IsPending() bool {
|
||||
status.DataLock.RLock()
|
||||
defer status.DataLock.RUnlock()
|
||||
|
||||
return !status.Data.DataV1.PendingSince.IsZero()
|
||||
}
|
||||
|
||||
func (status *ConfigurationStatus) IsFromFailure() bool {
|
||||
status.DataLock.RLock()
|
||||
defer status.DataLock.RUnlock()
|
||||
|
||||
return status.Data.DataV1.FailureDetails != ""
|
||||
}
|
||||
|
||||
func (status *ConfigurationStatus) ApplySuccess() error {
|
||||
status.DataLock.Lock()
|
||||
defer status.DataLock.Unlock()
|
||||
|
||||
status.Data.init()
|
||||
status.Data.DataV1.PendingSince = time.Time{}
|
||||
return status.Save()
|
||||
}
|
||||
|
||||
func (status *ConfigurationStatus) ApplyFailure(err string) error {
|
||||
status.DataLock.Lock()
|
||||
defer status.DataLock.Unlock()
|
||||
|
||||
status.Data.init()
|
||||
status.Data.DataV1.FailureDetails = err
|
||||
return status.Save()
|
||||
}
|
||||
|
||||
func (status *ConfigurationStatus) ApplyProgress() error {
|
||||
status.DataLock.Lock()
|
||||
defer status.DataLock.Unlock()
|
||||
|
||||
status.Data.DataV1.LastProgress = time.Now()
|
||||
return status.Save()
|
||||
}
|
||||
|
||||
func (status *ConfigurationStatus) RecordLinkClicked(link uint) error {
|
||||
status.DataLock.Lock()
|
||||
defer status.DataLock.Unlock()
|
||||
|
||||
if !status.Data.hasLinkClicked(link) {
|
||||
status.Data.setClickedLink(link)
|
||||
return status.Save()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (status *ConfigurationStatus) ReportClicked() error {
|
||||
status.DataLock.Lock()
|
||||
defer status.DataLock.Unlock()
|
||||
|
||||
if !status.Data.DataV1.ReportClick {
|
||||
status.Data.DataV1.ReportClick = true
|
||||
return status.Save()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (status *ConfigurationStatus) ReportSent() error {
|
||||
status.DataLock.Lock()
|
||||
defer status.DataLock.Unlock()
|
||||
|
||||
if !status.Data.DataV1.ReportSent {
|
||||
status.Data.DataV1.ReportSent = true
|
||||
return status.Save()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (status *ConfigurationStatus) AutoconfigUsed(client string) error {
|
||||
status.DataLock.Lock()
|
||||
defer status.DataLock.Unlock()
|
||||
|
||||
if client != status.Data.DataV1.Autoconf {
|
||||
status.Data.DataV1.Autoconf = client
|
||||
return status.Save()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (status *ConfigurationStatus) Remove() error {
|
||||
status.DataLock.Lock()
|
||||
defer status.DataLock.Unlock()
|
||||
return os.Remove(status.FilePath)
|
||||
}
|
||||
|
||||
func (data *ConfigurationStatusData) init() {
|
||||
data.Metadata = Metadata{
|
||||
Version: version,
|
||||
}
|
||||
data.DataV1.PendingSince = time.Now()
|
||||
data.DataV1.LastProgress = time.Time{}
|
||||
data.DataV1.Autoconf = ""
|
||||
data.DataV1.ClickedLink = 0
|
||||
data.DataV1.ReportSent = false
|
||||
data.DataV1.ReportClick = false
|
||||
data.DataV1.FailureDetails = ""
|
||||
}
|
||||
|
||||
func (data *ConfigurationStatusData) setClickedLink(pos uint) {
|
||||
data.DataV1.ClickedLink |= 1 << pos
|
||||
}
|
||||
|
||||
func (data *ConfigurationStatusData) hasLinkClicked(pos uint) bool {
|
||||
val := data.DataV1.ClickedLink & (1 << pos)
|
||||
return val > 0
|
||||
}
|
||||
|
||||
func (data *ConfigurationStatusData) clickedLinkToString() string {
|
||||
var str = ""
|
||||
var first = true
|
||||
for i := 0; i < 64; i++ {
|
||||
if data.hasLinkClicked(uint(i)) {
|
||||
if !first {
|
||||
str += ","
|
||||
} else {
|
||||
first = false
|
||||
str += "["
|
||||
}
|
||||
str += strconv.Itoa(i)
|
||||
}
|
||||
}
|
||||
if str != "" {
|
||||
str += "]"
|
||||
}
|
||||
return str
|
||||
}
|
||||
252
internal/configstatus/config_status_test.go
Normal file
252
internal/configstatus/config_status_test.go
Normal file
@ -0,0 +1,252 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package configstatus_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/configstatus"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestConfigStatus_init_virgin(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "1.0.0", config.Data.Metadata.Version)
|
||||
|
||||
require.Equal(t, false, config.Data.DataV1.PendingSince.IsZero())
|
||||
require.Equal(t, true, config.Data.DataV1.LastProgress.IsZero())
|
||||
|
||||
require.Equal(t, "", config.Data.DataV1.Autoconf)
|
||||
require.Equal(t, uint64(0), config.Data.DataV1.ClickedLink)
|
||||
require.Equal(t, false, config.Data.DataV1.ReportSent)
|
||||
require.Equal(t, false, config.Data.DataV1.ReportClick)
|
||||
require.Equal(t, "", config.Data.DataV1.FailureDetails)
|
||||
}
|
||||
|
||||
func TestConfigStatus_init_existing(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
var data = configstatus.ConfigurationStatusData{
|
||||
Metadata: configstatus.Metadata{Version: "1.0.0"},
|
||||
DataV1: configstatus.DataV1{Autoconf: "Mr TBird"},
|
||||
}
|
||||
require.NoError(t, dumpConfigStatusInFile(&data, file))
|
||||
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "1.0.0", config.Data.Metadata.Version)
|
||||
require.Equal(t, "Mr TBird", config.Data.DataV1.Autoconf)
|
||||
}
|
||||
|
||||
func TestConfigStatus_init_bad_version(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
var data = configstatus.ConfigurationStatusData{
|
||||
Metadata: configstatus.Metadata{Version: "2.0.0"},
|
||||
DataV1: configstatus.DataV1{Autoconf: "Mr TBird"},
|
||||
}
|
||||
require.NoError(t, dumpConfigStatusInFile(&data, file))
|
||||
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "1.0.0", config.Data.Metadata.Version)
|
||||
require.Equal(t, "", config.Data.DataV1.Autoconf)
|
||||
}
|
||||
|
||||
func TestConfigStatus_IsPending(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, true, config.IsPending())
|
||||
config.Data.DataV1.PendingSince = time.Time{}
|
||||
require.Equal(t, false, config.IsPending())
|
||||
}
|
||||
|
||||
func TestConfigStatus_IsFromFailure(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, false, config.IsFromFailure())
|
||||
config.Data.DataV1.FailureDetails = "test"
|
||||
require.Equal(t, true, config.IsFromFailure())
|
||||
}
|
||||
|
||||
func TestConfigStatus_ApplySuccess(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, true, config.IsPending())
|
||||
require.NoError(t, config.ApplySuccess())
|
||||
require.Equal(t, false, config.IsPending())
|
||||
|
||||
config2, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "1.0.0", config2.Data.Metadata.Version)
|
||||
require.Equal(t, true, config2.Data.DataV1.PendingSince.IsZero())
|
||||
require.Equal(t, true, config2.Data.DataV1.LastProgress.IsZero())
|
||||
require.Equal(t, "", config2.Data.DataV1.Autoconf)
|
||||
require.Equal(t, uint64(0), config2.Data.DataV1.ClickedLink)
|
||||
require.Equal(t, false, config2.Data.DataV1.ReportSent)
|
||||
require.Equal(t, false, config2.Data.DataV1.ReportClick)
|
||||
require.Equal(t, "", config2.Data.DataV1.FailureDetails)
|
||||
}
|
||||
|
||||
func TestConfigStatus_ApplyFailure(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, config.ApplySuccess())
|
||||
|
||||
require.NoError(t, config.ApplyFailure("Big Failure"))
|
||||
require.Equal(t, true, config.IsFromFailure())
|
||||
require.Equal(t, true, config.IsPending())
|
||||
|
||||
config2, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "1.0.0", config2.Data.Metadata.Version)
|
||||
require.Equal(t, false, config2.Data.DataV1.PendingSince.IsZero())
|
||||
require.Equal(t, true, config2.Data.DataV1.LastProgress.IsZero())
|
||||
require.Equal(t, "", config2.Data.DataV1.Autoconf)
|
||||
require.Equal(t, uint64(0), config2.Data.DataV1.ClickedLink)
|
||||
require.Equal(t, false, config2.Data.DataV1.ReportSent)
|
||||
require.Equal(t, false, config2.Data.DataV1.ReportClick)
|
||||
require.Equal(t, "Big Failure", config2.Data.DataV1.FailureDetails)
|
||||
}
|
||||
|
||||
func TestConfigStatus_ApplyProgress(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, true, config.IsPending())
|
||||
require.Equal(t, true, config.Data.DataV1.LastProgress.IsZero())
|
||||
|
||||
require.NoError(t, config.ApplyProgress())
|
||||
|
||||
config2, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "1.0.0", config2.Data.Metadata.Version)
|
||||
require.Equal(t, false, config2.Data.DataV1.PendingSince.IsZero())
|
||||
require.Equal(t, false, config2.Data.DataV1.LastProgress.IsZero())
|
||||
require.Equal(t, "", config2.Data.DataV1.Autoconf)
|
||||
require.Equal(t, uint64(0), config2.Data.DataV1.ClickedLink)
|
||||
require.Equal(t, false, config2.Data.DataV1.ReportSent)
|
||||
require.Equal(t, false, config2.Data.DataV1.ReportClick)
|
||||
require.Equal(t, "", config2.Data.DataV1.FailureDetails)
|
||||
}
|
||||
|
||||
func TestConfigStatus_RecordLinkClicked(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, uint64(0), config.Data.DataV1.ClickedLink)
|
||||
require.NoError(t, config.RecordLinkClicked(0))
|
||||
require.Equal(t, uint64(1), config.Data.DataV1.ClickedLink)
|
||||
require.NoError(t, config.RecordLinkClicked(1))
|
||||
require.Equal(t, uint64(3), config.Data.DataV1.ClickedLink)
|
||||
|
||||
config2, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "1.0.0", config2.Data.Metadata.Version)
|
||||
require.Equal(t, false, config2.Data.DataV1.PendingSince.IsZero())
|
||||
require.Equal(t, true, config2.Data.DataV1.LastProgress.IsZero())
|
||||
require.Equal(t, "", config2.Data.DataV1.Autoconf)
|
||||
require.Equal(t, uint64(3), config2.Data.DataV1.ClickedLink)
|
||||
require.Equal(t, false, config2.Data.DataV1.ReportSent)
|
||||
require.Equal(t, false, config2.Data.DataV1.ReportClick)
|
||||
require.Equal(t, "", config2.Data.DataV1.FailureDetails)
|
||||
}
|
||||
|
||||
func TestConfigStatus_ReportClicked(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, false, config.Data.DataV1.ReportClick)
|
||||
require.NoError(t, config.ReportClicked())
|
||||
require.Equal(t, true, config.Data.DataV1.ReportClick)
|
||||
|
||||
config2, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "1.0.0", config2.Data.Metadata.Version)
|
||||
require.Equal(t, false, config2.Data.DataV1.PendingSince.IsZero())
|
||||
require.Equal(t, true, config2.Data.DataV1.LastProgress.IsZero())
|
||||
require.Equal(t, "", config2.Data.DataV1.Autoconf)
|
||||
require.Equal(t, uint64(0), config2.Data.DataV1.ClickedLink)
|
||||
require.Equal(t, false, config2.Data.DataV1.ReportSent)
|
||||
require.Equal(t, true, config2.Data.DataV1.ReportClick)
|
||||
require.Equal(t, "", config2.Data.DataV1.FailureDetails)
|
||||
}
|
||||
|
||||
func TestConfigStatus_ReportSent(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, false, config.Data.DataV1.ReportSent)
|
||||
require.NoError(t, config.ReportSent())
|
||||
require.Equal(t, true, config.Data.DataV1.ReportSent)
|
||||
|
||||
config2, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "1.0.0", config2.Data.Metadata.Version)
|
||||
require.Equal(t, false, config2.Data.DataV1.PendingSince.IsZero())
|
||||
require.Equal(t, true, config2.Data.DataV1.LastProgress.IsZero())
|
||||
require.Equal(t, "", config2.Data.DataV1.Autoconf)
|
||||
require.Equal(t, uint64(0), config2.Data.DataV1.ClickedLink)
|
||||
require.Equal(t, true, config2.Data.DataV1.ReportSent)
|
||||
require.Equal(t, false, config2.Data.DataV1.ReportClick)
|
||||
require.Equal(t, "", config2.Data.DataV1.FailureDetails)
|
||||
}
|
||||
|
||||
func dumpConfigStatusInFile(data *configstatus.ConfigurationStatusData, file string) error {
|
||||
f, err := os.Create(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
return json.NewEncoder(f).Encode(data)
|
||||
}
|
||||
57
internal/configstatus/configuration_abort.go
Normal file
57
internal/configstatus/configuration_abort.go
Normal file
@ -0,0 +1,57 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package configstatus
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ConfigAbortValues struct {
|
||||
Duration int `json:"duration"`
|
||||
}
|
||||
|
||||
type ConfigAbortDimensions struct {
|
||||
ReportClick string `json:"report_click"`
|
||||
ReportSent string `json:"report_sent"`
|
||||
ClickedLink string `json:"clicked_link"`
|
||||
}
|
||||
|
||||
type ConfigAbortData struct {
|
||||
MeasurementGroup string
|
||||
Event string
|
||||
Values ConfigSuccessValues
|
||||
Dimensions ConfigSuccessDimensions
|
||||
}
|
||||
|
||||
type ConfigAbortBuilder struct{}
|
||||
|
||||
func (*ConfigAbortBuilder) New(data *ConfigurationStatusData) ConfigAbortData {
|
||||
return ConfigAbortData{
|
||||
MeasurementGroup: "bridge.any.configuration",
|
||||
Event: "bridge_config_abort",
|
||||
Values: ConfigSuccessValues{
|
||||
Duration: int(time.Since(data.DataV1.PendingSince).Minutes()),
|
||||
},
|
||||
Dimensions: ConfigSuccessDimensions{
|
||||
ReportClick: strconv.FormatBool(data.DataV1.ReportClick),
|
||||
ReportSent: strconv.FormatBool(data.DataV1.ReportSent),
|
||||
ClickedLink: data.clickedLinkToString(),
|
||||
},
|
||||
}
|
||||
}
|
||||
75
internal/configstatus/configuration_abort_test.go
Normal file
75
internal/configstatus/configuration_abort_test.go
Normal file
@ -0,0 +1,75 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package configstatus_test
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/configstatus"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestConfigurationAbort_default(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
var builder = configstatus.ConfigAbortBuilder{}
|
||||
req := builder.New(config.Data)
|
||||
|
||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||
require.Equal(t, "bridge_config_abort", req.Event)
|
||||
require.Equal(t, 0, req.Values.Duration)
|
||||
require.Equal(t, "false", req.Dimensions.ReportClick)
|
||||
require.Equal(t, "false", req.Dimensions.ReportSent)
|
||||
require.Equal(t, "", req.Dimensions.ClickedLink)
|
||||
}
|
||||
|
||||
func TestConfigurationAbort_fed(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
var data = configstatus.ConfigurationStatusData{
|
||||
Metadata: configstatus.Metadata{Version: "1.0.0"},
|
||||
DataV1: configstatus.DataV1{
|
||||
PendingSince: time.Now().Add(-10 * time.Minute),
|
||||
LastProgress: time.Time{},
|
||||
Autoconf: "Mr TBird",
|
||||
ClickedLink: 42,
|
||||
ReportSent: false,
|
||||
ReportClick: true,
|
||||
FailureDetails: "Not an error",
|
||||
},
|
||||
}
|
||||
require.NoError(t, dumpConfigStatusInFile(&data, file))
|
||||
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
var builder = configstatus.ConfigAbortBuilder{}
|
||||
req := builder.New(config.Data)
|
||||
|
||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||
require.Equal(t, "bridge_config_abort", req.Event)
|
||||
require.Equal(t, 10, req.Values.Duration)
|
||||
require.Equal(t, "true", req.Dimensions.ReportClick)
|
||||
require.Equal(t, "false", req.Dimensions.ReportSent)
|
||||
require.Equal(t, "[1,3,5]", req.Dimensions.ClickedLink)
|
||||
}
|
||||
60
internal/configstatus/configuration_progress.go
Normal file
60
internal/configstatus/configuration_progress.go
Normal file
@ -0,0 +1,60 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package configstatus
|
||||
|
||||
import "time"
|
||||
|
||||
type ConfigProgressValues struct {
|
||||
NbDay int `json:"nb_day"`
|
||||
NbDaySinceLast int `json:"nb_day_since_last"`
|
||||
}
|
||||
|
||||
type ConfigProgressData struct {
|
||||
MeasurementGroup string
|
||||
Event string
|
||||
Values ConfigProgressValues
|
||||
Dimensions struct{}
|
||||
}
|
||||
|
||||
type ConfigProgressBuilder struct{}
|
||||
|
||||
func (*ConfigProgressBuilder) New(data *ConfigurationStatusData) ConfigProgressData {
|
||||
return ConfigProgressData{
|
||||
MeasurementGroup: "bridge.any.configuration",
|
||||
Event: "bridge_config_progress",
|
||||
Values: ConfigProgressValues{
|
||||
NbDay: numberOfDay(time.Now(), data.DataV1.PendingSince),
|
||||
NbDaySinceLast: numberOfDay(time.Now(), data.DataV1.LastProgress),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func numberOfDay(now, prev time.Time) int {
|
||||
if now.IsZero() || prev.IsZero() {
|
||||
return 1
|
||||
}
|
||||
if now.Year() > prev.Year() {
|
||||
if now.YearDay() > prev.YearDay() {
|
||||
return 365 + (now.YearDay() - prev.YearDay())
|
||||
}
|
||||
return (prev.YearDay() + now.YearDay()) - 365
|
||||
} else if now.YearDay() > prev.YearDay() {
|
||||
return now.YearDay() - prev.YearDay()
|
||||
}
|
||||
return 0
|
||||
}
|
||||
71
internal/configstatus/configuration_progress_test.go
Normal file
71
internal/configstatus/configuration_progress_test.go
Normal file
@ -0,0 +1,71 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package configstatus_test
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/configstatus"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestConfigurationProgress_default(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
var builder = configstatus.ConfigProgressBuilder{}
|
||||
req := builder.New(config.Data)
|
||||
|
||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||
require.Equal(t, "bridge_config_progress", req.Event)
|
||||
require.Equal(t, 0, req.Values.NbDay)
|
||||
require.Equal(t, 1, req.Values.NbDaySinceLast)
|
||||
}
|
||||
|
||||
func TestConfigurationProgress_fed(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
var data = configstatus.ConfigurationStatusData{
|
||||
Metadata: configstatus.Metadata{Version: "1.0.0"},
|
||||
DataV1: configstatus.DataV1{
|
||||
PendingSince: time.Now().AddDate(0, 0, -5),
|
||||
LastProgress: time.Now().AddDate(0, 0, -2),
|
||||
Autoconf: "Mr TBird",
|
||||
ClickedLink: 42,
|
||||
ReportSent: false,
|
||||
ReportClick: true,
|
||||
FailureDetails: "Not an error",
|
||||
},
|
||||
}
|
||||
require.NoError(t, dumpConfigStatusInFile(&data, file))
|
||||
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
var builder = configstatus.ConfigProgressBuilder{}
|
||||
req := builder.New(config.Data)
|
||||
|
||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||
require.Equal(t, "bridge_config_progress", req.Event)
|
||||
require.Equal(t, 5, req.Values.NbDay)
|
||||
require.Equal(t, 2, req.Values.NbDaySinceLast)
|
||||
}
|
||||
61
internal/configstatus/configuration_recovery.go
Normal file
61
internal/configstatus/configuration_recovery.go
Normal file
@ -0,0 +1,61 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package configstatus
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ConfigRecoveryValues struct {
|
||||
Duration int `json:"duration"`
|
||||
}
|
||||
|
||||
type ConfigRecoveryDimensions struct {
|
||||
Autoconf string `json:"autoconf"`
|
||||
ReportClick string `json:"report_click"`
|
||||
ReportSent string `json:"report_sent"`
|
||||
ClickedLink string `json:"clicked_link"`
|
||||
FailureDetails string `json:"failure_details"`
|
||||
}
|
||||
|
||||
type ConfigRecoveryData struct {
|
||||
MeasurementGroup string
|
||||
Event string
|
||||
Values ConfigRecoveryValues
|
||||
Dimensions ConfigRecoveryDimensions
|
||||
}
|
||||
|
||||
type ConfigRecoveryBuilder struct{}
|
||||
|
||||
func (*ConfigRecoveryBuilder) New(data *ConfigurationStatusData) ConfigRecoveryData {
|
||||
return ConfigRecoveryData{
|
||||
MeasurementGroup: "bridge.any.configuration",
|
||||
Event: "bridge_config_recovery",
|
||||
Values: ConfigRecoveryValues{
|
||||
Duration: int(time.Since(data.DataV1.PendingSince).Minutes()),
|
||||
},
|
||||
Dimensions: ConfigRecoveryDimensions{
|
||||
Autoconf: data.DataV1.Autoconf,
|
||||
ReportClick: strconv.FormatBool(data.DataV1.ReportClick),
|
||||
ReportSent: strconv.FormatBool(data.DataV1.ReportSent),
|
||||
ClickedLink: data.clickedLinkToString(),
|
||||
FailureDetails: data.DataV1.FailureDetails,
|
||||
},
|
||||
}
|
||||
}
|
||||
79
internal/configstatus/configuration_recovery_test.go
Normal file
79
internal/configstatus/configuration_recovery_test.go
Normal file
@ -0,0 +1,79 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package configstatus_test
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/configstatus"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestConfigurationRecovery_default(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
var builder = configstatus.ConfigRecoveryBuilder{}
|
||||
req := builder.New(config.Data)
|
||||
|
||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||
require.Equal(t, "bridge_config_recovery", req.Event)
|
||||
require.Equal(t, 0, req.Values.Duration)
|
||||
require.Equal(t, "", req.Dimensions.Autoconf)
|
||||
require.Equal(t, "false", req.Dimensions.ReportClick)
|
||||
require.Equal(t, "false", req.Dimensions.ReportSent)
|
||||
require.Equal(t, "", req.Dimensions.ClickedLink)
|
||||
require.Equal(t, "", req.Dimensions.FailureDetails)
|
||||
}
|
||||
|
||||
func TestConfigurationRecovery_fed(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
var data = configstatus.ConfigurationStatusData{
|
||||
Metadata: configstatus.Metadata{Version: "1.0.0"},
|
||||
DataV1: configstatus.DataV1{
|
||||
PendingSince: time.Now().Add(-10 * time.Minute),
|
||||
LastProgress: time.Time{},
|
||||
Autoconf: "Mr TBird",
|
||||
ClickedLink: 42,
|
||||
ReportSent: false,
|
||||
ReportClick: true,
|
||||
FailureDetails: "Not an error",
|
||||
},
|
||||
}
|
||||
require.NoError(t, dumpConfigStatusInFile(&data, file))
|
||||
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
var builder = configstatus.ConfigRecoveryBuilder{}
|
||||
req := builder.New(config.Data)
|
||||
|
||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||
require.Equal(t, "bridge_config_recovery", req.Event)
|
||||
require.Equal(t, 10, req.Values.Duration)
|
||||
require.Equal(t, "Mr TBird", req.Dimensions.Autoconf)
|
||||
require.Equal(t, "true", req.Dimensions.ReportClick)
|
||||
require.Equal(t, "false", req.Dimensions.ReportSent)
|
||||
require.Equal(t, "[1,3,5]", req.Dimensions.ClickedLink)
|
||||
require.Equal(t, "Not an error", req.Dimensions.FailureDetails)
|
||||
}
|
||||
59
internal/configstatus/configuration_success.go
Normal file
59
internal/configstatus/configuration_success.go
Normal file
@ -0,0 +1,59 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package configstatus
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ConfigSuccessValues struct {
|
||||
Duration int `json:"duration"`
|
||||
}
|
||||
|
||||
type ConfigSuccessDimensions struct {
|
||||
Autoconf string `json:"autoconf"`
|
||||
ReportClick string `json:"report_click"`
|
||||
ReportSent string `json:"report_sent"`
|
||||
ClickedLink string `json:"clicked_link"`
|
||||
}
|
||||
|
||||
type ConfigSuccessData struct {
|
||||
MeasurementGroup string
|
||||
Event string
|
||||
Values ConfigSuccessValues
|
||||
Dimensions ConfigSuccessDimensions
|
||||
}
|
||||
|
||||
type ConfigSuccessBuilder struct{}
|
||||
|
||||
func (*ConfigSuccessBuilder) New(data *ConfigurationStatusData) ConfigSuccessData {
|
||||
return ConfigSuccessData{
|
||||
MeasurementGroup: "bridge.any.configuration",
|
||||
Event: "bridge_config_success",
|
||||
Values: ConfigSuccessValues{
|
||||
Duration: int(time.Since(data.DataV1.PendingSince).Minutes()),
|
||||
},
|
||||
Dimensions: ConfigSuccessDimensions{
|
||||
Autoconf: data.DataV1.Autoconf,
|
||||
ReportClick: strconv.FormatBool(data.DataV1.ReportClick),
|
||||
ReportSent: strconv.FormatBool(data.DataV1.ReportSent),
|
||||
ClickedLink: data.clickedLinkToString(),
|
||||
},
|
||||
}
|
||||
}
|
||||
77
internal/configstatus/configuration_success_test.go
Normal file
77
internal/configstatus/configuration_success_test.go
Normal file
@ -0,0 +1,77 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package configstatus_test
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/configstatus"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestConfigurationSuccess_default(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
var builder = configstatus.ConfigSuccessBuilder{}
|
||||
req := builder.New(config.Data)
|
||||
|
||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||
require.Equal(t, "bridge_config_success", req.Event)
|
||||
require.Equal(t, 0, req.Values.Duration)
|
||||
require.Equal(t, "", req.Dimensions.Autoconf)
|
||||
require.Equal(t, "false", req.Dimensions.ReportClick)
|
||||
require.Equal(t, "false", req.Dimensions.ReportSent)
|
||||
require.Equal(t, "", req.Dimensions.ClickedLink)
|
||||
}
|
||||
|
||||
func TestConfigurationSuccess_fed(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
var data = configstatus.ConfigurationStatusData{
|
||||
Metadata: configstatus.Metadata{Version: "1.0.0"},
|
||||
DataV1: configstatus.DataV1{
|
||||
PendingSince: time.Now().Add(-10 * time.Minute),
|
||||
LastProgress: time.Time{},
|
||||
Autoconf: "Mr TBird",
|
||||
ClickedLink: 42,
|
||||
ReportSent: false,
|
||||
ReportClick: true,
|
||||
FailureDetails: "Not an error",
|
||||
},
|
||||
}
|
||||
require.NoError(t, dumpConfigStatusInFile(&data, file))
|
||||
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
var builder = configstatus.ConfigSuccessBuilder{}
|
||||
req := builder.New(config.Data)
|
||||
|
||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||
require.Equal(t, "bridge_config_success", req.Event)
|
||||
require.Equal(t, 10, req.Values.Duration)
|
||||
require.Equal(t, "Mr TBird", req.Dimensions.Autoconf)
|
||||
require.Equal(t, "true", req.Dimensions.ReportClick)
|
||||
require.Equal(t, "false", req.Dimensions.ReportSent)
|
||||
require.Equal(t, "[1,3,5]", req.Dimensions.ClickedLink)
|
||||
}
|
||||
56
internal/configstatus/types_config_status.go
Normal file
56
internal/configstatus/types_config_status.go
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/>.
|
||||
|
||||
package configstatus
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||
)
|
||||
|
||||
const ProgressCheckInterval = time.Hour
|
||||
|
||||
type Metadata struct {
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
type MetadataOnly struct {
|
||||
Metadata Metadata `json:"metadata"`
|
||||
}
|
||||
|
||||
type DataV1 struct {
|
||||
PendingSince time.Time `json:"pending_since"`
|
||||
LastProgress time.Time `json:"last_progress"`
|
||||
Autoconf string `json:"auto_conf"`
|
||||
ClickedLink uint64 `json:"clicked_link"`
|
||||
ReportSent bool `json:"report_sent"`
|
||||
ReportClick bool `json:"report_click"`
|
||||
FailureDetails string `json:"failure_details"`
|
||||
}
|
||||
|
||||
type ConfigurationStatusData struct {
|
||||
Metadata Metadata `json:"metadata"`
|
||||
DataV1 DataV1 `json:"dataV1"`
|
||||
}
|
||||
|
||||
type ConfigurationStatus struct {
|
||||
FilePath string
|
||||
DataLock safe.RWMutex
|
||||
|
||||
Data *ConfigurationStatusData
|
||||
}
|
||||
@ -33,9 +33,12 @@ var (
|
||||
// Version of the build.
|
||||
Version = "0.0.0"
|
||||
|
||||
// Revision is current hash of the build.
|
||||
// Revision is build time commit hash.
|
||||
Revision = ""
|
||||
|
||||
// Tag is build time git describe.
|
||||
Tag = ""
|
||||
|
||||
// BuildTime stamp of the build.
|
||||
BuildTime = ""
|
||||
|
||||
|
||||
@ -93,6 +93,8 @@ func newProxyProvider(dialer TLSDialer, hostURL string, providers []string, pani
|
||||
}
|
||||
|
||||
// findReachableServer returns a working API server (either proxy or standard API).
|
||||
//
|
||||
//nolint:nakedret
|
||||
func (p *proxyProvider) findReachableServer() (proxy string, err error) {
|
||||
logrus.Debug("Trying to find a reachable server")
|
||||
|
||||
@ -204,6 +206,8 @@ func (p *proxyProvider) canReach(url string) bool {
|
||||
// It looks up DNS TXT records for the given query URL using the given DoH provider.
|
||||
// It returns a list of all found TXT records.
|
||||
// If the whole process takes more than proxyDoHTimeout then an error is returned.
|
||||
//
|
||||
//nolint:nakedret
|
||||
func (p *proxyProvider) defaultDoHLookup(ctx context.Context, query, dohProvider string) (data []string, err error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, p.dohTimeout)
|
||||
defer cancel()
|
||||
|
||||
@ -30,6 +30,7 @@ import (
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
"google.golang.org/protobuf/types/known/wrapperspb"
|
||||
)
|
||||
|
||||
// TryRaise tries to raise the application by dialing the focus service.
|
||||
@ -38,7 +39,7 @@ func TryRaise(settingsPath string) bool {
|
||||
var raised bool
|
||||
|
||||
if err := withClientConn(context.Background(), settingsPath, func(ctx context.Context, client proto.FocusClient) error {
|
||||
if _, err := client.Raise(ctx, &emptypb.Empty{}); err != nil {
|
||||
if _, err := client.Raise(ctx, &wrapperspb.StringValue{Value: "TryRaise"}); err != nil {
|
||||
return fmt.Errorf("failed to call client.Raise: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.28.0
|
||||
// protoc v3.21.3
|
||||
// protoc v3.21.12
|
||||
// source: focus.proto
|
||||
|
||||
package proto
|
||||
@ -27,6 +27,7 @@ import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
||||
wrapperspb "google.golang.org/protobuf/types/known/wrapperspb"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
)
|
||||
@ -91,22 +92,24 @@ var file_focus_proto_rawDesc = []byte{
|
||||
0x0a, 0x0b, 0x66, 0x6f, 0x63, 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x66,
|
||||
0x6f, 0x63, 0x75, 0x73, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f,
|
||||
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74,
|
||||
0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
|
||||
0x75, 0x66, 0x2f, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74,
|
||||
0x6f, 0x22, 0x2b, 0x0a, 0x0f, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70,
|
||||
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18,
|
||||
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x32, 0x7b,
|
||||
0x0a, 0x05, 0x46, 0x6f, 0x63, 0x75, 0x73, 0x12, 0x37, 0x0a, 0x05, 0x52, 0x61, 0x69, 0x73, 0x65,
|
||||
0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
|
||||
0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
|
||||
0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79,
|
||||
0x12, 0x39, 0x0a, 0x07, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f,
|
||||
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d,
|
||||
0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x66, 0x6f, 0x63, 0x75, 0x73, 0x2e, 0x56, 0x65, 0x72, 0x73,
|
||||
0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x3d, 0x5a, 0x3b, 0x67,
|
||||
0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x6e,
|
||||
0x4d, 0x61, 0x69, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x6e, 0x2d, 0x62, 0x72, 0x69, 0x64,
|
||||
0x67, 0x65, 0x2f, 0x76, 0x33, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x66,
|
||||
0x6f, 0x63, 0x75, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
|
||||
0x6f, 0x33,
|
||||
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x32, 0x81,
|
||||
0x01, 0x0a, 0x05, 0x46, 0x6f, 0x63, 0x75, 0x73, 0x12, 0x3d, 0x0a, 0x05, 0x52, 0x61, 0x69, 0x73,
|
||||
0x65, 0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
|
||||
0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a,
|
||||
0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
|
||||
0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x39, 0x0a, 0x07, 0x56, 0x65, 0x72, 0x73, 0x69,
|
||||
0x6f, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
|
||||
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x66, 0x6f, 0x63,
|
||||
0x75, 0x73, 0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
|
||||
0x73, 0x65, 0x42, 0x3d, 0x5a, 0x3b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
|
||||
0x2f, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x6e, 0x4d, 0x61, 0x69, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74,
|
||||
0x6f, 0x6e, 0x2d, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x2f, 0x76, 0x33, 0x2f, 0x69, 0x6e, 0x74,
|
||||
0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x66, 0x6f, 0x63, 0x75, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74,
|
||||
0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
@ -124,12 +127,13 @@ func file_focus_proto_rawDescGZIP() []byte {
|
||||
var file_focus_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
|
||||
var file_focus_proto_goTypes = []interface{}{
|
||||
(*VersionResponse)(nil), // 0: focus.VersionResponse
|
||||
(*emptypb.Empty)(nil), // 1: google.protobuf.Empty
|
||||
(*wrapperspb.StringValue)(nil), // 1: google.protobuf.StringValue
|
||||
(*emptypb.Empty)(nil), // 2: google.protobuf.Empty
|
||||
}
|
||||
var file_focus_proto_depIdxs = []int32{
|
||||
1, // 0: focus.Focus.Raise:input_type -> google.protobuf.Empty
|
||||
1, // 1: focus.Focus.Version:input_type -> google.protobuf.Empty
|
||||
1, // 2: focus.Focus.Raise:output_type -> google.protobuf.Empty
|
||||
1, // 0: focus.Focus.Raise:input_type -> google.protobuf.StringValue
|
||||
2, // 1: focus.Focus.Version:input_type -> google.protobuf.Empty
|
||||
2, // 2: focus.Focus.Raise:output_type -> google.protobuf.Empty
|
||||
0, // 3: focus.Focus.Version:output_type -> focus.VersionResponse
|
||||
2, // [2:4] is the sub-list for method output_type
|
||||
0, // [0:2] is the sub-list for method input_type
|
||||
|
||||
@ -18,6 +18,7 @@
|
||||
syntax = "proto3";
|
||||
|
||||
import "google/protobuf/empty.proto";
|
||||
import "google/protobuf/wrappers.proto";
|
||||
|
||||
option go_package = "github.com/ProtonMail/proton-bridge/v3/internal/focus/proto";
|
||||
|
||||
@ -27,7 +28,7 @@ package focus; // ignored by Go, used as namespace name in C++.
|
||||
// Service Declaration
|
||||
//**********************************************************************************************************************≠––
|
||||
service Focus {
|
||||
rpc Raise(google.protobuf.Empty) returns (google.protobuf.Empty);
|
||||
rpc Raise(google.protobuf.StringValue) returns (google.protobuf.Empty);
|
||||
rpc Version(google.protobuf.Empty) returns (VersionResponse);
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.2.0
|
||||
// - protoc v3.21.3
|
||||
// - protoc v3.21.12
|
||||
// source: focus.proto
|
||||
|
||||
package proto
|
||||
@ -12,6 +12,7 @@ import (
|
||||
codes "google.golang.org/grpc/codes"
|
||||
status "google.golang.org/grpc/status"
|
||||
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
||||
wrapperspb "google.golang.org/protobuf/types/known/wrapperspb"
|
||||
)
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
@ -23,7 +24,7 @@ const _ = grpc.SupportPackageIsVersion7
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
type FocusClient interface {
|
||||
Raise(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
Raise(ctx context.Context, in *wrapperspb.StringValue, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
Version(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*VersionResponse, error)
|
||||
}
|
||||
|
||||
@ -35,7 +36,7 @@ func NewFocusClient(cc grpc.ClientConnInterface) FocusClient {
|
||||
return &focusClient{cc}
|
||||
}
|
||||
|
||||
func (c *focusClient) Raise(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
||||
func (c *focusClient) Raise(ctx context.Context, in *wrapperspb.StringValue, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
||||
out := new(emptypb.Empty)
|
||||
err := c.cc.Invoke(ctx, "/focus.Focus/Raise", in, out, opts...)
|
||||
if err != nil {
|
||||
@ -57,7 +58,7 @@ func (c *focusClient) Version(ctx context.Context, in *emptypb.Empty, opts ...gr
|
||||
// All implementations must embed UnimplementedFocusServer
|
||||
// for forward compatibility
|
||||
type FocusServer interface {
|
||||
Raise(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
|
||||
Raise(context.Context, *wrapperspb.StringValue) (*emptypb.Empty, error)
|
||||
Version(context.Context, *emptypb.Empty) (*VersionResponse, error)
|
||||
mustEmbedUnimplementedFocusServer()
|
||||
}
|
||||
@ -66,7 +67,7 @@ type FocusServer interface {
|
||||
type UnimplementedFocusServer struct {
|
||||
}
|
||||
|
||||
func (UnimplementedFocusServer) Raise(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
|
||||
func (UnimplementedFocusServer) Raise(context.Context, *wrapperspb.StringValue) (*emptypb.Empty, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Raise not implemented")
|
||||
}
|
||||
func (UnimplementedFocusServer) Version(context.Context, *emptypb.Empty) (*VersionResponse, error) {
|
||||
@ -86,7 +87,7 @@ func RegisterFocusServer(s grpc.ServiceRegistrar, srv FocusServer) {
|
||||
}
|
||||
|
||||
func _Focus_Raise_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(emptypb.Empty)
|
||||
in := new(wrapperspb.StringValue)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -98,7 +99,7 @@ func _Focus_Raise_Handler(srv interface{}, ctx context.Context, dec func(interfa
|
||||
FullMethod: "/focus.Focus/Raise",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(FocusServer).Raise(ctx, req.(*emptypb.Empty))
|
||||
return srv.(FocusServer).Raise(ctx, req.(*wrapperspb.StringValue))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
@ -30,6 +30,7 @@ import (
|
||||
"github.com/sirupsen/logrus"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
"google.golang.org/protobuf/types/known/wrapperspb"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -45,6 +46,7 @@ type Service struct {
|
||||
raiseCh chan struct{}
|
||||
version *semver.Version
|
||||
|
||||
log *logrus.Entry
|
||||
panicHandler async.PanicHandler
|
||||
}
|
||||
|
||||
@ -55,13 +57,14 @@ func NewService(locator service.Locator, version *semver.Version, panicHandler a
|
||||
server: grpc.NewServer(),
|
||||
raiseCh: make(chan struct{}, 1),
|
||||
version: version,
|
||||
log: logrus.WithField("pkg", "focus/service"),
|
||||
panicHandler: panicHandler,
|
||||
}
|
||||
|
||||
proto.RegisterFocusServer(serv.server, serv)
|
||||
|
||||
if listener, err := net.Listen("tcp", net.JoinHostPort(Host, fmt.Sprint(0))); err != nil {
|
||||
logrus.WithError(err).Warn("Failed to start focus serv")
|
||||
serv.log.WithError(err).Warn("Failed to start focus service")
|
||||
} else {
|
||||
config := service.Config{}
|
||||
// retrieve the port assigned by the system, so that we can put it in the config file.
|
||||
@ -71,9 +74,9 @@ func NewService(locator service.Locator, version *semver.Version, panicHandler a
|
||||
}
|
||||
config.Port = address.Port
|
||||
if path, err := service.SaveGRPCServerConfigFile(locator, &config, serverConfigFileName); err != nil {
|
||||
logrus.WithError(err).WithField("path", path).Warn("Could not write focus gRPC service config file")
|
||||
serv.log.WithError(err).WithField("path", path).Warn("Could not write focus gRPC service config file")
|
||||
} else {
|
||||
logrus.WithField("path", path).Info("Successfully saved gRPC Focus service config file")
|
||||
serv.log.WithField("path", path).Info("Successfully saved gRPC Focus service config file")
|
||||
}
|
||||
|
||||
go func() {
|
||||
@ -89,13 +92,15 @@ func NewService(locator service.Locator, version *semver.Version, panicHandler a
|
||||
}
|
||||
|
||||
// Raise implements the gRPC FocusService interface; it raises the application.
|
||||
func (service *Service) Raise(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
|
||||
func (service *Service) Raise(_ context.Context, reason *wrapperspb.StringValue) (*emptypb.Empty, error) {
|
||||
service.log.WithField("Reason", reason.Value).Debug("Raise")
|
||||
service.raiseCh <- struct{}{}
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
// Version implements the gRPC FocusService interface; it returns the version of the service.
|
||||
func (service *Service) Version(context.Context, *emptypb.Empty) (*proto.VersionResponse, error) {
|
||||
service.log.Debug("Version")
|
||||
return &proto.VersionResponse{
|
||||
Version: service.version.Original(),
|
||||
}, nil
|
||||
|
||||
4
internal/frontend/bridge-gui/bridge-gui-tester/.lldbinit
Normal file
4
internal/frontend/bridge-gui/bridge-gui-tester/.lldbinit
Normal file
@ -0,0 +1,4 @@
|
||||
# The following fix an issue happening using LLDB with OpenSSL 3.1 on ARM64 architecture. (GODT-2680)
|
||||
# WARNING: this file is ignored if you do not enable reading lldb config from cwd in ~/.lldbinit (`settings set target.load-cwd-lldbinit true`)
|
||||
settings set platform.plugin.darwin.ignored-exceptions EXC_BAD_INSTRUCTION
|
||||
process handle SIGILL -n false -p true -s false
|
||||
@ -39,6 +39,7 @@ void GRPCQtProxy::connectSignals() {
|
||||
connect(this, &GRPCQtProxy::setIsAutostartOnReceived, &settingsTab, &SettingsTab::setIsAutostartOn);
|
||||
connect(this, &GRPCQtProxy::setIsBetaEnabledReceived, &settingsTab, &SettingsTab::setIsBetaEnabled);
|
||||
connect(this, &GRPCQtProxy::setIsAllMailVisibleReceived, &settingsTab, &SettingsTab::setIsAllMailVisible);
|
||||
connect(this, &GRPCQtProxy::setIsTelemetryDisabledReceived, &settingsTab, &SettingsTab::setIsTelemetryDisabled);
|
||||
connect(this, &GRPCQtProxy::setColorSchemeNameReceived, &settingsTab, &SettingsTab::setColorSchemeName);
|
||||
connect(this, &GRPCQtProxy::reportBugReceived, &settingsTab, &SettingsTab::setBugReport);
|
||||
connect(this, &GRPCQtProxy::exportTLSCertificatesReceived, &settingsTab, &SettingsTab::exportTLSCertificates);
|
||||
@ -89,6 +90,13 @@ void GRPCQtProxy::setIsAllMailVisible(bool visible) {
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] isDisabled Is telemetry disabled?
|
||||
//****************************************************************************************************************************************************
|
||||
void GRPCQtProxy::setIsTelemetryDisabled(bool isDisabled) {
|
||||
emit setIsTelemetryDisabledReceived(isDisabled);
|
||||
}
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] name The color scheme.
|
||||
//****************************************************************************************************************************************************
|
||||
|
||||
@ -41,6 +41,7 @@ public: // member functions.
|
||||
void setIsAutostartOn(bool on); ///< Forwards a SetIsAutostartOn call via a Qt signal.
|
||||
void setIsBetaEnabled(bool enabled); ///< Forwards a SetIsBetaEnabled call via a Qt signal.
|
||||
void setIsAllMailVisible(bool visible); ///< Forwards a SetIsAllMailVisible call via a Qt signal.
|
||||
void setIsTelemetryDisabled(bool isDisabled); ///< Forwards a SetIsTelemetryDisabled call via a Qt signal.
|
||||
void setColorSchemeName(QString const &name); ///< Forward a SetColorSchemeName call via a Qt Signal
|
||||
void reportBug(QString const &osType, QString const &osVersion, QString const &emailClient, QString const &address,
|
||||
QString const &description, bool includeLogs); ///< Forwards a ReportBug call via a Qt signal.
|
||||
@ -62,6 +63,7 @@ signals:
|
||||
void setIsAutostartOnReceived(bool on); ///< Forwards a SetIsAutostartOn call via a Qt signal.
|
||||
void setIsBetaEnabledReceived(bool enabled); ///< Forwards a SetIsBetaEnabled call via a Qt signal.
|
||||
void setIsAllMailVisibleReceived(bool enabled); ///< Forwards a SetIsBetaEnabled call via a Qt signal.
|
||||
void setIsTelemetryDisabledReceived(bool isDisabled); ///< Forwards a SetIsTelemetryDisabled call via a Qt signal.
|
||||
void setColorSchemeNameReceived(QString const &name); ///< Forward a SetColorScheme call via a Qt Signal
|
||||
void reportBugReceived(QString const &osType, QString const &osVersion, QString const &emailClient, QString const &address,
|
||||
QString const &description, bool includeLogs); ///< Signal for the ReportBug gRPC call
|
||||
|
||||
@ -192,6 +192,28 @@ Status GRPCService::IsAllMailVisible(ServerContext *, Empty const *request, Bool
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] request The request.
|
||||
/// \return The status for the call.
|
||||
//****************************************************************************************************************************************************
|
||||
grpc::Status GRPCService::SetIsTelemetryDisabled(::grpc::ServerContext *, ::google::protobuf::BoolValue const *request, ::google::protobuf::Empty *) {
|
||||
app().log().debug(__FUNCTION__);
|
||||
qtProxy_.setIsTelemetryDisabledReceived(request->value());
|
||||
return Status::OK;
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[out] response The response.
|
||||
/// \return The status for the call.
|
||||
//****************************************************************************************************************************************************
|
||||
grpc::Status GRPCService::IsTelemetryDisabled(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::BoolValue *response) {
|
||||
app().log().debug(__FUNCTION__);
|
||||
response->set_value(app().mainWindow().settingsTab().isTelemetryDisabled());
|
||||
return Status::OK;
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The status for the call.
|
||||
//****************************************************************************************************************************************************
|
||||
@ -820,3 +842,4 @@ void GRPCService::finishLogin() {
|
||||
|
||||
qtProxy_.sendDelayedEvent(newLoginFinishedEvent(user->id(), alreadyExist));
|
||||
}
|
||||
|
||||
|
||||
@ -51,6 +51,8 @@ public: // member functions.
|
||||
grpc::Status IsBetaEnabled(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::BoolValue *response) override;
|
||||
grpc::Status SetIsAllMailVisible(::grpc::ServerContext *context, ::google::protobuf::BoolValue const *request, ::google::protobuf::Empty *response) override;
|
||||
grpc::Status IsAllMailVisible(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::google::protobuf::BoolValue *response) override;
|
||||
grpc::Status SetIsTelemetryDisabled(::grpc::ServerContext *, ::google::protobuf::BoolValue const *request, ::google::protobuf::Empty *response) override;
|
||||
grpc::Status IsTelemetryDisabled(::grpc::ServerContext *, ::google::protobuf::Empty const *request, ::google::protobuf::BoolValue *response) override;
|
||||
grpc::Status TriggerReset(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::Empty *) override;
|
||||
grpc::Status Version(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::StringValue *response) override;
|
||||
grpc::Status LogsPath(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::StringValue *response) override;
|
||||
|
||||
@ -202,6 +202,22 @@ void SettingsTab::setIsAllMailVisible(bool visible) {
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return the value for the 'Disabled Telemetry' check.
|
||||
//****************************************************************************************************************************************************
|
||||
bool SettingsTab::isTelemetryDisabled() const {
|
||||
return ui_.checkIsTelemetryDisabled->isChecked();
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] isDisabled The new value for the 'Disable Telemetry' check box.
|
||||
//****************************************************************************************************************************************************
|
||||
void SettingsTab::setIsTelemetryDisabled(bool isDisabled) {
|
||||
ui_.checkIsTelemetryDisabled->setChecked(isDisabled);
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The delay to apply before sending automatically generated events.
|
||||
//****************************************************************************************************************************************************
|
||||
|
||||
@ -45,6 +45,7 @@ public: // member functions.
|
||||
bool isAutostartOn() const; ///< Get the value for the 'Autostart' check.
|
||||
bool isBetaEnabled() const; ///< Get the value for the 'Beta Enabled' check.
|
||||
bool isAllMailVisible() const; ///< Get the value for the 'All Mail Visible' check.
|
||||
bool isTelemetryDisabled() const; ///< Get the value for the 'Disable Telemetry' check box.
|
||||
QString colorSchemeName() const; ///< Get the value of the 'Use Dark Theme' checkbox.
|
||||
qint32 eventDelayMs() const; ///< Get the delay for sending automatically generated events.
|
||||
QString logsPath() const; ///< Get the content of the 'Logs Path' edit.
|
||||
@ -74,6 +75,7 @@ public slots:
|
||||
void setIsAutostartOn(bool on); ///< Set the value for the 'Autostart' check box.
|
||||
void setIsBetaEnabled(bool enabled); ///< Set the value for the 'Beta Enabled' check box.
|
||||
void setIsAllMailVisible(bool visible); ///< Set the value for the 'All Mail Visible' check box.
|
||||
void setIsTelemetryDisabled(bool isDisabled); ///< Set the value for the 'Disable Telemetry' check box.
|
||||
void setColorSchemeName(QString const &name); ///< Set the value for the 'Use Dark Theme' check box.
|
||||
void setBugReport(QString const &osType, QString const &osVersion, QString const &emailClient, QString const &address, QString const &description,
|
||||
bool includeLogs); ///< Set the content of the bug report box.
|
||||
|
||||
@ -170,6 +170,13 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QCheckBox" name="checkIsTelemetryDisabled">
|
||||
<property name="text">
|
||||
<string>Disable Telemetry</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
|
||||
4
internal/frontend/bridge-gui/bridge-gui/.lldbinit
Normal file
4
internal/frontend/bridge-gui/bridge-gui/.lldbinit
Normal file
@ -0,0 +1,4 @@
|
||||
# The following fix an issue happening using LLDB with OpenSSL 3.1 on ARM64 architecture. (GODT-2680)
|
||||
# WARNING: this file is ignored if you do not enable reading lldb config from cwd in ~/.lldbinit (`settings set target.load-cwd-lldbinit true`)
|
||||
settings set platform.plugin.darwin.ignored-exceptions EXC_BAD_INSTRUCTION
|
||||
process handle SIGILL -n false -p true -s false
|
||||
@ -117,7 +117,27 @@ void AppController::restart(bool isCrashing) {
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] launcher The launcher.
|
||||
/// \param[in] args The launcher arguments.
|
||||
//****************************************************************************************************************************************************
|
||||
void AppController::setLauncherArgs(const QString &launcher, const QStringList &args) {
|
||||
launcher_ = launcher;
|
||||
launcherArgs_ = args;
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] sessionID The sessionID.
|
||||
//****************************************************************************************************************************************************
|
||||
void AppController::setSessionID(const QString &sessionID) {
|
||||
sessionID_ = sessionID;
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The sessionID.
|
||||
//****************************************************************************************************************************************************
|
||||
QString AppController::sessionID() {
|
||||
return sessionID_;
|
||||
}
|
||||
|
||||
@ -52,7 +52,9 @@ public: // member functions.
|
||||
std::unique_ptr<bridgepp::Overseer> &bridgeOverseer() { return bridgeOverseer_; }; ///< Returns a reference the bridge overseer
|
||||
bridgepp::ProcessMonitor *bridgeMonitor() const; ///< Return the bridge worker.
|
||||
Settings &settings();; ///< Return the application settings.
|
||||
void setLauncherArgs(const QString &launcher, const QStringList &args);
|
||||
void setLauncherArgs(const QString &launcher, const QStringList &args); ///< Set the launcher arguments.
|
||||
void setSessionID(QString const &sessionID); ///< Set the sessionID.
|
||||
QString sessionID(); ///< Get the sessionID.
|
||||
|
||||
public slots:
|
||||
void onFatalError(bridgepp::Exception const &e); ///< Handle fatal errors.
|
||||
@ -67,8 +69,9 @@ private: // data members
|
||||
std::unique_ptr<bridgepp::Log> log_; ///< The log.
|
||||
std::unique_ptr<bridgepp::Overseer> bridgeOverseer_; ///< The overseer for the bridge monitor worker.
|
||||
std::unique_ptr<Settings> settings_; ///< The application settings.
|
||||
QString launcher_;
|
||||
QStringList launcherArgs_;
|
||||
QString launcher_; ///< The launcher.
|
||||
QStringList launcherArgs_; ///< The launcher arguments.
|
||||
QString sessionID_; ///< The sessionID.
|
||||
};
|
||||
|
||||
|
||||
|
||||
@ -23,6 +23,7 @@
|
||||
#define PROJECT_VENDOR "@BRIDGE_VENDOR@"
|
||||
#define PROJECT_VER "@BRIDGE_APP_VERSION@"
|
||||
#define PROJECT_REVISION "@BRIDGE_REVISION@"
|
||||
#define PROJECT_TAG "@BRIDGE_TAG@"
|
||||
#define PROJECT_BUILD_TIME "@BRIDGE_BUILD_TIME@"
|
||||
#define PROJECT_DSN_SENTRY "@BRIDGE_DSN_SENTRY@"
|
||||
#define PROJECT_BUILD_ENV "@BRIDGE_BUILD_ENV@"
|
||||
|
||||
@ -75,7 +75,7 @@ if(NOT UNIX)
|
||||
set(CMAKE_INSTALL_BINDIR ".")
|
||||
endif(NOT UNIX)
|
||||
|
||||
find_package(Qt6 COMPONENTS Core Quick Qml QuickControls2 Widgets REQUIRED)
|
||||
find_package(Qt6 COMPONENTS Core Quick Qml QuickControls2 Widgets Svg REQUIRED)
|
||||
qt_standard_project_setup()
|
||||
set(CMAKE_AUTORCC ON)
|
||||
message(STATUS "Using Qt ${Qt6_VERSION}")
|
||||
@ -114,6 +114,7 @@ add_executable(bridge-gui
|
||||
EventStreamWorker.cpp EventStreamWorker.h
|
||||
LogUtils.cpp LogUtils.h
|
||||
main.cpp
|
||||
TrayIcon.cpp TrayIcon.h
|
||||
Pch.h
|
||||
QMLBackend.cpp QMLBackend.h
|
||||
UserList.cpp UserList.h
|
||||
@ -146,6 +147,7 @@ target_link_libraries(bridge-gui
|
||||
Qt6::Quick
|
||||
Qt6::Qml
|
||||
Qt6::QuickControls2
|
||||
Qt6::Svg
|
||||
sentry::sentry
|
||||
bridgepp
|
||||
)
|
||||
|
||||
@ -19,6 +19,7 @@
|
||||
#include "Pch.h"
|
||||
#include "CommandLine.h"
|
||||
#include "Settings.h"
|
||||
#include <bridgepp/SessionID/SessionID.h>
|
||||
|
||||
|
||||
using namespace bridgepp;
|
||||
@ -142,5 +143,14 @@ CommandLineOptions parseCommandLine(int argc, char *argv[]) {
|
||||
|
||||
options.logLevel = parseLogLevel(argc, argv);
|
||||
|
||||
QString sessionID = parseGoCLIStringArgument(argc, argv, { "session-id" });
|
||||
if (sessionID.isEmpty()) {
|
||||
// The session ID was not passed to us on the command-line -> create one and add to the command-line for bridge
|
||||
sessionID = newSessionID();
|
||||
options.bridgeArgs.append("--session-id");
|
||||
options.bridgeArgs.append(sessionID);
|
||||
}
|
||||
app().setSessionID(sessionID);
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
@ -43,12 +43,9 @@ EventStreamReader::EventStreamReader(QObject *parent)
|
||||
void EventStreamReader::run() {
|
||||
try {
|
||||
emit started();
|
||||
|
||||
grpc::Status const status = app().grpc().runEventStreamReader();
|
||||
if (!status.ok()) {
|
||||
throw Exception(QString::fromStdString(status.error_message()));
|
||||
}
|
||||
|
||||
// Status code for the call below is ignored. The event stream may have interrupted by system shutdown or OS user sign-out, and we do not
|
||||
// want this to generate a sentry report.
|
||||
app().grpc().runEventStreamReader();
|
||||
emit finished();
|
||||
}
|
||||
catch (Exception const &e) {
|
||||
|
||||
@ -19,7 +19,6 @@
|
||||
#include "LogUtils.h"
|
||||
#include "BuildConfig.h"
|
||||
#include <bridgepp/Log/LogUtils.h>
|
||||
#include <bridgepp/BridgeUtils.h>
|
||||
|
||||
|
||||
using namespace bridgepp;
|
||||
@ -33,15 +32,10 @@ Log &initLog() {
|
||||
log.registerAsQtMessageHandler();
|
||||
log.setEchoInConsole(true);
|
||||
|
||||
// remove old gui log files
|
||||
QDir const logsDir(userLogsDir());
|
||||
for (QFileInfo const fileInfo: logsDir.entryInfoList({ "gui_v*.log" }, QDir::Filter::Files)) { // entryInfolist apparently only support wildcards, not regex.
|
||||
QFile(fileInfo.absoluteFilePath()).remove();
|
||||
}
|
||||
|
||||
// create new GUI log file
|
||||
QString error;
|
||||
if (!log.startWritingToFile(logsDir.absoluteFilePath(QString("gui_v%1_%2.log").arg(PROJECT_VER).arg(QDateTime::currentSecsSinceEpoch())), &error)) {
|
||||
if (!log.startWritingToFile(QDir(userLogsDir()).absoluteFilePath(QString("%1_gui_000_v%2_%3.log").arg(app().sessionID(),
|
||||
PROJECT_VER, PROJECT_TAG)), &error)) {
|
||||
log.error(error);
|
||||
}
|
||||
|
||||
|
||||
@ -33,7 +33,7 @@ using namespace bridgepp;
|
||||
/// \brief handle notification of attempt to re-open the application.
|
||||
//****************************************************************************************************************************************************
|
||||
void applicationShouldHandleReopen(id, SEL) {
|
||||
app().backend().showMainWindow();
|
||||
app().backend().showMainWindow("macOS applicationShouldHandleReopen notification");
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
#include <QtQml>
|
||||
#include <QtWidgets>
|
||||
#include <QtQuickControls2>
|
||||
#include <QtSvg>
|
||||
#include <AppController.h>
|
||||
|
||||
|
||||
|
||||
@ -24,7 +24,6 @@
|
||||
#include <bridgepp/Log/LogUtils.h>
|
||||
#include <bridgepp/GRPC/GRPCClient.h>
|
||||
#include <bridgepp/Worker/Overseer.h>
|
||||
#include <bridgepp/BridgeUtils.h>
|
||||
|
||||
|
||||
#define HANDLE_EXCEPTION(x) try { x } \
|
||||
@ -50,6 +49,9 @@ QMLBackend::QMLBackend()
|
||||
/// \param[in] serviceConfig
|
||||
//****************************************************************************************************************************************************
|
||||
void QMLBackend::init(GRPCConfig const &serviceConfig) {
|
||||
trayIcon_.reset(new TrayIcon());
|
||||
this->setNormalTrayIcon();
|
||||
|
||||
connect(this, &QMLBackend::fatalError, &app(), &AppController::onFatalError);
|
||||
|
||||
users_ = new UserList(this);
|
||||
@ -59,7 +61,7 @@ void QMLBackend::init(GRPCConfig const &serviceConfig) {
|
||||
app().grpc().setLog(&log);
|
||||
this->connectGrpcEvents();
|
||||
|
||||
app().grpc().connectToServer(bridgepp::userConfigDir(), serviceConfig, app().bridgeMonitor());
|
||||
app().grpc().connectToServer(app().sessionID(), bridgepp::userConfigDir(), serviceConfig, app().bridgeMonitor());
|
||||
app().log().info("Connected to backend via gRPC service.");
|
||||
|
||||
QString bridgeVer;
|
||||
@ -100,6 +102,60 @@ bool QMLBackend::waitForEventStreamReaderToFinish(qint32 timeoutMs) {
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The list of users
|
||||
//****************************************************************************************************************************************************
|
||||
UserList const &QMLBackend::users() const {
|
||||
return *users_;
|
||||
}
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return the if bridge considers internet is on.
|
||||
//****************************************************************************************************************************************************
|
||||
bool QMLBackend::isInternetOn() const {
|
||||
return isInternetOn_;
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] reason The reason for the request.
|
||||
//****************************************************************************************************************************************************
|
||||
void QMLBackend::showMainWindow(QString const&reason) {
|
||||
app().log().debug(QString("main window show requested: %1").arg(reason));
|
||||
emit showMainWindow();
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] reason The reason for the request.
|
||||
//****************************************************************************************************************************************************
|
||||
void QMLBackend::showHelp(QString const&reason) {
|
||||
app().log().debug(QString("main window show requested (help page): %1").arg(reason));
|
||||
emit showHelp();
|
||||
}
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] reason The reason for the request.
|
||||
//****************************************************************************************************************************************************
|
||||
void QMLBackend::showSettings(QString const&reason) {
|
||||
app().log().debug(QString("main window show requested (settings page): %1").arg(reason));
|
||||
emit showSettings();
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] userID The userID.
|
||||
/// \param[in] forceShowWindow Should the window be force to display.
|
||||
/// \param[in] reason The reason for the request.
|
||||
//****************************************************************************************************************************************************
|
||||
void QMLBackend::selectUser(QString const &userID, bool forceShowWindow, QString const &reason) {
|
||||
if (forceShowWindow) {
|
||||
app().log().debug(QString("main window show requested (user page): %1").arg(reason));
|
||||
}
|
||||
emit selectUser(userID, forceShowWindow);
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The build year as a string (e.g. 2023)
|
||||
//****************************************************************************************************************************************************
|
||||
@ -276,7 +332,7 @@ QString QMLBackend::vendor() const {
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The value for the 'vendor' property.
|
||||
/// \return The value for the 'version' property.
|
||||
//****************************************************************************************************************************************************
|
||||
QString QMLBackend::version() const {
|
||||
HANDLE_EXCEPTION_RETURN_QSTRING(
|
||||
@ -286,6 +342,15 @@ QString QMLBackend::version() const {
|
||||
)
|
||||
}
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The value for the 'tag' property.
|
||||
//****************************************************************************************************************************************************
|
||||
QString QMLBackend::tag() const {
|
||||
HANDLE_EXCEPTION_RETURN_QSTRING(
|
||||
return QString(PROJECT_TAG);
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The value for the 'hostname' property.
|
||||
@ -335,6 +400,18 @@ bool QMLBackend::isAllMailVisible() const {
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The value for the 'isAllMailVisible' property.
|
||||
//****************************************************************************************************************************************************
|
||||
bool QMLBackend::isTelemetryDisabled() const {
|
||||
HANDLE_EXCEPTION_RETURN_BOOL(
|
||||
bool v;
|
||||
app().grpc().isTelemetryDisabled(v);
|
||||
return v;
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The value for the 'colorSchemeName' property.
|
||||
//****************************************************************************************************************************************************
|
||||
@ -569,6 +646,17 @@ void QMLBackend::changeIsAllMailVisible(bool isVisible) {
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] isDisabled The new state of the 'Is telemetry disabled property'.
|
||||
//****************************************************************************************************************************************************
|
||||
void QMLBackend::toggleIsTelemetryDisabled(bool isDisabled) {
|
||||
HANDLE_EXCEPTION(
|
||||
app().grpc().setIsTelemetryDisabled(isDisabled);
|
||||
emit isTelemetryDisabledChanged(isDisabled);
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] scheme the scheme name
|
||||
//****************************************************************************************************************************************************
|
||||
@ -598,7 +686,7 @@ void QMLBackend::login(QString const &username, QString const &password) const {
|
||||
HANDLE_EXCEPTION(
|
||||
if (username.compare("coco@bandicoot", Qt::CaseInsensitive) == 0) {
|
||||
throw Exception("User requested bridge-gui to crash by trying to log as coco@bandicoot",
|
||||
"This error exists for test purposes and should be ignored.", __func__, tailOfLatestBridgeLog());
|
||||
"This error exists for test purposes and should be ignored.", __func__, tailOfLatestBridgeLog(app().sessionID()));
|
||||
}
|
||||
app().grpc().login(username, password);
|
||||
)
|
||||
@ -832,7 +920,94 @@ void QMLBackend::sendBadEventUserFeedback(QString const &userID, bool doResync)
|
||||
if (!badEventDisplayQueue_.isEmpty()) {
|
||||
// we introduce a small delay here, so that the user notices the dialog disappear and pops up again.
|
||||
QTimer::singleShot(500, [&]() { this->displayBadEventDialog(badEventDisplayQueue_.front()); });
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
///
|
||||
//****************************************************************************************************************************************************
|
||||
void QMLBackend::notifyReportBugClicked() const {
|
||||
HANDLE_EXCEPTION(
|
||||
app().grpc().reportBugClicked();
|
||||
)
|
||||
}
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] client The selected Mail client for autoconfig.
|
||||
//****************************************************************************************************************************************************
|
||||
void QMLBackend::notifyAutoconfigClicked(QString const &client) const {
|
||||
HANDLE_EXCEPTION(
|
||||
app().grpc().autoconfigClicked(client);
|
||||
)
|
||||
}
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] article The url of the KB article.
|
||||
//****************************************************************************************************************************************************
|
||||
void QMLBackend::notifyKBArticleClicked(QString const &article) const {
|
||||
HANDLE_EXCEPTION(
|
||||
app().grpc().KBArticleClicked(article);
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
//
|
||||
//****************************************************************************************************************************************************
|
||||
void QMLBackend::setNormalTrayIcon() {
|
||||
if (trayIcon_) {
|
||||
trayIcon_->setState(TrayIcon::State::Normal, tr("Connected"), ":/qml/icons/ic-connected.svg");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] stateString A string describing the state.
|
||||
/// \param[in] statusIcon The path of the status icon.
|
||||
//****************************************************************************************************************************************************
|
||||
void QMLBackend::setErrorTrayIcon(QString const &stateString, QString const &statusIcon) {
|
||||
if (trayIcon_) {
|
||||
trayIcon_->setState(TrayIcon::State::Error, stateString, statusIcon);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] stateString A string describing the state.
|
||||
/// \param[in] statusIcon The path of the status icon.
|
||||
//****************************************************************************************************************************************************
|
||||
void QMLBackend::setWarnTrayIcon(QString const &stateString, QString const &statusIcon) {
|
||||
if (trayIcon_) {
|
||||
trayIcon_->setState(TrayIcon::State::Warn, stateString, statusIcon);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] stateString A string describing the state.
|
||||
/// \param[in] statusIcon The path of the status icon.
|
||||
//****************************************************************************************************************************************************
|
||||
void QMLBackend::setUpdateTrayIcon(QString const &stateString, QString const &statusIcon) {
|
||||
if (trayIcon_) {
|
||||
trayIcon_->setState(TrayIcon::State::Update, stateString, statusIcon);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] isOn Does bridge consider internet as on.
|
||||
//****************************************************************************************************************************************************
|
||||
void QMLBackend::internetStatusChanged(bool isOn) {
|
||||
HANDLE_EXCEPTION(
|
||||
if (isInternetOn_ == isOn) {
|
||||
return;
|
||||
}
|
||||
|
||||
isInternetOn_ = isOn;
|
||||
if (isOn) {
|
||||
emit internetOn();
|
||||
} else {
|
||||
emit internetOff();
|
||||
}
|
||||
)
|
||||
}
|
||||
@ -918,14 +1093,44 @@ void QMLBackend::onUserBadEvent(QString const &userID, QString const& ) {
|
||||
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
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
if (user->isInIMAPLoginFailureCooldown())
|
||||
|
||||
qint64 const cooldownDurationMs = 10 * 60 * 1000; // 10 minutes cooldown period for notifications
|
||||
switch (user->state()) {
|
||||
case UserState::SignedOut:
|
||||
if (user->isNotificationInCooldown(User::ENotification::IMAPLoginWhileSignedOut)) {
|
||||
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());
|
||||
}
|
||||
user->startNotificationCooldownPeriod(User::ENotification::IMAPLoginWhileSignedOut, cooldownDurationMs);
|
||||
emit selectUser(user->id(), true);
|
||||
emit imapLoginWhileSignedOut(username);
|
||||
break;
|
||||
|
||||
case UserState::Connected:
|
||||
if (user->isNotificationInCooldown(User::ENotification::IMAPPasswordFailure)) {
|
||||
return;
|
||||
}
|
||||
user->startNotificationCooldownPeriod(User::ENotification::IMAPPasswordFailure, cooldownDurationMs);
|
||||
emit selectUser(user->id(), false);
|
||||
trayIcon_->showErrorPopupNotification(tr("Incorrect password"),
|
||||
tr("Your email client can't connect to Proton Bridge. Make sure you are using the local Bridge password shown in Bridge."));
|
||||
break;
|
||||
|
||||
case UserState::Locked:
|
||||
if (user->isNotificationInCooldown(User::ENotification::IMAPLoginWhileLocked)) {
|
||||
return;
|
||||
}
|
||||
user->startNotificationCooldownPeriod(User::ENotification::IMAPLoginWhileLocked, cooldownDurationMs);
|
||||
emit selectUser(user->id(), false);
|
||||
trayIcon_->showErrorPopupNotification(tr("Connection in progress"),
|
||||
tr("Your Proton account in Bridge is being connected. Please wait or restart Bridge."));
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@ -971,13 +1176,13 @@ void QMLBackend::connectGrpcEvents() {
|
||||
GRPCClient *client = &app().grpc();
|
||||
|
||||
// app events
|
||||
connect(client, &GRPCClient::internetStatus, this, [&](bool isOn) { if (isOn) { emit internetOn(); } else { emit internetOff(); }});
|
||||
connect(client, &GRPCClient::internetStatus, this, &QMLBackend::internetStatusChanged);
|
||||
connect(client, &GRPCClient::toggleAutostartFinished, this, &QMLBackend::toggleAutostartFinished);
|
||||
connect(client, &GRPCClient::resetFinished, this, &QMLBackend::onResetFinished);
|
||||
connect(client, &GRPCClient::reportBugFinished, this, &QMLBackend::reportBugFinished);
|
||||
connect(client, &GRPCClient::reportBugSuccess, this, &QMLBackend::bugReportSendSuccess);
|
||||
connect(client, &GRPCClient::reportBugError, this, &QMLBackend::bugReportSendError);
|
||||
connect(client, &GRPCClient::showMainWindow, this, &QMLBackend::showMainWindow);
|
||||
connect(client, &GRPCClient::showMainWindow, [&]() { this->showMainWindow("gRPC showMainWindow event"); });
|
||||
|
||||
// cache events
|
||||
connect(client, &GRPCClient::diskCacheUnavailable, this, &QMLBackend::diskCacheUnavailable);
|
||||
@ -1057,7 +1262,7 @@ void QMLBackend::displayBadEventDialog(QString const &userID) {
|
||||
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 selectUser(userID, true);
|
||||
emit showMainWindow();
|
||||
)
|
||||
}
|
||||
|
||||
@ -22,6 +22,7 @@
|
||||
|
||||
#include "MacOS/DockIcon.h"
|
||||
#include "BuildConfig.h"
|
||||
#include "TrayIcon.h"
|
||||
#include "UserList.h"
|
||||
#include <bridgepp/GRPC/GRPCClient.h>
|
||||
#include <bridgepp/GRPC/GRPCUtils.h>
|
||||
@ -43,6 +44,12 @@ public: // member functions.
|
||||
QMLBackend &operator=(QMLBackend &&) = delete; ///< Disabled move assignment operator.
|
||||
void init(GRPCConfig const &serviceConfig); ///< Initialize the backend.
|
||||
bool waitForEventStreamReaderToFinish(qint32 timeoutMs); ///< Wait for the event stream reader to finish.
|
||||
UserList const& users() const; ///< Return the list of users
|
||||
bool isInternetOn() const; ///< Check if bridge considers internet as on.
|
||||
void showMainWindow(QString const &reason); ///< Show the main window.
|
||||
void showHelp(QString const &reason); ///< Show the help page.
|
||||
void showSettings(QString const &reason); ///< Show the settings page.
|
||||
void selectUser(QString const &userID, bool forceShowWindow, QString const &reason); ///< Select the user and display its account details (or login screen).
|
||||
|
||||
// invokable methods can be called from QML. They generally return a value, which slots cannot do.
|
||||
Q_INVOKABLE static QString buildYear(); ///< Return the application build year.
|
||||
@ -63,10 +70,12 @@ public: // Qt/QML properties. Note that the NOTIFY-er signal is required even fo
|
||||
Q_PROPERTY(QString appname READ appname NOTIFY appnameChanged)
|
||||
Q_PROPERTY(QString vendor READ vendor NOTIFY vendorChanged)
|
||||
Q_PROPERTY(QString version READ version NOTIFY versionChanged)
|
||||
Q_PROPERTY(QString tag READ tag NOTIFY tagChanged)
|
||||
Q_PROPERTY(QString hostname READ hostname NOTIFY hostnameChanged)
|
||||
Q_PROPERTY(bool isAutostartOn READ isAutostartOn NOTIFY isAutostartOnChanged)
|
||||
Q_PROPERTY(bool isBetaEnabled READ isBetaEnabled NOTIFY isBetaEnabledChanged)
|
||||
Q_PROPERTY(bool isAllMailVisible READ isAllMailVisible NOTIFY isAllMailVisibleChanged)
|
||||
Q_PROPERTY(bool isTelemetryDisabled READ isTelemetryDisabled NOTIFY isTelemetryDisabledChanged)
|
||||
Q_PROPERTY(QString colorSchemeName READ colorSchemeName NOTIFY colorSchemeNameChanged)
|
||||
Q_PROPERTY(QUrl diskCachePath READ diskCachePath NOTIFY diskCachePathChanged)
|
||||
Q_PROPERTY(bool useSSLForIMAP READ useSSLForIMAP WRITE setUseSSLForIMAP NOTIFY useSSLForIMAPChanged)
|
||||
@ -81,7 +90,6 @@ public: // Qt/QML properties. Note that the NOTIFY-er signal is required even fo
|
||||
Q_PROPERTY(UserList *users MEMBER users_ NOTIFY usersChanged)
|
||||
Q_PROPERTY(bool dockIconVisible READ dockIconVisible WRITE setDockIconVisible NOTIFY dockIconVisibleChanged)
|
||||
|
||||
|
||||
// Qt Property system setters & getters.
|
||||
bool showOnStartup() const; ///< Getter for the 'showOnStartup' property.
|
||||
void setShowSplashScreen(bool show); ///< Setter for the 'showSplashScreen' property.
|
||||
@ -95,10 +103,12 @@ public: // Qt/QML properties. Note that the NOTIFY-er signal is required even fo
|
||||
QString appname() const; ///< Getter for the 'appname' property.
|
||||
QString vendor() const; ///< Getter for the 'vendor' property.
|
||||
QString version() const; ///< Getter for the 'version' property.
|
||||
QString tag() const; ///< Getter for the 'tag' property.
|
||||
QString hostname() const; ///< Getter for the 'hostname' property.
|
||||
bool isAutostartOn() const; ///< Getter for the 'isAutostartOn' property.
|
||||
bool isBetaEnabled() const; ///< Getter for the 'isBetaEnabled' property.
|
||||
bool isAllMailVisible() const; ///< Getter for the 'isAllMailVisible' property.
|
||||
bool isTelemetryDisabled() const; ///< Getter for the 'isTelemetryDisabled' property.
|
||||
QString colorSchemeName() const; ///< Getter for the 'colorSchemeName' property.
|
||||
QUrl diskCachePath() const; ///< Getter for the 'diskCachePath' property.
|
||||
void setUseSSLForIMAP(bool value); ///< Setter for the 'useSSLForIMAP' property.
|
||||
@ -129,6 +139,7 @@ signals: // Signal used by the Qt property system. Many of them are unused but r
|
||||
void isAutomaticUpdateOnChanged(bool value); ///<Signal for the change of the 'isAutomaticUpdateOn' property.
|
||||
void isBetaEnabledChanged(bool value); ///<Signal for the change of the 'isBetaEnabled' property.
|
||||
void isAllMailVisibleChanged(bool value); ///<Signal for the change of the 'isAllMailVisible' property.
|
||||
void isTelemetryDisabledChanged(bool isDisabled); ///<Signal for the change of the 'isTelemetryDisabled' property.
|
||||
void colorSchemeNameChanged(QString const &scheme); ///<Signal for the change of the 'colorSchemeName' property.
|
||||
void isDoHEnabledChanged(bool value); ///<Signal for the change of the 'isDoHEnabled' property.
|
||||
void logsPathChanged(QUrl const &path); ///<Signal for the change of the 'logsPath' property.
|
||||
@ -139,6 +150,7 @@ signals: // Signal used by the Qt property system. Many of them are unused but r
|
||||
void appnameChanged(QString const &appname); ///<Signal for the change of the 'appname' property.
|
||||
void vendorChanged(QString const &vendor); ///<Signal for the change of the 'vendor' property.
|
||||
void versionChanged(QString const &version); ///<Signal for the change of the 'version' property.
|
||||
void tagChanged(QString const &tag); ///<Signal for the change of the 'tag' property.
|
||||
void currentEmailClientChanged(QString const &email); ///<Signal for the change of the 'currentEmailClient' property.
|
||||
void currentKeychainChanged(QString const &keychain); ///<Signal for the change of the 'currentKeychain' property.
|
||||
void availableKeychainChanged(QStringList const &keychains); ///<Signal for the change of the 'availableKeychain' property.
|
||||
@ -151,6 +163,7 @@ public slots: // slot for signals received from QML -> To be forwarded to Bridge
|
||||
void toggleAutostart(bool active); ///< Slot for the autostart toggle.
|
||||
void toggleBeta(bool active); ///< Slot for the beta toggle.
|
||||
void changeIsAllMailVisible(bool isVisible); ///< Slot for the changing of 'All Mail' visibility.
|
||||
void toggleIsTelemetryDisabled(bool isDisabled); ///< Slot for toggling telemetry on/off.
|
||||
void changeColorScheme(QString const &scheme); ///< Slot for the change of the theme.
|
||||
void setDiskCachePath(QUrl const &path) const; ///< Slot for the change of the disk cache path.
|
||||
void login(QString const &username, QString const &password) const; ///< Slot for the login button (initial login).
|
||||
@ -174,8 +187,18 @@ public slots: // slot for signals received from QML -> To be forwarded to Bridge
|
||||
void onVersionChanged(); ///< Slot for the version change signal.
|
||||
void setMailServerSettings(int imapPort, int smtpPort, bool useSSLForIMAP, bool useSSLForSMTP) const; ///< Forwards a connection mode change request from QML to gRPC
|
||||
void sendBadEventUserFeedback(QString const &userID, bool doResync); ///< Slot the providing user feedback for a bad event.
|
||||
void notifyReportBugClicked() const; ///< Slot for the ReportBugClicked gRPC event.
|
||||
void notifyAutoconfigClicked(QString const &client) const; ///< Slot for gAutoconfigClicked gRPC event.
|
||||
void notifyKBArticleClicked(QString const &article) const; ///< Slot for KBArticleClicked gRPC event.
|
||||
|
||||
public slots: // slots for functions that need to be processed locally.
|
||||
void setNormalTrayIcon(); ///< Set the tray icon to normal.
|
||||
void setErrorTrayIcon(QString const& stateString, QString const &statusIcon); ///< Set the tray icon to 'error' state.
|
||||
void setWarnTrayIcon(QString const& stateString, QString const &statusIcon); ///< Set the tray icon to 'warn' state.
|
||||
void setUpdateTrayIcon(QString const& stateString, QString const &statusIcon); ///< Set the tray icon to 'update' state.
|
||||
|
||||
public slots: // slot for signals received from gRPC that need transformation instead of simple forwarding
|
||||
void internetStatusChanged(bool isOn); ///< Check if bridge considers internet as on.
|
||||
void onMailServerSettingsChanged(int imapPort, int smtpPort, bool useSSLForIMAP, bool useSSLForSMTP); ///< Slot for the ConnectionModeChanged gRPC event.
|
||||
void onGenericError(bridgepp::ErrorInfo const &info); ///< Slot for generic errors received from the gRPC service.
|
||||
void onLoginFinished(QString const &userID, bool wasSignedOut); ///< Slot for LoginFinished gRPC event.
|
||||
@ -233,8 +256,10 @@ signals: // Signals received from the Go backend, to be forwarded to QML
|
||||
void bugReportSendError(); ///< Signal for the 'bugReportSendError' gRPC stream event.
|
||||
void showMainWindow(); ///< Signal for the 'showMainWindow' gRPC stream event.
|
||||
void hideMainWindow(); ///< Signal for the 'hideMainWindow' gRPC stream event.
|
||||
void showHelp(); ///< Signal for the 'showHelp' event (from the context menu).
|
||||
void showSettings(); ///< Signal for the 'showHelp' event (from the context menu).
|
||||
void selectUser(QString const& userID, bool forceShowWindow); ///< Signal emitted in order to selected a user with a given ID in the list.
|
||||
void genericError(QString const &title, QString const &description); ///< Signal for the 'genericError' gRPC stream event.
|
||||
void selectUser(QString const); ///< Signal that request the given user account to be displayed.
|
||||
void imapLoginWhileSignedOut(QString const& username); ///< Signal for the notification of IMAP login attempt on a signed out account.
|
||||
|
||||
// This signal is emitted when an exception is intercepted is calls triggered by QML. QML engine would intercept the exception otherwise.
|
||||
@ -256,8 +281,9 @@ private: // data members
|
||||
int smtpPort_ { 0 }; ///< The cached value for the SMTP port.
|
||||
bool useSSLForIMAP_ { false }; ///< The cached value for useSSLForIMAP.
|
||||
bool useSSLForSMTP_ { false }; ///< The cached value for useSSLForSMTP.
|
||||
bool isInternetOn_ { true }; ///< Does bridge consider internet as on?
|
||||
QList<QString> badEventDisplayQueue_; ///< THe queue for displaying 'bad event feedback request dialog'.
|
||||
|
||||
std::unique_ptr<TrayIcon> trayIcon_; ///< The tray icon for the application.
|
||||
friend class AppController;
|
||||
};
|
||||
|
||||
|
||||
@ -5,11 +5,7 @@
|
||||
<file>qml/AccountView.qml</file>
|
||||
<file>qml/Banner.qml</file>
|
||||
<file>qml/Bridge.qml</file>
|
||||
<file>qml/Bridge_test.qml</file>
|
||||
<file>qml/bridgeqml.qmlproject</file>
|
||||
<file>qml/BridgeTest/UserControl.qml</file>
|
||||
<file>qml/BridgeTest/UserList.qml</file>
|
||||
<file>qml/BridgeTest/UserModel.qml</file>
|
||||
<file>qml/BugReportView.qml</file>
|
||||
<file>qml/Configuration.qml</file>
|
||||
<file>qml/ConfigurationItem.qml</file>
|
||||
@ -23,11 +19,13 @@
|
||||
<file>qml/icons/ic-card-identity.svg</file>
|
||||
<file>qml/icons/ic-check.svg</file>
|
||||
<file>qml/icons/ic-chevron-down.svg</file>
|
||||
<file>qml/icons/ic-chevron-right.svg</file>
|
||||
<file>qml/icons/ic-chevron-up.svg</file>
|
||||
<file>qml/icons/ic-cog-wheel.svg</file>
|
||||
<file>qml/icons/ic-connected.svg</file>
|
||||
<file>qml/icons/ic-copy.svg</file>
|
||||
<file>qml/icons/ic-cross-close.svg</file>
|
||||
<file>qml/icons/ic-dot.svg</file>
|
||||
<file>qml/icons/ic-drive.svg</file>
|
||||
<file>qml/icons/ic-exclamation-circle-filled.svg</file>
|
||||
<file>qml/icons/ic-external-link.svg</file>
|
||||
@ -104,17 +102,6 @@
|
||||
<file>qml/ConnectionModeSettings.qml</file>
|
||||
<file>qml/SplashScreen.qml</file>
|
||||
<file>qml/Status.qml</file>
|
||||
<file>qml/StatusWindow.qml</file>
|
||||
<file>qml/tests/Buttons.qml</file>
|
||||
<file>qml/tests/ButtonsColumn.qml</file>
|
||||
<file>qml/tests/CheckBoxes.qml</file>
|
||||
<file>qml/tests/ComboBoxes.qml</file>
|
||||
<file>qml/tests/RadioButtons.qml</file>
|
||||
<file>qml/tests/Switches.qml</file>
|
||||
<file>qml/tests/Test.qml</file>
|
||||
<file>qml/tests/TestComponents.qml</file>
|
||||
<file>qml/tests/TextAreas.qml</file>
|
||||
<file>qml/tests/TextFields.qml</file>
|
||||
<file>qml/WelcomeGuide.qml</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
@ -49,7 +49,7 @@ QString sentryAttachmentFilePath() {
|
||||
//****************************************************************************************************************************************************
|
||||
QByteArray getProtectedHostname() {
|
||||
QByteArray hostname = QCryptographicHash::hash(QSysInfo::machineHostName().toUtf8(), QCryptographicHash::Sha256);
|
||||
return hostname.toHex();
|
||||
return hostname.toBase64();
|
||||
}
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
|
||||
@ -22,6 +22,7 @@
|
||||
#include <sentry.h>
|
||||
|
||||
void initSentry();
|
||||
QByteArray getProtectedHostname();
|
||||
void setSentryReportScope();
|
||||
sentry_options_t* newSentryOptions(const char * sentryDNS, const char * cacheDir);
|
||||
sentry_uuid_t reportSentryEvent(sentry_level_t level, const char *message);
|
||||
|
||||
388
internal/frontend/bridge-gui/bridge-gui/TrayIcon.cpp
Normal file
388
internal/frontend/bridge-gui/bridge-gui/TrayIcon.cpp
Normal file
@ -0,0 +1,388 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
#include "TrayIcon.h"
|
||||
#include "QMLBackend.h"
|
||||
#include <bridgepp/Exception/Exception.h>
|
||||
#include <bridgepp/BridgeUtils.h>
|
||||
|
||||
|
||||
using namespace bridgepp;
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
|
||||
QColor const normalColor(30, 168, 133); /// The normal state color.
|
||||
QColor const errorColor(220, 50, 81); ///< The error state color.
|
||||
QColor const warnColor(255, 153, 0); ///< The warn state color.
|
||||
QColor const updateColor(35, 158, 206); ///< The warn state color.
|
||||
QColor const greyColor(112, 109, 107); ///< The grey color.
|
||||
qint64 const iconRefreshTimerIntervalMs = 1000; ///< The interval for the refresh timer when switching DPI / screen config, in milliseconds.
|
||||
qint64 const iconRefreshDurationSecs = 10; ///< The total number of seconds during which we periodically refresh the icon after a DPI change.
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \brief Create a single resolution icon from an image. Throw an exception in case of failure.
|
||||
//****************************************************************************************************************************************************
|
||||
QIcon loadIconFromImage(QString const &path) {
|
||||
QPixmap const pixmap(path);
|
||||
if (pixmap.isNull()) {
|
||||
throw Exception(QString("Could not create an icon from an image '%1'.").arg(path));
|
||||
}
|
||||
return QIcon(pixmap);
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \brief Generate an icon from a SVG renderer (a.k.a. path).
|
||||
///
|
||||
/// \param[in] renderer The SVG renderer.
|
||||
/// \param[in] color The color to use in case the SVG path is to be used as a mask.
|
||||
/// \return The icon.
|
||||
//****************************************************************************************************************************************************
|
||||
QIcon loadIconFromSVGRenderer(QSvgRenderer &renderer, QColor const &color = QColor()) {
|
||||
if (!renderer.isValid()) {
|
||||
return QIcon();
|
||||
}
|
||||
QIcon icon;
|
||||
qint32 size = 256;
|
||||
|
||||
while (size >= 16) {
|
||||
QPixmap pixmap(size, size);
|
||||
pixmap.fill(QColor(0, 0, 0, 0));
|
||||
QPainter painter(&pixmap);
|
||||
renderer.render(&painter);
|
||||
if (color.isValid()) {
|
||||
painter.setCompositionMode(QPainter::CompositionMode_SourceIn);
|
||||
painter.fillRect(pixmap.rect(), color);
|
||||
}
|
||||
painter.end();
|
||||
icon.addPixmap(pixmap);
|
||||
size /= 2;
|
||||
}
|
||||
|
||||
return icon;
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \brief Load a multi-resolution icon from a SVG file. The image is assumed to be square. SVG is rasterized in 256, 128, 64, 32 and 16px.
|
||||
///
|
||||
/// Note: QPixmap can load SVG files directly, but our SVG file are defined in small shape size and QPixmap will rasterize them a very low resolution
|
||||
/// by default (eg. 16x16), which is insufficient for some uses. As a consequence, we manually generate a multi-resolution icon that render smoothly
|
||||
/// at any acceptable resolution for an icon.
|
||||
///
|
||||
/// \param[in] path The path of the SVG file.
|
||||
/// \return The icon.
|
||||
//****************************************************************************************************************************************************
|
||||
QIcon loadIconFromSVG(QString const &path, QColor const &color = QColor()) {
|
||||
QSvgRenderer renderer(path);
|
||||
QIcon const icon = loadIconFromSVGRenderer(renderer, color);
|
||||
if (icon.isNull()) {
|
||||
Exception(QString("Could not create an icon from a vector image '%1'.").arg(path));
|
||||
}
|
||||
return icon;
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
//
|
||||
//****************************************************************************************************************************************************
|
||||
QIcon loadIcon(QString const &path) {
|
||||
if (path.endsWith(".svg", Qt::CaseInsensitive)) {
|
||||
return loadIconFromSVG(path);
|
||||
}
|
||||
return loadIconFromImage(path);
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \brief Retrieve the color associated with a tray icon state.
|
||||
///
|
||||
/// \param[in] state The state.
|
||||
/// \return The color associated with a given tray icon state.
|
||||
//****************************************************************************************************************************************************
|
||||
QColor stateColor(TrayIcon::State state) {
|
||||
switch (state) {
|
||||
case TrayIcon::State::Normal:
|
||||
return normalColor;
|
||||
case TrayIcon::State::Error:
|
||||
return errorColor;
|
||||
case TrayIcon::State::Warn:
|
||||
return warnColor;
|
||||
case TrayIcon::State::Update:
|
||||
return updateColor;
|
||||
default:
|
||||
app().log().error(QString("Unknown tray icon state %1.").arg(static_cast<qint32>(state)));
|
||||
return normalColor;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \brief Return the text identifying a state in resource file names.
|
||||
///
|
||||
/// \param[in] state The state.
|
||||
/// \param[in] The text identifying the state in resource file names.
|
||||
//****************************************************************************************************************************************************
|
||||
QString stateText(TrayIcon::State state) {
|
||||
switch (state) {
|
||||
case TrayIcon::State::Normal:
|
||||
return "norm";
|
||||
case TrayIcon::State::Error:
|
||||
return "error";
|
||||
case TrayIcon::State::Warn:
|
||||
return "warn";
|
||||
case TrayIcon::State::Update:
|
||||
return "update";
|
||||
default:
|
||||
app().log().error(QString("Unknown tray icon state %1.").arg(static_cast<qint32>(state)));
|
||||
return "norm";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \brief converts a QML resource path to Qt resource path.
|
||||
/// QML resource paths are a bit different from qt resource paths
|
||||
/// \param[in] path The resource path.
|
||||
/// \return
|
||||
//****************************************************************************************************************************************************
|
||||
QString qmlResourcePathToQt(QString const &path) {
|
||||
QString result = path;
|
||||
result.replace(QRegularExpression(R"(^\.\/)"), ":/qml/");
|
||||
return result;
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
//
|
||||
//****************************************************************************************************************************************************
|
||||
TrayIcon::TrayIcon()
|
||||
: QSystemTrayIcon()
|
||||
, menu_(new QMenu)
|
||||
, notificationErrorIcon_(loadIconFromSVG(":/qml/icons/ic-alert.svg")) {
|
||||
this->generateDotIcons();
|
||||
this->setContextMenu(menu_.get());
|
||||
|
||||
connect(menu_.get(), &QMenu::aboutToShow, this, &TrayIcon::onMenuAboutToShow);
|
||||
connect(this, &TrayIcon::selectUser, &app().backend(), [](QString const& userID, bool forceShowWindow) {
|
||||
app().backend().selectUser(userID, forceShowWindow);
|
||||
});
|
||||
connect(this, &TrayIcon::activated, this, &TrayIcon::onActivated);
|
||||
// some OSes/Desktop managers will automatically show main window when clicked, but not all, so we do it manually.
|
||||
if (!onLinux()) { // we disable this on linux because of a Qt bug that causes the signal to be emitted for other apps (GODT-2750)
|
||||
connect(this, &TrayIcon::messageClicked, []() { app().backend().showMainWindow("tray icon popup notification clicked"); });
|
||||
}
|
||||
this->show();
|
||||
|
||||
// TrayIcon does not expose its screen, so we connect relevant screen events to our DPI change handler.
|
||||
for (QScreen *screen: QGuiApplication::screens()) {
|
||||
connect(screen, &QScreen::logicalDotsPerInchChanged, this, &TrayIcon::handleDPIChange);
|
||||
}
|
||||
connect(qApp, &QApplication::screenAdded, [&](QScreen *screen) { connect(screen, &QScreen::logicalDotsPerInchChanged, this, &TrayIcon::handleDPIChange); });
|
||||
connect(qApp, &QApplication::primaryScreenChanged, [&](QScreen *screen) { connect(screen, &QScreen::logicalDotsPerInchChanged, this, &TrayIcon::handleDPIChange); });
|
||||
|
||||
iconRefreshTimer_.setSingleShot(false);
|
||||
iconRefreshTimer_.setInterval(iconRefreshTimerIntervalMs);
|
||||
connect(&iconRefreshTimer_, &QTimer::timeout, this, &TrayIcon::onIconRefreshTimer);
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
//
|
||||
//****************************************************************************************************************************************************
|
||||
void TrayIcon::onMenuAboutToShow() {
|
||||
this->refreshContextMenu();
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
//
|
||||
//****************************************************************************************************************************************************
|
||||
void TrayIcon::onUserClicked() {
|
||||
try {
|
||||
auto action = dynamic_cast<QAction *>(this->sender());
|
||||
if (!action) {
|
||||
throw Exception("Could not retrieve context menu action.");
|
||||
}
|
||||
|
||||
QString const &userID = action->data().toString();
|
||||
if (userID.isNull()) {
|
||||
throw Exception("Could not retrieve context menu's selected user.");
|
||||
}
|
||||
|
||||
app().backend().selectUser(userID, true, "tray menu user clicked");
|
||||
} catch (Exception const &e) {
|
||||
app().log().error(e.qwhat());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] reason The icon activation reason.
|
||||
//****************************************************************************************************************************************************
|
||||
void TrayIcon::onActivated(QSystemTrayIcon::ActivationReason reason) {
|
||||
if ((QSystemTrayIcon::Trigger == reason) && !onMacOS()) {
|
||||
app().backend().showMainWindow("tray icon activated");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
//
|
||||
//****************************************************************************************************************************************************
|
||||
void TrayIcon::handleDPIChange() {
|
||||
this->setIcon();
|
||||
|
||||
// Windows forces us to apply a hack. Tray icon does not redraw by itself, so we use the Qt signal that detects screen and DPI changes.
|
||||
// But the moment we get the signal the DPI change is not yet in effect. so redrawing now will have no effect, and we don't really
|
||||
// know when we can safely redraw. So we will redraw the icon every second for some time.
|
||||
iconRefreshDeadline_ = QDateTime::currentDateTime().addSecs(iconRefreshDurationSecs);
|
||||
if (!iconRefreshTimer_.isActive()) {
|
||||
iconRefreshTimer_.start();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
//
|
||||
//****************************************************************************************************************************************************
|
||||
void TrayIcon::setIcon() {
|
||||
QString const style = onMacOS() ? "mono" : "color";
|
||||
QString const text = stateText(state_);
|
||||
|
||||
QIcon icon = loadIconFromImage(QString(":/qml/icons/systray-%1-%2.png").arg(style, text));
|
||||
icon.setIsMask(true);
|
||||
QSystemTrayIcon::setIcon(icon);
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
//
|
||||
//****************************************************************************************************************************************************
|
||||
void TrayIcon::onIconRefreshTimer() {
|
||||
this->setIcon();
|
||||
if (QDateTime::currentDateTime() > iconRefreshDeadline_) {
|
||||
iconRefreshTimer_.stop();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
//
|
||||
//****************************************************************************************************************************************************
|
||||
void TrayIcon::generateDotIcons() {
|
||||
QSvgRenderer dotSVG(QString(":/qml/icons/ic-dot.svg"));
|
||||
|
||||
struct IconColor {
|
||||
QIcon &icon;
|
||||
QColor color;
|
||||
};
|
||||
for (auto pair: QList<IconColor> {{ greenDot_, normalColor }, { greyDot_, greyColor }, { orangeDot_, warnColor }}) {
|
||||
pair.icon = loadIconFromSVGRenderer(dotSVG, pair.color);
|
||||
if (pair.icon.isNull()) {
|
||||
throw Exception("Could not generate dot icon from vector file.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] state The state.
|
||||
/// \param[in] stateString A string describing the state.
|
||||
/// \param[in] statusIconPath The status icon path.
|
||||
/// \param[in] statusIconColor The color for the status icon in hex.
|
||||
//****************************************************************************************************************************************************
|
||||
void TrayIcon::setState(TrayIcon::State state, QString const &stateString, QString const &statusIconPath) {
|
||||
stateString_ = stateString;
|
||||
state_ = state;
|
||||
this->setIcon();
|
||||
this->generateStatusIcon(statusIconPath, stateColor(state));
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] title The title.
|
||||
/// \param[in] message The message.
|
||||
//****************************************************************************************************************************************************
|
||||
void TrayIcon::showErrorPopupNotification(QString const &title, QString const &message) {
|
||||
this->showMessage(title, message, notificationErrorIcon_);
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] svgPath The path of the SVG file for the icon.
|
||||
/// \param[in] color The color to apply to the icon.
|
||||
//****************************************************************************************************************************************************
|
||||
void TrayIcon::generateStatusIcon(QString const &svgPath, QColor const &color) {
|
||||
// We use the SVG path as pixmap mask and fill it with the appropriate color
|
||||
statusIcon_ = loadIconFromSVG(qmlResourcePathToQt(svgPath), color);
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
//
|
||||
//****************************************************************************************************************************************************
|
||||
void TrayIcon::refreshContextMenu() {
|
||||
if (!menu_) {
|
||||
app().log().error("Native tray icon context menu is null.");
|
||||
return;
|
||||
}
|
||||
|
||||
bool const internetOn = app().backend().isInternetOn();
|
||||
menu_->clear();
|
||||
menu_->addAction(statusIcon_, stateString_, []() {app().backend().showMainWindow("tray menu status clicked");});
|
||||
menu_->addSeparator();
|
||||
QKeySequence noShortcut;
|
||||
UserList const &users = app().backend().users();
|
||||
qint32 const userCount = users.count();
|
||||
bool const onMac = onMacOS();
|
||||
for (qint32 i = 0; i < userCount; i++) {
|
||||
User const &user = *users.get(i);
|
||||
UserState const state = user.state();
|
||||
auto action = new QAction(user.primaryEmailOrUsername());
|
||||
if (internetOn) {
|
||||
action->setIcon((UserState::Connected == state) ? greenDot_ : (UserState::Locked == state ? orangeDot_ : greyDot_));
|
||||
}
|
||||
action->setData(user.id());
|
||||
connect(action, &QAction::triggered, this, &TrayIcon::onUserClicked);
|
||||
if ((i < 10) && onMac) {
|
||||
action->setShortcut(QKeySequence(QString("Ctrl+%1").arg((i + 1) % 10)));
|
||||
}
|
||||
menu_->addAction(action);
|
||||
}
|
||||
if (userCount) {
|
||||
menu_->addSeparator();
|
||||
}
|
||||
|
||||
menu_->addAction(tr("&Open Bridge"), onMac ? QKeySequence("Ctrl+O") : noShortcut, []() {
|
||||
app().backend().showMainWindow("tray menu 'open bridge' clicked");
|
||||
});
|
||||
menu_->addAction(tr("&Help"), onMac ? QKeySequence("Ctrl+F1") : noShortcut, []() {
|
||||
app().backend().showHelp("tray menu 'Help' clicked");
|
||||
});
|
||||
menu_->addAction(tr("&Settings"), onMac ? QKeySequence("Ctrl+,") : noShortcut, []() {
|
||||
app().backend().showSettings("tray menu 'Settings' clicked");
|
||||
});
|
||||
menu_->addSeparator();
|
||||
menu_->addAction(tr("&Quit Bridge"), onMac ? QKeySequence("Ctrl+Q") : noShortcut, &app().backend(), &QMLBackend::quit);
|
||||
}
|
||||
78
internal/frontend/bridge-gui/bridge-gui/TrayIcon.h
Normal file
78
internal/frontend/bridge-gui/bridge-gui/TrayIcon.h
Normal file
@ -0,0 +1,78 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
#ifndef BRIDGE_GUI_NATIVE_TRAY_ICON_H
|
||||
#define BRIDGE_GUI_NATIVE_TRAY_ICON_H
|
||||
|
||||
|
||||
//**********************************************************************************************************************
|
||||
/// \brief A native tray icon.
|
||||
//**********************************************************************************************************************
|
||||
class TrayIcon: public QSystemTrayIcon {
|
||||
Q_OBJECT
|
||||
public: // typedef enum
|
||||
enum class State {
|
||||
Normal,
|
||||
Error,
|
||||
Warn,
|
||||
Update,
|
||||
}; ///< Enumeration for the state.
|
||||
|
||||
public: // data members
|
||||
TrayIcon(); ///< Default constructor.
|
||||
~TrayIcon() override = default; ///< Destructor.
|
||||
TrayIcon(TrayIcon const&) = delete; ///< Disabled copy-constructor.
|
||||
TrayIcon(TrayIcon&&) = delete; ///< Disabled assignment copy-constructor.
|
||||
TrayIcon& operator=(TrayIcon const&) = delete; ///< Disabled assignment operator.
|
||||
TrayIcon& operator=(TrayIcon&&) = delete; ///< Disabled move assignment operator.
|
||||
void setState(State state, QString const& stateString, QString const &statusIconPath); ///< Set the state of the icon
|
||||
void showErrorPopupNotification(QString const& title, QString const &message); ///< Display a pop up notification.
|
||||
|
||||
signals:
|
||||
void selectUser(QString const& userID, bool forceShowWindow); ///< Signal for selecting a user with a given userID
|
||||
|
||||
private slots:
|
||||
void onMenuAboutToShow(); ///< Slot called before the context menu is shown.
|
||||
void onUserClicked(); ///< Slot triggered when clicking on a user in the context menu.
|
||||
static void onActivated(QSystemTrayIcon::ActivationReason reason); ///< Slot for the activation of the system tray icon.
|
||||
void handleDPIChange(); ///< Handles DPI change.
|
||||
void setIcon(); ///< set the tray icon.
|
||||
void onIconRefreshTimer(); ///< Timer for icon refresh.
|
||||
|
||||
private: // member functions.
|
||||
void generateDotIcons(); ///< generate the colored dot icons used for user status.
|
||||
void generateStatusIcon(QString const &svgPath, QColor const& color); ///< Generate the status icon.
|
||||
void refreshContextMenu(); ///< Refresh the context menu.
|
||||
|
||||
private: // data members
|
||||
State state_ { State::Normal }; ///< The state of the tray icon.
|
||||
QString stateString_; ///< The current state string.
|
||||
std::unique_ptr<QMenu> menu_; ///< The context menu for the tray icon. Not owned by the tray icon.
|
||||
QIcon statusIcon_; ///< The path of the status icon displayed in the context menu.
|
||||
QIcon greenDot_; ///< The green dot icon.
|
||||
QIcon greyDot_; ///< The grey dot icon.
|
||||
QIcon orangeDot_; ///< The orange dot icon.
|
||||
QIcon const notificationErrorIcon_; ///< The error icon used for notifications.
|
||||
|
||||
QTimer iconRefreshTimer_; ///< The timer used to periodically refresh the icon when DPI changes.
|
||||
QDateTime iconRefreshDeadline_; ///< The deadline for refreshing the icon
|
||||
};
|
||||
|
||||
|
||||
|
||||
#endif //BRIDGE_GUI_NATIVE_TRAY_ICON_H
|
||||
@ -76,6 +76,7 @@ function check_exit() {
|
||||
Write-host "Running build for version $bridgeVersion - $buildConfig in $buildDir"
|
||||
|
||||
$REVISION_HASH = git rev-parse --short=10 HEAD
|
||||
$bridgeTag = ($env:BRIDGE_TAG)
|
||||
$bridgeDsnSentry = ($env:BRIDGE_DSN_SENTRY)
|
||||
$bridgeBuidTime = ($env:BRIDGE_BUILD_TIME)
|
||||
|
||||
@ -93,6 +94,7 @@ git submodule update --init --recursive $vcpkgRoot
|
||||
-DBRIDGE_APP_FULL_NAME="$bridgeFullName" `
|
||||
-DBRIDGE_VENDOR="$bridgeVendor" `
|
||||
-DBRIDGE_REVISION="$REVISION_HASH" `
|
||||
-DBRIDGE_TAG="$bridgeTag" `
|
||||
-DBRIDGE_APP_VERSION="$bridgeVersion" `
|
||||
-DBRIDGE_BUILD_TIME="$bridgeBuidTime" `
|
||||
-DBRIDGE_DSN_SENTRY="$bridgeDsnSentry" `
|
||||
|
||||
@ -56,6 +56,7 @@ 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_TAG=${BRIDGE_TAG:-"NOTAG"}
|
||||
BRIDGE_DSN_SENTRY=${BRIDGE_DSN_SENTRY}
|
||||
BRIDGE_BUILD_TIME=${BRIDGE_BUILD_TIME}
|
||||
BRIDGE_BUILD_ENV= ${BRIDGE_BUILD_ENV:-"dev"}
|
||||
@ -97,6 +98,7 @@ cmake \
|
||||
-DBRIDGE_APP_FULL_NAME="${BRIDGE_APP_FULL_NAME}" \
|
||||
-DBRIDGE_VENDOR="${BRIDGE_VENDOR}" \
|
||||
-DBRIDGE_REVISION="${BRIDGE_REVISION}" \
|
||||
-DBRIDGE_TAG="${BRIDGE_TAG}" \
|
||||
-DBRIDGE_DSN_SENTRY="${BRIDGE_DSN_SENTRY}" \
|
||||
-DBRIDGE_BRIDGE_TIME="${BRIDGE_BRIDGE_TIME}" \
|
||||
-DBRIDGE_BUILD_ENV="${BRIDGE_BUILD_ENV}" \
|
||||
@ -106,10 +108,10 @@ cmake \
|
||||
-B "${BUILD_DIR}"
|
||||
check_exit "CMake failed"
|
||||
|
||||
cmake --build "${BUILD_DIR}"
|
||||
cmake --build "${BUILD_DIR}" -v
|
||||
check_exit "build failed"
|
||||
|
||||
if [ "$1" == "install" ]; then
|
||||
cmake --install "${BUILD_DIR}"
|
||||
cmake --install "${BUILD_DIR}" -v
|
||||
check_exit "install failed"
|
||||
fi
|
||||
|
||||
@ -196,7 +196,7 @@ bool isBridgeRunning() {
|
||||
//****************************************************************************************************************************************************
|
||||
void focusOtherInstance() {
|
||||
try {
|
||||
FocusGRPCClient client;
|
||||
FocusGRPCClient client(app().log());
|
||||
GRPCConfig sc;
|
||||
QString const path = FocusGRPCClient::grpcFocusServerConfigPath(bridgepp::userConfigDir());
|
||||
QFile file(path);
|
||||
@ -209,12 +209,11 @@ void focusOtherInstance() {
|
||||
throw Exception("Server did not provide gRPC Focus service configuration.");
|
||||
}
|
||||
|
||||
|
||||
QString error;
|
||||
if (!client.connectToServer(5000, sc.port, &error)) {
|
||||
throw Exception("Could not connect to bridge focus service for a raise call.", error);
|
||||
}
|
||||
if (!client.raise().ok()) {
|
||||
if (!client.raise("focusOtherInstance").ok()) {
|
||||
throw Exception(QString("The raise call to the bridge focus service failed."));
|
||||
}
|
||||
}
|
||||
@ -286,7 +285,9 @@ int main(int argc, char *argv[]) {
|
||||
|
||||
initQtApplication();
|
||||
|
||||
CommandLineOptions const cliOptions = parseCommandLine(argc, argv);
|
||||
Log &log = initLog();
|
||||
log.setLevel(cliOptions.logLevel);
|
||||
|
||||
QLockFile lock(bridgepp::userCacheDir() + "/" + bridgeGUILock);
|
||||
if (!checkSingleInstance(lock)) {
|
||||
@ -294,8 +295,6 @@ int main(int argc, char *argv[]) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
CommandLineOptions const cliOptions = parseCommandLine(argc, argv);
|
||||
|
||||
#ifdef Q_OS_MACOS
|
||||
registerSecondInstanceHandler();
|
||||
setDockIconVisibleState(!cliOptions.noWindow);
|
||||
@ -304,23 +303,25 @@ int main(int argc, char *argv[]) {
|
||||
// In attached mode, we do not intercept stderr and stdout of bridge, as we did not launch it ourselves, so we output the log to the console.
|
||||
// When not in attached mode, log entries are forwarded to bridge, which output it on stdout/stderr. bridge-gui's process monitor intercept
|
||||
// these outputs and output them on the command-line.
|
||||
log.setLevel(cliOptions.logLevel);
|
||||
QString bridgeexec;
|
||||
log.info(QString("New Sentry reporter - id: %1.").arg(getProtectedHostname()));
|
||||
|
||||
QString const &sessionID = app().sessionID();
|
||||
QString bridgeExe;
|
||||
if (!cliOptions.attach) {
|
||||
if (isBridgeRunning()) {
|
||||
throw Exception("An orphan instance of bridge is already running. Please terminate it and relaunch the application.",
|
||||
QString(), __FUNCTION__, tailOfLatestBridgeLog());
|
||||
QString(), __FUNCTION__, tailOfLatestBridgeLog(sessionID));
|
||||
}
|
||||
|
||||
// before launching bridge, we remove any trailing service config file, because we need to make sure we get a newly generated one.
|
||||
FocusGRPCClient::removeServiceConfigFile(configDir);
|
||||
GRPCClient::removeServiceConfigFile(configDir);
|
||||
bridgeexec = launchBridge(cliOptions.bridgeArgs);
|
||||
bridgeExe = launchBridge(cliOptions.bridgeArgs);
|
||||
}
|
||||
|
||||
log.info(QString("Retrieving gRPC service configuration from '%1'").arg(QDir::toNativeSeparators(grpcServerConfigPath(configDir))));
|
||||
app().backend().init(GRPCClient::waitAndRetrieveServiceConfig(configDir, cliOptions.attach ? 0 : grpcServiceConfigWaitDelayMs,
|
||||
app().bridgeMonitor()));
|
||||
app().backend().init(GRPCClient::waitAndRetrieveServiceConfig(sessionID, configDir,
|
||||
cliOptions.attach ? 0 : grpcServiceConfigWaitDelayMs, app().bridgeMonitor()));
|
||||
if (!cliOptions.attach) {
|
||||
GRPCClient::removeServiceConfigFile(configDir);
|
||||
}
|
||||
@ -378,9 +379,9 @@ int main(int argc, char *argv[]) {
|
||||
QStringList args = cliOptions.bridgeGuiArgs;
|
||||
args.append(waitFlag);
|
||||
args.append(mainexec);
|
||||
if (!bridgeexec.isEmpty()) {
|
||||
if (!bridgeExe.isEmpty()) {
|
||||
args.append(waitFlag);
|
||||
args.append(bridgeexec);
|
||||
args.append(bridgeExe);
|
||||
}
|
||||
app().setLauncherArgs(cliOptions.launcher, args);
|
||||
result = QGuiApplication::exec();
|
||||
|
||||
@ -27,67 +27,55 @@ Item {
|
||||
property var notifications
|
||||
property var user
|
||||
|
||||
signal showSignIn()
|
||||
signal showSignIn
|
||||
|
||||
signal showSetupGuide(var user, string address)
|
||||
|
||||
property int _leftMargin: 64
|
||||
property int _rightMargin: 64
|
||||
property int _contentWidth: 640
|
||||
property int _topMargin: 32
|
||||
property int _detailsTopMargin: 25
|
||||
property int _bottomMargin: 12
|
||||
property int _detailsMargin: 25
|
||||
property int _spacing: 20
|
||||
property int _lineWidth: 1
|
||||
property int _lineThickness: 1
|
||||
property bool _connected: root.user ? root.user.state === EUserState.Connected : false
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: root.colorScheme.background_weak
|
||||
|
||||
ScrollView {
|
||||
id: scrollView
|
||||
clip: true
|
||||
|
||||
anchors.fill: parent
|
||||
Component.onCompleted: contentItem.boundsBehavior = Flickable.StopAtBounds // Disable the springy effect when scroll reaches top/bottom.
|
||||
|
||||
Item {
|
||||
// can't use parent here because parent is not ScrollView (Flickable inside contentItem inside ScrollView)
|
||||
width: scrollView.availableWidth
|
||||
height: scrollView.availableHeight
|
||||
|
||||
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
|
||||
// do not set implicitWidth because implicit width of ColumnLayout will be equal to maximum implicit width of
|
||||
// internal items. And if one of internal items would be a Text or Label - implicit width of those is always
|
||||
// equal to non-wrapped text (i.e. one line only). That will lead to enabling horizontal scroll when not needed
|
||||
implicitWidth: width
|
||||
Component.onCompleted: contentItem.boundsBehavior = Flickable.StopAtBounds
|
||||
|
||||
ColumnLayout {
|
||||
id: topLevelColumnLayout
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
Rectangle {
|
||||
id: topRectangle
|
||||
id: topArea
|
||||
color: root.colorScheme.background_norm
|
||||
|
||||
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
|
||||
implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
|
||||
|
||||
clip: true
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: childrenRect.height
|
||||
|
||||
ColumnLayout {
|
||||
spacing: root._spacing
|
||||
id: topLayout
|
||||
width: _contentWidth
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: _spacing
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: root._leftMargin
|
||||
anchors.rightMargin: root._rightMargin
|
||||
anchors.topMargin: root._topMargin
|
||||
anchors.bottomMargin: root._bottomMargin
|
||||
|
||||
RowLayout { // account delegate with action buttons
|
||||
RowLayout {
|
||||
// account delegate with action buttons
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: _topMargin
|
||||
|
||||
AccountDelegate {
|
||||
Layout.fillWidth: true
|
||||
colorScheme: root.colorScheme
|
||||
user: root.user
|
||||
type: AccountDelegate.LargeView
|
||||
enabled: root.user ? (root.user.state === EUserState.Connected) : false
|
||||
enabled: _connected
|
||||
}
|
||||
|
||||
Button {
|
||||
@ -95,10 +83,11 @@ Item {
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Sign out")
|
||||
secondary: true
|
||||
visible: root.user ? (root.user.state === EUserState.Connected) : false
|
||||
visible: _connected
|
||||
onClicked: {
|
||||
if (!root.user) return
|
||||
root.user.logout()
|
||||
if (!root.user)
|
||||
return;
|
||||
root.user.logout();
|
||||
}
|
||||
}
|
||||
|
||||
@ -109,8 +98,9 @@ Item {
|
||||
secondary: true
|
||||
visible: root.user ? (root.user.state === EUserState.SignedOut) : false
|
||||
onClicked: {
|
||||
if (!root.user) return
|
||||
root.showSignIn()
|
||||
if (!root.user)
|
||||
return;
|
||||
root.showSignIn();
|
||||
}
|
||||
}
|
||||
|
||||
@ -120,8 +110,9 @@ Item {
|
||||
icon.source: "/qml/icons/ic-trash.svg"
|
||||
secondary: true
|
||||
onClicked: {
|
||||
if (!root.user) return
|
||||
root.notifications.askDeleteAccount(root.user)
|
||||
if (!root.user)
|
||||
return;
|
||||
root.notifications.askDeleteAccount(root.user);
|
||||
}
|
||||
visible: root.user ? root.user.state !== EUserState.Locked : false
|
||||
}
|
||||
@ -129,7 +120,7 @@ Item {
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: root._lineWidth
|
||||
height: root._lineThickness
|
||||
color: root.colorScheme.border_weak
|
||||
}
|
||||
|
||||
@ -139,12 +130,12 @@ Item {
|
||||
actionText: qsTr("Configure")
|
||||
description: qsTr("Using the mailbox details below (re)configure your client.")
|
||||
type: SettingsItem.Button
|
||||
enabled: root.user ? root.user.state === EUserState.Connected : false
|
||||
visible: root.user ? !root.user.splitMode || root.user.addresses.length==1 : false
|
||||
visible: _connected && (!root.user.splitMode) || (root.user.addresses.length === 1)
|
||||
showSeparator: splitMode.visible
|
||||
onClicked: {
|
||||
if (!root.user) return
|
||||
root.showSetupGuide(root.user, user.addresses[0])
|
||||
if (!root.user)
|
||||
return;
|
||||
root.showSetupGuide(root.user, user.addresses[0]);
|
||||
}
|
||||
|
||||
Layout.fillWidth: true
|
||||
@ -157,15 +148,14 @@ Item {
|
||||
description: qsTr("Setup multiple email addresses individually.")
|
||||
type: SettingsItem.Toggle
|
||||
checked: root.user ? root.user.splitMode : false
|
||||
visible: root.user ? root.user.addresses.length > 1 : false
|
||||
enabled: root.user ? (root.user.state === EUserState.Connected) : false
|
||||
visible: _connected && root.user.addresses.length > 1
|
||||
showSeparator: addressSelector.visible
|
||||
onClicked: {
|
||||
if (!splitMode.checked) {
|
||||
root.notifications.askEnableSplitMode(user)
|
||||
root.notifications.askEnableSplitMode(user);
|
||||
} else {
|
||||
addressSelector.currentIndex = 0
|
||||
root.user.toggleSplitMode(!splitMode.checked)
|
||||
addressSelector.currentIndex = 0;
|
||||
root.user.toggleSplitMode(!splitMode.checked);
|
||||
}
|
||||
}
|
||||
|
||||
@ -174,8 +164,8 @@ Item {
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
enabled: root.user ? (root.user.state === EUserState.Connected) : false
|
||||
visible: root.user ? root.user.splitMode : false
|
||||
Layout.bottomMargin: _spacing
|
||||
visible: _connected && root.user.splitMode
|
||||
|
||||
ComboBox {
|
||||
id: addressSelector
|
||||
@ -189,43 +179,49 @@ Item {
|
||||
text: qsTr("Configure")
|
||||
secondary: true
|
||||
onClicked: {
|
||||
if (!root.user) return
|
||||
root.showSetupGuide(root.user, addressSelector.displayText)
|
||||
}
|
||||
}
|
||||
if (!root.user)
|
||||
return;
|
||||
root.showSetupGuide(root.user, addressSelector.displayText);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
height: 0
|
||||
} // just for some extra space before separator
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: bottomArea
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: bottomLayout.implicitHeight
|
||||
color: root.colorScheme.background_weak
|
||||
|
||||
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
|
||||
implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
ColumnLayout {
|
||||
id: configuration
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: root._leftMargin
|
||||
anchors.rightMargin: root._rightMargin
|
||||
anchors.topMargin: root._detailsTopMargin
|
||||
anchors.bottomMargin: root._spacing
|
||||
|
||||
spacing: root._spacing
|
||||
visible: root.user ? (root.user.state === EUserState.Connected) : false
|
||||
|
||||
property string currentAddress: addressSelector.displayText
|
||||
id: bottomLayout
|
||||
width: _contentWidth
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: _spacing
|
||||
visible: _connected
|
||||
|
||||
Label {
|
||||
Layout.topMargin: _detailsMargin
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Mailbox details")
|
||||
type: Label.Body_semibold
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: configuration
|
||||
spacing: _spacing
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
property string currentAddress: addressSelector.displayText
|
||||
|
||||
Configuration {
|
||||
Layout.fillWidth: true
|
||||
colorScheme: root.colorScheme
|
||||
title: qsTr("IMAP")
|
||||
hostname: Backend.hostname
|
||||
@ -236,6 +232,7 @@ Item {
|
||||
}
|
||||
|
||||
Configuration {
|
||||
Layout.fillWidth: true
|
||||
colorScheme: root.colorScheme
|
||||
title: qsTr("SMTP")
|
||||
hostname: Backend.hostname
|
||||
@ -250,3 +247,4 @@ Item {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,11 +26,8 @@ import Notifications
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
function isInInterval(num, lower_limit, upper_limit) {
|
||||
return lower_limit <= num && num <= upper_limit
|
||||
}
|
||||
function bound(num, lower_limit, upper_limit) {
|
||||
return Math.max(lower_limit, Math.min(upper_limit, num))
|
||||
function bound(num, lowerLimit, upperLimit) {
|
||||
return Math.max(lowerLimit, Math.min(upperLimit, num))
|
||||
}
|
||||
|
||||
property var title: Backend.appname
|
||||
@ -38,10 +35,30 @@ QtObject {
|
||||
property Notifications _notifications: Notifications {
|
||||
id: notifications
|
||||
frontendMain: mainWindow
|
||||
frontendStatus: statusWindow
|
||||
frontendTray: trayIcon
|
||||
}
|
||||
|
||||
property NotificationFilter _trayNotificationFilter: NotificationFilter {
|
||||
id: trayNotificationFilter
|
||||
source: root._notifications ? root._notifications.all : undefined
|
||||
onTopmostChanged: {
|
||||
if (topmost) {
|
||||
switch (topmost.type) {
|
||||
case Notification.NotificationType.Danger:
|
||||
Backend.setErrorTrayIcon(topmost.brief, topmost.icon)
|
||||
return
|
||||
case Notification.NotificationType.Warning:
|
||||
Backend.setWarnTrayIcon(topmost.brief, topmost.icon)
|
||||
return
|
||||
case Notification.NotificationType.Info:
|
||||
Backend.setUpdateTrayIcon(topmost.brief, topmost.icon)
|
||||
return
|
||||
}
|
||||
}
|
||||
Backend.setNormalTrayIcon()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
property MainWindow _mainWindow: MainWindow {
|
||||
id: mainWindow
|
||||
visible: false
|
||||
@ -66,190 +83,6 @@ QtObject {
|
||||
}
|
||||
}
|
||||
|
||||
property StatusWindow _statusWindow: StatusWindow {
|
||||
id: statusWindow
|
||||
visible: false
|
||||
|
||||
title: root.title
|
||||
notifications: root._notifications
|
||||
|
||||
onShowMainWindow: {
|
||||
mainWindow.showAndRise()
|
||||
}
|
||||
|
||||
onShowHelp: {
|
||||
mainWindow.showHelp()
|
||||
mainWindow.showAndRise()
|
||||
}
|
||||
|
||||
onShowSettings: {
|
||||
mainWindow.showSettings()
|
||||
mainWindow.showAndRise()
|
||||
}
|
||||
|
||||
onSelectUser: function(userID) {
|
||||
mainWindow.selectUser(userID)
|
||||
mainWindow.showAndRise()
|
||||
}
|
||||
|
||||
onQuit: {
|
||||
mainWindow.hide()
|
||||
trayIcon.visible = false
|
||||
Backend.quit()
|
||||
}
|
||||
|
||||
property rect screenRect
|
||||
property rect iconRect
|
||||
|
||||
// use binding from function with width and height as arguments so it will be recalculated every time width and height are changed
|
||||
property point position: getPosition(width, height)
|
||||
x: position.x
|
||||
y: position.y
|
||||
|
||||
function getPosition(_width, _height) {
|
||||
if (screenRect.width === 0 || screenRect.height === 0) {
|
||||
return Qt.point(0, 0)
|
||||
}
|
||||
|
||||
var _x = 0
|
||||
var _y = 0
|
||||
|
||||
// fit above
|
||||
_y = iconRect.top - height
|
||||
if (isInInterval(_y, screenRect.top, screenRect.bottom - height)) {
|
||||
// position preferably in the horizontal center but bound to the screen rect
|
||||
_x = bound(iconRect.left + (iconRect.width - width)/2, screenRect.left, screenRect.right - width)
|
||||
return Qt.point(_x, _y)
|
||||
}
|
||||
|
||||
// fit below
|
||||
_y = iconRect.bottom
|
||||
if (isInInterval(_y, screenRect.top, screenRect.bottom - height)) {
|
||||
// position preferably in the horizontal center but bound to the screen rect
|
||||
_x = bound(iconRect.left + (iconRect.width - width)/2, screenRect.left, screenRect.right - width)
|
||||
return Qt.point(_x, _y)
|
||||
}
|
||||
|
||||
// fit to the left
|
||||
_x = iconRect.left - width
|
||||
if (isInInterval(_x, screenRect.left, screenRect.right - width)) {
|
||||
// position preferably in the vertical center but bound to the screen rect
|
||||
_y = bound(iconRect.top + (iconRect.height - height)/2, screenRect.top, screenRect.bottom - height)
|
||||
return Qt.point(_x, _y)
|
||||
}
|
||||
|
||||
// fit to the right
|
||||
_x = iconRect.right
|
||||
if (isInInterval(_x, screenRect.left, screenRect.right - width)) {
|
||||
// position preferably in the vertical center but bound to the screen rect
|
||||
_y = bound(iconRect.top + (iconRect.height - height)/2, screenRect.top, screenRect.bottom - height)
|
||||
return Qt.point(_x, _y)
|
||||
}
|
||||
|
||||
// Fallback: position status window right above icon and let window manager decide.
|
||||
console.warn("Can't position status window: screenRect =", screenRect, "iconRect =", iconRect)
|
||||
_x = bound(iconRect.left + (iconRect.width - width)/2, screenRect.left, screenRect.right - width)
|
||||
_y = bound(iconRect.top + (iconRect.height - height)/2, screenRect.top, screenRect.bottom - height)
|
||||
return Qt.point(_x, _y)
|
||||
}
|
||||
}
|
||||
|
||||
property SystemTrayIcon _trayIcon: SystemTrayIcon {
|
||||
id: trayIcon
|
||||
visible: true
|
||||
icon.source: getTrayIconPath()
|
||||
icon.mask: true // make sure that systems like macOS will use proper color
|
||||
tooltip: `${root.title} v${Backend.version}`
|
||||
onActivated: function(reason) {
|
||||
function calcStatusWindowPosition() {
|
||||
// On some platforms (X11 / Plasma) Qt does not provide icon position and geometry info.
|
||||
// In this case we rely on cursor position
|
||||
var iconRect = Qt.rect(geometry.x, geometry.y, geometry.width, geometry.height)
|
||||
if (geometry.width == 0 && geometry.height == 0) {
|
||||
var mousePos = Backend.getCursorPos()
|
||||
iconRect.x = mousePos.x
|
||||
iconRect.y = mousePos.y
|
||||
iconRect.width = 0
|
||||
iconRect.height = 0
|
||||
}
|
||||
|
||||
// Find screen
|
||||
var screen
|
||||
for (var i in Qt.application.screens) {
|
||||
var _screen = Qt.application.screens[i]
|
||||
if (
|
||||
isInInterval(iconRect.x, _screen.virtualX, _screen.virtualX + _screen.width) &&
|
||||
isInInterval(iconRect.y, _screen.virtualY, _screen.virtualY + _screen.height)
|
||||
) {
|
||||
screen = _screen
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!screen) {
|
||||
// Fallback to primary screen
|
||||
screen = Qt.application.screens[0]
|
||||
}
|
||||
|
||||
// In case we used mouse to detect icon position - we want to make a fake icon rectangle from a point
|
||||
if (iconRect.width == 0 && iconRect.height == 0) {
|
||||
iconRect.x = bound(iconRect.x - 16, screen.virtualX, screen.virtualX + screen.width - 32)
|
||||
iconRect.y = bound(iconRect.y - 16, screen.virtualY, screen.virtualY + screen.height - 32)
|
||||
iconRect.width = 32
|
||||
iconRect.height = 32
|
||||
}
|
||||
|
||||
statusWindow.screenRect = Qt.rect(screen.virtualX, screen.virtualY, screen.width, screen.height)
|
||||
statusWindow.iconRect = iconRect
|
||||
}
|
||||
|
||||
function toggleWindow(win) {
|
||||
if (win.visible) {
|
||||
win.close()
|
||||
} else {
|
||||
win.showAndRise()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
switch (reason) {
|
||||
case SystemTrayIcon.Unknown:
|
||||
break;
|
||||
case SystemTrayIcon.Context:
|
||||
case SystemTrayIcon.Trigger:
|
||||
case SystemTrayIcon.DoubleClick:
|
||||
case SystemTrayIcon.MiddleClick:
|
||||
calcStatusWindowPosition()
|
||||
toggleWindow(statusWindow)
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
property NotificationFilter _systrayfilter: NotificationFilter {
|
||||
source: root._notifications ? root._notifications.all : undefined
|
||||
}
|
||||
|
||||
function getTrayIconPath() {
|
||||
var color = Backend.goos == "darwin" ? "mono" : "color"
|
||||
|
||||
var level = "norm"
|
||||
if (_systrayfilter.topmost) {
|
||||
switch (_systrayfilter.topmost.type) {
|
||||
case Notification.NotificationType.Danger:
|
||||
level = "error"
|
||||
break;
|
||||
case Notification.NotificationType.Warning:
|
||||
level = "warn"
|
||||
break;
|
||||
case Notification.NotificationType.Info:
|
||||
level = "update"
|
||||
break;
|
||||
}
|
||||
}
|
||||
return `qrc:/qml/icons/systray-${color}-${level}.png`
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (!Backend) {
|
||||
@ -266,7 +99,7 @@ QtObject {
|
||||
var c = Backend.users.count
|
||||
var u = Backend.users.get(0)
|
||||
// DEBUG
|
||||
if (c != 0) {
|
||||
if (c !== 0) {
|
||||
console.log("users non zero", c)
|
||||
console.log("first user", u )
|
||||
}
|
||||
@ -290,7 +123,7 @@ QtObject {
|
||||
}
|
||||
|
||||
function setColorScheme() {
|
||||
if (Backend.colorSchemeName == "light") ProtonStyle.currentStyle = ProtonStyle.lightStyle
|
||||
if (Backend.colorSchemeName == "dark") ProtonStyle.currentStyle = ProtonStyle.darkStyle
|
||||
if (Backend.colorSchemeName === "light") ProtonStyle.currentStyle = ProtonStyle.lightStyle
|
||||
if (Backend.colorSchemeName === "dark") ProtonStyle.currentStyle = ProtonStyle.darkStyle
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,315 +0,0 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import QtQml
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
|
||||
import Proton
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
property var user
|
||||
property var userIndex
|
||||
|
||||
spacing : 5
|
||||
|
||||
Layout.fillHeight: true
|
||||
//Layout.fillWidth: true
|
||||
|
||||
property ColorScheme colorScheme
|
||||
|
||||
TextField {
|
||||
colorScheme: root.colorScheme
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: user !== undefined ? user.username : ""
|
||||
|
||||
onEditingFinished: {
|
||||
user.username = text
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Switch {
|
||||
id: userLoginSwitch
|
||||
colorScheme: root.colorScheme
|
||||
|
||||
text: "LoggedIn"
|
||||
enabled: user !== undefined && user.username.length > 0
|
||||
|
||||
checked: user ? root.user.state == EUserState.Connected : false
|
||||
|
||||
onCheckedChanged: {
|
||||
if (!user) {
|
||||
return
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
if (user === Backend.loginUser) {
|
||||
var newUserObject = Backend.userComponent.createObject(Backend, {username: user.username, loggedIn: true, setupGuideSeen: user.setupGuideSeen})
|
||||
Backend.users.append( { object: newUserObject } )
|
||||
|
||||
user.username = ""
|
||||
user.resetLoginRequests()
|
||||
return
|
||||
}
|
||||
|
||||
user.state = EUserState.Connected
|
||||
user.resetLoginRequests()
|
||||
return
|
||||
} else {
|
||||
user.state = EUserState.SignedOut
|
||||
user.resetLoginRequests()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Switch {
|
||||
colorScheme: root.colorScheme
|
||||
|
||||
text: "Setup guide seen"
|
||||
enabled: user !== undefined && user.username.length > 0
|
||||
|
||||
checked: user ? user.setupGuideSeen : false
|
||||
|
||||
onCheckedChanged: {
|
||||
if (!user) {
|
||||
return
|
||||
}
|
||||
|
||||
user.setupGuideSeen = checked
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Label {
|
||||
colorScheme: root.colorScheme
|
||||
id: loginLabel
|
||||
text: "Login:"
|
||||
|
||||
Layout.preferredWidth: Math.max(loginLabel.implicitWidth, faLabel.implicitWidth, passLabel.implicitWidth)
|
||||
}
|
||||
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
text: "name/pass error"
|
||||
enabled: user !== undefined //&& user.isLoginRequested && !user.isLogin2FARequested && !user.isLogin2PasswordProvided
|
||||
|
||||
onClicked: {
|
||||
Backend.loginUsernamePasswordError("")
|
||||
user.resetLoginRequests()
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
text: "free user error"
|
||||
enabled: user !== undefined //&& user.isLoginRequested
|
||||
onClicked: {
|
||||
Backend.loginFreeUserError()
|
||||
user.resetLoginRequests()
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
text: "connection error"
|
||||
enabled: user !== undefined //&& user.isLoginRequested
|
||||
onClicked: {
|
||||
Backend.loginConnectionError("")
|
||||
user.resetLoginRequests()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Label {
|
||||
colorScheme: root.colorScheme
|
||||
id: faLabel
|
||||
text: "2FA:"
|
||||
|
||||
Layout.preferredWidth: Math.max(loginLabel.implicitWidth, faLabel.implicitWidth, passLabel.implicitWidth)
|
||||
}
|
||||
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
text: "request"
|
||||
|
||||
enabled: user !== undefined //&& user.isLoginRequested && !user.isLogin2FARequested && !user.isLogin2PasswordRequested
|
||||
onClicked: {
|
||||
Backend.login2FARequested(user.username)
|
||||
user.isLogin2FARequested = true
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
text: "error"
|
||||
|
||||
enabled: user !== undefined //&& user.isLogin2FAProvided && !(user.isLogin2PasswordRequested && !user.isLogin2PasswordProvided)
|
||||
onClicked: {
|
||||
Backend.login2FAError("")
|
||||
user.isLogin2FAProvided = false
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
text: "Abort"
|
||||
|
||||
enabled: user !== undefined //&& user.isLogin2FAProvided && !(user.isLogin2PasswordRequested && !user.isLogin2PasswordProvided)
|
||||
onClicked: {
|
||||
Backend.login2FAErrorAbort("")
|
||||
user.resetLoginRequests()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Label {
|
||||
colorScheme: root.colorScheme
|
||||
id: passLabel
|
||||
text: "2 Password:"
|
||||
|
||||
Layout.preferredWidth: Math.max(loginLabel.implicitWidth, faLabel.implicitWidth, passLabel.implicitWidth)
|
||||
}
|
||||
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
text: "request"
|
||||
|
||||
enabled: user !== undefined //&& user.isLoginRequested && !user.isLogin2PasswordRequested && !(user.isLogin2FARequested && !user.isLogin2FAProvided)
|
||||
onClicked: {
|
||||
Backend.login2PasswordRequested("")
|
||||
user.isLogin2PasswordRequested = true
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
text: "error"
|
||||
|
||||
enabled: user !== undefined //&& user.isLogin2PasswordProvided && !(user.isLogin2FARequested && !user.isLogin2FAProvided)
|
||||
onClicked: {
|
||||
Backend.login2PasswordError("")
|
||||
|
||||
user.isLogin2PasswordProvided = false
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
text: "Abort"
|
||||
|
||||
enabled: user !== undefined //&& user.isLogin2PasswordProvided && !(user.isLogin2FARequested && !user.isLogin2FAProvided)
|
||||
onClicked: {
|
||||
Backend.login2PasswordErrorAbort("")
|
||||
user.resetLoginRequests()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
text: "Login Finished"
|
||||
|
||||
onClicked: {
|
||||
Backend.loginFinished(0+loginFinishedIndex.text)
|
||||
user.resetLoginRequests()
|
||||
}
|
||||
}
|
||||
TextField {
|
||||
id: loginFinishedIndex
|
||||
colorScheme: root.colorScheme
|
||||
label: "Index:"
|
||||
text: root.userIndex
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
text: "Already logged in"
|
||||
|
||||
onClicked: {
|
||||
Backend.loginAlreadyLoggedIn(0+loginAlreadyLoggedInIndex.text)
|
||||
user.resetLoginRequests()
|
||||
}
|
||||
}
|
||||
TextField {
|
||||
id: loginAlreadyLoggedInIndex
|
||||
colorScheme: root.colorScheme
|
||||
label: "Index:"
|
||||
text: root.userIndex
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
TextField {
|
||||
colorScheme: root.colorScheme
|
||||
label: "used:"
|
||||
text: user && user.usedBytes ? user.usedBytes : 0
|
||||
onEditingFinished: {
|
||||
user.usedBytes = parseFloat(text)
|
||||
}
|
||||
implicitWidth: 200
|
||||
}
|
||||
TextField {
|
||||
colorScheme: root.colorScheme
|
||||
label: "total:"
|
||||
text: user && user.totalBytes ? user.totalBytes : 0
|
||||
onEditingFinished: {
|
||||
user.totalBytes = parseFloat(text)
|
||||
}
|
||||
implicitWidth: 200
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Label {colorScheme: root.colorScheme; text: "Split mode"}
|
||||
Toggle { colorScheme: root.colorScheme; checked: user ? user.splitMode : false; onClicked: {user.splitMode = !user.splitMode}}
|
||||
Button { colorScheme: root.colorScheme; text: "Toggle Finished"; onClicked: {user.toggleSplitModeFinished()}}
|
||||
}
|
||||
|
||||
TextArea { // TODO: this is causing binding loop on implicitWidth
|
||||
colorScheme: root.colorScheme
|
||||
text: user && user.addresses ? user.addresses.join("\n") : "user@protonmail.com"
|
||||
Layout.fillWidth: true
|
||||
|
||||
onEditingFinished: {
|
||||
user.addresses = text.split("\n")
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
@ -1,101 +0,0 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
|
||||
import Proton
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
property ColorScheme colorScheme
|
||||
|
||||
property alias currentIndex: usersListView.currentIndex
|
||||
ListView {
|
||||
id: usersListView
|
||||
Layout.fillHeight: true
|
||||
Layout.preferredWidth: 200
|
||||
|
||||
model: Backend.usersTest
|
||||
highlightFollowsCurrentItem: true
|
||||
|
||||
delegate: Item {
|
||||
|
||||
implicitHeight: children[0].implicitHeight + anchors.topMargin + anchors.bottomMargin
|
||||
implicitWidth: children[0].implicitWidth + anchors.leftMargin + anchors.rightMargin
|
||||
|
||||
width: usersListView.width
|
||||
|
||||
anchors.margins: 10
|
||||
|
||||
Label {
|
||||
colorScheme: root.colorScheme
|
||||
text: modelData.username
|
||||
anchors.margins: 10
|
||||
anchors.fill: parent
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
usersListView.currentIndex = index
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
highlight: Rectangle {
|
||||
color: root.colorScheme.interaction_default_active
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
|
||||
text: "+"
|
||||
|
||||
onClicked: {
|
||||
var newUserObject = Backend.userComponent.createObject(Backend)
|
||||
newUserObject.username = Backend.loginUser.username.length > 0 ? Backend.loginUser.username : "test@protonmail.com"
|
||||
newUserObject.state = EUserState.Connected
|
||||
newUserObject.setupGuideSeen = true // Backend.loginUser.setupGuideSeen
|
||||
|
||||
Backend.loginUser.username = ""
|
||||
Backend.loginUser.state = EUserState.SignedOut
|
||||
Backend.loginUser.setupGuideSeen = false
|
||||
|
||||
Backend.users.append( { object: newUserObject } )
|
||||
}
|
||||
}
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
text: "-"
|
||||
|
||||
enabled: usersListView.currentIndex != 0
|
||||
|
||||
onClicked: {
|
||||
// var userObject = Backend.users.get(usersListView.currentIndex - 1)
|
||||
Backend.users.remove(usersListView.currentIndex - 1)
|
||||
// userObject.deleteLater()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,982 +0,0 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import QtQml
|
||||
import QtQuick
|
||||
import QtQuick.Window
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
|
||||
import QtQml.Models
|
||||
|
||||
import Qt.labs.platform
|
||||
|
||||
import Proton
|
||||
|
||||
import "./BridgeTest"
|
||||
import BridgePreview
|
||||
|
||||
import Notifications
|
||||
|
||||
Window {
|
||||
id: root
|
||||
|
||||
x: 10
|
||||
y: 10
|
||||
width: 800
|
||||
height: 800
|
||||
|
||||
property ColorScheme colorScheme: ProtonStyle.darkStyle
|
||||
|
||||
flags : Qt.Window | Qt.Dialog
|
||||
visible : true
|
||||
title : "Bridge Test GUI"
|
||||
|
||||
// This is needed because on MacOS if first window shown is not transparent -
|
||||
// all other windows of application will not have transparent background (black
|
||||
// instead of transparency). In our case that mean that if BridgeTest will be
|
||||
// shown before StatusWindow - StatusWindow will not have transparent corners.
|
||||
color: "transparent"
|
||||
|
||||
function getCursorPos() {
|
||||
return BridgePreview.getCursorPos()
|
||||
}
|
||||
|
||||
function restart() {
|
||||
root.quit()
|
||||
console.log("Restarting....")
|
||||
root.openBridge()
|
||||
}
|
||||
|
||||
function openBridge() {
|
||||
bridge = bridgeComponent.createObject()
|
||||
var showSetupGuide = false
|
||||
if (showSetupGuide) {
|
||||
var newUserObject = root.userComponent.createObject(root)
|
||||
newUserObject.username = "LerooooyJenkins@protonmail.com"
|
||||
newUserObject.state = EUserState.Connected
|
||||
newUserObject.setupGuideSeen = false
|
||||
root.users.append( { object: newUserObject } )
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function quit() {
|
||||
if (bridge !== undefined && bridge !== null) {
|
||||
bridge.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
function guiReady() {
|
||||
console.log("Gui Ready")
|
||||
}
|
||||
|
||||
function _log(msg, color) {
|
||||
logTextArea.text += "<p style='color: " + color + ";'>" + msg + "</p>"
|
||||
logTextArea.text += "\n"
|
||||
}
|
||||
|
||||
function log(msg) {
|
||||
console.log(msg)
|
||||
_log(msg, root.colorScheme.signal_info)
|
||||
}
|
||||
|
||||
function error(msg) {
|
||||
console.error(msg)
|
||||
_log(msg, root.colorScheme.signal_danger)
|
||||
}
|
||||
|
||||
// No user object should be put in this list until a successful login
|
||||
property var users: UserModel {
|
||||
id: _users
|
||||
|
||||
onRowsInserted: {
|
||||
for (var i = first; i <= last; i++) {
|
||||
_usersTest.insert(i + 1, { object: get(i) } )
|
||||
}
|
||||
}
|
||||
|
||||
onRowsRemoved: {
|
||||
_usersTest.remove(first + 1, first - last + 1)
|
||||
}
|
||||
|
||||
onRowsMoved: {
|
||||
_usersTest.move(start + 1, row + 1, end - start + 1)
|
||||
}
|
||||
|
||||
onDataChanged: {
|
||||
for (var i = topLeft.row; i <= bottomRight.row; i++) {
|
||||
_usersTest.set(i + 1, { object: get(i) } )
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// this list is used on test gui: it contains same users list as users above + fake user to represent login request of new user on pos 0
|
||||
property var usersTest: UserModel {
|
||||
id: _usersTest
|
||||
}
|
||||
|
||||
property var userComponent: Component {
|
||||
id: _userComponent
|
||||
|
||||
QtObject {
|
||||
property string username: ""
|
||||
property bool loggedIn: false
|
||||
property bool splitMode: false
|
||||
|
||||
property bool setupGuideSeen: true
|
||||
|
||||
property var usedBytes: 5350*1024*1024
|
||||
property var totalBytes: 20*1024*1024*1024
|
||||
property string avatarText: "jd"
|
||||
|
||||
property string password: "SMj975NnEYYsqu55GGmlpv"
|
||||
property var addresses: [
|
||||
"jaanedoe@protonmail.com",
|
||||
"jane@pm.me",
|
||||
"jdoe@pm.me"
|
||||
]
|
||||
|
||||
signal loginUsernamePasswordError()
|
||||
signal loginFreeUserError()
|
||||
signal loginConnectionError()
|
||||
signal login2FARequested()
|
||||
signal login2FAError()
|
||||
signal login2FAErrorAbort()
|
||||
signal login2PasswordRequested()
|
||||
signal login2PasswordError()
|
||||
signal login2PasswordErrorAbort()
|
||||
|
||||
// Test purpose only:
|
||||
property bool isFakeUser: this === root.loginUser
|
||||
|
||||
function userSignal(msg) {
|
||||
if (isFakeUser) {
|
||||
return
|
||||
}
|
||||
|
||||
root.log("<- User (" + username + "): " + msg)
|
||||
}
|
||||
|
||||
function toggleSplitMode(makeActive) {
|
||||
userSignal("toggle split mode "+makeActive)
|
||||
}
|
||||
signal toggleSplitModeFinished()
|
||||
|
||||
function configureAppleMail(address){
|
||||
userSignal("configure apple mail "+address)
|
||||
}
|
||||
|
||||
function logout(){
|
||||
userSignal("logout")
|
||||
loggedIn = false
|
||||
}
|
||||
function remove(){
|
||||
console.log("remove this", users.count)
|
||||
for (var i=0; i<users.count; i++) {
|
||||
if (users.get(i) === this) {
|
||||
users.remove(i,1)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
onLoginUsernamePasswordError: {
|
||||
userSignal("loginUsernamePasswordError")
|
||||
}
|
||||
onLoginFreeUserError: {
|
||||
userSignal("loginFreeUserError")
|
||||
}
|
||||
onLoginConnectionError: {
|
||||
userSignal("loginConnectionError")
|
||||
}
|
||||
onLogin2FARequested: {
|
||||
userSignal("login2FARequested")
|
||||
}
|
||||
onLogin2FAError: {
|
||||
userSignal("login2FAError")
|
||||
}
|
||||
onLogin2FAErrorAbort: {
|
||||
userSignal("login2FAErrorAbort")
|
||||
}
|
||||
onLogin2PasswordRequested: {
|
||||
userSignal("login2PasswordRequested")
|
||||
}
|
||||
onLogin2PasswordError: {
|
||||
userSignal("login2PasswordError")
|
||||
}
|
||||
onLogin2PasswordErrorAbort: {
|
||||
userSignal("login2PasswordErrorAbort")
|
||||
}
|
||||
|
||||
function resetLoginRequests() {
|
||||
isLoginRequested = false
|
||||
isLogin2FARequested = false
|
||||
isLogin2FAProvided = false
|
||||
isLogin2PasswordRequested = false
|
||||
isLogin2PasswordProvided = false
|
||||
}
|
||||
|
||||
property bool isLoginRequested: false
|
||||
|
||||
property bool isLogin2FARequested: false
|
||||
property bool isLogin2FAProvided: false
|
||||
|
||||
property bool isLogin2PasswordRequested: false
|
||||
property bool isLogin2PasswordProvided: false
|
||||
}
|
||||
}
|
||||
|
||||
// this it fake user used only for representing first login request
|
||||
property var loginUser
|
||||
Component.onCompleted: {
|
||||
var newLoginUser = _userComponent.createObject()
|
||||
root.loginUser = newLoginUser
|
||||
root.loginUser.setupGuideSeen = false
|
||||
_usersTest.append({object: newLoginUser})
|
||||
|
||||
newLoginUser.loginUsernamePasswordError.connect(root.loginUsernamePasswordError)
|
||||
newLoginUser.loginFreeUserError.connect(root.loginFreeUserError)
|
||||
newLoginUser.loginConnectionError.connect(root.loginConnectionError)
|
||||
newLoginUser.login2FARequested.connect(root.login2FARequested)
|
||||
newLoginUser.login2FAError.connect(root.login2FAError)
|
||||
newLoginUser.login2FAErrorAbort.connect(root.login2FAErrorAbort)
|
||||
newLoginUser.login2PasswordRequested.connect(root.login2PasswordRequested)
|
||||
newLoginUser.login2PasswordError.connect(root.login2PasswordError)
|
||||
newLoginUser.login2PasswordErrorAbort.connect(root.login2PasswordErrorAbort)
|
||||
|
||||
|
||||
// add one user on start
|
||||
var hasUserOnStart = false
|
||||
if (hasUserOnStart) {
|
||||
var newUserObject = root.userComponent.createObject(root)
|
||||
newUserObject.username = "LerooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooyJenkins@protonmail.com"
|
||||
newUserObject.loggedIn = EUserState.Connected
|
||||
newUserObject.setupGuideSeen = true
|
||||
root.users.append( { object: newUserObject } )
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
TabBar {
|
||||
id: tabBar
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
TabButton {
|
||||
text: "Global settings"
|
||||
}
|
||||
|
||||
TabButton {
|
||||
text: "User control"
|
||||
}
|
||||
|
||||
TabButton {
|
||||
text: "Notifications"
|
||||
}
|
||||
|
||||
TabButton {
|
||||
text: "Log"
|
||||
}
|
||||
|
||||
TabButton {
|
||||
text: "Settings signals"
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
color: root.colorScheme.background_norm
|
||||
|
||||
anchors.top: tabBar.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
implicitHeight: children[0].contentHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
|
||||
implicitWidth: children[0].contentWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
|
||||
|
||||
StackLayout {
|
||||
anchors.fill: parent
|
||||
currentIndex: tabBar.currentIndex
|
||||
anchors.margins: 10
|
||||
|
||||
RowLayout {
|
||||
id: globalTab
|
||||
spacing : 5
|
||||
|
||||
ColumnLayout {
|
||||
spacing : 5
|
||||
|
||||
Label {
|
||||
colorScheme: root.colorScheme
|
||||
text: "Global settings"
|
||||
}
|
||||
|
||||
ButtonGroup {
|
||||
id: styleRadioGroup
|
||||
}
|
||||
|
||||
RadioButton {
|
||||
colorScheme: root.colorScheme
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: "Light UI"
|
||||
checked: ProtonStyle.currentStyle === ProtonStyle.lightStyle
|
||||
ButtonGroup.group: styleRadioGroup
|
||||
|
||||
onCheckedChanged: {
|
||||
if (checked && ProtonStyle.currentStyle !== ProtonStyle.lightStyle) {
|
||||
root.colorSchemeName = "light"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RadioButton {
|
||||
colorScheme: root.colorScheme
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: "Dark UI"
|
||||
checked: ProtonStyle.currentStyle === ProtonStyle.darkStyle
|
||||
ButtonGroup.group: styleRadioGroup
|
||||
|
||||
onCheckedChanged: {
|
||||
if (checked && ProtonStyle.currentStyle !== ProtonStyle.darkStyle) {
|
||||
root.colorSchemeName = "dark"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
id: showOnStartupCheckbox
|
||||
colorScheme: root.colorScheme
|
||||
text: "Show on startup"
|
||||
checked: root.showOnStartup
|
||||
onCheckedChanged: {
|
||||
root.showOnStartup = checked
|
||||
}
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
id: showSplashScreen
|
||||
colorScheme: root.colorScheme
|
||||
text: "Show splash screen"
|
||||
checked: root.showSplashScreen
|
||||
onCheckedChanged: {
|
||||
root.showSplashScreen = checked
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
//Layout.fillWidth: true
|
||||
|
||||
text: "Open Bridge"
|
||||
enabled: bridge === undefined || bridge === null
|
||||
onClicked: root.openBridge()
|
||||
}
|
||||
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
//Layout.fillWidth: true
|
||||
|
||||
text: "Close Bridge"
|
||||
enabled: bridge !== undefined && bridge !== null
|
||||
onClicked: {
|
||||
bridge.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing : 5
|
||||
|
||||
Label {
|
||||
colorScheme: root.colorScheme
|
||||
text: "Notifications"
|
||||
}
|
||||
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
text: "Notify: danger"
|
||||
enabled: bridge !== undefined && bridge !== null
|
||||
onClicked: {
|
||||
bridge.mainWindow.notifyOnlyPaidUsers()
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
text: "Notify: warning"
|
||||
enabled: bridge !== undefined && bridge !== null
|
||||
onClicked: {
|
||||
bridge.mainWindow.notifyUpdateManually()
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
text: "Notify: success"
|
||||
enabled: bridge !== undefined && bridge !== null
|
||||
onClicked: {
|
||||
bridge.mainWindow.notifyUserAdded()
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: usersTab
|
||||
UserList {
|
||||
id: usersListView
|
||||
Layout.fillHeight: true
|
||||
colorScheme: root.colorScheme
|
||||
backend: root
|
||||
}
|
||||
|
||||
UserControl {
|
||||
colorScheme: root.colorScheme
|
||||
backend: root
|
||||
user: ((root.usersTest.count > usersListView.currentIndex) && usersListView.currentIndex != -1) ? root.usersTest.get(usersListView.currentIndex) : undefined
|
||||
userIndex: usersListView.currentIndex - 1 // -1 because 0 index is fake user
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: notificationsTab
|
||||
spacing: 5
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 5
|
||||
|
||||
Switch {
|
||||
text: "Internet connection"
|
||||
colorScheme: root.colorScheme
|
||||
checked: true
|
||||
onCheckedChanged: {
|
||||
checked ? root.internetOn() : root.internetOff()
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Update manual ready"
|
||||
colorScheme: root.colorScheme
|
||||
onClicked: {
|
||||
root.updateManualReady("3.14.1592")
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Update manual done"
|
||||
colorScheme: root.colorScheme
|
||||
onClicked: {
|
||||
root.updateManualRestartNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Update manual error"
|
||||
colorScheme: root.colorScheme
|
||||
onClicked: {
|
||||
root.updateManualError()
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Update force"
|
||||
colorScheme: root.colorScheme
|
||||
onClicked: {
|
||||
root.updateForce("3.14.1592")
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Update force error"
|
||||
colorScheme: root.colorScheme
|
||||
onClicked: {
|
||||
root.updateForceError()
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Update silent done"
|
||||
colorScheme: root.colorScheme
|
||||
onClicked: {
|
||||
root.updateSilentRestartNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Update silent error"
|
||||
colorScheme: root.colorScheme
|
||||
onClicked: {
|
||||
root.updateSilentError()
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Update is latest version"
|
||||
colorScheme: root.colorScheme
|
||||
onClicked: {
|
||||
root.updateIsLatestVersion()
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Bug report send OK"
|
||||
colorScheme: root.colorScheme
|
||||
onClicked: {
|
||||
root.reportBugFinished()
|
||||
root.bugReportSendSuccess()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 5
|
||||
|
||||
Button {
|
||||
text: "Bug report send error"
|
||||
colorScheme: root.colorScheme
|
||||
onClicked: {
|
||||
root.reportBugFinished()
|
||||
root.bugReportSendError()
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Cache anavailable"
|
||||
colorScheme: root.colorScheme
|
||||
onClicked: {
|
||||
root.cacheUnavailable()
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Cache can't move"
|
||||
colorScheme: root.colorScheme
|
||||
onClicked: {
|
||||
root.cacheCantMove()
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Cache location change success"
|
||||
onClicked: {
|
||||
root.cacheLocationChangeSuccess()
|
||||
}
|
||||
colorScheme: root.colorScheme
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Disk full"
|
||||
colorScheme: root.colorScheme
|
||||
onClicked: {
|
||||
root.diskFull()
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "No keychain"
|
||||
colorScheme: root.colorScheme
|
||||
onClicked: {
|
||||
root.notifyHasNoKeychain()
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Rebuild keychain"
|
||||
colorScheme: root.colorScheme
|
||||
onClicked: {
|
||||
root.notifyRebuildKeychain()
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Address changed"
|
||||
colorScheme: root.colorScheme
|
||||
onClicked: {
|
||||
root.addressChanged("p@v.el")
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Address changed + Logout"
|
||||
colorScheme: root.colorScheme
|
||||
onClicked: {
|
||||
root.addressChangedLogout("p@v.el")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextArea {
|
||||
id: logTextArea
|
||||
colorScheme: root.colorScheme
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
|
||||
Layout.preferredWidth: 400
|
||||
Layout.preferredHeight: 200
|
||||
|
||||
textFormat: TextEdit.RichText
|
||||
//readOnly: true
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
id: settingsTab
|
||||
ColumnLayout {
|
||||
RowLayout {
|
||||
Label {colorScheme : root.colorScheme ; text : "GOOS : "}
|
||||
Button {colorScheme : root.colorScheme ; text : "Linux" ; onClicked : root.goos = "linux" ; enabled: root.goos != "linux"}
|
||||
Button {colorScheme : root.colorScheme ; text : "Windows" ; onClicked : root.goos = "windows" ; enabled: root.goos != "windows"}
|
||||
Button {colorScheme : root.colorScheme ; text : "macOS" ; onClicked : root.goos = "darwin" ; enabled: root.goos != "darwin"}
|
||||
}
|
||||
RowLayout {
|
||||
Label {colorScheme: root.colorScheme; text: "Automatic updates:"}
|
||||
Toggle {colorScheme: root.colorScheme; checked: root.isAutomaticUpdateOn; onClicked: root.isAutomaticUpdateOn = !root.isAutomaticUpdateOn}
|
||||
}
|
||||
RowLayout {
|
||||
Label {colorScheme: root.colorScheme; text: "Autostart:"}
|
||||
Toggle {colorScheme: root.colorScheme; checked: root.isAutostartOn; onClicked: root.isAutostartOn = !root.isAutostartOn}
|
||||
Button {colorScheme: root.colorScheme; text: "Toggle finished"; onClicked: root.toggleAutostartFinished()}
|
||||
}
|
||||
RowLayout {
|
||||
Label {colorScheme: root.colorScheme; text: "Beta:"}
|
||||
Toggle {colorScheme: root.colorScheme; checked: root.isBetaEnabled; onClicked: root.isBetaEnabled = !root.isBetaEnabled}
|
||||
}
|
||||
RowLayout {
|
||||
Label {colorScheme: root.colorScheme; text: "DoH:"}
|
||||
Toggle {colorScheme: root.colorScheme; checked: root.isDoHEnabled; onClicked: root.isDoHEnabled = !root.isDoHEnabled}
|
||||
}
|
||||
RowLayout {
|
||||
Label {colorScheme: root.colorScheme; text: "All Mail disabled:"}
|
||||
Toggle {colorScheme: root.colorScheme; checked: root.isAllMailVisible; onClicked: root.isAllMailVisible = !root.isAllMailVisible}
|
||||
}
|
||||
RowLayout {
|
||||
Label {colorScheme: root.colorScheme; text: "Ports:"}
|
||||
TextField {
|
||||
colorScheme:root.colorScheme
|
||||
label: "IMAP"
|
||||
text: root.portIMAP
|
||||
onEditingFinished: root.portIMAP = this.text*1
|
||||
validator: IntValidator {bottom: 1; top: 65536}
|
||||
}
|
||||
TextField {
|
||||
colorScheme:root.colorScheme
|
||||
label: "SMTP"
|
||||
text: root.portSMTP
|
||||
onEditingFinished: root.portSMTP = this.text*1
|
||||
validator: IntValidator {bottom: 1; top: 65536}
|
||||
}
|
||||
Button {colorScheme: root.colorScheme; text: "Change finished"; onClicked: root.changePortFinished()}
|
||||
}
|
||||
RowLayout {
|
||||
Label {colorScheme: root.colorScheme; text: "SMTP using SSL:"}
|
||||
Toggle {colorScheme: root.colorScheme; checked: root.useSSLForSMTP; onClicked: root.useSSLForSMTP = !root.useSSLForSMTP}
|
||||
}
|
||||
RowLayout {
|
||||
Label {colorScheme: root.colorScheme; text: "Local cache:"}
|
||||
Toggle {colorScheme: root.colorScheme; checked: root.isDiskCacheEnabled; onClicked: root.isDiskCacheEnabled = !root.isDiskCacheEnabled}
|
||||
TextField {
|
||||
colorScheme:root.colorScheme
|
||||
label: "Path"
|
||||
text: root.diskCachePath.toString().replace("file://", "")
|
||||
implicitWidth: 160
|
||||
onEditingFinished: {
|
||||
root.diskCachePath = Qt.resolvedUrl("file://"+text)
|
||||
}
|
||||
}
|
||||
Button {colorScheme: root.colorScheme; text: "Change finished"; onClicked: root.changeLocalCacheFinished()}
|
||||
}
|
||||
RowLayout {
|
||||
Label {colorScheme: root.colorScheme; text: "Reset:"}
|
||||
Button {colorScheme: root.colorScheme; text: "Finished"; onClicked: root.resetFinished()}
|
||||
}
|
||||
RowLayout {
|
||||
Label {colorScheme: root.colorScheme; text: "Check update:"}
|
||||
Button {colorScheme: root.colorScheme; text: "Finished"; onClicked: root.checkUpdatesFinished()}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property Bridge bridge
|
||||
|
||||
property string goos: "darwin"
|
||||
|
||||
property bool showOnStartup: true // this actually needs to be false, but since we use Bridge_test for testing purpose - lets default this to true just for convenience
|
||||
property bool dockIconVisible: false
|
||||
|
||||
// this signals are used only when trying to login with new user (i.e. not in users model)
|
||||
signal loginUsernamePasswordError(string errorMsg)
|
||||
signal loginFreeUserError()
|
||||
signal loginConnectionError(string errorMsg)
|
||||
signal login2FARequested(string username)
|
||||
signal login2FAError(string errorMsg)
|
||||
signal login2FAErrorAbort(string errorMsg)
|
||||
signal login2PasswordRequested()
|
||||
signal login2PasswordError(string errorMsg)
|
||||
signal login2PasswordErrorAbort(string errorMsg)
|
||||
signal loginFinished(int index)
|
||||
signal loginAlreadyLoggedIn(int index)
|
||||
|
||||
signal internetOff()
|
||||
signal internetOn()
|
||||
|
||||
signal updateManualReady(var version)
|
||||
signal updateManualRestartNeeded()
|
||||
signal updateManualError()
|
||||
signal updateForce(var version)
|
||||
signal updateForceError()
|
||||
signal updateSilentRestartNeeded()
|
||||
signal updateSilentError()
|
||||
signal updateIsLatestVersion()
|
||||
function checkUpdates(){
|
||||
console.log("check updates")
|
||||
}
|
||||
signal checkUpdatesFinished()
|
||||
function installUpdate() {
|
||||
console.log("manuall install update triggered")
|
||||
}
|
||||
|
||||
|
||||
property bool isDiskCacheEnabled: true
|
||||
// Qt.resolvedUrl("file:///C:/Users/user/AppData/Roaming/protonmail/bridge-v3/cache/c11/messages")
|
||||
property url diskCachePath: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
|
||||
signal cacheUnavailable()
|
||||
signal cacheCantMove()
|
||||
signal cacheLocationChangeSuccess()
|
||||
signal diskFull()
|
||||
function changeLocalCache(enableDiskCache, diskCachePath) {
|
||||
console.debug("-> disk cache", enableDiskCache, diskCachePath)
|
||||
}
|
||||
signal changeLocalCacheFinished()
|
||||
|
||||
|
||||
// Settings
|
||||
property bool isAutomaticUpdateOn : true
|
||||
function toggleAutomaticUpdate(makeItActive) {
|
||||
console.debug("-> silent updates", makeItActive, root.isAutomaticUpdateOn)
|
||||
var callback = function () {
|
||||
root.isAutomaticUpdateOn = makeItActive;
|
||||
console.debug("-> CHANGED silent updates", makeItActive, root.isAutomaticUpdateOn)
|
||||
}
|
||||
atimer.onTriggered.connect(callback)
|
||||
atimer.restart()
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: atimer
|
||||
interval: 2000
|
||||
running: false
|
||||
repeat: false
|
||||
}
|
||||
|
||||
property bool isAutostartOn : true // Example of settings with loading state
|
||||
function toggleAutostart(makeItActive) {
|
||||
console.debug("-> autostart", makeItActive, root.isAutostartOn)
|
||||
}
|
||||
signal toggleAutostartFinished()
|
||||
|
||||
property bool isBetaEnabled : false
|
||||
function toggleBeta(makeItActive){
|
||||
console.debug("-> beta", makeItActive, root.isBetaEnabled)
|
||||
root.isBetaEnabled = makeItActive
|
||||
}
|
||||
|
||||
property bool isDoHEnabled : true
|
||||
function toggleDoH(makeItActive){
|
||||
console.debug("-> DoH", makeItActive, root.isDoHEnabled)
|
||||
root.isDoHEnabled = makeItActive
|
||||
}
|
||||
|
||||
property bool isAllMailVisible : true
|
||||
function changeIsAllMailVisible(isVisible){
|
||||
console.debug("-> All Mail Visible", isVisible, root.isAllMailVisible)
|
||||
root.isAllMailVisible = isVisible
|
||||
}
|
||||
|
||||
|
||||
property bool useSSLForSMTP: false
|
||||
function toggleUseSSLForSMTP(makeItActive){
|
||||
console.debug("-> SMTP SSL", makeItActive, root.useSSLForSMTP)
|
||||
}
|
||||
signal toggleUseSSLFinished()
|
||||
|
||||
property string hostname: "127.0.0.1"
|
||||
property int portIMAP: 1143
|
||||
property int portSMTP: 1025
|
||||
function changePorts(imapPort, smtpPort){
|
||||
console.debug("-> ports", imapPort, smtpPort)
|
||||
}
|
||||
function isPortFree(port){
|
||||
if (port == portIMAP) return false
|
||||
if (port == portSMTP) return false
|
||||
if (port == 12345) return false
|
||||
return true
|
||||
}
|
||||
signal changePortFinished()
|
||||
signal imapPortStartupError()
|
||||
signal smtpPortStartupError()
|
||||
|
||||
function triggerReset() {
|
||||
console.debug("-> trigger reset")
|
||||
}
|
||||
signal resetFinished()
|
||||
|
||||
property string version: "2.0.X-BridePreview"
|
||||
property url logsPath: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
|
||||
property url licensePath: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
|
||||
property url releaseNotesLink: Qt.resolvedUrl("https://proton.me/download/bridge/early_releases.html")
|
||||
property url dependencyLicensesLink: Qt.resolvedUrl("https://github.com/ProtonMail/proton-bridge/v3/blob/master/COPYING_NOTES.md#dependencies")
|
||||
property url landingPageLink: Qt.resolvedUrl("https://proton.me/mail/bridge#download")
|
||||
|
||||
property string colorSchemeName: "light"
|
||||
function changeColorScheme(newScheme){
|
||||
root.colorSchemeName = newScheme
|
||||
}
|
||||
|
||||
|
||||
property string currentEmailClient: "" // "Apple Mail 14.0"
|
||||
function updateCurrentMailClient(){
|
||||
currentEmailClient = "Apple Mail 14.0"
|
||||
}
|
||||
|
||||
function reportBug(description,address,emailClient,includeLogs){
|
||||
console.log("report bug")
|
||||
console.log(" description",description)
|
||||
console.log(" address",address)
|
||||
console.log(" emailClient",emailClient)
|
||||
console.log(" includeLogs",includeLogs)
|
||||
}
|
||||
signal reportBugFinished()
|
||||
signal bugReportSendSuccess()
|
||||
signal bugReportSendError()
|
||||
|
||||
property var availableKeychain: ["gnome-keyring", "pass", "macos-keychain", "windows-credentials"]
|
||||
property string currentKeychain: availableKeychain[0]
|
||||
function changeKeychain(wantedKeychain){
|
||||
console.log("Changing keychain from", root.currentKeychain, "to", wantedKeychain)
|
||||
root.currentKeychain = wantedKeychain
|
||||
root.changeKeychainFinished()
|
||||
}
|
||||
signal changeKeychainFinished()
|
||||
signal notifyHasNoKeychain()
|
||||
signal notifyRebuildKeychain()
|
||||
|
||||
signal noActiveKeyForRecipient(string email)
|
||||
signal showMainWindow()
|
||||
|
||||
signal addressChanged(string address)
|
||||
signal addressChangedLogout(string address)
|
||||
signal userDisconnected(string username)
|
||||
signal apiCertIssue()
|
||||
|
||||
property bool showSplashScreen: false
|
||||
|
||||
|
||||
function login(username, password) {
|
||||
root.log("-> login(" + username + ", " + password + ")")
|
||||
|
||||
loginUser.username = username
|
||||
loginUser.isLoginRequested = true
|
||||
}
|
||||
|
||||
function login2FA(username, code) {
|
||||
root.log("-> login2FA(" + username + ", " + code + ")")
|
||||
|
||||
loginUser.isLogin2FAProvided = true
|
||||
}
|
||||
|
||||
function login2Password(username, password) {
|
||||
root.log("-> login2FA(" + username + ", " + password + ")")
|
||||
|
||||
loginUser.isLogin2PasswordProvided = true
|
||||
}
|
||||
|
||||
function loginAbort(username) {
|
||||
root.log("-> loginAbort(" + username + ")")
|
||||
|
||||
loginUser.resetLoginRequests()
|
||||
}
|
||||
|
||||
|
||||
onLoginUsernamePasswordError: {
|
||||
console.debug("<- loginUsernamePasswordError")
|
||||
}
|
||||
onLoginFreeUserError: {
|
||||
console.debug("<- loginFreeUserError")
|
||||
}
|
||||
onLoginConnectionError: {
|
||||
console.debug("<- loginConnectionError")
|
||||
}
|
||||
onLogin2FARequested: {
|
||||
console.debug("<- login2FARequested", username)
|
||||
}
|
||||
onLogin2FAError: {
|
||||
console.debug("<- login2FAError")
|
||||
}
|
||||
onLogin2FAErrorAbort: {
|
||||
console.debug("<- login2FAErrorAbort")
|
||||
}
|
||||
onLogin2PasswordRequested: {
|
||||
console.debug("<- login2PasswordRequested")
|
||||
}
|
||||
onLogin2PasswordError: {
|
||||
console.debug("<- login2PasswordError")
|
||||
}
|
||||
onLogin2PasswordErrorAbort: {
|
||||
console.debug("<- login2PasswordErrorAbort")
|
||||
}
|
||||
onLoginFinished: {
|
||||
console.debug("<- loginFinished", index)
|
||||
}
|
||||
onLoginAlreadyLoggedIn: {
|
||||
console.debug("<- loginAlreadyLoggedIn", index)
|
||||
}
|
||||
|
||||
onInternetOff: {
|
||||
console.debug("<- internetOff")
|
||||
}
|
||||
onInternetOn: {
|
||||
console.debug("<- internetOn")
|
||||
}
|
||||
|
||||
Component {
|
||||
id: bridgeComponent
|
||||
|
||||
Bridge {
|
||||
backend: root
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
onClosing: {
|
||||
Qt.quit()
|
||||
}
|
||||
}
|
||||
@ -61,8 +61,6 @@ Rectangle {
|
||||
type: Label.Body_semibold
|
||||
}
|
||||
|
||||
Item{}
|
||||
|
||||
ConfigurationItem{ colorScheme: root.colorScheme; label: qsTr("Hostname") ; value: root.hostname }
|
||||
ConfigurationItem{ colorScheme: root.colorScheme; label: qsTr("Port") ; value: root.port }
|
||||
ConfigurationItem{ colorScheme: root.colorScheme; label: qsTr("Username") ; value: root.username }
|
||||
|
||||
@ -90,9 +90,9 @@ SettingsView {
|
||||
|
||||
RowLayout {
|
||||
ColorImage {
|
||||
Layout.alignment: Qt.AlignTop
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
|
||||
source: root._isAdvancedShown ? "/qml/icons/ic-chevron-up.svg" : "/qml/icons/ic-chevron-down.svg"
|
||||
source: root._isAdvancedShown ? "/qml/icons/ic-chevron-down.svg" : "/qml/icons/ic-chevron-right.svg"
|
||||
color: root.colorScheme.interaction_norm
|
||||
height: root.colorScheme.body_font_size
|
||||
sourceSize.height: root.colorScheme.body_font_size
|
||||
@ -169,6 +169,19 @@ SettingsView {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
SettingsItem {
|
||||
id: telemetry
|
||||
Layout.fillWidth: true
|
||||
checked: !Backend.isTelemetryDisabled
|
||||
colorScheme: root.colorScheme
|
||||
description: qsTr("Help us improve Proton services by sending anonymous usage statistics.")
|
||||
text: qsTr("Collect usage diagnostics")
|
||||
type: SettingsItem.Toggle
|
||||
visible: root._isAdvancedShown
|
||||
|
||||
onClicked: Backend.toggleIsTelemetryDisabled(telemetry.checked)
|
||||
}
|
||||
|
||||
SettingsItem {
|
||||
id: ports
|
||||
visible: root._isAdvancedShown
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user