Compare commits

..

2 Commits

397 changed files with 15663 additions and 37777 deletions

View File

@ -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...

View File

@ -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

View File

@ -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,7 +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)
* [go-smtp](https://github.com/ProtonMail/go-smtp) available under [license](https://github.com/ProtonMail/go-smtp/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 -->

View File

@ -3,223 +3,10 @@
Changelog [format](http://keepachangelog.com/en/1.0.0/) Changelog [format](http://keepachangelog.com/en/1.0.0/)
## Wakato Bridge 3.7.1 ## Trift Bridge 3.4.2
### Added
* Test(GODT-2740): Sending Plain text messages to internal recipient.
* Test(GODT-2892): Create fake log file.
* GODT-3122: Added test, changed interface for accessing display name.
### Changed
* Remove debug prints.
* GODT-2576: Forward and $Forward Flag Support.
* GODT-3053: Use smaller bridge window on small screens.
* GODT-3113: Only force UTF-8 charset for HTML part when needed.
* GODT-3113: Do not render HTML for attachment.
* GODT-3112: Replaced error message when bridge exists prematurely. Added a link to support form.
* GODT-2947: Remove 'blame it on the weather' error part from go-smtp.
* GODT-3010: Log MimeType parsing issue.
* GODT-3104: Added log entry for cert install status on startup on macOS.
* GODT-2277: Move Keychain helpers creation in main.
### Fixed ### Fixed
* GODT-3054: Only delete drafts after message has been Sent. * GODT-2902: Do not check for changed values. Related to GODT-2857.
* GODT-2576: Correctly handle Forwarded messages from Thunderbird.
* GODT-3122: Use display name as 'Email Account Name' in macOS profile.
* GODT-3125: Heartbeat crash on exit.
* GODT-2617: Validate user can send from the SMTP sender address.
* GODT-3123: Trigger bad event on empty EventID on existing accounts.
* GODT-3118: Do not reset EventID when migrating sync settings.
* GODT-3116: Panic on closed channel.
* GODT-1623: Throttle SMTP failed requests.
* GODT-3047: Fixed 'disk full' error message.
* GODT-3054: Delete draft create from reply.
* GODT-3048: WKD Policy behavior.
## Wakato Bridge 3.7.0
### 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
* GODT-3106: Broken import route.
* 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
@ -268,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.
@ -353,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.
@ -413,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
@ -554,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.
@ -571,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.
@ -636,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.
@ -916,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.
@ -1070,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.
@ -1167,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.
@ -1209,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.
@ -1460,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.
@ -1550,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.
@ -1874,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.
@ -2039,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.
@ -2171,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.
@ -2179,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.
@ -2227,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.
@ -2301,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.
@ -2323,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.
@ -2340,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.
@ -2364,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.
@ -2383,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).
@ -2455,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)
@ -2521,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
@ -2553,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).
@ -2583,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).

View File

@ -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.1+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}

View File

@ -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)
}
} }

View File

@ -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
``` ```

View File

@ -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

27
go.mod
View File

@ -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.20231114153341-2ecbdd2739f7 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.20231116144214-8a47c8d92fbc 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,20 +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/emersion/go-smtp => github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865
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
) )

64
go.sum
View File

@ -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,25 +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.20231114153341-2ecbdd2739f7 h1:w+VoSAq9FQvKMm3DlH1MIEZ1KGe7LJ+81EJFVwSV4VU= github.com/ProtonMail/gluon v0.16.1-0.20230901124123-075229a92cc4 h1:Uq2v2NYEtlTaK2WTh9BMph2Kv51JxMgvTkd7CjGPYc8=
github.com/ProtonMail/gluon v0.17.1-0.20231114153341-2ecbdd2739f7/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.20231116144214-8a47c8d92fbc h1:GBRKoFAldApEMkMrsFN1ZxG0eG797w6LTv/dFMDcsqQ= 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.20231116144214-8a47c8d92fbc/go.mod h1:WEXJqj5DSc2YI77SgXdpMY0nk33Qy92Vu2r4tOEazA8= github.com/ProtonMail/go-proton-api v0.4.1-0.20230727082922-9115b4750ec7/go.mod h1:+aTJoYu8bqzGECXL2DOdiZTZ64bGn3w0NC8VcFpJrFM=
github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865 h1:EP1gnxLL5Z7xBSymE9nSTM27nRYINuvssAtDmG0suD8=
github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
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=
@ -67,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=
@ -124,6 +120,8 @@ github.com/emersion/go-imap-id v0.0.0-20190926060100-f94a56b9ecde/go.mod h1:sPwp
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead h1:fI1Jck0vUrXT8bnphprS1EoVRe2Q5CKCX8iDlpqjQ/Y= github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead h1:fI1Jck0vUrXT8bnphprS1EoVRe2Q5CKCX8iDlpqjQ/Y=
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-smtp v0.15.1-0.20221021114529-49b17434419d h1:hFRM6zCBSc+Xa0rBOqSlG6Qe9dKC/2vLhGAuZlWxTsc=
github.com/emersion/go-smtp v0.15.1-0.20221021114529-49b17434419d/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY= github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3 h1:hQ1wTMaKcGfobYRT88RM8NFNyX+IQHvagkm/tqViU98= github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3 h1:hQ1wTMaKcGfobYRT88RM8NFNyX+IQHvagkm/tqViU98=
@ -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=

View File

@ -41,7 +41,6 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/sentry" "github.com/ProtonMail/proton-bridge/v3/internal/sentry"
"github.com/ProtonMail/proton-bridge/v3/internal/useragent" "github.com/ProtonMail/proton-bridge/v3/internal/useragent"
"github.com/ProtonMail/proton-bridge/v3/internal/vault" "github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
"github.com/ProtonMail/proton-bridge/v3/pkg/restarter" "github.com/ProtonMail/proton-bridge/v3/pkg/restarter"
"github.com/pkg/profile" "github.com/pkg/profile"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -53,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"
@ -100,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},
@ -205,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()
@ -235,56 +226,53 @@ func run(c *cli.Context) error {
} }
return withSingleInstance(settings, locations.GetLockFile(), version, func() error { return withSingleInstance(settings, locations.GetLockFile(), version, func() error {
// Look for available keychains // Unlock the encrypted vault.
return WithKeychainList(func(keychains *keychain.List) error { return WithVault(locations, crashHandler, func(v *vault.Vault, insecure, corrupt bool) error {
// Unlock the encrypted vault. if !v.Migrated() {
return WithVault(locations, keychains, crashHandler, func(v *vault.Vault, insecure, corrupt bool) error { // Migrate old settings into the vault.
if !v.Migrated() { if err := migrateOldSettings(v); err != nil {
// Migrate old settings into the vault. logrus.WithError(err).Error("Failed to migrate old settings")
if err := migrateOldSettings(v); err != nil {
logrus.WithError(err).Error("Failed to migrate old settings")
}
// Migrate old accounts into the vault.
if err := migrateOldAccounts(locations, keychains, v); err != nil {
logrus.WithError(err).Error("Failed to migrate old accounts")
}
// The vault has been migrated.
if err := v.SetMigrated(); err != nil {
logrus.WithError(err).Error("Failed to mark vault as migrated")
}
} }
logrus.WithFields(logrus.Fields{ // Migrate old accounts into the vault.
"lastVersion": v.GetLastVersion().String(), if err := migrateOldAccounts(locations, v); err != nil {
"showAllMail": v.GetShowAllMail(), logrus.WithError(err).Error("Failed to migrate old accounts")
"updateCh": v.GetUpdateChannel(), }
"autoUpdate": v.GetAutoUpdate(),
"rollout": v.GetUpdateRollout(),
"DoH": v.GetProxyAllowed(),
}).Info("Vault loaded")
// Load the cookies from the vault. // The vault has been migrated.
return withCookieJar(v, func(cookieJar http.CookieJar) error { if err := v.SetMigrated(); err != nil {
// Create a new bridge instance. logrus.WithError(err).Error("Failed to mark vault as migrated")
return withBridge(c, exe, locations, version, identifier, crashHandler, reporter, v, cookieJar, keychains, func(b *bridge.Bridge, eventCh <-chan events.Event) error { }
if insecure { }
logrus.Warn("The vault key could not be retrieved; the vault will not be encrypted")
b.PushError(bridge.ErrVaultInsecure)
}
if corrupt { logrus.WithFields(logrus.Fields{
logrus.Warn("The vault is corrupt and has been wiped") "lastVersion": v.GetLastVersion().String(),
b.PushError(bridge.ErrVaultCorrupt) "showAllMail": v.GetShowAllMail(),
} "updateCh": v.GetUpdateChannel(),
"autoUpdate": v.GetAutoUpdate(),
"rollout": v.GetUpdateRollout(),
"DoH": v.GetProxyAllowed(),
}).Info("Vault loaded")
// Remove old updates files // Load the cookies from the vault.
b.RemoveOldUpdates() return withCookieJar(v, func(cookieJar http.CookieJar) error {
// Create a new bridge instance.
return withBridge(c, exe, locations, version, identifier, crashHandler, reporter, v, cookieJar, func(b *bridge.Bridge, eventCh <-chan events.Event) error {
if insecure {
logrus.Warn("The vault key could not be retrieved; the vault will not be encrypted")
b.PushError(bridge.ErrVaultInsecure)
}
// Run the frontend. if corrupt {
return runFrontend(c, crashHandler, restarter, locations, b, eventCh, quitCh, c.Int(flagParentPID)) logrus.Warn("The vault is corrupt and has been wiped")
}) b.PushError(bridge.ErrVaultCorrupt)
}
// Start telemetry heartbeat process
b.StartHeartbeat(b)
// Run the frontend.
return runFrontend(c, crashHandler, restarter, locations, b, eventCh, quitCh, c.Int(flagParentPID))
}) })
}) })
}) })
@ -294,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.
@ -398,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()
@ -481,13 +457,6 @@ func withCookieJar(vault *vault.Vault, fn func(http.CookieJar) error) error {
return fn(persister) return fn(persister)
} }
// WithKeychainList init the list of usable keychains.
func WithKeychainList(fn func(*keychain.List) error) error {
logrus.Debug("Creating keychain list")
defer logrus.Debug("Keychain list stop")
return fn(keychain.NewList())
}
func setDeviceCookies(jar *cookies.Jar) error { func setDeviceCookies(jar *cookies.Jar) error {
url, err := url.Parse(constants.APIHost) url, err := url.Parse(constants.APIHost)
if err != nil { if err != nil {

View File

@ -37,7 +37,6 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/useragent" "github.com/ProtonMail/proton-bridge/v3/internal/useragent"
"github.com/ProtonMail/proton-bridge/v3/internal/vault" "github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/ProtonMail/proton-bridge/v3/internal/versioner" "github.com/ProtonMail/proton-bridge/v3/internal/versioner"
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@ -56,7 +55,6 @@ func withBridge(
reporter *sentry.Reporter, reporter *sentry.Reporter,
vault *vault.Vault, vault *vault.Vault,
cookieJar http.CookieJar, cookieJar http.CookieJar,
keychains *keychain.List,
fn func(*bridge.Bridge, <-chan events.Event) error, fn func(*bridge.Bridge, <-chan events.Event) error,
) error { ) error {
logrus.Debug("Creating bridge") logrus.Debug("Creating bridge")
@ -99,7 +97,6 @@ func withBridge(
autostarter, autostarter,
updater, updater,
version, version,
keychains,
// The API stuff. // The API stuff.
constants.APIHost, constants.APIHost,
@ -113,7 +110,6 @@ func withBridge(
crashHandler, crashHandler,
reporter, reporter,
imap.DefaultEpochUIDValidityGenerator(), imap.DefaultEpochUIDValidityGenerator(),
nil,
// The logging stuff. // The logging stuff.
c.String(flagLogIMAP) == "client" || c.String(flagLogIMAP) == "all", c.String(flagLogIMAP) == "client" || c.String(flagLogIMAP) == "all",
@ -159,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,

View File

@ -122,7 +122,7 @@ func migrateOldSettingsWithDir(configDir string, v *vault.Vault) error {
return v.SetBridgeTLSCertKey(certPEM, keyPEM) return v.SetBridgeTLSCertKey(certPEM, keyPEM)
} }
func migrateOldAccounts(locations *locations.Locations, keychains *keychain.List, v *vault.Vault) error { func migrateOldAccounts(locations *locations.Locations, v *vault.Vault) error {
logrus.Info("Migrating accounts") logrus.Info("Migrating accounts")
settings, err := locations.ProvideSettingsPath() settings, err := locations.ProvideSettingsPath()
@ -134,7 +134,8 @@ func migrateOldAccounts(locations *locations.Locations, keychains *keychain.List
if err != nil { if err != nil {
return fmt.Errorf("failed to get helper: %w", err) return fmt.Errorf("failed to get helper: %w", err)
} }
keychain, err := keychain.NewKeychain(helper, "bridge", keychains.GetHelpers(), keychains.GetDefaultHelper())
keychain, err := keychain.NewKeychain(helper, "bridge")
if err != nil { if err != nil {
return fmt.Errorf("failed to create keychain: %w", err) return fmt.Errorf("failed to create keychain: %w", err)
} }

View File

@ -35,6 +35,7 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/vault" "github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/ProtonMail/proton-bridge/v3/pkg/algo" "github.com/ProtonMail/proton-bridge/v3/pkg/algo"
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain" "github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
dockerCredentials "github.com/docker/docker-credential-helpers/credentials"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -132,9 +133,11 @@ func TestKeychainMigration(t *testing.T) {
} }
func TestUserMigration(t *testing.T) { func TestUserMigration(t *testing.T) {
kcl := keychain.NewTestKeychainsList() keychainHelper := keychain.NewTestHelper()
kc, err := keychain.NewKeychain("mock", "bridge", kcl.GetHelpers(), kcl.GetDefaultHelper()) keychain.Helpers["mock"] = func(string) (dockerCredentials.Helper, error) { return keychainHelper, nil }
kc, err := keychain.NewKeychain("mock", "bridge")
require.NoError(t, err) require.NoError(t, err)
require.NoError(t, kc.Put("brokenID", "broken")) require.NoError(t, kc.Put("brokenID", "broken"))
@ -175,7 +178,7 @@ func TestUserMigration(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.False(t, corrupt) require.False(t, corrupt)
require.NoError(t, migrateOldAccounts(locations, kcl, v)) require.NoError(t, migrateOldAccounts(locations, v))
require.Equal(t, []string{wantCredentials.UserID}, v.GetUserIDs()) require.Equal(t, []string{wantCredentials.UserID}, v.GetUserIDs())
require.NoError(t, v.GetUser(wantCredentials.UserID, func(u *vault.User) { require.NoError(t, v.GetUser(wantCredentials.UserID, func(u *vault.User) {

View File

@ -30,12 +30,12 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
func WithVault(locations *locations.Locations, keychains *keychain.List, panicHandler async.PanicHandler, fn func(*vault.Vault, bool, bool) error) error { func WithVault(locations *locations.Locations, panicHandler async.PanicHandler, fn func(*vault.Vault, bool, bool) error) error {
logrus.Debug("Creating vault") logrus.Debug("Creating vault")
defer logrus.Debug("Vault stopped") defer logrus.Debug("Vault stopped")
// Create the encVault. // Create the encVault.
encVault, insecure, corrupt, err := newVault(locations, keychains, panicHandler) encVault, insecure, corrupt, err := newVault(locations, panicHandler)
if err != nil { if err != nil {
return fmt.Errorf("could not create vault: %w", err) return fmt.Errorf("could not create vault: %w", err)
} }
@ -45,15 +45,29 @@ func WithVault(locations *locations.Locations, keychains *keychain.List, panicHa
"corrupt": corrupt, "corrupt": corrupt,
}).Debug("Vault created") }).Debug("Vault created")
cert, _ := encVault.GetBridgeTLSCert() // Install the certificates if needed.
certs.NewInstaller().LogCertInstallStatus(cert) 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)
} }
func newVault(locations *locations.Locations, keychains *keychain.List, panicHandler async.PanicHandler) (*vault.Vault, bool, bool, error) { func newVault(locations *locations.Locations, panicHandler async.PanicHandler) (*vault.Vault, bool, bool, error) {
vaultDir, err := locations.ProvideSettingsPath() vaultDir, err := locations.ProvideSettingsPath()
if err != nil { if err != nil {
return nil, false, false, fmt.Errorf("could not get vault dir: %w", err) return nil, false, false, fmt.Errorf("could not get vault dir: %w", err)
@ -66,7 +80,7 @@ func newVault(locations *locations.Locations, keychains *keychain.List, panicHan
insecure bool insecure bool
) )
if key, err := loadVaultKey(vaultDir, keychains); err != nil { if key, err := loadVaultKey(vaultDir); err != nil {
logrus.WithError(err).Error("Could not load/create vault key") logrus.WithError(err).Error("Could not load/create vault key")
insecure = true insecure = true
@ -89,13 +103,13 @@ func newVault(locations *locations.Locations, keychains *keychain.List, panicHan
return vault, insecure, corrupt, nil return vault, insecure, corrupt, nil
} }
func loadVaultKey(vaultDir string, keychains *keychain.List) ([]byte, error) { func loadVaultKey(vaultDir string) ([]byte, error) {
helper, err := vault.GetHelper(vaultDir) helper, err := vault.GetHelper(vaultDir)
if err != nil { if err != nil {
return nil, fmt.Errorf("could not get keychain helper: %w", err) return nil, fmt.Errorf("could not get keychain helper: %w", err)
} }
kc, err := keychain.NewKeychain(helper, constants.KeyChainName, keychains.GetHelpers(), keychains.GetDefaultHelper()) kc, err := keychain.NewKeychain(helper, constants.KeyChainName)
if err != nil { if err != nil {
return nil, fmt.Errorf("could not create keychain: %w", err) return nil, fmt.Errorf("could not create keychain: %w", err)
} }

View File

@ -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

View File

@ -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

View File

@ -23,6 +23,7 @@ import (
"crypto/tls" "crypto/tls"
"errors" "errors"
"fmt" "fmt"
"net"
"net/http" "net/http"
"sync" "sync"
"time" "time"
@ -37,15 +38,11 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/constants" "github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/events" "github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/focus" "github.com/ProtonMail/proton-bridge/v3/internal/focus"
"github.com/ProtonMail/proton-bridge/v3/internal/identifier"
"github.com/ProtonMail/proton-bridge/v3/internal/safe" "github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/sentry" "github.com/ProtonMail/proton-bridge/v3/internal/sentry"
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapsmtpserver"
"github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice"
"github.com/ProtonMail/proton-bridge/v3/internal/telemetry" "github.com/ProtonMail/proton-bridge/v3/internal/telemetry"
"github.com/ProtonMail/proton-bridge/v3/internal/user" "github.com/ProtonMail/proton-bridge/v3/internal/user"
"github.com/ProtonMail/proton-bridge/v3/internal/vault" "github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
"github.com/bradenaw/juniper/xslices" "github.com/bradenaw/juniper/xslices"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -62,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
@ -75,7 +72,7 @@ type Bridge struct {
installCh chan installJob installCh chan installJob
// heartbeat is the telemetry heartbeat for metrics. // heartbeat is the telemetry heartbeat for metrics.
heartbeat *heartBeatState heartbeat telemetry.Heartbeat
// curVersion is the current version of the bridge, // curVersion is the current version of the bridge,
// newVersion is the version that was installed by the updater. // newVersion is the version that was installed by the updater.
@ -83,9 +80,6 @@ type Bridge struct {
newVersion *semver.Version newVersion *semver.Version
newVersionLock safe.RWMutex newVersionLock safe.RWMutex
// keychains is the utils that own usable keychains found in the OS.
keychains *keychain.List
// focusService is used to raise the bridge window when needed. // focusService is used to raise the bridge window when needed.
focusService *focus.Service focusService *focus.Service
@ -128,8 +122,12 @@ type Bridge struct {
// goUpdate triggers a check/install of updates. // goUpdate triggers a check/install of updates.
goUpdate func() goUpdate func()
serverManager *imapsmtpserver.Service // goHeartbeat triggers a check/sending if heartbeat is needed.
syncService *syncservice.Service goHeartbeat func()
uidValidityGenerator imap.UIDValidityGenerator
serverManager *ServerManager
} }
// New creates a new bridge. // New creates a new bridge.
@ -139,18 +137,16 @@ func New(
autostarter Autostarter, // the autostarter to manage autostart settings autostarter Autostarter, // the autostarter to manage autostart settings
updater Updater, // the updater to fetch and install updates updater Updater, // the updater to fetch and install updates
curVersion *semver.Version, // the current version of the bridge curVersion *semver.Version, // the current version of the bridge
keychains *keychain.List, // usable keychains
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
panicHandler async.PanicHandler, panicHandler async.PanicHandler,
reporter reporter.Reporter, reporter reporter.Reporter,
uidValidityGenerator imap.UIDValidityGenerator, uidValidityGenerator imap.UIDValidityGenerator,
heartBeatManager telemetry.HeartbeatManager,
logIMAPClient, logIMAPServer bool, // whether to log IMAP client/server activity logIMAPClient, logIMAPServer bool, // whether to log IMAP client/server activity
logSMTP bool, // whether to log SMTP activity logSMTP bool, // whether to log SMTP activity
@ -166,7 +162,6 @@ func New(
// bridge is the bridge. // bridge is the bridge.
bridge, err := newBridge( bridge, err := newBridge(
context.Background(),
tasks, tasks,
imapEventCh, imapEventCh,
@ -175,7 +170,6 @@ func New(
autostarter, autostarter,
updater, updater,
curVersion, curVersion,
keychains,
panicHandler, panicHandler,
reporter, reporter,
@ -183,7 +177,6 @@ func New(
identifier, identifier,
proxyCtl, proxyCtl,
uidValidityGenerator, uidValidityGenerator,
heartBeatManager,
logIMAPClient, logIMAPServer, logSMTP, logIMAPClient, logIMAPServer, logSMTP,
) )
if err != nil { if err != nil {
@ -202,7 +195,6 @@ func New(
} }
func newBridge( func newBridge(
ctx context.Context,
tasks *async.Group, tasks *async.Group,
imapEventCh chan imapEvents.Event, imapEventCh chan imapEvents.Event,
@ -211,15 +203,13 @@ func newBridge(
autostarter Autostarter, autostarter Autostarter,
updater Updater, updater Updater,
curVersion *semver.Version, curVersion *semver.Version,
keychains *keychain.List,
panicHandler async.PanicHandler, panicHandler async.PanicHandler,
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,
heartbeatManager telemetry.HeartbeatManager,
logIMAPClient, logIMAPServer, logSMTP bool, logIMAPClient, logIMAPServer, logSMTP bool,
) (*Bridge, error) { ) (*Bridge, error) {
@ -265,13 +255,9 @@ func newBridge(
newVersion: curVersion, newVersion: curVersion,
newVersionLock: safe.NewRWMutex(), newVersionLock: safe.NewRWMutex(),
keychains: keychains,
panicHandler: panicHandler, panicHandler: panicHandler,
reporter: reporter, reporter: reporter,
heartbeat: newHeartBeatState(ctx, panicHandler),
focusService: focusService, focusService: focusService,
autostarter: autostarter, autostarter: autostarter,
locator: locator, locator: locator,
@ -283,32 +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
} }
if heartbeatManager == nil {
bridge.heartbeat.init(bridge, bridge)
} else {
bridge.heartbeat.init(bridge, heartbeatManager)
}
bridge.syncService.Run(bridge.tasks)
return bridge, nil return bridge, nil
} }
@ -436,8 +407,10 @@ 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")
// Stop heart beat before closing users. // Close the servers
bridge.heartbeat.stop() 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() {
@ -446,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()
@ -509,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():
@ -547,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

View File

@ -44,19 +44,16 @@ 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"
"github.com/ProtonMail/proton-bridge/v3/internal/vault" "github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
"github.com/ProtonMail/proton-bridge/v3/tests" "github.com/ProtonMail/proton-bridge/v3/tests"
"github.com/bradenaw/juniper/xslices" "github.com/bradenaw/juniper/xslices"
imapid "github.com/emersion/go-imap-id" imapid "github.com/emersion/go-imap-id"
"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 (
@ -586,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) {
// ... // ...
}) })
}) })
@ -624,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)
@ -648,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)
@ -703,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))
}) })
}) })
@ -779,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.
@ -876,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()
@ -951,7 +945,6 @@ func withBridgeNoMocks(
mocks.Autostarter, mocks.Autostarter,
mocks.Updater, mocks.Updater,
v2_3_0, v2_3_0,
keychain.NewTestKeychainsList(),
// The API stuff. // The API stuff.
apiURL, apiURL,
@ -963,7 +956,6 @@ func withBridgeNoMocks(
mocks.CrashHandler, mocks.CrashHandler,
mocks.Reporter, mocks.Reporter,
testUIDValidityGenerator, testUIDValidityGenerator,
mocks.Heartbeat,
// The logging stuff. // The logging stuff.
os.Getenv("BRIDGE_LOG_IMAP_CLIENT") == "1", os.Getenv("BRIDGE_LOG_IMAP_CLIENT") == "1",
@ -973,6 +965,9 @@ func withBridgeNoMocks(
require.NoError(t, err) require.NoError(t, err)
require.Empty(t, bridge.GetErrors()) require.Empty(t, bridge.GetErrors())
// Start the Heartbeat process.
bridge.StartHeartbeat(mocks.Heartbeat)
// Wait for bridge to finish loading users. // Wait for bridge to finish loading users.
waitForEvent(t, eventCh, events.AllUsersLoaded{}) waitForEvent(t, eventCh, events.AllUsersLoaded{})
@ -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 {

View File

@ -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...)
} }

View File

@ -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)
} }

View File

@ -19,7 +19,6 @@ package bridge
import ( import (
"context" "context"
"errors"
"strings" "strings"
"github.com/ProtonMail/proton-bridge/v3/internal/clientconfig" "github.com/ProtonMail/proton-bridge/v3/internal/clientconfig"
@ -31,8 +30,8 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
// ConfigureAppleMail configures Apple Mail for the given userID and address. // ConfigureAppleMail configures apple mail for the given userID and address.
// If configuring Apple Mail for Catalina or newer, it ensures Bridge is using SSL. // If configuring apple mail for Catalina or newer, it ensures Bridge is using SSL.
func (bridge *Bridge) ConfigureAppleMail(ctx context.Context, userID, address string) error { func (bridge *Bridge) ConfigureAppleMail(ctx context.Context, userID, address string) error {
logrus.WithFields(logrus.Fields{ logrus.WithFields(logrus.Fields{
"userID": userID, "userID": userID,
@ -45,28 +44,16 @@ func (bridge *Bridge) ConfigureAppleMail(ctx context.Context, userID, address st
return ErrNoSuchUser return ErrNoSuchUser
} }
emails := user.Emails()
displayNames := user.DisplayNames()
if (len(emails) == 0) || (len(displayNames) == 0) {
return errors.New("could not retrieve user address info")
}
if address == "" { if address == "" {
address = emails[0] address = user.Emails()[0]
} }
var username, displayName, addresses string username := address
addresses := address
if user.GetAddressMode() == vault.CombinedMode { if user.GetAddressMode() == vault.CombinedMode {
username = address username = user.Emails()[0]
displayName = displayNames[username] addresses = strings.Join(user.Emails(), ",")
addresses = strings.Join(emails, ",")
} else {
username = address
addresses = address
displayName = displayNames[address]
if len(displayName) == 0 {
displayName = address
}
} }
if useragent.IsCatalinaOrNewer() && !bridge.vault.GetSMTPSSL() { if useragent.IsCatalinaOrNewer() && !bridge.vault.GetSMTPSSL() {
@ -82,7 +69,6 @@ func (bridge *Bridge) ConfigureAppleMail(ctx context.Context, userID, address st
bridge.vault.GetIMAPSSL(), bridge.vault.GetIMAPSSL(),
bridge.vault.GetSMTPSSL(), bridge.vault.GetSMTPSSL(),
username, username,
displayName,
addresses, addresses,
user.BridgePass(), user.BridgePass(),
) )

View File

@ -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

View File

@ -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)
}

View File

@ -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
} }

View File

@ -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)
} }

View File

@ -20,100 +20,18 @@ package bridge
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"sync"
"time" "time"
"github.com/ProtonMail/gluon/async"
"github.com/ProtonMail/gluon/reporter" "github.com/ProtonMail/gluon/reporter"
"github.com/ProtonMail/proton-bridge/v3/internal/safe" "github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/telemetry" "github.com/ProtonMail/proton-bridge/v3/internal/telemetry"
"github.com/ProtonMail/proton-bridge/v3/internal/vault" "github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
const HeartbeatCheckInterval = time.Hour const HeartbeatCheckInterval = time.Hour
type heartBeatState struct {
task *async.Group
telemetry.Heartbeat
taskLock sync.Mutex
taskStarted bool
taskInterval time.Duration
}
func newHeartBeatState(ctx context.Context, panicHandler async.PanicHandler) *heartBeatState {
return &heartBeatState{
task: async.NewGroup(ctx, panicHandler),
}
}
func (h *heartBeatState) init(bridge *Bridge, manager telemetry.HeartbeatManager) {
h.Heartbeat = telemetry.NewHeartbeat(manager, 1143, 1025, bridge.GetGluonCacheDir(), bridge.keychains.GetDefaultHelper())
h.taskInterval = manager.GetHeartbeatPeriodicInterval()
h.SetRollout(bridge.GetUpdateRollout())
h.SetAutoStart(bridge.GetAutostart())
h.SetAutoUpdate(bridge.GetAutoUpdate())
h.SetBeta(bridge.GetUpdateChannel())
h.SetDoh(bridge.GetProxyAllowed())
h.SetShowAllMail(bridge.GetShowAllMail())
h.SetIMAPConnectionMode(bridge.GetIMAPSSL())
h.SetSMTPConnectionMode(bridge.GetSMTPSSL())
h.SetIMAPPort(bridge.GetIMAPPort())
h.SetSMTPPort(bridge.GetSMTPPort())
h.SetCacheLocation(bridge.GetGluonCacheDir())
if val, err := bridge.GetKeychainApp(); err != nil {
h.SetKeyChainPref(val)
} else {
h.SetKeyChainPref(bridge.keychains.GetDefaultHelper())
}
h.SetPrevVersion(bridge.GetLastVersion().String())
safe.RLock(func() {
var splitMode = false
for _, user := range bridge.users {
if user.GetAddressMode() == vault.SplitMode {
splitMode = true
break
}
}
var nbAccount = len(bridge.users)
h.SetNbAccount(nbAccount)
h.SetSplitMode(splitMode)
// Do not try to send if there is no user yet.
if nbAccount > 0 {
defer h.start()
}
}, bridge.usersLock)
}
func (h *heartBeatState) start() {
h.taskLock.Lock()
defer h.taskLock.Unlock()
if h.taskStarted {
return
}
h.taskStarted = true
h.task.PeriodicOrTrigger(h.taskInterval, 0, func(ctx context.Context) {
logrus.Debug("Checking for heartbeat")
h.TrySending(ctx)
})
}
func (h *heartBeatState) stop() {
h.taskLock.Lock()
defer h.taskLock.Unlock()
if !h.taskStarted {
return
}
h.task.CancelAndWait()
h.taskStarted = false
}
func (bridge *Bridge) IsTelemetryAvailable(ctx context.Context) bool { func (bridge *Bridge) IsTelemetryAvailable(ctx context.Context) bool {
var flag = true var flag = true
if bridge.GetTelemetryDisabled() { if bridge.GetTelemetryDisabled() {
@ -162,6 +80,49 @@ func (bridge *Bridge) SetLastHeartbeatSent(timestamp time.Time) error {
return bridge.vault.SetLastHeartbeatSent(timestamp) return bridge.vault.SetLastHeartbeatSent(timestamp)
} }
func (bridge *Bridge) GetHeartbeatPeriodicInterval() time.Duration { func (bridge *Bridge) StartHeartbeat(manager telemetry.HeartbeatManager) {
return HeartbeatCheckInterval bridge.heartbeat = telemetry.NewHeartbeat(manager, 1143, 1025, bridge.GetGluonCacheDir(), keychain.DefaultHelper)
// Check for heartbeat when triggered.
bridge.goHeartbeat = bridge.tasks.PeriodicOrTrigger(HeartbeatCheckInterval, 0, func(ctx context.Context) {
logrus.Debug("Checking for heartbeat")
bridge.heartbeat.TrySending(ctx)
})
bridge.heartbeat.SetRollout(bridge.GetUpdateRollout())
bridge.heartbeat.SetAutoStart(bridge.GetAutostart())
bridge.heartbeat.SetAutoUpdate(bridge.GetAutoUpdate())
bridge.heartbeat.SetBeta(bridge.GetUpdateChannel())
bridge.heartbeat.SetDoh(bridge.GetProxyAllowed())
bridge.heartbeat.SetShowAllMail(bridge.GetShowAllMail())
bridge.heartbeat.SetIMAPConnectionMode(bridge.GetIMAPSSL())
bridge.heartbeat.SetSMTPConnectionMode(bridge.GetSMTPSSL())
bridge.heartbeat.SetIMAPPort(bridge.GetIMAPPort())
bridge.heartbeat.SetSMTPPort(bridge.GetSMTPPort())
bridge.heartbeat.SetCacheLocation(bridge.GetGluonCacheDir())
if val, err := bridge.GetKeychainApp(); err != nil {
bridge.heartbeat.SetKeyChainPref(val)
} else {
bridge.heartbeat.SetKeyChainPref(keychain.DefaultHelper)
}
bridge.heartbeat.SetPrevVersion(bridge.GetLastVersion().String())
safe.RLock(func() {
var splitMode = false
for _, user := range bridge.users {
if user.GetAddressMode() == vault.SplitMode {
splitMode = true
break
}
}
var nbAccount = len(bridge.users)
bridge.heartbeat.SetNbAccount(nbAccount)
bridge.heartbeat.SetSplitMode(splitMode)
// Do not try to send if there is no user yet.
if nbAccount > 0 {
defer bridge.goHeartbeat()
}
}, bridge.usersLock)
} }

View File

@ -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)
}

View File

@ -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))
} }

View File

@ -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)
}

View File

@ -1,24 +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 "golang.org/x/exp/maps"
func (bridge *Bridge) GetHelpersNames() []string {
return maps.Keys(bridge.keychains.GetHelpers())
}

View 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.")
}
}
}
}

View File

@ -7,7 +7,6 @@ import (
"os" "os"
"sync" "sync"
"testing" "testing"
"time"
"github.com/Masterminds/semver/v3" "github.com/Masterminds/semver/v3"
"github.com/ProtonMail/proton-bridge/v3/internal/bridge/mocks" "github.com/ProtonMail/proton-bridge/v3/internal/bridge/mocks"
@ -52,7 +51,6 @@ func NewMocks(tb testing.TB, version, minAuto *semver.Version) *Mocks {
// this is called at start of heartbeat process. // this is called at start of heartbeat process.
mocks.Heartbeat.EXPECT().IsTelemetryAvailable(gomock.Any()).AnyTimes() mocks.Heartbeat.EXPECT().IsTelemetryAvailable(gomock.Any()).AnyTimes()
mocks.Heartbeat.EXPECT().GetHeartbeatPeriodicInterval().AnyTimes().Return(500 * time.Millisecond)
return mocks return mocks
} }
@ -156,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
}

View File

@ -36,20 +36,6 @@ func (m *MockHeartbeatManager) EXPECT() *MockHeartbeatManagerMockRecorder {
return m.recorder return m.recorder
} }
// GetHeartbeatPeriodicInterval mocks base method.
func (m *MockHeartbeatManager) GetHeartbeatPeriodicInterval() time.Duration {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetHeartbeatPeriodicInterval")
ret0, _ := ret[0].(time.Duration)
return ret0
}
// GetHeartbeatPeriodicInterval indicates an expected call of GetHeartbeatPeriodicInterval.
func (mr *MockHeartbeatManagerMockRecorder) GetHeartbeatPeriodicInterval() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetHeartbeatPeriodicInterval", reflect.TypeOf((*MockHeartbeatManager)(nil).GetHeartbeatPeriodicInterval))
}
// GetLastHeartbeatSent mocks base method. // GetLastHeartbeatSent mocks base method.
func (m *MockHeartbeatManager) GetLastHeartbeatSent() time.Time { func (m *MockHeartbeatManager) GetLastHeartbeatSent() time.Time {
m.ctrl.T.Helper() m.ctrl.T.Helper()

View File

@ -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()

View File

@ -33,7 +33,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"
smtpservice "github.com/ProtonMail/proton-bridge/v3/internal/services/smtp"
"github.com/emersion/go-imap" "github.com/emersion/go-imap"
"github.com/emersion/go-sasl" "github.com/emersion/go-sasl"
"github.com/emersion/go-smtp" "github.com/emersion/go-smtp"
@ -337,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
@ -347,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"
@ -364,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"
@ -471,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 {
@ -524,234 +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)
})
})
}
func TestBridge_SendAddressDisabled(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
recipientUserID, _, err := s.CreateUser("recipient", password)
require.NoError(t, err)
senderUserID, addrID, err := s.CreateUser("sender", password)
require.NoError(t, err)
require.NoError(t, s.ChangeAddressAllowSend(senderUserID, addrID, false))
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, "sender", password, nil, nil)
require.NoError(t, err)
_, err = bridge.LoginFull(ctx, "recipient", password, nil, nil)
require.NoError(t, err)
smtpWaiter.Wait()
recipientInfo, err := bridge.GetUserInfo(recipientUserID)
require.NoError(t, err)
senderInfo, err := bridge.GetUserInfo(senderUserID)
require.NoError(t, err)
// 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}))
require.NoError(t, client.Auth(sasl.NewLoginClient(
senderInfo.Addresses[0],
string(senderInfo.BridgePass)),
))
// Send the message.
err = client.SendMail(
senderInfo.Addresses[0],
[]string{recipientInfo.Addresses[0]},
strings.NewReader("Subject: Test 1\r\n\r\nHello world!"),
)
smtpErr := smtpservice.NewErrCanNotSendOnAddress(senderInfo.Addresses[0])
require.Equal(t, fmt.Sprintf("Error: %v", smtpErr.Error()), err.Error())
})
})
}

View 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
}

View File

@ -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()
} }
@ -261,12 +247,9 @@ func (bridge *Bridge) SetTelemetryDisabled(isDisabled bool) error {
return err return err
} }
// If telemetry is re-enabled locally, try to send the heartbeat. // If telemetry is re-enabled locally, try to send the heartbeat.
if isDisabled { if !isDisabled {
bridge.heartbeat.stop() defer bridge.goHeartbeat()
} else {
bridge.heartbeat.start()
} }
return nil return nil
} }
@ -335,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
}
}

View File

@ -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) {

View File

@ -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}
} }

View File

@ -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.")

View File

@ -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()),

View File

@ -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

View File

@ -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
} }

View File

@ -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")
}
}

View File

@ -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 {
@ -494,7 +483,7 @@ func (bridge *Bridge) addUser(
return fmt.Errorf("failed to add vault user: %w", err) return fmt.Errorf("failed to add vault user: %w", err)
} }
if err := bridge.addUserWithVault(ctx, client, apiUser, vaultUser, isNew); err != nil { if err := bridge.addUserWithVault(ctx, client, apiUser, vaultUser); err != nil {
if _, ok := err.(*resty.ResponseError); ok || isLogin { if _, ok := err.(*resty.ResponseError); ok || isLogin {
logrus.WithError(err).Error("Failed to add user, clearing its secrets from vault") logrus.WithError(err).Error("Failed to add user, clearing its secrets from vault")
@ -529,17 +518,14 @@ func (bridge *Bridge) addUserWithVault(
client *proton.Client, client *proton.Client,
apiUser proton.User, apiUser proton.User,
vault *vault.User, vault *vault.User,
isNew bool,
) error { ) error {
statsPath, err := bridge.locator.ProvideStatsPath() statsPath, err := bridge.locator.ProvideStatsPath()
if err != nil { if err != nil {
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,
@ -552,17 +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,
isNew,
) )
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) {
@ -572,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)
}
}) })
}) })
@ -594,7 +582,7 @@ func (bridge *Bridge) addUserWithVault(
}, bridge.usersLock) }, bridge.usersLock)
// As we need at least one user to send heartbeat, try to send it. // As we need at least one user to send heartbeat, try to send it.
bridge.heartbeat.start() defer bridge.goHeartbeat()
return nil return nil
} }
@ -624,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")
} }

View File

@ -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)
} }

View File

@ -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,120 +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)
}
}
func osSupportCertInstall() bool {
return true
}
// 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 {
@ -370,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 {
@ -396,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)
}

View File

@ -25,74 +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.True(t, osSupportCertInstall())
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.
} }

View File

@ -17,10 +17,6 @@
package certs package certs
func osSupportCertInstall() bool {
return false
}
func installCert([]byte) error { func installCert([]byte) error {
return nil // Linux doesn't have a root cert store. return nil // Linux doesn't have a root cert store.
} }
@ -28,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
}

View File

@ -17,10 +17,6 @@
package certs package certs
func osSupportCertInstall() bool {
return false
}
func installCert([]byte) error { func installCert([]byte) error {
return nil // NOTE(GODT-986): Install certs to root cert store? return nil // NOTE(GODT-986): Install certs to root cert store?
} }
@ -28,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
}

View File

@ -17,66 +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) OSSupportCertInstall() bool {
return osSupportCertInstall()
} }
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)
}
// LogCertInstallStatus reports the current status of the certificate installation in the log.
// If certificate installation is not supported on the platform, this function does nothing.
func (installer *Installer) LogCertInstallStatus(certPEM []byte) {
if installer.OSSupportCertInstall() {
if installer.IsCertInstalled(certPEM) {
installer.log.Info("The Bridge TLS certificate is installed in the OS keychain")
} else {
installer.log.Info("The Bridge TLS certificate is not installed in the OS keychain")
}
}
} }

View File

@ -39,10 +39,10 @@ func (c *AppleMail) Configure(
hostname string, hostname string,
imapPort, smtpPort int, imapPort, smtpPort int,
imapSSL, smtpSSL bool, imapSSL, smtpSSL bool,
username, displayName, addresses string, username, addresses string,
password []byte, password []byte,
) error { ) error {
mc := prepareMobileConfig(hostname, imapPort, smtpPort, imapSSL, smtpSSL, username, displayName, addresses, password) mc := prepareMobileConfig(hostname, imapPort, smtpPort, imapSSL, smtpSSL, username, addresses, password)
confPath, err := saveConfigTemporarily(mc) confPath, err := saveConfigTemporarily(mc)
if err != nil { if err != nil {
@ -66,13 +66,13 @@ func prepareMobileConfig(
hostname string, hostname string,
imapPort, smtpPort int, imapPort, smtpPort int,
imapSSL, smtpSSL bool, imapSSL, smtpSSL bool,
username, displayName, addresses string, username, addresses string,
password []byte, password []byte,
) *mobileconfig.Config { ) *mobileconfig.Config {
return &mobileconfig.Config{ return &mobileconfig.Config{
DisplayName: username, DisplayName: username,
EmailAddress: addresses, EmailAddress: addresses,
AccountName: displayName, AccountName: username,
AccountDescription: username, AccountDescription: username,
Identifier: "protonmail " + username + strconv.FormatInt(time.Now().Unix(), 10), Identifier: "protonmail " + username + strconv.FormatInt(time.Now().Unix(), 10),
IMAP: &mobileconfig.IMAP{ IMAP: &mobileconfig.IMAP{

View File

@ -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()

View File

@ -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(),
}, },
} }
} }

View File

@ -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)

View File

@ -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),
}, },
} }
} }

View File

@ -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)

View File

@ -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,
}, },
} }
} }

View File

@ -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)

View File

@ -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(),
}, },
} }
} }

View File

@ -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)

View File

@ -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{}
}

View File

@ -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)
}

View File

@ -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

View File

@ -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 {

View File

@ -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.
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************

View File

@ -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.

View File

@ -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));
} }

View File

@ -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.

View File

@ -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");
} }

View File

@ -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.

View File

@ -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>

View File

@ -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);
} }

View File

@ -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.

View File

@ -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;

View File

@ -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).

View File

@ -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();
} }

View File

@ -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

View File

@ -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;
}; };

View File

@ -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>

View File

@ -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.

View File

@ -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);

View File

@ -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"

View File

@ -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;
} }
@ -415,11 +406,7 @@ int main(int argc, char *argv[]) {
} }
catch (Exception const &e) { catch (Exception const &e) {
sentry_uuid_s const uuid = reportSentryException("Exception occurred during main", e); sentry_uuid_s const uuid = reportSentryException("Exception occurred during main", e);
QString message = e.qwhat(); QMessageBox::critical(nullptr, "Error", e.qwhat());
if (e.showSupportLink()) {
message += R"(<br/><br/>If the issue persists, please contact our <a href="https://proton.me/support/contact">customer support</a>.)";
}
QMessageBox::critical(nullptr, "Error", message);
QTextStream(stderr) << "reportID: " << QByteArray(uuid.bytes, 16).toHex() << " Captured exception :" << e.detailedWhat() << "\n"; QTextStream(stderr) << "reportID: " << QByteArray(uuid.bytes, 16).toHex() << " Captured exception :" << e.detailedWhat() << "\n";
return EXIT_FAILURE; return EXIT_FAILURE;
} }

View File

@ -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
} }

View File

@ -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"
} }
} }
} }

View File

@ -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
} }
} }
} }

View File

@ -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
} }
} }

View File

@ -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
}
}

View File

@ -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();
}
}
}

View File

@ -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();
}
}
}
}
}

View File

@ -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()
}
}

View File

@ -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
}
}

View File

@ -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
}
} }
} }

View File

@ -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
} }
} }
} }

View File

@ -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()
}
} }

View File

@ -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)
}
}

View File

@ -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
} }

View File

@ -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 Protons 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 Protons 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
} }
} }

Some files were not shown because too many files have changed in this diff Show More