forked from Silverfish/proton-bridge
Compare commits
285 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b7ef6e1486 | |||
| 0d03f84711 | |||
| 949666724d | |||
| bbe19bf960 | |||
| bfe25e3a46 | |||
| 236c958703 | |||
| e6b312b437 | |||
| 45d2e9ea63 | |||
| 86e8a566c7 | |||
| 7689139cb3 | |||
| 6269b1ab88 | |||
| 79524185a8 | |||
| 635b81314a | |||
| 4c76e35a2d | |||
| b0ac20425e | |||
| 21a56a0725 | |||
| c57b52ef23 | |||
| 44e103edd5 | |||
| 970de4c205 | |||
| a189c35899 | |||
| c9323a40c8 | |||
| 7a192d50db | |||
| a5a9bd762d | |||
| dd7db00f74 | |||
| a6c20f698c | |||
| f7252ed40e | |||
| 66c30716ea | |||
| 06d7fdf26f | |||
| 8aee732fe8 | |||
| 47233752f5 | |||
| cb6e51531b | |||
| 0b9b886039 | |||
| 1fa0d77b10 | |||
| efbe84964f | |||
| aa77a67a1c | |||
| 78f7cbdc79 | |||
| a731237701 | |||
| f557666b4d | |||
| 5f89e85139 | |||
| 9fb922d18f | |||
| 884c6ed932 | |||
| db77bd4983 | |||
| 85bfcf7158 | |||
| 09a5f8ac0f | |||
| 81f81a63e8 | |||
| e724fafe2f | |||
| cba73997cd | |||
| 29f44fc312 | |||
| 41c125f65e | |||
| 0a555bf767 | |||
| 24331f9715 | |||
| a5500629e5 | |||
| a46533dcf2 | |||
| 12183fbf05 | |||
| c90248920a | |||
| ccb9b7f81c | |||
| 78c0651661 | |||
| d72980e443 | |||
| b24937b666 | |||
| 5ca9ec6674 | |||
| 4ee6da4baa | |||
| e8ce8942be | |||
| 9d0d3708af | |||
| 4c908aac7c | |||
| 6eb1878f66 | |||
| 826dc2e5c3 | |||
| e6ab874308 | |||
| 20b188368a | |||
| ded4f370dc | |||
| 519a6ecdb7 | |||
| c35344d6f1 | |||
| a9865976a3 | |||
| 9a96588afb | |||
| 1f2c573803 | |||
| cc33423c1f | |||
| 1b95c290f1 | |||
| 9b88778c43 | |||
| ae4705ba70 | |||
| 243ddf47ab | |||
| 80d729e3e5 | |||
| 3d64c5f894 | |||
| c4bcc38c53 | |||
| 2c2f816f3a | |||
| 86e115b2f3 | |||
| 1a2783a63b | |||
| cbbab71f5c | |||
| 80add80be2 | |||
| 84adbbc461 | |||
| 75811d22e8 | |||
| e26c7683d2 | |||
| f0e2688a8e | |||
| 06639ff6cd | |||
| 716de7f45a | |||
| 750de0cd31 | |||
| 823ca4d207 | |||
| a187747c7c | |||
| 11ebb16933 | |||
| 0048767022 | |||
| c4b75c6f34 | |||
| 32448063dc | |||
| 86bde91958 | |||
| 90c34406ba | |||
| d7b71aceda | |||
| 25a787529b | |||
| f82965b825 | |||
| f1cf4ee194 | |||
| 5136919c36 | |||
| 334a256638 | |||
| da528f2d9b | |||
| 1b0f930471 | |||
| 09c523e2d2 | |||
| 0b35f41ffd | |||
| 7be1a8ae8a | |||
| bdbf1bdd76 | |||
| 776976a8a2 | |||
| 040ddadb7a | |||
| 11f6f84dd6 | |||
| 82efa16d65 | |||
| 110286b81c | |||
| bc66841cdc | |||
| 8d028966c7 | |||
| d120bbeffc | |||
| eda49483e2 | |||
| 3ef526333a | |||
| f454f1a74f | |||
| e97a4d8847 | |||
| cb8174dbfd | |||
| cfca429067 | |||
| 1ffb9089ba | |||
| c0fc23d7cd | |||
| 4f8ecd598f | |||
| 65365281eb | |||
| c05dfb36d3 | |||
| 42178806d1 | |||
| e34050282e | |||
| b2830b39e0 | |||
| 7997ad2b93 | |||
| efb6ba0f1b | |||
| 80194ad797 | |||
| cda6b2a728 | |||
| 44bde86fde | |||
| 6cb52cacc9 | |||
| 8248491833 | |||
| fbdede28f0 | |||
| 66bc911652 | |||
| c860741ffa | |||
| 80d743afec | |||
| ac9857a965 | |||
| 0e9fd46a5c | |||
| 305d180a5f | |||
| c4f80103b6 | |||
| 0d57e3645a | |||
| 0afdc31f96 | |||
| 91de6e001e | |||
| 908ed3e723 | |||
| 7411073c08 | |||
| 7d838375bb | |||
| f545f30ec0 | |||
| 6579cdfc7f | |||
| 40c48ba804 | |||
| 1c2cb4f439 | |||
| 650dac37a7 | |||
| 55275b23ee | |||
| bce69e1a1b | |||
| f1917ad0de | |||
| 300d243331 | |||
| 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 | |||
| c4abb14ae6 | |||
| 38a0cdb4ab | |||
| c587dfc0dc | |||
| 7a090ffcc9 | |||
| 6ab49367ba | |||
| a06fd31f49 | |||
| 016319784e | |||
| 12f9fb03c3 | |||
| ac00ef1b64 | |||
| 1e9a77c7b2 | |||
| a8dd52800e | |||
| fab063f194 | |||
| 4902898880 | |||
| c46b3245b8 | |||
| 3afd94c61d | |||
| 89632b7acd | |||
| b8cf193911 | |||
| fb4d34ef5b | |||
| 7f7e360cd7 | |||
| fc06665d2b | |||
| bfaf9765ae | |||
| a702e19dff | |||
| 1d595b933a | |||
| 39cd165bd1 | |||
| afef730870 | |||
| f691795ca1 | |||
| 9798cdec5c | |||
| 9ae899f420 | |||
| f2a8990972 | |||
| 8db30a89c0 | |||
| 449b580056 | |||
| 38397accf5 | |||
| 5d49cf2c09 | |||
| c1918e2b1b | |||
| 60a779c653 | |||
| 4adc05d354 | |||
| 86fbf4415a | |||
| b4ea667858 | |||
| 8f45fe823a | |||
| 3b1c3b9d44 | |||
| 60256fd076 | |||
| 9c1b9e8df2 | |||
| fb44de4f18 | |||
| c821d02f67 | |||
| ec2a4f9111 | |||
| 968a01053f | |||
| 6f914a4973 | |||
| 76bdc21fef | |||
| dd29ff4731 | |||
| 8b94a28e00 | |||
| 60100ad7f0 | |||
| 62a50fd7fc |
4
.gitignore
vendored
4
.gitignore
vendored
@ -41,3 +41,7 @@ cmake-build-*/
|
|||||||
|
|
||||||
# Doxygen doc files
|
# Doxygen doc files
|
||||||
_doc/
|
_doc/
|
||||||
|
|
||||||
|
# gRPC auto-generated C++ source files
|
||||||
|
*.pb.cc
|
||||||
|
*.pb.h
|
||||||
|
|||||||
378
.gitlab-ci.yml
378
.gitlab-ci.yml
@ -30,13 +30,6 @@ stages:
|
|||||||
- test
|
- test
|
||||||
- build
|
- 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-branch-and-MR-manual:
|
||||||
rules:
|
rules:
|
||||||
- if: $CI_COMMIT_BRANCH || $CI_PIPELINE_SOURCE == "merge_request_event"
|
- if: $CI_COMMIT_BRANCH || $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||||
@ -44,16 +37,6 @@ stages:
|
|||||||
allow_failure: true
|
allow_failure: true
|
||||||
- when: never
|
- 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-branch-manual-MR-and-devel-always:
|
||||||
rules:
|
rules:
|
||||||
- if: $CI_COMMIT_BRANCH == "devel" || $CI_PIPELINE_SOURCE == "merge_request_event"
|
- if: $CI_COMMIT_BRANCH == "devel" || $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||||
@ -64,173 +47,23 @@ stages:
|
|||||||
allow_failure: true
|
allow_failure: true
|
||||||
- when: never
|
- when: never
|
||||||
|
|
||||||
.after-script-code-coverage:
|
.rules-branch-manual-scheduled-and-test-branch-always:
|
||||||
after_script:
|
|
||||||
- go get github.com/boumenot/gocover-cobertura
|
|
||||||
- go run github.com/boumenot/gocover-cobertura < /tmp/coverage.out > coverage.xml
|
|
||||||
- "go tool cover -func=/tmp/coverage.out | grep total:"
|
|
||||||
coverage: '/total:.*\(statements\).*\d+\.\d+%/'
|
|
||||||
artifacts:
|
|
||||||
reports:
|
|
||||||
coverage_report:
|
|
||||||
coverage_format: cobertura
|
|
||||||
path: coverage.xml
|
|
||||||
|
|
||||||
# Stage: TEST
|
|
||||||
|
|
||||||
lint:
|
|
||||||
stage: test
|
|
||||||
extends:
|
|
||||||
- .rules-branch-and-MR-always
|
|
||||||
script:
|
|
||||||
- make lint
|
|
||||||
tags:
|
|
||||||
- medium
|
|
||||||
|
|
||||||
|
|
||||||
.test-base:
|
|
||||||
stage: test
|
|
||||||
script:
|
|
||||||
- make test
|
|
||||||
|
|
||||||
test-linux:
|
|
||||||
extends:
|
|
||||||
- .test-base
|
|
||||||
- .rules-branch-manual-MR-and-devel-always
|
|
||||||
- .after-script-code-coverage
|
|
||||||
tags:
|
|
||||||
- large
|
|
||||||
|
|
||||||
test-linux-race:
|
|
||||||
extends:
|
|
||||||
- test-linux
|
|
||||||
- .rules-branch-and-MR-manual
|
|
||||||
script:
|
|
||||||
- make test-race
|
|
||||||
|
|
||||||
test-integration:
|
|
||||||
extends:
|
|
||||||
- test-linux
|
|
||||||
script:
|
|
||||||
- make test-integration
|
|
||||||
tags:
|
|
||||||
- large
|
|
||||||
|
|
||||||
test-integration-race:
|
|
||||||
extends:
|
|
||||||
- test-integration
|
|
||||||
- .rules-branch-and-MR-manual
|
|
||||||
script:
|
|
||||||
- make test-integration-race
|
|
||||||
|
|
||||||
|
|
||||||
.windows-base:
|
|
||||||
before_script:
|
|
||||||
- export GOROOT=/c/Go1.20
|
|
||||||
- export PATH=$GOROOT/bin:$PATH
|
|
||||||
- export GOARCH=amd64
|
|
||||||
- export GOPATH=~/go1.20
|
|
||||||
- export GO111MODULE=on
|
|
||||||
- export PATH=$GOPATH/bin:$PATH
|
|
||||||
- export MSYSTEM=
|
|
||||||
tags:
|
|
||||||
- windows-bridge
|
|
||||||
|
|
||||||
test-windows:
|
|
||||||
extends:
|
|
||||||
- .rules-branch-manual-MR-always
|
|
||||||
- .windows-base
|
|
||||||
stage: test
|
|
||||||
script:
|
|
||||||
- make test
|
|
||||||
|
|
||||||
# Stage: BUILD
|
|
||||||
|
|
||||||
.build-base:
|
|
||||||
stage: build
|
|
||||||
needs: ["lint"]
|
|
||||||
rules:
|
rules:
|
||||||
# GODT-1833: use `=~ /qa/` after mac and windows runners are fixed
|
- if: $CI_PIPELINE_SOURCE == "schedule"
|
||||||
- if: $CI_JOB_NAME =~ /build-linux-qa/ && $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
||||||
when: always
|
when: always
|
||||||
allow_failure: false
|
allow_failure: false
|
||||||
- if: $CI_COMMIT_BRANCH || $CI_PIPELINE_SOURCE == "merge_request_event"
|
- if: $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME=~ /^test/
|
||||||
|
when: always
|
||||||
|
allow_failure: false
|
||||||
|
- if: $CI_COMMIT_BRANCH
|
||||||
when: manual
|
when: manual
|
||||||
allow_failure: true
|
allow_failure: true
|
||||||
- when: never
|
- when: never
|
||||||
script:
|
|
||||||
- make build
|
|
||||||
- git diff && git diff-index --quiet HEAD
|
|
||||||
- make vault-editor
|
|
||||||
artifacts:
|
|
||||||
expire_in: 1 day
|
|
||||||
when: always
|
|
||||||
name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"
|
|
||||||
paths:
|
|
||||||
- bridge_*.tgz
|
|
||||||
- vault-editor
|
|
||||||
|
|
||||||
|
# ENV
|
||||||
.linux-build-setup:
|
.env-windows:
|
||||||
image: gitlab.protontech.ch:4567/go/bridge-internal:build-go1.20-qt6.3.2
|
|
||||||
variables:
|
|
||||||
VCPKG_DEFAULT_BINARY_CACHE: ${CI_PROJECT_DIR}/.cache
|
|
||||||
cache:
|
|
||||||
key: linux-vcpkg
|
|
||||||
paths:
|
|
||||||
- .cache
|
|
||||||
when: 'always'
|
|
||||||
before_script:
|
|
||||||
- mkdir -p .cache/bin
|
|
||||||
- export PATH=$(pwd)/.cache/bin:$PATH
|
|
||||||
- export GOPATH="$CI_PROJECT_DIR/.cache"
|
|
||||||
- export PATH=$PATH:$QT6DIR/bin
|
|
||||||
- $(git config --global -l | grep -o 'url.*gitlab.protontech.ch.*insteadof' | xargs -L 1 git config --global --unset &> /dev/null) || echo "nothing to remove"
|
|
||||||
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
|
|
||||||
tags:
|
|
||||||
- large
|
|
||||||
|
|
||||||
build-linux:
|
|
||||||
extends:
|
|
||||||
- .build-base
|
|
||||||
- .linux-build-setup
|
|
||||||
|
|
||||||
build-linux-qa:
|
|
||||||
extends:
|
|
||||||
- build-linux
|
|
||||||
variables:
|
|
||||||
BUILD_TAGS: "build_qa"
|
|
||||||
|
|
||||||
|
|
||||||
.darwin-build-setup:
|
|
||||||
before_script:
|
|
||||||
- export PATH=/usr/local/bin:$PATH
|
|
||||||
- export PATH=/usr/local/opt/git/bin:$PATH
|
|
||||||
- export PATH=/usr/local/opt/make/libexec/gnubin:$PATH
|
|
||||||
- export PATH=/usr/local/opt/go@1.13/bin:$PATH
|
|
||||||
- export PATH=/usr/local/opt/gnu-sed/libexec/gnubin:$PATH
|
|
||||||
- export GOPATH=~/go1.20
|
|
||||||
- export PATH=$GOPATH/bin:$PATH
|
|
||||||
- export CGO_CPPFLAGS='-Wno-error -Wno-nullability-completeness -Wno-expansion-to-defined -Wno-builtin-requires-header'
|
|
||||||
- $(git config --global -l | grep -o 'url.*gitlab.protontech.ch.*insteadof' | xargs -L 1 git config --global --unset &> /dev/null) || echo "nothing to remove"
|
|
||||||
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
|
|
||||||
cache: {}
|
|
||||||
tags:
|
|
||||||
- macOS
|
|
||||||
|
|
||||||
build-darwin:
|
|
||||||
extends:
|
|
||||||
- .build-base
|
|
||||||
- .darwin-build-setup
|
|
||||||
|
|
||||||
build-darwin-qa:
|
|
||||||
extends:
|
|
||||||
- build-darwin
|
|
||||||
variables:
|
|
||||||
BUILD_TAGS: "build_qa"
|
|
||||||
|
|
||||||
.windows-build-setup:
|
|
||||||
before_script:
|
before_script:
|
||||||
|
- export BRIDGE_SYNC_FORCE_MINIMUM_SPEC=1
|
||||||
- export GOROOT=/c/Go1.20/
|
- export GOROOT=/c/Go1.20/
|
||||||
- export PATH=$GOROOT/bin:$PATH
|
- export PATH=$GOROOT/bin:$PATH
|
||||||
- export GOARCH=amd64
|
- export GOARCH=amd64
|
||||||
@ -249,10 +82,199 @@ build-darwin-qa:
|
|||||||
tags:
|
tags:
|
||||||
- windows-bridge
|
- windows-bridge
|
||||||
|
|
||||||
|
.env-darwin:
|
||||||
|
before_script:
|
||||||
|
- export BRIDGE_SYNC_FORCE_MINIMUM_SPEC=1
|
||||||
|
- export PATH=/usr/local/bin:$PATH
|
||||||
|
- export PATH=/usr/local/opt/git/bin:$PATH
|
||||||
|
- export PATH=/usr/local/opt/make/libexec/gnubin:$PATH
|
||||||
|
- export PATH=/usr/local/opt/gnu-sed/libexec/gnubin:$PATH
|
||||||
|
- export GOROOT=~/local/opt/go@1.20
|
||||||
|
- export PATH="${GOROOT}/bin:$PATH"
|
||||||
|
- export GOPATH=~/go1.20
|
||||||
|
- export PATH="${GOPATH}/bin:$PATH"
|
||||||
|
- export QT6DIR=/opt/Qt/6.3.2/macos
|
||||||
|
- export PATH="${QT6DIR}/bin:$PATH"
|
||||||
|
- uname -a
|
||||||
|
cache: {}
|
||||||
|
tags:
|
||||||
|
- macos-m1-bridge
|
||||||
|
|
||||||
|
.env-linux-build:
|
||||||
|
image: gitlab.protontech.ch:4567/go/bridge-internal:build-go1.20-qt6.3.2
|
||||||
|
variables:
|
||||||
|
VCPKG_DEFAULT_BINARY_CACHE: ${CI_PROJECT_DIR}/.cache
|
||||||
|
cache:
|
||||||
|
key: linux-vcpkg
|
||||||
|
paths:
|
||||||
|
- .cache
|
||||||
|
when: 'always'
|
||||||
|
before_script:
|
||||||
|
- mkdir -p .cache/bin
|
||||||
|
- export BRIDGE_SYNC_FORCE_MINIMUM_SPEC=1
|
||||||
|
- export PATH=$(pwd)/.cache/bin:$PATH
|
||||||
|
- export GOPATH="$CI_PROJECT_DIR/.cache"
|
||||||
|
- export PATH=$PATH:$QT6DIR/bin
|
||||||
|
- $(git config --global -l | grep -o 'url.*gitlab.protontech.ch.*insteadof' | xargs -L 1 git config --global --unset &> /dev/null) || echo "nothing to remove"
|
||||||
|
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
|
||||||
|
tags:
|
||||||
|
- large
|
||||||
|
|
||||||
|
# Stage: TEST
|
||||||
|
|
||||||
|
lint:
|
||||||
|
stage: test
|
||||||
|
extends:
|
||||||
|
- .rules-branch-manual-MR-and-devel-always
|
||||||
|
script:
|
||||||
|
- make lint
|
||||||
|
tags:
|
||||||
|
- medium
|
||||||
|
|
||||||
|
bug-report-preview:
|
||||||
|
stage: test
|
||||||
|
extends:
|
||||||
|
- .rules-branch-and-MR-manual
|
||||||
|
script:
|
||||||
|
- make lint-bug-report-preview
|
||||||
|
tags:
|
||||||
|
- medium
|
||||||
|
|
||||||
|
.script-test:
|
||||||
|
stage: test
|
||||||
|
extends:
|
||||||
|
- .rules-branch-manual-MR-and-devel-always
|
||||||
|
script:
|
||||||
|
- make test
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- coverage/**
|
||||||
|
|
||||||
|
test-linux:
|
||||||
|
extends:
|
||||||
|
- .script-test
|
||||||
|
tags:
|
||||||
|
- large
|
||||||
|
|
||||||
|
fuzz-linux:
|
||||||
|
stage: test
|
||||||
|
extends:
|
||||||
|
- .rules-branch-manual-MR-and-devel-always
|
||||||
|
script:
|
||||||
|
- make fuzz
|
||||||
|
tags:
|
||||||
|
- large
|
||||||
|
|
||||||
|
test-linux-race:
|
||||||
|
extends:
|
||||||
|
- test-linux
|
||||||
|
- .rules-branch-and-MR-manual
|
||||||
|
script:
|
||||||
|
- make test-race
|
||||||
|
|
||||||
|
test-integration:
|
||||||
|
extends:
|
||||||
|
- test-linux
|
||||||
|
script:
|
||||||
|
- make test-integration
|
||||||
|
|
||||||
|
test-integration-race:
|
||||||
|
extends:
|
||||||
|
- test-integration
|
||||||
|
- .rules-branch-and-MR-manual
|
||||||
|
script:
|
||||||
|
- make test-integration-race
|
||||||
|
|
||||||
|
test-integration-nightly:
|
||||||
|
extends:
|
||||||
|
- test-integration
|
||||||
|
- .rules-branch-manual-scheduled-and-test-branch-always
|
||||||
|
needs:
|
||||||
|
- test-integration
|
||||||
|
script:
|
||||||
|
- make test-integration-nightly
|
||||||
|
|
||||||
|
test-windows:
|
||||||
|
extends:
|
||||||
|
- .env-windows
|
||||||
|
- .script-test
|
||||||
|
|
||||||
|
test-darwin:
|
||||||
|
extends:
|
||||||
|
- .env-darwin
|
||||||
|
- .script-test
|
||||||
|
|
||||||
|
test-coverage:
|
||||||
|
stage: test
|
||||||
|
extends:
|
||||||
|
- .rules-branch-manual-scheduled-and-test-branch-always
|
||||||
|
script:
|
||||||
|
- ./utils/coverage.sh
|
||||||
|
coverage: '/total:.*\(statements\).*\d+\.\d+%/'
|
||||||
|
needs:
|
||||||
|
- test-linux
|
||||||
|
- test-windows
|
||||||
|
- test-darwin
|
||||||
|
- test-integration
|
||||||
|
- test-integration-nightly
|
||||||
|
tags:
|
||||||
|
- small
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- coverage*
|
||||||
|
- coverage/**
|
||||||
|
when: 'always'
|
||||||
|
reports:
|
||||||
|
coverage_report:
|
||||||
|
coverage_format: cobertura
|
||||||
|
path: coverage.xml
|
||||||
|
|
||||||
|
# Stage: BUILD
|
||||||
|
|
||||||
|
.script-build:
|
||||||
|
stage: build
|
||||||
|
needs: ["lint"]
|
||||||
|
extends:
|
||||||
|
- .rules-branch-and-MR-manual
|
||||||
|
script:
|
||||||
|
- make build
|
||||||
|
- git diff && git diff-index --quiet HEAD
|
||||||
|
- make vault-editor
|
||||||
|
artifacts:
|
||||||
|
expire_in: 1 day
|
||||||
|
when: always
|
||||||
|
name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"
|
||||||
|
paths:
|
||||||
|
- bridge_*.tgz
|
||||||
|
- vault-editor
|
||||||
|
|
||||||
|
build-linux:
|
||||||
|
extends:
|
||||||
|
- .script-build
|
||||||
|
- .env-linux-build
|
||||||
|
|
||||||
|
build-linux-qa:
|
||||||
|
extends:
|
||||||
|
- build-linux
|
||||||
|
- .rules-branch-manual-MR-and-devel-always
|
||||||
|
variables:
|
||||||
|
BUILD_TAGS: "build_qa"
|
||||||
|
|
||||||
|
build-darwin:
|
||||||
|
extends:
|
||||||
|
- .script-build
|
||||||
|
- .env-darwin
|
||||||
|
|
||||||
|
build-darwin-qa:
|
||||||
|
extends:
|
||||||
|
- build-darwin
|
||||||
|
variables:
|
||||||
|
BUILD_TAGS: "build_qa"
|
||||||
|
|
||||||
build-windows:
|
build-windows:
|
||||||
extends:
|
extends:
|
||||||
- .build-base
|
- .script-build
|
||||||
- .windows-build-setup
|
- .env-windows
|
||||||
|
|
||||||
build-windows-qa:
|
build-windows-qa:
|
||||||
extends:
|
extends:
|
||||||
|
|||||||
@ -36,6 +36,14 @@ issues:
|
|||||||
- gosec
|
- gosec
|
||||||
- goconst
|
- goconst
|
||||||
- dogsled
|
- dogsled
|
||||||
|
- path: utils/smtp-send
|
||||||
|
linters:
|
||||||
|
- dupl
|
||||||
|
- gochecknoglobals
|
||||||
|
- gochecknoinits
|
||||||
|
- gosec
|
||||||
|
- goconst
|
||||||
|
- dogsled
|
||||||
|
|
||||||
linters-settings:
|
linters-settings:
|
||||||
godox:
|
godox:
|
||||||
|
|||||||
@ -58,7 +58,7 @@ Proton Mail Bridge includes the following 3rd party software:
|
|||||||
* [testify](https://github.com/stretchr/testify) available under [license](https://github.com/stretchr/testify/blob/master/LICENSE)
|
* [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)
|
* [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)
|
* [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)
|
* [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)
|
* [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)
|
* [sys](https://golang.org/x/sys) available under [license](https://cs.opensource.google/go/x/sys/+/master:LICENSE)
|
||||||
@ -66,16 +66,12 @@ Proton Mail Bridge includes the following 3rd party software:
|
|||||||
* [grpc](https://google.golang.org/grpc) available under [license](https://github.com/grpc/grpc-go/blob/master/LICENSE)
|
* [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)
|
* [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)
|
* [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)
|
* [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-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-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)
|
* [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)
|
* [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)
|
* [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)
|
* [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)
|
* [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)
|
* [test](https://github.com/chzyer/test) available under [license](https://github.com/chzyer/test/blob/master/LICENSE)
|
||||||
@ -93,7 +89,6 @@ Proton Mail Bridge includes the following 3rd party software:
|
|||||||
* [mimetype](https://github.com/gabriel-vasile/mimetype) available under [license](https://github.com/gabriel-vasile/mimetype/blob/master/LICENSE)
|
* [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)
|
* [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)
|
* [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)
|
* [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)
|
* [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)
|
* [validator](https://github.com/go-playground/validator/v10) available under [license](https://github.com/go-playground/validator/v10/blob/master/LICENSE)
|
||||||
@ -105,7 +100,6 @@ Proton Mail Bridge includes the following 3rd party software:
|
|||||||
* [go-immutable-radix](https://github.com/hashicorp/go-immutable-radix) available under [license](https://github.com/hashicorp/go-immutable-radix/blob/master/LICENSE)
|
* [go-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)
|
* [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)
|
* [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)
|
* [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)
|
* [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)
|
* [cpuid](https://github.com/klauspost/cpuid/v2) available under [license](https://github.com/klauspost/cpuid/v2/blob/master/LICENSE)
|
||||||
@ -114,7 +108,6 @@ Proton Mail Bridge includes the following 3rd party software:
|
|||||||
* [go-isatty](https://github.com/mattn/go-isatty) available under [license](https://github.com/mattn/go-isatty/blob/master/LICENSE)
|
* [go-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-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-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)
|
* [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)
|
* [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)
|
* [tablewriter](https://github.com/olekukonko/tablewriter) available under [license](https://github.com/olekukonko/tablewriter/blob/master/LICENSE)
|
||||||
@ -130,14 +123,13 @@ Proton Mail Bridge includes the following 3rd party software:
|
|||||||
* [codec](https://github.com/ugorji/go/codec) available under [license](https://github.com/ugorji/go/codec/blob/master/LICENSE)
|
* [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)
|
* [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)
|
* [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)
|
* [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)
|
* [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)
|
* [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)
|
* [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)
|
* [tools](https://golang.org/x/tools) available under [license](https://cs.opensource.google/go/x/tools/+/master:LICENSE)
|
||||||
* [genproto](https://google.golang.org/genproto)
|
* [genproto](https://google.golang.org/genproto) available under [license](https://pkg.go.dev/google.golang.org/genproto?tab=licenses)
|
||||||
gopkg.in/yaml.v3
|
* [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)
|
* [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-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)
|
* [go-keychain](https://github.com/cuthix/go-keychain) available under [license](https://github.com/cuthix/go-keychain/blob/master/LICENSE)
|
||||||
|
|||||||
229
Changelog.md
229
Changelog.md
@ -3,6 +3,181 @@
|
|||||||
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||||
|
|
||||||
|
|
||||||
|
## Umshiang Bridge 3.5.1
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* GODT-2963: Use multi error to report file removal errors.
|
||||||
|
* GODT-2956: Restore old deletion rules.
|
||||||
|
* GODT-2951: Negative WaitGroup Counter.
|
||||||
|
* GODT-2590: Fix send on closed channel.
|
||||||
|
* GODT-2949: Fix close of close channel in event service.
|
||||||
|
|
||||||
|
|
||||||
|
## Umshiang Bridge 3.5.0
|
||||||
|
|
||||||
|
### Added
|
||||||
|
* GODT-2734: Add testing steps to modify account settings.
|
||||||
|
* GODT-2746: Integration tests for reporting a problem.
|
||||||
|
* GODT-2891: Allow message create & delete during sync.
|
||||||
|
* GODT-2848: Decouple IMAP service from Event Loop.
|
||||||
|
* Add trace profiling option.
|
||||||
|
* GODT-2829: New Sync Service.
|
||||||
|
* Test: oss-fuzz support for fuzzing.
|
||||||
|
* GODT-2799: SMTP Service.
|
||||||
|
* GODT-2800: User Event Service.
|
||||||
|
* GODT-2801: Identity Service.
|
||||||
|
* GODT-2802: IMAP Serivce.
|
||||||
|
* GODT-2788: Add preview to bug report validation and JSON file validator.
|
||||||
|
* GODT-2803: Bridge Database access.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* GODT-2909: Remove Timeout on event publish.
|
||||||
|
* GODT-2913: Reduce the number of configuration failure detected.
|
||||||
|
* GODT-2828: Increase sync progress report frequency.
|
||||||
|
* Test: Fix TestBridge_SyncWithOnGoingEvents.
|
||||||
|
* GODT-2871: Is telemetry enabled as service.
|
||||||
|
* Test(GODT-2873): Wait for Gluon Watcher to finish.
|
||||||
|
* Test(GODT-2744): Add integration tests for moving messages (with MOVE support).
|
||||||
|
* Test(GODT-2872): Fix nightly job.
|
||||||
|
* Test(GODT-2742): Add more integration tests regarding drafts.
|
||||||
|
* GODT-2787: Force Scrollview to top when re-opening questions set.
|
||||||
|
* GODT-2787: Tweaking Bug Report form with last Review.
|
||||||
|
* Ci(GODT-2717): Create a job that will run on schedule.
|
||||||
|
* GODT-2787: Fix vertical alignement on CategoryItem.
|
||||||
|
* GODT-2842: Implement Bug Report Fallback notification.
|
||||||
|
* Chore(GODT-2848): Simplify User Event Service.
|
||||||
|
* GODT-2808: Apply comment from Bug Report content review.
|
||||||
|
* Test(GODT-2743): Sync high number of messages.
|
||||||
|
* GODT-2814: Standalone Server Manager.
|
||||||
|
* GODT-2808: Initial list of categories and questions.
|
||||||
|
* GODT-2787: Replace the PathTracker by a more visual NavigationIndicator.
|
||||||
|
* GODT-2816: Wait until mandatory fields are filled then fill body and title.
|
||||||
|
* GODT-2794: Clear cached answers when report is sent.
|
||||||
|
* GODT-2793: Feed the bug report body with the answered questions.
|
||||||
|
* GODT-2791: Parse the Bug Report Flow description file and ensure forward compatibility (GODT-2789).
|
||||||
|
* GODT-2821: Display questions in one page.
|
||||||
|
* GODT-2786: Init bug report flow description file.
|
||||||
|
* GODT-2792: Implement display of question set for bug report.
|
||||||
|
* Use qmlformat on qml files, and removed deprecated tests.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* GODT-2828: Fix negative report time.
|
||||||
|
* GODT-2828: Fix sync progress report after restart.
|
||||||
|
* GODT-2867: Do not crash on timeout or context cancel.
|
||||||
|
* GODT-2693: Duplicate messages in sent folder.
|
||||||
|
* GODT-2867: Get attachment returns API error on network problem.
|
||||||
|
* GODT-2805: Ignore Contact Group Labels.
|
||||||
|
* GODT-2866: Add 429/5xx Retry to Event Service.
|
||||||
|
* GODT-2855: Fix for text overlapping in settings view.
|
||||||
|
* Test: Verify leaks at end of WithEnv.
|
||||||
|
* Test: Fix event registration in TestBridge_SyncWithOngoingEvents.
|
||||||
|
* Test: Fix deadlock in chToType.
|
||||||
|
* GODT-2865: Add error on failed unlock.
|
||||||
|
* GODT-2857: Do not check changed values in clear recent flag.
|
||||||
|
* GODT-2827: Restore ticker to event poller.
|
||||||
|
* Test: TestBridge_SendAddTextBodyPartIfNotExists eventually fix.
|
||||||
|
* GODT-2813: Write new vault to temporary file first.
|
||||||
|
* GODT-2807: Fix issue where sessionID would not be removed from command-line on restart by bridge-gui.
|
||||||
|
* GODT-2687: Tabs after header field colon.
|
||||||
|
* GODT-2764: Allow perma-delete for messages which still have labels.
|
||||||
|
* GODT-2693: Fix message appearing twice after sent.
|
||||||
|
* GODT-2781: Try to remove stale lock file before failing in checkSingleInstance.
|
||||||
|
* GODT-2780: Fix 'QSystemTrayIcon::setVisible: No Icon set' warning in bridge-gui log on startup.
|
||||||
|
* GODT-2778: Fix login screen being disabled after an 'already logged in' error.
|
||||||
|
* Fix typos found by codespell.
|
||||||
|
* GODT-2577: Answered flag should only be applied to replied messages.
|
||||||
|
|
||||||
|
|
||||||
|
## 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-2670: 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
|
## Stone Bridge 3.3.0
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
@ -44,7 +219,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
|||||||
* GODT-2437: Silence harmless report to sentry.
|
* GODT-2437: Silence harmless report to sentry.
|
||||||
* GODT-2649: Clean up cache files after failed connector create (Gluon).
|
* GODT-2649: Clean up cache files after failed connector create (Gluon).
|
||||||
* GODT-2638: Validate messages before import.
|
* GODT-2638: Validate messages before import.
|
||||||
* GODT-2646: Bump GPA and Gluon dependecy after CIRCL upgrade.
|
* GODT-2646: Bump GPA and Gluon dependency after CIRCL upgrade.
|
||||||
* GODT-2454: Only Send status update if transaction succeeded.
|
* GODT-2454: Only Send status update if transaction succeeded.
|
||||||
* Test: fix flaky tests.
|
* Test: fix flaky tests.
|
||||||
* GODT-2628: Attempt to fix closed channel panic on logout.
|
* GODT-2628: Attempt to fix closed channel panic on logout.
|
||||||
@ -104,7 +279,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
|||||||
* GODT-2574: Fix label/unlabel of large amounts of messages.
|
* GODT-2574: Fix label/unlabel of large amounts of messages.
|
||||||
* GODT-2573: Handle invalid header fields in message.
|
* GODT-2573: Handle invalid header fields in message.
|
||||||
* GODT-2573: Crash on null update.
|
* GODT-2573: Crash on null update.
|
||||||
* GODT-2407: Replace invalid email addresses with emtpy for new Drafts.
|
* GODT-2407: Replace invalid email addresses with empty for new Drafts.
|
||||||
|
|
||||||
## [Bridge 3.1.3] Quebec
|
## [Bridge 3.1.3] Quebec
|
||||||
|
|
||||||
@ -245,7 +420,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
|||||||
* GODT-2429: Do not report context cancel to sentry.
|
* GODT-2429: Do not report context cancel to sentry.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
* GODT-2467: elide long email adresses in 'bad event' QML notification dialog.
|
* GODT-2467: elide long email addresses in 'bad event' QML notification dialog.
|
||||||
* GODT-2449: fix bug in Bridge-GUI's Exception::what().
|
* GODT-2449: fix bug in Bridge-GUI's Exception::what().
|
||||||
* GODT-2427: Parsing header issues.
|
* GODT-2427: Parsing header issues.
|
||||||
* GODT-2426: Fix crash on user delete.
|
* GODT-2426: Fix crash on user delete.
|
||||||
@ -262,7 +437,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
|||||||
* GODT-2404: Handle unexpected EOF.
|
* GODT-2404: Handle unexpected EOF.
|
||||||
* GODT-2400: Allow state updates to be applied if command fails.
|
* GODT-2400: Allow state updates to be applied if command fails.
|
||||||
* GODT-2399: Fix immediate message deletion during updates.
|
* GODT-2399: Fix immediate message deletion during updates.
|
||||||
* GODT-2390: Missing changes from pervious commit.
|
* GODT-2390: Missing changes from previous commit.
|
||||||
* GODT-2390: Add reports for uncaught json and net.opErr.
|
* GODT-2390: Add reports for uncaught json and net.opErr.
|
||||||
* GODT-2414: Multiple deletion bug in WriteControlledStore.
|
* GODT-2414: Multiple deletion bug in WriteControlledStore.
|
||||||
|
|
||||||
@ -327,7 +502,7 @@ GODT-1804: Preserve MIME parameters when uploading attachments.
|
|||||||
* GODT-2223: Improve event handling.
|
* GODT-2223: Improve event handling.
|
||||||
* GODT-2305: Detect missing gluon DB.
|
* GODT-2305: Detect missing gluon DB.
|
||||||
* GODT-2291: Change gluon store default location from Cache to Data.
|
* GODT-2291: Change gluon store default location from Cache to Data.
|
||||||
* Other: Disable dialer test until badssl cert is bumbed.
|
* Other: Disable dialer test until badssl cert is bumped.
|
||||||
* GODT-2292: Updated BUILDS.md doc.
|
* GODT-2292: Updated BUILDS.md doc.
|
||||||
* GODT-2258: suggest email as login when signing in via status window.
|
* GODT-2258: suggest email as login when signing in via status window.
|
||||||
* Other: Report corrupt and/or insecure vaults to sentry.
|
* Other: Report corrupt and/or insecure vaults to sentry.
|
||||||
@ -607,7 +782,7 @@ GODT-1804: Preserve MIME parameters when uploading attachments.
|
|||||||
## [Bridge 2.4.6] Osney
|
## [Bridge 2.4.6] Osney
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
* GODT-2019: When signing out and a single user is connecte* we do not go back to the welcome screen.
|
* GODT-2019: When signing out and a single user is connected we do not go back to the welcome screen.
|
||||||
* GODT-2071: Bridge-gui report error if an orphan bridge is detected.
|
* GODT-2071: Bridge-gui report error if an orphan bridge is detected.
|
||||||
* GODT-2046: Bridge-gui log is included in optional archive sent with bug reports.
|
* GODT-2046: Bridge-gui log is included in optional archive sent with bug reports.
|
||||||
* GODT-2039: Bridge monitors bridge-gui via its PID.
|
* GODT-2039: Bridge monitors bridge-gui via its PID.
|
||||||
@ -761,7 +936,7 @@ GODT-1804: Preserve MIME parameters when uploading attachments.
|
|||||||
* GODT-1260: Renaming.
|
* GODT-1260: Renaming.
|
||||||
* GODT-1502: Rebranding: color and radius.
|
* GODT-1502: Rebranding: color and radius.
|
||||||
* GODT-1549: Add notification when address list changes.
|
* GODT-1549: Add notification when address list changes.
|
||||||
* GODT-1560: Dependecy licenses update and link.
|
* GODT-1560: Dependency licenses update and link.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
* GODT-1543: Using one buffered event for off and on connection.
|
* GODT-1543: Using one buffered event for off and on connection.
|
||||||
@ -858,7 +1033,7 @@ GODT-1537: Manual in-app update mechanism.
|
|||||||
* GODT-1338: GODT-1343 Help view buttons.
|
* GODT-1338: GODT-1343 Help view buttons.
|
||||||
* GODT-1340: Not crashing, user list updating in main thread.
|
* GODT-1340: Not crashing, user list updating in main thread.
|
||||||
* GODT-1345: Adding panic handlers.
|
* GODT-1345: Adding panic handlers.
|
||||||
* GODT-1271: Fix Status margings.
|
* GODT-1271: Fix Status margins.
|
||||||
* GODT-1320: Add loading property to each action within a notification.
|
* GODT-1320: Add loading property to each action within a notification.
|
||||||
* GODT-1210: Add "free user" banner.
|
* GODT-1210: Add "free user" banner.
|
||||||
* GODT-1314: Limit description field length within 150/800 bounds.
|
* GODT-1314: Limit description field length within 150/800 bounds.
|
||||||
@ -900,7 +1075,7 @@ GODT-1537: Manual in-app update mechanism.
|
|||||||
* GODT-1381 Treat readonly folder as failure for cache on disk.
|
* GODT-1381 Treat readonly folder as failure for cache on disk.
|
||||||
* GODT-1431 Prevent watcher when not using disk on cache.
|
* GODT-1431 Prevent watcher when not using disk on cache.
|
||||||
* GODT-1381: Use in-memory cache in case local cache is unavailable.
|
* GODT-1381: Use in-memory cache in case local cache is unavailable.
|
||||||
* GODT-1356 GODT-1302: Cache on disk concurency and API retries.
|
* GODT-1356 GODT-1302: Cache on disk concurrency and API retries.
|
||||||
* GODT-1332 Added tests for cache move functions.
|
* GODT-1332 Added tests for cache move functions.
|
||||||
* GODT-1332: moved cache related functions to separate file.
|
* GODT-1332: moved cache related functions to separate file.
|
||||||
* GODT-1332 moving cache does not work on Windows.
|
* GODT-1332 moving cache does not work on Windows.
|
||||||
@ -1151,7 +1326,7 @@ GODT-1537: Manual in-app update mechanism.
|
|||||||
### Fixed
|
### Fixed
|
||||||
* GODT-1029 Fix tray icon not updating under certain conditions.
|
* GODT-1029 Fix tray icon not updating under certain conditions.
|
||||||
* GODT-1062 Fix lost notification bar when window is closed.
|
* GODT-1062 Fix lost notification bar when window is closed.
|
||||||
* GODT-1058 Install version after chaning channel right away only in case of downgrade.
|
* GODT-1058 Install version after changing channel right away only in case of downgrade.
|
||||||
* GODT-1073 Re-write autostart link on every start if turned on in preferences.
|
* GODT-1073 Re-write autostart link on every start if turned on in preferences.
|
||||||
* GODT-1055 Fix flaky empty trash test.
|
* GODT-1055 Fix flaky empty trash test.
|
||||||
|
|
||||||
@ -1241,7 +1416,7 @@ GODT-1537: Manual in-app update mechanism.
|
|||||||
* GODT-820 Added GUI notification on impossibility of update installation (both silent and manual).
|
* GODT-820 Added GUI notification on impossibility of update installation (both silent and manual).
|
||||||
* GODT-870 Added GUI notification on error during silent update.
|
* GODT-870 Added GUI notification on error during silent update.
|
||||||
* GODT-805 Added GUI notification on update available.
|
* GODT-805 Added GUI notification on update available.
|
||||||
* GODT-804 Added GUI notification on silent update installed (promt to restart).
|
* GODT-804 Added GUI notification on silent update installed (prompt to restart).
|
||||||
* GODT-275 Added option to disable autoupdates in settings (default autoupdate is enabled).
|
* GODT-275 Added option to disable autoupdates in settings (default autoupdate is enabled).
|
||||||
* GODT-874 Added manual triggers to Updater module.
|
* GODT-874 Added manual triggers to Updater module.
|
||||||
* GODT-851 Added support of UID EXPUNGE.
|
* GODT-851 Added support of UID EXPUNGE.
|
||||||
@ -1565,7 +1740,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
|
|||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
* GODT-360 Detect charset embedded in html/xml.
|
* GODT-360 Detect charset embedded in html/xml.
|
||||||
* GODT-354 Do not label/unlabel messsages from `All Mail` folder.
|
* GODT-354 Do not label/unlabel messages from `All Mail` folder.
|
||||||
* GODT-388 Support for both bridge and import/export credentials by package users.
|
* GODT-388 Support for both bridge and import/export credentials by package users.
|
||||||
* GODT-387 Store factory to make store optional.
|
* GODT-387 Store factory to make store optional.
|
||||||
* GODT-386 Renamed bridge to general users and keep bridge only for bridge stuff.
|
* GODT-386 Renamed bridge to general users and keep bridge only for bridge stuff.
|
||||||
@ -1730,13 +1905,13 @@ CSB-331 Fix sending error due to mixed case in sender address.
|
|||||||
* GODT-88 Run mbox sync in parallel when switch password mode (re-init not user).
|
* GODT-88 Run mbox sync in parallel when switch password mode (re-init not user).
|
||||||
* GODT-95 Do not throw error when trying to create new mailbox in IMAP root.
|
* GODT-95 Do not throw error when trying to create new mailbox in IMAP root.
|
||||||
* GODT-75 Do not fail on unlabel inside delete.
|
* GODT-75 Do not fail on unlabel inside delete.
|
||||||
* #1095 always delete IMAP USER including wrong pasword.
|
* #1095 always delete IMAP USER including wrong password.
|
||||||
* Unique pmapi client userID (including #1098).
|
* Unique pmapi client userID (including #1098).
|
||||||
* Using go.enmime@v0.6.1 snapshot.
|
* Using go.enmime@v0.6.1 snapshot.
|
||||||
* Better detection of non-auth-error.
|
* Better detection of non-auth-error.
|
||||||
* Reset `hasAuthChannel` during logout for proper login functionality (set up auth channel and unlock keys).
|
* Reset `hasAuthChannel` during logout for proper login functionality (set up auth channel and unlock keys).
|
||||||
* Allow `APPEND` messages without parsable email address in sender field.
|
* Allow `APPEND` messages without parsable email address in sender field.
|
||||||
* #1060 avoid `Append` after internal message ID was found and message was copyed to mailbox using `MessageLabel`.
|
* #1060 avoid `Append` after internal message ID was found and message was copied to mailbox using `MessageLabel`.
|
||||||
* #1049 Basic usage of store in SMTP package to poll event loop during sending message.
|
* #1049 Basic usage of store in SMTP package to poll event loop during sending message.
|
||||||
* #1050 pollNow waits for events to be processed.
|
* #1050 pollNow waits for events to be processed.
|
||||||
* #1047 Fix fetch of empty mailbox.
|
* #1047 Fix fetch of empty mailbox.
|
||||||
@ -1862,7 +2037,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
|
|||||||
* #903 added http.Client timeout to not hang out forever.
|
* #903 added http.Client timeout to not hang out forever.
|
||||||
* Closing body after checking internet connection.
|
* Closing body after checking internet connection.
|
||||||
* Pedantic lint for bridgeUtils.
|
* Pedantic lint for bridgeUtils.
|
||||||
* Selected events are buffered and emited again when frontend loop is ready.
|
* Selected events are buffered and emitted again when frontend loop is ready.
|
||||||
* #890 implemented 2FA endpoint (auth split).
|
* #890 implemented 2FA endpoint (auth split).
|
||||||
* #888 TLS Cert.
|
* #888 TLS Cert.
|
||||||
* Error bar and modal with explanation in GUI.
|
* Error bar and modal with explanation in GUI.
|
||||||
@ -1870,7 +2045,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
|
|||||||
* Add pinning to bridge (only for live API builds).
|
* Add pinning to bridge (only for live API builds).
|
||||||
* #887 #883:
|
* #887 #883:
|
||||||
* Wait before clearing data.
|
* Wait before clearing data.
|
||||||
* Configer which provides pmapi.ClientConfig and app directories.
|
* Configure which provides pmapi.ClientConfig and app directories.
|
||||||
* #861 restart after clear data.
|
* #861 restart after clear data.
|
||||||
* Panic handler for all goroutines.
|
* Panic handler for all goroutines.
|
||||||
* CD for linux.
|
* CD for linux.
|
||||||
@ -1918,7 +2093,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
|
|||||||
* #882 unassign PMAPI client after logout and force to run garbage collector.
|
* #882 unassign PMAPI client after logout and force to run garbage collector.
|
||||||
* #880, #884, #885, #886 fix of informing user about outgoing non-encrypted e-mail.
|
* #880, #884, #885, #886 fix of informing user about outgoing non-encrypted e-mail.
|
||||||
* #838 `Sirupsen` -> `sirupsen`.
|
* #838 `Sirupsen` -> `sirupsen`.
|
||||||
* #893 save panic report file everytime.
|
* #893 save panic report file every time.
|
||||||
* #880 fix of informing user about outgoing non-encrypted e-mail.
|
* #880 fix of informing user about outgoing non-encrypted e-mail.
|
||||||
* Fix aliases in split mode.
|
* Fix aliases in split mode.
|
||||||
* Fix decrypted data in log notification.
|
* Fix decrypted data in log notification.
|
||||||
@ -1992,7 +2167,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
|
|||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
* Fix custom message format.
|
* Fix custom message format.
|
||||||
* #802 acumulated long lines while parsing body structure.
|
* #802 accumulated long lines while parsing body structure.
|
||||||
* Process `AddressEvent` before `MessageEvent`.
|
* Process `AddressEvent` before `MessageEvent`.
|
||||||
* #791 updated crypto: fix wrong signature format.
|
* #791 updated crypto: fix wrong signature format.
|
||||||
* #793 fix returning size.
|
* #793 fix returning size.
|
||||||
@ -2014,7 +2189,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
|
|||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
* #748 when charset missing assume utf8 and check the validity.
|
* #748 when charset missing assume utf8 and check the validity.
|
||||||
* #750 before sync check that events are uptodate, if not poll events instead of sync.
|
* #750 before sync check that events are up-to-date, if not poll events instead of sync.
|
||||||
* Use pmapi with support of decrypted access token.
|
* Use pmapi with support of decrypted access token.
|
||||||
* #750 Status is using DB status instead of API.
|
* #750 Status is using DB status instead of API.
|
||||||
* Format panic error as string instead of struct dump.
|
* Format panic error as string instead of struct dump.
|
||||||
@ -2031,7 +2206,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
|
|||||||
* Full version of program visible on release notes.
|
* Full version of program visible on release notes.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
* #720 only one concurent DB sync.
|
* #720 only one concurrent DB sync.
|
||||||
* #720 sync every 3 pages.
|
* #720 sync every 3 pages.
|
||||||
* #512 extending list of charsets go-pm-mime!4.
|
* #512 extending list of charsets go-pm-mime!4.
|
||||||
|
|
||||||
@ -2055,7 +2230,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
|
|||||||
* Fix srp modulus issue with new `ProtonMail/crypto`.
|
* Fix srp modulus issue with new `ProtonMail/crypto`.
|
||||||
* Generate version files from main file.
|
* Generate version files from main file.
|
||||||
* Be able to set update set on build.
|
* Be able to set update set on build.
|
||||||
* #597 check on start that certificat will be still valid after one month and generate new cert if not.
|
* #597 check on start that certificate will be still valid after one month and generate new cert if not.
|
||||||
* #597 extended certificate validity to 2 years.
|
* #597 extended certificate validity to 2 years.
|
||||||
* Copyright 2019.
|
* Copyright 2019.
|
||||||
* Exclude `protontech` repos from credits.
|
* Exclude `protontech` repos from credits.
|
||||||
@ -2074,7 +2249,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
|
|||||||
* #592 internal references are added only when not present already.
|
* #592 internal references are added only when not present already.
|
||||||
* #592 field `Date` changed to m.Time only when wrong format or missing `Date`.
|
* #592 field `Date` changed to m.Time only when wrong format or missing `Date`.
|
||||||
* #645 pmapi#26 `Message.Flags` instead of `IsEncrypted`, `Type`, `IsReplied`, `IsRepliedAll`, `IsForwarded`.
|
* #645 pmapi#26 `Message.Flags` instead of `IsEncrypted`, `Type`, `IsReplied`, `IsRepliedAll`, `IsForwarded`.
|
||||||
* DB: do not allow to put Body or Attachements to db.
|
* DB: do not allow to put Body or Attachments to db.
|
||||||
* #574 SMTP: can now send more than one email.
|
* #574 SMTP: can now send more than one email.
|
||||||
* #671 Verbosity levels: `debug` (only bridge), `debug-client` (bridge and client communication), `debug-server` (bridge, whole SMTP/IMAP communication).
|
* #671 Verbosity levels: `debug` (only bridge), `debug-client` (bridge and client communication), `debug-server` (bridge, whole SMTP/IMAP communication).
|
||||||
* #644 Return rfc.size 0 or correct size of fetched body (stored in DB).
|
* #644 Return rfc.size 0 or correct size of fetched body (stored in DB).
|
||||||
@ -2146,7 +2321,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
|
|||||||
* Start with new versioning.
|
* Start with new versioning.
|
||||||
|
|
||||||
1.1.0
|
1.1.0
|
||||||
| | `--- bug fix number (internal, irregular, beta relases)
|
| | `--- bug fix number (internal, irregular, beta releases)
|
||||||
| `----- minor version (features, release once per month, live release, milestones)
|
| `----- minor version (features, release once per month, live release, milestones)
|
||||||
`------- major version (big changes, once per year, breaking changes, api force upgrade)
|
`------- major version (big changes, once per year, breaking changes, api force upgrade)
|
||||||
|
|
||||||
@ -2212,7 +2387,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
|
|||||||
* All `client.Do` errors are interpreted as connection issue.
|
* All `client.Do` errors are interpreted as connection issue.
|
||||||
* Moved to internal gitlab.
|
* Moved to internal gitlab.
|
||||||
* Typo `frontend-qml`.
|
* Typo `frontend-qml`.
|
||||||
* Better message for case when server is not reacheable.
|
* Better message for case when server is not reachable.
|
||||||
* Setting 1min timeout to IMAP connection.
|
* Setting 1min timeout to IMAP connection.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
@ -2244,12 +2419,12 @@ CSB-331 Fix sending error due to mixed case in sender address.
|
|||||||
* Keychain format and function refactor.
|
* Keychain format and function refactor.
|
||||||
* Create crash file on panic with full trace.
|
* Create crash file on panic with full trace.
|
||||||
* Clear old data only in main process (no double keychain typing).
|
* Clear old data only in main process (no double keychain typing).
|
||||||
* Create label udpate API route.
|
* Create label update API route.
|
||||||
* Selectable text in release notes.
|
* Selectable text in release notes.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
* Support sending to external PGP recipients.
|
* Support sending to external PGP recipients.
|
||||||
* Return error codes: `0: Ok`, `2: Frontend crashed`, `3: Bridge already running`, `4: Uknown argument`, `42: Restart application`.
|
* Return error codes: `0: Ok`, `2: Frontend crashed`, `3: Bridge already running`, `4: Unknown argument`, `42: Restart application`.
|
||||||
|
|
||||||
### Release notes
|
### Release notes
|
||||||
* Support of encryption to external PGP recipients using contacts created on beta.protonmail.com (see https://protonmail.com/blog/pgp-vulnerability-efail/ to understand the vulnerabilities that may be associated with sending to other PGP clients).
|
* Support of encryption to external PGP recipients using contacts created on beta.protonmail.com (see https://protonmail.com/blog/pgp-vulnerability-efail/ to understand the vulnerabilities that may be associated with sending to other PGP clients).
|
||||||
@ -2274,7 +2449,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
|
|||||||
* Bug report window.
|
* Bug report window.
|
||||||
* Checkbox and with label (only I/E).
|
* Checkbox and with label (only I/E).
|
||||||
* Error dialog and Info tooltip (only I/E).
|
* Error dialog and Info tooltip (only I/E).
|
||||||
* Add user modal formating (colors, text).
|
* Add user modal formatting (colors, text).
|
||||||
* Account view style.
|
* Account view style.
|
||||||
* Input box style (used in bug report).
|
* Input box style (used in bug report).
|
||||||
* Input field style (used in add account and change port).
|
* Input field style (used in add account and change port).
|
||||||
|
|||||||
58
Makefile
58
Makefile
@ -11,7 +11,7 @@ ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
|
|||||||
.PHONY: build build-gui build-nogui build-launcher versioner hasher
|
.PHONY: build build-gui build-nogui build-launcher versioner hasher
|
||||||
|
|
||||||
# Keep version hardcoded so app build works also without Git repository.
|
# Keep version hardcoded so app build works also without Git repository.
|
||||||
BRIDGE_APP_VERSION?=3.3.0+git
|
BRIDGE_APP_VERSION?=3.5.1+git
|
||||||
APP_VERSION:=${BRIDGE_APP_VERSION}
|
APP_VERSION:=${BRIDGE_APP_VERSION}
|
||||||
APP_FULL_NAME:=Proton Mail Bridge
|
APP_FULL_NAME:=Proton Mail Bridge
|
||||||
APP_VENDOR:=Proton AG
|
APP_VENDOR:=Proton AG
|
||||||
@ -229,14 +229,28 @@ add-license:
|
|||||||
change-copyright-year:
|
change-copyright-year:
|
||||||
./utils/missing_license.sh change-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
|
test: gofiles
|
||||||
go test -v -timeout=20m -p=1 -count=1 -coverprofile=/tmp/coverage.out -run=${TESTRUN} ./internal/... ./pkg/...
|
mkdir -p coverage/unit-${GOOS}
|
||||||
|
go test \
|
||||||
|
-v -timeout=20m -p=1 -count=1 \
|
||||||
|
${GOCOVERAGE} \
|
||||||
|
-run=${TESTRUN} ./internal/... ./pkg/... \
|
||||||
|
${GOCOVERDIR}/unit-${GOOS}
|
||||||
|
|
||||||
test-race: gofiles
|
test-race: gofiles
|
||||||
go test -v -timeout=40m -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
|
test-integration: gofiles
|
||||||
go test -v -timeout=60m -p=1 -count=1 github.com/ProtonMail/proton-bridge/v3/tests
|
mkdir -p coverage/integration
|
||||||
|
go test \
|
||||||
|
-v -timeout=60m -p=1 -count=1 -tags=test_integration \
|
||||||
|
${GOCOVERAGE} \
|
||||||
|
github.com/ProtonMail/proton-bridge/v3/tests \
|
||||||
|
${GOCOVERDIR}/integration
|
||||||
|
|
||||||
|
|
||||||
test-integration-debug: gofiles
|
test-integration-debug: gofiles
|
||||||
dlv test github.com/ProtonMail/proton-bridge/v3/tests -- -test.v -test.timeout=10m -test.parallel=1 -test.count=1
|
dlv test github.com/ProtonMail/proton-bridge/v3/tests -- -test.v -test.timeout=10m -test.parallel=1 -test.count=1
|
||||||
@ -244,6 +258,22 @@ test-integration-debug: gofiles
|
|||||||
test-integration-race: gofiles
|
test-integration-race: gofiles
|
||||||
go test -v -timeout=60m -p=1 -count=1 -race -failfast github.com/ProtonMail/proton-bridge/v3/tests
|
go test -v -timeout=60m -p=1 -count=1 -race -failfast github.com/ProtonMail/proton-bridge/v3/tests
|
||||||
|
|
||||||
|
test-integration-nightly: gofiles
|
||||||
|
mkdir -p coverage/integration
|
||||||
|
go test \
|
||||||
|
-v -timeout=90m -p=1 -count=1 -tags=test_integration \
|
||||||
|
${GOCOVERAGE} \
|
||||||
|
github.com/ProtonMail/proton-bridge/v3/tests \
|
||||||
|
${GOCOVERDIR}/integration \
|
||||||
|
nightly
|
||||||
|
|
||||||
|
fuzz: gofiles
|
||||||
|
go test -fuzz=FuzzUnmarshal -parallel=4 -fuzztime=60s $(PWD)/internal/legacy/credentials
|
||||||
|
go test -fuzz=FuzzNewParser -parallel=4 -fuzztime=60s $(PWD)/pkg/message/parser
|
||||||
|
go test -fuzz=FuzzReadHeaderBody -parallel=4 -fuzztime=60s $(PWD)/pkg/message
|
||||||
|
go test -fuzz=FuzzDecodeHeader -parallel=4 -fuzztime=60s $(PWD)/pkg/mime
|
||||||
|
go test -fuzz=FuzzDecodeCharset -parallel=4 -fuzztime=60s $(PWD)/pkg/mime
|
||||||
|
|
||||||
bench:
|
bench:
|
||||||
go test -run '^$$' -bench=. -memprofile bench_mem.pprof -cpuprofile bench_cpu.pprof ./internal/store
|
go test -run '^$$' -bench=. -memprofile bench_mem.pprof -cpuprofile bench_cpu.pprof ./internal/store
|
||||||
go tool pprof -png -output bench_mem.png bench_mem.pprof
|
go tool pprof -png -output bench_mem.png bench_mem.pprof
|
||||||
@ -260,8 +290,22 @@ mocks:
|
|||||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/updater Downloader,Installer > internal/updater/mocks/mocks.go
|
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/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
|
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
|
cp internal/telemetry/mocks/mocks.go internal/bridge/mocks/telemetry_mocks.go
|
||||||
|
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/services/userevents \
|
||||||
|
EventSource,EventIDStore > internal/services/userevents/mocks/mocks.go
|
||||||
|
mockgen --package userevents github.com/ProtonMail/proton-bridge/v3/internal/services/userevents \
|
||||||
|
EventSubscriber,MessageEventHandler,LabelEventHandler,AddressEventHandler,RefreshEventHandler,UserEventHandler,UserUsedSpaceEventHandler > tmp
|
||||||
|
mv tmp internal/services/userevents/mocks_test.go
|
||||||
|
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/events EventPublisher \
|
||||||
|
> internal/events/mocks/mocks.go
|
||||||
|
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/services/useridentity IdentityProvider,Telemetry \
|
||||||
|
> internal/services/useridentity/mocks/mocks.go
|
||||||
|
mockgen --self_package "github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice" -package syncservice github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice \
|
||||||
|
ApplyStageInput,BuildStageInput,BuildStageOutput,DownloadStageInput,DownloadStageOutput,MetadataStageInput,MetadataStageOutput,\
|
||||||
|
StateProvider,Regulator,UpdateApplier,MessageBuilder,APIClient,Reporter,DownloadRateModifier \
|
||||||
|
> tmp
|
||||||
|
mv tmp internal/services/syncservice/mocks_test.go
|
||||||
|
|
||||||
lint: gofiles lint-golang lint-license lint-dependencies lint-changelog
|
lint: gofiles lint-golang lint-license lint-dependencies lint-changelog lint-bug-report
|
||||||
|
|
||||||
lint-license:
|
lint-license:
|
||||||
./utils/missing_license.sh check
|
./utils/missing_license.sh check
|
||||||
@ -277,6 +321,12 @@ lint-golang:
|
|||||||
$(info linting with GOMAXPROCS=${GOMAXPROCS})
|
$(info linting with GOMAXPROCS=${GOMAXPROCS})
|
||||||
golangci-lint run ./...
|
golangci-lint run ./...
|
||||||
|
|
||||||
|
lint-bug-report:
|
||||||
|
python3 utils/validate_bug_report_file.py --file "internal/frontend/bridge-gui/bridge-gui/qml/Resources/bug_report_flow.json"
|
||||||
|
|
||||||
|
lint-bug-report-preview:
|
||||||
|
python3 utils/validate_bug_report_file.py --file "internal/frontend/bridge-gui/bridge-gui/qml/Resources/bug_report_flow.json" --preview
|
||||||
|
|
||||||
gobinsec: gobinsec-cache.yml build
|
gobinsec: gobinsec-cache.yml build
|
||||||
gobinsec -wait -cache -config utils/gobinsec_conf.yml ${EXE_TARGET} ${DEPLOY_DIR}/${TARGET_OS}/${LAUNCHER_EXE}
|
gobinsec -wait -cache -config utils/gobinsec_conf.yml ${EXE_TARGET} ${DEPLOY_DIR}/${TARGET_OS}/${LAUNCHER_EXE}
|
||||||
|
|
||||||
|
|||||||
@ -18,6 +18,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
@ -43,9 +44,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
appName = "Proton Mail Launcher"
|
appName = "Proton Mail Launcher"
|
||||||
exeName = "bridge"
|
exeName = "bridge"
|
||||||
guiName = "bridge-gui"
|
guiName = "bridge-gui"
|
||||||
|
launcherName = "launcher"
|
||||||
|
|
||||||
FlagCLI = "cli"
|
FlagCLI = "cli"
|
||||||
FlagCLIShort = "c"
|
FlagCLIShort = "c"
|
||||||
@ -53,6 +55,7 @@ const (
|
|||||||
FlagNonInteractiveShort = "n"
|
FlagNonInteractiveShort = "n"
|
||||||
FlagLauncher = "--launcher"
|
FlagLauncher = "--launcher"
|
||||||
FlagWait = "--wait"
|
FlagWait = "--wait"
|
||||||
|
FlagSessionID = "--session-id"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() { //nolint:funlen
|
func main() { //nolint:funlen
|
||||||
@ -75,12 +78,26 @@ func main() { //nolint:funlen
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
l.WithError(err).Fatal("Failed to get logs path")
|
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")
|
l.WithError(err).Fatal("Failed to setup logging")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
_ = logging.Close(closer)
|
||||||
|
}()
|
||||||
|
|
||||||
updatesPath, err := locations.ProvideUpdatesPath()
|
updatesPath, err := locations.ProvideUpdatesPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.WithError(err).Fatal("Failed to get updates path")
|
l.WithError(err).Fatal("Failed to get updates path")
|
||||||
@ -134,7 +151,7 @@ func main() { //nolint:funlen
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := execabs.Command(exe, appendLauncherPath(launcher, args)...) //nolint:gosec
|
cmd := execabs.Command(exe, appendLauncherPath(launcher, append(args, FlagSessionID, string(sessionID)))...) //nolint:gosec
|
||||||
|
|
||||||
cmd.Stdin = os.Stdin
|
cmd.Stdin = os.Stdin
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
|
|||||||
@ -2,13 +2,13 @@
|
|||||||
|
|
||||||
## First login and sync
|
## First login and sync
|
||||||
|
|
||||||
When user logs in to the bridge for the first time, immediatelly starts the first sync.
|
When user logs in to the bridge for the first time, immediately starts the first sync.
|
||||||
First sync downloads all headers of all e-mails and creates database to have proper UIDs
|
First sync downloads all headers of all e-mails and creates database to have proper UIDs
|
||||||
and indexes for IMAP. See [database](database.md) for more information.
|
and indexes for IMAP. See [database](database.md) for more information.
|
||||||
|
|
||||||
By default, whenever it's possible, sync downloads only all e-mails maiblox which already
|
By default, whenever it's possible, sync downloads only all e-mails maiblox which already
|
||||||
have list of labels so we can construct all mailboxes (inbox, sent, trash, custom folders
|
have list of labels so we can construct all mailboxes (inbox, sent, trash, custom folders
|
||||||
and lables) without need to download each e-mail headers many times.
|
and labels) without need to download each e-mail headers many times.
|
||||||
|
|
||||||
Note that we need to download also bodies to calculate size of the e-mail and set proper
|
Note that we need to download also bodies to calculate size of the e-mail and set proper
|
||||||
content type (clients uses content type for guess if e-mail contains attachment)--but only
|
content type (clients uses content type for guess if e-mail contains attachment)--but only
|
||||||
@ -22,7 +22,7 @@ client right after adding account.
|
|||||||
|
|
||||||
When account is added to client, client start the sync. This sync will ask Bridge app
|
When account is added to client, client start the sync. This sync will ask Bridge app
|
||||||
for all headers (done quickly) and then starts to download all bodies and attachment.
|
for all headers (done quickly) and then starts to download all bodies and attachment.
|
||||||
Unfortunatelly for some e-mail more than once if the same e-mail is in more mailboxes
|
Unfortunately for some e-mail more than once if the same e-mail is in more mailboxes
|
||||||
(e.g. inbox and all mail)--there is no way to tell over IMAP it's the same message.
|
(e.g. inbox and all mail)--there is no way to tell over IMAP it's the same message.
|
||||||
|
|
||||||
After successful login of client to IMAP, Bridge starts event loop. That periodicly ask
|
After successful login of client to IMAP, Bridge starts event loop. That periodicly ask
|
||||||
@ -37,7 +37,7 @@ sequenceDiagram
|
|||||||
Note right of B: Set up PM account<br/>by user
|
Note right of B: Set up PM account<br/>by user
|
||||||
|
|
||||||
loop First sync
|
loop First sync
|
||||||
B ->> S: Fetch body and attachements
|
B ->> S: Fetch body and attachments
|
||||||
Note right of B: Build local database<br/>(e-mail UIDs)
|
Note right of B: Build local database<br/>(e-mail UIDs)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -58,8 +58,8 @@ sequenceDiagram
|
|||||||
C ->> B: IMAP SELECT directory
|
C ->> B: IMAP SELECT directory
|
||||||
C ->> B: IMAP SEARCH e-mails UIDs
|
C ->> B: IMAP SEARCH e-mails UIDs
|
||||||
C ->> B: IMAP FETCH of e-mail UID
|
C ->> B: IMAP FETCH of e-mail UID
|
||||||
B ->> S: Fetch body and attachements
|
B ->> S: Fetch body and attachments
|
||||||
Note right of B: Decrypt message<br/>and attachement
|
Note right of B: Decrypt message<br/>and attachment
|
||||||
B ->> C: IMAP response
|
B ->> C: IMAP response
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
# Update mechanism of Bridge
|
# Update mechanism of Bridge
|
||||||
|
|
||||||
There are mulitple options how to change version of application:
|
There are multiple options how to change version of application:
|
||||||
* Automatic in-app update
|
* Automatic in-app update
|
||||||
* Manual in-app update
|
* Manual in-app update
|
||||||
* Manual install
|
* Manual install
|
||||||
|
|
||||||
In-app update ends with restarting bridge into new version. Automatic in-app
|
In-app update ends with restarting bridge into new version. Automatic in-app
|
||||||
update is downloading, verifying and installing the new version immediatelly
|
update is downloading, verifying and installing the new version immediately
|
||||||
without user confirmation. For manual in-app update user needs to confirm first.
|
without user confirmation. For manual in-app update user needs to confirm first.
|
||||||
Update is done from special update file published on website.
|
Update is done from special update file published on website.
|
||||||
|
|
||||||
@ -25,7 +25,7 @@ The bridge is installed and executed differently for given OS:
|
|||||||
|
|
||||||
* macOS app does not use launcher
|
* macOS app does not use launcher
|
||||||
* No launcher, only one executable
|
* No launcher, only one executable
|
||||||
* In-App udpate replaces the bridge files in installation path directly
|
* In-App update replaces the bridge files in installation path directly
|
||||||
|
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
|
|||||||
14
go.mod
14
go.mod
@ -5,9 +5,9 @@ go 1.20
|
|||||||
require (
|
require (
|
||||||
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
|
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
|
||||||
github.com/Masterminds/semver/v3 v3.2.0
|
github.com/Masterminds/semver/v3 v3.2.0
|
||||||
github.com/ProtonMail/gluon v0.16.1-0.20230607122549-dbdb8e1cc0c3
|
github.com/ProtonMail/gluon v0.17.1-0.20230829112217-5d5c25c504b5
|
||||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
|
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
|
||||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20230605082423-67859aec0317
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20230831064234-0e3a549b3f36
|
||||||
github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton
|
github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton
|
||||||
github.com/PuerkitoBio/goquery v1.8.1
|
github.com/PuerkitoBio/goquery v1.8.1
|
||||||
github.com/abiosoft/ishell v2.0.0+incompatible
|
github.com/abiosoft/ishell v2.0.0+incompatible
|
||||||
@ -51,16 +51,12 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
ariga.io/atlas v0.9.1-0.20230119145809-92243f7c55cb // indirect
|
|
||||||
entgo.io/ent v0.11.8 // indirect
|
|
||||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf // indirect
|
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf // indirect
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903 // 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-mime v0.0.0-20230322103455-7d82a3887f2f // indirect
|
||||||
github.com/ProtonMail/go-srp v0.0.7 // indirect
|
github.com/ProtonMail/go-srp v0.0.7 // indirect
|
||||||
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect
|
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect
|
||||||
github.com/agext/levenshtein v1.2.3 // indirect
|
|
||||||
github.com/andybalholm/cascadia v1.3.2 // indirect
|
github.com/andybalholm/cascadia v1.3.2 // indirect
|
||||||
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
|
|
||||||
github.com/bytedance/sonic v1.9.1 // indirect
|
github.com/bytedance/sonic v1.9.1 // indirect
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||||
github.com/chzyer/test v1.0.0 // indirect
|
github.com/chzyer/test v1.0.0 // indirect
|
||||||
@ -78,7 +74,6 @@ require (
|
|||||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
github.com/gin-gonic/gin v1.9.1 // indirect
|
github.com/gin-gonic/gin v1.9.1 // indirect
|
||||||
github.com/go-openapi/inflect v0.19.0 // indirect
|
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
||||||
@ -90,7 +85,6 @@ require (
|
|||||||
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
||||||
github.com/hashicorp/go-memdb v1.3.3 // indirect
|
github.com/hashicorp/go-memdb v1.3.3 // indirect
|
||||||
github.com/hashicorp/golang-lru v0.5.4 // 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/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
||||||
@ -98,8 +92,7 @@ require (
|
|||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||||
github.com/mattn/go-sqlite3 v1.14.16 // indirect
|
github.com/mattn/go-sqlite3 v1.14.17 // indirect
|
||||||
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||||
@ -115,7 +108,6 @@ require (
|
|||||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||||
github.com/zclconf/go-cty v1.12.1 // indirect
|
|
||||||
golang.org/x/arch v0.3.0 // indirect
|
golang.org/x/arch v0.3.0 // indirect
|
||||||
golang.org/x/crypto v0.9.0 // indirect
|
golang.org/x/crypto v0.9.0 // indirect
|
||||||
golang.org/x/mod v0.8.0 // indirect
|
golang.org/x/mod v0.8.0 // indirect
|
||||||
|
|||||||
34
go.sum
34
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.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.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
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/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
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=
|
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 h1:l6surSnJ3RP4qA1qmKJ+hQn3UjytosdoG27WGjrDlVs=
|
||||||
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557/go.mod h1:sTrmvD/TxuypdOERsDOS7SndZg0rzzcCi1b6wQMXUYM=
|
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/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/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 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
|
||||||
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||||
@ -28,10 +23,8 @@ github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs
|
|||||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
|
github.com/ProtonMail/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 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
|
||||||
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
|
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
|
||||||
github.com/ProtonMail/gluon v0.16.1-0.20230607083802-83f92429ca8d h1:+1BKm++zxmfGwj81q3jFkiDpgVwg529qznGbI//uXpk=
|
github.com/ProtonMail/gluon v0.17.1-0.20230829112217-5d5c25c504b5 h1:C/8P5NHAKi2yCKez+OZ5rSR8SsL7k8si4pK4SE2QtV8=
|
||||||
github.com/ProtonMail/gluon v0.16.1-0.20230607083802-83f92429ca8d/go.mod h1:ERZikuN+2i/oTeSwS5fq7J0Fms76uUcBlTAwT4KaEAk=
|
github.com/ProtonMail/gluon v0.17.1-0.20230829112217-5d5c25c504b5/go.mod h1:Og5/Dz1MiGpCJn51XujZwxiLG7WzvvjE5PRpZBQmAHo=
|
||||||
github.com/ProtonMail/gluon v0.16.1-0.20230607122549-dbdb8e1cc0c3 h1:VMbbJD3dcGPPIgbdQTS5Z4nX0QU/SsVZWdmsMVVBBsI=
|
|
||||||
github.com/ProtonMail/gluon v0.16.1-0.20230607122549-dbdb8e1cc0c3/go.mod h1:ERZikuN+2i/oTeSwS5fq7J0Fms76uUcBlTAwT4KaEAk=
|
|
||||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
|
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-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
|
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
|
||||||
@ -42,8 +35,8 @@ github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7 h1:+j+Kd/
|
|||||||
github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
|
github.com/ProtonMail/go-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 h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
|
||||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
|
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
|
||||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20230605082423-67859aec0317 h1:Mo293UO7CfX1mL+jrBoSrftvhEyq6hyRDia2d6+Kx9U=
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20230831064234-0e3a549b3f36 h1:JVMK2w90bCWayUCXJIb3wkQ5+j2P/NbnrX3BrDoLzsc=
|
||||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20230605082423-67859aec0317/go.mod h1:+aTJoYu8bqzGECXL2DOdiZTZ64bGn3w0NC8VcFpJrFM=
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20230831064234-0e3a549b3f36/go.mod h1:nS8hMGjJLgC0Iej0JMYbsI388LesEkM1Hj/jCCxQeaQ=
|
||||||
github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
|
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/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 h1:YS6M20yvjCJPR1r4ADW5TPn6rahs4iAyZaACei86bEc=
|
||||||
@ -54,8 +47,6 @@ github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma
|
|||||||
github.com/abiosoft/ishell v2.0.0+incompatible/go.mod h1:HQR9AqF2R3P4XXpMpI0NAzgHf/aS6+zVXRj14cVk9qg=
|
github.com/abiosoft/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 h1:CjPUSXOiYptLbTdr1RceuZgSFDQ7U15ITERUGrUORx8=
|
||||||
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db/go.mod h1:rB3B4rKii8V21ydCbIzH5hZiCQE7f5E9SzUb/ZZx530=
|
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/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/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 h1:28uU3TtuvQ6KRndxg9TrC868jBWmSKgh0GTXkACCXmA=
|
||||||
@ -63,8 +54,6 @@ github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37/go.m
|
|||||||
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
|
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
|
||||||
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
|
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/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
||||||
github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw=
|
|
||||||
github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
|
|
||||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
github.com/armon/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-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=
|
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||||
@ -159,8 +148,6 @@ github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9
|
|||||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
github.com/go-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.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
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/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 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
@ -171,7 +158,6 @@ github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QX
|
|||||||
github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
|
github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
|
||||||
github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
|
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-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
|
|
||||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
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 h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4=
|
||||||
@ -248,8 +234,6 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
|
|||||||
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
|
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
|
||||||
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
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 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/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/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
||||||
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
||||||
@ -282,7 +266,6 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
|||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/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.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4=
|
|
||||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
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/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
@ -299,8 +282,8 @@ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
|
|||||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
github.com/mattn/go-runewidth v0.0.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 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
||||||
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
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.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
||||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
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/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.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||||
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
|
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
|
||||||
@ -309,8 +292,6 @@ github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceT
|
|||||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
github.com/mitchellh/go-homedir v1.1.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-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/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
|
||||||
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
||||||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
@ -365,7 +346,6 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
|
|||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/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/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/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/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.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y=
|
github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y=
|
||||||
@ -419,8 +399,6 @@ github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRT
|
|||||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
github.com/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.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
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.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.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||||
|
|||||||
@ -19,6 +19,7 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/cookiejar"
|
"net/http/cookiejar"
|
||||||
"net/url"
|
"net/url"
|
||||||
@ -51,6 +52,9 @@ const (
|
|||||||
flagCPUProfile = "cpu-prof"
|
flagCPUProfile = "cpu-prof"
|
||||||
flagCPUProfileShort = "p"
|
flagCPUProfileShort = "p"
|
||||||
|
|
||||||
|
flagTraceProfile = "trace-prof"
|
||||||
|
flagTraceProfileShort = "t"
|
||||||
|
|
||||||
flagMemProfile = "mem-prof"
|
flagMemProfile = "mem-prof"
|
||||||
flagMemProfileShort = "m"
|
flagMemProfileShort = "m"
|
||||||
|
|
||||||
@ -76,10 +80,12 @@ const (
|
|||||||
flagNoWindow = "no-window"
|
flagNoWindow = "no-window"
|
||||||
flagParentPID = "parent-pid"
|
flagParentPID = "parent-pid"
|
||||||
flagSoftwareRenderer = "software-renderer"
|
flagSoftwareRenderer = "software-renderer"
|
||||||
|
flagSessionID = "session-id"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
appUsage = "Proton Mail IMAP and SMTP Bridge"
|
appUsage = "Proton Mail IMAP and SMTP Bridge"
|
||||||
|
appShortName = "bridge"
|
||||||
)
|
)
|
||||||
|
|
||||||
func New() *cli.App {
|
func New() *cli.App {
|
||||||
@ -93,6 +99,11 @@ func New() *cli.App {
|
|||||||
Aliases: []string{flagCPUProfileShort},
|
Aliases: []string{flagCPUProfileShort},
|
||||||
Usage: "Generate CPU profile",
|
Usage: "Generate CPU profile",
|
||||||
},
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: flagTraceProfile,
|
||||||
|
Aliases: []string{flagTraceProfileShort},
|
||||||
|
Usage: "Generate Trace profile",
|
||||||
|
},
|
||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
Name: flagMemProfile,
|
Name: flagMemProfile,
|
||||||
Aliases: []string{flagMemProfileShort},
|
Aliases: []string{flagMemProfileShort},
|
||||||
@ -150,6 +161,10 @@ func New() *cli.App {
|
|||||||
Hidden: true,
|
Hidden: true,
|
||||||
Value: false,
|
Value: false,
|
||||||
},
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: flagSessionID,
|
||||||
|
Hidden: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
app.Action = run
|
app.Action = run
|
||||||
@ -183,6 +198,11 @@ func run(c *cli.Context) error {
|
|||||||
exe = os.Args[0]
|
exe = os.Args[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var logCloser io.Closer
|
||||||
|
defer func() {
|
||||||
|
_ = logging.Close(logCloser)
|
||||||
|
}()
|
||||||
|
|
||||||
// Restart the app if requested.
|
// Restart the app if requested.
|
||||||
return withRestarter(exe, func(restarter *restarter.Restarter) error {
|
return withRestarter(exe, func(restarter *restarter.Restarter) error {
|
||||||
// Handle crashes with various actions.
|
// Handle crashes with various actions.
|
||||||
@ -199,7 +219,9 @@ func run(c *cli.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize logging.
|
// 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 there was an error during migration, log it now.
|
||||||
if migrationErr != nil {
|
if migrationErr != nil {
|
||||||
logrus.WithError(migrationErr).Error("Failed to migrate old app data")
|
logrus.WithError(migrationErr).Error("Failed to migrate old app data")
|
||||||
@ -298,7 +320,7 @@ func withSingleInstance(settingPath, lockFile string, version *semver.Version, f
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize our logging system.
|
// 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")
|
logrus.Debug("Initializing logging")
|
||||||
defer logrus.Debug("Logging stopped")
|
defer logrus.Debug("Logging stopped")
|
||||||
|
|
||||||
@ -311,12 +333,21 @@ func withLogging(c *cli.Context, crashHandler *crash.Handler, locations *locatio
|
|||||||
logrus.WithField("path", logsPath).Debug("Received logs path")
|
logrus.WithField("path", logsPath).Debug("Received logs path")
|
||||||
|
|
||||||
// Initialize logging.
|
// 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)
|
return fmt.Errorf("could not initialize logging: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure we dump a stack trace if we crash.
|
// Ensure we dump a stack trace if we crash.
|
||||||
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath))
|
crashHandler.AddRecoveryAction(logging.DumpStackTrace(logsPath, sessionID, appShortName))
|
||||||
|
|
||||||
logrus.
|
logrus.
|
||||||
WithField("appName", constants.FullAppName).
|
WithField("appName", constants.FullAppName).
|
||||||
@ -329,7 +360,7 @@ func withLogging(c *cli.Context, crashHandler *crash.Handler, locations *locatio
|
|||||||
WithField("SentryID", sentry.GetProtectedHostname()).
|
WithField("SentryID", sentry.GetProtectedHostname()).
|
||||||
Info("Run app")
|
Info("Run app")
|
||||||
|
|
||||||
return fn()
|
return fn(closer)
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithLocations provides access to locations where we store our files.
|
// WithLocations provides access to locations where we store our files.
|
||||||
@ -356,6 +387,11 @@ func withProfiler(c *cli.Context, fn func() error) error {
|
|||||||
defer profile.Start(profile.CPUProfile, profile.ProfilePath(".")).Stop()
|
defer profile.Start(profile.CPUProfile, profile.ProfilePath(".")).Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.Bool(flagTraceProfile) {
|
||||||
|
logrus.Debug("Running with Trace profiling")
|
||||||
|
defer profile.Start(profile.TraceProfile, profile.ProfilePath(".")).Stop()
|
||||||
|
}
|
||||||
|
|
||||||
if c.Bool(flagMemProfile) {
|
if c.Bool(flagMemProfile) {
|
||||||
logrus.Debug("Running with memory profiling")
|
logrus.Debug("Running with memory profiling")
|
||||||
defer profile.Start(profile.MemProfile, profile.MemProfileAllocs, profile.ProfilePath(".")).Stop()
|
defer profile.Start(profile.MemProfile, profile.MemProfileAllocs, profile.ProfilePath(".")).Stop()
|
||||||
|
|||||||
@ -44,7 +44,7 @@ import (
|
|||||||
// deleteOldGoIMAPFiles Set with `-ldflags -X app.deleteOldGoIMAPFiles=true` to enable cleanup of old imap cache data.
|
// deleteOldGoIMAPFiles Set with `-ldflags -X app.deleteOldGoIMAPFiles=true` to enable cleanup of old imap cache data.
|
||||||
var deleteOldGoIMAPFiles bool //nolint:gochecknoglobals
|
var deleteOldGoIMAPFiles bool //nolint:gochecknoglobals
|
||||||
|
|
||||||
// withBridge creates creates and tears down the bridge.
|
// withBridge creates and tears down the bridge.
|
||||||
func withBridge(
|
func withBridge(
|
||||||
c *cli.Context,
|
c *cli.Context,
|
||||||
exe string,
|
exe string,
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// 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/>.
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
//go:build !build_qa
|
//go:build !build_qa && !test_integration
|
||||||
|
|
||||||
package bridge
|
package bridge
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// 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/>.
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
//go:build build_qa
|
//go:build build_qa || test_integration
|
||||||
|
|
||||||
package bridge
|
package bridge
|
||||||
|
|
||||||
|
|||||||
@ -23,7 +23,6 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@ -38,8 +37,11 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/focus"
|
"github.com/ProtonMail/proton-bridge/v3/internal/focus"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/identifier"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/sentry"
|
"github.com/ProtonMail/proton-bridge/v3/internal/sentry"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapsmtpserver"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/telemetry"
|
"github.com/ProtonMail/proton-bridge/v3/internal/telemetry"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||||
@ -59,7 +61,7 @@ type Bridge struct {
|
|||||||
// api manages user API clients.
|
// api manages user API clients.
|
||||||
api *proton.Manager
|
api *proton.Manager
|
||||||
proxyCtl ProxyController
|
proxyCtl ProxyController
|
||||||
identifier Identifier
|
identifier identifier.Identifier
|
||||||
|
|
||||||
// tlsConfig holds the bridge TLS config used by the IMAP and SMTP servers.
|
// tlsConfig holds the bridge TLS config used by the IMAP and SMTP servers.
|
||||||
tlsConfig *tls.Config
|
tlsConfig *tls.Config
|
||||||
@ -125,9 +127,8 @@ type Bridge struct {
|
|||||||
// goHeartbeat triggers a check/sending if heartbeat is needed.
|
// goHeartbeat triggers a check/sending if heartbeat is needed.
|
||||||
goHeartbeat func()
|
goHeartbeat func()
|
||||||
|
|
||||||
uidValidityGenerator imap.UIDValidityGenerator
|
serverManager *imapsmtpserver.Service
|
||||||
|
syncService *syncservice.Service
|
||||||
serverManager *ServerManager
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new bridge.
|
// New creates a new bridge.
|
||||||
@ -140,7 +141,7 @@ func New(
|
|||||||
|
|
||||||
apiURL string, // the URL of the API to use
|
apiURL string, // the URL of the API to use
|
||||||
cookieJar http.CookieJar, // the cookie jar to use
|
cookieJar http.CookieJar, // the cookie jar to use
|
||||||
identifier Identifier, // the identifier to keep track of the user agent
|
identifier identifier.Identifier, // the identifier to keep track of the user agent
|
||||||
tlsReporter TLSReporter, // the TLS reporter to report TLS errors
|
tlsReporter TLSReporter, // the TLS reporter to report TLS errors
|
||||||
roundTripper http.RoundTripper, // the round tripper to use for API requests
|
roundTripper http.RoundTripper, // the round tripper to use for API requests
|
||||||
proxyCtl ProxyController, // the DoH controller
|
proxyCtl ProxyController, // the DoH controller
|
||||||
@ -207,7 +208,7 @@ func newBridge(
|
|||||||
reporter reporter.Reporter,
|
reporter reporter.Reporter,
|
||||||
|
|
||||||
api *proton.Manager,
|
api *proton.Manager,
|
||||||
identifier Identifier,
|
identifier identifier.Identifier,
|
||||||
proxyCtl ProxyController,
|
proxyCtl ProxyController,
|
||||||
uidValidityGenerator imap.UIDValidityGenerator,
|
uidValidityGenerator imap.UIDValidityGenerator,
|
||||||
|
|
||||||
@ -269,17 +270,26 @@ func newBridge(
|
|||||||
firstStart: firstStart,
|
firstStart: firstStart,
|
||||||
lastVersion: lastVersion,
|
lastVersion: lastVersion,
|
||||||
|
|
||||||
tasks: tasks,
|
tasks: tasks,
|
||||||
|
syncService: syncservice.NewService(reporter, panicHandler),
|
||||||
uidValidityGenerator: uidValidityGenerator,
|
|
||||||
|
|
||||||
serverManager: newServerManager(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := bridge.serverManager.Init(bridge); err != nil {
|
bridge.serverManager = imapsmtpserver.NewService(context.Background(),
|
||||||
|
&bridgeSMTPSettings{b: bridge},
|
||||||
|
&bridgeIMAPSettings{b: bridge},
|
||||||
|
&bridgeEventPublisher{b: bridge},
|
||||||
|
panicHandler,
|
||||||
|
reporter,
|
||||||
|
uidValidityGenerator,
|
||||||
|
&bridgeIMAPSMTPTelemetry{b: bridge},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := bridge.serverManager.Init(context.Background(), bridge.tasks, &bridgeEventSubscription{b: bridge}); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bridge.syncService.Run(bridge.tasks)
|
||||||
|
|
||||||
return bridge, nil
|
return bridge, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -407,11 +417,6 @@ func (bridge *Bridge) GetErrors() []error {
|
|||||||
func (bridge *Bridge) Close(ctx context.Context) {
|
func (bridge *Bridge) Close(ctx context.Context) {
|
||||||
logrus.Info("Closing bridge")
|
logrus.Info("Closing bridge")
|
||||||
|
|
||||||
// Close the servers
|
|
||||||
if err := bridge.serverManager.CloseServers(ctx); err != nil {
|
|
||||||
logrus.WithError(err).Error("Failed to close servers")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close all users.
|
// Close all users.
|
||||||
safe.Lock(func() {
|
safe.Lock(func() {
|
||||||
for _, user := range bridge.users {
|
for _, user := range bridge.users {
|
||||||
@ -419,6 +424,11 @@ func (bridge *Bridge) Close(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
}, bridge.usersLock)
|
}, bridge.usersLock)
|
||||||
|
|
||||||
|
// Close the servers
|
||||||
|
if err := bridge.serverManager.CloseServers(ctx); err != nil {
|
||||||
|
logrus.WithError(err).Error("Failed to close servers")
|
||||||
|
}
|
||||||
|
|
||||||
// Stop all ongoing tasks.
|
// Stop all ongoing tasks.
|
||||||
bridge.tasks.CancelAndWait()
|
bridge.tasks.CancelAndWait()
|
||||||
|
|
||||||
@ -527,24 +537,6 @@ func loadTLSConfig(vault *vault.Vault) (*tls.Config, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newListener(port int, useTLS bool, tlsConfig *tls.Config) (net.Listener, error) {
|
|
||||||
if useTLS {
|
|
||||||
tlsListener, err := tls.Listen("tcp", fmt.Sprintf("%v:%v", constants.Host, port), tlsConfig)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return tlsListener, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
netListener, err := net.Listen("tcp", fmt.Sprintf("%v:%v", constants.Host, port))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return netListener, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func min(a, b time.Duration) time.Duration {
|
func min(a, b time.Duration) time.Duration {
|
||||||
if a < b {
|
if a < b {
|
||||||
return a
|
return a
|
||||||
|
|||||||
@ -25,6 +25,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@ -43,6 +44,7 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/focus"
|
"github.com/ProtonMail/proton-bridge/v3/internal/focus"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
|
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapsmtpserver"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
|
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
|
||||||
@ -53,6 +55,7 @@ import (
|
|||||||
"github.com/emersion/go-sasl"
|
"github.com/emersion/go-sasl"
|
||||||
"github.com/emersion/go-smtp"
|
"github.com/emersion/go-smtp"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.uber.org/goleak"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -300,8 +303,11 @@ func TestBridge_UserAgentFromSMTPClient(t *testing.T) {
|
|||||||
string(info.BridgePass)),
|
string(info.BridgePass)),
|
||||||
))
|
))
|
||||||
|
|
||||||
currentUserAgent = b.GetCurrentUserAgent()
|
require.Eventually(t, func() bool {
|
||||||
require.Contains(t, currentUserAgent, "UnknownClient/0.0.1")
|
currentUserAgent = b.GetCurrentUserAgent()
|
||||||
|
|
||||||
|
return strings.Contains(currentUserAgent, "UnknownClient/0.0.1")
|
||||||
|
}, time.Minute, 5*time.Second)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -617,6 +623,10 @@ func TestBridge_AddressWithoutKeys(t *testing.T) {
|
|||||||
defer m.Close()
|
defer m.Close()
|
||||||
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
|
// Watch for sync finished event.
|
||||||
|
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
||||||
|
defer done()
|
||||||
|
|
||||||
// Create a user which will have an address without keys.
|
// Create a user which will have an address without keys.
|
||||||
userID, _, err := s.CreateUser("nokeys", []byte("password"))
|
userID, _, err := s.CreateUser("nokeys", []byte("password"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -637,10 +647,6 @@ func TestBridge_AddressWithoutKeys(t *testing.T) {
|
|||||||
// Remove the address keys.
|
// Remove the address keys.
|
||||||
require.NoError(t, s.RemoveAddressKey(userID, aliasAddrID, aliasAddr.Keys[0].ID))
|
require.NoError(t, s.RemoveAddressKey(userID, aliasAddrID, aliasAddr.Keys[0].ID))
|
||||||
|
|
||||||
// Watch for sync finished event.
|
|
||||||
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
|
||||||
defer done()
|
|
||||||
|
|
||||||
// We should be able to log the user in.
|
// We should be able to log the user in.
|
||||||
require.NoError(t, getErr(bridge.LoginFull(context.Background(), "nokeys", []byte("password"), nil, nil)))
|
require.NoError(t, getErr(bridge.LoginFull(context.Background(), "nokeys", []byte("password"), nil, nil)))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -696,10 +702,10 @@ func TestBridge_InitGluonDirectory(t *testing.T) {
|
|||||||
configDir, err := b.GetGluonDataDir()
|
configDir, err := b.GetGluonDataDir()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
_, err = os.ReadDir(bridge.ApplyGluonCachePathSuffix(b.GetGluonCacheDir()))
|
_, err = os.ReadDir(imapsmtpserver.ApplyGluonCachePathSuffix(b.GetGluonCacheDir()))
|
||||||
require.False(t, os.IsNotExist(err))
|
require.False(t, os.IsNotExist(err))
|
||||||
|
|
||||||
_, err = os.ReadDir(bridge.ApplyGluonConfigPathSuffix(configDir))
|
_, err = os.ReadDir(imapsmtpserver.ApplyGluonConfigPathSuffix(configDir))
|
||||||
require.False(t, os.IsNotExist(err))
|
require.False(t, os.IsNotExist(err))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -772,16 +778,16 @@ func TestBridge_ChangeCacheDirectory(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Old store should no more exists.
|
// Old store should no more exists.
|
||||||
_, err = os.ReadDir(bridge.ApplyGluonCachePathSuffix(currentCacheDir))
|
_, err = os.ReadDir(imapsmtpserver.ApplyGluonCachePathSuffix(currentCacheDir))
|
||||||
require.True(t, os.IsNotExist(err))
|
require.True(t, os.IsNotExist(err))
|
||||||
// Database should not have changed.
|
// Database should not have changed.
|
||||||
_, err = os.ReadDir(bridge.ApplyGluonConfigPathSuffix(configDir))
|
_, err = os.ReadDir(imapsmtpserver.ApplyGluonConfigPathSuffix(configDir))
|
||||||
require.False(t, os.IsNotExist(err))
|
require.False(t, os.IsNotExist(err))
|
||||||
|
|
||||||
// New path should have Gluon sub-folder.
|
// New path should have Gluon sub-folder.
|
||||||
require.Equal(t, filepath.Join(newCacheDir, "gluon"), b.GetGluonCacheDir())
|
require.Equal(t, filepath.Join(newCacheDir, "gluon"), b.GetGluonCacheDir())
|
||||||
// And store should be inside it.
|
// And store should be inside it.
|
||||||
_, err = os.ReadDir(bridge.ApplyGluonCachePathSuffix(b.GetGluonCacheDir()))
|
_, err = os.ReadDir(imapsmtpserver.ApplyGluonCachePathSuffix(b.GetGluonCacheDir()))
|
||||||
require.False(t, os.IsNotExist(err))
|
require.False(t, os.IsNotExist(err))
|
||||||
|
|
||||||
// We should be able to fetch.
|
// We should be able to fetch.
|
||||||
@ -869,6 +875,9 @@ func TestBridge_ChangeAddressOrder(t *testing.T) {
|
|||||||
|
|
||||||
// withEnv creates the full test environment and runs the tests.
|
// withEnv creates the full test environment and runs the tests.
|
||||||
func withEnv(t *testing.T, tests func(context.Context, *server.Server, *proton.NetCtl, bridge.Locator, []byte), opts ...server.Option) {
|
func withEnv(t *testing.T, tests func(context.Context, *server.Server, *proton.NetCtl, bridge.Locator, []byte), opts ...server.Option) {
|
||||||
|
opt := goleak.IgnoreCurrent()
|
||||||
|
defer goleak.VerifyNone(t, opt)
|
||||||
|
|
||||||
server := server.New(opts...)
|
server := server.New(opts...)
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
@ -1053,6 +1062,7 @@ func getConnectedUserIDs(t *testing.T, b *bridge.Bridge) []string {
|
|||||||
func chToType[In, Out any](inCh <-chan In, done func()) (<-chan Out, func()) {
|
func chToType[In, Out any](inCh <-chan In, done func()) (<-chan Out, func()) {
|
||||||
outCh := make(chan Out)
|
outCh := make(chan Out)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
go func() {
|
go func() {
|
||||||
defer close(outCh)
|
defer close(outCh)
|
||||||
|
|
||||||
@ -1062,11 +1072,19 @@ func chToType[In, Out any](inCh <-chan In, done func()) (<-chan Out, func()) {
|
|||||||
panic(fmt.Sprintf("unexpected type %T", in))
|
panic(fmt.Sprintf("unexpected type %T", in))
|
||||||
}
|
}
|
||||||
|
|
||||||
outCh <- out
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
|
||||||
|
case outCh <- out:
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return outCh, done
|
return outCh, func() {
|
||||||
|
cancel()
|
||||||
|
done()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type eventWaiter struct {
|
type eventWaiter struct {
|
||||||
|
|||||||
@ -18,26 +18,22 @@
|
|||||||
package bridge
|
package bridge
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/zip"
|
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
|
||||||
|
|
||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
MaxTotalAttachmentSize = 7 * (1 << 20)
|
DefaultMaxBugReportZipSize = 7 * 1024 * 1024
|
||||||
MaxCompressedFilesCount = 6
|
DefaultMaxSessionCountForBugReport = 10
|
||||||
)
|
)
|
||||||
|
|
||||||
func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, description, username, email, client string, attachLogs bool) error {
|
func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, title, description, username, email, client string, attachLogs bool) error {
|
||||||
var account string
|
var account string
|
||||||
|
|
||||||
if info, err := bridge.QueryUserInfo(username); err == nil {
|
if info, err := bridge.QueryUserInfo(username); err == nil {
|
||||||
@ -50,54 +46,25 @@ func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, descript
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var atts []proton.ReportBugAttachment
|
var attachment []proton.ReportBugAttachment
|
||||||
|
|
||||||
if attachLogs {
|
if attachLogs {
|
||||||
logs, err := getMatchingLogs(bridge.locator, func(filename string) bool {
|
logsPath, err := bridge.locator.ProvideLogsPath()
|
||||||
return logging.MatchLogName(filename) && !logging.MatchStackTraceName(filename)
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
crashes, err := getMatchingLogs(bridge.locator, func(filename string) bool {
|
buffer, err := logging.ZipLogsForBugReport(logsPath, DefaultMaxSessionCountForBugReport, DefaultMaxBugReportZipSize)
|
||||||
return logging.MatchLogName(filename) && logging.MatchStackTraceName(filename)
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
guiLogs, err := getMatchingLogs(bridge.locator, func(filename string) bool {
|
body, err := io.ReadAll(buffer)
|
||||||
return logging.MatchGUILogName(filename) && !logging.MatchStackTraceName(filename)
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var matchFiles []string
|
attachment = append(attachment, proton.ReportBugAttachment{
|
||||||
|
|
||||||
// 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{
|
|
||||||
Name: "logs.zip",
|
Name: "logs.zip",
|
||||||
Filename: "logs.zip",
|
Filename: "logs.zip",
|
||||||
MIMEType: "application/zip",
|
MIMEType: "application/zip",
|
||||||
@ -105,11 +72,17 @@ 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{
|
return bridge.api.ReportBug(ctx, proton.ReportBugReq{
|
||||||
OS: osType,
|
OS: osType,
|
||||||
OSVersion: osVersion,
|
OSVersion: osVersion,
|
||||||
|
|
||||||
Title: "[Bridge] Bug",
|
Title: "[Bridge] Bug - " + title,
|
||||||
Description: description,
|
Description: description,
|
||||||
|
|
||||||
Client: client,
|
Client: client,
|
||||||
@ -118,116 +91,5 @@ func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, descript
|
|||||||
|
|
||||||
Username: account,
|
Username: account,
|
||||||
Email: email,
|
Email: email,
|
||||||
}, atts...)
|
}, attachment...)
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|||||||
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)
|
||||||
|
}
|
||||||
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(ctx, 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())
|
||||||
|
}
|
||||||
45
internal/bridge/events.go
Normal file
45
internal/bridge/events.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
// 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"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/gluon/watcher"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
|
)
|
||||||
|
|
||||||
|
type bridgeEventSubscription struct {
|
||||||
|
b *Bridge
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b bridgeEventSubscription) Add(ofType ...events.Event) *watcher.Watcher[events.Event] {
|
||||||
|
return b.b.addWatcher(ofType...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b bridgeEventSubscription) Remove(watcher *watcher.Watcher[events.Event]) {
|
||||||
|
b.b.remWatcher(watcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
type bridgeEventPublisher struct {
|
||||||
|
b *Bridge
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b bridgeEventPublisher) PublishEvent(_ context.Context, event events.Event) {
|
||||||
|
b.b.publish(event)
|
||||||
|
}
|
||||||
@ -32,7 +32,7 @@ import (
|
|||||||
|
|
||||||
const HeartbeatCheckInterval = time.Hour
|
const HeartbeatCheckInterval = time.Hour
|
||||||
|
|
||||||
func (bridge *Bridge) IsTelemetryAvailable() bool {
|
func (bridge *Bridge) IsTelemetryAvailable(ctx context.Context) bool {
|
||||||
var flag = true
|
var flag = true
|
||||||
if bridge.GetTelemetryDisabled() {
|
if bridge.GetTelemetryDisabled() {
|
||||||
return false
|
return false
|
||||||
@ -40,14 +40,14 @@ func (bridge *Bridge) IsTelemetryAvailable() bool {
|
|||||||
|
|
||||||
safe.RLock(func() {
|
safe.RLock(func() {
|
||||||
for _, user := range bridge.users {
|
for _, user := range bridge.users {
|
||||||
flag = flag && user.IsTelemetryEnabled(context.Background())
|
flag = flag && user.IsTelemetryEnabled(ctx)
|
||||||
}
|
}
|
||||||
}, bridge.usersLock)
|
}, bridge.usersLock)
|
||||||
|
|
||||||
return flag
|
return flag
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) SendHeartbeat(heartbeat *telemetry.HeartbeatData) bool {
|
func (bridge *Bridge) SendHeartbeat(ctx context.Context, heartbeat *telemetry.HeartbeatData) bool {
|
||||||
data, err := json.Marshal(heartbeat)
|
data, err := json.Marshal(heartbeat)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err := bridge.reporter.ReportMessageWithContext("Cannot parse heartbeat data.", reporter.Context{
|
if err := bridge.reporter.ReportMessageWithContext("Cannot parse heartbeat data.", reporter.Context{
|
||||||
@ -62,7 +62,7 @@ func (bridge *Bridge) SendHeartbeat(heartbeat *telemetry.HeartbeatData) bool {
|
|||||||
|
|
||||||
safe.RLock(func() {
|
safe.RLock(func() {
|
||||||
for _, user := range bridge.users {
|
for _, user := range bridge.users {
|
||||||
if err := user.SendTelemetry(context.Background(), data); err == nil {
|
if err := user.SendTelemetry(ctx, data); err == nil {
|
||||||
sent = true
|
sent = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -87,7 +87,7 @@ func (bridge *Bridge) StartHeartbeat(manager telemetry.HeartbeatManager) {
|
|||||||
bridge.goHeartbeat = bridge.tasks.PeriodicOrTrigger(HeartbeatCheckInterval, 0, func(ctx context.Context) {
|
bridge.goHeartbeat = bridge.tasks.PeriodicOrTrigger(HeartbeatCheckInterval, 0, func(ctx context.Context) {
|
||||||
logrus.Debug("Checking for heartbeat")
|
logrus.Debug("Checking for heartbeat")
|
||||||
|
|
||||||
bridge.heartbeat.TrySending()
|
bridge.heartbeat.TrySending(ctx)
|
||||||
})
|
})
|
||||||
|
|
||||||
bridge.heartbeat.SetRollout(bridge.GetUpdateRollout())
|
bridge.heartbeat.SetRollout(bridge.GetUpdateRollout())
|
||||||
|
|||||||
@ -40,3 +40,35 @@ func (bridge *Bridge) setUserAgent(name, version string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type bridgeUserAgentUpdater struct {
|
||||||
|
*Bridge
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bridgeUserAgentUpdater) GetUserAgent() string {
|
||||||
|
return b.identifier.GetUserAgent()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bridgeUserAgentUpdater) HasClient() bool {
|
||||||
|
return b.identifier.HasClient()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bridgeUserAgentUpdater) SetClient(name, version string) {
|
||||||
|
b.identifier.SetClient(name, version)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bridgeUserAgentUpdater) SetPlatform(platform string) {
|
||||||
|
b.identifier.SetPlatform(platform)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bridgeUserAgentUpdater) SetClientString(client string) {
|
||||||
|
b.identifier.SetClientString(client)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bridgeUserAgentUpdater) GetClientString() string {
|
||||||
|
return b.identifier.GetClientString()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bridgeUserAgentUpdater) SetUserAgent(name, version string) {
|
||||||
|
b.setUserAgent(name, version)
|
||||||
|
}
|
||||||
|
|||||||
@ -20,23 +20,12 @@ package bridge
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/Masterminds/semver/v3"
|
"github.com/Masterminds/semver/v3"
|
||||||
"github.com/ProtonMail/gluon"
|
|
||||||
"github.com/ProtonMail/gluon/async"
|
|
||||||
imapEvents "github.com/ProtonMail/gluon/events"
|
imapEvents "github.com/ProtonMail/gluon/events"
|
||||||
"github.com/ProtonMail/gluon/imap"
|
|
||||||
"github.com/ProtonMail/gluon/reporter"
|
|
||||||
"github.com/ProtonMail/gluon/store"
|
|
||||||
"github.com/ProtonMail/gluon/store/fallback_v0"
|
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapsmtpserver"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
|
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
@ -45,16 +34,6 @@ func (bridge *Bridge) restartIMAP(ctx context.Context) error {
|
|||||||
return bridge.serverManager.RestartIMAP(ctx)
|
return bridge.serverManager.RestartIMAP(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// addIMAPUser connects the given user to gluon.
|
|
||||||
func (bridge *Bridge) addIMAPUser(ctx context.Context, user *user.User) error {
|
|
||||||
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 {
|
|
||||||
return bridge.serverManager.RemoveIMAPUser(ctx, user, withData)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) {
|
func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) {
|
||||||
switch event := event.(type) {
|
switch event := event.(type) {
|
||||||
case imapEvents.UserAdded:
|
case imapEvents.UserAdded:
|
||||||
@ -92,108 +71,59 @@ func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ApplyGluonCachePathSuffix(basePath string) string {
|
type bridgeIMAPSettings struct {
|
||||||
return filepath.Join(basePath, "backend", "store")
|
b *Bridge
|
||||||
}
|
}
|
||||||
|
|
||||||
func ApplyGluonConfigPathSuffix(basePath string) string {
|
func (b *bridgeIMAPSettings) EventPublisher() imapsmtpserver.IMAPEventPublisher {
|
||||||
return filepath.Join(basePath, "backend", "db")
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
func newIMAPServer(
|
func (b *bridgeIMAPSettings) TLSConfig() *tls.Config {
|
||||||
gluonCacheDir, gluonConfigDir string,
|
return b.b.tlsConfig
|
||||||
version *semver.Version,
|
}
|
||||||
tlsConfig *tls.Config,
|
|
||||||
reporter reporter.Reporter,
|
|
||||||
logClient, logServer bool,
|
|
||||||
eventCh chan<- imapEvents.Event,
|
|
||||||
tasks *async.Group,
|
|
||||||
uidValidityGenerator imap.UIDValidityGenerator,
|
|
||||||
panicHandler async.PanicHandler,
|
|
||||||
) (*gluon.Server, error) {
|
|
||||||
gluonCacheDir = ApplyGluonCachePathSuffix(gluonCacheDir)
|
|
||||||
gluonConfigDir = ApplyGluonConfigPathSuffix(gluonConfigDir)
|
|
||||||
|
|
||||||
logrus.WithFields(logrus.Fields{
|
func (b *bridgeIMAPSettings) LogClient() bool {
|
||||||
"gluonStore": gluonCacheDir,
|
return b.b.logIMAPClient
|
||||||
"gluonDB": gluonConfigDir,
|
}
|
||||||
"version": version,
|
|
||||||
"logClient": logClient,
|
|
||||||
"logServer": logServer,
|
|
||||||
}).Info("Creating IMAP server")
|
|
||||||
|
|
||||||
if logClient || logServer {
|
func (b *bridgeIMAPSettings) LogServer() bool {
|
||||||
log := logrus.WithField("protocol", "IMAP")
|
return b.b.logIMAPServer
|
||||||
log.Warning("================================================")
|
}
|
||||||
log.Warning("THIS LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA")
|
|
||||||
log.Warning("================================================")
|
func (b *bridgeIMAPSettings) Port() int {
|
||||||
|
return b.b.vault.GetIMAPPort()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bridgeIMAPSettings) SetPort(i int) error {
|
||||||
|
return b.b.vault.SetIMAPPort(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bridgeIMAPSettings) UseSSL() bool {
|
||||||
|
return b.b.vault.GetIMAPSSL()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bridgeIMAPSettings) CacheDirectory() string {
|
||||||
|
return b.b.GetGluonCacheDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bridgeIMAPSettings) DataDirectory() (string, error) {
|
||||||
|
return b.b.GetGluonDataDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bridgeIMAPSettings) SetCacheDirectory(s string) error {
|
||||||
|
return b.b.vault.SetGluonDir(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bridgeIMAPSettings) Version() *semver.Version {
|
||||||
|
return b.b.curVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bridgeIMAPSettings) PublishIMAPEvent(ctx context.Context, event imapEvents.Event) {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case b.b.imapEventCh <- event:
|
||||||
|
// do nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
var imapClientLog io.Writer
|
|
||||||
|
|
||||||
if logClient {
|
|
||||||
imapClientLog = logging.NewIMAPLogger()
|
|
||||||
} else {
|
|
||||||
imapClientLog = io.Discard
|
|
||||||
}
|
|
||||||
|
|
||||||
var imapServerLog io.Writer
|
|
||||||
|
|
||||||
if logServer {
|
|
||||||
imapServerLog = logging.NewIMAPLogger()
|
|
||||||
} else {
|
|
||||||
imapServerLog = io.Discard
|
|
||||||
}
|
|
||||||
|
|
||||||
imapServer, err := gluon.New(
|
|
||||||
gluon.WithTLS(tlsConfig),
|
|
||||||
gluon.WithDataDir(gluonCacheDir),
|
|
||||||
gluon.WithDatabaseDir(gluonConfigDir),
|
|
||||||
gluon.WithStoreBuilder(new(storeBuilder)),
|
|
||||||
gluon.WithLogger(imapClientLog, imapServerLog),
|
|
||||||
getGluonVersionInfo(version),
|
|
||||||
gluon.WithReporter(reporter),
|
|
||||||
gluon.WithUIDValidityGenerator(uidValidityGenerator),
|
|
||||||
gluon.WithPanicHandler(panicHandler),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.Once(func(ctx context.Context) {
|
|
||||||
async.ForwardContext(ctx, eventCh, imapServer.AddWatcher())
|
|
||||||
})
|
|
||||||
|
|
||||||
tasks.Once(func(ctx context.Context) {
|
|
||||||
async.RangeContext(ctx, imapServer.GetErrorCh(), func(err error) {
|
|
||||||
logrus.WithError(err).Error("IMAP server error")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return imapServer, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getGluonVersionInfo(version *semver.Version) gluon.Option {
|
|
||||||
return gluon.WithVersionInfo(
|
|
||||||
int(version.Major()),
|
|
||||||
int(version.Minor()),
|
|
||||||
int(version.Patch()),
|
|
||||||
constants.FullAppName,
|
|
||||||
"TODO",
|
|
||||||
"TODO",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
type storeBuilder struct{}
|
|
||||||
|
|
||||||
func (*storeBuilder) New(path, userID string, passphrase []byte) (store.Store, error) {
|
|
||||||
return store.NewOnDiskStore(
|
|
||||||
filepath.Join(path, userID),
|
|
||||||
passphrase,
|
|
||||||
store.WithFallback(fallback_v0.NewOnDiskStoreV0WithCompressor(&fallback_v0.GZipCompressor{})),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*storeBuilder) Delete(path, userID string) error {
|
|
||||||
return os.RemoveAll(filepath.Join(path, userID))
|
|
||||||
}
|
}
|
||||||
|
|||||||
26
internal/bridge/imapsmtp_telemetry.go
Normal file
26
internal/bridge/imapsmtp_telemetry.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
// 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
|
||||||
|
|
||||||
|
type bridgeIMAPSMTPTelemetry struct {
|
||||||
|
b *Bridge
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b bridgeIMAPSMTPTelemetry) SetCacheLocation(s string) {
|
||||||
|
b.b.heartbeat.SetCacheLocation(s)
|
||||||
|
}
|
||||||
@ -50,7 +50,7 @@ func NewMocks(tb testing.TB, version, minAuto *semver.Version) *Mocks {
|
|||||||
mocks.CrashHandler.EXPECT().HandlePanic(gomock.Any()).AnyTimes()
|
mocks.CrashHandler.EXPECT().HandlePanic(gomock.Any()).AnyTimes()
|
||||||
|
|
||||||
// this is called at start of heartbeat process.
|
// this is called at start of heartbeat process.
|
||||||
mocks.Heartbeat.EXPECT().IsTelemetryAvailable().AnyTimes()
|
mocks.Heartbeat.EXPECT().IsTelemetryAvailable(gomock.Any()).AnyTimes()
|
||||||
|
|
||||||
return mocks
|
return mocks
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
package mocks
|
package mocks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
context "context"
|
||||||
reflect "reflect"
|
reflect "reflect"
|
||||||
time "time"
|
time "time"
|
||||||
|
|
||||||
@ -50,31 +51,31 @@ func (mr *MockHeartbeatManagerMockRecorder) GetLastHeartbeatSent() *gomock.Call
|
|||||||
}
|
}
|
||||||
|
|
||||||
// IsTelemetryAvailable mocks base method.
|
// IsTelemetryAvailable mocks base method.
|
||||||
func (m *MockHeartbeatManager) IsTelemetryAvailable() bool {
|
func (m *MockHeartbeatManager) IsTelemetryAvailable(arg0 context.Context) bool {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "IsTelemetryAvailable")
|
ret := m.ctrl.Call(m, "IsTelemetryAvailable", arg0)
|
||||||
ret0, _ := ret[0].(bool)
|
ret0, _ := ret[0].(bool)
|
||||||
return ret0
|
return ret0
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsTelemetryAvailable indicates an expected call of IsTelemetryAvailable.
|
// IsTelemetryAvailable indicates an expected call of IsTelemetryAvailable.
|
||||||
func (mr *MockHeartbeatManagerMockRecorder) IsTelemetryAvailable() *gomock.Call {
|
func (mr *MockHeartbeatManagerMockRecorder) IsTelemetryAvailable(arg0 interface{}) *gomock.Call {
|
||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsTelemetryAvailable", reflect.TypeOf((*MockHeartbeatManager)(nil).IsTelemetryAvailable))
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsTelemetryAvailable", reflect.TypeOf((*MockHeartbeatManager)(nil).IsTelemetryAvailable), arg0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendHeartbeat mocks base method.
|
// SendHeartbeat mocks base method.
|
||||||
func (m *MockHeartbeatManager) SendHeartbeat(arg0 *telemetry.HeartbeatData) bool {
|
func (m *MockHeartbeatManager) SendHeartbeat(arg0 context.Context, arg1 *telemetry.HeartbeatData) bool {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "SendHeartbeat", arg0)
|
ret := m.ctrl.Call(m, "SendHeartbeat", arg0, arg1)
|
||||||
ret0, _ := ret[0].(bool)
|
ret0, _ := ret[0].(bool)
|
||||||
return ret0
|
return ret0
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendHeartbeat indicates an expected call of SendHeartbeat.
|
// SendHeartbeat indicates an expected call of SendHeartbeat.
|
||||||
func (mr *MockHeartbeatManagerMockRecorder) SendHeartbeat(arg0 interface{}) *gomock.Call {
|
func (mr *MockHeartbeatManagerMockRecorder) SendHeartbeat(arg0, arg1 interface{}) *gomock.Call {
|
||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendHeartbeat", reflect.TypeOf((*MockHeartbeatManager)(nil).SendHeartbeat), arg0)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendHeartbeat", reflect.TypeOf((*MockHeartbeatManager)(nil).SendHeartbeat), arg0, arg1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetLastHeartbeatSent mocks base method.
|
// SetLastHeartbeatSent mocks base method.
|
||||||
|
|||||||
@ -84,6 +84,11 @@ func TestBridge_Refresh(t *testing.T) {
|
|||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).AnyTimes()
|
mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).AnyTimes()
|
||||||
|
|
||||||
|
// Wait for refresh event first
|
||||||
|
refreshCh, refreshChDone := chToType[events.Event, events.UserRefreshed](b.GetEvents(events.UserRefreshed{}))
|
||||||
|
defer refreshChDone()
|
||||||
|
require.Equal(t, userID, (<-refreshCh).UserID)
|
||||||
|
// Then sync event
|
||||||
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
|
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
|
||||||
defer done()
|
defer done()
|
||||||
|
|
||||||
|
|||||||
@ -467,7 +467,9 @@ SGVsbG8gd29ybGQK
|
|||||||
require.Eventually(t, func() bool {
|
require.Eventually(t, func() bool {
|
||||||
messages, err := clientFetch(senderIMAPClient, `Sent`, imap.FetchBodyStructure)
|
messages, err := clientFetch(senderIMAPClient, `Sent`, imap.FetchBodyStructure)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, 4, len(messages))
|
if len(messages) != 4 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// messages may not be in order
|
// messages may not be in order
|
||||||
for _, message := range messages {
|
for _, message := range messages {
|
||||||
|
|||||||
@ -1,696 +0,0 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
|
||||||
//
|
|
||||||
// This file is part of Proton Mail Bridge.
|
|
||||||
//
|
|
||||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU General Public License
|
|
||||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package 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
|
|
||||||
}
|
|
||||||
@ -20,11 +20,10 @@ package bridge
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/Masterminds/semver/v3"
|
"github.com/Masterminds/semver/v3"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/userevents"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
@ -131,26 +130,41 @@ func (bridge *Bridge) GetGluonDataDir() (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) SetGluonDir(ctx context.Context, newGluonDir string) error {
|
func (bridge *Bridge) SetGluonDir(ctx context.Context, newGluonDir string) error {
|
||||||
|
bridge.usersLock.RLock()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
logrus.Info("Restarting user event loops")
|
||||||
|
for _, u := range bridge.users {
|
||||||
|
u.ResumeEventLoop()
|
||||||
|
}
|
||||||
|
|
||||||
|
bridge.usersLock.RUnlock()
|
||||||
|
}()
|
||||||
|
|
||||||
|
type waiter struct {
|
||||||
|
w *userevents.EventPollWaiter
|
||||||
|
id string
|
||||||
|
}
|
||||||
|
|
||||||
|
waiters := make([]waiter, 0, len(bridge.users))
|
||||||
|
|
||||||
|
logrus.Info("Pausing user event loops for gluon dir change")
|
||||||
|
for id, u := range bridge.users {
|
||||||
|
waiters = append(waiters, waiter{w: u.PauseEventLoopWithWaiter(), id: id})
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Info("Waiting on user event loop completion")
|
||||||
|
for _, waiter := range waiters {
|
||||||
|
if err := waiter.w.WaitPollFinished(ctx); err != nil {
|
||||||
|
logrus.WithError(err).Errorf("Failed to wait on event loop pause for user %v", waiter.id)
|
||||||
|
return fmt.Errorf("failed on event loop pause: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Info("Changing gluon directory")
|
||||||
return bridge.serverManager.SetGluonDir(ctx, newGluonDir)
|
return bridge.serverManager.SetGluonDir(ctx, newGluonDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) moveGluonCacheDir(oldGluonDir, newGluonDir string) error {
|
|
||||||
logrus.Infof("gluon cache moving from %s to %s", oldGluonDir, newGluonDir)
|
|
||||||
oldCacheDir := ApplyGluonCachePathSuffix(oldGluonDir)
|
|
||||||
if err := copyDir(oldCacheDir, ApplyGluonCachePathSuffix(newGluonDir)); err != nil {
|
|
||||||
return fmt.Errorf("failed to copy gluon dir: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := bridge.vault.SetGluonDir(newGluonDir); err != nil {
|
|
||||||
return fmt.Errorf("failed to set new gluon cache dir: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.RemoveAll(oldCacheDir); err != nil {
|
|
||||||
logrus.WithError(err).Error("failed to remove old gluon cache dir")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bridge *Bridge) GetProxyAllowed() bool {
|
func (bridge *Bridge) GetProxyAllowed() bool {
|
||||||
return bridge.vault.GetProxyAllowed()
|
return bridge.vault.GetProxyAllowed()
|
||||||
}
|
}
|
||||||
@ -297,10 +311,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,
|
// 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.
|
// which we need at next startup to decrypt the vault.
|
||||||
func (bridge *Bridge) FactoryReset(ctx context.Context) {
|
func (bridge *Bridge) FactoryReset(ctx context.Context) {
|
||||||
|
useTelemetry := !bridge.GetTelemetryDisabled()
|
||||||
// Delete all the users.
|
// Delete all the users.
|
||||||
safe.Lock(func() {
|
safe.Lock(func() {
|
||||||
for _, user := range bridge.users {
|
for _, user := range bridge.users {
|
||||||
bridge.logoutUser(ctx, user, true, true)
|
bridge.logoutUser(ctx, user, true, true, useTelemetry)
|
||||||
}
|
}
|
||||||
}, bridge.usersLock)
|
}, bridge.usersLock)
|
||||||
|
|
||||||
@ -317,16 +332,3 @@ func (bridge *Bridge) FactoryReset(ctx context.Context) {
|
|||||||
logrus.WithError(err).Error("Failed to clear data paths")
|
logrus.WithError(err).Error("Failed to clear data paths")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPort(addr net.Addr) int {
|
|
||||||
switch addr := addr.(type) {
|
|
||||||
case *net.TCPAddr:
|
|
||||||
return addr.Port
|
|
||||||
|
|
||||||
case *net.UDPAddr:
|
|
||||||
return addr.Port
|
|
||||||
|
|
||||||
default:
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -25,6 +25,7 @@ import (
|
|||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
"github.com/ProtonMail/go-proton-api/server"
|
"github.com/ProtonMail/go-proton-api/server"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -51,6 +52,45 @@ func TestBridge_Settings_GluonDir(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBridge_Settings_GluonDirWithOnGoingEvents(t *testing.T) {
|
||||||
|
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)
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
|
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
||||||
|
defer done()
|
||||||
|
|
||||||
|
_, err := bridge.LoginFull(context.Background(), "imap", password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
<-syncCh
|
||||||
|
})
|
||||||
|
|
||||||
|
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, 200)
|
||||||
|
})
|
||||||
|
|
||||||
|
withBridgeWaitForServers(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
|
// Create a new location for the Gluon data.
|
||||||
|
newGluonDir := t.TempDir()
|
||||||
|
|
||||||
|
// Move the gluon dir; it should also move the user's data.
|
||||||
|
require.NoError(t, bridge.SetGluonDir(context.Background(), newGluonDir))
|
||||||
|
|
||||||
|
// Check that the new directory is not empty.
|
||||||
|
entries, err := os.ReadDir(newGluonDir)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// There should be at least one entry.
|
||||||
|
require.NotEmpty(t, entries)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestBridge_Settings_IMAPPort(t *testing.T) {
|
func TestBridge_Settings_IMAPPort(t *testing.T) {
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
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) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
|
|||||||
@ -21,43 +21,37 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
"github.com/ProtonMail/proton-bridge/v3/internal/identifier"
|
||||||
"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) restartSMTP(ctx context.Context) error {
|
func (bridge *Bridge) restartSMTP(ctx context.Context) error {
|
||||||
return bridge.serverManager.RestartSMTP(ctx)
|
return bridge.serverManager.RestartSMTP(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newSMTPServer(bridge *Bridge, tlsConfig *tls.Config, logSMTP bool) *smtp.Server {
|
type bridgeSMTPSettings struct {
|
||||||
logrus.WithField("logSMTP", logSMTP).Info("Creating SMTP server")
|
b *Bridge
|
||||||
|
}
|
||||||
smtpServer := smtp.NewServer(&smtpBackend{Bridge: bridge})
|
|
||||||
|
func (b *bridgeSMTPSettings) TLSConfig() *tls.Config {
|
||||||
smtpServer.TLSConfig = tlsConfig
|
return b.b.tlsConfig
|
||||||
smtpServer.Domain = constants.Host
|
}
|
||||||
smtpServer.AllowInsecureAuth = true
|
|
||||||
smtpServer.MaxLineLength = 1 << 16
|
func (b *bridgeSMTPSettings) Log() bool {
|
||||||
smtpServer.ErrorLog = logging.NewSMTPLogger()
|
return b.b.logSMTP
|
||||||
|
}
|
||||||
// go-smtp suppors SASL PLAIN but not LOGIN. We need to add LOGIN support ourselves.
|
|
||||||
smtpServer.EnableAuth(sasl.Login, func(conn *smtp.Conn) sasl.Server {
|
func (b *bridgeSMTPSettings) Port() int {
|
||||||
return sasl.NewLoginServer(func(username, password string) error {
|
return b.b.vault.GetSMTPPort()
|
||||||
return conn.Session().AuthPlain(username, password)
|
}
|
||||||
})
|
|
||||||
})
|
func (b *bridgeSMTPSettings) SetPort(i int) error {
|
||||||
|
return b.b.vault.SetSMTPPort(i)
|
||||||
if logSMTP {
|
}
|
||||||
log := logrus.WithField("protocol", "SMTP")
|
|
||||||
log.Warning("================================================")
|
func (b *bridgeSMTPSettings) UseSSL() bool {
|
||||||
log.Warning("THIS LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA")
|
return b.b.vault.GetSMTPSSL()
|
||||||
log.Warning("================================================")
|
}
|
||||||
|
|
||||||
smtpServer.Debug = logging.NewSMTPDebugLogger()
|
func (b *bridgeSMTPSettings) Identifier() identifier.UserAgentUpdater {
|
||||||
}
|
return &bridgeUserAgentUpdater{Bridge: b.b}
|
||||||
|
|
||||||
return smtpServer
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,9 +21,11 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@ -252,14 +254,17 @@ func TestBridge_SyncWithOngoingEvents(t *testing.T) {
|
|||||||
|
|
||||||
// Login the user; its sync should fail.
|
// Login the user; its sync should fail.
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
|
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
|
||||||
|
defer done()
|
||||||
|
|
||||||
{
|
{
|
||||||
syncCh, done := chToType[events.Event, events.SyncFailed](b.GetEvents(events.SyncFailed{}))
|
syncFailedCh, syncFailedDone := chToType[events.Event, events.SyncFailed](b.GetEvents(events.SyncFailed{}))
|
||||||
defer done()
|
defer syncFailedDone()
|
||||||
|
|
||||||
userID, err := b.LoginFull(ctx, "imap", password, nil, nil)
|
userID, err := b.LoginFull(ctx, "imap", password, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
require.Equal(t, userID, (<-syncCh).UserID)
|
require.Equal(t, userID, (<-syncFailedCh).UserID)
|
||||||
|
|
||||||
info, err := b.GetUserInfo(userID)
|
info, err := b.GetUserInfo(userID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -282,11 +287,7 @@ func TestBridge_SyncWithOngoingEvents(t *testing.T) {
|
|||||||
|
|
||||||
// Remove the network limit, allowing the sync to finish.
|
// Remove the network limit, allowing the sync to finish.
|
||||||
netCtl.SetReadLimit(0)
|
netCtl.SetReadLimit(0)
|
||||||
|
|
||||||
{
|
{
|
||||||
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
|
|
||||||
defer done()
|
|
||||||
|
|
||||||
require.Equal(t, userID, (<-syncCh).UserID)
|
require.Equal(t, userID, (<-syncCh).UserID)
|
||||||
|
|
||||||
info, err := b.GetUserInfo(userID)
|
info, err := b.GetUserInfo(userID)
|
||||||
@ -298,12 +299,6 @@ func TestBridge_SyncWithOngoingEvents(t *testing.T) {
|
|||||||
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
||||||
defer func() { _ = client.Logout() }()
|
defer func() { _ = client.Logout() }()
|
||||||
|
|
||||||
status, err := client.Select(`Folders/folder`, false)
|
|
||||||
require.NoError(t, err)
|
|
||||||
// Original folder should have more than 0 messages and less than the total.
|
|
||||||
require.Greater(t, status.Messages, uint32(0))
|
|
||||||
require.Less(t, status.Messages, uint32(numMsg))
|
|
||||||
|
|
||||||
// Check that the new messages arrive in the right location.
|
// Check that the new messages arrive in the right location.
|
||||||
require.Eventually(t, func() bool {
|
require.Eventually(t, func() bool {
|
||||||
status, err := client.Select(`Folders/folder2`, true)
|
status, err := client.Select(`Folders/folder2`, true)
|
||||||
@ -321,6 +316,269 @@ func TestBridge_SyncWithOngoingEvents(t *testing.T) {
|
|||||||
}, server.WithTLS(false))
|
}, server.WithTLS(false))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBridge_CanProcessEventsDuringSync(t *testing.T) {
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Simulate 429 to prevent sync from progressing.
|
||||||
|
s.AddStatusHook(func(request *http.Request) (int, bool) {
|
||||||
|
if strings.Contains(request.URL.Path, "/mail/v4/messages/") {
|
||||||
|
return http.StatusTooManyRequests, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, false
|
||||||
|
})
|
||||||
|
|
||||||
|
// The initial user should be fully synced.
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
syncStartedCh, syncStartedDone := chToType[events.Event, events.SyncStarted](bridge.GetEvents(events.SyncStarted{}))
|
||||||
|
defer syncStartedDone()
|
||||||
|
|
||||||
|
addressCreatedCh, addressCreatedDone := chToType[events.Event, events.UserAddressCreated](bridge.GetEvents(events.UserAddressCreated{}))
|
||||||
|
defer addressCreatedDone()
|
||||||
|
|
||||||
|
userID, err := bridge.LoginFull(ctx, "imap", password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, userID, (<-syncStartedCh).UserID)
|
||||||
|
|
||||||
|
// Create a new address
|
||||||
|
newAddress := "foo@proton.ch"
|
||||||
|
addrID, err := s.CreateAddress(userID, newAddress, password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
event := <-addressCreatedCh
|
||||||
|
require.Equal(t, userID, event.UserID)
|
||||||
|
require.Equal(t, newAddress, event.Email)
|
||||||
|
require.Equal(t, addrID, event.AddressID)
|
||||||
|
})
|
||||||
|
}, server.WithTLS(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBridge_RefreshDuringSyncRestartSync(t *testing.T) {
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
|
||||||
|
var refreshPerformed atomic.Bool
|
||||||
|
refreshPerformed.Store(false)
|
||||||
|
|
||||||
|
// Simulate 429 to prevent sync from progressing.
|
||||||
|
s.AddStatusHook(func(request *http.Request) (int, bool) {
|
||||||
|
if strings.Contains(request.URL.Path, "/mail/v4/messages/") {
|
||||||
|
if !refreshPerformed.Load() {
|
||||||
|
return http.StatusTooManyRequests, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, false
|
||||||
|
})
|
||||||
|
|
||||||
|
// The initial user should be fully synced.
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
||||||
|
defer done()
|
||||||
|
|
||||||
|
userID, err := bridge.LoginFull(ctx, "imap", password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
syncStartedCh, syncStartedDone := chToType[events.Event, events.SyncStarted](bridge.GetEvents(events.SyncStarted{}))
|
||||||
|
defer syncStartedDone()
|
||||||
|
|
||||||
|
require.Equal(t, userID, (<-syncStartedCh).UserID)
|
||||||
|
|
||||||
|
require.NoError(t, err, s.RefreshUser(userID, proton.RefreshMail))
|
||||||
|
require.Equal(t, userID, (<-syncStartedCh).UserID)
|
||||||
|
refreshPerformed.Store(true)
|
||||||
|
|
||||||
|
require.Equal(t, userID, (<-syncCh).UserID)
|
||||||
|
})
|
||||||
|
}, server.WithTLS(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBridge_EventReplayAfterSyncHasFinished(t *testing.T) {
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
|
||||||
|
addrID1, err := s.CreateAddress(userID, "foo@proton.ch", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var allowSyncToProgress atomic.Bool
|
||||||
|
allowSyncToProgress.Store(false)
|
||||||
|
|
||||||
|
// Simulate 429 to prevent sync from progressing.
|
||||||
|
s.AddStatusHook(func(request *http.Request) (int, bool) {
|
||||||
|
if request.Method == "GET" && strings.Contains(request.URL.Path, "/mail/v4/messages/") {
|
||||||
|
if !allowSyncToProgress.Load() {
|
||||||
|
return http.StatusTooManyRequests, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, false
|
||||||
|
})
|
||||||
|
|
||||||
|
// The initial user should be fully synced.
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
||||||
|
defer done()
|
||||||
|
|
||||||
|
syncStartedCh, syncStartedDone := chToType[events.Event, events.SyncStarted](bridge.GetEvents(events.SyncStarted{}))
|
||||||
|
defer syncStartedDone()
|
||||||
|
|
||||||
|
addressCreatedCh, addressCreatedDone := chToType[events.Event, events.UserAddressCreated](bridge.GetEvents(events.UserAddressCreated{}))
|
||||||
|
defer addressCreatedDone()
|
||||||
|
|
||||||
|
userID, err := bridge.LoginFull(ctx, "imap", password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, userID, (<-syncStartedCh).UserID)
|
||||||
|
|
||||||
|
// create 20 more messages and move them to inbox
|
||||||
|
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 20)
|
||||||
|
})
|
||||||
|
|
||||||
|
// User AddrID2 event as a check point to see when the new address was created.
|
||||||
|
addrID2, err := s.CreateAddress(userID, "bar@proton.ch", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
allowSyncToProgress.Store(true)
|
||||||
|
require.Equal(t, userID, (<-syncCh).UserID)
|
||||||
|
|
||||||
|
// At most two events can be published, one for the first address, then for the second.
|
||||||
|
// if the second event is not `addrID2` then something went wrong.
|
||||||
|
event := <-addressCreatedCh
|
||||||
|
if event.AddressID == addrID1 {
|
||||||
|
event = <-addressCreatedCh
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Equal(t, addrID2, event.AddressID)
|
||||||
|
|
||||||
|
info, err := bridge.GetUserInfo(userID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
client, 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() }()
|
||||||
|
|
||||||
|
// Finally check if the 20 messages are in INBOX.
|
||||||
|
status, err := client.Status("INBOX", []imap.StatusItem{imap.StatusMessages})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, uint32(20), status.Messages)
|
||||||
|
|
||||||
|
// Finally check if the numMsg are in the folder.
|
||||||
|
status, err = client.Status("Folders/folder", []imap.StatusItem{imap.StatusMessages})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, uint32(numMsg), status.Messages)
|
||||||
|
})
|
||||||
|
}, server.WithTLS(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBridge_MessageCreateDuringSync(t *testing.T) {
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
|
||||||
|
var allowSyncToProgress atomic.Bool
|
||||||
|
allowSyncToProgress.Store(false)
|
||||||
|
|
||||||
|
// Simulate 429 to prevent sync from progressing.
|
||||||
|
s.AddStatusHook(func(request *http.Request) (int, bool) {
|
||||||
|
if request.Method == "GET" && strings.Contains(request.URL.Path, "/mail/v4/messages/") {
|
||||||
|
if !allowSyncToProgress.Load() {
|
||||||
|
return http.StatusTooManyRequests, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, false
|
||||||
|
})
|
||||||
|
|
||||||
|
// The initial user should be fully synced.
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
syncStartedCh, syncStartedDone := chToType[events.Event, events.SyncStarted](bridge.GetEvents(events.SyncStarted{}))
|
||||||
|
defer syncStartedDone()
|
||||||
|
|
||||||
|
addressCreatedCh, addressCreatedDone := chToType[events.Event, events.UserAddressCreated](bridge.GetEvents(events.UserAddressCreated{}))
|
||||||
|
defer addressCreatedDone()
|
||||||
|
|
||||||
|
userID, err := bridge.LoginFull(ctx, "imap", password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, userID, (<-syncStartedCh).UserID)
|
||||||
|
|
||||||
|
// create 20 more messages and move them to inbox
|
||||||
|
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 20)
|
||||||
|
})
|
||||||
|
|
||||||
|
// User AddrID2 event as a check point to see when the new address was created.
|
||||||
|
addrID, err := s.CreateAddress(userID, "bar@proton.ch", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// At most two events can be published, one for the first address, then for the second.
|
||||||
|
// if the second event is not `addrID` then something went wrong.
|
||||||
|
event := <-addressCreatedCh
|
||||||
|
require.Equal(t, addrID, event.AddressID)
|
||||||
|
allowSyncToProgress.Store(true)
|
||||||
|
|
||||||
|
info, err := bridge.GetUserInfo(userID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
client, 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.Eventually(t, func() bool {
|
||||||
|
// Finally check if the 20 messages are in INBOX.
|
||||||
|
status, err := client.Status("INBOX", []imap.StatusItem{imap.StatusMessages})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return uint32(20) == status.Messages
|
||||||
|
}, 10*time.Second, time.Second)
|
||||||
|
})
|
||||||
|
}, server.WithTLS(false))
|
||||||
|
}
|
||||||
|
|
||||||
func withClient(ctx context.Context, t *testing.T, s *server.Server, username string, password []byte, fn func(context.Context, *proton.Client)) { //nolint:unparam
|
func withClient(ctx context.Context, t *testing.T, s *server.Server, username string, password []byte, fn func(context.Context, *proton.Client)) { //nolint:unparam
|
||||||
m := proton.New(
|
m := proton.New(
|
||||||
proton.WithHostURL(s.GetHostURL()),
|
proton.WithHostURL(s.GetHostURL()),
|
||||||
@ -399,6 +657,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 {
|
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)
|
user, err := c.GetUser(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@ -417,6 +679,13 @@ func createMessages(ctx context.Context, t *testing.T, c *proton.Client, addrID,
|
|||||||
_, ok := addrKRs[addrID]
|
_, ok := addrKRs[addrID]
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
|
|
||||||
|
var msgFlags proton.MessageFlag
|
||||||
|
if flags == 0 {
|
||||||
|
msgFlags = proton.MessageFlagReceived
|
||||||
|
} else {
|
||||||
|
msgFlags = flags
|
||||||
|
}
|
||||||
|
|
||||||
str, err := c.ImportMessages(
|
str, err := c.ImportMessages(
|
||||||
ctx,
|
ctx,
|
||||||
addrKRs[addrID],
|
addrKRs[addrID],
|
||||||
@ -427,7 +696,7 @@ func createMessages(ctx context.Context, t *testing.T, c *proton.Client, addrID,
|
|||||||
Metadata: proton.ImportMetadata{
|
Metadata: proton.ImportMetadata{
|
||||||
AddressID: addrID,
|
AddressID: addrID,
|
||||||
LabelIDs: []string{labelID},
|
LabelIDs: []string{labelID},
|
||||||
Flags: proton.MessageFlagReceived,
|
Flags: msgFlags,
|
||||||
},
|
},
|
||||||
Message: message,
|
Message: message,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,7 +31,7 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Disabled due to flakyness.
|
// Disabled due to flakiness.
|
||||||
func _TestBridge_SyncExistsWithErrorWhenTooManyFilesAreOpen(t *testing.T) { //nolint:unused
|
func _TestBridge_SyncExistsWithErrorWhenTooManyFilesAreOpen(t *testing.T) { //nolint:unused
|
||||||
var rlimitCurrent syscall.Rlimit
|
var rlimitCurrent syscall.Rlimit
|
||||||
|
|
||||||
|
|||||||
@ -28,18 +28,11 @@ type Locator interface {
|
|||||||
ProvideLogsPath() (string, error)
|
ProvideLogsPath() (string, error)
|
||||||
ProvideGluonCachePath() (string, error)
|
ProvideGluonCachePath() (string, error)
|
||||||
ProvideGluonDataPath() (string, error)
|
ProvideGluonDataPath() (string, error)
|
||||||
|
ProvideStatsPath() (string, error)
|
||||||
GetLicenseFilePath() string
|
GetLicenseFilePath() string
|
||||||
GetDependencyLicensesLink() string
|
GetDependencyLicensesLink() string
|
||||||
Clear(...string) error
|
Clear(...string) error
|
||||||
}
|
ProvideIMAPSyncConfigPath() (string, error)
|
||||||
|
|
||||||
type Identifier interface {
|
|
||||||
GetUserAgent() string
|
|
||||||
HasClient() bool
|
|
||||||
SetClient(name, version string)
|
|
||||||
SetPlatform(platform string)
|
|
||||||
SetClientString(client string)
|
|
||||||
GetClientString() string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProxyController interface {
|
type ProxyController interface {
|
||||||
|
|||||||
@ -30,6 +30,7 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/try"
|
"github.com/ProtonMail/proton-bridge/v3/internal/try"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||||
@ -229,7 +230,7 @@ func (bridge *Bridge) LogoutUser(ctx context.Context, userID string) error {
|
|||||||
return ErrNoSuchUser
|
return ErrNoSuchUser
|
||||||
}
|
}
|
||||||
|
|
||||||
bridge.logoutUser(ctx, user, true, false)
|
bridge.logoutUser(ctx, user, true, false, false)
|
||||||
|
|
||||||
bridge.publish(events.UserLoggedOut{
|
bridge.publish(events.UserLoggedOut{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
@ -243,13 +244,22 @@ func (bridge *Bridge) LogoutUser(ctx context.Context, userID string) error {
|
|||||||
func (bridge *Bridge) DeleteUser(ctx context.Context, userID string) error {
|
func (bridge *Bridge) DeleteUser(ctx context.Context, userID string) error {
|
||||||
logrus.WithField("userID", userID).Info("Deleting user")
|
logrus.WithField("userID", userID).Info("Deleting user")
|
||||||
|
|
||||||
|
syncConfigDir, err := bridge.locator.ProvideIMAPSyncConfigPath()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get sync config path")
|
||||||
|
}
|
||||||
|
|
||||||
return safe.LockRet(func() error {
|
return safe.LockRet(func() error {
|
||||||
if !bridge.vault.HasUser(userID) {
|
if !bridge.vault.HasUser(userID) {
|
||||||
return ErrNoSuchUser
|
return ErrNoSuchUser
|
||||||
}
|
}
|
||||||
|
|
||||||
if user, ok := bridge.users[userID]; ok {
|
if user, ok := bridge.users[userID]; ok {
|
||||||
bridge.logoutUser(ctx, user, true, true)
|
bridge.logoutUser(ctx, user, true, true, !bridge.GetTelemetryDisabled())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := imapservice.DeleteSyncState(syncConfigDir, userID); err != nil {
|
||||||
|
return fmt.Errorf("failed to delete use sync config")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := bridge.vault.DeleteUser(userID); err != nil {
|
if err := bridge.vault.DeleteUser(userID); err != nil {
|
||||||
@ -278,18 +288,10 @@ func (bridge *Bridge) SetAddressMode(ctx context.Context, userID string, mode va
|
|||||||
return fmt.Errorf("address mode is already %q", mode)
|
return fmt.Errorf("address mode is already %q", mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := bridge.removeIMAPUser(ctx, user, true); err != nil {
|
|
||||||
return fmt.Errorf("failed to remove IMAP user: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := user.SetAddressMode(ctx, mode); err != nil {
|
if err := user.SetAddressMode(ctx, mode); err != nil {
|
||||||
return fmt.Errorf("failed to set address mode: %w", err)
|
return fmt.Errorf("failed to set address mode: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := bridge.addIMAPUser(ctx, user); err != nil {
|
|
||||||
return fmt.Errorf("failed to add IMAP user: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
bridge.publish(events.AddressModeChanged{
|
bridge.publish(events.AddressModeChanged{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
AddressMode: mode,
|
AddressMode: mode,
|
||||||
@ -335,13 +337,7 @@ func (bridge *Bridge) SendBadEventUserFeedback(_ context.Context, userID string,
|
|||||||
logrus.WithError(rerr).Error("Failed to report feedback failure")
|
logrus.WithError(rerr).Error("Failed to report feedback failure")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := bridge.addIMAPUser(ctx, user); err != nil {
|
return user.BadEventFeedbackResync(ctx)
|
||||||
return fmt.Errorf("failed to add IMAP user: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
user.BadEventFeedbackResync(ctx)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if rerr := bridge.reporter.ReportMessageWithContext(
|
if rerr := bridge.reporter.ReportMessageWithContext(
|
||||||
@ -351,7 +347,7 @@ func (bridge *Bridge) SendBadEventUserFeedback(_ context.Context, userID string,
|
|||||||
logrus.WithError(rerr).Error("Failed to report feedback failure")
|
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{
|
bridge.publish(events.UserLoggedOut{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
@ -519,6 +515,16 @@ func (bridge *Bridge) addUserWithVault(
|
|||||||
apiUser proton.User,
|
apiUser proton.User,
|
||||||
vault *vault.User,
|
vault *vault.User,
|
||||||
) error {
|
) error {
|
||||||
|
statsPath, err := bridge.locator.ProvideStatsPath()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get Statistics directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
syncSettingsPath, err := bridge.locator.ProvideIMAPSyncConfigPath()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get IMAP sync config path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
user, err := user.New(
|
user, err := user.New(
|
||||||
ctx,
|
ctx,
|
||||||
vault,
|
vault,
|
||||||
@ -528,16 +534,18 @@ func (bridge *Bridge) addUserWithVault(
|
|||||||
bridge.panicHandler,
|
bridge.panicHandler,
|
||||||
bridge.vault.GetShowAllMail(),
|
bridge.vault.GetShowAllMail(),
|
||||||
bridge.vault.GetMaxSyncMemory(),
|
bridge.vault.GetMaxSyncMemory(),
|
||||||
|
statsPath,
|
||||||
|
bridge,
|
||||||
|
bridge.serverManager,
|
||||||
|
bridge.serverManager,
|
||||||
|
&bridgeEventSubscription{b: bridge},
|
||||||
|
bridge.syncService,
|
||||||
|
syncSettingsPath,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create user: %w", err)
|
return fmt.Errorf("failed to create user: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connect the user's address(es) to gluon.
|
|
||||||
if err := bridge.addIMAPUser(ctx, user); err != nil {
|
|
||||||
return fmt.Errorf("failed to add IMAP user: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle events coming from the user before forwarding them to the bridge.
|
// Handle events coming from the user before forwarding them to the bridge.
|
||||||
// For example, if the user's addresses change, we need to update them in gluon.
|
// For example, if the user's addresses change, we need to update them in gluon.
|
||||||
bridge.tasks.Once(func(ctx context.Context) {
|
bridge.tasks.Once(func(ctx context.Context) {
|
||||||
@ -547,11 +555,8 @@ func (bridge *Bridge) addUserWithVault(
|
|||||||
"event": event,
|
"event": event,
|
||||||
}).Debug("Received user event")
|
}).Debug("Received user event")
|
||||||
|
|
||||||
if err := bridge.handleUserEvent(ctx, user, event); err != nil {
|
bridge.handleUserEvent(ctx, user, event)
|
||||||
logrus.WithError(err).Error("Failed to handle user event")
|
bridge.publish(event)
|
||||||
} else {
|
|
||||||
bridge.publish(event)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -588,19 +593,20 @@ func (bridge *Bridge) newVaultUser(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// logout logs out the given user, optionally logging them out from the API too.
|
// 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())
|
defer delete(bridge.users, user.ID())
|
||||||
|
|
||||||
|
// if this is actually a remove account
|
||||||
|
if withData && withAPI {
|
||||||
|
user.SendConfigStatusAbort(ctx, withTelemetry)
|
||||||
|
}
|
||||||
|
|
||||||
logrus.WithFields(logrus.Fields{
|
logrus.WithFields(logrus.Fields{
|
||||||
"userID": user.ID(),
|
"userID": user.ID(),
|
||||||
"withAPI": withAPI,
|
"withAPI": withAPI,
|
||||||
"withData": withData,
|
"withData": withData,
|
||||||
}).Debug("Logging out user")
|
}).Debug("Logging out user")
|
||||||
|
|
||||||
if err := bridge.removeIMAPUser(ctx, user, withData); err != nil {
|
|
||||||
logrus.WithError(err).Error("Failed to remove IMAP user")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := user.Logout(ctx, withAPI); err != nil {
|
if err := user.Logout(ctx, withAPI); err != nil {
|
||||||
logrus.WithError(err).Error("Failed to logout user")
|
logrus.WithError(err).Error("Failed to logout user")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,44 +19,17 @@ package bridge
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/ProtonMail/gluon/reporter"
|
"github.com/ProtonMail/gluon/reporter"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal"
|
"github.com/ProtonMail/proton-bridge/v3/internal"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (bridge *Bridge) handleUserEvent(ctx context.Context, user *user.User, event events.Event) error {
|
func (bridge *Bridge) handleUserEvent(ctx context.Context, user *user.User, event events.Event) {
|
||||||
switch event := event.(type) {
|
switch event := event.(type) {
|
||||||
case events.UserAddressCreated:
|
|
||||||
if err := bridge.handleUserAddressCreated(ctx, user, event); err != nil {
|
|
||||||
return fmt.Errorf("failed to handle user address created event: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
case events.UserAddressEnabled:
|
|
||||||
if err := bridge.handleUserAddressEnabled(ctx, user, event); err != nil {
|
|
||||||
return fmt.Errorf("failed to handle user address enabled event: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
case events.UserAddressDisabled:
|
|
||||||
if err := bridge.handleUserAddressDisabled(ctx, user, event); err != nil {
|
|
||||||
return fmt.Errorf("failed to handle user address disabled event: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
case events.UserAddressDeleted:
|
|
||||||
if err := bridge.handleUserAddressDeleted(ctx, user, event); err != nil {
|
|
||||||
return fmt.Errorf("failed to handle user address deleted event: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
case events.UserRefreshed:
|
|
||||||
if err := bridge.handleUserRefreshed(ctx, user, event); err != nil {
|
|
||||||
return fmt.Errorf("failed to handle user refreshed event: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
case events.UserDeauth:
|
case events.UserDeauth:
|
||||||
bridge.handleUserDeauth(ctx, user)
|
bridge.handleUserDeauth(ctx, user)
|
||||||
|
|
||||||
@ -66,111 +39,16 @@ func (bridge *Bridge) handleUserEvent(ctx context.Context, user *user.User, even
|
|||||||
case events.UncategorizedEventError:
|
case events.UncategorizedEventError:
|
||||||
bridge.handleUncategorizedErrorEvent(event)
|
bridge.handleUncategorizedErrorEvent(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bridge *Bridge) handleUserAddressCreated(ctx context.Context, user *user.User, event events.UserAddressCreated) error {
|
|
||||||
if user.GetAddressMode() == vault.CombinedMode {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := user.SetGluonID(event.AddressID, gluonID); err != nil {
|
|
||||||
return fmt.Errorf("failed to set gluon ID: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bridge *Bridge) handleUserAddressEnabled(ctx context.Context, user *user.User, event events.UserAddressEnabled) error {
|
|
||||||
if user.GetAddressMode() == vault.CombinedMode {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := user.SetGluonID(event.AddressID, gluonID); err != nil {
|
|
||||||
return fmt.Errorf("failed to set gluon ID: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bridge *Bridge) handleUserAddressDisabled(ctx context.Context, user *user.User, event events.UserAddressDisabled) error {
|
|
||||||
if user.GetAddressMode() == vault.CombinedMode {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
gluonID, ok := user.GetGluonID(event.AddressID)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("gluon ID not found for address %s", event.AddressID)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := bridge.serverManager.RemoveGluonUser(ctx, gluonID); err != nil {
|
|
||||||
return fmt.Errorf("failed to remove user from IMAP server: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := user.RemoveGluonID(event.AddressID, gluonID); err != nil {
|
|
||||||
return fmt.Errorf("failed to remove gluon ID for address: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bridge *Bridge) handleUserAddressDeleted(ctx context.Context, user *user.User, event events.UserAddressDeleted) error {
|
|
||||||
if user.GetAddressMode() == vault.CombinedMode {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
gluonID, ok := user.GetGluonID(event.AddressID)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("gluon ID not found for address %s", event.AddressID)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := bridge.serverManager.handleRemoveGluonUser(ctx, gluonID); err != nil {
|
|
||||||
return fmt.Errorf("failed to remove user from IMAP server: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := user.RemoveGluonID(event.AddressID, gluonID); err != nil {
|
|
||||||
return fmt.Errorf("failed to remove gluon ID for address: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bridge *Bridge) handleUserRefreshed(ctx context.Context, user *user.User, event events.UserRefreshed) error {
|
|
||||||
return safe.RLockRet(func() error {
|
|
||||||
if event.CancelEventPool {
|
|
||||||
user.CancelSyncAndEventPoll()
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := bridge.removeIMAPUser(ctx, user, true); err != nil {
|
|
||||||
return fmt.Errorf("failed to remove IMAP user: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := bridge.addIMAPUser(ctx, user); err != nil {
|
|
||||||
return fmt.Errorf("failed to add IMAP user: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}, bridge.usersLock)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) handleUserDeauth(ctx context.Context, user *user.User) {
|
func (bridge *Bridge) handleUserDeauth(ctx context.Context, user *user.User) {
|
||||||
safe.Lock(func() {
|
safe.Lock(func() {
|
||||||
bridge.logoutUser(ctx, user, false, false)
|
bridge.logoutUser(ctx, user, false, false, false)
|
||||||
|
user.ReportConfigStatusFailure("User deauth.")
|
||||||
}, bridge.usersLock)
|
}, bridge.usersLock)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) handleUserBadEvent(_ context.Context, user *user.User, event events.UserBadEvent) {
|
func (bridge *Bridge) handleUserBadEvent(ctx context.Context, user *user.User, event events.UserBadEvent) {
|
||||||
safe.Lock(func() {
|
safe.Lock(func() {
|
||||||
if rerr := bridge.reporter.ReportMessageWithContext("Failed to handle event", reporter.Context{
|
if rerr := bridge.reporter.ReportMessageWithContext("Failed to handle event", reporter.Context{
|
||||||
"user_id": user.ID(),
|
"user_id": user.ID(),
|
||||||
@ -183,12 +61,7 @@ func (bridge *Bridge) handleUserBadEvent(_ context.Context, user *user.User, eve
|
|||||||
logrus.WithError(rerr).Error("Failed to report failed event handling")
|
logrus.WithError(rerr).Error("Failed to report failed event handling")
|
||||||
}
|
}
|
||||||
|
|
||||||
user.CancelSyncAndEventPoll()
|
user.OnBadEvent(ctx)
|
||||||
|
|
||||||
// Disable IMAP user
|
|
||||||
if err := bridge.removeIMAPUser(context.Background(), user, false); err != nil {
|
|
||||||
logrus.WithError(err).Error("Failed to remove IMAP user")
|
|
||||||
}
|
|
||||||
}, bridge.usersLock)
|
}, bridge.usersLock)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -70,9 +70,11 @@ func prepareMobileConfig(
|
|||||||
password []byte,
|
password []byte,
|
||||||
) *mobileconfig.Config {
|
) *mobileconfig.Config {
|
||||||
return &mobileconfig.Config{
|
return &mobileconfig.Config{
|
||||||
DisplayName: username,
|
DisplayName: username,
|
||||||
EmailAddress: addresses,
|
EmailAddress: addresses,
|
||||||
Identifier: "protonmail " + username + strconv.FormatInt(time.Now().Unix(), 10),
|
AccountName: username,
|
||||||
|
AccountDescription: username,
|
||||||
|
Identifier: "protonmail " + username + strconv.FormatInt(time.Now().Unix(), 10),
|
||||||
IMAP: &mobileconfig.IMAP{
|
IMAP: &mobileconfig.IMAP{
|
||||||
Hostname: hostname,
|
Hostname: hostname,
|
||||||
Port: imapPort,
|
Port: imapPort,
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
@ -17,7 +17,13 @@
|
|||||||
|
|
||||||
package events
|
package events
|
||||||
|
|
||||||
import "fmt"
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/gluon/async"
|
||||||
|
"github.com/ProtonMail/gluon/watcher"
|
||||||
|
)
|
||||||
|
|
||||||
type Event interface {
|
type Event interface {
|
||||||
fmt.Stringer
|
fmt.Stringer
|
||||||
@ -28,3 +34,30 @@ type Event interface {
|
|||||||
type eventBase struct{}
|
type eventBase struct{}
|
||||||
|
|
||||||
func (eventBase) _isEvent() {}
|
func (eventBase) _isEvent() {}
|
||||||
|
|
||||||
|
type EventPublisher interface {
|
||||||
|
PublishEvent(ctx context.Context, event Event)
|
||||||
|
}
|
||||||
|
|
||||||
|
type NullEventPublisher struct{}
|
||||||
|
|
||||||
|
func (NullEventPublisher) PublishEvent(_ context.Context, _ Event) {}
|
||||||
|
|
||||||
|
type Subscription interface {
|
||||||
|
Add(ofType ...Event) *watcher.Watcher[Event]
|
||||||
|
Remove(watcher *watcher.Watcher[Event])
|
||||||
|
}
|
||||||
|
|
||||||
|
type NullSubscription struct{}
|
||||||
|
|
||||||
|
func (n NullSubscription) Add(ofType ...Event) *watcher.Watcher[Event] {
|
||||||
|
return watcher.New[Event](&async.NoopPanicHandler{}, ofType...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n NullSubscription) Remove(watcher *watcher.Watcher[Event]) {
|
||||||
|
watcher.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNullSubscription() *NullSubscription {
|
||||||
|
return &NullSubscription{}
|
||||||
|
}
|
||||||
|
|||||||
48
internal/events/mocks/mocks.go
Normal file
48
internal/events/mocks/mocks.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
// Code generated by MockGen. DO NOT EDIT.
|
||||||
|
// Source: github.com/ProtonMail/proton-bridge/v3/internal/events (interfaces: EventPublisher)
|
||||||
|
|
||||||
|
// Package mocks is a generated GoMock package.
|
||||||
|
package mocks
|
||||||
|
|
||||||
|
import (
|
||||||
|
context "context"
|
||||||
|
reflect "reflect"
|
||||||
|
|
||||||
|
events "github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
|
gomock "github.com/golang/mock/gomock"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockEventPublisher is a mock of EventPublisher interface.
|
||||||
|
type MockEventPublisher struct {
|
||||||
|
ctrl *gomock.Controller
|
||||||
|
recorder *MockEventPublisherMockRecorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockEventPublisherMockRecorder is the mock recorder for MockEventPublisher.
|
||||||
|
type MockEventPublisherMockRecorder struct {
|
||||||
|
mock *MockEventPublisher
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockEventPublisher creates a new mock instance.
|
||||||
|
func NewMockEventPublisher(ctrl *gomock.Controller) *MockEventPublisher {
|
||||||
|
mock := &MockEventPublisher{ctrl: ctrl}
|
||||||
|
mock.recorder = &MockEventPublisherMockRecorder{mock}
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||||
|
func (m *MockEventPublisher) EXPECT() *MockEventPublisherMockRecorder {
|
||||||
|
return m.recorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublishEvent mocks base method.
|
||||||
|
func (m *MockEventPublisher) PublishEvent(arg0 context.Context, arg1 events.Event) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
m.ctrl.Call(m, "PublishEvent", arg0, arg1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublishEvent indicates an expected call of PublishEvent.
|
||||||
|
func (mr *MockEventPublisherMockRecorder) PublishEvent(arg0, arg1 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PublishEvent", reflect.TypeOf((*MockEventPublisher)(nil).PublishEvent), arg0, arg1)
|
||||||
|
}
|
||||||
@ -37,6 +37,22 @@ func (event IMAPServerStopped) String() string {
|
|||||||
return "IMAPServerStopped"
|
return "IMAPServerStopped"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type IMAPServerClosed struct {
|
||||||
|
eventBase
|
||||||
|
}
|
||||||
|
|
||||||
|
func (event IMAPServerClosed) String() string {
|
||||||
|
return "IMAPServerClosed"
|
||||||
|
}
|
||||||
|
|
||||||
|
type IMAPServerCreated struct {
|
||||||
|
eventBase
|
||||||
|
}
|
||||||
|
|
||||||
|
func (event IMAPServerCreated) String() string {
|
||||||
|
return "IMAPServerCreated"
|
||||||
|
}
|
||||||
|
|
||||||
type IMAPServerError struct {
|
type IMAPServerError struct {
|
||||||
eventBase
|
eventBase
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// 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/>.
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package bridge
|
package files
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -24,7 +24,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
func moveDir(from, to string) error {
|
func MoveDir(from, to string) error {
|
||||||
entries, err := os.ReadDir(from)
|
entries, err := os.ReadDir(from)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -36,7 +36,7 @@ func moveDir(from, to string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := moveDir(filepath.Join(from, entry.Name()), filepath.Join(to, entry.Name())); err != nil {
|
if err := MoveDir(filepath.Join(from, entry.Name()), filepath.Join(to, entry.Name())); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,12 +61,12 @@ func moveFile(from, to string) error {
|
|||||||
return os.Rename(from, to)
|
return os.Rename(from, to)
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyDir(from, to string) error {
|
func CopyDir(from, to string) error {
|
||||||
entries, err := os.ReadDir(from)
|
entries, err := os.ReadDir(from)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := createIfNotExists(to, 0o700); err != nil {
|
if err := CreateIfNotExists(to, 0o700); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
@ -74,11 +74,11 @@ func copyDir(from, to string) error {
|
|||||||
destPath := filepath.Join(to, entry.Name())
|
destPath := filepath.Join(to, entry.Name())
|
||||||
|
|
||||||
if entry.IsDir() {
|
if entry.IsDir() {
|
||||||
if err := copyDir(sourcePath, destPath); err != nil {
|
if err := CopyDir(sourcePath, destPath); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if err := copyFile(sourcePath, destPath); err != nil {
|
if err := CopyFile(sourcePath, destPath); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -86,7 +86,7 @@ func copyDir(from, to string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyFile(srcFile, dstFile string) error {
|
func CopyFile(srcFile, dstFile string) error {
|
||||||
out, err := os.Create(filepath.Clean(dstFile))
|
out, err := os.Create(filepath.Clean(dstFile))
|
||||||
defer func(out *os.File) {
|
defer func(out *os.File) {
|
||||||
_ = out.Close()
|
_ = out.Close()
|
||||||
@ -113,7 +113,7 @@ func copyFile(srcFile, dstFile string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func exists(filePath string) bool {
|
func Exists(filePath string) bool {
|
||||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -121,8 +121,8 @@ func exists(filePath string) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func createIfNotExists(dir string, perm os.FileMode) error {
|
func CreateIfNotExists(dir string, perm os.FileMode) error {
|
||||||
if exists(dir) {
|
if Exists(dir) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// 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/>.
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package bridge
|
package files
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
@ -41,7 +41,7 @@ func TestMoveDir(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Move the files.
|
// Move the files.
|
||||||
if err := moveDir(from, to); err != nil {
|
if err := MoveDir(from, to); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,6 +30,7 @@ import (
|
|||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/credentials/insecure"
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
"google.golang.org/protobuf/types/known/emptypb"
|
"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.
|
// TryRaise tries to raise the application by dialing the focus service.
|
||||||
@ -38,7 +39,7 @@ func TryRaise(settingsPath string) bool {
|
|||||||
var raised bool
|
var raised bool
|
||||||
|
|
||||||
if err := withClientConn(context.Background(), settingsPath, func(ctx context.Context, client proto.FocusClient) error {
|
if err := withClientConn(context.Background(), settingsPath, func(ctx context.Context, client proto.FocusClient) error {
|
||||||
if _, err := client.Raise(ctx, &emptypb.Empty{}); err != nil {
|
if _, err := client.Raise(ctx, &wrapperspb.StringValue{Value: "TryRaise"}); err != nil {
|
||||||
return fmt.Errorf("failed to call client.Raise: %w", err)
|
return fmt.Errorf("failed to call client.Raise: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -18,7 +18,7 @@
|
|||||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// protoc-gen-go v1.28.0
|
// protoc-gen-go v1.28.0
|
||||||
// protoc v3.21.3
|
// protoc v3.21.12
|
||||||
// source: focus.proto
|
// source: focus.proto
|
||||||
|
|
||||||
package proto
|
package proto
|
||||||
@ -27,6 +27,7 @@ import (
|
|||||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||||
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
||||||
|
wrapperspb "google.golang.org/protobuf/types/known/wrapperspb"
|
||||||
reflect "reflect"
|
reflect "reflect"
|
||||||
sync "sync"
|
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,
|
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,
|
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,
|
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, 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,
|
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,
|
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x32, 0x81,
|
||||||
0x0a, 0x05, 0x46, 0x6f, 0x63, 0x75, 0x73, 0x12, 0x37, 0x0a, 0x05, 0x52, 0x61, 0x69, 0x73, 0x65,
|
0x01, 0x0a, 0x05, 0x46, 0x6f, 0x63, 0x75, 0x73, 0x12, 0x3d, 0x0a, 0x05, 0x52, 0x61, 0x69, 0x73,
|
||||||
0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
|
0x65, 0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
|
||||||
0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
|
0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a,
|
||||||
0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79,
|
0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
|
||||||
0x12, 0x39, 0x0a, 0x07, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f,
|
0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x39, 0x0a, 0x07, 0x56, 0x65, 0x72, 0x73, 0x69,
|
||||||
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d,
|
0x6f, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
|
||||||
0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x66, 0x6f, 0x63, 0x75, 0x73, 0x2e, 0x56, 0x65, 0x72, 0x73,
|
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x66, 0x6f, 0x63,
|
||||||
0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x3d, 0x5a, 0x3b, 0x67,
|
0x75, 0x73, 0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
|
||||||
0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x6e,
|
0x73, 0x65, 0x42, 0x3d, 0x5a, 0x3b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
|
||||||
0x4d, 0x61, 0x69, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x6e, 0x2d, 0x62, 0x72, 0x69, 0x64,
|
0x2f, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x6e, 0x4d, 0x61, 0x69, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74,
|
||||||
0x67, 0x65, 0x2f, 0x76, 0x33, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x66,
|
0x6f, 0x6e, 0x2d, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x2f, 0x76, 0x33, 0x2f, 0x69, 0x6e, 0x74,
|
||||||
0x6f, 0x63, 0x75, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
|
0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x66, 0x6f, 0x63, 0x75, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74,
|
||||||
0x6f, 0x33,
|
0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -123,13 +126,14 @@ func file_focus_proto_rawDescGZIP() []byte {
|
|||||||
|
|
||||||
var file_focus_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
|
var file_focus_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
|
||||||
var file_focus_proto_goTypes = []interface{}{
|
var file_focus_proto_goTypes = []interface{}{
|
||||||
(*VersionResponse)(nil), // 0: focus.VersionResponse
|
(*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{
|
var file_focus_proto_depIdxs = []int32{
|
||||||
1, // 0: focus.Focus.Raise:input_type -> google.protobuf.Empty
|
1, // 0: focus.Focus.Raise:input_type -> google.protobuf.StringValue
|
||||||
1, // 1: focus.Focus.Version:input_type -> google.protobuf.Empty
|
2, // 1: focus.Focus.Version:input_type -> google.protobuf.Empty
|
||||||
1, // 2: focus.Focus.Raise:output_type -> google.protobuf.Empty
|
2, // 2: focus.Focus.Raise:output_type -> google.protobuf.Empty
|
||||||
0, // 3: focus.Focus.Version:output_type -> focus.VersionResponse
|
0, // 3: focus.Focus.Version:output_type -> focus.VersionResponse
|
||||||
2, // [2:4] is the sub-list for method output_type
|
2, // [2:4] is the sub-list for method output_type
|
||||||
0, // [0:2] is the sub-list for method input_type
|
0, // [0:2] is the sub-list for method input_type
|
||||||
|
|||||||
@ -18,6 +18,7 @@
|
|||||||
syntax = "proto3";
|
syntax = "proto3";
|
||||||
|
|
||||||
import "google/protobuf/empty.proto";
|
import "google/protobuf/empty.proto";
|
||||||
|
import "google/protobuf/wrappers.proto";
|
||||||
|
|
||||||
option go_package = "github.com/ProtonMail/proton-bridge/v3/internal/focus/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 Declaration
|
||||||
//**********************************************************************************************************************≠––
|
//**********************************************************************************************************************≠––
|
||||||
service Focus {
|
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);
|
rpc Version(google.protobuf.Empty) returns (VersionResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// - protoc-gen-go-grpc v1.2.0
|
// - protoc-gen-go-grpc v1.2.0
|
||||||
// - protoc v3.21.3
|
// - protoc v3.21.12
|
||||||
// source: focus.proto
|
// source: focus.proto
|
||||||
|
|
||||||
package proto
|
package proto
|
||||||
@ -12,6 +12,7 @@ import (
|
|||||||
codes "google.golang.org/grpc/codes"
|
codes "google.golang.org/grpc/codes"
|
||||||
status "google.golang.org/grpc/status"
|
status "google.golang.org/grpc/status"
|
||||||
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
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
|
// 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.
|
// 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 {
|
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)
|
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}
|
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)
|
out := new(emptypb.Empty)
|
||||||
err := c.cc.Invoke(ctx, "/focus.Focus/Raise", in, out, opts...)
|
err := c.cc.Invoke(ctx, "/focus.Focus/Raise", in, out, opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -57,7 +58,7 @@ func (c *focusClient) Version(ctx context.Context, in *emptypb.Empty, opts ...gr
|
|||||||
// All implementations must embed UnimplementedFocusServer
|
// All implementations must embed UnimplementedFocusServer
|
||||||
// for forward compatibility
|
// for forward compatibility
|
||||||
type FocusServer interface {
|
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)
|
Version(context.Context, *emptypb.Empty) (*VersionResponse, error)
|
||||||
mustEmbedUnimplementedFocusServer()
|
mustEmbedUnimplementedFocusServer()
|
||||||
}
|
}
|
||||||
@ -66,7 +67,7 @@ type FocusServer interface {
|
|||||||
type UnimplementedFocusServer struct {
|
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")
|
return nil, status.Errorf(codes.Unimplemented, "method Raise not implemented")
|
||||||
}
|
}
|
||||||
func (UnimplementedFocusServer) Version(context.Context, *emptypb.Empty) (*VersionResponse, error) {
|
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) {
|
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 {
|
if err := dec(in); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -98,7 +99,7 @@ func _Focus_Raise_Handler(srv interface{}, ctx context.Context, dec func(interfa
|
|||||||
FullMethod: "/focus.Focus/Raise",
|
FullMethod: "/focus.Focus/Raise",
|
||||||
}
|
}
|
||||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
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)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,6 +30,7 @@ import (
|
|||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/protobuf/types/known/emptypb"
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
|
"google.golang.org/protobuf/types/known/wrapperspb"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -45,6 +46,7 @@ type Service struct {
|
|||||||
raiseCh chan struct{}
|
raiseCh chan struct{}
|
||||||
version *semver.Version
|
version *semver.Version
|
||||||
|
|
||||||
|
log *logrus.Entry
|
||||||
panicHandler async.PanicHandler
|
panicHandler async.PanicHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,13 +57,14 @@ func NewService(locator service.Locator, version *semver.Version, panicHandler a
|
|||||||
server: grpc.NewServer(),
|
server: grpc.NewServer(),
|
||||||
raiseCh: make(chan struct{}, 1),
|
raiseCh: make(chan struct{}, 1),
|
||||||
version: version,
|
version: version,
|
||||||
|
log: logrus.WithField("pkg", "focus/service"),
|
||||||
panicHandler: panicHandler,
|
panicHandler: panicHandler,
|
||||||
}
|
}
|
||||||
|
|
||||||
proto.RegisterFocusServer(serv.server, serv)
|
proto.RegisterFocusServer(serv.server, serv)
|
||||||
|
|
||||||
if listener, err := net.Listen("tcp", net.JoinHostPort(Host, fmt.Sprint(0))); err != nil {
|
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 {
|
} else {
|
||||||
config := service.Config{}
|
config := service.Config{}
|
||||||
// retrieve the port assigned by the system, so that we can put it in the config file.
|
// 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
|
config.Port = address.Port
|
||||||
if path, err := service.SaveGRPCServerConfigFile(locator, &config, serverConfigFileName); err != nil {
|
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 {
|
} 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() {
|
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.
|
// 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{}{}
|
service.raiseCh <- struct{}{}
|
||||||
return &emptypb.Empty{}, nil
|
return &emptypb.Empty{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Version implements the gRPC FocusService interface; it returns the version of the service.
|
// Version implements the gRPC FocusService interface; it returns the version of the service.
|
||||||
func (service *Service) Version(context.Context, *emptypb.Empty) (*proto.VersionResponse, error) {
|
func (service *Service) Version(context.Context, *emptypb.Empty) (*proto.VersionResponse, error) {
|
||||||
|
service.log.Debug("Version")
|
||||||
return &proto.VersionResponse{
|
return &proto.VersionResponse{
|
||||||
Version: service.version.Original(),
|
Version: service.version.Original(),
|
||||||
}, nil
|
}, 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
|
||||||
@ -385,6 +385,14 @@ Status GRPCService::Login(ServerContext *, LoginRequest const *request, Empty *)
|
|||||||
app().log().debug(__FUNCTION__);
|
app().log().debug(__FUNCTION__);
|
||||||
UsersTab &usersTab = app().mainWindow().usersTab();
|
UsersTab &usersTab = app().mainWindow().usersTab();
|
||||||
loginUsername_ = QString::fromStdString(request->username());
|
loginUsername_ = QString::fromStdString(request->username());
|
||||||
|
|
||||||
|
SPUser const& user = usersTab.userTable().userWithUsernameOrEmail(QString::fromStdString(request->username()));
|
||||||
|
if (user) {
|
||||||
|
qtProxy_.sendDelayedEvent(newLoginAlreadyLoggedInEvent(user->id()));
|
||||||
|
return Status::OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (usersTab.nextUserUsernamePasswordError()) {
|
if (usersTab.nextUserUsernamePasswordError()) {
|
||||||
qtProxy_.sendDelayedEvent(newLoginError(LoginErrorType::USERNAME_PASSWORD_ERROR, usersTab.usernamePasswordErrorMessage()));
|
qtProxy_.sendDelayedEvent(newLoginError(LoginErrorType::USERNAME_PASSWORD_ERROR, usersTab.usernamePasswordErrorMessage()));
|
||||||
return Status::OK;
|
return Status::OK;
|
||||||
@ -826,7 +834,7 @@ bool GRPCService::sendEvent(SPStreamEvent const &event) {
|
|||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
void GRPCService::finishLogin() {
|
void GRPCService::finishLogin() {
|
||||||
UsersTab &usersTab = app().mainWindow().usersTab();
|
UsersTab &usersTab = app().mainWindow().usersTab();
|
||||||
SPUser user = usersTab.userWithUsername(loginUsername_);
|
SPUser user = usersTab.userWithUsernameOrEmail(loginUsername_);
|
||||||
bool const alreadyExist = user.get();
|
bool const alreadyExist = user.get();
|
||||||
if (!user) {
|
if (!user) {
|
||||||
user = randomUser();
|
user = randomUser();
|
||||||
|
|||||||
@ -272,8 +272,8 @@ bridgepp::SPUser UsersTab::userWithID(QString const &userID) {
|
|||||||
/// \return The user with the given username.
|
/// \return The user with the given username.
|
||||||
/// \return A null pointer if the user is not in the list.
|
/// \return A null pointer if the user is not in the list.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
bridgepp::SPUser UsersTab::userWithUsername(QString const &username) {
|
bridgepp::SPUser UsersTab::userWithUsernameOrEmail(QString const &username) {
|
||||||
return users_.userWithUsername(username);
|
return users_.userWithUsernameOrEmail(username);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -38,7 +38,7 @@ public: // member functions.
|
|||||||
UsersTab &operator=(UsersTab &&) = delete; ///< Disabled move assignment operator.
|
UsersTab &operator=(UsersTab &&) = delete; ///< Disabled move assignment operator.
|
||||||
UserTable &userTable(); ///< Returns a reference to the user table.
|
UserTable &userTable(); ///< Returns a reference to the user table.
|
||||||
bridgepp::SPUser userWithID(QString const &userID); ///< Get the user with the given ID.
|
bridgepp::SPUser userWithID(QString const &userID); ///< Get the user with the given ID.
|
||||||
bridgepp::SPUser userWithUsername(QString const &username); ///< Get the user with the given username.
|
bridgepp::SPUser userWithUsernameOrEmail(QString const &username); ///< Get the user with the given username.
|
||||||
bool nextUserUsernamePasswordError() const; ///< Check if next user login should trigger a username/password error.
|
bool nextUserUsernamePasswordError() const; ///< Check if next user login should trigger a username/password error.
|
||||||
bool nextUserFreeUserError() const; ///< Check if next user login should trigger a Free user error.
|
bool nextUserFreeUserError() const; ///< Check if next user login should trigger a Free user error.
|
||||||
bool nextUserTFARequired() const; ///< Check if next user login should requires 2FA.
|
bool nextUserTFARequired() const; ///< Check if next user login should requires 2FA.
|
||||||
|
|||||||
@ -150,13 +150,16 @@ bridgepp::SPUser UserTable::userWithID(QString const &userID) {
|
|||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \param[in] username The username.
|
/// \param[in] username The username, or any email address attached to the account.
|
||||||
/// \return The user with the given username.
|
/// \return The user with the given username.
|
||||||
/// \return A null pointer if the user is not in the list.
|
/// \return A null pointer if the user is not in the list.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
bridgepp::SPUser UserTable::userWithUsername(QString const &username) {
|
bridgepp::SPUser UserTable::userWithUsernameOrEmail(QString const &username) {
|
||||||
QList<SPUser>::const_iterator it = std::find_if(users_.constBegin(), users_.constEnd(), [&username](SPUser const &user) -> bool {
|
QList<SPUser>::const_iterator it = std::find_if(users_.constBegin(), users_.constEnd(), [&username](SPUser const &user) -> bool {
|
||||||
return user->username() == username;
|
if (user->username().compare(username, Qt::CaseInsensitive) == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return user->addresses().contains(username, Qt::CaseInsensitive);
|
||||||
});
|
});
|
||||||
|
|
||||||
return it == users_.end() ? nullptr : *it;
|
return it == users_.end() ? nullptr : *it;
|
||||||
|
|||||||
@ -40,7 +40,7 @@ public: // member functions.
|
|||||||
void append(bridgepp::SPUser const &user); ///< Append a user.
|
void append(bridgepp::SPUser const &user); ///< Append a user.
|
||||||
bridgepp::SPUser userAtIndex(qint32 index); ///< Return the user at the given index.
|
bridgepp::SPUser userAtIndex(qint32 index); ///< Return the user at the given index.
|
||||||
bridgepp::SPUser userWithID(QString const &userID); ///< Return the user with a given id.
|
bridgepp::SPUser userWithID(QString const &userID); ///< Return the user with a given id.
|
||||||
bridgepp::SPUser userWithUsername(QString const &username); ///< Return the user with a given username.
|
bridgepp::SPUser userWithUsernameOrEmail(QString const &username); ///< Return the user with a given username.
|
||||||
qint32 indexOfUser(QString const &userID); ///< Return the index of a given User.
|
qint32 indexOfUser(QString const &userID); ///< Return the index of a given User.
|
||||||
void touch(qint32 index); ///< touch the user at a given index (indicates it has been modified).
|
void touch(qint32 index); ///< touch the user at a given index (indicates it has been modified).
|
||||||
void touch(QString const& userID); ///< touch the user with the given userID (indicates it has been modified).
|
void touch(QString const& userID); ///< touch the user with the given userID (indicates it has been modified).
|
||||||
|
|||||||
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
|
||||||
@ -20,6 +20,7 @@
|
|||||||
#include "QMLBackend.h"
|
#include "QMLBackend.h"
|
||||||
#include "SentryUtils.h"
|
#include "SentryUtils.h"
|
||||||
#include "Settings.h"
|
#include "Settings.h"
|
||||||
|
#include <bridgepp/CLI/CLIUtils.h>
|
||||||
#include <bridgepp/GRPC/GRPCClient.h>
|
#include <bridgepp/GRPC/GRPCClient.h>
|
||||||
#include <bridgepp/Exception/Exception.h>
|
#include <bridgepp/Exception/Exception.h>
|
||||||
#include <bridgepp/ProcessMonitor.h>
|
#include <bridgepp/ProcessMonitor.h>
|
||||||
@ -101,23 +102,47 @@ void AppController::onFatalError(Exception const &exception) {
|
|||||||
qApp->exit(EXIT_FAILURE);
|
qApp->exit(EXIT_FAILURE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \param[in] isCrashing Is the restart triggered by a crash.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
void AppController::restart(bool isCrashing) {
|
void AppController::restart(bool isCrashing) {
|
||||||
if (!launcher_.isEmpty()) {
|
if (launcher_.isEmpty()) {
|
||||||
QProcess p;
|
return;
|
||||||
log_->info(QString("Restarting - App : %1 - Args : %2").arg(launcher_, launcherArgs_.join(" ")));
|
|
||||||
QStringList args = launcherArgs_;
|
|
||||||
if (isCrashing) {
|
|
||||||
args.append(noWindowFlag);
|
|
||||||
}
|
|
||||||
|
|
||||||
p.startDetached(launcher_, args);
|
|
||||||
p.waitForStarted();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QProcess p;
|
||||||
|
QStringList args = stripStringParameterFromCommandLine("--session-id", launcherArgs_);
|
||||||
|
if (isCrashing) {
|
||||||
|
args.append(noWindowFlag);
|
||||||
|
}
|
||||||
|
|
||||||
|
log_->info(QString("Restarting - App : %1 - Args : %2").arg(launcher_, args.join(" ")));
|
||||||
|
p.startDetached(launcher_, args);
|
||||||
|
p.waitForStarted();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \param[in] launcher The launcher.
|
||||||
|
/// \param[in] args The launcher arguments.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
void AppController::setLauncherArgs(const QString &launcher, const QStringList &args) {
|
void AppController::setLauncherArgs(const QString &launcher, const QStringList &args) {
|
||||||
launcher_ = launcher;
|
launcher_ = launcher;
|
||||||
launcherArgs_ = args;
|
launcherArgs_ = args;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \param[in] sessionID The sessionID.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
void AppController::setSessionID(const QString &sessionID) {
|
||||||
|
sessionID_ = sessionID;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \return The sessionID.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
QString AppController::sessionID() {
|
||||||
|
return sessionID_;
|
||||||
|
}
|
||||||
|
|||||||
@ -37,7 +37,7 @@ class Exception;
|
|||||||
/// \brief App controller class.
|
/// \brief App controller class.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
class AppController : public QObject {
|
class AppController : public QObject {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
friend AppController &app();
|
friend AppController &app();
|
||||||
|
|
||||||
public: // member functions.
|
public: // member functions.
|
||||||
@ -52,10 +52,12 @@ public: // member functions.
|
|||||||
std::unique_ptr<bridgepp::Overseer> &bridgeOverseer() { return bridgeOverseer_; }; ///< Returns a reference the bridge overseer
|
std::unique_ptr<bridgepp::Overseer> &bridgeOverseer() { return bridgeOverseer_; }; ///< Returns a reference the bridge overseer
|
||||||
bridgepp::ProcessMonitor *bridgeMonitor() const; ///< Return the bridge worker.
|
bridgepp::ProcessMonitor *bridgeMonitor() const; ///< Return the bridge worker.
|
||||||
Settings &settings();; ///< Return the application settings.
|
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:
|
public slots:
|
||||||
void onFatalError(bridgepp::Exception const& e); ///< Handle fatal errors.
|
void onFatalError(bridgepp::Exception const &e); ///< Handle fatal errors.
|
||||||
|
|
||||||
private: // member functions
|
private: // member functions
|
||||||
AppController(); ///< Default constructor.
|
AppController(); ///< Default constructor.
|
||||||
@ -67,8 +69,9 @@ private: // data members
|
|||||||
std::unique_ptr<bridgepp::Log> log_; ///< The log.
|
std::unique_ptr<bridgepp::Log> log_; ///< The log.
|
||||||
std::unique_ptr<bridgepp::Overseer> bridgeOverseer_; ///< The overseer for the bridge monitor worker.
|
std::unique_ptr<bridgepp::Overseer> bridgeOverseer_; ///< The overseer for the bridge monitor worker.
|
||||||
std::unique_ptr<Settings> settings_; ///< The application settings.
|
std::unique_ptr<Settings> settings_; ///< The application settings.
|
||||||
QString launcher_;
|
QString launcher_; ///< The launcher.
|
||||||
QStringList launcherArgs_;
|
QStringList launcherArgs_; ///< The launcher arguments.
|
||||||
|
QString sessionID_; ///< The sessionID.
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -19,6 +19,7 @@
|
|||||||
#include "Pch.h"
|
#include "Pch.h"
|
||||||
#include "CommandLine.h"
|
#include "CommandLine.h"
|
||||||
#include "Settings.h"
|
#include "Settings.h"
|
||||||
|
#include <bridgepp/SessionID/SessionID.h>
|
||||||
|
|
||||||
|
|
||||||
using namespace bridgepp;
|
using namespace bridgepp;
|
||||||
@ -142,5 +143,14 @@ CommandLineOptions parseCommandLine(int argc, char *argv[]) {
|
|||||||
|
|
||||||
options.logLevel = parseLogLevel(argc, 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;
|
return options;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -43,12 +43,9 @@ EventStreamReader::EventStreamReader(QObject *parent)
|
|||||||
void EventStreamReader::run() {
|
void EventStreamReader::run() {
|
||||||
try {
|
try {
|
||||||
emit started();
|
emit started();
|
||||||
|
// 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
|
||||||
grpc::Status const status = app().grpc().runEventStreamReader();
|
// want this to generate a sentry report.
|
||||||
if (!status.ok()) {
|
app().grpc().runEventStreamReader();
|
||||||
throw Exception(QString::fromStdString(status.error_message()));
|
|
||||||
}
|
|
||||||
|
|
||||||
emit finished();
|
emit finished();
|
||||||
}
|
}
|
||||||
catch (Exception const &e) {
|
catch (Exception const &e) {
|
||||||
|
|||||||
@ -19,7 +19,6 @@
|
|||||||
#include "LogUtils.h"
|
#include "LogUtils.h"
|
||||||
#include "BuildConfig.h"
|
#include "BuildConfig.h"
|
||||||
#include <bridgepp/Log/LogUtils.h>
|
#include <bridgepp/Log/LogUtils.h>
|
||||||
#include <bridgepp/BridgeUtils.h>
|
|
||||||
|
|
||||||
|
|
||||||
using namespace bridgepp;
|
using namespace bridgepp;
|
||||||
@ -33,15 +32,10 @@ Log &initLog() {
|
|||||||
log.registerAsQtMessageHandler();
|
log.registerAsQtMessageHandler();
|
||||||
log.setEchoInConsole(true);
|
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
|
// create new GUI log file
|
||||||
QString error;
|
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);
|
log.error(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -33,7 +33,7 @@ using namespace bridgepp;
|
|||||||
/// \brief handle notification of attempt to re-open the application.
|
/// \brief handle notification of attempt to re-open the application.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
void applicationShouldHandleReopen(id, SEL) {
|
void applicationShouldHandleReopen(id, SEL) {
|
||||||
app().backend().showMainWindow();
|
app().backend().showMainWindow("macOS applicationShouldHandleReopen notification");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -37,6 +37,15 @@
|
|||||||
using namespace bridgepp;
|
using namespace bridgepp;
|
||||||
|
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
|
||||||
|
QString const bugReportFile = ":qml/Resources/bug_report_flow.json";
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
//
|
//
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
@ -61,7 +70,7 @@ void QMLBackend::init(GRPCConfig const &serviceConfig) {
|
|||||||
app().grpc().setLog(&log);
|
app().grpc().setLog(&log);
|
||||||
this->connectGrpcEvents();
|
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.");
|
app().log().info("Connected to backend via gRPC service.");
|
||||||
|
|
||||||
QString bridgeVer;
|
QString bridgeVer;
|
||||||
@ -89,6 +98,8 @@ void QMLBackend::init(GRPCConfig const &serviceConfig) {
|
|||||||
this->setUseSSLForIMAP(sslForIMAP);
|
this->setUseSSLForIMAP(sslForIMAP);
|
||||||
this->setUseSSLForSMTP(sslForSMTP);
|
this->setUseSSLForSMTP(sslForSMTP);
|
||||||
this->retrieveUserList();
|
this->retrieveUserList();
|
||||||
|
if (!reportFlow_.parse(bugReportFile))
|
||||||
|
app().log().error(QString("Cannot parse BugReportFlow description file: %1").arg(bugReportFile));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -109,6 +120,52 @@ UserList const &QMLBackend::users() const {
|
|||||||
return *users_;
|
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)
|
/// \return The build year as a string (e.g. 2023)
|
||||||
@ -164,6 +221,63 @@ bool QMLBackend::areSameFileOrFolder(QUrl const &lhs, QUrl const &rhs) const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \param[in] categoryId The id of the bug category.
|
||||||
|
/// \return Set of question for this category.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
QString QMLBackend::getBugCategory(quint8 categoryId) const {
|
||||||
|
return reportFlow_.getCategory(categoryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \param[in] categoryId The id of the bug category.
|
||||||
|
/// \return Set of question for this category.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
QVariantList QMLBackend::getQuestionSet(quint8 categoryId) const {
|
||||||
|
QVariantList list = reportFlow_.questionSet(categoryId);
|
||||||
|
if (list.count() == 0)
|
||||||
|
app().log().error(QString("Bug category not found (id: %1)").arg(categoryId));
|
||||||
|
return list;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \param[in] questionId The id of the question.
|
||||||
|
/// \param[in] answer The answer to that question.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
void QMLBackend::setQuestionAnswer(quint8 questionId, QString const &answer) {
|
||||||
|
if (!reportFlow_.setAnswer(questionId, answer))
|
||||||
|
app().log().error(QString("Bug Report Question not found (id: %1)").arg(questionId));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \param[in] questionId The id of the question.
|
||||||
|
/// \return answer for the given question.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
QString QMLBackend::getQuestionAnswer(quint8 questionId) const {
|
||||||
|
return reportFlow_.getAnswer(questionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \param[in] categoryId The id of the question set.
|
||||||
|
/// \return concatenate answers for set of questions.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
QString QMLBackend::collectAnswers(quint8 categoryId) const {
|
||||||
|
return reportFlow_.collectAnswers(categoryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
//
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
void QMLBackend::clearAnswers() {
|
||||||
|
reportFlow_.clearAnswers();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \return The value for the 'showOnStartup' property.
|
/// \return The value for the 'showOnStartup' property.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
@ -535,6 +649,21 @@ QStringList QMLBackend::availableKeychain() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \return The value for the 'bugCategories' property.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
QVariantList QMLBackend::bugCategories() const {
|
||||||
|
return reportFlow_.categories();
|
||||||
|
}
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \return The value for the 'bugQuestions' property.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
QVariantList QMLBackend::bugQuestions() const {
|
||||||
|
return reportFlow_.questions();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \return The value for the 'currentKeychain' property.
|
/// \return The value for the 'currentKeychain' property.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
@ -640,7 +769,7 @@ void QMLBackend::login(QString const &username, QString const &password) const {
|
|||||||
HANDLE_EXCEPTION(
|
HANDLE_EXCEPTION(
|
||||||
if (username.compare("coco@bandicoot", Qt::CaseInsensitive) == 0) {
|
if (username.compare("coco@bandicoot", Qt::CaseInsensitive) == 0) {
|
||||||
throw Exception("User requested bridge-gui to crash by trying to log as coco@bandicoot",
|
throw Exception("User requested bridge-gui to crash by trying to log as coco@bandicoot",
|
||||||
"This error exists for test purposes and should be ignored.", __func__, tailOfLatestBridgeLog());
|
"This error exists for test purposes and should be ignored.", __func__, tailOfLatestBridgeLog(app().sessionID()));
|
||||||
}
|
}
|
||||||
app().grpc().login(username, password);
|
app().grpc().login(username, password);
|
||||||
)
|
)
|
||||||
@ -799,14 +928,15 @@ void QMLBackend::triggerReset() const {
|
|||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
|
/// \param[in] category The category of the bug.
|
||||||
/// \param[in] description The description of the bug.
|
/// \param[in] description The description of the bug.
|
||||||
/// \param[in] address The email address.
|
/// \param[in] address The email address.
|
||||||
/// \param[in] emailClient The email client.
|
/// \param[in] emailClient The email client.
|
||||||
/// \param[in] includeLogs Should the logs be included in the report.
|
/// \param[in] includeLogs Should the logs be included in the report.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
void QMLBackend::reportBug(QString const &description, QString const &address, QString const &emailClient, bool includeLogs) const {
|
void QMLBackend::reportBug(QString const &category, QString const &description, QString const &address, QString const &emailClient, bool includeLogs) const {
|
||||||
HANDLE_EXCEPTION(
|
HANDLE_EXCEPTION(
|
||||||
app().grpc().reportBug(description, address, emailClient, includeLogs);
|
app().grpc().reportBug(category, description, address, emailClient, includeLogs);
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -874,11 +1004,36 @@ void QMLBackend::sendBadEventUserFeedback(QString const &userID, bool doResync)
|
|||||||
if (!badEventDisplayQueue_.isEmpty()) {
|
if (!badEventDisplayQueue_.isEmpty()) {
|
||||||
// we introduce a small delay here, so that the user notices the dialog disappear and pops up again.
|
// 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()); });
|
QTimer::singleShot(500, [&]() { this->displayBadEventDialog(badEventDisplayQueue_.front()); });
|
||||||
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
///
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
void QMLBackend::notifyReportBugClicked() const {
|
||||||
|
HANDLE_EXCEPTION(
|
||||||
|
app().grpc().reportBugClicked();
|
||||||
|
)
|
||||||
|
}
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \param[in] client The selected Mail client for autoconfig.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
void QMLBackend::notifyAutoconfigClicked(QString const &client) const {
|
||||||
|
HANDLE_EXCEPTION(
|
||||||
|
app().grpc().autoconfigClicked(client);
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \param[in] article The url of the KB article.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
void QMLBackend::notifyKBArticleClicked(QString const &article) const {
|
||||||
|
HANDLE_EXCEPTION(
|
||||||
|
app().grpc().KBArticleClicked(article);
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
//
|
//
|
||||||
@ -923,6 +1078,25 @@ void QMLBackend::setUpdateTrayIcon(QString const &stateString, QString const &st
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \param[in] isOn Does bridge consider internet as on.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
void QMLBackend::internetStatusChanged(bool isOn) {
|
||||||
|
HANDLE_EXCEPTION(
|
||||||
|
if (isInternetOn_ == isOn) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isInternetOn_ = isOn;
|
||||||
|
if (isOn) {
|
||||||
|
emit internetOn();
|
||||||
|
} else {
|
||||||
|
emit internetOff();
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \param[in] imapPort The IMAP port.
|
/// \param[in] imapPort The IMAP port.
|
||||||
/// \param[in] smtpPort The SMTP port.
|
/// \param[in] smtpPort The SMTP port.
|
||||||
@ -1086,13 +1260,14 @@ void QMLBackend::connectGrpcEvents() {
|
|||||||
GRPCClient *client = &app().grpc();
|
GRPCClient *client = &app().grpc();
|
||||||
|
|
||||||
// app events
|
// 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::toggleAutostartFinished, this, &QMLBackend::toggleAutostartFinished);
|
||||||
connect(client, &GRPCClient::resetFinished, this, &QMLBackend::onResetFinished);
|
connect(client, &GRPCClient::resetFinished, this, &QMLBackend::onResetFinished);
|
||||||
connect(client, &GRPCClient::reportBugFinished, this, &QMLBackend::reportBugFinished);
|
connect(client, &GRPCClient::reportBugFinished, this, &QMLBackend::reportBugFinished);
|
||||||
connect(client, &GRPCClient::reportBugSuccess, this, &QMLBackend::bugReportSendSuccess);
|
connect(client, &GRPCClient::reportBugSuccess, this, &QMLBackend::bugReportSendSuccess);
|
||||||
|
connect(client, &GRPCClient::reportBugFallback, this, &QMLBackend::bugReportSendFallback);
|
||||||
connect(client, &GRPCClient::reportBugError, this, &QMLBackend::bugReportSendError);
|
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
|
// cache events
|
||||||
connect(client, &GRPCClient::diskCacheUnavailable, this, &QMLBackend::diskCacheUnavailable);
|
connect(client, &GRPCClient::diskCacheUnavailable, this, &QMLBackend::diskCacheUnavailable);
|
||||||
|
|||||||
@ -24,6 +24,7 @@
|
|||||||
#include "BuildConfig.h"
|
#include "BuildConfig.h"
|
||||||
#include "TrayIcon.h"
|
#include "TrayIcon.h"
|
||||||
#include "UserList.h"
|
#include "UserList.h"
|
||||||
|
#include <bridgepp/BugReportFlow/BugReportFlow.h>
|
||||||
#include <bridgepp/GRPC/GRPCClient.h>
|
#include <bridgepp/GRPC/GRPCClient.h>
|
||||||
#include <bridgepp/GRPC/GRPCUtils.h>
|
#include <bridgepp/GRPC/GRPCUtils.h>
|
||||||
#include <bridgepp/Worker/Overseer.h>
|
#include <bridgepp/Worker/Overseer.h>
|
||||||
@ -45,13 +46,24 @@ public: // member functions.
|
|||||||
void init(GRPCConfig const &serviceConfig); ///< Initialize the backend.
|
void init(GRPCConfig const &serviceConfig); ///< Initialize the backend.
|
||||||
bool waitForEventStreamReaderToFinish(qint32 timeoutMs); ///< Wait for the event stream reader to finish.
|
bool waitForEventStreamReaderToFinish(qint32 timeoutMs); ///< Wait for the event stream reader to finish.
|
||||||
UserList const& users() const; ///< Return the list of users
|
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.
|
// invocable 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.
|
Q_INVOKABLE static QString buildYear(); ///< Return the application build year.
|
||||||
Q_INVOKABLE QPoint getCursorPos() const; ///< Retrieve the cursor position.
|
Q_INVOKABLE QPoint getCursorPos() const; ///< Retrieve the cursor position.
|
||||||
Q_INVOKABLE bool isPortFree(int port) const; ///< Check if a given network port is available.
|
Q_INVOKABLE bool isPortFree(int port) const; ///< Check if a given network port is available.
|
||||||
Q_INVOKABLE QString nativePath(QUrl const &url) const; ///< Retrieve the native path of a local URL.
|
Q_INVOKABLE QString nativePath(QUrl const &url) const; ///< Retrieve the native path of a local URL.
|
||||||
Q_INVOKABLE bool areSameFileOrFolder(QUrl const &lhs, QUrl const &rhs) const; ///< Check if two local URL point to the same file.
|
Q_INVOKABLE bool areSameFileOrFolder(QUrl const &lhs, QUrl const &rhs) const; ///< Check if two local URL point to the same file.
|
||||||
|
Q_INVOKABLE QString getBugCategory(quint8 categoryId) const; ///< Get a Category name.
|
||||||
|
Q_INVOKABLE QVariantList getQuestionSet(quint8 categoryId) const; ///< Retrieve the set of question for a given bug category.
|
||||||
|
Q_INVOKABLE void setQuestionAnswer(quint8 questionId, QString const &answer); ///< Feed an answer for a given question.
|
||||||
|
Q_INVOKABLE QString getQuestionAnswer(quint8 questionId) const; ///< Get the answer for a given question.
|
||||||
|
Q_INVOKABLE QString collectAnswers(quint8 categoryId) const; ///< Collect answer for a given set of questions.
|
||||||
|
Q_INVOKABLE void clearAnswers(); ///< Clear all collected answers.
|
||||||
|
|
||||||
public: // Qt/QML properties. Note that the NOTIFY-er signal is required even for read-only properties (QML warning otherwise)
|
public: // Qt/QML properties. Note that the NOTIFY-er signal is required even for read-only properties (QML warning otherwise)
|
||||||
Q_PROPERTY(bool showOnStartup READ showOnStartup NOTIFY showOnStartupChanged)
|
Q_PROPERTY(bool showOnStartup READ showOnStartup NOTIFY showOnStartupChanged)
|
||||||
@ -82,10 +94,11 @@ public: // Qt/QML properties. Note that the NOTIFY-er signal is required even fo
|
|||||||
Q_PROPERTY(QString currentEmailClient READ currentEmailClient NOTIFY currentEmailClientChanged)
|
Q_PROPERTY(QString currentEmailClient READ currentEmailClient NOTIFY currentEmailClientChanged)
|
||||||
Q_PROPERTY(QStringList availableKeychain READ availableKeychain NOTIFY availableKeychainChanged)
|
Q_PROPERTY(QStringList availableKeychain READ availableKeychain NOTIFY availableKeychainChanged)
|
||||||
Q_PROPERTY(QString currentKeychain READ currentKeychain NOTIFY currentKeychainChanged)
|
Q_PROPERTY(QString currentKeychain READ currentKeychain NOTIFY currentKeychainChanged)
|
||||||
|
Q_PROPERTY(QVariantList bugCategories READ bugCategories NOTIFY bugCategoriesChanged)
|
||||||
|
Q_PROPERTY(QVariantList bugQuestions READ bugQuestions NOTIFY bugQuestionsChanged)
|
||||||
Q_PROPERTY(UserList *users MEMBER users_ NOTIFY usersChanged)
|
Q_PROPERTY(UserList *users MEMBER users_ NOTIFY usersChanged)
|
||||||
Q_PROPERTY(bool dockIconVisible READ dockIconVisible WRITE setDockIconVisible NOTIFY dockIconVisibleChanged)
|
Q_PROPERTY(bool dockIconVisible READ dockIconVisible WRITE setDockIconVisible NOTIFY dockIconVisibleChanged)
|
||||||
|
|
||||||
|
|
||||||
// Qt Property system setters & getters.
|
// Qt Property system setters & getters.
|
||||||
bool showOnStartup() const; ///< Getter for the 'showOnStartup' property.
|
bool showOnStartup() const; ///< Getter for the 'showOnStartup' property.
|
||||||
void setShowSplashScreen(bool show); ///< Setter for the 'showSplashScreen' property.
|
void setShowSplashScreen(bool show); ///< Setter for the 'showSplashScreen' property.
|
||||||
@ -120,6 +133,8 @@ public: // Qt/QML properties. Note that the NOTIFY-er signal is required even fo
|
|||||||
QString currentEmailClient() const; ///< Getter for the 'currentEmail' property.
|
QString currentEmailClient() const; ///< Getter for the 'currentEmail' property.
|
||||||
QStringList availableKeychain() const; ///< Getter for the 'availableKeychain' property.
|
QStringList availableKeychain() const; ///< Getter for the 'availableKeychain' property.
|
||||||
QString currentKeychain() const; ///< Getter for the 'currentKeychain' property.
|
QString currentKeychain() const; ///< Getter for the 'currentKeychain' property.
|
||||||
|
QVariantList bugCategories() const; ///< Getter for the 'bugCategories' property.
|
||||||
|
QVariantList bugQuestions() const; ///< Getter for the 'bugQuestions' property.
|
||||||
void setDockIconVisible(bool visible); ///< Setter for the 'dockIconVisible' property.
|
void setDockIconVisible(bool visible); ///< Setter for the 'dockIconVisible' property.
|
||||||
bool dockIconVisible() const;; ///< Getter for the 'dockIconVisible' property.
|
bool dockIconVisible() const;; ///< Getter for the 'dockIconVisible' property.
|
||||||
|
|
||||||
@ -149,6 +164,8 @@ signals: // Signal used by the Qt property system. Many of them are unused but r
|
|||||||
void tagChanged(QString const &tag); ///<Signal for the change of the 'tag' 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 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 currentKeychainChanged(QString const &keychain); ///<Signal for the change of the 'currentKeychain' property.
|
||||||
|
void bugCategoriesChanged(QVariantList const &bugCategories); ///<Signal for the change of the 'bugCategories' property.
|
||||||
|
void bugQuestionsChanged(QVariantList const &bugQuestions); ///<Signal for the change of the 'bugQuestions' property.
|
||||||
void availableKeychainChanged(QStringList const &keychains); ///<Signal for the change of the 'availableKeychain' property.
|
void availableKeychainChanged(QStringList const &keychains); ///<Signal for the change of the 'availableKeychain' property.
|
||||||
void hostnameChanged(QString const &hostname); ///<Signal for the change of the 'hostname' property.
|
void hostnameChanged(QString const &hostname); ///<Signal for the change of the 'hostname' property.
|
||||||
void isAutostartOnChanged(bool value); ///<Signal for the change of the 'isAutostartOn' property.
|
void isAutostartOnChanged(bool value); ///<Signal for the change of the 'isAutostartOn' property.
|
||||||
@ -177,12 +194,15 @@ public slots: // slot for signals received from QML -> To be forwarded to Bridge
|
|||||||
void checkUpdates() const; ///< Slot for the update check.
|
void checkUpdates() const; ///< Slot for the update check.
|
||||||
void installUpdate() const; ///< Slot for the update install.
|
void installUpdate() const; ///< Slot for the update install.
|
||||||
void triggerReset() const; ///< Slot for the triggering of reset.
|
void triggerReset() const; ///< Slot for the triggering of reset.
|
||||||
void reportBug(QString const &description, QString const &address, QString const &emailClient, bool includeLogs) const; ///< Slot for the bug report.
|
void reportBug(QString const &category, QString const &description, QString const &address, QString const &emailClient, bool includeLogs) const; ///< Slot for the bug report.
|
||||||
void exportTLSCertificates() const; ///< Slot for the export of the TLS certificates.
|
void exportTLSCertificates() const; ///< Slot for the export of the TLS certificates.
|
||||||
void onResetFinished(); ///< Slot for the reset finish signal.
|
void onResetFinished(); ///< Slot for the reset finish signal.
|
||||||
void onVersionChanged(); ///< Slot for the version change signal.
|
void onVersionChanged(); ///< Slot for the version change signal.
|
||||||
void setMailServerSettings(int imapPort, int smtpPort, bool useSSLForIMAP, bool useSSLForSMTP) const; ///< Forwards a connection mode change request from QML to gRPC
|
void 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 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.
|
public slots: // slots for functions that need to be processed locally.
|
||||||
void setNormalTrayIcon(); ///< Set the tray icon to normal.
|
void setNormalTrayIcon(); ///< Set the tray icon to normal.
|
||||||
@ -191,6 +211,7 @@ public slots: // slots for functions that need to be processed locally.
|
|||||||
void setUpdateTrayIcon(QString const& stateString, QString const &statusIcon); ///< Set the tray icon to 'update' state.
|
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
|
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 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 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.
|
void onLoginFinished(QString const &userID, bool wasSignedOut); ///< Slot for LoginFinished gRPC event.
|
||||||
@ -245,6 +266,7 @@ signals: // Signals received from the Go backend, to be forwarded to QML
|
|||||||
void resetFinished(); ///< Signal for the 'resetFinished' gRPC stream event.
|
void resetFinished(); ///< Signal for the 'resetFinished' gRPC stream event.
|
||||||
void reportBugFinished(); ///< Signal for the 'reportBugFinished' gRPC stream event.
|
void reportBugFinished(); ///< Signal for the 'reportBugFinished' gRPC stream event.
|
||||||
void bugReportSendSuccess(); ///< Signal for the 'bugReportSendSuccess' gRPC stream event.
|
void bugReportSendSuccess(); ///< Signal for the 'bugReportSendSuccess' gRPC stream event.
|
||||||
|
void bugReportSendFallback(); ///< Signal for the 'bugReportSendFallback' gRPC stream event.
|
||||||
void bugReportSendError(); ///< Signal for the 'bugReportSendError' gRPC stream event.
|
void bugReportSendError(); ///< Signal for the 'bugReportSendError' gRPC stream event.
|
||||||
void showMainWindow(); ///< Signal for the 'showMainWindow' gRPC stream event.
|
void showMainWindow(); ///< Signal for the 'showMainWindow' gRPC stream event.
|
||||||
void hideMainWindow(); ///< Signal for the 'hideMainWindow' gRPC stream event.
|
void hideMainWindow(); ///< Signal for the 'hideMainWindow' gRPC stream event.
|
||||||
@ -273,8 +295,10 @@ private: // data members
|
|||||||
int smtpPort_ { 0 }; ///< The cached value for the SMTP port.
|
int smtpPort_ { 0 }; ///< The cached value for the SMTP port.
|
||||||
bool useSSLForIMAP_ { false }; ///< The cached value for useSSLForIMAP.
|
bool useSSLForIMAP_ { false }; ///< The cached value for useSSLForIMAP.
|
||||||
bool useSSLForSMTP_ { false }; ///< The cached value for useSSLForSMTP.
|
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'.
|
QList<QString> badEventDisplayQueue_; ///< THe queue for displaying 'bad event feedback request dialog'.
|
||||||
std::unique_ptr<TrayIcon> trayIcon_;
|
std::unique_ptr<TrayIcon> trayIcon_; ///< The tray icon for the application.
|
||||||
|
bridgepp::BugReportFlow reportFlow_; ///< The bug report flow.
|
||||||
friend class AppController;
|
friend class AppController;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,11 @@
|
|||||||
<file>qml/Banner.qml</file>
|
<file>qml/Banner.qml</file>
|
||||||
<file>qml/Bridge.qml</file>
|
<file>qml/Bridge.qml</file>
|
||||||
<file>qml/bridgeqml.qmlproject</file>
|
<file>qml/bridgeqml.qmlproject</file>
|
||||||
|
<file>qml/BugCategoryView.qml</file>
|
||||||
|
<file>qml/BugQuestionView.qml</file>
|
||||||
|
<file>qml/BugReportFlow.qml</file>
|
||||||
<file>qml/BugReportView.qml</file>
|
<file>qml/BugReportView.qml</file>
|
||||||
|
<file>qml/CategoryItem.qml</file>
|
||||||
<file>qml/Configuration.qml</file>
|
<file>qml/Configuration.qml</file>
|
||||||
<file>qml/ConfigurationItem.qml</file>
|
<file>qml/ConfigurationItem.qml</file>
|
||||||
<file>qml/ContentWrapper.qml</file>
|
<file>qml/ContentWrapper.qml</file>
|
||||||
@ -19,6 +23,7 @@
|
|||||||
<file>qml/icons/ic-card-identity.svg</file>
|
<file>qml/icons/ic-card-identity.svg</file>
|
||||||
<file>qml/icons/ic-check.svg</file>
|
<file>qml/icons/ic-check.svg</file>
|
||||||
<file>qml/icons/ic-chevron-down.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-chevron-up.svg</file>
|
||||||
<file>qml/icons/ic-cog-wheel.svg</file>
|
<file>qml/icons/ic-cog-wheel.svg</file>
|
||||||
<file>qml/icons/ic-connected.svg</file>
|
<file>qml/icons/ic-connected.svg</file>
|
||||||
@ -31,6 +36,7 @@
|
|||||||
<file>qml/icons/ic-eye-slash.svg</file>
|
<file>qml/icons/ic-eye-slash.svg</file>
|
||||||
<file>qml/icons/ic-eye.svg</file>
|
<file>qml/icons/ic-eye.svg</file>
|
||||||
<file>qml/icons/ic-illustrative-view-html-code.svg</file>
|
<file>qml/icons/ic-illustrative-view-html-code.svg</file>
|
||||||
|
<file>qml/icons/ic-info-circle.svg</file>
|
||||||
<file>qml/icons/ic-info-circle-filled.svg</file>
|
<file>qml/icons/ic-info-circle-filled.svg</file>
|
||||||
<file>qml/icons/ic-info.svg</file>
|
<file>qml/icons/ic-info.svg</file>
|
||||||
<file>qml/icons/ic-microsoft-outlook.svg</file>
|
<file>qml/icons/ic-microsoft-outlook.svg</file>
|
||||||
@ -94,6 +100,8 @@
|
|||||||
<file>qml/Proton/TextArea.qml</file>
|
<file>qml/Proton/TextArea.qml</file>
|
||||||
<file>qml/Proton/TextField.qml</file>
|
<file>qml/Proton/TextField.qml</file>
|
||||||
<file>qml/Proton/Toggle.qml</file>
|
<file>qml/Proton/Toggle.qml</file>
|
||||||
|
<file>qml/QuestionItem.qml</file>
|
||||||
|
<file>qml/Resources/bug_report_flow.json</file>
|
||||||
<file>qml/SettingsItem.qml</file>
|
<file>qml/SettingsItem.qml</file>
|
||||||
<file>qml/SettingsView.qml</file>
|
<file>qml/SettingsView.qml</file>
|
||||||
<file>qml/SetupGuide.qml</file>
|
<file>qml/SetupGuide.qml</file>
|
||||||
|
|||||||
@ -34,7 +34,7 @@ QColor const warnColor(255, 153, 0); ///< The warn state color.
|
|||||||
QColor const updateColor(35, 158, 206); ///< The warn state color.
|
QColor const updateColor(35, 158, 206); ///< The warn state color.
|
||||||
QColor const greyColor(112, 109, 107); ///< The grey 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 iconRefreshTimerIntervalMs = 1000; ///< The interval for the refresh timer when switching DPI / screen config, in milliseconds.
|
||||||
qint64 const iconRefreshDurationSecs = 10; ///< The total number of seconds during wich we periodically refresh the icon after a DPI change.
|
qint64 const iconRefreshDurationSecs = 10; ///< The total number of seconds during which we periodically refresh the icon after a DPI change.
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
@ -184,10 +184,16 @@ TrayIcon::TrayIcon()
|
|||||||
this->setContextMenu(menu_.get());
|
this->setContextMenu(menu_.get());
|
||||||
|
|
||||||
connect(menu_.get(), &QMenu::aboutToShow, this, &TrayIcon::onMenuAboutToShow);
|
connect(menu_.get(), &QMenu::aboutToShow, this, &TrayIcon::onMenuAboutToShow);
|
||||||
connect(this, &TrayIcon::selectUser, &app().backend(), &QMLBackend::selectUser);
|
connect(this, &TrayIcon::selectUser, &app().backend(), [](QString const& userID, bool forceShowWindow) {
|
||||||
|
app().backend().selectUser(userID, forceShowWindow);
|
||||||
|
});
|
||||||
connect(this, &TrayIcon::activated, this, &TrayIcon::onActivated);
|
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.
|
// some OSes/Desktop managers will automatically show main window when clicked, but not all, so we do it manually.
|
||||||
connect(this, &TrayIcon::messageClicked, &app().backend(), &QMLBackend::showMainWindow);
|
if (!onLinux()) { // we disable this on linux because of a Qt bug that causes the signal to be emitted for other apps (GODT-2750)
|
||||||
|
connect(this, &TrayIcon::messageClicked, []() { app().backend().showMainWindow("tray icon popup notification clicked"); });
|
||||||
|
}
|
||||||
|
|
||||||
|
this->setIcon();
|
||||||
this->show();
|
this->show();
|
||||||
|
|
||||||
// TrayIcon does not expose its screen, so we connect relevant screen events to our DPI change handler.
|
// TrayIcon does not expose its screen, so we connect relevant screen events to our DPI change handler.
|
||||||
@ -226,7 +232,7 @@ void TrayIcon::onUserClicked() {
|
|||||||
throw Exception("Could not retrieve context menu's selected user.");
|
throw Exception("Could not retrieve context menu's selected user.");
|
||||||
}
|
}
|
||||||
|
|
||||||
emit selectUser(userID, true);
|
app().backend().selectUser(userID, true, "tray menu user clicked");
|
||||||
} catch (Exception const &e) {
|
} catch (Exception const &e) {
|
||||||
app().log().error(e.qwhat());
|
app().log().error(e.qwhat());
|
||||||
}
|
}
|
||||||
@ -238,7 +244,7 @@ void TrayIcon::onUserClicked() {
|
|||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
void TrayIcon::onActivated(QSystemTrayIcon::ActivationReason reason) {
|
void TrayIcon::onActivated(QSystemTrayIcon::ActivationReason reason) {
|
||||||
if ((QSystemTrayIcon::Trigger == reason) && !onMacOS()) {
|
if ((QSystemTrayIcon::Trigger == reason) && !onMacOS()) {
|
||||||
app().backend().showMainWindow();
|
app().backend().showMainWindow("tray icon activated");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -344,8 +350,9 @@ void TrayIcon::refreshContextMenu() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool const internetOn = app().backend().isInternetOn();
|
||||||
menu_->clear();
|
menu_->clear();
|
||||||
menu_->addAction(statusIcon_, stateString_, &app().backend(), &QMLBackend::showMainWindow);
|
menu_->addAction(statusIcon_, stateString_, []() {app().backend().showMainWindow("tray menu status clicked");});
|
||||||
menu_->addSeparator();
|
menu_->addSeparator();
|
||||||
QKeySequence noShortcut;
|
QKeySequence noShortcut;
|
||||||
UserList const &users = app().backend().users();
|
UserList const &users = app().backend().users();
|
||||||
@ -355,7 +362,9 @@ void TrayIcon::refreshContextMenu() {
|
|||||||
User const &user = *users.get(i);
|
User const &user = *users.get(i);
|
||||||
UserState const state = user.state();
|
UserState const state = user.state();
|
||||||
auto action = new QAction(user.primaryEmailOrUsername());
|
auto action = new QAction(user.primaryEmailOrUsername());
|
||||||
action->setIcon((UserState::Connected == state) ? greenDot_ : (UserState::Locked == state ? orangeDot_ : greyDot_));
|
if (internetOn) {
|
||||||
|
action->setIcon((UserState::Connected == state) ? greenDot_ : (UserState::Locked == state ? orangeDot_ : greyDot_));
|
||||||
|
}
|
||||||
action->setData(user.id());
|
action->setData(user.id());
|
||||||
connect(action, &QAction::triggered, this, &TrayIcon::onUserClicked);
|
connect(action, &QAction::triggered, this, &TrayIcon::onUserClicked);
|
||||||
if ((i < 10) && onMac) {
|
if ((i < 10) && onMac) {
|
||||||
@ -367,11 +376,15 @@ void TrayIcon::refreshContextMenu() {
|
|||||||
menu_->addSeparator();
|
menu_->addSeparator();
|
||||||
}
|
}
|
||||||
|
|
||||||
menu_->addAction(tr("&Open Bridge"), onMac ? QKeySequence("Ctrl+O") : noShortcut, &app().backend(), &QMLBackend::showMainWindow);
|
menu_->addAction(tr("&Open Bridge"), onMac ? QKeySequence("Ctrl+O") : noShortcut, []() {
|
||||||
menu_->addAction(tr("&Help"), onMac ? QKeySequence("Ctrl+F1") : noShortcut, &app().backend(), &QMLBackend::showHelp);
|
app().backend().showMainWindow("tray menu 'open bridge' clicked");
|
||||||
menu_->addAction(tr("&Settings"), onMac ? QKeySequence("Ctrl+,") : noShortcut, &app().backend(), &QMLBackend::showSettings);
|
});
|
||||||
|
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_->addSeparator();
|
||||||
menu_->addAction(tr("&Quit Bridge"), onMac ? QKeySequence("Ctrl+Q") : noShortcut, &app().backend(), &QMLBackend::quit);
|
menu_->addAction(tr("&Quit Bridge"), onMac ? QKeySequence("Ctrl+Q") : noShortcut, &app().backend(), &QMLBackend::quit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -63,7 +63,7 @@ BRIDGE_BUILD_ENV= ${BRIDGE_BUILD_ENV:-"dev"}
|
|||||||
git submodule update --init --recursive ${VCPKG_ROOT}
|
git submodule update --init --recursive ${VCPKG_ROOT}
|
||||||
check_exit "Failed to initialize vcpkg as a submodule."
|
check_exit "Failed to initialize vcpkg as a submodule."
|
||||||
|
|
||||||
echo submodule udpated
|
echo submodule updated
|
||||||
|
|
||||||
VCPKG_EXE="${VCPKG_ROOT}/vcpkg"
|
VCPKG_EXE="${VCPKG_ROOT}/vcpkg"
|
||||||
VCPKG_BOOTSTRAP="${VCPKG_ROOT}/bootstrap-vcpkg.sh"
|
VCPKG_BOOTSTRAP="${VCPKG_ROOT}/bootstrap-vcpkg.sh"
|
||||||
|
|||||||
@ -137,12 +137,21 @@ bool checkSingleInstance(QLockFile &lock) {
|
|||||||
if (lock.getLockInfo(&pid, &hostname, &appName)) {
|
if (lock.getLockInfo(&pid, &hostname, &appName)) {
|
||||||
details = QString("(PID : %1 - Host : %2 - App : %3)").arg(pid).arg(hostname, appName);
|
details = QString("(PID : %1 - Host : %2 - App : %3)").arg(pid).arg(hostname, appName);
|
||||||
}
|
}
|
||||||
|
if (lock.error() == QLockFile::LockFailedError) {
|
||||||
|
// This happens if a stale lock file exists and another process uses that PID.
|
||||||
|
// Try removing the stale file, which will fail if a real process is holding a
|
||||||
|
// file-level lock. A false error is more problematic than not locking properly
|
||||||
|
// on corner-case systems.
|
||||||
|
if (lock.removeStaleLockFile() && lock.tryLock()) {
|
||||||
|
app().log().info("Removed stale lock file");
|
||||||
|
app().log().info(QString("lock file created %1").arg(lock.fileName()));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
app().log().error(QString("Instance already exists %1 %2").arg(lock.fileName(), details));
|
app().log().error(QString("Instance already exists %1 %2").arg(lock.fileName(), details));
|
||||||
return false;
|
return false;
|
||||||
} else {
|
|
||||||
app().log().info(QString("lock file created %1").arg(lock.fileName()));
|
|
||||||
}
|
}
|
||||||
|
app().log().info(QString("lock file created %1").arg(lock.fileName()));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -196,7 +205,7 @@ bool isBridgeRunning() {
|
|||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
void focusOtherInstance() {
|
void focusOtherInstance() {
|
||||||
try {
|
try {
|
||||||
FocusGRPCClient client;
|
FocusGRPCClient client(app().log());
|
||||||
GRPCConfig sc;
|
GRPCConfig sc;
|
||||||
QString const path = FocusGRPCClient::grpcFocusServerConfigPath(bridgepp::userConfigDir());
|
QString const path = FocusGRPCClient::grpcFocusServerConfigPath(bridgepp::userConfigDir());
|
||||||
QFile file(path);
|
QFile file(path);
|
||||||
@ -209,12 +218,11 @@ void focusOtherInstance() {
|
|||||||
throw Exception("Server did not provide gRPC Focus service configuration.");
|
throw Exception("Server did not provide gRPC Focus service configuration.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
QString error;
|
QString error;
|
||||||
if (!client.connectToServer(5000, sc.port, &error)) {
|
if (!client.connectToServer(5000, sc.port, &error)) {
|
||||||
throw Exception("Could not connect to bridge focus service for a raise call.", 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."));
|
throw Exception(QString("The raise call to the bridge focus service failed."));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -286,7 +294,9 @@ int main(int argc, char *argv[]) {
|
|||||||
|
|
||||||
initQtApplication();
|
initQtApplication();
|
||||||
|
|
||||||
|
CommandLineOptions const cliOptions = parseCommandLine(argc, argv);
|
||||||
Log &log = initLog();
|
Log &log = initLog();
|
||||||
|
log.setLevel(cliOptions.logLevel);
|
||||||
|
|
||||||
QLockFile lock(bridgepp::userCacheDir() + "/" + bridgeGUILock);
|
QLockFile lock(bridgepp::userCacheDir() + "/" + bridgeGUILock);
|
||||||
if (!checkSingleInstance(lock)) {
|
if (!checkSingleInstance(lock)) {
|
||||||
@ -294,8 +304,6 @@ int main(int argc, char *argv[]) {
|
|||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
CommandLineOptions const cliOptions = parseCommandLine(argc, argv);
|
|
||||||
|
|
||||||
#ifdef Q_OS_MACOS
|
#ifdef Q_OS_MACOS
|
||||||
registerSecondInstanceHandler();
|
registerSecondInstanceHandler();
|
||||||
setDockIconVisibleState(!cliOptions.noWindow);
|
setDockIconVisibleState(!cliOptions.noWindow);
|
||||||
@ -304,25 +312,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.
|
// 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
|
// 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.
|
// these outputs and output them on the command-line.
|
||||||
log.setLevel(cliOptions.logLevel);
|
|
||||||
log.info(QString("New Sentry reporter - id: %1.").arg(getProtectedHostname()));
|
log.info(QString("New Sentry reporter - id: %1.").arg(getProtectedHostname()));
|
||||||
|
|
||||||
QString bridgeexec;
|
QString const &sessionID = app().sessionID();
|
||||||
|
QString bridgeExe;
|
||||||
if (!cliOptions.attach) {
|
if (!cliOptions.attach) {
|
||||||
if (isBridgeRunning()) {
|
if (isBridgeRunning()) {
|
||||||
throw Exception("An orphan instance of bridge is already running. Please terminate it and relaunch the application.",
|
throw Exception("An orphan instance of bridge is already running. Please terminate it and relaunch the application.",
|
||||||
QString(), __FUNCTION__, tailOfLatestBridgeLog());
|
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.
|
// 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);
|
FocusGRPCClient::removeServiceConfigFile(configDir);
|
||||||
GRPCClient::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))));
|
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().backend().init(GRPCClient::waitAndRetrieveServiceConfig(sessionID, configDir,
|
||||||
app().bridgeMonitor()));
|
cliOptions.attach ? 0 : grpcServiceConfigWaitDelayMs, app().bridgeMonitor()));
|
||||||
if (!cliOptions.attach) {
|
if (!cliOptions.attach) {
|
||||||
GRPCClient::removeServiceConfigFile(configDir);
|
GRPCClient::removeServiceConfigFile(configDir);
|
||||||
}
|
}
|
||||||
@ -380,9 +388,9 @@ int main(int argc, char *argv[]) {
|
|||||||
QStringList args = cliOptions.bridgeGuiArgs;
|
QStringList args = cliOptions.bridgeGuiArgs;
|
||||||
args.append(waitFlag);
|
args.append(waitFlag);
|
||||||
args.append(mainexec);
|
args.append(mainexec);
|
||||||
if (!bridgeexec.isEmpty()) {
|
if (!bridgeExe.isEmpty()) {
|
||||||
args.append(waitFlag);
|
args.append(waitFlag);
|
||||||
args.append(bridgeexec);
|
args.append(bridgeExe);
|
||||||
}
|
}
|
||||||
app().setLauncherArgs(cliOptions.launcher, args);
|
app().setLauncherArgs(cliOptions.launcher, args);
|
||||||
result = QGuiApplication::exec();
|
result = QGuiApplication::exec();
|
||||||
|
|||||||
@ -1,251 +1,252 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
//
|
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
|
||||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
// 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
|
// it under the terms of the GNU General Public License as published by
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
// (at your option) any later version.
|
// (at your option) any later version.
|
||||||
//
|
|
||||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
// GNU General Public License for more details.
|
// GNU General Public License for more details.
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU General Public License
|
// 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/>.
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
|
|
||||||
import Proton
|
import Proton
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
enum ViewType {
|
||||||
property ColorScheme colorScheme
|
SmallView,
|
||||||
property var user
|
LargeView
|
||||||
|
}
|
||||||
|
|
||||||
property var _spacing: 12 * ProtonStyle.px
|
property var _spacing: 12 * ProtonStyle.px
|
||||||
|
property ColorScheme colorScheme
|
||||||
property color progressColor : {
|
property color progressColor: {
|
||||||
if (!root.enabled) return root.colorScheme.text_weak
|
if (!root.enabled)
|
||||||
if (root.type == AccountDelegate.SmallView) return root.colorScheme.text_weak
|
return root.colorScheme.text_weak;
|
||||||
if (root.user && root.user.isSyncing) return root.colorScheme.text_weak
|
if (root.type === AccountDelegate.SmallView)
|
||||||
if (root.progressRatio < .50) return root.colorScheme.signal_success
|
return root.colorScheme.text_weak;
|
||||||
if (root.progressRatio < .75) return root.colorScheme.signal_warning
|
if (root.user && root.user.isSyncing)
|
||||||
return root.colorScheme.signal_danger
|
return root.colorScheme.text_weak;
|
||||||
|
if (root.progressRatio < .50)
|
||||||
|
return root.colorScheme.signal_success;
|
||||||
|
if (root.progressRatio < .75)
|
||||||
|
return root.colorScheme.signal_warning;
|
||||||
|
return root.colorScheme.signal_danger;
|
||||||
}
|
}
|
||||||
property real progressRatio: {
|
property real progressRatio: {
|
||||||
if (!root.user)
|
if (!root.user)
|
||||||
return 0
|
return 0;
|
||||||
return root.user.isSyncing ? root.user.syncProgress : reasonableFraction(root.user.usedBytes, root.user.totalBytes)
|
return root.user.isSyncing ? root.user.syncProgress : reasonableFraction(root.user.usedBytes, root.user.totalBytes);
|
||||||
}
|
}
|
||||||
property string totalSpace: root.spaceWithUnits(root.user ? root.reasonableBytes(root.user.totalBytes) : 0)
|
property string totalSpace: root.spaceWithUnits(root.user ? root.reasonableBytes(root.user.totalBytes) : 0)
|
||||||
|
property var type: AccountDelegate.SmallView
|
||||||
property string usedSpace: root.spaceWithUnits(root.user ? root.reasonableBytes(root.user.usedBytes) : 0)
|
property string usedSpace: root.spaceWithUnits(root.user ? root.reasonableBytes(root.user.usedBytes) : 0)
|
||||||
|
property var user
|
||||||
function reasonableFraction(used, total){
|
|
||||||
var usedSafe = root.reasonableBytes(used)
|
|
||||||
var totalSafe = root.reasonableBytes(total)
|
|
||||||
if (totalSafe == 0 || usedSafe == 0) return 0
|
|
||||||
if (totalSafe <= usedSafe) return 1
|
|
||||||
return usedSafe / totalSafe
|
|
||||||
}
|
|
||||||
|
|
||||||
function reasonableBytes(bytes){
|
|
||||||
var safeBytes = bytes+0
|
|
||||||
if (safeBytes != bytes) return 0
|
|
||||||
if (safeBytes < 0) return 0
|
|
||||||
return Math.ceil(safeBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
function spaceWithUnits(bytes){
|
|
||||||
if (bytes*1 !== bytes || bytes == 0 ) return "0 kB"
|
|
||||||
var units = ['B',"kB", "MB", "GB", "TB"];
|
|
||||||
var i = parseInt(Math.floor(Math.log(bytes)/Math.log(1024)));
|
|
||||||
|
|
||||||
return Math.round(bytes*10 / Math.pow(1024, i))/10 + " " + units[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
function primaryEmail() {
|
function primaryEmail() {
|
||||||
return root.user ? root.user.primaryEmailOrUsername() : ""
|
return root.user ? root.user.primaryEmailOrUsername() : "";
|
||||||
|
}
|
||||||
|
function reasonableBytes(bytes) {
|
||||||
|
const safeBytes = bytes + 0;
|
||||||
|
if (safeBytes !== bytes)
|
||||||
|
return 0;
|
||||||
|
if (safeBytes < 0)
|
||||||
|
return 0;
|
||||||
|
return Math.ceil(safeBytes);
|
||||||
|
}
|
||||||
|
function reasonableFraction(used, total) {
|
||||||
|
const usedSafe = root.reasonableBytes(used);
|
||||||
|
const totalSafe = root.reasonableBytes(total);
|
||||||
|
if (totalSafe === 0 || usedSafe === 0)
|
||||||
|
return 0;
|
||||||
|
if (totalSafe <= usedSafe)
|
||||||
|
return 1;
|
||||||
|
return usedSafe / totalSafe;
|
||||||
|
}
|
||||||
|
function spaceWithUnits(bytes) {
|
||||||
|
if (bytes * 1 !== bytes || bytes === 0)
|
||||||
|
return "0 kB";
|
||||||
|
const units = ['B', "kB", "MB", "GB", "TB"];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||||
|
return Math.round(bytes * 10 / Math.pow(1024, i)) / 10 + " " + units[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
// width expected to be set by parent object
|
// width expected to be set by parent object
|
||||||
implicitHeight : children[0].implicitHeight
|
implicitHeight: children[0].implicitHeight
|
||||||
|
|
||||||
enum ViewType{
|
|
||||||
SmallView, LargeView
|
|
||||||
}
|
|
||||||
property var type : AccountDelegate.SmallView
|
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
spacing: root._spacing
|
spacing: root._spacing
|
||||||
|
|
||||||
anchors {
|
anchors {
|
||||||
top: root.top
|
|
||||||
left: root.left
|
left: root.left
|
||||||
right: root.right
|
right: root.right
|
||||||
|
top: root.top
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: avatar
|
id: avatar
|
||||||
|
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
Layout.preferredWidth: height
|
Layout.preferredWidth: height
|
||||||
|
color: root.colorScheme.background_avatar
|
||||||
radius: ProtonStyle.avatar_radius
|
radius: ProtonStyle.avatar_radius
|
||||||
|
|
||||||
color: root.colorScheme.background_avatar
|
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
colorScheme: root.colorScheme
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
text: root.user ? root.user.avatarText.toUpperCase(): ""
|
color: "#FFFFFF"
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
font.weight: Font.Normal
|
||||||
|
horizontalAlignment: Qt.AlignHCenter
|
||||||
|
text: root.user ? root.user.avatarText.toUpperCase() : ""
|
||||||
type: {
|
type: {
|
||||||
switch (root.type) {
|
switch (root.type) {
|
||||||
case AccountDelegate.SmallView: return Label.Body
|
case AccountDelegate.SmallView:
|
||||||
case AccountDelegate.LargeView: return Label.Title
|
return Label.Body;
|
||||||
|
case AccountDelegate.LargeView:
|
||||||
|
return Label.Title;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
font.weight: Font.Normal
|
|
||||||
color: "#FFFFFF"
|
|
||||||
horizontalAlignment: Qt.AlignHCenter
|
|
||||||
verticalAlignment: Qt.AlignVCenter
|
verticalAlignment: Qt.AlignVCenter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: account
|
id: account
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
spacing: 0
|
spacing: 0
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
id: labelEmail
|
id: labelEmail
|
||||||
Layout.maximumWidth: root.width - (root._spacing + avatar.width)
|
Layout.maximumWidth: root.width - (root._spacing + avatar.width)
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
|
elide: Text.ElideMiddle
|
||||||
text: primaryEmail()
|
text: primaryEmail()
|
||||||
type: {
|
type: {
|
||||||
switch (root.type) {
|
switch (root.type) {
|
||||||
case AccountDelegate.SmallView: return Label.Body
|
case AccountDelegate.SmallView:
|
||||||
case AccountDelegate.LargeView: return Label.Title
|
return Label.Body;
|
||||||
|
case AccountDelegate.LargeView:
|
||||||
|
return Label.Title;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
elide: Text.ElideMiddle
|
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: labelArea
|
id: labelArea
|
||||||
anchors.fill:parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolTip {
|
ToolTip {
|
||||||
id: toolTipEmail
|
id: toolTipEmail
|
||||||
visible: labelArea.containsMouse && labelEmail.truncated
|
|
||||||
text: primaryEmail()
|
|
||||||
delay: 1000
|
delay: 1000
|
||||||
|
text: primaryEmail()
|
||||||
|
visible: labelArea.containsMouse && labelEmail.truncated
|
||||||
|
|
||||||
background: Rectangle {
|
background: Rectangle {
|
||||||
border.color: root.colorScheme.background_strong
|
border.color: root.colorScheme.background_strong
|
||||||
color: root.colorScheme.background_norm
|
color: root.colorScheme.background_norm
|
||||||
}
|
}
|
||||||
|
|
||||||
contentItem: Text {
|
contentItem: Text {
|
||||||
color: root.colorScheme.text_norm
|
color: root.colorScheme.text_norm
|
||||||
text: toolTipEmail.text
|
text: toolTipEmail.text
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Item {
|
||||||
Item { implicitHeight: root.type == AccountDelegate.LargeView ? 6 * ProtonStyle.px : 0 }
|
implicitHeight: root.type === AccountDelegate.LargeView ? 6 * ProtonStyle.px : 0
|
||||||
|
}
|
||||||
RowLayout {
|
RowLayout {
|
||||||
spacing: 0
|
spacing: 0
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
|
color: root.progressColor
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
text: {
|
text: {
|
||||||
if (!root.user)
|
if (!root.user)
|
||||||
return qsTr("Signed out")
|
return qsTr("Signed out");
|
||||||
switch (root.user.state) {
|
switch (root.user.state) {
|
||||||
case EUserState.SignedOut:
|
case EUserState.SignedOut:
|
||||||
default:
|
default:
|
||||||
return qsTr("Signed out")
|
return qsTr("Signed out");
|
||||||
case EUserState.Locked:
|
case EUserState.Locked:
|
||||||
return qsTr("Connecting") + dotsTimer.dots
|
return qsTr("Connecting") + dotsTimer.dots;
|
||||||
case EUserState.Connected:
|
case EUserState.Connected:
|
||||||
if (root.user.isSyncing)
|
if (root.user.isSyncing)
|
||||||
return qsTr("Synchronizing (%1%)").arg(Math.floor(root.user.syncProgress * 100)) + dotsTimer.dots
|
return qsTr("Synchronizing (%1%)").arg(Math.floor(root.user.syncProgress * 100)) + dotsTimer.dots;
|
||||||
else
|
else
|
||||||
return root.usedSpace
|
return root.usedSpace;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Timer { // dots animation while connecting & syncing.
|
|
||||||
id:dotsTimer
|
|
||||||
property string dots: ""
|
|
||||||
interval: 500;
|
|
||||||
repeat: true;
|
|
||||||
running: (root.user != null) && ((root.user.state === EUserState.Locked) || (root.user.isSyncing))
|
|
||||||
onTriggered: {
|
|
||||||
dots += "."
|
|
||||||
if (dots.length > 3)
|
|
||||||
dots = ""
|
|
||||||
}
|
|
||||||
onRunningChanged: {
|
|
||||||
dots = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
color: root.progressColor
|
|
||||||
type: {
|
type: {
|
||||||
switch (root.type) {
|
switch (root.type) {
|
||||||
case AccountDelegate.SmallView: return Label.Caption
|
case AccountDelegate.SmallView:
|
||||||
case AccountDelegate.LargeView: return Label.Body
|
return Label.Caption;
|
||||||
|
case AccountDelegate.LargeView:
|
||||||
|
return Label.Body;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
// dots animation while connecting & syncing.
|
||||||
|
id: dotsTimer
|
||||||
|
|
||||||
|
property string dots: ""
|
||||||
|
|
||||||
|
interval: 500
|
||||||
|
repeat: true
|
||||||
|
running: (root.user != null) && ((root.user.state === EUserState.Locked) || (root.user.isSyncing))
|
||||||
|
|
||||||
|
onRunningChanged: {
|
||||||
|
dots = "";
|
||||||
|
}
|
||||||
|
onTriggered: {
|
||||||
|
dots += ".";
|
||||||
|
if (dots.length > 3)
|
||||||
|
dots = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
colorScheme: root.colorScheme
|
|
||||||
text: root.user && root.user.state == EUserState.Connected && !root.user.isSyncing ? " / " + root.totalSpace : ""
|
|
||||||
color: root.colorScheme.text_weak
|
color: root.colorScheme.text_weak
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
text: root.user && root.user.state === EUserState.Connected && !root.user.isSyncing ? " / " + root.totalSpace : ""
|
||||||
type: {
|
type: {
|
||||||
switch (root.type) {
|
switch (root.type) {
|
||||||
case AccountDelegate.SmallView: return Label.Caption
|
case AccountDelegate.SmallView:
|
||||||
case AccountDelegate.LargeView: return Label.Body
|
return Label.Caption;
|
||||||
|
case AccountDelegate.LargeView:
|
||||||
|
return Label.Body;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Item {
|
||||||
Item { implicitHeight: root.type == AccountDelegate.LargeView ? 3 * ProtonStyle.px : 0 }
|
implicitHeight: root.type === AccountDelegate.LargeView ? 3 * ProtonStyle.px : 0
|
||||||
|
}
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: progress_bar
|
id: progress_bar
|
||||||
visible: root.user ? root.type == AccountDelegate.LargeView : false
|
color: root.colorScheme.border_weak
|
||||||
width: 140 * ProtonStyle.px
|
|
||||||
height: 4 * ProtonStyle.px
|
height: 4 * ProtonStyle.px
|
||||||
radius: ProtonStyle.progress_bar_radius
|
radius: ProtonStyle.progress_bar_radius
|
||||||
color: root.colorScheme.border_weak
|
visible: root.user ? root.type === AccountDelegate.LargeView : false
|
||||||
|
width: 140 * ProtonStyle.px
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: progress_bar_filled
|
id: progress_bar_filled
|
||||||
radius: ProtonStyle.progress_bar_radius
|
|
||||||
color: root.progressColor
|
color: root.progressColor
|
||||||
visible: root.user ? parent.visible && (root.user.state == EUserState.Connected): false
|
radius: ProtonStyle.progress_bar_radius
|
||||||
|
visible: root.user ? parent.visible && (root.user.state === EUserState.Connected) : false
|
||||||
|
width: Math.min(1, Math.max(0.02, root.progressRatio)) * parent.width
|
||||||
|
|
||||||
anchors {
|
anchors {
|
||||||
top : parent.top
|
bottom: parent.bottom
|
||||||
bottom : parent.bottom
|
left: parent.left
|
||||||
left : parent.left
|
top: parent.top
|
||||||
}
|
}
|
||||||
width: Math.min(1,Math.max(0.02,root.progressRatio)) * parent.width
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,42 +1,35 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
//
|
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
|
||||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
// 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
|
// it under the terms of the GNU General Public License as published by
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
// (at your option) any later version.
|
// (at your option) any later version.
|
||||||
//
|
|
||||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
// GNU General Public License for more details.
|
// GNU General Public License for more details.
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU General Public License
|
// 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/>.
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
|
|
||||||
import Proton
|
import Proton
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
property bool _connected: root.user ? root.user.state === EUserState.Connected : false
|
||||||
|
property int _contentWidth: 640
|
||||||
|
property int _detailsMargin: 25
|
||||||
|
property int _lineThickness: 1
|
||||||
|
property int _spacing: 20
|
||||||
|
property int _topMargin: 32
|
||||||
property ColorScheme colorScheme
|
property ColorScheme colorScheme
|
||||||
property var notifications
|
property var notifications
|
||||||
property var user
|
property var user
|
||||||
|
|
||||||
signal showSignIn
|
|
||||||
|
|
||||||
signal showSetupGuide(var user, string address)
|
signal showSetupGuide(var user, string address)
|
||||||
|
signal showSignIn
|
||||||
property int _contentWidth: 640
|
|
||||||
property int _topMargin: 32
|
|
||||||
property int _detailsMargin: 25
|
|
||||||
property int _spacing: 20
|
|
||||||
property int _lineThickness: 1
|
|
||||||
property bool _connected: root.user ? root.user.state === EUserState.Connected : false
|
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@ -45,6 +38,7 @@ Item {
|
|||||||
ScrollView {
|
ScrollView {
|
||||||
id: scrollView
|
id: scrollView
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
||||||
Component.onCompleted: contentItem.boundsBehavior = Flickable.StopAtBounds
|
Component.onCompleted: contentItem.boundsBehavior = Flickable.StopAtBounds
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
@ -54,16 +48,16 @@ Item {
|
|||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: topArea
|
id: topArea
|
||||||
color: root.colorScheme.background_norm
|
|
||||||
clip: true
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
clip: true
|
||||||
|
color: root.colorScheme.background_norm
|
||||||
implicitHeight: childrenRect.height
|
implicitHeight: childrenRect.height
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: topLayout
|
id: topLayout
|
||||||
width: _contentWidth
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
spacing: _spacing
|
spacing: _spacing
|
||||||
|
width: _contentWidth
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
// account delegate with action buttons
|
// account delegate with action buttons
|
||||||
@ -73,83 +67,82 @@ Item {
|
|||||||
AccountDelegate {
|
AccountDelegate {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
user: root.user
|
|
||||||
type: AccountDelegate.LargeView
|
|
||||||
enabled: _connected
|
enabled: _connected
|
||||||
|
type: AccountDelegate.LargeView
|
||||||
|
user: root.user
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
Layout.alignment: Qt.AlignTop
|
Layout.alignment: Qt.AlignTop
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
text: qsTr("Sign out")
|
|
||||||
secondary: true
|
secondary: true
|
||||||
|
text: qsTr("Sign out")
|
||||||
visible: _connected
|
visible: _connected
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (!root.user)
|
if (!root.user)
|
||||||
return;
|
return;
|
||||||
root.user.logout();
|
root.user.logout();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
Layout.alignment: Qt.AlignTop
|
Layout.alignment: Qt.AlignTop
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
text: qsTr("Sign in")
|
|
||||||
secondary: true
|
secondary: true
|
||||||
|
text: qsTr("Sign in")
|
||||||
visible: root.user ? (root.user.state === EUserState.SignedOut) : false
|
visible: root.user ? (root.user.state === EUserState.SignedOut) : false
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (!root.user)
|
if (!root.user)
|
||||||
return;
|
return;
|
||||||
root.showSignIn();
|
root.showSignIn();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
Layout.alignment: Qt.AlignTop
|
Layout.alignment: Qt.AlignTop
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
icon.source: "/qml/icons/ic-trash.svg"
|
icon.source: "/qml/icons/ic-trash.svg"
|
||||||
secondary: true
|
secondary: true
|
||||||
|
visible: root.user ? root.user.state !== EUserState.Locked : false
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (!root.user)
|
if (!root.user)
|
||||||
return;
|
return;
|
||||||
root.notifications.askDeleteAccount(root.user);
|
root.notifications.askDeleteAccount(root.user);
|
||||||
}
|
}
|
||||||
visible: root.user ? root.user.state !== EUserState.Locked : false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
height: root._lineThickness
|
|
||||||
color: root.colorScheme.border_weak
|
color: root.colorScheme.border_weak
|
||||||
|
height: root._lineThickness
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsItem {
|
SettingsItem {
|
||||||
colorScheme: root.colorScheme
|
Layout.fillWidth: true
|
||||||
text: qsTr("Email clients")
|
|
||||||
actionText: qsTr("Configure")
|
actionText: qsTr("Configure")
|
||||||
|
colorScheme: root.colorScheme
|
||||||
description: qsTr("Using the mailbox details below (re)configure your client.")
|
description: qsTr("Using the mailbox details below (re)configure your client.")
|
||||||
|
showSeparator: splitMode.visible
|
||||||
|
text: qsTr("Email clients")
|
||||||
type: SettingsItem.Button
|
type: SettingsItem.Button
|
||||||
visible: _connected && (!root.user.splitMode) || (root.user.addresses.length === 1)
|
visible: _connected && (!root.user.splitMode) || (root.user.addresses.length === 1)
|
||||||
showSeparator: splitMode.visible
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (!root.user)
|
if (!root.user)
|
||||||
return;
|
return;
|
||||||
root.showSetupGuide(root.user, user.addresses[0]);
|
root.showSetupGuide(root.user, user.addresses[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsItem {
|
SettingsItem {
|
||||||
id: splitMode
|
id: splitMode
|
||||||
colorScheme: root.colorScheme
|
Layout.fillWidth: true
|
||||||
text: qsTr("Split addresses")
|
|
||||||
description: qsTr("Setup multiple email addresses individually.")
|
|
||||||
type: SettingsItem.Toggle
|
|
||||||
checked: root.user ? root.user.splitMode : false
|
checked: root.user ? root.user.splitMode : false
|
||||||
visible: _connected && root.user.addresses.length > 1
|
colorScheme: root.colorScheme
|
||||||
|
description: qsTr("Setup multiple email addresses individually.")
|
||||||
showSeparator: addressSelector.visible
|
showSeparator: addressSelector.visible
|
||||||
|
text: qsTr("Split addresses")
|
||||||
|
type: SettingsItem.Toggle
|
||||||
|
visible: _connected && root.user.addresses.length > 1
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (!splitMode.checked) {
|
if (!splitMode.checked) {
|
||||||
root.notifications.askEnableSplitMode(user);
|
root.notifications.askEnableSplitMode(user);
|
||||||
@ -158,26 +151,23 @@ Item {
|
|||||||
root.user.toggleSplitMode(!splitMode.checked);
|
root.user.toggleSplitMode(!splitMode.checked);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.bottomMargin: _spacing
|
Layout.bottomMargin: _spacing
|
||||||
|
Layout.fillWidth: true
|
||||||
visible: _connected && root.user.splitMode
|
visible: _connected && root.user.splitMode
|
||||||
|
|
||||||
ComboBox {
|
ComboBox {
|
||||||
id: addressSelector
|
id: addressSelector
|
||||||
colorScheme: root.colorScheme
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
colorScheme: root.colorScheme
|
||||||
model: root.user ? root.user.addresses : null
|
model: root.user ? root.user.addresses : null
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
text: qsTr("Configure")
|
|
||||||
secondary: true
|
secondary: true
|
||||||
|
text: qsTr("Configure")
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (!root.user)
|
if (!root.user)
|
||||||
return;
|
return;
|
||||||
@ -185,25 +175,23 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
height: 0
|
height: 0
|
||||||
} // just for some extra space before separator
|
} // just for some extra space before separator
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: bottomArea
|
id: bottomArea
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
implicitHeight: bottomLayout.implicitHeight
|
|
||||||
color: root.colorScheme.background_weak
|
color: root.colorScheme.background_weak
|
||||||
|
implicitHeight: bottomLayout.implicitHeight
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: bottomLayout
|
id: bottomLayout
|
||||||
width: _contentWidth
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
spacing: _spacing
|
spacing: _spacing
|
||||||
visible: _connected
|
visible: _connected
|
||||||
|
width: _contentWidth
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
Layout.topMargin: _detailsMargin
|
Layout.topMargin: _detailsMargin
|
||||||
@ -211,35 +199,34 @@ Item {
|
|||||||
text: qsTr("Mailbox details")
|
text: qsTr("Mailbox details")
|
||||||
type: Label.Body_semibold
|
type: Label.Body_semibold
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
id: configuration
|
id: configuration
|
||||||
spacing: _spacing
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.fillHeight: true
|
|
||||||
|
|
||||||
property string currentAddress: addressSelector.displayText
|
property string currentAddress: addressSelector.displayText
|
||||||
|
|
||||||
Configuration {
|
Layout.fillHeight: true
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
colorScheme: root.colorScheme
|
spacing: _spacing
|
||||||
title: qsTr("IMAP")
|
|
||||||
hostname: Backend.hostname
|
|
||||||
port: Backend.imapPort.toString()
|
|
||||||
username: configuration.currentAddress
|
|
||||||
password: root.user ? root.user.password : ""
|
|
||||||
security: Backend.useSSLForIMAP ? "SSL" : "STARTTLS"
|
|
||||||
}
|
|
||||||
|
|
||||||
Configuration {
|
Configuration {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
title: qsTr("SMTP")
|
|
||||||
hostname: Backend.hostname
|
hostname: Backend.hostname
|
||||||
port: Backend.smtpPort.toString()
|
|
||||||
username: configuration.currentAddress
|
|
||||||
password: root.user ? root.user.password : ""
|
password: root.user ? root.user.password : ""
|
||||||
|
port: Backend.imapPort.toString()
|
||||||
|
security: Backend.useSSLForIMAP ? "SSL" : "STARTTLS"
|
||||||
|
title: qsTr("IMAP")
|
||||||
|
username: configuration.currentAddress
|
||||||
|
}
|
||||||
|
Configuration {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
hostname: Backend.hostname
|
||||||
|
password: root.user ? root.user.password : ""
|
||||||
|
port: Backend.smtpPort.toString()
|
||||||
security: Backend.useSSLForSMTP ? "SSL" : "STARTTLS"
|
security: Backend.useSSLForSMTP ? "SSL" : "STARTTLS"
|
||||||
|
title: qsTr("SMTP")
|
||||||
|
username: configuration.currentAddress
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,25 +1,19 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
//
|
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
|
||||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
// 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
|
// it under the terms of the GNU General Public License as published by
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
// (at your option) any later version.
|
// (at your option) any later version.
|
||||||
//
|
|
||||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
// GNU General Public License for more details.
|
// GNU General Public License for more details.
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU General Public License
|
// 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/>.
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import QtQuick.Controls.impl
|
import QtQuick.Controls.impl
|
||||||
|
|
||||||
import Proton
|
import Proton
|
||||||
import Notifications
|
import Notifications
|
||||||
|
|
||||||
@ -27,34 +21,28 @@ Popup {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
property ColorScheme colorScheme
|
property ColorScheme colorScheme
|
||||||
property Notification notification
|
|
||||||
property var mainWindow
|
property var mainWindow
|
||||||
|
property Notification notification
|
||||||
topMargin: 37
|
|
||||||
leftMargin: (mainWindow.width - root.implicitWidth)/2
|
|
||||||
|
|
||||||
implicitHeight: contentLayout.implicitHeight + contentLayout.anchors.topMargin + contentLayout.anchors.bottomMargin
|
implicitHeight: contentLayout.implicitHeight + contentLayout.anchors.topMargin + contentLayout.anchors.bottomMargin
|
||||||
implicitWidth: 600 // contentLayout.implicitWidth + contentLayout.anchors.leftMargin + contentLayout.anchors.rightMargin
|
implicitWidth: 600 // contentLayout.implicitWidth + contentLayout.anchors.leftMargin + contentLayout.anchors.rightMargin
|
||||||
|
leftMargin: (mainWindow.width - root.implicitWidth) / 2
|
||||||
popupType: ApplicationWindow.PopupType.Banner
|
|
||||||
|
|
||||||
shouldShow: notification ? (notification.active && !notification.dismissed) : false
|
|
||||||
|
|
||||||
modal: false
|
modal: false
|
||||||
|
popupType: ApplicationWindow.PopupType.Banner
|
||||||
|
shouldShow: notification ? (notification.active && !notification.dismissed) : false
|
||||||
|
topMargin: 37
|
||||||
|
|
||||||
Action {
|
Action {
|
||||||
id: defaultDismissAction
|
id: defaultDismissAction
|
||||||
|
|
||||||
text: qsTr("OK")
|
text: qsTr("OK")
|
||||||
|
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
if (!root.notification) {
|
if (!root.notification) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
root.notification.dismissed = true;
|
||||||
root.notification.dismissed = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
id: contentLayout
|
id: contentLayout
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@ -63,170 +51,148 @@ Popup {
|
|||||||
Item {
|
Item {
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
clip: true
|
clip: true
|
||||||
implicitHeight: children[1].implicitHeight + children[1].anchors.topMargin + children[1].anchors.bottomMargin
|
implicitHeight: children[1].implicitHeight + children[1].anchors.topMargin + children[1].anchors.bottomMargin
|
||||||
implicitWidth: children[1].implicitWidth + children[1].anchors.leftMargin + children[1].anchors.rightMargin
|
implicitWidth: children[1].implicitWidth + children[1].anchors.leftMargin + children[1].anchors.rightMargin
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
width: parent.width + 10
|
anchors.top: parent.top
|
||||||
radius: ProtonStyle.banner_radius
|
|
||||||
color: {
|
color: {
|
||||||
if (!root.notification) {
|
if (!root.notification) {
|
||||||
return "transparent"
|
return "transparent";
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (root.notification.type) {
|
switch (root.notification.type) {
|
||||||
case Notification.NotificationType.Info:
|
case Notification.NotificationType.Info:
|
||||||
return root.colorScheme.signal_info
|
return root.colorScheme.signal_info;
|
||||||
case Notification.NotificationType.Success:
|
case Notification.NotificationType.Success:
|
||||||
return root.colorScheme.signal_success
|
return root.colorScheme.signal_success;
|
||||||
case Notification.NotificationType.Warning:
|
case Notification.NotificationType.Warning:
|
||||||
return root.colorScheme.signal_warning
|
return root.colorScheme.signal_warning;
|
||||||
case Notification.NotificationType.Danger:
|
case Notification.NotificationType.Danger:
|
||||||
return root.colorScheme.signal_danger
|
return root.colorScheme.signal_danger;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
radius: ProtonStyle.banner_radius
|
||||||
|
width: parent.width + 10
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
anchors.fill: parent
|
|
||||||
|
|
||||||
anchors.topMargin: 14
|
|
||||||
anchors.bottomMargin: 14
|
anchors.bottomMargin: 14
|
||||||
|
anchors.fill: parent
|
||||||
anchors.leftMargin: 16
|
anchors.leftMargin: 16
|
||||||
|
anchors.topMargin: 14
|
||||||
spacing: 8
|
spacing: 8
|
||||||
|
|
||||||
ColorImage {
|
ColorImage {
|
||||||
color: root.colorScheme.text_invert
|
|
||||||
width: 24
|
|
||||||
height: 24
|
|
||||||
|
|
||||||
sourceSize.width: 24
|
|
||||||
sourceSize.height: 24
|
|
||||||
|
|
||||||
Layout.preferredHeight: 24
|
Layout.preferredHeight: 24
|
||||||
Layout.preferredWidth: 24
|
Layout.preferredWidth: 24
|
||||||
|
color: root.colorScheme.text_invert
|
||||||
|
height: 24
|
||||||
source: {
|
source: {
|
||||||
if (!root.notification) {
|
if (!root.notification) {
|
||||||
return ""
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (root.notification.type) {
|
switch (root.notification.type) {
|
||||||
case Notification.NotificationType.Info:
|
case Notification.NotificationType.Info:
|
||||||
return "/qml/icons/ic-info-circle-filled.svg"
|
return "/qml/icons/ic-info-circle-filled.svg";
|
||||||
case Notification.NotificationType.Success:
|
case Notification.NotificationType.Success:
|
||||||
return "/qml/icons/ic-info-circle-filled.svg"
|
return "/qml/icons/ic-info-circle-filled.svg";
|
||||||
case Notification.NotificationType.Warning:
|
case Notification.NotificationType.Warning:
|
||||||
return "/qml/icons/ic-exclamation-circle-filled.svg"
|
return "/qml/icons/ic-exclamation-circle-filled.svg";
|
||||||
case Notification.NotificationType.Danger:
|
case Notification.NotificationType.Danger:
|
||||||
return "/qml/icons/ic-exclamation-circle-filled.svg"
|
return "/qml/icons/ic-exclamation-circle-filled.svg";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
sourceSize.height: 24
|
||||||
|
sourceSize.width: 24
|
||||||
|
width: 24
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
colorScheme: root.colorScheme
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.alignment: Qt.AlignVCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
Layout.fillWidth: true
|
||||||
Layout.leftMargin: 16
|
Layout.leftMargin: 16
|
||||||
|
|
||||||
color: root.colorScheme.text_invert
|
color: root.colorScheme.text_invert
|
||||||
|
colorScheme: root.colorScheme
|
||||||
text: root.notification ? root.notification.description : ""
|
text: root.notification ? root.notification.description : ""
|
||||||
|
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
width: 1
|
|
||||||
color: {
|
color: {
|
||||||
if (!root.notification) {
|
if (!root.notification) {
|
||||||
return "transparent"
|
return "transparent";
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (root.notification.type) {
|
switch (root.notification.type) {
|
||||||
case Notification.NotificationType.Info:
|
case Notification.NotificationType.Info:
|
||||||
return root.colorScheme.signal_info_active
|
return root.colorScheme.signal_info_active;
|
||||||
case Notification.NotificationType.Success:
|
case Notification.NotificationType.Success:
|
||||||
return root.colorScheme.signal_success_active
|
return root.colorScheme.signal_success_active;
|
||||||
case Notification.NotificationType.Warning:
|
case Notification.NotificationType.Warning:
|
||||||
return root.colorScheme.signal_warning_active
|
return root.colorScheme.signal_warning_active;
|
||||||
case Notification.NotificationType.Danger:
|
case Notification.NotificationType.Danger:
|
||||||
return root.colorScheme.signal_danger_active
|
return root.colorScheme.signal_danger_active;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
width: 1
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
colorScheme: root.colorScheme
|
|
||||||
Layout.fillHeight: true
|
|
||||||
|
|
||||||
id: actionButton
|
id: actionButton
|
||||||
|
Layout.fillHeight: true
|
||||||
action: (root.notification && root.notification.action.length > 0) ? root.notification.action[0] : defaultDismissAction
|
action: (root.notification && root.notification.action.length > 0) ? root.notification.action[0] : defaultDismissAction
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
|
||||||
background: Item {
|
background: Item {
|
||||||
clip: true
|
clip: true
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
width: parent.width + 10
|
anchors.top: parent.top
|
||||||
radius: ProtonStyle.banner_radius
|
|
||||||
color: {
|
color: {
|
||||||
if (!root.notification) {
|
if (!root.notification) {
|
||||||
return "transparent"
|
return "transparent";
|
||||||
}
|
}
|
||||||
|
let norm;
|
||||||
var norm
|
let hover;
|
||||||
var hover
|
let active;
|
||||||
var active
|
|
||||||
|
|
||||||
switch (root.notification.type) {
|
switch (root.notification.type) {
|
||||||
case Notification.NotificationType.Info:
|
case Notification.NotificationType.Info:
|
||||||
norm = root.colorScheme.signal_info
|
norm = root.colorScheme.signal_info;
|
||||||
hover = root.colorScheme.signal_info_hover
|
hover = root.colorScheme.signal_info_hover;
|
||||||
active = root.colorScheme.signal_info_active
|
active = root.colorScheme.signal_info_active;
|
||||||
break;
|
break;
|
||||||
case Notification.NotificationType.Success:
|
case Notification.NotificationType.Success:
|
||||||
norm = root.colorScheme.signal_success
|
norm = root.colorScheme.signal_success;
|
||||||
hover = root.colorScheme.signal_success_hover
|
hover = root.colorScheme.signal_success_hover;
|
||||||
active = root.colorScheme.signal_success_active
|
active = root.colorScheme.signal_success_active;
|
||||||
break;
|
break;
|
||||||
case Notification.NotificationType.Warning:
|
case Notification.NotificationType.Warning:
|
||||||
norm = root.colorScheme.signal_warning
|
norm = root.colorScheme.signal_warning;
|
||||||
hover = root.colorScheme.signal_warning_hover
|
hover = root.colorScheme.signal_warning_hover;
|
||||||
active = root.colorScheme.signal_warning_active
|
active = root.colorScheme.signal_warning_active;
|
||||||
break;
|
break;
|
||||||
case Notification.NotificationType.Danger:
|
case Notification.NotificationType.Danger:
|
||||||
norm = root.colorScheme.signal_danger
|
norm = root.colorScheme.signal_danger;
|
||||||
hover = root.colorScheme.signal_danger_hover
|
hover = root.colorScheme.signal_danger_hover;
|
||||||
active = root.colorScheme.signal_danger_active
|
active = root.colorScheme.signal_danger_active;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (actionButton.down) {
|
if (actionButton.down) {
|
||||||
return active
|
return active;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (actionButton.enabled && (actionButton.highlighted || actionButton.hovered || actionButton.checked)) {
|
if (actionButton.enabled && (actionButton.highlighted || actionButton.hovered || actionButton.checked)) {
|
||||||
return hover
|
return hover;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (actionButton.loading) {
|
if (actionButton.loading) {
|
||||||
return hover
|
return hover;
|
||||||
}
|
}
|
||||||
|
return norm;
|
||||||
return norm
|
|
||||||
}
|
}
|
||||||
|
radius: ProtonStyle.banner_radius
|
||||||
|
width: parent.width + 10
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,129 +1,112 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
//
|
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
|
||||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
// 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
|
// it under the terms of the GNU General Public License as published by
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
// (at your option) any later version.
|
// (at your option) any later version.
|
||||||
//
|
|
||||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
// GNU General Public License for more details.
|
// GNU General Public License for more details.
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU General Public License
|
// 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/>.
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import QtQml
|
import QtQml
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Window
|
import QtQuick.Window
|
||||||
import Qt.labs.platform
|
import Qt.labs.platform
|
||||||
|
|
||||||
import Proton
|
import Proton
|
||||||
import Notifications
|
import Notifications
|
||||||
|
|
||||||
QtObject {
|
QtObject {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
function bound(num, lowerLimit, upperLimit) {
|
property MainWindow _mainWindow: MainWindow {
|
||||||
return Math.max(lowerLimit, Math.min(upperLimit, num))
|
id: mainWindow
|
||||||
|
notifications: root._notifications
|
||||||
|
title: root.title
|
||||||
|
visible: false
|
||||||
|
|
||||||
|
onVisibleChanged: {
|
||||||
|
Backend.dockIconVisible = visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
function onColorSchemeNameChanged(scheme) {
|
||||||
|
root.setColorScheme();
|
||||||
|
}
|
||||||
|
function onDiskCacheUnavailable() {
|
||||||
|
mainWindow.showAndRise();
|
||||||
|
}
|
||||||
|
function onHideMainWindow() {
|
||||||
|
mainWindow.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
target: Backend
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
property var title: Backend.appname
|
|
||||||
|
|
||||||
property Notifications _notifications: Notifications {
|
property Notifications _notifications: Notifications {
|
||||||
id: notifications
|
id: notifications
|
||||||
frontendMain: mainWindow
|
frontendMain: mainWindow
|
||||||
}
|
}
|
||||||
|
|
||||||
property NotificationFilter _trayNotificationFilter: NotificationFilter {
|
property NotificationFilter _trayNotificationFilter: NotificationFilter {
|
||||||
id: trayNotificationFilter
|
id: trayNotificationFilter
|
||||||
source: root._notifications ? root._notifications.all : undefined
|
source: root._notifications ? root._notifications.all : undefined
|
||||||
onTopmostChanged: {
|
|
||||||
if (topmost) {
|
onTopmostChanged: {
|
||||||
switch (topmost.type) {
|
if (topmost) {
|
||||||
case Notification.NotificationType.Danger:
|
switch (topmost.type) {
|
||||||
Backend.setErrorTrayIcon(topmost.brief, topmost.icon)
|
case Notification.NotificationType.Danger:
|
||||||
return
|
Backend.setErrorTrayIcon(topmost.brief, topmost.icon);
|
||||||
case Notification.NotificationType.Warning:
|
return;
|
||||||
Backend.setWarnTrayIcon(topmost.brief, topmost.icon)
|
case Notification.NotificationType.Warning:
|
||||||
return
|
Backend.setWarnTrayIcon(topmost.brief, topmost.icon);
|
||||||
case Notification.NotificationType.Info:
|
return;
|
||||||
Backend.setUpdateTrayIcon(topmost.brief, topmost.icon)
|
case Notification.NotificationType.Info:
|
||||||
return
|
Backend.setUpdateTrayIcon(topmost.brief, topmost.icon);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Backend.setNormalTrayIcon()
|
Backend.setNormalTrayIcon();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
property var title: Backend.appname
|
||||||
|
|
||||||
|
function bound(num, lowerLimit, upperLimit) {
|
||||||
property MainWindow _mainWindow: MainWindow {
|
return Math.max(lowerLimit, Math.min(upperLimit, num));
|
||||||
id: mainWindow
|
}
|
||||||
visible: false
|
function setColorScheme() {
|
||||||
|
if (Backend.colorSchemeName === "light")
|
||||||
title: root.title
|
ProtonStyle.currentStyle = ProtonStyle.lightStyle;
|
||||||
notifications: root._notifications
|
if (Backend.colorSchemeName === "dark")
|
||||||
|
ProtonStyle.currentStyle = ProtonStyle.darkStyle;
|
||||||
onVisibleChanged: {
|
|
||||||
Backend.dockIconVisible = visible
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: Backend
|
|
||||||
function onDiskCacheUnavailable() {
|
|
||||||
mainWindow.showAndRise()
|
|
||||||
}
|
|
||||||
function onColorSchemeNameChanged(scheme) { root.setColorScheme() }
|
|
||||||
|
|
||||||
function onHideMainWindow() {
|
|
||||||
mainWindow.hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
if (!Backend) {
|
if (!Backend) {
|
||||||
console.log("Backend not loaded")
|
console.log("Backend not loaded");
|
||||||
}
|
}
|
||||||
|
root.setColorScheme();
|
||||||
root.setColorScheme()
|
|
||||||
|
|
||||||
|
|
||||||
if (!Backend.users) {
|
if (!Backend.users) {
|
||||||
console.log("users not loaded")
|
console.log("users not loaded");
|
||||||
}
|
}
|
||||||
|
const c = Backend.users.count;
|
||||||
var c = Backend.users.count
|
const u = Backend.users.get(0);
|
||||||
var u = Backend.users.get(0)
|
|
||||||
// DEBUG
|
// DEBUG
|
||||||
if (c !== 0) {
|
if (c !== 0) {
|
||||||
console.log("users non zero", c)
|
console.log("users non zero", c);
|
||||||
console.log("first user", u )
|
console.log("first user", u);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (c === 0) {
|
if (c === 0) {
|
||||||
mainWindow.showAndRise()
|
mainWindow.showAndRise();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (u) {
|
if (u) {
|
||||||
if (c === 1 && (u.state === EUserState.SignedOut)) {
|
if (c === 1 && (u.state === EUserState.SignedOut)) {
|
||||||
mainWindow.showAndRise()
|
mainWindow.showAndRise();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Backend.guiReady();
|
||||||
Backend.guiReady()
|
if (Backend.showOnStartup || Backend.showSplashScreen) {
|
||||||
|
mainWindow.showAndRise();
|
||||||
if (Backend.showOnStartup || Backend.showSplashScreen) {
|
|
||||||
mainWindow.showAndRise()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function setColorScheme() {
|
|
||||||
if (Backend.colorSchemeName === "light") ProtonStyle.currentStyle = ProtonStyle.lightStyle
|
|
||||||
if (Backend.colorSchemeName === "dark") ProtonStyle.currentStyle = ProtonStyle.darkStyle
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,52 @@
|
|||||||
|
// 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
|
||||||
|
|
||||||
|
SettingsView {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
signal categorySelected(int categoryId)
|
||||||
|
|
||||||
|
fillHeight: true
|
||||||
|
|
||||||
|
property var categories: Backend.bugCategories
|
||||||
|
|
||||||
|
Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
text: qsTr("What do you want to report?")
|
||||||
|
type: Label.Heading
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: root.categories
|
||||||
|
|
||||||
|
CategoryItem {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
actionIcon: "/qml/icons/ic-chevron-right.svg"
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
text: modelData.name
|
||||||
|
hint: modelData.hint ? modelData.hint: ""
|
||||||
|
|
||||||
|
onClicked: root.categorySelected(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fill height so the footer label will always be attached to the bottom
|
||||||
|
Item {
|
||||||
|
Layout.fillHeight: true
|
||||||
|
}
|
||||||
|
}
|
||||||
130
internal/frontend/bridge-gui/bridge-gui/qml/BugQuestionView.qml
Normal file
130
internal/frontend/bridge-gui/bridge-gui/qml/BugQuestionView.qml
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
// 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
|
||||||
|
|
||||||
|
SettingsView {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var questions:Backend.bugQuestions
|
||||||
|
property var categoryId:0
|
||||||
|
property var questionSet:ListModel{}
|
||||||
|
property bool error: questionRepeater.error
|
||||||
|
signal questionAnswered
|
||||||
|
|
||||||
|
function setCategoryId(catId) {
|
||||||
|
root.categoryId = catId;
|
||||||
|
}
|
||||||
|
function submit() {
|
||||||
|
root.questionAnswered();
|
||||||
|
}
|
||||||
|
|
||||||
|
fillHeight: true
|
||||||
|
|
||||||
|
onCategoryIdChanged: {
|
||||||
|
root.questionSet = Backend.getQuestionSet(root.categoryId)
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
text: qsTr("Provide more details")
|
||||||
|
type: Label.Heading
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
text: qsTr(Backend.getBugCategory(root.categoryId))
|
||||||
|
type: Label.Title
|
||||||
|
}
|
||||||
|
|
||||||
|
TextEdit {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
color: root.colorScheme.text_weak
|
||||||
|
font.family: ProtonStyle.font_family
|
||||||
|
font.letterSpacing: ProtonStyle.caption_letter_spacing
|
||||||
|
font.pixelSize: ProtonStyle.caption_font_size
|
||||||
|
font.weight: ProtonStyle.fontWeight_400
|
||||||
|
textFormat: Text.MarkdownText
|
||||||
|
readOnly: true
|
||||||
|
selectByMouse: true
|
||||||
|
selectedTextColor: root.colorScheme.text_invert
|
||||||
|
// No way to set lineHeight: ProtonStyle.caption_line_height
|
||||||
|
selectionColor: root.colorScheme.interaction_norm
|
||||||
|
text: qsTr("* Mandatory questions")
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
id: questionRepeater
|
||||||
|
model: root.questionSet
|
||||||
|
property bool error :{
|
||||||
|
for (var i = 0; i < questionRepeater.count; i++) {
|
||||||
|
if (questionRepeater.itemAt(i).error)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validate(){
|
||||||
|
for (var i = 0; i < questionRepeater.count; i++) {
|
||||||
|
questionRepeater.itemAt(i).validate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QuestionItem {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
showSeparator: index < (root.questionSet.length - 1)
|
||||||
|
|
||||||
|
text: root.questions[modelData].text
|
||||||
|
tips: root.questions[modelData].tips ? root.questions[modelData].tips : ""
|
||||||
|
label: root.questions[modelData].label ? root.questions[modelData].label : ""
|
||||||
|
type: root.questions[modelData].type
|
||||||
|
mandatory: root.questions[modelData].mandatory ? root.questions[modelData].mandatory : false
|
||||||
|
answerList: root.questions[modelData].answerList ? root.questions[modelData].answerList : []
|
||||||
|
maxChar: root.questions[modelData].maxChar ? root.questions[modelData].maxChar : 150
|
||||||
|
|
||||||
|
onAnswerChanged: {
|
||||||
|
Backend.setQuestionAnswer(modelData, answer);
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
function onVisibleChanged() {
|
||||||
|
setDefaultValue(Backend.getQuestionAnswer(modelData))
|
||||||
|
}
|
||||||
|
target: root
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// fill height so the footer label will always be attached to the bottom
|
||||||
|
Item {
|
||||||
|
Layout.fillHeight: true
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
id: continueButton
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
enabled: !loading && !root.error
|
||||||
|
text: qsTr("Continue")
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
questionRepeater.validate()
|
||||||
|
if (!root.error)
|
||||||
|
submit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,99 @@
|
|||||||
|
// 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
|
||||||
|
import Notifications
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property ColorScheme colorScheme
|
||||||
|
property string selectedAddress
|
||||||
|
property int categoryId: -1
|
||||||
|
|
||||||
|
signal back
|
||||||
|
signal bugReportWasSent
|
||||||
|
|
||||||
|
onVisibleChanged: {
|
||||||
|
root.showBugCategory();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showBugCategory() {
|
||||||
|
bugReportFlow.currentIndex = 0;
|
||||||
|
}
|
||||||
|
function showBugQuestion() {
|
||||||
|
bugQuestion.setCategoryId(root.categoryId);
|
||||||
|
bugQuestion.positionViewAtBegining();
|
||||||
|
bugReportFlow.currentIndex = 1;
|
||||||
|
}
|
||||||
|
function showBugReport() {
|
||||||
|
bugReport.setCategoryId(root.categoryId);
|
||||||
|
bugReportFlow.currentIndex = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
Layout.fillHeight: true // right content background
|
||||||
|
Layout.fillWidth: true
|
||||||
|
color: colorScheme.background_norm
|
||||||
|
|
||||||
|
StackLayout {
|
||||||
|
id: bugReportFlow
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
BugCategoryView {
|
||||||
|
// 0
|
||||||
|
id: bugCategory
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
|
||||||
|
onBack: {
|
||||||
|
root.back()
|
||||||
|
}
|
||||||
|
onCategorySelected: function(categoryId){
|
||||||
|
root.categoryId = categoryId
|
||||||
|
root.showBugQuestion();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BugQuestionView {
|
||||||
|
// 1
|
||||||
|
id: bugQuestion
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
|
||||||
|
onBack: {
|
||||||
|
root.showBugCategory();
|
||||||
|
}
|
||||||
|
onQuestionAnswered: {
|
||||||
|
root.showBugReport();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BugReportView {
|
||||||
|
// 2
|
||||||
|
id: bugReport
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
selectedAddress: root.selectedAddress
|
||||||
|
|
||||||
|
onBack: {
|
||||||
|
root.showBugQuestion();
|
||||||
|
}
|
||||||
|
onBugReportWasSent: {
|
||||||
|
Backend.clearAnswers();
|
||||||
|
root.bugReportWasSent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,202 +1,161 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
//
|
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
|
||||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
// 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
|
// it under the terms of the GNU General Public License as published by
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
// (at your option) any later version.
|
// (at your option) any later version.
|
||||||
//
|
|
||||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
// GNU General Public License for more details.
|
// GNU General Public License for more details.
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU General Public License
|
// 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/>.
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
|
|
||||||
import Proton
|
import Proton
|
||||||
|
|
||||||
SettingsView {
|
SettingsView {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
fillHeight: true
|
|
||||||
|
|
||||||
property var selectedAddress
|
property var selectedAddress
|
||||||
|
property var categoryId:-1
|
||||||
|
property string category: Backend.getBugCategory(root.categoryId)
|
||||||
|
|
||||||
signal bugReportWasSent()
|
signal bugReportWasSent
|
||||||
|
|
||||||
Label {
|
function isValidEmail(text) {
|
||||||
text: qsTr("Report a problem")
|
const reEmail = /^[^@]+@[^@]+\.[A-Za-z]+\s*$/;
|
||||||
colorScheme: root.colorScheme
|
return reEmail.test(text);
|
||||||
type: Label.Heading
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setCategoryId(catId) {
|
||||||
|
root.categoryId = catId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDefaultValue() {
|
||||||
|
description.text = Backend.collectAnswers(root.categoryId);
|
||||||
|
address.text = root.selectedAddress;
|
||||||
|
emailClient.text = Backend.currentEmailClient;
|
||||||
|
includeLogs.checked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function submit() {
|
||||||
|
sendButton.loading = true;
|
||||||
|
Backend.reportBug(root.category, description.text, address.text, emailClient.text, includeLogs.checked);
|
||||||
|
}
|
||||||
|
|
||||||
|
fillHeight: true
|
||||||
|
|
||||||
|
onVisibleChanged: {
|
||||||
|
root.setDefaultValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
text: qsTr("Send report")
|
||||||
|
type: Label.Heading
|
||||||
|
}
|
||||||
TextArea {
|
TextArea {
|
||||||
id: description
|
id: description
|
||||||
property int _minLength: 150
|
|
||||||
property int _maxLength: 800
|
|
||||||
|
|
||||||
label: qsTr("Description")
|
|
||||||
colorScheme: root.colorScheme
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.fillHeight: true
|
|
||||||
Layout.minimumHeight: heightForLinesVisible(4)
|
|
||||||
hint: description.text.length + "/" + _maxLength
|
|
||||||
placeholderText: qsTr("Tell us what went wrong or isn't working (min. %1 characters).").arg(_minLength)
|
|
||||||
|
|
||||||
validator: function(text) {
|
|
||||||
if (description.text.length < description._minLength) {
|
|
||||||
return qsTr("Enter a problem description (min. %1 characters).").arg(_minLength)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (description.text.length > description._maxLength) {
|
|
||||||
return qsTr("Enter a problem description (max. %1 characters).").arg(_maxLength)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
onTextChanged: {
|
|
||||||
// Rise max length error immediately while typing
|
|
||||||
if (description.text.length > description._maxLength) {
|
|
||||||
validate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
KeyNavigation.priority: KeyNavigation.BeforeItem
|
KeyNavigation.priority: KeyNavigation.BeforeItem
|
||||||
KeyNavigation.tab: address
|
KeyNavigation.tab: address
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.minimumHeight: heightForLinesVisible(4)
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
textFormat: Text.MarkdownText
|
||||||
|
|
||||||
// set implicitHeight to explicit height because se don't
|
// set implicitHeight to explicit height because se don't
|
||||||
// want TextArea implicitHeight (which is height of all text)
|
// want TextArea implicitHeight (which is height of all text)
|
||||||
// to be considered in SettingsView internal scroll view
|
// to be considered in SettingsView internal scroll view
|
||||||
implicitHeight: height
|
implicitHeight: height
|
||||||
|
label: "Your answers to: " + qsTr(root.category);
|
||||||
|
readOnly : true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
TextField {
|
TextField {
|
||||||
id: address
|
id: address
|
||||||
|
|
||||||
label: qsTr("Your contact email")
|
|
||||||
colorScheme: root.colorScheme
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
label: qsTr("Your contact email")
|
||||||
placeholderText: qsTr("e.g. jane.doe@protonmail.com")
|
placeholderText: qsTr("e.g. jane.doe@protonmail.com")
|
||||||
|
validator: function (str) {
|
||||||
validator: function(str) {
|
|
||||||
if (!isValidEmail(str)) {
|
if (!isValidEmail(str)) {
|
||||||
return qsTr("Enter valid email address")
|
return qsTr("Enter valid email address");
|
||||||
}
|
}
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TextField {
|
TextField {
|
||||||
id: emailClient
|
id: emailClient
|
||||||
|
|
||||||
label: qsTr("Your email client (including version)")
|
|
||||||
colorScheme: root.colorScheme
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
label: qsTr("Your email client (including version)")
|
||||||
placeholderText: qsTr("e.g. Apple Mail 14.0")
|
placeholderText: qsTr("e.g. Apple Mail 14.0")
|
||||||
|
validator: function (str) {
|
||||||
validator: function(str) {
|
|
||||||
if (str.length === 0) {
|
if (str.length === 0) {
|
||||||
return qsTr("Enter an email client name and version")
|
return qsTr("Enter an email client name and version");
|
||||||
}
|
}
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
CheckBox {
|
CheckBox {
|
||||||
id: includeLogs
|
id: includeLogs
|
||||||
text: qsTr("Include my recent logs")
|
|
||||||
colorScheme: root.colorScheme
|
|
||||||
checked: true
|
checked: true
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
text: qsTr("Include my recent logs")
|
||||||
}
|
}
|
||||||
Button {
|
Button {
|
||||||
Layout.leftMargin: 12
|
Layout.leftMargin: 12
|
||||||
text: qsTr("View logs")
|
|
||||||
secondary: true
|
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
|
secondary: true
|
||||||
|
text: qsTr("View logs")
|
||||||
|
|
||||||
onClicked: Qt.openUrlExternally(Backend.logsPath)
|
onClicked: Qt.openUrlExternally(Backend.logsPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TextEdit {
|
TextEdit {
|
||||||
text: qsTr("Reports are not end-to-end encrypted, please do not send any sensitive information.")
|
|
||||||
|
|
||||||
readOnly: true
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
color: root.colorScheme.text_weak
|
color: root.colorScheme.text_weak
|
||||||
font.family: ProtonStyle.font_family
|
font.family: ProtonStyle.font_family
|
||||||
font.weight: ProtonStyle.fontWeight_400
|
|
||||||
font.pixelSize: ProtonStyle.caption_font_size
|
|
||||||
font.letterSpacing: ProtonStyle.caption_letter_spacing
|
font.letterSpacing: ProtonStyle.caption_letter_spacing
|
||||||
|
font.pixelSize: ProtonStyle.caption_font_size
|
||||||
|
font.weight: ProtonStyle.fontWeight_400
|
||||||
|
readOnly: true
|
||||||
|
selectByMouse: true
|
||||||
|
selectedTextColor: root.colorScheme.text_invert
|
||||||
// No way to set lineHeight: ProtonStyle.caption_line_height
|
// No way to set lineHeight: ProtonStyle.caption_line_height
|
||||||
selectionColor: root.colorScheme.interaction_norm
|
selectionColor: root.colorScheme.interaction_norm
|
||||||
selectedTextColor: root.colorScheme.text_invert
|
text: qsTr("Reports are not end-to-end encrypted, please do not send any sensitive information.")
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
selectByMouse: true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
id: sendButton
|
id: sendButton
|
||||||
text: qsTr("Send")
|
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
enabled: !loading
|
enabled: !loading
|
||||||
|
text: qsTr("Send")
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
description.validate()
|
description.validate();
|
||||||
address.validate()
|
address.validate();
|
||||||
emailClient.validate()
|
emailClient.validate();
|
||||||
|
|
||||||
if (description.error || address.error || emailClient.error) {
|
if (description.error || address.error || emailClient.error) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
submit();
|
||||||
submit()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
|
function onBugReportSendSuccess() {
|
||||||
|
root.bugReportWasSent();
|
||||||
|
}
|
||||||
|
function onReportBugFinished() {
|
||||||
|
sendButton.loading = false;
|
||||||
|
}
|
||||||
|
|
||||||
target: Backend
|
target: Backend
|
||||||
function onReportBugFinished() { sendButton.loading = false }
|
|
||||||
function onBugReportSendSuccess() { root.bugReportWasSent() }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
function setDescription(message) {
|
|
||||||
description.text = message
|
|
||||||
}
|
|
||||||
|
|
||||||
function setDefaultValue() {
|
|
||||||
description.text = ""
|
|
||||||
address.text = root.selectedAddress
|
|
||||||
emailClient.text = Backend.currentEmailClient
|
|
||||||
includeLogs.checked = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function isValidEmail(text){
|
|
||||||
var reEmail = /^[^@]+@[^@]+\.[A-Za-z]+\s*$/
|
|
||||||
return reEmail.test(text)
|
|
||||||
}
|
|
||||||
|
|
||||||
function submit() {
|
|
||||||
sendButton.loading = true
|
|
||||||
Backend.reportBug(
|
|
||||||
description.text,
|
|
||||||
address.text,
|
|
||||||
emailClient.text,
|
|
||||||
includeLogs.checked
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
onVisibleChanged: {
|
|
||||||
root.setDefaultValue()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
113
internal/frontend/bridge-gui/bridge-gui/qml/CategoryItem.qml
Normal file
113
internal/frontend/bridge-gui/bridge-gui/qml/CategoryItem.qml
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
// 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
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var _bottomMargin: 20
|
||||||
|
property var _lineHeight: 1
|
||||||
|
property string actionIcon: ""
|
||||||
|
property var colorScheme
|
||||||
|
property bool showSeparator: true
|
||||||
|
property string text: "Text"
|
||||||
|
property string hint: ""
|
||||||
|
|
||||||
|
signal clicked
|
||||||
|
|
||||||
|
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: 16
|
||||||
|
|
||||||
|
Label {
|
||||||
|
id: mainLabel
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
text: root.text
|
||||||
|
type: Label.Body
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
Layout.bottomMargin: root._bottomMargin
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
ColorImage {
|
||||||
|
id: infoImage
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
Layout.bottomMargin: root._bottomMargin
|
||||||
|
color: root.colorScheme.interaction_norm
|
||||||
|
height: 21
|
||||||
|
width: 21
|
||||||
|
source: "/qml/icons/ic-info-circle.svg"
|
||||||
|
sourceSize.height: 21
|
||||||
|
sourceSize.width: 21
|
||||||
|
visible: root.hint !== ""
|
||||||
|
MouseArea {
|
||||||
|
id: imageArea
|
||||||
|
anchors.fill: infoImage
|
||||||
|
hoverEnabled: true
|
||||||
|
}
|
||||||
|
ToolTip {
|
||||||
|
id: toolTipinfo
|
||||||
|
text: root.hint
|
||||||
|
visible: imageArea.containsMouse
|
||||||
|
implicitWidth: Math.min(400, tooltipText.implicitWidth)
|
||||||
|
background: Rectangle {
|
||||||
|
radius: 4
|
||||||
|
border.color: root.colorScheme.border_weak
|
||||||
|
color: root.colorScheme.background_weak
|
||||||
|
}
|
||||||
|
contentItem: Text {
|
||||||
|
id: tooltipText
|
||||||
|
color: root.colorScheme.text_hint
|
||||||
|
text: toolTipinfo.text
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fill height so the footer label will always be attached to the bottom
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
id: button
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
Layout.bottomMargin: root._bottomMargin
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
icon.source: root.actionIcon
|
||||||
|
text: ""
|
||||||
|
secondary: true
|
||||||
|
visible: root.actionIcon !== ""
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
if (!root.loading)
|
||||||
|
root.clicked();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Rectangle {
|
||||||
|
anchors.bottom: root.bottom
|
||||||
|
anchors.left: root.left
|
||||||
|
anchors.right: root.right
|
||||||
|
color: colorScheme.border_weak
|
||||||
|
height: root._lineHeight
|
||||||
|
visible: root.showSeparator
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,71 +1,80 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
//
|
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
|
||||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
// 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
|
// it under the terms of the GNU General Public License as published by
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
// (at your option) any later version.
|
// (at your option) any later version.
|
||||||
//
|
|
||||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
// GNU General Public License for more details.
|
// GNU General Public License for more details.
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU General Public License
|
// 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/>.
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import QtQuick.Controls.impl
|
import QtQuick.Controls.impl
|
||||||
|
|
||||||
import Proton
|
import Proton
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
property int _margin: 24
|
||||||
property ColorScheme colorScheme
|
property ColorScheme colorScheme
|
||||||
property string title
|
|
||||||
property string hostname
|
property string hostname
|
||||||
property string port
|
|
||||||
property string username
|
|
||||||
property string password
|
property string password
|
||||||
|
property string port
|
||||||
property string security
|
property string security
|
||||||
|
property string title
|
||||||
implicitWidth: 304
|
property string username
|
||||||
implicitHeight: content.height + 2*root._margin
|
|
||||||
|
|
||||||
color: root.colorScheme.background_norm
|
color: root.colorScheme.background_norm
|
||||||
|
implicitHeight: content.height + 2 * root._margin
|
||||||
|
implicitWidth: 304
|
||||||
radius: ProtonStyle.card_radius
|
radius: ProtonStyle.card_radius
|
||||||
|
|
||||||
property int _margin: 24
|
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: content
|
id: content
|
||||||
width: root.width - 2*root._margin
|
|
||||||
anchors{
|
|
||||||
top: root.top
|
|
||||||
left: root.left
|
|
||||||
leftMargin : root._margin
|
|
||||||
rightMargin : root._margin
|
|
||||||
topMargin : root._margin
|
|
||||||
bottomMargin : root._margin
|
|
||||||
}
|
|
||||||
|
|
||||||
spacing: 12
|
spacing: 12
|
||||||
|
width: root.width - 2 * root._margin
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
bottomMargin: root._margin
|
||||||
|
left: root.left
|
||||||
|
leftMargin: root._margin
|
||||||
|
rightMargin: root._margin
|
||||||
|
top: root.top
|
||||||
|
topMargin: root._margin
|
||||||
|
}
|
||||||
Label {
|
Label {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
text: root.title
|
text: root.title
|
||||||
type: Label.Body_semibold
|
type: Label.Body_semibold
|
||||||
}
|
}
|
||||||
|
ConfigurationItem {
|
||||||
ConfigurationItem{ colorScheme: root.colorScheme; label: qsTr("Hostname") ; value: root.hostname }
|
colorScheme: root.colorScheme
|
||||||
ConfigurationItem{ colorScheme: root.colorScheme; label: qsTr("Port") ; value: root.port }
|
label: qsTr("Hostname")
|
||||||
ConfigurationItem{ colorScheme: root.colorScheme; label: qsTr("Username") ; value: root.username }
|
value: root.hostname
|
||||||
ConfigurationItem{ colorScheme: root.colorScheme; label: qsTr("Password") ; value: root.password }
|
}
|
||||||
ConfigurationItem{ colorScheme: root.colorScheme; label: qsTr("Security") ; value: root.security }
|
ConfigurationItem {
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
label: qsTr("Port")
|
||||||
|
value: root.port
|
||||||
|
}
|
||||||
|
ConfigurationItem {
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
label: qsTr("Username")
|
||||||
|
value: root.username
|
||||||
|
}
|
||||||
|
ConfigurationItem {
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
label: qsTr("Password")
|
||||||
|
value: root.password
|
||||||
|
}
|
||||||
|
ConfigurationItem {
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
label: qsTr("Security")
|
||||||
|
value: root.security
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,35 +1,29 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
//
|
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
|
||||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
// 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
|
// it under the terms of the GNU General Public License as published by
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
// (at your option) any later version.
|
// (at your option) any later version.
|
||||||
//
|
|
||||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
// GNU General Public License for more details.
|
// GNU General Public License for more details.
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU General Public License
|
// 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/>.
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import QtQuick.Controls.impl
|
import QtQuick.Controls.impl
|
||||||
|
|
||||||
import Proton
|
import Proton
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
Layout.fillWidth: true
|
|
||||||
|
|
||||||
property var colorScheme
|
property var colorScheme
|
||||||
property string label
|
property string label
|
||||||
property string value
|
property string value
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
implicitHeight: children[0].implicitHeight
|
implicitHeight: children[0].implicitHeight
|
||||||
implicitWidth: children[0].implicitWidth
|
implicitWidth: children[0].implicitWidth
|
||||||
|
|
||||||
@ -47,45 +41,42 @@ Item {
|
|||||||
}
|
}
|
||||||
TextEdit {
|
TextEdit {
|
||||||
id: valueText
|
id: valueText
|
||||||
text: root.value
|
Layout.fillWidth: true
|
||||||
color: root.colorScheme.text_weak
|
color: root.colorScheme.text_weak
|
||||||
readOnly: true
|
readOnly: true
|
||||||
selectByMouse: true
|
|
||||||
selectByKeyboard: true
|
selectByKeyboard: true
|
||||||
|
selectByMouse: true
|
||||||
selectionColor: root.colorScheme.text_weak
|
selectionColor: root.colorScheme.text_weak
|
||||||
|
text: root.value
|
||||||
wrapMode: Text.WrapAnywhere
|
wrapMode: Text.WrapAnywhere
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
ColorImage {
|
ColorImage {
|
||||||
source: "/qml/icons/ic-copy.svg"
|
|
||||||
color: root.colorScheme.text_norm
|
color: root.colorScheme.text_norm
|
||||||
height: root.colorScheme.body_font_size
|
height: root.colorScheme.body_font_size
|
||||||
|
source: "/qml/icons/ic-copy.svg"
|
||||||
sourceSize.height: root.colorScheme.body_font_size
|
sourceSize.height: root.colorScheme.body_font_size
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
onClicked : {
|
|
||||||
valueText.select(0, valueText.length)
|
onClicked: {
|
||||||
valueText.copy()
|
valueText.select(0, valueText.length);
|
||||||
valueText.deselect()
|
valueText.copy();
|
||||||
|
valueText.deselect();
|
||||||
}
|
}
|
||||||
onPressed: parent.scale = 0.90
|
onPressed: parent.scale = 0.90
|
||||||
onReleased: parent.scale = 1
|
onReleased: parent.scale = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
height: 1
|
|
||||||
color: root.colorScheme.border_norm
|
color: root.colorScheme.border_norm
|
||||||
|
height: 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,155 +1,138 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
//
|
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
|
||||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
// 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
|
// it under the terms of the GNU General Public License as published by
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
// (at your option) any later version.
|
// (at your option) any later version.
|
||||||
//
|
|
||||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
// GNU General Public License for more details.
|
// GNU General Public License for more details.
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU General Public License
|
// 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/>.
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import QtQuick.Controls.impl
|
import QtQuick.Controls.impl
|
||||||
|
|
||||||
import Proton
|
import Proton
|
||||||
|
|
||||||
SettingsView {
|
SettingsView {
|
||||||
id: root
|
id: root
|
||||||
|
function setDefaultValues() {
|
||||||
|
imapSSLButton.checked = Backend.useSSLForIMAP;
|
||||||
|
imapSTARTTLSButton.checked = !Backend.useSSLForIMAP;
|
||||||
|
smtpSSLButton.checked = Backend.useSSLForSMTP;
|
||||||
|
smtpSTARTTLSButton.checked = !Backend.useSSLForSMTP;
|
||||||
|
}
|
||||||
|
function submit() {
|
||||||
|
submitButton.loading = true;
|
||||||
|
Backend.setMailServerSettings(Backend.imapPort, Backend.smtpPort, imapSSLButton.checked, smtpSSLButton.checked);
|
||||||
|
}
|
||||||
|
|
||||||
fillHeight: false
|
fillHeight: false
|
||||||
|
|
||||||
|
onVisibleChanged: {
|
||||||
|
root.setDefaultValues();
|
||||||
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
text: qsTr("Connection mode")
|
text: qsTr("Connection mode")
|
||||||
type: Label.Heading
|
type: Label.Heading
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
color: root.colorScheme.text_weak
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
text: qsTr("Change the protocol Bridge and the email client use to connect for IMAP and SMTP.")
|
text: qsTr("Change the protocol Bridge and the email client use to connect for IMAP and SMTP.")
|
||||||
type: Label.Body
|
type: Label.Body
|
||||||
color: root.colorScheme.text_weak
|
|
||||||
Layout.fillWidth: true
|
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
spacing: 16
|
spacing: 16
|
||||||
|
|
||||||
ButtonGroup{ id: imapProtocolSelection }
|
ButtonGroup {
|
||||||
|
id: imapProtocolSelection
|
||||||
|
}
|
||||||
Label {
|
Label {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
text: qsTr("IMAP connection")
|
text: qsTr("IMAP connection")
|
||||||
}
|
}
|
||||||
|
|
||||||
RadioButton {
|
RadioButton {
|
||||||
id: imapSSLButton
|
id: imapSSLButton
|
||||||
colorScheme: root.colorScheme
|
|
||||||
ButtonGroup.group: imapProtocolSelection
|
ButtonGroup.group: imapProtocolSelection
|
||||||
|
colorScheme: root.colorScheme
|
||||||
text: qsTr("SSL")
|
text: qsTr("SSL")
|
||||||
}
|
}
|
||||||
|
|
||||||
RadioButton {
|
RadioButton {
|
||||||
id: imapSTARTTLSButton
|
id: imapSTARTTLSButton
|
||||||
colorScheme: root.colorScheme
|
|
||||||
ButtonGroup.group: imapProtocolSelection
|
ButtonGroup.group: imapProtocolSelection
|
||||||
|
colorScheme: root.colorScheme
|
||||||
text: qsTr("STARTTLS")
|
text: qsTr("STARTTLS")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
height: 1
|
|
||||||
color: root.colorScheme.border_weak
|
color: root.colorScheme.border_weak
|
||||||
|
height: 1
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
spacing: 16
|
spacing: 16
|
||||||
|
|
||||||
ButtonGroup{ id: smtpProtocolSelection }
|
ButtonGroup {
|
||||||
|
id: smtpProtocolSelection
|
||||||
|
}
|
||||||
Label {
|
Label {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
text: qsTr("SMTP connection")
|
text: qsTr("SMTP connection")
|
||||||
}
|
}
|
||||||
|
|
||||||
RadioButton {
|
RadioButton {
|
||||||
id: smtpSSLButton
|
id: smtpSSLButton
|
||||||
colorScheme: root.colorScheme
|
|
||||||
ButtonGroup.group: smtpProtocolSelection
|
ButtonGroup.group: smtpProtocolSelection
|
||||||
|
colorScheme: root.colorScheme
|
||||||
text: qsTr("SSL")
|
text: qsTr("SSL")
|
||||||
}
|
}
|
||||||
|
|
||||||
RadioButton {
|
RadioButton {
|
||||||
id: smtpSTARTTLSButton
|
id: smtpSTARTTLSButton
|
||||||
colorScheme: root.colorScheme
|
|
||||||
ButtonGroup.group: smtpProtocolSelection
|
ButtonGroup.group: smtpProtocolSelection
|
||||||
|
colorScheme: root.colorScheme
|
||||||
text: qsTr("STARTTLS")
|
text: qsTr("STARTTLS")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
height: 1
|
|
||||||
color: root.colorScheme.border_weak
|
color: root.colorScheme.border_weak
|
||||||
|
height: 1
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
spacing: 12
|
spacing: 12
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
id: submitButton
|
id: submitButton
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
text: qsTr("Save")
|
|
||||||
onClicked: {
|
|
||||||
submitButton.loading = true
|
|
||||||
root.submit()
|
|
||||||
}
|
|
||||||
|
|
||||||
enabled: (!loading) && ((imapSSLButton.checked !== Backend.useSSLForIMAP) || (smtpSSLButton.checked !== Backend.useSSLForSMTP))
|
enabled: (!loading) && ((imapSSLButton.checked !== Backend.useSSLForIMAP) || (smtpSSLButton.checked !== Backend.useSSLForSMTP))
|
||||||
}
|
text: qsTr("Save")
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
submitButton.loading = true;
|
||||||
|
root.submit();
|
||||||
|
}
|
||||||
|
}
|
||||||
Button {
|
Button {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
text: qsTr("Cancel")
|
|
||||||
onClicked: root.back()
|
|
||||||
secondary: true
|
secondary: true
|
||||||
}
|
text: qsTr("Cancel")
|
||||||
|
|
||||||
|
onClicked: root.back()
|
||||||
|
}
|
||||||
Connections {
|
Connections {
|
||||||
target: Backend
|
|
||||||
|
|
||||||
function onChangeMailServerSettingsFinished() {
|
function onChangeMailServerSettingsFinished() {
|
||||||
submitButton.loading = false
|
submitButton.loading = false;
|
||||||
root.back()
|
root.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
target: Backend
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function submit(){
|
|
||||||
submitButton.loading = true
|
|
||||||
Backend.setMailServerSettings(Backend.imapPort, Backend.smtpPort, imapSSLButton.checked, smtpSSLButton.checked)
|
|
||||||
}
|
|
||||||
|
|
||||||
function setDefaultValues(){
|
|
||||||
imapSSLButton.checked = Backend.useSSLForIMAP
|
|
||||||
imapSTARTTLSButton.checked = !Backend.useSSLForIMAP
|
|
||||||
smtpSSLButton.checked = Backend.useSSLForSMTP
|
|
||||||
smtpSTARTTLSButton.checked = !Backend.useSSLForSMTP
|
|
||||||
}
|
|
||||||
|
|
||||||
onVisibleChanged: {
|
|
||||||
root.setDefaultValues()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,36 +1,58 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
//
|
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
|
||||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
// 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
|
// it under the terms of the GNU General Public License as published by
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
// (at your option) any later version.
|
// (at your option) any later version.
|
||||||
//
|
|
||||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
// GNU General Public License for more details.
|
// GNU General Public License for more details.
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU General Public License
|
// 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/>.
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
|
|
||||||
import Proton
|
import Proton
|
||||||
import Notifications
|
import Notifications
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
property ColorScheme colorScheme
|
|
||||||
|
|
||||||
|
property ColorScheme colorScheme
|
||||||
property var notifications
|
property var notifications
|
||||||
|
|
||||||
|
signal closeWindow
|
||||||
|
signal quitBridge
|
||||||
signal showSetupGuide(var user, string address)
|
signal showSetupGuide(var user, string address)
|
||||||
signal closeWindow()
|
|
||||||
signal quitBridge()
|
function selectUser(userID) {
|
||||||
|
const users = Backend.users;
|
||||||
|
for (let i = 0; i < users.count; i++) {
|
||||||
|
const user = users.get(i);
|
||||||
|
if (user.id !== userID) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
accounts.currentIndex = i;
|
||||||
|
if (user.state === EUserState.SignedOut)
|
||||||
|
showSignIn(user.primaryEmailOrUsername());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.error("User with ID ", userID, " was not found in the account list");
|
||||||
|
}
|
||||||
|
function showHelp() {
|
||||||
|
rightContent.showHelpView();
|
||||||
|
}
|
||||||
|
function showLocalCacheSettings() {
|
||||||
|
rightContent.showLocalCacheSettings();
|
||||||
|
}
|
||||||
|
function showSettings() {
|
||||||
|
rightContent.showGeneralSettings();
|
||||||
|
}
|
||||||
|
function showSignIn(username) {
|
||||||
|
signIn.username = username;
|
||||||
|
rightContent.showSignIn();
|
||||||
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@ -38,13 +60,13 @@ Item {
|
|||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: leftBar
|
id: leftBar
|
||||||
|
|
||||||
property ColorScheme colorScheme: root.colorScheme.prominent
|
property ColorScheme colorScheme: root.colorScheme.prominent
|
||||||
|
|
||||||
Layout.minimumWidth: 264
|
|
||||||
Layout.maximumWidth: 320
|
|
||||||
Layout.preferredWidth: 320
|
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
|
Layout.maximumWidth: 320
|
||||||
|
Layout.minimumWidth: 264
|
||||||
|
Layout.preferredWidth: 320
|
||||||
color: colorScheme.background_norm
|
color: colorScheme.background_norm
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
@ -52,24 +74,21 @@ Item {
|
|||||||
spacing: 0
|
spacing: 0
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
id:topLeftBar
|
id: topLeftBar
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.minimumHeight: 60
|
|
||||||
Layout.maximumHeight: 60
|
Layout.maximumHeight: 60
|
||||||
|
Layout.minimumHeight: 60
|
||||||
Layout.preferredHeight: 60
|
Layout.preferredHeight: 60
|
||||||
spacing: 0
|
spacing: 0
|
||||||
|
|
||||||
Status {
|
Status {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
Layout.bottomMargin: 17
|
||||||
Layout.leftMargin: 16
|
Layout.leftMargin: 16
|
||||||
Layout.topMargin: 24
|
Layout.topMargin: 24
|
||||||
Layout.bottomMargin: 17
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
|
|
||||||
colorScheme: leftBar.colorScheme
|
colorScheme: leftBar.colorScheme
|
||||||
notifications: root.notifications
|
|
||||||
|
|
||||||
notificationWhitelist: Notifications.Group.Connection | Notifications.Group.ForceUpdate
|
notificationWhitelist: Notifications.Group.Connection | Notifications.Group.ForceUpdate
|
||||||
|
notifications: root.notifications
|
||||||
}
|
}
|
||||||
|
|
||||||
// just a placeholder
|
// just a placeholder
|
||||||
@ -77,47 +96,38 @@ Item {
|
|||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
colorScheme: leftBar.colorScheme
|
|
||||||
Layout.minimumHeight: 36
|
|
||||||
Layout.maximumHeight: 36
|
|
||||||
Layout.preferredHeight: 36
|
|
||||||
Layout.minimumWidth: 36
|
|
||||||
Layout.maximumWidth: 36
|
|
||||||
Layout.preferredWidth: 36
|
|
||||||
|
|
||||||
Layout.topMargin: 16
|
|
||||||
Layout.bottomMargin: 9
|
Layout.bottomMargin: 9
|
||||||
|
Layout.maximumHeight: 36
|
||||||
|
Layout.maximumWidth: 36
|
||||||
|
Layout.minimumHeight: 36
|
||||||
|
Layout.minimumWidth: 36
|
||||||
|
Layout.preferredHeight: 36
|
||||||
|
Layout.preferredWidth: 36
|
||||||
Layout.rightMargin: 4
|
Layout.rightMargin: 4
|
||||||
|
Layout.topMargin: 16
|
||||||
|
colorScheme: leftBar.colorScheme
|
||||||
horizontalPadding: 0
|
horizontalPadding: 0
|
||||||
|
|
||||||
icon.source: "/qml/icons/ic-question-circle.svg"
|
icon.source: "/qml/icons/ic-question-circle.svg"
|
||||||
|
|
||||||
onClicked: rightContent.showHelpView()
|
onClicked: rightContent.showHelpView()
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
colorScheme: leftBar.colorScheme
|
|
||||||
Layout.minimumHeight: 36
|
|
||||||
Layout.maximumHeight: 36
|
|
||||||
Layout.preferredHeight: 36
|
|
||||||
Layout.minimumWidth: 36
|
|
||||||
Layout.maximumWidth: 36
|
|
||||||
Layout.preferredWidth: 36
|
|
||||||
|
|
||||||
Layout.topMargin: 16
|
|
||||||
Layout.bottomMargin: 9
|
Layout.bottomMargin: 9
|
||||||
|
Layout.maximumHeight: 36
|
||||||
|
Layout.maximumWidth: 36
|
||||||
|
Layout.minimumHeight: 36
|
||||||
|
Layout.minimumWidth: 36
|
||||||
|
Layout.preferredHeight: 36
|
||||||
|
Layout.preferredWidth: 36
|
||||||
Layout.rightMargin: 4
|
Layout.rightMargin: 4
|
||||||
|
Layout.topMargin: 16
|
||||||
|
colorScheme: leftBar.colorScheme
|
||||||
horizontalPadding: 0
|
horizontalPadding: 0
|
||||||
|
|
||||||
icon.source: "/qml/icons/ic-cog-wheel.svg"
|
icon.source: "/qml/icons/ic-cog-wheel.svg"
|
||||||
|
|
||||||
onClicked: rightContent.showGeneralSettings()
|
onClicked: rightContent.showGeneralSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
id: dotMenuButton
|
id: dotMenuButton
|
||||||
Layout.bottomMargin: 9
|
Layout.bottomMargin: 9
|
||||||
@ -134,7 +144,7 @@ Item {
|
|||||||
icon.source: "/qml/icons/ic-three-dots-vertical.svg"
|
icon.source: "/qml/icons/ic-three-dots-vertical.svg"
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
dotMenu.open()
|
dotMenu.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
Menu {
|
Menu {
|
||||||
@ -143,332 +153,319 @@ Item {
|
|||||||
modal: true
|
modal: true
|
||||||
y: dotMenuButton.Layout.preferredHeight + dotMenuButton.Layout.bottomMargin
|
y: dotMenuButton.Layout.preferredHeight + dotMenuButton.Layout.bottomMargin
|
||||||
|
|
||||||
|
onClosed: {
|
||||||
|
parent.checked = false;
|
||||||
|
}
|
||||||
|
onOpened: {
|
||||||
|
parent.checked = true;
|
||||||
|
}
|
||||||
|
|
||||||
MenuItem {
|
MenuItem {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
text: qsTr("Close window")
|
text: qsTr("Close window")
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
root.closeWindow()
|
root.closeWindow();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MenuItem {
|
MenuItem {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
text: qsTr("Quit Bridge")
|
text: qsTr("Quit Bridge")
|
||||||
onClicked: {
|
|
||||||
root.quitBridge()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onClosed: {
|
onClicked: {
|
||||||
parent.checked = false
|
root.quitBridge();
|
||||||
}
|
}
|
||||||
onOpened: {
|
|
||||||
parent.checked = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Item {
|
||||||
Item {implicitHeight:10}
|
implicitHeight: 10
|
||||||
|
}
|
||||||
|
|
||||||
// Separator line
|
// Separator line
|
||||||
Rectangle {
|
Rectangle {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.minimumHeight: 1
|
|
||||||
Layout.maximumHeight: 1
|
Layout.maximumHeight: 1
|
||||||
|
Layout.minimumHeight: 1
|
||||||
color: leftBar.colorScheme.border_weak
|
color: leftBar.colorScheme.border_weak
|
||||||
}
|
}
|
||||||
|
|
||||||
ListView {
|
ListView {
|
||||||
id: accounts
|
id: accounts
|
||||||
|
|
||||||
property var _topBottomMargins: 24
|
|
||||||
property var _leftRightMargins: 16
|
property var _leftRightMargins: 16
|
||||||
|
property var _topBottomMargins: 24
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.bottomMargin: accounts._topBottomMargins
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
|
Layout.fillWidth: true
|
||||||
Layout.leftMargin: accounts._leftRightMargins
|
Layout.leftMargin: accounts._leftRightMargins
|
||||||
Layout.rightMargin: accounts._leftRightMargins
|
Layout.rightMargin: accounts._leftRightMargins
|
||||||
Layout.topMargin: accounts._topBottomMargins
|
Layout.topMargin: accounts._topBottomMargins
|
||||||
Layout.bottomMargin: accounts._topBottomMargins
|
|
||||||
|
|
||||||
spacing: 12
|
|
||||||
clip: true
|
|
||||||
boundsBehavior: Flickable.StopAtBounds
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
|
clip: true
|
||||||
|
model: Backend.users
|
||||||
|
spacing: 12
|
||||||
|
|
||||||
header: Rectangle {
|
delegate: Item {
|
||||||
height: headerLabel.height+16
|
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
|
||||||
// color: ProtonStyle.transparent
|
implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
|
||||||
Label{
|
width: leftBar.width - 2 * accounts._leftRightMargins
|
||||||
|
|
||||||
|
AccountDelegate {
|
||||||
|
id: accountDelegate
|
||||||
|
anchors.bottomMargin: 8
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: 12
|
||||||
|
anchors.rightMargin: 12
|
||||||
|
anchors.topMargin: 8
|
||||||
colorScheme: leftBar.colorScheme
|
colorScheme: leftBar.colorScheme
|
||||||
|
user: Backend.users.get(index)
|
||||||
|
}
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
const user = Backend.users.get(index);
|
||||||
|
accounts.currentIndex = index;
|
||||||
|
if (!user)
|
||||||
|
return;
|
||||||
|
if (user.state !== EUserState.SignedOut) {
|
||||||
|
rightContent.showAccount();
|
||||||
|
} else {
|
||||||
|
signIn.username = user.primaryEmailOrUsername();
|
||||||
|
rightContent.showSignIn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
header: Rectangle {
|
||||||
|
height: headerLabel.height + 16
|
||||||
|
|
||||||
|
// color: ProtonStyle.transparent
|
||||||
|
Label {
|
||||||
id: headerLabel
|
id: headerLabel
|
||||||
|
colorScheme: leftBar.colorScheme
|
||||||
text: qsTr("Accounts")
|
text: qsTr("Accounts")
|
||||||
type: Label.LabelType.Body
|
type: Label.LabelType.Body
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
highlight: Rectangle {
|
highlight: Rectangle {
|
||||||
color: leftBar.colorScheme.interaction_default_active
|
color: leftBar.colorScheme.interaction_default_active
|
||||||
radius: ProtonStyle.account_row_radius
|
radius: ProtonStyle.account_row_radius
|
||||||
}
|
}
|
||||||
|
|
||||||
model: Backend.users
|
|
||||||
delegate: Item {
|
|
||||||
width: leftBar.width - 2*accounts._leftRightMargins
|
|
||||||
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
|
|
||||||
implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
|
|
||||||
|
|
||||||
AccountDelegate {
|
|
||||||
id: accountDelegate
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.topMargin: 8
|
|
||||||
anchors.bottomMargin: 8
|
|
||||||
anchors.leftMargin: 12
|
|
||||||
anchors.rightMargin: 12
|
|
||||||
|
|
||||||
colorScheme: leftBar.colorScheme
|
|
||||||
user: Backend.users.get(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
onClicked: {
|
|
||||||
var user = Backend.users.get(index)
|
|
||||||
accounts.currentIndex = index
|
|
||||||
if (!user) return
|
|
||||||
if (user.state !== EUserState.SignedOut) {
|
|
||||||
rightContent.showAccount()
|
|
||||||
} else {
|
|
||||||
signIn.username = user.primaryEmailOrUsername()
|
|
||||||
rightContent.showSignIn()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Separator
|
// Separator
|
||||||
Rectangle {
|
Rectangle {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.minimumHeight: 1
|
|
||||||
Layout.maximumHeight: 1
|
Layout.maximumHeight: 1
|
||||||
|
Layout.minimumHeight: 1
|
||||||
color: leftBar.colorScheme.border_weak
|
color: leftBar.colorScheme.border_weak
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: bottomLeftBar
|
id: bottomLeftBar
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.minimumHeight: 52
|
|
||||||
Layout.maximumHeight: 52
|
Layout.maximumHeight: 52
|
||||||
|
Layout.minimumHeight: 52
|
||||||
Layout.preferredHeight: 52
|
Layout.preferredHeight: 52
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
colorScheme: leftBar.colorScheme
|
|
||||||
width: 36
|
|
||||||
height: 36
|
|
||||||
|
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.top: parent.top
|
|
||||||
|
|
||||||
anchors.leftMargin: 16
|
anchors.leftMargin: 16
|
||||||
|
anchors.top: parent.top
|
||||||
anchors.topMargin: 7
|
anchors.topMargin: 7
|
||||||
|
colorScheme: leftBar.colorScheme
|
||||||
|
height: 36
|
||||||
horizontalPadding: 0
|
horizontalPadding: 0
|
||||||
|
|
||||||
icon.source: "/qml/icons/ic-plus.svg"
|
icon.source: "/qml/icons/ic-plus.svg"
|
||||||
|
width: 36
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
signIn.username = ""
|
signIn.username = "";
|
||||||
rightContent.showSignIn()
|
rightContent.showSignIn();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Rectangle {
|
||||||
Rectangle { // right content background
|
Layout.fillHeight: true // right content background
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
|
||||||
|
|
||||||
color: colorScheme.background_norm
|
color: colorScheme.background_norm
|
||||||
|
|
||||||
StackLayout {
|
StackLayout {
|
||||||
id: rightContent
|
id: rightContent
|
||||||
|
function showAccount(index) {
|
||||||
|
if (index !== undefined && index >= 0) {
|
||||||
|
accounts.currentIndex = index;
|
||||||
|
}
|
||||||
|
rightContent.currentIndex = 0;
|
||||||
|
}
|
||||||
|
function showBugReport() {
|
||||||
|
rightContent.currentIndex = 8;
|
||||||
|
}
|
||||||
|
function showConnectionModeSettings() {
|
||||||
|
rightContent.currentIndex = 5;
|
||||||
|
}
|
||||||
|
function showGeneralSettings() {
|
||||||
|
rightContent.currentIndex = 2;
|
||||||
|
}
|
||||||
|
function showHelpView() {
|
||||||
|
rightContent.currentIndex = 7;
|
||||||
|
}
|
||||||
|
function showKeychainSettings() {
|
||||||
|
rightContent.currentIndex = 3;
|
||||||
|
}
|
||||||
|
function showLocalCacheSettings() {
|
||||||
|
rightContent.currentIndex = 6;
|
||||||
|
}
|
||||||
|
function showPortSettings() {
|
||||||
|
rightContent.currentIndex = 4;
|
||||||
|
}
|
||||||
|
function showSignIn() {
|
||||||
|
rightContent.currentIndex = 1;
|
||||||
|
signIn.focus = true;
|
||||||
|
}
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
||||||
AccountView { // 0
|
AccountView {
|
||||||
|
// 0
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
notifications: root.notifications
|
notifications: root.notifications
|
||||||
user: {
|
user: {
|
||||||
if (accounts.currentIndex < 0) return undefined
|
if (accounts.currentIndex < 0)
|
||||||
if (Backend.users.count == 0) return undefined
|
return undefined;
|
||||||
return Backend.users.get(accounts.currentIndex)
|
if (Backend.users.count === 0)
|
||||||
|
return undefined;
|
||||||
|
return Backend.users.get(accounts.currentIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
onShowSetupGuide: function (user, address) {
|
||||||
|
root.showSetupGuide(user, address);
|
||||||
}
|
}
|
||||||
onShowSignIn: {
|
onShowSignIn: {
|
||||||
var user = this.user
|
const user = this.user;
|
||||||
signIn.username = user ? user.primaryEmailOrUsername() : ""
|
signIn.username = user ? user.primaryEmailOrUsername() : "";
|
||||||
rightContent.showSignIn()
|
rightContent.showSignIn();
|
||||||
}
|
|
||||||
onShowSetupGuide: function(user, address) {
|
|
||||||
root.showSetupGuide(user,address)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
GridLayout {
|
||||||
GridLayout { // 1 Sign In
|
// 1 Sign In
|
||||||
columns: 2
|
columns: 2
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
id: backButton
|
id: backButton
|
||||||
|
Layout.alignment: Qt.AlignTop
|
||||||
Layout.leftMargin: 18
|
Layout.leftMargin: 18
|
||||||
Layout.topMargin: 10
|
Layout.topMargin: 10
|
||||||
Layout.alignment: Qt.AlignTop
|
|
||||||
|
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
onClicked: {
|
horizontalPadding: 8
|
||||||
signIn.abort()
|
|
||||||
rightContent.showAccount()
|
|
||||||
}
|
|
||||||
icon.source: "/qml/icons/ic-arrow-left.svg"
|
icon.source: "/qml/icons/ic-arrow-left.svg"
|
||||||
secondary: true
|
secondary: true
|
||||||
horizontalPadding: 8
|
|
||||||
}
|
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
signIn.abort();
|
||||||
|
rightContent.showAccount();
|
||||||
|
}
|
||||||
|
}
|
||||||
SignIn {
|
SignIn {
|
||||||
id: signIn
|
id: signIn
|
||||||
Layout.topMargin: 68
|
|
||||||
Layout.leftMargin: 80 - backButton.width - 18
|
|
||||||
Layout.rightMargin: 80
|
|
||||||
Layout.bottomMargin: 68
|
Layout.bottomMargin: 68
|
||||||
Layout.preferredWidth: 320
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.leftMargin: 80 - backButton.width - 18
|
||||||
|
Layout.preferredWidth: 320
|
||||||
|
Layout.rightMargin: 80
|
||||||
|
Layout.topMargin: 68
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
GeneralSettings {
|
||||||
GeneralSettings { // 2
|
// 2
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
notifications: root.notifications
|
notifications: root.notifications
|
||||||
|
|
||||||
onBack: {
|
onBack: {
|
||||||
rightContent.showAccount()
|
rightContent.showAccount();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
KeychainSettings {
|
||||||
KeychainSettings { // 3
|
// 3
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
|
|
||||||
onBack: {
|
onBack: {
|
||||||
rightContent.showGeneralSettings()
|
rightContent.showGeneralSettings();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
PortSettings {
|
||||||
PortSettings { // 4
|
// 4
|
||||||
colorScheme: root.colorScheme
|
|
||||||
notifications: root.notifications
|
|
||||||
onBack: {
|
|
||||||
rightContent.showGeneralSettings()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ConnectionModeSettings { // 5
|
|
||||||
colorScheme: root.colorScheme
|
|
||||||
|
|
||||||
onBack: {
|
|
||||||
rightContent.showGeneralSettings()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LocalCacheSettings { // 6
|
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
notifications: root.notifications
|
notifications: root.notifications
|
||||||
|
|
||||||
onBack: {
|
onBack: {
|
||||||
rightContent.showGeneralSettings()
|
rightContent.showGeneralSettings();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ConnectionModeSettings {
|
||||||
HelpView { // 7
|
// 5
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
|
|
||||||
onBack: {
|
onBack: {
|
||||||
rightContent.showAccount()
|
rightContent.showGeneralSettings();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
LocalCacheSettings {
|
||||||
|
// 6
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
notifications: root.notifications
|
||||||
|
|
||||||
BugReportView { // 8
|
onBack: {
|
||||||
|
rightContent.showGeneralSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HelpView {
|
||||||
|
// 7
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
|
||||||
|
onBack: {
|
||||||
|
rightContent.showAccount();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BugReportFlow {
|
||||||
|
// 8
|
||||||
id: bugReport
|
id: bugReport
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
selectedAddress: {
|
selectedAddress: {
|
||||||
if (accounts.currentIndex < 0) return ""
|
if (accounts.currentIndex < 0)
|
||||||
if (Backend.users.count == 0) return ""
|
return "";
|
||||||
var user = Backend.users.get(accounts.currentIndex)
|
if (Backend.users.count === 0)
|
||||||
if (!user) return ""
|
return "";
|
||||||
return user.addresses[0]
|
const user = Backend.users.get(accounts.currentIndex);
|
||||||
|
if (!user)
|
||||||
|
return "";
|
||||||
|
return user.addresses[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
onBack: {
|
onBack: {
|
||||||
rightContent.showHelpView()
|
rightContent.showHelpView();
|
||||||
}
|
}
|
||||||
|
|
||||||
onBugReportWasSent: {
|
onBugReportWasSent: {
|
||||||
rightContent.showAccount()
|
rightContent.showAccount();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showAccount(index) {
|
|
||||||
if (index !== undefined && index >= 0){
|
|
||||||
accounts.currentIndex = index
|
|
||||||
}
|
|
||||||
rightContent.currentIndex = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
function showSignIn () { rightContent.currentIndex = 1; signIn.focus = true }
|
|
||||||
function showGeneralSettings () { rightContent.currentIndex = 2 }
|
|
||||||
function showKeychainSettings () { rightContent.currentIndex = 3 }
|
|
||||||
function showPortSettings () { rightContent.currentIndex = 4 }
|
|
||||||
function showConnectionModeSettings() { rightContent.currentIndex = 5 }
|
|
||||||
function showLocalCacheSettings () { rightContent.currentIndex = 6 }
|
|
||||||
function showHelpView () { rightContent.currentIndex = 7 }
|
|
||||||
function showBugReport () { rightContent.currentIndex = 8 }
|
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: Backend
|
function onLoginAlreadyLoggedIn(index) {
|
||||||
|
rightContent.showAccount(index);
|
||||||
|
}
|
||||||
|
function onLoginFinished(index) {
|
||||||
|
rightContent.showAccount(index);
|
||||||
|
}
|
||||||
|
|
||||||
function onLoginFinished(index) { rightContent.showAccount(index) }
|
target: Backend
|
||||||
function onLoginAlreadyLoggedIn(index) { rightContent.showAccount(index) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
function showLocalCacheSettings(){rightContent.showLocalCacheSettings() }
|
|
||||||
function showSettings(){rightContent.showGeneralSettings() }
|
|
||||||
function showHelp(){rightContent.showHelpView() }
|
|
||||||
function showSignIn(username){
|
|
||||||
signIn.username = username
|
|
||||||
rightContent.showSignIn()
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectUser(userID) {
|
|
||||||
var users = Backend.users;
|
|
||||||
for (var i = 0; i < users.count; i++) {
|
|
||||||
var user = users.get(i)
|
|
||||||
if (user.id !== userID) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
accounts.currentIndex = i;
|
|
||||||
if (user.state === EUserState.SignedOut)
|
|
||||||
showSignIn(user.primaryEmailOrUsername())
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.error("User with ID ", userID, " was not found in the account list")
|
|
||||||
}
|
|
||||||
|
|
||||||
function showBugReportAndPrefill(description) {
|
|
||||||
rightContent.showBugReport()
|
|
||||||
bugReport.setDescription(description)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,54 +1,45 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
//
|
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
|
||||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
// 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
|
// it under the terms of the GNU General Public License as published by
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
// (at your option) any later version.
|
// (at your option) any later version.
|
||||||
//
|
|
||||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
// GNU General Public License for more details.
|
// GNU General Public License for more details.
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU General Public License
|
// 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/>.
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
|
|
||||||
import "."
|
import "."
|
||||||
import "./Proton"
|
import "Proton"
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
property var target: parent
|
property var target: parent
|
||||||
|
|
||||||
x: target.x
|
|
||||||
y: target.y
|
|
||||||
width: target.width
|
|
||||||
height: target.height
|
|
||||||
|
|
||||||
color: "transparent"
|
|
||||||
border.color: "red"
|
border.color: "red"
|
||||||
border.width: 1
|
border.width: 1
|
||||||
|
color: "transparent"
|
||||||
|
height: target.height
|
||||||
|
width: target.width
|
||||||
|
x: target.x
|
||||||
|
y: target.y
|
||||||
//z: parent.z - 1
|
//z: parent.z - 1
|
||||||
z: 10000000
|
z: 10000000
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
text: parent.width + "x" + parent.height
|
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
color: "black"
|
color: "black"
|
||||||
colorScheme: ProtonStyle.currentStyle
|
colorScheme: ProtonStyle.currentStyle
|
||||||
|
text: parent.width + "x" + parent.height
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: target.implicitWidth
|
|
||||||
height: target.implicitHeight
|
|
||||||
|
|
||||||
color: "transparent"
|
|
||||||
border.color: "green"
|
border.color: "green"
|
||||||
border.width: 1
|
border.width: 1
|
||||||
|
color: "transparent"
|
||||||
|
height: target.implicitHeight
|
||||||
|
width: target.implicitWidth
|
||||||
//z: parent.z - 1
|
//z: parent.z - 1
|
||||||
z: 10000000
|
z: 10000000
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,25 +1,19 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
//
|
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
|
||||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
// 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
|
// it under the terms of the GNU General Public License as published by
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
// (at your option) any later version.
|
// (at your option) any later version.
|
||||||
//
|
|
||||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
// GNU General Public License for more details.
|
// GNU General Public License for more details.
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU General Public License
|
// 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/>.
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import QtQuick.Controls.impl
|
import QtQuick.Controls.impl
|
||||||
|
|
||||||
import Proton
|
import Proton
|
||||||
|
|
||||||
SettingsView {
|
SettingsView {
|
||||||
@ -31,144 +25,138 @@ SettingsView {
|
|||||||
fillHeight: false
|
fillHeight: false
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
text: qsTr("Settings")
|
text: qsTr("Settings")
|
||||||
type: Label.Heading
|
type: Label.Heading
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsItem {
|
SettingsItem {
|
||||||
id: autoUpdate
|
id: autoUpdate
|
||||||
colorScheme: root.colorScheme
|
|
||||||
text: qsTr("Automatic updates")
|
|
||||||
description: qsTr("Bridge will automatically update in the background.")
|
|
||||||
type: SettingsItem.Toggle
|
|
||||||
checked: Backend.isAutomaticUpdateOn
|
|
||||||
onClicked: Backend.toggleAutomaticUpdate(!autoUpdate.checked)
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
checked: Backend.isAutomaticUpdateOn
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
description: qsTr("Bridge will automatically update in the background.")
|
||||||
|
text: qsTr("Automatic updates")
|
||||||
|
type: SettingsItem.Toggle
|
||||||
|
|
||||||
|
onClicked: Backend.toggleAutomaticUpdate(!autoUpdate.checked)
|
||||||
|
}
|
||||||
SettingsItem {
|
SettingsItem {
|
||||||
id: autostart
|
id: autostart
|
||||||
colorScheme: root.colorScheme
|
|
||||||
text: qsTr("Open on startup")
|
|
||||||
description: qsTr("Bridge will open upon startup.")
|
|
||||||
type: SettingsItem.Toggle
|
|
||||||
checked: Backend.isAutostartOn
|
|
||||||
onClicked: {
|
|
||||||
autostart.loading = true
|
|
||||||
Backend.toggleAutostart(!autostart.checked)
|
|
||||||
}
|
|
||||||
Connections{
|
|
||||||
target: Backend
|
|
||||||
function onToggleAutostartFinished() {
|
|
||||||
autostart.loading = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
checked: Backend.isAutostartOn
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
description: qsTr("Bridge will open upon startup.")
|
||||||
|
text: qsTr("Open on startup")
|
||||||
|
type: SettingsItem.Toggle
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
autostart.loading = true;
|
||||||
|
Backend.toggleAutostart(!autostart.checked);
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
function onToggleAutostartFinished() {
|
||||||
|
autostart.loading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
target: Backend
|
||||||
|
}
|
||||||
|
}
|
||||||
SettingsItem {
|
SettingsItem {
|
||||||
id: beta
|
id: beta
|
||||||
colorScheme: root.colorScheme
|
Layout.fillWidth: true
|
||||||
text: qsTr("Beta access")
|
|
||||||
description: qsTr("Be among the first to try new features.")
|
|
||||||
type: SettingsItem.Toggle
|
|
||||||
checked: Backend.isBetaEnabled
|
checked: Backend.isBetaEnabled
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
description: qsTr("Be among the first to try new features.")
|
||||||
|
text: qsTr("Beta access")
|
||||||
|
type: SettingsItem.Toggle
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (!beta.checked) {
|
if (!beta.checked) {
|
||||||
root.notifications.askEnableBeta()
|
root.notifications.askEnableBeta();
|
||||||
} else {
|
} else {
|
||||||
Backend.toggleBeta(false)
|
Backend.toggleBeta(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
ColorImage {
|
ColorImage {
|
||||||
Layout.alignment: Qt.AlignTop
|
Layout.alignment: Qt.AlignCenter
|
||||||
|
|
||||||
source: root._isAdvancedShown ? "/qml/icons/ic-chevron-up.svg" : "/qml/icons/ic-chevron-down.svg"
|
|
||||||
color: root.colorScheme.interaction_norm
|
color: root.colorScheme.interaction_norm
|
||||||
height: root.colorScheme.body_font_size
|
height: root.colorScheme.body_font_size
|
||||||
|
source: root._isAdvancedShown ? "/qml/icons/ic-chevron-down.svg" : "/qml/icons/ic-chevron-right.svg"
|
||||||
sourceSize.height: root.colorScheme.body_font_size
|
sourceSize.height: root.colorScheme.body_font_size
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
||||||
onClicked: root._isAdvancedShown = !root._isAdvancedShown
|
onClicked: root._isAdvancedShown = !root._isAdvancedShown
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
id: advSettLabel
|
id: advSettLabel
|
||||||
|
color: root.colorScheme.interaction_norm
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
text: qsTr("Advanced settings")
|
text: qsTr("Advanced settings")
|
||||||
color: root.colorScheme.interaction_norm
|
|
||||||
type: Label.Body
|
type: Label.Body
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
||||||
onClicked: root._isAdvancedShown = !root._isAdvancedShown
|
onClicked: root._isAdvancedShown = !root._isAdvancedShown
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsItem {
|
SettingsItem {
|
||||||
id: keychains
|
id: keychains
|
||||||
visible: root._isAdvancedShown && Backend.availableKeychain.length > 1
|
|
||||||
colorScheme: root.colorScheme
|
|
||||||
text: qsTr("Change keychain")
|
|
||||||
description: qsTr("Change which keychain Bridge uses as default")
|
|
||||||
actionText: qsTr("Change")
|
|
||||||
type: SettingsItem.Button
|
|
||||||
checked: Backend.isDoHEnabled
|
|
||||||
onClicked: root.parent.showKeychainSettings()
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
actionText: qsTr("Change")
|
||||||
|
checked: Backend.isDoHEnabled
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
description: qsTr("Change which keychain Bridge uses as default")
|
||||||
|
text: qsTr("Change keychain")
|
||||||
|
type: SettingsItem.Button
|
||||||
|
visible: root._isAdvancedShown && Backend.availableKeychain.length > 1
|
||||||
|
|
||||||
|
onClicked: root.parent.showKeychainSettings()
|
||||||
|
}
|
||||||
SettingsItem {
|
SettingsItem {
|
||||||
id: doh
|
id: doh
|
||||||
visible: root._isAdvancedShown
|
|
||||||
colorScheme: root.colorScheme
|
|
||||||
text: qsTr("Alternative routing")
|
|
||||||
description: qsTr("If Proton’s servers are blocked in your location, alternative network routing will be used to reach Proton.")
|
|
||||||
type: SettingsItem.Toggle
|
|
||||||
checked: Backend.isDoHEnabled
|
|
||||||
onClicked: Backend.toggleDoH(!doh.checked)
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
checked: Backend.isDoHEnabled
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
description: qsTr("If Proton’s servers are blocked in your location, alternative network routing will be used to reach Proton.")
|
||||||
|
text: qsTr("Alternative routing")
|
||||||
|
type: SettingsItem.Toggle
|
||||||
|
visible: root._isAdvancedShown
|
||||||
|
|
||||||
|
onClicked: Backend.toggleDoH(!doh.checked)
|
||||||
|
}
|
||||||
SettingsItem {
|
SettingsItem {
|
||||||
id: darkMode
|
id: darkMode
|
||||||
visible: root._isAdvancedShown
|
|
||||||
colorScheme: root.colorScheme
|
|
||||||
text: qsTr("Dark mode")
|
|
||||||
description: qsTr("Choose dark color theme.")
|
|
||||||
type: SettingsItem.Toggle
|
|
||||||
checked: Backend.colorSchemeName == "dark"
|
|
||||||
onClicked: Backend.changeColorScheme( darkMode.checked ? "light" : "dark")
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
checked: Backend.colorSchemeName === "dark"
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
description: qsTr("Choose dark color theme.")
|
||||||
|
text: qsTr("Dark mode")
|
||||||
|
type: SettingsItem.Toggle
|
||||||
|
visible: root._isAdvancedShown
|
||||||
|
|
||||||
|
onClicked: Backend.changeColorScheme(darkMode.checked ? "light" : "dark")
|
||||||
|
}
|
||||||
SettingsItem {
|
SettingsItem {
|
||||||
id: allMail
|
id: allMail
|
||||||
visible: root._isAdvancedShown
|
|
||||||
colorScheme: root.colorScheme
|
|
||||||
text: qsTr("Show All Mail")
|
|
||||||
description: qsTr("Choose to list the All Mail folder in your local client.")
|
|
||||||
type: SettingsItem.Toggle
|
|
||||||
checked: Backend.isAllMailVisible
|
|
||||||
onClicked: root.notifications.askChangeAllMailVisibility(Backend.isAllMailVisible)
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
checked: Backend.isAllMailVisible
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
description: qsTr("Choose to list the All Mail folder in your local client.")
|
||||||
|
text: qsTr("Show All Mail")
|
||||||
|
type: SettingsItem.Toggle
|
||||||
|
visible: root._isAdvancedShown
|
||||||
|
|
||||||
|
onClicked: root.notifications.askChangeAllMailVisibility(Backend.isAllMailVisible)
|
||||||
|
}
|
||||||
SettingsItem {
|
SettingsItem {
|
||||||
id: telemetry
|
id: telemetry
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
@ -181,73 +169,68 @@ SettingsView {
|
|||||||
|
|
||||||
onClicked: Backend.toggleIsTelemetryDisabled(telemetry.checked)
|
onClicked: Backend.toggleIsTelemetryDisabled(telemetry.checked)
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsItem {
|
SettingsItem {
|
||||||
id: ports
|
id: ports
|
||||||
visible: root._isAdvancedShown
|
|
||||||
colorScheme: root.colorScheme
|
|
||||||
text: qsTr("Default ports")
|
|
||||||
actionText: qsTr("Change")
|
|
||||||
description: qsTr("Choose which ports are used by default.")
|
|
||||||
type: SettingsItem.Button
|
|
||||||
onClicked: root.parent.showPortSettings()
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
actionText: qsTr("Change")
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
description: qsTr("Choose which ports are used by default.")
|
||||||
|
text: qsTr("Default ports")
|
||||||
|
type: SettingsItem.Button
|
||||||
|
visible: root._isAdvancedShown
|
||||||
|
|
||||||
|
onClicked: root.parent.showPortSettings()
|
||||||
|
}
|
||||||
SettingsItem {
|
SettingsItem {
|
||||||
id: imap
|
id: imap
|
||||||
visible: root._isAdvancedShown
|
|
||||||
colorScheme: root.colorScheme
|
|
||||||
text: qsTr("Connection mode")
|
|
||||||
actionText: qsTr("Change")
|
|
||||||
description: qsTr("Change the protocol Bridge and the email client use to connect for IMAP and SMTP.")
|
|
||||||
type: SettingsItem.Button
|
|
||||||
onClicked: root.parent.showConnectionModeSettings()
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
actionText: qsTr("Change")
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
description: qsTr("Change the protocol Bridge and the email client use to connect for IMAP and SMTP.")
|
||||||
|
text: qsTr("Connection mode")
|
||||||
|
type: SettingsItem.Button
|
||||||
|
visible: root._isAdvancedShown
|
||||||
|
|
||||||
|
onClicked: root.parent.showConnectionModeSettings()
|
||||||
|
}
|
||||||
SettingsItem {
|
SettingsItem {
|
||||||
id: cache
|
id: cache
|
||||||
visible: root._isAdvancedShown
|
|
||||||
colorScheme: root.colorScheme
|
|
||||||
text: qsTr("Local cache")
|
|
||||||
actionText: qsTr("Configure")
|
|
||||||
description: qsTr("Configure Bridge's local cache.")
|
|
||||||
type: SettingsItem.Button
|
|
||||||
onClicked: root.parent.showLocalCacheSettings()
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
actionText: qsTr("Configure")
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
description: qsTr("Configure Bridge's local cache.")
|
||||||
|
text: qsTr("Local cache")
|
||||||
|
type: SettingsItem.Button
|
||||||
|
visible: root._isAdvancedShown
|
||||||
|
|
||||||
|
onClicked: root.parent.showLocalCacheSettings()
|
||||||
|
}
|
||||||
SettingsItem {
|
SettingsItem {
|
||||||
id: exportTLSCertificates
|
id: exportTLSCertificates
|
||||||
visible: root._isAdvancedShown
|
|
||||||
colorScheme: root.colorScheme
|
|
||||||
text: qsTr("Export TLS certificates")
|
|
||||||
actionText: qsTr("Export")
|
|
||||||
description: qsTr("Export the TLS private key and certificate used by the IMAP and SMTP servers.")
|
|
||||||
type: SettingsItem.Button
|
|
||||||
onClicked: {
|
|
||||||
Backend.exportTLSCertificates()
|
|
||||||
}
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
actionText: qsTr("Export")
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
description: qsTr("Export the TLS private key and certificate used by the IMAP and SMTP servers.")
|
||||||
|
text: qsTr("Export TLS certificates")
|
||||||
|
type: SettingsItem.Button
|
||||||
|
visible: root._isAdvancedShown
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
Backend.exportTLSCertificates();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsItem {
|
SettingsItem {
|
||||||
id: reset
|
id: reset
|
||||||
visible: root._isAdvancedShown
|
|
||||||
colorScheme: root.colorScheme
|
|
||||||
text: qsTr("Reset Bridge")
|
|
||||||
actionText: qsTr("Reset")
|
|
||||||
description: qsTr("Remove all accounts, clear cached data, and restore the original settings.")
|
|
||||||
type: SettingsItem.Button
|
|
||||||
onClicked: {
|
|
||||||
root.notifications.askResetBridge()
|
|
||||||
}
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
actionText: qsTr("Reset")
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
description: qsTr("Remove all accounts, clear cached data, and restore the original settings.")
|
||||||
|
text: qsTr("Reset Bridge")
|
||||||
|
type: SettingsItem.Button
|
||||||
|
visible: root._isAdvancedShown
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
root.notifications.askResetBridge();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,123 +1,110 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
//
|
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
|
||||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
// 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
|
// it under the terms of the GNU General Public License as published by
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
// (at your option) any later version.
|
// (at your option) any later version.
|
||||||
//
|
|
||||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
// GNU General Public License for more details.
|
// GNU General Public License for more details.
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU General Public License
|
// 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/>.
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
|
|
||||||
import Proton
|
import Proton
|
||||||
|
|
||||||
SettingsView {
|
SettingsView {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
fillHeight: true
|
fillHeight: true
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
text: qsTr("Help")
|
text: qsTr("Help")
|
||||||
type: Label.Heading
|
type: Label.Heading
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsItem {
|
SettingsItem {
|
||||||
id: setupPage
|
id: setupPage
|
||||||
colorScheme: root.colorScheme
|
|
||||||
text: qsTr("Installation and setup")
|
|
||||||
actionText: qsTr("Go to help topics")
|
|
||||||
actionIcon: "/qml/icons/ic-external-link.svg"
|
|
||||||
description: qsTr("Get help setting up your client with our instructions and FAQs.")
|
|
||||||
type: SettingsItem.PrimaryButton
|
|
||||||
onClicked: {Qt.openUrlExternally("https://proton.me/support/bridge")}
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
actionIcon: "/qml/icons/ic-external-link.svg"
|
||||||
|
actionText: qsTr("Go to help topics")
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
description: qsTr("Get help setting up your client with our instructions and FAQs.")
|
||||||
|
text: qsTr("Installation and setup")
|
||||||
|
type: SettingsItem.PrimaryButton
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
Backend.notifyKBArticleClicked("https://proton.me/support/bridge");
|
||||||
|
Qt.openUrlExternally("https://proton.me/support/bridge");
|
||||||
|
}
|
||||||
|
}
|
||||||
SettingsItem {
|
SettingsItem {
|
||||||
id: checkUpdates
|
id: checkUpdates
|
||||||
colorScheme: root.colorScheme
|
Layout.fillWidth: true
|
||||||
text: qsTr("Updates")
|
|
||||||
actionText: qsTr("Check now")
|
actionText: qsTr("Check now")
|
||||||
description: qsTr("Check that you're using the latest version of Bridge. To stay up to date, enable auto-updates in settings.")
|
colorScheme: root.colorScheme
|
||||||
|
description: qsTr("Check that you're using the latest version of Bridge.\nTo stay up to date, enable auto-updates in settings.")
|
||||||
|
text: qsTr("Updates")
|
||||||
type: SettingsItem.Button
|
type: SettingsItem.Button
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
checkUpdates.loading = true
|
checkUpdates.loading = true;
|
||||||
Backend.checkUpdates()
|
Backend.checkUpdates();
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
|
function onCheckUpdatesFinished() {
|
||||||
|
checkUpdates.loading = false;
|
||||||
|
}
|
||||||
|
|
||||||
target: Backend
|
target: Backend
|
||||||
function onCheckUpdatesFinished() { checkUpdates.loading = false }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsItem {
|
SettingsItem {
|
||||||
id: logs
|
id: logs
|
||||||
colorScheme: root.colorScheme
|
|
||||||
text: qsTr("Logs")
|
|
||||||
actionText: qsTr("View logs")
|
|
||||||
description: qsTr("Open and review logs to troubleshoot.")
|
|
||||||
type: SettingsItem.Button
|
|
||||||
onClicked: Qt.openUrlExternally(Backend.logsPath)
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
actionText: qsTr("View logs")
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
description: qsTr("Open and review logs to troubleshoot.")
|
||||||
|
text: qsTr("Logs")
|
||||||
|
type: SettingsItem.Button
|
||||||
|
|
||||||
|
onClicked: Qt.openUrlExternally(Backend.logsPath)
|
||||||
|
}
|
||||||
SettingsItem {
|
SettingsItem {
|
||||||
id: reportBug
|
id: reportBug
|
||||||
colorScheme: root.colorScheme
|
|
||||||
text: qsTr("Report a problem")
|
|
||||||
actionText: qsTr("Report a problem")
|
|
||||||
description: qsTr("Something not working as expected? Let us know.")
|
|
||||||
type: SettingsItem.Button
|
|
||||||
onClicked: {
|
|
||||||
Backend.updateCurrentMailClient()
|
|
||||||
root.parent.showBugReport()
|
|
||||||
}
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
actionText: qsTr("Report problem")
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
description: qsTr("Something not working as expected? Let us know.")
|
||||||
|
text: qsTr("Report a problem")
|
||||||
|
type: SettingsItem.Button
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
Backend.updateCurrentMailClient();
|
||||||
|
Backend.notifyReportBugClicked();
|
||||||
|
root.parent.showBugReport();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// fill height so the footer label will be always attached to the bottom
|
// fill height so the footer label will always be attached to the bottom
|
||||||
Item {
|
Item {
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
colorScheme: root.colorScheme
|
|
||||||
type: Label.Caption
|
|
||||||
color: root.colorScheme.text_weak
|
color: root.colorScheme.text_weak
|
||||||
textFormat: Text.StyledText
|
colorScheme: root.colorScheme
|
||||||
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
text: qsTr("%1 v%2 (%3)<br>© 2017-%4 %5<br>%6 %7<br>%8").arg(Backend.appname).arg(Backend.version).arg(Backend.tag).arg(Backend.buildYear()).arg(Backend.vendor).arg(link(Backend.licensePath, qsTr("License"))).arg(link(Backend.dependencyLicensesLink, qsTr("Dependencies"))).arg(link(Backend.releaseNotesLink, qsTr("Release notes")))
|
||||||
|
textFormat: Text.StyledText
|
||||||
|
type: Label.Caption
|
||||||
|
|
||||||
text: qsTr("%1 v%2 (%3)<br>© 2017-%4 %5<br>%6 %7<br>%8").
|
onLinkActivated: function (link) {
|
||||||
arg(Backend.appname).
|
Qt.openUrlExternally(link);
|
||||||
arg(Backend.version).
|
}
|
||||||
arg(Backend.tag).
|
|
||||||
arg(Backend.buildYear()).
|
|
||||||
arg(Backend.vendor).
|
|
||||||
arg(link(Backend.licensePath, qsTr("License"))).
|
|
||||||
arg(link(Backend.dependencyLicensesLink, qsTr("Dependencies"))).
|
|
||||||
arg(link(Backend.releaseNotesLink, qsTr("Release notes")))
|
|
||||||
|
|
||||||
onLinkActivated: function(link) { Qt.openUrlExternally(link) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,116 +1,105 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
//
|
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
|
||||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
// 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
|
// it under the terms of the GNU General Public License as published by
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
// (at your option) any later version.
|
// (at your option) any later version.
|
||||||
//
|
|
||||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
// GNU General Public License for more details.
|
// GNU General Public License for more details.
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU General Public License
|
// 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/>.
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import QtQuick.Controls.impl
|
import QtQuick.Controls.impl
|
||||||
|
|
||||||
import Proton
|
import Proton
|
||||||
|
|
||||||
SettingsView {
|
SettingsView {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
fillHeight: false
|
property bool _valuesChanged: keychainSelection.checkedButton && keychainSelection.checkedButton.text !== Backend.currentKeychain
|
||||||
|
|
||||||
property bool _valuesChanged: keychainSelection.checkedButton && keychainSelection.checkedButton.text != Backend.currentKeychain
|
function setDefaultValues() {
|
||||||
|
for (const bi in keychainSelection.buttons) {
|
||||||
Label {
|
const button = keychainSelection.buttons[bi];
|
||||||
colorScheme: root.colorScheme
|
if (button.text === Backend.currentKeychain) {
|
||||||
text: qsTr("Default keychain")
|
button.checked = true;
|
||||||
type: Label.Heading
|
break;
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Label {
|
|
||||||
colorScheme: root.colorScheme
|
|
||||||
text: qsTr("Change which keychain Bridge uses as default")
|
|
||||||
type: Label.Body
|
|
||||||
color: root.colorScheme.text_weak
|
|
||||||
Layout.fillWidth: true
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
spacing: 16
|
|
||||||
|
|
||||||
ButtonGroup{ id: keychainSelection }
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: Backend.availableKeychain
|
|
||||||
|
|
||||||
RadioButton {
|
|
||||||
colorScheme: root.colorScheme
|
|
||||||
ButtonGroup.group: keychainSelection
|
|
||||||
text: modelData
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fillHeight: false
|
||||||
|
|
||||||
Rectangle {
|
Component.onCompleted: root.setDefaultValues()
|
||||||
Layout.fillWidth: true
|
onBack: {
|
||||||
height: 1
|
root.setDefaultValues();
|
||||||
color: root.colorScheme.border_weak
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
text: qsTr("Default keychain")
|
||||||
|
type: Label.Heading
|
||||||
|
}
|
||||||
|
Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
color: root.colorScheme.text_weak
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
text: qsTr("Change which keychain Bridge uses as default")
|
||||||
|
type: Label.Body
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: 16
|
||||||
|
|
||||||
|
ButtonGroup {
|
||||||
|
id: keychainSelection
|
||||||
|
}
|
||||||
|
Repeater {
|
||||||
|
model: Backend.availableKeychain
|
||||||
|
|
||||||
|
RadioButton {
|
||||||
|
ButtonGroup.group: keychainSelection
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
text: modelData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
color: root.colorScheme.border_weak
|
||||||
|
height: 1
|
||||||
|
}
|
||||||
RowLayout {
|
RowLayout {
|
||||||
spacing: 12
|
spacing: 12
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
id: submitButton
|
id: submitButton
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
text: qsTr("Save and restart")
|
|
||||||
enabled: root._valuesChanged
|
enabled: root._valuesChanged
|
||||||
|
text: qsTr("Save and restart")
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
Backend.changeKeychain(keychainSelection.checkedButton.text)
|
Backend.changeKeychain(keychainSelection.checkedButton.text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
text: qsTr("Cancel")
|
|
||||||
onClicked: root.back()
|
|
||||||
secondary: true
|
secondary: true
|
||||||
}
|
text: qsTr("Cancel")
|
||||||
|
|
||||||
|
onClicked: root.back()
|
||||||
|
}
|
||||||
Connections {
|
Connections {
|
||||||
target: Backend
|
|
||||||
|
|
||||||
function onChangeKeychainFinished() {
|
function onChangeKeychainFinished() {
|
||||||
submitButton.loading = false
|
submitButton.loading = false;
|
||||||
root.back()
|
root.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
target: Backend
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onBack: {
|
|
||||||
root.setDefaultValues()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setDefaultValues(){
|
|
||||||
for (var bi in keychainSelection.buttons){
|
|
||||||
var button = keychainSelection.buttons[bi]
|
|
||||||
if (button.text == Backend.currentKeychain) {
|
|
||||||
button.checked = true
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: root.setDefaultValues()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,81 +1,88 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
//
|
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
|
||||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
// 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
|
// it under the terms of the GNU General Public License as published by
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
// (at your option) any later version.
|
// (at your option) any later version.
|
||||||
//
|
|
||||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
// GNU General Public License for more details.
|
// GNU General Public License for more details.
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU General Public License
|
// 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/>.
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import QtQuick.Controls.impl
|
import QtQuick.Controls.impl
|
||||||
import QtQuick.Dialogs
|
import QtQuick.Dialogs
|
||||||
|
|
||||||
import Proton
|
import Proton
|
||||||
|
|
||||||
SettingsView {
|
SettingsView {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
fillHeight: false
|
|
||||||
|
|
||||||
property var notifications
|
|
||||||
property url diskCachePath: pathDialog.shortcuts.home
|
property url diskCachePath: pathDialog.shortcuts.home
|
||||||
|
property var notifications
|
||||||
|
|
||||||
function refresh() {
|
function refresh() {
|
||||||
diskCacheSetting.description = Backend.nativePath(root.diskCachePath)
|
diskCacheSetting.description = Backend.nativePath(root.diskCachePath);
|
||||||
submitButton.enabled = (!submitButton.loading) && !Backend.areSameFileOrFolder(Backend.diskCachePath, root.diskCachePath)
|
submitButton.enabled = (!submitButton.loading) && !Backend.areSameFileOrFolder(Backend.diskCachePath, root.diskCachePath);
|
||||||
|
}
|
||||||
|
function setDefaultValues() {
|
||||||
|
root.diskCachePath = Backend.diskCachePath;
|
||||||
|
root.refresh();
|
||||||
|
}
|
||||||
|
function submit() {
|
||||||
|
submitButton.loading = true;
|
||||||
|
Backend.setDiskCachePath(root.diskCachePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
fillHeight: false
|
||||||
|
|
||||||
|
onBack: {
|
||||||
|
root.setDefaultValues();
|
||||||
|
}
|
||||||
|
onVisibleChanged: {
|
||||||
|
root.setDefaultValues();
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
text: qsTr("Local cache")
|
text: qsTr("Local cache")
|
||||||
type: Label.Heading
|
type: Label.Heading
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
color: root.colorScheme.text_weak
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
text: qsTr("Bridge stores your encrypted messages locally to optimize communication with your client.")
|
text: qsTr("Bridge stores your encrypted messages locally to optimize communication with your client.")
|
||||||
type: Label.Body
|
type: Label.Body
|
||||||
color: root.colorScheme.text_weak
|
|
||||||
Layout.fillWidth: true
|
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsItem {
|
SettingsItem {
|
||||||
id: diskCacheSetting
|
id: diskCacheSetting
|
||||||
colorScheme: root.colorScheme
|
|
||||||
text: qsTr("Current cache location")
|
|
||||||
actionText: qsTr("Change location")
|
|
||||||
descriptionWrap: Text.WrapAnywhere
|
|
||||||
type: SettingsItem.Button
|
|
||||||
onClicked: {
|
|
||||||
pathDialog.open()
|
|
||||||
}
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
actionText: qsTr("Change location")
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
descriptionWrap: Text.WrapAnywhere
|
||||||
|
text: qsTr("Current cache location")
|
||||||
|
type: SettingsItem.Button
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
pathDialog.open();
|
||||||
|
}
|
||||||
|
|
||||||
FolderDialog {
|
FolderDialog {
|
||||||
id: pathDialog
|
id: pathDialog
|
||||||
title: qsTr("Select cache location")
|
|
||||||
currentFolder: root.diskCachePath
|
currentFolder: root.diskCachePath
|
||||||
onAccepted: {
|
title: qsTr("Select cache location")
|
||||||
root.diskCachePath = pathDialog.selectedFolder
|
|
||||||
root.refresh()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
onAccepted: {
|
||||||
|
root.diskCachePath = pathDialog.selectedFolder;
|
||||||
|
root.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
RowLayout {
|
RowLayout {
|
||||||
spacing: 12
|
spacing: 12
|
||||||
|
|
||||||
@ -83,43 +90,25 @@ SettingsView {
|
|||||||
id: submitButton
|
id: submitButton
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
text: qsTr("Save")
|
text: qsTr("Save")
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
root.submit()
|
root.submit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
text: qsTr("Cancel")
|
|
||||||
onClicked: root.back()
|
|
||||||
secondary: true
|
secondary: true
|
||||||
}
|
text: qsTr("Cancel")
|
||||||
|
|
||||||
|
onClicked: root.back()
|
||||||
|
}
|
||||||
Connections {
|
Connections {
|
||||||
target: Backend
|
|
||||||
|
|
||||||
function onDiskCachePathChangeFinished() {
|
function onDiskCachePathChangeFinished() {
|
||||||
submitButton.loading = false
|
submitButton.loading = false;
|
||||||
root.setDefaultValues()
|
root.setDefaultValues();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
target: Backend
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onBack: {
|
|
||||||
root.setDefaultValues()
|
|
||||||
}
|
|
||||||
|
|
||||||
function submit() {
|
|
||||||
submitButton.loading = true
|
|
||||||
Backend.setDiskCachePath(root.diskCachePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
function setDefaultValues(){
|
|
||||||
root.diskCachePath = Backend.diskCachePath
|
|
||||||
root.refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
onVisibleChanged: {
|
|
||||||
root.setDefaultValues()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,232 +1,199 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
//
|
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
|
||||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
// 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
|
// it under the terms of the GNU General Public License as published by
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
// (at your option) any later version.
|
// (at your option) any later version.
|
||||||
//
|
|
||||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
// GNU General Public License for more details.
|
// GNU General Public License for more details.
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU General Public License
|
// 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/>.
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import QtQml
|
import QtQml
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Window
|
import QtQuick.Window
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
|
|
||||||
import Proton
|
import Proton
|
||||||
import Notifications
|
import Notifications
|
||||||
|
|
||||||
ApplicationWindow {
|
ApplicationWindow {
|
||||||
id: root
|
id: root
|
||||||
colorScheme: ProtonStyle.currentStyle
|
|
||||||
visible: true
|
|
||||||
|
|
||||||
|
|
||||||
property int _defaultWidth: 1080
|
|
||||||
property int _defaultHeight: 780
|
property int _defaultHeight: 780
|
||||||
width: _defaultWidth
|
property int _defaultWidth: 1080
|
||||||
|
property var notifications
|
||||||
|
|
||||||
|
function selectUser(userID) {
|
||||||
|
contentWrapper.selectUser(userID);
|
||||||
|
}
|
||||||
|
function showAndRise() {
|
||||||
|
root.show();
|
||||||
|
root.raise();
|
||||||
|
if (!root.active) {
|
||||||
|
root.requestActivate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function showHelp() {
|
||||||
|
contentWrapper.showHelp();
|
||||||
|
}
|
||||||
|
function showLocalCacheSettings() {
|
||||||
|
contentWrapper.showLocalCacheSettings();
|
||||||
|
}
|
||||||
|
function showSettings() {
|
||||||
|
contentWrapper.showSettings();
|
||||||
|
}
|
||||||
|
function showSetup(user, address) {
|
||||||
|
setupGuide.user = user;
|
||||||
|
setupGuide.address = address;
|
||||||
|
setupGuide.reset();
|
||||||
|
contentLayout._showSetup = !!setupGuide.user;
|
||||||
|
}
|
||||||
|
function showSignIn(username) {
|
||||||
|
if (contentLayout.currentIndex === 1)
|
||||||
|
return;
|
||||||
|
contentWrapper.showSignIn(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
colorScheme: ProtonStyle.currentStyle
|
||||||
height: _defaultHeight
|
height: _defaultHeight
|
||||||
minimumWidth: _defaultWidth
|
minimumWidth: _defaultWidth
|
||||||
|
visible: true
|
||||||
property var notifications
|
width: _defaultWidth
|
||||||
|
|
||||||
// show Setup Guide on every new user
|
// show Setup Guide on every new user
|
||||||
Connections {
|
Connections {
|
||||||
target: Backend.users
|
|
||||||
|
|
||||||
function onRowsInserted(parent, first, last) {
|
|
||||||
// considering that users are added one-by-one
|
|
||||||
var user = Backend.users.get(first)
|
|
||||||
|
|
||||||
if (user.state === EUserState.SignedOut) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.setupGuideSeen) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
root.showSetup(user,user.addresses[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
function onRowsAboutToBeRemoved(parent, first, last) {
|
function onRowsAboutToBeRemoved(parent, first, last) {
|
||||||
for (var i = first; i <= last; i++ ) {
|
for (let i = first; i <= last; i++) {
|
||||||
var user = Backend.users.get(i)
|
const user = Backend.users.get(i);
|
||||||
|
|
||||||
if (setupGuide.user === user) {
|
if (setupGuide.user === user) {
|
||||||
setupGuide.user = null
|
setupGuide.user = null;
|
||||||
contentLayout._showSetup = false
|
contentLayout._showSetup = false;
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
function onRowsInserted(parent, first, _) {
|
||||||
|
// considering that users are added one-by-one
|
||||||
|
const user = Backend.users.get(first);
|
||||||
|
if (user.state === EUserState.SignedOut) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (user.setupGuideSeen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
root.showSetup(user, user.addresses[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
target: Backend.users
|
||||||
|
}
|
||||||
Connections {
|
Connections {
|
||||||
target: Backend
|
|
||||||
|
|
||||||
function onShowMainWindow() {
|
|
||||||
root.showAndRise()
|
|
||||||
}
|
|
||||||
|
|
||||||
function onLoginFinished(index, wasSignedOut) {
|
function onLoginFinished(index, wasSignedOut) {
|
||||||
var user = Backend.users.get(index)
|
const user = Backend.users.get(index);
|
||||||
if (user && !wasSignedOut) {
|
if (user && !wasSignedOut) {
|
||||||
root.showSetup(user, user.addresses[0])
|
root.showSetup(user, user.addresses[0]);
|
||||||
}
|
}
|
||||||
console.debug("Login finished", index)
|
console.debug("Login finished", index);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onShowHelp() {
|
|
||||||
root.showHelp()
|
|
||||||
root.showAndRise()
|
|
||||||
}
|
|
||||||
|
|
||||||
function onShowSettings() {
|
|
||||||
root.showSettings()
|
|
||||||
root.showAndRise()
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSelectUser(userID, forceShowWindow) {
|
function onSelectUser(userID, forceShowWindow) {
|
||||||
contentWrapper.selectUser(userID)
|
contentWrapper.selectUser(userID);
|
||||||
if (forceShowWindow) {
|
if (forceShowWindow) {
|
||||||
root.showAndRise()
|
root.showAndRise();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
function onShowHelp() {
|
||||||
|
root.showHelp();
|
||||||
|
root.showAndRise();
|
||||||
|
}
|
||||||
|
function onShowMainWindow() {
|
||||||
|
root.showAndRise();
|
||||||
|
}
|
||||||
|
function onShowSettings() {
|
||||||
|
root.showSettings();
|
||||||
|
root.showAndRise();
|
||||||
|
}
|
||||||
|
|
||||||
|
target: Backend
|
||||||
|
}
|
||||||
StackLayout {
|
StackLayout {
|
||||||
id: contentLayout
|
id: contentLayout
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
|
|
||||||
property bool _showSetup: false
|
property bool _showSetup: false
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
currentIndex: {
|
currentIndex: {
|
||||||
// show welcome when there are no users
|
// show welcome when there are no users
|
||||||
if (Backend.users.count === 0) {
|
if (Backend.users.count === 0) {
|
||||||
return 1
|
return 1;
|
||||||
}
|
}
|
||||||
|
const u = Backend.users.get(0);
|
||||||
var u = Backend.users.get(0)
|
|
||||||
|
|
||||||
if (!u) {
|
if (!u) {
|
||||||
console.trace()
|
console.trace();
|
||||||
console.log("empty user")
|
console.log("empty user");
|
||||||
return 1
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((Backend.users.count === 1) && (u.state === EUserState.SignedOut)) {
|
if ((Backend.users.count === 1) && (u.state === EUserState.SignedOut)) {
|
||||||
showSignIn(u.primaryEmailOrUsername())
|
showSignIn(u.primaryEmailOrUsername());
|
||||||
return 0
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contentLayout._showSetup) {
|
if (contentLayout._showSetup) {
|
||||||
return 2
|
return 2;
|
||||||
}
|
}
|
||||||
|
return 0;
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ContentWrapper { // 0
|
ContentWrapper {
|
||||||
|
// 0
|
||||||
id: contentWrapper
|
id: contentWrapper
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.fillWidth: true
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
notifications: root.notifications
|
notifications: root.notifications
|
||||||
|
|
||||||
Layout.fillHeight: true
|
|
||||||
Layout.fillWidth: true
|
|
||||||
|
|
||||||
onShowSetupGuide: function(user, address) {
|
|
||||||
root.showSetup(user,address)
|
|
||||||
}
|
|
||||||
|
|
||||||
onCloseWindow: {
|
onCloseWindow: {
|
||||||
root.close()
|
root.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
onQuitBridge: {
|
onQuitBridge: {
|
||||||
// If we ever want to add a confirmation dialog before quitting:
|
// If we ever want to add a confirmation dialog before quitting:
|
||||||
//root.notifications.askQuestion("Quit Bridge", "Insert warning message here.", "Quit", "Cancel", Backend.quit, null)
|
//root.notifications.askQuestion("Quit Bridge", "Insert warning message here.", "Quit", "Cancel", Backend.quit, null)
|
||||||
root.close()
|
root.close();
|
||||||
Backend.quit()
|
Backend.quit();
|
||||||
|
}
|
||||||
|
onShowSetupGuide: function (user, address) {
|
||||||
|
root.showSetup(user, address);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
WelcomeGuide {
|
||||||
WelcomeGuide { // 1
|
|
||||||
colorScheme: root.colorScheme
|
|
||||||
|
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true // 1
|
||||||
|
colorScheme: root.colorScheme
|
||||||
}
|
}
|
||||||
|
SetupGuide {
|
||||||
SetupGuide { // 2
|
// 2
|
||||||
id: setupGuide
|
id: setupGuide
|
||||||
colorScheme: root.colorScheme
|
|
||||||
|
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
|
||||||
onDismissed: {
|
onDismissed: {
|
||||||
root.showSetup(null,"")
|
root.showSetup(null, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
onFinished: {
|
onFinished: {
|
||||||
// TODO: Do not close window. Trigger Backend to check that
|
// TODO: Do not close window. Trigger Backend to check that
|
||||||
// there is a successfully connected client. Then Backend
|
// there is a successfully connected client. Then Backend
|
||||||
// should send another signal to close the setup guide.
|
// should send another signal to close the setup guide.
|
||||||
root.showSetup(null,"")
|
root.showSetup(null, "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationPopups {
|
NotificationPopups {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
notifications: root.notifications
|
|
||||||
mainWindow: root
|
mainWindow: root
|
||||||
|
notifications: root.notifications
|
||||||
}
|
}
|
||||||
|
|
||||||
SplashScreen {
|
SplashScreen {
|
||||||
id: splashScreen
|
id: splashScreen
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
}
|
}
|
||||||
|
|
||||||
function showLocalCacheSettings() { contentWrapper.showLocalCacheSettings() }
|
|
||||||
function showSettings() { contentWrapper.showSettings() }
|
|
||||||
function showHelp() { contentWrapper.showHelp() }
|
|
||||||
function selectUser(userID) { contentWrapper.selectUser(userID) }
|
|
||||||
|
|
||||||
function showBugReportAndPrefill(message) {
|
|
||||||
contentWrapper.showBugReportAndPrefill(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
function showSignIn(username) {
|
|
||||||
if (contentLayout.currentIndex == 1) return
|
|
||||||
contentWrapper.showSignIn(username)
|
|
||||||
}
|
|
||||||
|
|
||||||
function showSetup(user, address) {
|
|
||||||
setupGuide.user = user
|
|
||||||
setupGuide.address = address
|
|
||||||
setupGuide.reset()
|
|
||||||
if (setupGuide.user) {
|
|
||||||
contentLayout._showSetup = true
|
|
||||||
} else {
|
|
||||||
contentLayout._showSetup = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showAndRise() {
|
|
||||||
root.show()
|
|
||||||
root.raise()
|
|
||||||
if (!root.active) {
|
|
||||||
root.requestActivate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,118 +1,99 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
//
|
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
|
||||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
// 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
|
// it under the terms of the GNU General Public License as published by
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
// (at your option) any later version.
|
// (at your option) any later version.
|
||||||
//
|
|
||||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
// GNU General Public License for more details.
|
// GNU General Public License for more details.
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU General Public License
|
// 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/>.
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import QtQml
|
import QtQml
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
|
|
||||||
import Proton
|
import Proton
|
||||||
import Notifications
|
import Notifications
|
||||||
|
|
||||||
Dialog {
|
Dialog {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
default property alias data: additionalChildrenContainer.children
|
||||||
property var notification
|
property var notification
|
||||||
|
|
||||||
shouldShow: notification && notification.active && !notification.dismissed
|
|
||||||
modal: true
|
modal: true
|
||||||
|
shouldShow: notification && notification.active && !notification.dismissed
|
||||||
default property alias data: additionalChildrenContainer.children
|
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
spacing: 0
|
spacing: 0
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
Layout.bottomMargin: 16
|
||||||
sourceSize.width: 64
|
|
||||||
sourceSize.height: 64
|
|
||||||
|
|
||||||
Layout.preferredHeight: 64
|
Layout.preferredHeight: 64
|
||||||
Layout.preferredWidth: 64
|
Layout.preferredWidth: 64
|
||||||
|
|
||||||
Layout.bottomMargin: 16
|
|
||||||
|
|
||||||
visible: source != ""
|
|
||||||
|
|
||||||
source: {
|
source: {
|
||||||
if (!root.notification) {
|
if (!root.notification) {
|
||||||
return ""
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (root.notification.type) {
|
switch (root.notification.type) {
|
||||||
case Notification.NotificationType.Info:
|
case Notification.NotificationType.Info:
|
||||||
return "/qml/icons/ic-info.svg"
|
return "/qml/icons/ic-info.svg";
|
||||||
case Notification.NotificationType.Success:
|
case Notification.NotificationType.Success:
|
||||||
return "/qml/icons/ic-success.svg"
|
return "/qml/icons/ic-success.svg";
|
||||||
case Notification.NotificationType.Warning:
|
case Notification.NotificationType.Warning:
|
||||||
case Notification.NotificationType.Danger:
|
case Notification.NotificationType.Danger:
|
||||||
return "/qml/icons/ic-alert.svg"
|
return "/qml/icons/ic-alert.svg";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
sourceSize.height: 64
|
||||||
|
sourceSize.width: 64
|
||||||
|
visible: source != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
Layout.bottomMargin: 8
|
Layout.bottomMargin: 8
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
text: root.notification.title
|
text: root.notification.title
|
||||||
type: Label.LabelType.Title
|
type: Label.LabelType.Title
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
|
Layout.bottomMargin: 16
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.preferredWidth: 240
|
Layout.preferredWidth: 240
|
||||||
Layout.bottomMargin: 16
|
|
||||||
|
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
text: root.notification.description
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
text: root.notification.description
|
||||||
type: Label.LabelType.Body
|
type: Label.LabelType.Body
|
||||||
onLinkActivated: function(link) { Qt.openUrlExternally(link) }
|
wrapMode: Text.WordWrap
|
||||||
}
|
|
||||||
|
|
||||||
|
onLinkActivated: function (link) {
|
||||||
|
Qt.openUrlExternally(link);
|
||||||
|
}
|
||||||
|
}
|
||||||
Item {
|
Item {
|
||||||
id: additionalChildrenContainer
|
id: additionalChildrenContainer
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.bottomMargin: 16
|
Layout.bottomMargin: 16
|
||||||
|
Layout.fillWidth: true
|
||||||
visible: children.length > 0
|
|
||||||
|
|
||||||
implicitHeight: additionalChildrenContainer.childrenRect.height
|
implicitHeight: additionalChildrenContainer.childrenRect.height
|
||||||
implicitWidth: additionalChildrenContainer.childrenRect.width
|
implicitWidth: additionalChildrenContainer.childrenRect.width
|
||||||
|
visible: children.length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
spacing: 8
|
spacing: 8
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: root.notification.action
|
model: root.notification.action
|
||||||
|
|
||||||
delegate: Button {
|
delegate: Button {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
colorScheme: root.colorScheme
|
|
||||||
action: modelData
|
action: modelData
|
||||||
|
colorScheme: root.colorScheme
|
||||||
secondary: index > 0
|
|
||||||
|
|
||||||
loading: modelData.loading
|
loading: modelData.loading
|
||||||
|
secondary: index > 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user