forked from Silverfish/proton-bridge
Compare commits
277 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d29571fb01 | |||
| d6000d025e | |||
| 09ef3b20db | |||
| 405331d59b | |||
| eff7df2136 | |||
| 5823e3a99f | |||
| 26d866bbbd | |||
| d3f7be059d | |||
| b52706a3ca | |||
| aebe7baed0 | |||
| ef31e2917c | |||
| 9eea26459a | |||
| 5747b85543 | |||
| ff78a23084 | |||
| 2a95e1ab41 | |||
| ab76cab533 | |||
| dda2a5d01a | |||
| c2afb42fd4 | |||
| 1d53044803 | |||
| d3f8297eb4 | |||
| b02203e3d3 | |||
| 5c7e4e04f9 | |||
| d7dadd7578 | |||
| ab9a758d63 | |||
| cb0935be96 | |||
| 441b388f62 | |||
| cdbcd30d15 | |||
| acc7ca8d4a | |||
| 42e1dd4c41 | |||
| 4cbd3ca832 | |||
| de0b6c0737 | |||
| 1c344211d1 | |||
| c11a87c16a | |||
| 3bf4282037 | |||
| 0c212fbef4 | |||
| 48d1ca1e72 | |||
| 52addb2582 | |||
| 742d9eeef3 | |||
| 55a9d4973c | |||
| 8402657108 | |||
| 8a6f96f9f2 | |||
| 56c53e9188 | |||
| bb67d95669 | |||
| 50acc0dcfb | |||
| e9c73c2d0d | |||
| 07c03c6920 | |||
| f4958b9b53 | |||
| 76f2e7fdb9 | |||
| c0992e8801 | |||
| cf3abaa96f | |||
| e422b28bc3 | |||
| a1a5ffba5d | |||
| f8b86a76dd | |||
| ab1281ceee | |||
| 0ab0f2f4ff | |||
| 09d87023f1 | |||
| 139ad75394 | |||
| c8cf90abfe | |||
| 5d4f8f7d40 | |||
| ea26dc0e97 | |||
| 8d346ea511 | |||
| 44df3cfd4a | |||
| 683458e264 | |||
| 36651698cb | |||
| 0c7e17701f | |||
| 86cd2437aa | |||
| 53f5f9aa43 | |||
| c849762445 | |||
| 32f2c72575 | |||
| 958e1280d7 | |||
| df09d6d221 | |||
| e0875dc928 | |||
| b3a5270bdc | |||
| f617a44d28 | |||
| 75ed3ca660 | |||
| 69f3029430 | |||
| 1203709ab9 | |||
| 15c18189d3 | |||
| a9e95f618b | |||
| 272f9cf59b | |||
| 6e86c95640 | |||
| 81afc5fb1f | |||
| 53ea5e9adc | |||
| 6f420f9098 | |||
| 65846ff40f | |||
| 43f7a989be | |||
| 452d3068f0 | |||
| 69190daf3f | |||
| f57a40677e | |||
| 2d6f42e0b5 | |||
| bccf31501d | |||
| 9b546b5412 | |||
| f48a60d58c | |||
| 0a51c7a6b0 | |||
| 7355c7dfd6 | |||
| bb5a91ee6d | |||
| ca5f7ce9f6 | |||
| ad31e6a9c5 | |||
| 9ef7d133c0 | |||
| 83b842b19d | |||
| df02e39fe1 | |||
| a35c8424a3 | |||
| 5d207810bd | |||
| 6c9d96d5e1 | |||
| 0fc41d1966 | |||
| dd5e745e37 | |||
| c8f0d7f32a | |||
| bd986901c3 | |||
| cdc19492ee | |||
| 635b2a4891 | |||
| e5bac33a04 | |||
| 7b96a07cf5 | |||
| 87e79fdcba | |||
| 03c3404044 | |||
| fa794a982b | |||
| cab32d5d5a | |||
| 8e5a892c45 | |||
| 50dc5c4085 | |||
| 3b58078595 | |||
| 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 | |||
| f0e2688a8e | |||
| 06639ff6cd | |||
| 716de7f45a | |||
| 750de0cd31 | |||
| 823ca4d207 | |||
| a187747c7c | |||
| 11ebb16933 | |||
| 0048767022 | |||
| c4b75c6f34 | |||
| 32448063dc | |||
| 86bde91958 | |||
| 90c34406ba | |||
| 334a256638 | |||
| da528f2d9b | |||
| 1b0f930471 | |||
| 09c523e2d2 | |||
| 0b35f41ffd | |||
| 7be1a8ae8a | |||
| bdbf1bdd76 | |||
| 776976a8a2 | |||
| 040ddadb7a | |||
| 11f6f84dd6 | |||
| 82efa16d65 | |||
| 110286b81c | |||
| bc66841cdc | |||
| 8d028966c7 | |||
| d120bbeffc | |||
| eda49483e2 | |||
| 3ef526333a | |||
| f454f1a74f | |||
| e97a4d8847 | |||
| cb8174dbfd | |||
| cfca429067 | |||
| 1ffb9089ba | |||
| c0fc23d7cd | |||
| 42178806d1 | |||
| e34050282e | |||
| efb6ba0f1b | |||
| 80194ad797 | |||
| cda6b2a728 | |||
| 44bde86fde | |||
| 6cb52cacc9 | |||
| 8248491833 | |||
| fbdede28f0 | |||
| 66bc911652 | |||
| c860741ffa | |||
| 300d243331 | |||
| c4abb14ae6 | |||
| a06fd31f49 | |||
| 016319784e | |||
| 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 |
@ -18,6 +18,10 @@
|
|||||||
---
|
---
|
||||||
image: gitlab.protontech.ch:4567/go/bridge-internal:test-go1.20
|
image: gitlab.protontech.ch:4567/go/bridge-internal:test-go1.20
|
||||||
|
|
||||||
|
default:
|
||||||
|
tags:
|
||||||
|
- shared-small
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
GOPRIVATE: gitlab.protontech.ch
|
GOPRIVATE: gitlab.protontech.ch
|
||||||
GOMAXPROCS: $(( ${CI_TAG_CPU} / 2 ))
|
GOMAXPROCS: $(( ${CI_TAG_CPU} / 2 ))
|
||||||
@ -47,6 +51,19 @@ stages:
|
|||||||
allow_failure: true
|
allow_failure: true
|
||||||
- when: never
|
- when: never
|
||||||
|
|
||||||
|
.rules-branch-manual-scheduled-and-test-branch-always:
|
||||||
|
rules:
|
||||||
|
- if: $CI_PIPELINE_SOURCE == "schedule"
|
||||||
|
when: always
|
||||||
|
allow_failure: false
|
||||||
|
- if: $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME=~ /^test/
|
||||||
|
when: always
|
||||||
|
allow_failure: false
|
||||||
|
- if: $CI_COMMIT_BRANCH
|
||||||
|
when: manual
|
||||||
|
allow_failure: true
|
||||||
|
- when: never
|
||||||
|
|
||||||
# ENV
|
# ENV
|
||||||
.env-windows:
|
.env-windows:
|
||||||
before_script:
|
before_script:
|
||||||
@ -58,7 +75,7 @@ stages:
|
|||||||
- export GO111MODULE=on
|
- export GO111MODULE=on
|
||||||
- export PATH="${GOPATH}/bin:${PATH}"
|
- export PATH="${GOPATH}/bin:${PATH}"
|
||||||
- export MSYSTEM=
|
- export MSYSTEM=
|
||||||
- export QT6DIR=/c/grrrQt/6.3.2/msvc2019_64
|
- export QT6DIR=/c/grrrQt/6.4.3/msvc2019_64
|
||||||
- export PATH=$PATH:${QT6DIR}/bin
|
- export PATH=$PATH:${QT6DIR}/bin
|
||||||
- export PATH="/c/Program Files/Microsoft Visual Studio/2022/Community/Common7/IDE/CommonExtensions/Microsoft/CMake/CMake/bin:$PATH"
|
- export PATH="/c/Program Files/Microsoft Visual Studio/2022/Community/Common7/IDE/CommonExtensions/Microsoft/CMake/CMake/bin:$PATH"
|
||||||
- $(git config --global -l | grep -o 'url.*gitlab.protontech.ch.*insteadof' | xargs -L 1 git config --global --unset &> /dev/null) || echo "nothing to remove"
|
- $(git config --global -l | grep -o 'url.*gitlab.protontech.ch.*insteadof' | xargs -L 1 git config --global --unset &> /dev/null) || echo "nothing to remove"
|
||||||
@ -80,7 +97,7 @@ stages:
|
|||||||
- export PATH="${GOROOT}/bin:$PATH"
|
- export PATH="${GOROOT}/bin:$PATH"
|
||||||
- export GOPATH=~/go1.20
|
- export GOPATH=~/go1.20
|
||||||
- export PATH="${GOPATH}/bin:$PATH"
|
- export PATH="${GOPATH}/bin:$PATH"
|
||||||
- export QT6DIR=/opt/Qt/6.3.2/macos
|
- export QT6DIR=/opt/Qt/6.4.3/macos
|
||||||
- export PATH="${QT6DIR}/bin:$PATH"
|
- export PATH="${QT6DIR}/bin:$PATH"
|
||||||
- uname -a
|
- uname -a
|
||||||
cache: {}
|
cache: {}
|
||||||
@ -88,7 +105,7 @@ stages:
|
|||||||
- macos-m1-bridge
|
- macos-m1-bridge
|
||||||
|
|
||||||
.env-linux-build:
|
.env-linux-build:
|
||||||
image: gitlab.protontech.ch:4567/go/bridge-internal:build-go1.20-qt6.3.2
|
image: gitlab.protontech.ch:4567/go/bridge-internal:build-go1.20-qt6.4.3
|
||||||
variables:
|
variables:
|
||||||
VCPKG_DEFAULT_BINARY_CACHE: ${CI_PROJECT_DIR}/.cache
|
VCPKG_DEFAULT_BINARY_CACHE: ${CI_PROJECT_DIR}/.cache
|
||||||
cache:
|
cache:
|
||||||
@ -105,7 +122,7 @@ stages:
|
|||||||
- $(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 -l | grep -o 'url.*gitlab.protontech.ch.*insteadof' | xargs -L 1 git config --global --unset &> /dev/null) || echo "nothing to remove"
|
||||||
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
|
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
|
||||||
tags:
|
tags:
|
||||||
- large
|
- shared-large
|
||||||
|
|
||||||
# Stage: TEST
|
# Stage: TEST
|
||||||
|
|
||||||
@ -116,7 +133,16 @@ lint:
|
|||||||
script:
|
script:
|
||||||
- make lint
|
- make lint
|
||||||
tags:
|
tags:
|
||||||
- medium
|
- shared-medium
|
||||||
|
|
||||||
|
bug-report-preview:
|
||||||
|
stage: test
|
||||||
|
extends:
|
||||||
|
- .rules-branch-and-MR-manual
|
||||||
|
script:
|
||||||
|
- make lint-bug-report-preview
|
||||||
|
tags:
|
||||||
|
- shared-medium
|
||||||
|
|
||||||
.script-test:
|
.script-test:
|
||||||
stage: test
|
stage: test
|
||||||
@ -132,7 +158,16 @@ test-linux:
|
|||||||
extends:
|
extends:
|
||||||
- .script-test
|
- .script-test
|
||||||
tags:
|
tags:
|
||||||
- large
|
- shared-large
|
||||||
|
|
||||||
|
fuzz-linux:
|
||||||
|
stage: test
|
||||||
|
extends:
|
||||||
|
- .rules-branch-manual-MR-and-devel-always
|
||||||
|
script:
|
||||||
|
- make fuzz
|
||||||
|
tags:
|
||||||
|
- shared-large
|
||||||
|
|
||||||
test-linux-race:
|
test-linux-race:
|
||||||
extends:
|
extends:
|
||||||
@ -154,11 +189,19 @@ test-integration-race:
|
|||||||
script:
|
script:
|
||||||
- make test-integration-race
|
- 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:
|
test-windows:
|
||||||
extends:
|
extends:
|
||||||
- .env-windows
|
- .env-windows
|
||||||
- .script-test
|
- .script-test
|
||||||
- .rules-branch-and-MR-manual
|
|
||||||
|
|
||||||
test-darwin:
|
test-darwin:
|
||||||
extends:
|
extends:
|
||||||
@ -168,17 +211,18 @@ test-darwin:
|
|||||||
test-coverage:
|
test-coverage:
|
||||||
stage: test
|
stage: test
|
||||||
extends:
|
extends:
|
||||||
- .rules-branch-manual-MR-and-devel-always
|
- .rules-branch-manual-scheduled-and-test-branch-always
|
||||||
script:
|
script:
|
||||||
- ./utils/coverage.sh
|
- ./utils/coverage.sh
|
||||||
coverage: '/total:.*\(statements\).*\d+\.\d+%/'
|
coverage: '/total:.*\(statements\).*\d+\.\d+%/'
|
||||||
needs:
|
needs:
|
||||||
- test-linux
|
- test-linux
|
||||||
#- test-windows
|
- test-windows
|
||||||
- test-darwin
|
- test-darwin
|
||||||
- test-integration
|
- test-integration
|
||||||
|
- test-integration-nightly
|
||||||
tags:
|
tags:
|
||||||
- small
|
- shared-small
|
||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- coverage*
|
- coverage*
|
||||||
@ -242,4 +286,18 @@ build-windows-qa:
|
|||||||
variables:
|
variables:
|
||||||
BUILD_TAGS: "build_qa"
|
BUILD_TAGS: "build_qa"
|
||||||
|
|
||||||
|
trigger-qa-installer:
|
||||||
|
stage: build
|
||||||
|
needs: ["lint"]
|
||||||
|
extends:
|
||||||
|
- .rules-branch-and-MR-manual
|
||||||
|
variables:
|
||||||
|
APP: bridge
|
||||||
|
WORKFLOW: build-all
|
||||||
|
SRC_TAG: $CI_COMMIT_BRANCH
|
||||||
|
SRC_HASH: $CI_COMMIT_SHA
|
||||||
|
trigger:
|
||||||
|
project: "jcuth/bridge-release"
|
||||||
|
branch: master
|
||||||
|
|
||||||
# TODO: PUT BACK ALL THE JOBS! JUST DID THIS FOR NOW TO GET CI WORKING AGAIN...
|
# TODO: PUT BACK ALL THE JOBS! JUST DID THIS FOR NOW TO GET CI WORKING AGAIN...
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
* Windres (Windows)
|
* Windres (Windows)
|
||||||
* libglvnd and libsecret development files (Linux)
|
* libglvnd and libsecret development files (Linux)
|
||||||
* pkg-config (Linux)
|
* pkg-config (Linux)
|
||||||
* cmake, ninja-build and Qt 6 are required to build the graphical user interface. On Linux,
|
* cmake, ninja-build and Qt 6.4.3 are required to build the graphical user interface. On Linux,
|
||||||
the Mesa OpenGL development files are also needed.
|
the Mesa OpenGL development files are also needed.
|
||||||
|
|
||||||
To enable the sending of crash reports using Sentry please set the
|
To enable the sending of crash reports using Sentry please set the
|
||||||
@ -19,7 +19,7 @@ Otherwise, the sending of crash reports will be disabled.
|
|||||||
|
|
||||||
## Build
|
## Build
|
||||||
In order to build Bridge app with Qt interface we are using
|
In order to build Bridge app with Qt interface we are using
|
||||||
[Qt 6.3](https://doc.qt.io/qt-6/gettingstarted.html).
|
[Qt 6.4.3](https://doc.qt.io/qt-6/gettingstarted.html).
|
||||||
|
|
||||||
Please note that qmake path must be in your `PATH` to ensure Qt to be found.
|
Please note that qmake path must be in your `PATH` to ensure Qt to be found.
|
||||||
Also, before you start build **on Windows**, please unset the `MSYSTEM` variable
|
Also, before you start build **on Windows**, please unset the `MSYSTEM` variable
|
||||||
|
|||||||
@ -40,6 +40,7 @@ Proton Mail Bridge includes the following 3rd party software:
|
|||||||
* [go-message](https://github.com/emersion/go-message) available under [license](https://github.com/emersion/go-message/blob/master/LICENSE)
|
* [go-message](https://github.com/emersion/go-message) available under [license](https://github.com/emersion/go-message/blob/master/LICENSE)
|
||||||
* [go-sasl](https://github.com/emersion/go-sasl) available under [license](https://github.com/emersion/go-sasl/blob/master/LICENSE)
|
* [go-sasl](https://github.com/emersion/go-sasl) available under [license](https://github.com/emersion/go-sasl/blob/master/LICENSE)
|
||||||
* [go-smtp](https://github.com/emersion/go-smtp) available under [license](https://github.com/emersion/go-smtp/blob/master/LICENSE)
|
* [go-smtp](https://github.com/emersion/go-smtp) available under [license](https://github.com/emersion/go-smtp/blob/master/LICENSE)
|
||||||
|
* [go-vcard](https://github.com/emersion/go-vcard) available under [license](https://github.com/emersion/go-vcard/blob/master/LICENSE)
|
||||||
* [color](https://github.com/fatih/color) available under [license](https://github.com/fatih/color/blob/master/LICENSE)
|
* [color](https://github.com/fatih/color) available under [license](https://github.com/fatih/color/blob/master/LICENSE)
|
||||||
* [sentry-go](https://github.com/getsentry/sentry-go) available under [license](https://github.com/getsentry/sentry-go/blob/master/LICENSE)
|
* [sentry-go](https://github.com/getsentry/sentry-go) available under [license](https://github.com/getsentry/sentry-go/blob/master/LICENSE)
|
||||||
* [resty](https://github.com/go-resty/resty/v2) available under [license](https://github.com/go-resty/resty/v2/blob/master/LICENSE)
|
* [resty](https://github.com/go-resty/resty/v2) available under [license](https://github.com/go-resty/resty/v2/blob/master/LICENSE)
|
||||||
@ -83,7 +84,6 @@ Proton Mail Bridge includes the following 3rd party software:
|
|||||||
* [go-spew](https://github.com/davecgh/go-spew) available under [license](https://github.com/davecgh/go-spew/blob/master/LICENSE)
|
* [go-spew](https://github.com/davecgh/go-spew) available under [license](https://github.com/davecgh/go-spew/blob/master/LICENSE)
|
||||||
* [go-windows](https://github.com/elastic/go-windows) available under [license](https://github.com/elastic/go-windows/blob/master/LICENSE)
|
* [go-windows](https://github.com/elastic/go-windows) available under [license](https://github.com/elastic/go-windows/blob/master/LICENSE)
|
||||||
* [go-textwrapper](https://github.com/emersion/go-textwrapper) available under [license](https://github.com/emersion/go-textwrapper/blob/master/LICENSE)
|
* [go-textwrapper](https://github.com/emersion/go-textwrapper) available under [license](https://github.com/emersion/go-textwrapper/blob/master/LICENSE)
|
||||||
* [go-vcard](https://github.com/emersion/go-vcard) available under [license](https://github.com/emersion/go-vcard/blob/master/LICENSE)
|
|
||||||
* [fgprof](https://github.com/felixge/fgprof) available under [license](https://github.com/felixge/fgprof/blob/master/LICENSE)
|
* [fgprof](https://github.com/felixge/fgprof) available under [license](https://github.com/felixge/fgprof/blob/master/LICENSE)
|
||||||
* [go-shlex](https://github.com/flynn-archive/go-shlex) available under [license](https://github.com/flynn-archive/go-shlex/blob/master/LICENSE)
|
* [go-shlex](https://github.com/flynn-archive/go-shlex) available under [license](https://github.com/flynn-archive/go-shlex/blob/master/LICENSE)
|
||||||
* [mimetype](https://github.com/gabriel-vasile/mimetype) available under [license](https://github.com/gabriel-vasile/mimetype/blob/master/LICENSE)
|
* [mimetype](https://github.com/gabriel-vasile/mimetype) available under [license](https://github.com/gabriel-vasile/mimetype/blob/master/LICENSE)
|
||||||
@ -123,6 +123,7 @@ 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-ordered-json](https://gitlab.com/c0b/go-ordered-json)
|
||||||
* [arch](https://golang.org/x/arch) available under [license](https://cs.opensource.google/go/x/arch/+/master:LICENSE)
|
* [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)
|
||||||
|
|||||||
192
Changelog.md
192
Changelog.md
@ -3,6 +3,142 @@
|
|||||||
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||||
|
|
||||||
|
|
||||||
|
## Vasco da Gama Bridge 3.6.0
|
||||||
|
|
||||||
|
### Added
|
||||||
|
* GODT-2762: Setup wizard.
|
||||||
|
* GODT-2772: Setup wizard content.
|
||||||
|
* GODT-2769: Setup Wizard architecture.
|
||||||
|
* GODT-2767: Setup Wizard foundations.
|
||||||
|
* GODT-2725: Implement receive message step with expected structure exposed.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* GODT-2960: Added content in empty view when there is no account.
|
||||||
|
* GODT-2771: Cert related tools for macOS.
|
||||||
|
* GODT-2770: Proof of concept for web view as a tool window and overlay (not used).
|
||||||
|
* GODT-2916: Split Decryption from Message Building.
|
||||||
|
* GODT-2597: Implement contact specific settings in integration tests.
|
||||||
|
* GODT-2664: Trigger QA installer.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* GODT-2992: Fix link in 'no account view' in main window after 2FA or TOTP are cancelled.
|
||||||
|
* GODT-2989: Allow to send bug report when no account connected.
|
||||||
|
* GODT-2988: Fix setup wizard KB links.
|
||||||
|
* GODT-2968: Use proper base64 encoded string even for bad password test.
|
||||||
|
* GODT-2965: Fix multipart/mixed testdata + structure parsing steps related to this.
|
||||||
|
* GODT-2932: Fix syncing not being reported in GUI.
|
||||||
|
* GODT-2967: Tray menu entries close the setup wizard when needed.
|
||||||
|
* GODT-2212: Preserver Header order in message building.
|
||||||
|
* Fixed missing GoOs gRPC call in bridge-gui-tester.
|
||||||
|
* GODT-2929: Message dedup with different text transfer encoding.
|
||||||
|
|
||||||
|
|
||||||
|
## Umshiang Bridge 3.5.3
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* GODT-3004: Update gopenpgp and dependencies.
|
||||||
|
|
||||||
|
|
||||||
|
## Umshiang Bridge 3.5.2
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* GODT-3003: Ensure IMAP State is reset after vault corruption.
|
||||||
|
* GODT-3001: Only create system labels during system label sync.
|
||||||
|
|
||||||
|
|
||||||
|
## 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
|
## Trift Bridge 3.4.0
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@ -41,7 +177,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
|||||||
* GODT-2578: Refresh literals appended to Sent folder.
|
* GODT-2578: Refresh literals appended to Sent folder.
|
||||||
* GODT-2753: Vault test now check that value auto-assigned is first available port.
|
* GODT-2753: Vault test now check that value auto-assigned is first available port.
|
||||||
* GODT-2522: Handle migration with unreferenced db values.
|
* GODT-2522: Handle migration with unreferenced db values.
|
||||||
* GODT-2693: Allow missing whitespace after header field colon.
|
* GODT-2670: Allow missing whitespace after header field colon.
|
||||||
* GODT-2653: Only log when err is not nil.
|
* 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-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.
|
* GODT-2675: Update GPA to applye togin-gonic/gin patch + update COPYING_NOTES.
|
||||||
@ -126,7 +262,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.
|
||||||
@ -186,7 +322,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
|
||||||
|
|
||||||
@ -327,7 +463,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.
|
||||||
@ -344,7 +480,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.
|
||||||
|
|
||||||
@ -409,7 +545,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.
|
||||||
@ -689,7 +825,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.
|
||||||
@ -843,7 +979,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.
|
||||||
@ -940,7 +1076,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.
|
||||||
@ -982,7 +1118,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.
|
||||||
@ -1233,7 +1369,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.
|
||||||
|
|
||||||
@ -1323,7 +1459,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.
|
||||||
@ -1647,7 +1783,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.
|
||||||
@ -1812,13 +1948,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.
|
||||||
@ -1944,7 +2080,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.
|
||||||
@ -1952,7 +2088,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.
|
||||||
@ -2000,7 +2136,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.
|
||||||
@ -2074,7 +2210,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.
|
||||||
@ -2096,7 +2232,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.
|
||||||
@ -2113,7 +2249,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.
|
||||||
|
|
||||||
@ -2137,7 +2273,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.
|
||||||
@ -2156,7 +2292,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).
|
||||||
@ -2228,7 +2364,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)
|
||||||
|
|
||||||
@ -2294,7 +2430,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
|
||||||
@ -2326,12 +2462,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).
|
||||||
@ -2356,7 +2492,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).
|
||||||
|
|||||||
44
Makefile
44
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.4.0+git
|
BRIDGE_APP_VERSION?=3.6.0+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
|
||||||
@ -246,7 +246,7 @@ test-race: gofiles
|
|||||||
test-integration: gofiles
|
test-integration: gofiles
|
||||||
mkdir -p coverage/integration
|
mkdir -p coverage/integration
|
||||||
go test \
|
go test \
|
||||||
-v -timeout=60m -p=1 -count=1 \
|
-v -timeout=60m -p=1 -count=1 -tags=test_integration \
|
||||||
${GOCOVERAGE} \
|
${GOCOVERAGE} \
|
||||||
github.com/ProtonMail/proton-bridge/v3/tests \
|
github.com/ProtonMail/proton-bridge/v3/tests \
|
||||||
${GOCOVERDIR}/integration
|
${GOCOVERDIR}/integration
|
||||||
@ -258,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
|
||||||
@ -274,9 +290,23 @@ 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/user MessageDownloader > internal/user/mocks/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
|
||||||
|
mockgen --package mocks github.com/ProtonMail/gluon/connector IMAPStateWrite > internal/services/imapservice/mocks/mocks.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
|
||||||
@ -292,6 +322,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}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
25
go.mod
25
go.mod
@ -5,10 +5,10 @@ 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.20230706110757-a9327fb18611
|
github.com/ProtonMail/gluon v0.17.1-0.20231025125916-5c7941465df8
|
||||||
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.20230727082922-9115b4750ec7
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20231030091225-8fc2478b27f4
|
||||||
github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton
|
github.com/ProtonMail/gopenpgp/v2 v2.7.4-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
|
||||||
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37
|
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37
|
||||||
@ -22,6 +22,7 @@ require (
|
|||||||
github.com/emersion/go-message v0.16.0
|
github.com/emersion/go-message v0.16.0
|
||||||
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead
|
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead
|
||||||
github.com/emersion/go-smtp v0.15.1-0.20221021114529-49b17434419d
|
github.com/emersion/go-smtp v0.15.1-0.20221021114529-49b17434419d
|
||||||
|
github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3
|
||||||
github.com/fatih/color v1.13.0
|
github.com/fatih/color v1.13.0
|
||||||
github.com/getsentry/sentry-go v0.15.0
|
github.com/getsentry/sentry-go v0.15.0
|
||||||
github.com/go-resty/resty/v2 v2.7.0
|
github.com/go-resty/resty/v2 v2.7.0
|
||||||
@ -42,17 +43,17 @@ require (
|
|||||||
github.com/vmihailenco/msgpack/v5 v5.3.5
|
github.com/vmihailenco/msgpack/v5 v5.3.5
|
||||||
go.uber.org/goleak v1.2.1
|
go.uber.org/goleak v1.2.1
|
||||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
|
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
|
||||||
golang.org/x/net v0.10.0
|
golang.org/x/net v0.17.0
|
||||||
golang.org/x/sys v0.8.0
|
golang.org/x/sys v0.13.0
|
||||||
golang.org/x/text v0.9.0
|
golang.org/x/text v0.13.0
|
||||||
google.golang.org/grpc v1.53.0
|
google.golang.org/grpc v1.56.3
|
||||||
google.golang.org/protobuf v1.30.0
|
google.golang.org/protobuf v1.30.0
|
||||||
howett.net/plist v1.0.0
|
howett.net/plist v1.0.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
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-20230717121622-edf196117233 // 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
|
||||||
@ -68,7 +69,6 @@ require (
|
|||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/elastic/go-windows v1.0.1 // indirect
|
github.com/elastic/go-windows v1.0.1 // indirect
|
||||||
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect
|
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect
|
||||||
github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3 // indirect
|
|
||||||
github.com/felixge/fgprof v0.9.3 // indirect
|
github.com/felixge/fgprof v0.9.3 // indirect
|
||||||
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
|
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||||
@ -79,7 +79,7 @@ require (
|
|||||||
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.2 // indirect
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
github.com/gofrs/uuid v4.3.0+incompatible // indirect
|
github.com/gofrs/uuid v4.3.0+incompatible // indirect
|
||||||
github.com/golang/protobuf v1.5.2 // indirect
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect
|
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect
|
||||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
||||||
@ -108,12 +108,13 @@ 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
|
||||||
|
gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a // 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.14.0 // indirect
|
||||||
golang.org/x/mod v0.8.0 // indirect
|
golang.org/x/mod v0.8.0 // indirect
|
||||||
golang.org/x/sync v0.2.0 // indirect
|
golang.org/x/sync v0.2.0 // indirect
|
||||||
golang.org/x/tools v0.6.0 // indirect
|
golang.org/x/tools v0.6.0 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20230221151758-ace64dc21148 // indirect
|
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
53
go.sum
53
go.sum
@ -23,24 +23,23 @@ 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.20230706110757-a9327fb18611 h1:QVydPr/+pgz5xihc2ujNNV+qnq3oTidIXvF0PgkcY6U=
|
github.com/ProtonMail/gluon v0.17.1-0.20231025125916-5c7941465df8 h1:sG0o5pEoS2z2jNR9zK7Juq5Tr3X+GfHmQ8L99RPowaE=
|
||||||
github.com/ProtonMail/gluon v0.16.1-0.20230706110757-a9327fb18611/go.mod h1:Og5/Dz1MiGpCJn51XujZwxiLG7WzvvjE5PRpZBQmAHo=
|
github.com/ProtonMail/gluon v0.17.1-0.20231025125916-5c7941465df8/go.mod h1:Og5/Dz1MiGpCJn51XujZwxiLG7WzvvjE5PRpZBQmAHo=
|
||||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
|
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a 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=
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230322105811-d73448b7e800/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
|
github.com/ProtonMail/go-crypto v0.0.0-20230717121622-edf196117233 h1:bdoKdh0f66/lrgVfYlxw0aqISY/KOqXmFJyGt7rGmnc=
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903 h1:ZK3C5DtzV2nVAQTx5S5jQvMeDqWtD1By5mOoyY/xJek=
|
github.com/ProtonMail/go-crypto v0.0.0-20230717121622-edf196117233/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
|
|
||||||
github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7 h1:+j+Kd/DyZ/qGfMT9htAT7HxqIEbZHsatsx+m8AoV6fc=
|
github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7 h1:+j+Kd/DyZ/qGfMT9htAT7HxqIEbZHsatsx+m8AoV6fc=
|
||||||
github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
|
github.com/ProtonMail/go-message v0.13.1-0.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.20230727082922-9115b4750ec7 h1:Rmg3TPK6vFGNWR4hxmPoBhV75Sl716iB46wEi2U4Q+c=
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20231030091225-8fc2478b27f4 h1:1XISoHi1FmaVW3vm/y5FuXmrSMo53U0sM3zZpgczWTc=
|
||||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20230727082922-9115b4750ec7/go.mod h1:+aTJoYu8bqzGECXL2DOdiZTZ64bGn3w0NC8VcFpJrFM=
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20231030091225-8fc2478b27f4/go.mod h1:qgCy0LgMJy3bfVYyLljPScdB1bybc2adEkMr9WhBB5c=
|
||||||
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.4-proton h1:8tqHYM6IGsdEc6Vxf1TWiwpHNj8yIEQNACPhxsDagrk=
|
||||||
github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton/go.mod h1:S1lYsaGHykYpxxh2SnJL6ypcAlANKj5NRSY6HxKryKQ=
|
github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton/go.mod h1:omVkSsfPAhmptzPF/piMXb16wKIWUvVhZbVW7sJKh0A=
|
||||||
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
|
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
|
||||||
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
|
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
|
||||||
github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw=
|
github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw=
|
||||||
@ -64,6 +63,7 @@ github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJm
|
|||||||
github.com/bradenaw/juniper v0.12.0 h1:Q/7icpPQD1nH/La5DobQfNEtwyrBSiSu47jOQx7lJEM=
|
github.com/bradenaw/juniper v0.12.0 h1:Q/7icpPQD1nH/La5DobQfNEtwyrBSiSu47jOQx7lJEM=
|
||||||
github.com/bradenaw/juniper v0.12.0/go.mod h1:Z2B7aJlQ7xbfWsnMLROj5t/5FQ94/MkIdKC30J4WvzI=
|
github.com/bradenaw/juniper v0.12.0/go.mod h1:Z2B7aJlQ7xbfWsnMLROj5t/5FQ94/MkIdKC30J4WvzI=
|
||||||
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||||
|
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||||
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
||||||
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||||
@ -178,8 +178,8 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
|
|||||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
@ -399,6 +399,8 @@ 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=
|
||||||
|
gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a h1:DxppxFKRqJ8WD6oJ3+ZXKDY0iMONQDl5UTg2aTyHh8k=
|
||||||
|
gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a/go.mod h1:NREvu3a57BaK0R1+ztrEzHWiZAihohNLQ6trPxlIqZI=
|
||||||
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=
|
||||||
@ -417,9 +419,10 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
|
|||||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||||
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
|
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||||
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||||
@ -464,12 +467,13 @@ golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qx
|
|||||||
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||||
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
@ -512,13 +516,16 @@ golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||||
@ -529,10 +536,12 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|||||||
golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
|
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||||
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
@ -580,13 +589,13 @@ google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98
|
|||||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||||
google.golang.org/genproto v0.0.0-20230221151758-ace64dc21148 h1:muK+gVBJBfFb4SejshDBlN2/UgxCCOKH9Y34ljqEGOc=
|
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
|
||||||
google.golang.org/genproto v0.0.0-20230221151758-ace64dc21148/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw=
|
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
|
||||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||||
google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc=
|
google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc=
|
||||||
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
|
google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||||
|
|||||||
@ -52,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"
|
||||||
|
|
||||||
@ -96,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},
|
||||||
@ -379,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()
|
||||||
|
|||||||
@ -22,7 +22,6 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
|
|
||||||
"github.com/ProtonMail/gluon/async"
|
"github.com/ProtonMail/gluon/async"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/certs"
|
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
|
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||||
@ -45,23 +44,6 @@ func WithVault(locations *locations.Locations, panicHandler async.PanicHandler,
|
|||||||
"corrupt": corrupt,
|
"corrupt": corrupt,
|
||||||
}).Debug("Vault created")
|
}).Debug("Vault created")
|
||||||
|
|
||||||
// Install the certificates if needed.
|
|
||||||
if installed := encVault.GetCertsInstalled(); !installed {
|
|
||||||
logrus.Debug("Installing certificates")
|
|
||||||
|
|
||||||
certPEM, _ := encVault.GetBridgeTLSCert()
|
|
||||||
|
|
||||||
if err := certs.NewInstaller().InstallCert(certPEM); err != nil {
|
|
||||||
return fmt.Errorf("failed to install certs: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := encVault.SetCertsInstalled(true); err != nil {
|
|
||||||
return fmt.Errorf("failed to set certs installed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
logrus.Debug("Certificates successfully installed")
|
|
||||||
}
|
|
||||||
|
|
||||||
// GODT-1950: Add teardown actions (e.g. to close the vault).
|
// GODT-1950: Add teardown actions (e.g. to close the vault).
|
||||||
|
|
||||||
return fn(encVault, insecure, corrupt)
|
return fn(encVault, insecure, corrupt)
|
||||||
|
|||||||
@ -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()
|
||||||
|
|
||||||
@ -477,27 +487,15 @@ func (bridge *Bridge) remWatcher(watcher *watcher.Watcher[events.Event]) {
|
|||||||
watcher.Close()
|
watcher.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) onStatusUp(ctx context.Context) {
|
func (bridge *Bridge) onStatusUp(_ context.Context) {
|
||||||
logrus.Info("Handling API status up")
|
logrus.Info("Handling API status up")
|
||||||
|
|
||||||
safe.RLock(func() {
|
|
||||||
for _, user := range bridge.users {
|
|
||||||
user.OnStatusUp(ctx)
|
|
||||||
}
|
|
||||||
}, bridge.usersLock)
|
|
||||||
|
|
||||||
bridge.goLoad()
|
bridge.goLoad()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) onStatusDown(ctx context.Context) {
|
func (bridge *Bridge) onStatusDown(ctx context.Context) {
|
||||||
logrus.Info("Handling API status down")
|
logrus.Info("Handling API status down")
|
||||||
|
|
||||||
safe.RLock(func() {
|
|
||||||
for _, user := range bridge.users {
|
|
||||||
user.OnStatusDown(ctx)
|
|
||||||
}
|
|
||||||
}, bridge.usersLock)
|
|
||||||
|
|
||||||
for backoff := time.Second; ; backoff = min(backoff*2, 30*time.Second) {
|
for backoff := time.Second; ; backoff = min(backoff*2, 30*time.Second) {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
@ -527,24 +525,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
|
||||||
|
|||||||
@ -44,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"
|
||||||
@ -54,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 (
|
||||||
@ -583,7 +585,7 @@ func TestBridge_MissingGluonStore(t *testing.T) {
|
|||||||
require.NoError(t, os.RemoveAll(gluonDir))
|
require.NoError(t, os.RemoveAll(gluonDir))
|
||||||
|
|
||||||
// Bridge starts but can't find the gluon store dir; there should be no error.
|
// Bridge starts but can't find the gluon store dir; there should be no error.
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridgeWaitForServers(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
// ...
|
// ...
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -621,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)
|
||||||
@ -641,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)
|
||||||
@ -700,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))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -776,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.
|
||||||
@ -873,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()
|
||||||
|
|
||||||
@ -1057,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)
|
||||||
|
|
||||||
@ -1066,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 {
|
||||||
|
|||||||
@ -19,6 +19,7 @@ package bridge
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
@ -33,63 +34,133 @@ const (
|
|||||||
DefaultMaxSessionCountForBugReport = 10
|
DefaultMaxSessionCountForBugReport = 10
|
||||||
)
|
)
|
||||||
|
|
||||||
func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, description, username, email, client string, attachLogs bool) error {
|
type ReportBugReq struct {
|
||||||
var account string
|
OSType string
|
||||||
|
OSVersion string
|
||||||
|
Title string
|
||||||
|
Description string
|
||||||
|
Username string
|
||||||
|
Email string
|
||||||
|
EmailClient string
|
||||||
|
IncludeLogs bool
|
||||||
|
}
|
||||||
|
|
||||||
if info, err := bridge.QueryUserInfo(username); err == nil {
|
func (bridge *Bridge) ReportBug(ctx context.Context, report *ReportBugReq) error {
|
||||||
account = info.Username
|
if info, err := bridge.QueryUserInfo(report.Username); err == nil {
|
||||||
|
report.Username = info.Username
|
||||||
} else if userIDs := bridge.GetUserIDs(); len(userIDs) > 0 {
|
} else if userIDs := bridge.GetUserIDs(); len(userIDs) > 0 {
|
||||||
if err := bridge.vault.GetUser(userIDs[0], func(user *vault.User) {
|
if err := bridge.vault.GetUser(userIDs[0], func(user *vault.User) {
|
||||||
account = user.Username()
|
report.Username = user.Username()
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var attachment []proton.ReportBugAttachment
|
var attachments []proton.ReportBugAttachment
|
||||||
|
if report.IncludeLogs {
|
||||||
if attachLogs {
|
logs, err := bridge.CollectLogs()
|
||||||
logsPath, err := bridge.locator.ProvideLogsPath()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
attachments = append(attachments, logs)
|
||||||
buffer, err := logging.ZipLogsForBugReport(logsPath, DefaultMaxSessionCountForBugReport, DefaultMaxBugReportZipSize)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := io.ReadAll(buffer)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
attachment = append(attachment, proton.ReportBugAttachment{
|
|
||||||
Name: "logs.zip",
|
|
||||||
Filename: "logs.zip",
|
|
||||||
MIMEType: "application/zip",
|
|
||||||
Body: body,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
safe.Lock(func() {
|
var firstAtt proton.ReportBugAttachment
|
||||||
|
if len(attachments) > 0 && report.IncludeLogs {
|
||||||
|
firstAtt = attachments[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
attachmentType := proton.AttachmentTypeSync
|
||||||
|
if len(attachments) > 1 {
|
||||||
|
attachmentType = proton.AttachmentTypeAsync
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := bridge.createTicket(ctx, report, attachmentType, firstAtt)
|
||||||
|
if err != nil || token == "" {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
safe.RLock(func() {
|
||||||
for _, user := range bridge.users {
|
for _, user := range bridge.users {
|
||||||
user.ReportBugSent()
|
user.ReportBugSent()
|
||||||
}
|
}
|
||||||
}, bridge.usersLock)
|
}, bridge.usersLock)
|
||||||
|
|
||||||
return bridge.api.ReportBug(ctx, proton.ReportBugReq{
|
// if we have a token we can append more attachment to the bugReport
|
||||||
OS: osType,
|
for i, att := range attachments {
|
||||||
OSVersion: osVersion,
|
if i == 0 && report.IncludeLogs {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err := bridge.appendComment(ctx, token, att)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
Title: "[Bridge] Bug",
|
func (bridge *Bridge) CollectLogs() (proton.ReportBugAttachment, error) {
|
||||||
Description: description,
|
logsPath, err := bridge.locator.ProvideLogsPath()
|
||||||
|
if err != nil {
|
||||||
|
return proton.ReportBugAttachment{}, err
|
||||||
|
}
|
||||||
|
|
||||||
Client: client,
|
buffer, err := logging.ZipLogsForBugReport(logsPath, DefaultMaxSessionCountForBugReport, DefaultMaxBugReportZipSize)
|
||||||
|
if err != nil {
|
||||||
|
return proton.ReportBugAttachment{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(buffer)
|
||||||
|
if err != nil {
|
||||||
|
return proton.ReportBugAttachment{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return proton.ReportBugAttachment{
|
||||||
|
Name: "logs.zip",
|
||||||
|
Filename: "logs.zip",
|
||||||
|
MIMEType: "application/zip",
|
||||||
|
Body: body,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) createTicket(ctx context.Context, report *ReportBugReq,
|
||||||
|
asyncAttach proton.AttachmentType, att proton.ReportBugAttachment) (string, error) {
|
||||||
|
var attachments []proton.ReportBugAttachment
|
||||||
|
attachments = append(attachments, att)
|
||||||
|
res, err := bridge.api.ReportBug(ctx, proton.ReportBugReq{
|
||||||
|
OS: report.OSType,
|
||||||
|
OSVersion: report.OSVersion,
|
||||||
|
|
||||||
|
Title: "[Bridge] Bug - " + report.Title,
|
||||||
|
Description: report.Description,
|
||||||
|
|
||||||
|
Client: report.EmailClient,
|
||||||
ClientType: proton.ClientTypeEmail,
|
ClientType: proton.ClientTypeEmail,
|
||||||
ClientVersion: constants.AppVersion(bridge.curVersion.Original()),
|
ClientVersion: constants.AppVersion(bridge.curVersion.Original()),
|
||||||
|
|
||||||
Username: account,
|
Username: report.Username,
|
||||||
Email: email,
|
Email: report.Email,
|
||||||
}, attachment...)
|
|
||||||
|
AsyncAttachments: asyncAttach,
|
||||||
|
}, attachments...)
|
||||||
|
|
||||||
|
if err != nil || asyncAttach != proton.AttachmentTypeAsync {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if asyncAttach == proton.AttachmentTypeAsync && res.Token == nil {
|
||||||
|
return "", errors.New("no token returns for AsyncAttachments")
|
||||||
|
}
|
||||||
|
|
||||||
|
return *res.Token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) appendComment(ctx context.Context, token string, att proton.ReportBugAttachment) error {
|
||||||
|
var attachments []proton.ReportBugAttachment
|
||||||
|
attachments = append(attachments, att)
|
||||||
|
return bridge.api.ReportBugAttachement(ctx, proton.ReportBugAttachmentReq{
|
||||||
|
Product: proton.ClientTypeEmail,
|
||||||
|
Body: "Comment adding attachment: " + att.Filename,
|
||||||
|
Token: token,
|
||||||
|
}, attachments...)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,7 +22,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (bridge *Bridge) ReportBugClicked() {
|
func (bridge *Bridge) ReportBugClicked() {
|
||||||
safe.Lock(func() {
|
safe.RLock(func() {
|
||||||
for _, user := range bridge.users {
|
for _, user := range bridge.users {
|
||||||
user.ReportBugClicked()
|
user.ReportBugClicked()
|
||||||
}
|
}
|
||||||
@ -30,7 +30,7 @@ func (bridge *Bridge) ReportBugClicked() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) AutoconfigUsed(client string) {
|
func (bridge *Bridge) AutoconfigUsed(client string) {
|
||||||
safe.Lock(func() {
|
safe.RLock(func() {
|
||||||
for _, user := range bridge.users {
|
for _, user := range bridge.users {
|
||||||
user.AutoconfigUsed(client)
|
user.AutoconfigUsed(client)
|
||||||
}
|
}
|
||||||
@ -38,7 +38,7 @@ func (bridge *Bridge) AutoconfigUsed(client string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) KBArticleOpened(article string) {
|
func (bridge *Bridge) KBArticleOpened(article string) {
|
||||||
safe.Lock(func() {
|
safe.RLock(func() {
|
||||||
for _, user := range bridge.users {
|
for _, user := range bridge.users {
|
||||||
user.KBArticleOpened(article)
|
user.KBArticleOpened(article)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -79,7 +79,7 @@ func (bridge *Bridge) CheckClientState(ctx context.Context, checkFlags bool, pro
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.Debug("Building state")
|
log.Debug("Building state")
|
||||||
state, err := meta.BuildMailboxToMessageMap(usr)
|
state, err := meta.BuildMailboxToMessageMap(ctx, usr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Error("Failed to build state")
|
log.WithError(err).Error("Failed to build state")
|
||||||
return result, err
|
return result, err
|
||||||
@ -230,7 +230,7 @@ func (bridge *Bridge) DebugDownloadFailedMessages(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func clientGetMessageIDs(client *goimapclient.Client, mailbox string) (map[string]imap.FlagSet, error) {
|
func clientGetMessageIDs(client *goimapclient.Client, mailbox string) (map[string]imap.FlagSet, error) {
|
||||||
status, err := client.Select(mailbox, false)
|
status, err := client.Select(mailbox, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
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)
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
}
|
||||||
@ -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()
|
||||||
}
|
}
|
||||||
@ -318,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"
|
||||||
@ -35,6 +37,7 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||||
"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/services/imapservice"
|
||||||
"github.com/bradenaw/juniper/iterator"
|
"github.com/bradenaw/juniper/iterator"
|
||||||
"github.com/bradenaw/juniper/stream"
|
"github.com/bradenaw/juniper/stream"
|
||||||
"github.com/bradenaw/juniper/xslices"
|
"github.com/bradenaw/juniper/xslices"
|
||||||
@ -252,14 +255,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 +288,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 +300,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 +317,330 @@ 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 TestBridge_CorruptedVaultClearsPreviousIMAPSyncState(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||||
|
userID, addrID, err := s.CreateUser("imap", password)
|
||||||
|
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, 100)
|
||||||
|
})
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
|
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
||||||
|
defer done()
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
userID, err = bridge.LoginFull(context.Background(), "imap", password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Wait for sync to finish
|
||||||
|
require.Equal(t, userID, (<-syncCh).UserID)
|
||||||
|
})
|
||||||
|
|
||||||
|
settingsPath, err := locator.ProvideSettingsPath()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
syncConfigPath, err := locator.ProvideIMAPSyncConfigPath()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
syncStatePath := imapservice.GetSyncConfigPath(syncConfigPath, userID)
|
||||||
|
// Check sync state is complete
|
||||||
|
{
|
||||||
|
state, err := imapservice.NewSyncState(syncStatePath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
syncStatus, err := state.GetSyncStatus(context.Background())
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, syncStatus.IsComplete())
|
||||||
|
}
|
||||||
|
|
||||||
|
// corrupt the vault
|
||||||
|
require.NoError(t, os.WriteFile(filepath.Join(settingsPath, "vault.enc"), []byte("Trash!"), 0o600))
|
||||||
|
|
||||||
|
// Bridge starts but can't find the gluon database dir; there should be no error.
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
|
_, err := bridge.LoginFull(context.Background(), "imap", password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Check sync state is reset.
|
||||||
|
{
|
||||||
|
state, err := imapservice.NewSyncState(syncStatePath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
syncStatus, err := state.GetSyncStatus(context.Background())
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.False(t, syncStatus.IsComplete())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
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()),
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -32,15 +32,7 @@ type Locator interface {
|
|||||||
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"
|
||||||
@ -45,6 +46,8 @@ const (
|
|||||||
Connected
|
Connected
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var ErrFailedToUnlock = errors.New("failed to unlock user keys")
|
||||||
|
|
||||||
type UserInfo struct {
|
type UserInfo struct {
|
||||||
// UserID is the user's API ID.
|
// UserID is the user's API ID.
|
||||||
UserID string
|
UserID string
|
||||||
@ -65,10 +68,10 @@ type UserInfo struct {
|
|||||||
BridgePass []byte
|
BridgePass []byte
|
||||||
|
|
||||||
// UsedSpace is the amount of space used by the user.
|
// UsedSpace is the amount of space used by the user.
|
||||||
UsedSpace int
|
UsedSpace uint64
|
||||||
|
|
||||||
// MaxSpace is the total amount of space available to the user.
|
// MaxSpace is the total amount of space available to the user.
|
||||||
MaxSpace int
|
MaxSpace uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserIDs returns the IDs of all known users (authorized or not).
|
// GetUserIDs returns the IDs of all known users (authorized or not).
|
||||||
@ -156,11 +159,15 @@ func (bridge *Bridge) LoginUser(
|
|||||||
func() (string, error) {
|
func() (string, error) {
|
||||||
return bridge.loginUser(ctx, client, auth.UID, auth.RefreshToken, keyPass)
|
return bridge.loginUser(ctx, client, auth.UID, auth.RefreshToken, keyPass)
|
||||||
},
|
},
|
||||||
func() error {
|
|
||||||
return client.AuthDelete(ctx)
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
// Failure to unlock will allow retries, so we do not delete auth.
|
||||||
|
if !errors.Is(err, ErrFailedToUnlock) {
|
||||||
|
if deleteErr := client.AuthDelete(ctx); deleteErr != nil {
|
||||||
|
logrus.WithError(deleteErr).Error("Failed to delete auth")
|
||||||
|
}
|
||||||
|
}
|
||||||
return "", fmt.Errorf("failed to login user: %w", err)
|
return "", fmt.Errorf("failed to login user: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -216,7 +223,16 @@ func (bridge *Bridge) LoginFull(
|
|||||||
keyPass = password
|
keyPass = password
|
||||||
}
|
}
|
||||||
|
|
||||||
return bridge.LoginUser(ctx, client, auth, keyPass)
|
userID, err := bridge.LoginUser(ctx, client, auth, keyPass)
|
||||||
|
if err != nil {
|
||||||
|
if deleteErr := client.AuthDelete(ctx); deleteErr != nil {
|
||||||
|
logrus.WithError(err).Error("Failed to delete auth")
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return userID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// LogoutUser logs out the given user.
|
// LogoutUser logs out the given user.
|
||||||
@ -243,6 +259,11 @@ 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
|
||||||
@ -252,6 +273,10 @@ func (bridge *Bridge) DeleteUser(ctx context.Context, userID string) error {
|
|||||||
bridge.logoutUser(ctx, user, true, true, !bridge.GetTelemetryDisabled())
|
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 {
|
||||||
logrus.WithError(err).Error("Failed to delete vault user")
|
logrus.WithError(err).Error("Failed to delete vault user")
|
||||||
}
|
}
|
||||||
@ -278,18 +303,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,
|
||||||
@ -312,7 +329,7 @@ func (bridge *Bridge) SetAddressMode(ctx context.Context, userID string, mode va
|
|||||||
func (bridge *Bridge) SendBadEventUserFeedback(_ context.Context, userID string, doResync bool) error {
|
func (bridge *Bridge) SendBadEventUserFeedback(_ context.Context, userID string, doResync bool) error {
|
||||||
logrus.WithField("userID", userID).WithField("doResync", doResync).Info("Passing bad event feedback to user")
|
logrus.WithField("userID", userID).WithField("doResync", doResync).Info("Passing bad event feedback to user")
|
||||||
|
|
||||||
return safe.LockRet(func() error {
|
return safe.RLockRet(func() error {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
user, ok := bridge.users[userID]
|
user, ok := bridge.users[userID]
|
||||||
@ -335,13 +352,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(
|
||||||
@ -378,9 +389,9 @@ func (bridge *Bridge) loginUser(ctx context.Context, client *proton.Client, auth
|
|||||||
}
|
}
|
||||||
|
|
||||||
if userKR, err := apiUser.Keys.Unlock(saltedKeyPass, nil); err != nil {
|
if userKR, err := apiUser.Keys.Unlock(saltedKeyPass, nil); err != nil {
|
||||||
return "", fmt.Errorf("failed to unlock user keys: %w", err)
|
return "", fmt.Errorf("%w: %w", ErrFailedToUnlock, err)
|
||||||
} else if userKR.CountDecryptionEntities() == 0 {
|
} else if userKR.CountDecryptionEntities() == 0 {
|
||||||
return "", fmt.Errorf("failed to unlock user keys")
|
return "", ErrFailedToUnlock
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := bridge.addUser(ctx, client, apiUser, authUID, authRef, saltedKeyPass, true); err != nil {
|
if err := bridge.addUser(ctx, client, apiUser, authUID, authRef, saltedKeyPass, true); err != nil {
|
||||||
@ -524,6 +535,11 @@ func (bridge *Bridge) addUserWithVault(
|
|||||||
return fmt.Errorf("failed to get Statistics directory: %w", err)
|
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,
|
||||||
@ -535,16 +551,16 @@ func (bridge *Bridge) addUserWithVault(
|
|||||||
bridge.vault.GetMaxSyncMemory(),
|
bridge.vault.GetMaxSyncMemory(),
|
||||||
statsPath,
|
statsPath,
|
||||||
bridge,
|
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) {
|
||||||
@ -554,11 +570,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)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -609,10 +622,6 @@ func (bridge *Bridge) logoutUser(ctx context.Context, user *user.User, 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,102 +39,6 @@ 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) {
|
||||||
@ -171,8 +48,8 @@ func (bridge *Bridge) handleUserDeauth(ctx context.Context, user *user.User) {
|
|||||||
}, 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.RLock(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(),
|
||||||
"old_event_id": event.OldEventID,
|
"old_event_id": event.OldEventID,
|
||||||
@ -184,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -23,71 +23,200 @@ package certs
|
|||||||
#import <Foundation/Foundation.h>
|
#import <Foundation/Foundation.h>
|
||||||
#import <Security/Security.h>
|
#import <Security/Security.h>
|
||||||
|
|
||||||
|
// Memory management rules:
|
||||||
|
// Foundation object (Objective-C prefixed with `NS`) get ARC (Automatic Reference Counting), and do not need to be released manually.
|
||||||
|
// Core Foundation objects (C), prefixed with need to be released manually using CFRelease() unless:
|
||||||
|
// - They're obtained using a CF method containing the word Get (a.k.a. the Get Rule).
|
||||||
|
// - They're obtained using toll-free bridging from a Foundation Object (using the __bridge keyword).
|
||||||
|
|
||||||
int installTrustedCert(char const *bytes, unsigned long long length) {
|
//****************************************************************************************************************************************************
|
||||||
if (length == 0) {
|
/// \brief Create a certificate object from DER-encoded data.
|
||||||
return errSecInvalidData;
|
///
|
||||||
}
|
/// \return The certifcation. The caller is responsible for releasing the object using CFRelease.
|
||||||
|
/// \return NULL if data is not a valid DER-encoded certificate.
|
||||||
NSData *der = [NSData dataWithBytes:bytes length:length];
|
//****************************************************************************************************************************************************
|
||||||
|
SecCertificateRef certFromData(char const* data, uint64_t length) {
|
||||||
// Step 1. Import the certificate in the keychain.
|
NSData *der = [NSData dataWithBytes:data length:length];
|
||||||
SecCertificateRef cert = SecCertificateCreateWithData(NULL, (CFDataRef) der);
|
return SecCertificateCreateWithData(NULL, (__bridge CFDataRef)der);
|
||||||
NSDictionary* addQuery = @{
|
|
||||||
(id)kSecValueRef: (__bridge id) cert,
|
|
||||||
(id)kSecClass: (id)kSecClassCertificate,
|
|
||||||
};
|
|
||||||
|
|
||||||
OSStatus status = SecItemAdd((__bridge CFDictionaryRef) addQuery, NULL);
|
|
||||||
if ((errSecSuccess != status) && (errSecDuplicateItem != status)) {
|
|
||||||
CFRelease(cert);
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 2. Set the trust for the certificate.
|
|
||||||
SecPolicyRef policy = SecPolicyCreateSSL(true, NULL); // we limit our trust to SSL
|
|
||||||
NSDictionary *trustSettings = @{
|
|
||||||
(id)kSecTrustSettingsResult: [NSNumber numberWithInt:kSecTrustSettingsResultTrustRoot],
|
|
||||||
(id)kSecTrustSettingsPolicy: (__bridge id) policy,
|
|
||||||
};
|
|
||||||
status = SecTrustSettingsSetTrustSettings(cert, kSecTrustSettingsDomainUser, (__bridge CFTypeRef)(trustSettings));
|
|
||||||
CFRelease(policy);
|
|
||||||
CFRelease(cert);
|
|
||||||
|
|
||||||
return status;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
int removeTrustedCert(char const *bytes, unsigned long long length) {
|
//****************************************************************************************************************************************************
|
||||||
if (0 == length) {
|
/// \brief Check if a certificate is in the user's keychain.
|
||||||
return errSecInvalidData;
|
///
|
||||||
}
|
/// \param[in] cert The certificate.
|
||||||
|
/// \return true iff the certificate is in the user's keychain.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
bool _isCertificateInKeychain(SecCertificateRef const cert) {
|
||||||
|
NSDictionary *attrs = @{
|
||||||
|
(id)kSecMatchItemList: @[(__bridge id)cert],
|
||||||
|
(id)kSecClass: (id)kSecClassCertificate,
|
||||||
|
(id)kSecReturnData: @YES
|
||||||
|
};
|
||||||
|
return errSecSuccess == SecItemCopyMatching((__bridge CFDictionaryRef)attrs, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
NSData *der = [NSData dataWithBytes: bytes length: length];
|
//****************************************************************************************************************************************************
|
||||||
SecCertificateRef cert = SecCertificateCreateWithData(NULL, (CFDataRef) der);
|
/// \brief Check if a certificate is in the user's keychain.
|
||||||
|
///
|
||||||
|
/// \param[in] certData The certificate data in DER encoded format.
|
||||||
|
/// \param[in] certSize The size of the certData in bytes.
|
||||||
|
/// \return true iff the certificate is in the user's keychain.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
bool isCertificateInKeychain(char const* certData, uint64_t certSize) {
|
||||||
|
return _isCertificateInKeychain(certFromData(certData, certSize));
|
||||||
|
}
|
||||||
|
|
||||||
// Step 1. Unset the trust for the certificate.
|
|
||||||
SecPolicyRef policy = SecPolicyCreateSSL(true, NULL);
|
|
||||||
NSDictionary * trustSettings = @{
|
|
||||||
(id)kSecTrustSettingsResult: [NSNumber numberWithInt:kSecTrustSettingsResultUnspecified],
|
|
||||||
(id)kSecTrustSettingsPolicy: (__bridge id) policy,
|
|
||||||
};
|
|
||||||
OSStatus status = SecTrustSettingsSetTrustSettings(cert, kSecTrustSettingsDomainUser, (__bridge CFTypeRef)(trustSettings));
|
|
||||||
CFRelease(policy);
|
|
||||||
if (errSecSuccess != status) {
|
|
||||||
CFRelease(cert);
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 2. Remove the certificate from the keychain.
|
//****************************************************************************************************************************************************
|
||||||
NSDictionary *query = @{ (id)kSecClass: (id)kSecClassCertificate,
|
/// \brief Add a certificate to the user's keychain.
|
||||||
(id)kSecMatchItemList: @[(__bridge id)cert],
|
///
|
||||||
(id)kSecMatchLimit: (id)kSecMatchLimitOne,
|
/// \param[in] cert The certificate.
|
||||||
};
|
/// \return The status for the operation.
|
||||||
status = SecItemDelete((__bridge CFDictionaryRef) query);
|
//****************************************************************************************************************************************************
|
||||||
|
OSStatus _addCertificateToKeychain(SecCertificateRef const cert) {
|
||||||
|
NSDictionary* addQuery = @{
|
||||||
|
(id)kSecValueRef: (__bridge id) cert,
|
||||||
|
(id)kSecClass: (id)kSecClassCertificate,
|
||||||
|
};
|
||||||
|
return SecItemAdd((__bridge CFDictionaryRef) addQuery, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
CFRelease(cert);
|
//****************************************************************************************************************************************************
|
||||||
return status;
|
/// \brief Add a certificate to the user's keychain.
|
||||||
|
///
|
||||||
|
/// \param[in] certData The certificate data in DER encoded format.
|
||||||
|
/// \param[in] certSize The size of the certData in bytes.
|
||||||
|
/// \return The status for the operation.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
OSStatus addCertificateToKeychain(char const* certData, uint64_t certSize) {
|
||||||
|
return _addCertificateToKeychain(certFromData(certData, certSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \brief Add a certificate to the user's keychain.
|
||||||
|
///
|
||||||
|
/// \param[in] cert The certificate.
|
||||||
|
/// \return The status for the operation.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
OSStatus _removeCertificateFromKeychain(SecCertificateRef const cert) {
|
||||||
|
NSDictionary *query = @{ (id)kSecClass: (id)kSecClassCertificate,
|
||||||
|
(id)kSecMatchItemList: @[(__bridge id)cert],
|
||||||
|
(id)kSecMatchLimit: (id)kSecMatchLimitOne,
|
||||||
|
};
|
||||||
|
return SecItemDelete((__bridge CFDictionaryRef) query);
|
||||||
|
}
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \brief Add a certificate to the user's keychain.
|
||||||
|
///
|
||||||
|
/// \param[in] certData The certificate data in DER encoded format.
|
||||||
|
/// \param[in] certSize The size of the certData in bytes.
|
||||||
|
/// \return The status for the operation.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
OSStatus removeCertificateFromKeychain(char const* certData, uint64_t certSize) {
|
||||||
|
return _removeCertificateFromKeychain(certFromData(certData, certSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \brief Check if a certificate is trusted in the user's keychain.
|
||||||
|
///
|
||||||
|
/// \param[in] cert The certificate.
|
||||||
|
/// \return true iff the certificate is trusted in the user's keychain.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
bool _isCertificateTrusted(SecCertificateRef const cert) {
|
||||||
|
CFArrayRef trustSettings = NULL;
|
||||||
|
OSStatus status = SecTrustSettingsCopyTrustSettings(cert, kSecTrustSettingsDomainUser, &trustSettings);
|
||||||
|
if (status != errSecSuccess) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
CFIndex count = CFArrayGetCount(trustSettings);
|
||||||
|
bool result = false;
|
||||||
|
for (CFIndex index = 0; index < count; ++index) {
|
||||||
|
CFDictionaryRef dict = (CFDictionaryRef)CFArrayGetValueAtIndex(trustSettings, index);
|
||||||
|
if (!dict) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
CFNumberRef num = (CFNumberRef)CFDictionaryGetValue(dict, kSecTrustSettingsResult);
|
||||||
|
int value;
|
||||||
|
if (num && CFNumberGetValue(num, kCFNumberSInt32Type, &value) && (value == kSecTrustSettingsResultTrustRoot)) {
|
||||||
|
result = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CFRelease(trustSettings);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \brief Check if a certificate is trusted in the user's keychain.
|
||||||
|
///
|
||||||
|
/// \param[in] certData The certificate data in DER encoded format.
|
||||||
|
/// \param[in] certSize The size of the certData in bytes.
|
||||||
|
/// \return true iff the certificate is trusted in the user's keychain.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
bool isCertificateTrusted(char const* certData, uint64_t certSize) {
|
||||||
|
return _isCertificateTrusted(certFromData(certData, certSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \brief Set the trust level for a certificate in the user's keychain. This call will trigger a security prompt.
|
||||||
|
///
|
||||||
|
/// \param[in] cert The certificate.
|
||||||
|
/// \param[in] trustLevel The trust level.
|
||||||
|
/// \return The status for the operation.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
OSStatus _setCertificateTrustLevel(SecCertificateRef const cert, int trustLevel) {
|
||||||
|
SecPolicyRef policy = SecPolicyCreateSSL(true, NULL); // we limit our trust to SSL
|
||||||
|
NSDictionary *trustSettings = @{
|
||||||
|
(id)kSecTrustSettingsResult: [NSNumber numberWithInt:trustLevel],
|
||||||
|
(id)kSecTrustSettingsPolicy: (__bridge id) policy,
|
||||||
|
};
|
||||||
|
OSStatus status = SecTrustSettingsSetTrustSettings(cert, kSecTrustSettingsDomainUser, (__bridge CFTypeRef)(trustSettings));
|
||||||
|
CFRelease(policy);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \brief Set a certificate as trusted in the user's keychain. This call will trigger a security prompt.
|
||||||
|
///
|
||||||
|
/// \param[in] cert The certificate.
|
||||||
|
/// \return The status for the operation.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
OSStatus _setCertificateTrusted(SecCertificateRef cert) {
|
||||||
|
return _setCertificateTrustLevel(cert, kSecTrustSettingsResultTrustRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \brief Set a certificate as trusted in the user's keychain. This call will trigger a security prompt.
|
||||||
|
///
|
||||||
|
/// \param[in] certData The certificate data in DER encoded format.
|
||||||
|
/// \param[in] certSize The size of the certData in bytes.
|
||||||
|
/// \return The status for the operation.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
OSStatus setCertificateTrusted(char const* certData, uint64_t certSize) {
|
||||||
|
return _setCertificateTrusted(certFromData(certData, certSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \brief Remove the trust level of a certificate in the user's keychain.
|
||||||
|
///
|
||||||
|
/// \param[in] cert The certificate.
|
||||||
|
/// \return The status for the operation.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
OSStatus _removeCertificateTrust(SecCertificateRef cert) {
|
||||||
|
return _setCertificateTrustLevel(cert, kSecTrustSettingsResultUnspecified);
|
||||||
|
}
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \brief Remove the trust level of a certificate in the user's keychain.
|
||||||
|
///
|
||||||
|
/// \param[in] certData The certificate data in DER encoded format.
|
||||||
|
/// \param[in] certSize The size of the certData in bytes.
|
||||||
|
/// \return The status for the operation.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
OSStatus removeCertificateTrust(char const* certData, uint64_t certSize) {
|
||||||
|
return _removeCertificateTrust(certFromData(certData, certSize));
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
import "C"
|
import "C"
|
||||||
@ -119,6 +248,116 @@ func certPEMToDER(certPEM []byte) ([]byte, error) {
|
|||||||
return block.Bytes, nil
|
return block.Bytes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// wrapCGoCertCallReturningBool wrap call to a CGo function returning a bool.
|
||||||
|
// if the certificate is invalid the call will return false.
|
||||||
|
func wrapCGoCertCallReturningBool(certPEM []byte, fn func(*C.char, C.ulonglong) bool) bool {
|
||||||
|
certDER, err := certPEMToDER(certPEM)
|
||||||
|
if err != nil {
|
||||||
|
return false // error are ignored
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer := C.CBytes(certDER)
|
||||||
|
defer C.free(unsafe.Pointer(buffer)) //nolint:unconvert
|
||||||
|
|
||||||
|
return fn((*C.char)(buffer), C.ulonglong(len(certDER)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// wrapCGoCertCallReturningBool wrap call to a CGo function returning an error
|
||||||
|
func wrapCGoCertCallReturningError(certPEM []byte, fn func(*C.char, C.ulonglong) error) error {
|
||||||
|
certDER, err := certPEMToDER(certPEM)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer := C.CBytes(certDER)
|
||||||
|
defer C.free(unsafe.Pointer(buffer)) //nolint:unconvert
|
||||||
|
|
||||||
|
return fn((*C.char)(buffer), C.ulonglong(len(certDER)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// isCertInKeychain returns true if the given certificate is stored in the user's keychain.
|
||||||
|
func isCertInKeychain(certPEM []byte) bool {
|
||||||
|
return wrapCGoCertCallReturningBool(certPEM, isCertInKeychainCGo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isCertInKeychainCGo(buffer *C.char, size C.ulonglong) bool {
|
||||||
|
return bool(C.isCertificateInKeychain(buffer, size))
|
||||||
|
}
|
||||||
|
|
||||||
|
// addCertToKeychain adds a certificate to the user's keychain.
|
||||||
|
// Trying to add a certificate that is already in the keychain will result in an error.
|
||||||
|
func addCertToKeychain(certPEM []byte) error {
|
||||||
|
return wrapCGoCertCallReturningError(certPEM, addCertToKeychainCGo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func addCertToKeychainCGo(buffer *C.char, size C.ulonglong) error {
|
||||||
|
if errCode := C.addCertificateToKeychain(buffer, size); errCode != errSecSuccess {
|
||||||
|
return fmt.Errorf("could not add certificate to keychain (error %v)", errCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeCertFromKeychain removes a certificate from the user's keychain.
|
||||||
|
// Trying to remove a certificate that is not in the keychain will result in an error.
|
||||||
|
func removeCertFromKeychain(certPEM []byte) error {
|
||||||
|
return wrapCGoCertCallReturningError(certPEM, removeCertFromKeychainCGo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeCertFromKeychainCGo(buffer *C.char, size C.ulonglong) error {
|
||||||
|
if errCode := C.removeCertificateFromKeychain(buffer, size); errCode != errSecSuccess {
|
||||||
|
return fmt.Errorf("could not remove certificate from keychain (error %v)", errCode)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isCertTrusted check if a certificate is trusted in the user's keychain.
|
||||||
|
func isCertTrusted(certPEM []byte) bool {
|
||||||
|
return wrapCGoCertCallReturningBool(certPEM, isCertTrustedCGo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isCertTrustedCGo(buffer *C.char, size C.ulonglong) bool {
|
||||||
|
return bool(C.isCertificateTrusted(buffer, size))
|
||||||
|
}
|
||||||
|
|
||||||
|
// setCertTrusted sets a certificate as trusted in the user's keychain.
|
||||||
|
// This function will trigger a security prompt from the system.
|
||||||
|
func setCertTrusted(certPEM []byte) error {
|
||||||
|
return wrapCGoCertCallReturningError(certPEM, setCertTrustedCGo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setCertTrustedCGo(buffer *C.char, size C.ulonglong) error {
|
||||||
|
errCode := C.setCertificateTrusted(buffer, size)
|
||||||
|
switch errCode {
|
||||||
|
case errSecSuccess:
|
||||||
|
return nil
|
||||||
|
case errAuthorizationCanceled:
|
||||||
|
return ErrUserCanceledCertificateInstall
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("could not set certificate trust in keychain (error %v)", errCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeCertTrust remove the trust level of the certificated from the user's keychain.
|
||||||
|
// This function will trigger a security prompt from the system.
|
||||||
|
func removeCertTrust(certPEM []byte) error {
|
||||||
|
return wrapCGoCertCallReturningError(certPEM, removeCertTrustCGo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeCertTrustCGo(buffer *C.char, size C.ulonglong) error {
|
||||||
|
errCode := C.removeCertificateTrust(buffer, size)
|
||||||
|
switch errCode {
|
||||||
|
case errSecSuccess:
|
||||||
|
return nil
|
||||||
|
case errAuthorizationCanceled:
|
||||||
|
return ErrUserCanceledCertificateInstall
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("could not set certificate trust in keychain (error %v)", errCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// installCert installs a certificate in the keychain. The certificate is added to the keychain and it is set as trusted.
|
||||||
|
// This function will trigger a security prompt from the system, unless the certificate is already trusted in the user keychain.
|
||||||
func installCert(certPEM []byte) error {
|
func installCert(certPEM []byte) error {
|
||||||
certDER, err := certPEMToDER(certPEM)
|
certDER, err := certPEMToDER(certPEM)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -127,18 +366,24 @@ func installCert(certPEM []byte) error {
|
|||||||
|
|
||||||
p := C.CBytes(certDER)
|
p := C.CBytes(certDER)
|
||||||
defer C.free(unsafe.Pointer(p)) //nolint:unconvert
|
defer C.free(unsafe.Pointer(p)) //nolint:unconvert
|
||||||
|
buffer := (*C.char)(p)
|
||||||
|
size := C.ulonglong(len(certDER))
|
||||||
|
|
||||||
errCode := C.installTrustedCert((*C.char)(p), (C.ulonglong)(len(certDER)))
|
if !isCertInKeychainCGo(buffer, size) {
|
||||||
switch errCode {
|
if err := addCertToKeychainCGo(buffer, size); err != nil {
|
||||||
case errSecSuccess:
|
return err
|
||||||
return nil
|
}
|
||||||
case errAuthorizationCanceled:
|
|
||||||
return fmt.Errorf("the user cancelled the authorization dialog")
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("could not install certification into keychain (error %v)", errCode)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !isCertTrustedCGo(buffer, size) {
|
||||||
|
return setCertTrustedCGo(buffer, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// uninstallCert uninstalls a certificate in the keychain. The certificate trust is removed and the certificated is deleted from the keychain.
|
||||||
|
// This function will trigger a security prompt from the system, unless the certificate is not trusted in the user keychain.
|
||||||
func uninstallCert(certPEM []byte) error {
|
func uninstallCert(certPEM []byte) error {
|
||||||
certDER, err := certPEMToDER(certPEM)
|
certDER, err := certPEMToDER(certPEM)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -147,10 +392,32 @@ func uninstallCert(certPEM []byte) error {
|
|||||||
|
|
||||||
p := C.CBytes(certDER)
|
p := C.CBytes(certDER)
|
||||||
defer C.free(unsafe.Pointer(p)) //nolint:unconvert
|
defer C.free(unsafe.Pointer(p)) //nolint:unconvert
|
||||||
|
buffer := (*C.char)(p)
|
||||||
|
size := C.ulonglong(len(certDER))
|
||||||
|
|
||||||
if errCode := C.removeTrustedCert((*C.char)(p), (C.ulonglong)(len(certDER))); errCode != 0 {
|
if isCertTrustedCGo(buffer, size) {
|
||||||
return fmt.Errorf("could not install certificate from keychain (error %v)", errCode)
|
if err := removeCertTrustCGo(buffer, size); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if isCertInKeychainCGo(buffer, size) {
|
||||||
|
return removeCertFromKeychainCGo(buffer, size)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isCertInstalled(certPEM []byte) bool {
|
||||||
|
certDER, err := certPEMToDER(certPEM)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
p := C.CBytes(certDER)
|
||||||
|
defer C.free(unsafe.Pointer(p)) //nolint:unconvert
|
||||||
|
buffer := (*C.char)(p)
|
||||||
|
size := C.ulonglong(len(certDER))
|
||||||
|
|
||||||
|
return isCertInKeychainCGo(buffer, size) && isCertTrustedCGo(buffer, size)
|
||||||
|
}
|
||||||
|
|||||||
@ -25,20 +25,73 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
// This test implies human interactions to enter password and is disabled by default.
|
func TestCertInKeychain(t *testing.T) {
|
||||||
func _TestTrustedCertsDarwin(t *testing.T) { //nolint:unused
|
// no trust settings change is performed, so this test will not trigger an OS security prompt.
|
||||||
|
certPEM := generatePEMCertificate(t)
|
||||||
|
require.False(t, isCertInKeychain(certPEM))
|
||||||
|
require.NoError(t, addCertToKeychain(certPEM))
|
||||||
|
require.True(t, isCertInKeychain(certPEM))
|
||||||
|
require.Error(t, addCertToKeychain(certPEM))
|
||||||
|
require.True(t, isCertInKeychain(certPEM))
|
||||||
|
require.NoError(t, removeCertFromKeychain(certPEM))
|
||||||
|
require.False(t, isCertInKeychain(certPEM))
|
||||||
|
require.Error(t, removeCertFromKeychain(certPEM))
|
||||||
|
require.False(t, isCertInKeychain(certPEM))
|
||||||
|
}
|
||||||
|
|
||||||
|
// This test require human interaction (macOS security prompts), and is disabled by default.
|
||||||
|
func _TestCertificateTrust(t *testing.T) { //nolint:unused
|
||||||
|
certPEM := generatePEMCertificate(t)
|
||||||
|
require.False(t, isCertTrusted(certPEM))
|
||||||
|
require.NoError(t, addCertToKeychain(certPEM))
|
||||||
|
require.NoError(t, setCertTrusted(certPEM))
|
||||||
|
require.True(t, isCertTrusted(certPEM))
|
||||||
|
require.NoError(t, removeCertTrust(certPEM))
|
||||||
|
require.False(t, isCertTrusted(certPEM))
|
||||||
|
require.NoError(t, removeCertFromKeychain(certPEM))
|
||||||
|
}
|
||||||
|
|
||||||
|
// This test require human interaction (macOS security prompts), and is disabled by default.
|
||||||
|
func _TestInstallAndRemove(t *testing.T) { //nolint:unused
|
||||||
|
certPEM := generatePEMCertificate(t)
|
||||||
|
|
||||||
|
// fresh install
|
||||||
|
require.False(t, isCertInstalled(certPEM))
|
||||||
|
require.NoError(t, installCert(certPEM))
|
||||||
|
require.True(t, isCertInKeychain(certPEM))
|
||||||
|
require.True(t, isCertTrusted(certPEM))
|
||||||
|
require.True(t, isCertInstalled(certPEM))
|
||||||
|
require.NoError(t, uninstallCert(certPEM))
|
||||||
|
require.False(t, isCertInKeychain(certPEM))
|
||||||
|
require.False(t, isCertTrusted(certPEM))
|
||||||
|
require.False(t, isCertInstalled(certPEM))
|
||||||
|
|
||||||
|
// Install where certificate is already in Keychain, but not trusted.
|
||||||
|
require.NoError(t, addCertToKeychain(certPEM))
|
||||||
|
require.False(t, isCertInstalled(certPEM))
|
||||||
|
require.NoError(t, installCert(certPEM))
|
||||||
|
require.True(t, isCertInstalled(certPEM))
|
||||||
|
|
||||||
|
// Install where certificate is already installed
|
||||||
|
require.NoError(t, installCert(certPEM))
|
||||||
|
|
||||||
|
// Remove when certificate is not trusted.
|
||||||
|
require.NoError(t, removeCertTrust(certPEM))
|
||||||
|
require.NoError(t, uninstallCert(certPEM))
|
||||||
|
require.False(t, isCertInstalled(certPEM))
|
||||||
|
|
||||||
|
// Remove when certificate has already been removed.
|
||||||
|
require.NoError(t, uninstallCert(certPEM))
|
||||||
|
require.False(t, isCertTrusted(certPEM))
|
||||||
|
require.False(t, isCertInKeychain(certPEM))
|
||||||
|
}
|
||||||
|
|
||||||
|
func generatePEMCertificate(t *testing.T) []byte {
|
||||||
template, err := NewTLSTemplate()
|
template, err := NewTLSTemplate()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
certPEM, _, err := GenerateCert(template)
|
certPEM, _, err := GenerateCert(template)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
require.Error(t, installCert([]byte{0})) // Cannot install an invalid cert.
|
return certPEM
|
||||||
require.Error(t, uninstallCert(certPEM)) // Cannot uninstall a cert that is not installed.
|
|
||||||
require.NoError(t, installCert(certPEM)) // Can install a valid cert.
|
|
||||||
require.NoError(t, installCert(certPEM)) // Can install an already installed cert.
|
|
||||||
require.NoError(t, uninstallCert(certPEM)) // Can uninstall an installed cert.
|
|
||||||
require.Error(t, uninstallCert(certPEM)) // Cannot uninstall an already uninstalled cert.
|
|
||||||
require.NoError(t, installCert(certPEM)) // Can reinstall an uninstalled cert.
|
|
||||||
require.NoError(t, uninstallCert(certPEM)) // Can uninstall a reinstalled cert.
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,3 +24,7 @@ func installCert([]byte) error {
|
|||||||
func uninstallCert([]byte) error {
|
func uninstallCert([]byte) error {
|
||||||
return nil // Linux doesn't have a root cert store.
|
return nil // Linux doesn't have a root cert store.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isCertInstalled([]byte) bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@ -24,3 +24,7 @@ func installCert([]byte) error {
|
|||||||
func uninstallCert([]byte) error {
|
func uninstallCert([]byte) error {
|
||||||
return nil // NOTE(GODT-986): Uninstall certs from root cert store?
|
return nil // NOTE(GODT-986): Uninstall certs from root cert store?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isCertInstalled([]byte) bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@ -17,16 +17,50 @@
|
|||||||
|
|
||||||
package certs
|
package certs
|
||||||
|
|
||||||
type Installer struct{}
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrUserCanceledCertificateInstall = errors.New("the user cancelled the authorization dialog")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Installer struct {
|
||||||
|
log *logrus.Entry
|
||||||
|
}
|
||||||
|
|
||||||
func NewInstaller() *Installer {
|
func NewInstaller() *Installer {
|
||||||
return &Installer{}
|
return &Installer{
|
||||||
|
log: logrus.WithField("pkg", "certs"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (installer *Installer) InstallCert(certPEM []byte) error {
|
func (installer *Installer) InstallCert(certPEM []byte) error {
|
||||||
return installCert(certPEM)
|
installer.log.Info("Installing the Bridge TLS certificate in the OS keychain")
|
||||||
|
|
||||||
|
if err := installCert(certPEM); err != nil {
|
||||||
|
installer.log.WithError(err).Error("The Bridge TLS certificate could not be installed in the OS keychain")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
installer.log.Info("The Bridge TLS certificate was successfully installed in the OS keychain")
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (installer *Installer) UninstallCert(certPEM []byte) error {
|
func (installer *Installer) UninstallCert(certPEM []byte) error {
|
||||||
return uninstallCert(certPEM)
|
installer.log.Info("Uninstalling the Bridge TLS certificate from the OS keychain")
|
||||||
|
|
||||||
|
if err := uninstallCert(certPEM); err != nil {
|
||||||
|
installer.log.WithError(err).Error("The Bridge TLS certificate could not be uninstalled from the OS keychain")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
installer.log.Info("The Bridge TLS certificate was successfully uninstalled from the OS keychain")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (installer *Installer) IsCertInstalled(certPEM []byte) bool {
|
||||||
|
return isCertInstalled(certPEM)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -95,6 +95,13 @@ func (status *ConfigurationStatus) IsPending() bool {
|
|||||||
return !status.Data.DataV1.PendingSince.IsZero()
|
return !status.Data.DataV1.PendingSince.IsZero()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (status *ConfigurationStatus) isPendingSinceMin() int {
|
||||||
|
if min := int(time.Since(status.Data.DataV1.PendingSince).Minutes()); min > 0 {
|
||||||
|
return min
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
func (status *ConfigurationStatus) IsFromFailure() bool {
|
func (status *ConfigurationStatus) IsFromFailure() bool {
|
||||||
status.DataLock.RLock()
|
status.DataLock.RLock()
|
||||||
defer status.DataLock.RUnlock()
|
defer status.DataLock.RUnlock()
|
||||||
|
|||||||
@ -19,7 +19,6 @@ package configstatus
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ConfigAbortValues struct {
|
type ConfigAbortValues struct {
|
||||||
@ -41,17 +40,20 @@ type ConfigAbortData struct {
|
|||||||
|
|
||||||
type ConfigAbortBuilder struct{}
|
type ConfigAbortBuilder struct{}
|
||||||
|
|
||||||
func (*ConfigAbortBuilder) New(data *ConfigurationStatusData) ConfigAbortData {
|
func (*ConfigAbortBuilder) New(config *ConfigurationStatus) ConfigAbortData {
|
||||||
|
config.DataLock.RLock()
|
||||||
|
defer config.DataLock.RUnlock()
|
||||||
|
|
||||||
return ConfigAbortData{
|
return ConfigAbortData{
|
||||||
MeasurementGroup: "bridge.any.configuration",
|
MeasurementGroup: "bridge.any.configuration",
|
||||||
Event: "bridge_config_abort",
|
Event: "bridge_config_abort",
|
||||||
Values: ConfigSuccessValues{
|
Values: ConfigSuccessValues{
|
||||||
Duration: int(time.Since(data.DataV1.PendingSince).Minutes()),
|
Duration: config.isPendingSinceMin(),
|
||||||
},
|
},
|
||||||
Dimensions: ConfigSuccessDimensions{
|
Dimensions: ConfigSuccessDimensions{
|
||||||
ReportClick: strconv.FormatBool(data.DataV1.ReportClick),
|
ReportClick: strconv.FormatBool(config.Data.DataV1.ReportClick),
|
||||||
ReportSent: strconv.FormatBool(data.DataV1.ReportSent),
|
ReportSent: strconv.FormatBool(config.Data.DataV1.ReportSent),
|
||||||
ClickedLink: data.clickedLinkToString(),
|
ClickedLink: config.Data.clickedLinkToString(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,7 +33,7 @@ func TestConfigurationAbort_default(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var builder = configstatus.ConfigAbortBuilder{}
|
var builder = configstatus.ConfigAbortBuilder{}
|
||||||
req := builder.New(config.Data)
|
req := builder.New(config)
|
||||||
|
|
||||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||||
require.Equal(t, "bridge_config_abort", req.Event)
|
require.Equal(t, "bridge_config_abort", req.Event)
|
||||||
@ -64,7 +64,7 @@ func TestConfigurationAbort_fed(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var builder = configstatus.ConfigAbortBuilder{}
|
var builder = configstatus.ConfigAbortBuilder{}
|
||||||
req := builder.New(config.Data)
|
req := builder.New(config)
|
||||||
|
|
||||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||||
require.Equal(t, "bridge_config_abort", req.Event)
|
require.Equal(t, "bridge_config_abort", req.Event)
|
||||||
|
|||||||
@ -33,13 +33,16 @@ type ConfigProgressData struct {
|
|||||||
|
|
||||||
type ConfigProgressBuilder struct{}
|
type ConfigProgressBuilder struct{}
|
||||||
|
|
||||||
func (*ConfigProgressBuilder) New(data *ConfigurationStatusData) ConfigProgressData {
|
func (*ConfigProgressBuilder) New(config *ConfigurationStatus) ConfigProgressData {
|
||||||
|
config.DataLock.RLock()
|
||||||
|
defer config.DataLock.RUnlock()
|
||||||
|
|
||||||
return ConfigProgressData{
|
return ConfigProgressData{
|
||||||
MeasurementGroup: "bridge.any.configuration",
|
MeasurementGroup: "bridge.any.configuration",
|
||||||
Event: "bridge_config_progress",
|
Event: "bridge_config_progress",
|
||||||
Values: ConfigProgressValues{
|
Values: ConfigProgressValues{
|
||||||
NbDay: numberOfDay(time.Now(), data.DataV1.PendingSince),
|
NbDay: numberOfDay(time.Now(), config.Data.DataV1.PendingSince),
|
||||||
NbDaySinceLast: numberOfDay(time.Now(), data.DataV1.LastProgress),
|
NbDaySinceLast: numberOfDay(time.Now(), config.Data.DataV1.LastProgress),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,7 +33,7 @@ func TestConfigurationProgress_default(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var builder = configstatus.ConfigProgressBuilder{}
|
var builder = configstatus.ConfigProgressBuilder{}
|
||||||
req := builder.New(config.Data)
|
req := builder.New(config)
|
||||||
|
|
||||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||||
require.Equal(t, "bridge_config_progress", req.Event)
|
require.Equal(t, "bridge_config_progress", req.Event)
|
||||||
@ -62,7 +62,7 @@ func TestConfigurationProgress_fed(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var builder = configstatus.ConfigProgressBuilder{}
|
var builder = configstatus.ConfigProgressBuilder{}
|
||||||
req := builder.New(config.Data)
|
req := builder.New(config)
|
||||||
|
|
||||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||||
require.Equal(t, "bridge_config_progress", req.Event)
|
require.Equal(t, "bridge_config_progress", req.Event)
|
||||||
|
|||||||
@ -19,7 +19,6 @@ package configstatus
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ConfigRecoveryValues struct {
|
type ConfigRecoveryValues struct {
|
||||||
@ -43,19 +42,22 @@ type ConfigRecoveryData struct {
|
|||||||
|
|
||||||
type ConfigRecoveryBuilder struct{}
|
type ConfigRecoveryBuilder struct{}
|
||||||
|
|
||||||
func (*ConfigRecoveryBuilder) New(data *ConfigurationStatusData) ConfigRecoveryData {
|
func (*ConfigRecoveryBuilder) New(config *ConfigurationStatus) ConfigRecoveryData {
|
||||||
|
config.DataLock.RLock()
|
||||||
|
defer config.DataLock.RUnlock()
|
||||||
|
|
||||||
return ConfigRecoveryData{
|
return ConfigRecoveryData{
|
||||||
MeasurementGroup: "bridge.any.configuration",
|
MeasurementGroup: "bridge.any.configuration",
|
||||||
Event: "bridge_config_recovery",
|
Event: "bridge_config_recovery",
|
||||||
Values: ConfigRecoveryValues{
|
Values: ConfigRecoveryValues{
|
||||||
Duration: int(time.Since(data.DataV1.PendingSince).Minutes()),
|
Duration: config.isPendingSinceMin(),
|
||||||
},
|
},
|
||||||
Dimensions: ConfigRecoveryDimensions{
|
Dimensions: ConfigRecoveryDimensions{
|
||||||
Autoconf: data.DataV1.Autoconf,
|
Autoconf: config.Data.DataV1.Autoconf,
|
||||||
ReportClick: strconv.FormatBool(data.DataV1.ReportClick),
|
ReportClick: strconv.FormatBool(config.Data.DataV1.ReportClick),
|
||||||
ReportSent: strconv.FormatBool(data.DataV1.ReportSent),
|
ReportSent: strconv.FormatBool(config.Data.DataV1.ReportSent),
|
||||||
ClickedLink: data.clickedLinkToString(),
|
ClickedLink: config.Data.clickedLinkToString(),
|
||||||
FailureDetails: data.DataV1.FailureDetails,
|
FailureDetails: config.Data.DataV1.FailureDetails,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,7 +33,7 @@ func TestConfigurationRecovery_default(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var builder = configstatus.ConfigRecoveryBuilder{}
|
var builder = configstatus.ConfigRecoveryBuilder{}
|
||||||
req := builder.New(config.Data)
|
req := builder.New(config)
|
||||||
|
|
||||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||||
require.Equal(t, "bridge_config_recovery", req.Event)
|
require.Equal(t, "bridge_config_recovery", req.Event)
|
||||||
@ -66,7 +66,7 @@ func TestConfigurationRecovery_fed(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var builder = configstatus.ConfigRecoveryBuilder{}
|
var builder = configstatus.ConfigRecoveryBuilder{}
|
||||||
req := builder.New(config.Data)
|
req := builder.New(config)
|
||||||
|
|
||||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||||
require.Equal(t, "bridge_config_recovery", req.Event)
|
require.Equal(t, "bridge_config_recovery", req.Event)
|
||||||
|
|||||||
@ -19,7 +19,6 @@ package configstatus
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ConfigSuccessValues struct {
|
type ConfigSuccessValues struct {
|
||||||
@ -42,18 +41,21 @@ type ConfigSuccessData struct {
|
|||||||
|
|
||||||
type ConfigSuccessBuilder struct{}
|
type ConfigSuccessBuilder struct{}
|
||||||
|
|
||||||
func (*ConfigSuccessBuilder) New(data *ConfigurationStatusData) ConfigSuccessData {
|
func (*ConfigSuccessBuilder) New(config *ConfigurationStatus) ConfigSuccessData {
|
||||||
|
config.DataLock.RLock()
|
||||||
|
defer config.DataLock.RUnlock()
|
||||||
|
|
||||||
return ConfigSuccessData{
|
return ConfigSuccessData{
|
||||||
MeasurementGroup: "bridge.any.configuration",
|
MeasurementGroup: "bridge.any.configuration",
|
||||||
Event: "bridge_config_success",
|
Event: "bridge_config_success",
|
||||||
Values: ConfigSuccessValues{
|
Values: ConfigSuccessValues{
|
||||||
Duration: int(time.Since(data.DataV1.PendingSince).Minutes()),
|
Duration: config.isPendingSinceMin(),
|
||||||
},
|
},
|
||||||
Dimensions: ConfigSuccessDimensions{
|
Dimensions: ConfigSuccessDimensions{
|
||||||
Autoconf: data.DataV1.Autoconf,
|
Autoconf: config.Data.DataV1.Autoconf,
|
||||||
ReportClick: strconv.FormatBool(data.DataV1.ReportClick),
|
ReportClick: strconv.FormatBool(config.Data.DataV1.ReportClick),
|
||||||
ReportSent: strconv.FormatBool(data.DataV1.ReportSent),
|
ReportSent: strconv.FormatBool(config.Data.DataV1.ReportSent),
|
||||||
ClickedLink: data.clickedLinkToString(),
|
ClickedLink: config.Data.clickedLinkToString(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,7 +33,7 @@ func TestConfigurationSuccess_default(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var builder = configstatus.ConfigSuccessBuilder{}
|
var builder = configstatus.ConfigSuccessBuilder{}
|
||||||
req := builder.New(config.Data)
|
req := builder.New(config)
|
||||||
|
|
||||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||||
require.Equal(t, "bridge_config_success", req.Event)
|
require.Equal(t, "bridge_config_success", req.Event)
|
||||||
@ -65,7 +65,7 @@ func TestConfigurationSuccess_fed(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var builder = configstatus.ConfigSuccessBuilder{}
|
var builder = configstatus.ConfigSuccessBuilder{}
|
||||||
req := builder.New(config.Data)
|
req := builder.New(config)
|
||||||
|
|
||||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||||
require.Equal(t, "bridge_config_success", req.Event)
|
require.Equal(t, "bridge_config_success", req.Event)
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -175,7 +175,7 @@ type UsedSpaceChanged struct {
|
|||||||
|
|
||||||
UserID string
|
UserID string
|
||||||
|
|
||||||
UsedSpace int
|
UsedSpace uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (event UsedSpaceChanged) String() string {
|
func (event UsedSpaceChanged) String() 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/>.
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,6 +42,7 @@ void GRPCQtProxy::connectSignals() {
|
|||||||
connect(this, &GRPCQtProxy::setIsTelemetryDisabledReceived, &settingsTab, &SettingsTab::setIsTelemetryDisabled);
|
connect(this, &GRPCQtProxy::setIsTelemetryDisabledReceived, &settingsTab, &SettingsTab::setIsTelemetryDisabled);
|
||||||
connect(this, &GRPCQtProxy::setColorSchemeNameReceived, &settingsTab, &SettingsTab::setColorSchemeName);
|
connect(this, &GRPCQtProxy::setColorSchemeNameReceived, &settingsTab, &SettingsTab::setColorSchemeName);
|
||||||
connect(this, &GRPCQtProxy::reportBugReceived, &settingsTab, &SettingsTab::setBugReport);
|
connect(this, &GRPCQtProxy::reportBugReceived, &settingsTab, &SettingsTab::setBugReport);
|
||||||
|
connect(this, &GRPCQtProxy::installTLSCertificateReceived, &settingsTab, &SettingsTab::installTLSCertificate);
|
||||||
connect(this, &GRPCQtProxy::exportTLSCertificatesReceived, &settingsTab, &SettingsTab::exportTLSCertificates);
|
connect(this, &GRPCQtProxy::exportTLSCertificatesReceived, &settingsTab, &SettingsTab::exportTLSCertificates);
|
||||||
connect(this, &GRPCQtProxy::setIsStreamingReceived, &settingsTab, &SettingsTab::setIsStreaming);
|
connect(this, &GRPCQtProxy::setIsStreamingReceived, &settingsTab, &SettingsTab::setIsStreaming);
|
||||||
connect(this, &GRPCQtProxy::setClientPlatformReceived, &settingsTab, &SettingsTab::setClientPlatform);
|
connect(this, &GRPCQtProxy::setClientPlatformReceived, &settingsTab, &SettingsTab::setClientPlatform);
|
||||||
@ -119,6 +120,13 @@ void GRPCQtProxy::reportBug(QString const &osType, QString const &osVersion, QSt
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
//
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
void GRPCQtProxy::installTLSCertificate() {
|
||||||
|
emit installTLSCertificateReceived();
|
||||||
|
}
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \param[in] folderPath The folder path.
|
/// \param[in] folderPath The folder path.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
|
|||||||
@ -45,6 +45,7 @@ public: // member functions.
|
|||||||
void setColorSchemeName(QString const &name); ///< Forward a SetColorSchemeName call via a Qt Signal
|
void setColorSchemeName(QString const &name); ///< Forward a SetColorSchemeName call via a Qt Signal
|
||||||
void reportBug(QString const &osType, QString const &osVersion, QString const &emailClient, QString const &address,
|
void reportBug(QString const &osType, QString const &osVersion, QString const &emailClient, QString const &address,
|
||||||
QString const &description, bool includeLogs); ///< Forwards a ReportBug call via a Qt signal.
|
QString const &description, bool includeLogs); ///< Forwards a ReportBug call via a Qt signal.
|
||||||
|
void installTLSCertificate(); ///< Forwards a InstallTLScertificate call via a Qt signal.
|
||||||
void exportTLSCertificates(QString const &folderPath); //< Forward an 'ExportTLSCertificates' call via a Qt signal.
|
void exportTLSCertificates(QString const &folderPath); //< Forward an 'ExportTLSCertificates' call via a Qt signal.
|
||||||
void setIsStreaming(bool isStreaming); ///< Forward a isStreaming internal messages via a Qt signal.
|
void setIsStreaming(bool isStreaming); ///< Forward a isStreaming internal messages via a Qt signal.
|
||||||
void setClientPlatform(QString const &clientPlatform); ///< Forward a setClientPlatform call via a Qt signal.
|
void setClientPlatform(QString const &clientPlatform); ///< Forward a setClientPlatform call via a Qt signal.
|
||||||
@ -67,6 +68,7 @@ signals:
|
|||||||
void setColorSchemeNameReceived(QString const &name); ///< Forward a SetColorScheme call via a Qt Signal
|
void setColorSchemeNameReceived(QString const &name); ///< Forward a SetColorScheme call via a Qt Signal
|
||||||
void reportBugReceived(QString const &osType, QString const &osVersion, QString const &emailClient, QString const &address,
|
void reportBugReceived(QString const &osType, QString const &osVersion, QString const &emailClient, QString const &address,
|
||||||
QString const &description, bool includeLogs); ///< Signal for the ReportBug gRPC call
|
QString const &description, bool includeLogs); ///< Signal for the ReportBug gRPC call
|
||||||
|
void installTLSCertificateReceived(); ///< Signal for the InstallTLSCertificate gRPC call.
|
||||||
void exportTLSCertificatesReceived(QString const &folderPath); ///< Signal for the ExportTLSCertificates gRPC call.
|
void exportTLSCertificatesReceived(QString const &folderPath); ///< Signal for the ExportTLSCertificates gRPC call.
|
||||||
void setIsStreamingReceived(bool isStreaming); ///< Signal for the IsStreaming internal message.
|
void setIsStreamingReceived(bool isStreaming); ///< Signal for the IsStreaming internal message.
|
||||||
void setClientPlatformReceived(QString const &clientPlatform); ///< Signal for the SetClientPlatform gRPC call.
|
void setClientPlatformReceived(QString const &clientPlatform); ///< Signal for the SetClientPlatform gRPC call.
|
||||||
|
|||||||
@ -214,6 +214,16 @@ grpc::Status GRPCService::IsTelemetryDisabled(::grpc::ServerContext *, ::google:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \param[out] response The response.
|
||||||
|
/// \return The status for the call.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
Status GRPCService::GoOs(ServerContext *, Empty const*, StringValue *response) {
|
||||||
|
response->set_value(app().mainWindow().settingsTab().os().toStdString());
|
||||||
|
return Status::OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \return The status for the call.
|
/// \return The status for the call.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
@ -361,22 +371,6 @@ Status GRPCService::ReportBug(ServerContext *, ReportBugRequest const *request,
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
/// \param[in] request The request
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
Status GRPCService::ExportTLSCertificates(ServerContext *, StringValue const *request, Empty *response) {
|
|
||||||
SettingsTab &tab = app().mainWindow().settingsTab();
|
|
||||||
if (!tab.nextTLSCertExportWillSucceed()) {
|
|
||||||
qtProxy_.sendDelayedEvent(newGenericErrorEvent(grpc::TLS_CERT_EXPORT_ERROR));
|
|
||||||
}
|
|
||||||
if (!tab.nextTLSKeyExportWillSucceed()) {
|
|
||||||
qtProxy_.sendDelayedEvent(newGenericErrorEvent(grpc::TLS_KEY_EXPORT_ERROR));
|
|
||||||
}
|
|
||||||
qtProxy_.exportTLSCertificates(QString::fromStdString(request->value()));
|
|
||||||
return Status::OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \param[in] request The request.
|
/// \param[in] request The request.
|
||||||
/// \return The status for the call.
|
/// \return The status for the call.
|
||||||
@ -385,6 +379,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;
|
||||||
@ -398,7 +400,7 @@ Status GRPCService::Login(ServerContext *, LoginRequest const *request, Empty *)
|
|||||||
return Status::OK;
|
return Status::OK;
|
||||||
}
|
}
|
||||||
if (usersTab.nextUserTwoPasswordsRequired()) {
|
if (usersTab.nextUserTwoPasswordsRequired()) {
|
||||||
qtProxy_.sendDelayedEvent(newLoginTwoPasswordsRequestedEvent());
|
qtProxy_.sendDelayedEvent(newLoginTwoPasswordsRequestedEvent(loginUsername_));
|
||||||
return Status::OK;
|
return Status::OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -423,7 +425,7 @@ Status GRPCService::Login2FA(ServerContext *, LoginRequest const *request, Empty
|
|||||||
return Status::OK;
|
return Status::OK;
|
||||||
}
|
}
|
||||||
if (usersTab.nextUserTwoPasswordsRequired()) {
|
if (usersTab.nextUserTwoPasswordsRequired()) {
|
||||||
qtProxy_.sendDelayedEvent(newLoginTwoPasswordsRequestedEvent());
|
qtProxy_.sendDelayedEvent(newLoginTwoPasswordsRequestedEvent(loginUsername_));
|
||||||
return Status::OK;
|
return Status::OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -750,9 +752,86 @@ Status GRPCService::ConfigureUserAppleMail(ServerContext *, ConfigureAppleMailRe
|
|||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \param[in] request The request
|
/// \param[in] request The request
|
||||||
/// \param[in] writer The writer
|
|
||||||
/// \return The status for the call.
|
/// \return The status for the call.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
|
Status GRPCService::ExportTLSCertificates(ServerContext *, StringValue const *request, Empty *response) {
|
||||||
|
app().log().debug(__FUNCTION__);
|
||||||
|
SettingsTab &tab = app().mainWindow().settingsTab();
|
||||||
|
if (!tab.nextTLSCertExportWillSucceed()) {
|
||||||
|
qtProxy_.sendDelayedEvent(newGenericErrorEvent(grpc::TLS_CERT_EXPORT_ERROR));
|
||||||
|
}
|
||||||
|
if (!tab.nextTLSKeyExportWillSucceed()) {
|
||||||
|
qtProxy_.sendDelayedEvent(newGenericErrorEvent(grpc::TLS_KEY_EXPORT_ERROR));
|
||||||
|
}
|
||||||
|
qtProxy_.exportTLSCertificates(QString::fromStdString(request->value()));
|
||||||
|
return Status::OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \param[in] response The reponse.
|
||||||
|
/// \return The status for the call.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
Status GRPCService::IsTLSCertificateInstalled(ServerContext *, const Empty *request, BoolValue *response) {
|
||||||
|
app().log().debug(__FUNCTION__);
|
||||||
|
response->set_value(app().mainWindow().settingsTab().isTLSCertificateInstalled());
|
||||||
|
return Status::OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
//
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
Status GRPCService::InstallTLSCertificate(ServerContext *, Empty const *, Empty *) {
|
||||||
|
app().log().debug(__FUNCTION__);
|
||||||
|
SPStreamEvent event;
|
||||||
|
qtProxy_.installTLSCertificate();
|
||||||
|
switch (app().mainWindow().settingsTab().nextTLSCertIntallResult()) {
|
||||||
|
case SettingsTab::TLSCertInstallResult::Success:
|
||||||
|
event = newCertificateInstallSuccessEvent();
|
||||||
|
break;
|
||||||
|
case SettingsTab::TLSCertInstallResult::Canceled:
|
||||||
|
event = newCertificateInstallCanceledEvent();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
event = newCertificateInstallFailedEvent();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
qtProxy_.sendDelayedEvent(event);
|
||||||
|
return Status::OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \param[in] request The request.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
Status GRPCService::KBArticleClicked(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) {
|
||||||
|
app().log().debug(QString("%1 - URL = %2").arg(__FUNCTION__, QString::fromStdString(request->value())));
|
||||||
|
return Status::OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
//
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
Status GRPCService::ReportBugClicked(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::Empty *) {
|
||||||
|
app().log().debug(__FUNCTION__);
|
||||||
|
return Status::OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \param[in] request The request.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
Status GRPCService::AutoconfigClicked(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *response) {
|
||||||
|
app().log().debug(QString("%1 - Client = %2").arg(__FUNCTION__, QString::fromStdString(request->value())));
|
||||||
|
return Status::OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \param[in] request The request
|
||||||
|
/// \param[in] writer The writer
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
Status GRPCService::RunEventStream(ServerContext *ctx, EventStreamRequest const *request, ServerWriter<StreamEvent> *writer) {
|
Status GRPCService::RunEventStream(ServerContext *ctx, EventStreamRequest const *request, ServerWriter<StreamEvent> *writer) {
|
||||||
app().log().debug(__FUNCTION__);
|
app().log().debug(__FUNCTION__);
|
||||||
{
|
{
|
||||||
@ -826,7 +905,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();
|
||||||
@ -842,4 +921,3 @@ void GRPCService::finishLogin() {
|
|||||||
|
|
||||||
qtProxy_.sendDelayedEvent(newLoginFinishedEvent(user->id(), alreadyExist));
|
qtProxy_.sendDelayedEvent(newLoginFinishedEvent(user->id(), alreadyExist));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -53,6 +53,7 @@ public: // member functions.
|
|||||||
grpc::Status IsAllMailVisible(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::google::protobuf::BoolValue *response) override;
|
grpc::Status IsAllMailVisible(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::google::protobuf::BoolValue *response) override;
|
||||||
grpc::Status SetIsTelemetryDisabled(::grpc::ServerContext *, ::google::protobuf::BoolValue const *request, ::google::protobuf::Empty *response) override;
|
grpc::Status SetIsTelemetryDisabled(::grpc::ServerContext *, ::google::protobuf::BoolValue const *request, ::google::protobuf::Empty *response) override;
|
||||||
grpc::Status IsTelemetryDisabled(::grpc::ServerContext *, ::google::protobuf::Empty const *request, ::google::protobuf::BoolValue *response) override;
|
grpc::Status IsTelemetryDisabled(::grpc::ServerContext *, ::google::protobuf::Empty const *request, ::google::protobuf::BoolValue *response) override;
|
||||||
|
grpc::Status GoOs(::grpc::ServerContext *, ::google::protobuf::Empty const*, ::google::protobuf::StringValue *response) override;
|
||||||
grpc::Status TriggerReset(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::Empty *) override;
|
grpc::Status TriggerReset(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::Empty *) override;
|
||||||
grpc::Status Version(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::StringValue *response) override;
|
grpc::Status Version(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::StringValue *response) override;
|
||||||
grpc::Status LogsPath(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::StringValue *response) override;
|
grpc::Status LogsPath(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::StringValue *response) override;
|
||||||
@ -64,7 +65,6 @@ public: // member functions.
|
|||||||
grpc::Status ColorSchemeName(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::StringValue *response) override;
|
grpc::Status ColorSchemeName(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::StringValue *response) override;
|
||||||
grpc::Status CurrentEmailClient(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::StringValue *response) override;
|
grpc::Status CurrentEmailClient(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::StringValue *response) override;
|
||||||
grpc::Status ReportBug(::grpc::ServerContext *, ::grpc::ReportBugRequest const *request, ::google::protobuf::Empty *) override;
|
grpc::Status ReportBug(::grpc::ServerContext *, ::grpc::ReportBugRequest const *request, ::google::protobuf::Empty *) override;
|
||||||
grpc::Status ExportTLSCertificates(::grpc::ServerContext *context, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *response) override;
|
|
||||||
grpc::Status ForceLauncher(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
|
grpc::Status ForceLauncher(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
|
||||||
grpc::Status SetMainExecutable(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
|
grpc::Status SetMainExecutable(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
|
||||||
grpc::Status Login(::grpc::ServerContext *, ::grpc::LoginRequest const *request, ::google::protobuf::Empty *) override;
|
grpc::Status Login(::grpc::ServerContext *, ::grpc::LoginRequest const *request, ::google::protobuf::Empty *) override;
|
||||||
@ -93,6 +93,12 @@ public: // member functions.
|
|||||||
grpc::Status LogoutUser(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
|
grpc::Status LogoutUser(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
|
||||||
grpc::Status RemoveUser(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
|
grpc::Status RemoveUser(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
|
||||||
grpc::Status ConfigureUserAppleMail(::grpc::ServerContext *, ::grpc::ConfigureAppleMailRequest const *request, ::google::protobuf::Empty *) override;
|
grpc::Status ConfigureUserAppleMail(::grpc::ServerContext *, ::grpc::ConfigureAppleMailRequest const *request, ::google::protobuf::Empty *) override;
|
||||||
|
grpc::Status IsTLSCertificateInstalled(::grpc::ServerContext *, ::google::protobuf::Empty const*, ::google::protobuf::BoolValue *response) override;
|
||||||
|
grpc::Status InstallTLSCertificate(::grpc::ServerContext *, ::google::protobuf::Empty const*, ::google::protobuf::Empty *) override;
|
||||||
|
grpc::Status ExportTLSCertificates(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
|
||||||
|
grpc::Status ReportBugClicked(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::google::protobuf::Empty *) override;
|
||||||
|
grpc::Status AutoconfigClicked(::grpc::ServerContext *context, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
|
||||||
|
grpc::Status KBArticleClicked(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
|
||||||
grpc::Status RunEventStream(::grpc::ServerContext *ctx, ::grpc::EventStreamRequest const *request, ::grpc::ServerWriter<::grpc::StreamEvent> *writer) override;
|
grpc::Status RunEventStream(::grpc::ServerContext *ctx, ::grpc::EventStreamRequest const *request, ::grpc::ServerWriter<::grpc::StreamEvent> *writer) override;
|
||||||
grpc::Status StopEventStream(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::Empty *) override;
|
grpc::Status StopEventStream(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::Empty *) override;
|
||||||
bool sendEvent(bridgepp::SPStreamEvent const &event); ///< Queue an event for sending through the event stream.
|
bool sendEvent(bridgepp::SPStreamEvent const &event); ///< Queue an event for sending through the event stream.
|
||||||
|
|||||||
@ -285,11 +285,20 @@ void SettingsTab::setBugReport(QString const &osType, QString const &osVersion,
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
//
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
void SettingsTab::installTLSCertificate() {
|
||||||
|
ui_.labelLastTLSCertInstall->setText(QString("Last install: %1").arg(QDateTime::currentDateTime().toString(Qt::ISODateWithMs)));
|
||||||
|
ui_.checkTLSCertIsInstalled->setChecked(this->nextTLSCertIntallResult() == TLSCertInstallResult::Success);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \param[in] folderPath The folder path.
|
/// \param[in] folderPath The folder path.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
void SettingsTab::exportTLSCertificates(QString const &folderPath) {
|
void SettingsTab::exportTLSCertificates(QString const &folderPath) {
|
||||||
ui_.labeLastTLSCertsExport->setText(QString("%1 Export to %2")
|
ui_.labeLastTLSCertExport->setText(QString("%1 Export to %2")
|
||||||
.arg(QDateTime::currentDateTime().toString(Qt::ISODateWithMs))
|
.arg(QDateTime::currentDateTime().toString(Qt::ISODateWithMs))
|
||||||
.arg(folderPath));
|
.arg(folderPath));
|
||||||
}
|
}
|
||||||
@ -303,6 +312,22 @@ bool SettingsTab::nextBugReportWillSucceed() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \return the state of the 'TLS Certificate is installed' check box.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
bool SettingsTab::isTLSCertificateInstalled() const {
|
||||||
|
return ui_.checkTLSCertIsInstalled->isChecked();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \return The value for the 'Next TLS cert install result'.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
SettingsTab::TLSCertInstallResult SettingsTab::nextTLSCertIntallResult() const {
|
||||||
|
return TLSCertInstallResult(ui_.comboNextTLSCertInstallResult->currentIndex());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \return true if the 'Next TLS key export will succeed' check box is checked
|
/// \return true if the 'Next TLS key export will succeed' check box is checked
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
@ -505,4 +530,11 @@ void SettingsTab::resetUI() {
|
|||||||
ui_.comboCacheError->setCurrentIndex(0);
|
ui_.comboCacheError->setCurrentIndex(0);
|
||||||
|
|
||||||
ui_.checkAutomaticUpdate->setChecked(true);
|
ui_.checkAutomaticUpdate->setChecked(true);
|
||||||
|
|
||||||
|
ui_.checkTLSCertIsInstalled->setChecked(false);
|
||||||
|
ui_.comboNextTLSCertInstallResult->setCurrentIndex(0);
|
||||||
|
ui_.checkTLSCertExportWillSucceed->setChecked(true);
|
||||||
|
ui_.checkTLSKeyExportWillSucceed->setChecked(true);
|
||||||
|
ui_.labeLastTLSCertExport->setText("Last export: never");
|
||||||
|
ui_.labelLastTLSCertInstall->setText("Last install: never");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,6 +28,13 @@
|
|||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
class SettingsTab : public QWidget {
|
class SettingsTab : public QWidget {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
public: // data types.
|
||||||
|
enum class TLSCertInstallResult {
|
||||||
|
Success = 0,
|
||||||
|
Canceled = 1,
|
||||||
|
Failure = 2
|
||||||
|
}; ///< Enumberation for the result of a TLS certificate installation.
|
||||||
|
|
||||||
public: // member functions.
|
public: // member functions.
|
||||||
explicit SettingsTab(QWidget *parent = nullptr); ///< Default constructor.
|
explicit SettingsTab(QWidget *parent = nullptr); ///< Default constructor.
|
||||||
SettingsTab(SettingsTab const &) = delete; ///< Disabled copy-constructor.
|
SettingsTab(SettingsTab const &) = delete; ///< Disabled copy-constructor.
|
||||||
@ -54,6 +61,8 @@ public: // member functions.
|
|||||||
QString dependencyLicenseLink() const; ///< Get the content of the 'Dependency License Link' edit.
|
QString dependencyLicenseLink() const; ///< Get the content of the 'Dependency License Link' edit.
|
||||||
QString landingPageLink() const; ///< Get the content of the 'Landing Page Link' edit.
|
QString landingPageLink() const; ///< Get the content of the 'Landing Page Link' edit.
|
||||||
bool nextBugReportWillSucceed() const; ///< Get the status of the 'Next Bug Report Will Fail' check box.
|
bool nextBugReportWillSucceed() const; ///< Get the status of the 'Next Bug Report Will Fail' check box.
|
||||||
|
bool isTLSCertificateInstalled() const; ///< Get the status of the 'TLS Certificate is installed' check box.
|
||||||
|
TLSCertInstallResult nextTLSCertIntallResult() const; ///< Get the value of the 'Next TLS Certificate install result' combo box.
|
||||||
bool nextTLSCertExportWillSucceed() const; ///< Get the status of the 'Next TLS Cert export will succeed' check box.
|
bool nextTLSCertExportWillSucceed() const; ///< Get the status of the 'Next TLS Cert export will succeed' check box.
|
||||||
bool nextTLSKeyExportWillSucceed() const; ///< Get the status of the 'Next TLS Key export will succeed' check box.
|
bool nextTLSKeyExportWillSucceed() const; ///< Get the status of the 'Next TLS Key export will succeed' check box.
|
||||||
QString hostname() const; ///< Get the value of the 'Hostname' edit.
|
QString hostname() const; ///< Get the value of the 'Hostname' edit.
|
||||||
@ -79,6 +88,7 @@ public slots:
|
|||||||
void setColorSchemeName(QString const &name); ///< Set the value for the 'Use Dark Theme' check box.
|
void setColorSchemeName(QString const &name); ///< Set the value for the 'Use Dark Theme' check box.
|
||||||
void setBugReport(QString const &osType, QString const &osVersion, QString const &emailClient, QString const &address, QString const &description,
|
void setBugReport(QString const &osType, QString const &osVersion, QString const &emailClient, QString const &address, QString const &description,
|
||||||
bool includeLogs); ///< Set the content of the bug report box.
|
bool includeLogs); ///< Set the content of the bug report box.
|
||||||
|
void installTLSCertificate(); ///< Install the TLS certificate.
|
||||||
void exportTLSCertificates(QString const &folderPath); ///< Export the TLS certificates.
|
void exportTLSCertificates(QString const &folderPath); ///< Export the TLS certificates.
|
||||||
void setMailServerSettings(qint32 imapPort, qint32 smtpPort, bool useSSLForIMAP, bool useSSLForSMTP); ///< Change the mail server settings.
|
void setMailServerSettings(qint32 imapPort, qint32 smtpPort, bool useSSLForIMAP, bool useSSLForSMTP); ///< Change the mail server settings.
|
||||||
void setIsDoHEnabled(bool enabled); ///< Set the value for the 'DoH Enabled' check box.
|
void setIsDoHEnabled(bool enabled); ///< Set the value for the 'DoH Enabled' check box.
|
||||||
|
|||||||
@ -370,7 +370,7 @@
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QGroupBox" name="groupCache_2">
|
<widget class="QGroupBox" name="groupCert">
|
||||||
<property name="minimumSize">
|
<property name="minimumSize">
|
||||||
<size>
|
<size>
|
||||||
<width>0</width>
|
<width>0</width>
|
||||||
@ -380,34 +380,81 @@
|
|||||||
<property name="title">
|
<property name="title">
|
||||||
<string>TLS Certficates</string>
|
<string>TLS Certficates</string>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_9">
|
<layout class="QGridLayout" name="gridLayout_4" columnstretch="1,1">
|
||||||
<item>
|
<item row="0" column="0">
|
||||||
<widget class="QLabel" name="labeLastTLSCertsExport">
|
<widget class="QCheckBox" name="checkTLSCertIsInstalled">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Last Export: Never</string>
|
<string>Certificate is installed</string>
|
||||||
|
</property>
|
||||||
|
<property name="checked">
|
||||||
|
<bool>false</bool>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item row="0" column="1">
|
||||||
<widget class="QCheckBox" name="checkTLSCertExportWillSucceed">
|
<widget class="QCheckBox" name="checkTLSCertExportWillSucceed">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>TLS certificate export will succeed</string>
|
<string>Certificate export will succeed</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="checked">
|
<property name="checked">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item row="1" column="1">
|
||||||
<widget class="QCheckBox" name="checkTLSKeyExportWillSucceed">
|
<widget class="QCheckBox" name="checkTLSKeyExportWillSucceed">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>TLS private key export will succeed</string>
|
<string>Key export will succeed</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="checked">
|
<property name="checked">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="2" column="1">
|
||||||
|
<widget class="QLabel" name="labeLastTLSCertExport">
|
||||||
|
<property name="text">
|
||||||
|
<string>Last Export: never</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0">
|
||||||
|
<widget class="QLabel" name="labelLastTLSCertInstall">
|
||||||
|
<property name="text">
|
||||||
|
<string>Last install: never</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0">
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_14" stretch="0,1">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="labelNextInstall">
|
||||||
|
<property name="text">
|
||||||
|
<string>Next install will</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QComboBox" name="comboNextTLSCertInstallResult">
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>Succeed</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>Be Canceled</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>Fail</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
|||||||
@ -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).
|
||||||
|
|||||||
@ -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,19 +102,23 @@ 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -37,6 +37,16 @@
|
|||||||
using namespace bridgepp;
|
using namespace bridgepp;
|
||||||
|
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
|
||||||
|
QString const bugReportFile = ":qml/Resources/bug_report_flow.json";
|
||||||
|
QString const bridgeKBUrl = "https://proton.me/support/bridge"; ///< The URL for the root of the bridge knowledge base.
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
//
|
//
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
@ -89,6 +99,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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -210,6 +222,87 @@ 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 true iff the Bridge TLS certificate is installed.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
bool QMLBackend::isTLSCertificateInstalled() {
|
||||||
|
HANDLE_EXCEPTION_RETURN_BOOL(
|
||||||
|
bool v = false;
|
||||||
|
app().grpc().isTLSCertificateInstalled(v);
|
||||||
|
return v;
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \param[in] url The URL of the knowledge base article. If empty/invalid, the home page for the Bridge knowledge base is opened.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
void QMLBackend::openKBArticle(QString const &url) {
|
||||||
|
HANDLE_EXCEPTION(
|
||||||
|
QString const u = url.isEmpty() ? bridgeKBUrl : url;
|
||||||
|
QDesktopServices::openUrl(u);
|
||||||
|
emit notifyKBArticleClicked(u);
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \return The value for the 'showOnStartup' property.
|
/// \return The value for the 'showOnStartup' property.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
@ -581,6 +674,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.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
@ -845,18 +953,28 @@ 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);
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
//
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
void QMLBackend::installTLSCertificate() {
|
||||||
|
HANDLE_EXCEPTION(
|
||||||
|
app().grpc().installTLSCertificate();
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
//
|
//
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
@ -1181,7 +1299,11 @@ void QMLBackend::connectGrpcEvents() {
|
|||||||
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::certificateInstallSuccess, this, &QMLBackend::certificateInstallSuccess);
|
||||||
|
connect(client, &GRPCClient::certificateInstallCanceled, this, &QMLBackend::certificateInstallCanceled);
|
||||||
|
connect(client, &GRPCClient::certificateInstallFailed, this, &QMLBackend::certificateInstallFailed);
|
||||||
connect(client, &GRPCClient::showMainWindow, [&]() { this->showMainWindow("gRPC showMainWindow event"); });
|
connect(client, &GRPCClient::showMainWindow, [&]() { this->showMainWindow("gRPC showMainWindow event"); });
|
||||||
|
|
||||||
// cache events
|
// cache events
|
||||||
|
|||||||
@ -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>
|
||||||
@ -51,12 +52,20 @@ public: // member functions.
|
|||||||
void showSettings(QString const &reason); ///< Show the settings 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).
|
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.
|
||||||
|
Q_INVOKABLE bool isTLSCertificateInstalled(); ///< Check if the bridge certificate is installed in the OS keychain.
|
||||||
|
Q_INVOKABLE void openKBArticle(QString const & url = QString()); ///< Open a knowledge base article.
|
||||||
|
|
||||||
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)
|
||||||
@ -87,6 +96,8 @@ 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)
|
||||||
|
|
||||||
@ -124,6 +135,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.
|
||||||
|
|
||||||
@ -153,6 +166,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.
|
||||||
@ -181,7 +196,8 @@ 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 installTLSCertificate(); ///< Installs the Bridge TLS certificate in the Keychain.
|
||||||
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.
|
||||||
@ -218,7 +234,7 @@ signals: // Signals received from the Go backend, to be forwarded to QML
|
|||||||
void login2FARequested(QString const &username); ///< Signal for the 'login2FARequested' gRPC stream event.
|
void login2FARequested(QString const &username); ///< Signal for the 'login2FARequested' gRPC stream event.
|
||||||
void login2FAError(QString const &errorMsg); ///< Signal for the 'login2FAError' gRPC stream event.
|
void login2FAError(QString const &errorMsg); ///< Signal for the 'login2FAError' gRPC stream event.
|
||||||
void login2FAErrorAbort(QString const &errorMsg); ///< Signal for the 'login2FAErrorAbort' gRPC stream event.
|
void login2FAErrorAbort(QString const &errorMsg); ///< Signal for the 'login2FAErrorAbort' gRPC stream event.
|
||||||
void login2PasswordRequested(); ///< Signal for the 'login2PasswordRequested' gRPC stream event.
|
void login2PasswordRequested(QString const &username); ///< Signal for the 'login2PasswordRequested' gRPC stream event.
|
||||||
void login2PasswordError(QString const &errorMsg); ///< Signal for the 'login2PasswordError' gRPC stream event.
|
void login2PasswordError(QString const &errorMsg); ///< Signal for the 'login2PasswordError' gRPC stream event.
|
||||||
void login2PasswordErrorAbort(QString const &errorMsg); ///< Signal for the 'login2PasswordErrorAbort' gRPC stream event.
|
void login2PasswordErrorAbort(QString const &errorMsg); ///< Signal for the 'login2PasswordErrorAbort' gRPC stream event.
|
||||||
void loginFinished(int index, bool wasSignedOut); ///< Signal for the 'loginFinished' gRPC stream event.
|
void loginFinished(int index, bool wasSignedOut); ///< Signal for the 'loginFinished' gRPC stream event.
|
||||||
@ -253,11 +269,15 @@ 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 certificateInstallSuccess(); ///< Signal for the 'certificateInstallSuccess' gRPC stream event.
|
||||||
|
void certificateInstallCanceled(); ///< Signal for the 'certificateInstallCanceled' gRPC stream event.
|
||||||
|
void certificateInstallFailed(); /// Signal for the 'certificateInstallFailed' 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.
|
||||||
void showHelp(); ///< Signal for the 'showHelp' event (from the context menu).
|
void showHelp(); ///< Signal for the 'showHelp' event (from the context menu).
|
||||||
void showSettings(); ///< Signal for the 'showHelp' event (from the context menu).
|
void showSettings(); ///< Signal for the 'showSettings' event (from the context menu).
|
||||||
void selectUser(QString const& userID, bool forceShowWindow); ///< Signal emitted in order to selected a user with a given ID in the list.
|
void selectUser(QString const& userID, bool forceShowWindow); ///< Signal emitted in order to selected a user with a given ID in the list.
|
||||||
void genericError(QString const &title, QString const &description); ///< Signal for the 'genericError' gRPC stream event.
|
void genericError(QString const &title, QString const &description); ///< Signal for the 'genericError' gRPC stream event.
|
||||||
void imapLoginWhileSignedOut(QString const& username); ///< Signal for the notification of IMAP login attempt on a signed out account.
|
void imapLoginWhileSignedOut(QString const& username); ///< Signal for the notification of IMAP login attempt on a signed out account.
|
||||||
@ -284,6 +304,7 @@ private: // data members
|
|||||||
bool isInternetOn_ { true }; ///< Does bridge consider internet as on?
|
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_; ///< The tray icon for the application.
|
std::unique_ptr<TrayIcon> trayIcon_; ///< The tray icon for the application.
|
||||||
|
bridgepp::BugReportFlow reportFlow_; ///< The bug report flow.
|
||||||
friend class AppController;
|
friend class AppController;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -5,8 +5,11 @@
|
|||||||
<file>qml/AccountView.qml</file>
|
<file>qml/AccountView.qml</file>
|
||||||
<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/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>
|
||||||
@ -16,9 +19,11 @@
|
|||||||
<file>qml/icons/ic-alert.svg</file>
|
<file>qml/icons/ic-alert.svg</file>
|
||||||
<file>qml/icons/ic-apple-mail.svg</file>
|
<file>qml/icons/ic-apple-mail.svg</file>
|
||||||
<file>qml/icons/ic-arrow-left.svg</file>
|
<file>qml/icons/ic-arrow-left.svg</file>
|
||||||
|
<file>qml/icons/ic-bridge.svg</file>
|
||||||
<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-left.svg</file>
|
||||||
<file>qml/icons/ic-chevron-right.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>
|
||||||
@ -32,6 +37,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>
|
||||||
@ -44,13 +50,18 @@
|
|||||||
<file>qml/icons/ic-success.svg</file>
|
<file>qml/icons/ic-success.svg</file>
|
||||||
<file>qml/icons/ic-three-dots-vertical.svg</file>
|
<file>qml/icons/ic-three-dots-vertical.svg</file>
|
||||||
<file>qml/icons/ic-trash.svg</file>
|
<file>qml/icons/ic-trash.svg</file>
|
||||||
|
<file>qml/icons/ic-warning-orange.svg</file>
|
||||||
|
<file>qml/icons/img-client-config-selector.svg</file>
|
||||||
|
<file>qml/icons/img-client-config-success.svg</file>
|
||||||
|
<file>qml/icons/img-macos-cert-screenshot.png</file>
|
||||||
|
<file>qml/icons/img-macos-profile-screenshot.png</file>
|
||||||
|
<file>qml/icons/img-mail-clients.svg</file>
|
||||||
|
<file>qml/icons/img-mail-logo-wordmark-dark.svg</file>
|
||||||
|
<file>qml/icons/img-mail-logo-wordmark.svg</file>
|
||||||
<file>qml/icons/img-proton-logos.png</file>
|
<file>qml/icons/img-proton-logos.png</file>
|
||||||
<file>qml/icons/img-proton-logos.svg</file>
|
<file>qml/icons/img-proton-logos.svg</file>
|
||||||
<file>qml/icons/img-splash.png</file>
|
<file>qml/icons/img-splash.png</file>
|
||||||
<file>qml/icons/img-splash.svg</file>
|
<file>qml/icons/img-splash.svg</file>
|
||||||
<file>qml/icons/img-welcome-dark.png</file>
|
|
||||||
<file>qml/icons/img-welcome-dark.svg</file>
|
|
||||||
<file>qml/icons/img-welcome.png</file>
|
|
||||||
<file>qml/icons/img-welcome.svg</file>
|
<file>qml/icons/img-welcome.svg</file>
|
||||||
<file>qml/icons/Loader_16.svg</file>
|
<file>qml/icons/Loader_16.svg</file>
|
||||||
<file>qml/icons/Loader_48.svg</file>
|
<file>qml/icons/Loader_48.svg</file>
|
||||||
@ -70,6 +81,7 @@
|
|||||||
<file>qml/KeychainSettings.qml</file>
|
<file>qml/KeychainSettings.qml</file>
|
||||||
<file>qml/LocalCacheSettings.qml</file>
|
<file>qml/LocalCacheSettings.qml</file>
|
||||||
<file>qml/MainWindow.qml</file>
|
<file>qml/MainWindow.qml</file>
|
||||||
|
<file>qml/NoAccountView.qml</file>
|
||||||
<file>qml/NotificationDialog.qml</file>
|
<file>qml/NotificationDialog.qml</file>
|
||||||
<file>qml/NotificationPopups.qml</file>
|
<file>qml/NotificationPopups.qml</file>
|
||||||
<file>qml/Notifications/Notification.qml</file>
|
<file>qml/Notifications/Notification.qml</file>
|
||||||
@ -85,6 +97,7 @@
|
|||||||
<file>qml/Proton/ComboBox.qml</file>
|
<file>qml/Proton/ComboBox.qml</file>
|
||||||
<file>qml/Proton/Dialog.qml</file>
|
<file>qml/Proton/Dialog.qml</file>
|
||||||
<file>qml/Proton/Label.qml</file>
|
<file>qml/Proton/Label.qml</file>
|
||||||
|
<file>qml/Proton/LinkLabel.qml</file>
|
||||||
<file>qml/Proton/Menu.qml</file>
|
<file>qml/Proton/Menu.qml</file>
|
||||||
<file>qml/Proton/MenuItem.qml</file>
|
<file>qml/Proton/MenuItem.qml</file>
|
||||||
<file>qml/Proton/Popup.qml</file>
|
<file>qml/Proton/Popup.qml</file>
|
||||||
@ -95,13 +108,27 @@
|
|||||||
<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/Resources/Help/Template.html</file>
|
||||||
|
<file>qml/Resources/Help/WhyBridge.html</file>
|
||||||
|
<file>qml/Resources/Help/WhyCertificate.html</file>
|
||||||
|
<file>qml/Resources/Help/WhyProfileWarning.html</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/SetupWizard/ClientListItem.qml</file>
|
||||||
<file>qml/SignIn.qml</file>
|
<file>qml/SetupWizard/LeftPane.qml</file>
|
||||||
|
<file>qml/SetupWizard/ClientConfigAppleMail.qml</file>
|
||||||
|
<file>qml/SetupWizard/ClientConfigEnd.qml</file>
|
||||||
|
<file>qml/SetupWizard/ClientConfigParameters.qml</file>
|
||||||
|
<file>qml/SetupWizard/ClientConfigSelector.qml</file>
|
||||||
|
<file>qml/SetupWizard/HelpButton.qml</file>
|
||||||
|
<file>qml/SetupWizard/SetupWizard.qml</file>
|
||||||
|
<file>qml/SetupWizard/Login.qml</file>
|
||||||
|
<file>qml/SetupWizard/Onboarding.qml</file>
|
||||||
|
<file>qml/SetupWizard/StepDescriptionBox.qml</file>
|
||||||
<file>qml/ConnectionModeSettings.qml</file>
|
<file>qml/ConnectionModeSettings.qml</file>
|
||||||
<file>qml/SplashScreen.qml</file>
|
<file>qml/SplashScreen.qml</file>
|
||||||
<file>qml/Status.qml</file>
|
<file>qml/Status.qml</file>
|
||||||
<file>qml/WelcomeGuide.qml</file>
|
|
||||||
</qresource>
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
||||||
|
|||||||
@ -192,6 +192,8 @@ TrayIcon::TrayIcon()
|
|||||||
if (!onLinux()) { // we disable this on linux because of a Qt bug that causes the signal to be emitted for other apps (GODT-2750)
|
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"); });
|
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.
|
||||||
|
|||||||
@ -262,7 +262,7 @@ void UserList::onUsedBytesChanged(QString const &userID, qint64 usedBytes) {
|
|||||||
void UserList::onSyncStarted(QString const &userID) {
|
void UserList::onSyncStarted(QString const &userID) {
|
||||||
int const index = this->rowOfUserID(userID);
|
int const index = this->rowOfUserID(userID);
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
app().log().error(QString("Received onSyncStarted event for unknown userID %1").arg(userID));
|
app().log().error(QString("Received syncStarted event for unknown userID %1").arg(userID));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
users_[index]->setIsSyncing(true);
|
users_[index]->setIsSyncing(true);
|
||||||
@ -275,7 +275,7 @@ void UserList::onSyncStarted(QString const &userID) {
|
|||||||
void UserList::onSyncFinished(QString const &userID) {
|
void UserList::onSyncFinished(QString const &userID) {
|
||||||
int const index = this->rowOfUserID(userID);
|
int const index = this->rowOfUserID(userID);
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
app().log().error(QString("Received onSyncFinished event for unknown userID %1").arg(userID));
|
app().log().error(QString("Received syncFinished event for unknown userID %1").arg(userID));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
users_[index]->setIsSyncing(false);
|
users_[index]->setIsSyncing(false);
|
||||||
@ -293,7 +293,7 @@ void UserList::onSyncProgress(QString const &userID, double progress, float elap
|
|||||||
Q_UNUSED(remainingMs)
|
Q_UNUSED(remainingMs)
|
||||||
int const index = this->rowOfUserID(userID);
|
int const index = this->rowOfUserID(userID);
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
app().log().error(QString("Received onSyncFinished event for unknown userID %1").arg(userID));
|
app().log().error(QString("Received syncProgress event for unknown userID %1").arg(userID));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
users_[index]->setSyncProgress(progress);
|
users_[index]->setSyncProgress(progress);
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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 {
|
||||||
|
SmallView,
|
||||||
|
LargeView
|
||||||
|
}
|
||||||
|
|
||||||
|
property var _spacing: 12
|
||||||
property ColorScheme colorScheme
|
property ColorScheme colorScheme
|
||||||
property var user
|
property color progressColor: {
|
||||||
|
if (!root.enabled)
|
||||||
property var _spacing: 12 * ProtonStyle.px
|
return root.colorScheme.text_weak;
|
||||||
|
if (root.type === AccountDelegate.SmallView)
|
||||||
property color progressColor : {
|
return root.colorScheme.text_weak;
|
||||||
if (!root.enabled) return root.colorScheme.text_weak
|
if (root.user && root.user.isSyncing)
|
||||||
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.progressRatio < .50)
|
||||||
if (root.progressRatio < .50) return root.colorScheme.signal_success
|
return root.colorScheme.signal_success;
|
||||||
if (root.progressRatio < .75) return root.colorScheme.signal_warning
|
if (root.progressRatio < .75)
|
||||||
return root.colorScheme.signal_danger
|
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 : 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 : 0
|
||||||
|
}
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: progress_bar
|
id: progress_bar
|
||||||
visible: root.user ? root.type == AccountDelegate.LargeView : false
|
|
||||||
width: 140 * ProtonStyle.px
|
|
||||||
height: 4 * ProtonStyle.px
|
|
||||||
radius: ProtonStyle.progress_bar_radius
|
|
||||||
color: root.colorScheme.border_weak
|
color: root.colorScheme.border_weak
|
||||||
|
height: 4
|
||||||
|
radius: ProtonStyle.progress_bar_radius
|
||||||
|
visible: root.user ? root.type === AccountDelegate.LargeView : false
|
||||||
|
width: 140
|
||||||
|
|
||||||
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,36 @@
|
|||||||
// 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 _buttonSpacing: 8
|
||||||
|
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 showClientConfigurator(var user, string address, bool justLoggedIn)
|
||||||
|
signal showLogin(var username)
|
||||||
signal showSetupGuide(var user, string address)
|
|
||||||
|
|
||||||
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 +39,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,102 +49,101 @@ 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
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.topMargin: _topMargin
|
Layout.topMargin: _topMargin
|
||||||
|
spacing: _buttonSpacing
|
||||||
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 (user) {
|
||||||
return;
|
root.showLogin(user.primaryEmailOrUsername());
|
||||||
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 {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
actionText: qsTr("Configure email client")
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
text: qsTr("Email clients")
|
|
||||||
actionText: qsTr("Configure")
|
|
||||||
description: qsTr("Using the mailbox details below (re)configure your client.")
|
description: qsTr("Using the mailbox details below (re)configure your client.")
|
||||||
type: SettingsItem.Button
|
|
||||||
visible: _connected && (!root.user.splitMode) || (root.user.addresses.length === 1)
|
|
||||||
showSeparator: splitMode.visible
|
showSeparator: splitMode.visible
|
||||||
|
text: qsTr("Email clients")
|
||||||
|
type: SettingsItem.PrimaryButton
|
||||||
|
visible: _connected && ((!root.user.splitMode) || (root.user.addresses.length === 1))
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (!root.user)
|
if (!root.user)
|
||||||
return;
|
return;
|
||||||
root.showSetupGuide(root.user, user.addresses[0]);
|
root.showClientConfigurator(root.user, user.addresses[0], false);
|
||||||
}
|
}
|
||||||
|
|
||||||
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,52 +152,47 @@ 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: false
|
||||||
secondary: true
|
text: qsTr("Configure email client")
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (!root.user)
|
if (!root.user)
|
||||||
return;
|
return;
|
||||||
root.showSetupGuide(root.user, addressSelector.displayText);
|
root.showClientConfigurator(root.user, addressSelector.displayText, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 +200,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,111 @@
|
|||||||
// 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,82 @@
|
|||||||
// 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 bool highlightPassword
|
||||||
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: highlightPassword ? qsTr("Use this password") : qsTr("Password")
|
||||||
|
labelColor: highlightPassword ? colorScheme.signal_warning_active : colorScheme.text_norm
|
||||||
|
value: root.password
|
||||||
|
}
|
||||||
|
ConfigurationItem {
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
label: qsTr("Security")
|
||||||
|
value: root.security
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,35 +1,30 @@
|
|||||||
// 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 labelColor: root.colorScheme.text_norm
|
||||||
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
|
||||||
|
|
||||||
@ -41,51 +36,49 @@ Item {
|
|||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
Label {
|
Label {
|
||||||
|
color: labelColor
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
text: root.label
|
text: root.label
|
||||||
type: Label.Body
|
type: Label.Body_semibold
|
||||||
}
|
}
|
||||||
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,62 @@
|
|||||||
// 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 showSetupGuide(var user, string address)
|
signal closeWindow
|
||||||
signal closeWindow()
|
signal quitBridge
|
||||||
signal quitBridge()
|
signal showClientConfigurator(var user, string address, bool justLoggedIn)
|
||||||
|
signal showLogin(var username)
|
||||||
|
|
||||||
|
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)
|
||||||
|
showLogin(user.primaryEmailOrUsername());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.error("User with ID ", userID, " was not found in the account list");
|
||||||
|
}
|
||||||
|
function showBugReport() {
|
||||||
|
rightContent.showBugReport();
|
||||||
|
}
|
||||||
|
function showHelp() {
|
||||||
|
rightContent.showHelpView();
|
||||||
|
}
|
||||||
|
function showLocalCacheSettings() {
|
||||||
|
rightContent.showLocalCacheSettings();
|
||||||
|
}
|
||||||
|
function showSettings() {
|
||||||
|
rightContent.showGeneralSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasAccount() {
|
||||||
|
return Backend.users.count > 0
|
||||||
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@ -38,13 +64,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 +78,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 +100,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 +148,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 +157,329 @@ 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
|
||||||
}
|
}
|
||||||
|
Item {
|
||||||
|
id: noAccountBox
|
||||||
|
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 24
|
||||||
|
visible: !hasAccount()
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Label {
|
||||||
|
colorScheme: leftBar.colorScheme
|
||||||
|
color: colorScheme.text_weak
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
text: qsTr("No accounts")
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.leftMargin: 16
|
||||||
|
Layout.rightMargin: 16
|
||||||
|
colorScheme: leftBar.colorScheme
|
||||||
|
text: qsTr("Add an account")
|
||||||
|
secondary: true
|
||||||
|
onClicked: root.showLogin("")
|
||||||
|
}
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
|
visible: hasAccount()
|
||||||
|
delegate: Item {
|
||||||
|
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
|
||||||
|
implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
|
||||||
|
width: leftBar.width - 2 * accounts._leftRightMargins
|
||||||
|
|
||||||
header: Rectangle {
|
AccountDelegate {
|
||||||
height: headerLabel.height+16
|
id: accountDelegate
|
||||||
// color: ProtonStyle.transparent
|
anchors.bottomMargin: 8
|
||||||
Label{
|
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 {
|
||||||
|
showLogin(user.primaryEmailOrUsername());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 = ""
|
root.showLogin("");
|
||||||
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;
|
||||||
|
}
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
||||||
AccountView { // 0
|
StackLayout {
|
||||||
colorScheme: root.colorScheme
|
// 0
|
||||||
notifications: root.notifications
|
currentIndex: hasAccount() ? 1 : 0
|
||||||
user: {
|
NoAccountView {
|
||||||
if (accounts.currentIndex < 0) return undefined
|
|
||||||
if (Backend.users.count == 0) return undefined
|
|
||||||
return Backend.users.get(accounts.currentIndex)
|
|
||||||
}
|
|
||||||
onShowSignIn: {
|
|
||||||
var user = this.user
|
|
||||||
signIn.username = user ? user.primaryEmailOrUsername() : ""
|
|
||||||
rightContent.showSignIn()
|
|
||||||
}
|
|
||||||
onShowSetupGuide: function(user, address) {
|
|
||||||
root.showSetupGuide(user,address)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
GridLayout { // 1 Sign In
|
|
||||||
columns: 2
|
|
||||||
|
|
||||||
Button {
|
|
||||||
id: backButton
|
|
||||||
Layout.leftMargin: 18
|
|
||||||
Layout.topMargin: 10
|
|
||||||
Layout.alignment: Qt.AlignTop
|
|
||||||
|
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
onClicked: {
|
onStartSetup: {
|
||||||
signIn.abort()
|
root.showLogin("")
|
||||||
rightContent.showAccount()
|
|
||||||
}
|
}
|
||||||
icon.source: "/qml/icons/ic-arrow-left.svg"
|
|
||||||
secondary: true
|
|
||||||
horizontalPadding: 8
|
|
||||||
}
|
}
|
||||||
|
AccountView {
|
||||||
SignIn {
|
|
||||||
id: signIn
|
|
||||||
Layout.topMargin: 68
|
|
||||||
Layout.leftMargin: 80 - backButton.width - 18
|
|
||||||
Layout.rightMargin: 80
|
|
||||||
Layout.bottomMargin: 68
|
|
||||||
Layout.preferredWidth: 320
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.fillHeight: true
|
|
||||||
|
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
|
notifications: root.notifications
|
||||||
|
user: {
|
||||||
|
if (accounts.currentIndex < 0)
|
||||||
|
return undefined;
|
||||||
|
if (Backend.users.count === 0)
|
||||||
|
return undefined;
|
||||||
|
return Backend.users.get(accounts.currentIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
onShowClientConfigurator: function (user, address, justLoggedIn) {
|
||||||
|
root.showClientConfigurator(user, address, justLoggedIn);
|
||||||
|
}
|
||||||
|
onShowLogin: function (username) {
|
||||||
|
root.showLogin(username);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Rectangle {
|
||||||
GeneralSettings { // 2
|
Layout.fillHeight: true
|
||||||
|
Layout.fillWidth: true
|
||||||
|
color: "#ff9900"
|
||||||
|
}
|
||||||
|
GeneralSettings {
|
||||||
|
// 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.AlignCenter
|
Layout.alignment: Qt.AlignCenter
|
||||||
|
|
||||||
source: root._isAdvancedShown ? "/qml/icons/ic-chevron-down.svg" : "/qml/icons/ic-chevron-right.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,126 +1,109 @@
|
|||||||
// 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: {
|
|
||||||
Backend.notifyKBArticleClicked("https://proton.me/support/bridge");
|
|
||||||
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.openKBArticle();
|
||||||
|
}
|
||||||
|
}
|
||||||
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()
|
|
||||||
Backend.notifyReportBugClicked()
|
|
||||||
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,196 @@
|
|||||||
// 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
|
||||||
|
import "SetupWizard"
|
||||||
|
|
||||||
ApplicationWindow {
|
ApplicationWindow {
|
||||||
id: root
|
id: root
|
||||||
colorScheme: ProtonStyle.currentStyle
|
|
||||||
visible: true
|
|
||||||
|
|
||||||
|
|
||||||
property int _defaultWidth: 1080
|
|
||||||
property int _defaultHeight: 780
|
|
||||||
width: _defaultWidth
|
|
||||||
height: _defaultHeight
|
|
||||||
minimumWidth: _defaultWidth
|
|
||||||
|
|
||||||
property var notifications
|
property var notifications
|
||||||
|
|
||||||
|
function layoutForUserCount(userCount) {
|
||||||
|
if (userCount === 0) {
|
||||||
|
contentLayout.currentIndex = 1;
|
||||||
|
setupWizard.showOnboarding();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const u = Backend.users.get(0);
|
||||||
|
if (!u) {
|
||||||
|
console.trace();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ((userCount === 1) && (u.state === EUserState.SignedOut)) {
|
||||||
|
contentLayout.currentIndex = 1;
|
||||||
|
setupWizard.showLogin(u.primaryEmailOrUsername());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function selectUser(userID) {
|
||||||
|
contentWrapper.selectUser(userID);
|
||||||
|
}
|
||||||
|
function showAndRise() {
|
||||||
|
root.show();
|
||||||
|
root.raise();
|
||||||
|
if (!root.active) {
|
||||||
|
root.requestActivate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function showClientConfigurator(user, address, justLoggedIn) {
|
||||||
|
contentLayout.currentIndex = 1;
|
||||||
|
setupWizard.showClientConfig(user, address, justLoggedIn);
|
||||||
|
}
|
||||||
|
function showHelp() {
|
||||||
|
contentWrapper.showHelp();
|
||||||
|
}
|
||||||
|
function showLocalCacheSettings() {
|
||||||
|
contentWrapper.showLocalCacheSettings();
|
||||||
|
}
|
||||||
|
function showLogin(username = "") {
|
||||||
|
contentLayout.currentIndex = 1;
|
||||||
|
setupWizard.showLogin(username);
|
||||||
|
}
|
||||||
|
function showSettings() {
|
||||||
|
contentWrapper.showSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
colorScheme: ProtonStyle.currentStyle
|
||||||
|
height: ProtonStyle.window_default_height
|
||||||
|
minimumHeight:ProtonStyle.window_minimum_height
|
||||||
|
minimumWidth: ProtonStyle.window_minimum_width
|
||||||
|
visible: true
|
||||||
|
width: ProtonStyle.window_default_width
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
layoutForUserCount(Backend.users.count);
|
||||||
|
}
|
||||||
|
|
||||||
// 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 (setupWizard.user === user) {
|
||||||
if (setupGuide.user === user) {
|
setupWizard.closeWizard();
|
||||||
setupGuide.user = null
|
|
||||||
contentLayout._showSetup = false
|
|
||||||
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.showClientConfigurator(user, user.addresses[0], false);
|
||||||
|
}
|
||||||
|
|
||||||
|
target: Backend.users
|
||||||
|
}
|
||||||
Connections {
|
Connections {
|
||||||
target: Backend
|
|
||||||
|
|
||||||
function onShowMainWindow() {
|
|
||||||
root.showAndRise()
|
|
||||||
}
|
|
||||||
|
|
||||||
function onLoginFinished(index, wasSignedOut) {
|
|
||||||
var user = Backend.users.get(index)
|
|
||||||
if (user && !wasSignedOut) {
|
|
||||||
root.showSetup(user, user.addresses[0])
|
|
||||||
}
|
|
||||||
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 (setupWizard.visible) {
|
||||||
|
setupWizard.closeWizard()
|
||||||
|
}
|
||||||
if (forceShowWindow) {
|
if (forceShowWindow) {
|
||||||
root.showAndRise()
|
root.showAndRise();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
function onShowHelp() {
|
||||||
|
root.showHelp();
|
||||||
|
if (setupWizard.visible) {
|
||||||
|
setupWizard.closeWizard()
|
||||||
|
}
|
||||||
|
|
||||||
|
root.showAndRise();
|
||||||
|
}
|
||||||
|
function onShowMainWindow() {
|
||||||
|
root.showAndRise();
|
||||||
|
}
|
||||||
|
function onShowSettings() {
|
||||||
|
if (setupWizard.visible) {
|
||||||
|
setupWizard.closeWizard()
|
||||||
|
}
|
||||||
|
root.showSettings();
|
||||||
|
root.showAndRise();
|
||||||
|
}
|
||||||
|
|
||||||
|
target: Backend
|
||||||
|
}
|
||||||
|
Connections {
|
||||||
|
function onCountChanged(count) {
|
||||||
|
layoutForUserCount(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
target: Backend.users
|
||||||
|
}
|
||||||
StackLayout {
|
StackLayout {
|
||||||
id: contentLayout
|
id: contentLayout
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
currentIndex: 0
|
||||||
|
|
||||||
property bool _showSetup: false
|
ContentWrapper {
|
||||||
currentIndex: {
|
// 0
|
||||||
// show welcome when there are no users
|
|
||||||
if (Backend.users.count === 0) {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
var u = Backend.users.get(0)
|
|
||||||
|
|
||||||
if (!u) {
|
|
||||||
console.trace()
|
|
||||||
console.log("empty user")
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((Backend.users.count === 1) && (u.state === EUserState.SignedOut)) {
|
|
||||||
showSignIn(u.primaryEmailOrUsername())
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
if (contentLayout._showSetup) {
|
|
||||||
return 2
|
|
||||||
}
|
|
||||||
|
|
||||||
return 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();
|
||||||
|
}
|
||||||
|
onShowClientConfigurator: function (user, address, justLoggedIn) {
|
||||||
|
root.showClientConfigurator(user, address, justLoggedIn);
|
||||||
|
}
|
||||||
|
onShowLogin: function (username) {
|
||||||
|
root.showLogin(username);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
SetupWizard {
|
||||||
WelcomeGuide { // 1
|
id: setupWizard
|
||||||
colorScheme: root.colorScheme
|
|
||||||
|
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
|
||||||
|
|
||||||
SetupGuide { // 2
|
|
||||||
id: setupGuide
|
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
|
|
||||||
Layout.fillHeight: true
|
onBugReportRequested: {
|
||||||
Layout.fillWidth: true
|
contentWrapper.showBugReport();
|
||||||
|
|
||||||
onDismissed: {
|
|
||||||
root.showSetup(null,"")
|
|
||||||
}
|
}
|
||||||
|
onWizardEnded: {
|
||||||
onFinished: {
|
contentLayout.currentIndex = 0;
|
||||||
// TODO: Do not close window. Trigger Backend to check that
|
|
||||||
// there is a successfully connected client. Then Backend
|
|
||||||
// should send another signal to close the setup guide.
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,58 @@
|
|||||||
|
// Copyright (c) 2023 Proton AG
|
||||||
|
// This file is part of Proton Mail Bridge.
|
||||||
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Proton
|
||||||
|
import "SetupWizard"
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property ColorScheme colorScheme
|
||||||
|
|
||||||
|
color: root.colorScheme.background_norm
|
||||||
|
|
||||||
|
signal startSetup()
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
// we use the setup wizard left pane (onboarding version)
|
||||||
|
LeftPane {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.preferredWidth: ProtonStyle.wizard_pane_width
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
wizard: setupWizard
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
showNoAccount();
|
||||||
|
}
|
||||||
|
onStartSetup: {
|
||||||
|
root.startSetup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Image {
|
||||||
|
id: mailLogoWithWordmark
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
Layout.bottomMargin: ProtonStyle.wizard_window_margin
|
||||||
|
height: sourceSize.height
|
||||||
|
source: root.colorScheme.mail_logo_with_wordmark
|
||||||
|
sourceSize.height: 36
|
||||||
|
sourceSize.width: 134
|
||||||
|
width: sourceSize.width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 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
|
||||||
|
|
||||||
@ -27,118 +21,98 @@ Item {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
property ColorScheme colorScheme
|
property ColorScheme colorScheme
|
||||||
property var notifications
|
|
||||||
property var mainWindow
|
property var mainWindow
|
||||||
|
|
||||||
property int notificationWhitelist: NotificationFilter.FilterConsts.All
|
|
||||||
property int notificationBlacklist: NotificationFilter.FilterConsts.None
|
property int notificationBlacklist: NotificationFilter.FilterConsts.None
|
||||||
|
property int notificationWhitelist: NotificationFilter.FilterConsts.All
|
||||||
|
property var notifications
|
||||||
|
|
||||||
NotificationFilter {
|
NotificationFilter {
|
||||||
id: bannerNotificationFilter
|
id: bannerNotificationFilter
|
||||||
|
|
||||||
source: root.notifications.all
|
|
||||||
blacklist: Notifications.Group.Dialogs
|
blacklist: Notifications.Group.Dialogs
|
||||||
|
source: root.notifications.all
|
||||||
}
|
}
|
||||||
|
|
||||||
Banner {
|
Banner {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
notification: bannerNotificationFilter.topmost
|
|
||||||
mainWindow: root.mainWindow
|
mainWindow: root.mainWindow
|
||||||
|
notification: bannerNotificationFilter.topmost
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationDialog {
|
NotificationDialog {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
notification: root.notifications.updateManualReady
|
notification: root.notifications.updateManualReady
|
||||||
|
|
||||||
Switch {
|
Switch {
|
||||||
id:autoUpdate
|
id: autoUpdate
|
||||||
|
checked: Backend.isAutomaticUpdateOn
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
text: qsTr("Update automatically in the future")
|
text: qsTr("Update automatically in the future")
|
||||||
checked: Backend.isAutomaticUpdateOn
|
|
||||||
onClicked: Backend.toggleAutomaticUpdate(autoUpdate.checked)
|
onClicked: Backend.toggleAutomaticUpdate(autoUpdate.checked)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationDialog {
|
NotificationDialog {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
notification: root.notifications.updateForce
|
notification: root.notifications.updateForce
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationDialog {
|
NotificationDialog {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
notification: root.notifications.updateForceError
|
notification: root.notifications.updateForceError
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationDialog {
|
NotificationDialog {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
notification: root.notifications.enableBeta
|
notification: root.notifications.enableBeta
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationDialog {
|
NotificationDialog {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
notification: root.notifications.cacheUnavailable
|
notification: root.notifications.cacheUnavailable
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationDialog {
|
NotificationDialog {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
notification: root.notifications.cacheCantMove
|
notification: root.notifications.cacheCantMove
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationDialog {
|
NotificationDialog {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
notification: root.notifications.diskFull
|
notification: root.notifications.diskFull
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationDialog {
|
NotificationDialog {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
notification: root.notifications.enableSplitMode
|
notification: root.notifications.enableSplitMode
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationDialog {
|
NotificationDialog {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
notification: root.notifications.resetBridge
|
notification: root.notifications.resetBridge
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationDialog {
|
NotificationDialog {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
notification: root.notifications.changeAllMailVisibility
|
notification: root.notifications.changeAllMailVisibility
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationDialog {
|
NotificationDialog {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
notification: root.notifications.deleteAccount
|
notification: root.notifications.deleteAccount
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationDialog {
|
NotificationDialog {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
notification: root.notifications.noKeychain
|
notification: root.notifications.noKeychain
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationDialog {
|
NotificationDialog {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
notification: root.notifications.rebuildKeychain
|
notification: root.notifications.rebuildKeychain
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationDialog {
|
NotificationDialog {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
notification: root.notifications.apiCertIssue
|
notification: root.notifications.apiCertIssue
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationDialog {
|
NotificationDialog {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
notification: root.notifications.noActiveKeyForRecipient
|
notification: root.notifications.noActiveKeyForRecipient
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationDialog {
|
NotificationDialog {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
notification: root.notifications.userBadEvent
|
notification: root.notifications.userBadEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationDialog {
|
NotificationDialog {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
notification: root.notifications.genericError
|
notification: root.notifications.genericError
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationDialog {
|
NotificationDialog {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
notification: root.notifications.genericQuestion
|
notification: root.notifications.genericQuestion
|
||||||
|
|||||||
@ -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 QtQml
|
import QtQml
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
|
|
||||||
QtObject {
|
QtObject {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
default property var children
|
|
||||||
|
|
||||||
enum NotificationType {
|
enum NotificationType {
|
||||||
Info = 0,
|
Info,
|
||||||
Success = 1,
|
Success,
|
||||||
Warning = 2,
|
Warning,
|
||||||
Danger = 3
|
Danger
|
||||||
}
|
}
|
||||||
|
|
||||||
|
property list<Action> action
|
||||||
|
property bool active: false
|
||||||
|
// brief is used in status view only
|
||||||
|
property string brief
|
||||||
|
default property var children
|
||||||
|
property var data
|
||||||
|
// description is used in banners and in dialogs as description
|
||||||
|
property string description
|
||||||
|
property bool dismissed: false
|
||||||
|
property int group
|
||||||
|
property string icon
|
||||||
|
readonly property var occurred: active ? new Date() : undefined
|
||||||
|
|
||||||
// title is used in dialogs only
|
// title is used in dialogs only
|
||||||
property string title
|
property string title
|
||||||
// description is used in banners and in dialogs as description
|
|
||||||
property string description
|
|
||||||
// brief is used in status view only
|
|
||||||
property string brief
|
|
||||||
|
|
||||||
property string icon
|
|
||||||
property list<Action> action
|
|
||||||
property int type
|
property int type
|
||||||
property int group
|
|
||||||
|
|
||||||
property bool dismissed: false
|
|
||||||
property bool active: false
|
|
||||||
readonly property var occurred: active ? new Date() : undefined
|
|
||||||
|
|
||||||
property var data
|
|
||||||
|
|
||||||
onActiveChanged: {
|
onActiveChanged: {
|
||||||
dismissed = false
|
dismissed = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,114 +1,95 @@
|
|||||||
// 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 QtQml.Models
|
import QtQml.Models
|
||||||
|
|
||||||
// contains notifications that satisfy black- and whitelist and are sorted in time-occurred order
|
// contains notifications that satisfy black- and whitelist and are sorted in time-occurred order
|
||||||
ListModel {
|
ListModel {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
enum FilterConsts {
|
enum FilterConsts {
|
||||||
None = 0,
|
None,
|
||||||
All = 255
|
All = 255
|
||||||
}
|
}
|
||||||
|
|
||||||
property int whitelist: NotificationFilter.FilterConsts.All
|
|
||||||
property int blacklist: NotificationFilter.FilterConsts.None
|
property int blacklist: NotificationFilter.FilterConsts.None
|
||||||
|
|
||||||
property Notification topmost
|
|
||||||
property var source
|
|
||||||
|
|
||||||
property bool componentCompleted: false
|
property bool componentCompleted: false
|
||||||
Component.onCompleted: {
|
property var source
|
||||||
root.componentCompleted = true
|
property Notification topmost
|
||||||
root.rebuildList()
|
property int whitelist: NotificationFilter.FilterConsts.All
|
||||||
}
|
|
||||||
|
|
||||||
// overriding get method to ignore any role and return directly object itself
|
// overriding get method to ignore any role and return directly object itself
|
||||||
function get(row) {
|
function get(row) {
|
||||||
if (row < 0 || row >= count) {
|
if (row < 0 || row >= count) {
|
||||||
return undefined
|
return undefined;
|
||||||
}
|
}
|
||||||
return data(index(row, 0), Qt.DisplayRole)
|
return data(index(row, 0), Qt.DisplayRole);
|
||||||
}
|
}
|
||||||
|
|
||||||
function rebuildList() {
|
function rebuildList() {
|
||||||
|
let i;
|
||||||
// avoid evaluation of the list before Component.onCompleted
|
// avoid evaluation of the list before Component.onCompleted
|
||||||
if (!root.componentCompleted) {
|
if (!root.componentCompleted) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
for (i = 0; i < root.count; i++) {
|
||||||
for (var i = 0; i < root.count; i++) {
|
root.get(i).onActiveChanged.disconnect(root.updateList);
|
||||||
root.get(i).onActiveChanged.disconnect( root.updateList )
|
|
||||||
}
|
}
|
||||||
|
root.clear();
|
||||||
root.clear()
|
|
||||||
|
|
||||||
if (!root.source) {
|
if (!root.source) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (i = 0; i < root.source.length; i++) {
|
for (i = 0; i < root.source.length; i++) {
|
||||||
var obj = root.source[i]
|
const obj = root.source[i];
|
||||||
if (obj.group & root.blacklist) {
|
if (obj.group & root.blacklist) {
|
||||||
continue
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(obj.group & root.whitelist)) {
|
if (!(obj.group & root.whitelist)) {
|
||||||
continue
|
continue;
|
||||||
}
|
}
|
||||||
|
root.append({
|
||||||
root.append({obj})
|
"obj": obj
|
||||||
obj.onActiveChanged.connect( root.updateList )
|
});
|
||||||
|
obj.onActiveChanged.connect(root.updateList);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateList() {
|
function updateList() {
|
||||||
var topmost = null
|
let topmost = null;
|
||||||
|
for (let i = 0; i < root.count; i++) {
|
||||||
for (var i = 0; i < root.count; i++) {
|
const obj = root.get(i);
|
||||||
var obj = root.get(i)
|
|
||||||
|
|
||||||
if (!obj.active) {
|
if (!obj.active) {
|
||||||
continue
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (topmost && (topmost.type > obj.type)) {
|
if (topmost && (topmost.type > obj.type)) {
|
||||||
continue
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (topmost && (topmost.type === obj.type) && (topmost.occurred > obj.occurred)) {
|
if (topmost && (topmost.type === obj.type) && (topmost.occurred > obj.occurred)) {
|
||||||
continue
|
continue;
|
||||||
}
|
}
|
||||||
|
topmost = obj;
|
||||||
topmost = obj
|
|
||||||
}
|
}
|
||||||
|
root.topmost = topmost;
|
||||||
root.topmost = topmost
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onWhitelistChanged: {
|
Component.onCompleted: {
|
||||||
root.rebuildList()
|
root.componentCompleted = true;
|
||||||
|
root.rebuildList();
|
||||||
}
|
}
|
||||||
onBlacklistChanged: {
|
onBlacklistChanged: {
|
||||||
root.rebuildList()
|
root.rebuildList();
|
||||||
}
|
}
|
||||||
onSourceChanged: {
|
onSourceChanged: {
|
||||||
root.rebuildList()
|
root.rebuildList();
|
||||||
|
}
|
||||||
|
onWhitelistChanged: {
|
||||||
|
root.rebuildList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,178 +1,157 @@
|
|||||||
// 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: (imapField.text * 1 !== Backend.imapPort || smtpField.text * 1 !== Backend.smtpPort)
|
||||||
|
|
||||||
property var notifications
|
property var notifications
|
||||||
|
|
||||||
property bool _valuesChanged: (
|
function isPortFree(field) {
|
||||||
imapField.text*1 !== Backend.imapPort ||
|
const num = field.text * 1;
|
||||||
smtpField.text*1 !== Backend.smtpPort
|
if (num === Backend.imapPort)
|
||||||
)
|
return true;
|
||||||
|
if (num === Backend.smtpPort)
|
||||||
|
return true;
|
||||||
|
if (!Backend.isPortFree(num)) {
|
||||||
|
field.error = true;
|
||||||
|
field.errorString = qsTr("Port occupied");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
function setDefaultValues() {
|
||||||
|
imapField.text = Backend.imapPort;
|
||||||
|
smtpField.text = Backend.smtpPort;
|
||||||
|
imapField.error = false;
|
||||||
|
smtpField.error = false;
|
||||||
|
}
|
||||||
|
function validate(port) {
|
||||||
|
const num = port * 1;
|
||||||
|
if (!(num > 1 && num < 65536)) {
|
||||||
|
return qsTr("Invalid port number");
|
||||||
|
}
|
||||||
|
if (imapField.text === smtpField.text) {
|
||||||
|
return qsTr("Port numbers must be different");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fillHeight: false
|
||||||
|
|
||||||
|
Component.onCompleted: root.setDefaultValues()
|
||||||
|
onBack: {
|
||||||
|
root.setDefaultValues();
|
||||||
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
text: qsTr("Default ports")
|
text: qsTr("Default ports")
|
||||||
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("Changes require reconfiguration of your email client.")
|
text: qsTr("Changes require reconfiguration of your email client.")
|
||||||
type: Label.Body
|
type: Label.Body
|
||||||
color: root.colorScheme.text_weak
|
|
||||||
Layout.fillWidth: true
|
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
spacing: 16
|
spacing: 16
|
||||||
|
|
||||||
TextField {
|
TextField {
|
||||||
id: imapField
|
id: imapField
|
||||||
|
Layout.alignment: Qt.AlignTop | Qt.AlignLeft
|
||||||
|
Layout.preferredWidth: 160
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
label: qsTr("IMAP port")
|
label: qsTr("IMAP port")
|
||||||
Layout.preferredWidth: 160
|
|
||||||
Layout.alignment: Qt.AlignTop | Qt.AlignLeft
|
|
||||||
validator: root.validate
|
validator: root.validate
|
||||||
}
|
}
|
||||||
TextField {
|
TextField {
|
||||||
id: smtpField
|
id: smtpField
|
||||||
|
Layout.alignment: Qt.AlignTop | Qt.AlignLeft
|
||||||
|
Layout.preferredWidth: 160
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
label: qsTr("SMTP port")
|
label: qsTr("SMTP port")
|
||||||
Layout.preferredWidth: 160
|
|
||||||
Layout.alignment: Qt.AlignTop | Qt.AlignLeft
|
|
||||||
validator: root.validate
|
validator: root.validate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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")
|
|
||||||
enabled: (!loading) && root._valuesChanged
|
enabled: (!loading) && root._valuesChanged
|
||||||
|
text: qsTr("Save")
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
// removing error here because we may have set it manually (port occupied)
|
// removing error here because we may have set it manually (port occupied)
|
||||||
imapField.error = false
|
imapField.error = false;
|
||||||
smtpField.error = false
|
smtpField.error = false;
|
||||||
|
|
||||||
// checking errors separately because we want to display "same port" error only once
|
// checking errors separately because we want to display "same port" error only once
|
||||||
imapField.validate()
|
imapField.validate();
|
||||||
if (imapField.error) {
|
if (imapField.error) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
smtpField.validate()
|
smtpField.validate();
|
||||||
if (smtpField.error) {
|
if (smtpField.error) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
submitButton.loading = true;
|
||||||
submitButton.loading = true
|
|
||||||
|
|
||||||
// check both ports before returning an error
|
// check both ports before returning an error
|
||||||
var err = false
|
let err = false;
|
||||||
err |= !isPortFree(imapField)
|
err |= !isPortFree(imapField);
|
||||||
err |= !isPortFree(smtpField)
|
err |= !isPortFree(smtpField);
|
||||||
if (err) {
|
if (err) {
|
||||||
submitButton.loading = false
|
submitButton.loading = false;
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We turn off all port error notification. They well be restored if problems persist
|
// We turn off all port error notification. They will be restored if problems persist
|
||||||
root.notifications.imapPortStartupError.active = false
|
root.notifications.imapPortStartupError.active = false;
|
||||||
root.notifications.smtpPortStartupError.active = false
|
root.notifications.smtpPortStartupError.active = false;
|
||||||
root.notifications.imapPortChangeError.active = false
|
root.notifications.imapPortChangeError.active = false;
|
||||||
root.notifications.smtpPortChangeError.active = false
|
root.notifications.smtpPortChangeError.active = false;
|
||||||
|
Backend.setMailServerSettings(imapField.text, smtpField.text, Backend.useSSLForIMAP, Backend.useSSLForSMTP);
|
||||||
Backend.setMailServerSettings(imapField.text, smtpField.text, Backend.useSSLForIMAP, Backend.useSSLForSMTP)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
target: Backend
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onBack: {
|
|
||||||
root.setDefaultValues()
|
|
||||||
}
|
|
||||||
|
|
||||||
function validate(port) {
|
|
||||||
var num = port*1
|
|
||||||
if (! (num > 1 && num < 65536) ) {
|
|
||||||
return qsTr("Invalid port number")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (imapField.text == smtpField.text) {
|
|
||||||
return qsTr("Port numbers must be different")
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
function isPortFree(field) {
|
|
||||||
var num = field.text*1
|
|
||||||
if (num === Backend.imapPort) return true
|
|
||||||
if (num === Backend.smtpPort) return true
|
|
||||||
if (!Backend.isPortFree(num)) {
|
|
||||||
field.error = true
|
|
||||||
field.errorString = qsTr("Port occupied")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
function setDefaultValues(){
|
|
||||||
imapField.text = Backend.imapPort
|
|
||||||
smtpField.text = Backend.smtpPort
|
|
||||||
imapField.error = false
|
|
||||||
smtpField.error = false
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: root.setDefaultValues()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,20 +1,15 @@
|
|||||||
// 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.Templates as T
|
import QtQuick.Templates as T
|
||||||
|
|
||||||
|
|||||||
@ -1,20 +1,15 @@
|
|||||||
// 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
|
||||||
@ -25,14 +20,14 @@ import QtQuick.Templates as T
|
|||||||
T.ApplicationWindow {
|
T.ApplicationWindow {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property ColorScheme colorScheme
|
|
||||||
|
|
||||||
// popup priority based on types
|
// popup priority based on types
|
||||||
enum PopupType {
|
enum PopupType {
|
||||||
Banner = 0,
|
Banner,
|
||||||
Dialog = 1
|
Dialog
|
||||||
}
|
}
|
||||||
|
|
||||||
|
property ColorScheme colorScheme
|
||||||
|
|
||||||
// contains currently visible popup
|
// contains currently visible popup
|
||||||
property var popupVisible: null
|
property var popupVisible: null
|
||||||
|
|
||||||
@ -41,85 +36,61 @@ T.ApplicationWindow {
|
|||||||
// overriding get method to ignore any role and return directly object itself
|
// overriding get method to ignore any role and return directly object itself
|
||||||
function get(row) {
|
function get(row) {
|
||||||
if (row < 0 || row >= count) {
|
if (row < 0 || row >= count) {
|
||||||
return undefined
|
return undefined;
|
||||||
}
|
}
|
||||||
return data(index(row, 0), Qt.DisplayRole)
|
return data(index(row, 0), Qt.DisplayRole);
|
||||||
}
|
|
||||||
|
|
||||||
onRowsInserted: function(parent, first, last) {
|
|
||||||
for (var i = first; i <= last; i++) {
|
|
||||||
var obj = popups.get(i)
|
|
||||||
obj.onShouldShowChanged.connect( root.processPopups )
|
|
||||||
}
|
|
||||||
|
|
||||||
processPopups()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onRowsAboutToBeRemoved: function (parent, first, last) {
|
onRowsAboutToBeRemoved: function (parent, first, last) {
|
||||||
for (var i = first; i <= last; i++ ) {
|
for (let i = first; i <= last; i++) {
|
||||||
var obj = popups.get(i)
|
const obj = popups.get(i);
|
||||||
obj.onShouldShowChanged.disconnect( root.processPopups )
|
obj.onShouldShowChanged.disconnect(root.processPopups);
|
||||||
|
|
||||||
// if currently visible popup was removed
|
// if currently visible popup was removed
|
||||||
if (root.popupVisible === obj) {
|
if (root.popupVisible === obj) {
|
||||||
root.popupVisible.visible = false
|
root.popupVisible.visible = false;
|
||||||
root.popupVisible = null
|
root.popupVisible = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
processPopups();
|
||||||
processPopups()
|
}
|
||||||
|
onRowsInserted: function (parent, first, last) {
|
||||||
|
for (let i = first; i <= last; i++) {
|
||||||
|
const obj = popups.get(i);
|
||||||
|
obj.onShouldShowChanged.connect(root.processPopups);
|
||||||
|
}
|
||||||
|
processPopups();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function processPopups() {
|
function processPopups() {
|
||||||
if ((root.popupVisible) && (!root.popupVisible.shouldShow)) {
|
if ((root.popupVisible) && (!root.popupVisible.shouldShow)) {
|
||||||
root.popupVisible.visible = false
|
root.popupVisible.visible = false;
|
||||||
}
|
}
|
||||||
|
let topmost = null;
|
||||||
var topmost = null
|
for (let i = 0; i < popups.count; i++) {
|
||||||
for (var i = 0; i < popups.count; i++) {
|
const obj = popups.get(i);
|
||||||
var obj = popups.get(i)
|
|
||||||
|
|
||||||
if (obj.shouldShow === false) {
|
if (obj.shouldShow === false) {
|
||||||
continue
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (topmost && (topmost.popupType > obj.popupType)) {
|
if (topmost && (topmost.popupType > obj.popupType)) {
|
||||||
continue
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (topmost && (topmost.popupType === obj.popupType) && (topmost.occurred > obj.occurred)) {
|
if (topmost && (topmost.popupType === obj.popupType) && (topmost.occurred > obj.occurred)) {
|
||||||
continue
|
continue;
|
||||||
}
|
}
|
||||||
|
topmost = obj;
|
||||||
topmost = obj
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (root.popupVisible !== topmost) {
|
if (root.popupVisible !== topmost) {
|
||||||
if (root.popupVisible) {
|
if (root.popupVisible) {
|
||||||
root.popupVisible.visible = false
|
root.popupVisible.visible = false;
|
||||||
}
|
}
|
||||||
root.popupVisible = topmost
|
root.popupVisible = topmost;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!root.popupVisible) {
|
if (!root.popupVisible) {
|
||||||
return
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
root.popupVisible.visible = true
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: root.popupVisible
|
|
||||||
|
|
||||||
function onVisibleChanged() {
|
|
||||||
if (root.popupVisible.visible) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
root.popupVisible = null
|
|
||||||
root.processPopups()
|
|
||||||
}
|
}
|
||||||
|
root.popupVisible.visible = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
color: root.colorScheme.background_norm
|
color: root.colorScheme.background_norm
|
||||||
@ -127,8 +98,19 @@ T.ApplicationWindow {
|
|||||||
Overlay.modal: Rectangle {
|
Overlay.modal: Rectangle {
|
||||||
color: root.colorScheme.backdrop_norm
|
color: root.colorScheme.backdrop_norm
|
||||||
}
|
}
|
||||||
|
|
||||||
Overlay.modeless: Rectangle {
|
Overlay.modeless: Rectangle {
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
function onVisibleChanged() {
|
||||||
|
if (root.popupVisible.visible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
root.popupVisible = null;
|
||||||
|
root.processPopups();
|
||||||
|
}
|
||||||
|
|
||||||
|
target: root.popupVisible
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user