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
|
||||
|
||||
default:
|
||||
tags:
|
||||
- shared-small
|
||||
|
||||
variables:
|
||||
GOPRIVATE: gitlab.protontech.ch
|
||||
GOMAXPROCS: $(( ${CI_TAG_CPU} / 2 ))
|
||||
@ -47,6 +51,19 @@ stages:
|
||||
allow_failure: true
|
||||
- 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-windows:
|
||||
before_script:
|
||||
@ -58,7 +75,7 @@ stages:
|
||||
- export GO111MODULE=on
|
||||
- export PATH="${GOPATH}/bin:${PATH}"
|
||||
- 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="/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"
|
||||
@ -80,7 +97,7 @@ stages:
|
||||
- export PATH="${GOROOT}/bin:$PATH"
|
||||
- export GOPATH=~/go1.20
|
||||
- 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"
|
||||
- uname -a
|
||||
cache: {}
|
||||
@ -88,7 +105,7 @@ stages:
|
||||
- macos-m1-bridge
|
||||
|
||||
.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:
|
||||
VCPKG_DEFAULT_BINARY_CACHE: ${CI_PROJECT_DIR}/.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 url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
|
||||
tags:
|
||||
- large
|
||||
- shared-large
|
||||
|
||||
# Stage: TEST
|
||||
|
||||
@ -116,7 +133,16 @@ lint:
|
||||
script:
|
||||
- make lint
|
||||
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:
|
||||
stage: test
|
||||
@ -132,7 +158,16 @@ test-linux:
|
||||
extends:
|
||||
- .script-test
|
||||
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:
|
||||
extends:
|
||||
@ -154,11 +189,19 @@ test-integration-race:
|
||||
script:
|
||||
- make test-integration-race
|
||||
|
||||
test-integration-nightly:
|
||||
extends:
|
||||
- test-integration
|
||||
- .rules-branch-manual-scheduled-and-test-branch-always
|
||||
needs:
|
||||
- test-integration
|
||||
script:
|
||||
- make test-integration-nightly
|
||||
|
||||
test-windows:
|
||||
extends:
|
||||
- .env-windows
|
||||
- .script-test
|
||||
- .rules-branch-and-MR-manual
|
||||
|
||||
test-darwin:
|
||||
extends:
|
||||
@ -168,17 +211,18 @@ test-darwin:
|
||||
test-coverage:
|
||||
stage: test
|
||||
extends:
|
||||
- .rules-branch-manual-MR-and-devel-always
|
||||
- .rules-branch-manual-scheduled-and-test-branch-always
|
||||
script:
|
||||
- ./utils/coverage.sh
|
||||
coverage: '/total:.*\(statements\).*\d+\.\d+%/'
|
||||
needs:
|
||||
- test-linux
|
||||
#- test-windows
|
||||
- test-windows
|
||||
- test-darwin
|
||||
- test-integration
|
||||
- test-integration-nightly
|
||||
tags:
|
||||
- small
|
||||
- shared-small
|
||||
artifacts:
|
||||
paths:
|
||||
- coverage*
|
||||
@ -242,4 +286,18 @@ build-windows-qa:
|
||||
variables:
|
||||
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...
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
* Windres (Windows)
|
||||
* libglvnd and libsecret development files (Linux)
|
||||
* pkg-config (Linux)
|
||||
* cmake, ninja-build and Qt 6 are required to build the graphical user interface. On Linux,
|
||||
* 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.
|
||||
|
||||
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
|
||||
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.
|
||||
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-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-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)
|
||||
* [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)
|
||||
@ -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-windows](https://github.com/elastic/go-windows) available under [license](https://github.com/elastic/go-windows/blob/master/LICENSE)
|
||||
* [go-textwrapper](https://github.com/emersion/go-textwrapper) available under [license](https://github.com/emersion/go-textwrapper/blob/master/LICENSE)
|
||||
* [go-vcard](https://github.com/emersion/go-vcard) available under [license](https://github.com/emersion/go-vcard/blob/master/LICENSE)
|
||||
* [fgprof](https://github.com/felixge/fgprof) available under [license](https://github.com/felixge/fgprof/blob/master/LICENSE)
|
||||
* [go-shlex](https://github.com/flynn-archive/go-shlex) available under [license](https://github.com/flynn-archive/go-shlex/blob/master/LICENSE)
|
||||
* [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)
|
||||
* [tagparser](https://github.com/vmihailenco/tagparser/v2) available under [license](https://github.com/vmihailenco/tagparser/v2/blob/master/LICENSE)
|
||||
* [smetrics](https://github.com/xrash/smetrics) available under [license](https://github.com/xrash/smetrics/blob/master/LICENSE)
|
||||
* [go-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)
|
||||
* [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)
|
||||
|
||||
192
Changelog.md
192
Changelog.md
@ -3,6 +3,142 @@
|
||||
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
|
||||
|
||||
### Added
|
||||
@ -41,7 +177,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
* GODT-2578: Refresh literals appended to Sent folder.
|
||||
* GODT-2753: Vault test now check that value auto-assigned is first available port.
|
||||
* GODT-2522: Handle migration with unreferenced db values.
|
||||
* GODT-2693: Allow missing whitespace after header field colon.
|
||||
* GODT-2670: Allow missing whitespace after header field colon.
|
||||
* GODT-2653: Only log when err is not nil.
|
||||
* GODT-2680: Fix for C++ debugger not working on ARM64 because of OpenSSL 3.1.
|
||||
* GODT-2675: Update GPA to applye togin-gonic/gin patch + update COPYING_NOTES.
|
||||
@ -126,7 +262,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
* GODT-2437: Silence harmless report to sentry.
|
||||
* GODT-2649: Clean up cache files after failed connector create (Gluon).
|
||||
* GODT-2638: Validate messages before import.
|
||||
* GODT-2646: Bump GPA and Gluon dependecy after CIRCL upgrade.
|
||||
* GODT-2646: Bump GPA and Gluon dependency after CIRCL upgrade.
|
||||
* GODT-2454: Only Send status update if transaction succeeded.
|
||||
* Test: fix flaky tests.
|
||||
* GODT-2628: Attempt to fix closed channel panic on logout.
|
||||
@ -186,7 +322,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
* GODT-2574: Fix label/unlabel of large amounts of messages.
|
||||
* GODT-2573: Handle invalid header fields in message.
|
||||
* GODT-2573: Crash on null update.
|
||||
* GODT-2407: Replace invalid email addresses with emtpy for new Drafts.
|
||||
* GODT-2407: Replace invalid email addresses with empty for new Drafts.
|
||||
|
||||
## [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.
|
||||
|
||||
### 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-2427: Parsing header issues.
|
||||
* 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-2400: Allow state updates to be applied if command fails.
|
||||
* GODT-2399: Fix immediate message deletion during updates.
|
||||
* GODT-2390: Missing changes from pervious commit.
|
||||
* GODT-2390: Missing changes from previous commit.
|
||||
* GODT-2390: Add reports for uncaught json and net.opErr.
|
||||
* 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-2305: Detect missing gluon DB.
|
||||
* 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-2258: suggest email as login when signing in via status window.
|
||||
* 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
|
||||
|
||||
### 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-2046: Bridge-gui log is included in optional archive sent with bug reports.
|
||||
* 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-1502: Rebranding: color and radius.
|
||||
* GODT-1549: Add notification when address list changes.
|
||||
* GODT-1560: Dependecy licenses update and link.
|
||||
* GODT-1560: Dependency licenses update and link.
|
||||
|
||||
### Changed
|
||||
* 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-1340: Not crashing, user list updating in main thread.
|
||||
* 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-1210: Add "free user" banner.
|
||||
* 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-1431 Prevent watcher when not using disk on cache.
|
||||
* 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: moved cache related functions to separate file.
|
||||
* GODT-1332 moving cache does not work on Windows.
|
||||
@ -1233,7 +1369,7 @@ GODT-1537: Manual in-app update mechanism.
|
||||
### Fixed
|
||||
* GODT-1029 Fix tray icon not updating under certain conditions.
|
||||
* 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-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-870 Added GUI notification on error during silent update.
|
||||
* 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-874 Added manual triggers to Updater module.
|
||||
* GODT-851 Added support of UID EXPUNGE.
|
||||
@ -1647,7 +1783,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
|
||||
|
||||
### Changed
|
||||
* 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-387 Store factory to make store optional.
|
||||
* 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-95 Do not throw error when trying to create new mailbox in IMAP root.
|
||||
* 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).
|
||||
* Using go.enmime@v0.6.1 snapshot.
|
||||
* Better detection of non-auth-error.
|
||||
* 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.
|
||||
* #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.
|
||||
* #1050 pollNow waits for events to be processed.
|
||||
* #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.
|
||||
* Closing body after checking internet connection.
|
||||
* 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).
|
||||
* #888 TLS Cert.
|
||||
* 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).
|
||||
* #887 #883:
|
||||
* 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.
|
||||
* Panic handler for all goroutines.
|
||||
* 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.
|
||||
* #880, #884, #885, #886 fix of informing user about outgoing non-encrypted e-mail.
|
||||
* #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.
|
||||
* Fix aliases in split mode.
|
||||
* Fix decrypted data in log notification.
|
||||
@ -2074,7 +2210,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
|
||||
|
||||
### Changed
|
||||
* Fix custom message format.
|
||||
* #802 acumulated long lines while parsing body structure.
|
||||
* #802 accumulated long lines while parsing body structure.
|
||||
* Process `AddressEvent` before `MessageEvent`.
|
||||
* #791 updated crypto: fix wrong signature format.
|
||||
* #793 fix returning size.
|
||||
@ -2096,7 +2232,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
|
||||
|
||||
### Changed
|
||||
* #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.
|
||||
* #750 Status is using DB status instead of API.
|
||||
* 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.
|
||||
|
||||
### Changed
|
||||
* #720 only one concurent DB sync.
|
||||
* #720 only one concurrent DB sync.
|
||||
* #720 sync every 3 pages.
|
||||
* #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`.
|
||||
* Generate version files from main file.
|
||||
* 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.
|
||||
* Copyright 2019.
|
||||
* 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 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`.
|
||||
* 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.
|
||||
* #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).
|
||||
@ -2228,7 +2364,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
|
||||
* Start with new versioning.
|
||||
|
||||
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)
|
||||
`------- 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.
|
||||
* Moved to internal gitlab.
|
||||
* 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.
|
||||
|
||||
### Changed
|
||||
@ -2326,12 +2462,12 @@ CSB-331 Fix sending error due to mixed case in sender address.
|
||||
* Keychain format and function refactor.
|
||||
* Create crash file on panic with full trace.
|
||||
* 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.
|
||||
|
||||
### Added
|
||||
* 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
|
||||
* 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.
|
||||
* Checkbox and with label (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.
|
||||
* Input box style (used in bug report).
|
||||
* 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
|
||||
|
||||
# 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_FULL_NAME:=Proton Mail Bridge
|
||||
APP_VENDOR:=Proton AG
|
||||
@ -246,7 +246,7 @@ test-race: gofiles
|
||||
test-integration: gofiles
|
||||
mkdir -p coverage/integration
|
||||
go test \
|
||||
-v -timeout=60m -p=1 -count=1 \
|
||||
-v -timeout=60m -p=1 -count=1 -tags=test_integration \
|
||||
${GOCOVERAGE} \
|
||||
github.com/ProtonMail/proton-bridge/v3/tests \
|
||||
${GOCOVERDIR}/integration
|
||||
@ -258,6 +258,22 @@ test-integration-debug: gofiles
|
||||
test-integration-race: gofiles
|
||||
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:
|
||||
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
|
||||
@ -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/telemetry HeartbeatManager > internal/telemetry/mocks/mocks.go
|
||||
cp internal/telemetry/mocks/mocks.go internal/bridge/mocks/telemetry_mocks.go
|
||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/user MessageDownloader > internal/user/mocks/mocks.go
|
||||
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:
|
||||
./utils/missing_license.sh check
|
||||
@ -292,6 +322,12 @@ lint-golang:
|
||||
$(info linting with GOMAXPROCS=${GOMAXPROCS})
|
||||
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 -wait -cache -config utils/gobinsec_conf.yml ${EXE_TARGET} ${DEPLOY_DIR}/${TARGET_OS}/${LAUNCHER_EXE}
|
||||
|
||||
|
||||
@ -2,13 +2,13 @@
|
||||
|
||||
## 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
|
||||
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
|
||||
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
|
||||
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
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
end
|
||||
|
||||
@ -58,8 +58,8 @@ sequenceDiagram
|
||||
C ->> B: IMAP SELECT directory
|
||||
C ->> B: IMAP SEARCH e-mails UIDs
|
||||
C ->> B: IMAP FETCH of e-mail UID
|
||||
B ->> S: Fetch body and attachements
|
||||
Note right of B: Decrypt message<br/>and attachement
|
||||
B ->> S: Fetch body and attachments
|
||||
Note right of B: Decrypt message<br/>and attachment
|
||||
B ->> C: IMAP response
|
||||
end
|
||||
```
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
# 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
|
||||
* Manual in-app update
|
||||
* Manual install
|
||||
|
||||
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.
|
||||
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
|
||||
* 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
|
||||
|
||||
25
go.mod
25
go.mod
@ -5,10 +5,10 @@ go 1.20
|
||||
require (
|
||||
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
|
||||
github.com/Masterminds/semver/v3 v3.2.0
|
||||
github.com/ProtonMail/gluon v0.16.1-0.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-proton-api v0.4.1-0.20230727082922-9115b4750ec7
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20231030091225-8fc2478b27f4
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton
|
||||
github.com/PuerkitoBio/goquery v1.8.1
|
||||
github.com/abiosoft/ishell v2.0.0+incompatible
|
||||
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37
|
||||
@ -22,6 +22,7 @@ require (
|
||||
github.com/emersion/go-message v0.16.0
|
||||
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-vcard v0.0.0-20230331202150-f3d26859ccd3
|
||||
github.com/fatih/color v1.13.0
|
||||
github.com/getsentry/sentry-go v0.15.0
|
||||
github.com/go-resty/resty/v2 v2.7.0
|
||||
@ -42,17 +43,17 @@ require (
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.5
|
||||
go.uber.org/goleak v1.2.1
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
|
||||
golang.org/x/net v0.10.0
|
||||
golang.org/x/sys v0.8.0
|
||||
golang.org/x/text v0.9.0
|
||||
google.golang.org/grpc v1.53.0
|
||||
golang.org/x/net v0.17.0
|
||||
golang.org/x/sys v0.13.0
|
||||
golang.org/x/text v0.13.0
|
||||
google.golang.org/grpc v1.56.3
|
||||
google.golang.org/protobuf v1.30.0
|
||||
howett.net/plist v1.0.0
|
||||
)
|
||||
|
||||
require (
|
||||
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-srp v0.0.7 // 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/elastic/go-windows v1.0.1 // indirect
|
||||
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect
|
||||
github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3 // indirect
|
||||
github.com/felixge/fgprof v0.9.3 // indirect
|
||||
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
@ -79,7 +79,7 @@ require (
|
||||
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/gofrs/uuid v4.3.0+incompatible // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
||||
@ -108,12 +108,13 @@ require (
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||
gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a // 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/sync v0.2.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
|
||||
)
|
||||
|
||||
|
||||
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/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
|
||||
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
|
||||
github.com/ProtonMail/gluon v0.16.1-0.20230706110757-a9327fb18611 h1:QVydPr/+pgz5xihc2ujNNV+qnq3oTidIXvF0PgkcY6U=
|
||||
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 h1:sG0o5pEoS2z2jNR9zK7Juq5Tr3X+GfHmQ8L99RPowaE=
|
||||
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/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-20230322105811-d73448b7e800/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903 h1:ZK3C5DtzV2nVAQTx5S5jQvMeDqWtD1By5mOoyY/xJek=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230717121622-edf196117233 h1:bdoKdh0f66/lrgVfYlxw0aqISY/KOqXmFJyGt7rGmnc=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230717121622-edf196117233/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
||||
github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7 h1:+j+Kd/DyZ/qGfMT9htAT7HxqIEbZHsatsx+m8AoV6fc=
|
||||
github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20230727082922-9115b4750ec7 h1:Rmg3TPK6vFGNWR4hxmPoBhV75Sl716iB46wEi2U4Q+c=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20230727082922-9115b4750ec7/go.mod h1:+aTJoYu8bqzGECXL2DOdiZTZ64bGn3w0NC8VcFpJrFM=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20231030091225-8fc2478b27f4 h1:1XISoHi1FmaVW3vm/y5FuXmrSMo53U0sM3zZpgczWTc=
|
||||
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/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton h1:YS6M20yvjCJPR1r4ADW5TPn6rahs4iAyZaACei86bEc=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton/go.mod h1:S1lYsaGHykYpxxh2SnJL6ypcAlANKj5NRSY6HxKryKQ=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton h1:8tqHYM6IGsdEc6Vxf1TWiwpHNj8yIEQNACPhxsDagrk=
|
||||
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/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
|
||||
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/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.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||
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/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.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.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
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 v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
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/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
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.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
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-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.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.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
|
||||
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
||||
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||
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-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
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-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.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.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
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-20190226205417-e64efc72b421/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-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.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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
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-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.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
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.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/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.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||
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-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
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-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-20230221151758-ace64dc21148 h1:muK+gVBJBfFb4SejshDBlN2/UgxCCOKH9Y34ljqEGOc=
|
||||
google.golang.org/genproto v0.0.0-20230221151758-ace64dc21148/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw=
|
||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
|
||||
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.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
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.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
|
||||
google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc=
|
||||
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/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||
|
||||
@ -52,6 +52,9 @@ const (
|
||||
flagCPUProfile = "cpu-prof"
|
||||
flagCPUProfileShort = "p"
|
||||
|
||||
flagTraceProfile = "trace-prof"
|
||||
flagTraceProfileShort = "t"
|
||||
|
||||
flagMemProfile = "mem-prof"
|
||||
flagMemProfileShort = "m"
|
||||
|
||||
@ -96,6 +99,11 @@ func New() *cli.App {
|
||||
Aliases: []string{flagCPUProfileShort},
|
||||
Usage: "Generate CPU profile",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flagTraceProfile,
|
||||
Aliases: []string{flagTraceProfileShort},
|
||||
Usage: "Generate Trace profile",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flagMemProfile,
|
||||
Aliases: []string{flagMemProfileShort},
|
||||
@ -379,6 +387,11 @@ func withProfiler(c *cli.Context, fn func() error) error {
|
||||
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) {
|
||||
logrus.Debug("Running with memory profiling")
|
||||
defer profile.Start(profile.MemProfile, profile.MemProfileAllocs, profile.ProfilePath(".")).Stop()
|
||||
|
||||
@ -22,7 +22,6 @@ import (
|
||||
"path"
|
||||
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/certs"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
@ -45,23 +44,6 @@ func WithVault(locations *locations.Locations, panicHandler async.PanicHandler,
|
||||
"corrupt": corrupt,
|
||||
}).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).
|
||||
|
||||
return fn(encVault, insecure, corrupt)
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//go:build !build_qa
|
||||
//go:build !build_qa && !test_integration
|
||||
|
||||
package bridge
|
||||
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//go:build build_qa
|
||||
//go:build build_qa || test_integration
|
||||
|
||||
package bridge
|
||||
|
||||
|
||||
@ -23,7 +23,6 @@ import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
@ -38,8 +37,11 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"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/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/user"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
@ -59,7 +61,7 @@ type Bridge struct {
|
||||
// api manages user API clients.
|
||||
api *proton.Manager
|
||||
proxyCtl ProxyController
|
||||
identifier Identifier
|
||||
identifier identifier.Identifier
|
||||
|
||||
// tlsConfig holds the bridge TLS config used by the IMAP and SMTP servers.
|
||||
tlsConfig *tls.Config
|
||||
@ -125,9 +127,8 @@ type Bridge struct {
|
||||
// goHeartbeat triggers a check/sending if heartbeat is needed.
|
||||
goHeartbeat func()
|
||||
|
||||
uidValidityGenerator imap.UIDValidityGenerator
|
||||
|
||||
serverManager *ServerManager
|
||||
serverManager *imapsmtpserver.Service
|
||||
syncService *syncservice.Service
|
||||
}
|
||||
|
||||
// New creates a new bridge.
|
||||
@ -140,7 +141,7 @@ func New(
|
||||
|
||||
apiURL string, // the URL of the API 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
|
||||
roundTripper http.RoundTripper, // the round tripper to use for API requests
|
||||
proxyCtl ProxyController, // the DoH controller
|
||||
@ -207,7 +208,7 @@ func newBridge(
|
||||
reporter reporter.Reporter,
|
||||
|
||||
api *proton.Manager,
|
||||
identifier Identifier,
|
||||
identifier identifier.Identifier,
|
||||
proxyCtl ProxyController,
|
||||
uidValidityGenerator imap.UIDValidityGenerator,
|
||||
|
||||
@ -269,17 +270,26 @@ func newBridge(
|
||||
firstStart: firstStart,
|
||||
lastVersion: lastVersion,
|
||||
|
||||
tasks: tasks,
|
||||
|
||||
uidValidityGenerator: uidValidityGenerator,
|
||||
|
||||
serverManager: newServerManager(),
|
||||
tasks: tasks,
|
||||
syncService: syncservice.NewService(reporter, panicHandler),
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
bridge.syncService.Run(bridge.tasks)
|
||||
|
||||
return bridge, nil
|
||||
}
|
||||
|
||||
@ -407,11 +417,6 @@ func (bridge *Bridge) GetErrors() []error {
|
||||
func (bridge *Bridge) Close(ctx context.Context) {
|
||||
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.
|
||||
safe.Lock(func() {
|
||||
for _, user := range bridge.users {
|
||||
@ -419,6 +424,11 @@ func (bridge *Bridge) Close(ctx context.Context) {
|
||||
}
|
||||
}, 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.
|
||||
bridge.tasks.CancelAndWait()
|
||||
|
||||
@ -477,27 +487,15 @@ func (bridge *Bridge) remWatcher(watcher *watcher.Watcher[events.Event]) {
|
||||
watcher.Close()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) onStatusUp(ctx context.Context) {
|
||||
func (bridge *Bridge) onStatusUp(_ context.Context) {
|
||||
logrus.Info("Handling API status up")
|
||||
|
||||
safe.RLock(func() {
|
||||
for _, user := range bridge.users {
|
||||
user.OnStatusUp(ctx)
|
||||
}
|
||||
}, bridge.usersLock)
|
||||
|
||||
bridge.goLoad()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) onStatusDown(ctx context.Context) {
|
||||
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) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@ -527,24 +525,6 @@ func loadTLSConfig(vault *vault.Vault) (*tls.Config, error) {
|
||||
}, 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 {
|
||||
if a < b {
|
||||
return a
|
||||
|
||||
@ -44,6 +44,7 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/focus"
|
||||
"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/user"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
|
||||
@ -54,6 +55,7 @@ import (
|
||||
"github.com/emersion/go-sasl"
|
||||
"github.com/emersion/go-smtp"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/goleak"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -583,7 +585,7 @@ func TestBridge_MissingGluonStore(t *testing.T) {
|
||||
require.NoError(t, os.RemoveAll(gluonDir))
|
||||
|
||||
// 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()
|
||||
|
||||
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.
|
||||
userID, _, err := s.CreateUser("nokeys", []byte("password"))
|
||||
require.NoError(t, err)
|
||||
@ -641,10 +647,6 @@ func TestBridge_AddressWithoutKeys(t *testing.T) {
|
||||
// Remove the address keys.
|
||||
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.
|
||||
require.NoError(t, getErr(bridge.LoginFull(context.Background(), "nokeys", []byte("password"), nil, nil)))
|
||||
require.NoError(t, err)
|
||||
@ -700,10 +702,10 @@ func TestBridge_InitGluonDirectory(t *testing.T) {
|
||||
configDir, err := b.GetGluonDataDir()
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = os.ReadDir(bridge.ApplyGluonCachePathSuffix(b.GetGluonCacheDir()))
|
||||
_, err = os.ReadDir(imapsmtpserver.ApplyGluonCachePathSuffix(b.GetGluonCacheDir()))
|
||||
require.False(t, os.IsNotExist(err))
|
||||
|
||||
_, err = os.ReadDir(bridge.ApplyGluonConfigPathSuffix(configDir))
|
||||
_, err = os.ReadDir(imapsmtpserver.ApplyGluonConfigPathSuffix(configDir))
|
||||
require.False(t, os.IsNotExist(err))
|
||||
})
|
||||
})
|
||||
@ -776,16 +778,16 @@ func TestBridge_ChangeCacheDirectory(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Old store should no more exists.
|
||||
_, err = os.ReadDir(bridge.ApplyGluonCachePathSuffix(currentCacheDir))
|
||||
_, err = os.ReadDir(imapsmtpserver.ApplyGluonCachePathSuffix(currentCacheDir))
|
||||
require.True(t, os.IsNotExist(err))
|
||||
// Database should not have changed.
|
||||
_, err = os.ReadDir(bridge.ApplyGluonConfigPathSuffix(configDir))
|
||||
_, err = os.ReadDir(imapsmtpserver.ApplyGluonConfigPathSuffix(configDir))
|
||||
require.False(t, os.IsNotExist(err))
|
||||
|
||||
// New path should have Gluon sub-folder.
|
||||
require.Equal(t, filepath.Join(newCacheDir, "gluon"), b.GetGluonCacheDir())
|
||||
// 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))
|
||||
|
||||
// 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.
|
||||
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...)
|
||||
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()) {
|
||||
outCh := make(chan Out)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
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))
|
||||
}
|
||||
|
||||
outCh <- out
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
|
||||
case outCh <- out:
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return outCh, done
|
||||
return outCh, func() {
|
||||
cancel()
|
||||
done()
|
||||
}
|
||||
}
|
||||
|
||||
type eventWaiter struct {
|
||||
|
||||
@ -19,6 +19,7 @@ package bridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
@ -33,63 +34,133 @@ const (
|
||||
DefaultMaxSessionCountForBugReport = 10
|
||||
)
|
||||
|
||||
func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, description, username, email, client string, attachLogs bool) error {
|
||||
var account string
|
||||
type ReportBugReq struct {
|
||||
OSType string
|
||||
OSVersion string
|
||||
Title string
|
||||
Description string
|
||||
Username string
|
||||
Email string
|
||||
EmailClient string
|
||||
IncludeLogs bool
|
||||
}
|
||||
|
||||
if info, err := bridge.QueryUserInfo(username); err == nil {
|
||||
account = info.Username
|
||||
func (bridge *Bridge) ReportBug(ctx context.Context, report *ReportBugReq) error {
|
||||
if info, err := bridge.QueryUserInfo(report.Username); err == nil {
|
||||
report.Username = info.Username
|
||||
} else if userIDs := bridge.GetUserIDs(); len(userIDs) > 0 {
|
||||
if err := bridge.vault.GetUser(userIDs[0], func(user *vault.User) {
|
||||
account = user.Username()
|
||||
report.Username = user.Username()
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var attachment []proton.ReportBugAttachment
|
||||
|
||||
if attachLogs {
|
||||
logsPath, err := bridge.locator.ProvideLogsPath()
|
||||
var attachments []proton.ReportBugAttachment
|
||||
if report.IncludeLogs {
|
||||
logs, err := bridge.CollectLogs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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,
|
||||
})
|
||||
attachments = append(attachments, logs)
|
||||
}
|
||||
|
||||
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 {
|
||||
user.ReportBugSent()
|
||||
}
|
||||
}, bridge.usersLock)
|
||||
|
||||
return bridge.api.ReportBug(ctx, proton.ReportBugReq{
|
||||
OS: osType,
|
||||
OSVersion: osVersion,
|
||||
// if we have a token we can append more attachment to the bugReport
|
||||
for i, att := range attachments {
|
||||
if i == 0 && report.IncludeLogs {
|
||||
continue
|
||||
}
|
||||
err := bridge.appendComment(ctx, token, att)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
Title: "[Bridge] Bug",
|
||||
Description: description,
|
||||
func (bridge *Bridge) CollectLogs() (proton.ReportBugAttachment, error) {
|
||||
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,
|
||||
ClientVersion: constants.AppVersion(bridge.curVersion.Original()),
|
||||
|
||||
Username: account,
|
||||
Email: email,
|
||||
}, attachment...)
|
||||
Username: report.Username,
|
||||
Email: report.Email,
|
||||
|
||||
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() {
|
||||
safe.Lock(func() {
|
||||
safe.RLock(func() {
|
||||
for _, user := range bridge.users {
|
||||
user.ReportBugClicked()
|
||||
}
|
||||
@ -30,7 +30,7 @@ func (bridge *Bridge) ReportBugClicked() {
|
||||
}
|
||||
|
||||
func (bridge *Bridge) AutoconfigUsed(client string) {
|
||||
safe.Lock(func() {
|
||||
safe.RLock(func() {
|
||||
for _, user := range bridge.users {
|
||||
user.AutoconfigUsed(client)
|
||||
}
|
||||
@ -38,7 +38,7 @@ func (bridge *Bridge) AutoconfigUsed(client string) {
|
||||
}
|
||||
|
||||
func (bridge *Bridge) KBArticleOpened(article string) {
|
||||
safe.Lock(func() {
|
||||
safe.RLock(func() {
|
||||
for _, user := range bridge.users {
|
||||
user.KBArticleOpened(article)
|
||||
}
|
||||
|
||||
@ -79,7 +79,7 @@ func (bridge *Bridge) CheckClientState(ctx context.Context, checkFlags bool, pro
|
||||
}
|
||||
|
||||
log.Debug("Building state")
|
||||
state, err := meta.BuildMailboxToMessageMap(usr)
|
||||
state, err := meta.BuildMailboxToMessageMap(ctx, usr)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Failed to build state")
|
||||
return result, err
|
||||
@ -230,7 +230,7 @@ func (bridge *Bridge) DebugDownloadFailedMessages(
|
||||
}
|
||||
|
||||
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 {
|
||||
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 (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/gluon"
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
imapEvents "github.com/ProtonMail/gluon/events"
|
||||
"github.com/ProtonMail/gluon/imap"
|
||||
"github.com/ProtonMail/gluon/reporter"
|
||||
"github.com/ProtonMail/gluon/store"
|
||||
"github.com/ProtonMail/gluon/store/fallback_v0"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapsmtpserver"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
@ -45,16 +34,6 @@ func (bridge *Bridge) restartIMAP(ctx context.Context) error {
|
||||
return bridge.serverManager.RestartIMAP(ctx)
|
||||
}
|
||||
|
||||
// addIMAPUser connects the given user to gluon.
|
||||
func (bridge *Bridge) addIMAPUser(ctx context.Context, user *user.User) error {
|
||||
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) {
|
||||
switch event := event.(type) {
|
||||
case imapEvents.UserAdded:
|
||||
@ -92,108 +71,59 @@ func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) {
|
||||
}
|
||||
}
|
||||
|
||||
func ApplyGluonCachePathSuffix(basePath string) string {
|
||||
return filepath.Join(basePath, "backend", "store")
|
||||
type bridgeIMAPSettings struct {
|
||||
b *Bridge
|
||||
}
|
||||
|
||||
func ApplyGluonConfigPathSuffix(basePath string) string {
|
||||
return filepath.Join(basePath, "backend", "db")
|
||||
func (b *bridgeIMAPSettings) EventPublisher() imapsmtpserver.IMAPEventPublisher {
|
||||
return b
|
||||
}
|
||||
|
||||
func newIMAPServer(
|
||||
gluonCacheDir, gluonConfigDir string,
|
||||
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)
|
||||
func (b *bridgeIMAPSettings) TLSConfig() *tls.Config {
|
||||
return b.b.tlsConfig
|
||||
}
|
||||
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"gluonStore": gluonCacheDir,
|
||||
"gluonDB": gluonConfigDir,
|
||||
"version": version,
|
||||
"logClient": logClient,
|
||||
"logServer": logServer,
|
||||
}).Info("Creating IMAP server")
|
||||
func (b *bridgeIMAPSettings) LogClient() bool {
|
||||
return b.b.logIMAPClient
|
||||
}
|
||||
|
||||
if logClient || logServer {
|
||||
log := logrus.WithField("protocol", "IMAP")
|
||||
log.Warning("================================================")
|
||||
log.Warning("THIS LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA")
|
||||
log.Warning("================================================")
|
||||
func (b *bridgeIMAPSettings) LogServer() bool {
|
||||
return b.b.logIMAPServer
|
||||
}
|
||||
|
||||
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) {
|
||||
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{}))
|
||||
defer done()
|
||||
|
||||
|
||||
@ -467,7 +467,9 @@ SGVsbG8gd29ybGQK
|
||||
require.Eventually(t, func() bool {
|
||||
messages, err := clientFetch(senderIMAPClient, `Sent`, imap.FetchBodyStructure)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 4, len(messages))
|
||||
if len(messages) != 4 {
|
||||
return false
|
||||
}
|
||||
|
||||
// messages may not be in order
|
||||
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 (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"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/vault"
|
||||
"github.com/sirupsen/logrus"
|
||||
@ -131,26 +130,41 @@ func (bridge *Bridge) GetGluonDataDir() (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)
|
||||
}
|
||||
|
||||
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 {
|
||||
return bridge.vault.GetProxyAllowed()
|
||||
}
|
||||
@ -318,16 +332,3 @@ func (bridge *Bridge) FactoryReset(ctx context.Context) {
|
||||
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/server"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"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) {
|
||||
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) {
|
||||
|
||||
@ -21,43 +21,37 @@ import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
||||
"github.com/emersion/go-sasl"
|
||||
"github.com/emersion/go-smtp"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/identifier"
|
||||
)
|
||||
|
||||
func (bridge *Bridge) restartSMTP(ctx context.Context) error {
|
||||
return bridge.serverManager.RestartSMTP(ctx)
|
||||
}
|
||||
|
||||
func newSMTPServer(bridge *Bridge, tlsConfig *tls.Config, logSMTP bool) *smtp.Server {
|
||||
logrus.WithField("logSMTP", logSMTP).Info("Creating SMTP server")
|
||||
|
||||
smtpServer := smtp.NewServer(&smtpBackend{Bridge: bridge})
|
||||
|
||||
smtpServer.TLSConfig = tlsConfig
|
||||
smtpServer.Domain = constants.Host
|
||||
smtpServer.AllowInsecureAuth = true
|
||||
smtpServer.MaxLineLength = 1 << 16
|
||||
smtpServer.ErrorLog = logging.NewSMTPLogger()
|
||||
|
||||
// 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 {
|
||||
return sasl.NewLoginServer(func(username, password string) error {
|
||||
return conn.Session().AuthPlain(username, password)
|
||||
})
|
||||
})
|
||||
|
||||
if logSMTP {
|
||||
log := logrus.WithField("protocol", "SMTP")
|
||||
log.Warning("================================================")
|
||||
log.Warning("THIS LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA")
|
||||
log.Warning("================================================")
|
||||
|
||||
smtpServer.Debug = logging.NewSMTPDebugLogger()
|
||||
}
|
||||
|
||||
return smtpServer
|
||||
type bridgeSMTPSettings struct {
|
||||
b *Bridge
|
||||
}
|
||||
|
||||
func (b *bridgeSMTPSettings) TLSConfig() *tls.Config {
|
||||
return b.b.tlsConfig
|
||||
}
|
||||
|
||||
func (b *bridgeSMTPSettings) Log() bool {
|
||||
return b.b.logSMTP
|
||||
}
|
||||
|
||||
func (b *bridgeSMTPSettings) Port() int {
|
||||
return b.b.vault.GetSMTPPort()
|
||||
}
|
||||
|
||||
func (b *bridgeSMTPSettings) SetPort(i int) error {
|
||||
return b.b.vault.SetSMTPPort(i)
|
||||
}
|
||||
|
||||
func (b *bridgeSMTPSettings) UseSSL() bool {
|
||||
return b.b.vault.GetSMTPSSL()
|
||||
}
|
||||
|
||||
func (b *bridgeSMTPSettings) Identifier() identifier.UserAgentUpdater {
|
||||
return &bridgeUserAgentUpdater{Bridge: b.b}
|
||||
}
|
||||
|
||||
@ -21,9 +21,11 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
@ -35,6 +37,7 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice"
|
||||
"github.com/bradenaw/juniper/iterator"
|
||||
"github.com/bradenaw/juniper/stream"
|
||||
"github.com/bradenaw/juniper/xslices"
|
||||
@ -252,14 +255,17 @@ func TestBridge_SyncWithOngoingEvents(t *testing.T) {
|
||||
|
||||
// Login the user; its sync should fail.
|
||||
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{}))
|
||||
defer done()
|
||||
syncFailedCh, syncFailedDone := chToType[events.Event, events.SyncFailed](b.GetEvents(events.SyncFailed{}))
|
||||
defer syncFailedDone()
|
||||
|
||||
userID, err := b.LoginFull(ctx, "imap", password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, userID, (<-syncCh).UserID)
|
||||
require.Equal(t, userID, (<-syncFailedCh).UserID)
|
||||
|
||||
info, err := b.GetUserInfo(userID)
|
||||
require.NoError(t, err)
|
||||
@ -282,11 +288,7 @@ func TestBridge_SyncWithOngoingEvents(t *testing.T) {
|
||||
|
||||
// Remove the network limit, allowing the sync to finish.
|
||||
netCtl.SetReadLimit(0)
|
||||
|
||||
{
|
||||
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
|
||||
defer done()
|
||||
|
||||
require.Equal(t, userID, (<-syncCh).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)))
|
||||
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.
|
||||
require.Eventually(t, func() bool {
|
||||
status, err := client.Select(`Folders/folder2`, true)
|
||||
@ -321,6 +317,330 @@ func TestBridge_SyncWithOngoingEvents(t *testing.T) {
|
||||
}, 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
|
||||
m := proton.New(
|
||||
proton.WithHostURL(s.GetHostURL()),
|
||||
|
||||
@ -31,7 +31,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Disabled due to flakyness.
|
||||
// Disabled due to flakiness.
|
||||
func _TestBridge_SyncExistsWithErrorWhenTooManyFilesAreOpen(t *testing.T) { //nolint:unused
|
||||
var rlimitCurrent syscall.Rlimit
|
||||
|
||||
|
||||
@ -32,15 +32,7 @@ type Locator interface {
|
||||
GetLicenseFilePath() string
|
||||
GetDependencyLicensesLink() string
|
||||
Clear(...string) error
|
||||
}
|
||||
|
||||
type Identifier interface {
|
||||
GetUserAgent() string
|
||||
HasClient() bool
|
||||
SetClient(name, version string)
|
||||
SetPlatform(platform string)
|
||||
SetClientString(client string)
|
||||
GetClientString() string
|
||||
ProvideIMAPSyncConfigPath() (string, error)
|
||||
}
|
||||
|
||||
type ProxyController interface {
|
||||
|
||||
@ -30,6 +30,7 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
||||
"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/user"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
@ -45,6 +46,8 @@ const (
|
||||
Connected
|
||||
)
|
||||
|
||||
var ErrFailedToUnlock = errors.New("failed to unlock user keys")
|
||||
|
||||
type UserInfo struct {
|
||||
// UserID is the user's API ID.
|
||||
UserID string
|
||||
@ -65,10 +68,10 @@ type UserInfo struct {
|
||||
BridgePass []byte
|
||||
|
||||
// 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 int
|
||||
MaxSpace uint64
|
||||
}
|
||||
|
||||
// GetUserIDs returns the IDs of all known users (authorized or not).
|
||||
@ -156,11 +159,15 @@ func (bridge *Bridge) LoginUser(
|
||||
func() (string, error) {
|
||||
return bridge.loginUser(ctx, client, auth.UID, auth.RefreshToken, keyPass)
|
||||
},
|
||||
func() error {
|
||||
return client.AuthDelete(ctx)
|
||||
},
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@ -216,7 +223,16 @@ func (bridge *Bridge) LoginFull(
|
||||
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.
|
||||
@ -243,6 +259,11 @@ func (bridge *Bridge) LogoutUser(ctx context.Context, userID string) error {
|
||||
func (bridge *Bridge) DeleteUser(ctx context.Context, userID string) error {
|
||||
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 {
|
||||
if !bridge.vault.HasUser(userID) {
|
||||
return ErrNoSuchUser
|
||||
@ -252,6 +273,10 @@ func (bridge *Bridge) DeleteUser(ctx context.Context, userID string) error {
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
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{
|
||||
UserID: userID,
|
||||
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 {
|
||||
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()
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
if err := bridge.addIMAPUser(ctx, user); err != nil {
|
||||
return fmt.Errorf("failed to add IMAP user: %w", err)
|
||||
}
|
||||
|
||||
user.BadEventFeedbackResync(ctx)
|
||||
|
||||
return nil
|
||||
return user.BadEventFeedbackResync(ctx)
|
||||
}
|
||||
|
||||
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 {
|
||||
return "", fmt.Errorf("failed to unlock user keys: %w", err)
|
||||
return "", fmt.Errorf("%w: %w", ErrFailedToUnlock, err)
|
||||
} 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 {
|
||||
@ -524,6 +535,11 @@ func (bridge *Bridge) addUserWithVault(
|
||||
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(
|
||||
ctx,
|
||||
vault,
|
||||
@ -535,16 +551,16 @@ func (bridge *Bridge) addUserWithVault(
|
||||
bridge.vault.GetMaxSyncMemory(),
|
||||
statsPath,
|
||||
bridge,
|
||||
bridge.serverManager,
|
||||
bridge.serverManager,
|
||||
&bridgeEventSubscription{b: bridge},
|
||||
bridge.syncService,
|
||||
syncSettingsPath,
|
||||
)
|
||||
if err != nil {
|
||||
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.
|
||||
// For example, if the user's addresses change, we need to update them in gluon.
|
||||
bridge.tasks.Once(func(ctx context.Context) {
|
||||
@ -554,11 +570,8 @@ func (bridge *Bridge) addUserWithVault(
|
||||
"event": event,
|
||||
}).Debug("Received user event")
|
||||
|
||||
if err := bridge.handleUserEvent(ctx, user, event); err != nil {
|
||||
logrus.WithError(err).Error("Failed to handle user event")
|
||||
} else {
|
||||
bridge.publish(event)
|
||||
}
|
||||
bridge.handleUserEvent(ctx, user, event)
|
||||
bridge.publish(event)
|
||||
})
|
||||
})
|
||||
|
||||
@ -609,10 +622,6 @@ func (bridge *Bridge) logoutUser(ctx context.Context, user *user.User, withAPI,
|
||||
"withData": withData,
|
||||
}).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 {
|
||||
logrus.WithError(err).Error("Failed to logout user")
|
||||
}
|
||||
|
||||
@ -19,44 +19,17 @@ package bridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/ProtonMail/gluon/reporter"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
"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) {
|
||||
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:
|
||||
bridge.handleUserDeauth(ctx, user)
|
||||
|
||||
@ -66,102 +39,6 @@ func (bridge *Bridge) handleUserEvent(ctx context.Context, user *user.User, even
|
||||
case events.UncategorizedEventError:
|
||||
bridge.handleUncategorizedErrorEvent(event)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bridge *Bridge) handleUserAddressCreated(ctx context.Context, user *user.User, event events.UserAddressCreated) error {
|
||||
if user.GetAddressMode() == vault.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) {
|
||||
@ -171,8 +48,8 @@ func (bridge *Bridge) handleUserDeauth(ctx context.Context, user *user.User) {
|
||||
}, bridge.usersLock)
|
||||
}
|
||||
|
||||
func (bridge *Bridge) handleUserBadEvent(_ context.Context, user *user.User, event events.UserBadEvent) {
|
||||
safe.Lock(func() {
|
||||
func (bridge *Bridge) handleUserBadEvent(ctx context.Context, user *user.User, event events.UserBadEvent) {
|
||||
safe.RLock(func() {
|
||||
if rerr := bridge.reporter.ReportMessageWithContext("Failed to handle event", reporter.Context{
|
||||
"user_id": user.ID(),
|
||||
"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")
|
||||
}
|
||||
|
||||
user.CancelSyncAndEventPoll()
|
||||
|
||||
// Disable IMAP user
|
||||
if err := bridge.removeIMAPUser(context.Background(), user, false); err != nil {
|
||||
logrus.WithError(err).Error("Failed to remove IMAP user")
|
||||
}
|
||||
user.OnBadEvent(ctx)
|
||||
}, bridge.usersLock)
|
||||
}
|
||||
|
||||
|
||||
@ -23,71 +23,200 @@ package certs
|
||||
#import <Foundation/Foundation.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) {
|
||||
return errSecInvalidData;
|
||||
}
|
||||
|
||||
NSData *der = [NSData dataWithBytes:bytes length:length];
|
||||
|
||||
// Step 1. Import the certificate in the keychain.
|
||||
SecCertificateRef cert = SecCertificateCreateWithData(NULL, (CFDataRef) der);
|
||||
NSDictionary* addQuery = @{
|
||||
(id)kSecValueRef: (__bridge id) cert,
|
||||
(id)kSecClass: (id)kSecClassCertificate,
|
||||
};
|
||||
|
||||
OSStatus status = SecItemAdd((__bridge CFDictionaryRef) addQuery, NULL);
|
||||
if ((errSecSuccess != status) && (errSecDuplicateItem != status)) {
|
||||
CFRelease(cert);
|
||||
return status;
|
||||
}
|
||||
|
||||
// Step 2. Set the trust for the certificate.
|
||||
SecPolicyRef policy = SecPolicyCreateSSL(true, NULL); // we limit our trust to SSL
|
||||
NSDictionary *trustSettings = @{
|
||||
(id)kSecTrustSettingsResult: [NSNumber numberWithInt:kSecTrustSettingsResultTrustRoot],
|
||||
(id)kSecTrustSettingsPolicy: (__bridge id) policy,
|
||||
};
|
||||
status = SecTrustSettingsSetTrustSettings(cert, kSecTrustSettingsDomainUser, (__bridge CFTypeRef)(trustSettings));
|
||||
CFRelease(policy);
|
||||
CFRelease(cert);
|
||||
|
||||
return status;
|
||||
//****************************************************************************************************************************************************
|
||||
/// \brief Create a certificate object from DER-encoded data.
|
||||
///
|
||||
/// \return The certifcation. The caller is responsible for releasing the object using CFRelease.
|
||||
/// \return NULL if data is not a valid DER-encoded certificate.
|
||||
//****************************************************************************************************************************************************
|
||||
SecCertificateRef certFromData(char const* data, uint64_t length) {
|
||||
NSData *der = [NSData dataWithBytes:data length:length];
|
||||
return SecCertificateCreateWithData(NULL, (__bridge CFDataRef)der);
|
||||
}
|
||||
|
||||
|
||||
int removeTrustedCert(char const *bytes, unsigned long long length) {
|
||||
if (0 == length) {
|
||||
return errSecInvalidData;
|
||||
}
|
||||
//****************************************************************************************************************************************************
|
||||
/// \brief Check if a certificate is in the user's keychain.
|
||||
///
|
||||
/// \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,
|
||||
(id)kSecMatchItemList: @[(__bridge id)cert],
|
||||
(id)kSecMatchLimit: (id)kSecMatchLimitOne,
|
||||
};
|
||||
status = SecItemDelete((__bridge CFDictionaryRef) query);
|
||||
//****************************************************************************************************************************************************
|
||||
/// \brief Add a certificate to the user's keychain.
|
||||
///
|
||||
/// \param[in] cert The certificate.
|
||||
/// \return The status for the operation.
|
||||
//****************************************************************************************************************************************************
|
||||
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"
|
||||
@ -119,6 +248,116 @@ func certPEMToDER(certPEM []byte) ([]byte, error) {
|
||||
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 {
|
||||
certDER, err := certPEMToDER(certPEM)
|
||||
if err != nil {
|
||||
@ -127,18 +366,24 @@ func installCert(certPEM []byte) error {
|
||||
|
||||
p := C.CBytes(certDER)
|
||||
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)))
|
||||
switch errCode {
|
||||
case errSecSuccess:
|
||||
return nil
|
||||
case errAuthorizationCanceled:
|
||||
return fmt.Errorf("the user cancelled the authorization dialog")
|
||||
default:
|
||||
return fmt.Errorf("could not install certification into keychain (error %v)", errCode)
|
||||
if !isCertInKeychainCGo(buffer, size) {
|
||||
if err := addCertToKeychainCGo(buffer, size); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
certDER, err := certPEMToDER(certPEM)
|
||||
if err != nil {
|
||||
@ -147,10 +392,32 @@ func uninstallCert(certPEM []byte) error {
|
||||
|
||||
p := C.CBytes(certDER)
|
||||
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 {
|
||||
return fmt.Errorf("could not install certificate from keychain (error %v)", errCode)
|
||||
if isCertTrustedCGo(buffer, size) {
|
||||
if err := removeCertTrustCGo(buffer, size); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if isCertInKeychainCGo(buffer, size) {
|
||||
return removeCertFromKeychainCGo(buffer, size)
|
||||
}
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
// This test implies human interactions to enter password and is disabled by default.
|
||||
func _TestTrustedCertsDarwin(t *testing.T) { //nolint:unused
|
||||
func TestCertInKeychain(t *testing.T) {
|
||||
// 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()
|
||||
require.NoError(t, err)
|
||||
|
||||
certPEM, _, err := GenerateCert(template)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Error(t, installCert([]byte{0})) // Cannot install an invalid cert.
|
||||
require.Error(t, uninstallCert(certPEM)) // Cannot uninstall a cert that is not installed.
|
||||
require.NoError(t, installCert(certPEM)) // Can install a valid cert.
|
||||
require.NoError(t, installCert(certPEM)) // Can install an already installed cert.
|
||||
require.NoError(t, uninstallCert(certPEM)) // Can uninstall an installed cert.
|
||||
require.Error(t, uninstallCert(certPEM)) // Cannot uninstall an already uninstalled cert.
|
||||
require.NoError(t, installCert(certPEM)) // Can reinstall an uninstalled cert.
|
||||
require.NoError(t, uninstallCert(certPEM)) // Can uninstall a reinstalled cert.
|
||||
return certPEM
|
||||
}
|
||||
|
||||
@ -24,3 +24,7 @@ func installCert([]byte) error {
|
||||
func uninstallCert([]byte) error {
|
||||
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 {
|
||||
return nil // NOTE(GODT-986): Uninstall certs from root cert store?
|
||||
}
|
||||
|
||||
func isCertInstalled([]byte) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
@ -17,16 +17,50 @@
|
||||
|
||||
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 {
|
||||
return &Installer{}
|
||||
return &Installer{
|
||||
log: logrus.WithField("pkg", "certs"),
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
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()
|
||||
}
|
||||
|
||||
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 {
|
||||
status.DataLock.RLock()
|
||||
defer status.DataLock.RUnlock()
|
||||
|
||||
@ -19,7 +19,6 @@ package configstatus
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ConfigAbortValues struct {
|
||||
@ -41,17 +40,20 @@ type ConfigAbortData 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{
|
||||
MeasurementGroup: "bridge.any.configuration",
|
||||
Event: "bridge_config_abort",
|
||||
Values: ConfigSuccessValues{
|
||||
Duration: int(time.Since(data.DataV1.PendingSince).Minutes()),
|
||||
Duration: config.isPendingSinceMin(),
|
||||
},
|
||||
Dimensions: ConfigSuccessDimensions{
|
||||
ReportClick: strconv.FormatBool(data.DataV1.ReportClick),
|
||||
ReportSent: strconv.FormatBool(data.DataV1.ReportSent),
|
||||
ClickedLink: data.clickedLinkToString(),
|
||||
ReportClick: strconv.FormatBool(config.Data.DataV1.ReportClick),
|
||||
ReportSent: strconv.FormatBool(config.Data.DataV1.ReportSent),
|
||||
ClickedLink: config.Data.clickedLinkToString(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,7 +33,7 @@ func TestConfigurationAbort_default(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
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_config_abort", req.Event)
|
||||
@ -64,7 +64,7 @@ func TestConfigurationAbort_fed(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
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_config_abort", req.Event)
|
||||
|
||||
@ -33,13 +33,16 @@ type ConfigProgressData 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{
|
||||
MeasurementGroup: "bridge.any.configuration",
|
||||
Event: "bridge_config_progress",
|
||||
Values: ConfigProgressValues{
|
||||
NbDay: numberOfDay(time.Now(), data.DataV1.PendingSince),
|
||||
NbDaySinceLast: numberOfDay(time.Now(), data.DataV1.LastProgress),
|
||||
NbDay: numberOfDay(time.Now(), config.Data.DataV1.PendingSince),
|
||||
NbDaySinceLast: numberOfDay(time.Now(), config.Data.DataV1.LastProgress),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,7 +33,7 @@ func TestConfigurationProgress_default(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
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_config_progress", req.Event)
|
||||
@ -62,7 +62,7 @@ func TestConfigurationProgress_fed(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
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_config_progress", req.Event)
|
||||
|
||||
@ -19,7 +19,6 @@ package configstatus
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ConfigRecoveryValues struct {
|
||||
@ -43,19 +42,22 @@ type ConfigRecoveryData 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{
|
||||
MeasurementGroup: "bridge.any.configuration",
|
||||
Event: "bridge_config_recovery",
|
||||
Values: ConfigRecoveryValues{
|
||||
Duration: int(time.Since(data.DataV1.PendingSince).Minutes()),
|
||||
Duration: config.isPendingSinceMin(),
|
||||
},
|
||||
Dimensions: ConfigRecoveryDimensions{
|
||||
Autoconf: data.DataV1.Autoconf,
|
||||
ReportClick: strconv.FormatBool(data.DataV1.ReportClick),
|
||||
ReportSent: strconv.FormatBool(data.DataV1.ReportSent),
|
||||
ClickedLink: data.clickedLinkToString(),
|
||||
FailureDetails: data.DataV1.FailureDetails,
|
||||
Autoconf: config.Data.DataV1.Autoconf,
|
||||
ReportClick: strconv.FormatBool(config.Data.DataV1.ReportClick),
|
||||
ReportSent: strconv.FormatBool(config.Data.DataV1.ReportSent),
|
||||
ClickedLink: config.Data.clickedLinkToString(),
|
||||
FailureDetails: config.Data.DataV1.FailureDetails,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,7 +33,7 @@ func TestConfigurationRecovery_default(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
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_config_recovery", req.Event)
|
||||
@ -66,7 +66,7 @@ func TestConfigurationRecovery_fed(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
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_config_recovery", req.Event)
|
||||
|
||||
@ -19,7 +19,6 @@ package configstatus
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ConfigSuccessValues struct {
|
||||
@ -42,18 +41,21 @@ type ConfigSuccessData 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{
|
||||
MeasurementGroup: "bridge.any.configuration",
|
||||
Event: "bridge_config_success",
|
||||
Values: ConfigSuccessValues{
|
||||
Duration: int(time.Since(data.DataV1.PendingSince).Minutes()),
|
||||
Duration: config.isPendingSinceMin(),
|
||||
},
|
||||
Dimensions: ConfigSuccessDimensions{
|
||||
Autoconf: data.DataV1.Autoconf,
|
||||
ReportClick: strconv.FormatBool(data.DataV1.ReportClick),
|
||||
ReportSent: strconv.FormatBool(data.DataV1.ReportSent),
|
||||
ClickedLink: data.clickedLinkToString(),
|
||||
Autoconf: config.Data.DataV1.Autoconf,
|
||||
ReportClick: strconv.FormatBool(config.Data.DataV1.ReportClick),
|
||||
ReportSent: strconv.FormatBool(config.Data.DataV1.ReportSent),
|
||||
ClickedLink: config.Data.clickedLinkToString(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,7 +33,7 @@ func TestConfigurationSuccess_default(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
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_config_success", req.Event)
|
||||
@ -65,7 +65,7 @@ func TestConfigurationSuccess_fed(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
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_config_success", req.Event)
|
||||
|
||||
@ -17,7 +17,13 @@
|
||||
|
||||
package events
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
"github.com/ProtonMail/gluon/watcher"
|
||||
)
|
||||
|
||||
type Event interface {
|
||||
fmt.Stringer
|
||||
@ -28,3 +34,30 @@ type Event interface {
|
||||
type eventBase struct{}
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
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 {
|
||||
eventBase
|
||||
|
||||
|
||||
@ -175,7 +175,7 @@ type UsedSpaceChanged struct {
|
||||
|
||||
UserID string
|
||||
|
||||
UsedSpace int
|
||||
UsedSpace uint64
|
||||
}
|
||||
|
||||
func (event UsedSpaceChanged) String() string {
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
// 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
|
||||
package files
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@ -24,7 +24,7 @@ import (
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func moveDir(from, to string) error {
|
||||
func MoveDir(from, to string) error {
|
||||
entries, err := os.ReadDir(from)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -36,7 +36,7 @@ func moveDir(from, to string) error {
|
||||
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
|
||||
}
|
||||
|
||||
@ -61,12 +61,12 @@ func moveFile(from, to string) error {
|
||||
return os.Rename(from, to)
|
||||
}
|
||||
|
||||
func copyDir(from, to string) error {
|
||||
func CopyDir(from, to string) error {
|
||||
entries, err := os.ReadDir(from)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := createIfNotExists(to, 0o700); err != nil {
|
||||
if err := CreateIfNotExists(to, 0o700); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, entry := range entries {
|
||||
@ -74,11 +74,11 @@ func copyDir(from, to string) error {
|
||||
destPath := filepath.Join(to, entry.Name())
|
||||
|
||||
if entry.IsDir() {
|
||||
if err := copyDir(sourcePath, destPath); err != nil {
|
||||
if err := CopyDir(sourcePath, destPath); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := copyFile(sourcePath, destPath); err != nil {
|
||||
if err := CopyFile(sourcePath, destPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@ -86,7 +86,7 @@ func copyDir(from, to string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyFile(srcFile, dstFile string) error {
|
||||
func CopyFile(srcFile, dstFile string) error {
|
||||
out, err := os.Create(filepath.Clean(dstFile))
|
||||
defer func(out *os.File) {
|
||||
_ = out.Close()
|
||||
@ -113,7 +113,7 @@ func copyFile(srcFile, dstFile string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func exists(filePath string) bool {
|
||||
func Exists(filePath string) bool {
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
@ -121,8 +121,8 @@ func exists(filePath string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func createIfNotExists(dir string, perm os.FileMode) error {
|
||||
if exists(dir) {
|
||||
func CreateIfNotExists(dir string, perm os.FileMode) error {
|
||||
if Exists(dir) {
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
// 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
|
||||
package files
|
||||
|
||||
import (
|
||||
"os"
|
||||
@ -41,7 +41,7 @@ func TestMoveDir(t *testing.T) {
|
||||
}
|
||||
|
||||
// Move the files.
|
||||
if err := moveDir(from, to); err != nil {
|
||||
if err := MoveDir(from, to); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@ -42,6 +42,7 @@ void GRPCQtProxy::connectSignals() {
|
||||
connect(this, &GRPCQtProxy::setIsTelemetryDisabledReceived, &settingsTab, &SettingsTab::setIsTelemetryDisabled);
|
||||
connect(this, &GRPCQtProxy::setColorSchemeNameReceived, &settingsTab, &SettingsTab::setColorSchemeName);
|
||||
connect(this, &GRPCQtProxy::reportBugReceived, &settingsTab, &SettingsTab::setBugReport);
|
||||
connect(this, &GRPCQtProxy::installTLSCertificateReceived, &settingsTab, &SettingsTab::installTLSCertificate);
|
||||
connect(this, &GRPCQtProxy::exportTLSCertificatesReceived, &settingsTab, &SettingsTab::exportTLSCertificates);
|
||||
connect(this, &GRPCQtProxy::setIsStreamingReceived, &settingsTab, &SettingsTab::setIsStreaming);
|
||||
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.
|
||||
//****************************************************************************************************************************************************
|
||||
|
||||
@ -45,6 +45,7 @@ public: // member functions.
|
||||
void setColorSchemeName(QString const &name); ///< Forward a SetColorSchemeName call via a Qt Signal
|
||||
void reportBug(QString const &osType, QString const &osVersion, QString const &emailClient, QString const &address,
|
||||
QString const &description, bool includeLogs); ///< Forwards a ReportBug call via a Qt signal.
|
||||
void installTLSCertificate(); ///< Forwards a InstallTLScertificate 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 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 reportBugReceived(QString const &osType, QString const &osVersion, QString const &emailClient, QString const &address,
|
||||
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 setIsStreamingReceived(bool isStreaming); ///< Signal for the IsStreaming internal message.
|
||||
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.
|
||||
//****************************************************************************************************************************************************
|
||||
@ -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.
|
||||
/// \return The status for the call.
|
||||
@ -385,6 +379,14 @@ Status GRPCService::Login(ServerContext *, LoginRequest const *request, Empty *)
|
||||
app().log().debug(__FUNCTION__);
|
||||
UsersTab &usersTab = app().mainWindow().usersTab();
|
||||
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()) {
|
||||
qtProxy_.sendDelayedEvent(newLoginError(LoginErrorType::USERNAME_PASSWORD_ERROR, usersTab.usernamePasswordErrorMessage()));
|
||||
return Status::OK;
|
||||
@ -398,7 +400,7 @@ Status GRPCService::Login(ServerContext *, LoginRequest const *request, Empty *)
|
||||
return Status::OK;
|
||||
}
|
||||
if (usersTab.nextUserTwoPasswordsRequired()) {
|
||||
qtProxy_.sendDelayedEvent(newLoginTwoPasswordsRequestedEvent());
|
||||
qtProxy_.sendDelayedEvent(newLoginTwoPasswordsRequestedEvent(loginUsername_));
|
||||
return Status::OK;
|
||||
}
|
||||
|
||||
@ -423,7 +425,7 @@ Status GRPCService::Login2FA(ServerContext *, LoginRequest const *request, Empty
|
||||
return Status::OK;
|
||||
}
|
||||
if (usersTab.nextUserTwoPasswordsRequired()) {
|
||||
qtProxy_.sendDelayedEvent(newLoginTwoPasswordsRequestedEvent());
|
||||
qtProxy_.sendDelayedEvent(newLoginTwoPasswordsRequestedEvent(loginUsername_));
|
||||
return Status::OK;
|
||||
}
|
||||
|
||||
@ -750,9 +752,86 @@ Status GRPCService::ConfigureUserAppleMail(ServerContext *, ConfigureAppleMailRe
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] request The request
|
||||
/// \param[in] writer The writer
|
||||
/// \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) {
|
||||
app().log().debug(__FUNCTION__);
|
||||
{
|
||||
@ -826,7 +905,7 @@ bool GRPCService::sendEvent(SPStreamEvent const &event) {
|
||||
//****************************************************************************************************************************************************
|
||||
void GRPCService::finishLogin() {
|
||||
UsersTab &usersTab = app().mainWindow().usersTab();
|
||||
SPUser user = usersTab.userWithUsername(loginUsername_);
|
||||
SPUser user = usersTab.userWithUsernameOrEmail(loginUsername_);
|
||||
bool const alreadyExist = user.get();
|
||||
if (!user) {
|
||||
user = randomUser();
|
||||
@ -842,4 +921,3 @@ void GRPCService::finishLogin() {
|
||||
|
||||
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 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 GoOs(::grpc::ServerContext *, ::google::protobuf::Empty const*, ::google::protobuf::StringValue *response) override;
|
||||
grpc::Status TriggerReset(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::Empty *) override;
|
||||
grpc::Status Version(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::StringValue *response) override;
|
||||
grpc::Status LogsPath(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::StringValue *response) override;
|
||||
@ -64,7 +65,6 @@ public: // member functions.
|
||||
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 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 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;
|
||||
@ -93,6 +93,12 @@ public: // member functions.
|
||||
grpc::Status LogoutUser(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
|
||||
grpc::Status RemoveUser(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
|
||||
grpc::Status ConfigureUserAppleMail(::grpc::ServerContext *, ::grpc::ConfigureAppleMailRequest const *request, ::google::protobuf::Empty *) override;
|
||||
grpc::Status 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 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.
|
||||
|
||||
@ -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.
|
||||
//****************************************************************************************************************************************************
|
||||
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(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
|
||||
//****************************************************************************************************************************************************
|
||||
@ -505,4 +530,11 @@ void SettingsTab::resetUI() {
|
||||
ui_.comboCacheError->setCurrentIndex(0);
|
||||
|
||||
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 {
|
||||
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.
|
||||
explicit SettingsTab(QWidget *parent = nullptr); ///< Default 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 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 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 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.
|
||||
@ -79,6 +88,7 @@ public slots:
|
||||
void setColorSchemeName(QString const &name); ///< Set the value for the 'Use Dark Theme' check box.
|
||||
void setBugReport(QString const &osType, QString const &osVersion, QString const &emailClient, QString const &address, QString const &description,
|
||||
bool includeLogs); ///< Set the content of the bug report box.
|
||||
void installTLSCertificate(); ///< Install the TLS certificate.
|
||||
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 setIsDoHEnabled(bool enabled); ///< Set the value for the 'DoH Enabled' check box.
|
||||
|
||||
@ -370,7 +370,7 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupCache_2">
|
||||
<widget class="QGroupBox" name="groupCert">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
@ -380,34 +380,81 @@
|
||||
<property name="title">
|
||||
<string>TLS Certficates</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_9">
|
||||
<item>
|
||||
<widget class="QLabel" name="labeLastTLSCertsExport">
|
||||
<layout class="QGridLayout" name="gridLayout_4" columnstretch="1,1">
|
||||
<item row="0" column="0">
|
||||
<widget class="QCheckBox" name="checkTLSCertIsInstalled">
|
||||
<property name="text">
|
||||
<string>Last Export: Never</string>
|
||||
<string>Certificate is installed</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QCheckBox" name="checkTLSCertExportWillSucceed">
|
||||
<property name="text">
|
||||
<string>TLS certificate export will succeed</string>
|
||||
<string>Certificate export will succeed</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QCheckBox" name="checkTLSKeyExportWillSucceed">
|
||||
<property name="text">
|
||||
<string>TLS private key export will succeed</string>
|
||||
<string>Key export will succeed</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</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>
|
||||
</widget>
|
||||
</item>
|
||||
|
||||
@ -272,8 +272,8 @@ bridgepp::SPUser UsersTab::userWithID(QString const &userID) {
|
||||
/// \return The user with the given username.
|
||||
/// \return A null pointer if the user is not in the list.
|
||||
//****************************************************************************************************************************************************
|
||||
bridgepp::SPUser UsersTab::userWithUsername(QString const &username) {
|
||||
return users_.userWithUsername(username);
|
||||
bridgepp::SPUser UsersTab::userWithUsernameOrEmail(QString const &username) {
|
||||
return users_.userWithUsernameOrEmail(username);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -38,7 +38,7 @@ public: // member functions.
|
||||
UsersTab &operator=(UsersTab &&) = delete; ///< Disabled move assignment operator.
|
||||
UserTable &userTable(); ///< Returns a reference to the user table.
|
||||
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 nextUserFreeUserError() const; ///< Check if next user login should trigger a Free user error.
|
||||
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 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 {
|
||||
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;
|
||||
|
||||
@ -40,7 +40,7 @@ public: // member functions.
|
||||
void append(bridgepp::SPUser const &user); ///< Append a user.
|
||||
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 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.
|
||||
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).
|
||||
|
||||
@ -20,6 +20,7 @@
|
||||
#include "QMLBackend.h"
|
||||
#include "SentryUtils.h"
|
||||
#include "Settings.h"
|
||||
#include <bridgepp/CLI/CLIUtils.h>
|
||||
#include <bridgepp/GRPC/GRPCClient.h>
|
||||
#include <bridgepp/Exception/Exception.h>
|
||||
#include <bridgepp/ProcessMonitor.h>
|
||||
@ -101,19 +102,23 @@ void AppController::onFatalError(Exception const &exception) {
|
||||
qApp->exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] isCrashing Is the restart triggered by a crash.
|
||||
//****************************************************************************************************************************************************
|
||||
void AppController::restart(bool isCrashing) {
|
||||
if (!launcher_.isEmpty()) {
|
||||
QProcess p;
|
||||
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();
|
||||
if (launcher_.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
|
||||
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->setUseSSLForSMTP(sslForSMTP);
|
||||
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.
|
||||
//****************************************************************************************************************************************************
|
||||
@ -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.
|
||||
//****************************************************************************************************************************************************
|
||||
@ -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] address The email address.
|
||||
/// \param[in] emailClient The email client.
|
||||
/// \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(
|
||||
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::reportBugFinished, this, &QMLBackend::reportBugFinished);
|
||||
connect(client, &GRPCClient::reportBugSuccess, this, &QMLBackend::bugReportSendSuccess);
|
||||
connect(client, &GRPCClient::reportBugFallback, this, &QMLBackend::bugReportSendFallback);
|
||||
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"); });
|
||||
|
||||
// cache events
|
||||
|
||||
@ -24,6 +24,7 @@
|
||||
#include "BuildConfig.h"
|
||||
#include "TrayIcon.h"
|
||||
#include "UserList.h"
|
||||
#include <bridgepp/BugReportFlow/BugReportFlow.h>
|
||||
#include <bridgepp/GRPC/GRPCClient.h>
|
||||
#include <bridgepp/GRPC/GRPCUtils.h>
|
||||
#include <bridgepp/Worker/Overseer.h>
|
||||
@ -51,12 +52,20 @@ public: // member functions.
|
||||
void showSettings(QString const &reason); ///< Show the settings page.
|
||||
void selectUser(QString const &userID, bool forceShowWindow, QString const &reason); ///< Select the user and display its account details (or login screen).
|
||||
|
||||
// invokable methods can be called from QML. They generally return a value, which slots cannot do.
|
||||
// invocable methods can be called from QML. They generally return a value, which slots cannot do.
|
||||
Q_INVOKABLE static QString buildYear(); ///< Return the application build year.
|
||||
Q_INVOKABLE QPoint getCursorPos() const; ///< Retrieve the cursor position.
|
||||
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 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)
|
||||
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(QStringList availableKeychain READ availableKeychain NOTIFY availableKeychainChanged)
|
||||
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(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.
|
||||
QStringList availableKeychain() const; ///< Getter for the 'availableKeychain' 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.
|
||||
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 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 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 hostnameChanged(QString const &hostname); ///<Signal for the change of the 'hostname' 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 installUpdate() const; ///< Slot for the update install.
|
||||
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 onResetFinished(); ///< Slot for the reset finish 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 login2FAError(QString const &errorMsg); ///< Signal for the 'login2FAError' 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 login2PasswordErrorAbort(QString const &errorMsg); ///< Signal for the 'login2PasswordErrorAbort' 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 reportBugFinished(); ///< Signal for the 'reportBugFinished' 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 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 hideMainWindow(); ///< Signal for the 'hideMainWindow' gRPC stream event.
|
||||
void showHelp(); ///< Signal for the 'showHelp' event (from the context menu).
|
||||
void showSettings(); ///< Signal for the 'showHelp' event (from the context menu).
|
||||
void 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 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.
|
||||
@ -284,6 +304,7 @@ private: // data members
|
||||
bool isInternetOn_ { true }; ///< Does bridge consider internet as on?
|
||||
QList<QString> badEventDisplayQueue_; ///< THe queue for displaying 'bad event feedback request dialog'.
|
||||
std::unique_ptr<TrayIcon> trayIcon_; ///< The tray icon for the application.
|
||||
bridgepp::BugReportFlow reportFlow_; ///< The bug report flow.
|
||||
friend class AppController;
|
||||
};
|
||||
|
||||
|
||||
@ -5,8 +5,11 @@
|
||||
<file>qml/AccountView.qml</file>
|
||||
<file>qml/Banner.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/CategoryItem.qml</file>
|
||||
<file>qml/Configuration.qml</file>
|
||||
<file>qml/ConfigurationItem.qml</file>
|
||||
<file>qml/ContentWrapper.qml</file>
|
||||
@ -16,9 +19,11 @@
|
||||
<file>qml/icons/ic-alert.svg</file>
|
||||
<file>qml/icons/ic-apple-mail.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-check.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-up.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.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.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-three-dots-vertical.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.svg</file>
|
||||
<file>qml/icons/img-splash.png</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/Loader_16.svg</file>
|
||||
<file>qml/icons/Loader_48.svg</file>
|
||||
@ -70,6 +81,7 @@
|
||||
<file>qml/KeychainSettings.qml</file>
|
||||
<file>qml/LocalCacheSettings.qml</file>
|
||||
<file>qml/MainWindow.qml</file>
|
||||
<file>qml/NoAccountView.qml</file>
|
||||
<file>qml/NotificationDialog.qml</file>
|
||||
<file>qml/NotificationPopups.qml</file>
|
||||
<file>qml/Notifications/Notification.qml</file>
|
||||
@ -85,6 +97,7 @@
|
||||
<file>qml/Proton/ComboBox.qml</file>
|
||||
<file>qml/Proton/Dialog.qml</file>
|
||||
<file>qml/Proton/Label.qml</file>
|
||||
<file>qml/Proton/LinkLabel.qml</file>
|
||||
<file>qml/Proton/Menu.qml</file>
|
||||
<file>qml/Proton/MenuItem.qml</file>
|
||||
<file>qml/Proton/Popup.qml</file>
|
||||
@ -95,13 +108,27 @@
|
||||
<file>qml/Proton/TextArea.qml</file>
|
||||
<file>qml/Proton/TextField.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/SettingsView.qml</file>
|
||||
<file>qml/SetupGuide.qml</file>
|
||||
<file>qml/SignIn.qml</file>
|
||||
<file>qml/SetupWizard/ClientListItem.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/SplashScreen.qml</file>
|
||||
<file>qml/Status.qml</file>
|
||||
<file>qml/WelcomeGuide.qml</file>
|
||||
</qresource>
|
||||
</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)
|
||||
connect(this, &TrayIcon::messageClicked, []() { app().backend().showMainWindow("tray icon popup notification clicked"); });
|
||||
}
|
||||
|
||||
this->setIcon();
|
||||
this->show();
|
||||
|
||||
// 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) {
|
||||
int const index = this->rowOfUserID(userID);
|
||||
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;
|
||||
}
|
||||
users_[index]->setIsSyncing(true);
|
||||
@ -275,7 +275,7 @@ void UserList::onSyncStarted(QString const &userID) {
|
||||
void UserList::onSyncFinished(QString const &userID) {
|
||||
int const index = this->rowOfUserID(userID);
|
||||
if (index < 0) {
|
||||
app().log().error(QString("Received onSyncFinished event for unknown userID %1").arg(userID));
|
||||
app().log().error(QString("Received syncFinished event for unknown userID %1").arg(userID));
|
||||
return;
|
||||
}
|
||||
users_[index]->setIsSyncing(false);
|
||||
@ -293,7 +293,7 @@ void UserList::onSyncProgress(QString const &userID, double progress, float elap
|
||||
Q_UNUSED(remainingMs)
|
||||
int const index = this->rowOfUserID(userID);
|
||||
if (index < 0) {
|
||||
app().log().error(QString("Received onSyncFinished event for unknown userID %1").arg(userID));
|
||||
app().log().error(QString("Received syncProgress event for unknown userID %1").arg(userID));
|
||||
return;
|
||||
}
|
||||
users_[index]->setSyncProgress(progress);
|
||||
|
||||
@ -63,7 +63,7 @@ BRIDGE_BUILD_ENV= ${BRIDGE_BUILD_ENV:-"dev"}
|
||||
git submodule update --init --recursive ${VCPKG_ROOT}
|
||||
check_exit "Failed to initialize vcpkg as a submodule."
|
||||
|
||||
echo submodule udpated
|
||||
echo submodule updated
|
||||
|
||||
VCPKG_EXE="${VCPKG_ROOT}/vcpkg"
|
||||
VCPKG_BOOTSTRAP="${VCPKG_ROOT}/bootstrap-vcpkg.sh"
|
||||
|
||||
@ -137,12 +137,21 @@ bool checkSingleInstance(QLockFile &lock) {
|
||||
if (lock.getLockInfo(&pid, &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));
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@ -1,251 +1,252 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
|
||||
import Proton
|
||||
|
||||
Item {
|
||||
id: root
|
||||
enum ViewType {
|
||||
SmallView,
|
||||
LargeView
|
||||
}
|
||||
|
||||
property var _spacing: 12
|
||||
property ColorScheme colorScheme
|
||||
property var user
|
||||
|
||||
property var _spacing: 12 * ProtonStyle.px
|
||||
|
||||
property color progressColor : {
|
||||
if (!root.enabled) return root.colorScheme.text_weak
|
||||
if (root.type == AccountDelegate.SmallView) return root.colorScheme.text_weak
|
||||
if (root.user && root.user.isSyncing) return root.colorScheme.text_weak
|
||||
if (root.progressRatio < .50) return root.colorScheme.signal_success
|
||||
if (root.progressRatio < .75) return root.colorScheme.signal_warning
|
||||
return root.colorScheme.signal_danger
|
||||
property color progressColor: {
|
||||
if (!root.enabled)
|
||||
return root.colorScheme.text_weak;
|
||||
if (root.type === AccountDelegate.SmallView)
|
||||
return root.colorScheme.text_weak;
|
||||
if (root.user && root.user.isSyncing)
|
||||
return root.colorScheme.text_weak;
|
||||
if (root.progressRatio < .50)
|
||||
return root.colorScheme.signal_success;
|
||||
if (root.progressRatio < .75)
|
||||
return root.colorScheme.signal_warning;
|
||||
return root.colorScheme.signal_danger;
|
||||
}
|
||||
property real progressRatio: {
|
||||
if (!root.user)
|
||||
return 0
|
||||
return root.user.isSyncing ? root.user.syncProgress : reasonableFraction(root.user.usedBytes, root.user.totalBytes)
|
||||
return 0;
|
||||
return root.user.isSyncing ? root.user.syncProgress : reasonableFraction(root.user.usedBytes, root.user.totalBytes);
|
||||
}
|
||||
property string totalSpace: root.spaceWithUnits(root.user ? root.reasonableBytes(root.user.totalBytes) : 0)
|
||||
property var type: AccountDelegate.SmallView
|
||||
property string usedSpace: root.spaceWithUnits(root.user ? root.reasonableBytes(root.user.usedBytes) : 0)
|
||||
|
||||
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]
|
||||
}
|
||||
property var user
|
||||
|
||||
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
|
||||
implicitHeight : children[0].implicitHeight
|
||||
|
||||
enum ViewType{
|
||||
SmallView, LargeView
|
||||
}
|
||||
property var type : AccountDelegate.SmallView
|
||||
implicitHeight: children[0].implicitHeight
|
||||
|
||||
RowLayout {
|
||||
spacing: root._spacing
|
||||
|
||||
anchors {
|
||||
top: root.top
|
||||
left: root.left
|
||||
right: root.right
|
||||
top: root.top
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: avatar
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.preferredWidth: height
|
||||
|
||||
color: root.colorScheme.background_avatar
|
||||
radius: ProtonStyle.avatar_radius
|
||||
|
||||
color: root.colorScheme.background_avatar
|
||||
|
||||
Label {
|
||||
colorScheme: root.colorScheme
|
||||
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: {
|
||||
switch (root.type) {
|
||||
case AccountDelegate.SmallView: return Label.Body
|
||||
case AccountDelegate.LargeView: return Label.Title
|
||||
case AccountDelegate.SmallView:
|
||||
return Label.Body;
|
||||
case AccountDelegate.LargeView:
|
||||
return Label.Title;
|
||||
}
|
||||
}
|
||||
font.weight: Font.Normal
|
||||
color: "#FFFFFF"
|
||||
horizontalAlignment: Qt.AlignHCenter
|
||||
verticalAlignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: account
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
|
||||
spacing: 0
|
||||
|
||||
Label {
|
||||
id: labelEmail
|
||||
Layout.maximumWidth: root.width - (root._spacing + avatar.width)
|
||||
colorScheme: root.colorScheme
|
||||
elide: Text.ElideMiddle
|
||||
text: primaryEmail()
|
||||
type: {
|
||||
switch (root.type) {
|
||||
case AccountDelegate.SmallView: return Label.Body
|
||||
case AccountDelegate.LargeView: return Label.Title
|
||||
case AccountDelegate.SmallView:
|
||||
return Label.Body;
|
||||
case AccountDelegate.LargeView:
|
||||
return Label.Title;
|
||||
}
|
||||
}
|
||||
elide: Text.ElideMiddle
|
||||
|
||||
MouseArea {
|
||||
id: labelArea
|
||||
anchors.fill:parent
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
}
|
||||
|
||||
ToolTip {
|
||||
id: toolTipEmail
|
||||
visible: labelArea.containsMouse && labelEmail.truncated
|
||||
text: primaryEmail()
|
||||
delay: 1000
|
||||
text: primaryEmail()
|
||||
visible: labelArea.containsMouse && labelEmail.truncated
|
||||
|
||||
background: Rectangle {
|
||||
border.color: root.colorScheme.background_strong
|
||||
color: root.colorScheme.background_norm
|
||||
}
|
||||
|
||||
contentItem: Text {
|
||||
color: root.colorScheme.text_norm
|
||||
text: toolTipEmail.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item { implicitHeight: root.type == AccountDelegate.LargeView ? 6 * ProtonStyle.px : 0 }
|
||||
|
||||
Item {
|
||||
implicitHeight: root.type === AccountDelegate.LargeView ? 6 : 0
|
||||
}
|
||||
RowLayout {
|
||||
spacing: 0
|
||||
|
||||
Label {
|
||||
color: root.progressColor
|
||||
colorScheme: root.colorScheme
|
||||
text: {
|
||||
if (!root.user)
|
||||
return qsTr("Signed out")
|
||||
return qsTr("Signed out");
|
||||
switch (root.user.state) {
|
||||
case EUserState.SignedOut:
|
||||
default:
|
||||
return qsTr("Signed out")
|
||||
return qsTr("Signed out");
|
||||
case EUserState.Locked:
|
||||
return qsTr("Connecting") + dotsTimer.dots
|
||||
return qsTr("Connecting") + dotsTimer.dots;
|
||||
case EUserState.Connected:
|
||||
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
|
||||
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: {
|
||||
switch (root.type) {
|
||||
case AccountDelegate.SmallView: return Label.Caption
|
||||
case AccountDelegate.LargeView: return Label.Body
|
||||
case AccountDelegate.SmallView:
|
||||
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 {
|
||||
colorScheme: root.colorScheme
|
||||
text: root.user && root.user.state == EUserState.Connected && !root.user.isSyncing ? " / " + root.totalSpace : ""
|
||||
color: root.colorScheme.text_weak
|
||||
colorScheme: root.colorScheme
|
||||
text: root.user && root.user.state === EUserState.Connected && !root.user.isSyncing ? " / " + root.totalSpace : ""
|
||||
type: {
|
||||
switch (root.type) {
|
||||
case AccountDelegate.SmallView: return Label.Caption
|
||||
case AccountDelegate.LargeView: return Label.Body
|
||||
case AccountDelegate.SmallView:
|
||||
return Label.Caption;
|
||||
case AccountDelegate.LargeView:
|
||||
return Label.Body;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item { implicitHeight: root.type == AccountDelegate.LargeView ? 3 * ProtonStyle.px : 0 }
|
||||
|
||||
Item {
|
||||
implicitHeight: root.type === AccountDelegate.LargeView ? 3 : 0
|
||||
}
|
||||
Rectangle {
|
||||
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
|
||||
height: 4
|
||||
radius: ProtonStyle.progress_bar_radius
|
||||
visible: root.user ? root.type === AccountDelegate.LargeView : false
|
||||
width: 140
|
||||
|
||||
Rectangle {
|
||||
id: progress_bar_filled
|
||||
radius: ProtonStyle.progress_bar_radius
|
||||
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 {
|
||||
top : parent.top
|
||||
bottom : parent.bottom
|
||||
left : parent.left
|
||||
bottom: parent.bottom
|
||||
left: parent.left
|
||||
top: parent.top
|
||||
}
|
||||
width: Math.min(1,Math.max(0.02,root.progressRatio)) * parent.width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
@ -1,42 +1,36 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
|
||||
import Proton
|
||||
|
||||
Item {
|
||||
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 var notifications
|
||||
property var user
|
||||
|
||||
signal showSignIn
|
||||
|
||||
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
|
||||
signal showClientConfigurator(var user, string address, bool justLoggedIn)
|
||||
signal showLogin(var username)
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
@ -45,6 +39,7 @@ Item {
|
||||
ScrollView {
|
||||
id: scrollView
|
||||
anchors.fill: parent
|
||||
|
||||
Component.onCompleted: contentItem.boundsBehavior = Flickable.StopAtBounds
|
||||
|
||||
ColumnLayout {
|
||||
@ -54,102 +49,101 @@ Item {
|
||||
|
||||
Rectangle {
|
||||
id: topArea
|
||||
color: root.colorScheme.background_norm
|
||||
clip: true
|
||||
Layout.fillWidth: true
|
||||
clip: true
|
||||
color: root.colorScheme.background_norm
|
||||
implicitHeight: childrenRect.height
|
||||
|
||||
ColumnLayout {
|
||||
id: topLayout
|
||||
width: _contentWidth
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: _spacing
|
||||
width: _contentWidth
|
||||
|
||||
RowLayout {
|
||||
// account delegate with action buttons
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: _topMargin
|
||||
|
||||
spacing: _buttonSpacing
|
||||
AccountDelegate {
|
||||
Layout.fillWidth: true
|
||||
colorScheme: root.colorScheme
|
||||
user: root.user
|
||||
type: AccountDelegate.LargeView
|
||||
enabled: _connected
|
||||
type: AccountDelegate.LargeView
|
||||
user: root.user
|
||||
}
|
||||
|
||||
Button {
|
||||
Layout.alignment: Qt.AlignTop
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Sign out")
|
||||
secondary: true
|
||||
text: qsTr("Sign out")
|
||||
visible: _connected
|
||||
|
||||
onClicked: {
|
||||
if (!root.user)
|
||||
return;
|
||||
root.user.logout();
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
Layout.alignment: Qt.AlignTop
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Sign in")
|
||||
secondary: true
|
||||
text: qsTr("Sign in")
|
||||
visible: root.user ? (root.user.state === EUserState.SignedOut) : false
|
||||
|
||||
onClicked: {
|
||||
if (!root.user)
|
||||
return;
|
||||
root.showSignIn();
|
||||
if (user) {
|
||||
root.showLogin(user.primaryEmailOrUsername());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
Layout.alignment: Qt.AlignTop
|
||||
colorScheme: root.colorScheme
|
||||
icon.source: "/qml/icons/ic-trash.svg"
|
||||
secondary: true
|
||||
visible: root.user ? root.user.state !== EUserState.Locked : false
|
||||
|
||||
onClicked: {
|
||||
if (!root.user)
|
||||
return;
|
||||
root.notifications.askDeleteAccount(root.user);
|
||||
}
|
||||
visible: root.user ? root.user.state !== EUserState.Locked : false
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: root._lineThickness
|
||||
color: root.colorScheme.border_weak
|
||||
height: root._lineThickness
|
||||
}
|
||||
|
||||
SettingsItem {
|
||||
Layout.fillWidth: true
|
||||
actionText: qsTr("Configure email client")
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Email clients")
|
||||
actionText: qsTr("Configure")
|
||||
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
|
||||
text: qsTr("Email clients")
|
||||
type: SettingsItem.PrimaryButton
|
||||
visible: _connected && ((!root.user.splitMode) || (root.user.addresses.length === 1))
|
||||
|
||||
onClicked: {
|
||||
if (!root.user)
|
||||
return;
|
||||
root.showSetupGuide(root.user, user.addresses[0]);
|
||||
root.showClientConfigurator(root.user, user.addresses[0], false);
|
||||
}
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
SettingsItem {
|
||||
id: splitMode
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Split addresses")
|
||||
description: qsTr("Setup multiple email addresses individually.")
|
||||
type: SettingsItem.Toggle
|
||||
Layout.fillWidth: true
|
||||
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
|
||||
text: qsTr("Split addresses")
|
||||
type: SettingsItem.Toggle
|
||||
visible: _connected && root.user.addresses.length > 1
|
||||
|
||||
onClicked: {
|
||||
if (!splitMode.checked) {
|
||||
root.notifications.askEnableSplitMode(user);
|
||||
@ -158,52 +152,47 @@ Item {
|
||||
root.user.toggleSplitMode(!splitMode.checked);
|
||||
}
|
||||
}
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.bottomMargin: _spacing
|
||||
Layout.fillWidth: true
|
||||
visible: _connected && root.user.splitMode
|
||||
|
||||
ComboBox {
|
||||
id: addressSelector
|
||||
colorScheme: root.colorScheme
|
||||
Layout.fillWidth: true
|
||||
colorScheme: root.colorScheme
|
||||
model: root.user ? root.user.addresses : null
|
||||
}
|
||||
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Configure")
|
||||
secondary: true
|
||||
secondary: false
|
||||
text: qsTr("Configure email client")
|
||||
|
||||
onClicked: {
|
||||
if (!root.user)
|
||||
return;
|
||||
root.showSetupGuide(root.user, addressSelector.displayText);
|
||||
root.showClientConfigurator(root.user, addressSelector.displayText, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
height: 0
|
||||
} // just for some extra space before separator
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: bottomArea
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: bottomLayout.implicitHeight
|
||||
color: root.colorScheme.background_weak
|
||||
implicitHeight: bottomLayout.implicitHeight
|
||||
|
||||
ColumnLayout {
|
||||
id: bottomLayout
|
||||
width: _contentWidth
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: _spacing
|
||||
visible: _connected
|
||||
width: _contentWidth
|
||||
|
||||
Label {
|
||||
Layout.topMargin: _detailsMargin
|
||||
@ -211,35 +200,34 @@ Item {
|
||||
text: qsTr("Mailbox details")
|
||||
type: Label.Body_semibold
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: configuration
|
||||
spacing: _spacing
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
property string currentAddress: addressSelector.displayText
|
||||
|
||||
Configuration {
|
||||
Layout.fillWidth: true
|
||||
colorScheme: root.colorScheme
|
||||
title: qsTr("IMAP")
|
||||
hostname: Backend.hostname
|
||||
port: Backend.imapPort.toString()
|
||||
username: configuration.currentAddress
|
||||
password: root.user ? root.user.password : ""
|
||||
security: Backend.useSSLForIMAP ? "SSL" : "STARTTLS"
|
||||
}
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
spacing: _spacing
|
||||
|
||||
Configuration {
|
||||
Layout.fillWidth: true
|
||||
colorScheme: root.colorScheme
|
||||
title: qsTr("SMTP")
|
||||
hostname: Backend.hostname
|
||||
port: Backend.smtpPort.toString()
|
||||
username: configuration.currentAddress
|
||||
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"
|
||||
title: qsTr("SMTP")
|
||||
username: configuration.currentAddress
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,25 +1,19 @@
|
||||
// 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 QtQuick.Controls.impl
|
||||
|
||||
import Proton
|
||||
import Notifications
|
||||
|
||||
@ -27,34 +21,28 @@ Popup {
|
||||
id: root
|
||||
|
||||
property ColorScheme colorScheme
|
||||
property Notification notification
|
||||
property var mainWindow
|
||||
|
||||
topMargin: 37
|
||||
leftMargin: (mainWindow.width - root.implicitWidth)/2
|
||||
property Notification notification
|
||||
|
||||
implicitHeight: contentLayout.implicitHeight + contentLayout.anchors.topMargin + contentLayout.anchors.bottomMargin
|
||||
implicitWidth: 600 // contentLayout.implicitWidth + contentLayout.anchors.leftMargin + contentLayout.anchors.rightMargin
|
||||
|
||||
popupType: ApplicationWindow.PopupType.Banner
|
||||
|
||||
shouldShow: notification ? (notification.active && !notification.dismissed) : false
|
||||
|
||||
leftMargin: (mainWindow.width - root.implicitWidth) / 2
|
||||
modal: false
|
||||
popupType: ApplicationWindow.PopupType.Banner
|
||||
shouldShow: notification ? (notification.active && !notification.dismissed) : false
|
||||
topMargin: 37
|
||||
|
||||
Action {
|
||||
id: defaultDismissAction
|
||||
|
||||
text: qsTr("OK")
|
||||
|
||||
onTriggered: {
|
||||
if (!root.notification) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
root.notification.dismissed = true
|
||||
root.notification.dismissed = true;
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: contentLayout
|
||||
anchors.fill: parent
|
||||
@ -63,170 +51,148 @@ Popup {
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
|
||||
clip: true
|
||||
implicitHeight: children[1].implicitHeight + children[1].anchors.topMargin + children[1].anchors.bottomMargin
|
||||
implicitWidth: children[1].implicitWidth + children[1].anchors.leftMargin + children[1].anchors.rightMargin
|
||||
|
||||
Rectangle {
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
width: parent.width + 10
|
||||
radius: ProtonStyle.banner_radius
|
||||
anchors.top: parent.top
|
||||
color: {
|
||||
if (!root.notification) {
|
||||
return "transparent"
|
||||
return "transparent";
|
||||
}
|
||||
|
||||
switch (root.notification.type) {
|
||||
case Notification.NotificationType.Info:
|
||||
return root.colorScheme.signal_info
|
||||
case Notification.NotificationType.Success:
|
||||
return root.colorScheme.signal_success
|
||||
case Notification.NotificationType.Warning:
|
||||
return root.colorScheme.signal_warning
|
||||
case Notification.NotificationType.Danger:
|
||||
return root.colorScheme.signal_danger
|
||||
case Notification.NotificationType.Info:
|
||||
return root.colorScheme.signal_info;
|
||||
case Notification.NotificationType.Success:
|
||||
return root.colorScheme.signal_success;
|
||||
case Notification.NotificationType.Warning:
|
||||
return root.colorScheme.signal_warning;
|
||||
case Notification.NotificationType.Danger:
|
||||
return root.colorScheme.signal_danger;
|
||||
}
|
||||
}
|
||||
radius: ProtonStyle.banner_radius
|
||||
width: parent.width + 10
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
|
||||
anchors.topMargin: 14
|
||||
anchors.bottomMargin: 14
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 16
|
||||
|
||||
anchors.topMargin: 14
|
||||
spacing: 8
|
||||
|
||||
ColorImage {
|
||||
color: root.colorScheme.text_invert
|
||||
width: 24
|
||||
height: 24
|
||||
|
||||
sourceSize.width: 24
|
||||
sourceSize.height: 24
|
||||
|
||||
Layout.preferredHeight: 24
|
||||
Layout.preferredWidth: 24
|
||||
|
||||
color: root.colorScheme.text_invert
|
||||
height: 24
|
||||
source: {
|
||||
if (!root.notification) {
|
||||
return ""
|
||||
return "";
|
||||
}
|
||||
|
||||
switch (root.notification.type) {
|
||||
case Notification.NotificationType.Info:
|
||||
return "/qml/icons/ic-info-circle-filled.svg"
|
||||
case Notification.NotificationType.Success:
|
||||
return "/qml/icons/ic-info-circle-filled.svg"
|
||||
case Notification.NotificationType.Warning:
|
||||
return "/qml/icons/ic-exclamation-circle-filled.svg"
|
||||
case Notification.NotificationType.Danger:
|
||||
return "/qml/icons/ic-exclamation-circle-filled.svg"
|
||||
case Notification.NotificationType.Info:
|
||||
return "/qml/icons/ic-info-circle-filled.svg";
|
||||
case Notification.NotificationType.Success:
|
||||
return "/qml/icons/ic-info-circle-filled.svg";
|
||||
case Notification.NotificationType.Warning:
|
||||
return "/qml/icons/ic-exclamation-circle-filled.svg";
|
||||
case Notification.NotificationType.Danger:
|
||||
return "/qml/icons/ic-exclamation-circle-filled.svg";
|
||||
}
|
||||
}
|
||||
sourceSize.height: 24
|
||||
sourceSize.width: 24
|
||||
width: 24
|
||||
}
|
||||
|
||||
Label {
|
||||
colorScheme: root.colorScheme
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
|
||||
color: root.colorScheme.text_invert
|
||||
colorScheme: root.colorScheme
|
||||
text: root.notification ? root.notification.description : ""
|
||||
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillHeight: true
|
||||
width: 1
|
||||
color: {
|
||||
if (!root.notification) {
|
||||
return "transparent"
|
||||
return "transparent";
|
||||
}
|
||||
|
||||
switch (root.notification.type) {
|
||||
case Notification.NotificationType.Info:
|
||||
return root.colorScheme.signal_info_active
|
||||
case Notification.NotificationType.Success:
|
||||
return root.colorScheme.signal_success_active
|
||||
case Notification.NotificationType.Warning:
|
||||
return root.colorScheme.signal_warning_active
|
||||
case Notification.NotificationType.Danger:
|
||||
return root.colorScheme.signal_danger_active
|
||||
case Notification.NotificationType.Info:
|
||||
return root.colorScheme.signal_info_active;
|
||||
case Notification.NotificationType.Success:
|
||||
return root.colorScheme.signal_success_active;
|
||||
case Notification.NotificationType.Warning:
|
||||
return root.colorScheme.signal_warning_active;
|
||||
case Notification.NotificationType.Danger:
|
||||
return root.colorScheme.signal_danger_active;
|
||||
}
|
||||
}
|
||||
width: 1
|
||||
}
|
||||
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
Layout.fillHeight: true
|
||||
|
||||
id: actionButton
|
||||
|
||||
Layout.fillHeight: true
|
||||
action: (root.notification && root.notification.action.length > 0) ? root.notification.action[0] : defaultDismissAction
|
||||
colorScheme: root.colorScheme
|
||||
|
||||
background: Item {
|
||||
clip: true
|
||||
|
||||
Rectangle {
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
width: parent.width + 10
|
||||
radius: ProtonStyle.banner_radius
|
||||
anchors.top: parent.top
|
||||
color: {
|
||||
if (!root.notification) {
|
||||
return "transparent"
|
||||
return "transparent";
|
||||
}
|
||||
|
||||
var norm
|
||||
var hover
|
||||
var active
|
||||
|
||||
let norm;
|
||||
let hover;
|
||||
let active;
|
||||
switch (root.notification.type) {
|
||||
case Notification.NotificationType.Info:
|
||||
norm = root.colorScheme.signal_info
|
||||
hover = root.colorScheme.signal_info_hover
|
||||
active = root.colorScheme.signal_info_active
|
||||
case Notification.NotificationType.Info:
|
||||
norm = root.colorScheme.signal_info;
|
||||
hover = root.colorScheme.signal_info_hover;
|
||||
active = root.colorScheme.signal_info_active;
|
||||
break;
|
||||
case Notification.NotificationType.Success:
|
||||
norm = root.colorScheme.signal_success
|
||||
hover = root.colorScheme.signal_success_hover
|
||||
active = root.colorScheme.signal_success_active
|
||||
case Notification.NotificationType.Success:
|
||||
norm = root.colorScheme.signal_success;
|
||||
hover = root.colorScheme.signal_success_hover;
|
||||
active = root.colorScheme.signal_success_active;
|
||||
break;
|
||||
case Notification.NotificationType.Warning:
|
||||
norm = root.colorScheme.signal_warning
|
||||
hover = root.colorScheme.signal_warning_hover
|
||||
active = root.colorScheme.signal_warning_active
|
||||
case Notification.NotificationType.Warning:
|
||||
norm = root.colorScheme.signal_warning;
|
||||
hover = root.colorScheme.signal_warning_hover;
|
||||
active = root.colorScheme.signal_warning_active;
|
||||
break;
|
||||
case Notification.NotificationType.Danger:
|
||||
norm = root.colorScheme.signal_danger
|
||||
hover = root.colorScheme.signal_danger_hover
|
||||
active = root.colorScheme.signal_danger_active
|
||||
case Notification.NotificationType.Danger:
|
||||
norm = root.colorScheme.signal_danger;
|
||||
hover = root.colorScheme.signal_danger_hover;
|
||||
active = root.colorScheme.signal_danger_active;
|
||||
break;
|
||||
}
|
||||
|
||||
if (actionButton.down) {
|
||||
return active
|
||||
return active;
|
||||
}
|
||||
|
||||
if (actionButton.enabled && (actionButton.highlighted || actionButton.hovered || actionButton.checked)) {
|
||||
return hover
|
||||
return hover;
|
||||
}
|
||||
|
||||
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
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import QtQml
|
||||
import QtQuick
|
||||
import QtQuick.Window
|
||||
import Qt.labs.platform
|
||||
|
||||
import Proton
|
||||
import Notifications
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
function bound(num, lowerLimit, upperLimit) {
|
||||
return Math.max(lowerLimit, Math.min(upperLimit, num))
|
||||
property MainWindow _mainWindow: MainWindow {
|
||||
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 {
|
||||
id: notifications
|
||||
frontendMain: mainWindow
|
||||
}
|
||||
|
||||
property NotificationFilter _trayNotificationFilter: NotificationFilter {
|
||||
id: trayNotificationFilter
|
||||
source: root._notifications ? root._notifications.all : undefined
|
||||
onTopmostChanged: {
|
||||
if (topmost) {
|
||||
switch (topmost.type) {
|
||||
case Notification.NotificationType.Danger:
|
||||
Backend.setErrorTrayIcon(topmost.brief, topmost.icon)
|
||||
return
|
||||
case Notification.NotificationType.Warning:
|
||||
Backend.setWarnTrayIcon(topmost.brief, topmost.icon)
|
||||
return
|
||||
case Notification.NotificationType.Info:
|
||||
Backend.setUpdateTrayIcon(topmost.brief, topmost.icon)
|
||||
return
|
||||
id: trayNotificationFilter
|
||||
source: root._notifications ? root._notifications.all : undefined
|
||||
|
||||
onTopmostChanged: {
|
||||
if (topmost) {
|
||||
switch (topmost.type) {
|
||||
case Notification.NotificationType.Danger:
|
||||
Backend.setErrorTrayIcon(topmost.brief, topmost.icon);
|
||||
return;
|
||||
case Notification.NotificationType.Warning:
|
||||
Backend.setWarnTrayIcon(topmost.brief, topmost.icon);
|
||||
return;
|
||||
case Notification.NotificationType.Info:
|
||||
Backend.setUpdateTrayIcon(topmost.brief, topmost.icon);
|
||||
return;
|
||||
}
|
||||
}
|
||||
Backend.setNormalTrayIcon()
|
||||
Backend.setNormalTrayIcon();
|
||||
}
|
||||
}
|
||||
property var title: Backend.appname
|
||||
|
||||
|
||||
property MainWindow _mainWindow: MainWindow {
|
||||
id: mainWindow
|
||||
visible: false
|
||||
|
||||
title: root.title
|
||||
notifications: root._notifications
|
||||
|
||||
onVisibleChanged: {
|
||||
Backend.dockIconVisible = visible
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Backend
|
||||
function onDiskCacheUnavailable() {
|
||||
mainWindow.showAndRise()
|
||||
}
|
||||
function onColorSchemeNameChanged(scheme) { root.setColorScheme() }
|
||||
|
||||
function onHideMainWindow() {
|
||||
mainWindow.hide();
|
||||
}
|
||||
}
|
||||
function bound(num, lowerLimit, upperLimit) {
|
||||
return Math.max(lowerLimit, Math.min(upperLimit, num));
|
||||
}
|
||||
function setColorScheme() {
|
||||
if (Backend.colorSchemeName === "light")
|
||||
ProtonStyle.currentStyle = ProtonStyle.lightStyle;
|
||||
if (Backend.colorSchemeName === "dark")
|
||||
ProtonStyle.currentStyle = ProtonStyle.darkStyle;
|
||||
}
|
||||
|
||||
|
||||
Component.onCompleted: {
|
||||
if (!Backend) {
|
||||
console.log("Backend not loaded")
|
||||
console.log("Backend not loaded");
|
||||
}
|
||||
|
||||
root.setColorScheme()
|
||||
|
||||
|
||||
root.setColorScheme();
|
||||
if (!Backend.users) {
|
||||
console.log("users not loaded")
|
||||
console.log("users not loaded");
|
||||
}
|
||||
|
||||
var c = Backend.users.count
|
||||
var u = Backend.users.get(0)
|
||||
const c = Backend.users.count;
|
||||
const u = Backend.users.get(0);
|
||||
// DEBUG
|
||||
if (c !== 0) {
|
||||
console.log("users non zero", c)
|
||||
console.log("first user", u )
|
||||
console.log("users non zero", c);
|
||||
console.log("first user", u);
|
||||
}
|
||||
|
||||
if (c === 0) {
|
||||
mainWindow.showAndRise()
|
||||
mainWindow.showAndRise();
|
||||
}
|
||||
|
||||
if (u) {
|
||||
if (c === 1 && (u.state === EUserState.SignedOut)) {
|
||||
mainWindow.showAndRise()
|
||||
mainWindow.showAndRise();
|
||||
}
|
||||
}
|
||||
|
||||
Backend.guiReady()
|
||||
|
||||
if (Backend.showOnStartup || Backend.showSplashScreen) {
|
||||
mainWindow.showAndRise()
|
||||
Backend.guiReady();
|
||||
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
|
||||
//
|
||||
// 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
|
||||
|
||||
fillHeight: true
|
||||
|
||||
property var selectedAddress
|
||||
property var categoryId:-1
|
||||
property string category: Backend.getBugCategory(root.categoryId)
|
||||
|
||||
signal bugReportWasSent()
|
||||
signal bugReportWasSent
|
||||
|
||||
Label {
|
||||
text: qsTr("Report a problem")
|
||||
colorScheme: root.colorScheme
|
||||
type: Label.Heading
|
||||
function isValidEmail(text) {
|
||||
const reEmail = /^[^@]+@[^@]+\.[A-Za-z]+\s*$/;
|
||||
return reEmail.test(text);
|
||||
}
|
||||
|
||||
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 {
|
||||
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.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
|
||||
// want TextArea implicitHeight (which is height of all text)
|
||||
// to be considered in SettingsView internal scroll view
|
||||
implicitHeight: height
|
||||
label: "Your answers to: " + qsTr(root.category);
|
||||
readOnly : true
|
||||
}
|
||||
|
||||
|
||||
TextField {
|
||||
id: address
|
||||
|
||||
label: qsTr("Your contact email")
|
||||
colorScheme: root.colorScheme
|
||||
Layout.fillWidth: true
|
||||
colorScheme: root.colorScheme
|
||||
label: qsTr("Your contact email")
|
||||
placeholderText: qsTr("e.g. jane.doe@protonmail.com")
|
||||
|
||||
validator: function(str) {
|
||||
validator: function (str) {
|
||||
if (!isValidEmail(str)) {
|
||||
return qsTr("Enter valid email address")
|
||||
return qsTr("Enter valid email address");
|
||||
}
|
||||
return
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: emailClient
|
||||
|
||||
label: qsTr("Your email client (including version)")
|
||||
colorScheme: root.colorScheme
|
||||
Layout.fillWidth: true
|
||||
colorScheme: root.colorScheme
|
||||
label: qsTr("Your email client (including version)")
|
||||
placeholderText: qsTr("e.g. Apple Mail 14.0")
|
||||
|
||||
validator: function(str) {
|
||||
validator: function (str) {
|
||||
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 {
|
||||
CheckBox {
|
||||
id: includeLogs
|
||||
text: qsTr("Include my recent logs")
|
||||
colorScheme: root.colorScheme
|
||||
checked: true
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Include my recent logs")
|
||||
}
|
||||
Button {
|
||||
Layout.leftMargin: 12
|
||||
text: qsTr("View logs")
|
||||
secondary: true
|
||||
colorScheme: root.colorScheme
|
||||
secondary: true
|
||||
text: qsTr("View logs")
|
||||
|
||||
onClicked: Qt.openUrlExternally(Backend.logsPath)
|
||||
}
|
||||
}
|
||||
|
||||
TextEdit {
|
||||
text: qsTr("Reports are not end-to-end encrypted, please do not send any sensitive information.")
|
||||
|
||||
readOnly: true
|
||||
|
||||
Layout.fillWidth: true
|
||||
color: root.colorScheme.text_weak
|
||||
font.family: ProtonStyle.font_family
|
||||
font.weight: ProtonStyle.fontWeight_400
|
||||
font.pixelSize: ProtonStyle.caption_font_size
|
||||
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
|
||||
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
|
||||
selectByMouse: true
|
||||
}
|
||||
|
||||
Button {
|
||||
id: sendButton
|
||||
text: qsTr("Send")
|
||||
colorScheme: root.colorScheme
|
||||
enabled: !loading
|
||||
text: qsTr("Send")
|
||||
|
||||
onClicked: {
|
||||
description.validate()
|
||||
address.validate()
|
||||
emailClient.validate()
|
||||
|
||||
description.validate();
|
||||
address.validate();
|
||||
emailClient.validate();
|
||||
if (description.error || address.error || emailClient.error) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
submit()
|
||||
submit();
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onBugReportSendSuccess() {
|
||||
root.bugReportWasSent();
|
||||
}
|
||||
function onReportBugFinished() {
|
||||
sendButton.loading = false;
|
||||
}
|
||||
|
||||
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
|
||||
//
|
||||
// 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 QtQuick.Controls.impl
|
||||
|
||||
import Proton
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property int _margin: 24
|
||||
property ColorScheme colorScheme
|
||||
property string title
|
||||
property bool highlightPassword
|
||||
property string hostname
|
||||
property string port
|
||||
property string username
|
||||
property string password
|
||||
property string port
|
||||
property string security
|
||||
|
||||
implicitWidth: 304
|
||||
implicitHeight: content.height + 2*root._margin
|
||||
property string title
|
||||
property string username
|
||||
|
||||
color: root.colorScheme.background_norm
|
||||
implicitHeight: content.height + 2 * root._margin
|
||||
implicitWidth: 304
|
||||
radius: ProtonStyle.card_radius
|
||||
|
||||
property int _margin: 24
|
||||
|
||||
ColumnLayout {
|
||||
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
|
||||
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 {
|
||||
colorScheme: root.colorScheme
|
||||
text: root.title
|
||||
type: Label.Body_semibold
|
||||
}
|
||||
|
||||
ConfigurationItem{ colorScheme: root.colorScheme; label: qsTr("Hostname") ; value: root.hostname }
|
||||
ConfigurationItem{ colorScheme: root.colorScheme; label: qsTr("Port") ; value: root.port }
|
||||
ConfigurationItem{ colorScheme: root.colorScheme; label: qsTr("Username") ; value: root.username }
|
||||
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("Hostname")
|
||||
value: root.hostname
|
||||
}
|
||||
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
|
||||
//
|
||||
// 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 QtQuick.Controls.impl
|
||||
|
||||
import Proton
|
||||
|
||||
Item {
|
||||
id: root
|
||||
Layout.fillWidth: true
|
||||
|
||||
property var colorScheme
|
||||
property string label
|
||||
property string labelColor: root.colorScheme.text_norm
|
||||
property string value
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: children[0].implicitHeight
|
||||
implicitWidth: children[0].implicitWidth
|
||||
|
||||
@ -41,51 +36,49 @@ Item {
|
||||
|
||||
ColumnLayout {
|
||||
Label {
|
||||
color: labelColor
|
||||
colorScheme: root.colorScheme
|
||||
text: root.label
|
||||
type: Label.Body
|
||||
type: Label.Body_semibold
|
||||
}
|
||||
TextEdit {
|
||||
id: valueText
|
||||
text: root.value
|
||||
Layout.fillWidth: true
|
||||
color: root.colorScheme.text_weak
|
||||
readOnly: true
|
||||
selectByMouse: true
|
||||
selectByKeyboard: true
|
||||
selectByMouse: true
|
||||
selectionColor: root.colorScheme.text_weak
|
||||
text: root.value
|
||||
wrapMode: Text.WrapAnywhere
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ColorImage {
|
||||
source: "/qml/icons/ic-copy.svg"
|
||||
color: root.colorScheme.text_norm
|
||||
height: root.colorScheme.body_font_size
|
||||
source: "/qml/icons/ic-copy.svg"
|
||||
sourceSize.height: root.colorScheme.body_font_size
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked : {
|
||||
valueText.select(0, valueText.length)
|
||||
valueText.copy()
|
||||
valueText.deselect()
|
||||
|
||||
onClicked: {
|
||||
valueText.select(0, valueText.length);
|
||||
valueText.copy();
|
||||
valueText.deselect();
|
||||
}
|
||||
onPressed: parent.scale = 0.90
|
||||
onReleased: parent.scale = 1
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 1
|
||||
color: root.colorScheme.border_norm
|
||||
height: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,155 +1,138 @@
|
||||
// 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 QtQuick.Controls.impl
|
||||
|
||||
import Proton
|
||||
|
||||
SettingsView {
|
||||
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
|
||||
|
||||
onVisibleChanged: {
|
||||
root.setDefaultValues();
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Connection mode")
|
||||
type: Label.Heading
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
color: root.colorScheme.text_weak
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Change the protocol Bridge and the email client use to connect for IMAP and SMTP.")
|
||||
type: Label.Body
|
||||
color: root.colorScheme.text_weak
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 16
|
||||
|
||||
ButtonGroup{ id: imapProtocolSelection }
|
||||
|
||||
ButtonGroup {
|
||||
id: imapProtocolSelection
|
||||
}
|
||||
Label {
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("IMAP connection")
|
||||
}
|
||||
|
||||
RadioButton {
|
||||
id: imapSSLButton
|
||||
colorScheme: root.colorScheme
|
||||
ButtonGroup.group: imapProtocolSelection
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("SSL")
|
||||
}
|
||||
|
||||
RadioButton {
|
||||
id: imapSTARTTLSButton
|
||||
colorScheme: root.colorScheme
|
||||
ButtonGroup.group: imapProtocolSelection
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("STARTTLS")
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 1
|
||||
color: root.colorScheme.border_weak
|
||||
height: 1
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 16
|
||||
|
||||
ButtonGroup{ id: smtpProtocolSelection }
|
||||
|
||||
ButtonGroup {
|
||||
id: smtpProtocolSelection
|
||||
}
|
||||
Label {
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("SMTP connection")
|
||||
}
|
||||
|
||||
RadioButton {
|
||||
id: smtpSSLButton
|
||||
colorScheme: root.colorScheme
|
||||
ButtonGroup.group: smtpProtocolSelection
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("SSL")
|
||||
}
|
||||
|
||||
RadioButton {
|
||||
id: smtpSTARTTLSButton
|
||||
colorScheme: root.colorScheme
|
||||
ButtonGroup.group: smtpProtocolSelection
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("STARTTLS")
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 1
|
||||
color: root.colorScheme.border_weak
|
||||
height: 1
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: 12
|
||||
|
||||
Button {
|
||||
id: submitButton
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Save")
|
||||
onClicked: {
|
||||
submitButton.loading = true
|
||||
root.submit()
|
||||
}
|
||||
|
||||
enabled: (!loading) && ((imapSSLButton.checked !== Backend.useSSLForIMAP) || (smtpSSLButton.checked !== Backend.useSSLForSMTP))
|
||||
}
|
||||
text: qsTr("Save")
|
||||
|
||||
onClicked: {
|
||||
submitButton.loading = true;
|
||||
root.submit();
|
||||
}
|
||||
}
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Cancel")
|
||||
onClicked: root.back()
|
||||
secondary: true
|
||||
}
|
||||
text: qsTr("Cancel")
|
||||
|
||||
onClicked: root.back()
|
||||
}
|
||||
Connections {
|
||||
target: Backend
|
||||
|
||||
function onChangeMailServerSettingsFinished() {
|
||||
submitButton.loading = false
|
||||
root.back()
|
||||
submitButton.loading = false;
|
||||
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
|
||||
//
|
||||
// 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 ColorScheme colorScheme
|
||||
property var notifications
|
||||
|
||||
signal showSetupGuide(var user, string address)
|
||||
signal closeWindow()
|
||||
signal quitBridge()
|
||||
signal closeWindow
|
||||
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 {
|
||||
anchors.fill: parent
|
||||
@ -38,13 +64,13 @@ Item {
|
||||
|
||||
Rectangle {
|
||||
id: leftBar
|
||||
|
||||
property ColorScheme colorScheme: root.colorScheme.prominent
|
||||
|
||||
Layout.minimumWidth: 264
|
||||
Layout.maximumWidth: 320
|
||||
Layout.preferredWidth: 320
|
||||
Layout.fillHeight: true
|
||||
|
||||
Layout.maximumWidth: 320
|
||||
Layout.minimumWidth: 264
|
||||
Layout.preferredWidth: 320
|
||||
color: colorScheme.background_norm
|
||||
|
||||
ColumnLayout {
|
||||
@ -52,24 +78,21 @@ Item {
|
||||
spacing: 0
|
||||
|
||||
RowLayout {
|
||||
id:topLeftBar
|
||||
|
||||
id: topLeftBar
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumHeight: 60
|
||||
Layout.maximumHeight: 60
|
||||
Layout.minimumHeight: 60
|
||||
Layout.preferredHeight: 60
|
||||
spacing: 0
|
||||
|
||||
Status {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.bottomMargin: 17
|
||||
Layout.leftMargin: 16
|
||||
Layout.topMargin: 24
|
||||
Layout.bottomMargin: 17
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
colorScheme: leftBar.colorScheme
|
||||
notifications: root.notifications
|
||||
|
||||
notificationWhitelist: Notifications.Group.Connection | Notifications.Group.ForceUpdate
|
||||
notifications: root.notifications
|
||||
}
|
||||
|
||||
// just a placeholder
|
||||
@ -77,47 +100,38 @@ Item {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
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.maximumHeight: 36
|
||||
Layout.maximumWidth: 36
|
||||
Layout.minimumHeight: 36
|
||||
Layout.minimumWidth: 36
|
||||
Layout.preferredHeight: 36
|
||||
Layout.preferredWidth: 36
|
||||
Layout.rightMargin: 4
|
||||
|
||||
Layout.topMargin: 16
|
||||
colorScheme: leftBar.colorScheme
|
||||
horizontalPadding: 0
|
||||
|
||||
icon.source: "/qml/icons/ic-question-circle.svg"
|
||||
|
||||
onClicked: rightContent.showHelpView()
|
||||
}
|
||||
|
||||
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.maximumHeight: 36
|
||||
Layout.maximumWidth: 36
|
||||
Layout.minimumHeight: 36
|
||||
Layout.minimumWidth: 36
|
||||
Layout.preferredHeight: 36
|
||||
Layout.preferredWidth: 36
|
||||
Layout.rightMargin: 4
|
||||
|
||||
Layout.topMargin: 16
|
||||
colorScheme: leftBar.colorScheme
|
||||
horizontalPadding: 0
|
||||
|
||||
icon.source: "/qml/icons/ic-cog-wheel.svg"
|
||||
|
||||
onClicked: rightContent.showGeneralSettings()
|
||||
}
|
||||
|
||||
Button {
|
||||
id: dotMenuButton
|
||||
Layout.bottomMargin: 9
|
||||
@ -134,7 +148,7 @@ Item {
|
||||
icon.source: "/qml/icons/ic-three-dots-vertical.svg"
|
||||
|
||||
onClicked: {
|
||||
dotMenu.open()
|
||||
dotMenu.open();
|
||||
}
|
||||
|
||||
Menu {
|
||||
@ -143,332 +157,329 @@ Item {
|
||||
modal: true
|
||||
y: dotMenuButton.Layout.preferredHeight + dotMenuButton.Layout.bottomMargin
|
||||
|
||||
onClosed: {
|
||||
parent.checked = false;
|
||||
}
|
||||
onOpened: {
|
||||
parent.checked = true;
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Close window")
|
||||
|
||||
onClicked: {
|
||||
root.closeWindow()
|
||||
root.closeWindow();
|
||||
}
|
||||
}
|
||||
MenuItem {
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Quit Bridge")
|
||||
onClicked: {
|
||||
root.quitBridge()
|
||||
}
|
||||
}
|
||||
|
||||
onClosed: {
|
||||
parent.checked = false
|
||||
}
|
||||
onOpened: {
|
||||
parent.checked = true
|
||||
onClicked: {
|
||||
root.quitBridge();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {implicitHeight:10}
|
||||
Item {
|
||||
implicitHeight: 10
|
||||
}
|
||||
|
||||
// Separator line
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumHeight: 1
|
||||
Layout.maximumHeight: 1
|
||||
Layout.minimumHeight: 1
|
||||
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 {
|
||||
id: accounts
|
||||
|
||||
property var _topBottomMargins: 24
|
||||
property var _leftRightMargins: 16
|
||||
property var _topBottomMargins: 24
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.bottomMargin: accounts._topBottomMargins
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: accounts._leftRightMargins
|
||||
Layout.rightMargin: accounts._leftRightMargins
|
||||
Layout.topMargin: accounts._topBottomMargins
|
||||
Layout.bottomMargin: accounts._topBottomMargins
|
||||
|
||||
spacing: 12
|
||||
clip: true
|
||||
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 {
|
||||
height: headerLabel.height+16
|
||||
// color: ProtonStyle.transparent
|
||||
Label{
|
||||
AccountDelegate {
|
||||
id: accountDelegate
|
||||
anchors.bottomMargin: 8
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 12
|
||||
anchors.rightMargin: 12
|
||||
anchors.topMargin: 8
|
||||
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
|
||||
colorScheme: leftBar.colorScheme
|
||||
text: qsTr("Accounts")
|
||||
type: Label.LabelType.Body
|
||||
}
|
||||
}
|
||||
|
||||
highlight: Rectangle {
|
||||
color: leftBar.colorScheme.interaction_default_active
|
||||
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
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumHeight: 1
|
||||
Layout.maximumHeight: 1
|
||||
Layout.minimumHeight: 1
|
||||
color: leftBar.colorScheme.border_weak
|
||||
}
|
||||
|
||||
Item {
|
||||
id: bottomLeftBar
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumHeight: 52
|
||||
Layout.maximumHeight: 52
|
||||
Layout.minimumHeight: 52
|
||||
Layout.preferredHeight: 52
|
||||
|
||||
Button {
|
||||
colorScheme: leftBar.colorScheme
|
||||
width: 36
|
||||
height: 36
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
|
||||
anchors.leftMargin: 16
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 7
|
||||
|
||||
colorScheme: leftBar.colorScheme
|
||||
height: 36
|
||||
horizontalPadding: 0
|
||||
|
||||
icon.source: "/qml/icons/ic-plus.svg"
|
||||
width: 36
|
||||
|
||||
onClicked: {
|
||||
signIn.username = ""
|
||||
rightContent.showSignIn()
|
||||
root.showLogin("");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle { // right content background
|
||||
Rectangle {
|
||||
Layout.fillHeight: true // right content background
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
color: colorScheme.background_norm
|
||||
|
||||
StackLayout {
|
||||
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
|
||||
|
||||
AccountView { // 0
|
||||
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)
|
||||
}
|
||||
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
|
||||
|
||||
StackLayout {
|
||||
// 0
|
||||
currentIndex: hasAccount() ? 1 : 0
|
||||
NoAccountView {
|
||||
colorScheme: root.colorScheme
|
||||
onClicked: {
|
||||
signIn.abort()
|
||||
rightContent.showAccount()
|
||||
onStartSetup: {
|
||||
root.showLogin("")
|
||||
}
|
||||
icon.source: "/qml/icons/ic-arrow-left.svg"
|
||||
secondary: true
|
||||
horizontalPadding: 8
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
AccountView {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GeneralSettings { // 2
|
||||
Rectangle {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
color: "#ff9900"
|
||||
}
|
||||
GeneralSettings {
|
||||
// 2
|
||||
colorScheme: root.colorScheme
|
||||
notifications: root.notifications
|
||||
|
||||
onBack: {
|
||||
rightContent.showAccount()
|
||||
rightContent.showAccount();
|
||||
}
|
||||
}
|
||||
|
||||
KeychainSettings { // 3
|
||||
KeychainSettings {
|
||||
// 3
|
||||
colorScheme: root.colorScheme
|
||||
|
||||
onBack: {
|
||||
rightContent.showGeneralSettings()
|
||||
rightContent.showGeneralSettings();
|
||||
}
|
||||
}
|
||||
|
||||
PortSettings { // 4
|
||||
colorScheme: root.colorScheme
|
||||
notifications: root.notifications
|
||||
onBack: {
|
||||
rightContent.showGeneralSettings()
|
||||
}
|
||||
}
|
||||
|
||||
ConnectionModeSettings { // 5
|
||||
colorScheme: root.colorScheme
|
||||
|
||||
onBack: {
|
||||
rightContent.showGeneralSettings()
|
||||
}
|
||||
}
|
||||
|
||||
LocalCacheSettings { // 6
|
||||
PortSettings {
|
||||
// 4
|
||||
colorScheme: root.colorScheme
|
||||
notifications: root.notifications
|
||||
|
||||
onBack: {
|
||||
rightContent.showGeneralSettings()
|
||||
rightContent.showGeneralSettings();
|
||||
}
|
||||
}
|
||||
|
||||
HelpView { // 7
|
||||
ConnectionModeSettings {
|
||||
// 5
|
||||
colorScheme: root.colorScheme
|
||||
|
||||
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
|
||||
colorScheme: root.colorScheme
|
||||
selectedAddress: {
|
||||
if (accounts.currentIndex < 0) return ""
|
||||
if (Backend.users.count == 0) return ""
|
||||
var user = Backend.users.get(accounts.currentIndex)
|
||||
if (!user) return ""
|
||||
return user.addresses[0]
|
||||
if (accounts.currentIndex < 0)
|
||||
return "";
|
||||
if (Backend.users.count === 0)
|
||||
return "";
|
||||
const user = Backend.users.get(accounts.currentIndex);
|
||||
if (!user)
|
||||
return "";
|
||||
return user.addresses[0];
|
||||
}
|
||||
|
||||
onBack: {
|
||||
rightContent.showHelpView()
|
||||
rightContent.showHelpView();
|
||||
}
|
||||
|
||||
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 {
|
||||
target: Backend
|
||||
function onLoginAlreadyLoggedIn(index) {
|
||||
rightContent.showAccount(index);
|
||||
}
|
||||
function onLoginFinished(index) {
|
||||
rightContent.showAccount(index);
|
||||
}
|
||||
|
||||
function onLoginFinished(index) { rightContent.showAccount(index) }
|
||||
function onLoginAlreadyLoggedIn(index) { rightContent.showAccount(index) }
|
||||
target: Backend
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
//
|
||||
// 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.Controls
|
||||
|
||||
import "."
|
||||
import "./Proton"
|
||||
import "Proton"
|
||||
|
||||
Rectangle {
|
||||
property var target: parent
|
||||
|
||||
x: target.x
|
||||
y: target.y
|
||||
width: target.width
|
||||
height: target.height
|
||||
|
||||
color: "transparent"
|
||||
border.color: "red"
|
||||
border.width: 1
|
||||
color: "transparent"
|
||||
height: target.height
|
||||
width: target.width
|
||||
x: target.x
|
||||
y: target.y
|
||||
//z: parent.z - 1
|
||||
z: 10000000
|
||||
|
||||
Label {
|
||||
text: parent.width + "x" + parent.height
|
||||
anchors.centerIn: parent
|
||||
color: "black"
|
||||
colorScheme: ProtonStyle.currentStyle
|
||||
text: parent.width + "x" + parent.height
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: target.implicitWidth
|
||||
height: target.implicitHeight
|
||||
|
||||
color: "transparent"
|
||||
border.color: "green"
|
||||
border.width: 1
|
||||
color: "transparent"
|
||||
height: target.implicitHeight
|
||||
width: target.implicitWidth
|
||||
//z: parent.z - 1
|
||||
z: 10000000
|
||||
}
|
||||
|
||||
@ -1,25 +1,19 @@
|
||||
// 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 QtQuick.Controls.impl
|
||||
|
||||
import Proton
|
||||
|
||||
SettingsView {
|
||||
@ -31,144 +25,138 @@ SettingsView {
|
||||
fillHeight: false
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Settings")
|
||||
type: Label.Heading
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
SettingsItem {
|
||||
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
|
||||
}
|
||||
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 {
|
||||
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
|
||||
}
|
||||
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 {
|
||||
id: beta
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Beta access")
|
||||
description: qsTr("Be among the first to try new features.")
|
||||
type: SettingsItem.Toggle
|
||||
Layout.fillWidth: true
|
||||
checked: Backend.isBetaEnabled
|
||||
colorScheme: root.colorScheme
|
||||
description: qsTr("Be among the first to try new features.")
|
||||
text: qsTr("Beta access")
|
||||
type: SettingsItem.Toggle
|
||||
|
||||
onClicked: {
|
||||
if (!beta.checked) {
|
||||
root.notifications.askEnableBeta()
|
||||
root.notifications.askEnableBeta();
|
||||
} else {
|
||||
Backend.toggleBeta(false)
|
||||
Backend.toggleBeta(false);
|
||||
}
|
||||
}
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
ColorImage {
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
|
||||
source: root._isAdvancedShown ? "/qml/icons/ic-chevron-down.svg" : "/qml/icons/ic-chevron-right.svg"
|
||||
color: root.colorScheme.interaction_norm
|
||||
height: root.colorScheme.body_font_size
|
||||
source: root._isAdvancedShown ? "/qml/icons/ic-chevron-down.svg" : "/qml/icons/ic-chevron-right.svg"
|
||||
sourceSize.height: root.colorScheme.body_font_size
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
|
||||
onClicked: root._isAdvancedShown = !root._isAdvancedShown
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
id: advSettLabel
|
||||
color: root.colorScheme.interaction_norm
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Advanced settings")
|
||||
color: root.colorScheme.interaction_norm
|
||||
type: Label.Body
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
|
||||
onClicked: root._isAdvancedShown = !root._isAdvancedShown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsItem {
|
||||
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
|
||||
}
|
||||
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 {
|
||||
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
|
||||
}
|
||||
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 {
|
||||
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
|
||||
}
|
||||
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 {
|
||||
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
|
||||
}
|
||||
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 {
|
||||
id: telemetry
|
||||
Layout.fillWidth: true
|
||||
@ -181,73 +169,68 @@ SettingsView {
|
||||
|
||||
onClicked: Backend.toggleIsTelemetryDisabled(telemetry.checked)
|
||||
}
|
||||
|
||||
SettingsItem {
|
||||
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
|
||||
}
|
||||
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 {
|
||||
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
|
||||
}
|
||||
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 {
|
||||
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
|
||||
}
|
||||
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 {
|
||||
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
|
||||
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 {
|
||||
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
|
||||
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
|
||||
//
|
||||
// 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
|
||||
|
||||
fillHeight: true
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Help")
|
||||
type: Label.Heading
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
SettingsItem {
|
||||
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
|
||||
}
|
||||
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 {
|
||||
id: checkUpdates
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Updates")
|
||||
Layout.fillWidth: true
|
||||
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
|
||||
|
||||
onClicked: {
|
||||
checkUpdates.loading = true
|
||||
Backend.checkUpdates()
|
||||
checkUpdates.loading = true;
|
||||
Backend.checkUpdates();
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onCheckUpdatesFinished() {
|
||||
checkUpdates.loading = false;
|
||||
}
|
||||
|
||||
target: Backend
|
||||
function onCheckUpdatesFinished() { checkUpdates.loading = false }
|
||||
}
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
SettingsItem {
|
||||
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
|
||||
}
|
||||
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 {
|
||||
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
|
||||
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 {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
colorScheme: root.colorScheme
|
||||
type: Label.Caption
|
||||
color: root.colorScheme.text_weak
|
||||
textFormat: Text.StyledText
|
||||
|
||||
colorScheme: root.colorScheme
|
||||
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").
|
||||
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")))
|
||||
|
||||
onLinkActivated: function(link) { Qt.openUrlExternally(link) }
|
||||
onLinkActivated: function (link) {
|
||||
Qt.openUrlExternally(link)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,116 +1,105 @@
|
||||
// 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 QtQuick.Controls.impl
|
||||
|
||||
import Proton
|
||||
|
||||
SettingsView {
|
||||
id: root
|
||||
|
||||
fillHeight: false
|
||||
property bool _valuesChanged: keychainSelection.checkedButton && keychainSelection.checkedButton.text !== Backend.currentKeychain
|
||||
|
||||
property bool _valuesChanged: keychainSelection.checkedButton && keychainSelection.checkedButton.text != Backend.currentKeychain
|
||||
|
||||
Label {
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Default keychain")
|
||||
type: Label.Heading
|
||||
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
|
||||
function setDefaultValues() {
|
||||
for (const bi in keychainSelection.buttons) {
|
||||
const button = keychainSelection.buttons[bi];
|
||||
if (button.text === Backend.currentKeychain) {
|
||||
button.checked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fillHeight: false
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 1
|
||||
color: root.colorScheme.border_weak
|
||||
Component.onCompleted: root.setDefaultValues()
|
||||
onBack: {
|
||||
root.setDefaultValues();
|
||||
}
|
||||
|
||||
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 {
|
||||
spacing: 12
|
||||
|
||||
Button {
|
||||
id: submitButton
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Save and restart")
|
||||
enabled: root._valuesChanged
|
||||
text: qsTr("Save and restart")
|
||||
|
||||
onClicked: {
|
||||
Backend.changeKeychain(keychainSelection.checkedButton.text)
|
||||
Backend.changeKeychain(keychainSelection.checkedButton.text);
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Cancel")
|
||||
onClicked: root.back()
|
||||
secondary: true
|
||||
}
|
||||
text: qsTr("Cancel")
|
||||
|
||||
onClicked: root.back()
|
||||
}
|
||||
Connections {
|
||||
target: Backend
|
||||
|
||||
function onChangeKeychainFinished() {
|
||||
submitButton.loading = false
|
||||
root.back()
|
||||
submitButton.loading = false;
|
||||
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
|
||||
//
|
||||
// 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 QtQuick.Controls.impl
|
||||
import QtQuick.Dialogs
|
||||
|
||||
import Proton
|
||||
|
||||
SettingsView {
|
||||
id: root
|
||||
|
||||
fillHeight: false
|
||||
|
||||
property var notifications
|
||||
property url diskCachePath: pathDialog.shortcuts.home
|
||||
property var notifications
|
||||
|
||||
function refresh() {
|
||||
diskCacheSetting.description = Backend.nativePath(root.diskCachePath)
|
||||
submitButton.enabled = (!submitButton.loading) && !Backend.areSameFileOrFolder(Backend.diskCachePath, root.diskCachePath)
|
||||
diskCacheSetting.description = Backend.nativePath(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 {
|
||||
Layout.fillWidth: true
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Local cache")
|
||||
type: Label.Heading
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
color: root.colorScheme.text_weak
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Bridge stores your encrypted messages locally to optimize communication with your client.")
|
||||
type: Label.Body
|
||||
color: root.colorScheme.text_weak
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
SettingsItem {
|
||||
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
|
||||
actionText: qsTr("Change location")
|
||||
colorScheme: root.colorScheme
|
||||
descriptionWrap: Text.WrapAnywhere
|
||||
text: qsTr("Current cache location")
|
||||
type: SettingsItem.Button
|
||||
|
||||
onClicked: {
|
||||
pathDialog.open();
|
||||
}
|
||||
|
||||
FolderDialog {
|
||||
id: pathDialog
|
||||
title: qsTr("Select cache location")
|
||||
currentFolder: root.diskCachePath
|
||||
onAccepted: {
|
||||
root.diskCachePath = pathDialog.selectedFolder
|
||||
root.refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
title: qsTr("Select cache location")
|
||||
|
||||
onAccepted: {
|
||||
root.diskCachePath = pathDialog.selectedFolder;
|
||||
root.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
RowLayout {
|
||||
spacing: 12
|
||||
|
||||
@ -83,43 +90,25 @@ SettingsView {
|
||||
id: submitButton
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Save")
|
||||
|
||||
onClicked: {
|
||||
root.submit()
|
||||
root.submit();
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Cancel")
|
||||
onClicked: root.back()
|
||||
secondary: true
|
||||
}
|
||||
text: qsTr("Cancel")
|
||||
|
||||
onClicked: root.back()
|
||||
}
|
||||
Connections {
|
||||
target: Backend
|
||||
|
||||
function onDiskCachePathChangeFinished() {
|
||||
submitButton.loading = false
|
||||
root.setDefaultValues()
|
||||
submitButton.loading = false;
|
||||
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
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import QtQml
|
||||
import QtQuick
|
||||
import QtQuick.Window
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
|
||||
import Proton
|
||||
import Notifications
|
||||
import "SetupWizard"
|
||||
|
||||
ApplicationWindow {
|
||||
id: root
|
||||
colorScheme: ProtonStyle.currentStyle
|
||||
visible: true
|
||||
|
||||
|
||||
property int _defaultWidth: 1080
|
||||
property int _defaultHeight: 780
|
||||
width: _defaultWidth
|
||||
height: _defaultHeight
|
||||
minimumWidth: _defaultWidth
|
||||
|
||||
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
|
||||
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) {
|
||||
for (var i = first; i <= last; i++ ) {
|
||||
var user = Backend.users.get(i)
|
||||
|
||||
if (setupGuide.user === user) {
|
||||
setupGuide.user = null
|
||||
contentLayout._showSetup = false
|
||||
return
|
||||
for (let i = first; i <= last; i++) {
|
||||
const user = Backend.users.get(i);
|
||||
if (setupWizard.user === user) {
|
||||
setupWizard.closeWizard();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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 {
|
||||
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) {
|
||||
contentWrapper.selectUser(userID)
|
||||
contentWrapper.selectUser(userID);
|
||||
if (setupWizard.visible) {
|
||||
setupWizard.closeWizard()
|
||||
}
|
||||
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 {
|
||||
id: contentLayout
|
||||
|
||||
anchors.fill: parent
|
||||
currentIndex: 0
|
||||
|
||||
property bool _showSetup: false
|
||||
currentIndex: {
|
||||
// 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
|
||||
ContentWrapper {
|
||||
// 0
|
||||
id: contentWrapper
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
colorScheme: root.colorScheme
|
||||
notifications: root.notifications
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
|
||||
onShowSetupGuide: function(user, address) {
|
||||
root.showSetup(user,address)
|
||||
}
|
||||
|
||||
onCloseWindow: {
|
||||
root.close()
|
||||
root.close();
|
||||
}
|
||||
|
||||
onQuitBridge: {
|
||||
// If we ever want to add a confirmation dialog before quitting:
|
||||
//root.notifications.askQuestion("Quit Bridge", "Insert warning message here.", "Quit", "Cancel", Backend.quit, null)
|
||||
root.close()
|
||||
Backend.quit()
|
||||
root.close();
|
||||
Backend.quit();
|
||||
}
|
||||
onShowClientConfigurator: function (user, address, justLoggedIn) {
|
||||
root.showClientConfigurator(user, address, justLoggedIn);
|
||||
}
|
||||
onShowLogin: function (username) {
|
||||
root.showLogin(username);
|
||||
}
|
||||
}
|
||||
|
||||
WelcomeGuide { // 1
|
||||
colorScheme: root.colorScheme
|
||||
|
||||
SetupWizard {
|
||||
id: setupWizard
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
SetupGuide { // 2
|
||||
id: setupGuide
|
||||
colorScheme: root.colorScheme
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
|
||||
onDismissed: {
|
||||
root.showSetup(null,"")
|
||||
onBugReportRequested: {
|
||||
contentWrapper.showBugReport();
|
||||
}
|
||||
|
||||
onFinished: {
|
||||
// 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,"")
|
||||
onWizardEnded: {
|
||||
contentLayout.currentIndex = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NotificationPopups {
|
||||
colorScheme: root.colorScheme
|
||||
notifications: root.notifications
|
||||
mainWindow: root
|
||||
notifications: root.notifications
|
||||
}
|
||||
|
||||
SplashScreen {
|
||||
id: splashScreen
|
||||
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
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import QtQml
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
|
||||
import Proton
|
||||
import Notifications
|
||||
|
||||
Dialog {
|
||||
id: root
|
||||
|
||||
default property alias data: additionalChildrenContainer.children
|
||||
property var notification
|
||||
|
||||
shouldShow: notification && notification.active && !notification.dismissed
|
||||
modal: true
|
||||
|
||||
default property alias data: additionalChildrenContainer.children
|
||||
shouldShow: notification && notification.active && !notification.dismissed
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 0
|
||||
|
||||
Image {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
sourceSize.width: 64
|
||||
sourceSize.height: 64
|
||||
|
||||
Layout.bottomMargin: 16
|
||||
Layout.preferredHeight: 64
|
||||
Layout.preferredWidth: 64
|
||||
|
||||
Layout.bottomMargin: 16
|
||||
|
||||
visible: source != ""
|
||||
|
||||
source: {
|
||||
if (!root.notification) {
|
||||
return ""
|
||||
return "";
|
||||
}
|
||||
|
||||
switch (root.notification.type) {
|
||||
case Notification.NotificationType.Info:
|
||||
return "/qml/icons/ic-info.svg"
|
||||
case Notification.NotificationType.Success:
|
||||
return "/qml/icons/ic-success.svg"
|
||||
case Notification.NotificationType.Warning:
|
||||
case Notification.NotificationType.Danger:
|
||||
return "/qml/icons/ic-alert.svg"
|
||||
case Notification.NotificationType.Info:
|
||||
return "/qml/icons/ic-info.svg";
|
||||
case Notification.NotificationType.Success:
|
||||
return "/qml/icons/ic-success.svg";
|
||||
case Notification.NotificationType.Warning:
|
||||
case Notification.NotificationType.Danger:
|
||||
return "/qml/icons/ic-alert.svg";
|
||||
}
|
||||
}
|
||||
sourceSize.height: 64
|
||||
sourceSize.width: 64
|
||||
visible: source != ""
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
Layout.bottomMargin: 8
|
||||
colorScheme: root.colorScheme
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: root.notification.title
|
||||
type: Label.LabelType.Title
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.bottomMargin: 16
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: 240
|
||||
Layout.bottomMargin: 16
|
||||
|
||||
colorScheme: root.colorScheme
|
||||
text: root.notification.description
|
||||
wrapMode: Text.WordWrap
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: root.notification.description
|
||||
type: Label.LabelType.Body
|
||||
onLinkActivated: function(link) { Qt.openUrlExternally(link) }
|
||||
}
|
||||
wrapMode: Text.WordWrap
|
||||
|
||||
onLinkActivated: function (link) {
|
||||
Qt.openUrlExternally(link);
|
||||
}
|
||||
}
|
||||
Item {
|
||||
id: additionalChildrenContainer
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.bottomMargin: 16
|
||||
|
||||
visible: children.length > 0
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: additionalChildrenContainer.childrenRect.height
|
||||
implicitWidth: additionalChildrenContainer.childrenRect.width
|
||||
visible: children.length > 0
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 8
|
||||
|
||||
Repeater {
|
||||
model: root.notification.action
|
||||
|
||||
delegate: Button {
|
||||
Layout.fillWidth: true
|
||||
|
||||
colorScheme: root.colorScheme
|
||||
action: modelData
|
||||
|
||||
secondary: index > 0
|
||||
|
||||
colorScheme: root.colorScheme
|
||||
loading: modelData.loading
|
||||
secondary: index > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,25 +1,19 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import QtQml
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
|
||||
import Proton
|
||||
import Notifications
|
||||
|
||||
@ -27,118 +21,98 @@ Item {
|
||||
id: root
|
||||
|
||||
property ColorScheme colorScheme
|
||||
property var notifications
|
||||
property var mainWindow
|
||||
|
||||
property int notificationWhitelist: NotificationFilter.FilterConsts.All
|
||||
property int notificationBlacklist: NotificationFilter.FilterConsts.None
|
||||
property int notificationWhitelist: NotificationFilter.FilterConsts.All
|
||||
property var notifications
|
||||
|
||||
NotificationFilter {
|
||||
id: bannerNotificationFilter
|
||||
|
||||
source: root.notifications.all
|
||||
blacklist: Notifications.Group.Dialogs
|
||||
source: root.notifications.all
|
||||
}
|
||||
|
||||
Banner {
|
||||
colorScheme: root.colorScheme
|
||||
notification: bannerNotificationFilter.topmost
|
||||
mainWindow: root.mainWindow
|
||||
notification: bannerNotificationFilter.topmost
|
||||
}
|
||||
|
||||
NotificationDialog {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.updateManualReady
|
||||
|
||||
Switch {
|
||||
id:autoUpdate
|
||||
id: autoUpdate
|
||||
checked: Backend.isAutomaticUpdateOn
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Update automatically in the future")
|
||||
checked: Backend.isAutomaticUpdateOn
|
||||
|
||||
onClicked: Backend.toggleAutomaticUpdate(autoUpdate.checked)
|
||||
}
|
||||
}
|
||||
|
||||
NotificationDialog {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.updateForce
|
||||
}
|
||||
|
||||
NotificationDialog {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.updateForceError
|
||||
}
|
||||
|
||||
NotificationDialog {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.enableBeta
|
||||
}
|
||||
|
||||
NotificationDialog {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.cacheUnavailable
|
||||
}
|
||||
|
||||
NotificationDialog {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.cacheCantMove
|
||||
}
|
||||
|
||||
NotificationDialog {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.diskFull
|
||||
}
|
||||
|
||||
NotificationDialog {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.enableSplitMode
|
||||
}
|
||||
|
||||
NotificationDialog {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.resetBridge
|
||||
}
|
||||
|
||||
NotificationDialog {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.changeAllMailVisibility
|
||||
}
|
||||
|
||||
NotificationDialog {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.deleteAccount
|
||||
}
|
||||
|
||||
NotificationDialog {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.noKeychain
|
||||
}
|
||||
|
||||
NotificationDialog {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.rebuildKeychain
|
||||
}
|
||||
|
||||
NotificationDialog {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.apiCertIssue
|
||||
}
|
||||
|
||||
NotificationDialog {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.noActiveKeyForRecipient
|
||||
}
|
||||
|
||||
NotificationDialog {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.userBadEvent
|
||||
}
|
||||
|
||||
NotificationDialog {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.genericError
|
||||
}
|
||||
|
||||
NotificationDialog {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.genericQuestion
|
||||
|
||||
@ -1,54 +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/>.
|
||||
|
||||
import QtQml
|
||||
import QtQuick.Controls
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
default property var children
|
||||
|
||||
enum NotificationType {
|
||||
Info = 0,
|
||||
Success = 1,
|
||||
Warning = 2,
|
||||
Danger = 3
|
||||
Info,
|
||||
Success,
|
||||
Warning,
|
||||
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
|
||||
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 group
|
||||
|
||||
property bool dismissed: false
|
||||
property bool active: false
|
||||
readonly property var occurred: active ? new Date() : undefined
|
||||
|
||||
property var data
|
||||
|
||||
onActiveChanged: {
|
||||
dismissed = false
|
||||
dismissed = false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,114 +1,95 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import QtQml
|
||||
import QtQml.Models
|
||||
|
||||
// contains notifications that satisfy black- and whitelist and are sorted in time-occurred order
|
||||
ListModel {
|
||||
id: root
|
||||
|
||||
enum FilterConsts {
|
||||
None = 0,
|
||||
None,
|
||||
All = 255
|
||||
}
|
||||
|
||||
property int whitelist: NotificationFilter.FilterConsts.All
|
||||
property int blacklist: NotificationFilter.FilterConsts.None
|
||||
|
||||
property Notification topmost
|
||||
property var source
|
||||
|
||||
property bool componentCompleted: false
|
||||
Component.onCompleted: {
|
||||
root.componentCompleted = true
|
||||
root.rebuildList()
|
||||
}
|
||||
property var source
|
||||
property Notification topmost
|
||||
property int whitelist: NotificationFilter.FilterConsts.All
|
||||
|
||||
// overriding get method to ignore any role and return directly object itself
|
||||
function get(row) {
|
||||
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() {
|
||||
let i;
|
||||
// avoid evaluation of the list before Component.onCompleted
|
||||
if (!root.componentCompleted) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < root.count; i++) {
|
||||
root.get(i).onActiveChanged.disconnect( root.updateList )
|
||||
for (i = 0; i < root.count; i++) {
|
||||
root.get(i).onActiveChanged.disconnect(root.updateList);
|
||||
}
|
||||
|
||||
root.clear()
|
||||
|
||||
root.clear();
|
||||
if (!root.source) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
for (i = 0; i < root.source.length; i++) {
|
||||
var obj = root.source[i]
|
||||
const obj = root.source[i];
|
||||
if (obj.group & root.blacklist) {
|
||||
continue
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!(obj.group & root.whitelist)) {
|
||||
continue
|
||||
continue;
|
||||
}
|
||||
|
||||
root.append({obj})
|
||||
obj.onActiveChanged.connect( root.updateList )
|
||||
root.append({
|
||||
"obj": obj
|
||||
});
|
||||
obj.onActiveChanged.connect(root.updateList);
|
||||
}
|
||||
}
|
||||
|
||||
function updateList() {
|
||||
var topmost = null
|
||||
|
||||
for (var i = 0; i < root.count; i++) {
|
||||
var obj = root.get(i)
|
||||
|
||||
let topmost = null;
|
||||
for (let i = 0; i < root.count; i++) {
|
||||
const obj = root.get(i);
|
||||
if (!obj.active) {
|
||||
continue
|
||||
continue;
|
||||
}
|
||||
|
||||
if (topmost && (topmost.type > obj.type)) {
|
||||
continue
|
||||
continue;
|
||||
}
|
||||
|
||||
if (topmost && (topmost.type === obj.type) && (topmost.occurred > obj.occurred)) {
|
||||
continue
|
||||
continue;
|
||||
}
|
||||
|
||||
topmost = obj
|
||||
topmost = obj;
|
||||
}
|
||||
|
||||
root.topmost = topmost
|
||||
root.topmost = topmost;
|
||||
}
|
||||
|
||||
onWhitelistChanged: {
|
||||
root.rebuildList()
|
||||
Component.onCompleted: {
|
||||
root.componentCompleted = true;
|
||||
root.rebuildList();
|
||||
}
|
||||
onBlacklistChanged: {
|
||||
root.rebuildList()
|
||||
root.rebuildList();
|
||||
}
|
||||
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
|
||||
//
|
||||
// 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 QtQuick.Controls.impl
|
||||
|
||||
import Proton
|
||||
|
||||
SettingsView {
|
||||
id: root
|
||||
|
||||
fillHeight: false
|
||||
|
||||
property bool _valuesChanged: (imapField.text * 1 !== Backend.imapPort || smtpField.text * 1 !== Backend.smtpPort)
|
||||
property var notifications
|
||||
|
||||
property bool _valuesChanged: (
|
||||
imapField.text*1 !== Backend.imapPort ||
|
||||
smtpField.text*1 !== Backend.smtpPort
|
||||
)
|
||||
function isPortFree(field) {
|
||||
const 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;
|
||||
}
|
||||
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 {
|
||||
Layout.fillWidth: true
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Default ports")
|
||||
type: Label.Heading
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
color: root.colorScheme.text_weak
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Changes require reconfiguration of your email client.")
|
||||
type: Label.Body
|
||||
color: root.colorScheme.text_weak
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: 16
|
||||
|
||||
TextField {
|
||||
id: imapField
|
||||
Layout.alignment: Qt.AlignTop | Qt.AlignLeft
|
||||
Layout.preferredWidth: 160
|
||||
colorScheme: root.colorScheme
|
||||
label: qsTr("IMAP port")
|
||||
Layout.preferredWidth: 160
|
||||
Layout.alignment: Qt.AlignTop | Qt.AlignLeft
|
||||
validator: root.validate
|
||||
}
|
||||
TextField {
|
||||
id: smtpField
|
||||
Layout.alignment: Qt.AlignTop | Qt.AlignLeft
|
||||
Layout.preferredWidth: 160
|
||||
colorScheme: root.colorScheme
|
||||
label: qsTr("SMTP port")
|
||||
Layout.preferredWidth: 160
|
||||
Layout.alignment: Qt.AlignTop | Qt.AlignLeft
|
||||
validator: root.validate
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 1
|
||||
color: root.colorScheme.border_weak
|
||||
height: 1
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: 12
|
||||
|
||||
Button {
|
||||
id: submitButton
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Save")
|
||||
enabled: (!loading) && root._valuesChanged
|
||||
text: qsTr("Save")
|
||||
|
||||
onClicked: {
|
||||
// removing error here because we may have set it manually (port occupied)
|
||||
imapField.error = false
|
||||
smtpField.error = false
|
||||
imapField.error = false;
|
||||
smtpField.error = false;
|
||||
|
||||
// checking errors separately because we want to display "same port" error only once
|
||||
imapField.validate()
|
||||
imapField.validate();
|
||||
if (imapField.error) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
smtpField.validate()
|
||||
smtpField.validate();
|
||||
if (smtpField.error) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
submitButton.loading = true
|
||||
submitButton.loading = true;
|
||||
|
||||
// check both ports before returning an error
|
||||
var err = false
|
||||
err |= !isPortFree(imapField)
|
||||
err |= !isPortFree(smtpField)
|
||||
let err = false;
|
||||
err |= !isPortFree(imapField);
|
||||
err |= !isPortFree(smtpField);
|
||||
if (err) {
|
||||
submitButton.loading = false
|
||||
return
|
||||
submitButton.loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// We turn off all port error notification. They well be restored if problems persist
|
||||
root.notifications.imapPortStartupError.active = false
|
||||
root.notifications.smtpPortStartupError.active = false
|
||||
root.notifications.imapPortChangeError.active = false
|
||||
root.notifications.smtpPortChangeError.active = false
|
||||
|
||||
Backend.setMailServerSettings(imapField.text, smtpField.text, Backend.useSSLForIMAP, Backend.useSSLForSMTP)
|
||||
// We turn off all port error notification. They will be restored if problems persist
|
||||
root.notifications.imapPortStartupError.active = false;
|
||||
root.notifications.smtpPortStartupError.active = false;
|
||||
root.notifications.imapPortChangeError.active = false;
|
||||
root.notifications.smtpPortChangeError.active = false;
|
||||
Backend.setMailServerSettings(imapField.text, smtpField.text, Backend.useSSLForIMAP, Backend.useSSLForSMTP);
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Cancel")
|
||||
onClicked: root.back()
|
||||
secondary: true
|
||||
}
|
||||
text: qsTr("Cancel")
|
||||
|
||||
onClicked: root.back()
|
||||
}
|
||||
Connections {
|
||||
target: Backend
|
||||
|
||||
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
|
||||
//
|
||||
// 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.Templates as T
|
||||
|
||||
|
||||
@ -1,20 +1,15 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import QtQml
|
||||
import QtQuick
|
||||
import QtQuick.Window
|
||||
@ -25,14 +20,14 @@ import QtQuick.Templates as T
|
||||
T.ApplicationWindow {
|
||||
id: root
|
||||
|
||||
property ColorScheme colorScheme
|
||||
|
||||
// popup priority based on types
|
||||
enum PopupType {
|
||||
Banner = 0,
|
||||
Dialog = 1
|
||||
Banner,
|
||||
Dialog
|
||||
}
|
||||
|
||||
property ColorScheme colorScheme
|
||||
|
||||
// contains currently visible popup
|
||||
property var popupVisible: null
|
||||
|
||||
@ -41,85 +36,61 @@ T.ApplicationWindow {
|
||||
// overriding get method to ignore any role and return directly object itself
|
||||
function get(row) {
|
||||
if (row < 0 || row >= count) {
|
||||
return undefined
|
||||
return undefined;
|
||||
}
|
||||
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()
|
||||
return data(index(row, 0), Qt.DisplayRole);
|
||||
}
|
||||
|
||||
onRowsAboutToBeRemoved: function (parent, first, last) {
|
||||
for (var i = first; i <= last; i++ ) {
|
||||
var obj = popups.get(i)
|
||||
obj.onShouldShowChanged.disconnect( root.processPopups )
|
||||
for (let i = first; i <= last; i++) {
|
||||
const obj = popups.get(i);
|
||||
obj.onShouldShowChanged.disconnect(root.processPopups);
|
||||
|
||||
// if currently visible popup was removed
|
||||
if (root.popupVisible === obj) {
|
||||
root.popupVisible.visible = false
|
||||
root.popupVisible = null
|
||||
root.popupVisible.visible = false;
|
||||
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() {
|
||||
if ((root.popupVisible) && (!root.popupVisible.shouldShow)) {
|
||||
root.popupVisible.visible = false
|
||||
root.popupVisible.visible = false;
|
||||
}
|
||||
|
||||
var topmost = null
|
||||
for (var i = 0; i < popups.count; i++) {
|
||||
var obj = popups.get(i)
|
||||
|
||||
let topmost = null;
|
||||
for (let i = 0; i < popups.count; i++) {
|
||||
const obj = popups.get(i);
|
||||
if (obj.shouldShow === false) {
|
||||
continue
|
||||
continue;
|
||||
}
|
||||
|
||||
if (topmost && (topmost.popupType > obj.popupType)) {
|
||||
continue
|
||||
continue;
|
||||
}
|
||||
|
||||
if (topmost && (topmost.popupType === obj.popupType) && (topmost.occurred > obj.occurred)) {
|
||||
continue
|
||||
continue;
|
||||
}
|
||||
|
||||
topmost = obj
|
||||
topmost = obj;
|
||||
}
|
||||
|
||||
if (root.popupVisible !== topmost) {
|
||||
if (root.popupVisible) {
|
||||
root.popupVisible.visible = false
|
||||
root.popupVisible.visible = false;
|
||||
}
|
||||
root.popupVisible = topmost
|
||||
root.popupVisible = topmost;
|
||||
}
|
||||
|
||||
if (!root.popupVisible) {
|
||||
return
|
||||
}
|
||||
|
||||
root.popupVisible.visible = true
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.popupVisible
|
||||
|
||||
function onVisibleChanged() {
|
||||
if (root.popupVisible.visible) {
|
||||
return
|
||||
}
|
||||
|
||||
root.popupVisible = null
|
||||
root.processPopups()
|
||||
return;
|
||||
}
|
||||
root.popupVisible.visible = true;
|
||||
}
|
||||
|
||||
color: root.colorScheme.background_norm
|
||||
@ -127,8 +98,19 @@ T.ApplicationWindow {
|
||||
Overlay.modal: Rectangle {
|
||||
color: root.colorScheme.backdrop_norm
|
||||
}
|
||||
|
||||
Overlay.modeless: Rectangle {
|
||||
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