mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-10 12:46:46 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a80fd92018 | |||
| 71063ac5ee |
@ -18,10 +18,6 @@
|
|||||||
---
|
---
|
||||||
image: gitlab.protontech.ch:4567/go/bridge-internal:test-go1.20
|
image: gitlab.protontech.ch:4567/go/bridge-internal:test-go1.20
|
||||||
|
|
||||||
default:
|
|
||||||
tags:
|
|
||||||
- shared-small
|
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
GOPRIVATE: gitlab.protontech.ch
|
GOPRIVATE: gitlab.protontech.ch
|
||||||
GOMAXPROCS: $(( ${CI_TAG_CPU} / 2 ))
|
GOMAXPROCS: $(( ${CI_TAG_CPU} / 2 ))
|
||||||
@ -51,19 +47,6 @@ stages:
|
|||||||
allow_failure: true
|
allow_failure: true
|
||||||
- when: never
|
- when: never
|
||||||
|
|
||||||
.rules-branch-manual-scheduled-and-test-branch-always:
|
|
||||||
rules:
|
|
||||||
- if: $CI_PIPELINE_SOURCE == "schedule"
|
|
||||||
when: always
|
|
||||||
allow_failure: false
|
|
||||||
- if: $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME=~ /^test/
|
|
||||||
when: always
|
|
||||||
allow_failure: false
|
|
||||||
- if: $CI_COMMIT_BRANCH
|
|
||||||
when: manual
|
|
||||||
allow_failure: true
|
|
||||||
- when: never
|
|
||||||
|
|
||||||
# ENV
|
# ENV
|
||||||
.env-windows:
|
.env-windows:
|
||||||
before_script:
|
before_script:
|
||||||
@ -75,7 +58,7 @@ stages:
|
|||||||
- export GO111MODULE=on
|
- export GO111MODULE=on
|
||||||
- export PATH="${GOPATH}/bin:${PATH}"
|
- export PATH="${GOPATH}/bin:${PATH}"
|
||||||
- export MSYSTEM=
|
- export MSYSTEM=
|
||||||
- export QT6DIR=/c/grrrQt/6.4.3/msvc2019_64
|
- export QT6DIR=/c/grrrQt/6.3.2/msvc2019_64
|
||||||
- export PATH=$PATH:${QT6DIR}/bin
|
- export PATH=$PATH:${QT6DIR}/bin
|
||||||
- export PATH="/c/Program Files/Microsoft Visual Studio/2022/Community/Common7/IDE/CommonExtensions/Microsoft/CMake/CMake/bin:$PATH"
|
- export PATH="/c/Program Files/Microsoft Visual Studio/2022/Community/Common7/IDE/CommonExtensions/Microsoft/CMake/CMake/bin:$PATH"
|
||||||
- $(git config --global -l | grep -o 'url.*gitlab.protontech.ch.*insteadof' | xargs -L 1 git config --global --unset &> /dev/null) || echo "nothing to remove"
|
- $(git config --global -l | grep -o 'url.*gitlab.protontech.ch.*insteadof' | xargs -L 1 git config --global --unset &> /dev/null) || echo "nothing to remove"
|
||||||
@ -97,7 +80,7 @@ stages:
|
|||||||
- export PATH="${GOROOT}/bin:$PATH"
|
- export PATH="${GOROOT}/bin:$PATH"
|
||||||
- export GOPATH=~/go1.20
|
- export GOPATH=~/go1.20
|
||||||
- export PATH="${GOPATH}/bin:$PATH"
|
- export PATH="${GOPATH}/bin:$PATH"
|
||||||
- export QT6DIR=/opt/Qt/6.4.3/macos
|
- export QT6DIR=/opt/Qt/6.3.2/macos
|
||||||
- export PATH="${QT6DIR}/bin:$PATH"
|
- export PATH="${QT6DIR}/bin:$PATH"
|
||||||
- uname -a
|
- uname -a
|
||||||
cache: {}
|
cache: {}
|
||||||
@ -105,7 +88,7 @@ stages:
|
|||||||
- macos-m1-bridge
|
- macos-m1-bridge
|
||||||
|
|
||||||
.env-linux-build:
|
.env-linux-build:
|
||||||
image: gitlab.protontech.ch:4567/go/bridge-internal:build-go1.20-qt6.4.3
|
image: gitlab.protontech.ch:4567/go/bridge-internal:build-go1.20-qt6.3.2
|
||||||
variables:
|
variables:
|
||||||
VCPKG_DEFAULT_BINARY_CACHE: ${CI_PROJECT_DIR}/.cache
|
VCPKG_DEFAULT_BINARY_CACHE: ${CI_PROJECT_DIR}/.cache
|
||||||
cache:
|
cache:
|
||||||
@ -122,7 +105,7 @@ stages:
|
|||||||
- $(git config --global -l | grep -o 'url.*gitlab.protontech.ch.*insteadof' | xargs -L 1 git config --global --unset &> /dev/null) || echo "nothing to remove"
|
- $(git config --global -l | grep -o 'url.*gitlab.protontech.ch.*insteadof' | xargs -L 1 git config --global --unset &> /dev/null) || echo "nothing to remove"
|
||||||
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
|
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
|
||||||
tags:
|
tags:
|
||||||
- shared-large
|
- large
|
||||||
|
|
||||||
# Stage: TEST
|
# Stage: TEST
|
||||||
|
|
||||||
@ -133,16 +116,7 @@ lint:
|
|||||||
script:
|
script:
|
||||||
- make lint
|
- make lint
|
||||||
tags:
|
tags:
|
||||||
- shared-medium
|
- medium
|
||||||
|
|
||||||
bug-report-preview:
|
|
||||||
stage: test
|
|
||||||
extends:
|
|
||||||
- .rules-branch-and-MR-manual
|
|
||||||
script:
|
|
||||||
- make lint-bug-report-preview
|
|
||||||
tags:
|
|
||||||
- shared-medium
|
|
||||||
|
|
||||||
.script-test:
|
.script-test:
|
||||||
stage: test
|
stage: test
|
||||||
@ -158,16 +132,7 @@ test-linux:
|
|||||||
extends:
|
extends:
|
||||||
- .script-test
|
- .script-test
|
||||||
tags:
|
tags:
|
||||||
- shared-large
|
- large
|
||||||
|
|
||||||
fuzz-linux:
|
|
||||||
stage: test
|
|
||||||
extends:
|
|
||||||
- .rules-branch-manual-MR-and-devel-always
|
|
||||||
script:
|
|
||||||
- make fuzz
|
|
||||||
tags:
|
|
||||||
- shared-large
|
|
||||||
|
|
||||||
test-linux-race:
|
test-linux-race:
|
||||||
extends:
|
extends:
|
||||||
@ -189,19 +154,11 @@ test-integration-race:
|
|||||||
script:
|
script:
|
||||||
- make test-integration-race
|
- make test-integration-race
|
||||||
|
|
||||||
test-integration-nightly:
|
|
||||||
extends:
|
|
||||||
- test-integration
|
|
||||||
- .rules-branch-manual-scheduled-and-test-branch-always
|
|
||||||
needs:
|
|
||||||
- test-integration
|
|
||||||
script:
|
|
||||||
- make test-integration-nightly
|
|
||||||
|
|
||||||
test-windows:
|
test-windows:
|
||||||
extends:
|
extends:
|
||||||
- .env-windows
|
- .env-windows
|
||||||
- .script-test
|
- .script-test
|
||||||
|
- .rules-branch-and-MR-manual
|
||||||
|
|
||||||
test-darwin:
|
test-darwin:
|
||||||
extends:
|
extends:
|
||||||
@ -211,18 +168,17 @@ test-darwin:
|
|||||||
test-coverage:
|
test-coverage:
|
||||||
stage: test
|
stage: test
|
||||||
extends:
|
extends:
|
||||||
- .rules-branch-manual-scheduled-and-test-branch-always
|
- .rules-branch-manual-MR-and-devel-always
|
||||||
script:
|
script:
|
||||||
- ./utils/coverage.sh
|
- ./utils/coverage.sh
|
||||||
coverage: '/total:.*\(statements\).*\d+\.\d+%/'
|
coverage: '/total:.*\(statements\).*\d+\.\d+%/'
|
||||||
needs:
|
needs:
|
||||||
- test-linux
|
- test-linux
|
||||||
- test-windows
|
#- test-windows
|
||||||
- test-darwin
|
- test-darwin
|
||||||
- test-integration
|
- test-integration
|
||||||
- test-integration-nightly
|
|
||||||
tags:
|
tags:
|
||||||
- shared-small
|
- small
|
||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- coverage*
|
- coverage*
|
||||||
@ -286,18 +242,4 @@ build-windows-qa:
|
|||||||
variables:
|
variables:
|
||||||
BUILD_TAGS: "build_qa"
|
BUILD_TAGS: "build_qa"
|
||||||
|
|
||||||
trigger-qa-installer:
|
|
||||||
stage: build
|
|
||||||
needs: ["lint"]
|
|
||||||
extends:
|
|
||||||
- .rules-branch-and-MR-manual
|
|
||||||
variables:
|
|
||||||
APP: bridge
|
|
||||||
WORKFLOW: build-all
|
|
||||||
SRC_TAG: $CI_COMMIT_BRANCH
|
|
||||||
SRC_HASH: $CI_COMMIT_SHA
|
|
||||||
trigger:
|
|
||||||
project: "jcuth/bridge-release"
|
|
||||||
branch: master
|
|
||||||
|
|
||||||
# TODO: PUT BACK ALL THE JOBS! JUST DID THIS FOR NOW TO GET CI WORKING AGAIN...
|
# TODO: PUT BACK ALL THE JOBS! JUST DID THIS FOR NOW TO GET CI WORKING AGAIN...
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
* Windres (Windows)
|
* Windres (Windows)
|
||||||
* libglvnd and libsecret development files (Linux)
|
* libglvnd and libsecret development files (Linux)
|
||||||
* pkg-config (Linux)
|
* pkg-config (Linux)
|
||||||
* cmake, ninja-build and Qt 6.4.3 are required to build the graphical user interface. On Linux,
|
* cmake, ninja-build and Qt 6 are required to build the graphical user interface. On Linux,
|
||||||
the Mesa OpenGL development files are also needed.
|
the Mesa OpenGL development files are also needed.
|
||||||
|
|
||||||
To enable the sending of crash reports using Sentry please set the
|
To enable the sending of crash reports using Sentry please set the
|
||||||
@ -19,7 +19,7 @@ Otherwise, the sending of crash reports will be disabled.
|
|||||||
|
|
||||||
## Build
|
## Build
|
||||||
In order to build Bridge app with Qt interface we are using
|
In order to build Bridge app with Qt interface we are using
|
||||||
[Qt 6.4.3](https://doc.qt.io/qt-6/gettingstarted.html).
|
[Qt 6.3](https://doc.qt.io/qt-6/gettingstarted.html).
|
||||||
|
|
||||||
Please note that qmake path must be in your `PATH` to ensure Qt to be found.
|
Please note that qmake path must be in your `PATH` to ensure Qt to be found.
|
||||||
Also, before you start build **on Windows**, please unset the `MSYSTEM` variable
|
Also, before you start build **on Windows**, please unset the `MSYSTEM` variable
|
||||||
|
|||||||
@ -40,7 +40,6 @@ Proton Mail Bridge includes the following 3rd party software:
|
|||||||
* [go-message](https://github.com/emersion/go-message) available under [license](https://github.com/emersion/go-message/blob/master/LICENSE)
|
* [go-message](https://github.com/emersion/go-message) available under [license](https://github.com/emersion/go-message/blob/master/LICENSE)
|
||||||
* [go-sasl](https://github.com/emersion/go-sasl) available under [license](https://github.com/emersion/go-sasl/blob/master/LICENSE)
|
* [go-sasl](https://github.com/emersion/go-sasl) available under [license](https://github.com/emersion/go-sasl/blob/master/LICENSE)
|
||||||
* [go-smtp](https://github.com/emersion/go-smtp) available under [license](https://github.com/emersion/go-smtp/blob/master/LICENSE)
|
* [go-smtp](https://github.com/emersion/go-smtp) available under [license](https://github.com/emersion/go-smtp/blob/master/LICENSE)
|
||||||
* [go-vcard](https://github.com/emersion/go-vcard) available under [license](https://github.com/emersion/go-vcard/blob/master/LICENSE)
|
|
||||||
* [color](https://github.com/fatih/color) available under [license](https://github.com/fatih/color/blob/master/LICENSE)
|
* [color](https://github.com/fatih/color) available under [license](https://github.com/fatih/color/blob/master/LICENSE)
|
||||||
* [sentry-go](https://github.com/getsentry/sentry-go) available under [license](https://github.com/getsentry/sentry-go/blob/master/LICENSE)
|
* [sentry-go](https://github.com/getsentry/sentry-go) available under [license](https://github.com/getsentry/sentry-go/blob/master/LICENSE)
|
||||||
* [resty](https://github.com/go-resty/resty/v2) available under [license](https://github.com/go-resty/resty/v2/blob/master/LICENSE)
|
* [resty](https://github.com/go-resty/resty/v2) available under [license](https://github.com/go-resty/resty/v2/blob/master/LICENSE)
|
||||||
@ -84,6 +83,7 @@ Proton Mail Bridge includes the following 3rd party software:
|
|||||||
* [go-spew](https://github.com/davecgh/go-spew) available under [license](https://github.com/davecgh/go-spew/blob/master/LICENSE)
|
* [go-spew](https://github.com/davecgh/go-spew) available under [license](https://github.com/davecgh/go-spew/blob/master/LICENSE)
|
||||||
* [go-windows](https://github.com/elastic/go-windows) available under [license](https://github.com/elastic/go-windows/blob/master/LICENSE)
|
* [go-windows](https://github.com/elastic/go-windows) available under [license](https://github.com/elastic/go-windows/blob/master/LICENSE)
|
||||||
* [go-textwrapper](https://github.com/emersion/go-textwrapper) available under [license](https://github.com/emersion/go-textwrapper/blob/master/LICENSE)
|
* [go-textwrapper](https://github.com/emersion/go-textwrapper) available under [license](https://github.com/emersion/go-textwrapper/blob/master/LICENSE)
|
||||||
|
* [go-vcard](https://github.com/emersion/go-vcard) available under [license](https://github.com/emersion/go-vcard/blob/master/LICENSE)
|
||||||
* [fgprof](https://github.com/felixge/fgprof) available under [license](https://github.com/felixge/fgprof/blob/master/LICENSE)
|
* [fgprof](https://github.com/felixge/fgprof) available under [license](https://github.com/felixge/fgprof/blob/master/LICENSE)
|
||||||
* [go-shlex](https://github.com/flynn-archive/go-shlex) available under [license](https://github.com/flynn-archive/go-shlex/blob/master/LICENSE)
|
* [go-shlex](https://github.com/flynn-archive/go-shlex) available under [license](https://github.com/flynn-archive/go-shlex/blob/master/LICENSE)
|
||||||
* [mimetype](https://github.com/gabriel-vasile/mimetype) available under [license](https://github.com/gabriel-vasile/mimetype/blob/master/LICENSE)
|
* [mimetype](https://github.com/gabriel-vasile/mimetype) available under [license](https://github.com/gabriel-vasile/mimetype/blob/master/LICENSE)
|
||||||
@ -123,7 +123,6 @@ Proton Mail Bridge includes the following 3rd party software:
|
|||||||
* [codec](https://github.com/ugorji/go/codec) available under [license](https://github.com/ugorji/go/codec/blob/master/LICENSE)
|
* [codec](https://github.com/ugorji/go/codec) available under [license](https://github.com/ugorji/go/codec/blob/master/LICENSE)
|
||||||
* [tagparser](https://github.com/vmihailenco/tagparser/v2) available under [license](https://github.com/vmihailenco/tagparser/v2/blob/master/LICENSE)
|
* [tagparser](https://github.com/vmihailenco/tagparser/v2) available under [license](https://github.com/vmihailenco/tagparser/v2/blob/master/LICENSE)
|
||||||
* [smetrics](https://github.com/xrash/smetrics) available under [license](https://github.com/xrash/smetrics/blob/master/LICENSE)
|
* [smetrics](https://github.com/xrash/smetrics) available under [license](https://github.com/xrash/smetrics/blob/master/LICENSE)
|
||||||
* [go-ordered-json](https://gitlab.com/c0b/go-ordered-json)
|
|
||||||
* [arch](https://golang.org/x/arch) available under [license](https://cs.opensource.google/go/x/arch/+/master:LICENSE)
|
* [arch](https://golang.org/x/arch) available under [license](https://cs.opensource.google/go/x/arch/+/master:LICENSE)
|
||||||
* [crypto](https://golang.org/x/crypto) available under [license](https://cs.opensource.google/go/x/crypto/+/master:LICENSE)
|
* [crypto](https://golang.org/x/crypto) available under [license](https://cs.opensource.google/go/x/crypto/+/master:LICENSE)
|
||||||
* [mod](https://golang.org/x/mod) available under [license](https://cs.opensource.google/go/x/mod/+/master:LICENSE)
|
* [mod](https://golang.org/x/mod) available under [license](https://cs.opensource.google/go/x/mod/+/master:LICENSE)
|
||||||
@ -133,6 +132,5 @@ Proton Mail Bridge includes the following 3rd party software:
|
|||||||
* [yaml](https://gopkg.in/yaml.v3) available under [license](https://github.com/go-yaml/yaml/blob/v3.0.1/LICENSE)
|
* [yaml](https://gopkg.in/yaml.v3) available under [license](https://github.com/go-yaml/yaml/blob/v3.0.1/LICENSE)
|
||||||
* [docker-credential-helpers](https://github.com/ProtonMail/docker-credential-helpers) available under [license](https://github.com/ProtonMail/docker-credential-helpers/blob/master/LICENSE)
|
* [docker-credential-helpers](https://github.com/ProtonMail/docker-credential-helpers) available under [license](https://github.com/ProtonMail/docker-credential-helpers/blob/master/LICENSE)
|
||||||
* [go-message](https://github.com/ProtonMail/go-message) available under [license](https://github.com/ProtonMail/go-message/blob/master/LICENSE)
|
* [go-message](https://github.com/ProtonMail/go-message) available under [license](https://github.com/ProtonMail/go-message/blob/master/LICENSE)
|
||||||
* [resty](https://github.com/LBeernaertProton/resty/v2) available under [license](https://github.com/LBeernaertProton/resty/v2/blob/master/LICENSE)
|
|
||||||
* [go-keychain](https://github.com/cuthix/go-keychain) available under [license](https://github.com/cuthix/go-keychain/blob/master/LICENSE)
|
* [go-keychain](https://github.com/cuthix/go-keychain) available under [license](https://github.com/cuthix/go-keychain/blob/master/LICENSE)
|
||||||
<!-- END AUTOGEN -->
|
<!-- END AUTOGEN -->
|
||||||
|
|||||||
239
Changelog.md
239
Changelog.md
@ -3,189 +3,10 @@
|
|||||||
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||||
|
|
||||||
|
|
||||||
## Wakato Bridge 3.7.0
|
## Trift Bridge 3.4.2
|
||||||
|
|
||||||
### Added
|
|
||||||
* Test(GODT-1224): Add testing around package creation.
|
|
||||||
* Add debug_assemble binary.
|
|
||||||
* Test(GODT-2723): Add importing a message with remote content.
|
|
||||||
* Test(GODT-2737): Sending HTML messages to internal.
|
|
||||||
* Test(GODT-3036): Keep inline attachment order on GPA Fake Server.
|
|
||||||
* GODT-3015: Add simple algorithm to deal with multiple attachment for bug report.
|
|
||||||
* Test: make message structure check more verbose.
|
|
||||||
* Test: Add test around account settings.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
* GODT-3097: Warn about PGPInline encryption scheme which will be deprecated.
|
|
||||||
* Test: Support multiple users when waiting for sync event.
|
|
||||||
* Test: Update fake server with defautl draft content-type and test it.
|
|
||||||
* Test: be less aggressive while checking for message structure.
|
|
||||||
* GODT-2996: Set password fields to hidden when resetting the login form.
|
|
||||||
* GODT-2990: Change runner tags.
|
|
||||||
* GODT-2835: Bump GPA adding support for AsyncAttachments for BugReport +...
|
|
||||||
* GODT-2940: Allow 3 attempts for mailbox password.
|
|
||||||
* GODT-3095: Update GOpenPGP.
|
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
* GODT-3106: Broken import route.
|
* GODT-2902: Do not check for changed values. Related to GODT-2857.
|
||||||
* GODT-3041: Fix Invalid Or Missing message signature during send.
|
|
||||||
* GODT-3087: Exclude attachment content-disposition part when determining...
|
|
||||||
* GODT-2887: Inline images with Apple Mail.
|
|
||||||
* GODT-3100: Fix issue where a fatal error that bubble up to cli.Run() is not written in the log file.
|
|
||||||
* GODT-3094: Clean up old update files on bridge startup.
|
|
||||||
* GODT-3012: Fix multipart request retries.
|
|
||||||
* GODT-2935: Do not allow parentID into drafts.
|
|
||||||
* GODT-2935: Correct error message when draft fails to create.
|
|
||||||
* GODT-2970: Correctly handle rename of Inbox.
|
|
||||||
* GODT-2969: Prevent duration corruption for config status event.
|
|
||||||
* Fixed type in QA installer CI job name.
|
|
||||||
* GODT-3019: Fix title of main window when no account is connected.
|
|
||||||
* GODT-3013: IMAP service getting "stuck".
|
|
||||||
* GODT-2966: Allow permissive parsing of MediaType parameters for import.
|
|
||||||
* GODT-2966: Add more test regarding quoted/unquoted filename in attachment.
|
|
||||||
* GODT-2490: Fix sync progress not being reset when toggling split mode.
|
|
||||||
* GODT-2515: Customized notification of unavailable keychain on macOS.
|
|
||||||
|
|
||||||
|
|
||||||
## Vasco da Gama Bridge 3.6.1
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
* GODT-3033: Unable to receive new mail.
|
|
||||||
|
|
||||||
|
|
||||||
## Umshiang Bridge 3.5.4
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
* GODT-3033: Unable to receive new mail.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 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
|
## Trift Bridge 3.4.1
|
||||||
@ -234,7 +55,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
|||||||
* GODT-2578: Refresh literals appended to Sent folder.
|
* GODT-2578: Refresh literals appended to Sent folder.
|
||||||
* GODT-2753: Vault test now check that value auto-assigned is first available port.
|
* GODT-2753: Vault test now check that value auto-assigned is first available port.
|
||||||
* GODT-2522: Handle migration with unreferenced db values.
|
* GODT-2522: Handle migration with unreferenced db values.
|
||||||
* GODT-2670: Allow missing whitespace after header field colon.
|
* GODT-2693: Allow missing whitespace after header field colon.
|
||||||
* GODT-2653: Only log when err is not nil.
|
* GODT-2653: Only log when err is not nil.
|
||||||
* GODT-2680: Fix for C++ debugger not working on ARM64 because of OpenSSL 3.1.
|
* GODT-2680: Fix for C++ debugger not working on ARM64 because of OpenSSL 3.1.
|
||||||
* GODT-2675: Update GPA to applye togin-gonic/gin patch + update COPYING_NOTES.
|
* GODT-2675: Update GPA to applye togin-gonic/gin patch + update COPYING_NOTES.
|
||||||
@ -319,7 +140,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
|||||||
* GODT-2437: Silence harmless report to sentry.
|
* GODT-2437: Silence harmless report to sentry.
|
||||||
* GODT-2649: Clean up cache files after failed connector create (Gluon).
|
* GODT-2649: Clean up cache files after failed connector create (Gluon).
|
||||||
* GODT-2638: Validate messages before import.
|
* GODT-2638: Validate messages before import.
|
||||||
* GODT-2646: Bump GPA and Gluon dependency after CIRCL upgrade.
|
* GODT-2646: Bump GPA and Gluon dependecy after CIRCL upgrade.
|
||||||
* GODT-2454: Only Send status update if transaction succeeded.
|
* GODT-2454: Only Send status update if transaction succeeded.
|
||||||
* Test: fix flaky tests.
|
* Test: fix flaky tests.
|
||||||
* GODT-2628: Attempt to fix closed channel panic on logout.
|
* GODT-2628: Attempt to fix closed channel panic on logout.
|
||||||
@ -379,7 +200,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
|||||||
* GODT-2574: Fix label/unlabel of large amounts of messages.
|
* GODT-2574: Fix label/unlabel of large amounts of messages.
|
||||||
* GODT-2573: Handle invalid header fields in message.
|
* GODT-2573: Handle invalid header fields in message.
|
||||||
* GODT-2573: Crash on null update.
|
* GODT-2573: Crash on null update.
|
||||||
* GODT-2407: Replace invalid email addresses with empty for new Drafts.
|
* GODT-2407: Replace invalid email addresses with emtpy for new Drafts.
|
||||||
|
|
||||||
## [Bridge 3.1.3] Quebec
|
## [Bridge 3.1.3] Quebec
|
||||||
|
|
||||||
@ -520,7 +341,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
|||||||
* GODT-2429: Do not report context cancel to sentry.
|
* GODT-2429: Do not report context cancel to sentry.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
* GODT-2467: elide long email addresses in 'bad event' QML notification dialog.
|
* GODT-2467: elide long email adresses in 'bad event' QML notification dialog.
|
||||||
* GODT-2449: fix bug in Bridge-GUI's Exception::what().
|
* GODT-2449: fix bug in Bridge-GUI's Exception::what().
|
||||||
* GODT-2427: Parsing header issues.
|
* GODT-2427: Parsing header issues.
|
||||||
* GODT-2426: Fix crash on user delete.
|
* GODT-2426: Fix crash on user delete.
|
||||||
@ -537,7 +358,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
|||||||
* GODT-2404: Handle unexpected EOF.
|
* GODT-2404: Handle unexpected EOF.
|
||||||
* GODT-2400: Allow state updates to be applied if command fails.
|
* GODT-2400: Allow state updates to be applied if command fails.
|
||||||
* GODT-2399: Fix immediate message deletion during updates.
|
* GODT-2399: Fix immediate message deletion during updates.
|
||||||
* GODT-2390: Missing changes from previous commit.
|
* GODT-2390: Missing changes from pervious commit.
|
||||||
* GODT-2390: Add reports for uncaught json and net.opErr.
|
* GODT-2390: Add reports for uncaught json and net.opErr.
|
||||||
* GODT-2414: Multiple deletion bug in WriteControlledStore.
|
* GODT-2414: Multiple deletion bug in WriteControlledStore.
|
||||||
|
|
||||||
@ -602,7 +423,7 @@ GODT-1804: Preserve MIME parameters when uploading attachments.
|
|||||||
* GODT-2223: Improve event handling.
|
* GODT-2223: Improve event handling.
|
||||||
* GODT-2305: Detect missing gluon DB.
|
* GODT-2305: Detect missing gluon DB.
|
||||||
* GODT-2291: Change gluon store default location from Cache to Data.
|
* GODT-2291: Change gluon store default location from Cache to Data.
|
||||||
* Other: Disable dialer test until badssl cert is bumped.
|
* Other: Disable dialer test until badssl cert is bumbed.
|
||||||
* GODT-2292: Updated BUILDS.md doc.
|
* GODT-2292: Updated BUILDS.md doc.
|
||||||
* GODT-2258: suggest email as login when signing in via status window.
|
* GODT-2258: suggest email as login when signing in via status window.
|
||||||
* Other: Report corrupt and/or insecure vaults to sentry.
|
* Other: Report corrupt and/or insecure vaults to sentry.
|
||||||
@ -882,7 +703,7 @@ GODT-1804: Preserve MIME parameters when uploading attachments.
|
|||||||
## [Bridge 2.4.6] Osney
|
## [Bridge 2.4.6] Osney
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
* GODT-2019: When signing out and a single user is connected we do not go back to the welcome screen.
|
* GODT-2019: When signing out and a single user is connecte* we do not go back to the welcome screen.
|
||||||
* GODT-2071: Bridge-gui report error if an orphan bridge is detected.
|
* GODT-2071: Bridge-gui report error if an orphan bridge is detected.
|
||||||
* GODT-2046: Bridge-gui log is included in optional archive sent with bug reports.
|
* GODT-2046: Bridge-gui log is included in optional archive sent with bug reports.
|
||||||
* GODT-2039: Bridge monitors bridge-gui via its PID.
|
* GODT-2039: Bridge monitors bridge-gui via its PID.
|
||||||
@ -1036,7 +857,7 @@ GODT-1804: Preserve MIME parameters when uploading attachments.
|
|||||||
* GODT-1260: Renaming.
|
* GODT-1260: Renaming.
|
||||||
* GODT-1502: Rebranding: color and radius.
|
* GODT-1502: Rebranding: color and radius.
|
||||||
* GODT-1549: Add notification when address list changes.
|
* GODT-1549: Add notification when address list changes.
|
||||||
* GODT-1560: Dependency licenses update and link.
|
* GODT-1560: Dependecy licenses update and link.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
* GODT-1543: Using one buffered event for off and on connection.
|
* GODT-1543: Using one buffered event for off and on connection.
|
||||||
@ -1133,7 +954,7 @@ GODT-1537: Manual in-app update mechanism.
|
|||||||
* GODT-1338: GODT-1343 Help view buttons.
|
* GODT-1338: GODT-1343 Help view buttons.
|
||||||
* GODT-1340: Not crashing, user list updating in main thread.
|
* GODT-1340: Not crashing, user list updating in main thread.
|
||||||
* GODT-1345: Adding panic handlers.
|
* GODT-1345: Adding panic handlers.
|
||||||
* GODT-1271: Fix Status margins.
|
* GODT-1271: Fix Status margings.
|
||||||
* GODT-1320: Add loading property to each action within a notification.
|
* GODT-1320: Add loading property to each action within a notification.
|
||||||
* GODT-1210: Add "free user" banner.
|
* GODT-1210: Add "free user" banner.
|
||||||
* GODT-1314: Limit description field length within 150/800 bounds.
|
* GODT-1314: Limit description field length within 150/800 bounds.
|
||||||
@ -1175,7 +996,7 @@ GODT-1537: Manual in-app update mechanism.
|
|||||||
* GODT-1381 Treat readonly folder as failure for cache on disk.
|
* GODT-1381 Treat readonly folder as failure for cache on disk.
|
||||||
* GODT-1431 Prevent watcher when not using disk on cache.
|
* GODT-1431 Prevent watcher when not using disk on cache.
|
||||||
* GODT-1381: Use in-memory cache in case local cache is unavailable.
|
* GODT-1381: Use in-memory cache in case local cache is unavailable.
|
||||||
* GODT-1356 GODT-1302: Cache on disk concurrency and API retries.
|
* GODT-1356 GODT-1302: Cache on disk concurency and API retries.
|
||||||
* GODT-1332 Added tests for cache move functions.
|
* GODT-1332 Added tests for cache move functions.
|
||||||
* GODT-1332: moved cache related functions to separate file.
|
* GODT-1332: moved cache related functions to separate file.
|
||||||
* GODT-1332 moving cache does not work on Windows.
|
* GODT-1332 moving cache does not work on Windows.
|
||||||
@ -1426,7 +1247,7 @@ GODT-1537: Manual in-app update mechanism.
|
|||||||
### Fixed
|
### Fixed
|
||||||
* GODT-1029 Fix tray icon not updating under certain conditions.
|
* GODT-1029 Fix tray icon not updating under certain conditions.
|
||||||
* GODT-1062 Fix lost notification bar when window is closed.
|
* GODT-1062 Fix lost notification bar when window is closed.
|
||||||
* GODT-1058 Install version after changing channel right away only in case of downgrade.
|
* GODT-1058 Install version after chaning channel right away only in case of downgrade.
|
||||||
* GODT-1073 Re-write autostart link on every start if turned on in preferences.
|
* GODT-1073 Re-write autostart link on every start if turned on in preferences.
|
||||||
* GODT-1055 Fix flaky empty trash test.
|
* GODT-1055 Fix flaky empty trash test.
|
||||||
|
|
||||||
@ -1516,7 +1337,7 @@ GODT-1537: Manual in-app update mechanism.
|
|||||||
* GODT-820 Added GUI notification on impossibility of update installation (both silent and manual).
|
* GODT-820 Added GUI notification on impossibility of update installation (both silent and manual).
|
||||||
* GODT-870 Added GUI notification on error during silent update.
|
* GODT-870 Added GUI notification on error during silent update.
|
||||||
* GODT-805 Added GUI notification on update available.
|
* GODT-805 Added GUI notification on update available.
|
||||||
* GODT-804 Added GUI notification on silent update installed (prompt to restart).
|
* GODT-804 Added GUI notification on silent update installed (promt to restart).
|
||||||
* GODT-275 Added option to disable autoupdates in settings (default autoupdate is enabled).
|
* GODT-275 Added option to disable autoupdates in settings (default autoupdate is enabled).
|
||||||
* GODT-874 Added manual triggers to Updater module.
|
* GODT-874 Added manual triggers to Updater module.
|
||||||
* GODT-851 Added support of UID EXPUNGE.
|
* GODT-851 Added support of UID EXPUNGE.
|
||||||
@ -1840,7 +1661,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
|
|||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
* GODT-360 Detect charset embedded in html/xml.
|
* GODT-360 Detect charset embedded in html/xml.
|
||||||
* GODT-354 Do not label/unlabel messages from `All Mail` folder.
|
* GODT-354 Do not label/unlabel messsages from `All Mail` folder.
|
||||||
* GODT-388 Support for both bridge and import/export credentials by package users.
|
* GODT-388 Support for both bridge and import/export credentials by package users.
|
||||||
* GODT-387 Store factory to make store optional.
|
* GODT-387 Store factory to make store optional.
|
||||||
* GODT-386 Renamed bridge to general users and keep bridge only for bridge stuff.
|
* GODT-386 Renamed bridge to general users and keep bridge only for bridge stuff.
|
||||||
@ -2005,13 +1826,13 @@ CSB-331 Fix sending error due to mixed case in sender address.
|
|||||||
* GODT-88 Run mbox sync in parallel when switch password mode (re-init not user).
|
* GODT-88 Run mbox sync in parallel when switch password mode (re-init not user).
|
||||||
* GODT-95 Do not throw error when trying to create new mailbox in IMAP root.
|
* GODT-95 Do not throw error when trying to create new mailbox in IMAP root.
|
||||||
* GODT-75 Do not fail on unlabel inside delete.
|
* GODT-75 Do not fail on unlabel inside delete.
|
||||||
* #1095 always delete IMAP USER including wrong password.
|
* #1095 always delete IMAP USER including wrong pasword.
|
||||||
* Unique pmapi client userID (including #1098).
|
* Unique pmapi client userID (including #1098).
|
||||||
* Using go.enmime@v0.6.1 snapshot.
|
* Using go.enmime@v0.6.1 snapshot.
|
||||||
* Better detection of non-auth-error.
|
* Better detection of non-auth-error.
|
||||||
* Reset `hasAuthChannel` during logout for proper login functionality (set up auth channel and unlock keys).
|
* Reset `hasAuthChannel` during logout for proper login functionality (set up auth channel and unlock keys).
|
||||||
* Allow `APPEND` messages without parsable email address in sender field.
|
* Allow `APPEND` messages without parsable email address in sender field.
|
||||||
* #1060 avoid `Append` after internal message ID was found and message was copied to mailbox using `MessageLabel`.
|
* #1060 avoid `Append` after internal message ID was found and message was copyed to mailbox using `MessageLabel`.
|
||||||
* #1049 Basic usage of store in SMTP package to poll event loop during sending message.
|
* #1049 Basic usage of store in SMTP package to poll event loop during sending message.
|
||||||
* #1050 pollNow waits for events to be processed.
|
* #1050 pollNow waits for events to be processed.
|
||||||
* #1047 Fix fetch of empty mailbox.
|
* #1047 Fix fetch of empty mailbox.
|
||||||
@ -2137,7 +1958,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
|
|||||||
* #903 added http.Client timeout to not hang out forever.
|
* #903 added http.Client timeout to not hang out forever.
|
||||||
* Closing body after checking internet connection.
|
* Closing body after checking internet connection.
|
||||||
* Pedantic lint for bridgeUtils.
|
* Pedantic lint for bridgeUtils.
|
||||||
* Selected events are buffered and emitted again when frontend loop is ready.
|
* Selected events are buffered and emited again when frontend loop is ready.
|
||||||
* #890 implemented 2FA endpoint (auth split).
|
* #890 implemented 2FA endpoint (auth split).
|
||||||
* #888 TLS Cert.
|
* #888 TLS Cert.
|
||||||
* Error bar and modal with explanation in GUI.
|
* Error bar and modal with explanation in GUI.
|
||||||
@ -2145,7 +1966,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
|
|||||||
* Add pinning to bridge (only for live API builds).
|
* Add pinning to bridge (only for live API builds).
|
||||||
* #887 #883:
|
* #887 #883:
|
||||||
* Wait before clearing data.
|
* Wait before clearing data.
|
||||||
* Configure which provides pmapi.ClientConfig and app directories.
|
* Configer which provides pmapi.ClientConfig and app directories.
|
||||||
* #861 restart after clear data.
|
* #861 restart after clear data.
|
||||||
* Panic handler for all goroutines.
|
* Panic handler for all goroutines.
|
||||||
* CD for linux.
|
* CD for linux.
|
||||||
@ -2193,7 +2014,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
|
|||||||
* #882 unassign PMAPI client after logout and force to run garbage collector.
|
* #882 unassign PMAPI client after logout and force to run garbage collector.
|
||||||
* #880, #884, #885, #886 fix of informing user about outgoing non-encrypted e-mail.
|
* #880, #884, #885, #886 fix of informing user about outgoing non-encrypted e-mail.
|
||||||
* #838 `Sirupsen` -> `sirupsen`.
|
* #838 `Sirupsen` -> `sirupsen`.
|
||||||
* #893 save panic report file every time.
|
* #893 save panic report file everytime.
|
||||||
* #880 fix of informing user about outgoing non-encrypted e-mail.
|
* #880 fix of informing user about outgoing non-encrypted e-mail.
|
||||||
* Fix aliases in split mode.
|
* Fix aliases in split mode.
|
||||||
* Fix decrypted data in log notification.
|
* Fix decrypted data in log notification.
|
||||||
@ -2267,7 +2088,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
|
|||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
* Fix custom message format.
|
* Fix custom message format.
|
||||||
* #802 accumulated long lines while parsing body structure.
|
* #802 acumulated long lines while parsing body structure.
|
||||||
* Process `AddressEvent` before `MessageEvent`.
|
* Process `AddressEvent` before `MessageEvent`.
|
||||||
* #791 updated crypto: fix wrong signature format.
|
* #791 updated crypto: fix wrong signature format.
|
||||||
* #793 fix returning size.
|
* #793 fix returning size.
|
||||||
@ -2289,7 +2110,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
|
|||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
* #748 when charset missing assume utf8 and check the validity.
|
* #748 when charset missing assume utf8 and check the validity.
|
||||||
* #750 before sync check that events are up-to-date, if not poll events instead of sync.
|
* #750 before sync check that events are uptodate, if not poll events instead of sync.
|
||||||
* Use pmapi with support of decrypted access token.
|
* Use pmapi with support of decrypted access token.
|
||||||
* #750 Status is using DB status instead of API.
|
* #750 Status is using DB status instead of API.
|
||||||
* Format panic error as string instead of struct dump.
|
* Format panic error as string instead of struct dump.
|
||||||
@ -2306,7 +2127,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
|
|||||||
* Full version of program visible on release notes.
|
* Full version of program visible on release notes.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
* #720 only one concurrent DB sync.
|
* #720 only one concurent DB sync.
|
||||||
* #720 sync every 3 pages.
|
* #720 sync every 3 pages.
|
||||||
* #512 extending list of charsets go-pm-mime!4.
|
* #512 extending list of charsets go-pm-mime!4.
|
||||||
|
|
||||||
@ -2330,7 +2151,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
|
|||||||
* Fix srp modulus issue with new `ProtonMail/crypto`.
|
* Fix srp modulus issue with new `ProtonMail/crypto`.
|
||||||
* Generate version files from main file.
|
* Generate version files from main file.
|
||||||
* Be able to set update set on build.
|
* Be able to set update set on build.
|
||||||
* #597 check on start that certificate will be still valid after one month and generate new cert if not.
|
* #597 check on start that certificat will be still valid after one month and generate new cert if not.
|
||||||
* #597 extended certificate validity to 2 years.
|
* #597 extended certificate validity to 2 years.
|
||||||
* Copyright 2019.
|
* Copyright 2019.
|
||||||
* Exclude `protontech` repos from credits.
|
* Exclude `protontech` repos from credits.
|
||||||
@ -2349,7 +2170,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
|
|||||||
* #592 internal references are added only when not present already.
|
* #592 internal references are added only when not present already.
|
||||||
* #592 field `Date` changed to m.Time only when wrong format or missing `Date`.
|
* #592 field `Date` changed to m.Time only when wrong format or missing `Date`.
|
||||||
* #645 pmapi#26 `Message.Flags` instead of `IsEncrypted`, `Type`, `IsReplied`, `IsRepliedAll`, `IsForwarded`.
|
* #645 pmapi#26 `Message.Flags` instead of `IsEncrypted`, `Type`, `IsReplied`, `IsRepliedAll`, `IsForwarded`.
|
||||||
* DB: do not allow to put Body or Attachments to db.
|
* DB: do not allow to put Body or Attachements to db.
|
||||||
* #574 SMTP: can now send more than one email.
|
* #574 SMTP: can now send more than one email.
|
||||||
* #671 Verbosity levels: `debug` (only bridge), `debug-client` (bridge and client communication), `debug-server` (bridge, whole SMTP/IMAP communication).
|
* #671 Verbosity levels: `debug` (only bridge), `debug-client` (bridge and client communication), `debug-server` (bridge, whole SMTP/IMAP communication).
|
||||||
* #644 Return rfc.size 0 or correct size of fetched body (stored in DB).
|
* #644 Return rfc.size 0 or correct size of fetched body (stored in DB).
|
||||||
@ -2421,7 +2242,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
|
|||||||
* Start with new versioning.
|
* Start with new versioning.
|
||||||
|
|
||||||
1.1.0
|
1.1.0
|
||||||
| | `--- bug fix number (internal, irregular, beta releases)
|
| | `--- bug fix number (internal, irregular, beta relases)
|
||||||
| `----- minor version (features, release once per month, live release, milestones)
|
| `----- minor version (features, release once per month, live release, milestones)
|
||||||
`------- major version (big changes, once per year, breaking changes, api force upgrade)
|
`------- major version (big changes, once per year, breaking changes, api force upgrade)
|
||||||
|
|
||||||
@ -2487,7 +2308,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
|
|||||||
* All `client.Do` errors are interpreted as connection issue.
|
* All `client.Do` errors are interpreted as connection issue.
|
||||||
* Moved to internal gitlab.
|
* Moved to internal gitlab.
|
||||||
* Typo `frontend-qml`.
|
* Typo `frontend-qml`.
|
||||||
* Better message for case when server is not reachable.
|
* Better message for case when server is not reacheable.
|
||||||
* Setting 1min timeout to IMAP connection.
|
* Setting 1min timeout to IMAP connection.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
@ -2519,12 +2340,12 @@ CSB-331 Fix sending error due to mixed case in sender address.
|
|||||||
* Keychain format and function refactor.
|
* Keychain format and function refactor.
|
||||||
* Create crash file on panic with full trace.
|
* Create crash file on panic with full trace.
|
||||||
* Clear old data only in main process (no double keychain typing).
|
* Clear old data only in main process (no double keychain typing).
|
||||||
* Create label update API route.
|
* Create label udpate API route.
|
||||||
* Selectable text in release notes.
|
* Selectable text in release notes.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
* Support sending to external PGP recipients.
|
* Support sending to external PGP recipients.
|
||||||
* Return error codes: `0: Ok`, `2: Frontend crashed`, `3: Bridge already running`, `4: Unknown argument`, `42: Restart application`.
|
* Return error codes: `0: Ok`, `2: Frontend crashed`, `3: Bridge already running`, `4: Uknown argument`, `42: Restart application`.
|
||||||
|
|
||||||
### Release notes
|
### Release notes
|
||||||
* Support of encryption to external PGP recipients using contacts created on beta.protonmail.com (see https://protonmail.com/blog/pgp-vulnerability-efail/ to understand the vulnerabilities that may be associated with sending to other PGP clients).
|
* Support of encryption to external PGP recipients using contacts created on beta.protonmail.com (see https://protonmail.com/blog/pgp-vulnerability-efail/ to understand the vulnerabilities that may be associated with sending to other PGP clients).
|
||||||
@ -2549,7 +2370,7 @@ CSB-331 Fix sending error due to mixed case in sender address.
|
|||||||
* Bug report window.
|
* Bug report window.
|
||||||
* Checkbox and with label (only I/E).
|
* Checkbox and with label (only I/E).
|
||||||
* Error dialog and Info tooltip (only I/E).
|
* Error dialog and Info tooltip (only I/E).
|
||||||
* Add user modal formatting (colors, text).
|
* Add user modal formating (colors, text).
|
||||||
* Account view style.
|
* Account view style.
|
||||||
* Input box style (used in bug report).
|
* Input box style (used in bug report).
|
||||||
* Input field style (used in add account and change port).
|
* Input field style (used in add account and change port).
|
||||||
|
|||||||
44
Makefile
44
Makefile
@ -11,7 +11,7 @@ ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
|
|||||||
.PHONY: build build-gui build-nogui build-launcher versioner hasher
|
.PHONY: build build-gui build-nogui build-launcher versioner hasher
|
||||||
|
|
||||||
# Keep version hardcoded so app build works also without Git repository.
|
# Keep version hardcoded so app build works also without Git repository.
|
||||||
BRIDGE_APP_VERSION?=3.7.0+git
|
BRIDGE_APP_VERSION?=3.4.2+git
|
||||||
APP_VERSION:=${BRIDGE_APP_VERSION}
|
APP_VERSION:=${BRIDGE_APP_VERSION}
|
||||||
APP_FULL_NAME:=Proton Mail Bridge
|
APP_FULL_NAME:=Proton Mail Bridge
|
||||||
APP_VENDOR:=Proton AG
|
APP_VENDOR:=Proton AG
|
||||||
@ -246,7 +246,7 @@ test-race: gofiles
|
|||||||
test-integration: gofiles
|
test-integration: gofiles
|
||||||
mkdir -p coverage/integration
|
mkdir -p coverage/integration
|
||||||
go test \
|
go test \
|
||||||
-v -timeout=60m -p=1 -count=1 -tags=test_integration \
|
-v -timeout=60m -p=1 -count=1 \
|
||||||
${GOCOVERAGE} \
|
${GOCOVERAGE} \
|
||||||
github.com/ProtonMail/proton-bridge/v3/tests \
|
github.com/ProtonMail/proton-bridge/v3/tests \
|
||||||
${GOCOVERDIR}/integration
|
${GOCOVERDIR}/integration
|
||||||
@ -258,22 +258,6 @@ test-integration-debug: gofiles
|
|||||||
test-integration-race: gofiles
|
test-integration-race: gofiles
|
||||||
go test -v -timeout=60m -p=1 -count=1 -race -failfast github.com/ProtonMail/proton-bridge/v3/tests
|
go test -v -timeout=60m -p=1 -count=1 -race -failfast github.com/ProtonMail/proton-bridge/v3/tests
|
||||||
|
|
||||||
test-integration-nightly: gofiles
|
|
||||||
mkdir -p coverage/integration
|
|
||||||
go test \
|
|
||||||
-v -timeout=90m -p=1 -count=1 -tags=test_integration \
|
|
||||||
${GOCOVERAGE} \
|
|
||||||
github.com/ProtonMail/proton-bridge/v3/tests \
|
|
||||||
${GOCOVERDIR}/integration \
|
|
||||||
nightly
|
|
||||||
|
|
||||||
fuzz: gofiles
|
|
||||||
go test -fuzz=FuzzUnmarshal -parallel=4 -fuzztime=60s $(PWD)/internal/legacy/credentials
|
|
||||||
go test -fuzz=FuzzNewParser -parallel=4 -fuzztime=60s $(PWD)/pkg/message/parser
|
|
||||||
go test -fuzz=FuzzReadHeaderBody -parallel=4 -fuzztime=60s $(PWD)/pkg/message
|
|
||||||
go test -fuzz=FuzzDecodeHeader -parallel=4 -fuzztime=60s $(PWD)/pkg/mime
|
|
||||||
go test -fuzz=FuzzDecodeCharset -parallel=4 -fuzztime=60s $(PWD)/pkg/mime
|
|
||||||
|
|
||||||
bench:
|
bench:
|
||||||
go test -run '^$$' -bench=. -memprofile bench_mem.pprof -cpuprofile bench_cpu.pprof ./internal/store
|
go test -run '^$$' -bench=. -memprofile bench_mem.pprof -cpuprofile bench_cpu.pprof ./internal/store
|
||||||
go tool pprof -png -output bench_mem.png bench_mem.pprof
|
go tool pprof -png -output bench_mem.png bench_mem.pprof
|
||||||
@ -290,23 +274,9 @@ mocks:
|
|||||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/updater Downloader,Installer > internal/updater/mocks/mocks.go
|
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/updater Downloader,Installer > internal/updater/mocks/mocks.go
|
||||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/telemetry HeartbeatManager > internal/telemetry/mocks/mocks.go
|
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/telemetry HeartbeatManager > internal/telemetry/mocks/mocks.go
|
||||||
cp internal/telemetry/mocks/mocks.go internal/bridge/mocks/telemetry_mocks.go
|
cp internal/telemetry/mocks/mocks.go internal/bridge/mocks/telemetry_mocks.go
|
||||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/services/userevents \
|
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/user MessageDownloader > internal/user/mocks/mocks.go
|
||||||
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-bug-report
|
lint: gofiles lint-golang lint-license lint-dependencies lint-changelog
|
||||||
|
|
||||||
lint-license:
|
lint-license:
|
||||||
./utils/missing_license.sh check
|
./utils/missing_license.sh check
|
||||||
@ -322,12 +292,6 @@ lint-golang:
|
|||||||
$(info linting with GOMAXPROCS=${GOMAXPROCS})
|
$(info linting with GOMAXPROCS=${GOMAXPROCS})
|
||||||
golangci-lint run ./...
|
golangci-lint run ./...
|
||||||
|
|
||||||
lint-bug-report:
|
|
||||||
python3 utils/validate_bug_report_file.py --file "internal/frontend/bridge-gui/bridge-gui/qml/Resources/bug_report_flow.json"
|
|
||||||
|
|
||||||
lint-bug-report-preview:
|
|
||||||
python3 utils/validate_bug_report_file.py --file "internal/frontend/bridge-gui/bridge-gui/qml/Resources/bug_report_flow.json" --preview
|
|
||||||
|
|
||||||
gobinsec: gobinsec-cache.yml build
|
gobinsec: gobinsec-cache.yml build
|
||||||
gobinsec -wait -cache -config utils/gobinsec_conf.yml ${EXE_TARGET} ${DEPLOY_DIR}/${TARGET_OS}/${LAUNCHER_EXE}
|
gobinsec -wait -cache -config utils/gobinsec_conf.yml ${EXE_TARGET} ${DEPLOY_DIR}/${TARGET_OS}/${LAUNCHER_EXE}
|
||||||
|
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import (
|
|||||||
|
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/app"
|
"github.com/ProtonMail/proton-bridge/v3/internal/app"
|
||||||
"github.com/bradenaw/juniper/xslices"
|
"github.com/bradenaw/juniper/xslices"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -43,5 +44,7 @@ import (
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
_ = app.New().Run(xslices.Filter(os.Args, func(arg string) bool { return !strings.Contains(arg, "-psn_") }))
|
if err := app.New().Run(xslices.Filter(os.Args, func(arg string) bool { return !strings.Contains(arg, "-psn_") })); err != nil {
|
||||||
|
logrus.Fatal(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,13 +2,13 @@
|
|||||||
|
|
||||||
## First login and sync
|
## First login and sync
|
||||||
|
|
||||||
When user logs in to the bridge for the first time, immediately starts the first sync.
|
When user logs in to the bridge for the first time, immediatelly starts the first sync.
|
||||||
First sync downloads all headers of all e-mails and creates database to have proper UIDs
|
First sync downloads all headers of all e-mails and creates database to have proper UIDs
|
||||||
and indexes for IMAP. See [database](database.md) for more information.
|
and indexes for IMAP. See [database](database.md) for more information.
|
||||||
|
|
||||||
By default, whenever it's possible, sync downloads only all e-mails maiblox which already
|
By default, whenever it's possible, sync downloads only all e-mails maiblox which already
|
||||||
have list of labels so we can construct all mailboxes (inbox, sent, trash, custom folders
|
have list of labels so we can construct all mailboxes (inbox, sent, trash, custom folders
|
||||||
and labels) without need to download each e-mail headers many times.
|
and lables) without need to download each e-mail headers many times.
|
||||||
|
|
||||||
Note that we need to download also bodies to calculate size of the e-mail and set proper
|
Note that we need to download also bodies to calculate size of the e-mail and set proper
|
||||||
content type (clients uses content type for guess if e-mail contains attachment)--but only
|
content type (clients uses content type for guess if e-mail contains attachment)--but only
|
||||||
@ -22,7 +22,7 @@ client right after adding account.
|
|||||||
|
|
||||||
When account is added to client, client start the sync. This sync will ask Bridge app
|
When account is added to client, client start the sync. This sync will ask Bridge app
|
||||||
for all headers (done quickly) and then starts to download all bodies and attachment.
|
for all headers (done quickly) and then starts to download all bodies and attachment.
|
||||||
Unfortunately for some e-mail more than once if the same e-mail is in more mailboxes
|
Unfortunatelly for some e-mail more than once if the same e-mail is in more mailboxes
|
||||||
(e.g. inbox and all mail)--there is no way to tell over IMAP it's the same message.
|
(e.g. inbox and all mail)--there is no way to tell over IMAP it's the same message.
|
||||||
|
|
||||||
After successful login of client to IMAP, Bridge starts event loop. That periodicly ask
|
After successful login of client to IMAP, Bridge starts event loop. That periodicly ask
|
||||||
@ -37,7 +37,7 @@ sequenceDiagram
|
|||||||
Note right of B: Set up PM account<br/>by user
|
Note right of B: Set up PM account<br/>by user
|
||||||
|
|
||||||
loop First sync
|
loop First sync
|
||||||
B ->> S: Fetch body and attachments
|
B ->> S: Fetch body and attachements
|
||||||
Note right of B: Build local database<br/>(e-mail UIDs)
|
Note right of B: Build local database<br/>(e-mail UIDs)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -58,8 +58,8 @@ sequenceDiagram
|
|||||||
C ->> B: IMAP SELECT directory
|
C ->> B: IMAP SELECT directory
|
||||||
C ->> B: IMAP SEARCH e-mails UIDs
|
C ->> B: IMAP SEARCH e-mails UIDs
|
||||||
C ->> B: IMAP FETCH of e-mail UID
|
C ->> B: IMAP FETCH of e-mail UID
|
||||||
B ->> S: Fetch body and attachments
|
B ->> S: Fetch body and attachements
|
||||||
Note right of B: Decrypt message<br/>and attachment
|
Note right of B: Decrypt message<br/>and attachement
|
||||||
B ->> C: IMAP response
|
B ->> C: IMAP response
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
# Update mechanism of Bridge
|
# Update mechanism of Bridge
|
||||||
|
|
||||||
There are multiple options how to change version of application:
|
There are mulitple options how to change version of application:
|
||||||
* Automatic in-app update
|
* Automatic in-app update
|
||||||
* Manual in-app update
|
* Manual in-app update
|
||||||
* Manual install
|
* Manual install
|
||||||
|
|
||||||
In-app update ends with restarting bridge into new version. Automatic in-app
|
In-app update ends with restarting bridge into new version. Automatic in-app
|
||||||
update is downloading, verifying and installing the new version immediately
|
update is downloading, verifying and installing the new version immediatelly
|
||||||
without user confirmation. For manual in-app update user needs to confirm first.
|
without user confirmation. For manual in-app update user needs to confirm first.
|
||||||
Update is done from special update file published on website.
|
Update is done from special update file published on website.
|
||||||
|
|
||||||
@ -25,7 +25,7 @@ The bridge is installed and executed differently for given OS:
|
|||||||
|
|
||||||
* macOS app does not use launcher
|
* macOS app does not use launcher
|
||||||
* No launcher, only one executable
|
* No launcher, only one executable
|
||||||
* In-App update replaces the bridge files in installation path directly
|
* In-App udpate replaces the bridge files in installation path directly
|
||||||
|
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
|
|||||||
26
go.mod
26
go.mod
@ -5,10 +5,10 @@ go 1.20
|
|||||||
require (
|
require (
|
||||||
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
|
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
|
||||||
github.com/Masterminds/semver/v3 v3.2.0
|
github.com/Masterminds/semver/v3 v3.2.0
|
||||||
github.com/ProtonMail/gluon v0.17.1-0.20231025125916-5c7941465df8
|
github.com/ProtonMail/gluon v0.16.1-0.20230901124123-075229a92cc4
|
||||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
|
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
|
||||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20231106093533-5f248dfc820d
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20230727082922-9115b4750ec7
|
||||||
github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton
|
github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton
|
||||||
github.com/PuerkitoBio/goquery v1.8.1
|
github.com/PuerkitoBio/goquery v1.8.1
|
||||||
github.com/abiosoft/ishell v2.0.0+incompatible
|
github.com/abiosoft/ishell v2.0.0+incompatible
|
||||||
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37
|
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37
|
||||||
@ -22,7 +22,6 @@ require (
|
|||||||
github.com/emersion/go-message v0.16.0
|
github.com/emersion/go-message v0.16.0
|
||||||
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead
|
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead
|
||||||
github.com/emersion/go-smtp v0.15.1-0.20221021114529-49b17434419d
|
github.com/emersion/go-smtp v0.15.1-0.20221021114529-49b17434419d
|
||||||
github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3
|
|
||||||
github.com/fatih/color v1.13.0
|
github.com/fatih/color v1.13.0
|
||||||
github.com/getsentry/sentry-go v0.15.0
|
github.com/getsentry/sentry-go v0.15.0
|
||||||
github.com/go-resty/resty/v2 v2.7.0
|
github.com/go-resty/resty/v2 v2.7.0
|
||||||
@ -43,17 +42,17 @@ require (
|
|||||||
github.com/vmihailenco/msgpack/v5 v5.3.5
|
github.com/vmihailenco/msgpack/v5 v5.3.5
|
||||||
go.uber.org/goleak v1.2.1
|
go.uber.org/goleak v1.2.1
|
||||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
|
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
|
||||||
golang.org/x/net v0.17.0
|
golang.org/x/net v0.10.0
|
||||||
golang.org/x/sys v0.13.0
|
golang.org/x/sys v0.8.0
|
||||||
golang.org/x/text v0.13.0
|
golang.org/x/text v0.9.0
|
||||||
google.golang.org/grpc v1.56.3
|
google.golang.org/grpc v1.53.0
|
||||||
google.golang.org/protobuf v1.30.0
|
google.golang.org/protobuf v1.30.0
|
||||||
howett.net/plist v1.0.0
|
howett.net/plist v1.0.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf // indirect
|
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf // indirect
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230717121622-edf196117233 // indirect
|
github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903 // indirect
|
||||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect
|
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect
|
||||||
github.com/ProtonMail/go-srp v0.0.7 // indirect
|
github.com/ProtonMail/go-srp v0.0.7 // indirect
|
||||||
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect
|
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect
|
||||||
@ -69,6 +68,7 @@ require (
|
|||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/elastic/go-windows v1.0.1 // indirect
|
github.com/elastic/go-windows v1.0.1 // indirect
|
||||||
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect
|
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect
|
||||||
|
github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3 // indirect
|
||||||
github.com/felixge/fgprof v0.9.3 // indirect
|
github.com/felixge/fgprof v0.9.3 // indirect
|
||||||
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
|
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||||
@ -79,7 +79,7 @@ require (
|
|||||||
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.2 // indirect
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
github.com/gofrs/uuid v4.3.0+incompatible // indirect
|
github.com/gofrs/uuid v4.3.0+incompatible // indirect
|
||||||
github.com/golang/protobuf v1.5.3 // indirect
|
github.com/golang/protobuf v1.5.2 // indirect
|
||||||
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect
|
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect
|
||||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
||||||
@ -108,19 +108,17 @@ require (
|
|||||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||||
gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a // indirect
|
|
||||||
golang.org/x/arch v0.3.0 // indirect
|
golang.org/x/arch v0.3.0 // indirect
|
||||||
golang.org/x/crypto v0.14.0 // indirect
|
golang.org/x/crypto v0.9.0 // indirect
|
||||||
golang.org/x/mod v0.8.0 // indirect
|
golang.org/x/mod v0.8.0 // indirect
|
||||||
golang.org/x/sync v0.2.0 // indirect
|
golang.org/x/sync v0.2.0 // indirect
|
||||||
golang.org/x/tools v0.6.0 // indirect
|
golang.org/x/tools v0.6.0 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
|
google.golang.org/genproto v0.0.0-20230221151758-ace64dc21148 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
replace (
|
replace (
|
||||||
github.com/docker/docker-credential-helpers => github.com/ProtonMail/docker-credential-helpers v1.1.0
|
github.com/docker/docker-credential-helpers => github.com/ProtonMail/docker-credential-helpers v1.1.0
|
||||||
github.com/emersion/go-message => github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7
|
github.com/emersion/go-message => github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7
|
||||||
github.com/go-resty/resty/v2 => github.com/LBeernaertProton/resty/v2 v2.0.0-20231030122409-92db8bee3605
|
|
||||||
github.com/keybase/go-keychain => github.com/cuthix/go-keychain v0.0.0-20230517073537-fc1740a83768
|
github.com/keybase/go-keychain => github.com/cuthix/go-keychain v0.0.0-20230517073537-fc1740a83768
|
||||||
)
|
)
|
||||||
|
|||||||
60
go.sum
60
go.sum
@ -15,8 +15,6 @@ github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557 h1:l6surSnJ3RP4qA
|
|||||||
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557/go.mod h1:sTrmvD/TxuypdOERsDOS7SndZg0rzzcCi1b6wQMXUYM=
|
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557/go.mod h1:sTrmvD/TxuypdOERsDOS7SndZg0rzzcCi1b6wQMXUYM=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||||
github.com/LBeernaertProton/resty/v2 v2.0.0-20231030122409-92db8bee3605 h1:54Fh3JS6s2Tjy6ZIRLtt1amZOqfYDcjErdye45z8fkQ=
|
|
||||||
github.com/LBeernaertProton/resty/v2 v2.0.0-20231030122409-92db8bee3605/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A=
|
|
||||||
github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
|
github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
|
||||||
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||||
@ -25,23 +23,24 @@ github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs
|
|||||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
|
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
|
||||||
github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
|
github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
|
||||||
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
|
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
|
||||||
github.com/ProtonMail/gluon v0.17.1-0.20231025125916-5c7941465df8 h1:sG0o5pEoS2z2jNR9zK7Juq5Tr3X+GfHmQ8L99RPowaE=
|
github.com/ProtonMail/gluon v0.16.1-0.20230901124123-075229a92cc4 h1:Uq2v2NYEtlTaK2WTh9BMph2Kv51JxMgvTkd7CjGPYc8=
|
||||||
github.com/ProtonMail/gluon v0.17.1-0.20231025125916-5c7941465df8/go.mod h1:Og5/Dz1MiGpCJn51XujZwxiLG7WzvvjE5PRpZBQmAHo=
|
github.com/ProtonMail/gluon v0.16.1-0.20230901124123-075229a92cc4/go.mod h1:Og5/Dz1MiGpCJn51XujZwxiLG7WzvvjE5PRpZBQmAHo=
|
||||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
|
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
|
||||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
|
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
|
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230717121622-edf196117233 h1:bdoKdh0f66/lrgVfYlxw0aqISY/KOqXmFJyGt7rGmnc=
|
github.com/ProtonMail/go-crypto v0.0.0-20230322105811-d73448b7e800/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230717121622-edf196117233/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903 h1:ZK3C5DtzV2nVAQTx5S5jQvMeDqWtD1By5mOoyY/xJek=
|
||||||
|
github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
|
||||||
github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7 h1:+j+Kd/DyZ/qGfMT9htAT7HxqIEbZHsatsx+m8AoV6fc=
|
github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7 h1:+j+Kd/DyZ/qGfMT9htAT7HxqIEbZHsatsx+m8AoV6fc=
|
||||||
github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
|
github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
|
||||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
|
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
|
||||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
|
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
|
||||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20231106093533-5f248dfc820d h1:LI2kvxBisX19f7lyMh0H6NcAHHg/Y7/x/xZWtxVrXOc=
|
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.20231106093533-5f248dfc820d/go.mod h1:WEXJqj5DSc2YI77SgXdpMY0nk33Qy92Vu2r4tOEazA8=
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20230727082922-9115b4750ec7/go.mod h1:+aTJoYu8bqzGECXL2DOdiZTZ64bGn3w0NC8VcFpJrFM=
|
||||||
github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
|
github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
|
||||||
github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk=
|
github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk=
|
||||||
github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton h1:8tqHYM6IGsdEc6Vxf1TWiwpHNj8yIEQNACPhxsDagrk=
|
github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton h1:YS6M20yvjCJPR1r4ADW5TPn6rahs4iAyZaACei86bEc=
|
||||||
github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton/go.mod h1:omVkSsfPAhmptzPF/piMXb16wKIWUvVhZbVW7sJKh0A=
|
github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton/go.mod h1:S1lYsaGHykYpxxh2SnJL6ypcAlANKj5NRSY6HxKryKQ=
|
||||||
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
|
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
|
||||||
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
|
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
|
||||||
github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw=
|
github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw=
|
||||||
@ -65,7 +64,6 @@ github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJm
|
|||||||
github.com/bradenaw/juniper v0.12.0 h1:Q/7icpPQD1nH/La5DobQfNEtwyrBSiSu47jOQx7lJEM=
|
github.com/bradenaw/juniper v0.12.0 h1:Q/7icpPQD1nH/La5DobQfNEtwyrBSiSu47jOQx7lJEM=
|
||||||
github.com/bradenaw/juniper v0.12.0/go.mod h1:Z2B7aJlQ7xbfWsnMLROj5t/5FQ94/MkIdKC30J4WvzI=
|
github.com/bradenaw/juniper v0.12.0/go.mod h1:Z2B7aJlQ7xbfWsnMLROj5t/5FQ94/MkIdKC30J4WvzI=
|
||||||
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||||
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
|
||||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||||
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
||||||
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||||
@ -157,6 +155,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
|
|||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
|
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
|
||||||
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||||
|
github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
|
||||||
|
github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
|
||||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
@ -178,8 +178,8 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
|
|||||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
@ -399,8 +399,6 @@ github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRT
|
|||||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a h1:DxppxFKRqJ8WD6oJ3+ZXKDY0iMONQDl5UTg2aTyHh8k=
|
|
||||||
gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a/go.mod h1:NREvu3a57BaK0R1+ztrEzHWiZAihohNLQ6trPxlIqZI=
|
|
||||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||||
@ -419,10 +417,9 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
|
|||||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
|
||||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||||
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
|
||||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||||
@ -465,15 +462,14 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
|
|||||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||||
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-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.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||||
|
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
||||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
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-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
@ -516,22 +512,16 @@ golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
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-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
|
||||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
@ -539,16 +529,12 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|||||||
golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
|
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
|
||||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
|
||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
|
||||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
|
||||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
@ -594,13 +580,13 @@ google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98
|
|||||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
|
google.golang.org/genproto v0.0.0-20230221151758-ace64dc21148 h1:muK+gVBJBfFb4SejshDBlN2/UgxCCOKH9Y34ljqEGOc=
|
||||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
|
google.golang.org/genproto v0.0.0-20230221151758-ace64dc21148/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw=
|
||||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||||
google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc=
|
google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc=
|
||||||
google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
|
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||||
|
|||||||
@ -52,9 +52,6 @@ const (
|
|||||||
flagCPUProfile = "cpu-prof"
|
flagCPUProfile = "cpu-prof"
|
||||||
flagCPUProfileShort = "p"
|
flagCPUProfileShort = "p"
|
||||||
|
|
||||||
flagTraceProfile = "trace-prof"
|
|
||||||
flagTraceProfileShort = "t"
|
|
||||||
|
|
||||||
flagMemProfile = "mem-prof"
|
flagMemProfile = "mem-prof"
|
||||||
flagMemProfileShort = "m"
|
flagMemProfileShort = "m"
|
||||||
|
|
||||||
@ -99,11 +96,6 @@ func New() *cli.App {
|
|||||||
Aliases: []string{flagCPUProfileShort},
|
Aliases: []string{flagCPUProfileShort},
|
||||||
Usage: "Generate CPU profile",
|
Usage: "Generate CPU profile",
|
||||||
},
|
},
|
||||||
&cli.BoolFlag{
|
|
||||||
Name: flagTraceProfile,
|
|
||||||
Aliases: []string{flagTraceProfileShort},
|
|
||||||
Usage: "Generate Trace profile",
|
|
||||||
},
|
|
||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
Name: flagMemProfile,
|
Name: flagMemProfile,
|
||||||
Aliases: []string{flagMemProfileShort},
|
Aliases: []string{flagMemProfileShort},
|
||||||
@ -204,7 +196,7 @@ func run(c *cli.Context) error {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
// Restart the app if requested.
|
// Restart the app if requested.
|
||||||
err = withRestarter(exe, func(restarter *restarter.Restarter) error {
|
return withRestarter(exe, func(restarter *restarter.Restarter) error {
|
||||||
// Handle crashes with various actions.
|
// Handle crashes with various actions.
|
||||||
return withCrashHandler(restarter, reporter, func(crashHandler *crash.Handler, quitCh <-chan struct{}) error {
|
return withCrashHandler(restarter, reporter, func(crashHandler *crash.Handler, quitCh <-chan struct{}) error {
|
||||||
migrationErr := migrateOldVersions()
|
migrationErr := migrateOldVersions()
|
||||||
@ -276,9 +268,6 @@ func run(c *cli.Context) error {
|
|||||||
b.PushError(bridge.ErrVaultCorrupt)
|
b.PushError(bridge.ErrVaultCorrupt)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove old updates files
|
|
||||||
b.RemoveOldUpdates()
|
|
||||||
|
|
||||||
// Start telemetry heartbeat process
|
// Start telemetry heartbeat process
|
||||||
b.StartHeartbeat(b)
|
b.StartHeartbeat(b)
|
||||||
|
|
||||||
@ -293,13 +282,6 @@ func run(c *cli.Context) error {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// if an error occurs, it must be logged now because we're about to close the log file.
|
|
||||||
if err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there's another instance already running, try to raise it and exit.
|
// If there's another instance already running, try to raise it and exit.
|
||||||
@ -397,11 +379,6 @@ func withProfiler(c *cli.Context, fn func() error) error {
|
|||||||
defer profile.Start(profile.CPUProfile, profile.ProfilePath(".")).Stop()
|
defer profile.Start(profile.CPUProfile, profile.ProfilePath(".")).Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.Bool(flagTraceProfile) {
|
|
||||||
logrus.Debug("Running with Trace profiling")
|
|
||||||
defer profile.Start(profile.TraceProfile, profile.ProfilePath(".")).Stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.Bool(flagMemProfile) {
|
if c.Bool(flagMemProfile) {
|
||||||
logrus.Debug("Running with memory profiling")
|
logrus.Debug("Running with memory profiling")
|
||||||
defer profile.Start(profile.MemProfile, profile.MemProfileAllocs, profile.ProfilePath(".")).Stop()
|
defer profile.Start(profile.MemProfile, profile.MemProfileAllocs, profile.ProfilePath(".")).Stop()
|
||||||
|
|||||||
@ -155,7 +155,7 @@ func newUpdater(locations *locations.Locations) (*updater.Updater, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return updater.NewUpdater(
|
return updater.NewUpdater(
|
||||||
versioner.New(updatesDir),
|
updater.NewInstaller(versioner.New(updatesDir)),
|
||||||
verifier,
|
verifier,
|
||||||
constants.UpdateName,
|
constants.UpdateName,
|
||||||
runtime.GOOS,
|
runtime.GOOS,
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
|
|
||||||
"github.com/ProtonMail/gluon/async"
|
"github.com/ProtonMail/gluon/async"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/certs"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
|
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||||
@ -44,6 +45,23 @@ func WithVault(locations *locations.Locations, panicHandler async.PanicHandler,
|
|||||||
"corrupt": corrupt,
|
"corrupt": corrupt,
|
||||||
}).Debug("Vault created")
|
}).Debug("Vault created")
|
||||||
|
|
||||||
|
// Install the certificates if needed.
|
||||||
|
if installed := encVault.GetCertsInstalled(); !installed {
|
||||||
|
logrus.Debug("Installing certificates")
|
||||||
|
|
||||||
|
certPEM, _ := encVault.GetBridgeTLSCert()
|
||||||
|
|
||||||
|
if err := certs.NewInstaller().InstallCert(certPEM); err != nil {
|
||||||
|
return fmt.Errorf("failed to install certs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := encVault.SetCertsInstalled(true); err != nil {
|
||||||
|
return fmt.Errorf("failed to set certs installed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Debug("Certificates successfully installed")
|
||||||
|
}
|
||||||
|
|
||||||
// GODT-1950: Add teardown actions (e.g. to close the vault).
|
// GODT-1950: Add teardown actions (e.g. to close the vault).
|
||||||
|
|
||||||
return fn(encVault, insecure, corrupt)
|
return fn(encVault, insecure, corrupt)
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
//go:build !build_qa && !test_integration
|
//go:build !build_qa
|
||||||
|
|
||||||
package bridge
|
package bridge
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
//go:build build_qa || test_integration
|
//go:build build_qa
|
||||||
|
|
||||||
package bridge
|
package bridge
|
||||||
|
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@ -37,11 +38,8 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/focus"
|
"github.com/ProtonMail/proton-bridge/v3/internal/focus"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/identifier"
|
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/sentry"
|
"github.com/ProtonMail/proton-bridge/v3/internal/sentry"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapsmtpserver"
|
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice"
|
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/telemetry"
|
"github.com/ProtonMail/proton-bridge/v3/internal/telemetry"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||||
@ -61,7 +59,7 @@ type Bridge struct {
|
|||||||
// api manages user API clients.
|
// api manages user API clients.
|
||||||
api *proton.Manager
|
api *proton.Manager
|
||||||
proxyCtl ProxyController
|
proxyCtl ProxyController
|
||||||
identifier identifier.Identifier
|
identifier Identifier
|
||||||
|
|
||||||
// tlsConfig holds the bridge TLS config used by the IMAP and SMTP servers.
|
// tlsConfig holds the bridge TLS config used by the IMAP and SMTP servers.
|
||||||
tlsConfig *tls.Config
|
tlsConfig *tls.Config
|
||||||
@ -127,8 +125,9 @@ type Bridge struct {
|
|||||||
// goHeartbeat triggers a check/sending if heartbeat is needed.
|
// goHeartbeat triggers a check/sending if heartbeat is needed.
|
||||||
goHeartbeat func()
|
goHeartbeat func()
|
||||||
|
|
||||||
serverManager *imapsmtpserver.Service
|
uidValidityGenerator imap.UIDValidityGenerator
|
||||||
syncService *syncservice.Service
|
|
||||||
|
serverManager *ServerManager
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new bridge.
|
// New creates a new bridge.
|
||||||
@ -141,7 +140,7 @@ func New(
|
|||||||
|
|
||||||
apiURL string, // the URL of the API to use
|
apiURL string, // the URL of the API to use
|
||||||
cookieJar http.CookieJar, // the cookie jar to use
|
cookieJar http.CookieJar, // the cookie jar to use
|
||||||
identifier identifier.Identifier, // the identifier to keep track of the user agent
|
identifier Identifier, // the identifier to keep track of the user agent
|
||||||
tlsReporter TLSReporter, // the TLS reporter to report TLS errors
|
tlsReporter TLSReporter, // the TLS reporter to report TLS errors
|
||||||
roundTripper http.RoundTripper, // the round tripper to use for API requests
|
roundTripper http.RoundTripper, // the round tripper to use for API requests
|
||||||
proxyCtl ProxyController, // the DoH controller
|
proxyCtl ProxyController, // the DoH controller
|
||||||
@ -208,7 +207,7 @@ func newBridge(
|
|||||||
reporter reporter.Reporter,
|
reporter reporter.Reporter,
|
||||||
|
|
||||||
api *proton.Manager,
|
api *proton.Manager,
|
||||||
identifier identifier.Identifier,
|
identifier Identifier,
|
||||||
proxyCtl ProxyController,
|
proxyCtl ProxyController,
|
||||||
uidValidityGenerator imap.UIDValidityGenerator,
|
uidValidityGenerator imap.UIDValidityGenerator,
|
||||||
|
|
||||||
@ -270,26 +269,17 @@ func newBridge(
|
|||||||
firstStart: firstStart,
|
firstStart: firstStart,
|
||||||
lastVersion: lastVersion,
|
lastVersion: lastVersion,
|
||||||
|
|
||||||
tasks: tasks,
|
tasks: tasks,
|
||||||
syncService: syncservice.NewService(reporter, panicHandler),
|
|
||||||
|
uidValidityGenerator: uidValidityGenerator,
|
||||||
|
|
||||||
|
serverManager: newServerManager(),
|
||||||
}
|
}
|
||||||
|
|
||||||
bridge.serverManager = imapsmtpserver.NewService(context.Background(),
|
if err := bridge.serverManager.Init(bridge); err != nil {
|
||||||
&bridgeSMTPSettings{b: bridge},
|
|
||||||
&bridgeIMAPSettings{b: bridge},
|
|
||||||
&bridgeEventPublisher{b: bridge},
|
|
||||||
panicHandler,
|
|
||||||
reporter,
|
|
||||||
uidValidityGenerator,
|
|
||||||
&bridgeIMAPSMTPTelemetry{b: bridge},
|
|
||||||
)
|
|
||||||
|
|
||||||
if err := bridge.serverManager.Init(context.Background(), bridge.tasks, &bridgeEventSubscription{b: bridge}); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
bridge.syncService.Run(bridge.tasks)
|
|
||||||
|
|
||||||
return bridge, nil
|
return bridge, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -417,6 +407,11 @@ func (bridge *Bridge) GetErrors() []error {
|
|||||||
func (bridge *Bridge) Close(ctx context.Context) {
|
func (bridge *Bridge) Close(ctx context.Context) {
|
||||||
logrus.Info("Closing bridge")
|
logrus.Info("Closing bridge")
|
||||||
|
|
||||||
|
// Close the servers
|
||||||
|
if err := bridge.serverManager.CloseServers(ctx); err != nil {
|
||||||
|
logrus.WithError(err).Error("Failed to close servers")
|
||||||
|
}
|
||||||
|
|
||||||
// Close all users.
|
// Close all users.
|
||||||
safe.Lock(func() {
|
safe.Lock(func() {
|
||||||
for _, user := range bridge.users {
|
for _, user := range bridge.users {
|
||||||
@ -424,11 +419,6 @@ func (bridge *Bridge) Close(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
}, bridge.usersLock)
|
}, bridge.usersLock)
|
||||||
|
|
||||||
// Close the servers
|
|
||||||
if err := bridge.serverManager.CloseServers(ctx); err != nil {
|
|
||||||
logrus.WithError(err).Error("Failed to close servers")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop all ongoing tasks.
|
// Stop all ongoing tasks.
|
||||||
bridge.tasks.CancelAndWait()
|
bridge.tasks.CancelAndWait()
|
||||||
|
|
||||||
@ -487,15 +477,27 @@ func (bridge *Bridge) remWatcher(watcher *watcher.Watcher[events.Event]) {
|
|||||||
watcher.Close()
|
watcher.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) onStatusUp(_ context.Context) {
|
func (bridge *Bridge) onStatusUp(ctx context.Context) {
|
||||||
logrus.Info("Handling API status up")
|
logrus.Info("Handling API status up")
|
||||||
|
|
||||||
|
safe.RLock(func() {
|
||||||
|
for _, user := range bridge.users {
|
||||||
|
user.OnStatusUp(ctx)
|
||||||
|
}
|
||||||
|
}, bridge.usersLock)
|
||||||
|
|
||||||
bridge.goLoad()
|
bridge.goLoad()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) onStatusDown(ctx context.Context) {
|
func (bridge *Bridge) onStatusDown(ctx context.Context) {
|
||||||
logrus.Info("Handling API status down")
|
logrus.Info("Handling API status down")
|
||||||
|
|
||||||
|
safe.RLock(func() {
|
||||||
|
for _, user := range bridge.users {
|
||||||
|
user.OnStatusDown(ctx)
|
||||||
|
}
|
||||||
|
}, bridge.usersLock)
|
||||||
|
|
||||||
for backoff := time.Second; ; backoff = min(backoff*2, 30*time.Second) {
|
for backoff := time.Second; ; backoff = min(backoff*2, 30*time.Second) {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
@ -525,6 +527,24 @@ func loadTLSConfig(vault *vault.Vault) (*tls.Config, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newListener(port int, useTLS bool, tlsConfig *tls.Config) (net.Listener, error) {
|
||||||
|
if useTLS {
|
||||||
|
tlsListener, err := tls.Listen("tcp", fmt.Sprintf("%v:%v", constants.Host, port), tlsConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tlsListener, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
netListener, err := net.Listen("tcp", fmt.Sprintf("%v:%v", constants.Host, port))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return netListener, nil
|
||||||
|
}
|
||||||
|
|
||||||
func min(a, b time.Duration) time.Duration {
|
func min(a, b time.Duration) time.Duration {
|
||||||
if a < b {
|
if a < b {
|
||||||
return a
|
return a
|
||||||
|
|||||||
@ -44,7 +44,6 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/focus"
|
"github.com/ProtonMail/proton-bridge/v3/internal/focus"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
|
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapsmtpserver"
|
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
|
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
|
||||||
@ -55,7 +54,6 @@ import (
|
|||||||
"github.com/emersion/go-sasl"
|
"github.com/emersion/go-sasl"
|
||||||
"github.com/emersion/go-smtp"
|
"github.com/emersion/go-smtp"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"go.uber.org/goleak"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -585,7 +583,7 @@ func TestBridge_MissingGluonStore(t *testing.T) {
|
|||||||
require.NoError(t, os.RemoveAll(gluonDir))
|
require.NoError(t, os.RemoveAll(gluonDir))
|
||||||
|
|
||||||
// Bridge starts but can't find the gluon store dir; there should be no error.
|
// Bridge starts but can't find the gluon store dir; there should be no error.
|
||||||
withBridgeWaitForServers(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
// ...
|
// ...
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -623,10 +621,6 @@ func TestBridge_AddressWithoutKeys(t *testing.T) {
|
|||||||
defer m.Close()
|
defer m.Close()
|
||||||
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
// Watch for sync finished event.
|
|
||||||
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
|
||||||
defer done()
|
|
||||||
|
|
||||||
// Create a user which will have an address without keys.
|
// Create a user which will have an address without keys.
|
||||||
userID, _, err := s.CreateUser("nokeys", []byte("password"))
|
userID, _, err := s.CreateUser("nokeys", []byte("password"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -647,6 +641,10 @@ func TestBridge_AddressWithoutKeys(t *testing.T) {
|
|||||||
// Remove the address keys.
|
// Remove the address keys.
|
||||||
require.NoError(t, s.RemoveAddressKey(userID, aliasAddrID, aliasAddr.Keys[0].ID))
|
require.NoError(t, s.RemoveAddressKey(userID, aliasAddrID, aliasAddr.Keys[0].ID))
|
||||||
|
|
||||||
|
// Watch for sync finished event.
|
||||||
|
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
||||||
|
defer done()
|
||||||
|
|
||||||
// We should be able to log the user in.
|
// We should be able to log the user in.
|
||||||
require.NoError(t, getErr(bridge.LoginFull(context.Background(), "nokeys", []byte("password"), nil, nil)))
|
require.NoError(t, getErr(bridge.LoginFull(context.Background(), "nokeys", []byte("password"), nil, nil)))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -702,10 +700,10 @@ func TestBridge_InitGluonDirectory(t *testing.T) {
|
|||||||
configDir, err := b.GetGluonDataDir()
|
configDir, err := b.GetGluonDataDir()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
_, err = os.ReadDir(imapsmtpserver.ApplyGluonCachePathSuffix(b.GetGluonCacheDir()))
|
_, err = os.ReadDir(bridge.ApplyGluonCachePathSuffix(b.GetGluonCacheDir()))
|
||||||
require.False(t, os.IsNotExist(err))
|
require.False(t, os.IsNotExist(err))
|
||||||
|
|
||||||
_, err = os.ReadDir(imapsmtpserver.ApplyGluonConfigPathSuffix(configDir))
|
_, err = os.ReadDir(bridge.ApplyGluonConfigPathSuffix(configDir))
|
||||||
require.False(t, os.IsNotExist(err))
|
require.False(t, os.IsNotExist(err))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -778,16 +776,16 @@ func TestBridge_ChangeCacheDirectory(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Old store should no more exists.
|
// Old store should no more exists.
|
||||||
_, err = os.ReadDir(imapsmtpserver.ApplyGluonCachePathSuffix(currentCacheDir))
|
_, err = os.ReadDir(bridge.ApplyGluonCachePathSuffix(currentCacheDir))
|
||||||
require.True(t, os.IsNotExist(err))
|
require.True(t, os.IsNotExist(err))
|
||||||
// Database should not have changed.
|
// Database should not have changed.
|
||||||
_, err = os.ReadDir(imapsmtpserver.ApplyGluonConfigPathSuffix(configDir))
|
_, err = os.ReadDir(bridge.ApplyGluonConfigPathSuffix(configDir))
|
||||||
require.False(t, os.IsNotExist(err))
|
require.False(t, os.IsNotExist(err))
|
||||||
|
|
||||||
// New path should have Gluon sub-folder.
|
// New path should have Gluon sub-folder.
|
||||||
require.Equal(t, filepath.Join(newCacheDir, "gluon"), b.GetGluonCacheDir())
|
require.Equal(t, filepath.Join(newCacheDir, "gluon"), b.GetGluonCacheDir())
|
||||||
// And store should be inside it.
|
// And store should be inside it.
|
||||||
_, err = os.ReadDir(imapsmtpserver.ApplyGluonCachePathSuffix(b.GetGluonCacheDir()))
|
_, err = os.ReadDir(bridge.ApplyGluonCachePathSuffix(b.GetGluonCacheDir()))
|
||||||
require.False(t, os.IsNotExist(err))
|
require.False(t, os.IsNotExist(err))
|
||||||
|
|
||||||
// We should be able to fetch.
|
// We should be able to fetch.
|
||||||
@ -875,9 +873,6 @@ func TestBridge_ChangeAddressOrder(t *testing.T) {
|
|||||||
|
|
||||||
// withEnv creates the full test environment and runs the tests.
|
// withEnv creates the full test environment and runs the tests.
|
||||||
func withEnv(t *testing.T, tests func(context.Context, *server.Server, *proton.NetCtl, bridge.Locator, []byte), opts ...server.Option) {
|
func withEnv(t *testing.T, tests func(context.Context, *server.Server, *proton.NetCtl, bridge.Locator, []byte), opts ...server.Option) {
|
||||||
opt := goleak.IgnoreCurrent()
|
|
||||||
defer goleak.VerifyNone(t, opt)
|
|
||||||
|
|
||||||
server := server.New(opts...)
|
server := server.New(opts...)
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
@ -1062,7 +1057,6 @@ func getConnectedUserIDs(t *testing.T, b *bridge.Bridge) []string {
|
|||||||
func chToType[In, Out any](inCh <-chan In, done func()) (<-chan Out, func()) {
|
func chToType[In, Out any](inCh <-chan In, done func()) (<-chan Out, func()) {
|
||||||
outCh := make(chan Out)
|
outCh := make(chan Out)
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
go func() {
|
go func() {
|
||||||
defer close(outCh)
|
defer close(outCh)
|
||||||
|
|
||||||
@ -1072,19 +1066,11 @@ func chToType[In, Out any](inCh <-chan In, done func()) (<-chan Out, func()) {
|
|||||||
panic(fmt.Sprintf("unexpected type %T", in))
|
panic(fmt.Sprintf("unexpected type %T", in))
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
outCh <- out
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
|
|
||||||
case outCh <- out:
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return outCh, func() {
|
return outCh, done
|
||||||
cancel()
|
|
||||||
done()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type eventWaiter struct {
|
type eventWaiter struct {
|
||||||
|
|||||||
@ -19,7 +19,6 @@ package bridge
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
@ -34,133 +33,63 @@ const (
|
|||||||
DefaultMaxSessionCountForBugReport = 10
|
DefaultMaxSessionCountForBugReport = 10
|
||||||
)
|
)
|
||||||
|
|
||||||
type ReportBugReq struct {
|
func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, description, username, email, client string, attachLogs bool) error {
|
||||||
OSType string
|
var account string
|
||||||
OSVersion string
|
|
||||||
Title string
|
|
||||||
Description string
|
|
||||||
Username string
|
|
||||||
Email string
|
|
||||||
EmailClient string
|
|
||||||
IncludeLogs bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bridge *Bridge) ReportBug(ctx context.Context, report *ReportBugReq) error {
|
if info, err := bridge.QueryUserInfo(username); err == nil {
|
||||||
if info, err := bridge.QueryUserInfo(report.Username); err == nil {
|
account = info.Username
|
||||||
report.Username = info.Username
|
|
||||||
} else if userIDs := bridge.GetUserIDs(); len(userIDs) > 0 {
|
} else if userIDs := bridge.GetUserIDs(); len(userIDs) > 0 {
|
||||||
if err := bridge.vault.GetUser(userIDs[0], func(user *vault.User) {
|
if err := bridge.vault.GetUser(userIDs[0], func(user *vault.User) {
|
||||||
report.Username = user.Username()
|
account = user.Username()
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var attachments []proton.ReportBugAttachment
|
var attachment []proton.ReportBugAttachment
|
||||||
if report.IncludeLogs {
|
|
||||||
logs, err := bridge.CollectLogs()
|
if attachLogs {
|
||||||
|
logsPath, err := bridge.locator.ProvideLogsPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
attachments = append(attachments, logs)
|
|
||||||
|
buffer, err := logging.ZipLogsForBugReport(logsPath, DefaultMaxSessionCountForBugReport, DefaultMaxBugReportZipSize)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(buffer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
attachment = append(attachment, proton.ReportBugAttachment{
|
||||||
|
Name: "logs.zip",
|
||||||
|
Filename: "logs.zip",
|
||||||
|
MIMEType: "application/zip",
|
||||||
|
Body: body,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
var firstAtt proton.ReportBugAttachment
|
safe.Lock(func() {
|
||||||
if len(attachments) > 0 && report.IncludeLogs {
|
|
||||||
firstAtt = attachments[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
attachmentType := proton.AttachmentTypeSync
|
|
||||||
if len(attachments) > 1 {
|
|
||||||
attachmentType = proton.AttachmentTypeAsync
|
|
||||||
}
|
|
||||||
|
|
||||||
token, err := bridge.createTicket(ctx, report, attachmentType, firstAtt)
|
|
||||||
if err != nil || token == "" {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
safe.RLock(func() {
|
|
||||||
for _, user := range bridge.users {
|
for _, user := range bridge.users {
|
||||||
user.ReportBugSent()
|
user.ReportBugSent()
|
||||||
}
|
}
|
||||||
}, bridge.usersLock)
|
}, bridge.usersLock)
|
||||||
|
|
||||||
// if we have a token we can append more attachment to the bugReport
|
return bridge.api.ReportBug(ctx, proton.ReportBugReq{
|
||||||
for i, att := range attachments {
|
OS: osType,
|
||||||
if i == 0 && report.IncludeLogs {
|
OSVersion: osVersion,
|
||||||
continue
|
|
||||||
}
|
|
||||||
err := bridge.appendComment(ctx, token, att)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bridge *Bridge) CollectLogs() (proton.ReportBugAttachment, error) {
|
Title: "[Bridge] Bug",
|
||||||
logsPath, err := bridge.locator.ProvideLogsPath()
|
Description: description,
|
||||||
if err != nil {
|
|
||||||
return proton.ReportBugAttachment{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer, err := logging.ZipLogsForBugReport(logsPath, DefaultMaxSessionCountForBugReport, DefaultMaxBugReportZipSize)
|
Client: client,
|
||||||
if err != nil {
|
|
||||||
return proton.ReportBugAttachment{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := io.ReadAll(buffer)
|
|
||||||
if err != nil {
|
|
||||||
return proton.ReportBugAttachment{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return proton.ReportBugAttachment{
|
|
||||||
Name: "logs.zip",
|
|
||||||
Filename: "logs.zip",
|
|
||||||
MIMEType: "application/zip",
|
|
||||||
Body: body,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bridge *Bridge) createTicket(ctx context.Context, report *ReportBugReq,
|
|
||||||
asyncAttach proton.AttachmentType, att proton.ReportBugAttachment) (string, error) {
|
|
||||||
var attachments []proton.ReportBugAttachment
|
|
||||||
attachments = append(attachments, att)
|
|
||||||
res, err := bridge.api.ReportBug(ctx, proton.ReportBugReq{
|
|
||||||
OS: report.OSType,
|
|
||||||
OSVersion: report.OSVersion,
|
|
||||||
|
|
||||||
Title: "[Bridge] Bug - " + report.Title,
|
|
||||||
Description: report.Description,
|
|
||||||
|
|
||||||
Client: report.EmailClient,
|
|
||||||
ClientType: proton.ClientTypeEmail,
|
ClientType: proton.ClientTypeEmail,
|
||||||
ClientVersion: constants.AppVersion(bridge.curVersion.Original()),
|
ClientVersion: constants.AppVersion(bridge.curVersion.Original()),
|
||||||
|
|
||||||
Username: report.Username,
|
Username: account,
|
||||||
Email: report.Email,
|
Email: email,
|
||||||
|
}, attachment...)
|
||||||
AsyncAttachments: asyncAttach,
|
|
||||||
}, attachments...)
|
|
||||||
|
|
||||||
if err != nil || asyncAttach != proton.AttachmentTypeAsync {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if asyncAttach == proton.AttachmentTypeAsync && res.Token == nil {
|
|
||||||
return "", errors.New("no token returns for AsyncAttachments")
|
|
||||||
}
|
|
||||||
|
|
||||||
return *res.Token, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bridge *Bridge) appendComment(ctx context.Context, token string, att proton.ReportBugAttachment) error {
|
|
||||||
var attachments []proton.ReportBugAttachment
|
|
||||||
attachments = append(attachments, att)
|
|
||||||
return bridge.api.ReportBugAttachement(ctx, proton.ReportBugAttachmentReq{
|
|
||||||
Product: proton.ClientTypeEmail,
|
|
||||||
Body: "Comment adding attachment: " + att.Filename,
|
|
||||||
Token: token,
|
|
||||||
}, attachments...)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,7 +22,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (bridge *Bridge) ReportBugClicked() {
|
func (bridge *Bridge) ReportBugClicked() {
|
||||||
safe.RLock(func() {
|
safe.Lock(func() {
|
||||||
for _, user := range bridge.users {
|
for _, user := range bridge.users {
|
||||||
user.ReportBugClicked()
|
user.ReportBugClicked()
|
||||||
}
|
}
|
||||||
@ -30,7 +30,7 @@ func (bridge *Bridge) ReportBugClicked() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) AutoconfigUsed(client string) {
|
func (bridge *Bridge) AutoconfigUsed(client string) {
|
||||||
safe.RLock(func() {
|
safe.Lock(func() {
|
||||||
for _, user := range bridge.users {
|
for _, user := range bridge.users {
|
||||||
user.AutoconfigUsed(client)
|
user.AutoconfigUsed(client)
|
||||||
}
|
}
|
||||||
@ -38,7 +38,7 @@ func (bridge *Bridge) AutoconfigUsed(client string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) KBArticleOpened(article string) {
|
func (bridge *Bridge) KBArticleOpened(article string) {
|
||||||
safe.RLock(func() {
|
safe.Lock(func() {
|
||||||
for _, user := range bridge.users {
|
for _, user := range bridge.users {
|
||||||
user.KBArticleOpened(article)
|
user.KBArticleOpened(article)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -79,7 +79,7 @@ func (bridge *Bridge) CheckClientState(ctx context.Context, checkFlags bool, pro
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.Debug("Building state")
|
log.Debug("Building state")
|
||||||
state, err := meta.BuildMailboxToMessageMap(ctx, usr)
|
state, err := meta.BuildMailboxToMessageMap(usr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Error("Failed to build state")
|
log.WithError(err).Error("Failed to build state")
|
||||||
return result, err
|
return result, err
|
||||||
|
|||||||
@ -1,45 +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"
|
|
||||||
|
|
||||||
"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)
|
|
||||||
}
|
|
||||||
@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package files
|
package bridge
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -24,7 +24,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
func MoveDir(from, to string) error {
|
func moveDir(from, to string) error {
|
||||||
entries, err := os.ReadDir(from)
|
entries, err := os.ReadDir(from)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -36,7 +36,7 @@ func MoveDir(from, to string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := MoveDir(filepath.Join(from, entry.Name()), filepath.Join(to, entry.Name())); err != nil {
|
if err := moveDir(filepath.Join(from, entry.Name()), filepath.Join(to, entry.Name())); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,12 +61,12 @@ func moveFile(from, to string) error {
|
|||||||
return os.Rename(from, to)
|
return os.Rename(from, to)
|
||||||
}
|
}
|
||||||
|
|
||||||
func CopyDir(from, to string) error {
|
func copyDir(from, to string) error {
|
||||||
entries, err := os.ReadDir(from)
|
entries, err := os.ReadDir(from)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := CreateIfNotExists(to, 0o700); err != nil {
|
if err := createIfNotExists(to, 0o700); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
@ -74,11 +74,11 @@ func CopyDir(from, to string) error {
|
|||||||
destPath := filepath.Join(to, entry.Name())
|
destPath := filepath.Join(to, entry.Name())
|
||||||
|
|
||||||
if entry.IsDir() {
|
if entry.IsDir() {
|
||||||
if err := CopyDir(sourcePath, destPath); err != nil {
|
if err := copyDir(sourcePath, destPath); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if err := CopyFile(sourcePath, destPath); err != nil {
|
if err := copyFile(sourcePath, destPath); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -86,7 +86,7 @@ func CopyDir(from, to string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func CopyFile(srcFile, dstFile string) error {
|
func copyFile(srcFile, dstFile string) error {
|
||||||
out, err := os.Create(filepath.Clean(dstFile))
|
out, err := os.Create(filepath.Clean(dstFile))
|
||||||
defer func(out *os.File) {
|
defer func(out *os.File) {
|
||||||
_ = out.Close()
|
_ = out.Close()
|
||||||
@ -113,7 +113,7 @@ func CopyFile(srcFile, dstFile string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Exists(filePath string) bool {
|
func exists(filePath string) bool {
|
||||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -121,8 +121,8 @@ func Exists(filePath string) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateIfNotExists(dir string, perm os.FileMode) error {
|
func createIfNotExists(dir string, perm os.FileMode) error {
|
||||||
if Exists(dir) {
|
if exists(dir) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package files
|
package bridge
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
@ -41,7 +41,7 @@ func TestMoveDir(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Move the files.
|
// Move the files.
|
||||||
if err := MoveDir(from, to); err != nil {
|
if err := moveDir(from, to); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,35 +40,3 @@ 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,12 +20,23 @@ package bridge
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/Masterminds/semver/v3"
|
"github.com/Masterminds/semver/v3"
|
||||||
|
"github.com/ProtonMail/gluon"
|
||||||
|
"github.com/ProtonMail/gluon/async"
|
||||||
imapEvents "github.com/ProtonMail/gluon/events"
|
imapEvents "github.com/ProtonMail/gluon/events"
|
||||||
|
"github.com/ProtonMail/gluon/imap"
|
||||||
|
"github.com/ProtonMail/gluon/reporter"
|
||||||
|
"github.com/ProtonMail/gluon/store"
|
||||||
|
"github.com/ProtonMail/gluon/store/fallback_v0"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapsmtpserver"
|
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
|
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
@ -34,6 +45,16 @@ func (bridge *Bridge) restartIMAP(ctx context.Context) error {
|
|||||||
return bridge.serverManager.RestartIMAP(ctx)
|
return bridge.serverManager.RestartIMAP(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// addIMAPUser connects the given user to gluon.
|
||||||
|
func (bridge *Bridge) addIMAPUser(ctx context.Context, user *user.User) error {
|
||||||
|
return bridge.serverManager.AddIMAPUser(ctx, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeIMAPUser disconnects the given user from gluon, optionally also removing its files.
|
||||||
|
func (bridge *Bridge) removeIMAPUser(ctx context.Context, user *user.User, withData bool) error {
|
||||||
|
return bridge.serverManager.RemoveIMAPUser(ctx, user, withData)
|
||||||
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) {
|
func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) {
|
||||||
switch event := event.(type) {
|
switch event := event.(type) {
|
||||||
case imapEvents.UserAdded:
|
case imapEvents.UserAdded:
|
||||||
@ -71,59 +92,108 @@ func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type bridgeIMAPSettings struct {
|
func ApplyGluonCachePathSuffix(basePath string) string {
|
||||||
b *Bridge
|
return filepath.Join(basePath, "backend", "store")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *bridgeIMAPSettings) EventPublisher() imapsmtpserver.IMAPEventPublisher {
|
func ApplyGluonConfigPathSuffix(basePath string) string {
|
||||||
return b
|
return filepath.Join(basePath, "backend", "db")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *bridgeIMAPSettings) TLSConfig() *tls.Config {
|
func newIMAPServer(
|
||||||
return b.b.tlsConfig
|
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) LogClient() bool {
|
logrus.WithFields(logrus.Fields{
|
||||||
return b.b.logIMAPClient
|
"gluonStore": gluonCacheDir,
|
||||||
}
|
"gluonDB": gluonConfigDir,
|
||||||
|
"version": version,
|
||||||
|
"logClient": logClient,
|
||||||
|
"logServer": logServer,
|
||||||
|
}).Info("Creating IMAP server")
|
||||||
|
|
||||||
func (b *bridgeIMAPSettings) LogServer() bool {
|
if logClient || logServer {
|
||||||
return b.b.logIMAPServer
|
log := logrus.WithField("protocol", "IMAP")
|
||||||
}
|
log.Warning("================================================")
|
||||||
|
log.Warning("THIS LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA")
|
||||||
func (b *bridgeIMAPSettings) Port() int {
|
log.Warning("================================================")
|
||||||
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))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,26 +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
|
|
||||||
|
|
||||||
type bridgeIMAPSMTPTelemetry struct {
|
|
||||||
b *Bridge
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b bridgeIMAPSMTPTelemetry) SetCacheLocation(s string) {
|
|
||||||
b.b.heartbeat.SetCacheLocation(s)
|
|
||||||
}
|
|
||||||
55
internal/bridge/migration.go
Normal file
55
internal/bridge/migration.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
// Copyright (c) 2023 Proton AG
|
||||||
|
//
|
||||||
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package bridge
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/gluon/reporter"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (bridge *Bridge) databaseResyncNeeded() bool {
|
||||||
|
if strings.HasPrefix(bridge.lastVersion.String(), "3.4.0") &&
|
||||||
|
strings.HasPrefix(bridge.curVersion.String(), "3.4.1") {
|
||||||
|
logrus.WithFields(logrus.Fields{
|
||||||
|
"lastVersion": bridge.lastVersion.String(),
|
||||||
|
"currVersion": bridge.curVersion.String(),
|
||||||
|
}).Warning("Database re-synchronisation needed")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) migrateUser(vault *vault.User) {
|
||||||
|
if bridge.databaseResyncNeeded() {
|
||||||
|
if err := bridge.reporter.ReportMessage("Database need to be re-sync for migration."); err != nil {
|
||||||
|
logrus.WithError(err).Error("Failed to report database re-sync for migration.")
|
||||||
|
}
|
||||||
|
if err := vault.ClearSyncStatus(); err != nil {
|
||||||
|
logrus.WithError(err).Error("Failed reset to SyncStatus.")
|
||||||
|
if err2 := bridge.reporter.ReportMessageWithContext("Failed to reset SyncStatus for Database migration.",
|
||||||
|
reporter.Context{
|
||||||
|
"error": err,
|
||||||
|
}); err2 != nil {
|
||||||
|
logrus.WithError(err2).Error("Failed to report reset SyncStatus error.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -154,7 +154,3 @@ func (testUpdater *TestUpdater) GetVersionInfo(_ context.Context, _ updater.Down
|
|||||||
func (testUpdater *TestUpdater) InstallUpdate(_ context.Context, _ updater.Downloader, _ updater.VersionInfo) error {
|
func (testUpdater *TestUpdater) InstallUpdate(_ context.Context, _ updater.Downloader, _ updater.VersionInfo) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (testUpdater *TestUpdater) RemoveOldUpdates() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@ -84,11 +84,6 @@ func TestBridge_Refresh(t *testing.T) {
|
|||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).AnyTimes()
|
mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).AnyTimes()
|
||||||
|
|
||||||
// Wait for refresh event first
|
|
||||||
refreshCh, refreshChDone := chToType[events.Event, events.UserRefreshed](b.GetEvents(events.UserRefreshed{}))
|
|
||||||
defer refreshChDone()
|
|
||||||
require.Equal(t, userID, (<-refreshCh).UserID)
|
|
||||||
// Then sync event
|
|
||||||
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
|
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
|
||||||
defer done()
|
defer done()
|
||||||
|
|
||||||
|
|||||||
@ -336,9 +336,6 @@ func TestBridge_SendInvite(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestBridge_SendAddTextBodyPartIfNotExists(t *testing.T) {
|
func TestBridge_SendAddTextBodyPartIfNotExists(t *testing.T) {
|
||||||
// NOTE: Prior to GODT-2887, these tests had inline images, however after the implementation to support
|
|
||||||
// inline images new parts are injected to reference inline images without content-id set. The images
|
|
||||||
// in this test have been changed to regular attachments to keep the original checks in place.
|
|
||||||
const messageMultipartWithoutText = `Content-Type: multipart/mixed;
|
const messageMultipartWithoutText = `Content-Type: multipart/mixed;
|
||||||
boundary="Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84"
|
boundary="Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84"
|
||||||
Subject: A new message
|
Subject: A new message
|
||||||
@ -346,7 +343,7 @@ Date: Mon, 13 Mar 2023 16:06:16 +0100
|
|||||||
|
|
||||||
|
|
||||||
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
|
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
|
||||||
Content-Disposition: attachment;
|
Content-Disposition: inline;
|
||||||
filename=Cat_August_2010-4.jpeg
|
filename=Cat_August_2010-4.jpeg
|
||||||
Content-Type: image/jpeg;
|
Content-Type: image/jpeg;
|
||||||
name="Cat_August_2010-4.jpeg"
|
name="Cat_August_2010-4.jpeg"
|
||||||
@ -363,7 +360,7 @@ Subject: A new message Part2
|
|||||||
Date: Mon, 13 Mar 2023 16:06:16 +0100
|
Date: Mon, 13 Mar 2023 16:06:16 +0100
|
||||||
|
|
||||||
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
|
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
|
||||||
Content-Disposition: attachment;
|
Content-Disposition: inline;
|
||||||
filename=Cat_August_2010-4.jpeg
|
filename=Cat_August_2010-4.jpeg
|
||||||
Content-Type: image/jpeg;
|
Content-Type: image/jpeg;
|
||||||
name="Cat_August_2010-4.jpeg"
|
name="Cat_August_2010-4.jpeg"
|
||||||
@ -470,9 +467,7 @@ SGVsbG8gd29ybGQK
|
|||||||
require.Eventually(t, func() bool {
|
require.Eventually(t, func() bool {
|
||||||
messages, err := clientFetch(senderIMAPClient, `Sent`, imap.FetchBodyStructure)
|
messages, err := clientFetch(senderIMAPClient, `Sent`, imap.FetchBodyStructure)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
if len(messages) != 4 {
|
require.Equal(t, 4, len(messages))
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// messages may not be in order
|
// messages may not be in order
|
||||||
for _, message := range messages {
|
for _, message := range messages {
|
||||||
@ -523,181 +518,3 @@ SGVsbG8gd29ybGQK
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBridge_SendInlineImage(t *testing.T) {
|
|
||||||
const messageInlineImageOnly = `Content-Type: multipart/mixed;
|
|
||||||
boundary="Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84"
|
|
||||||
Subject: A new message
|
|
||||||
Date: Mon, 13 Mar 2023 16:06:16 +0100
|
|
||||||
|
|
||||||
|
|
||||||
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
|
|
||||||
Content-Disposition: inline;
|
|
||||||
filename=Cat_August_2010-4.jpeg
|
|
||||||
Content-Type: image/jpeg;
|
|
||||||
name="Cat_August_2010-4.jpeg"
|
|
||||||
Content-Transfer-Encoding: base64
|
|
||||||
|
|
||||||
SGVsbG8gd29ybGQ=
|
|
||||||
|
|
||||||
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84--
|
|
||||||
`
|
|
||||||
|
|
||||||
const messageInlineImageWithHTML = `Content-Type: multipart/mixed;
|
|
||||||
boundary="Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84"
|
|
||||||
Subject: A new message Part2
|
|
||||||
Date: Mon, 13 Mar 2023 16:06:16 +0100
|
|
||||||
|
|
||||||
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
|
|
||||||
Content-Type: text/html;charset=utf8
|
|
||||||
Content-Transfer-Encoding: quoted-printable
|
|
||||||
|
|
||||||
Hello world
|
|
||||||
|
|
||||||
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
|
|
||||||
Content-Disposition: inline;
|
|
||||||
filename=Cat_August_2010-4.jpeg
|
|
||||||
Content-Type: image/jpeg;
|
|
||||||
name="Cat_August_2010-4.jpeg"
|
|
||||||
Content-Transfer-Encoding: base64
|
|
||||||
|
|
||||||
SGVsbG8gd29ybGQ=
|
|
||||||
|
|
||||||
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84--
|
|
||||||
`
|
|
||||||
|
|
||||||
const messageInlineImageWithText = `Content-Type: multipart/mixed;
|
|
||||||
boundary="Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84"
|
|
||||||
Subject: A new message Part3
|
|
||||||
Date: Mon, 13 Mar 2023 16:06:16 +0100
|
|
||||||
|
|
||||||
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
|
|
||||||
Content-Type: text/plain;charset=utf8
|
|
||||||
Content-Transfer-Encoding: quoted-printable
|
|
||||||
|
|
||||||
Hello world
|
|
||||||
|
|
||||||
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
|
|
||||||
Content-Disposition: inline;
|
|
||||||
filename=Cat_August_2010-4.jpeg
|
|
||||||
Content-Type: image/jpeg;
|
|
||||||
name="Cat_August_2010-4.jpeg"
|
|
||||||
Content-Transfer-Encoding: base64
|
|
||||||
|
|
||||||
SGVsbG8gd29ybGQ=
|
|
||||||
|
|
||||||
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84--
|
|
||||||
`
|
|
||||||
|
|
||||||
const messageInlineImageFollowedByText = `Content-Type: multipart/mixed;
|
|
||||||
boundary="Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84"
|
|
||||||
Subject: A new message Part4
|
|
||||||
Date: Mon, 13 Mar 2023 16:06:16 +0100
|
|
||||||
|
|
||||||
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
|
|
||||||
Content-Disposition: inline;
|
|
||||||
filename=Cat_August_2010-4.jpeg
|
|
||||||
Content-Type: image/jpeg;
|
|
||||||
name="Cat_August_2010-4.jpeg"
|
|
||||||
Content-Transfer-Encoding: base64
|
|
||||||
|
|
||||||
SGVsbG8gd29ybGQ=
|
|
||||||
|
|
||||||
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
|
|
||||||
Content-Type: text/plain;charset=utf8
|
|
||||||
Content-Transfer-Encoding: quoted-printable
|
|
||||||
|
|
||||||
Hello world
|
|
||||||
|
|
||||||
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84--
|
|
||||||
`
|
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
|
||||||
_, _, err := s.CreateUser("recipient", password)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
|
||||||
smtpWaiter := waitForSMTPServerReady(bridge)
|
|
||||||
defer smtpWaiter.Done()
|
|
||||||
|
|
||||||
senderUserID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
recipientUserID, err := bridge.LoginFull(ctx, "recipient", password, nil, nil)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
senderInfo, err := bridge.GetUserInfo(senderUserID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
recipientInfo, err := bridge.GetUserInfo(recipientUserID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
messages := []string{
|
|
||||||
messageInlineImageOnly,
|
|
||||||
messageInlineImageWithHTML,
|
|
||||||
messageInlineImageWithText,
|
|
||||||
messageInlineImageFollowedByText,
|
|
||||||
}
|
|
||||||
|
|
||||||
smtpWaiter.Wait()
|
|
||||||
|
|
||||||
for _, m := range messages {
|
|
||||||
// Dial the server.
|
|
||||||
client, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort())))
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer client.Close() //nolint:errcheck
|
|
||||||
|
|
||||||
// Upgrade to TLS.
|
|
||||||
require.NoError(t, client.StartTLS(&tls.Config{InsecureSkipVerify: true}))
|
|
||||||
|
|
||||||
// Authorize with SASL LOGIN.
|
|
||||||
require.NoError(t, client.Auth(sasl.NewLoginClient(
|
|
||||||
senderInfo.Addresses[0],
|
|
||||||
string(senderInfo.BridgePass)),
|
|
||||||
))
|
|
||||||
|
|
||||||
// Send the message.
|
|
||||||
require.NoError(t, client.SendMail(
|
|
||||||
senderInfo.Addresses[0],
|
|
||||||
[]string{recipientInfo.Addresses[0]},
|
|
||||||
strings.NewReader(m),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect the sender IMAP client.
|
|
||||||
senderIMAPClient, err := eventuallyDial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NoError(t, senderIMAPClient.Login(senderInfo.Addresses[0], string(senderInfo.BridgePass)))
|
|
||||||
defer senderIMAPClient.Logout() //nolint:errcheck
|
|
||||||
|
|
||||||
// Connect the recipient IMAP client.
|
|
||||||
recipientIMAPClient, err := eventuallyDial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NoError(t, recipientIMAPClient.Login(recipientInfo.Addresses[0], string(recipientInfo.BridgePass)))
|
|
||||||
defer recipientIMAPClient.Logout() //nolint:errcheck
|
|
||||||
|
|
||||||
require.Eventually(t, func() bool {
|
|
||||||
messages, err := clientFetch(senderIMAPClient, `Sent`, imap.FetchBodyStructure)
|
|
||||||
require.NoError(t, err)
|
|
||||||
if len(messages) != 4 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// messages may not be in order
|
|
||||||
for _, message := range messages {
|
|
||||||
require.Equal(t, 1, len(message.BodyStructure.Parts))
|
|
||||||
require.Equal(t, "multipart", message.BodyStructure.MIMEType)
|
|
||||||
require.Equal(t, "mixed", message.BodyStructure.MIMESubType)
|
|
||||||
require.Equal(t, "multipart", message.BodyStructure.Parts[0].MIMEType)
|
|
||||||
require.Equal(t, "related", message.BodyStructure.Parts[0].MIMESubType)
|
|
||||||
require.Len(t, message.BodyStructure.Parts[0].Parts, 2)
|
|
||||||
require.Equal(t, "text", message.BodyStructure.Parts[0].Parts[0].MIMEType)
|
|
||||||
require.Equal(t, "html", message.BodyStructure.Parts[0].Parts[0].MIMESubType)
|
|
||||||
require.Equal(t, "image", message.BodyStructure.Parts[0].Parts[1].MIMEType)
|
|
||||||
require.Equal(t, "jpeg", message.BodyStructure.Parts[0].Parts[1].MIMESubType)
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}, 10*time.Second, 100*time.Millisecond)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
696
internal/bridge/server_manager.go
Normal file
696
internal/bridge/server_manager.go
Normal file
@ -0,0 +1,696 @@
|
|||||||
|
// Copyright (c) 2023 Proton AG
|
||||||
|
//
|
||||||
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package bridge
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/gluon"
|
||||||
|
"github.com/ProtonMail/gluon/connector"
|
||||||
|
"github.com/ProtonMail/gluon/logging"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/pkg/cpc"
|
||||||
|
"github.com/emersion/go-smtp"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ServerManager manages the IMAP & SMTP servers and their listeners.
|
||||||
|
type ServerManager struct {
|
||||||
|
requests *cpc.CPC
|
||||||
|
|
||||||
|
imapServer *gluon.Server
|
||||||
|
imapListener net.Listener
|
||||||
|
|
||||||
|
smtpServer *smtp.Server
|
||||||
|
smtpListener net.Listener
|
||||||
|
|
||||||
|
loadedUserCount int
|
||||||
|
}
|
||||||
|
|
||||||
|
func newServerManager() *ServerManager {
|
||||||
|
return &ServerManager{
|
||||||
|
requests: cpc.NewCPC(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *ServerManager) Init(bridge *Bridge) error {
|
||||||
|
imapServer, err := createIMAPServer(bridge)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
smtpServer := createSMTPServer(bridge)
|
||||||
|
|
||||||
|
sm.imapServer = imapServer
|
||||||
|
sm.smtpServer = smtpServer
|
||||||
|
|
||||||
|
bridge.tasks.Once(func(ctx context.Context) {
|
||||||
|
logging.DoAnnotated(ctx, func(ctx context.Context) {
|
||||||
|
sm.run(ctx, bridge)
|
||||||
|
}, logging.Labels{
|
||||||
|
"service": "server-manager",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *ServerManager) CloseServers(ctx context.Context) error {
|
||||||
|
defer sm.requests.Close()
|
||||||
|
_, err := sm.requests.Send(ctx, &smRequestClose{})
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *ServerManager) RestartIMAP(ctx context.Context) error {
|
||||||
|
_, err := sm.requests.Send(ctx, &smRequestRestartIMAP{})
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *ServerManager) RestartSMTP(ctx context.Context) error {
|
||||||
|
_, err := sm.requests.Send(ctx, &smRequestRestartSMTP{})
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *ServerManager) AddIMAPUser(ctx context.Context, user *user.User) error {
|
||||||
|
_, err := sm.requests.Send(ctx, &smRequestAddIMAPUser{user: user})
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *ServerManager) RemoveIMAPUser(ctx context.Context, user *user.User, withData bool) error {
|
||||||
|
_, err := sm.requests.Send(ctx, &smRequestRemoveIMAPUser{
|
||||||
|
user: user,
|
||||||
|
withData: withData,
|
||||||
|
})
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *ServerManager) SetGluonDir(ctx context.Context, gluonDir string) error {
|
||||||
|
_, err := sm.requests.Send(ctx, &smRequestSetGluonDir{
|
||||||
|
dir: gluonDir,
|
||||||
|
})
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *ServerManager) AddGluonUser(ctx context.Context, conn connector.Connector, passphrase []byte) (string, error) {
|
||||||
|
reply, err := cpc.SendTyped[string](ctx, sm.requests, &smRequestAddGluonUser{
|
||||||
|
conn: conn,
|
||||||
|
passphrase: passphrase,
|
||||||
|
})
|
||||||
|
|
||||||
|
return reply, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *ServerManager) RemoveGluonUser(ctx context.Context, gluonID string) error {
|
||||||
|
_, err := sm.requests.Send(ctx, &smRequestRemoveGluonUser{
|
||||||
|
userID: gluonID,
|
||||||
|
})
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *ServerManager) run(ctx context.Context, bridge *Bridge) {
|
||||||
|
eventCh, cancel := bridge.GetEvents()
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
sm.handleClose(ctx, bridge)
|
||||||
|
return
|
||||||
|
|
||||||
|
case evt := <-eventCh:
|
||||||
|
switch evt.(type) {
|
||||||
|
case events.ConnStatusDown:
|
||||||
|
logrus.Info("Server Manager, network down stopping listeners")
|
||||||
|
if err := sm.closeSMTPServer(bridge); err != nil {
|
||||||
|
logrus.WithError(err).Error("Failed to close SMTP server")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := sm.stopIMAPListener(bridge); err != nil {
|
||||||
|
logrus.WithError(err)
|
||||||
|
}
|
||||||
|
case events.ConnStatusUp:
|
||||||
|
logrus.Info("Server Manager, network up starting listeners")
|
||||||
|
sm.handleLoadedUserCountChange(ctx, bridge)
|
||||||
|
}
|
||||||
|
|
||||||
|
case request, ok := <-sm.requests.ReceiveCh():
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch r := request.Value().(type) {
|
||||||
|
case *smRequestClose:
|
||||||
|
sm.handleClose(ctx, bridge)
|
||||||
|
request.Reply(ctx, nil, nil)
|
||||||
|
return
|
||||||
|
|
||||||
|
case *smRequestRestartSMTP:
|
||||||
|
err := sm.restartSMTP(bridge)
|
||||||
|
request.Reply(ctx, nil, err)
|
||||||
|
|
||||||
|
case *smRequestRestartIMAP:
|
||||||
|
err := sm.restartIMAP(ctx, bridge)
|
||||||
|
request.Reply(ctx, nil, err)
|
||||||
|
|
||||||
|
case *smRequestAddIMAPUser:
|
||||||
|
err := sm.handleAddIMAPUser(ctx, r.user)
|
||||||
|
request.Reply(ctx, nil, err)
|
||||||
|
if err == nil {
|
||||||
|
sm.loadedUserCount++
|
||||||
|
sm.handleLoadedUserCountChange(ctx, bridge)
|
||||||
|
}
|
||||||
|
|
||||||
|
case *smRequestRemoveIMAPUser:
|
||||||
|
err := sm.handleRemoveIMAPUser(ctx, r.user, r.withData)
|
||||||
|
request.Reply(ctx, nil, err)
|
||||||
|
if err == nil {
|
||||||
|
sm.loadedUserCount--
|
||||||
|
sm.handleLoadedUserCountChange(ctx, bridge)
|
||||||
|
}
|
||||||
|
|
||||||
|
case *smRequestSetGluonDir:
|
||||||
|
err := sm.handleSetGluonDir(ctx, bridge, r.dir)
|
||||||
|
request.Reply(ctx, nil, err)
|
||||||
|
|
||||||
|
case *smRequestAddGluonUser:
|
||||||
|
id, err := sm.handleAddGluonUser(ctx, r.conn, r.passphrase)
|
||||||
|
request.Reply(ctx, id, err)
|
||||||
|
|
||||||
|
case *smRequestRemoveGluonUser:
|
||||||
|
err := sm.handleRemoveGluonUser(ctx, r.userID)
|
||||||
|
request.Reply(ctx, nil, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *ServerManager) handleLoadedUserCountChange(ctx context.Context, bridge *Bridge) {
|
||||||
|
logrus.Infof("Validating Listener State %v", sm.loadedUserCount)
|
||||||
|
if sm.shouldStartServers() {
|
||||||
|
if sm.imapListener == nil {
|
||||||
|
if err := sm.serveIMAP(ctx, bridge); err != nil {
|
||||||
|
logrus.WithError(err).Error("Failed to start IMAP server")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if sm.smtpListener == nil {
|
||||||
|
if err := sm.restartSMTP(bridge); err != nil {
|
||||||
|
logrus.WithError(err).Error("Failed to start SMTP server")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if sm.imapListener != nil {
|
||||||
|
if err := sm.stopIMAPListener(bridge); err != nil {
|
||||||
|
logrus.WithError(err).Error("Failed to stop IMAP server")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if sm.smtpListener != nil {
|
||||||
|
if err := sm.closeSMTPServer(bridge); err != nil {
|
||||||
|
logrus.WithError(err).Error("Failed to stop SMTP server")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *ServerManager) handleClose(ctx context.Context, bridge *Bridge) {
|
||||||
|
// Close the IMAP server.
|
||||||
|
if err := sm.closeIMAPServer(ctx, bridge); err != nil {
|
||||||
|
logrus.WithError(err).Error("Failed to close IMAP server")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the SMTP server.
|
||||||
|
if err := sm.closeSMTPServer(bridge); err != nil {
|
||||||
|
logrus.WithError(err).Error("Failed to close SMTP server")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *ServerManager) handleAddIMAPUser(ctx context.Context, user *user.User) error {
|
||||||
|
if sm.imapServer == nil {
|
||||||
|
return fmt.Errorf("no imap server instance running")
|
||||||
|
}
|
||||||
|
|
||||||
|
imapConn, err := user.NewIMAPConnectors()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create IMAP connectors: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for addrID, imapConn := range imapConn {
|
||||||
|
log := logrus.WithFields(logrus.Fields{
|
||||||
|
"userID": user.ID(),
|
||||||
|
"addrID": addrID,
|
||||||
|
})
|
||||||
|
|
||||||
|
if gluonID, ok := user.GetGluonID(addrID); ok {
|
||||||
|
log.WithField("gluonID", gluonID).Info("Loading existing IMAP user")
|
||||||
|
|
||||||
|
// Load the user, checking whether the DB was newly created.
|
||||||
|
isNew, err := sm.imapServer.LoadUser(ctx, imapConn, gluonID, user.GluonKey())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load IMAP user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isNew {
|
||||||
|
// If the DB was newly created, clear the sync status; gluon's DB was not found.
|
||||||
|
logrus.Warn("IMAP user DB was newly created, clearing sync status")
|
||||||
|
|
||||||
|
// Remove the user from IMAP so we can clear the sync status.
|
||||||
|
if err := sm.imapServer.RemoveUser(ctx, gluonID, false); err != nil {
|
||||||
|
return fmt.Errorf("failed to remove IMAP user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the sync status -- we need to resync all messages.
|
||||||
|
if err := user.ClearSyncStatus(); err != nil {
|
||||||
|
return fmt.Errorf("failed to clear sync status: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the user back to the IMAP server.
|
||||||
|
if isNew, err := sm.imapServer.LoadUser(ctx, imapConn, gluonID, user.GluonKey()); err != nil {
|
||||||
|
return fmt.Errorf("failed to add IMAP user: %w", err)
|
||||||
|
} else if isNew {
|
||||||
|
panic("IMAP user should already have a database")
|
||||||
|
}
|
||||||
|
} else if status := user.GetSyncStatus(); !status.HasLabels {
|
||||||
|
// Otherwise, the DB already exists -- if the labels are not yet synced, we need to re-create the DB.
|
||||||
|
if err := sm.imapServer.RemoveUser(ctx, gluonID, true); err != nil {
|
||||||
|
return fmt.Errorf("failed to remove old IMAP user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := user.RemoveGluonID(addrID, gluonID); err != nil {
|
||||||
|
return fmt.Errorf("failed to remove old IMAP user ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gluonID, err := sm.imapServer.AddUser(ctx, imapConn, user.GluonKey())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to add IMAP user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := user.SetGluonID(addrID, gluonID); err != nil {
|
||||||
|
return fmt.Errorf("failed to set IMAP user ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.WithField("gluonID", gluonID).Info("Re-created IMAP user")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Info("Creating new IMAP user")
|
||||||
|
|
||||||
|
gluonID, err := sm.imapServer.AddUser(ctx, imapConn, user.GluonKey())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to add IMAP user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := user.SetGluonID(addrID, gluonID); err != nil {
|
||||||
|
return fmt.Errorf("failed to set IMAP user ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.WithField("gluonID", gluonID).Info("Created new IMAP user")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger a sync for the user, if needed.
|
||||||
|
user.TriggerSync()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *ServerManager) handleRemoveIMAPUser(ctx context.Context, user *user.User, withData bool) error {
|
||||||
|
if sm.imapServer == nil {
|
||||||
|
return fmt.Errorf("no imap server instance running")
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.WithFields(logrus.Fields{
|
||||||
|
"userID": user.ID(),
|
||||||
|
"withData": withData,
|
||||||
|
}).Debug("Removing IMAP user")
|
||||||
|
|
||||||
|
for addrID, gluonID := range user.GetGluonIDs() {
|
||||||
|
if err := sm.imapServer.RemoveUser(ctx, gluonID, withData); err != nil {
|
||||||
|
return fmt.Errorf("failed to remove IMAP user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if withData {
|
||||||
|
if err := user.RemoveGluonID(addrID, gluonID); err != nil {
|
||||||
|
return fmt.Errorf("failed to remove IMAP user ID: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createIMAPServer(bridge *Bridge) (*gluon.Server, error) {
|
||||||
|
gluonDataDir, err := bridge.GetGluonDataDir()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get Gluon Database directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return newIMAPServer(
|
||||||
|
bridge.vault.GetGluonCacheDir(),
|
||||||
|
gluonDataDir,
|
||||||
|
bridge.curVersion,
|
||||||
|
bridge.tlsConfig,
|
||||||
|
bridge.reporter,
|
||||||
|
bridge.logIMAPClient,
|
||||||
|
bridge.logIMAPServer,
|
||||||
|
bridge.imapEventCh,
|
||||||
|
bridge.tasks,
|
||||||
|
bridge.uidValidityGenerator,
|
||||||
|
bridge.panicHandler,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createSMTPServer(bridge *Bridge) *smtp.Server {
|
||||||
|
return newSMTPServer(bridge, bridge.tlsConfig, bridge.logSMTP)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *ServerManager) closeSMTPServer(bridge *Bridge) error {
|
||||||
|
// We close the listener ourselves even though it's also closed by smtpServer.Close().
|
||||||
|
// This is because smtpServer.Serve() is called in a separate goroutine and might be executed
|
||||||
|
// after we've already closed the server. However, go-smtp has a bug; it blocks on the listener
|
||||||
|
// even after the server has been closed. So we close the listener ourselves to unblock it.
|
||||||
|
|
||||||
|
if sm.smtpListener != nil {
|
||||||
|
logrus.Info("Closing SMTP Listener")
|
||||||
|
if err := sm.smtpListener.Close(); err != nil {
|
||||||
|
return fmt.Errorf("failed to close SMTP listener: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sm.smtpListener = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if sm.smtpServer != nil {
|
||||||
|
logrus.Info("Closing SMTP server")
|
||||||
|
if err := sm.smtpServer.Close(); err != nil {
|
||||||
|
logrus.WithError(err).Debug("Failed to close SMTP server (expected -- we close the listener ourselves)")
|
||||||
|
}
|
||||||
|
|
||||||
|
sm.smtpServer = nil
|
||||||
|
|
||||||
|
bridge.publish(events.SMTPServerStopped{})
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *ServerManager) closeIMAPServer(ctx context.Context, bridge *Bridge) error {
|
||||||
|
if sm.imapListener != nil {
|
||||||
|
logrus.Info("Closing IMAP Listener")
|
||||||
|
|
||||||
|
if err := sm.imapListener.Close(); err != nil {
|
||||||
|
return fmt.Errorf("failed to close IMAP listener: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sm.imapListener = nil
|
||||||
|
|
||||||
|
bridge.publish(events.IMAPServerStopped{})
|
||||||
|
}
|
||||||
|
|
||||||
|
if sm.imapServer != nil {
|
||||||
|
logrus.Info("Closing IMAP server")
|
||||||
|
if err := sm.imapServer.Close(ctx); err != nil {
|
||||||
|
return fmt.Errorf("failed to close IMAP server: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sm.imapServer = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *ServerManager) restartIMAP(ctx context.Context, bridge *Bridge) error {
|
||||||
|
logrus.Info("Restarting IMAP server")
|
||||||
|
|
||||||
|
if sm.imapListener != nil {
|
||||||
|
if err := sm.imapListener.Close(); err != nil {
|
||||||
|
return fmt.Errorf("failed to close IMAP listener: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sm.imapListener = nil
|
||||||
|
|
||||||
|
bridge.publish(events.IMAPServerStopped{})
|
||||||
|
}
|
||||||
|
|
||||||
|
if sm.shouldStartServers() {
|
||||||
|
return sm.serveIMAP(ctx, bridge)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *ServerManager) restartSMTP(bridge *Bridge) error {
|
||||||
|
logrus.Info("Restarting SMTP server")
|
||||||
|
|
||||||
|
if err := sm.closeSMTPServer(bridge); err != nil {
|
||||||
|
return fmt.Errorf("failed to close SMTP: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bridge.publish(events.SMTPServerStopped{})
|
||||||
|
|
||||||
|
sm.smtpServer = newSMTPServer(bridge, bridge.tlsConfig, bridge.logSMTP)
|
||||||
|
|
||||||
|
if sm.shouldStartServers() {
|
||||||
|
return sm.serveSMTP(bridge)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *ServerManager) serveSMTP(bridge *Bridge) error {
|
||||||
|
port, err := func() (int, error) {
|
||||||
|
logrus.WithFields(logrus.Fields{
|
||||||
|
"port": bridge.vault.GetSMTPPort(),
|
||||||
|
"ssl": bridge.vault.GetSMTPSSL(),
|
||||||
|
}).Info("Starting SMTP server")
|
||||||
|
|
||||||
|
smtpListener, err := newListener(bridge.vault.GetSMTPPort(), bridge.vault.GetSMTPSSL(), bridge.tlsConfig)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("failed to create SMTP listener: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sm.smtpListener = smtpListener
|
||||||
|
|
||||||
|
bridge.tasks.Once(func(context.Context) {
|
||||||
|
if err := sm.smtpServer.Serve(smtpListener); err != nil {
|
||||||
|
logrus.WithError(err).Info("SMTP server stopped")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := bridge.vault.SetSMTPPort(getPort(smtpListener.Addr())); err != nil {
|
||||||
|
return 0, fmt.Errorf("failed to store SMTP port in vault: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return getPort(smtpListener.Addr()), nil
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
bridge.publish(events.SMTPServerError{
|
||||||
|
Error: err,
|
||||||
|
})
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
bridge.publish(events.SMTPServerReady{
|
||||||
|
Port: port,
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *ServerManager) serveIMAP(ctx context.Context, bridge *Bridge) error {
|
||||||
|
port, err := func() (int, error) {
|
||||||
|
if sm.imapServer == nil {
|
||||||
|
return 0, fmt.Errorf("no IMAP server instance running")
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.WithFields(logrus.Fields{
|
||||||
|
"port": bridge.vault.GetIMAPPort(),
|
||||||
|
"ssl": bridge.vault.GetIMAPSSL(),
|
||||||
|
}).Info("Starting IMAP server")
|
||||||
|
|
||||||
|
imapListener, err := newListener(bridge.vault.GetIMAPPort(), bridge.vault.GetIMAPSSL(), bridge.tlsConfig)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("failed to create IMAP listener: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sm.imapListener = imapListener
|
||||||
|
|
||||||
|
if err := sm.imapServer.Serve(ctx, sm.imapListener); err != nil {
|
||||||
|
return 0, fmt.Errorf("failed to serve IMAP: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bridge.vault.SetIMAPPort(getPort(imapListener.Addr())); err != nil {
|
||||||
|
return 0, fmt.Errorf("failed to store IMAP port in vault: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return getPort(imapListener.Addr()), nil
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
bridge.publish(events.IMAPServerError{
|
||||||
|
Error: err,
|
||||||
|
})
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
bridge.publish(events.IMAPServerReady{
|
||||||
|
Port: port,
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *ServerManager) stopIMAPListener(bridge *Bridge) error {
|
||||||
|
logrus.Info("Stopping IMAP listener")
|
||||||
|
if sm.imapListener != nil {
|
||||||
|
if err := sm.imapListener.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
sm.imapListener = nil
|
||||||
|
|
||||||
|
bridge.publish(events.IMAPServerStopped{})
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *ServerManager) handleSetGluonDir(ctx context.Context, bridge *Bridge, newGluonDir string) error {
|
||||||
|
return safe.RLockRet(func() error {
|
||||||
|
currentGluonDir := bridge.GetGluonCacheDir()
|
||||||
|
newGluonDir = filepath.Join(newGluonDir, "gluon")
|
||||||
|
if newGluonDir == currentGluonDir {
|
||||||
|
return fmt.Errorf("new gluon dir is the same as the old one")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := sm.closeIMAPServer(context.Background(), bridge); err != nil {
|
||||||
|
return fmt.Errorf("failed to close IMAP: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sm.loadedUserCount = 0
|
||||||
|
|
||||||
|
if err := bridge.moveGluonCacheDir(currentGluonDir, newGluonDir); err != nil {
|
||||||
|
logrus.WithError(err).Error("failed to move GluonCacheDir")
|
||||||
|
|
||||||
|
if err := bridge.vault.SetGluonDir(currentGluonDir); err != nil {
|
||||||
|
return fmt.Errorf("failed to revert GluonCacheDir: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bridge.heartbeat.SetCacheLocation(newGluonDir)
|
||||||
|
|
||||||
|
gluonDataDir, err := bridge.GetGluonDataDir()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get Gluon Database directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
imapServer, err := newIMAPServer(
|
||||||
|
bridge.vault.GetGluonCacheDir(),
|
||||||
|
gluonDataDir,
|
||||||
|
bridge.curVersion,
|
||||||
|
bridge.tlsConfig,
|
||||||
|
bridge.reporter,
|
||||||
|
bridge.logIMAPClient,
|
||||||
|
bridge.logIMAPServer,
|
||||||
|
bridge.imapEventCh,
|
||||||
|
bridge.tasks,
|
||||||
|
bridge.uidValidityGenerator,
|
||||||
|
bridge.panicHandler,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create new IMAP server: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sm.imapServer = imapServer
|
||||||
|
for _, bridgeUser := range bridge.users {
|
||||||
|
if err := sm.handleAddIMAPUser(ctx, bridgeUser); err != nil {
|
||||||
|
return fmt.Errorf("failed to add users to new IMAP server: %w", err)
|
||||||
|
}
|
||||||
|
sm.loadedUserCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
if sm.shouldStartServers() {
|
||||||
|
if err := sm.serveIMAP(ctx, bridge); err != nil {
|
||||||
|
return fmt.Errorf("failed to serve IMAP: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}, bridge.usersLock)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *ServerManager) handleAddGluonUser(ctx context.Context, conn connector.Connector, passphrase []byte) (string, error) {
|
||||||
|
if sm.imapServer == nil {
|
||||||
|
return "", fmt.Errorf("no imap server instance running")
|
||||||
|
}
|
||||||
|
|
||||||
|
return sm.imapServer.AddUser(ctx, conn, passphrase)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *ServerManager) handleRemoveGluonUser(ctx context.Context, userID string) error {
|
||||||
|
if sm.imapServer == nil {
|
||||||
|
return fmt.Errorf("no imap server instance running")
|
||||||
|
}
|
||||||
|
|
||||||
|
return sm.imapServer.RemoveUser(ctx, userID, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *ServerManager) shouldStartServers() bool {
|
||||||
|
return sm.loadedUserCount >= 1
|
||||||
|
}
|
||||||
|
|
||||||
|
type smRequestClose struct{}
|
||||||
|
|
||||||
|
type smRequestRestartIMAP struct{}
|
||||||
|
|
||||||
|
type smRequestRestartSMTP struct{}
|
||||||
|
|
||||||
|
type smRequestAddIMAPUser struct {
|
||||||
|
user *user.User
|
||||||
|
}
|
||||||
|
|
||||||
|
type smRequestRemoveIMAPUser struct {
|
||||||
|
user *user.User
|
||||||
|
withData bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type smRequestSetGluonDir struct {
|
||||||
|
dir string
|
||||||
|
}
|
||||||
|
|
||||||
|
type smRequestAddGluonUser struct {
|
||||||
|
conn connector.Connector
|
||||||
|
passphrase []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type smRequestRemoveGluonUser struct {
|
||||||
|
userID string
|
||||||
|
}
|
||||||
@ -20,10 +20,11 @@ package bridge
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/Masterminds/semver/v3"
|
"github.com/Masterminds/semver/v3"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/userevents"
|
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
@ -130,41 +131,26 @@ func (bridge *Bridge) GetGluonDataDir() (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) SetGluonDir(ctx context.Context, newGluonDir string) error {
|
func (bridge *Bridge) SetGluonDir(ctx context.Context, newGluonDir string) error {
|
||||||
bridge.usersLock.RLock()
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
logrus.Info("Restarting user event loops")
|
|
||||||
for _, u := range bridge.users {
|
|
||||||
u.ResumeEventLoop()
|
|
||||||
}
|
|
||||||
|
|
||||||
bridge.usersLock.RUnlock()
|
|
||||||
}()
|
|
||||||
|
|
||||||
type waiter struct {
|
|
||||||
w *userevents.EventPollWaiter
|
|
||||||
id string
|
|
||||||
}
|
|
||||||
|
|
||||||
waiters := make([]waiter, 0, len(bridge.users))
|
|
||||||
|
|
||||||
logrus.Info("Pausing user event loops for gluon dir change")
|
|
||||||
for id, u := range bridge.users {
|
|
||||||
waiters = append(waiters, waiter{w: u.PauseEventLoopWithWaiter(), id: id})
|
|
||||||
}
|
|
||||||
|
|
||||||
logrus.Info("Waiting on user event loop completion")
|
|
||||||
for _, waiter := range waiters {
|
|
||||||
if err := waiter.w.WaitPollFinished(ctx); err != nil {
|
|
||||||
logrus.WithError(err).Errorf("Failed to wait on event loop pause for user %v", waiter.id)
|
|
||||||
return fmt.Errorf("failed on event loop pause: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logrus.Info("Changing gluon directory")
|
|
||||||
return bridge.serverManager.SetGluonDir(ctx, newGluonDir)
|
return bridge.serverManager.SetGluonDir(ctx, newGluonDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) moveGluonCacheDir(oldGluonDir, newGluonDir string) error {
|
||||||
|
logrus.Infof("gluon cache moving from %s to %s", oldGluonDir, newGluonDir)
|
||||||
|
oldCacheDir := ApplyGluonCachePathSuffix(oldGluonDir)
|
||||||
|
if err := copyDir(oldCacheDir, ApplyGluonCachePathSuffix(newGluonDir)); err != nil {
|
||||||
|
return fmt.Errorf("failed to copy gluon dir: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bridge.vault.SetGluonDir(newGluonDir); err != nil {
|
||||||
|
return fmt.Errorf("failed to set new gluon cache dir: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.RemoveAll(oldCacheDir); err != nil {
|
||||||
|
logrus.WithError(err).Error("failed to remove old gluon cache dir")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) GetProxyAllowed() bool {
|
func (bridge *Bridge) GetProxyAllowed() bool {
|
||||||
return bridge.vault.GetProxyAllowed()
|
return bridge.vault.GetProxyAllowed()
|
||||||
}
|
}
|
||||||
@ -332,3 +318,16 @@ func (bridge *Bridge) FactoryReset(ctx context.Context) {
|
|||||||
logrus.WithError(err).Error("Failed to clear data paths")
|
logrus.WithError(err).Error("Failed to clear data paths")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getPort(addr net.Addr) int {
|
||||||
|
switch addr := addr.(type) {
|
||||||
|
case *net.TCPAddr:
|
||||||
|
return addr.Port
|
||||||
|
|
||||||
|
case *net.UDPAddr:
|
||||||
|
return addr.Port
|
||||||
|
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -25,7 +25,6 @@ import (
|
|||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
"github.com/ProtonMail/go-proton-api/server"
|
"github.com/ProtonMail/go-proton-api/server"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -52,45 +51,6 @@ func TestBridge_Settings_GluonDir(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBridge_Settings_GluonDirWithOnGoingEvents(t *testing.T) {
|
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
|
||||||
userID, addrID, err := s.CreateUser("imap", password)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
|
||||||
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
|
||||||
defer done()
|
|
||||||
|
|
||||||
_, err := bridge.LoginFull(context.Background(), "imap", password, nil, nil)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
<-syncCh
|
|
||||||
})
|
|
||||||
|
|
||||||
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
|
|
||||||
createNumMessages(ctx, t, c, addrID, labelID, 200)
|
|
||||||
})
|
|
||||||
|
|
||||||
withBridgeWaitForServers(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
|
||||||
// Create a new location for the Gluon data.
|
|
||||||
newGluonDir := t.TempDir()
|
|
||||||
|
|
||||||
// Move the gluon dir; it should also move the user's data.
|
|
||||||
require.NoError(t, bridge.SetGluonDir(context.Background(), newGluonDir))
|
|
||||||
|
|
||||||
// Check that the new directory is not empty.
|
|
||||||
entries, err := os.ReadDir(newGluonDir)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// There should be at least one entry.
|
|
||||||
require.NotEmpty(t, entries)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBridge_Settings_IMAPPort(t *testing.T) {
|
func TestBridge_Settings_IMAPPort(t *testing.T) {
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
|
|||||||
@ -21,37 +21,43 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/identifier"
|
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
||||||
|
"github.com/emersion/go-sasl"
|
||||||
|
"github.com/emersion/go-smtp"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (bridge *Bridge) restartSMTP(ctx context.Context) error {
|
func (bridge *Bridge) restartSMTP(ctx context.Context) error {
|
||||||
return bridge.serverManager.RestartSMTP(ctx)
|
return bridge.serverManager.RestartSMTP(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
type bridgeSMTPSettings struct {
|
func newSMTPServer(bridge *Bridge, tlsConfig *tls.Config, logSMTP bool) *smtp.Server {
|
||||||
b *Bridge
|
logrus.WithField("logSMTP", logSMTP).Info("Creating SMTP server")
|
||||||
}
|
|
||||||
|
|
||||||
func (b *bridgeSMTPSettings) TLSConfig() *tls.Config {
|
smtpServer := smtp.NewServer(&smtpBackend{Bridge: bridge})
|
||||||
return b.b.tlsConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *bridgeSMTPSettings) Log() bool {
|
smtpServer.TLSConfig = tlsConfig
|
||||||
return b.b.logSMTP
|
smtpServer.Domain = constants.Host
|
||||||
}
|
smtpServer.AllowInsecureAuth = true
|
||||||
|
smtpServer.MaxLineLength = 1 << 16
|
||||||
|
smtpServer.ErrorLog = logging.NewSMTPLogger()
|
||||||
|
|
||||||
func (b *bridgeSMTPSettings) Port() int {
|
// go-smtp suppors SASL PLAIN but not LOGIN. We need to add LOGIN support ourselves.
|
||||||
return b.b.vault.GetSMTPPort()
|
smtpServer.EnableAuth(sasl.Login, func(conn *smtp.Conn) sasl.Server {
|
||||||
}
|
return sasl.NewLoginServer(func(username, password string) error {
|
||||||
|
return conn.Session().AuthPlain(username, password)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
func (b *bridgeSMTPSettings) SetPort(i int) error {
|
if logSMTP {
|
||||||
return b.b.vault.SetSMTPPort(i)
|
log := logrus.WithField("protocol", "SMTP")
|
||||||
}
|
log.Warning("================================================")
|
||||||
|
log.Warning("THIS LOG WILL CONTAIN **DECRYPTED** MESSAGE DATA")
|
||||||
|
log.Warning("================================================")
|
||||||
|
|
||||||
func (b *bridgeSMTPSettings) UseSSL() bool {
|
smtpServer.Debug = logging.NewSMTPDebugLogger()
|
||||||
return b.b.vault.GetSMTPSSL()
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func (b *bridgeSMTPSettings) Identifier() identifier.UserAgentUpdater {
|
return smtpServer
|
||||||
return &bridgeUserAgentUpdater{Bridge: b.b}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,36 +15,26 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package smtp
|
package bridge
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/identifier"
|
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
|
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
|
||||||
"github.com/emersion/go-smtp"
|
"github.com/emersion/go-smtp"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Backend struct {
|
type smtpBackend struct {
|
||||||
accounts *Accounts
|
*Bridge
|
||||||
userAgent identifier.UserAgentUpdater
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewBackend(accounts *Accounts, userAgent identifier.UserAgentUpdater) *Backend {
|
|
||||||
return &Backend{
|
|
||||||
accounts: accounts,
|
|
||||||
userAgent: userAgent,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type smtpSession struct {
|
type smtpSession struct {
|
||||||
accounts *Accounts
|
*Bridge
|
||||||
userAgent identifier.UserAgentUpdater
|
|
||||||
|
|
||||||
userID string
|
userID string
|
||||||
authID string
|
authID string
|
||||||
@ -53,32 +43,45 @@ type smtpSession struct {
|
|||||||
to []string
|
to []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (be *Backend) NewSession(*smtp.Conn) (smtp.Session, error) {
|
func (be *smtpBackend) NewSession(*smtp.Conn) (smtp.Session, error) {
|
||||||
return &smtpSession{accounts: be.accounts, userAgent: be.userAgent}, nil
|
return &smtpSession{Bridge: be.Bridge}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *smtpSession) AuthPlain(username, password string) error {
|
func (s *smtpSession) AuthPlain(username, password string) error {
|
||||||
userID, authID, err := s.accounts.CheckAuth(username, []byte(password))
|
return safe.RLockRet(func() error {
|
||||||
if err != nil {
|
for _, user := range s.users {
|
||||||
if !errors.Is(err, ErrNoSuchUser) {
|
addrID, err := user.CheckAuth(username, []byte(password))
|
||||||
return fmt.Errorf("unknown error")
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
s.userID = user.ID()
|
||||||
|
s.authID = addrID
|
||||||
|
|
||||||
|
if strings.Contains(s.Bridge.GetCurrentUserAgent(), useragent.DefaultUserAgent) {
|
||||||
|
s.Bridge.setUserAgent(useragent.UnknownClient, useragent.DefaultVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
user.SendConfigStatusSuccess(context.Background())
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.WithFields(logrus.Fields{
|
logrus.WithFields(logrus.Fields{
|
||||||
"username": username,
|
"username": username,
|
||||||
"pkg": "smtp",
|
"pkg": "smtp",
|
||||||
}).Error("Incorrect login credentials.")
|
}).Error("Incorrect login credentials.")
|
||||||
|
err := fmt.Errorf("invalid username or password")
|
||||||
return fmt.Errorf("invalid username or password")
|
for _, user := range s.users {
|
||||||
}
|
for _, mail := range user.Emails() {
|
||||||
|
if mail == username {
|
||||||
s.userID = userID
|
user.ReportConfigStatusFailure(err.Error())
|
||||||
s.authID = authID
|
return err
|
||||||
|
}
|
||||||
if strings.Contains(s.userAgent.GetUserAgent(), useragent.DefaultUserAgent) {
|
}
|
||||||
s.userAgent.SetUserAgent(useragent.UnknownClient, useragent.DefaultVersion)
|
}
|
||||||
}
|
return err
|
||||||
|
}, s.usersLock)
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *smtpSession) Reset() {
|
func (s *smtpSession) Reset() {
|
||||||
@ -105,7 +108,14 @@ func (s *smtpSession) Rcpt(to string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *smtpSession) Data(r io.Reader) error {
|
func (s *smtpSession) Data(r io.Reader) error {
|
||||||
err := s.accounts.SendMail(context.Background(), s.userID, s.authID, s.from, s.to, r)
|
err := safe.RLockRet(func() error {
|
||||||
|
user, ok := s.users[s.userID]
|
||||||
|
if !ok {
|
||||||
|
return ErrNoSuchUser
|
||||||
|
}
|
||||||
|
|
||||||
|
return user.SendMail(s.authID, s.from, s.to, r)
|
||||||
|
}, s.usersLock)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.WithField("pkg", "smtp").WithError(err).Error("Send mail failed.")
|
logrus.WithField("pkg", "smtp").WithError(err).Error("Send mail failed.")
|
||||||
@ -21,11 +21,9 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@ -37,7 +35,6 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice"
|
|
||||||
"github.com/bradenaw/juniper/iterator"
|
"github.com/bradenaw/juniper/iterator"
|
||||||
"github.com/bradenaw/juniper/stream"
|
"github.com/bradenaw/juniper/stream"
|
||||||
"github.com/bradenaw/juniper/xslices"
|
"github.com/bradenaw/juniper/xslices"
|
||||||
@ -255,17 +252,14 @@ func TestBridge_SyncWithOngoingEvents(t *testing.T) {
|
|||||||
|
|
||||||
// Login the user; its sync should fail.
|
// Login the user; its sync should fail.
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
|
|
||||||
defer done()
|
|
||||||
|
|
||||||
{
|
{
|
||||||
syncFailedCh, syncFailedDone := chToType[events.Event, events.SyncFailed](b.GetEvents(events.SyncFailed{}))
|
syncCh, done := chToType[events.Event, events.SyncFailed](b.GetEvents(events.SyncFailed{}))
|
||||||
defer syncFailedDone()
|
defer done()
|
||||||
|
|
||||||
userID, err := b.LoginFull(ctx, "imap", password, nil, nil)
|
userID, err := b.LoginFull(ctx, "imap", password, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
require.Equal(t, userID, (<-syncFailedCh).UserID)
|
require.Equal(t, userID, (<-syncCh).UserID)
|
||||||
|
|
||||||
info, err := b.GetUserInfo(userID)
|
info, err := b.GetUserInfo(userID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -288,7 +282,11 @@ func TestBridge_SyncWithOngoingEvents(t *testing.T) {
|
|||||||
|
|
||||||
// Remove the network limit, allowing the sync to finish.
|
// Remove the network limit, allowing the sync to finish.
|
||||||
netCtl.SetReadLimit(0)
|
netCtl.SetReadLimit(0)
|
||||||
|
|
||||||
{
|
{
|
||||||
|
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
|
||||||
|
defer done()
|
||||||
|
|
||||||
require.Equal(t, userID, (<-syncCh).UserID)
|
require.Equal(t, userID, (<-syncCh).UserID)
|
||||||
|
|
||||||
info, err := b.GetUserInfo(userID)
|
info, err := b.GetUserInfo(userID)
|
||||||
@ -300,6 +298,12 @@ func TestBridge_SyncWithOngoingEvents(t *testing.T) {
|
|||||||
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
||||||
defer func() { _ = client.Logout() }()
|
defer func() { _ = client.Logout() }()
|
||||||
|
|
||||||
|
status, err := client.Select(`Folders/folder`, false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
// Original folder should have more than 0 messages and less than the total.
|
||||||
|
require.Greater(t, status.Messages, uint32(0))
|
||||||
|
require.Less(t, status.Messages, uint32(numMsg))
|
||||||
|
|
||||||
// Check that the new messages arrive in the right location.
|
// Check that the new messages arrive in the right location.
|
||||||
require.Eventually(t, func() bool {
|
require.Eventually(t, func() bool {
|
||||||
status, err := client.Select(`Folders/folder2`, true)
|
status, err := client.Select(`Folders/folder2`, true)
|
||||||
@ -317,330 +321,6 @@ func TestBridge_SyncWithOngoingEvents(t *testing.T) {
|
|||||||
}, server.WithTLS(false))
|
}, server.WithTLS(false))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBridge_CanProcessEventsDuringSync(t *testing.T) {
|
|
||||||
numMsg := 1 << 8
|
|
||||||
|
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
|
||||||
userID, addrID, err := s.CreateUser("imap", password)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
|
|
||||||
createNumMessages(ctx, t, c, addrID, labelID, numMsg)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Simulate 429 to prevent sync from progressing.
|
|
||||||
s.AddStatusHook(func(request *http.Request) (int, bool) {
|
|
||||||
if strings.Contains(request.URL.Path, "/mail/v4/messages/") {
|
|
||||||
return http.StatusTooManyRequests, true
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0, false
|
|
||||||
})
|
|
||||||
|
|
||||||
// The initial user should be fully synced.
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
|
||||||
syncStartedCh, syncStartedDone := chToType[events.Event, events.SyncStarted](bridge.GetEvents(events.SyncStarted{}))
|
|
||||||
defer syncStartedDone()
|
|
||||||
|
|
||||||
addressCreatedCh, addressCreatedDone := chToType[events.Event, events.UserAddressCreated](bridge.GetEvents(events.UserAddressCreated{}))
|
|
||||||
defer addressCreatedDone()
|
|
||||||
|
|
||||||
userID, err := bridge.LoginFull(ctx, "imap", password, nil, nil)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.Equal(t, userID, (<-syncStartedCh).UserID)
|
|
||||||
|
|
||||||
// Create a new address
|
|
||||||
newAddress := "foo@proton.ch"
|
|
||||||
addrID, err := s.CreateAddress(userID, newAddress, password)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
event := <-addressCreatedCh
|
|
||||||
require.Equal(t, userID, event.UserID)
|
|
||||||
require.Equal(t, newAddress, event.Email)
|
|
||||||
require.Equal(t, addrID, event.AddressID)
|
|
||||||
})
|
|
||||||
}, server.WithTLS(false))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBridge_RefreshDuringSyncRestartSync(t *testing.T) {
|
|
||||||
numMsg := 1 << 8
|
|
||||||
|
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
|
||||||
userID, addrID, err := s.CreateUser("imap", password)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
|
|
||||||
createNumMessages(ctx, t, c, addrID, labelID, numMsg)
|
|
||||||
})
|
|
||||||
|
|
||||||
var refreshPerformed atomic.Bool
|
|
||||||
refreshPerformed.Store(false)
|
|
||||||
|
|
||||||
// Simulate 429 to prevent sync from progressing.
|
|
||||||
s.AddStatusHook(func(request *http.Request) (int, bool) {
|
|
||||||
if strings.Contains(request.URL.Path, "/mail/v4/messages/") {
|
|
||||||
if !refreshPerformed.Load() {
|
|
||||||
return http.StatusTooManyRequests, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0, false
|
|
||||||
})
|
|
||||||
|
|
||||||
// The initial user should be fully synced.
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
|
||||||
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
|
||||||
defer done()
|
|
||||||
|
|
||||||
userID, err := bridge.LoginFull(ctx, "imap", password, nil, nil)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
syncStartedCh, syncStartedDone := chToType[events.Event, events.SyncStarted](bridge.GetEvents(events.SyncStarted{}))
|
|
||||||
defer syncStartedDone()
|
|
||||||
|
|
||||||
require.Equal(t, userID, (<-syncStartedCh).UserID)
|
|
||||||
|
|
||||||
require.NoError(t, err, s.RefreshUser(userID, proton.RefreshMail))
|
|
||||||
require.Equal(t, userID, (<-syncStartedCh).UserID)
|
|
||||||
refreshPerformed.Store(true)
|
|
||||||
|
|
||||||
require.Equal(t, userID, (<-syncCh).UserID)
|
|
||||||
})
|
|
||||||
}, server.WithTLS(false))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBridge_EventReplayAfterSyncHasFinished(t *testing.T) {
|
|
||||||
numMsg := 1 << 8
|
|
||||||
|
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
|
||||||
userID, addrID, err := s.CreateUser("imap", password)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
|
|
||||||
createNumMessages(ctx, t, c, addrID, labelID, numMsg)
|
|
||||||
})
|
|
||||||
|
|
||||||
addrID1, err := s.CreateAddress(userID, "foo@proton.ch", password)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
var allowSyncToProgress atomic.Bool
|
|
||||||
allowSyncToProgress.Store(false)
|
|
||||||
|
|
||||||
// Simulate 429 to prevent sync from progressing.
|
|
||||||
s.AddStatusHook(func(request *http.Request) (int, bool) {
|
|
||||||
if request.Method == "GET" && strings.Contains(request.URL.Path, "/mail/v4/messages/") {
|
|
||||||
if !allowSyncToProgress.Load() {
|
|
||||||
return http.StatusTooManyRequests, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0, false
|
|
||||||
})
|
|
||||||
|
|
||||||
// The initial user should be fully synced.
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
|
||||||
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
|
||||||
defer done()
|
|
||||||
|
|
||||||
syncStartedCh, syncStartedDone := chToType[events.Event, events.SyncStarted](bridge.GetEvents(events.SyncStarted{}))
|
|
||||||
defer syncStartedDone()
|
|
||||||
|
|
||||||
addressCreatedCh, addressCreatedDone := chToType[events.Event, events.UserAddressCreated](bridge.GetEvents(events.UserAddressCreated{}))
|
|
||||||
defer addressCreatedDone()
|
|
||||||
|
|
||||||
userID, err := bridge.LoginFull(ctx, "imap", password, nil, nil)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.Equal(t, userID, (<-syncStartedCh).UserID)
|
|
||||||
|
|
||||||
// create 20 more messages and move them to inbox
|
|
||||||
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
|
|
||||||
createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 20)
|
|
||||||
})
|
|
||||||
|
|
||||||
// User AddrID2 event as a check point to see when the new address was created.
|
|
||||||
addrID2, err := s.CreateAddress(userID, "bar@proton.ch", password)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
allowSyncToProgress.Store(true)
|
|
||||||
require.Equal(t, userID, (<-syncCh).UserID)
|
|
||||||
|
|
||||||
// At most two events can be published, one for the first address, then for the second.
|
|
||||||
// if the second event is not `addrID2` then something went wrong.
|
|
||||||
event := <-addressCreatedCh
|
|
||||||
if event.AddressID == addrID1 {
|
|
||||||
event = <-addressCreatedCh
|
|
||||||
}
|
|
||||||
|
|
||||||
require.Equal(t, addrID2, event.AddressID)
|
|
||||||
|
|
||||||
info, err := bridge.GetUserInfo(userID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
|
||||||
defer func() { _ = client.Logout() }()
|
|
||||||
|
|
||||||
// Finally check if the 20 messages are in INBOX.
|
|
||||||
status, err := client.Status("INBOX", []imap.StatusItem{imap.StatusMessages})
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, uint32(20), status.Messages)
|
|
||||||
|
|
||||||
// Finally check if the numMsg are in the folder.
|
|
||||||
status, err = client.Status("Folders/folder", []imap.StatusItem{imap.StatusMessages})
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, uint32(numMsg), status.Messages)
|
|
||||||
})
|
|
||||||
}, server.WithTLS(false))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBridge_MessageCreateDuringSync(t *testing.T) {
|
|
||||||
numMsg := 1 << 8
|
|
||||||
|
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
|
||||||
userID, addrID, err := s.CreateUser("imap", password)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
|
|
||||||
createNumMessages(ctx, t, c, addrID, labelID, numMsg)
|
|
||||||
})
|
|
||||||
|
|
||||||
var allowSyncToProgress atomic.Bool
|
|
||||||
allowSyncToProgress.Store(false)
|
|
||||||
|
|
||||||
// Simulate 429 to prevent sync from progressing.
|
|
||||||
s.AddStatusHook(func(request *http.Request) (int, bool) {
|
|
||||||
if request.Method == "GET" && strings.Contains(request.URL.Path, "/mail/v4/messages/") {
|
|
||||||
if !allowSyncToProgress.Load() {
|
|
||||||
return http.StatusTooManyRequests, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0, false
|
|
||||||
})
|
|
||||||
|
|
||||||
// The initial user should be fully synced.
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
|
||||||
syncStartedCh, syncStartedDone := chToType[events.Event, events.SyncStarted](bridge.GetEvents(events.SyncStarted{}))
|
|
||||||
defer syncStartedDone()
|
|
||||||
|
|
||||||
addressCreatedCh, addressCreatedDone := chToType[events.Event, events.UserAddressCreated](bridge.GetEvents(events.UserAddressCreated{}))
|
|
||||||
defer addressCreatedDone()
|
|
||||||
|
|
||||||
userID, err := bridge.LoginFull(ctx, "imap", password, nil, nil)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.Equal(t, userID, (<-syncStartedCh).UserID)
|
|
||||||
|
|
||||||
// create 20 more messages and move them to inbox
|
|
||||||
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
|
|
||||||
createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 20)
|
|
||||||
})
|
|
||||||
|
|
||||||
// User AddrID2 event as a check point to see when the new address was created.
|
|
||||||
addrID, err := s.CreateAddress(userID, "bar@proton.ch", password)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// At most two events can be published, one for the first address, then for the second.
|
|
||||||
// if the second event is not `addrID` then something went wrong.
|
|
||||||
event := <-addressCreatedCh
|
|
||||||
require.Equal(t, addrID, event.AddressID)
|
|
||||||
allowSyncToProgress.Store(true)
|
|
||||||
|
|
||||||
info, err := bridge.GetUserInfo(userID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
|
||||||
defer func() { _ = client.Logout() }()
|
|
||||||
|
|
||||||
require.Eventually(t, func() bool {
|
|
||||||
// Finally check if the 20 messages are in INBOX.
|
|
||||||
status, err := client.Status("INBOX", []imap.StatusItem{imap.StatusMessages})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
return uint32(20) == status.Messages
|
|
||||||
}, 10*time.Second, time.Second)
|
|
||||||
})
|
|
||||||
}, server.WithTLS(false))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBridge_CorruptedVaultClearsPreviousIMAPSyncState(t *testing.T) {
|
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
|
||||||
userID, addrID, err := s.CreateUser("imap", password)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
|
|
||||||
createNumMessages(ctx, t, c, addrID, labelID, 100)
|
|
||||||
})
|
|
||||||
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
|
||||||
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
|
||||||
defer done()
|
|
||||||
|
|
||||||
var err error
|
|
||||||
|
|
||||||
userID, err = bridge.LoginFull(context.Background(), "imap", password, nil, nil)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Wait for sync to finish
|
|
||||||
require.Equal(t, userID, (<-syncCh).UserID)
|
|
||||||
})
|
|
||||||
|
|
||||||
settingsPath, err := locator.ProvideSettingsPath()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
syncConfigPath, err := locator.ProvideIMAPSyncConfigPath()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
syncStatePath := imapservice.GetSyncConfigPath(syncConfigPath, userID)
|
|
||||||
// Check sync state is complete
|
|
||||||
{
|
|
||||||
state, err := imapservice.NewSyncState(syncStatePath)
|
|
||||||
require.NoError(t, err)
|
|
||||||
syncStatus, err := state.GetSyncStatus(context.Background())
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.True(t, syncStatus.IsComplete())
|
|
||||||
}
|
|
||||||
|
|
||||||
// corrupt the vault
|
|
||||||
require.NoError(t, os.WriteFile(filepath.Join(settingsPath, "vault.enc"), []byte("Trash!"), 0o600))
|
|
||||||
|
|
||||||
// Bridge starts but can't find the gluon database dir; there should be no error.
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
|
||||||
_, err := bridge.LoginFull(context.Background(), "imap", password, nil, nil)
|
|
||||||
require.NoError(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Check sync state is reset.
|
|
||||||
{
|
|
||||||
state, err := imapservice.NewSyncState(syncStatePath)
|
|
||||||
require.NoError(t, err)
|
|
||||||
syncStatus, err := state.GetSyncStatus(context.Background())
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.False(t, syncStatus.IsComplete())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func withClient(ctx context.Context, t *testing.T, s *server.Server, username string, password []byte, fn func(context.Context, *proton.Client)) { //nolint:unparam
|
func withClient(ctx context.Context, t *testing.T, s *server.Server, username string, password []byte, fn func(context.Context, *proton.Client)) { //nolint:unparam
|
||||||
m := proton.New(
|
m := proton.New(
|
||||||
proton.WithHostURL(s.GetHostURL()),
|
proton.WithHostURL(s.GetHostURL()),
|
||||||
|
|||||||
@ -31,7 +31,7 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Disabled due to flakiness.
|
// Disabled due to flakyness.
|
||||||
func _TestBridge_SyncExistsWithErrorWhenTooManyFilesAreOpen(t *testing.T) { //nolint:unused
|
func _TestBridge_SyncExistsWithErrorWhenTooManyFilesAreOpen(t *testing.T) { //nolint:unused
|
||||||
var rlimitCurrent syscall.Rlimit
|
var rlimitCurrent syscall.Rlimit
|
||||||
|
|
||||||
|
|||||||
@ -32,7 +32,15 @@ type Locator interface {
|
|||||||
GetLicenseFilePath() string
|
GetLicenseFilePath() string
|
||||||
GetDependencyLicensesLink() string
|
GetDependencyLicensesLink() string
|
||||||
Clear(...string) error
|
Clear(...string) error
|
||||||
ProvideIMAPSyncConfigPath() (string, error)
|
}
|
||||||
|
|
||||||
|
type Identifier interface {
|
||||||
|
GetUserAgent() string
|
||||||
|
HasClient() bool
|
||||||
|
SetClient(name, version string)
|
||||||
|
SetPlatform(platform string)
|
||||||
|
SetClientString(client string)
|
||||||
|
GetClientString() string
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProxyController interface {
|
type ProxyController interface {
|
||||||
@ -53,5 +61,4 @@ type Autostarter interface {
|
|||||||
type Updater interface {
|
type Updater interface {
|
||||||
GetVersionInfo(context.Context, updater.Downloader, updater.Channel) (updater.VersionInfo, error)
|
GetVersionInfo(context.Context, updater.Downloader, updater.Channel) (updater.VersionInfo, error)
|
||||||
InstallUpdate(context.Context, updater.Downloader, updater.VersionInfo) error
|
InstallUpdate(context.Context, updater.Downloader, updater.VersionInfo) error
|
||||||
RemoveOldUpdates() error
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -139,9 +139,3 @@ func (bridge *Bridge) installUpdate(ctx context.Context, job installJob) {
|
|||||||
}
|
}
|
||||||
}, bridge.newVersionLock)
|
}, bridge.newVersionLock)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) RemoveOldUpdates() {
|
|
||||||
if err := bridge.updater.RemoveOldUpdates(); err != nil {
|
|
||||||
logrus.WithError(err).Error("Remove old updates fails")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -30,7 +30,6 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice"
|
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/try"
|
"github.com/ProtonMail/proton-bridge/v3/internal/try"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||||
@ -46,8 +45,6 @@ const (
|
|||||||
Connected
|
Connected
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrFailedToUnlock = errors.New("failed to unlock user keys")
|
|
||||||
|
|
||||||
type UserInfo struct {
|
type UserInfo struct {
|
||||||
// UserID is the user's API ID.
|
// UserID is the user's API ID.
|
||||||
UserID string
|
UserID string
|
||||||
@ -68,10 +65,10 @@ type UserInfo struct {
|
|||||||
BridgePass []byte
|
BridgePass []byte
|
||||||
|
|
||||||
// UsedSpace is the amount of space used by the user.
|
// UsedSpace is the amount of space used by the user.
|
||||||
UsedSpace uint64
|
UsedSpace int
|
||||||
|
|
||||||
// MaxSpace is the total amount of space available to the user.
|
// MaxSpace is the total amount of space available to the user.
|
||||||
MaxSpace uint64
|
MaxSpace int
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserIDs returns the IDs of all known users (authorized or not).
|
// GetUserIDs returns the IDs of all known users (authorized or not).
|
||||||
@ -159,15 +156,11 @@ func (bridge *Bridge) LoginUser(
|
|||||||
func() (string, error) {
|
func() (string, error) {
|
||||||
return bridge.loginUser(ctx, client, auth.UID, auth.RefreshToken, keyPass)
|
return bridge.loginUser(ctx, client, auth.UID, auth.RefreshToken, keyPass)
|
||||||
},
|
},
|
||||||
|
func() error {
|
||||||
|
return client.AuthDelete(ctx)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Failure to unlock will allow retries, so we do not delete auth.
|
|
||||||
if !errors.Is(err, ErrFailedToUnlock) {
|
|
||||||
if deleteErr := client.AuthDelete(ctx); deleteErr != nil {
|
|
||||||
logrus.WithError(deleteErr).Error("Failed to delete auth")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "", fmt.Errorf("failed to login user: %w", err)
|
return "", fmt.Errorf("failed to login user: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -223,16 +216,7 @@ func (bridge *Bridge) LoginFull(
|
|||||||
keyPass = password
|
keyPass = password
|
||||||
}
|
}
|
||||||
|
|
||||||
userID, err := bridge.LoginUser(ctx, client, auth, keyPass)
|
return bridge.LoginUser(ctx, client, auth, keyPass)
|
||||||
if err != nil {
|
|
||||||
if deleteErr := client.AuthDelete(ctx); deleteErr != nil {
|
|
||||||
logrus.WithError(err).Error("Failed to delete auth")
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return userID, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// LogoutUser logs out the given user.
|
// LogoutUser logs out the given user.
|
||||||
@ -259,11 +243,6 @@ func (bridge *Bridge) LogoutUser(ctx context.Context, userID string) error {
|
|||||||
func (bridge *Bridge) DeleteUser(ctx context.Context, userID string) error {
|
func (bridge *Bridge) DeleteUser(ctx context.Context, userID string) error {
|
||||||
logrus.WithField("userID", userID).Info("Deleting user")
|
logrus.WithField("userID", userID).Info("Deleting user")
|
||||||
|
|
||||||
syncConfigDir, err := bridge.locator.ProvideIMAPSyncConfigPath()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get sync config path")
|
|
||||||
}
|
|
||||||
|
|
||||||
return safe.LockRet(func() error {
|
return safe.LockRet(func() error {
|
||||||
if !bridge.vault.HasUser(userID) {
|
if !bridge.vault.HasUser(userID) {
|
||||||
return ErrNoSuchUser
|
return ErrNoSuchUser
|
||||||
@ -273,10 +252,6 @@ func (bridge *Bridge) DeleteUser(ctx context.Context, userID string) error {
|
|||||||
bridge.logoutUser(ctx, user, true, true, !bridge.GetTelemetryDisabled())
|
bridge.logoutUser(ctx, user, true, true, !bridge.GetTelemetryDisabled())
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := imapservice.DeleteSyncState(syncConfigDir, userID); err != nil {
|
|
||||||
return fmt.Errorf("failed to delete use sync config")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := bridge.vault.DeleteUser(userID); err != nil {
|
if err := bridge.vault.DeleteUser(userID); err != nil {
|
||||||
logrus.WithError(err).Error("Failed to delete vault user")
|
logrus.WithError(err).Error("Failed to delete vault user")
|
||||||
}
|
}
|
||||||
@ -303,10 +278,18 @@ func (bridge *Bridge) SetAddressMode(ctx context.Context, userID string, mode va
|
|||||||
return fmt.Errorf("address mode is already %q", mode)
|
return fmt.Errorf("address mode is already %q", mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := bridge.removeIMAPUser(ctx, user, true); err != nil {
|
||||||
|
return fmt.Errorf("failed to remove IMAP user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := user.SetAddressMode(ctx, mode); err != nil {
|
if err := user.SetAddressMode(ctx, mode); err != nil {
|
||||||
return fmt.Errorf("failed to set address mode: %w", err)
|
return fmt.Errorf("failed to set address mode: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := bridge.addIMAPUser(ctx, user); err != nil {
|
||||||
|
return fmt.Errorf("failed to add IMAP user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
bridge.publish(events.AddressModeChanged{
|
bridge.publish(events.AddressModeChanged{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
AddressMode: mode,
|
AddressMode: mode,
|
||||||
@ -329,7 +312,7 @@ func (bridge *Bridge) SetAddressMode(ctx context.Context, userID string, mode va
|
|||||||
func (bridge *Bridge) SendBadEventUserFeedback(_ context.Context, userID string, doResync bool) error {
|
func (bridge *Bridge) SendBadEventUserFeedback(_ context.Context, userID string, doResync bool) error {
|
||||||
logrus.WithField("userID", userID).WithField("doResync", doResync).Info("Passing bad event feedback to user")
|
logrus.WithField("userID", userID).WithField("doResync", doResync).Info("Passing bad event feedback to user")
|
||||||
|
|
||||||
return safe.RLockRet(func() error {
|
return safe.LockRet(func() error {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
user, ok := bridge.users[userID]
|
user, ok := bridge.users[userID]
|
||||||
@ -352,7 +335,13 @@ func (bridge *Bridge) SendBadEventUserFeedback(_ context.Context, userID string,
|
|||||||
logrus.WithError(rerr).Error("Failed to report feedback failure")
|
logrus.WithError(rerr).Error("Failed to report feedback failure")
|
||||||
}
|
}
|
||||||
|
|
||||||
return user.BadEventFeedbackResync(ctx)
|
if err := bridge.addIMAPUser(ctx, user); err != nil {
|
||||||
|
return fmt.Errorf("failed to add IMAP user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
user.BadEventFeedbackResync(ctx)
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if rerr := bridge.reporter.ReportMessageWithContext(
|
if rerr := bridge.reporter.ReportMessageWithContext(
|
||||||
@ -389,9 +378,9 @@ func (bridge *Bridge) loginUser(ctx context.Context, client *proton.Client, auth
|
|||||||
}
|
}
|
||||||
|
|
||||||
if userKR, err := apiUser.Keys.Unlock(saltedKeyPass, nil); err != nil {
|
if userKR, err := apiUser.Keys.Unlock(saltedKeyPass, nil); err != nil {
|
||||||
return "", fmt.Errorf("%w: %w", ErrFailedToUnlock, err)
|
return "", fmt.Errorf("failed to unlock user keys: %w", err)
|
||||||
} else if userKR.CountDecryptionEntities() == 0 {
|
} else if userKR.CountDecryptionEntities() == 0 {
|
||||||
return "", ErrFailedToUnlock
|
return "", fmt.Errorf("failed to unlock user keys")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := bridge.addUser(ctx, client, apiUser, authUID, authRef, saltedKeyPass, true); err != nil {
|
if err := bridge.addUser(ctx, client, apiUser, authUID, authRef, saltedKeyPass, true); err != nil {
|
||||||
@ -535,10 +524,8 @@ func (bridge *Bridge) addUserWithVault(
|
|||||||
return fmt.Errorf("failed to get Statistics directory: %w", err)
|
return fmt.Errorf("failed to get Statistics directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
syncSettingsPath, err := bridge.locator.ProvideIMAPSyncConfigPath()
|
// re-set SyncStatus if database need to be re-synced for migration.
|
||||||
if err != nil {
|
bridge.migrateUser(vault)
|
||||||
return fmt.Errorf("failed to get IMAP sync config path: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := user.New(
|
user, err := user.New(
|
||||||
ctx,
|
ctx,
|
||||||
@ -551,16 +538,16 @@ func (bridge *Bridge) addUserWithVault(
|
|||||||
bridge.vault.GetMaxSyncMemory(),
|
bridge.vault.GetMaxSyncMemory(),
|
||||||
statsPath,
|
statsPath,
|
||||||
bridge,
|
bridge,
|
||||||
bridge.serverManager,
|
|
||||||
bridge.serverManager,
|
|
||||||
&bridgeEventSubscription{b: bridge},
|
|
||||||
bridge.syncService,
|
|
||||||
syncSettingsPath,
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create user: %w", err)
|
return fmt.Errorf("failed to create user: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Connect the user's address(es) to gluon.
|
||||||
|
if err := bridge.addIMAPUser(ctx, user); err != nil {
|
||||||
|
return fmt.Errorf("failed to add IMAP user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Handle events coming from the user before forwarding them to the bridge.
|
// Handle events coming from the user before forwarding them to the bridge.
|
||||||
// For example, if the user's addresses change, we need to update them in gluon.
|
// For example, if the user's addresses change, we need to update them in gluon.
|
||||||
bridge.tasks.Once(func(ctx context.Context) {
|
bridge.tasks.Once(func(ctx context.Context) {
|
||||||
@ -570,8 +557,11 @@ func (bridge *Bridge) addUserWithVault(
|
|||||||
"event": event,
|
"event": event,
|
||||||
}).Debug("Received user event")
|
}).Debug("Received user event")
|
||||||
|
|
||||||
bridge.handleUserEvent(ctx, user, event)
|
if err := bridge.handleUserEvent(ctx, user, event); err != nil {
|
||||||
bridge.publish(event)
|
logrus.WithError(err).Error("Failed to handle user event")
|
||||||
|
} else {
|
||||||
|
bridge.publish(event)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -622,6 +612,10 @@ func (bridge *Bridge) logoutUser(ctx context.Context, user *user.User, withAPI,
|
|||||||
"withData": withData,
|
"withData": withData,
|
||||||
}).Debug("Logging out user")
|
}).Debug("Logging out user")
|
||||||
|
|
||||||
|
if err := bridge.removeIMAPUser(ctx, user, withData); err != nil {
|
||||||
|
logrus.WithError(err).Error("Failed to remove IMAP user")
|
||||||
|
}
|
||||||
|
|
||||||
if err := user.Logout(ctx, withAPI); err != nil {
|
if err := user.Logout(ctx, withAPI); err != nil {
|
||||||
logrus.WithError(err).Error("Failed to logout user")
|
logrus.WithError(err).Error("Failed to logout user")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,17 +19,44 @@ package bridge
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/ProtonMail/gluon/reporter"
|
"github.com/ProtonMail/gluon/reporter"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal"
|
"github.com/ProtonMail/proton-bridge/v3/internal"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (bridge *Bridge) handleUserEvent(ctx context.Context, user *user.User, event events.Event) {
|
func (bridge *Bridge) handleUserEvent(ctx context.Context, user *user.User, event events.Event) error {
|
||||||
switch event := event.(type) {
|
switch event := event.(type) {
|
||||||
|
case events.UserAddressCreated:
|
||||||
|
if err := bridge.handleUserAddressCreated(ctx, user, event); err != nil {
|
||||||
|
return fmt.Errorf("failed to handle user address created event: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
case events.UserAddressEnabled:
|
||||||
|
if err := bridge.handleUserAddressEnabled(ctx, user, event); err != nil {
|
||||||
|
return fmt.Errorf("failed to handle user address enabled event: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
case events.UserAddressDisabled:
|
||||||
|
if err := bridge.handleUserAddressDisabled(ctx, user, event); err != nil {
|
||||||
|
return fmt.Errorf("failed to handle user address disabled event: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
case events.UserAddressDeleted:
|
||||||
|
if err := bridge.handleUserAddressDeleted(ctx, user, event); err != nil {
|
||||||
|
return fmt.Errorf("failed to handle user address deleted event: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
case events.UserRefreshed:
|
||||||
|
if err := bridge.handleUserRefreshed(ctx, user, event); err != nil {
|
||||||
|
return fmt.Errorf("failed to handle user refreshed event: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
case events.UserDeauth:
|
case events.UserDeauth:
|
||||||
bridge.handleUserDeauth(ctx, user)
|
bridge.handleUserDeauth(ctx, user)
|
||||||
|
|
||||||
@ -39,6 +66,102 @@ func (bridge *Bridge) handleUserEvent(ctx context.Context, user *user.User, even
|
|||||||
case events.UncategorizedEventError:
|
case events.UncategorizedEventError:
|
||||||
bridge.handleUncategorizedErrorEvent(event)
|
bridge.handleUncategorizedErrorEvent(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) handleUserAddressCreated(ctx context.Context, user *user.User, event events.UserAddressCreated) error {
|
||||||
|
if user.GetAddressMode() == vault.CombinedMode {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
gluonID, err := bridge.serverManager.AddGluonUser(ctx, user.NewIMAPConnector(event.AddressID), user.GluonKey())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to add user to IMAP server: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := user.SetGluonID(event.AddressID, gluonID); err != nil {
|
||||||
|
return fmt.Errorf("failed to set gluon ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) handleUserAddressEnabled(ctx context.Context, user *user.User, event events.UserAddressEnabled) error {
|
||||||
|
if user.GetAddressMode() == vault.CombinedMode {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
gluonID, err := bridge.serverManager.AddGluonUser(ctx, user.NewIMAPConnector(event.AddressID), user.GluonKey())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to add user to IMAP server: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := user.SetGluonID(event.AddressID, gluonID); err != nil {
|
||||||
|
return fmt.Errorf("failed to set gluon ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) handleUserAddressDisabled(ctx context.Context, user *user.User, event events.UserAddressDisabled) error {
|
||||||
|
if user.GetAddressMode() == vault.CombinedMode {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
gluonID, ok := user.GetGluonID(event.AddressID)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("gluon ID not found for address %s", event.AddressID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bridge.serverManager.RemoveGluonUser(ctx, gluonID); err != nil {
|
||||||
|
return fmt.Errorf("failed to remove user from IMAP server: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := user.RemoveGluonID(event.AddressID, gluonID); err != nil {
|
||||||
|
return fmt.Errorf("failed to remove gluon ID for address: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) handleUserAddressDeleted(ctx context.Context, user *user.User, event events.UserAddressDeleted) error {
|
||||||
|
if user.GetAddressMode() == vault.CombinedMode {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
gluonID, ok := user.GetGluonID(event.AddressID)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("gluon ID not found for address %s", event.AddressID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bridge.serverManager.handleRemoveGluonUser(ctx, gluonID); err != nil {
|
||||||
|
return fmt.Errorf("failed to remove user from IMAP server: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := user.RemoveGluonID(event.AddressID, gluonID); err != nil {
|
||||||
|
return fmt.Errorf("failed to remove gluon ID for address: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) handleUserRefreshed(ctx context.Context, user *user.User, event events.UserRefreshed) error {
|
||||||
|
return safe.RLockRet(func() error {
|
||||||
|
if event.CancelEventPool {
|
||||||
|
user.CancelSyncAndEventPoll()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bridge.removeIMAPUser(ctx, user, true); err != nil {
|
||||||
|
return fmt.Errorf("failed to remove IMAP user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bridge.addIMAPUser(ctx, user); err != nil {
|
||||||
|
return fmt.Errorf("failed to add IMAP user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}, bridge.usersLock)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) handleUserDeauth(ctx context.Context, user *user.User) {
|
func (bridge *Bridge) handleUserDeauth(ctx context.Context, user *user.User) {
|
||||||
@ -48,8 +171,8 @@ func (bridge *Bridge) handleUserDeauth(ctx context.Context, user *user.User) {
|
|||||||
}, bridge.usersLock)
|
}, bridge.usersLock)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) handleUserBadEvent(ctx context.Context, user *user.User, event events.UserBadEvent) {
|
func (bridge *Bridge) handleUserBadEvent(_ context.Context, user *user.User, event events.UserBadEvent) {
|
||||||
safe.RLock(func() {
|
safe.Lock(func() {
|
||||||
if rerr := bridge.reporter.ReportMessageWithContext("Failed to handle event", reporter.Context{
|
if rerr := bridge.reporter.ReportMessageWithContext("Failed to handle event", reporter.Context{
|
||||||
"user_id": user.ID(),
|
"user_id": user.ID(),
|
||||||
"old_event_id": event.OldEventID,
|
"old_event_id": event.OldEventID,
|
||||||
@ -61,7 +184,12 @@ func (bridge *Bridge) handleUserBadEvent(ctx context.Context, user *user.User, e
|
|||||||
logrus.WithError(rerr).Error("Failed to report failed event handling")
|
logrus.WithError(rerr).Error("Failed to report failed event handling")
|
||||||
}
|
}
|
||||||
|
|
||||||
user.OnBadEvent(ctx)
|
user.CancelSyncAndEventPoll()
|
||||||
|
|
||||||
|
// Disable IMAP user
|
||||||
|
if err := bridge.removeIMAPUser(context.Background(), user, false); err != nil {
|
||||||
|
logrus.WithError(err).Error("Failed to remove IMAP user")
|
||||||
|
}
|
||||||
}, bridge.usersLock)
|
}, bridge.usersLock)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -23,200 +23,71 @@ package certs
|
|||||||
#import <Foundation/Foundation.h>
|
#import <Foundation/Foundation.h>
|
||||||
#import <Security/Security.h>
|
#import <Security/Security.h>
|
||||||
|
|
||||||
// Memory management rules:
|
|
||||||
// Foundation object (Objective-C prefixed with `NS`) get ARC (Automatic Reference Counting), and do not need to be released manually.
|
|
||||||
// Core Foundation objects (C), prefixed with need to be released manually using CFRelease() unless:
|
|
||||||
// - They're obtained using a CF method containing the word Get (a.k.a. the Get Rule).
|
|
||||||
// - They're obtained using toll-free bridging from a Foundation Object (using the __bridge keyword).
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
int installTrustedCert(char const *bytes, unsigned long long length) {
|
||||||
/// \brief Create a certificate object from DER-encoded data.
|
if (length == 0) {
|
||||||
///
|
return errSecInvalidData;
|
||||||
/// \return The certifcation. The caller is responsible for releasing the object using CFRelease.
|
}
|
||||||
/// \return NULL if data is not a valid DER-encoded certificate.
|
|
||||||
//****************************************************************************************************************************************************
|
NSData *der = [NSData dataWithBytes:bytes length:length];
|
||||||
SecCertificateRef certFromData(char const* data, uint64_t length) {
|
|
||||||
NSData *der = [NSData dataWithBytes:data length:length];
|
// Step 1. Import the certificate in the keychain.
|
||||||
return SecCertificateCreateWithData(NULL, (__bridge CFDataRef)der);
|
SecCertificateRef cert = SecCertificateCreateWithData(NULL, (CFDataRef) der);
|
||||||
|
NSDictionary* addQuery = @{
|
||||||
|
(id)kSecValueRef: (__bridge id) cert,
|
||||||
|
(id)kSecClass: (id)kSecClassCertificate,
|
||||||
|
};
|
||||||
|
|
||||||
|
OSStatus status = SecItemAdd((__bridge CFDictionaryRef) addQuery, NULL);
|
||||||
|
if ((errSecSuccess != status) && (errSecDuplicateItem != status)) {
|
||||||
|
CFRelease(cert);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2. Set the trust for the certificate.
|
||||||
|
SecPolicyRef policy = SecPolicyCreateSSL(true, NULL); // we limit our trust to SSL
|
||||||
|
NSDictionary *trustSettings = @{
|
||||||
|
(id)kSecTrustSettingsResult: [NSNumber numberWithInt:kSecTrustSettingsResultTrustRoot],
|
||||||
|
(id)kSecTrustSettingsPolicy: (__bridge id) policy,
|
||||||
|
};
|
||||||
|
status = SecTrustSettingsSetTrustSettings(cert, kSecTrustSettingsDomainUser, (__bridge CFTypeRef)(trustSettings));
|
||||||
|
CFRelease(policy);
|
||||||
|
CFRelease(cert);
|
||||||
|
|
||||||
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
int removeTrustedCert(char const *bytes, unsigned long long length) {
|
||||||
/// \brief Check if a certificate is in the user's keychain.
|
if (0 == length) {
|
||||||
///
|
return errSecInvalidData;
|
||||||
/// \param[in] cert The certificate.
|
}
|
||||||
/// \return true iff the certificate is in the user's keychain.
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
bool _isCertificateInKeychain(SecCertificateRef const cert) {
|
|
||||||
NSDictionary *attrs = @{
|
|
||||||
(id)kSecMatchItemList: @[(__bridge id)cert],
|
|
||||||
(id)kSecClass: (id)kSecClassCertificate,
|
|
||||||
(id)kSecReturnData: @YES
|
|
||||||
};
|
|
||||||
return errSecSuccess == SecItemCopyMatching((__bridge CFDictionaryRef)attrs, NULL);
|
|
||||||
}
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
NSData *der = [NSData dataWithBytes: bytes length: length];
|
||||||
/// \brief Check if a certificate is in the user's keychain.
|
SecCertificateRef cert = SecCertificateCreateWithData(NULL, (CFDataRef) der);
|
||||||
///
|
|
||||||
/// \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.
|
||||||
/// \brief Add a certificate to the user's keychain.
|
NSDictionary *query = @{ (id)kSecClass: (id)kSecClassCertificate,
|
||||||
///
|
(id)kSecMatchItemList: @[(__bridge id)cert],
|
||||||
/// \param[in] cert The certificate.
|
(id)kSecMatchLimit: (id)kSecMatchLimitOne,
|
||||||
/// \return The status for the operation.
|
};
|
||||||
//****************************************************************************************************************************************************
|
status = SecItemDelete((__bridge CFDictionaryRef) query);
|
||||||
OSStatus _addCertificateToKeychain(SecCertificateRef const cert) {
|
|
||||||
NSDictionary* addQuery = @{
|
|
||||||
(id)kSecValueRef: (__bridge id) cert,
|
|
||||||
(id)kSecClass: (id)kSecClassCertificate,
|
|
||||||
};
|
|
||||||
return SecItemAdd((__bridge CFDictionaryRef) addQuery, NULL);
|
|
||||||
}
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
CFRelease(cert);
|
||||||
/// \brief Add a certificate to the user's keychain.
|
return status;
|
||||||
///
|
|
||||||
/// \param[in] certData The certificate data in DER encoded format.
|
|
||||||
/// \param[in] certSize The size of the certData in bytes.
|
|
||||||
/// \return The status for the operation.
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
OSStatus addCertificateToKeychain(char const* certData, uint64_t certSize) {
|
|
||||||
return _addCertificateToKeychain(certFromData(certData, certSize));
|
|
||||||
}
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
/// \brief Add a certificate to the user's keychain.
|
|
||||||
///
|
|
||||||
/// \param[in] cert The certificate.
|
|
||||||
/// \return The status for the operation.
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
OSStatus _removeCertificateFromKeychain(SecCertificateRef const cert) {
|
|
||||||
NSDictionary *query = @{ (id)kSecClass: (id)kSecClassCertificate,
|
|
||||||
(id)kSecMatchItemList: @[(__bridge id)cert],
|
|
||||||
(id)kSecMatchLimit: (id)kSecMatchLimitOne,
|
|
||||||
};
|
|
||||||
return SecItemDelete((__bridge CFDictionaryRef) query);
|
|
||||||
}
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
/// \brief Add a certificate to the user's keychain.
|
|
||||||
///
|
|
||||||
/// \param[in] certData The certificate data in DER encoded format.
|
|
||||||
/// \param[in] certSize The size of the certData in bytes.
|
|
||||||
/// \return The status for the operation.
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
OSStatus removeCertificateFromKeychain(char const* certData, uint64_t certSize) {
|
|
||||||
return _removeCertificateFromKeychain(certFromData(certData, certSize));
|
|
||||||
}
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
/// \brief Check if a certificate is trusted in the user's keychain.
|
|
||||||
///
|
|
||||||
/// \param[in] cert The certificate.
|
|
||||||
/// \return true iff the certificate is trusted in the user's keychain.
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
bool _isCertificateTrusted(SecCertificateRef const cert) {
|
|
||||||
CFArrayRef trustSettings = NULL;
|
|
||||||
OSStatus status = SecTrustSettingsCopyTrustSettings(cert, kSecTrustSettingsDomainUser, &trustSettings);
|
|
||||||
if (status != errSecSuccess) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
CFIndex count = CFArrayGetCount(trustSettings);
|
|
||||||
bool result = false;
|
|
||||||
for (CFIndex index = 0; index < count; ++index) {
|
|
||||||
CFDictionaryRef dict = (CFDictionaryRef)CFArrayGetValueAtIndex(trustSettings, index);
|
|
||||||
if (!dict) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
CFNumberRef num = (CFNumberRef)CFDictionaryGetValue(dict, kSecTrustSettingsResult);
|
|
||||||
int value;
|
|
||||||
if (num && CFNumberGetValue(num, kCFNumberSInt32Type, &value) && (value == kSecTrustSettingsResultTrustRoot)) {
|
|
||||||
result = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CFRelease(trustSettings);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
/// \brief Check if a certificate is trusted in the user's keychain.
|
|
||||||
///
|
|
||||||
/// \param[in] certData The certificate data in DER encoded format.
|
|
||||||
/// \param[in] certSize The size of the certData in bytes.
|
|
||||||
/// \return true iff the certificate is trusted in the user's keychain.
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
bool isCertificateTrusted(char const* certData, uint64_t certSize) {
|
|
||||||
return _isCertificateTrusted(certFromData(certData, certSize));
|
|
||||||
}
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
/// \brief Set the trust level for a certificate in the user's keychain. This call will trigger a security prompt.
|
|
||||||
///
|
|
||||||
/// \param[in] cert The certificate.
|
|
||||||
/// \param[in] trustLevel The trust level.
|
|
||||||
/// \return The status for the operation.
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
OSStatus _setCertificateTrustLevel(SecCertificateRef const cert, int trustLevel) {
|
|
||||||
SecPolicyRef policy = SecPolicyCreateSSL(true, NULL); // we limit our trust to SSL
|
|
||||||
NSDictionary *trustSettings = @{
|
|
||||||
(id)kSecTrustSettingsResult: [NSNumber numberWithInt:trustLevel],
|
|
||||||
(id)kSecTrustSettingsPolicy: (__bridge id) policy,
|
|
||||||
};
|
|
||||||
OSStatus status = SecTrustSettingsSetTrustSettings(cert, kSecTrustSettingsDomainUser, (__bridge CFTypeRef)(trustSettings));
|
|
||||||
CFRelease(policy);
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
/// \brief Set a certificate as trusted in the user's keychain. This call will trigger a security prompt.
|
|
||||||
///
|
|
||||||
/// \param[in] cert The certificate.
|
|
||||||
/// \return The status for the operation.
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
OSStatus _setCertificateTrusted(SecCertificateRef cert) {
|
|
||||||
return _setCertificateTrustLevel(cert, kSecTrustSettingsResultTrustRoot);
|
|
||||||
}
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
/// \brief Set a certificate as trusted in the user's keychain. This call will trigger a security prompt.
|
|
||||||
///
|
|
||||||
/// \param[in] certData The certificate data in DER encoded format.
|
|
||||||
/// \param[in] certSize The size of the certData in bytes.
|
|
||||||
/// \return The status for the operation.
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
OSStatus setCertificateTrusted(char const* certData, uint64_t certSize) {
|
|
||||||
return _setCertificateTrusted(certFromData(certData, certSize));
|
|
||||||
}
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
/// \brief Remove the trust level of a certificate in the user's keychain.
|
|
||||||
///
|
|
||||||
/// \param[in] cert The certificate.
|
|
||||||
/// \return The status for the operation.
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
OSStatus _removeCertificateTrust(SecCertificateRef cert) {
|
|
||||||
return _setCertificateTrustLevel(cert, kSecTrustSettingsResultUnspecified);
|
|
||||||
}
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
/// \brief Remove the trust level of a certificate in the user's keychain.
|
|
||||||
///
|
|
||||||
/// \param[in] certData The certificate data in DER encoded format.
|
|
||||||
/// \param[in] certSize The size of the certData in bytes.
|
|
||||||
/// \return The status for the operation.
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
OSStatus removeCertificateTrust(char const* certData, uint64_t certSize) {
|
|
||||||
return _removeCertificateTrust(certFromData(certData, certSize));
|
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
import "C"
|
import "C"
|
||||||
@ -248,116 +119,6 @@ func certPEMToDER(certPEM []byte) ([]byte, error) {
|
|||||||
return block.Bytes, nil
|
return block.Bytes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// wrapCGoCertCallReturningBool wrap call to a CGo function returning a bool.
|
|
||||||
// if the certificate is invalid the call will return false.
|
|
||||||
func wrapCGoCertCallReturningBool(certPEM []byte, fn func(*C.char, C.ulonglong) bool) bool {
|
|
||||||
certDER, err := certPEMToDER(certPEM)
|
|
||||||
if err != nil {
|
|
||||||
return false // error are ignored
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer := C.CBytes(certDER)
|
|
||||||
defer C.free(unsafe.Pointer(buffer)) //nolint:unconvert
|
|
||||||
|
|
||||||
return fn((*C.char)(buffer), C.ulonglong(len(certDER)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// wrapCGoCertCallReturningBool wrap call to a CGo function returning an error
|
|
||||||
func wrapCGoCertCallReturningError(certPEM []byte, fn func(*C.char, C.ulonglong) error) error {
|
|
||||||
certDER, err := certPEMToDER(certPEM)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer := C.CBytes(certDER)
|
|
||||||
defer C.free(unsafe.Pointer(buffer)) //nolint:unconvert
|
|
||||||
|
|
||||||
return fn((*C.char)(buffer), C.ulonglong(len(certDER)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// isCertInKeychain returns true if the given certificate is stored in the user's keychain.
|
|
||||||
func isCertInKeychain(certPEM []byte) bool {
|
|
||||||
return wrapCGoCertCallReturningBool(certPEM, isCertInKeychainCGo)
|
|
||||||
}
|
|
||||||
|
|
||||||
func isCertInKeychainCGo(buffer *C.char, size C.ulonglong) bool {
|
|
||||||
return bool(C.isCertificateInKeychain(buffer, size))
|
|
||||||
}
|
|
||||||
|
|
||||||
// addCertToKeychain adds a certificate to the user's keychain.
|
|
||||||
// Trying to add a certificate that is already in the keychain will result in an error.
|
|
||||||
func addCertToKeychain(certPEM []byte) error {
|
|
||||||
return wrapCGoCertCallReturningError(certPEM, addCertToKeychainCGo)
|
|
||||||
}
|
|
||||||
|
|
||||||
func addCertToKeychainCGo(buffer *C.char, size C.ulonglong) error {
|
|
||||||
if errCode := C.addCertificateToKeychain(buffer, size); errCode != errSecSuccess {
|
|
||||||
return fmt.Errorf("could not add certificate to keychain (error %v)", errCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// removeCertFromKeychain removes a certificate from the user's keychain.
|
|
||||||
// Trying to remove a certificate that is not in the keychain will result in an error.
|
|
||||||
func removeCertFromKeychain(certPEM []byte) error {
|
|
||||||
return wrapCGoCertCallReturningError(certPEM, removeCertFromKeychainCGo)
|
|
||||||
}
|
|
||||||
|
|
||||||
func removeCertFromKeychainCGo(buffer *C.char, size C.ulonglong) error {
|
|
||||||
if errCode := C.removeCertificateFromKeychain(buffer, size); errCode != errSecSuccess {
|
|
||||||
return fmt.Errorf("could not remove certificate from keychain (error %v)", errCode)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// isCertTrusted check if a certificate is trusted in the user's keychain.
|
|
||||||
func isCertTrusted(certPEM []byte) bool {
|
|
||||||
return wrapCGoCertCallReturningBool(certPEM, isCertTrustedCGo)
|
|
||||||
}
|
|
||||||
|
|
||||||
func isCertTrustedCGo(buffer *C.char, size C.ulonglong) bool {
|
|
||||||
return bool(C.isCertificateTrusted(buffer, size))
|
|
||||||
}
|
|
||||||
|
|
||||||
// setCertTrusted sets a certificate as trusted in the user's keychain.
|
|
||||||
// This function will trigger a security prompt from the system.
|
|
||||||
func setCertTrusted(certPEM []byte) error {
|
|
||||||
return wrapCGoCertCallReturningError(certPEM, setCertTrustedCGo)
|
|
||||||
}
|
|
||||||
|
|
||||||
func setCertTrustedCGo(buffer *C.char, size C.ulonglong) error {
|
|
||||||
errCode := C.setCertificateTrusted(buffer, size)
|
|
||||||
switch errCode {
|
|
||||||
case errSecSuccess:
|
|
||||||
return nil
|
|
||||||
case errAuthorizationCanceled:
|
|
||||||
return ErrUserCanceledCertificateInstall
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("could not set certificate trust in keychain (error %v)", errCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// removeCertTrust remove the trust level of the certificated from the user's keychain.
|
|
||||||
// This function will trigger a security prompt from the system.
|
|
||||||
func removeCertTrust(certPEM []byte) error {
|
|
||||||
return wrapCGoCertCallReturningError(certPEM, removeCertTrustCGo)
|
|
||||||
}
|
|
||||||
|
|
||||||
func removeCertTrustCGo(buffer *C.char, size C.ulonglong) error {
|
|
||||||
errCode := C.removeCertificateTrust(buffer, size)
|
|
||||||
switch errCode {
|
|
||||||
case errSecSuccess:
|
|
||||||
return nil
|
|
||||||
case errAuthorizationCanceled:
|
|
||||||
return ErrUserCanceledCertificateInstall
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("could not set certificate trust in keychain (error %v)", errCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// installCert installs a certificate in the keychain. The certificate is added to the keychain and it is set as trusted.
|
|
||||||
// This function will trigger a security prompt from the system, unless the certificate is already trusted in the user keychain.
|
|
||||||
func installCert(certPEM []byte) error {
|
func installCert(certPEM []byte) error {
|
||||||
certDER, err := certPEMToDER(certPEM)
|
certDER, err := certPEMToDER(certPEM)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -366,24 +127,18 @@ func installCert(certPEM []byte) error {
|
|||||||
|
|
||||||
p := C.CBytes(certDER)
|
p := C.CBytes(certDER)
|
||||||
defer C.free(unsafe.Pointer(p)) //nolint:unconvert
|
defer C.free(unsafe.Pointer(p)) //nolint:unconvert
|
||||||
buffer := (*C.char)(p)
|
|
||||||
size := C.ulonglong(len(certDER))
|
|
||||||
|
|
||||||
if !isCertInKeychainCGo(buffer, size) {
|
errCode := C.installTrustedCert((*C.char)(p), (C.ulonglong)(len(certDER)))
|
||||||
if err := addCertToKeychainCGo(buffer, size); err != nil {
|
switch errCode {
|
||||||
return err
|
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 !isCertTrustedCGo(buffer, size) {
|
|
||||||
return setCertTrustedCGo(buffer, size)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// uninstallCert uninstalls a certificate in the keychain. The certificate trust is removed and the certificated is deleted from the keychain.
|
|
||||||
// This function will trigger a security prompt from the system, unless the certificate is not trusted in the user keychain.
|
|
||||||
func uninstallCert(certPEM []byte) error {
|
func uninstallCert(certPEM []byte) error {
|
||||||
certDER, err := certPEMToDER(certPEM)
|
certDER, err := certPEMToDER(certPEM)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -392,32 +147,10 @@ func uninstallCert(certPEM []byte) error {
|
|||||||
|
|
||||||
p := C.CBytes(certDER)
|
p := C.CBytes(certDER)
|
||||||
defer C.free(unsafe.Pointer(p)) //nolint:unconvert
|
defer C.free(unsafe.Pointer(p)) //nolint:unconvert
|
||||||
buffer := (*C.char)(p)
|
|
||||||
size := C.ulonglong(len(certDER))
|
|
||||||
|
|
||||||
if isCertTrustedCGo(buffer, size) {
|
if errCode := C.removeTrustedCert((*C.char)(p), (C.ulonglong)(len(certDER))); errCode != 0 {
|
||||||
if err := removeCertTrustCGo(buffer, size); err != nil {
|
return fmt.Errorf("could not install certificate from keychain (error %v)", errCode)
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if isCertInKeychainCGo(buffer, size) {
|
|
||||||
return removeCertFromKeychainCGo(buffer, size)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func isCertInstalled(certPEM []byte) bool {
|
|
||||||
certDER, err := certPEMToDER(certPEM)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
p := C.CBytes(certDER)
|
|
||||||
defer C.free(unsafe.Pointer(p)) //nolint:unconvert
|
|
||||||
buffer := (*C.char)(p)
|
|
||||||
size := C.ulonglong(len(certDER))
|
|
||||||
|
|
||||||
return isCertInKeychainCGo(buffer, size) && isCertTrustedCGo(buffer, size)
|
|
||||||
}
|
|
||||||
|
|||||||
@ -25,73 +25,20 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCertInKeychain(t *testing.T) {
|
// This test implies human interactions to enter password and is disabled by default.
|
||||||
// no trust settings change is performed, so this test will not trigger an OS security prompt.
|
func _TestTrustedCertsDarwin(t *testing.T) { //nolint:unused
|
||||||
certPEM := generatePEMCertificate(t)
|
|
||||||
require.False(t, isCertInKeychain(certPEM))
|
|
||||||
require.NoError(t, addCertToKeychain(certPEM))
|
|
||||||
require.True(t, isCertInKeychain(certPEM))
|
|
||||||
require.Error(t, addCertToKeychain(certPEM))
|
|
||||||
require.True(t, isCertInKeychain(certPEM))
|
|
||||||
require.NoError(t, removeCertFromKeychain(certPEM))
|
|
||||||
require.False(t, isCertInKeychain(certPEM))
|
|
||||||
require.Error(t, removeCertFromKeychain(certPEM))
|
|
||||||
require.False(t, isCertInKeychain(certPEM))
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test require human interaction (macOS security prompts), and is disabled by default.
|
|
||||||
func _TestCertificateTrust(t *testing.T) { //nolint:unused
|
|
||||||
certPEM := generatePEMCertificate(t)
|
|
||||||
require.False(t, isCertTrusted(certPEM))
|
|
||||||
require.NoError(t, addCertToKeychain(certPEM))
|
|
||||||
require.NoError(t, setCertTrusted(certPEM))
|
|
||||||
require.True(t, isCertTrusted(certPEM))
|
|
||||||
require.NoError(t, removeCertTrust(certPEM))
|
|
||||||
require.False(t, isCertTrusted(certPEM))
|
|
||||||
require.NoError(t, removeCertFromKeychain(certPEM))
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test require human interaction (macOS security prompts), and is disabled by default.
|
|
||||||
func _TestInstallAndRemove(t *testing.T) { //nolint:unused
|
|
||||||
certPEM := generatePEMCertificate(t)
|
|
||||||
|
|
||||||
// fresh install
|
|
||||||
require.False(t, isCertInstalled(certPEM))
|
|
||||||
require.NoError(t, installCert(certPEM))
|
|
||||||
require.True(t, isCertInKeychain(certPEM))
|
|
||||||
require.True(t, isCertTrusted(certPEM))
|
|
||||||
require.True(t, isCertInstalled(certPEM))
|
|
||||||
require.NoError(t, uninstallCert(certPEM))
|
|
||||||
require.False(t, isCertInKeychain(certPEM))
|
|
||||||
require.False(t, isCertTrusted(certPEM))
|
|
||||||
require.False(t, isCertInstalled(certPEM))
|
|
||||||
|
|
||||||
// Install where certificate is already in Keychain, but not trusted.
|
|
||||||
require.NoError(t, addCertToKeychain(certPEM))
|
|
||||||
require.False(t, isCertInstalled(certPEM))
|
|
||||||
require.NoError(t, installCert(certPEM))
|
|
||||||
require.True(t, isCertInstalled(certPEM))
|
|
||||||
|
|
||||||
// Install where certificate is already installed
|
|
||||||
require.NoError(t, installCert(certPEM))
|
|
||||||
|
|
||||||
// Remove when certificate is not trusted.
|
|
||||||
require.NoError(t, removeCertTrust(certPEM))
|
|
||||||
require.NoError(t, uninstallCert(certPEM))
|
|
||||||
require.False(t, isCertInstalled(certPEM))
|
|
||||||
|
|
||||||
// Remove when certificate has already been removed.
|
|
||||||
require.NoError(t, uninstallCert(certPEM))
|
|
||||||
require.False(t, isCertTrusted(certPEM))
|
|
||||||
require.False(t, isCertInKeychain(certPEM))
|
|
||||||
}
|
|
||||||
|
|
||||||
func generatePEMCertificate(t *testing.T) []byte {
|
|
||||||
template, err := NewTLSTemplate()
|
template, err := NewTLSTemplate()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
certPEM, _, err := GenerateCert(template)
|
certPEM, _, err := GenerateCert(template)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
return certPEM
|
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.
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,7 +24,3 @@ func installCert([]byte) error {
|
|||||||
func uninstallCert([]byte) error {
|
func uninstallCert([]byte) error {
|
||||||
return nil // Linux doesn't have a root cert store.
|
return nil // Linux doesn't have a root cert store.
|
||||||
}
|
}
|
||||||
|
|
||||||
func isCertInstalled([]byte) bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|||||||
@ -24,7 +24,3 @@ func installCert([]byte) error {
|
|||||||
func uninstallCert([]byte) error {
|
func uninstallCert([]byte) error {
|
||||||
return nil // NOTE(GODT-986): Uninstall certs from root cert store?
|
return nil // NOTE(GODT-986): Uninstall certs from root cert store?
|
||||||
}
|
}
|
||||||
|
|
||||||
func isCertInstalled([]byte) bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|||||||
@ -17,50 +17,16 @@
|
|||||||
|
|
||||||
package certs
|
package certs
|
||||||
|
|
||||||
import (
|
type Installer struct{}
|
||||||
"errors"
|
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrUserCanceledCertificateInstall = errors.New("the user cancelled the authorization dialog")
|
|
||||||
)
|
|
||||||
|
|
||||||
type Installer struct {
|
|
||||||
log *logrus.Entry
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewInstaller() *Installer {
|
func NewInstaller() *Installer {
|
||||||
return &Installer{
|
return &Installer{}
|
||||||
log: logrus.WithField("pkg", "certs"),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (installer *Installer) InstallCert(certPEM []byte) error {
|
func (installer *Installer) InstallCert(certPEM []byte) error {
|
||||||
installer.log.Info("Installing the Bridge TLS certificate in the OS keychain")
|
return installCert(certPEM)
|
||||||
|
|
||||||
if err := installCert(certPEM); err != nil {
|
|
||||||
installer.log.WithError(err).Error("The Bridge TLS certificate could not be installed in the OS keychain")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
installer.log.Info("The Bridge TLS certificate was successfully installed in the OS keychain")
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (installer *Installer) UninstallCert(certPEM []byte) error {
|
func (installer *Installer) UninstallCert(certPEM []byte) error {
|
||||||
installer.log.Info("Uninstalling the Bridge TLS certificate from the OS keychain")
|
return uninstallCert(certPEM)
|
||||||
|
|
||||||
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,13 +95,6 @@ func (status *ConfigurationStatus) IsPending() bool {
|
|||||||
return !status.Data.DataV1.PendingSince.IsZero()
|
return !status.Data.DataV1.PendingSince.IsZero()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (status *ConfigurationStatus) isPendingSinceMin() int {
|
|
||||||
if min := int(time.Since(status.Data.DataV1.PendingSince).Minutes()); min > 0 {
|
|
||||||
return min
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (status *ConfigurationStatus) IsFromFailure() bool {
|
func (status *ConfigurationStatus) IsFromFailure() bool {
|
||||||
status.DataLock.RLock()
|
status.DataLock.RLock()
|
||||||
defer status.DataLock.RUnlock()
|
defer status.DataLock.RUnlock()
|
||||||
|
|||||||
@ -19,6 +19,7 @@ package configstatus
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ConfigAbortValues struct {
|
type ConfigAbortValues struct {
|
||||||
@ -40,20 +41,17 @@ type ConfigAbortData struct {
|
|||||||
|
|
||||||
type ConfigAbortBuilder struct{}
|
type ConfigAbortBuilder struct{}
|
||||||
|
|
||||||
func (*ConfigAbortBuilder) New(config *ConfigurationStatus) ConfigAbortData {
|
func (*ConfigAbortBuilder) New(data *ConfigurationStatusData) ConfigAbortData {
|
||||||
config.DataLock.RLock()
|
|
||||||
defer config.DataLock.RUnlock()
|
|
||||||
|
|
||||||
return ConfigAbortData{
|
return ConfigAbortData{
|
||||||
MeasurementGroup: "bridge.any.configuration",
|
MeasurementGroup: "bridge.any.configuration",
|
||||||
Event: "bridge_config_abort",
|
Event: "bridge_config_abort",
|
||||||
Values: ConfigSuccessValues{
|
Values: ConfigSuccessValues{
|
||||||
Duration: config.isPendingSinceMin(),
|
Duration: int(time.Since(data.DataV1.PendingSince).Minutes()),
|
||||||
},
|
},
|
||||||
Dimensions: ConfigSuccessDimensions{
|
Dimensions: ConfigSuccessDimensions{
|
||||||
ReportClick: strconv.FormatBool(config.Data.DataV1.ReportClick),
|
ReportClick: strconv.FormatBool(data.DataV1.ReportClick),
|
||||||
ReportSent: strconv.FormatBool(config.Data.DataV1.ReportSent),
|
ReportSent: strconv.FormatBool(data.DataV1.ReportSent),
|
||||||
ClickedLink: config.Data.clickedLinkToString(),
|
ClickedLink: data.clickedLinkToString(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,7 +33,7 @@ func TestConfigurationAbort_default(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var builder = configstatus.ConfigAbortBuilder{}
|
var builder = configstatus.ConfigAbortBuilder{}
|
||||||
req := builder.New(config)
|
req := builder.New(config.Data)
|
||||||
|
|
||||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||||
require.Equal(t, "bridge_config_abort", req.Event)
|
require.Equal(t, "bridge_config_abort", req.Event)
|
||||||
@ -64,7 +64,7 @@ func TestConfigurationAbort_fed(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var builder = configstatus.ConfigAbortBuilder{}
|
var builder = configstatus.ConfigAbortBuilder{}
|
||||||
req := builder.New(config)
|
req := builder.New(config.Data)
|
||||||
|
|
||||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||||
require.Equal(t, "bridge_config_abort", req.Event)
|
require.Equal(t, "bridge_config_abort", req.Event)
|
||||||
|
|||||||
@ -33,16 +33,13 @@ type ConfigProgressData struct {
|
|||||||
|
|
||||||
type ConfigProgressBuilder struct{}
|
type ConfigProgressBuilder struct{}
|
||||||
|
|
||||||
func (*ConfigProgressBuilder) New(config *ConfigurationStatus) ConfigProgressData {
|
func (*ConfigProgressBuilder) New(data *ConfigurationStatusData) ConfigProgressData {
|
||||||
config.DataLock.RLock()
|
|
||||||
defer config.DataLock.RUnlock()
|
|
||||||
|
|
||||||
return ConfigProgressData{
|
return ConfigProgressData{
|
||||||
MeasurementGroup: "bridge.any.configuration",
|
MeasurementGroup: "bridge.any.configuration",
|
||||||
Event: "bridge_config_progress",
|
Event: "bridge_config_progress",
|
||||||
Values: ConfigProgressValues{
|
Values: ConfigProgressValues{
|
||||||
NbDay: numberOfDay(time.Now(), config.Data.DataV1.PendingSince),
|
NbDay: numberOfDay(time.Now(), data.DataV1.PendingSince),
|
||||||
NbDaySinceLast: numberOfDay(time.Now(), config.Data.DataV1.LastProgress),
|
NbDaySinceLast: numberOfDay(time.Now(), data.DataV1.LastProgress),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,7 +33,7 @@ func TestConfigurationProgress_default(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var builder = configstatus.ConfigProgressBuilder{}
|
var builder = configstatus.ConfigProgressBuilder{}
|
||||||
req := builder.New(config)
|
req := builder.New(config.Data)
|
||||||
|
|
||||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||||
require.Equal(t, "bridge_config_progress", req.Event)
|
require.Equal(t, "bridge_config_progress", req.Event)
|
||||||
@ -62,7 +62,7 @@ func TestConfigurationProgress_fed(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var builder = configstatus.ConfigProgressBuilder{}
|
var builder = configstatus.ConfigProgressBuilder{}
|
||||||
req := builder.New(config)
|
req := builder.New(config.Data)
|
||||||
|
|
||||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||||
require.Equal(t, "bridge_config_progress", req.Event)
|
require.Equal(t, "bridge_config_progress", req.Event)
|
||||||
|
|||||||
@ -19,6 +19,7 @@ package configstatus
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ConfigRecoveryValues struct {
|
type ConfigRecoveryValues struct {
|
||||||
@ -42,22 +43,19 @@ type ConfigRecoveryData struct {
|
|||||||
|
|
||||||
type ConfigRecoveryBuilder struct{}
|
type ConfigRecoveryBuilder struct{}
|
||||||
|
|
||||||
func (*ConfigRecoveryBuilder) New(config *ConfigurationStatus) ConfigRecoveryData {
|
func (*ConfigRecoveryBuilder) New(data *ConfigurationStatusData) ConfigRecoveryData {
|
||||||
config.DataLock.RLock()
|
|
||||||
defer config.DataLock.RUnlock()
|
|
||||||
|
|
||||||
return ConfigRecoveryData{
|
return ConfigRecoveryData{
|
||||||
MeasurementGroup: "bridge.any.configuration",
|
MeasurementGroup: "bridge.any.configuration",
|
||||||
Event: "bridge_config_recovery",
|
Event: "bridge_config_recovery",
|
||||||
Values: ConfigRecoveryValues{
|
Values: ConfigRecoveryValues{
|
||||||
Duration: config.isPendingSinceMin(),
|
Duration: int(time.Since(data.DataV1.PendingSince).Minutes()),
|
||||||
},
|
},
|
||||||
Dimensions: ConfigRecoveryDimensions{
|
Dimensions: ConfigRecoveryDimensions{
|
||||||
Autoconf: config.Data.DataV1.Autoconf,
|
Autoconf: data.DataV1.Autoconf,
|
||||||
ReportClick: strconv.FormatBool(config.Data.DataV1.ReportClick),
|
ReportClick: strconv.FormatBool(data.DataV1.ReportClick),
|
||||||
ReportSent: strconv.FormatBool(config.Data.DataV1.ReportSent),
|
ReportSent: strconv.FormatBool(data.DataV1.ReportSent),
|
||||||
ClickedLink: config.Data.clickedLinkToString(),
|
ClickedLink: data.clickedLinkToString(),
|
||||||
FailureDetails: config.Data.DataV1.FailureDetails,
|
FailureDetails: data.DataV1.FailureDetails,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,7 +33,7 @@ func TestConfigurationRecovery_default(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var builder = configstatus.ConfigRecoveryBuilder{}
|
var builder = configstatus.ConfigRecoveryBuilder{}
|
||||||
req := builder.New(config)
|
req := builder.New(config.Data)
|
||||||
|
|
||||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||||
require.Equal(t, "bridge_config_recovery", req.Event)
|
require.Equal(t, "bridge_config_recovery", req.Event)
|
||||||
@ -66,7 +66,7 @@ func TestConfigurationRecovery_fed(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var builder = configstatus.ConfigRecoveryBuilder{}
|
var builder = configstatus.ConfigRecoveryBuilder{}
|
||||||
req := builder.New(config)
|
req := builder.New(config.Data)
|
||||||
|
|
||||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||||
require.Equal(t, "bridge_config_recovery", req.Event)
|
require.Equal(t, "bridge_config_recovery", req.Event)
|
||||||
|
|||||||
@ -19,6 +19,7 @@ package configstatus
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ConfigSuccessValues struct {
|
type ConfigSuccessValues struct {
|
||||||
@ -41,21 +42,18 @@ type ConfigSuccessData struct {
|
|||||||
|
|
||||||
type ConfigSuccessBuilder struct{}
|
type ConfigSuccessBuilder struct{}
|
||||||
|
|
||||||
func (*ConfigSuccessBuilder) New(config *ConfigurationStatus) ConfigSuccessData {
|
func (*ConfigSuccessBuilder) New(data *ConfigurationStatusData) ConfigSuccessData {
|
||||||
config.DataLock.RLock()
|
|
||||||
defer config.DataLock.RUnlock()
|
|
||||||
|
|
||||||
return ConfigSuccessData{
|
return ConfigSuccessData{
|
||||||
MeasurementGroup: "bridge.any.configuration",
|
MeasurementGroup: "bridge.any.configuration",
|
||||||
Event: "bridge_config_success",
|
Event: "bridge_config_success",
|
||||||
Values: ConfigSuccessValues{
|
Values: ConfigSuccessValues{
|
||||||
Duration: config.isPendingSinceMin(),
|
Duration: int(time.Since(data.DataV1.PendingSince).Minutes()),
|
||||||
},
|
},
|
||||||
Dimensions: ConfigSuccessDimensions{
|
Dimensions: ConfigSuccessDimensions{
|
||||||
Autoconf: config.Data.DataV1.Autoconf,
|
Autoconf: data.DataV1.Autoconf,
|
||||||
ReportClick: strconv.FormatBool(config.Data.DataV1.ReportClick),
|
ReportClick: strconv.FormatBool(data.DataV1.ReportClick),
|
||||||
ReportSent: strconv.FormatBool(config.Data.DataV1.ReportSent),
|
ReportSent: strconv.FormatBool(data.DataV1.ReportSent),
|
||||||
ClickedLink: config.Data.clickedLinkToString(),
|
ClickedLink: data.clickedLinkToString(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,7 +33,7 @@ func TestConfigurationSuccess_default(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var builder = configstatus.ConfigSuccessBuilder{}
|
var builder = configstatus.ConfigSuccessBuilder{}
|
||||||
req := builder.New(config)
|
req := builder.New(config.Data)
|
||||||
|
|
||||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||||
require.Equal(t, "bridge_config_success", req.Event)
|
require.Equal(t, "bridge_config_success", req.Event)
|
||||||
@ -65,7 +65,7 @@ func TestConfigurationSuccess_fed(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var builder = configstatus.ConfigSuccessBuilder{}
|
var builder = configstatus.ConfigSuccessBuilder{}
|
||||||
req := builder.New(config)
|
req := builder.New(config.Data)
|
||||||
|
|
||||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||||
require.Equal(t, "bridge_config_success", req.Event)
|
require.Equal(t, "bridge_config_success", req.Event)
|
||||||
|
|||||||
@ -17,13 +17,7 @@
|
|||||||
|
|
||||||
package events
|
package events
|
||||||
|
|
||||||
import (
|
import "fmt"
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/ProtonMail/gluon/async"
|
|
||||||
"github.com/ProtonMail/gluon/watcher"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Event interface {
|
type Event interface {
|
||||||
fmt.Stringer
|
fmt.Stringer
|
||||||
@ -34,30 +28,3 @@ type Event interface {
|
|||||||
type eventBase struct{}
|
type eventBase struct{}
|
||||||
|
|
||||||
func (eventBase) _isEvent() {}
|
func (eventBase) _isEvent() {}
|
||||||
|
|
||||||
type EventPublisher interface {
|
|
||||||
PublishEvent(ctx context.Context, event Event)
|
|
||||||
}
|
|
||||||
|
|
||||||
type NullEventPublisher struct{}
|
|
||||||
|
|
||||||
func (NullEventPublisher) PublishEvent(_ context.Context, _ Event) {}
|
|
||||||
|
|
||||||
type Subscription interface {
|
|
||||||
Add(ofType ...Event) *watcher.Watcher[Event]
|
|
||||||
Remove(watcher *watcher.Watcher[Event])
|
|
||||||
}
|
|
||||||
|
|
||||||
type NullSubscription struct{}
|
|
||||||
|
|
||||||
func (n NullSubscription) Add(ofType ...Event) *watcher.Watcher[Event] {
|
|
||||||
return watcher.New[Event](&async.NoopPanicHandler{}, ofType...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n NullSubscription) Remove(watcher *watcher.Watcher[Event]) {
|
|
||||||
watcher.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewNullSubscription() *NullSubscription {
|
|
||||||
return &NullSubscription{}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,48 +0,0 @@
|
|||||||
// 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,22 +37,6 @@ func (event IMAPServerStopped) String() string {
|
|||||||
return "IMAPServerStopped"
|
return "IMAPServerStopped"
|
||||||
}
|
}
|
||||||
|
|
||||||
type IMAPServerClosed struct {
|
|
||||||
eventBase
|
|
||||||
}
|
|
||||||
|
|
||||||
func (event IMAPServerClosed) String() string {
|
|
||||||
return "IMAPServerClosed"
|
|
||||||
}
|
|
||||||
|
|
||||||
type IMAPServerCreated struct {
|
|
||||||
eventBase
|
|
||||||
}
|
|
||||||
|
|
||||||
func (event IMAPServerCreated) String() string {
|
|
||||||
return "IMAPServerCreated"
|
|
||||||
}
|
|
||||||
|
|
||||||
type IMAPServerError struct {
|
type IMAPServerError struct {
|
||||||
eventBase
|
eventBase
|
||||||
|
|
||||||
|
|||||||
@ -175,7 +175,7 @@ type UsedSpaceChanged struct {
|
|||||||
|
|
||||||
UserID string
|
UserID string
|
||||||
|
|
||||||
UsedSpace uint64
|
UsedSpace int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (event UsedSpaceChanged) String() string {
|
func (event UsedSpaceChanged) String() string {
|
||||||
|
|||||||
@ -42,7 +42,6 @@ void GRPCQtProxy::connectSignals() {
|
|||||||
connect(this, &GRPCQtProxy::setIsTelemetryDisabledReceived, &settingsTab, &SettingsTab::setIsTelemetryDisabled);
|
connect(this, &GRPCQtProxy::setIsTelemetryDisabledReceived, &settingsTab, &SettingsTab::setIsTelemetryDisabled);
|
||||||
connect(this, &GRPCQtProxy::setColorSchemeNameReceived, &settingsTab, &SettingsTab::setColorSchemeName);
|
connect(this, &GRPCQtProxy::setColorSchemeNameReceived, &settingsTab, &SettingsTab::setColorSchemeName);
|
||||||
connect(this, &GRPCQtProxy::reportBugReceived, &settingsTab, &SettingsTab::setBugReport);
|
connect(this, &GRPCQtProxy::reportBugReceived, &settingsTab, &SettingsTab::setBugReport);
|
||||||
connect(this, &GRPCQtProxy::installTLSCertificateReceived, &settingsTab, &SettingsTab::installTLSCertificate);
|
|
||||||
connect(this, &GRPCQtProxy::exportTLSCertificatesReceived, &settingsTab, &SettingsTab::exportTLSCertificates);
|
connect(this, &GRPCQtProxy::exportTLSCertificatesReceived, &settingsTab, &SettingsTab::exportTLSCertificates);
|
||||||
connect(this, &GRPCQtProxy::setIsStreamingReceived, &settingsTab, &SettingsTab::setIsStreaming);
|
connect(this, &GRPCQtProxy::setIsStreamingReceived, &settingsTab, &SettingsTab::setIsStreaming);
|
||||||
connect(this, &GRPCQtProxy::setClientPlatformReceived, &settingsTab, &SettingsTab::setClientPlatform);
|
connect(this, &GRPCQtProxy::setClientPlatformReceived, &settingsTab, &SettingsTab::setClientPlatform);
|
||||||
@ -120,13 +119,6 @@ void GRPCQtProxy::reportBug(QString const &osType, QString const &osVersion, QSt
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
//
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
void GRPCQtProxy::installTLSCertificate() {
|
|
||||||
emit installTLSCertificateReceived();
|
|
||||||
}
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \param[in] folderPath The folder path.
|
/// \param[in] folderPath The folder path.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
|
|||||||
@ -45,7 +45,6 @@ public: // member functions.
|
|||||||
void setColorSchemeName(QString const &name); ///< Forward a SetColorSchemeName call via a Qt Signal
|
void setColorSchemeName(QString const &name); ///< Forward a SetColorSchemeName call via a Qt Signal
|
||||||
void reportBug(QString const &osType, QString const &osVersion, QString const &emailClient, QString const &address,
|
void reportBug(QString const &osType, QString const &osVersion, QString const &emailClient, QString const &address,
|
||||||
QString const &description, bool includeLogs); ///< Forwards a ReportBug call via a Qt signal.
|
QString const &description, bool includeLogs); ///< Forwards a ReportBug call via a Qt signal.
|
||||||
void installTLSCertificate(); ///< Forwards a InstallTLScertificate call via a Qt signal.
|
|
||||||
void exportTLSCertificates(QString const &folderPath); //< Forward an 'ExportTLSCertificates' call via a Qt signal.
|
void exportTLSCertificates(QString const &folderPath); //< Forward an 'ExportTLSCertificates' call via a Qt signal.
|
||||||
void setIsStreaming(bool isStreaming); ///< Forward a isStreaming internal messages via a Qt signal.
|
void setIsStreaming(bool isStreaming); ///< Forward a isStreaming internal messages via a Qt signal.
|
||||||
void setClientPlatform(QString const &clientPlatform); ///< Forward a setClientPlatform call via a Qt signal.
|
void setClientPlatform(QString const &clientPlatform); ///< Forward a setClientPlatform call via a Qt signal.
|
||||||
@ -68,7 +67,6 @@ signals:
|
|||||||
void setColorSchemeNameReceived(QString const &name); ///< Forward a SetColorScheme call via a Qt Signal
|
void setColorSchemeNameReceived(QString const &name); ///< Forward a SetColorScheme call via a Qt Signal
|
||||||
void reportBugReceived(QString const &osType, QString const &osVersion, QString const &emailClient, QString const &address,
|
void reportBugReceived(QString const &osType, QString const &osVersion, QString const &emailClient, QString const &address,
|
||||||
QString const &description, bool includeLogs); ///< Signal for the ReportBug gRPC call
|
QString const &description, bool includeLogs); ///< Signal for the ReportBug gRPC call
|
||||||
void installTLSCertificateReceived(); ///< Signal for the InstallTLSCertificate gRPC call.
|
|
||||||
void exportTLSCertificatesReceived(QString const &folderPath); ///< Signal for the ExportTLSCertificates gRPC call.
|
void exportTLSCertificatesReceived(QString const &folderPath); ///< Signal for the ExportTLSCertificates gRPC call.
|
||||||
void setIsStreamingReceived(bool isStreaming); ///< Signal for the IsStreaming internal message.
|
void setIsStreamingReceived(bool isStreaming); ///< Signal for the IsStreaming internal message.
|
||||||
void setClientPlatformReceived(QString const &clientPlatform); ///< Signal for the SetClientPlatform gRPC call.
|
void setClientPlatformReceived(QString const &clientPlatform); ///< Signal for the SetClientPlatform gRPC call.
|
||||||
|
|||||||
@ -214,16 +214,6 @@ grpc::Status GRPCService::IsTelemetryDisabled(::grpc::ServerContext *, ::google:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
/// \param[out] response The response.
|
|
||||||
/// \return The status for the call.
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
Status GRPCService::GoOs(ServerContext *, Empty const*, StringValue *response) {
|
|
||||||
response->set_value(app().mainWindow().settingsTab().os().toStdString());
|
|
||||||
return Status::OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \return The status for the call.
|
/// \return The status for the call.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
@ -371,6 +361,22 @@ Status GRPCService::ReportBug(ServerContext *, ReportBugRequest const *request,
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \param[in] request The request
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
Status GRPCService::ExportTLSCertificates(ServerContext *, StringValue const *request, Empty *response) {
|
||||||
|
SettingsTab &tab = app().mainWindow().settingsTab();
|
||||||
|
if (!tab.nextTLSCertExportWillSucceed()) {
|
||||||
|
qtProxy_.sendDelayedEvent(newGenericErrorEvent(grpc::TLS_CERT_EXPORT_ERROR));
|
||||||
|
}
|
||||||
|
if (!tab.nextTLSKeyExportWillSucceed()) {
|
||||||
|
qtProxy_.sendDelayedEvent(newGenericErrorEvent(grpc::TLS_KEY_EXPORT_ERROR));
|
||||||
|
}
|
||||||
|
qtProxy_.exportTLSCertificates(QString::fromStdString(request->value()));
|
||||||
|
return Status::OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \param[in] request The request.
|
/// \param[in] request The request.
|
||||||
/// \return The status for the call.
|
/// \return The status for the call.
|
||||||
@ -379,14 +385,6 @@ Status GRPCService::Login(ServerContext *, LoginRequest const *request, Empty *)
|
|||||||
app().log().debug(__FUNCTION__);
|
app().log().debug(__FUNCTION__);
|
||||||
UsersTab &usersTab = app().mainWindow().usersTab();
|
UsersTab &usersTab = app().mainWindow().usersTab();
|
||||||
loginUsername_ = QString::fromStdString(request->username());
|
loginUsername_ = QString::fromStdString(request->username());
|
||||||
|
|
||||||
SPUser const& user = usersTab.userTable().userWithUsernameOrEmail(QString::fromStdString(request->username()));
|
|
||||||
if (user) {
|
|
||||||
qtProxy_.sendDelayedEvent(newLoginAlreadyLoggedInEvent(user->id()));
|
|
||||||
return Status::OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (usersTab.nextUserUsernamePasswordError()) {
|
if (usersTab.nextUserUsernamePasswordError()) {
|
||||||
qtProxy_.sendDelayedEvent(newLoginError(LoginErrorType::USERNAME_PASSWORD_ERROR, usersTab.usernamePasswordErrorMessage()));
|
qtProxy_.sendDelayedEvent(newLoginError(LoginErrorType::USERNAME_PASSWORD_ERROR, usersTab.usernamePasswordErrorMessage()));
|
||||||
return Status::OK;
|
return Status::OK;
|
||||||
@ -400,7 +398,7 @@ Status GRPCService::Login(ServerContext *, LoginRequest const *request, Empty *)
|
|||||||
return Status::OK;
|
return Status::OK;
|
||||||
}
|
}
|
||||||
if (usersTab.nextUserTwoPasswordsRequired()) {
|
if (usersTab.nextUserTwoPasswordsRequired()) {
|
||||||
qtProxy_.sendDelayedEvent(newLoginTwoPasswordsRequestedEvent(loginUsername_));
|
qtProxy_.sendDelayedEvent(newLoginTwoPasswordsRequestedEvent());
|
||||||
return Status::OK;
|
return Status::OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -425,7 +423,7 @@ Status GRPCService::Login2FA(ServerContext *, LoginRequest const *request, Empty
|
|||||||
return Status::OK;
|
return Status::OK;
|
||||||
}
|
}
|
||||||
if (usersTab.nextUserTwoPasswordsRequired()) {
|
if (usersTab.nextUserTwoPasswordsRequired()) {
|
||||||
qtProxy_.sendDelayedEvent(newLoginTwoPasswordsRequestedEvent(loginUsername_));
|
qtProxy_.sendDelayedEvent(newLoginTwoPasswordsRequestedEvent());
|
||||||
return Status::OK;
|
return Status::OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -750,87 +748,10 @@ Status GRPCService::ConfigureUserAppleMail(ServerContext *, ConfigureAppleMailRe
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
/// \param[in] request The request
|
|
||||||
/// \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] request The request
|
||||||
/// \param[in] writer The writer
|
/// \param[in] writer The writer
|
||||||
|
/// \return The status for the call.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
Status GRPCService::RunEventStream(ServerContext *ctx, EventStreamRequest const *request, ServerWriter<StreamEvent> *writer) {
|
Status GRPCService::RunEventStream(ServerContext *ctx, EventStreamRequest const *request, ServerWriter<StreamEvent> *writer) {
|
||||||
app().log().debug(__FUNCTION__);
|
app().log().debug(__FUNCTION__);
|
||||||
@ -905,7 +826,7 @@ bool GRPCService::sendEvent(SPStreamEvent const &event) {
|
|||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
void GRPCService::finishLogin() {
|
void GRPCService::finishLogin() {
|
||||||
UsersTab &usersTab = app().mainWindow().usersTab();
|
UsersTab &usersTab = app().mainWindow().usersTab();
|
||||||
SPUser user = usersTab.userWithUsernameOrEmail(loginUsername_);
|
SPUser user = usersTab.userWithUsername(loginUsername_);
|
||||||
bool const alreadyExist = user.get();
|
bool const alreadyExist = user.get();
|
||||||
if (!user) {
|
if (!user) {
|
||||||
user = randomUser();
|
user = randomUser();
|
||||||
@ -921,3 +842,4 @@ void GRPCService::finishLogin() {
|
|||||||
|
|
||||||
qtProxy_.sendDelayedEvent(newLoginFinishedEvent(user->id(), alreadyExist));
|
qtProxy_.sendDelayedEvent(newLoginFinishedEvent(user->id(), alreadyExist));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -53,7 +53,6 @@ public: // member functions.
|
|||||||
grpc::Status IsAllMailVisible(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::google::protobuf::BoolValue *response) override;
|
grpc::Status IsAllMailVisible(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::google::protobuf::BoolValue *response) override;
|
||||||
grpc::Status SetIsTelemetryDisabled(::grpc::ServerContext *, ::google::protobuf::BoolValue const *request, ::google::protobuf::Empty *response) override;
|
grpc::Status SetIsTelemetryDisabled(::grpc::ServerContext *, ::google::protobuf::BoolValue const *request, ::google::protobuf::Empty *response) override;
|
||||||
grpc::Status IsTelemetryDisabled(::grpc::ServerContext *, ::google::protobuf::Empty const *request, ::google::protobuf::BoolValue *response) override;
|
grpc::Status IsTelemetryDisabled(::grpc::ServerContext *, ::google::protobuf::Empty const *request, ::google::protobuf::BoolValue *response) override;
|
||||||
grpc::Status GoOs(::grpc::ServerContext *, ::google::protobuf::Empty const*, ::google::protobuf::StringValue *response) override;
|
|
||||||
grpc::Status TriggerReset(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::Empty *) override;
|
grpc::Status TriggerReset(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::Empty *) override;
|
||||||
grpc::Status Version(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::StringValue *response) override;
|
grpc::Status Version(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::StringValue *response) override;
|
||||||
grpc::Status LogsPath(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::StringValue *response) override;
|
grpc::Status LogsPath(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::StringValue *response) override;
|
||||||
@ -65,6 +64,7 @@ public: // member functions.
|
|||||||
grpc::Status ColorSchemeName(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::StringValue *response) override;
|
grpc::Status ColorSchemeName(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::StringValue *response) override;
|
||||||
grpc::Status CurrentEmailClient(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::StringValue *response) override;
|
grpc::Status CurrentEmailClient(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::StringValue *response) override;
|
||||||
grpc::Status ReportBug(::grpc::ServerContext *, ::grpc::ReportBugRequest const *request, ::google::protobuf::Empty *) override;
|
grpc::Status ReportBug(::grpc::ServerContext *, ::grpc::ReportBugRequest const *request, ::google::protobuf::Empty *) override;
|
||||||
|
grpc::Status ExportTLSCertificates(::grpc::ServerContext *context, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *response) override;
|
||||||
grpc::Status ForceLauncher(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
|
grpc::Status ForceLauncher(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
|
||||||
grpc::Status SetMainExecutable(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
|
grpc::Status SetMainExecutable(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
|
||||||
grpc::Status Login(::grpc::ServerContext *, ::grpc::LoginRequest const *request, ::google::protobuf::Empty *) override;
|
grpc::Status Login(::grpc::ServerContext *, ::grpc::LoginRequest const *request, ::google::protobuf::Empty *) override;
|
||||||
@ -93,12 +93,6 @@ public: // member functions.
|
|||||||
grpc::Status LogoutUser(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
|
grpc::Status LogoutUser(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
|
||||||
grpc::Status RemoveUser(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
|
grpc::Status RemoveUser(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
|
||||||
grpc::Status ConfigureUserAppleMail(::grpc::ServerContext *, ::grpc::ConfigureAppleMailRequest const *request, ::google::protobuf::Empty *) override;
|
grpc::Status ConfigureUserAppleMail(::grpc::ServerContext *, ::grpc::ConfigureAppleMailRequest const *request, ::google::protobuf::Empty *) override;
|
||||||
grpc::Status IsTLSCertificateInstalled(::grpc::ServerContext *, ::google::protobuf::Empty const*, ::google::protobuf::BoolValue *response) override;
|
|
||||||
grpc::Status InstallTLSCertificate(::grpc::ServerContext *, ::google::protobuf::Empty const*, ::google::protobuf::Empty *) override;
|
|
||||||
grpc::Status ExportTLSCertificates(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
|
|
||||||
grpc::Status ReportBugClicked(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::google::protobuf::Empty *) override;
|
|
||||||
grpc::Status AutoconfigClicked(::grpc::ServerContext *context, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
|
|
||||||
grpc::Status KBArticleClicked(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
|
|
||||||
grpc::Status RunEventStream(::grpc::ServerContext *ctx, ::grpc::EventStreamRequest const *request, ::grpc::ServerWriter<::grpc::StreamEvent> *writer) override;
|
grpc::Status RunEventStream(::grpc::ServerContext *ctx, ::grpc::EventStreamRequest const *request, ::grpc::ServerWriter<::grpc::StreamEvent> *writer) override;
|
||||||
grpc::Status StopEventStream(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::Empty *) override;
|
grpc::Status StopEventStream(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::Empty *) override;
|
||||||
bool sendEvent(bridgepp::SPStreamEvent const &event); ///< Queue an event for sending through the event stream.
|
bool sendEvent(bridgepp::SPStreamEvent const &event); ///< Queue an event for sending through the event stream.
|
||||||
|
|||||||
@ -285,20 +285,11 @@ void SettingsTab::setBugReport(QString const &osType, QString const &osVersion,
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
//
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
void SettingsTab::installTLSCertificate() {
|
|
||||||
ui_.labelLastTLSCertInstall->setText(QString("Last install: %1").arg(QDateTime::currentDateTime().toString(Qt::ISODateWithMs)));
|
|
||||||
ui_.checkTLSCertIsInstalled->setChecked(this->nextTLSCertIntallResult() == TLSCertInstallResult::Success);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \param[in] folderPath The folder path.
|
/// \param[in] folderPath The folder path.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
void SettingsTab::exportTLSCertificates(QString const &folderPath) {
|
void SettingsTab::exportTLSCertificates(QString const &folderPath) {
|
||||||
ui_.labeLastTLSCertExport->setText(QString("%1 Export to %2")
|
ui_.labeLastTLSCertsExport->setText(QString("%1 Export to %2")
|
||||||
.arg(QDateTime::currentDateTime().toString(Qt::ISODateWithMs))
|
.arg(QDateTime::currentDateTime().toString(Qt::ISODateWithMs))
|
||||||
.arg(folderPath));
|
.arg(folderPath));
|
||||||
}
|
}
|
||||||
@ -312,22 +303,6 @@ bool SettingsTab::nextBugReportWillSucceed() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
/// \return the state of the 'TLS Certificate is installed' check box.
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
bool SettingsTab::isTLSCertificateInstalled() const {
|
|
||||||
return ui_.checkTLSCertIsInstalled->isChecked();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
/// \return The value for the 'Next TLS cert install result'.
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
SettingsTab::TLSCertInstallResult SettingsTab::nextTLSCertIntallResult() const {
|
|
||||||
return TLSCertInstallResult(ui_.comboNextTLSCertInstallResult->currentIndex());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \return true if the 'Next TLS key export will succeed' check box is checked
|
/// \return true if the 'Next TLS key export will succeed' check box is checked
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
@ -530,11 +505,4 @@ void SettingsTab::resetUI() {
|
|||||||
ui_.comboCacheError->setCurrentIndex(0);
|
ui_.comboCacheError->setCurrentIndex(0);
|
||||||
|
|
||||||
ui_.checkAutomaticUpdate->setChecked(true);
|
ui_.checkAutomaticUpdate->setChecked(true);
|
||||||
|
|
||||||
ui_.checkTLSCertIsInstalled->setChecked(false);
|
|
||||||
ui_.comboNextTLSCertInstallResult->setCurrentIndex(0);
|
|
||||||
ui_.checkTLSCertExportWillSucceed->setChecked(true);
|
|
||||||
ui_.checkTLSKeyExportWillSucceed->setChecked(true);
|
|
||||||
ui_.labeLastTLSCertExport->setText("Last export: never");
|
|
||||||
ui_.labelLastTLSCertInstall->setText("Last install: never");
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,13 +28,6 @@
|
|||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
class SettingsTab : public QWidget {
|
class SettingsTab : public QWidget {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public: // data types.
|
|
||||||
enum class TLSCertInstallResult {
|
|
||||||
Success = 0,
|
|
||||||
Canceled = 1,
|
|
||||||
Failure = 2
|
|
||||||
}; ///< Enumberation for the result of a TLS certificate installation.
|
|
||||||
|
|
||||||
public: // member functions.
|
public: // member functions.
|
||||||
explicit SettingsTab(QWidget *parent = nullptr); ///< Default constructor.
|
explicit SettingsTab(QWidget *parent = nullptr); ///< Default constructor.
|
||||||
SettingsTab(SettingsTab const &) = delete; ///< Disabled copy-constructor.
|
SettingsTab(SettingsTab const &) = delete; ///< Disabled copy-constructor.
|
||||||
@ -61,8 +54,6 @@ public: // member functions.
|
|||||||
QString dependencyLicenseLink() const; ///< Get the content of the 'Dependency License Link' edit.
|
QString dependencyLicenseLink() const; ///< Get the content of the 'Dependency License Link' edit.
|
||||||
QString landingPageLink() const; ///< Get the content of the 'Landing Page Link' edit.
|
QString landingPageLink() const; ///< Get the content of the 'Landing Page Link' edit.
|
||||||
bool nextBugReportWillSucceed() const; ///< Get the status of the 'Next Bug Report Will Fail' check box.
|
bool nextBugReportWillSucceed() const; ///< Get the status of the 'Next Bug Report Will Fail' check box.
|
||||||
bool isTLSCertificateInstalled() const; ///< Get the status of the 'TLS Certificate is installed' check box.
|
|
||||||
TLSCertInstallResult nextTLSCertIntallResult() const; ///< Get the value of the 'Next TLS Certificate install result' combo box.
|
|
||||||
bool nextTLSCertExportWillSucceed() const; ///< Get the status of the 'Next TLS Cert export will succeed' check box.
|
bool nextTLSCertExportWillSucceed() const; ///< Get the status of the 'Next TLS Cert export will succeed' check box.
|
||||||
bool nextTLSKeyExportWillSucceed() const; ///< Get the status of the 'Next TLS Key export will succeed' check box.
|
bool nextTLSKeyExportWillSucceed() const; ///< Get the status of the 'Next TLS Key export will succeed' check box.
|
||||||
QString hostname() const; ///< Get the value of the 'Hostname' edit.
|
QString hostname() const; ///< Get the value of the 'Hostname' edit.
|
||||||
@ -88,7 +79,6 @@ public slots:
|
|||||||
void setColorSchemeName(QString const &name); ///< Set the value for the 'Use Dark Theme' check box.
|
void setColorSchemeName(QString const &name); ///< Set the value for the 'Use Dark Theme' check box.
|
||||||
void setBugReport(QString const &osType, QString const &osVersion, QString const &emailClient, QString const &address, QString const &description,
|
void setBugReport(QString const &osType, QString const &osVersion, QString const &emailClient, QString const &address, QString const &description,
|
||||||
bool includeLogs); ///< Set the content of the bug report box.
|
bool includeLogs); ///< Set the content of the bug report box.
|
||||||
void installTLSCertificate(); ///< Install the TLS certificate.
|
|
||||||
void exportTLSCertificates(QString const &folderPath); ///< Export the TLS certificates.
|
void exportTLSCertificates(QString const &folderPath); ///< Export the TLS certificates.
|
||||||
void setMailServerSettings(qint32 imapPort, qint32 smtpPort, bool useSSLForIMAP, bool useSSLForSMTP); ///< Change the mail server settings.
|
void setMailServerSettings(qint32 imapPort, qint32 smtpPort, bool useSSLForIMAP, bool useSSLForSMTP); ///< Change the mail server settings.
|
||||||
void setIsDoHEnabled(bool enabled); ///< Set the value for the 'DoH Enabled' check box.
|
void setIsDoHEnabled(bool enabled); ///< Set the value for the 'DoH Enabled' check box.
|
||||||
|
|||||||
@ -370,7 +370,7 @@
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QGroupBox" name="groupCert">
|
<widget class="QGroupBox" name="groupCache_2">
|
||||||
<property name="minimumSize">
|
<property name="minimumSize">
|
||||||
<size>
|
<size>
|
||||||
<width>0</width>
|
<width>0</width>
|
||||||
@ -380,81 +380,34 @@
|
|||||||
<property name="title">
|
<property name="title">
|
||||||
<string>TLS Certficates</string>
|
<string>TLS Certficates</string>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QGridLayout" name="gridLayout_4" columnstretch="1,1">
|
<layout class="QVBoxLayout" name="verticalLayout_9">
|
||||||
<item row="0" column="0">
|
<item>
|
||||||
<widget class="QCheckBox" name="checkTLSCertIsInstalled">
|
<widget class="QLabel" name="labeLastTLSCertsExport">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Certificate is installed</string>
|
<string>Last Export: Never</string>
|
||||||
</property>
|
|
||||||
<property name="checked">
|
|
||||||
<bool>false</bool>
|
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="0" column="1">
|
<item>
|
||||||
<widget class="QCheckBox" name="checkTLSCertExportWillSucceed">
|
<widget class="QCheckBox" name="checkTLSCertExportWillSucceed">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Certificate export will succeed</string>
|
<string>TLS certificate export will succeed</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="checked">
|
<property name="checked">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="1">
|
<item>
|
||||||
<widget class="QCheckBox" name="checkTLSKeyExportWillSucceed">
|
<widget class="QCheckBox" name="checkTLSKeyExportWillSucceed">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Key export will succeed</string>
|
<string>TLS private key export will succeed</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="checked">
|
<property name="checked">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="2" column="1">
|
|
||||||
<widget class="QLabel" name="labeLastTLSCertExport">
|
|
||||||
<property name="text">
|
|
||||||
<string>Last Export: never</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="2" column="0">
|
|
||||||
<widget class="QLabel" name="labelLastTLSCertInstall">
|
|
||||||
<property name="text">
|
|
||||||
<string>Last install: never</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="0">
|
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout_14" stretch="0,1">
|
|
||||||
<item>
|
|
||||||
<widget class="QLabel" name="labelNextInstall">
|
|
||||||
<property name="text">
|
|
||||||
<string>Next install will</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QComboBox" name="comboNextTLSCertInstallResult">
|
|
||||||
<item>
|
|
||||||
<property name="text">
|
|
||||||
<string>Succeed</string>
|
|
||||||
</property>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<property name="text">
|
|
||||||
<string>Be Canceled</string>
|
|
||||||
</property>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<property name="text">
|
|
||||||
<string>Fail</string>
|
|
||||||
</property>
|
|
||||||
</item>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</item>
|
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
|||||||
@ -272,8 +272,8 @@ bridgepp::SPUser UsersTab::userWithID(QString const &userID) {
|
|||||||
/// \return The user with the given username.
|
/// \return The user with the given username.
|
||||||
/// \return A null pointer if the user is not in the list.
|
/// \return A null pointer if the user is not in the list.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
bridgepp::SPUser UsersTab::userWithUsernameOrEmail(QString const &username) {
|
bridgepp::SPUser UsersTab::userWithUsername(QString const &username) {
|
||||||
return users_.userWithUsernameOrEmail(username);
|
return users_.userWithUsername(username);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -38,7 +38,7 @@ public: // member functions.
|
|||||||
UsersTab &operator=(UsersTab &&) = delete; ///< Disabled move assignment operator.
|
UsersTab &operator=(UsersTab &&) = delete; ///< Disabled move assignment operator.
|
||||||
UserTable &userTable(); ///< Returns a reference to the user table.
|
UserTable &userTable(); ///< Returns a reference to the user table.
|
||||||
bridgepp::SPUser userWithID(QString const &userID); ///< Get the user with the given ID.
|
bridgepp::SPUser userWithID(QString const &userID); ///< Get the user with the given ID.
|
||||||
bridgepp::SPUser userWithUsernameOrEmail(QString const &username); ///< Get the user with the given username.
|
bridgepp::SPUser userWithUsername(QString const &username); ///< Get the user with the given username.
|
||||||
bool nextUserUsernamePasswordError() const; ///< Check if next user login should trigger a username/password error.
|
bool nextUserUsernamePasswordError() const; ///< Check if next user login should trigger a username/password error.
|
||||||
bool nextUserFreeUserError() const; ///< Check if next user login should trigger a Free user error.
|
bool nextUserFreeUserError() const; ///< Check if next user login should trigger a Free user error.
|
||||||
bool nextUserTFARequired() const; ///< Check if next user login should requires 2FA.
|
bool nextUserTFARequired() const; ///< Check if next user login should requires 2FA.
|
||||||
|
|||||||
@ -150,16 +150,13 @@ bridgepp::SPUser UserTable::userWithID(QString const &userID) {
|
|||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \param[in] username The username, or any email address attached to the account.
|
/// \param[in] username The username.
|
||||||
/// \return The user with the given username.
|
/// \return The user with the given username.
|
||||||
/// \return A null pointer if the user is not in the list.
|
/// \return A null pointer if the user is not in the list.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
bridgepp::SPUser UserTable::userWithUsernameOrEmail(QString const &username) {
|
bridgepp::SPUser UserTable::userWithUsername(QString const &username) {
|
||||||
QList<SPUser>::const_iterator it = std::find_if(users_.constBegin(), users_.constEnd(), [&username](SPUser const &user) -> bool {
|
QList<SPUser>::const_iterator it = std::find_if(users_.constBegin(), users_.constEnd(), [&username](SPUser const &user) -> bool {
|
||||||
if (user->username().compare(username, Qt::CaseInsensitive) == 0) {
|
return user->username() == username;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return user->addresses().contains(username, Qt::CaseInsensitive);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return it == users_.end() ? nullptr : *it;
|
return it == users_.end() ? nullptr : *it;
|
||||||
|
|||||||
@ -40,7 +40,7 @@ public: // member functions.
|
|||||||
void append(bridgepp::SPUser const &user); ///< Append a user.
|
void append(bridgepp::SPUser const &user); ///< Append a user.
|
||||||
bridgepp::SPUser userAtIndex(qint32 index); ///< Return the user at the given index.
|
bridgepp::SPUser userAtIndex(qint32 index); ///< Return the user at the given index.
|
||||||
bridgepp::SPUser userWithID(QString const &userID); ///< Return the user with a given id.
|
bridgepp::SPUser userWithID(QString const &userID); ///< Return the user with a given id.
|
||||||
bridgepp::SPUser userWithUsernameOrEmail(QString const &username); ///< Return the user with a given username.
|
bridgepp::SPUser userWithUsername(QString const &username); ///< Return the user with a given username.
|
||||||
qint32 indexOfUser(QString const &userID); ///< Return the index of a given User.
|
qint32 indexOfUser(QString const &userID); ///< Return the index of a given User.
|
||||||
void touch(qint32 index); ///< touch the user at a given index (indicates it has been modified).
|
void touch(qint32 index); ///< touch the user at a given index (indicates it has been modified).
|
||||||
void touch(QString const& userID); ///< touch the user with the given userID (indicates it has been modified).
|
void touch(QString const& userID); ///< touch the user with the given userID (indicates it has been modified).
|
||||||
|
|||||||
@ -20,7 +20,6 @@
|
|||||||
#include "QMLBackend.h"
|
#include "QMLBackend.h"
|
||||||
#include "SentryUtils.h"
|
#include "SentryUtils.h"
|
||||||
#include "Settings.h"
|
#include "Settings.h"
|
||||||
#include <bridgepp/CLI/CLIUtils.h>
|
|
||||||
#include <bridgepp/GRPC/GRPCClient.h>
|
#include <bridgepp/GRPC/GRPCClient.h>
|
||||||
#include <bridgepp/Exception/Exception.h>
|
#include <bridgepp/Exception/Exception.h>
|
||||||
#include <bridgepp/ProcessMonitor.h>
|
#include <bridgepp/ProcessMonitor.h>
|
||||||
@ -102,23 +101,19 @@ void AppController::onFatalError(Exception const &exception) {
|
|||||||
qApp->exit(EXIT_FAILURE);
|
qApp->exit(EXIT_FAILURE);
|
||||||
}
|
}
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
/// \param[in] isCrashing Is the restart triggered by a crash.
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
void AppController::restart(bool isCrashing) {
|
void AppController::restart(bool isCrashing) {
|
||||||
if (launcher_.isEmpty()) {
|
if (!launcher_.isEmpty()) {
|
||||||
return;
|
QProcess p;
|
||||||
}
|
log_->info(QString("Restarting - App : %1 - Args : %2").arg(launcher_, launcherArgs_.join(" ")));
|
||||||
|
QStringList args = launcherArgs_;
|
||||||
|
if (isCrashing) {
|
||||||
|
args.append(noWindowFlag);
|
||||||
|
}
|
||||||
|
|
||||||
QProcess p;
|
p.startDetached(launcher_, args);
|
||||||
QStringList args = stripStringParameterFromCommandLine("--session-id", launcherArgs_);
|
p.waitForStarted();
|
||||||
if (isCrashing) {
|
|
||||||
args.append(noWindowFlag);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log_->info(QString("Restarting - App : %1 - Args : %2").arg(launcher_, args.join(" ")));
|
|
||||||
p.startDetached(launcher_, args);
|
|
||||||
p.waitForStarted();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -37,16 +37,6 @@
|
|||||||
using namespace bridgepp;
|
using namespace bridgepp;
|
||||||
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
|
|
||||||
QString const bugReportFile = ":qml/Resources/bug_report_flow.json";
|
|
||||||
QString const bridgeKBUrl = "https://proton.me/support/bridge"; ///< The URL for the root of the bridge knowledge base.
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
//
|
//
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
@ -99,8 +89,6 @@ void QMLBackend::init(GRPCConfig const &serviceConfig) {
|
|||||||
this->setUseSSLForIMAP(sslForIMAP);
|
this->setUseSSLForIMAP(sslForIMAP);
|
||||||
this->setUseSSLForSMTP(sslForSMTP);
|
this->setUseSSLForSMTP(sslForSMTP);
|
||||||
this->retrieveUserList();
|
this->retrieveUserList();
|
||||||
if (!reportFlow_.parse(bugReportFile))
|
|
||||||
app().log().error(QString("Cannot parse BugReportFlow description file: %1").arg(bugReportFile));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -222,87 +210,6 @@ bool QMLBackend::areSameFileOrFolder(QUrl const &lhs, QUrl const &rhs) const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
/// \param[in] categoryId The id of the bug category.
|
|
||||||
/// \return Set of question for this category.
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
QString QMLBackend::getBugCategory(quint8 categoryId) const {
|
|
||||||
return reportFlow_.getCategory(categoryId);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
/// \param[in] categoryId The id of the bug category.
|
|
||||||
/// \return Set of question for this category.
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
QVariantList QMLBackend::getQuestionSet(quint8 categoryId) const {
|
|
||||||
QVariantList list = reportFlow_.questionSet(categoryId);
|
|
||||||
if (list.count() == 0)
|
|
||||||
app().log().error(QString("Bug category not found (id: %1)").arg(categoryId));
|
|
||||||
return list;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
/// \param[in] questionId The id of the question.
|
|
||||||
/// \param[in] answer The answer to that question.
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
void QMLBackend::setQuestionAnswer(quint8 questionId, QString const &answer) {
|
|
||||||
if (!reportFlow_.setAnswer(questionId, answer))
|
|
||||||
app().log().error(QString("Bug Report Question not found (id: %1)").arg(questionId));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
/// \param[in] questionId The id of the question.
|
|
||||||
/// \return answer for the given question.
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
QString QMLBackend::getQuestionAnswer(quint8 questionId) const {
|
|
||||||
return reportFlow_.getAnswer(questionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
/// \param[in] categoryId The id of the question set.
|
|
||||||
/// \return concatenate answers for set of questions.
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
QString QMLBackend::collectAnswers(quint8 categoryId) const {
|
|
||||||
return reportFlow_.collectAnswers(categoryId);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
//
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
void QMLBackend::clearAnswers() {
|
|
||||||
reportFlow_.clearAnswers();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
/// \return true iff the Bridge TLS certificate is installed.
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
bool QMLBackend::isTLSCertificateInstalled() {
|
|
||||||
HANDLE_EXCEPTION_RETURN_BOOL(
|
|
||||||
bool v = false;
|
|
||||||
app().grpc().isTLSCertificateInstalled(v);
|
|
||||||
return v;
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
/// \param[in] url The URL of the knowledge base article. If empty/invalid, the home page for the Bridge knowledge base is opened.
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
void QMLBackend::openKBArticle(QString const &url) {
|
|
||||||
HANDLE_EXCEPTION(
|
|
||||||
QString const u = url.isEmpty() ? bridgeKBUrl : url;
|
|
||||||
QDesktopServices::openUrl(u);
|
|
||||||
emit notifyKBArticleClicked(u);
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \return The value for the 'showOnStartup' property.
|
/// \return The value for the 'showOnStartup' property.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
@ -674,21 +581,6 @@ QStringList QMLBackend::availableKeychain() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
/// \return The value for the 'bugCategories' property.
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
QVariantList QMLBackend::bugCategories() const {
|
|
||||||
return reportFlow_.categories();
|
|
||||||
}
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
/// \return The value for the 'bugQuestions' property.
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
QVariantList QMLBackend::bugQuestions() const {
|
|
||||||
return reportFlow_.questions();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \return The value for the 'currentKeychain' property.
|
/// \return The value for the 'currentKeychain' property.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
@ -953,28 +845,18 @@ void QMLBackend::triggerReset() const {
|
|||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \param[in] category The category of the bug.
|
|
||||||
/// \param[in] description The description of the bug.
|
/// \param[in] description The description of the bug.
|
||||||
/// \param[in] address The email address.
|
/// \param[in] address The email address.
|
||||||
/// \param[in] emailClient The email client.
|
/// \param[in] emailClient The email client.
|
||||||
/// \param[in] includeLogs Should the logs be included in the report.
|
/// \param[in] includeLogs Should the logs be included in the report.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
void QMLBackend::reportBug(QString const &category, QString const &description, QString const &address, QString const &emailClient, bool includeLogs) const {
|
void QMLBackend::reportBug(QString const &description, QString const &address, QString const &emailClient, bool includeLogs) const {
|
||||||
HANDLE_EXCEPTION(
|
HANDLE_EXCEPTION(
|
||||||
app().grpc().reportBug(category, description, address, emailClient, includeLogs);
|
app().grpc().reportBug(description, address, emailClient, includeLogs);
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
//
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
void QMLBackend::installTLSCertificate() {
|
|
||||||
HANDLE_EXCEPTION(
|
|
||||||
app().grpc().installTLSCertificate();
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
//
|
//
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
@ -1299,11 +1181,7 @@ void QMLBackend::connectGrpcEvents() {
|
|||||||
connect(client, &GRPCClient::resetFinished, this, &QMLBackend::onResetFinished);
|
connect(client, &GRPCClient::resetFinished, this, &QMLBackend::onResetFinished);
|
||||||
connect(client, &GRPCClient::reportBugFinished, this, &QMLBackend::reportBugFinished);
|
connect(client, &GRPCClient::reportBugFinished, this, &QMLBackend::reportBugFinished);
|
||||||
connect(client, &GRPCClient::reportBugSuccess, this, &QMLBackend::bugReportSendSuccess);
|
connect(client, &GRPCClient::reportBugSuccess, this, &QMLBackend::bugReportSendSuccess);
|
||||||
connect(client, &GRPCClient::reportBugFallback, this, &QMLBackend::bugReportSendFallback);
|
|
||||||
connect(client, &GRPCClient::reportBugError, this, &QMLBackend::bugReportSendError);
|
connect(client, &GRPCClient::reportBugError, this, &QMLBackend::bugReportSendError);
|
||||||
connect(client, &GRPCClient::certificateInstallSuccess, this, &QMLBackend::certificateInstallSuccess);
|
|
||||||
connect(client, &GRPCClient::certificateInstallCanceled, this, &QMLBackend::certificateInstallCanceled);
|
|
||||||
connect(client, &GRPCClient::certificateInstallFailed, this, &QMLBackend::certificateInstallFailed);
|
|
||||||
connect(client, &GRPCClient::showMainWindow, [&]() { this->showMainWindow("gRPC showMainWindow event"); });
|
connect(client, &GRPCClient::showMainWindow, [&]() { this->showMainWindow("gRPC showMainWindow event"); });
|
||||||
|
|
||||||
// cache events
|
// cache events
|
||||||
|
|||||||
@ -24,7 +24,6 @@
|
|||||||
#include "BuildConfig.h"
|
#include "BuildConfig.h"
|
||||||
#include "TrayIcon.h"
|
#include "TrayIcon.h"
|
||||||
#include "UserList.h"
|
#include "UserList.h"
|
||||||
#include <bridgepp/BugReportFlow/BugReportFlow.h>
|
|
||||||
#include <bridgepp/GRPC/GRPCClient.h>
|
#include <bridgepp/GRPC/GRPCClient.h>
|
||||||
#include <bridgepp/GRPC/GRPCUtils.h>
|
#include <bridgepp/GRPC/GRPCUtils.h>
|
||||||
#include <bridgepp/Worker/Overseer.h>
|
#include <bridgepp/Worker/Overseer.h>
|
||||||
@ -52,20 +51,12 @@ public: // member functions.
|
|||||||
void showSettings(QString const &reason); ///< Show the settings page.
|
void showSettings(QString const &reason); ///< Show the settings page.
|
||||||
void selectUser(QString const &userID, bool forceShowWindow, QString const &reason); ///< Select the user and display its account details (or login screen).
|
void selectUser(QString const &userID, bool forceShowWindow, QString const &reason); ///< Select the user and display its account details (or login screen).
|
||||||
|
|
||||||
// invocable methods can be called from QML. They generally return a value, which slots cannot do.
|
// invokable methods can be called from QML. They generally return a value, which slots cannot do.
|
||||||
Q_INVOKABLE static QString buildYear(); ///< Return the application build year.
|
Q_INVOKABLE static QString buildYear(); ///< Return the application build year.
|
||||||
Q_INVOKABLE QPoint getCursorPos() const; ///< Retrieve the cursor position.
|
Q_INVOKABLE QPoint getCursorPos() const; ///< Retrieve the cursor position.
|
||||||
Q_INVOKABLE bool isPortFree(int port) const; ///< Check if a given network port is available.
|
Q_INVOKABLE bool isPortFree(int port) const; ///< Check if a given network port is available.
|
||||||
Q_INVOKABLE QString nativePath(QUrl const &url) const; ///< Retrieve the native path of a local URL.
|
Q_INVOKABLE QString nativePath(QUrl const &url) const; ///< Retrieve the native path of a local URL.
|
||||||
Q_INVOKABLE bool areSameFileOrFolder(QUrl const &lhs, QUrl const &rhs) const; ///< Check if two local URL point to the same file.
|
Q_INVOKABLE bool areSameFileOrFolder(QUrl const &lhs, QUrl const &rhs) const; ///< Check if two local URL point to the same file.
|
||||||
Q_INVOKABLE QString getBugCategory(quint8 categoryId) const; ///< Get a Category name.
|
|
||||||
Q_INVOKABLE QVariantList getQuestionSet(quint8 categoryId) const; ///< Retrieve the set of question for a given bug category.
|
|
||||||
Q_INVOKABLE void setQuestionAnswer(quint8 questionId, QString const &answer); ///< Feed an answer for a given question.
|
|
||||||
Q_INVOKABLE QString getQuestionAnswer(quint8 questionId) const; ///< Get the answer for a given question.
|
|
||||||
Q_INVOKABLE QString collectAnswers(quint8 categoryId) const; ///< Collect answer for a given set of questions.
|
|
||||||
Q_INVOKABLE void clearAnswers(); ///< Clear all collected answers.
|
|
||||||
Q_INVOKABLE bool isTLSCertificateInstalled(); ///< Check if the bridge certificate is installed in the OS keychain.
|
|
||||||
Q_INVOKABLE void openKBArticle(QString const & url = QString()); ///< Open a knowledge base article.
|
|
||||||
|
|
||||||
public: // Qt/QML properties. Note that the NOTIFY-er signal is required even for read-only properties (QML warning otherwise)
|
public: // Qt/QML properties. Note that the NOTIFY-er signal is required even for read-only properties (QML warning otherwise)
|
||||||
Q_PROPERTY(bool showOnStartup READ showOnStartup NOTIFY showOnStartupChanged)
|
Q_PROPERTY(bool showOnStartup READ showOnStartup NOTIFY showOnStartupChanged)
|
||||||
@ -96,8 +87,6 @@ public: // Qt/QML properties. Note that the NOTIFY-er signal is required even fo
|
|||||||
Q_PROPERTY(QString currentEmailClient READ currentEmailClient NOTIFY currentEmailClientChanged)
|
Q_PROPERTY(QString currentEmailClient READ currentEmailClient NOTIFY currentEmailClientChanged)
|
||||||
Q_PROPERTY(QStringList availableKeychain READ availableKeychain NOTIFY availableKeychainChanged)
|
Q_PROPERTY(QStringList availableKeychain READ availableKeychain NOTIFY availableKeychainChanged)
|
||||||
Q_PROPERTY(QString currentKeychain READ currentKeychain NOTIFY currentKeychainChanged)
|
Q_PROPERTY(QString currentKeychain READ currentKeychain NOTIFY currentKeychainChanged)
|
||||||
Q_PROPERTY(QVariantList bugCategories READ bugCategories NOTIFY bugCategoriesChanged)
|
|
||||||
Q_PROPERTY(QVariantList bugQuestions READ bugQuestions NOTIFY bugQuestionsChanged)
|
|
||||||
Q_PROPERTY(UserList *users MEMBER users_ NOTIFY usersChanged)
|
Q_PROPERTY(UserList *users MEMBER users_ NOTIFY usersChanged)
|
||||||
Q_PROPERTY(bool dockIconVisible READ dockIconVisible WRITE setDockIconVisible NOTIFY dockIconVisibleChanged)
|
Q_PROPERTY(bool dockIconVisible READ dockIconVisible WRITE setDockIconVisible NOTIFY dockIconVisibleChanged)
|
||||||
|
|
||||||
@ -135,8 +124,6 @@ public: // Qt/QML properties. Note that the NOTIFY-er signal is required even fo
|
|||||||
QString currentEmailClient() const; ///< Getter for the 'currentEmail' property.
|
QString currentEmailClient() const; ///< Getter for the 'currentEmail' property.
|
||||||
QStringList availableKeychain() const; ///< Getter for the 'availableKeychain' property.
|
QStringList availableKeychain() const; ///< Getter for the 'availableKeychain' property.
|
||||||
QString currentKeychain() const; ///< Getter for the 'currentKeychain' property.
|
QString currentKeychain() const; ///< Getter for the 'currentKeychain' property.
|
||||||
QVariantList bugCategories() const; ///< Getter for the 'bugCategories' property.
|
|
||||||
QVariantList bugQuestions() const; ///< Getter for the 'bugQuestions' property.
|
|
||||||
void setDockIconVisible(bool visible); ///< Setter for the 'dockIconVisible' property.
|
void setDockIconVisible(bool visible); ///< Setter for the 'dockIconVisible' property.
|
||||||
bool dockIconVisible() const;; ///< Getter for the 'dockIconVisible' property.
|
bool dockIconVisible() const;; ///< Getter for the 'dockIconVisible' property.
|
||||||
|
|
||||||
@ -166,8 +153,6 @@ signals: // Signal used by the Qt property system. Many of them are unused but r
|
|||||||
void tagChanged(QString const &tag); ///<Signal for the change of the 'tag' property.
|
void tagChanged(QString const &tag); ///<Signal for the change of the 'tag' property.
|
||||||
void currentEmailClientChanged(QString const &email); ///<Signal for the change of the 'currentEmailClient' property.
|
void currentEmailClientChanged(QString const &email); ///<Signal for the change of the 'currentEmailClient' property.
|
||||||
void currentKeychainChanged(QString const &keychain); ///<Signal for the change of the 'currentKeychain' property.
|
void currentKeychainChanged(QString const &keychain); ///<Signal for the change of the 'currentKeychain' property.
|
||||||
void bugCategoriesChanged(QVariantList const &bugCategories); ///<Signal for the change of the 'bugCategories' property.
|
|
||||||
void bugQuestionsChanged(QVariantList const &bugQuestions); ///<Signal for the change of the 'bugQuestions' property.
|
|
||||||
void availableKeychainChanged(QStringList const &keychains); ///<Signal for the change of the 'availableKeychain' property.
|
void availableKeychainChanged(QStringList const &keychains); ///<Signal for the change of the 'availableKeychain' property.
|
||||||
void hostnameChanged(QString const &hostname); ///<Signal for the change of the 'hostname' property.
|
void hostnameChanged(QString const &hostname); ///<Signal for the change of the 'hostname' property.
|
||||||
void isAutostartOnChanged(bool value); ///<Signal for the change of the 'isAutostartOn' property.
|
void isAutostartOnChanged(bool value); ///<Signal for the change of the 'isAutostartOn' property.
|
||||||
@ -196,8 +181,7 @@ public slots: // slot for signals received from QML -> To be forwarded to Bridge
|
|||||||
void checkUpdates() const; ///< Slot for the update check.
|
void checkUpdates() const; ///< Slot for the update check.
|
||||||
void installUpdate() const; ///< Slot for the update install.
|
void installUpdate() const; ///< Slot for the update install.
|
||||||
void triggerReset() const; ///< Slot for the triggering of reset.
|
void triggerReset() const; ///< Slot for the triggering of reset.
|
||||||
void reportBug(QString const &category, QString const &description, QString const &address, QString const &emailClient, bool includeLogs) const; ///< Slot for the bug report.
|
void reportBug(QString const &description, QString const &address, QString const &emailClient, bool includeLogs) const; ///< Slot for the bug report.
|
||||||
void installTLSCertificate(); ///< Installs the Bridge TLS certificate in the Keychain.
|
|
||||||
void exportTLSCertificates() const; ///< Slot for the export of the TLS certificates.
|
void exportTLSCertificates() const; ///< Slot for the export of the TLS certificates.
|
||||||
void onResetFinished(); ///< Slot for the reset finish signal.
|
void onResetFinished(); ///< Slot for the reset finish signal.
|
||||||
void onVersionChanged(); ///< Slot for the version change signal.
|
void onVersionChanged(); ///< Slot for the version change signal.
|
||||||
@ -234,7 +218,7 @@ signals: // Signals received from the Go backend, to be forwarded to QML
|
|||||||
void login2FARequested(QString const &username); ///< Signal for the 'login2FARequested' gRPC stream event.
|
void login2FARequested(QString const &username); ///< Signal for the 'login2FARequested' gRPC stream event.
|
||||||
void login2FAError(QString const &errorMsg); ///< Signal for the 'login2FAError' gRPC stream event.
|
void login2FAError(QString const &errorMsg); ///< Signal for the 'login2FAError' gRPC stream event.
|
||||||
void login2FAErrorAbort(QString const &errorMsg); ///< Signal for the 'login2FAErrorAbort' gRPC stream event.
|
void login2FAErrorAbort(QString const &errorMsg); ///< Signal for the 'login2FAErrorAbort' gRPC stream event.
|
||||||
void login2PasswordRequested(QString const &username); ///< Signal for the 'login2PasswordRequested' gRPC stream event.
|
void login2PasswordRequested(); ///< Signal for the 'login2PasswordRequested' gRPC stream event.
|
||||||
void login2PasswordError(QString const &errorMsg); ///< Signal for the 'login2PasswordError' gRPC stream event.
|
void login2PasswordError(QString const &errorMsg); ///< Signal for the 'login2PasswordError' gRPC stream event.
|
||||||
void login2PasswordErrorAbort(QString const &errorMsg); ///< Signal for the 'login2PasswordErrorAbort' gRPC stream event.
|
void login2PasswordErrorAbort(QString const &errorMsg); ///< Signal for the 'login2PasswordErrorAbort' gRPC stream event.
|
||||||
void loginFinished(int index, bool wasSignedOut); ///< Signal for the 'loginFinished' gRPC stream event.
|
void loginFinished(int index, bool wasSignedOut); ///< Signal for the 'loginFinished' gRPC stream event.
|
||||||
@ -269,15 +253,11 @@ signals: // Signals received from the Go backend, to be forwarded to QML
|
|||||||
void resetFinished(); ///< Signal for the 'resetFinished' gRPC stream event.
|
void resetFinished(); ///< Signal for the 'resetFinished' gRPC stream event.
|
||||||
void reportBugFinished(); ///< Signal for the 'reportBugFinished' gRPC stream event.
|
void reportBugFinished(); ///< Signal for the 'reportBugFinished' gRPC stream event.
|
||||||
void bugReportSendSuccess(); ///< Signal for the 'bugReportSendSuccess' gRPC stream event.
|
void bugReportSendSuccess(); ///< Signal for the 'bugReportSendSuccess' gRPC stream event.
|
||||||
void bugReportSendFallback(); ///< Signal for the 'bugReportSendFallback' gRPC stream event.
|
|
||||||
void bugReportSendError(); ///< Signal for the 'bugReportSendError' gRPC stream event.
|
void bugReportSendError(); ///< Signal for the 'bugReportSendError' gRPC stream event.
|
||||||
void certificateInstallSuccess(); ///< Signal for the 'certificateInstallSuccess' gRPC stream event.
|
|
||||||
void certificateInstallCanceled(); ///< Signal for the 'certificateInstallCanceled' gRPC stream event.
|
|
||||||
void certificateInstallFailed(); /// Signal for the 'certificateInstallFailed' gRPC stream event.
|
|
||||||
void showMainWindow(); ///< Signal for the 'showMainWindow' gRPC stream event.
|
void showMainWindow(); ///< Signal for the 'showMainWindow' gRPC stream event.
|
||||||
void hideMainWindow(); ///< Signal for the 'hideMainWindow' gRPC stream event.
|
void hideMainWindow(); ///< Signal for the 'hideMainWindow' gRPC stream event.
|
||||||
void showHelp(); ///< Signal for the 'showHelp' event (from the context menu).
|
void showHelp(); ///< Signal for the 'showHelp' event (from the context menu).
|
||||||
void showSettings(); ///< Signal for the 'showSettings' event (from the context menu).
|
void showSettings(); ///< Signal for the 'showHelp' event (from the context menu).
|
||||||
void selectUser(QString const& userID, bool forceShowWindow); ///< Signal emitted in order to selected a user with a given ID in the list.
|
void selectUser(QString const& userID, bool forceShowWindow); ///< Signal emitted in order to selected a user with a given ID in the list.
|
||||||
void genericError(QString const &title, QString const &description); ///< Signal for the 'genericError' gRPC stream event.
|
void genericError(QString const &title, QString const &description); ///< Signal for the 'genericError' gRPC stream event.
|
||||||
void imapLoginWhileSignedOut(QString const& username); ///< Signal for the notification of IMAP login attempt on a signed out account.
|
void imapLoginWhileSignedOut(QString const& username); ///< Signal for the notification of IMAP login attempt on a signed out account.
|
||||||
@ -304,7 +284,6 @@ private: // data members
|
|||||||
bool isInternetOn_ { true }; ///< Does bridge consider internet as on?
|
bool isInternetOn_ { true }; ///< Does bridge consider internet as on?
|
||||||
QList<QString> badEventDisplayQueue_; ///< THe queue for displaying 'bad event feedback request dialog'.
|
QList<QString> badEventDisplayQueue_; ///< THe queue for displaying 'bad event feedback request dialog'.
|
||||||
std::unique_ptr<TrayIcon> trayIcon_; ///< The tray icon for the application.
|
std::unique_ptr<TrayIcon> trayIcon_; ///< The tray icon for the application.
|
||||||
bridgepp::BugReportFlow reportFlow_; ///< The bug report flow.
|
|
||||||
friend class AppController;
|
friend class AppController;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -5,11 +5,8 @@
|
|||||||
<file>qml/AccountView.qml</file>
|
<file>qml/AccountView.qml</file>
|
||||||
<file>qml/Banner.qml</file>
|
<file>qml/Banner.qml</file>
|
||||||
<file>qml/Bridge.qml</file>
|
<file>qml/Bridge.qml</file>
|
||||||
<file>qml/BugCategoryView.qml</file>
|
<file>qml/bridgeqml.qmlproject</file>
|
||||||
<file>qml/BugQuestionView.qml</file>
|
|
||||||
<file>qml/BugReportFlow.qml</file>
|
|
||||||
<file>qml/BugReportView.qml</file>
|
<file>qml/BugReportView.qml</file>
|
||||||
<file>qml/CategoryItem.qml</file>
|
|
||||||
<file>qml/Configuration.qml</file>
|
<file>qml/Configuration.qml</file>
|
||||||
<file>qml/ConfigurationItem.qml</file>
|
<file>qml/ConfigurationItem.qml</file>
|
||||||
<file>qml/ContentWrapper.qml</file>
|
<file>qml/ContentWrapper.qml</file>
|
||||||
@ -19,11 +16,9 @@
|
|||||||
<file>qml/icons/ic-alert.svg</file>
|
<file>qml/icons/ic-alert.svg</file>
|
||||||
<file>qml/icons/ic-apple-mail.svg</file>
|
<file>qml/icons/ic-apple-mail.svg</file>
|
||||||
<file>qml/icons/ic-arrow-left.svg</file>
|
<file>qml/icons/ic-arrow-left.svg</file>
|
||||||
<file>qml/icons/ic-bridge.svg</file>
|
|
||||||
<file>qml/icons/ic-card-identity.svg</file>
|
<file>qml/icons/ic-card-identity.svg</file>
|
||||||
<file>qml/icons/ic-check.svg</file>
|
<file>qml/icons/ic-check.svg</file>
|
||||||
<file>qml/icons/ic-chevron-down.svg</file>
|
<file>qml/icons/ic-chevron-down.svg</file>
|
||||||
<file>qml/icons/ic-chevron-left.svg</file>
|
|
||||||
<file>qml/icons/ic-chevron-right.svg</file>
|
<file>qml/icons/ic-chevron-right.svg</file>
|
||||||
<file>qml/icons/ic-chevron-up.svg</file>
|
<file>qml/icons/ic-chevron-up.svg</file>
|
||||||
<file>qml/icons/ic-cog-wheel.svg</file>
|
<file>qml/icons/ic-cog-wheel.svg</file>
|
||||||
@ -37,7 +32,6 @@
|
|||||||
<file>qml/icons/ic-eye-slash.svg</file>
|
<file>qml/icons/ic-eye-slash.svg</file>
|
||||||
<file>qml/icons/ic-eye.svg</file>
|
<file>qml/icons/ic-eye.svg</file>
|
||||||
<file>qml/icons/ic-illustrative-view-html-code.svg</file>
|
<file>qml/icons/ic-illustrative-view-html-code.svg</file>
|
||||||
<file>qml/icons/ic-info-circle.svg</file>
|
|
||||||
<file>qml/icons/ic-info-circle-filled.svg</file>
|
<file>qml/icons/ic-info-circle-filled.svg</file>
|
||||||
<file>qml/icons/ic-info.svg</file>
|
<file>qml/icons/ic-info.svg</file>
|
||||||
<file>qml/icons/ic-microsoft-outlook.svg</file>
|
<file>qml/icons/ic-microsoft-outlook.svg</file>
|
||||||
@ -50,18 +44,13 @@
|
|||||||
<file>qml/icons/ic-success.svg</file>
|
<file>qml/icons/ic-success.svg</file>
|
||||||
<file>qml/icons/ic-three-dots-vertical.svg</file>
|
<file>qml/icons/ic-three-dots-vertical.svg</file>
|
||||||
<file>qml/icons/ic-trash.svg</file>
|
<file>qml/icons/ic-trash.svg</file>
|
||||||
<file>qml/icons/ic-warning-orange.svg</file>
|
|
||||||
<file>qml/icons/img-client-config-selector.svg</file>
|
|
||||||
<file>qml/icons/img-client-config-success.svg</file>
|
|
||||||
<file>qml/icons/img-macos-cert-screenshot.png</file>
|
|
||||||
<file>qml/icons/img-macos-profile-screenshot.png</file>
|
|
||||||
<file>qml/icons/img-mail-clients.svg</file>
|
|
||||||
<file>qml/icons/img-mail-logo-wordmark-dark.svg</file>
|
|
||||||
<file>qml/icons/img-mail-logo-wordmark.svg</file>
|
|
||||||
<file>qml/icons/img-proton-logos.png</file>
|
<file>qml/icons/img-proton-logos.png</file>
|
||||||
<file>qml/icons/img-proton-logos.svg</file>
|
<file>qml/icons/img-proton-logos.svg</file>
|
||||||
<file>qml/icons/img-splash.png</file>
|
<file>qml/icons/img-splash.png</file>
|
||||||
<file>qml/icons/img-splash.svg</file>
|
<file>qml/icons/img-splash.svg</file>
|
||||||
|
<file>qml/icons/img-welcome-dark.png</file>
|
||||||
|
<file>qml/icons/img-welcome-dark.svg</file>
|
||||||
|
<file>qml/icons/img-welcome.png</file>
|
||||||
<file>qml/icons/img-welcome.svg</file>
|
<file>qml/icons/img-welcome.svg</file>
|
||||||
<file>qml/icons/Loader_16.svg</file>
|
<file>qml/icons/Loader_16.svg</file>
|
||||||
<file>qml/icons/Loader_48.svg</file>
|
<file>qml/icons/Loader_48.svg</file>
|
||||||
@ -81,7 +70,6 @@
|
|||||||
<file>qml/KeychainSettings.qml</file>
|
<file>qml/KeychainSettings.qml</file>
|
||||||
<file>qml/LocalCacheSettings.qml</file>
|
<file>qml/LocalCacheSettings.qml</file>
|
||||||
<file>qml/MainWindow.qml</file>
|
<file>qml/MainWindow.qml</file>
|
||||||
<file>qml/NoAccountView.qml</file>
|
|
||||||
<file>qml/NotificationDialog.qml</file>
|
<file>qml/NotificationDialog.qml</file>
|
||||||
<file>qml/NotificationPopups.qml</file>
|
<file>qml/NotificationPopups.qml</file>
|
||||||
<file>qml/Notifications/Notification.qml</file>
|
<file>qml/Notifications/Notification.qml</file>
|
||||||
@ -97,7 +85,6 @@
|
|||||||
<file>qml/Proton/ComboBox.qml</file>
|
<file>qml/Proton/ComboBox.qml</file>
|
||||||
<file>qml/Proton/Dialog.qml</file>
|
<file>qml/Proton/Dialog.qml</file>
|
||||||
<file>qml/Proton/Label.qml</file>
|
<file>qml/Proton/Label.qml</file>
|
||||||
<file>qml/Proton/LinkLabel.qml</file>
|
|
||||||
<file>qml/Proton/Menu.qml</file>
|
<file>qml/Proton/Menu.qml</file>
|
||||||
<file>qml/Proton/MenuItem.qml</file>
|
<file>qml/Proton/MenuItem.qml</file>
|
||||||
<file>qml/Proton/Popup.qml</file>
|
<file>qml/Proton/Popup.qml</file>
|
||||||
@ -108,27 +95,13 @@
|
|||||||
<file>qml/Proton/TextArea.qml</file>
|
<file>qml/Proton/TextArea.qml</file>
|
||||||
<file>qml/Proton/TextField.qml</file>
|
<file>qml/Proton/TextField.qml</file>
|
||||||
<file>qml/Proton/Toggle.qml</file>
|
<file>qml/Proton/Toggle.qml</file>
|
||||||
<file>qml/QuestionItem.qml</file>
|
|
||||||
<file>qml/Resources/bug_report_flow.json</file>
|
|
||||||
<file>qml/Resources/Help/Template.html</file>
|
|
||||||
<file>qml/Resources/Help/WhyBridge.html</file>
|
|
||||||
<file>qml/Resources/Help/WhyCertificate.html</file>
|
|
||||||
<file>qml/Resources/Help/WhyProfileWarning.html</file>
|
|
||||||
<file>qml/SettingsItem.qml</file>
|
<file>qml/SettingsItem.qml</file>
|
||||||
<file>qml/SettingsView.qml</file>
|
<file>qml/SettingsView.qml</file>
|
||||||
<file>qml/SetupWizard/ClientListItem.qml</file>
|
<file>qml/SetupGuide.qml</file>
|
||||||
<file>qml/SetupWizard/LeftPane.qml</file>
|
<file>qml/SignIn.qml</file>
|
||||||
<file>qml/SetupWizard/ClientConfigAppleMail.qml</file>
|
|
||||||
<file>qml/SetupWizard/ClientConfigEnd.qml</file>
|
|
||||||
<file>qml/SetupWizard/ClientConfigParameters.qml</file>
|
|
||||||
<file>qml/SetupWizard/ClientConfigSelector.qml</file>
|
|
||||||
<file>qml/SetupWizard/HelpButton.qml</file>
|
|
||||||
<file>qml/SetupWizard/SetupWizard.qml</file>
|
|
||||||
<file>qml/SetupWizard/Login.qml</file>
|
|
||||||
<file>qml/SetupWizard/Onboarding.qml</file>
|
|
||||||
<file>qml/SetupWizard/StepDescriptionBox.qml</file>
|
|
||||||
<file>qml/ConnectionModeSettings.qml</file>
|
<file>qml/ConnectionModeSettings.qml</file>
|
||||||
<file>qml/SplashScreen.qml</file>
|
<file>qml/SplashScreen.qml</file>
|
||||||
<file>qml/Status.qml</file>
|
<file>qml/Status.qml</file>
|
||||||
|
<file>qml/WelcomeGuide.qml</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
||||||
|
|||||||
@ -192,8 +192,6 @@ TrayIcon::TrayIcon()
|
|||||||
if (!onLinux()) { // we disable this on linux because of a Qt bug that causes the signal to be emitted for other apps (GODT-2750)
|
if (!onLinux()) { // we disable this on linux because of a Qt bug that causes the signal to be emitted for other apps (GODT-2750)
|
||||||
connect(this, &TrayIcon::messageClicked, []() { app().backend().showMainWindow("tray icon popup notification clicked"); });
|
connect(this, &TrayIcon::messageClicked, []() { app().backend().showMainWindow("tray icon popup notification clicked"); });
|
||||||
}
|
}
|
||||||
|
|
||||||
this->setIcon();
|
|
||||||
this->show();
|
this->show();
|
||||||
|
|
||||||
// TrayIcon does not expose its screen, so we connect relevant screen events to our DPI change handler.
|
// TrayIcon does not expose its screen, so we connect relevant screen events to our DPI change handler.
|
||||||
|
|||||||
@ -262,7 +262,7 @@ void UserList::onUsedBytesChanged(QString const &userID, qint64 usedBytes) {
|
|||||||
void UserList::onSyncStarted(QString const &userID) {
|
void UserList::onSyncStarted(QString const &userID) {
|
||||||
int const index = this->rowOfUserID(userID);
|
int const index = this->rowOfUserID(userID);
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
app().log().error(QString("Received syncStarted event for unknown userID %1").arg(userID));
|
app().log().error(QString("Received onSyncStarted event for unknown userID %1").arg(userID));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
users_[index]->setIsSyncing(true);
|
users_[index]->setIsSyncing(true);
|
||||||
@ -275,7 +275,7 @@ void UserList::onSyncStarted(QString const &userID) {
|
|||||||
void UserList::onSyncFinished(QString const &userID) {
|
void UserList::onSyncFinished(QString const &userID) {
|
||||||
int const index = this->rowOfUserID(userID);
|
int const index = this->rowOfUserID(userID);
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
app().log().error(QString("Received syncFinished event for unknown userID %1").arg(userID));
|
app().log().error(QString("Received onSyncFinished event for unknown userID %1").arg(userID));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
users_[index]->setIsSyncing(false);
|
users_[index]->setIsSyncing(false);
|
||||||
@ -293,7 +293,7 @@ void UserList::onSyncProgress(QString const &userID, double progress, float elap
|
|||||||
Q_UNUSED(remainingMs)
|
Q_UNUSED(remainingMs)
|
||||||
int const index = this->rowOfUserID(userID);
|
int const index = this->rowOfUserID(userID);
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
app().log().error(QString("Received syncProgress event for unknown userID %1").arg(userID));
|
app().log().error(QString("Received onSyncFinished event for unknown userID %1").arg(userID));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
users_[index]->setSyncProgress(progress);
|
users_[index]->setSyncProgress(progress);
|
||||||
|
|||||||
@ -63,7 +63,7 @@ BRIDGE_BUILD_ENV= ${BRIDGE_BUILD_ENV:-"dev"}
|
|||||||
git submodule update --init --recursive ${VCPKG_ROOT}
|
git submodule update --init --recursive ${VCPKG_ROOT}
|
||||||
check_exit "Failed to initialize vcpkg as a submodule."
|
check_exit "Failed to initialize vcpkg as a submodule."
|
||||||
|
|
||||||
echo submodule updated
|
echo submodule udpated
|
||||||
|
|
||||||
VCPKG_EXE="${VCPKG_ROOT}/vcpkg"
|
VCPKG_EXE="${VCPKG_ROOT}/vcpkg"
|
||||||
VCPKG_BOOTSTRAP="${VCPKG_ROOT}/bootstrap-vcpkg.sh"
|
VCPKG_BOOTSTRAP="${VCPKG_ROOT}/bootstrap-vcpkg.sh"
|
||||||
|
|||||||
@ -137,21 +137,12 @@ bool checkSingleInstance(QLockFile &lock) {
|
|||||||
if (lock.getLockInfo(&pid, &hostname, &appName)) {
|
if (lock.getLockInfo(&pid, &hostname, &appName)) {
|
||||||
details = QString("(PID : %1 - Host : %2 - App : %3)").arg(pid).arg(hostname, appName);
|
details = QString("(PID : %1 - Host : %2 - App : %3)").arg(pid).arg(hostname, appName);
|
||||||
}
|
}
|
||||||
if (lock.error() == QLockFile::LockFailedError) {
|
|
||||||
// This happens if a stale lock file exists and another process uses that PID.
|
|
||||||
// Try removing the stale file, which will fail if a real process is holding a
|
|
||||||
// file-level lock. A false error is more problematic than not locking properly
|
|
||||||
// on corner-case systems.
|
|
||||||
if (lock.removeStaleLockFile() && lock.tryLock()) {
|
|
||||||
app().log().info("Removed stale lock file");
|
|
||||||
app().log().info(QString("lock file created %1").arg(lock.fileName()));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
app().log().error(QString("Instance already exists %1 %2").arg(lock.fileName(), details));
|
app().log().error(QString("Instance already exists %1 %2").arg(lock.fileName(), details));
|
||||||
return false;
|
return false;
|
||||||
|
} else {
|
||||||
|
app().log().info(QString("lock file created %1").arg(lock.fileName()));
|
||||||
}
|
}
|
||||||
app().log().info(QString("lock file created %1").arg(lock.fileName()));
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,252 +1,251 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
// it under the terms of the GNU General Public License as published by
|
// it under the terms of the GNU General Public License as published by
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
// (at your option) any later version.
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
// GNU General Public License for more details.
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
|
|
||||||
import Proton
|
import Proton
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
enum ViewType {
|
|
||||||
SmallView,
|
|
||||||
LargeView
|
|
||||||
}
|
|
||||||
|
|
||||||
property var _spacing: 12
|
|
||||||
property ColorScheme colorScheme
|
property ColorScheme colorScheme
|
||||||
property color progressColor: {
|
property var user
|
||||||
if (!root.enabled)
|
|
||||||
return root.colorScheme.text_weak;
|
property var _spacing: 12 * ProtonStyle.px
|
||||||
if (root.type === AccountDelegate.SmallView)
|
|
||||||
return root.colorScheme.text_weak;
|
property color progressColor : {
|
||||||
if (root.user && root.user.isSyncing)
|
if (!root.enabled) return root.colorScheme.text_weak
|
||||||
return root.colorScheme.text_weak;
|
if (root.type == AccountDelegate.SmallView) return root.colorScheme.text_weak
|
||||||
if (root.progressRatio < .50)
|
if (root.user && root.user.isSyncing) return root.colorScheme.text_weak
|
||||||
return root.colorScheme.signal_success;
|
if (root.progressRatio < .50) return root.colorScheme.signal_success
|
||||||
if (root.progressRatio < .75)
|
if (root.progressRatio < .75) return root.colorScheme.signal_warning
|
||||||
return root.colorScheme.signal_warning;
|
return root.colorScheme.signal_danger
|
||||||
return root.colorScheme.signal_danger;
|
|
||||||
}
|
}
|
||||||
property real progressRatio: {
|
property real progressRatio: {
|
||||||
if (!root.user)
|
if (!root.user)
|
||||||
return 0;
|
return 0
|
||||||
return root.user.isSyncing ? root.user.syncProgress : reasonableFraction(root.user.usedBytes, root.user.totalBytes);
|
return root.user.isSyncing ? root.user.syncProgress : reasonableFraction(root.user.usedBytes, root.user.totalBytes)
|
||||||
}
|
}
|
||||||
property string totalSpace: root.spaceWithUnits(root.user ? root.reasonableBytes(root.user.totalBytes) : 0)
|
property string totalSpace: root.spaceWithUnits(root.user ? root.reasonableBytes(root.user.totalBytes) : 0)
|
||||||
property var type: AccountDelegate.SmallView
|
|
||||||
property string usedSpace: root.spaceWithUnits(root.user ? root.reasonableBytes(root.user.usedBytes) : 0)
|
property string usedSpace: root.spaceWithUnits(root.user ? root.reasonableBytes(root.user.usedBytes) : 0)
|
||||||
property var user
|
|
||||||
|
function reasonableFraction(used, total){
|
||||||
|
var usedSafe = root.reasonableBytes(used)
|
||||||
|
var totalSafe = root.reasonableBytes(total)
|
||||||
|
if (totalSafe == 0 || usedSafe == 0) return 0
|
||||||
|
if (totalSafe <= usedSafe) return 1
|
||||||
|
return usedSafe / totalSafe
|
||||||
|
}
|
||||||
|
|
||||||
|
function reasonableBytes(bytes){
|
||||||
|
var safeBytes = bytes+0
|
||||||
|
if (safeBytes != bytes) return 0
|
||||||
|
if (safeBytes < 0) return 0
|
||||||
|
return Math.ceil(safeBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
function spaceWithUnits(bytes){
|
||||||
|
if (bytes*1 !== bytes || bytes == 0 ) return "0 kB"
|
||||||
|
var units = ['B',"kB", "MB", "GB", "TB"];
|
||||||
|
var i = parseInt(Math.floor(Math.log(bytes)/Math.log(1024)));
|
||||||
|
|
||||||
|
return Math.round(bytes*10 / Math.pow(1024, i))/10 + " " + units[i]
|
||||||
|
}
|
||||||
|
|
||||||
function primaryEmail() {
|
function primaryEmail() {
|
||||||
return root.user ? root.user.primaryEmailOrUsername() : "";
|
return root.user ? root.user.primaryEmailOrUsername() : ""
|
||||||
}
|
|
||||||
function reasonableBytes(bytes) {
|
|
||||||
const safeBytes = bytes + 0;
|
|
||||||
if (safeBytes !== bytes)
|
|
||||||
return 0;
|
|
||||||
if (safeBytes < 0)
|
|
||||||
return 0;
|
|
||||||
return Math.ceil(safeBytes);
|
|
||||||
}
|
|
||||||
function reasonableFraction(used, total) {
|
|
||||||
const usedSafe = root.reasonableBytes(used);
|
|
||||||
const totalSafe = root.reasonableBytes(total);
|
|
||||||
if (totalSafe === 0 || usedSafe === 0)
|
|
||||||
return 0;
|
|
||||||
if (totalSafe <= usedSafe)
|
|
||||||
return 1;
|
|
||||||
return usedSafe / totalSafe;
|
|
||||||
}
|
|
||||||
function spaceWithUnits(bytes) {
|
|
||||||
if (bytes * 1 !== bytes || bytes === 0)
|
|
||||||
return "0 kB";
|
|
||||||
const units = ['B', "kB", "MB", "GB", "TB"];
|
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
||||||
return Math.round(bytes * 10 / Math.pow(1024, i)) / 10 + " " + units[i];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// width expected to be set by parent object
|
// width expected to be set by parent object
|
||||||
implicitHeight: children[0].implicitHeight
|
implicitHeight : children[0].implicitHeight
|
||||||
|
|
||||||
|
enum ViewType{
|
||||||
|
SmallView, LargeView
|
||||||
|
}
|
||||||
|
property var type : AccountDelegate.SmallView
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
spacing: root._spacing
|
spacing: root._spacing
|
||||||
|
|
||||||
anchors {
|
anchors {
|
||||||
|
top: root.top
|
||||||
left: root.left
|
left: root.left
|
||||||
right: root.right
|
right: root.right
|
||||||
top: root.top
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: avatar
|
id: avatar
|
||||||
|
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
Layout.preferredWidth: height
|
Layout.preferredWidth: height
|
||||||
color: root.colorScheme.background_avatar
|
|
||||||
radius: ProtonStyle.avatar_radius
|
radius: ProtonStyle.avatar_radius
|
||||||
|
|
||||||
|
color: root.colorScheme.background_avatar
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
anchors.fill: parent
|
|
||||||
color: "#FFFFFF"
|
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
font.weight: Font.Normal
|
anchors.fill: parent
|
||||||
horizontalAlignment: Qt.AlignHCenter
|
text: root.user ? root.user.avatarText.toUpperCase(): ""
|
||||||
text: root.user ? root.user.avatarText.toUpperCase() : ""
|
|
||||||
type: {
|
type: {
|
||||||
switch (root.type) {
|
switch (root.type) {
|
||||||
case AccountDelegate.SmallView:
|
case AccountDelegate.SmallView: return Label.Body
|
||||||
return Label.Body;
|
case AccountDelegate.LargeView: return Label.Title
|
||||||
case AccountDelegate.LargeView:
|
|
||||||
return Label.Title;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
font.weight: Font.Normal
|
||||||
|
color: "#FFFFFF"
|
||||||
|
horizontalAlignment: Qt.AlignHCenter
|
||||||
verticalAlignment: Qt.AlignVCenter
|
verticalAlignment: Qt.AlignVCenter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: account
|
id: account
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
spacing: 0
|
spacing: 0
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
id: labelEmail
|
id: labelEmail
|
||||||
Layout.maximumWidth: root.width - (root._spacing + avatar.width)
|
Layout.maximumWidth: root.width - (root._spacing + avatar.width)
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
elide: Text.ElideMiddle
|
|
||||||
text: primaryEmail()
|
text: primaryEmail()
|
||||||
type: {
|
type: {
|
||||||
switch (root.type) {
|
switch (root.type) {
|
||||||
case AccountDelegate.SmallView:
|
case AccountDelegate.SmallView: return Label.Body
|
||||||
return Label.Body;
|
case AccountDelegate.LargeView: return Label.Title
|
||||||
case AccountDelegate.LargeView:
|
|
||||||
return Label.Title;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
elide: Text.ElideMiddle
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: labelArea
|
id: labelArea
|
||||||
anchors.fill: parent
|
anchors.fill:parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolTip {
|
ToolTip {
|
||||||
id: toolTipEmail
|
id: toolTipEmail
|
||||||
delay: 1000
|
|
||||||
text: primaryEmail()
|
|
||||||
visible: labelArea.containsMouse && labelEmail.truncated
|
visible: labelArea.containsMouse && labelEmail.truncated
|
||||||
|
text: primaryEmail()
|
||||||
|
delay: 1000
|
||||||
|
|
||||||
background: Rectangle {
|
background: Rectangle {
|
||||||
border.color: root.colorScheme.background_strong
|
border.color: root.colorScheme.background_strong
|
||||||
color: root.colorScheme.background_norm
|
color: root.colorScheme.background_norm
|
||||||
}
|
}
|
||||||
|
|
||||||
contentItem: Text {
|
contentItem: Text {
|
||||||
color: root.colorScheme.text_norm
|
color: root.colorScheme.text_norm
|
||||||
text: toolTipEmail.text
|
text: toolTipEmail.text
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Item {
|
|
||||||
implicitHeight: root.type === AccountDelegate.LargeView ? 6 : 0
|
Item { implicitHeight: root.type == AccountDelegate.LargeView ? 6 * ProtonStyle.px : 0 }
|
||||||
}
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
spacing: 0
|
spacing: 0
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
color: root.progressColor
|
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
text: {
|
text: {
|
||||||
if (!root.user)
|
if (!root.user)
|
||||||
return qsTr("Signed out");
|
return qsTr("Signed out")
|
||||||
switch (root.user.state) {
|
switch (root.user.state) {
|
||||||
case EUserState.SignedOut:
|
case EUserState.SignedOut:
|
||||||
default:
|
default:
|
||||||
return qsTr("Signed out");
|
return qsTr("Signed out")
|
||||||
case EUserState.Locked:
|
case EUserState.Locked:
|
||||||
return qsTr("Connecting") + dotsTimer.dots;
|
return qsTr("Connecting") + dotsTimer.dots
|
||||||
case EUserState.Connected:
|
case EUserState.Connected:
|
||||||
if (root.user.isSyncing)
|
if (root.user.isSyncing)
|
||||||
return qsTr("Synchronizing (%1%)").arg(Math.floor(root.user.syncProgress * 100)) + dotsTimer.dots;
|
return qsTr("Synchronizing (%1%)").arg(Math.floor(root.user.syncProgress * 100)) + dotsTimer.dots
|
||||||
else
|
else
|
||||||
return root.usedSpace;
|
return root.usedSpace
|
||||||
}
|
|
||||||
}
|
|
||||||
type: {
|
|
||||||
switch (root.type) {
|
|
||||||
case AccountDelegate.SmallView:
|
|
||||||
return Label.Caption;
|
|
||||||
case AccountDelegate.LargeView:
|
|
||||||
return Label.Body;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Timer {
|
Timer { // dots animation while connecting & syncing.
|
||||||
// dots animation while connecting & syncing.
|
id:dotsTimer
|
||||||
id: dotsTimer
|
|
||||||
|
|
||||||
property string dots: ""
|
property string dots: ""
|
||||||
|
interval: 500;
|
||||||
interval: 500
|
repeat: true;
|
||||||
repeat: true
|
|
||||||
running: (root.user != null) && ((root.user.state === EUserState.Locked) || (root.user.isSyncing))
|
running: (root.user != null) && ((root.user.state === EUserState.Locked) || (root.user.isSyncing))
|
||||||
|
|
||||||
onRunningChanged: {
|
|
||||||
dots = "";
|
|
||||||
}
|
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
dots += ".";
|
dots += "."
|
||||||
if (dots.length > 3)
|
if (dots.length > 3)
|
||||||
dots = "";
|
dots = ""
|
||||||
|
}
|
||||||
|
onRunningChanged: {
|
||||||
|
dots = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Label {
|
color: root.progressColor
|
||||||
color: root.colorScheme.text_weak
|
|
||||||
colorScheme: root.colorScheme
|
|
||||||
text: root.user && root.user.state === EUserState.Connected && !root.user.isSyncing ? " / " + root.totalSpace : ""
|
|
||||||
type: {
|
type: {
|
||||||
switch (root.type) {
|
switch (root.type) {
|
||||||
case AccountDelegate.SmallView:
|
case AccountDelegate.SmallView: return Label.Caption
|
||||||
return Label.Caption;
|
case AccountDelegate.LargeView: return Label.Body
|
||||||
case AccountDelegate.LargeView:
|
}
|
||||||
return Label.Body;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
text: root.user && root.user.state == EUserState.Connected && !root.user.isSyncing ? " / " + root.totalSpace : ""
|
||||||
|
color: root.colorScheme.text_weak
|
||||||
|
type: {
|
||||||
|
switch (root.type) {
|
||||||
|
case AccountDelegate.SmallView: return Label.Caption
|
||||||
|
case AccountDelegate.LargeView: return Label.Body
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Item {
|
|
||||||
implicitHeight: root.type === AccountDelegate.LargeView ? 3 : 0
|
Item { implicitHeight: root.type == AccountDelegate.LargeView ? 3 * ProtonStyle.px : 0 }
|
||||||
}
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: progress_bar
|
id: progress_bar
|
||||||
color: root.colorScheme.border_weak
|
visible: root.user ? root.type == AccountDelegate.LargeView : false
|
||||||
height: 4
|
width: 140 * ProtonStyle.px
|
||||||
|
height: 4 * ProtonStyle.px
|
||||||
radius: ProtonStyle.progress_bar_radius
|
radius: ProtonStyle.progress_bar_radius
|
||||||
visible: root.user ? root.type === AccountDelegate.LargeView : false
|
color: root.colorScheme.border_weak
|
||||||
width: 140
|
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: progress_bar_filled
|
id: progress_bar_filled
|
||||||
color: root.progressColor
|
|
||||||
radius: ProtonStyle.progress_bar_radius
|
radius: ProtonStyle.progress_bar_radius
|
||||||
visible: root.user ? parent.visible && (root.user.state === EUserState.Connected) : false
|
color: root.progressColor
|
||||||
width: Math.min(1, Math.max(0.02, root.progressRatio)) * parent.width
|
visible: root.user ? parent.visible && (root.user.state == EUserState.Connected): false
|
||||||
|
|
||||||
anchors {
|
anchors {
|
||||||
bottom: parent.bottom
|
top : parent.top
|
||||||
left: parent.left
|
bottom : parent.bottom
|
||||||
top: parent.top
|
left : parent.left
|
||||||
}
|
}
|
||||||
|
width: Math.min(1,Math.max(0.02,root.progressRatio)) * parent.width
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,36 +1,42 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
// it under the terms of the GNU General Public License as published by
|
// it under the terms of the GNU General Public License as published by
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
// (at your option) any later version.
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
// GNU General Public License for more details.
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
|
|
||||||
import Proton
|
import Proton
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property bool _connected: root.user ? root.user.state === EUserState.Connected : false
|
|
||||||
property int _contentWidth: 640
|
|
||||||
property int _detailsMargin: 25
|
|
||||||
property int _lineThickness: 1
|
|
||||||
property int _spacing: 20
|
|
||||||
property int _buttonSpacing: 8
|
|
||||||
property int _topMargin: 32
|
|
||||||
property ColorScheme colorScheme
|
property ColorScheme colorScheme
|
||||||
property var notifications
|
property var notifications
|
||||||
property var user
|
property var user
|
||||||
|
|
||||||
signal showClientConfigurator(var user, string address, bool justLoggedIn)
|
signal showSignIn
|
||||||
signal showLogin(var username)
|
|
||||||
|
signal showSetupGuide(var user, string address)
|
||||||
|
|
||||||
|
property int _contentWidth: 640
|
||||||
|
property int _topMargin: 32
|
||||||
|
property int _detailsMargin: 25
|
||||||
|
property int _spacing: 20
|
||||||
|
property int _lineThickness: 1
|
||||||
|
property bool _connected: root.user ? root.user.state === EUserState.Connected : false
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@ -39,7 +45,6 @@ Item {
|
|||||||
ScrollView {
|
ScrollView {
|
||||||
id: scrollView
|
id: scrollView
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
||||||
Component.onCompleted: contentItem.boundsBehavior = Flickable.StopAtBounds
|
Component.onCompleted: contentItem.boundsBehavior = Flickable.StopAtBounds
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
@ -49,101 +54,102 @@ Item {
|
|||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: topArea
|
id: topArea
|
||||||
Layout.fillWidth: true
|
|
||||||
clip: true
|
|
||||||
color: root.colorScheme.background_norm
|
color: root.colorScheme.background_norm
|
||||||
|
clip: true
|
||||||
|
Layout.fillWidth: true
|
||||||
implicitHeight: childrenRect.height
|
implicitHeight: childrenRect.height
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: topLayout
|
id: topLayout
|
||||||
|
width: _contentWidth
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
spacing: _spacing
|
spacing: _spacing
|
||||||
width: _contentWidth
|
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
// account delegate with action buttons
|
// account delegate with action buttons
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.topMargin: _topMargin
|
Layout.topMargin: _topMargin
|
||||||
spacing: _buttonSpacing
|
|
||||||
AccountDelegate {
|
AccountDelegate {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
enabled: _connected
|
|
||||||
type: AccountDelegate.LargeView
|
|
||||||
user: root.user
|
user: root.user
|
||||||
|
type: AccountDelegate.LargeView
|
||||||
|
enabled: _connected
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
Layout.alignment: Qt.AlignTop
|
Layout.alignment: Qt.AlignTop
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
secondary: true
|
|
||||||
text: qsTr("Sign out")
|
text: qsTr("Sign out")
|
||||||
|
secondary: true
|
||||||
visible: _connected
|
visible: _connected
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (!root.user)
|
if (!root.user)
|
||||||
return;
|
return;
|
||||||
root.user.logout();
|
root.user.logout();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
Layout.alignment: Qt.AlignTop
|
Layout.alignment: Qt.AlignTop
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
secondary: true
|
|
||||||
text: qsTr("Sign in")
|
text: qsTr("Sign in")
|
||||||
|
secondary: true
|
||||||
visible: root.user ? (root.user.state === EUserState.SignedOut) : false
|
visible: root.user ? (root.user.state === EUserState.SignedOut) : false
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (user) {
|
if (!root.user)
|
||||||
root.showLogin(user.primaryEmailOrUsername());
|
return;
|
||||||
}
|
root.showSignIn();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
Layout.alignment: Qt.AlignTop
|
Layout.alignment: Qt.AlignTop
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
icon.source: "/qml/icons/ic-trash.svg"
|
icon.source: "/qml/icons/ic-trash.svg"
|
||||||
secondary: true
|
secondary: true
|
||||||
visible: root.user ? root.user.state !== EUserState.Locked : false
|
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (!root.user)
|
if (!root.user)
|
||||||
return;
|
return;
|
||||||
root.notifications.askDeleteAccount(root.user);
|
root.notifications.askDeleteAccount(root.user);
|
||||||
}
|
}
|
||||||
|
visible: root.user ? root.user.state !== EUserState.Locked : false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
color: root.colorScheme.border_weak
|
|
||||||
height: root._lineThickness
|
height: root._lineThickness
|
||||||
|
color: root.colorScheme.border_weak
|
||||||
}
|
}
|
||||||
SettingsItem {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
actionText: qsTr("Configure email client")
|
|
||||||
colorScheme: root.colorScheme
|
|
||||||
description: qsTr("Using the mailbox details below (re)configure your client.")
|
|
||||||
showSeparator: splitMode.visible
|
|
||||||
text: qsTr("Email clients")
|
|
||||||
type: SettingsItem.PrimaryButton
|
|
||||||
visible: _connected && ((!root.user.splitMode) || (root.user.addresses.length === 1))
|
|
||||||
|
|
||||||
|
SettingsItem {
|
||||||
|
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
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (!root.user)
|
if (!root.user)
|
||||||
return;
|
return;
|
||||||
root.showClientConfigurator(root.user, user.addresses[0], false);
|
root.showSetupGuide(root.user, user.addresses[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsItem {
|
SettingsItem {
|
||||||
id: splitMode
|
id: splitMode
|
||||||
Layout.fillWidth: true
|
|
||||||
checked: root.user ? root.user.splitMode : false
|
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
description: qsTr("Setup multiple email addresses individually.")
|
|
||||||
showSeparator: addressSelector.visible
|
|
||||||
text: qsTr("Split addresses")
|
text: qsTr("Split addresses")
|
||||||
|
description: qsTr("Setup multiple email addresses individually.")
|
||||||
type: SettingsItem.Toggle
|
type: SettingsItem.Toggle
|
||||||
|
checked: root.user ? root.user.splitMode : false
|
||||||
visible: _connected && root.user.addresses.length > 1
|
visible: _connected && root.user.addresses.length > 1
|
||||||
|
showSeparator: addressSelector.visible
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (!splitMode.checked) {
|
if (!splitMode.checked) {
|
||||||
root.notifications.askEnableSplitMode(user);
|
root.notifications.askEnableSplitMode(user);
|
||||||
@ -152,47 +158,52 @@ Item {
|
|||||||
root.user.toggleSplitMode(!splitMode.checked);
|
root.user.toggleSplitMode(!splitMode.checked);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
RowLayout {
|
|
||||||
Layout.bottomMargin: _spacing
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.bottomMargin: _spacing
|
||||||
visible: _connected && root.user.splitMode
|
visible: _connected && root.user.splitMode
|
||||||
|
|
||||||
ComboBox {
|
ComboBox {
|
||||||
id: addressSelector
|
id: addressSelector
|
||||||
Layout.fillWidth: true
|
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
|
Layout.fillWidth: true
|
||||||
model: root.user ? root.user.addresses : null
|
model: root.user ? root.user.addresses : null
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
secondary: false
|
text: qsTr("Configure")
|
||||||
text: qsTr("Configure email client")
|
secondary: true
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (!root.user)
|
if (!root.user)
|
||||||
return;
|
return;
|
||||||
root.showClientConfigurator(root.user, addressSelector.displayText, false);
|
root.showSetupGuide(root.user, addressSelector.displayText);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
height: 0
|
height: 0
|
||||||
} // just for some extra space before separator
|
} // just for some extra space before separator
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: bottomArea
|
id: bottomArea
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
color: root.colorScheme.background_weak
|
|
||||||
implicitHeight: bottomLayout.implicitHeight
|
implicitHeight: bottomLayout.implicitHeight
|
||||||
|
color: root.colorScheme.background_weak
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: bottomLayout
|
id: bottomLayout
|
||||||
|
width: _contentWidth
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
spacing: _spacing
|
spacing: _spacing
|
||||||
visible: _connected
|
visible: _connected
|
||||||
width: _contentWidth
|
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
Layout.topMargin: _detailsMargin
|
Layout.topMargin: _detailsMargin
|
||||||
@ -200,34 +211,35 @@ Item {
|
|||||||
text: qsTr("Mailbox details")
|
text: qsTr("Mailbox details")
|
||||||
type: Label.Body_semibold
|
type: Label.Body_semibold
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
id: configuration
|
id: configuration
|
||||||
|
spacing: _spacing
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
|
||||||
property string currentAddress: addressSelector.displayText
|
property string currentAddress: addressSelector.displayText
|
||||||
|
|
||||||
Layout.fillHeight: true
|
Configuration {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
spacing: _spacing
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
Configuration {
|
Configuration {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
hostname: Backend.hostname
|
|
||||||
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")
|
title: qsTr("SMTP")
|
||||||
|
hostname: Backend.hostname
|
||||||
|
port: Backend.smtpPort.toString()
|
||||||
username: configuration.currentAddress
|
username: configuration.currentAddress
|
||||||
|
password: root.user ? root.user.password : ""
|
||||||
|
security: Backend.useSSLForSMTP ? "SSL" : "STARTTLS"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,19 +1,25 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
// it under the terms of the GNU General Public License as published by
|
// it under the terms of the GNU General Public License as published by
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
// (at your option) any later version.
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
// GNU General Public License for more details.
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import QtQuick.Controls.impl
|
import QtQuick.Controls.impl
|
||||||
|
|
||||||
import Proton
|
import Proton
|
||||||
import Notifications
|
import Notifications
|
||||||
|
|
||||||
@ -21,28 +27,34 @@ Popup {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
property ColorScheme colorScheme
|
property ColorScheme colorScheme
|
||||||
property var mainWindow
|
|
||||||
property Notification notification
|
property Notification notification
|
||||||
|
property var mainWindow
|
||||||
|
|
||||||
|
topMargin: 37
|
||||||
|
leftMargin: (mainWindow.width - root.implicitWidth)/2
|
||||||
|
|
||||||
implicitHeight: contentLayout.implicitHeight + contentLayout.anchors.topMargin + contentLayout.anchors.bottomMargin
|
implicitHeight: contentLayout.implicitHeight + contentLayout.anchors.topMargin + contentLayout.anchors.bottomMargin
|
||||||
implicitWidth: 600 // contentLayout.implicitWidth + contentLayout.anchors.leftMargin + contentLayout.anchors.rightMargin
|
implicitWidth: 600 // contentLayout.implicitWidth + contentLayout.anchors.leftMargin + contentLayout.anchors.rightMargin
|
||||||
leftMargin: (mainWindow.width - root.implicitWidth) / 2
|
|
||||||
modal: false
|
|
||||||
popupType: ApplicationWindow.PopupType.Banner
|
popupType: ApplicationWindow.PopupType.Banner
|
||||||
|
|
||||||
shouldShow: notification ? (notification.active && !notification.dismissed) : false
|
shouldShow: notification ? (notification.active && !notification.dismissed) : false
|
||||||
topMargin: 37
|
|
||||||
|
modal: false
|
||||||
|
|
||||||
Action {
|
Action {
|
||||||
id: defaultDismissAction
|
id: defaultDismissAction
|
||||||
text: qsTr("OK")
|
|
||||||
|
|
||||||
|
text: qsTr("OK")
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
if (!root.notification) {
|
if (!root.notification) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
root.notification.dismissed = true;
|
|
||||||
|
root.notification.dismissed = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
id: contentLayout
|
id: contentLayout
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@ -51,148 +63,170 @@ Popup {
|
|||||||
Item {
|
Item {
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
clip: true
|
clip: true
|
||||||
implicitHeight: children[1].implicitHeight + children[1].anchors.topMargin + children[1].anchors.bottomMargin
|
implicitHeight: children[1].implicitHeight + children[1].anchors.topMargin + children[1].anchors.bottomMargin
|
||||||
implicitWidth: children[1].implicitWidth + children[1].anchors.leftMargin + children[1].anchors.rightMargin
|
implicitWidth: children[1].implicitWidth + children[1].anchors.leftMargin + children[1].anchors.rightMargin
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
|
anchors.top: parent.top
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.top: parent.top
|
width: parent.width + 10
|
||||||
|
radius: ProtonStyle.banner_radius
|
||||||
color: {
|
color: {
|
||||||
if (!root.notification) {
|
if (!root.notification) {
|
||||||
return "transparent";
|
return "transparent"
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (root.notification.type) {
|
switch (root.notification.type) {
|
||||||
case Notification.NotificationType.Info:
|
case Notification.NotificationType.Info:
|
||||||
return root.colorScheme.signal_info;
|
return root.colorScheme.signal_info
|
||||||
case Notification.NotificationType.Success:
|
case Notification.NotificationType.Success:
|
||||||
return root.colorScheme.signal_success;
|
return root.colorScheme.signal_success
|
||||||
case Notification.NotificationType.Warning:
|
case Notification.NotificationType.Warning:
|
||||||
return root.colorScheme.signal_warning;
|
return root.colorScheme.signal_warning
|
||||||
case Notification.NotificationType.Danger:
|
case Notification.NotificationType.Danger:
|
||||||
return root.colorScheme.signal_danger;
|
return root.colorScheme.signal_danger
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
radius: ProtonStyle.banner_radius
|
|
||||||
width: parent.width + 10
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
anchors.bottomMargin: 14
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.leftMargin: 16
|
|
||||||
anchors.topMargin: 14
|
anchors.topMargin: 14
|
||||||
|
anchors.bottomMargin: 14
|
||||||
|
anchors.leftMargin: 16
|
||||||
|
|
||||||
spacing: 8
|
spacing: 8
|
||||||
|
|
||||||
ColorImage {
|
ColorImage {
|
||||||
|
color: root.colorScheme.text_invert
|
||||||
|
width: 24
|
||||||
|
height: 24
|
||||||
|
|
||||||
|
sourceSize.width: 24
|
||||||
|
sourceSize.height: 24
|
||||||
|
|
||||||
Layout.preferredHeight: 24
|
Layout.preferredHeight: 24
|
||||||
Layout.preferredWidth: 24
|
Layout.preferredWidth: 24
|
||||||
color: root.colorScheme.text_invert
|
|
||||||
height: 24
|
|
||||||
source: {
|
source: {
|
||||||
if (!root.notification) {
|
if (!root.notification) {
|
||||||
return "";
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (root.notification.type) {
|
switch (root.notification.type) {
|
||||||
case Notification.NotificationType.Info:
|
case Notification.NotificationType.Info:
|
||||||
return "/qml/icons/ic-info-circle-filled.svg";
|
return "/qml/icons/ic-info-circle-filled.svg"
|
||||||
case Notification.NotificationType.Success:
|
case Notification.NotificationType.Success:
|
||||||
return "/qml/icons/ic-info-circle-filled.svg";
|
return "/qml/icons/ic-info-circle-filled.svg"
|
||||||
case Notification.NotificationType.Warning:
|
case Notification.NotificationType.Warning:
|
||||||
return "/qml/icons/ic-exclamation-circle-filled.svg";
|
return "/qml/icons/ic-exclamation-circle-filled.svg"
|
||||||
case Notification.NotificationType.Danger:
|
case Notification.NotificationType.Danger:
|
||||||
return "/qml/icons/ic-exclamation-circle-filled.svg";
|
return "/qml/icons/ic-exclamation-circle-filled.svg"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sourceSize.height: 24
|
|
||||||
sourceSize.width: 24
|
|
||||||
width: 24
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
Layout.alignment: Qt.AlignVCenter
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.leftMargin: 16
|
|
||||||
color: root.colorScheme.text_invert
|
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
Layout.leftMargin: 16
|
||||||
|
|
||||||
|
color: root.colorScheme.text_invert
|
||||||
text: root.notification ? root.notification.description : ""
|
text: root.notification ? root.notification.description : ""
|
||||||
|
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
|
width: 1
|
||||||
color: {
|
color: {
|
||||||
if (!root.notification) {
|
if (!root.notification) {
|
||||||
return "transparent";
|
return "transparent"
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (root.notification.type) {
|
switch (root.notification.type) {
|
||||||
case Notification.NotificationType.Info:
|
case Notification.NotificationType.Info:
|
||||||
return root.colorScheme.signal_info_active;
|
return root.colorScheme.signal_info_active
|
||||||
case Notification.NotificationType.Success:
|
case Notification.NotificationType.Success:
|
||||||
return root.colorScheme.signal_success_active;
|
return root.colorScheme.signal_success_active
|
||||||
case Notification.NotificationType.Warning:
|
case Notification.NotificationType.Warning:
|
||||||
return root.colorScheme.signal_warning_active;
|
return root.colorScheme.signal_warning_active
|
||||||
case Notification.NotificationType.Danger:
|
case Notification.NotificationType.Danger:
|
||||||
return root.colorScheme.signal_danger_active;
|
return root.colorScheme.signal_danger_active
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
width: 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
id: actionButton
|
|
||||||
Layout.fillHeight: true
|
|
||||||
action: (root.notification && root.notification.action.length > 0) ? root.notification.action[0] : defaultDismissAction
|
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
|
Layout.fillHeight: true
|
||||||
|
|
||||||
|
id: actionButton
|
||||||
|
|
||||||
|
action: (root.notification && root.notification.action.length > 0) ? root.notification.action[0] : defaultDismissAction
|
||||||
|
|
||||||
background: Item {
|
background: Item {
|
||||||
clip: true
|
clip: true
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
|
anchors.top: parent.top
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.top: parent.top
|
width: parent.width + 10
|
||||||
|
radius: ProtonStyle.banner_radius
|
||||||
color: {
|
color: {
|
||||||
if (!root.notification) {
|
if (!root.notification) {
|
||||||
return "transparent";
|
return "transparent"
|
||||||
}
|
}
|
||||||
let norm;
|
|
||||||
let hover;
|
var norm
|
||||||
let active;
|
var hover
|
||||||
|
var active
|
||||||
|
|
||||||
switch (root.notification.type) {
|
switch (root.notification.type) {
|
||||||
case Notification.NotificationType.Info:
|
case Notification.NotificationType.Info:
|
||||||
norm = root.colorScheme.signal_info;
|
norm = root.colorScheme.signal_info
|
||||||
hover = root.colorScheme.signal_info_hover;
|
hover = root.colorScheme.signal_info_hover
|
||||||
active = root.colorScheme.signal_info_active;
|
active = root.colorScheme.signal_info_active
|
||||||
break;
|
break;
|
||||||
case Notification.NotificationType.Success:
|
case Notification.NotificationType.Success:
|
||||||
norm = root.colorScheme.signal_success;
|
norm = root.colorScheme.signal_success
|
||||||
hover = root.colorScheme.signal_success_hover;
|
hover = root.colorScheme.signal_success_hover
|
||||||
active = root.colorScheme.signal_success_active;
|
active = root.colorScheme.signal_success_active
|
||||||
break;
|
break;
|
||||||
case Notification.NotificationType.Warning:
|
case Notification.NotificationType.Warning:
|
||||||
norm = root.colorScheme.signal_warning;
|
norm = root.colorScheme.signal_warning
|
||||||
hover = root.colorScheme.signal_warning_hover;
|
hover = root.colorScheme.signal_warning_hover
|
||||||
active = root.colorScheme.signal_warning_active;
|
active = root.colorScheme.signal_warning_active
|
||||||
break;
|
break;
|
||||||
case Notification.NotificationType.Danger:
|
case Notification.NotificationType.Danger:
|
||||||
norm = root.colorScheme.signal_danger;
|
norm = root.colorScheme.signal_danger
|
||||||
hover = root.colorScheme.signal_danger_hover;
|
hover = root.colorScheme.signal_danger_hover
|
||||||
active = root.colorScheme.signal_danger_active;
|
active = root.colorScheme.signal_danger_active
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (actionButton.down) {
|
if (actionButton.down) {
|
||||||
return active;
|
return active
|
||||||
}
|
}
|
||||||
|
|
||||||
if (actionButton.enabled && (actionButton.highlighted || actionButton.hovered || actionButton.checked)) {
|
if (actionButton.enabled && (actionButton.highlighted || actionButton.hovered || actionButton.checked)) {
|
||||||
return hover;
|
return hover
|
||||||
}
|
}
|
||||||
|
|
||||||
if (actionButton.loading) {
|
if (actionButton.loading) {
|
||||||
return hover;
|
return hover
|
||||||
}
|
}
|
||||||
return norm;
|
|
||||||
|
return norm
|
||||||
}
|
}
|
||||||
radius: ProtonStyle.banner_radius
|
|
||||||
width: parent.width + 10
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,111 +1,129 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
// it under the terms of the GNU General Public License as published by
|
// it under the terms of the GNU General Public License as published by
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
// (at your option) any later version.
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
// GNU General Public License for more details.
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import QtQml
|
import QtQml
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Window
|
import QtQuick.Window
|
||||||
import Qt.labs.platform
|
import Qt.labs.platform
|
||||||
|
|
||||||
import Proton
|
import Proton
|
||||||
import Notifications
|
import Notifications
|
||||||
|
|
||||||
QtObject {
|
QtObject {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property MainWindow _mainWindow: MainWindow {
|
function bound(num, lowerLimit, upperLimit) {
|
||||||
id: mainWindow
|
return Math.max(lowerLimit, Math.min(upperLimit, num))
|
||||||
notifications: root._notifications
|
|
||||||
title: root.title
|
|
||||||
visible: false
|
|
||||||
|
|
||||||
onVisibleChanged: {
|
|
||||||
Backend.dockIconVisible = visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
function onColorSchemeNameChanged(scheme) {
|
|
||||||
root.setColorScheme();
|
|
||||||
}
|
|
||||||
function onDiskCacheUnavailable() {
|
|
||||||
mainWindow.showAndRise();
|
|
||||||
}
|
|
||||||
function onHideMainWindow() {
|
|
||||||
mainWindow.hide();
|
|
||||||
}
|
|
||||||
target: Backend
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
property var title: Backend.appname
|
||||||
|
|
||||||
property Notifications _notifications: Notifications {
|
property Notifications _notifications: Notifications {
|
||||||
id: notifications
|
id: notifications
|
||||||
frontendMain: mainWindow
|
frontendMain: mainWindow
|
||||||
}
|
}
|
||||||
property NotificationFilter _trayNotificationFilter: NotificationFilter {
|
|
||||||
id: trayNotificationFilter
|
|
||||||
source: root._notifications ? root._notifications.all : undefined
|
|
||||||
|
|
||||||
onTopmostChanged: {
|
property NotificationFilter _trayNotificationFilter: NotificationFilter {
|
||||||
if (topmost) {
|
id: trayNotificationFilter
|
||||||
switch (topmost.type) {
|
source: root._notifications ? root._notifications.all : undefined
|
||||||
case Notification.NotificationType.Danger:
|
onTopmostChanged: {
|
||||||
Backend.setErrorTrayIcon(topmost.brief, topmost.icon);
|
if (topmost) {
|
||||||
return;
|
switch (topmost.type) {
|
||||||
case Notification.NotificationType.Warning:
|
case Notification.NotificationType.Danger:
|
||||||
Backend.setWarnTrayIcon(topmost.brief, topmost.icon);
|
Backend.setErrorTrayIcon(topmost.brief, topmost.icon)
|
||||||
return;
|
return
|
||||||
case Notification.NotificationType.Info:
|
case Notification.NotificationType.Warning:
|
||||||
Backend.setUpdateTrayIcon(topmost.brief, topmost.icon);
|
Backend.setWarnTrayIcon(topmost.brief, topmost.icon)
|
||||||
return;
|
return
|
||||||
|
case Notification.NotificationType.Info:
|
||||||
|
Backend.setUpdateTrayIcon(topmost.brief, topmost.icon)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Backend.setNormalTrayIcon();
|
Backend.setNormalTrayIcon()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
property var title: Backend.appname
|
|
||||||
|
|
||||||
function bound(num, lowerLimit, upperLimit) {
|
|
||||||
return Math.max(lowerLimit, Math.min(upperLimit, num));
|
property MainWindow _mainWindow: MainWindow {
|
||||||
}
|
id: mainWindow
|
||||||
function setColorScheme() {
|
visible: false
|
||||||
if (Backend.colorSchemeName === "light")
|
|
||||||
ProtonStyle.currentStyle = ProtonStyle.lightStyle;
|
title: root.title
|
||||||
if (Backend.colorSchemeName === "dark")
|
notifications: root._notifications
|
||||||
ProtonStyle.currentStyle = ProtonStyle.darkStyle;
|
|
||||||
|
onVisibleChanged: {
|
||||||
|
Backend.dockIconVisible = visible
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: Backend
|
||||||
|
function onDiskCacheUnavailable() {
|
||||||
|
mainWindow.showAndRise()
|
||||||
|
}
|
||||||
|
function onColorSchemeNameChanged(scheme) { root.setColorScheme() }
|
||||||
|
|
||||||
|
function onHideMainWindow() {
|
||||||
|
mainWindow.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
if (!Backend) {
|
if (!Backend) {
|
||||||
console.log("Backend not loaded");
|
console.log("Backend not loaded")
|
||||||
}
|
}
|
||||||
root.setColorScheme();
|
|
||||||
|
root.setColorScheme()
|
||||||
|
|
||||||
|
|
||||||
if (!Backend.users) {
|
if (!Backend.users) {
|
||||||
console.log("users not loaded");
|
console.log("users not loaded")
|
||||||
}
|
}
|
||||||
const c = Backend.users.count;
|
|
||||||
const u = Backend.users.get(0);
|
var c = Backend.users.count
|
||||||
|
var u = Backend.users.get(0)
|
||||||
// DEBUG
|
// DEBUG
|
||||||
if (c !== 0) {
|
if (c !== 0) {
|
||||||
console.log("users non zero", c);
|
console.log("users non zero", c)
|
||||||
console.log("first user", u);
|
console.log("first user", u )
|
||||||
}
|
}
|
||||||
|
|
||||||
if (c === 0) {
|
if (c === 0) {
|
||||||
mainWindow.showAndRise();
|
mainWindow.showAndRise()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (u) {
|
if (u) {
|
||||||
if (c === 1 && (u.state === EUserState.SignedOut)) {
|
if (c === 1 && (u.state === EUserState.SignedOut)) {
|
||||||
mainWindow.showAndRise();
|
mainWindow.showAndRise()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Backend.guiReady();
|
|
||||||
if (Backend.showOnStartup || Backend.showSplashScreen) {
|
Backend.guiReady()
|
||||||
mainWindow.showAndRise();
|
|
||||||
|
if (Backend.showOnStartup || Backend.showSplashScreen) {
|
||||||
|
mainWindow.showAndRise()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function setColorScheme() {
|
||||||
|
if (Backend.colorSchemeName === "light") ProtonStyle.currentStyle = ProtonStyle.lightStyle
|
||||||
|
if (Backend.colorSchemeName === "dark") ProtonStyle.currentStyle = ProtonStyle.darkStyle
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,52 +0,0 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
|
||||||
// This file is part of Proton Mail Bridge.
|
|
||||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU General Public License for more details.
|
|
||||||
// You should have received a copy of the GNU General Public License
|
|
||||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Proton
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,130 +0,0 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
|
||||||
// This file is part of Proton Mail Bridge.
|
|
||||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU General Public License for more details.
|
|
||||||
// You should have received a copy of the GNU General Public License
|
|
||||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Proton
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,99 +0,0 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
|
||||||
// This file is part of Proton Mail Bridge.
|
|
||||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU General Public License for more details.
|
|
||||||
// You should have received a copy of the GNU General Public License
|
|
||||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Proton
|
|
||||||
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,161 +1,202 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
// it under the terms of the GNU General Public License as published by
|
// it under the terms of the GNU General Public License as published by
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
// (at your option) any later version.
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
// GNU General Public License for more details.
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
|
|
||||||
import Proton
|
import Proton
|
||||||
|
|
||||||
SettingsView {
|
SettingsView {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property var selectedAddress
|
|
||||||
property var categoryId:-1
|
|
||||||
property string category: Backend.getBugCategory(root.categoryId)
|
|
||||||
|
|
||||||
signal bugReportWasSent
|
|
||||||
|
|
||||||
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
|
fillHeight: true
|
||||||
|
|
||||||
onVisibleChanged: {
|
property var selectedAddress
|
||||||
root.setDefaultValue();
|
|
||||||
}
|
signal bugReportWasSent()
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
|
text: qsTr("Report a problem")
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
text: qsTr("Send report")
|
|
||||||
type: Label.Heading
|
type: Label.Heading
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
TextArea {
|
TextArea {
|
||||||
id: description
|
id: description
|
||||||
|
property int _minLength: 150
|
||||||
|
property int _maxLength: 800
|
||||||
|
|
||||||
|
label: qsTr("Description")
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.minimumHeight: heightForLinesVisible(4)
|
||||||
|
hint: description.text.length + "/" + _maxLength
|
||||||
|
placeholderText: qsTr("Tell us what went wrong or isn't working (min. %1 characters).").arg(_minLength)
|
||||||
|
|
||||||
|
validator: function(text) {
|
||||||
|
if (description.text.length < description._minLength) {
|
||||||
|
return qsTr("Enter a problem description (min. %1 characters).").arg(_minLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (description.text.length > description._maxLength) {
|
||||||
|
return qsTr("Enter a problem description (max. %1 characters).").arg(_maxLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
onTextChanged: {
|
||||||
|
// Rise max length error immediately while typing
|
||||||
|
if (description.text.length > description._maxLength) {
|
||||||
|
validate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
KeyNavigation.priority: KeyNavigation.BeforeItem
|
KeyNavigation.priority: KeyNavigation.BeforeItem
|
||||||
KeyNavigation.tab: address
|
KeyNavigation.tab: address
|
||||||
Layout.fillHeight: true
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.minimumHeight: heightForLinesVisible(4)
|
|
||||||
colorScheme: root.colorScheme
|
|
||||||
textFormat: Text.MarkdownText
|
|
||||||
|
|
||||||
// set implicitHeight to explicit height because se don't
|
// set implicitHeight to explicit height because se don't
|
||||||
// want TextArea implicitHeight (which is height of all text)
|
// want TextArea implicitHeight (which is height of all text)
|
||||||
// to be considered in SettingsView internal scroll view
|
// to be considered in SettingsView internal scroll view
|
||||||
implicitHeight: height
|
implicitHeight: height
|
||||||
label: "Your answers to: " + qsTr(root.category);
|
|
||||||
readOnly : true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
TextField {
|
TextField {
|
||||||
id: address
|
id: address
|
||||||
Layout.fillWidth: true
|
|
||||||
colorScheme: root.colorScheme
|
|
||||||
label: qsTr("Your contact email")
|
label: qsTr("Your contact email")
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
Layout.fillWidth: true
|
||||||
placeholderText: qsTr("e.g. jane.doe@protonmail.com")
|
placeholderText: qsTr("e.g. jane.doe@protonmail.com")
|
||||||
validator: function (str) {
|
|
||||||
|
validator: function(str) {
|
||||||
if (!isValidEmail(str)) {
|
if (!isValidEmail(str)) {
|
||||||
return qsTr("Enter valid email address");
|
return qsTr("Enter valid email address")
|
||||||
}
|
}
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TextField {
|
TextField {
|
||||||
id: emailClient
|
id: emailClient
|
||||||
Layout.fillWidth: true
|
|
||||||
colorScheme: root.colorScheme
|
|
||||||
label: qsTr("Your email client (including version)")
|
label: qsTr("Your email client (including version)")
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
Layout.fillWidth: true
|
||||||
placeholderText: qsTr("e.g. Apple Mail 14.0")
|
placeholderText: qsTr("e.g. Apple Mail 14.0")
|
||||||
validator: function (str) {
|
|
||||||
|
validator: function(str) {
|
||||||
if (str.length === 0) {
|
if (str.length === 0) {
|
||||||
return qsTr("Enter an email client name and version");
|
return qsTr("Enter an email client name and version")
|
||||||
}
|
}
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
CheckBox {
|
CheckBox {
|
||||||
id: includeLogs
|
id: includeLogs
|
||||||
checked: true
|
|
||||||
colorScheme: root.colorScheme
|
|
||||||
text: qsTr("Include my recent logs")
|
text: qsTr("Include my recent logs")
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
checked: true
|
||||||
}
|
}
|
||||||
Button {
|
Button {
|
||||||
Layout.leftMargin: 12
|
Layout.leftMargin: 12
|
||||||
colorScheme: root.colorScheme
|
|
||||||
secondary: true
|
|
||||||
text: qsTr("View logs")
|
text: qsTr("View logs")
|
||||||
|
secondary: true
|
||||||
|
colorScheme: root.colorScheme
|
||||||
onClicked: Qt.openUrlExternally(Backend.logsPath)
|
onClicked: Qt.openUrlExternally(Backend.logsPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TextEdit {
|
TextEdit {
|
||||||
|
text: qsTr("Reports are not end-to-end encrypted, please do not send any sensitive information.")
|
||||||
|
|
||||||
|
readOnly: true
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
color: root.colorScheme.text_weak
|
color: root.colorScheme.text_weak
|
||||||
font.family: ProtonStyle.font_family
|
font.family: ProtonStyle.font_family
|
||||||
font.letterSpacing: ProtonStyle.caption_letter_spacing
|
|
||||||
font.pixelSize: ProtonStyle.caption_font_size
|
|
||||||
font.weight: ProtonStyle.fontWeight_400
|
font.weight: ProtonStyle.fontWeight_400
|
||||||
readOnly: true
|
font.pixelSize: ProtonStyle.caption_font_size
|
||||||
selectByMouse: true
|
font.letterSpacing: ProtonStyle.caption_letter_spacing
|
||||||
selectedTextColor: root.colorScheme.text_invert
|
|
||||||
// No way to set lineHeight: ProtonStyle.caption_line_height
|
// No way to set lineHeight: ProtonStyle.caption_line_height
|
||||||
selectionColor: root.colorScheme.interaction_norm
|
selectionColor: root.colorScheme.interaction_norm
|
||||||
text: qsTr("Reports are not end-to-end encrypted, please do not send any sensitive information.")
|
selectedTextColor: root.colorScheme.text_invert
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
|
selectByMouse: true
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
id: sendButton
|
id: sendButton
|
||||||
|
text: qsTr("Send")
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
enabled: !loading
|
enabled: !loading
|
||||||
text: qsTr("Send")
|
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
description.validate();
|
description.validate()
|
||||||
address.validate();
|
address.validate()
|
||||||
emailClient.validate();
|
emailClient.validate()
|
||||||
|
|
||||||
if (description.error || address.error || emailClient.error) {
|
if (description.error || address.error || emailClient.error) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
submit();
|
|
||||||
|
submit()
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
function onBugReportSendSuccess() {
|
|
||||||
root.bugReportWasSent();
|
|
||||||
}
|
|
||||||
function onReportBugFinished() {
|
|
||||||
sendButton.loading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
target: Backend
|
target: Backend
|
||||||
|
function onReportBugFinished() { sendButton.loading = false }
|
||||||
|
function onBugReportSendSuccess() { root.bugReportWasSent() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
function setDescription(message) {
|
||||||
|
description.text = message
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDefaultValue() {
|
||||||
|
description.text = ""
|
||||||
|
address.text = root.selectedAddress
|
||||||
|
emailClient.text = Backend.currentEmailClient
|
||||||
|
includeLogs.checked = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidEmail(text){
|
||||||
|
var reEmail = /^[^@]+@[^@]+\.[A-Za-z]+\s*$/
|
||||||
|
return reEmail.test(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
function submit() {
|
||||||
|
sendButton.loading = true
|
||||||
|
Backend.reportBug(
|
||||||
|
description.text,
|
||||||
|
address.text,
|
||||||
|
emailClient.text,
|
||||||
|
includeLogs.checked
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
onVisibleChanged: {
|
||||||
|
root.setDefaultValue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,113 +0,0 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
|
||||||
// This file is part of Proton Mail Bridge.
|
|
||||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU General Public License for more details.
|
|
||||||
// You should have received a copy of the GNU General Public License
|
|
||||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Proton
|
|
||||||
|
|
||||||
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,82 +1,71 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
// it under the terms of the GNU General Public License as published by
|
// it under the terms of the GNU General Public License as published by
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
// (at your option) any later version.
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
// GNU General Public License for more details.
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import QtQuick.Controls.impl
|
import QtQuick.Controls.impl
|
||||||
|
|
||||||
import Proton
|
import Proton
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property int _margin: 24
|
|
||||||
property ColorScheme colorScheme
|
property ColorScheme colorScheme
|
||||||
property bool highlightPassword
|
|
||||||
property string hostname
|
|
||||||
property string password
|
|
||||||
property string port
|
|
||||||
property string security
|
|
||||||
property string title
|
property string title
|
||||||
|
property string hostname
|
||||||
|
property string port
|
||||||
property string username
|
property string username
|
||||||
|
property string password
|
||||||
|
property string security
|
||||||
|
|
||||||
|
implicitWidth: 304
|
||||||
|
implicitHeight: content.height + 2*root._margin
|
||||||
|
|
||||||
color: root.colorScheme.background_norm
|
color: root.colorScheme.background_norm
|
||||||
implicitHeight: content.height + 2 * root._margin
|
|
||||||
implicitWidth: 304
|
|
||||||
radius: ProtonStyle.card_radius
|
radius: ProtonStyle.card_radius
|
||||||
|
|
||||||
|
property int _margin: 24
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: content
|
id: content
|
||||||
spacing: 12
|
width: root.width - 2*root._margin
|
||||||
width: root.width - 2 * root._margin
|
anchors{
|
||||||
|
|
||||||
anchors {
|
|
||||||
bottomMargin: root._margin
|
|
||||||
left: root.left
|
|
||||||
leftMargin: root._margin
|
|
||||||
rightMargin: root._margin
|
|
||||||
top: root.top
|
top: root.top
|
||||||
topMargin: root._margin
|
left: root.left
|
||||||
|
leftMargin : root._margin
|
||||||
|
rightMargin : root._margin
|
||||||
|
topMargin : root._margin
|
||||||
|
bottomMargin : root._margin
|
||||||
}
|
}
|
||||||
|
|
||||||
|
spacing: 12
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
text: root.title
|
text: root.title
|
||||||
type: Label.Body_semibold
|
type: Label.Body_semibold
|
||||||
}
|
}
|
||||||
ConfigurationItem {
|
|
||||||
colorScheme: root.colorScheme
|
ConfigurationItem{ colorScheme: root.colorScheme; label: qsTr("Hostname") ; value: root.hostname }
|
||||||
label: qsTr("Hostname")
|
ConfigurationItem{ colorScheme: root.colorScheme; label: qsTr("Port") ; value: root.port }
|
||||||
value: root.hostname
|
ConfigurationItem{ colorScheme: root.colorScheme; label: qsTr("Username") ; value: root.username }
|
||||||
}
|
ConfigurationItem{ colorScheme: root.colorScheme; label: qsTr("Password") ; value: root.password }
|
||||||
ConfigurationItem {
|
ConfigurationItem{ colorScheme: root.colorScheme; label: qsTr("Security") ; value: root.security }
|
||||||
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,30 +1,35 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
// it under the terms of the GNU General Public License as published by
|
// it under the terms of the GNU General Public License as published by
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
// (at your option) any later version.
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
// GNU General Public License for more details.
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import QtQuick.Controls.impl
|
import QtQuick.Controls.impl
|
||||||
|
|
||||||
import Proton
|
import Proton
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
property var colorScheme
|
property var colorScheme
|
||||||
property string label
|
property string label
|
||||||
property string labelColor: root.colorScheme.text_norm
|
|
||||||
property string value
|
property string value
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
implicitHeight: children[0].implicitHeight
|
implicitHeight: children[0].implicitHeight
|
||||||
implicitWidth: children[0].implicitWidth
|
implicitWidth: children[0].implicitWidth
|
||||||
|
|
||||||
@ -36,49 +41,51 @@ Item {
|
|||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
Label {
|
Label {
|
||||||
color: labelColor
|
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
text: root.label
|
text: root.label
|
||||||
type: Label.Body_semibold
|
type: Label.Body
|
||||||
}
|
}
|
||||||
TextEdit {
|
TextEdit {
|
||||||
id: valueText
|
id: valueText
|
||||||
Layout.fillWidth: true
|
text: root.value
|
||||||
color: root.colorScheme.text_weak
|
color: root.colorScheme.text_weak
|
||||||
readOnly: true
|
readOnly: true
|
||||||
selectByKeyboard: true
|
|
||||||
selectByMouse: true
|
selectByMouse: true
|
||||||
|
selectByKeyboard: true
|
||||||
selectionColor: root.colorScheme.text_weak
|
selectionColor: root.colorScheme.text_weak
|
||||||
text: root.value
|
|
||||||
wrapMode: Text.WrapAnywhere
|
wrapMode: Text.WrapAnywhere
|
||||||
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
ColorImage {
|
ColorImage {
|
||||||
|
source: "/qml/icons/ic-copy.svg"
|
||||||
color: root.colorScheme.text_norm
|
color: root.colorScheme.text_norm
|
||||||
height: root.colorScheme.body_font_size
|
height: root.colorScheme.body_font_size
|
||||||
source: "/qml/icons/ic-copy.svg"
|
|
||||||
sourceSize.height: root.colorScheme.body_font_size
|
sourceSize.height: root.colorScheme.body_font_size
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
onClicked : {
|
||||||
onClicked: {
|
valueText.select(0, valueText.length)
|
||||||
valueText.select(0, valueText.length);
|
valueText.copy()
|
||||||
valueText.copy();
|
valueText.deselect()
|
||||||
valueText.deselect();
|
|
||||||
}
|
}
|
||||||
onPressed: parent.scale = 0.90
|
onPressed: parent.scale = 0.90
|
||||||
onReleased: parent.scale = 1
|
onReleased: parent.scale = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
color: root.colorScheme.border_norm
|
|
||||||
height: 1
|
height: 1
|
||||||
|
color: root.colorScheme.border_norm
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,138 +1,155 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
// it under the terms of the GNU General Public License as published by
|
// it under the terms of the GNU General Public License as published by
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
// (at your option) any later version.
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
// GNU General Public License for more details.
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import QtQuick.Controls.impl
|
import QtQuick.Controls.impl
|
||||||
|
|
||||||
import Proton
|
import Proton
|
||||||
|
|
||||||
SettingsView {
|
SettingsView {
|
||||||
id: root
|
id: root
|
||||||
function setDefaultValues() {
|
|
||||||
imapSSLButton.checked = Backend.useSSLForIMAP;
|
|
||||||
imapSTARTTLSButton.checked = !Backend.useSSLForIMAP;
|
|
||||||
smtpSSLButton.checked = Backend.useSSLForSMTP;
|
|
||||||
smtpSTARTTLSButton.checked = !Backend.useSSLForSMTP;
|
|
||||||
}
|
|
||||||
function submit() {
|
|
||||||
submitButton.loading = true;
|
|
||||||
Backend.setMailServerSettings(Backend.imapPort, Backend.smtpPort, imapSSLButton.checked, smtpSSLButton.checked);
|
|
||||||
}
|
|
||||||
|
|
||||||
fillHeight: false
|
fillHeight: false
|
||||||
|
|
||||||
onVisibleChanged: {
|
|
||||||
root.setDefaultValues();
|
|
||||||
}
|
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
Layout.fillWidth: true
|
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
text: qsTr("Connection mode")
|
text: qsTr("Connection mode")
|
||||||
type: Label.Heading
|
type: Label.Heading
|
||||||
}
|
|
||||||
Label {
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
color: root.colorScheme.text_weak
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
text: qsTr("Change the protocol Bridge and the email client use to connect for IMAP and SMTP.")
|
text: qsTr("Change the protocol Bridge and the email client use to connect for IMAP and SMTP.")
|
||||||
type: Label.Body
|
type: Label.Body
|
||||||
|
color: root.colorScheme.text_weak
|
||||||
|
Layout.fillWidth: true
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
spacing: 16
|
spacing: 16
|
||||||
|
|
||||||
ButtonGroup {
|
ButtonGroup{ id: imapProtocolSelection }
|
||||||
id: imapProtocolSelection
|
|
||||||
}
|
|
||||||
Label {
|
Label {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
text: qsTr("IMAP connection")
|
text: qsTr("IMAP connection")
|
||||||
}
|
}
|
||||||
|
|
||||||
RadioButton {
|
RadioButton {
|
||||||
id: imapSSLButton
|
id: imapSSLButton
|
||||||
ButtonGroup.group: imapProtocolSelection
|
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
|
ButtonGroup.group: imapProtocolSelection
|
||||||
text: qsTr("SSL")
|
text: qsTr("SSL")
|
||||||
}
|
}
|
||||||
|
|
||||||
RadioButton {
|
RadioButton {
|
||||||
id: imapSTARTTLSButton
|
id: imapSTARTTLSButton
|
||||||
ButtonGroup.group: imapProtocolSelection
|
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
|
ButtonGroup.group: imapProtocolSelection
|
||||||
text: qsTr("STARTTLS")
|
text: qsTr("STARTTLS")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
color: root.colorScheme.border_weak
|
|
||||||
height: 1
|
height: 1
|
||||||
|
color: root.colorScheme.border_weak
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
spacing: 16
|
spacing: 16
|
||||||
|
|
||||||
ButtonGroup {
|
ButtonGroup{ id: smtpProtocolSelection }
|
||||||
id: smtpProtocolSelection
|
|
||||||
}
|
|
||||||
Label {
|
Label {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
text: qsTr("SMTP connection")
|
text: qsTr("SMTP connection")
|
||||||
}
|
}
|
||||||
|
|
||||||
RadioButton {
|
RadioButton {
|
||||||
id: smtpSSLButton
|
id: smtpSSLButton
|
||||||
ButtonGroup.group: smtpProtocolSelection
|
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
|
ButtonGroup.group: smtpProtocolSelection
|
||||||
text: qsTr("SSL")
|
text: qsTr("SSL")
|
||||||
}
|
}
|
||||||
|
|
||||||
RadioButton {
|
RadioButton {
|
||||||
id: smtpSTARTTLSButton
|
id: smtpSTARTTLSButton
|
||||||
ButtonGroup.group: smtpProtocolSelection
|
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
|
ButtonGroup.group: smtpProtocolSelection
|
||||||
text: qsTr("STARTTLS")
|
text: qsTr("STARTTLS")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
color: root.colorScheme.border_weak
|
|
||||||
height: 1
|
height: 1
|
||||||
|
color: root.colorScheme.border_weak
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
spacing: 12
|
spacing: 12
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
id: submitButton
|
id: submitButton
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
enabled: (!loading) && ((imapSSLButton.checked !== Backend.useSSLForIMAP) || (smtpSSLButton.checked !== Backend.useSSLForSMTP))
|
|
||||||
text: qsTr("Save")
|
text: qsTr("Save")
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
submitButton.loading = true;
|
submitButton.loading = true
|
||||||
root.submit();
|
root.submit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enabled: (!loading) && ((imapSSLButton.checked !== Backend.useSSLForIMAP) || (smtpSSLButton.checked !== Backend.useSSLForSMTP))
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
secondary: true
|
|
||||||
text: qsTr("Cancel")
|
text: qsTr("Cancel")
|
||||||
|
|
||||||
onClicked: root.back()
|
onClicked: root.back()
|
||||||
|
secondary: true
|
||||||
}
|
}
|
||||||
Connections {
|
|
||||||
function onChangeMailServerSettingsFinished() {
|
|
||||||
submitButton.loading = false;
|
|
||||||
root.back();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
Connections {
|
||||||
target: Backend
|
target: Backend
|
||||||
|
|
||||||
|
function onChangeMailServerSettingsFinished() {
|
||||||
|
submitButton.loading = false
|
||||||
|
root.back()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,62 +1,36 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
// it under the terms of the GNU General Public License as published by
|
// it under the terms of the GNU General Public License as published by
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
// (at your option) any later version.
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
// GNU General Public License for more details.
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
|
|
||||||
import Proton
|
import Proton
|
||||||
import Notifications
|
import Notifications
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property ColorScheme colorScheme
|
property ColorScheme colorScheme
|
||||||
|
|
||||||
property var notifications
|
property var notifications
|
||||||
|
|
||||||
signal closeWindow
|
signal showSetupGuide(var user, string address)
|
||||||
signal quitBridge
|
signal closeWindow()
|
||||||
signal showClientConfigurator(var user, string address, bool justLoggedIn)
|
signal quitBridge()
|
||||||
signal showLogin(var username)
|
|
||||||
|
|
||||||
function selectUser(userID) {
|
|
||||||
const users = Backend.users;
|
|
||||||
for (let i = 0; i < users.count; i++) {
|
|
||||||
const user = users.get(i);
|
|
||||||
if (user.id !== userID) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
accounts.currentIndex = i;
|
|
||||||
if (user.state === EUserState.SignedOut)
|
|
||||||
showLogin(user.primaryEmailOrUsername());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.error("User with ID ", userID, " was not found in the account list");
|
|
||||||
}
|
|
||||||
function showBugReport() {
|
|
||||||
rightContent.showBugReport();
|
|
||||||
}
|
|
||||||
function showHelp() {
|
|
||||||
rightContent.showHelpView();
|
|
||||||
}
|
|
||||||
function showLocalCacheSettings() {
|
|
||||||
rightContent.showLocalCacheSettings();
|
|
||||||
}
|
|
||||||
function showSettings() {
|
|
||||||
rightContent.showGeneralSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasAccount() {
|
|
||||||
return Backend.users.count > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@ -64,13 +38,13 @@ Item {
|
|||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: leftBar
|
id: leftBar
|
||||||
|
|
||||||
property ColorScheme colorScheme: root.colorScheme.prominent
|
property ColorScheme colorScheme: root.colorScheme.prominent
|
||||||
|
|
||||||
Layout.fillHeight: true
|
|
||||||
Layout.maximumWidth: 320
|
|
||||||
Layout.minimumWidth: 264
|
Layout.minimumWidth: 264
|
||||||
|
Layout.maximumWidth: 320
|
||||||
Layout.preferredWidth: 320
|
Layout.preferredWidth: 320
|
||||||
|
Layout.fillHeight: true
|
||||||
|
|
||||||
color: colorScheme.background_norm
|
color: colorScheme.background_norm
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
@ -78,21 +52,24 @@ Item {
|
|||||||
spacing: 0
|
spacing: 0
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
id: topLeftBar
|
id:topLeftBar
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.maximumHeight: 60
|
|
||||||
Layout.minimumHeight: 60
|
Layout.minimumHeight: 60
|
||||||
|
Layout.maximumHeight: 60
|
||||||
Layout.preferredHeight: 60
|
Layout.preferredHeight: 60
|
||||||
spacing: 0
|
spacing: 0
|
||||||
|
|
||||||
Status {
|
Status {
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
Layout.bottomMargin: 17
|
|
||||||
Layout.leftMargin: 16
|
Layout.leftMargin: 16
|
||||||
Layout.topMargin: 24
|
Layout.topMargin: 24
|
||||||
|
Layout.bottomMargin: 17
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
|
||||||
colorScheme: leftBar.colorScheme
|
colorScheme: leftBar.colorScheme
|
||||||
notificationWhitelist: Notifications.Group.Connection | Notifications.Group.ForceUpdate
|
|
||||||
notifications: root.notifications
|
notifications: root.notifications
|
||||||
|
|
||||||
|
notificationWhitelist: Notifications.Group.Connection | Notifications.Group.ForceUpdate
|
||||||
}
|
}
|
||||||
|
|
||||||
// just a placeholder
|
// just a placeholder
|
||||||
@ -100,38 +77,47 @@ Item {
|
|||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
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
|
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.rightMargin: 4
|
||||||
|
|
||||||
horizontalPadding: 0
|
horizontalPadding: 0
|
||||||
|
|
||||||
icon.source: "/qml/icons/ic-question-circle.svg"
|
icon.source: "/qml/icons/ic-question-circle.svg"
|
||||||
|
|
||||||
onClicked: rightContent.showHelpView()
|
onClicked: rightContent.showHelpView()
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
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
|
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.rightMargin: 4
|
||||||
|
|
||||||
horizontalPadding: 0
|
horizontalPadding: 0
|
||||||
|
|
||||||
icon.source: "/qml/icons/ic-cog-wheel.svg"
|
icon.source: "/qml/icons/ic-cog-wheel.svg"
|
||||||
|
|
||||||
onClicked: rightContent.showGeneralSettings()
|
onClicked: rightContent.showGeneralSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
id: dotMenuButton
|
id: dotMenuButton
|
||||||
Layout.bottomMargin: 9
|
Layout.bottomMargin: 9
|
||||||
@ -148,7 +134,7 @@ Item {
|
|||||||
icon.source: "/qml/icons/ic-three-dots-vertical.svg"
|
icon.source: "/qml/icons/ic-three-dots-vertical.svg"
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
dotMenu.open();
|
dotMenu.open()
|
||||||
}
|
}
|
||||||
|
|
||||||
Menu {
|
Menu {
|
||||||
@ -157,329 +143,332 @@ Item {
|
|||||||
modal: true
|
modal: true
|
||||||
y: dotMenuButton.Layout.preferredHeight + dotMenuButton.Layout.bottomMargin
|
y: dotMenuButton.Layout.preferredHeight + dotMenuButton.Layout.bottomMargin
|
||||||
|
|
||||||
onClosed: {
|
|
||||||
parent.checked = false;
|
|
||||||
}
|
|
||||||
onOpened: {
|
|
||||||
parent.checked = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
MenuItem {
|
MenuItem {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
text: qsTr("Close window")
|
text: qsTr("Close window")
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
root.closeWindow();
|
root.closeWindow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MenuItem {
|
MenuItem {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
text: qsTr("Quit Bridge")
|
text: qsTr("Quit Bridge")
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
root.quitBridge();
|
root.quitBridge()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onClosed: {
|
||||||
|
parent.checked = false
|
||||||
|
}
|
||||||
|
onOpened: {
|
||||||
|
parent.checked = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Item {
|
|
||||||
implicitHeight: 10
|
Item {implicitHeight:10}
|
||||||
}
|
|
||||||
|
|
||||||
// Separator line
|
// Separator line
|
||||||
Rectangle {
|
Rectangle {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.maximumHeight: 1
|
|
||||||
Layout.minimumHeight: 1
|
Layout.minimumHeight: 1
|
||||||
|
Layout.maximumHeight: 1
|
||||||
color: leftBar.colorScheme.border_weak
|
color: leftBar.colorScheme.border_weak
|
||||||
}
|
}
|
||||||
Item {
|
|
||||||
id: noAccountBox
|
|
||||||
|
|
||||||
Layout.fillHeight: true
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.topMargin: 24
|
|
||||||
visible: !hasAccount()
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
spacing: 8
|
|
||||||
|
|
||||||
Label {
|
|
||||||
colorScheme: leftBar.colorScheme
|
|
||||||
color: colorScheme.text_weak
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
text: qsTr("No accounts")
|
|
||||||
}
|
|
||||||
Button {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.leftMargin: 16
|
|
||||||
Layout.rightMargin: 16
|
|
||||||
colorScheme: leftBar.colorScheme
|
|
||||||
text: qsTr("Add an account")
|
|
||||||
secondary: true
|
|
||||||
onClicked: root.showLogin("")
|
|
||||||
}
|
|
||||||
Item {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.fillHeight: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
ListView {
|
ListView {
|
||||||
id: accounts
|
id: accounts
|
||||||
|
|
||||||
property var _leftRightMargins: 16
|
|
||||||
property var _topBottomMargins: 24
|
property var _topBottomMargins: 24
|
||||||
|
property var _leftRightMargins: 16
|
||||||
|
|
||||||
Layout.bottomMargin: accounts._topBottomMargins
|
|
||||||
Layout.fillHeight: true
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
Layout.leftMargin: accounts._leftRightMargins
|
Layout.leftMargin: accounts._leftRightMargins
|
||||||
Layout.rightMargin: accounts._leftRightMargins
|
Layout.rightMargin: accounts._leftRightMargins
|
||||||
Layout.topMargin: accounts._topBottomMargins
|
Layout.topMargin: accounts._topBottomMargins
|
||||||
boundsBehavior: Flickable.StopAtBounds
|
Layout.bottomMargin: accounts._topBottomMargins
|
||||||
clip: true
|
|
||||||
model: Backend.users
|
|
||||||
spacing: 12
|
spacing: 12
|
||||||
visible: hasAccount()
|
clip: true
|
||||||
delegate: Item {
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
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
|
|
||||||
|
|
||||||
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 {
|
header: Rectangle {
|
||||||
height: headerLabel.height + 16
|
height: headerLabel.height+16
|
||||||
|
|
||||||
// color: ProtonStyle.transparent
|
// color: ProtonStyle.transparent
|
||||||
Label {
|
Label{
|
||||||
id: headerLabel
|
|
||||||
colorScheme: leftBar.colorScheme
|
colorScheme: leftBar.colorScheme
|
||||||
|
id: headerLabel
|
||||||
text: qsTr("Accounts")
|
text: qsTr("Accounts")
|
||||||
type: Label.LabelType.Body
|
type: Label.LabelType.Body
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
highlight: Rectangle {
|
highlight: Rectangle {
|
||||||
color: leftBar.colorScheme.interaction_default_active
|
color: leftBar.colorScheme.interaction_default_active
|
||||||
radius: ProtonStyle.account_row_radius
|
radius: ProtonStyle.account_row_radius
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model: Backend.users
|
||||||
|
delegate: Item {
|
||||||
|
width: leftBar.width - 2*accounts._leftRightMargins
|
||||||
|
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
|
||||||
|
implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
|
||||||
|
|
||||||
|
AccountDelegate {
|
||||||
|
id: accountDelegate
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.topMargin: 8
|
||||||
|
anchors.bottomMargin: 8
|
||||||
|
anchors.leftMargin: 12
|
||||||
|
anchors.rightMargin: 12
|
||||||
|
|
||||||
|
colorScheme: leftBar.colorScheme
|
||||||
|
user: Backend.users.get(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
onClicked: {
|
||||||
|
var user = Backend.users.get(index)
|
||||||
|
accounts.currentIndex = index
|
||||||
|
if (!user) return
|
||||||
|
if (user.state !== EUserState.SignedOut) {
|
||||||
|
rightContent.showAccount()
|
||||||
|
} else {
|
||||||
|
signIn.username = user.primaryEmailOrUsername()
|
||||||
|
rightContent.showSignIn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Separator
|
// Separator
|
||||||
Rectangle {
|
Rectangle {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.maximumHeight: 1
|
|
||||||
Layout.minimumHeight: 1
|
Layout.minimumHeight: 1
|
||||||
|
Layout.maximumHeight: 1
|
||||||
color: leftBar.colorScheme.border_weak
|
color: leftBar.colorScheme.border_weak
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: bottomLeftBar
|
id: bottomLeftBar
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.maximumHeight: 52
|
|
||||||
Layout.minimumHeight: 52
|
Layout.minimumHeight: 52
|
||||||
|
Layout.maximumHeight: 52
|
||||||
Layout.preferredHeight: 52
|
Layout.preferredHeight: 52
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: 16
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.topMargin: 7
|
|
||||||
colorScheme: leftBar.colorScheme
|
colorScheme: leftBar.colorScheme
|
||||||
height: 36
|
|
||||||
horizontalPadding: 0
|
|
||||||
icon.source: "/qml/icons/ic-plus.svg"
|
|
||||||
width: 36
|
width: 36
|
||||||
|
height: 36
|
||||||
|
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.top: parent.top
|
||||||
|
|
||||||
|
anchors.leftMargin: 16
|
||||||
|
anchors.topMargin: 7
|
||||||
|
|
||||||
|
horizontalPadding: 0
|
||||||
|
|
||||||
|
icon.source: "/qml/icons/ic-plus.svg"
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
root.showLogin("");
|
signIn.username = ""
|
||||||
|
rightContent.showSignIn()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Rectangle {
|
|
||||||
Layout.fillHeight: true // right content background
|
Rectangle { // right content background
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
|
||||||
color: colorScheme.background_norm
|
color: colorScheme.background_norm
|
||||||
|
|
||||||
StackLayout {
|
StackLayout {
|
||||||
id: rightContent
|
id: rightContent
|
||||||
function showAccount(index) {
|
|
||||||
if (index !== undefined && index >= 0) {
|
|
||||||
accounts.currentIndex = index;
|
|
||||||
}
|
|
||||||
rightContent.currentIndex = 0;
|
|
||||||
}
|
|
||||||
function showBugReport() {
|
|
||||||
rightContent.currentIndex = 8;
|
|
||||||
}
|
|
||||||
function showConnectionModeSettings() {
|
|
||||||
rightContent.currentIndex = 5;
|
|
||||||
}
|
|
||||||
function showGeneralSettings() {
|
|
||||||
rightContent.currentIndex = 2;
|
|
||||||
}
|
|
||||||
function showHelpView() {
|
|
||||||
rightContent.currentIndex = 7;
|
|
||||||
}
|
|
||||||
function showKeychainSettings() {
|
|
||||||
rightContent.currentIndex = 3;
|
|
||||||
}
|
|
||||||
function showLocalCacheSettings() {
|
|
||||||
rightContent.currentIndex = 6;
|
|
||||||
}
|
|
||||||
function showPortSettings() {
|
|
||||||
rightContent.currentIndex = 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
||||||
StackLayout {
|
AccountView { // 0
|
||||||
// 0
|
colorScheme: root.colorScheme
|
||||||
currentIndex: hasAccount() ? 1 : 0
|
notifications: root.notifications
|
||||||
NoAccountView {
|
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
|
||||||
|
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
onStartSetup: {
|
onClicked: {
|
||||||
root.showLogin("")
|
signIn.abort()
|
||||||
|
rightContent.showAccount()
|
||||||
}
|
}
|
||||||
|
icon.source: "/qml/icons/ic-arrow-left.svg"
|
||||||
|
secondary: true
|
||||||
|
horizontalPadding: 8
|
||||||
}
|
}
|
||||||
AccountView {
|
|
||||||
|
SignIn {
|
||||||
|
id: signIn
|
||||||
|
Layout.topMargin: 68
|
||||||
|
Layout.leftMargin: 80 - backButton.width - 18
|
||||||
|
Layout.rightMargin: 80
|
||||||
|
Layout.bottomMargin: 68
|
||||||
|
Layout.preferredWidth: 320
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
notifications: root.notifications
|
|
||||||
user: {
|
|
||||||
if (accounts.currentIndex < 0)
|
|
||||||
return undefined;
|
|
||||||
if (Backend.users.count === 0)
|
|
||||||
return undefined;
|
|
||||||
return Backend.users.get(accounts.currentIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
onShowClientConfigurator: function (user, address, justLoggedIn) {
|
|
||||||
root.showClientConfigurator(user, address, justLoggedIn);
|
|
||||||
}
|
|
||||||
onShowLogin: function (username) {
|
|
||||||
root.showLogin(username);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Rectangle {
|
|
||||||
Layout.fillHeight: true
|
GeneralSettings { // 2
|
||||||
Layout.fillWidth: true
|
|
||||||
color: "#ff9900"
|
|
||||||
}
|
|
||||||
GeneralSettings {
|
|
||||||
// 2
|
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
notifications: root.notifications
|
notifications: root.notifications
|
||||||
|
|
||||||
onBack: {
|
onBack: {
|
||||||
rightContent.showAccount();
|
rightContent.showAccount()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeychainSettings {
|
|
||||||
// 3
|
KeychainSettings { // 3
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
|
|
||||||
onBack: {
|
onBack: {
|
||||||
rightContent.showGeneralSettings();
|
rightContent.showGeneralSettings()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PortSettings {
|
|
||||||
// 4
|
PortSettings { // 4
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
notifications: root.notifications
|
||||||
|
onBack: {
|
||||||
|
rightContent.showGeneralSettings()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ConnectionModeSettings { // 5
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
|
||||||
|
onBack: {
|
||||||
|
rightContent.showGeneralSettings()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalCacheSettings { // 6
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
notifications: root.notifications
|
notifications: root.notifications
|
||||||
|
|
||||||
onBack: {
|
onBack: {
|
||||||
rightContent.showGeneralSettings();
|
rightContent.showGeneralSettings()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ConnectionModeSettings {
|
|
||||||
// 5
|
HelpView { // 7
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
|
|
||||||
onBack: {
|
onBack: {
|
||||||
rightContent.showGeneralSettings();
|
rightContent.showAccount()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LocalCacheSettings {
|
|
||||||
// 6
|
|
||||||
colorScheme: root.colorScheme
|
|
||||||
notifications: root.notifications
|
|
||||||
|
|
||||||
onBack: {
|
BugReportView { // 8
|
||||||
rightContent.showGeneralSettings();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
HelpView {
|
|
||||||
// 7
|
|
||||||
colorScheme: root.colorScheme
|
|
||||||
|
|
||||||
onBack: {
|
|
||||||
rightContent.showAccount();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
BugReportFlow {
|
|
||||||
// 8
|
|
||||||
id: bugReport
|
id: bugReport
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
selectedAddress: {
|
selectedAddress: {
|
||||||
if (accounts.currentIndex < 0)
|
if (accounts.currentIndex < 0) return ""
|
||||||
return "";
|
if (Backend.users.count == 0) return ""
|
||||||
if (Backend.users.count === 0)
|
var user = Backend.users.get(accounts.currentIndex)
|
||||||
return "";
|
if (!user) return ""
|
||||||
const user = Backend.users.get(accounts.currentIndex);
|
return user.addresses[0]
|
||||||
if (!user)
|
|
||||||
return "";
|
|
||||||
return user.addresses[0];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onBack: {
|
onBack: {
|
||||||
rightContent.showHelpView();
|
rightContent.showHelpView()
|
||||||
}
|
|
||||||
onBugReportWasSent: {
|
|
||||||
rightContent.showAccount();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Connections {
|
|
||||||
function onLoginAlreadyLoggedIn(index) {
|
|
||||||
rightContent.showAccount(index);
|
|
||||||
}
|
|
||||||
function onLoginFinished(index) {
|
|
||||||
rightContent.showAccount(index);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onBugReportWasSent: {
|
||||||
|
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
|
target: Backend
|
||||||
|
|
||||||
|
function onLoginFinished(index) { rightContent.showAccount(index) }
|
||||||
|
function onLoginAlreadyLoggedIn(index) { rightContent.showAccount(index) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
function showLocalCacheSettings(){rightContent.showLocalCacheSettings() }
|
||||||
|
function showSettings(){rightContent.showGeneralSettings() }
|
||||||
|
function showHelp(){rightContent.showHelpView() }
|
||||||
|
function showSignIn(username){
|
||||||
|
signIn.username = username
|
||||||
|
rightContent.showSignIn()
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectUser(userID) {
|
||||||
|
var users = Backend.users;
|
||||||
|
for (var i = 0; i < users.count; i++) {
|
||||||
|
var user = users.get(i)
|
||||||
|
if (user.id !== userID) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
accounts.currentIndex = i;
|
||||||
|
if (user.state === EUserState.SignedOut)
|
||||||
|
showSignIn(user.primaryEmailOrUsername())
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.error("User with ID ", userID, " was not found in the account list")
|
||||||
|
}
|
||||||
|
|
||||||
|
function showBugReportAndPrefill(description) {
|
||||||
|
rightContent.showBugReport()
|
||||||
|
bugReport.setDescription(description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,45 +1,54 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
// it under the terms of the GNU General Public License as published by
|
// it under the terms of the GNU General Public License as published by
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
// (at your option) any later version.
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
// GNU General Public License for more details.
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
|
|
||||||
import "."
|
import "."
|
||||||
import "Proton"
|
import "./Proton"
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
property var target: parent
|
property var target: parent
|
||||||
|
|
||||||
border.color: "red"
|
|
||||||
border.width: 1
|
|
||||||
color: "transparent"
|
|
||||||
height: target.height
|
|
||||||
width: target.width
|
|
||||||
x: target.x
|
x: target.x
|
||||||
y: target.y
|
y: target.y
|
||||||
|
width: target.width
|
||||||
|
height: target.height
|
||||||
|
|
||||||
|
color: "transparent"
|
||||||
|
border.color: "red"
|
||||||
|
border.width: 1
|
||||||
//z: parent.z - 1
|
//z: parent.z - 1
|
||||||
z: 10000000
|
z: 10000000
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
|
text: parent.width + "x" + parent.height
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
color: "black"
|
color: "black"
|
||||||
colorScheme: ProtonStyle.currentStyle
|
colorScheme: ProtonStyle.currentStyle
|
||||||
text: parent.width + "x" + parent.height
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
|
width: target.implicitWidth
|
||||||
|
height: target.implicitHeight
|
||||||
|
|
||||||
|
color: "transparent"
|
||||||
border.color: "green"
|
border.color: "green"
|
||||||
border.width: 1
|
border.width: 1
|
||||||
color: "transparent"
|
|
||||||
height: target.implicitHeight
|
|
||||||
width: target.implicitWidth
|
|
||||||
//z: parent.z - 1
|
//z: parent.z - 1
|
||||||
z: 10000000
|
z: 10000000
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,19 +1,25 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
// it under the terms of the GNU General Public License as published by
|
// it under the terms of the GNU General Public License as published by
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
// (at your option) any later version.
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
// GNU General Public License for more details.
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import QtQuick.Controls.impl
|
import QtQuick.Controls.impl
|
||||||
|
|
||||||
import Proton
|
import Proton
|
||||||
|
|
||||||
SettingsView {
|
SettingsView {
|
||||||
@ -25,138 +31,144 @@ SettingsView {
|
|||||||
fillHeight: false
|
fillHeight: false
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
Layout.fillWidth: true
|
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
text: qsTr("Settings")
|
text: qsTr("Settings")
|
||||||
type: Label.Heading
|
type: Label.Heading
|
||||||
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsItem {
|
SettingsItem {
|
||||||
id: autoUpdate
|
id: autoUpdate
|
||||||
Layout.fillWidth: true
|
|
||||||
checked: Backend.isAutomaticUpdateOn
|
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
description: qsTr("Bridge will automatically update in the background.")
|
|
||||||
text: qsTr("Automatic updates")
|
text: qsTr("Automatic updates")
|
||||||
|
description: qsTr("Bridge will automatically update in the background.")
|
||||||
type: SettingsItem.Toggle
|
type: SettingsItem.Toggle
|
||||||
|
checked: Backend.isAutomaticUpdateOn
|
||||||
onClicked: Backend.toggleAutomaticUpdate(!autoUpdate.checked)
|
onClicked: Backend.toggleAutomaticUpdate(!autoUpdate.checked)
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsItem {
|
SettingsItem {
|
||||||
id: autostart
|
id: autostart
|
||||||
Layout.fillWidth: true
|
|
||||||
checked: Backend.isAutostartOn
|
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
description: qsTr("Bridge will open upon startup.")
|
|
||||||
text: qsTr("Open on startup")
|
text: qsTr("Open on startup")
|
||||||
|
description: qsTr("Bridge will open upon startup.")
|
||||||
type: SettingsItem.Toggle
|
type: SettingsItem.Toggle
|
||||||
|
checked: Backend.isAutostartOn
|
||||||
onClicked: {
|
onClicked: {
|
||||||
autostart.loading = true;
|
autostart.loading = true
|
||||||
Backend.toggleAutostart(!autostart.checked);
|
Backend.toggleAutostart(!autostart.checked)
|
||||||
}
|
}
|
||||||
|
Connections{
|
||||||
Connections {
|
|
||||||
function onToggleAutostartFinished() {
|
|
||||||
autostart.loading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
target: Backend
|
target: Backend
|
||||||
|
function onToggleAutostartFinished() {
|
||||||
|
autostart.loading = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsItem {
|
SettingsItem {
|
||||||
id: beta
|
id: beta
|
||||||
Layout.fillWidth: true
|
|
||||||
checked: Backend.isBetaEnabled
|
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
description: qsTr("Be among the first to try new features.")
|
|
||||||
text: qsTr("Beta access")
|
text: qsTr("Beta access")
|
||||||
|
description: qsTr("Be among the first to try new features.")
|
||||||
type: SettingsItem.Toggle
|
type: SettingsItem.Toggle
|
||||||
|
checked: Backend.isBetaEnabled
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (!beta.checked) {
|
if (!beta.checked) {
|
||||||
root.notifications.askEnableBeta();
|
root.notifications.askEnableBeta()
|
||||||
} else {
|
} else {
|
||||||
Backend.toggleBeta(false);
|
Backend.toggleBeta(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
ColorImage {
|
ColorImage {
|
||||||
Layout.alignment: Qt.AlignCenter
|
Layout.alignment: Qt.AlignCenter
|
||||||
|
|
||||||
|
source: root._isAdvancedShown ? "/qml/icons/ic-chevron-down.svg" : "/qml/icons/ic-chevron-right.svg"
|
||||||
color: root.colorScheme.interaction_norm
|
color: root.colorScheme.interaction_norm
|
||||||
height: root.colorScheme.body_font_size
|
height: root.colorScheme.body_font_size
|
||||||
source: root._isAdvancedShown ? "/qml/icons/ic-chevron-down.svg" : "/qml/icons/ic-chevron-right.svg"
|
|
||||||
sourceSize.height: root.colorScheme.body_font_size
|
sourceSize.height: root.colorScheme.body_font_size
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
||||||
onClicked: root._isAdvancedShown = !root._isAdvancedShown
|
onClicked: root._isAdvancedShown = !root._isAdvancedShown
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
id: advSettLabel
|
id: advSettLabel
|
||||||
color: root.colorScheme.interaction_norm
|
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
text: qsTr("Advanced settings")
|
text: qsTr("Advanced settings")
|
||||||
|
color: root.colorScheme.interaction_norm
|
||||||
type: Label.Body
|
type: Label.Body
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
||||||
onClicked: root._isAdvancedShown = !root._isAdvancedShown
|
onClicked: root._isAdvancedShown = !root._isAdvancedShown
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsItem {
|
SettingsItem {
|
||||||
id: keychains
|
id: keychains
|
||||||
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
|
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()
|
onClicked: root.parent.showKeychainSettings()
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsItem {
|
SettingsItem {
|
||||||
id: doh
|
id: doh
|
||||||
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
|
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)
|
onClicked: Backend.toggleDoH(!doh.checked)
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsItem {
|
SettingsItem {
|
||||||
id: darkMode
|
id: darkMode
|
||||||
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
|
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")
|
||||||
|
|
||||||
onClicked: Backend.changeColorScheme(darkMode.checked ? "light" : "dark")
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsItem {
|
SettingsItem {
|
||||||
id: allMail
|
id: allMail
|
||||||
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
|
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)
|
onClicked: root.notifications.askChangeAllMailVisibility(Backend.isAllMailVisible)
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsItem {
|
SettingsItem {
|
||||||
id: telemetry
|
id: telemetry
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
@ -169,68 +181,73 @@ SettingsView {
|
|||||||
|
|
||||||
onClicked: Backend.toggleIsTelemetryDisabled(telemetry.checked)
|
onClicked: Backend.toggleIsTelemetryDisabled(telemetry.checked)
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsItem {
|
SettingsItem {
|
||||||
id: ports
|
id: ports
|
||||||
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
|
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()
|
onClicked: root.parent.showPortSettings()
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsItem {
|
SettingsItem {
|
||||||
id: imap
|
id: imap
|
||||||
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
|
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()
|
onClicked: root.parent.showConnectionModeSettings()
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsItem {
|
SettingsItem {
|
||||||
id: cache
|
id: cache
|
||||||
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
|
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()
|
onClicked: root.parent.showLocalCacheSettings()
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsItem {
|
SettingsItem {
|
||||||
id: exportTLSCertificates
|
id: 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
|
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: {
|
onClicked: {
|
||||||
Backend.exportTLSCertificates();
|
Backend.exportTLSCertificates()
|
||||||
}
|
}
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsItem {
|
SettingsItem {
|
||||||
id: reset
|
id: reset
|
||||||
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
|
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: {
|
onClicked: {
|
||||||
root.notifications.askResetBridge();
|
root.notifications.askResetBridge()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,109 +1,126 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
// it under the terms of the GNU General Public License as published by
|
// it under the terms of the GNU General Public License as published by
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
// (at your option) any later version.
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
// GNU General Public License for more details.
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
|
|
||||||
import Proton
|
import Proton
|
||||||
|
|
||||||
SettingsView {
|
SettingsView {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
fillHeight: true
|
fillHeight: true
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
Layout.fillWidth: true
|
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
text: qsTr("Help")
|
text: qsTr("Help")
|
||||||
type: Label.Heading
|
type: Label.Heading
|
||||||
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsItem {
|
SettingsItem {
|
||||||
id: setupPage
|
id: setupPage
|
||||||
Layout.fillWidth: true
|
|
||||||
actionIcon: "/qml/icons/ic-external-link.svg"
|
|
||||||
actionText: qsTr("Go to help topics")
|
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
description: qsTr("Get help setting up your client with our instructions and FAQs.")
|
|
||||||
text: qsTr("Installation and setup")
|
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
|
type: SettingsItem.PrimaryButton
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
Backend.openKBArticle();
|
Backend.notifyKBArticleClicked("https://proton.me/support/bridge");
|
||||||
}
|
Qt.openUrlExternally("https://proton.me/support/bridge")}
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsItem {
|
SettingsItem {
|
||||||
id: checkUpdates
|
id: checkUpdates
|
||||||
Layout.fillWidth: true
|
|
||||||
actionText: qsTr("Check now")
|
|
||||||
colorScheme: root.colorScheme
|
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")
|
text: qsTr("Updates")
|
||||||
|
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.")
|
||||||
type: SettingsItem.Button
|
type: SettingsItem.Button
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
checkUpdates.loading = true;
|
checkUpdates.loading = true
|
||||||
Backend.checkUpdates();
|
Backend.checkUpdates()
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
function onCheckUpdatesFinished() {
|
|
||||||
checkUpdates.loading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
target: Backend
|
target: Backend
|
||||||
|
function onCheckUpdatesFinished() { checkUpdates.loading = false }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsItem {
|
SettingsItem {
|
||||||
id: logs
|
id: logs
|
||||||
Layout.fillWidth: true
|
|
||||||
actionText: qsTr("View logs")
|
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
description: qsTr("Open and review logs to troubleshoot.")
|
|
||||||
text: qsTr("Logs")
|
text: qsTr("Logs")
|
||||||
|
actionText: qsTr("View logs")
|
||||||
|
description: qsTr("Open and review logs to troubleshoot.")
|
||||||
type: SettingsItem.Button
|
type: SettingsItem.Button
|
||||||
|
|
||||||
onClicked: Qt.openUrlExternally(Backend.logsPath)
|
onClicked: Qt.openUrlExternally(Backend.logsPath)
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsItem {
|
SettingsItem {
|
||||||
id: reportBug
|
id: reportBug
|
||||||
Layout.fillWidth: true
|
|
||||||
actionText: qsTr("Report problem")
|
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
description: qsTr("Something not working as expected? Let us know.")
|
|
||||||
text: qsTr("Report a problem")
|
text: qsTr("Report a problem")
|
||||||
|
actionText: qsTr("Report a problem")
|
||||||
|
description: qsTr("Something not working as expected? Let us know.")
|
||||||
type: SettingsItem.Button
|
type: SettingsItem.Button
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
Backend.updateCurrentMailClient();
|
Backend.updateCurrentMailClient()
|
||||||
Backend.notifyReportBugClicked();
|
Backend.notifyReportBugClicked()
|
||||||
root.parent.showBugReport();
|
root.parent.showBugReport()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
// fill height so the footer label will always be attached to the bottom
|
// fill height so the footer label will be always attached to the bottom
|
||||||
Item {
|
Item {
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
color: root.colorScheme.text_weak
|
|
||||||
colorScheme: root.colorScheme
|
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
|
type: Label.Caption
|
||||||
|
color: root.colorScheme.text_weak
|
||||||
|
textFormat: Text.StyledText
|
||||||
|
|
||||||
onLinkActivated: function (link) {
|
horizontalAlignment: Text.AlignHCenter
|
||||||
Qt.openUrlExternally(link)
|
|
||||||
}
|
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) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,105 +1,116 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
// it under the terms of the GNU General Public License as published by
|
// it under the terms of the GNU General Public License as published by
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
// (at your option) any later version.
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
// GNU General Public License for more details.
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import QtQuick.Controls.impl
|
import QtQuick.Controls.impl
|
||||||
|
|
||||||
import Proton
|
import Proton
|
||||||
|
|
||||||
SettingsView {
|
SettingsView {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property bool _valuesChanged: keychainSelection.checkedButton && keychainSelection.checkedButton.text !== Backend.currentKeychain
|
|
||||||
|
|
||||||
function setDefaultValues() {
|
|
||||||
for (const bi in keychainSelection.buttons) {
|
|
||||||
const button = keychainSelection.buttons[bi];
|
|
||||||
if (button.text === Backend.currentKeychain) {
|
|
||||||
button.checked = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fillHeight: false
|
fillHeight: false
|
||||||
|
|
||||||
Component.onCompleted: root.setDefaultValues()
|
property bool _valuesChanged: keychainSelection.checkedButton && keychainSelection.checkedButton.text != Backend.currentKeychain
|
||||||
onBack: {
|
|
||||||
root.setDefaultValues();
|
|
||||||
}
|
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
Layout.fillWidth: true
|
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
text: qsTr("Default keychain")
|
text: qsTr("Default keychain")
|
||||||
type: Label.Heading
|
type: Label.Heading
|
||||||
}
|
|
||||||
Label {
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
color: root.colorScheme.text_weak
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
text: qsTr("Change which keychain Bridge uses as default")
|
text: qsTr("Change which keychain Bridge uses as default")
|
||||||
type: Label.Body
|
type: Label.Body
|
||||||
|
color: root.colorScheme.text_weak
|
||||||
|
Layout.fillWidth: true
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
spacing: 16
|
spacing: 16
|
||||||
|
|
||||||
ButtonGroup {
|
ButtonGroup{ id: keychainSelection }
|
||||||
id: keychainSelection
|
|
||||||
}
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: Backend.availableKeychain
|
model: Backend.availableKeychain
|
||||||
|
|
||||||
RadioButton {
|
RadioButton {
|
||||||
ButtonGroup.group: keychainSelection
|
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
|
ButtonGroup.group: keychainSelection
|
||||||
text: modelData
|
text: modelData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
color: root.colorScheme.border_weak
|
|
||||||
height: 1
|
height: 1
|
||||||
|
color: root.colorScheme.border_weak
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
spacing: 12
|
spacing: 12
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
id: submitButton
|
id: submitButton
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
enabled: root._valuesChanged
|
|
||||||
text: qsTr("Save and restart")
|
text: qsTr("Save and restart")
|
||||||
|
enabled: root._valuesChanged
|
||||||
onClicked: {
|
onClicked: {
|
||||||
Backend.changeKeychain(keychainSelection.checkedButton.text);
|
Backend.changeKeychain(keychainSelection.checkedButton.text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
secondary: true
|
|
||||||
text: qsTr("Cancel")
|
text: qsTr("Cancel")
|
||||||
|
|
||||||
onClicked: root.back()
|
onClicked: root.back()
|
||||||
|
secondary: true
|
||||||
}
|
}
|
||||||
Connections {
|
|
||||||
function onChangeKeychainFinished() {
|
|
||||||
submitButton.loading = false;
|
|
||||||
root.back();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
Connections {
|
||||||
target: Backend
|
target: Backend
|
||||||
|
|
||||||
|
function onChangeKeychainFinished() {
|
||||||
|
submitButton.loading = false
|
||||||
|
root.back()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,88 +1,81 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
// it under the terms of the GNU General Public License as published by
|
// it under the terms of the GNU General Public License as published by
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
// (at your option) any later version.
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
// GNU General Public License for more details.
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import QtQuick.Controls.impl
|
import QtQuick.Controls.impl
|
||||||
import QtQuick.Dialogs
|
import QtQuick.Dialogs
|
||||||
|
|
||||||
import Proton
|
import Proton
|
||||||
|
|
||||||
SettingsView {
|
SettingsView {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
function setDefaultValues() {
|
|
||||||
root.diskCachePath = Backend.diskCachePath;
|
|
||||||
root.refresh();
|
|
||||||
}
|
|
||||||
function submit() {
|
|
||||||
submitButton.loading = true;
|
|
||||||
Backend.setDiskCachePath(root.diskCachePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
fillHeight: false
|
fillHeight: false
|
||||||
|
|
||||||
onBack: {
|
property var notifications
|
||||||
root.setDefaultValues();
|
property url diskCachePath: pathDialog.shortcuts.home
|
||||||
}
|
|
||||||
onVisibleChanged: {
|
function refresh() {
|
||||||
root.setDefaultValues();
|
diskCacheSetting.description = Backend.nativePath(root.diskCachePath)
|
||||||
|
submitButton.enabled = (!submitButton.loading) && !Backend.areSameFileOrFolder(Backend.diskCachePath, root.diskCachePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
Layout.fillWidth: true
|
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
text: qsTr("Local cache")
|
text: qsTr("Local cache")
|
||||||
type: Label.Heading
|
type: Label.Heading
|
||||||
}
|
|
||||||
Label {
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
color: root.colorScheme.text_weak
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
text: qsTr("Bridge stores your encrypted messages locally to optimize communication with your client.")
|
text: qsTr("Bridge stores your encrypted messages locally to optimize communication with your client.")
|
||||||
type: Label.Body
|
type: Label.Body
|
||||||
|
color: root.colorScheme.text_weak
|
||||||
|
Layout.fillWidth: true
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsItem {
|
SettingsItem {
|
||||||
id: diskCacheSetting
|
id: diskCacheSetting
|
||||||
Layout.fillWidth: true
|
|
||||||
actionText: qsTr("Change location")
|
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
descriptionWrap: Text.WrapAnywhere
|
|
||||||
text: qsTr("Current cache location")
|
text: qsTr("Current cache location")
|
||||||
|
actionText: qsTr("Change location")
|
||||||
|
descriptionWrap: Text.WrapAnywhere
|
||||||
type: SettingsItem.Button
|
type: SettingsItem.Button
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
pathDialog.open();
|
pathDialog.open()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
FolderDialog {
|
FolderDialog {
|
||||||
id: pathDialog
|
id: pathDialog
|
||||||
currentFolder: root.diskCachePath
|
|
||||||
title: qsTr("Select cache location")
|
title: qsTr("Select cache location")
|
||||||
|
currentFolder: root.diskCachePath
|
||||||
onAccepted: {
|
onAccepted: {
|
||||||
root.diskCachePath = pathDialog.selectedFolder;
|
root.diskCachePath = pathDialog.selectedFolder
|
||||||
root.refresh();
|
root.refresh()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
spacing: 12
|
spacing: 12
|
||||||
|
|
||||||
@ -90,25 +83,43 @@ SettingsView {
|
|||||||
id: submitButton
|
id: submitButton
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
text: qsTr("Save")
|
text: qsTr("Save")
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
root.submit();
|
root.submit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
secondary: true
|
|
||||||
text: qsTr("Cancel")
|
text: qsTr("Cancel")
|
||||||
|
|
||||||
onClicked: root.back()
|
onClicked: root.back()
|
||||||
|
secondary: true
|
||||||
}
|
}
|
||||||
Connections {
|
|
||||||
function onDiskCachePathChangeFinished() {
|
|
||||||
submitButton.loading = false;
|
|
||||||
root.setDefaultValues();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
Connections {
|
||||||
target: Backend
|
target: Backend
|
||||||
|
|
||||||
|
function onDiskCachePathChangeFinished() {
|
||||||
|
submitButton.loading = false
|
||||||
|
root.setDefaultValues()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onBack: {
|
||||||
|
root.setDefaultValues()
|
||||||
|
}
|
||||||
|
|
||||||
|
function submit() {
|
||||||
|
submitButton.loading = true
|
||||||
|
Backend.setDiskCachePath(root.diskCachePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDefaultValues(){
|
||||||
|
root.diskCachePath = Backend.diskCachePath
|
||||||
|
root.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
onVisibleChanged: {
|
||||||
|
root.setDefaultValues()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,196 +1,232 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
// it under the terms of the GNU General Public License as published by
|
// it under the terms of the GNU General Public License as published by
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
// (at your option) any later version.
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
// GNU General Public License for more details.
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import QtQml
|
import QtQml
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Window
|
import QtQuick.Window
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
|
|
||||||
import Proton
|
import Proton
|
||||||
import Notifications
|
import Notifications
|
||||||
import "SetupWizard"
|
|
||||||
|
|
||||||
ApplicationWindow {
|
ApplicationWindow {
|
||||||
id: root
|
id: root
|
||||||
|
colorScheme: ProtonStyle.currentStyle
|
||||||
|
visible: true
|
||||||
|
|
||||||
|
|
||||||
|
property int _defaultWidth: 1080
|
||||||
|
property int _defaultHeight: 780
|
||||||
|
width: _defaultWidth
|
||||||
|
height: _defaultHeight
|
||||||
|
minimumWidth: _defaultWidth
|
||||||
|
|
||||||
property var notifications
|
property var notifications
|
||||||
|
|
||||||
function layoutForUserCount(userCount) {
|
|
||||||
if (userCount === 0) {
|
|
||||||
contentLayout.currentIndex = 1;
|
|
||||||
setupWizard.showOnboarding();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const u = Backend.users.get(0);
|
|
||||||
if (!u) {
|
|
||||||
console.trace();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if ((userCount === 1) && (u.state === EUserState.SignedOut)) {
|
|
||||||
contentLayout.currentIndex = 1;
|
|
||||||
setupWizard.showLogin(u.primaryEmailOrUsername());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function selectUser(userID) {
|
|
||||||
contentWrapper.selectUser(userID);
|
|
||||||
}
|
|
||||||
function showAndRise() {
|
|
||||||
root.show();
|
|
||||||
root.raise();
|
|
||||||
if (!root.active) {
|
|
||||||
root.requestActivate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function showClientConfigurator(user, address, justLoggedIn) {
|
|
||||||
contentLayout.currentIndex = 1;
|
|
||||||
setupWizard.showClientConfig(user, address, justLoggedIn);
|
|
||||||
}
|
|
||||||
function showHelp() {
|
|
||||||
contentWrapper.showHelp();
|
|
||||||
}
|
|
||||||
function showLocalCacheSettings() {
|
|
||||||
contentWrapper.showLocalCacheSettings();
|
|
||||||
}
|
|
||||||
function showLogin(username = "") {
|
|
||||||
contentLayout.currentIndex = 1;
|
|
||||||
setupWizard.showLogin(username);
|
|
||||||
}
|
|
||||||
function showSettings() {
|
|
||||||
contentWrapper.showSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
colorScheme: ProtonStyle.currentStyle
|
|
||||||
height: ProtonStyle.window_default_height
|
|
||||||
minimumHeight:ProtonStyle.window_minimum_height
|
|
||||||
minimumWidth: ProtonStyle.window_minimum_width
|
|
||||||
visible: true
|
|
||||||
width: ProtonStyle.window_default_width
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
layoutForUserCount(Backend.users.count);
|
|
||||||
}
|
|
||||||
|
|
||||||
// show Setup Guide on every new user
|
// show Setup Guide on every new user
|
||||||
Connections {
|
Connections {
|
||||||
|
target: Backend.users
|
||||||
|
|
||||||
|
function onRowsInserted(parent, first, last) {
|
||||||
|
// considering that users are added one-by-one
|
||||||
|
var user = Backend.users.get(first)
|
||||||
|
|
||||||
|
if (user.state === EUserState.SignedOut) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.setupGuideSeen) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
root.showSetup(user,user.addresses[0])
|
||||||
|
}
|
||||||
|
|
||||||
function onRowsAboutToBeRemoved(parent, first, last) {
|
function onRowsAboutToBeRemoved(parent, first, last) {
|
||||||
for (let i = first; i <= last; i++) {
|
for (var i = first; i <= last; i++ ) {
|
||||||
const user = Backend.users.get(i);
|
var user = Backend.users.get(i)
|
||||||
if (setupWizard.user === user) {
|
|
||||||
setupWizard.closeWizard();
|
if (setupGuide.user === user) {
|
||||||
|
setupGuide.user = null
|
||||||
|
contentLayout._showSetup = false
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function onRowsInserted(parent, first, _) {
|
|
||||||
// considering that users are added one-by-one
|
|
||||||
const user = Backend.users.get(first);
|
|
||||||
if (user.state === EUserState.SignedOut) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (user.setupGuideSeen) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
root.showClientConfigurator(user, user.addresses[0], false);
|
|
||||||
}
|
|
||||||
|
|
||||||
target: Backend.users
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
function onSelectUser(userID, forceShowWindow) {
|
|
||||||
contentWrapper.selectUser(userID);
|
|
||||||
if (setupWizard.visible) {
|
|
||||||
setupWizard.closeWizard()
|
|
||||||
}
|
|
||||||
if (forceShowWindow) {
|
|
||||||
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
|
target: Backend
|
||||||
}
|
|
||||||
Connections {
|
function onShowMainWindow() {
|
||||||
function onCountChanged(count) {
|
root.showAndRise()
|
||||||
layoutForUserCount(count);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
target: Backend.users
|
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)
|
||||||
|
if (forceShowWindow) {
|
||||||
|
root.showAndRise()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StackLayout {
|
StackLayout {
|
||||||
id: contentLayout
|
id: contentLayout
|
||||||
anchors.fill: parent
|
|
||||||
currentIndex: 0
|
|
||||||
|
|
||||||
ContentWrapper {
|
anchors.fill: parent
|
||||||
// 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
|
||||||
id: contentWrapper
|
id: contentWrapper
|
||||||
Layout.fillHeight: true
|
|
||||||
Layout.fillWidth: true
|
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
notifications: root.notifications
|
notifications: root.notifications
|
||||||
|
|
||||||
onCloseWindow: {
|
Layout.fillHeight: true
|
||||||
root.close();
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
onShowSetupGuide: function(user, address) {
|
||||||
|
root.showSetup(user,address)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onCloseWindow: {
|
||||||
|
root.close()
|
||||||
|
}
|
||||||
|
|
||||||
onQuitBridge: {
|
onQuitBridge: {
|
||||||
// If we ever want to add a confirmation dialog before quitting:
|
// If we ever want to add a confirmation dialog before quitting:
|
||||||
//root.notifications.askQuestion("Quit Bridge", "Insert warning message here.", "Quit", "Cancel", Backend.quit, null)
|
//root.notifications.askQuestion("Quit Bridge", "Insert warning message here.", "Quit", "Cancel", Backend.quit, null)
|
||||||
root.close();
|
root.close()
|
||||||
Backend.quit();
|
Backend.quit()
|
||||||
}
|
|
||||||
onShowClientConfigurator: function (user, address, justLoggedIn) {
|
|
||||||
root.showClientConfigurator(user, address, justLoggedIn);
|
|
||||||
}
|
|
||||||
onShowLogin: function (username) {
|
|
||||||
root.showLogin(username);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SetupWizard {
|
|
||||||
id: setupWizard
|
WelcomeGuide { // 1
|
||||||
Layout.fillHeight: true
|
|
||||||
Layout.fillWidth: true
|
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
|
|
||||||
onBugReportRequested: {
|
Layout.fillHeight: true
|
||||||
contentWrapper.showBugReport();
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
SetupGuide { // 2
|
||||||
|
id: setupGuide
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
onDismissed: {
|
||||||
|
root.showSetup(null,"")
|
||||||
}
|
}
|
||||||
onWizardEnded: {
|
|
||||||
contentLayout.currentIndex = 0;
|
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,"")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationPopups {
|
NotificationPopups {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
mainWindow: root
|
|
||||||
notifications: root.notifications
|
notifications: root.notifications
|
||||||
|
mainWindow: root
|
||||||
}
|
}
|
||||||
|
|
||||||
SplashScreen {
|
SplashScreen {
|
||||||
id: splashScreen
|
id: splashScreen
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showLocalCacheSettings() { contentWrapper.showLocalCacheSettings() }
|
||||||
|
function showSettings() { contentWrapper.showSettings() }
|
||||||
|
function showHelp() { contentWrapper.showHelp() }
|
||||||
|
function selectUser(userID) { contentWrapper.selectUser(userID) }
|
||||||
|
|
||||||
|
function showBugReportAndPrefill(message) {
|
||||||
|
contentWrapper.showBugReportAndPrefill(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSignIn(username) {
|
||||||
|
if (contentLayout.currentIndex == 1) return
|
||||||
|
contentWrapper.showSignIn(username)
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSetup(user, address) {
|
||||||
|
setupGuide.user = user
|
||||||
|
setupGuide.address = address
|
||||||
|
setupGuide.reset()
|
||||||
|
if (setupGuide.user) {
|
||||||
|
contentLayout._showSetup = true
|
||||||
|
} else {
|
||||||
|
contentLayout._showSetup = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showAndRise() {
|
||||||
|
root.show()
|
||||||
|
root.raise()
|
||||||
|
if (!root.active) {
|
||||||
|
root.requestActivate()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,58 +0,0 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
|
||||||
// This file is part of Proton Mail Bridge.
|
|
||||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU General Public License for more details.
|
|
||||||
// You should have received a copy of the GNU General Public License
|
|
||||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Proton
|
|
||||||
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,99 +1,118 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
// it under the terms of the GNU General Public License as published by
|
// it under the terms of the GNU General Public License as published by
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
// (at your option) any later version.
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
// GNU General Public License for more details.
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import QtQml
|
import QtQml
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
|
|
||||||
import Proton
|
import Proton
|
||||||
import Notifications
|
import Notifications
|
||||||
|
|
||||||
Dialog {
|
Dialog {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
default property alias data: additionalChildrenContainer.children
|
|
||||||
property var notification
|
property var notification
|
||||||
|
|
||||||
modal: true
|
|
||||||
shouldShow: notification && notification.active && !notification.dismissed
|
shouldShow: notification && notification.active && !notification.dismissed
|
||||||
|
modal: true
|
||||||
|
|
||||||
|
default property alias data: additionalChildrenContainer.children
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
spacing: 0
|
spacing: 0
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
Layout.bottomMargin: 16
|
|
||||||
|
sourceSize.width: 64
|
||||||
|
sourceSize.height: 64
|
||||||
|
|
||||||
Layout.preferredHeight: 64
|
Layout.preferredHeight: 64
|
||||||
Layout.preferredWidth: 64
|
Layout.preferredWidth: 64
|
||||||
|
|
||||||
|
Layout.bottomMargin: 16
|
||||||
|
|
||||||
|
visible: source != ""
|
||||||
|
|
||||||
source: {
|
source: {
|
||||||
if (!root.notification) {
|
if (!root.notification) {
|
||||||
return "";
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (root.notification.type) {
|
switch (root.notification.type) {
|
||||||
case Notification.NotificationType.Info:
|
case Notification.NotificationType.Info:
|
||||||
return "/qml/icons/ic-info.svg";
|
return "/qml/icons/ic-info.svg"
|
||||||
case Notification.NotificationType.Success:
|
case Notification.NotificationType.Success:
|
||||||
return "/qml/icons/ic-success.svg";
|
return "/qml/icons/ic-success.svg"
|
||||||
case Notification.NotificationType.Warning:
|
case Notification.NotificationType.Warning:
|
||||||
case Notification.NotificationType.Danger:
|
case Notification.NotificationType.Danger:
|
||||||
return "/qml/icons/ic-alert.svg";
|
return "/qml/icons/ic-alert.svg"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sourceSize.height: 64
|
|
||||||
sourceSize.width: 64
|
|
||||||
visible: source != ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
Layout.bottomMargin: 8
|
Layout.bottomMargin: 8
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
text: root.notification.title
|
text: root.notification.title
|
||||||
type: Label.LabelType.Title
|
type: Label.LabelType.Title
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
Layout.bottomMargin: 16
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.preferredWidth: 240
|
Layout.preferredWidth: 240
|
||||||
colorScheme: root.colorScheme
|
Layout.bottomMargin: 16
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
text: root.notification.description
|
|
||||||
type: Label.LabelType.Body
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
|
|
||||||
onLinkActivated: function (link) {
|
colorScheme: root.colorScheme
|
||||||
Qt.openUrlExternally(link);
|
text: root.notification.description
|
||||||
}
|
wrapMode: Text.WordWrap
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
type: Label.LabelType.Body
|
||||||
|
onLinkActivated: function(link) { Qt.openUrlExternally(link) }
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: additionalChildrenContainer
|
id: additionalChildrenContainer
|
||||||
Layout.bottomMargin: 16
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
Layout.bottomMargin: 16
|
||||||
|
|
||||||
|
visible: children.length > 0
|
||||||
|
|
||||||
implicitHeight: additionalChildrenContainer.childrenRect.height
|
implicitHeight: additionalChildrenContainer.childrenRect.height
|
||||||
implicitWidth: additionalChildrenContainer.childrenRect.width
|
implicitWidth: additionalChildrenContainer.childrenRect.width
|
||||||
visible: children.length > 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
spacing: 8
|
spacing: 8
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: root.notification.action
|
model: root.notification.action
|
||||||
|
|
||||||
delegate: Button {
|
delegate: Button {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
action: modelData
|
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
loading: modelData.loading
|
action: modelData
|
||||||
|
|
||||||
secondary: index > 0
|
secondary: index > 0
|
||||||
|
|
||||||
|
loading: modelData.loading
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,19 +1,25 @@
|
|||||||
// Copyright (c) 2023 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
// it under the terms of the GNU General Public License as published by
|
// it under the terms of the GNU General Public License as published by
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
// (at your option) any later version.
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
// GNU General Public License for more details.
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import QtQml
|
import QtQml
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
|
|
||||||
import Proton
|
import Proton
|
||||||
import Notifications
|
import Notifications
|
||||||
|
|
||||||
@ -21,98 +27,118 @@ Item {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
property ColorScheme colorScheme
|
property ColorScheme colorScheme
|
||||||
property var mainWindow
|
|
||||||
property int notificationBlacklist: NotificationFilter.FilterConsts.None
|
|
||||||
property int notificationWhitelist: NotificationFilter.FilterConsts.All
|
|
||||||
property var notifications
|
property var notifications
|
||||||
|
property var mainWindow
|
||||||
|
|
||||||
|
property int notificationWhitelist: NotificationFilter.FilterConsts.All
|
||||||
|
property int notificationBlacklist: NotificationFilter.FilterConsts.None
|
||||||
|
|
||||||
NotificationFilter {
|
NotificationFilter {
|
||||||
id: bannerNotificationFilter
|
id: bannerNotificationFilter
|
||||||
blacklist: Notifications.Group.Dialogs
|
|
||||||
source: root.notifications.all
|
source: root.notifications.all
|
||||||
|
blacklist: Notifications.Group.Dialogs
|
||||||
}
|
}
|
||||||
|
|
||||||
Banner {
|
Banner {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
mainWindow: root.mainWindow
|
|
||||||
notification: bannerNotificationFilter.topmost
|
notification: bannerNotificationFilter.topmost
|
||||||
|
mainWindow: root.mainWindow
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationDialog {
|
NotificationDialog {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
notification: root.notifications.updateManualReady
|
notification: root.notifications.updateManualReady
|
||||||
|
|
||||||
Switch {
|
Switch {
|
||||||
id: autoUpdate
|
id:autoUpdate
|
||||||
checked: Backend.isAutomaticUpdateOn
|
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
text: qsTr("Update automatically in the future")
|
text: qsTr("Update automatically in the future")
|
||||||
|
checked: Backend.isAutomaticUpdateOn
|
||||||
onClicked: Backend.toggleAutomaticUpdate(autoUpdate.checked)
|
onClicked: Backend.toggleAutomaticUpdate(autoUpdate.checked)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationDialog {
|
NotificationDialog {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
notification: root.notifications.updateForce
|
notification: root.notifications.updateForce
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationDialog {
|
NotificationDialog {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
notification: root.notifications.updateForceError
|
notification: root.notifications.updateForceError
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationDialog {
|
NotificationDialog {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
notification: root.notifications.enableBeta
|
notification: root.notifications.enableBeta
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationDialog {
|
NotificationDialog {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
notification: root.notifications.cacheUnavailable
|
notification: root.notifications.cacheUnavailable
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationDialog {
|
NotificationDialog {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
notification: root.notifications.cacheCantMove
|
notification: root.notifications.cacheCantMove
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationDialog {
|
NotificationDialog {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
notification: root.notifications.diskFull
|
notification: root.notifications.diskFull
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationDialog {
|
NotificationDialog {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
notification: root.notifications.enableSplitMode
|
notification: root.notifications.enableSplitMode
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationDialog {
|
NotificationDialog {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
notification: root.notifications.resetBridge
|
notification: root.notifications.resetBridge
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationDialog {
|
NotificationDialog {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
notification: root.notifications.changeAllMailVisibility
|
notification: root.notifications.changeAllMailVisibility
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationDialog {
|
NotificationDialog {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
notification: root.notifications.deleteAccount
|
notification: root.notifications.deleteAccount
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationDialog {
|
NotificationDialog {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
notification: root.notifications.noKeychain
|
notification: root.notifications.noKeychain
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationDialog {
|
NotificationDialog {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
notification: root.notifications.rebuildKeychain
|
notification: root.notifications.rebuildKeychain
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationDialog {
|
NotificationDialog {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
notification: root.notifications.apiCertIssue
|
notification: root.notifications.apiCertIssue
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationDialog {
|
NotificationDialog {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
notification: root.notifications.noActiveKeyForRecipient
|
notification: root.notifications.noActiveKeyForRecipient
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationDialog {
|
NotificationDialog {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
notification: root.notifications.userBadEvent
|
notification: root.notifications.userBadEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationDialog {
|
NotificationDialog {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
notification: root.notifications.genericError
|
notification: root.notifications.genericError
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationDialog {
|
NotificationDialog {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
notification: root.notifications.genericQuestion
|
notification: root.notifications.genericQuestion
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user