Compare commits
161 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3210709810 | |||
| 8fd988d7c5 | |||
| bf89d548d3 | |||
| 51229cbb68 | |||
| 36c5c37dac | |||
| 5a434fafbc | |||
| ea1c2534df | |||
| 1cafbfcaaa | |||
| 2d44ccaee0 | |||
| 96517b7fb1 | |||
| bc381407a7 | |||
| ddc5e775b9 | |||
| ea26188dc0 | |||
| 159e1cee7d | |||
| 4394ad0e9b | |||
| 856bdd1321 | |||
| ff288145df | |||
| 83bbdbd63e | |||
| fa430ee0fb | |||
| 0303ba38e8 | |||
| 2a78b5c144 | |||
| a00b3cdb92 | |||
| 8d3e04679f | |||
| 21ff7b4b97 | |||
| 4ea161f7ad | |||
| dc584ea29b | |||
| 4a01c46aed | |||
| e8d9534b9c | |||
| 96904b160f | |||
| b535be72f8 | |||
| 40f2d8b30f | |||
| 95a1acec0d | |||
| 5ff074cc49 | |||
| 4f0660bb8c | |||
| 708184439e | |||
| b8a33b9618 | |||
| 1c385d5c9b | |||
| 96773f3225 | |||
| 0f320dbd80 | |||
| 6cb233473a | |||
| 1ac4e70115 | |||
| 07f93d276b | |||
| d29571fb01 | |||
| d6000d025e | |||
| 09ef3b20db | |||
| 405331d59b | |||
| eff7df2136 | |||
| 5823e3a99f | |||
| 26d866bbbd | |||
| d3f7be059d | |||
| b52706a3ca | |||
| aebe7baed0 | |||
| ef31e2917c | |||
| 9eea26459a | |||
| 5747b85543 | |||
| ff78a23084 | |||
| 2a95e1ab41 | |||
| ab76cab533 | |||
| dda2a5d01a | |||
| c2afb42fd4 | |||
| 1d53044803 | |||
| d3f8297eb4 | |||
| b02203e3d3 | |||
| 5c7e4e04f9 | |||
| d7dadd7578 | |||
| ab9a758d63 | |||
| cb0935be96 | |||
| 441b388f62 | |||
| cdbcd30d15 | |||
| acc7ca8d4a | |||
| 42e1dd4c41 | |||
| 4cbd3ca832 | |||
| de0b6c0737 | |||
| 1c344211d1 | |||
| c11a87c16a | |||
| 3bf4282037 | |||
| 0c212fbef4 | |||
| 48d1ca1e72 | |||
| 52addb2582 | |||
| 742d9eeef3 | |||
| 55a9d4973c | |||
| 8402657108 | |||
| 8a6f96f9f2 | |||
| 56c53e9188 | |||
| bb67d95669 | |||
| 50acc0dcfb | |||
| e9c73c2d0d | |||
| 07c03c6920 | |||
| f4958b9b53 | |||
| 76f2e7fdb9 | |||
| c0992e8801 | |||
| cf3abaa96f | |||
| e422b28bc3 | |||
| a1a5ffba5d | |||
| f8b86a76dd | |||
| ab1281ceee | |||
| 0ab0f2f4ff | |||
| 09d87023f1 | |||
| 139ad75394 | |||
| c8cf90abfe | |||
| 5d4f8f7d40 | |||
| ea26dc0e97 | |||
| 8d346ea511 | |||
| 44df3cfd4a | |||
| 683458e264 | |||
| 36651698cb | |||
| 0c7e17701f | |||
| 86cd2437aa | |||
| 53f5f9aa43 | |||
| c849762445 | |||
| 32f2c72575 | |||
| 958e1280d7 | |||
| df09d6d221 | |||
| e0875dc928 | |||
| b3a5270bdc | |||
| f617a44d28 | |||
| 75ed3ca660 | |||
| 69f3029430 | |||
| 1203709ab9 | |||
| 15c18189d3 | |||
| a9e95f618b | |||
| 272f9cf59b | |||
| 6e86c95640 | |||
| 81afc5fb1f | |||
| 53ea5e9adc | |||
| 6f420f9098 | |||
| 65846ff40f | |||
| 43f7a989be | |||
| 452d3068f0 | |||
| 69190daf3f | |||
| f57a40677e | |||
| 2d6f42e0b5 | |||
| bccf31501d | |||
| 9b546b5412 | |||
| f48a60d58c | |||
| 0a51c7a6b0 | |||
| 7355c7dfd6 | |||
| bb5a91ee6d | |||
| ca5f7ce9f6 | |||
| ad31e6a9c5 | |||
| 9ef7d133c0 | |||
| 83b842b19d | |||
| df02e39fe1 | |||
| a35c8424a3 | |||
| 5d207810bd | |||
| 6c9d96d5e1 | |||
| 0fc41d1966 | |||
| dd5e745e37 | |||
| c8f0d7f32a | |||
| bd986901c3 | |||
| cdc19492ee | |||
| 635b2a4891 | |||
| e5bac33a04 | |||
| 7b96a07cf5 | |||
| 87e79fdcba | |||
| 03c3404044 | |||
| fa794a982b | |||
| cab32d5d5a | |||
| 8e5a892c45 | |||
| 50dc5c4085 | |||
| 3b58078595 |
@ -18,6 +18,10 @@
|
||||
---
|
||||
image: gitlab.protontech.ch:4567/go/bridge-internal:test-go1.20
|
||||
|
||||
default:
|
||||
tags:
|
||||
- shared-small
|
||||
|
||||
variables:
|
||||
GOPRIVATE: gitlab.protontech.ch
|
||||
GOMAXPROCS: $(( ${CI_TAG_CPU} / 2 ))
|
||||
@ -71,7 +75,7 @@ stages:
|
||||
- export GO111MODULE=on
|
||||
- export PATH="${GOPATH}/bin:${PATH}"
|
||||
- export MSYSTEM=
|
||||
- export QT6DIR=/c/grrrQt/6.3.2/msvc2019_64
|
||||
- export QT6DIR=/c/grrrQt/6.4.3/msvc2019_64
|
||||
- export PATH=$PATH:${QT6DIR}/bin
|
||||
- export PATH="/c/Program Files/Microsoft Visual Studio/2022/Community/Common7/IDE/CommonExtensions/Microsoft/CMake/CMake/bin:$PATH"
|
||||
- $(git config --global -l | grep -o 'url.*gitlab.protontech.ch.*insteadof' | xargs -L 1 git config --global --unset &> /dev/null) || echo "nothing to remove"
|
||||
@ -93,7 +97,7 @@ stages:
|
||||
- export PATH="${GOROOT}/bin:$PATH"
|
||||
- export GOPATH=~/go1.20
|
||||
- export PATH="${GOPATH}/bin:$PATH"
|
||||
- export QT6DIR=/opt/Qt/6.3.2/macos
|
||||
- export QT6DIR=/opt/Qt/6.4.3/macos
|
||||
- export PATH="${QT6DIR}/bin:$PATH"
|
||||
- uname -a
|
||||
cache: {}
|
||||
@ -101,7 +105,7 @@ stages:
|
||||
- macos-m1-bridge
|
||||
|
||||
.env-linux-build:
|
||||
image: gitlab.protontech.ch:4567/go/bridge-internal:build-go1.20-qt6.3.2
|
||||
image: gitlab.protontech.ch:4567/go/bridge-internal:build-go1.20-qt6.4.3
|
||||
variables:
|
||||
VCPKG_DEFAULT_BINARY_CACHE: ${CI_PROJECT_DIR}/.cache
|
||||
cache:
|
||||
@ -118,7 +122,7 @@ stages:
|
||||
- $(git config --global -l | grep -o 'url.*gitlab.protontech.ch.*insteadof' | xargs -L 1 git config --global --unset &> /dev/null) || echo "nothing to remove"
|
||||
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
|
||||
tags:
|
||||
- large
|
||||
- shared-large
|
||||
|
||||
# Stage: TEST
|
||||
|
||||
@ -129,7 +133,7 @@ lint:
|
||||
script:
|
||||
- make lint
|
||||
tags:
|
||||
- medium
|
||||
- shared-medium
|
||||
|
||||
bug-report-preview:
|
||||
stage: test
|
||||
@ -138,7 +142,7 @@ bug-report-preview:
|
||||
script:
|
||||
- make lint-bug-report-preview
|
||||
tags:
|
||||
- medium
|
||||
- shared-medium
|
||||
|
||||
.script-test:
|
||||
stage: test
|
||||
@ -154,7 +158,7 @@ test-linux:
|
||||
extends:
|
||||
- .script-test
|
||||
tags:
|
||||
- large
|
||||
- shared-large
|
||||
|
||||
fuzz-linux:
|
||||
stage: test
|
||||
@ -163,7 +167,7 @@ fuzz-linux:
|
||||
script:
|
||||
- make fuzz
|
||||
tags:
|
||||
- large
|
||||
- shared-large
|
||||
|
||||
test-linux-race:
|
||||
extends:
|
||||
@ -218,7 +222,7 @@ test-coverage:
|
||||
- test-integration
|
||||
- test-integration-nightly
|
||||
tags:
|
||||
- small
|
||||
- shared-small
|
||||
artifacts:
|
||||
paths:
|
||||
- coverage*
|
||||
@ -282,4 +286,18 @@ build-windows-qa:
|
||||
variables:
|
||||
BUILD_TAGS: "build_qa"
|
||||
|
||||
trigger-qa-installer:
|
||||
stage: build
|
||||
needs: ["lint"]
|
||||
extends:
|
||||
- .rules-branch-and-MR-manual
|
||||
variables:
|
||||
APP: bridge
|
||||
WORKFLOW: build-all
|
||||
SRC_TAG: $CI_COMMIT_BRANCH
|
||||
SRC_HASH: $CI_COMMIT_SHA
|
||||
trigger:
|
||||
project: "jcuth/bridge-release"
|
||||
branch: master
|
||||
|
||||
# TODO: PUT BACK ALL THE JOBS! JUST DID THIS FOR NOW TO GET CI WORKING AGAIN...
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
* Windres (Windows)
|
||||
* libglvnd and libsecret development files (Linux)
|
||||
* pkg-config (Linux)
|
||||
* cmake, ninja-build and Qt 6 are required to build the graphical user interface. On Linux,
|
||||
* cmake, ninja-build and Qt 6.4.3 are required to build the graphical user interface. On Linux,
|
||||
the Mesa OpenGL development files are also needed.
|
||||
|
||||
To enable the sending of crash reports using Sentry please set the
|
||||
@ -19,7 +19,7 @@ Otherwise, the sending of crash reports will be disabled.
|
||||
|
||||
## Build
|
||||
In order to build Bridge app with Qt interface we are using
|
||||
[Qt 6.3](https://doc.qt.io/qt-6/gettingstarted.html).
|
||||
[Qt 6.4.3](https://doc.qt.io/qt-6/gettingstarted.html).
|
||||
|
||||
Please note that qmake path must be in your `PATH` to ensure Qt to be found.
|
||||
Also, before you start build **on Windows**, please unset the `MSYSTEM` variable
|
||||
|
||||
@ -40,6 +40,7 @@ Proton Mail Bridge includes the following 3rd party software:
|
||||
* [go-message](https://github.com/emersion/go-message) available under [license](https://github.com/emersion/go-message/blob/master/LICENSE)
|
||||
* [go-sasl](https://github.com/emersion/go-sasl) available under [license](https://github.com/emersion/go-sasl/blob/master/LICENSE)
|
||||
* [go-smtp](https://github.com/emersion/go-smtp) available under [license](https://github.com/emersion/go-smtp/blob/master/LICENSE)
|
||||
* [go-vcard](https://github.com/emersion/go-vcard) available under [license](https://github.com/emersion/go-vcard/blob/master/LICENSE)
|
||||
* [color](https://github.com/fatih/color) available under [license](https://github.com/fatih/color/blob/master/LICENSE)
|
||||
* [sentry-go](https://github.com/getsentry/sentry-go) available under [license](https://github.com/getsentry/sentry-go/blob/master/LICENSE)
|
||||
* [resty](https://github.com/go-resty/resty/v2) available under [license](https://github.com/go-resty/resty/v2/blob/master/LICENSE)
|
||||
@ -83,7 +84,6 @@ Proton Mail Bridge includes the following 3rd party software:
|
||||
* [go-spew](https://github.com/davecgh/go-spew) available under [license](https://github.com/davecgh/go-spew/blob/master/LICENSE)
|
||||
* [go-windows](https://github.com/elastic/go-windows) available under [license](https://github.com/elastic/go-windows/blob/master/LICENSE)
|
||||
* [go-textwrapper](https://github.com/emersion/go-textwrapper) available under [license](https://github.com/emersion/go-textwrapper/blob/master/LICENSE)
|
||||
* [go-vcard](https://github.com/emersion/go-vcard) available under [license](https://github.com/emersion/go-vcard/blob/master/LICENSE)
|
||||
* [fgprof](https://github.com/felixge/fgprof) available under [license](https://github.com/felixge/fgprof/blob/master/LICENSE)
|
||||
* [go-shlex](https://github.com/flynn-archive/go-shlex) available under [license](https://github.com/flynn-archive/go-shlex/blob/master/LICENSE)
|
||||
* [mimetype](https://github.com/gabriel-vasile/mimetype) available under [license](https://github.com/gabriel-vasile/mimetype/blob/master/LICENSE)
|
||||
@ -123,6 +123,7 @@ Proton Mail Bridge includes the following 3rd party software:
|
||||
* [codec](https://github.com/ugorji/go/codec) available under [license](https://github.com/ugorji/go/codec/blob/master/LICENSE)
|
||||
* [tagparser](https://github.com/vmihailenco/tagparser/v2) available under [license](https://github.com/vmihailenco/tagparser/v2/blob/master/LICENSE)
|
||||
* [smetrics](https://github.com/xrash/smetrics) available under [license](https://github.com/xrash/smetrics/blob/master/LICENSE)
|
||||
* [go-ordered-json](https://gitlab.com/c0b/go-ordered-json)
|
||||
* [arch](https://golang.org/x/arch) available under [license](https://cs.opensource.google/go/x/arch/+/master:LICENSE)
|
||||
* [crypto](https://golang.org/x/crypto) available under [license](https://cs.opensource.google/go/x/crypto/+/master:LICENSE)
|
||||
* [mod](https://golang.org/x/mod) available under [license](https://cs.opensource.google/go/x/mod/+/master:LICENSE)
|
||||
@ -132,5 +133,7 @@ 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)
|
||||
* [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-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)
|
||||
<!-- END AUTOGEN -->
|
||||
|
||||
145
Changelog.md
@ -3,6 +3,150 @@
|
||||
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
|
||||
|
||||
## Wakato Bridge 3.7.1
|
||||
|
||||
### 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
|
||||
* GODT-3054: Only delete drafts after message has been Sent.
|
||||
* 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
|
||||
@ -75,6 +219,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
* 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
|
||||
|
||||
3
Makefile
@ -11,7 +11,7 @@ ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
|
||||
.PHONY: build build-gui build-nogui build-launcher versioner hasher
|
||||
|
||||
# Keep version hardcoded so app build works also without Git repository.
|
||||
BRIDGE_APP_VERSION?=3.5.0+git
|
||||
BRIDGE_APP_VERSION?=3.7.1+git
|
||||
APP_VERSION:=${BRIDGE_APP_VERSION}
|
||||
APP_FULL_NAME:=Proton Mail Bridge
|
||||
APP_VENDOR:=Proton AG
|
||||
@ -304,6 +304,7 @@ ApplyStageInput,BuildStageInput,BuildStageOutput,DownloadStageInput,DownloadStag
|
||||
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
|
||||
|
||||
|
||||
@ -23,7 +23,6 @@ import (
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/app"
|
||||
"github.com/bradenaw/juniper/xslices"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
/*
|
||||
@ -44,7 +43,5 @@ import (
|
||||
*/
|
||||
|
||||
func main() {
|
||||
if err := app.New().Run(xslices.Filter(os.Args, func(arg string) bool { return !strings.Contains(arg, "-psn_") })); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
_ = app.New().Run(xslices.Filter(os.Args, func(arg string) bool { return !strings.Contains(arg, "-psn_") }))
|
||||
}
|
||||
|
||||
27
go.mod
@ -5,10 +5,10 @@ go 1.20
|
||||
require (
|
||||
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
|
||||
github.com/Masterminds/semver/v3 v3.2.0
|
||||
github.com/ProtonMail/gluon v0.17.1-0.20230829112217-5d5c25c504b5
|
||||
github.com/ProtonMail/gluon v0.17.1-0.20231114153341-2ecbdd2739f7
|
||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20230831064234-0e3a549b3f36
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20231116144214-8a47c8d92fbc
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton
|
||||
github.com/PuerkitoBio/goquery v1.8.1
|
||||
github.com/abiosoft/ishell v2.0.0+incompatible
|
||||
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37
|
||||
@ -22,6 +22,7 @@ require (
|
||||
github.com/emersion/go-message v0.16.0
|
||||
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead
|
||||
github.com/emersion/go-smtp v0.15.1-0.20221021114529-49b17434419d
|
||||
github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3
|
||||
github.com/fatih/color v1.13.0
|
||||
github.com/getsentry/sentry-go v0.15.0
|
||||
github.com/go-resty/resty/v2 v2.7.0
|
||||
@ -42,17 +43,17 @@ require (
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.5
|
||||
go.uber.org/goleak v1.2.1
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
|
||||
golang.org/x/net v0.10.0
|
||||
golang.org/x/sys v0.8.0
|
||||
golang.org/x/text v0.9.0
|
||||
google.golang.org/grpc v1.53.0
|
||||
golang.org/x/net v0.17.0
|
||||
golang.org/x/sys v0.13.0
|
||||
golang.org/x/text v0.13.0
|
||||
google.golang.org/grpc v1.56.3
|
||||
google.golang.org/protobuf v1.30.0
|
||||
howett.net/plist v1.0.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf // indirect
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903 // indirect
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230717121622-edf196117233 // indirect
|
||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect
|
||||
github.com/ProtonMail/go-srp v0.0.7 // indirect
|
||||
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect
|
||||
@ -68,7 +69,6 @@ require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/elastic/go-windows v1.0.1 // indirect
|
||||
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect
|
||||
github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3 // indirect
|
||||
github.com/felixge/fgprof v0.9.3 // indirect
|
||||
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
@ -79,7 +79,7 @@ require (
|
||||
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/gofrs/uuid v4.3.0+incompatible // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
||||
@ -108,17 +108,20 @@ require (
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||
gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/crypto v0.9.0 // indirect
|
||||
golang.org/x/crypto v0.14.0 // indirect
|
||||
golang.org/x/mod v0.8.0 // indirect
|
||||
golang.org/x/sync v0.2.0 // indirect
|
||||
golang.org/x/tools v0.6.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230221151758-ace64dc21148 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
replace (
|
||||
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-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
|
||||
)
|
||||
|
||||
64
go.sum
@ -15,6 +15,8 @@ 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/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/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/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
@ -23,24 +25,25 @@ github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
|
||||
github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
|
||||
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
|
||||
github.com/ProtonMail/gluon v0.17.1-0.20230829112217-5d5c25c504b5 h1:C/8P5NHAKi2yCKez+OZ5rSR8SsL7k8si4pK4SE2QtV8=
|
||||
github.com/ProtonMail/gluon v0.17.1-0.20230829112217-5d5c25c504b5/go.mod h1:Og5/Dz1MiGpCJn51XujZwxiLG7WzvvjE5PRpZBQmAHo=
|
||||
github.com/ProtonMail/gluon v0.17.1-0.20231114153341-2ecbdd2739f7 h1:w+VoSAq9FQvKMm3DlH1MIEZ1KGe7LJ+81EJFVwSV4VU=
|
||||
github.com/ProtonMail/gluon v0.17.1-0.20231114153341-2ecbdd2739f7/go.mod h1:Og5/Dz1MiGpCJn51XujZwxiLG7WzvvjE5PRpZBQmAHo=
|
||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
|
||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230322105811-d73448b7e800/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903 h1:ZK3C5DtzV2nVAQTx5S5jQvMeDqWtD1By5mOoyY/xJek=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230717121622-edf196117233 h1:bdoKdh0f66/lrgVfYlxw0aqISY/KOqXmFJyGt7rGmnc=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230717121622-edf196117233/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
||||
github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7 h1:+j+Kd/DyZ/qGfMT9htAT7HxqIEbZHsatsx+m8AoV6fc=
|
||||
github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20230831064234-0e3a549b3f36 h1:JVMK2w90bCWayUCXJIb3wkQ5+j2P/NbnrX3BrDoLzsc=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20230831064234-0e3a549b3f36/go.mod h1:nS8hMGjJLgC0Iej0JMYbsI388LesEkM1Hj/jCCxQeaQ=
|
||||
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.20231116144214-8a47c8d92fbc/go.mod h1:WEXJqj5DSc2YI77SgXdpMY0nk33Qy92Vu2r4tOEazA8=
|
||||
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/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton h1:YS6M20yvjCJPR1r4ADW5TPn6rahs4iAyZaACei86bEc=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton/go.mod h1:S1lYsaGHykYpxxh2SnJL6ypcAlANKj5NRSY6HxKryKQ=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton h1:8tqHYM6IGsdEc6Vxf1TWiwpHNj8yIEQNACPhxsDagrk=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton/go.mod h1:omVkSsfPAhmptzPF/piMXb16wKIWUvVhZbVW7sJKh0A=
|
||||
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
|
||||
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
|
||||
github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw=
|
||||
@ -64,6 +67,7 @@ github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJm
|
||||
github.com/bradenaw/juniper v0.12.0 h1:Q/7icpPQD1nH/La5DobQfNEtwyrBSiSu47jOQx7lJEM=
|
||||
github.com/bradenaw/juniper v0.12.0/go.mod h1:Z2B7aJlQ7xbfWsnMLROj5t/5FQ94/MkIdKC30J4WvzI=
|
||||
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
||||
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||
@ -120,8 +124,6 @@ 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-20220912192320-0145f2c60ead h1:fI1Jck0vUrXT8bnphprS1EoVRe2Q5CKCX8iDlpqjQ/Y=
|
||||
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/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
|
||||
github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3 h1:hQ1wTMaKcGfobYRT88RM8NFNyX+IQHvagkm/tqViU98=
|
||||
@ -155,8 +157,6 @@ 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/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-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/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
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.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
@ -399,6 +399,8 @@ github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRT
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a h1:DxppxFKRqJ8WD6oJ3+ZXKDY0iMONQDl5UTg2aTyHh8k=
|
||||
gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a/go.mod h1:NREvu3a57BaK0R1+ztrEzHWiZAihohNLQ6trPxlIqZI=
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
@ -417,9 +419,10 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
|
||||
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
||||
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@ -462,14 +465,15 @@ 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-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-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@ -512,16 +516,22 @@ golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
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.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
@ -529,12 +539,16 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/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-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@ -580,13 +594,13 @@ google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20230221151758-ace64dc21148 h1:muK+gVBJBfFb4SejshDBlN2/UgxCCOKH9Y34ljqEGOc=
|
||||
google.golang.org/genproto v0.0.0-20230221151758-ace64dc21148/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw=
|
||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
|
||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc=
|
||||
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
|
||||
google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc=
|
||||
google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||
|
||||
@ -41,6 +41,7 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/sentry"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
|
||||
"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/pkg/profile"
|
||||
"github.com/sirupsen/logrus"
|
||||
@ -204,7 +205,7 @@ func run(c *cli.Context) error {
|
||||
}()
|
||||
|
||||
// Restart the app if requested.
|
||||
return withRestarter(exe, func(restarter *restarter.Restarter) error {
|
||||
err = withRestarter(exe, func(restarter *restarter.Restarter) error {
|
||||
// Handle crashes with various actions.
|
||||
return withCrashHandler(restarter, reporter, func(crashHandler *crash.Handler, quitCh <-chan struct{}) error {
|
||||
migrationErr := migrateOldVersions()
|
||||
@ -234,53 +235,56 @@ func run(c *cli.Context) error {
|
||||
}
|
||||
|
||||
return withSingleInstance(settings, locations.GetLockFile(), version, func() error {
|
||||
// Unlock the encrypted vault.
|
||||
return WithVault(locations, crashHandler, func(v *vault.Vault, insecure, corrupt bool) error {
|
||||
if !v.Migrated() {
|
||||
// Migrate old settings into the vault.
|
||||
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, 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{
|
||||
"lastVersion": v.GetLastVersion().String(),
|
||||
"showAllMail": v.GetShowAllMail(),
|
||||
"updateCh": v.GetUpdateChannel(),
|
||||
"autoUpdate": v.GetAutoUpdate(),
|
||||
"rollout": v.GetUpdateRollout(),
|
||||
"DoH": v.GetProxyAllowed(),
|
||||
}).Info("Vault loaded")
|
||||
|
||||
// Load the cookies from the vault.
|
||||
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)
|
||||
// Look for available keychains
|
||||
return WithKeychainList(func(keychains *keychain.List) error {
|
||||
// Unlock the encrypted vault.
|
||||
return WithVault(locations, keychains, crashHandler, func(v *vault.Vault, insecure, corrupt bool) error {
|
||||
if !v.Migrated() {
|
||||
// Migrate old settings into the vault.
|
||||
if err := migrateOldSettings(v); err != nil {
|
||||
logrus.WithError(err).Error("Failed to migrate old settings")
|
||||
}
|
||||
|
||||
if corrupt {
|
||||
logrus.Warn("The vault is corrupt and has been wiped")
|
||||
b.PushError(bridge.ErrVaultCorrupt)
|
||||
// Migrate old accounts into the vault.
|
||||
if err := migrateOldAccounts(locations, keychains, v); err != nil {
|
||||
logrus.WithError(err).Error("Failed to migrate old accounts")
|
||||
}
|
||||
|
||||
// Start telemetry heartbeat process
|
||||
b.StartHeartbeat(b)
|
||||
// The vault has been migrated.
|
||||
if err := v.SetMigrated(); err != nil {
|
||||
logrus.WithError(err).Error("Failed to mark vault as migrated")
|
||||
}
|
||||
}
|
||||
|
||||
// Run the frontend.
|
||||
return runFrontend(c, crashHandler, restarter, locations, b, eventCh, quitCh, c.Int(flagParentPID))
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"lastVersion": v.GetLastVersion().String(),
|
||||
"showAllMail": v.GetShowAllMail(),
|
||||
"updateCh": v.GetUpdateChannel(),
|
||||
"autoUpdate": v.GetAutoUpdate(),
|
||||
"rollout": v.GetUpdateRollout(),
|
||||
"DoH": v.GetProxyAllowed(),
|
||||
}).Info("Vault loaded")
|
||||
|
||||
// Load the cookies from the vault.
|
||||
return withCookieJar(v, func(cookieJar http.CookieJar) error {
|
||||
// Create a new bridge instance.
|
||||
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.Warn("The vault is corrupt and has been wiped")
|
||||
b.PushError(bridge.ErrVaultCorrupt)
|
||||
}
|
||||
|
||||
// Remove old updates files
|
||||
b.RemoveOldUpdates()
|
||||
|
||||
// Run the frontend.
|
||||
return runFrontend(c, crashHandler, restarter, locations, b, eventCh, quitCh, c.Int(flagParentPID))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -290,6 +294,13 @@ 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.
|
||||
@ -470,6 +481,13 @@ func withCookieJar(vault *vault.Vault, fn func(http.CookieJar) error) error {
|
||||
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 {
|
||||
url, err := url.Parse(constants.APIHost)
|
||||
if err != nil {
|
||||
|
||||
@ -37,6 +37,7 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/versioner"
|
||||
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
@ -55,6 +56,7 @@ func withBridge(
|
||||
reporter *sentry.Reporter,
|
||||
vault *vault.Vault,
|
||||
cookieJar http.CookieJar,
|
||||
keychains *keychain.List,
|
||||
fn func(*bridge.Bridge, <-chan events.Event) error,
|
||||
) error {
|
||||
logrus.Debug("Creating bridge")
|
||||
@ -97,6 +99,7 @@ func withBridge(
|
||||
autostarter,
|
||||
updater,
|
||||
version,
|
||||
keychains,
|
||||
|
||||
// The API stuff.
|
||||
constants.APIHost,
|
||||
@ -110,6 +113,7 @@ func withBridge(
|
||||
crashHandler,
|
||||
reporter,
|
||||
imap.DefaultEpochUIDValidityGenerator(),
|
||||
nil,
|
||||
|
||||
// The logging stuff.
|
||||
c.String(flagLogIMAP) == "client" || c.String(flagLogIMAP) == "all",
|
||||
@ -155,7 +159,7 @@ func newUpdater(locations *locations.Locations) (*updater.Updater, error) {
|
||||
}
|
||||
|
||||
return updater.NewUpdater(
|
||||
updater.NewInstaller(versioner.New(updatesDir)),
|
||||
versioner.New(updatesDir),
|
||||
verifier,
|
||||
constants.UpdateName,
|
||||
runtime.GOOS,
|
||||
|
||||
@ -122,7 +122,7 @@ func migrateOldSettingsWithDir(configDir string, v *vault.Vault) error {
|
||||
return v.SetBridgeTLSCertKey(certPEM, keyPEM)
|
||||
}
|
||||
|
||||
func migrateOldAccounts(locations *locations.Locations, v *vault.Vault) error {
|
||||
func migrateOldAccounts(locations *locations.Locations, keychains *keychain.List, v *vault.Vault) error {
|
||||
logrus.Info("Migrating accounts")
|
||||
|
||||
settings, err := locations.ProvideSettingsPath()
|
||||
@ -134,8 +134,7 @@ func migrateOldAccounts(locations *locations.Locations, v *vault.Vault) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get helper: %w", err)
|
||||
}
|
||||
|
||||
keychain, err := keychain.NewKeychain(helper, "bridge")
|
||||
keychain, err := keychain.NewKeychain(helper, "bridge", keychains.GetHelpers(), keychains.GetDefaultHelper())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create keychain: %w", err)
|
||||
}
|
||||
|
||||
@ -35,7 +35,6 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
"github.com/ProtonMail/proton-bridge/v3/pkg/algo"
|
||||
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
|
||||
dockerCredentials "github.com/docker/docker-credential-helpers/credentials"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@ -133,11 +132,9 @@ func TestKeychainMigration(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUserMigration(t *testing.T) {
|
||||
keychainHelper := keychain.NewTestHelper()
|
||||
kcl := keychain.NewTestKeychainsList()
|
||||
|
||||
keychain.Helpers["mock"] = func(string) (dockerCredentials.Helper, error) { return keychainHelper, nil }
|
||||
|
||||
kc, err := keychain.NewKeychain("mock", "bridge")
|
||||
kc, err := keychain.NewKeychain("mock", "bridge", kcl.GetHelpers(), kcl.GetDefaultHelper())
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, kc.Put("brokenID", "broken"))
|
||||
@ -178,7 +175,7 @@ func TestUserMigration(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.False(t, corrupt)
|
||||
|
||||
require.NoError(t, migrateOldAccounts(locations, v))
|
||||
require.NoError(t, migrateOldAccounts(locations, kcl, v))
|
||||
require.Equal(t, []string{wantCredentials.UserID}, v.GetUserIDs())
|
||||
|
||||
require.NoError(t, v.GetUser(wantCredentials.UserID, func(u *vault.User) {
|
||||
|
||||
@ -30,12 +30,12 @@ import (
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func WithVault(locations *locations.Locations, panicHandler async.PanicHandler, fn func(*vault.Vault, bool, bool) error) error {
|
||||
func WithVault(locations *locations.Locations, keychains *keychain.List, panicHandler async.PanicHandler, fn func(*vault.Vault, bool, bool) error) error {
|
||||
logrus.Debug("Creating vault")
|
||||
defer logrus.Debug("Vault stopped")
|
||||
|
||||
// Create the encVault.
|
||||
encVault, insecure, corrupt, err := newVault(locations, panicHandler)
|
||||
encVault, insecure, corrupt, err := newVault(locations, keychains, panicHandler)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create vault: %w", err)
|
||||
}
|
||||
@ -45,29 +45,15 @@ func WithVault(locations *locations.Locations, panicHandler async.PanicHandler,
|
||||
"corrupt": corrupt,
|
||||
}).Debug("Vault created")
|
||||
|
||||
// Install the certificates if needed.
|
||||
if installed := encVault.GetCertsInstalled(); !installed {
|
||||
logrus.Debug("Installing certificates")
|
||||
|
||||
certPEM, _ := encVault.GetBridgeTLSCert()
|
||||
|
||||
if err := certs.NewInstaller().InstallCert(certPEM); err != nil {
|
||||
return fmt.Errorf("failed to install certs: %w", err)
|
||||
}
|
||||
|
||||
if err := encVault.SetCertsInstalled(true); err != nil {
|
||||
return fmt.Errorf("failed to set certs installed: %w", err)
|
||||
}
|
||||
|
||||
logrus.Debug("Certificates successfully installed")
|
||||
}
|
||||
cert, _ := encVault.GetBridgeTLSCert()
|
||||
certs.NewInstaller().LogCertInstallStatus(cert)
|
||||
|
||||
// GODT-1950: Add teardown actions (e.g. to close the vault).
|
||||
|
||||
return fn(encVault, insecure, corrupt)
|
||||
}
|
||||
|
||||
func newVault(locations *locations.Locations, panicHandler async.PanicHandler) (*vault.Vault, bool, bool, error) {
|
||||
func newVault(locations *locations.Locations, keychains *keychain.List, panicHandler async.PanicHandler) (*vault.Vault, bool, bool, error) {
|
||||
vaultDir, err := locations.ProvideSettingsPath()
|
||||
if err != nil {
|
||||
return nil, false, false, fmt.Errorf("could not get vault dir: %w", err)
|
||||
@ -80,7 +66,7 @@ func newVault(locations *locations.Locations, panicHandler async.PanicHandler) (
|
||||
insecure bool
|
||||
)
|
||||
|
||||
if key, err := loadVaultKey(vaultDir); err != nil {
|
||||
if key, err := loadVaultKey(vaultDir, keychains); err != nil {
|
||||
logrus.WithError(err).Error("Could not load/create vault key")
|
||||
insecure = true
|
||||
|
||||
@ -103,13 +89,13 @@ func newVault(locations *locations.Locations, panicHandler async.PanicHandler) (
|
||||
return vault, insecure, corrupt, nil
|
||||
}
|
||||
|
||||
func loadVaultKey(vaultDir string) ([]byte, error) {
|
||||
func loadVaultKey(vaultDir string, keychains *keychain.List) ([]byte, error) {
|
||||
helper, err := vault.GetHelper(vaultDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get keychain helper: %w", err)
|
||||
}
|
||||
|
||||
kc, err := keychain.NewKeychain(helper, constants.KeyChainName)
|
||||
kc, err := keychain.NewKeychain(helper, constants.KeyChainName, keychains.GetHelpers(), keychains.GetDefaultHelper())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create keychain: %w", err)
|
||||
}
|
||||
|
||||
@ -45,6 +45,7 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/telemetry"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
|
||||
"github.com/bradenaw/juniper/xslices"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
@ -74,7 +75,7 @@ type Bridge struct {
|
||||
installCh chan installJob
|
||||
|
||||
// heartbeat is the telemetry heartbeat for metrics.
|
||||
heartbeat telemetry.Heartbeat
|
||||
heartbeat *heartBeatState
|
||||
|
||||
// curVersion is the current version of the bridge,
|
||||
// newVersion is the version that was installed by the updater.
|
||||
@ -82,6 +83,9 @@ type Bridge struct {
|
||||
newVersion *semver.Version
|
||||
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 *focus.Service
|
||||
|
||||
@ -124,9 +128,6 @@ type Bridge struct {
|
||||
// goUpdate triggers a check/install of updates.
|
||||
goUpdate func()
|
||||
|
||||
// goHeartbeat triggers a check/sending if heartbeat is needed.
|
||||
goHeartbeat func()
|
||||
|
||||
serverManager *imapsmtpserver.Service
|
||||
syncService *syncservice.Service
|
||||
}
|
||||
@ -138,6 +139,7 @@ func New(
|
||||
autostarter Autostarter, // the autostarter to manage autostart settings
|
||||
updater Updater, // the updater to fetch and install updates
|
||||
curVersion *semver.Version, // the current version of the bridge
|
||||
keychains *keychain.List, // usable keychains
|
||||
|
||||
apiURL string, // the URL of the API to use
|
||||
cookieJar http.CookieJar, // the cookie jar to use
|
||||
@ -148,6 +150,7 @@ func New(
|
||||
panicHandler async.PanicHandler,
|
||||
reporter reporter.Reporter,
|
||||
uidValidityGenerator imap.UIDValidityGenerator,
|
||||
heartBeatManager telemetry.HeartbeatManager,
|
||||
|
||||
logIMAPClient, logIMAPServer bool, // whether to log IMAP client/server activity
|
||||
logSMTP bool, // whether to log SMTP activity
|
||||
@ -163,6 +166,7 @@ func New(
|
||||
|
||||
// bridge is the bridge.
|
||||
bridge, err := newBridge(
|
||||
context.Background(),
|
||||
tasks,
|
||||
imapEventCh,
|
||||
|
||||
@ -171,6 +175,7 @@ func New(
|
||||
autostarter,
|
||||
updater,
|
||||
curVersion,
|
||||
keychains,
|
||||
panicHandler,
|
||||
reporter,
|
||||
|
||||
@ -178,6 +183,7 @@ func New(
|
||||
identifier,
|
||||
proxyCtl,
|
||||
uidValidityGenerator,
|
||||
heartBeatManager,
|
||||
logIMAPClient, logIMAPServer, logSMTP,
|
||||
)
|
||||
if err != nil {
|
||||
@ -196,6 +202,7 @@ func New(
|
||||
}
|
||||
|
||||
func newBridge(
|
||||
ctx context.Context,
|
||||
tasks *async.Group,
|
||||
imapEventCh chan imapEvents.Event,
|
||||
|
||||
@ -204,6 +211,7 @@ func newBridge(
|
||||
autostarter Autostarter,
|
||||
updater Updater,
|
||||
curVersion *semver.Version,
|
||||
keychains *keychain.List,
|
||||
panicHandler async.PanicHandler,
|
||||
reporter reporter.Reporter,
|
||||
|
||||
@ -211,6 +219,7 @@ func newBridge(
|
||||
identifier identifier.Identifier,
|
||||
proxyCtl ProxyController,
|
||||
uidValidityGenerator imap.UIDValidityGenerator,
|
||||
heartbeatManager telemetry.HeartbeatManager,
|
||||
|
||||
logIMAPClient, logIMAPServer, logSMTP bool,
|
||||
) (*Bridge, error) {
|
||||
@ -256,9 +265,13 @@ func newBridge(
|
||||
newVersion: curVersion,
|
||||
newVersionLock: safe.NewRWMutex(),
|
||||
|
||||
keychains: keychains,
|
||||
|
||||
panicHandler: panicHandler,
|
||||
reporter: reporter,
|
||||
|
||||
heartbeat: newHeartBeatState(ctx, panicHandler),
|
||||
|
||||
focusService: focusService,
|
||||
autostarter: autostarter,
|
||||
locator: locator,
|
||||
@ -288,6 +301,12 @@ func newBridge(
|
||||
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
|
||||
@ -417,6 +436,9 @@ func (bridge *Bridge) GetErrors() []error {
|
||||
func (bridge *Bridge) Close(ctx context.Context) {
|
||||
logrus.Info("Closing bridge")
|
||||
|
||||
// Stop heart beat before closing users.
|
||||
bridge.heartbeat.stop()
|
||||
|
||||
// Close all users.
|
||||
safe.Lock(func() {
|
||||
for _, user := range bridge.users {
|
||||
@ -487,27 +509,15 @@ func (bridge *Bridge) remWatcher(watcher *watcher.Watcher[events.Event]) {
|
||||
watcher.Close()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) onStatusUp(ctx context.Context) {
|
||||
func (bridge *Bridge) onStatusUp(_ context.Context) {
|
||||
logrus.Info("Handling API status up")
|
||||
|
||||
safe.RLock(func() {
|
||||
for _, user := range bridge.users {
|
||||
user.OnStatusUp(ctx)
|
||||
}
|
||||
}, bridge.usersLock)
|
||||
|
||||
bridge.goLoad()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) onStatusDown(ctx context.Context) {
|
||||
logrus.Info("Handling API status down")
|
||||
|
||||
safe.RLock(func() {
|
||||
for _, user := range bridge.users {
|
||||
user.OnStatusDown(ctx)
|
||||
}
|
||||
}, bridge.usersLock)
|
||||
|
||||
for backoff := time.Second; ; backoff = min(backoff*2, 30*time.Second) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
|
||||
@ -49,6 +49,7 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
|
||||
"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/bradenaw/juniper/xslices"
|
||||
imapid "github.com/emersion/go-imap-id"
|
||||
@ -585,7 +586,7 @@ func TestBridge_MissingGluonStore(t *testing.T) {
|
||||
require.NoError(t, os.RemoveAll(gluonDir))
|
||||
|
||||
// Bridge starts but can't find the gluon store dir; there should be no error.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridgeWaitForServers(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// ...
|
||||
})
|
||||
})
|
||||
@ -950,6 +951,7 @@ func withBridgeNoMocks(
|
||||
mocks.Autostarter,
|
||||
mocks.Updater,
|
||||
v2_3_0,
|
||||
keychain.NewTestKeychainsList(),
|
||||
|
||||
// The API stuff.
|
||||
apiURL,
|
||||
@ -961,6 +963,7 @@ func withBridgeNoMocks(
|
||||
mocks.CrashHandler,
|
||||
mocks.Reporter,
|
||||
testUIDValidityGenerator,
|
||||
mocks.Heartbeat,
|
||||
|
||||
// The logging stuff.
|
||||
os.Getenv("BRIDGE_LOG_IMAP_CLIENT") == "1",
|
||||
@ -970,9 +973,6 @@ func withBridgeNoMocks(
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, bridge.GetErrors())
|
||||
|
||||
// Start the Heartbeat process.
|
||||
bridge.StartHeartbeat(mocks.Heartbeat)
|
||||
|
||||
// Wait for bridge to finish loading users.
|
||||
waitForEvent(t, eventCh, events.AllUsersLoaded{})
|
||||
|
||||
|
||||
@ -19,6 +19,7 @@ package bridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
@ -33,63 +34,133 @@ const (
|
||||
DefaultMaxSessionCountForBugReport = 10
|
||||
)
|
||||
|
||||
func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, title, description, username, email, client string, attachLogs bool) error {
|
||||
var account string
|
||||
type ReportBugReq struct {
|
||||
OSType string
|
||||
OSVersion string
|
||||
Title string
|
||||
Description string
|
||||
Username string
|
||||
Email string
|
||||
EmailClient string
|
||||
IncludeLogs bool
|
||||
}
|
||||
|
||||
if info, err := bridge.QueryUserInfo(username); err == nil {
|
||||
account = info.Username
|
||||
func (bridge *Bridge) ReportBug(ctx context.Context, report *ReportBugReq) error {
|
||||
if info, err := bridge.QueryUserInfo(report.Username); err == nil {
|
||||
report.Username = info.Username
|
||||
} else if userIDs := bridge.GetUserIDs(); len(userIDs) > 0 {
|
||||
if err := bridge.vault.GetUser(userIDs[0], func(user *vault.User) {
|
||||
account = user.Username()
|
||||
report.Username = user.Username()
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var attachment []proton.ReportBugAttachment
|
||||
|
||||
if attachLogs {
|
||||
logsPath, err := bridge.locator.ProvideLogsPath()
|
||||
var attachments []proton.ReportBugAttachment
|
||||
if report.IncludeLogs {
|
||||
logs, err := bridge.CollectLogs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buffer, err := logging.ZipLogsForBugReport(logsPath, DefaultMaxSessionCountForBugReport, DefaultMaxBugReportZipSize)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(buffer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
attachment = append(attachment, proton.ReportBugAttachment{
|
||||
Name: "logs.zip",
|
||||
Filename: "logs.zip",
|
||||
MIMEType: "application/zip",
|
||||
Body: body,
|
||||
})
|
||||
attachments = append(attachments, logs)
|
||||
}
|
||||
|
||||
safe.Lock(func() {
|
||||
var firstAtt proton.ReportBugAttachment
|
||||
if len(attachments) > 0 && report.IncludeLogs {
|
||||
firstAtt = attachments[0]
|
||||
}
|
||||
|
||||
attachmentType := proton.AttachmentTypeSync
|
||||
if len(attachments) > 1 {
|
||||
attachmentType = proton.AttachmentTypeAsync
|
||||
}
|
||||
|
||||
token, err := bridge.createTicket(ctx, report, attachmentType, firstAtt)
|
||||
if err != nil || token == "" {
|
||||
return err
|
||||
}
|
||||
|
||||
safe.RLock(func() {
|
||||
for _, user := range bridge.users {
|
||||
user.ReportBugSent()
|
||||
}
|
||||
}, bridge.usersLock)
|
||||
|
||||
return bridge.api.ReportBug(ctx, proton.ReportBugReq{
|
||||
OS: osType,
|
||||
OSVersion: osVersion,
|
||||
// if we have a token we can append more attachment to the bugReport
|
||||
for i, att := range attachments {
|
||||
if i == 0 && report.IncludeLogs {
|
||||
continue
|
||||
}
|
||||
err := bridge.appendComment(ctx, token, att)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
Title: "[Bridge] Bug - " + title,
|
||||
Description: description,
|
||||
func (bridge *Bridge) CollectLogs() (proton.ReportBugAttachment, error) {
|
||||
logsPath, err := bridge.locator.ProvideLogsPath()
|
||||
if err != nil {
|
||||
return proton.ReportBugAttachment{}, err
|
||||
}
|
||||
|
||||
Client: client,
|
||||
buffer, err := logging.ZipLogsForBugReport(logsPath, DefaultMaxSessionCountForBugReport, DefaultMaxBugReportZipSize)
|
||||
if err != nil {
|
||||
return proton.ReportBugAttachment{}, err
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(buffer)
|
||||
if err != nil {
|
||||
return proton.ReportBugAttachment{}, err
|
||||
}
|
||||
|
||||
return proton.ReportBugAttachment{
|
||||
Name: "logs.zip",
|
||||
Filename: "logs.zip",
|
||||
MIMEType: "application/zip",
|
||||
Body: body,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (bridge *Bridge) createTicket(ctx context.Context, report *ReportBugReq,
|
||||
asyncAttach proton.AttachmentType, att proton.ReportBugAttachment) (string, error) {
|
||||
var attachments []proton.ReportBugAttachment
|
||||
attachments = append(attachments, att)
|
||||
res, err := bridge.api.ReportBug(ctx, proton.ReportBugReq{
|
||||
OS: report.OSType,
|
||||
OSVersion: report.OSVersion,
|
||||
|
||||
Title: "[Bridge] Bug - " + report.Title,
|
||||
Description: report.Description,
|
||||
|
||||
Client: report.EmailClient,
|
||||
ClientType: proton.ClientTypeEmail,
|
||||
ClientVersion: constants.AppVersion(bridge.curVersion.Original()),
|
||||
|
||||
Username: account,
|
||||
Email: email,
|
||||
}, attachment...)
|
||||
Username: report.Username,
|
||||
Email: report.Email,
|
||||
|
||||
AsyncAttachments: asyncAttach,
|
||||
}, attachments...)
|
||||
|
||||
if err != nil || asyncAttach != proton.AttachmentTypeAsync {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if asyncAttach == proton.AttachmentTypeAsync && res.Token == nil {
|
||||
return "", errors.New("no token returns for AsyncAttachments")
|
||||
}
|
||||
|
||||
return *res.Token, nil
|
||||
}
|
||||
|
||||
func (bridge *Bridge) appendComment(ctx context.Context, token string, att proton.ReportBugAttachment) error {
|
||||
var attachments []proton.ReportBugAttachment
|
||||
attachments = append(attachments, att)
|
||||
return bridge.api.ReportBugAttachement(ctx, proton.ReportBugAttachmentReq{
|
||||
Product: proton.ClientTypeEmail,
|
||||
Body: "Comment adding attachment: " + att.Filename,
|
||||
Token: token,
|
||||
}, attachments...)
|
||||
}
|
||||
|
||||
@ -22,7 +22,7 @@ import (
|
||||
)
|
||||
|
||||
func (bridge *Bridge) ReportBugClicked() {
|
||||
safe.Lock(func() {
|
||||
safe.RLock(func() {
|
||||
for _, user := range bridge.users {
|
||||
user.ReportBugClicked()
|
||||
}
|
||||
@ -30,7 +30,7 @@ func (bridge *Bridge) ReportBugClicked() {
|
||||
}
|
||||
|
||||
func (bridge *Bridge) AutoconfigUsed(client string) {
|
||||
safe.Lock(func() {
|
||||
safe.RLock(func() {
|
||||
for _, user := range bridge.users {
|
||||
user.AutoconfigUsed(client)
|
||||
}
|
||||
@ -38,7 +38,7 @@ func (bridge *Bridge) AutoconfigUsed(client string) {
|
||||
}
|
||||
|
||||
func (bridge *Bridge) KBArticleOpened(article string) {
|
||||
safe.Lock(func() {
|
||||
safe.RLock(func() {
|
||||
for _, user := range bridge.users {
|
||||
user.KBArticleOpened(article)
|
||||
}
|
||||
|
||||
@ -19,6 +19,7 @@ package bridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/clientconfig"
|
||||
@ -30,8 +31,8 @@ import (
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// ConfigureAppleMail configures apple mail for the given userID and address.
|
||||
// If configuring apple mail for Catalina or newer, it ensures Bridge is using SSL.
|
||||
// ConfigureAppleMail configures Apple Mail for the given userID and address.
|
||||
// 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 {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"userID": userID,
|
||||
@ -44,16 +45,28 @@ func (bridge *Bridge) ConfigureAppleMail(ctx context.Context, userID, address st
|
||||
return ErrNoSuchUser
|
||||
}
|
||||
|
||||
if address == "" {
|
||||
address = user.Emails()[0]
|
||||
emails := user.Emails()
|
||||
displayNames := user.DisplayNames()
|
||||
if (len(emails) == 0) || (len(displayNames) == 0) {
|
||||
return errors.New("could not retrieve user address info")
|
||||
}
|
||||
|
||||
username := address
|
||||
addresses := address
|
||||
if address == "" {
|
||||
address = emails[0]
|
||||
}
|
||||
|
||||
var username, displayName, addresses string
|
||||
if user.GetAddressMode() == vault.CombinedMode {
|
||||
username = user.Emails()[0]
|
||||
addresses = strings.Join(user.Emails(), ",")
|
||||
username = address
|
||||
displayName = displayNames[username]
|
||||
addresses = strings.Join(emails, ",")
|
||||
} else {
|
||||
username = address
|
||||
addresses = address
|
||||
displayName = displayNames[address]
|
||||
if len(displayName) == 0 {
|
||||
displayName = address
|
||||
}
|
||||
}
|
||||
|
||||
if useragent.IsCatalinaOrNewer() && !bridge.vault.GetSMTPSSL() {
|
||||
@ -69,6 +82,7 @@ func (bridge *Bridge) ConfigureAppleMail(ctx context.Context, userID, address st
|
||||
bridge.vault.GetIMAPSSL(),
|
||||
bridge.vault.GetSMTPSSL(),
|
||||
username,
|
||||
displayName,
|
||||
addresses,
|
||||
user.BridgePass(),
|
||||
)
|
||||
|
||||
@ -20,18 +20,100 @@ package bridge
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
"github.com/ProtonMail/gluon/reporter"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/telemetry"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
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 {
|
||||
var flag = true
|
||||
if bridge.GetTelemetryDisabled() {
|
||||
@ -80,49 +162,6 @@ func (bridge *Bridge) SetLastHeartbeatSent(timestamp time.Time) error {
|
||||
return bridge.vault.SetLastHeartbeatSent(timestamp)
|
||||
}
|
||||
|
||||
func (bridge *Bridge) StartHeartbeat(manager telemetry.HeartbeatManager) {
|
||||
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)
|
||||
func (bridge *Bridge) GetHeartbeatPeriodicInterval() time.Duration {
|
||||
return HeartbeatCheckInterval
|
||||
}
|
||||
|
||||
24
internal/bridge/keychain.go
Normal file
@ -0,0 +1,24 @@
|
||||
// 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())
|
||||
}
|
||||
@ -7,6 +7,7 @@ import (
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge/mocks"
|
||||
@ -51,6 +52,7 @@ func NewMocks(tb testing.TB, version, minAuto *semver.Version) *Mocks {
|
||||
|
||||
// this is called at start of heartbeat process.
|
||||
mocks.Heartbeat.EXPECT().IsTelemetryAvailable(gomock.Any()).AnyTimes()
|
||||
mocks.Heartbeat.EXPECT().GetHeartbeatPeriodicInterval().AnyTimes().Return(500 * time.Millisecond)
|
||||
|
||||
return mocks
|
||||
}
|
||||
@ -154,3 +156,7 @@ func (testUpdater *TestUpdater) GetVersionInfo(_ context.Context, _ updater.Down
|
||||
func (testUpdater *TestUpdater) InstallUpdate(_ context.Context, _ updater.Downloader, _ updater.VersionInfo) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (testUpdater *TestUpdater) RemoveOldUpdates() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -36,6 +36,20 @@ func (m *MockHeartbeatManager) EXPECT() *MockHeartbeatManagerMockRecorder {
|
||||
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.
|
||||
func (m *MockHeartbeatManager) GetLastHeartbeatSent() time.Time {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
@ -33,6 +33,7 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
smtpservice "github.com/ProtonMail/proton-bridge/v3/internal/services/smtp"
|
||||
"github.com/emersion/go-imap"
|
||||
"github.com/emersion/go-sasl"
|
||||
"github.com/emersion/go-smtp"
|
||||
@ -336,6 +337,9 @@ func TestBridge_SendInvite(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;
|
||||
boundary="Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84"
|
||||
Subject: A new message
|
||||
@ -343,7 +347,7 @@ Date: Mon, 13 Mar 2023 16:06:16 +0100
|
||||
|
||||
|
||||
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
|
||||
Content-Disposition: inline;
|
||||
Content-Disposition: attachment;
|
||||
filename=Cat_August_2010-4.jpeg
|
||||
Content-Type: image/jpeg;
|
||||
name="Cat_August_2010-4.jpeg"
|
||||
@ -360,7 +364,7 @@ Subject: A new message Part2
|
||||
Date: Mon, 13 Mar 2023 16:06:16 +0100
|
||||
|
||||
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
|
||||
Content-Disposition: inline;
|
||||
Content-Disposition: attachment;
|
||||
filename=Cat_August_2010-4.jpeg
|
||||
Content-Type: image/jpeg;
|
||||
name="Cat_August_2010-4.jpeg"
|
||||
@ -520,3 +524,234 @@ 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())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@ -261,9 +261,12 @@ func (bridge *Bridge) SetTelemetryDisabled(isDisabled bool) error {
|
||||
return err
|
||||
}
|
||||
// If telemetry is re-enabled locally, try to send the heartbeat.
|
||||
if !isDisabled {
|
||||
defer bridge.goHeartbeat()
|
||||
if isDisabled {
|
||||
bridge.heartbeat.stop()
|
||||
} else {
|
||||
bridge.heartbeat.start()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@ -37,6 +37,7 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice"
|
||||
"github.com/bradenaw/juniper/iterator"
|
||||
"github.com/bradenaw/juniper/stream"
|
||||
"github.com/bradenaw/juniper/xslices"
|
||||
@ -579,6 +580,67 @@ func TestBridge_MessageCreateDuringSync(t *testing.T) {
|
||||
}, server.WithTLS(false))
|
||||
}
|
||||
|
||||
func TestBridge_CorruptedVaultClearsPreviousIMAPSyncState(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||
userID, addrID, err := s.CreateUser("imap", password)
|
||||
require.NoError(t, err)
|
||||
|
||||
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
|
||||
require.NoError(t, err)
|
||||
|
||||
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
|
||||
createNumMessages(ctx, t, c, addrID, labelID, 100)
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
||||
defer done()
|
||||
|
||||
var err error
|
||||
|
||||
userID, err = bridge.LoginFull(context.Background(), "imap", password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Wait for sync to finish
|
||||
require.Equal(t, userID, (<-syncCh).UserID)
|
||||
})
|
||||
|
||||
settingsPath, err := locator.ProvideSettingsPath()
|
||||
require.NoError(t, err)
|
||||
|
||||
syncConfigPath, err := locator.ProvideIMAPSyncConfigPath()
|
||||
require.NoError(t, err)
|
||||
|
||||
syncStatePath := imapservice.GetSyncConfigPath(syncConfigPath, userID)
|
||||
// Check sync state is complete
|
||||
{
|
||||
state, err := imapservice.NewSyncState(syncStatePath)
|
||||
require.NoError(t, err)
|
||||
syncStatus, err := state.GetSyncStatus(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.True(t, syncStatus.IsComplete())
|
||||
}
|
||||
|
||||
// corrupt the vault
|
||||
require.NoError(t, os.WriteFile(filepath.Join(settingsPath, "vault.enc"), []byte("Trash!"), 0o600))
|
||||
|
||||
// Bridge starts but can't find the gluon database dir; there should be no error.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
_, err := bridge.LoginFull(context.Background(), "imap", password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
// Check sync state is reset.
|
||||
{
|
||||
state, err := imapservice.NewSyncState(syncStatePath)
|
||||
require.NoError(t, err)
|
||||
syncStatus, err := state.GetSyncStatus(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.False(t, syncStatus.IsComplete())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func withClient(ctx context.Context, t *testing.T, s *server.Server, username string, password []byte, fn func(context.Context, *proton.Client)) { //nolint:unparam
|
||||
m := proton.New(
|
||||
proton.WithHostURL(s.GetHostURL()),
|
||||
|
||||
@ -53,4 +53,5 @@ type Autostarter interface {
|
||||
type Updater interface {
|
||||
GetVersionInfo(context.Context, updater.Downloader, updater.Channel) (updater.VersionInfo, error)
|
||||
InstallUpdate(context.Context, updater.Downloader, updater.VersionInfo) error
|
||||
RemoveOldUpdates() error
|
||||
}
|
||||
|
||||
@ -139,3 +139,9 @@ func (bridge *Bridge) installUpdate(ctx context.Context, job installJob) {
|
||||
}
|
||||
}, bridge.newVersionLock)
|
||||
}
|
||||
|
||||
func (bridge *Bridge) RemoveOldUpdates() {
|
||||
if err := bridge.updater.RemoveOldUpdates(); err != nil {
|
||||
logrus.WithError(err).Error("Remove old updates fails")
|
||||
}
|
||||
}
|
||||
|
||||
@ -46,6 +46,8 @@ const (
|
||||
Connected
|
||||
)
|
||||
|
||||
var ErrFailedToUnlock = errors.New("failed to unlock user keys")
|
||||
|
||||
type UserInfo struct {
|
||||
// UserID is the user's API ID.
|
||||
UserID string
|
||||
@ -66,10 +68,10 @@ type UserInfo struct {
|
||||
BridgePass []byte
|
||||
|
||||
// UsedSpace is the amount of space used by the user.
|
||||
UsedSpace int
|
||||
UsedSpace uint64
|
||||
|
||||
// MaxSpace is the total amount of space available to the user.
|
||||
MaxSpace int
|
||||
MaxSpace uint64
|
||||
}
|
||||
|
||||
// GetUserIDs returns the IDs of all known users (authorized or not).
|
||||
@ -157,11 +159,15 @@ func (bridge *Bridge) LoginUser(
|
||||
func() (string, error) {
|
||||
return bridge.loginUser(ctx, client, auth.UID, auth.RefreshToken, keyPass)
|
||||
},
|
||||
func() error {
|
||||
return client.AuthDelete(ctx)
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
// Failure to unlock will allow retries, so we do not delete auth.
|
||||
if !errors.Is(err, ErrFailedToUnlock) {
|
||||
if deleteErr := client.AuthDelete(ctx); deleteErr != nil {
|
||||
logrus.WithError(deleteErr).Error("Failed to delete auth")
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("failed to login user: %w", err)
|
||||
}
|
||||
|
||||
@ -217,7 +223,16 @@ func (bridge *Bridge) LoginFull(
|
||||
keyPass = password
|
||||
}
|
||||
|
||||
return bridge.LoginUser(ctx, client, auth, keyPass)
|
||||
userID, err := bridge.LoginUser(ctx, client, auth, keyPass)
|
||||
if err != nil {
|
||||
if deleteErr := client.AuthDelete(ctx); deleteErr != nil {
|
||||
logrus.WithError(err).Error("Failed to delete auth")
|
||||
}
|
||||
|
||||
return "", err
|
||||
}
|
||||
|
||||
return userID, nil
|
||||
}
|
||||
|
||||
// LogoutUser logs out the given user.
|
||||
@ -314,7 +329,7 @@ func (bridge *Bridge) SetAddressMode(ctx context.Context, userID string, mode va
|
||||
func (bridge *Bridge) SendBadEventUserFeedback(_ context.Context, userID string, doResync bool) error {
|
||||
logrus.WithField("userID", userID).WithField("doResync", doResync).Info("Passing bad event feedback to user")
|
||||
|
||||
return safe.LockRet(func() error {
|
||||
return safe.RLockRet(func() error {
|
||||
ctx := context.Background()
|
||||
|
||||
user, ok := bridge.users[userID]
|
||||
@ -374,9 +389,9 @@ func (bridge *Bridge) loginUser(ctx context.Context, client *proton.Client, auth
|
||||
}
|
||||
|
||||
if userKR, err := apiUser.Keys.Unlock(saltedKeyPass, nil); err != nil {
|
||||
return "", fmt.Errorf("failed to unlock user keys: %w", err)
|
||||
return "", fmt.Errorf("%w: %w", ErrFailedToUnlock, err)
|
||||
} else if userKR.CountDecryptionEntities() == 0 {
|
||||
return "", fmt.Errorf("failed to unlock user keys")
|
||||
return "", ErrFailedToUnlock
|
||||
}
|
||||
|
||||
if err := bridge.addUser(ctx, client, apiUser, authUID, authRef, saltedKeyPass, true); err != nil {
|
||||
@ -479,7 +494,7 @@ func (bridge *Bridge) addUser(
|
||||
return fmt.Errorf("failed to add vault user: %w", err)
|
||||
}
|
||||
|
||||
if err := bridge.addUserWithVault(ctx, client, apiUser, vaultUser); err != nil {
|
||||
if err := bridge.addUserWithVault(ctx, client, apiUser, vaultUser, isNew); err != nil {
|
||||
if _, ok := err.(*resty.ResponseError); ok || isLogin {
|
||||
logrus.WithError(err).Error("Failed to add user, clearing its secrets from vault")
|
||||
|
||||
@ -514,6 +529,7 @@ func (bridge *Bridge) addUserWithVault(
|
||||
client *proton.Client,
|
||||
apiUser proton.User,
|
||||
vault *vault.User,
|
||||
isNew bool,
|
||||
) error {
|
||||
statsPath, err := bridge.locator.ProvideStatsPath()
|
||||
if err != nil {
|
||||
@ -541,6 +557,7 @@ func (bridge *Bridge) addUserWithVault(
|
||||
&bridgeEventSubscription{b: bridge},
|
||||
bridge.syncService,
|
||||
syncSettingsPath,
|
||||
isNew,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create user: %w", err)
|
||||
@ -577,7 +594,7 @@ func (bridge *Bridge) addUserWithVault(
|
||||
}, bridge.usersLock)
|
||||
|
||||
// As we need at least one user to send heartbeat, try to send it.
|
||||
defer bridge.goHeartbeat()
|
||||
bridge.heartbeat.start()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -49,7 +49,7 @@ func (bridge *Bridge) handleUserDeauth(ctx context.Context, user *user.User) {
|
||||
}
|
||||
|
||||
func (bridge *Bridge) handleUserBadEvent(ctx context.Context, user *user.User, event events.UserBadEvent) {
|
||||
safe.Lock(func() {
|
||||
safe.RLock(func() {
|
||||
if rerr := bridge.reporter.ReportMessageWithContext("Failed to handle event", reporter.Context{
|
||||
"user_id": user.ID(),
|
||||
"old_event_id": event.OldEventID,
|
||||
|
||||
@ -23,71 +23,200 @@ package certs
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <Security/Security.h>
|
||||
|
||||
// Memory management rules:
|
||||
// Foundation object (Objective-C prefixed with `NS`) get ARC (Automatic Reference Counting), and do not need to be released manually.
|
||||
// Core Foundation objects (C), prefixed with need to be released manually using CFRelease() unless:
|
||||
// - They're obtained using a CF method containing the word Get (a.k.a. the Get Rule).
|
||||
// - They're obtained using toll-free bridging from a Foundation Object (using the __bridge keyword).
|
||||
|
||||
int installTrustedCert(char const *bytes, unsigned long long length) {
|
||||
if (length == 0) {
|
||||
return errSecInvalidData;
|
||||
}
|
||||
|
||||
NSData *der = [NSData dataWithBytes:bytes length:length];
|
||||
|
||||
// Step 1. Import the certificate in the keychain.
|
||||
SecCertificateRef cert = SecCertificateCreateWithData(NULL, (CFDataRef) der);
|
||||
NSDictionary* addQuery = @{
|
||||
(id)kSecValueRef: (__bridge id) cert,
|
||||
(id)kSecClass: (id)kSecClassCertificate,
|
||||
};
|
||||
|
||||
OSStatus status = SecItemAdd((__bridge CFDictionaryRef) addQuery, NULL);
|
||||
if ((errSecSuccess != status) && (errSecDuplicateItem != status)) {
|
||||
CFRelease(cert);
|
||||
return status;
|
||||
}
|
||||
|
||||
// Step 2. Set the trust for the certificate.
|
||||
SecPolicyRef policy = SecPolicyCreateSSL(true, NULL); // we limit our trust to SSL
|
||||
NSDictionary *trustSettings = @{
|
||||
(id)kSecTrustSettingsResult: [NSNumber numberWithInt:kSecTrustSettingsResultTrustRoot],
|
||||
(id)kSecTrustSettingsPolicy: (__bridge id) policy,
|
||||
};
|
||||
status = SecTrustSettingsSetTrustSettings(cert, kSecTrustSettingsDomainUser, (__bridge CFTypeRef)(trustSettings));
|
||||
CFRelease(policy);
|
||||
CFRelease(cert);
|
||||
|
||||
return status;
|
||||
//****************************************************************************************************************************************************
|
||||
/// \brief Create a certificate object from DER-encoded data.
|
||||
///
|
||||
/// \return The certifcation. The caller is responsible for releasing the object using CFRelease.
|
||||
/// \return NULL if data is not a valid DER-encoded certificate.
|
||||
//****************************************************************************************************************************************************
|
||||
SecCertificateRef certFromData(char const* data, uint64_t length) {
|
||||
NSData *der = [NSData dataWithBytes:data length:length];
|
||||
return SecCertificateCreateWithData(NULL, (__bridge CFDataRef)der);
|
||||
}
|
||||
|
||||
|
||||
int removeTrustedCert(char const *bytes, unsigned long long length) {
|
||||
if (0 == length) {
|
||||
return errSecInvalidData;
|
||||
}
|
||||
//****************************************************************************************************************************************************
|
||||
/// \brief Check if a certificate is in the user's keychain.
|
||||
///
|
||||
/// \param[in] cert The certificate.
|
||||
/// \return true iff the certificate is in the user's keychain.
|
||||
//****************************************************************************************************************************************************
|
||||
bool _isCertificateInKeychain(SecCertificateRef const cert) {
|
||||
NSDictionary *attrs = @{
|
||||
(id)kSecMatchItemList: @[(__bridge id)cert],
|
||||
(id)kSecClass: (id)kSecClassCertificate,
|
||||
(id)kSecReturnData: @YES
|
||||
};
|
||||
return errSecSuccess == SecItemCopyMatching((__bridge CFDictionaryRef)attrs, NULL);
|
||||
}
|
||||
|
||||
NSData *der = [NSData dataWithBytes: bytes length: length];
|
||||
SecCertificateRef cert = SecCertificateCreateWithData(NULL, (CFDataRef) der);
|
||||
//****************************************************************************************************************************************************
|
||||
/// \brief Check if a certificate is in the user's keychain.
|
||||
///
|
||||
/// \param[in] certData The certificate data in DER encoded format.
|
||||
/// \param[in] certSize The size of the certData in bytes.
|
||||
/// \return true iff the certificate is in the user's keychain.
|
||||
//****************************************************************************************************************************************************
|
||||
bool isCertificateInKeychain(char const* certData, uint64_t certSize) {
|
||||
return _isCertificateInKeychain(certFromData(certData, certSize));
|
||||
}
|
||||
|
||||
// Step 1. Unset the trust for the certificate.
|
||||
SecPolicyRef policy = SecPolicyCreateSSL(true, NULL);
|
||||
NSDictionary * trustSettings = @{
|
||||
(id)kSecTrustSettingsResult: [NSNumber numberWithInt:kSecTrustSettingsResultUnspecified],
|
||||
(id)kSecTrustSettingsPolicy: (__bridge id) policy,
|
||||
};
|
||||
OSStatus status = SecTrustSettingsSetTrustSettings(cert, kSecTrustSettingsDomainUser, (__bridge CFTypeRef)(trustSettings));
|
||||
CFRelease(policy);
|
||||
if (errSecSuccess != status) {
|
||||
CFRelease(cert);
|
||||
return status;
|
||||
}
|
||||
|
||||
// Step 2. Remove the certificate from the keychain.
|
||||
NSDictionary *query = @{ (id)kSecClass: (id)kSecClassCertificate,
|
||||
(id)kSecMatchItemList: @[(__bridge id)cert],
|
||||
(id)kSecMatchLimit: (id)kSecMatchLimitOne,
|
||||
};
|
||||
status = SecItemDelete((__bridge CFDictionaryRef) query);
|
||||
//****************************************************************************************************************************************************
|
||||
/// \brief Add a certificate to the user's keychain.
|
||||
///
|
||||
/// \param[in] cert The certificate.
|
||||
/// \return The status for the operation.
|
||||
//****************************************************************************************************************************************************
|
||||
OSStatus _addCertificateToKeychain(SecCertificateRef const cert) {
|
||||
NSDictionary* addQuery = @{
|
||||
(id)kSecValueRef: (__bridge id) cert,
|
||||
(id)kSecClass: (id)kSecClassCertificate,
|
||||
};
|
||||
return SecItemAdd((__bridge CFDictionaryRef) addQuery, NULL);
|
||||
}
|
||||
|
||||
CFRelease(cert);
|
||||
return status;
|
||||
//****************************************************************************************************************************************************
|
||||
/// \brief Add a certificate to the user's keychain.
|
||||
///
|
||||
/// \param[in] certData The certificate data in DER encoded format.
|
||||
/// \param[in] certSize The size of the certData in bytes.
|
||||
/// \return The status for the operation.
|
||||
//****************************************************************************************************************************************************
|
||||
OSStatus addCertificateToKeychain(char const* certData, uint64_t certSize) {
|
||||
return _addCertificateToKeychain(certFromData(certData, certSize));
|
||||
}
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \brief Add a certificate to the user's keychain.
|
||||
///
|
||||
/// \param[in] cert The certificate.
|
||||
/// \return The status for the operation.
|
||||
//****************************************************************************************************************************************************
|
||||
OSStatus _removeCertificateFromKeychain(SecCertificateRef const cert) {
|
||||
NSDictionary *query = @{ (id)kSecClass: (id)kSecClassCertificate,
|
||||
(id)kSecMatchItemList: @[(__bridge id)cert],
|
||||
(id)kSecMatchLimit: (id)kSecMatchLimitOne,
|
||||
};
|
||||
return SecItemDelete((__bridge CFDictionaryRef) query);
|
||||
}
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \brief Add a certificate to the user's keychain.
|
||||
///
|
||||
/// \param[in] certData The certificate data in DER encoded format.
|
||||
/// \param[in] certSize The size of the certData in bytes.
|
||||
/// \return The status for the operation.
|
||||
//****************************************************************************************************************************************************
|
||||
OSStatus removeCertificateFromKeychain(char const* certData, uint64_t certSize) {
|
||||
return _removeCertificateFromKeychain(certFromData(certData, certSize));
|
||||
}
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \brief Check if a certificate is trusted in the user's keychain.
|
||||
///
|
||||
/// \param[in] cert The certificate.
|
||||
/// \return true iff the certificate is trusted in the user's keychain.
|
||||
//****************************************************************************************************************************************************
|
||||
bool _isCertificateTrusted(SecCertificateRef const cert) {
|
||||
CFArrayRef trustSettings = NULL;
|
||||
OSStatus status = SecTrustSettingsCopyTrustSettings(cert, kSecTrustSettingsDomainUser, &trustSettings);
|
||||
if (status != errSecSuccess) {
|
||||
return false;
|
||||
}
|
||||
CFIndex count = CFArrayGetCount(trustSettings);
|
||||
bool result = false;
|
||||
for (CFIndex index = 0; index < count; ++index) {
|
||||
CFDictionaryRef dict = (CFDictionaryRef)CFArrayGetValueAtIndex(trustSettings, index);
|
||||
if (!dict) {
|
||||
continue;
|
||||
}
|
||||
CFNumberRef num = (CFNumberRef)CFDictionaryGetValue(dict, kSecTrustSettingsResult);
|
||||
int value;
|
||||
if (num && CFNumberGetValue(num, kCFNumberSInt32Type, &value) && (value == kSecTrustSettingsResultTrustRoot)) {
|
||||
result = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
CFRelease(trustSettings);
|
||||
return result;
|
||||
}
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \brief Check if a certificate is trusted in the user's keychain.
|
||||
///
|
||||
/// \param[in] certData The certificate data in DER encoded format.
|
||||
/// \param[in] certSize The size of the certData in bytes.
|
||||
/// \return true iff the certificate is trusted in the user's keychain.
|
||||
//****************************************************************************************************************************************************
|
||||
bool isCertificateTrusted(char const* certData, uint64_t certSize) {
|
||||
return _isCertificateTrusted(certFromData(certData, certSize));
|
||||
}
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \brief Set the trust level for a certificate in the user's keychain. This call will trigger a security prompt.
|
||||
///
|
||||
/// \param[in] cert The certificate.
|
||||
/// \param[in] trustLevel The trust level.
|
||||
/// \return The status for the operation.
|
||||
//****************************************************************************************************************************************************
|
||||
OSStatus _setCertificateTrustLevel(SecCertificateRef const cert, int trustLevel) {
|
||||
SecPolicyRef policy = SecPolicyCreateSSL(true, NULL); // we limit our trust to SSL
|
||||
NSDictionary *trustSettings = @{
|
||||
(id)kSecTrustSettingsResult: [NSNumber numberWithInt:trustLevel],
|
||||
(id)kSecTrustSettingsPolicy: (__bridge id) policy,
|
||||
};
|
||||
OSStatus status = SecTrustSettingsSetTrustSettings(cert, kSecTrustSettingsDomainUser, (__bridge CFTypeRef)(trustSettings));
|
||||
CFRelease(policy);
|
||||
return status;
|
||||
}
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \brief Set a certificate as trusted in the user's keychain. This call will trigger a security prompt.
|
||||
///
|
||||
/// \param[in] cert The certificate.
|
||||
/// \return The status for the operation.
|
||||
//****************************************************************************************************************************************************
|
||||
OSStatus _setCertificateTrusted(SecCertificateRef cert) {
|
||||
return _setCertificateTrustLevel(cert, kSecTrustSettingsResultTrustRoot);
|
||||
}
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \brief Set a certificate as trusted in the user's keychain. This call will trigger a security prompt.
|
||||
///
|
||||
/// \param[in] certData The certificate data in DER encoded format.
|
||||
/// \param[in] certSize The size of the certData in bytes.
|
||||
/// \return The status for the operation.
|
||||
//****************************************************************************************************************************************************
|
||||
OSStatus setCertificateTrusted(char const* certData, uint64_t certSize) {
|
||||
return _setCertificateTrusted(certFromData(certData, certSize));
|
||||
}
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \brief Remove the trust level of a certificate in the user's keychain.
|
||||
///
|
||||
/// \param[in] cert The certificate.
|
||||
/// \return The status for the operation.
|
||||
//****************************************************************************************************************************************************
|
||||
OSStatus _removeCertificateTrust(SecCertificateRef cert) {
|
||||
return _setCertificateTrustLevel(cert, kSecTrustSettingsResultUnspecified);
|
||||
}
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \brief Remove the trust level of a certificate in the user's keychain.
|
||||
///
|
||||
/// \param[in] certData The certificate data in DER encoded format.
|
||||
/// \param[in] certSize The size of the certData in bytes.
|
||||
/// \return The status for the operation.
|
||||
//****************************************************************************************************************************************************
|
||||
OSStatus removeCertificateTrust(char const* certData, uint64_t certSize) {
|
||||
return _removeCertificateTrust(certFromData(certData, certSize));
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
@ -119,6 +248,120 @@ func certPEMToDER(certPEM []byte) ([]byte, error) {
|
||||
return block.Bytes, nil
|
||||
}
|
||||
|
||||
// wrapCGoCertCallReturningBool wrap call to a CGo function returning a bool.
|
||||
// if the certificate is invalid the call will return false.
|
||||
func wrapCGoCertCallReturningBool(certPEM []byte, fn func(*C.char, C.ulonglong) bool) bool {
|
||||
certDER, err := certPEMToDER(certPEM)
|
||||
if err != nil {
|
||||
return false // error are ignored
|
||||
}
|
||||
|
||||
buffer := C.CBytes(certDER)
|
||||
defer C.free(unsafe.Pointer(buffer)) //nolint:unconvert
|
||||
|
||||
return fn((*C.char)(buffer), C.ulonglong(len(certDER)))
|
||||
}
|
||||
|
||||
// wrapCGoCertCallReturningBool wrap call to a CGo function returning an error
|
||||
func wrapCGoCertCallReturningError(certPEM []byte, fn func(*C.char, C.ulonglong) error) error {
|
||||
certDER, err := certPEMToDER(certPEM)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buffer := C.CBytes(certDER)
|
||||
defer C.free(unsafe.Pointer(buffer)) //nolint:unconvert
|
||||
|
||||
return fn((*C.char)(buffer), C.ulonglong(len(certDER)))
|
||||
}
|
||||
|
||||
// isCertInKeychain returns true if the given certificate is stored in the user's keychain.
|
||||
func isCertInKeychain(certPEM []byte) bool {
|
||||
return wrapCGoCertCallReturningBool(certPEM, isCertInKeychainCGo)
|
||||
}
|
||||
|
||||
func isCertInKeychainCGo(buffer *C.char, size C.ulonglong) bool {
|
||||
return bool(C.isCertificateInKeychain(buffer, size))
|
||||
}
|
||||
|
||||
// addCertToKeychain adds a certificate to the user's keychain.
|
||||
// Trying to add a certificate that is already in the keychain will result in an error.
|
||||
func addCertToKeychain(certPEM []byte) error {
|
||||
return wrapCGoCertCallReturningError(certPEM, addCertToKeychainCGo)
|
||||
}
|
||||
|
||||
func addCertToKeychainCGo(buffer *C.char, size C.ulonglong) error {
|
||||
if errCode := C.addCertificateToKeychain(buffer, size); errCode != errSecSuccess {
|
||||
return fmt.Errorf("could not add certificate to keychain (error %v)", errCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// removeCertFromKeychain removes a certificate from the user's keychain.
|
||||
// Trying to remove a certificate that is not in the keychain will result in an error.
|
||||
func removeCertFromKeychain(certPEM []byte) error {
|
||||
return wrapCGoCertCallReturningError(certPEM, removeCertFromKeychainCGo)
|
||||
}
|
||||
|
||||
func removeCertFromKeychainCGo(buffer *C.char, size C.ulonglong) error {
|
||||
if errCode := C.removeCertificateFromKeychain(buffer, size); errCode != errSecSuccess {
|
||||
return fmt.Errorf("could not remove certificate from keychain (error %v)", errCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isCertTrusted check if a certificate is trusted in the user's keychain.
|
||||
func isCertTrusted(certPEM []byte) bool {
|
||||
return wrapCGoCertCallReturningBool(certPEM, isCertTrustedCGo)
|
||||
}
|
||||
|
||||
func isCertTrustedCGo(buffer *C.char, size C.ulonglong) bool {
|
||||
return bool(C.isCertificateTrusted(buffer, size))
|
||||
}
|
||||
|
||||
// setCertTrusted sets a certificate as trusted in the user's keychain.
|
||||
// This function will trigger a security prompt from the system.
|
||||
func setCertTrusted(certPEM []byte) error {
|
||||
return wrapCGoCertCallReturningError(certPEM, setCertTrustedCGo)
|
||||
}
|
||||
|
||||
func setCertTrustedCGo(buffer *C.char, size C.ulonglong) error {
|
||||
errCode := C.setCertificateTrusted(buffer, size)
|
||||
switch errCode {
|
||||
case errSecSuccess:
|
||||
return nil
|
||||
case errAuthorizationCanceled:
|
||||
return ErrUserCanceledCertificateInstall
|
||||
default:
|
||||
return fmt.Errorf("could not set certificate trust in keychain (error %v)", errCode)
|
||||
}
|
||||
}
|
||||
|
||||
// removeCertTrust remove the trust level of the certificated from the user's keychain.
|
||||
// This function will trigger a security prompt from the system.
|
||||
func removeCertTrust(certPEM []byte) error {
|
||||
return wrapCGoCertCallReturningError(certPEM, removeCertTrustCGo)
|
||||
}
|
||||
|
||||
func removeCertTrustCGo(buffer *C.char, size C.ulonglong) error {
|
||||
errCode := C.removeCertificateTrust(buffer, size)
|
||||
switch errCode {
|
||||
case errSecSuccess:
|
||||
return nil
|
||||
case errAuthorizationCanceled:
|
||||
return ErrUserCanceledCertificateInstall
|
||||
default:
|
||||
return fmt.Errorf("could not set certificate trust in keychain (error %v)", errCode)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
certDER, err := certPEMToDER(certPEM)
|
||||
if err != nil {
|
||||
@ -127,18 +370,24 @@ func installCert(certPEM []byte) error {
|
||||
|
||||
p := C.CBytes(certDER)
|
||||
defer C.free(unsafe.Pointer(p)) //nolint:unconvert
|
||||
buffer := (*C.char)(p)
|
||||
size := C.ulonglong(len(certDER))
|
||||
|
||||
errCode := C.installTrustedCert((*C.char)(p), (C.ulonglong)(len(certDER)))
|
||||
switch errCode {
|
||||
case errSecSuccess:
|
||||
return nil
|
||||
case errAuthorizationCanceled:
|
||||
return fmt.Errorf("the user cancelled the authorization dialog")
|
||||
default:
|
||||
return fmt.Errorf("could not install certification into keychain (error %v)", errCode)
|
||||
if !isCertInKeychainCGo(buffer, size) {
|
||||
if err := addCertToKeychainCGo(buffer, size); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !isCertTrustedCGo(buffer, size) {
|
||||
return setCertTrustedCGo(buffer, size)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// uninstallCert uninstalls a certificate in the keychain. The certificate trust is removed and the certificated is deleted from the keychain.
|
||||
// This function will trigger a security prompt from the system, unless the certificate is not trusted in the user keychain.
|
||||
func uninstallCert(certPEM []byte) error {
|
||||
certDER, err := certPEMToDER(certPEM)
|
||||
if err != nil {
|
||||
@ -147,10 +396,32 @@ func uninstallCert(certPEM []byte) error {
|
||||
|
||||
p := C.CBytes(certDER)
|
||||
defer C.free(unsafe.Pointer(p)) //nolint:unconvert
|
||||
buffer := (*C.char)(p)
|
||||
size := C.ulonglong(len(certDER))
|
||||
|
||||
if errCode := C.removeTrustedCert((*C.char)(p), (C.ulonglong)(len(certDER))); errCode != 0 {
|
||||
return fmt.Errorf("could not install certificate from keychain (error %v)", errCode)
|
||||
if isCertTrustedCGo(buffer, size) {
|
||||
if err := removeCertTrustCGo(buffer, size); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if isCertInKeychainCGo(buffer, size) {
|
||||
return removeCertFromKeychainCGo(buffer, size)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isCertInstalled(certPEM []byte) bool {
|
||||
certDER, err := certPEMToDER(certPEM)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
p := C.CBytes(certDER)
|
||||
defer C.free(unsafe.Pointer(p)) //nolint:unconvert
|
||||
buffer := (*C.char)(p)
|
||||
size := C.ulonglong(len(certDER))
|
||||
|
||||
return isCertInKeychainCGo(buffer, size) && isCertTrustedCGo(buffer, size)
|
||||
}
|
||||
|
||||
@ -25,20 +25,74 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// This test implies human interactions to enter password and is disabled by default.
|
||||
func _TestTrustedCertsDarwin(t *testing.T) { //nolint:unused
|
||||
func TestCertInKeychain(t *testing.T) {
|
||||
// no trust settings change is performed, so this test will not trigger an OS security prompt.
|
||||
certPEM := generatePEMCertificate(t)
|
||||
require.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()
|
||||
require.NoError(t, err)
|
||||
|
||||
certPEM, _, err := GenerateCert(template)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Error(t, installCert([]byte{0})) // Cannot install an invalid cert.
|
||||
require.Error(t, uninstallCert(certPEM)) // Cannot uninstall a cert that is not installed.
|
||||
require.NoError(t, installCert(certPEM)) // Can install a valid cert.
|
||||
require.NoError(t, installCert(certPEM)) // Can install an already installed cert.
|
||||
require.NoError(t, uninstallCert(certPEM)) // Can uninstall an installed cert.
|
||||
require.Error(t, uninstallCert(certPEM)) // Cannot uninstall an already uninstalled cert.
|
||||
require.NoError(t, installCert(certPEM)) // Can reinstall an uninstalled cert.
|
||||
require.NoError(t, uninstallCert(certPEM)) // Can uninstall a reinstalled cert.
|
||||
return certPEM
|
||||
}
|
||||
|
||||
@ -17,6 +17,10 @@
|
||||
|
||||
package certs
|
||||
|
||||
func osSupportCertInstall() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func installCert([]byte) error {
|
||||
return nil // Linux doesn't have a root cert store.
|
||||
}
|
||||
@ -24,3 +28,7 @@ func installCert([]byte) error {
|
||||
func uninstallCert([]byte) error {
|
||||
return nil // Linux doesn't have a root cert store.
|
||||
}
|
||||
|
||||
func isCertInstalled([]byte) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
@ -17,6 +17,10 @@
|
||||
|
||||
package certs
|
||||
|
||||
func osSupportCertInstall() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func installCert([]byte) error {
|
||||
return nil // NOTE(GODT-986): Install certs to root cert store?
|
||||
}
|
||||
@ -24,3 +28,7 @@ func installCert([]byte) error {
|
||||
func uninstallCert([]byte) error {
|
||||
return nil // NOTE(GODT-986): Uninstall certs from root cert store?
|
||||
}
|
||||
|
||||
func isCertInstalled([]byte) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
@ -17,16 +17,66 @@
|
||||
|
||||
package certs
|
||||
|
||||
type Installer struct{}
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUserCanceledCertificateInstall = errors.New("the user cancelled the authorization dialog")
|
||||
)
|
||||
|
||||
type Installer struct {
|
||||
log *logrus.Entry
|
||||
}
|
||||
|
||||
func NewInstaller() *Installer {
|
||||
return &Installer{}
|
||||
return &Installer{
|
||||
log: logrus.WithField("pkg", "certs"),
|
||||
}
|
||||
}
|
||||
|
||||
func (installer *Installer) OSSupportCertInstall() bool {
|
||||
return osSupportCertInstall()
|
||||
}
|
||||
|
||||
func (installer *Installer) InstallCert(certPEM []byte) error {
|
||||
return installCert(certPEM)
|
||||
installer.log.Info("Installing the Bridge TLS certificate in the OS keychain")
|
||||
|
||||
if err := installCert(certPEM); err != nil {
|
||||
installer.log.WithError(err).Error("The Bridge TLS certificate could not be installed in the OS keychain")
|
||||
return err
|
||||
}
|
||||
|
||||
installer.log.Info("The Bridge TLS certificate was successfully installed in the OS keychain")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (installer *Installer) UninstallCert(certPEM []byte) error {
|
||||
return uninstallCert(certPEM)
|
||||
installer.log.Info("Uninstalling the Bridge TLS certificate from the OS keychain")
|
||||
|
||||
if err := uninstallCert(certPEM); err != nil {
|
||||
installer.log.WithError(err).Error("The Bridge TLS certificate could not be uninstalled from the OS keychain")
|
||||
return err
|
||||
}
|
||||
|
||||
installer.log.Info("The Bridge TLS certificate was successfully uninstalled from the OS keychain")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (installer *Installer) IsCertInstalled(certPEM []byte) bool {
|
||||
return isCertInstalled(certPEM)
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,10 +39,10 @@ func (c *AppleMail) Configure(
|
||||
hostname string,
|
||||
imapPort, smtpPort int,
|
||||
imapSSL, smtpSSL bool,
|
||||
username, addresses string,
|
||||
username, displayName, addresses string,
|
||||
password []byte,
|
||||
) error {
|
||||
mc := prepareMobileConfig(hostname, imapPort, smtpPort, imapSSL, smtpSSL, username, addresses, password)
|
||||
mc := prepareMobileConfig(hostname, imapPort, smtpPort, imapSSL, smtpSSL, username, displayName, addresses, password)
|
||||
|
||||
confPath, err := saveConfigTemporarily(mc)
|
||||
if err != nil {
|
||||
@ -66,13 +66,13 @@ func prepareMobileConfig(
|
||||
hostname string,
|
||||
imapPort, smtpPort int,
|
||||
imapSSL, smtpSSL bool,
|
||||
username, addresses string,
|
||||
username, displayName, addresses string,
|
||||
password []byte,
|
||||
) *mobileconfig.Config {
|
||||
return &mobileconfig.Config{
|
||||
DisplayName: username,
|
||||
EmailAddress: addresses,
|
||||
AccountName: username,
|
||||
AccountName: displayName,
|
||||
AccountDescription: username,
|
||||
Identifier: "protonmail " + username + strconv.FormatInt(time.Now().Unix(), 10),
|
||||
IMAP: &mobileconfig.IMAP{
|
||||
|
||||
@ -95,6 +95,13 @@ func (status *ConfigurationStatus) IsPending() bool {
|
||||
return !status.Data.DataV1.PendingSince.IsZero()
|
||||
}
|
||||
|
||||
func (status *ConfigurationStatus) isPendingSinceMin() int {
|
||||
if min := int(time.Since(status.Data.DataV1.PendingSince).Minutes()); min > 0 {
|
||||
return min
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (status *ConfigurationStatus) IsFromFailure() bool {
|
||||
status.DataLock.RLock()
|
||||
defer status.DataLock.RUnlock()
|
||||
|
||||
@ -19,7 +19,6 @@ package configstatus
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ConfigAbortValues struct {
|
||||
@ -41,17 +40,20 @@ type ConfigAbortData struct {
|
||||
|
||||
type ConfigAbortBuilder struct{}
|
||||
|
||||
func (*ConfigAbortBuilder) New(data *ConfigurationStatusData) ConfigAbortData {
|
||||
func (*ConfigAbortBuilder) New(config *ConfigurationStatus) ConfigAbortData {
|
||||
config.DataLock.RLock()
|
||||
defer config.DataLock.RUnlock()
|
||||
|
||||
return ConfigAbortData{
|
||||
MeasurementGroup: "bridge.any.configuration",
|
||||
Event: "bridge_config_abort",
|
||||
Values: ConfigSuccessValues{
|
||||
Duration: int(time.Since(data.DataV1.PendingSince).Minutes()),
|
||||
Duration: config.isPendingSinceMin(),
|
||||
},
|
||||
Dimensions: ConfigSuccessDimensions{
|
||||
ReportClick: strconv.FormatBool(data.DataV1.ReportClick),
|
||||
ReportSent: strconv.FormatBool(data.DataV1.ReportSent),
|
||||
ClickedLink: data.clickedLinkToString(),
|
||||
ReportClick: strconv.FormatBool(config.Data.DataV1.ReportClick),
|
||||
ReportSent: strconv.FormatBool(config.Data.DataV1.ReportSent),
|
||||
ClickedLink: config.Data.clickedLinkToString(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,7 +33,7 @@ func TestConfigurationAbort_default(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
var builder = configstatus.ConfigAbortBuilder{}
|
||||
req := builder.New(config.Data)
|
||||
req := builder.New(config)
|
||||
|
||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||
require.Equal(t, "bridge_config_abort", req.Event)
|
||||
@ -64,7 +64,7 @@ func TestConfigurationAbort_fed(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
var builder = configstatus.ConfigAbortBuilder{}
|
||||
req := builder.New(config.Data)
|
||||
req := builder.New(config)
|
||||
|
||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||
require.Equal(t, "bridge_config_abort", req.Event)
|
||||
|
||||
@ -33,13 +33,16 @@ type ConfigProgressData struct {
|
||||
|
||||
type ConfigProgressBuilder struct{}
|
||||
|
||||
func (*ConfigProgressBuilder) New(data *ConfigurationStatusData) ConfigProgressData {
|
||||
func (*ConfigProgressBuilder) New(config *ConfigurationStatus) ConfigProgressData {
|
||||
config.DataLock.RLock()
|
||||
defer config.DataLock.RUnlock()
|
||||
|
||||
return ConfigProgressData{
|
||||
MeasurementGroup: "bridge.any.configuration",
|
||||
Event: "bridge_config_progress",
|
||||
Values: ConfigProgressValues{
|
||||
NbDay: numberOfDay(time.Now(), data.DataV1.PendingSince),
|
||||
NbDaySinceLast: numberOfDay(time.Now(), data.DataV1.LastProgress),
|
||||
NbDay: numberOfDay(time.Now(), config.Data.DataV1.PendingSince),
|
||||
NbDaySinceLast: numberOfDay(time.Now(), config.Data.DataV1.LastProgress),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,7 +33,7 @@ func TestConfigurationProgress_default(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
var builder = configstatus.ConfigProgressBuilder{}
|
||||
req := builder.New(config.Data)
|
||||
req := builder.New(config)
|
||||
|
||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||
require.Equal(t, "bridge_config_progress", req.Event)
|
||||
@ -62,7 +62,7 @@ func TestConfigurationProgress_fed(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
var builder = configstatus.ConfigProgressBuilder{}
|
||||
req := builder.New(config.Data)
|
||||
req := builder.New(config)
|
||||
|
||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||
require.Equal(t, "bridge_config_progress", req.Event)
|
||||
|
||||
@ -19,7 +19,6 @@ package configstatus
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ConfigRecoveryValues struct {
|
||||
@ -43,19 +42,22 @@ type ConfigRecoveryData struct {
|
||||
|
||||
type ConfigRecoveryBuilder struct{}
|
||||
|
||||
func (*ConfigRecoveryBuilder) New(data *ConfigurationStatusData) ConfigRecoveryData {
|
||||
func (*ConfigRecoveryBuilder) New(config *ConfigurationStatus) ConfigRecoveryData {
|
||||
config.DataLock.RLock()
|
||||
defer config.DataLock.RUnlock()
|
||||
|
||||
return ConfigRecoveryData{
|
||||
MeasurementGroup: "bridge.any.configuration",
|
||||
Event: "bridge_config_recovery",
|
||||
Values: ConfigRecoveryValues{
|
||||
Duration: int(time.Since(data.DataV1.PendingSince).Minutes()),
|
||||
Duration: config.isPendingSinceMin(),
|
||||
},
|
||||
Dimensions: ConfigRecoveryDimensions{
|
||||
Autoconf: data.DataV1.Autoconf,
|
||||
ReportClick: strconv.FormatBool(data.DataV1.ReportClick),
|
||||
ReportSent: strconv.FormatBool(data.DataV1.ReportSent),
|
||||
ClickedLink: data.clickedLinkToString(),
|
||||
FailureDetails: data.DataV1.FailureDetails,
|
||||
Autoconf: config.Data.DataV1.Autoconf,
|
||||
ReportClick: strconv.FormatBool(config.Data.DataV1.ReportClick),
|
||||
ReportSent: strconv.FormatBool(config.Data.DataV1.ReportSent),
|
||||
ClickedLink: config.Data.clickedLinkToString(),
|
||||
FailureDetails: config.Data.DataV1.FailureDetails,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,7 +33,7 @@ func TestConfigurationRecovery_default(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
var builder = configstatus.ConfigRecoveryBuilder{}
|
||||
req := builder.New(config.Data)
|
||||
req := builder.New(config)
|
||||
|
||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||
require.Equal(t, "bridge_config_recovery", req.Event)
|
||||
@ -66,7 +66,7 @@ func TestConfigurationRecovery_fed(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
var builder = configstatus.ConfigRecoveryBuilder{}
|
||||
req := builder.New(config.Data)
|
||||
req := builder.New(config)
|
||||
|
||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||
require.Equal(t, "bridge_config_recovery", req.Event)
|
||||
|
||||
@ -19,7 +19,6 @@ package configstatus
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ConfigSuccessValues struct {
|
||||
@ -42,18 +41,21 @@ type ConfigSuccessData struct {
|
||||
|
||||
type ConfigSuccessBuilder struct{}
|
||||
|
||||
func (*ConfigSuccessBuilder) New(data *ConfigurationStatusData) ConfigSuccessData {
|
||||
func (*ConfigSuccessBuilder) New(config *ConfigurationStatus) ConfigSuccessData {
|
||||
config.DataLock.RLock()
|
||||
defer config.DataLock.RUnlock()
|
||||
|
||||
return ConfigSuccessData{
|
||||
MeasurementGroup: "bridge.any.configuration",
|
||||
Event: "bridge_config_success",
|
||||
Values: ConfigSuccessValues{
|
||||
Duration: int(time.Since(data.DataV1.PendingSince).Minutes()),
|
||||
Duration: config.isPendingSinceMin(),
|
||||
},
|
||||
Dimensions: ConfigSuccessDimensions{
|
||||
Autoconf: data.DataV1.Autoconf,
|
||||
ReportClick: strconv.FormatBool(data.DataV1.ReportClick),
|
||||
ReportSent: strconv.FormatBool(data.DataV1.ReportSent),
|
||||
ClickedLink: data.clickedLinkToString(),
|
||||
Autoconf: config.Data.DataV1.Autoconf,
|
||||
ReportClick: strconv.FormatBool(config.Data.DataV1.ReportClick),
|
||||
ReportSent: strconv.FormatBool(config.Data.DataV1.ReportSent),
|
||||
ClickedLink: config.Data.clickedLinkToString(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,7 +33,7 @@ func TestConfigurationSuccess_default(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
var builder = configstatus.ConfigSuccessBuilder{}
|
||||
req := builder.New(config.Data)
|
||||
req := builder.New(config)
|
||||
|
||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||
require.Equal(t, "bridge_config_success", req.Event)
|
||||
@ -65,7 +65,7 @@ func TestConfigurationSuccess_fed(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
var builder = configstatus.ConfigSuccessBuilder{}
|
||||
req := builder.New(config.Data)
|
||||
req := builder.New(config)
|
||||
|
||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||
require.Equal(t, "bridge_config_success", req.Event)
|
||||
|
||||
@ -175,7 +175,7 @@ type UsedSpaceChanged struct {
|
||||
|
||||
UserID string
|
||||
|
||||
UsedSpace int
|
||||
UsedSpace uint64
|
||||
}
|
||||
|
||||
func (event UsedSpaceChanged) String() string {
|
||||
|
||||
@ -42,6 +42,7 @@ void GRPCQtProxy::connectSignals() {
|
||||
connect(this, &GRPCQtProxy::setIsTelemetryDisabledReceived, &settingsTab, &SettingsTab::setIsTelemetryDisabled);
|
||||
connect(this, &GRPCQtProxy::setColorSchemeNameReceived, &settingsTab, &SettingsTab::setColorSchemeName);
|
||||
connect(this, &GRPCQtProxy::reportBugReceived, &settingsTab, &SettingsTab::setBugReport);
|
||||
connect(this, &GRPCQtProxy::installTLSCertificateReceived, &settingsTab, &SettingsTab::installTLSCertificate);
|
||||
connect(this, &GRPCQtProxy::exportTLSCertificatesReceived, &settingsTab, &SettingsTab::exportTLSCertificates);
|
||||
connect(this, &GRPCQtProxy::setIsStreamingReceived, &settingsTab, &SettingsTab::setIsStreaming);
|
||||
connect(this, &GRPCQtProxy::setClientPlatformReceived, &settingsTab, &SettingsTab::setClientPlatform);
|
||||
@ -119,6 +120,13 @@ void GRPCQtProxy::reportBug(QString const &osType, QString const &osVersion, QSt
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
//
|
||||
//****************************************************************************************************************************************************
|
||||
void GRPCQtProxy::installTLSCertificate() {
|
||||
emit installTLSCertificateReceived();
|
||||
}
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] folderPath The folder path.
|
||||
//****************************************************************************************************************************************************
|
||||
|
||||
@ -45,6 +45,7 @@ public: // member functions.
|
||||
void setColorSchemeName(QString const &name); ///< Forward a SetColorSchemeName call via a Qt Signal
|
||||
void reportBug(QString const &osType, QString const &osVersion, QString const &emailClient, QString const &address,
|
||||
QString const &description, bool includeLogs); ///< Forwards a ReportBug call via a Qt signal.
|
||||
void installTLSCertificate(); ///< Forwards a InstallTLScertificate call via a Qt signal.
|
||||
void exportTLSCertificates(QString const &folderPath); //< Forward an 'ExportTLSCertificates' call via a Qt signal.
|
||||
void setIsStreaming(bool isStreaming); ///< Forward a isStreaming internal messages via a Qt signal.
|
||||
void setClientPlatform(QString const &clientPlatform); ///< Forward a setClientPlatform call via a Qt signal.
|
||||
@ -67,6 +68,7 @@ signals:
|
||||
void setColorSchemeNameReceived(QString const &name); ///< Forward a SetColorScheme call via a Qt Signal
|
||||
void reportBugReceived(QString const &osType, QString const &osVersion, QString const &emailClient, QString const &address,
|
||||
QString const &description, bool includeLogs); ///< Signal for the ReportBug gRPC call
|
||||
void installTLSCertificateReceived(); ///< Signal for the InstallTLSCertificate gRPC call.
|
||||
void exportTLSCertificatesReceived(QString const &folderPath); ///< Signal for the ExportTLSCertificates gRPC call.
|
||||
void setIsStreamingReceived(bool isStreaming); ///< Signal for the IsStreaming internal message.
|
||||
void setClientPlatformReceived(QString const &clientPlatform); ///< Signal for the SetClientPlatform gRPC call.
|
||||
|
||||
@ -214,6 +214,16 @@ grpc::Status GRPCService::IsTelemetryDisabled(::grpc::ServerContext *, ::google:
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[out] response The response.
|
||||
/// \return The status for the call.
|
||||
//****************************************************************************************************************************************************
|
||||
Status GRPCService::GoOs(ServerContext *, Empty const*, StringValue *response) {
|
||||
response->set_value(app().mainWindow().settingsTab().os().toStdString());
|
||||
return Status::OK;
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The status for the call.
|
||||
//****************************************************************************************************************************************************
|
||||
@ -361,22 +371,6 @@ Status GRPCService::ReportBug(ServerContext *, ReportBugRequest const *request,
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] request The request
|
||||
//****************************************************************************************************************************************************
|
||||
Status GRPCService::ExportTLSCertificates(ServerContext *, StringValue const *request, Empty *response) {
|
||||
SettingsTab &tab = app().mainWindow().settingsTab();
|
||||
if (!tab.nextTLSCertExportWillSucceed()) {
|
||||
qtProxy_.sendDelayedEvent(newGenericErrorEvent(grpc::TLS_CERT_EXPORT_ERROR));
|
||||
}
|
||||
if (!tab.nextTLSKeyExportWillSucceed()) {
|
||||
qtProxy_.sendDelayedEvent(newGenericErrorEvent(grpc::TLS_KEY_EXPORT_ERROR));
|
||||
}
|
||||
qtProxy_.exportTLSCertificates(QString::fromStdString(request->value()));
|
||||
return Status::OK;
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] request The request.
|
||||
/// \return The status for the call.
|
||||
@ -406,7 +400,7 @@ Status GRPCService::Login(ServerContext *, LoginRequest const *request, Empty *)
|
||||
return Status::OK;
|
||||
}
|
||||
if (usersTab.nextUserTwoPasswordsRequired()) {
|
||||
qtProxy_.sendDelayedEvent(newLoginTwoPasswordsRequestedEvent());
|
||||
qtProxy_.sendDelayedEvent(newLoginTwoPasswordsRequestedEvent(loginUsername_));
|
||||
return Status::OK;
|
||||
}
|
||||
|
||||
@ -431,7 +425,7 @@ Status GRPCService::Login2FA(ServerContext *, LoginRequest const *request, Empty
|
||||
return Status::OK;
|
||||
}
|
||||
if (usersTab.nextUserTwoPasswordsRequired()) {
|
||||
qtProxy_.sendDelayedEvent(newLoginTwoPasswordsRequestedEvent());
|
||||
qtProxy_.sendDelayedEvent(newLoginTwoPasswordsRequestedEvent(loginUsername_));
|
||||
return Status::OK;
|
||||
}
|
||||
|
||||
@ -758,9 +752,86 @@ Status GRPCService::ConfigureUserAppleMail(ServerContext *, ConfigureAppleMailRe
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] request The request
|
||||
/// \param[in] writer The writer
|
||||
/// \return The status for the call.
|
||||
//****************************************************************************************************************************************************
|
||||
Status GRPCService::ExportTLSCertificates(ServerContext *, StringValue const *request, Empty *response) {
|
||||
app().log().debug(__FUNCTION__);
|
||||
SettingsTab &tab = app().mainWindow().settingsTab();
|
||||
if (!tab.nextTLSCertExportWillSucceed()) {
|
||||
qtProxy_.sendDelayedEvent(newGenericErrorEvent(grpc::TLS_CERT_EXPORT_ERROR));
|
||||
}
|
||||
if (!tab.nextTLSKeyExportWillSucceed()) {
|
||||
qtProxy_.sendDelayedEvent(newGenericErrorEvent(grpc::TLS_KEY_EXPORT_ERROR));
|
||||
}
|
||||
qtProxy_.exportTLSCertificates(QString::fromStdString(request->value()));
|
||||
return Status::OK;
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] response The reponse.
|
||||
/// \return The status for the call.
|
||||
//****************************************************************************************************************************************************
|
||||
Status GRPCService::IsTLSCertificateInstalled(ServerContext *, const Empty *request, BoolValue *response) {
|
||||
app().log().debug(__FUNCTION__);
|
||||
response->set_value(app().mainWindow().settingsTab().isTLSCertificateInstalled());
|
||||
return Status::OK;
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
//
|
||||
//****************************************************************************************************************************************************
|
||||
Status GRPCService::InstallTLSCertificate(ServerContext *, Empty const *, Empty *) {
|
||||
app().log().debug(__FUNCTION__);
|
||||
SPStreamEvent event;
|
||||
qtProxy_.installTLSCertificate();
|
||||
switch (app().mainWindow().settingsTab().nextTLSCertIntallResult()) {
|
||||
case SettingsTab::TLSCertInstallResult::Success:
|
||||
event = newCertificateInstallSuccessEvent();
|
||||
break;
|
||||
case SettingsTab::TLSCertInstallResult::Canceled:
|
||||
event = newCertificateInstallCanceledEvent();
|
||||
break;
|
||||
default:
|
||||
event = newCertificateInstallFailedEvent();
|
||||
break;
|
||||
}
|
||||
qtProxy_.sendDelayedEvent(event);
|
||||
return Status::OK;
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] request The request.
|
||||
//****************************************************************************************************************************************************
|
||||
Status GRPCService::KBArticleClicked(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) {
|
||||
app().log().debug(QString("%1 - URL = %2").arg(__FUNCTION__, QString::fromStdString(request->value())));
|
||||
return Status::OK;
|
||||
}
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
//
|
||||
//****************************************************************************************************************************************************
|
||||
Status GRPCService::ReportBugClicked(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::Empty *) {
|
||||
app().log().debug(__FUNCTION__);
|
||||
return Status::OK;
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] request The request.
|
||||
//****************************************************************************************************************************************************
|
||||
Status GRPCService::AutoconfigClicked(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *response) {
|
||||
app().log().debug(QString("%1 - Client = %2").arg(__FUNCTION__, QString::fromStdString(request->value())));
|
||||
return Status::OK;
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] request The request
|
||||
/// \param[in] writer The writer
|
||||
//****************************************************************************************************************************************************
|
||||
Status GRPCService::RunEventStream(ServerContext *ctx, EventStreamRequest const *request, ServerWriter<StreamEvent> *writer) {
|
||||
app().log().debug(__FUNCTION__);
|
||||
{
|
||||
@ -850,4 +921,3 @@ void GRPCService::finishLogin() {
|
||||
|
||||
qtProxy_.sendDelayedEvent(newLoginFinishedEvent(user->id(), alreadyExist));
|
||||
}
|
||||
|
||||
|
||||
@ -53,6 +53,7 @@ public: // member functions.
|
||||
grpc::Status IsAllMailVisible(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::google::protobuf::BoolValue *response) override;
|
||||
grpc::Status SetIsTelemetryDisabled(::grpc::ServerContext *, ::google::protobuf::BoolValue const *request, ::google::protobuf::Empty *response) override;
|
||||
grpc::Status IsTelemetryDisabled(::grpc::ServerContext *, ::google::protobuf::Empty const *request, ::google::protobuf::BoolValue *response) override;
|
||||
grpc::Status GoOs(::grpc::ServerContext *, ::google::protobuf::Empty const*, ::google::protobuf::StringValue *response) override;
|
||||
grpc::Status TriggerReset(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::Empty *) override;
|
||||
grpc::Status Version(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::StringValue *response) override;
|
||||
grpc::Status LogsPath(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::StringValue *response) override;
|
||||
@ -64,7 +65,6 @@ public: // member functions.
|
||||
grpc::Status ColorSchemeName(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::StringValue *response) override;
|
||||
grpc::Status CurrentEmailClient(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::StringValue *response) override;
|
||||
grpc::Status ReportBug(::grpc::ServerContext *, ::grpc::ReportBugRequest const *request, ::google::protobuf::Empty *) override;
|
||||
grpc::Status ExportTLSCertificates(::grpc::ServerContext *context, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *response) override;
|
||||
grpc::Status ForceLauncher(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
|
||||
grpc::Status SetMainExecutable(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
|
||||
grpc::Status Login(::grpc::ServerContext *, ::grpc::LoginRequest const *request, ::google::protobuf::Empty *) override;
|
||||
@ -93,6 +93,12 @@ public: // member functions.
|
||||
grpc::Status LogoutUser(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
|
||||
grpc::Status RemoveUser(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
|
||||
grpc::Status ConfigureUserAppleMail(::grpc::ServerContext *, ::grpc::ConfigureAppleMailRequest const *request, ::google::protobuf::Empty *) override;
|
||||
grpc::Status IsTLSCertificateInstalled(::grpc::ServerContext *, ::google::protobuf::Empty const*, ::google::protobuf::BoolValue *response) override;
|
||||
grpc::Status InstallTLSCertificate(::grpc::ServerContext *, ::google::protobuf::Empty const*, ::google::protobuf::Empty *) override;
|
||||
grpc::Status ExportTLSCertificates(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
|
||||
grpc::Status ReportBugClicked(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::google::protobuf::Empty *) override;
|
||||
grpc::Status AutoconfigClicked(::grpc::ServerContext *context, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
|
||||
grpc::Status KBArticleClicked(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
|
||||
grpc::Status RunEventStream(::grpc::ServerContext *ctx, ::grpc::EventStreamRequest const *request, ::grpc::ServerWriter<::grpc::StreamEvent> *writer) override;
|
||||
grpc::Status StopEventStream(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::Empty *) override;
|
||||
bool sendEvent(bridgepp::SPStreamEvent const &event); ///< Queue an event for sending through the event stream.
|
||||
|
||||
@ -285,11 +285,20 @@ void SettingsTab::setBugReport(QString const &osType, QString const &osVersion,
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
//
|
||||
//****************************************************************************************************************************************************
|
||||
void SettingsTab::installTLSCertificate() {
|
||||
ui_.labelLastTLSCertInstall->setText(QString("Last install: %1").arg(QDateTime::currentDateTime().toString(Qt::ISODateWithMs)));
|
||||
ui_.checkTLSCertIsInstalled->setChecked(this->nextTLSCertIntallResult() == TLSCertInstallResult::Success);
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] folderPath The folder path.
|
||||
//****************************************************************************************************************************************************
|
||||
void SettingsTab::exportTLSCertificates(QString const &folderPath) {
|
||||
ui_.labeLastTLSCertsExport->setText(QString("%1 Export to %2")
|
||||
ui_.labeLastTLSCertExport->setText(QString("%1 Export to %2")
|
||||
.arg(QDateTime::currentDateTime().toString(Qt::ISODateWithMs))
|
||||
.arg(folderPath));
|
||||
}
|
||||
@ -303,6 +312,22 @@ bool SettingsTab::nextBugReportWillSucceed() const {
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return the state of the 'TLS Certificate is installed' check box.
|
||||
//****************************************************************************************************************************************************
|
||||
bool SettingsTab::isTLSCertificateInstalled() const {
|
||||
return ui_.checkTLSCertIsInstalled->isChecked();
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The value for the 'Next TLS cert install result'.
|
||||
//****************************************************************************************************************************************************
|
||||
SettingsTab::TLSCertInstallResult SettingsTab::nextTLSCertIntallResult() const {
|
||||
return TLSCertInstallResult(ui_.comboNextTLSCertInstallResult->currentIndex());
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return true if the 'Next TLS key export will succeed' check box is checked
|
||||
//****************************************************************************************************************************************************
|
||||
@ -505,4 +530,11 @@ void SettingsTab::resetUI() {
|
||||
ui_.comboCacheError->setCurrentIndex(0);
|
||||
|
||||
ui_.checkAutomaticUpdate->setChecked(true);
|
||||
|
||||
ui_.checkTLSCertIsInstalled->setChecked(false);
|
||||
ui_.comboNextTLSCertInstallResult->setCurrentIndex(0);
|
||||
ui_.checkTLSCertExportWillSucceed->setChecked(true);
|
||||
ui_.checkTLSKeyExportWillSucceed->setChecked(true);
|
||||
ui_.labeLastTLSCertExport->setText("Last export: never");
|
||||
ui_.labelLastTLSCertInstall->setText("Last install: never");
|
||||
}
|
||||
|
||||
@ -28,6 +28,13 @@
|
||||
//****************************************************************************************************************************************************
|
||||
class SettingsTab : public QWidget {
|
||||
Q_OBJECT
|
||||
public: // data types.
|
||||
enum class TLSCertInstallResult {
|
||||
Success = 0,
|
||||
Canceled = 1,
|
||||
Failure = 2
|
||||
}; ///< Enumberation for the result of a TLS certificate installation.
|
||||
|
||||
public: // member functions.
|
||||
explicit SettingsTab(QWidget *parent = nullptr); ///< Default constructor.
|
||||
SettingsTab(SettingsTab const &) = delete; ///< Disabled copy-constructor.
|
||||
@ -54,6 +61,8 @@ public: // member functions.
|
||||
QString dependencyLicenseLink() const; ///< Get the content of the 'Dependency License Link' edit.
|
||||
QString landingPageLink() const; ///< Get the content of the 'Landing Page Link' edit.
|
||||
bool nextBugReportWillSucceed() const; ///< Get the status of the 'Next Bug Report Will Fail' check box.
|
||||
bool isTLSCertificateInstalled() const; ///< Get the status of the 'TLS Certificate is installed' check box.
|
||||
TLSCertInstallResult nextTLSCertIntallResult() const; ///< Get the value of the 'Next TLS Certificate install result' combo box.
|
||||
bool nextTLSCertExportWillSucceed() const; ///< Get the status of the 'Next TLS Cert export will succeed' check box.
|
||||
bool nextTLSKeyExportWillSucceed() const; ///< Get the status of the 'Next TLS Key export will succeed' check box.
|
||||
QString hostname() const; ///< Get the value of the 'Hostname' edit.
|
||||
@ -79,6 +88,7 @@ public slots:
|
||||
void setColorSchemeName(QString const &name); ///< Set the value for the 'Use Dark Theme' check box.
|
||||
void setBugReport(QString const &osType, QString const &osVersion, QString const &emailClient, QString const &address, QString const &description,
|
||||
bool includeLogs); ///< Set the content of the bug report box.
|
||||
void installTLSCertificate(); ///< Install the TLS certificate.
|
||||
void exportTLSCertificates(QString const &folderPath); ///< Export the TLS certificates.
|
||||
void setMailServerSettings(qint32 imapPort, qint32 smtpPort, bool useSSLForIMAP, bool useSSLForSMTP); ///< Change the mail server settings.
|
||||
void setIsDoHEnabled(bool enabled); ///< Set the value for the 'DoH Enabled' check box.
|
||||
|
||||
@ -370,7 +370,7 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupCache_2">
|
||||
<widget class="QGroupBox" name="groupCert">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
@ -380,34 +380,81 @@
|
||||
<property name="title">
|
||||
<string>TLS Certficates</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_9">
|
||||
<item>
|
||||
<widget class="QLabel" name="labeLastTLSCertsExport">
|
||||
<layout class="QGridLayout" name="gridLayout_4" columnstretch="1,1">
|
||||
<item row="0" column="0">
|
||||
<widget class="QCheckBox" name="checkTLSCertIsInstalled">
|
||||
<property name="text">
|
||||
<string>Last Export: Never</string>
|
||||
<string>Certificate is installed</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QCheckBox" name="checkTLSCertExportWillSucceed">
|
||||
<property name="text">
|
||||
<string>TLS certificate export will succeed</string>
|
||||
<string>Certificate export will succeed</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QCheckBox" name="checkTLSKeyExportWillSucceed">
|
||||
<property name="text">
|
||||
<string>TLS private key export will succeed</string>
|
||||
<string>Key export will succeed</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QLabel" name="labeLastTLSCertExport">
|
||||
<property name="text">
|
||||
<string>Last Export: never</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="labelLastTLSCertInstall">
|
||||
<property name="text">
|
||||
<string>Last install: never</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_14" stretch="0,1">
|
||||
<item>
|
||||
<widget class="QLabel" name="labelNextInstall">
|
||||
<property name="text">
|
||||
<string>Next install will</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="comboNextTLSCertInstallResult">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Succeed</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Be Canceled</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Fail</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
|
||||
@ -40,7 +40,8 @@ using namespace bridgepp;
|
||||
namespace {
|
||||
|
||||
|
||||
QString const bugReportFile = ":qml/Resources/bug_report_flow.json";
|
||||
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.
|
||||
|
||||
|
||||
}
|
||||
@ -278,6 +279,30 @@ void QMLBackend::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.
|
||||
//****************************************************************************************************************************************************
|
||||
@ -941,6 +966,15 @@ void QMLBackend::reportBug(QString const &category, QString const &description,
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
//
|
||||
//****************************************************************************************************************************************************
|
||||
void QMLBackend::installTLSCertificate() {
|
||||
HANDLE_EXCEPTION(
|
||||
app().grpc().installTLSCertificate();
|
||||
)
|
||||
}
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
//
|
||||
//****************************************************************************************************************************************************
|
||||
@ -1267,6 +1301,9 @@ void QMLBackend::connectGrpcEvents() {
|
||||
connect(client, &GRPCClient::reportBugSuccess, this, &QMLBackend::bugReportSendSuccess);
|
||||
connect(client, &GRPCClient::reportBugFallback, this, &QMLBackend::bugReportSendFallback);
|
||||
connect(client, &GRPCClient::reportBugError, this, &QMLBackend::bugReportSendError);
|
||||
connect(client, &GRPCClient::certificateInstallSuccess, this, &QMLBackend::certificateInstallSuccess);
|
||||
connect(client, &GRPCClient::certificateInstallCanceled, this, &QMLBackend::certificateInstallCanceled);
|
||||
connect(client, &GRPCClient::certificateInstallFailed, this, &QMLBackend::certificateInstallFailed);
|
||||
connect(client, &GRPCClient::showMainWindow, [&]() { this->showMainWindow("gRPC showMainWindow event"); });
|
||||
|
||||
// cache events
|
||||
|
||||
@ -64,6 +64,8 @@ public: // member functions.
|
||||
Q_INVOKABLE QString getQuestionAnswer(quint8 questionId) const; ///< Get the answer for a given question.
|
||||
Q_INVOKABLE QString collectAnswers(quint8 categoryId) const; ///< Collect answer for a given set of questions.
|
||||
Q_INVOKABLE void clearAnswers(); ///< Clear all collected answers.
|
||||
Q_INVOKABLE bool isTLSCertificateInstalled(); ///< Check if the bridge certificate is installed in the OS keychain.
|
||||
Q_INVOKABLE void openKBArticle(QString const & url = QString()); ///< Open a knowledge base article.
|
||||
|
||||
public: // Qt/QML properties. Note that the NOTIFY-er signal is required even for read-only properties (QML warning otherwise)
|
||||
Q_PROPERTY(bool showOnStartup READ showOnStartup NOTIFY showOnStartupChanged)
|
||||
@ -195,6 +197,7 @@ public slots: // slot for signals received from QML -> To be forwarded to Bridge
|
||||
void installUpdate() const; ///< Slot for the update install.
|
||||
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 installTLSCertificate(); ///< Installs the Bridge TLS certificate in the Keychain.
|
||||
void exportTLSCertificates() const; ///< Slot for the export of the TLS certificates.
|
||||
void onResetFinished(); ///< Slot for the reset finish signal.
|
||||
void onVersionChanged(); ///< Slot for the version change signal.
|
||||
@ -231,7 +234,7 @@ signals: // Signals received from the Go backend, to be forwarded to QML
|
||||
void login2FARequested(QString const &username); ///< Signal for the 'login2FARequested' gRPC stream event.
|
||||
void login2FAError(QString const &errorMsg); ///< Signal for the 'login2FAError' gRPC stream event.
|
||||
void login2FAErrorAbort(QString const &errorMsg); ///< Signal for the 'login2FAErrorAbort' gRPC stream event.
|
||||
void login2PasswordRequested(); ///< Signal for the 'login2PasswordRequested' gRPC stream event.
|
||||
void login2PasswordRequested(QString const &username); ///< Signal for the 'login2PasswordRequested' gRPC stream event.
|
||||
void login2PasswordError(QString const &errorMsg); ///< Signal for the 'login2PasswordError' gRPC stream event.
|
||||
void login2PasswordErrorAbort(QString const &errorMsg); ///< Signal for the 'login2PasswordErrorAbort' gRPC stream event.
|
||||
void loginFinished(int index, bool wasSignedOut); ///< Signal for the 'loginFinished' gRPC stream event.
|
||||
@ -268,10 +271,13 @@ signals: // Signals received from the Go backend, to be forwarded to QML
|
||||
void bugReportSendSuccess(); ///< Signal for the 'bugReportSendSuccess' gRPC stream event.
|
||||
void bugReportSendFallback(); ///< Signal for the 'bugReportSendFallback' gRPC stream event.
|
||||
void bugReportSendError(); ///< Signal for the 'bugReportSendError' gRPC stream event.
|
||||
void certificateInstallSuccess(); ///< Signal for the 'certificateInstallSuccess' gRPC stream event.
|
||||
void certificateInstallCanceled(); ///< Signal for the 'certificateInstallCanceled' gRPC stream event.
|
||||
void certificateInstallFailed(); /// Signal for the 'certificateInstallFailed' gRPC stream event.
|
||||
void showMainWindow(); ///< Signal for the 'showMainWindow' gRPC stream event.
|
||||
void hideMainWindow(); ///< Signal for the 'hideMainWindow' gRPC stream event.
|
||||
void showHelp(); ///< Signal for the 'showHelp' event (from the context menu).
|
||||
void showSettings(); ///< Signal for the 'showHelp' event (from the context menu).
|
||||
void showSettings(); ///< Signal for the 'showSettings' event (from the context menu).
|
||||
void selectUser(QString const& userID, bool forceShowWindow); ///< Signal emitted in order to selected a user with a given ID in the list.
|
||||
void genericError(QString const &title, QString const &description); ///< Signal for the 'genericError' gRPC stream event.
|
||||
void imapLoginWhileSignedOut(QString const& username); ///< Signal for the notification of IMAP login attempt on a signed out account.
|
||||
|
||||
@ -5,7 +5,6 @@
|
||||
<file>qml/AccountView.qml</file>
|
||||
<file>qml/Banner.qml</file>
|
||||
<file>qml/Bridge.qml</file>
|
||||
<file>qml/bridgeqml.qmlproject</file>
|
||||
<file>qml/BugCategoryView.qml</file>
|
||||
<file>qml/BugQuestionView.qml</file>
|
||||
<file>qml/BugReportFlow.qml</file>
|
||||
@ -20,9 +19,11 @@
|
||||
<file>qml/icons/ic-alert.svg</file>
|
||||
<file>qml/icons/ic-apple-mail.svg</file>
|
||||
<file>qml/icons/ic-arrow-left.svg</file>
|
||||
<file>qml/icons/ic-bridge.svg</file>
|
||||
<file>qml/icons/ic-card-identity.svg</file>
|
||||
<file>qml/icons/ic-check.svg</file>
|
||||
<file>qml/icons/ic-chevron-down.svg</file>
|
||||
<file>qml/icons/ic-chevron-left.svg</file>
|
||||
<file>qml/icons/ic-chevron-right.svg</file>
|
||||
<file>qml/icons/ic-chevron-up.svg</file>
|
||||
<file>qml/icons/ic-cog-wheel.svg</file>
|
||||
@ -49,13 +50,18 @@
|
||||
<file>qml/icons/ic-success.svg</file>
|
||||
<file>qml/icons/ic-three-dots-vertical.svg</file>
|
||||
<file>qml/icons/ic-trash.svg</file>
|
||||
<file>qml/icons/ic-warning-orange.svg</file>
|
||||
<file>qml/icons/img-client-config-selector.svg</file>
|
||||
<file>qml/icons/img-client-config-success.svg</file>
|
||||
<file>qml/icons/img-macos-cert-screenshot.png</file>
|
||||
<file>qml/icons/img-macos-profile-screenshot.png</file>
|
||||
<file>qml/icons/img-mail-clients.svg</file>
|
||||
<file>qml/icons/img-mail-logo-wordmark-dark.svg</file>
|
||||
<file>qml/icons/img-mail-logo-wordmark.svg</file>
|
||||
<file>qml/icons/img-proton-logos.png</file>
|
||||
<file>qml/icons/img-proton-logos.svg</file>
|
||||
<file>qml/icons/img-splash.png</file>
|
||||
<file>qml/icons/img-splash.svg</file>
|
||||
<file>qml/icons/img-welcome-dark.png</file>
|
||||
<file>qml/icons/img-welcome-dark.svg</file>
|
||||
<file>qml/icons/img-welcome.png</file>
|
||||
<file>qml/icons/img-welcome.svg</file>
|
||||
<file>qml/icons/Loader_16.svg</file>
|
||||
<file>qml/icons/Loader_48.svg</file>
|
||||
@ -75,6 +81,7 @@
|
||||
<file>qml/KeychainSettings.qml</file>
|
||||
<file>qml/LocalCacheSettings.qml</file>
|
||||
<file>qml/MainWindow.qml</file>
|
||||
<file>qml/NoAccountView.qml</file>
|
||||
<file>qml/NotificationDialog.qml</file>
|
||||
<file>qml/NotificationPopups.qml</file>
|
||||
<file>qml/Notifications/Notification.qml</file>
|
||||
@ -90,6 +97,7 @@
|
||||
<file>qml/Proton/ComboBox.qml</file>
|
||||
<file>qml/Proton/Dialog.qml</file>
|
||||
<file>qml/Proton/Label.qml</file>
|
||||
<file>qml/Proton/LinkLabel.qml</file>
|
||||
<file>qml/Proton/Menu.qml</file>
|
||||
<file>qml/Proton/MenuItem.qml</file>
|
||||
<file>qml/Proton/Popup.qml</file>
|
||||
@ -101,14 +109,26 @@
|
||||
<file>qml/Proton/TextField.qml</file>
|
||||
<file>qml/Proton/Toggle.qml</file>
|
||||
<file>qml/QuestionItem.qml</file>
|
||||
<file>qml/Resources/bug_report_flow.json</file>
|
||||
<file>qml/Resources/bug_report_flow.json</file>
|
||||
<file>qml/Resources/Help/Template.html</file>
|
||||
<file>qml/Resources/Help/WhyBridge.html</file>
|
||||
<file>qml/Resources/Help/WhyCertificate.html</file>
|
||||
<file>qml/Resources/Help/WhyProfileWarning.html</file>
|
||||
<file>qml/SettingsItem.qml</file>
|
||||
<file>qml/SettingsView.qml</file>
|
||||
<file>qml/SetupGuide.qml</file>
|
||||
<file>qml/SignIn.qml</file>
|
||||
<file>qml/SetupWizard/ClientListItem.qml</file>
|
||||
<file>qml/SetupWizard/LeftPane.qml</file>
|
||||
<file>qml/SetupWizard/ClientConfigAppleMail.qml</file>
|
||||
<file>qml/SetupWizard/ClientConfigEnd.qml</file>
|
||||
<file>qml/SetupWizard/ClientConfigParameters.qml</file>
|
||||
<file>qml/SetupWizard/ClientConfigSelector.qml</file>
|
||||
<file>qml/SetupWizard/HelpButton.qml</file>
|
||||
<file>qml/SetupWizard/SetupWizard.qml</file>
|
||||
<file>qml/SetupWizard/Login.qml</file>
|
||||
<file>qml/SetupWizard/Onboarding.qml</file>
|
||||
<file>qml/SetupWizard/StepDescriptionBox.qml</file>
|
||||
<file>qml/ConnectionModeSettings.qml</file>
|
||||
<file>qml/SplashScreen.qml</file>
|
||||
<file>qml/Status.qml</file>
|
||||
<file>qml/WelcomeGuide.qml</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
@ -262,7 +262,7 @@ void UserList::onUsedBytesChanged(QString const &userID, qint64 usedBytes) {
|
||||
void UserList::onSyncStarted(QString const &userID) {
|
||||
int const index = this->rowOfUserID(userID);
|
||||
if (index < 0) {
|
||||
app().log().error(QString("Received onSyncStarted event for unknown userID %1").arg(userID));
|
||||
app().log().error(QString("Received syncStarted event for unknown userID %1").arg(userID));
|
||||
return;
|
||||
}
|
||||
users_[index]->setIsSyncing(true);
|
||||
@ -275,7 +275,7 @@ void UserList::onSyncStarted(QString const &userID) {
|
||||
void UserList::onSyncFinished(QString const &userID) {
|
||||
int const index = this->rowOfUserID(userID);
|
||||
if (index < 0) {
|
||||
app().log().error(QString("Received onSyncFinished event for unknown userID %1").arg(userID));
|
||||
app().log().error(QString("Received syncFinished event for unknown userID %1").arg(userID));
|
||||
return;
|
||||
}
|
||||
users_[index]->setIsSyncing(false);
|
||||
@ -293,7 +293,7 @@ void UserList::onSyncProgress(QString const &userID, double progress, float elap
|
||||
Q_UNUSED(remainingMs)
|
||||
int const index = this->rowOfUserID(userID);
|
||||
if (index < 0) {
|
||||
app().log().error(QString("Received onSyncFinished event for unknown userID %1").arg(userID));
|
||||
app().log().error(QString("Received syncProgress event for unknown userID %1").arg(userID));
|
||||
return;
|
||||
}
|
||||
users_[index]->setSyncProgress(progress);
|
||||
|
||||
@ -415,7 +415,11 @@ int main(int argc, char *argv[]) {
|
||||
}
|
||||
catch (Exception const &e) {
|
||||
sentry_uuid_s const uuid = reportSentryException("Exception occurred during main", e);
|
||||
QMessageBox::critical(nullptr, "Error", e.qwhat());
|
||||
QString message = 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";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
@ -22,7 +22,7 @@ Item {
|
||||
LargeView
|
||||
}
|
||||
|
||||
property var _spacing: 12 * ProtonStyle.px
|
||||
property var _spacing: 12
|
||||
property ColorScheme colorScheme
|
||||
property color progressColor: {
|
||||
if (!root.enabled)
|
||||
@ -154,7 +154,7 @@ Item {
|
||||
}
|
||||
}
|
||||
Item {
|
||||
implicitHeight: root.type === AccountDelegate.LargeView ? 6 * ProtonStyle.px : 0
|
||||
implicitHeight: root.type === AccountDelegate.LargeView ? 6 : 0
|
||||
}
|
||||
RowLayout {
|
||||
spacing: 0
|
||||
@ -222,15 +222,15 @@ Item {
|
||||
}
|
||||
}
|
||||
Item {
|
||||
implicitHeight: root.type === AccountDelegate.LargeView ? 3 * ProtonStyle.px : 0
|
||||
implicitHeight: root.type === AccountDelegate.LargeView ? 3 : 0
|
||||
}
|
||||
Rectangle {
|
||||
id: progress_bar
|
||||
color: root.colorScheme.border_weak
|
||||
height: 4 * ProtonStyle.px
|
||||
height: 4
|
||||
radius: ProtonStyle.progress_bar_radius
|
||||
visible: root.user ? root.type === AccountDelegate.LargeView : false
|
||||
width: 140 * ProtonStyle.px
|
||||
width: 140
|
||||
|
||||
Rectangle {
|
||||
id: progress_bar_filled
|
||||
|
||||
@ -23,13 +23,14 @@ Item {
|
||||
property int _detailsMargin: 25
|
||||
property int _lineThickness: 1
|
||||
property int _spacing: 20
|
||||
property int _buttonSpacing: 8
|
||||
property int _topMargin: 32
|
||||
property ColorScheme colorScheme
|
||||
property var notifications
|
||||
property var user
|
||||
|
||||
signal showSetupGuide(var user, string address)
|
||||
signal showSignIn
|
||||
signal showClientConfigurator(var user, string address, bool justLoggedIn)
|
||||
signal showLogin(var username)
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
@ -63,7 +64,7 @@ Item {
|
||||
// account delegate with action buttons
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: _topMargin
|
||||
|
||||
spacing: _buttonSpacing
|
||||
AccountDelegate {
|
||||
Layout.fillWidth: true
|
||||
colorScheme: root.colorScheme
|
||||
@ -92,9 +93,9 @@ Item {
|
||||
visible: root.user ? (root.user.state === EUserState.SignedOut) : false
|
||||
|
||||
onClicked: {
|
||||
if (!root.user)
|
||||
return;
|
||||
root.showSignIn();
|
||||
if (user) {
|
||||
root.showLogin(user.primaryEmailOrUsername());
|
||||
}
|
||||
}
|
||||
}
|
||||
Button {
|
||||
@ -118,18 +119,18 @@ Item {
|
||||
}
|
||||
SettingsItem {
|
||||
Layout.fillWidth: true
|
||||
actionText: qsTr("Configure")
|
||||
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.Button
|
||||
visible: _connected && (!root.user.splitMode) || (root.user.addresses.length === 1)
|
||||
type: SettingsItem.PrimaryButton
|
||||
visible: _connected && ((!root.user.splitMode) || (root.user.addresses.length === 1))
|
||||
|
||||
onClicked: {
|
||||
if (!root.user)
|
||||
return;
|
||||
root.showSetupGuide(root.user, user.addresses[0]);
|
||||
root.showClientConfigurator(root.user, user.addresses[0], false);
|
||||
}
|
||||
}
|
||||
SettingsItem {
|
||||
@ -165,13 +166,13 @@ Item {
|
||||
}
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
secondary: true
|
||||
text: qsTr("Configure")
|
||||
secondary: false
|
||||
text: qsTr("Configure email client")
|
||||
|
||||
onClicked: {
|
||||
if (!root.user)
|
||||
return;
|
||||
root.showSetupGuide(root.user, addressSelector.displayText);
|
||||
root.showClientConfigurator(root.user, addressSelector.displayText, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -40,7 +40,6 @@ QtObject {
|
||||
function onHideMainWindow() {
|
||||
mainWindow.hide();
|
||||
}
|
||||
|
||||
target: Backend
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,6 +21,7 @@ Rectangle {
|
||||
|
||||
property int _margin: 24
|
||||
property ColorScheme colorScheme
|
||||
property bool highlightPassword
|
||||
property string hostname
|
||||
property string password
|
||||
property string port
|
||||
@ -68,7 +69,8 @@ Rectangle {
|
||||
}
|
||||
ConfigurationItem {
|
||||
colorScheme: root.colorScheme
|
||||
label: qsTr("Password")
|
||||
label: highlightPassword ? qsTr("Use this password") : qsTr("Password")
|
||||
labelColor: highlightPassword ? colorScheme.signal_warning_active : colorScheme.text_norm
|
||||
value: root.password
|
||||
}
|
||||
ConfigurationItem {
|
||||
|
||||
@ -21,6 +21,7 @@ Item {
|
||||
|
||||
property var colorScheme
|
||||
property string label
|
||||
property string labelColor: root.colorScheme.text_norm
|
||||
property string value
|
||||
|
||||
Layout.fillWidth: true
|
||||
@ -35,9 +36,10 @@ Item {
|
||||
|
||||
ColumnLayout {
|
||||
Label {
|
||||
color: labelColor
|
||||
colorScheme: root.colorScheme
|
||||
text: root.label
|
||||
type: Label.Body
|
||||
type: Label.Body_semibold
|
||||
}
|
||||
TextEdit {
|
||||
id: valueText
|
||||
|
||||
@ -24,7 +24,8 @@ Item {
|
||||
|
||||
signal closeWindow
|
||||
signal quitBridge
|
||||
signal showSetupGuide(var user, string address)
|
||||
signal showClientConfigurator(var user, string address, bool justLoggedIn)
|
||||
signal showLogin(var username)
|
||||
|
||||
function selectUser(userID) {
|
||||
const users = Backend.users;
|
||||
@ -35,11 +36,14 @@ Item {
|
||||
}
|
||||
accounts.currentIndex = i;
|
||||
if (user.state === EUserState.SignedOut)
|
||||
showSignIn(user.primaryEmailOrUsername());
|
||||
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();
|
||||
}
|
||||
@ -49,9 +53,9 @@ Item {
|
||||
function showSettings() {
|
||||
rightContent.showGeneralSettings();
|
||||
}
|
||||
function showSignIn(username) {
|
||||
signIn.username = username;
|
||||
rightContent.showSignIn();
|
||||
|
||||
function hasAccount() {
|
||||
return Backend.users.count > 0
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
@ -190,6 +194,41 @@ Item {
|
||||
Layout.minimumHeight: 1
|
||||
color: leftBar.colorScheme.border_weak
|
||||
}
|
||||
Item {
|
||||
id: noAccountBox
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 24
|
||||
visible: !hasAccount()
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 8
|
||||
|
||||
Label {
|
||||
colorScheme: leftBar.colorScheme
|
||||
color: colorScheme.text_weak
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: qsTr("No accounts")
|
||||
}
|
||||
Button {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
colorScheme: leftBar.colorScheme
|
||||
text: qsTr("Add an account")
|
||||
secondary: true
|
||||
onClicked: root.showLogin("")
|
||||
}
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ListView {
|
||||
id: accounts
|
||||
|
||||
@ -206,7 +245,7 @@ Item {
|
||||
clip: true
|
||||
model: Backend.users
|
||||
spacing: 12
|
||||
|
||||
visible: hasAccount()
|
||||
delegate: Item {
|
||||
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
|
||||
implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
|
||||
@ -233,8 +272,7 @@ Item {
|
||||
if (user.state !== EUserState.SignedOut) {
|
||||
rightContent.showAccount();
|
||||
} else {
|
||||
signIn.username = user.primaryEmailOrUsername();
|
||||
rightContent.showSignIn();
|
||||
showLogin(user.primaryEmailOrUsername());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -282,8 +320,7 @@ Item {
|
||||
width: 36
|
||||
|
||||
onClicked: {
|
||||
signIn.username = "";
|
||||
rightContent.showSignIn();
|
||||
root.showLogin("");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -323,65 +360,42 @@ Item {
|
||||
function showPortSettings() {
|
||||
rightContent.currentIndex = 4;
|
||||
}
|
||||
function showSignIn() {
|
||||
rightContent.currentIndex = 1;
|
||||
signIn.focus = true;
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
AccountView {
|
||||
StackLayout {
|
||||
// 0
|
||||
colorScheme: root.colorScheme
|
||||
notifications: root.notifications
|
||||
user: {
|
||||
if (accounts.currentIndex < 0)
|
||||
return undefined;
|
||||
if (Backend.users.count === 0)
|
||||
return undefined;
|
||||
return Backend.users.get(accounts.currentIndex);
|
||||
}
|
||||
|
||||
onShowSetupGuide: function (user, address) {
|
||||
root.showSetupGuide(user, address);
|
||||
}
|
||||
onShowSignIn: {
|
||||
const user = this.user;
|
||||
signIn.username = user ? user.primaryEmailOrUsername() : "";
|
||||
rightContent.showSignIn();
|
||||
}
|
||||
}
|
||||
GridLayout {
|
||||
// 1 Sign In
|
||||
columns: 2
|
||||
|
||||
Button {
|
||||
id: backButton
|
||||
Layout.alignment: Qt.AlignTop
|
||||
Layout.leftMargin: 18
|
||||
Layout.topMargin: 10
|
||||
currentIndex: hasAccount() ? 1 : 0
|
||||
NoAccountView {
|
||||
colorScheme: root.colorScheme
|
||||
horizontalPadding: 8
|
||||
icon.source: "/qml/icons/ic-arrow-left.svg"
|
||||
secondary: true
|
||||
|
||||
onClicked: {
|
||||
signIn.abort();
|
||||
rightContent.showAccount();
|
||||
onStartSetup: {
|
||||
root.showLogin("")
|
||||
}
|
||||
}
|
||||
SignIn {
|
||||
id: signIn
|
||||
Layout.bottomMargin: 68
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 80 - backButton.width - 18
|
||||
Layout.preferredWidth: 320
|
||||
Layout.rightMargin: 80
|
||||
Layout.topMargin: 68
|
||||
AccountView {
|
||||
colorScheme: root.colorScheme
|
||||
notifications: root.notifications
|
||||
user: {
|
||||
if (accounts.currentIndex < 0)
|
||||
return undefined;
|
||||
if (Backend.users.count === 0)
|
||||
return undefined;
|
||||
return Backend.users.get(accounts.currentIndex);
|
||||
}
|
||||
|
||||
onShowClientConfigurator: function (user, address, justLoggedIn) {
|
||||
root.showClientConfigurator(user, address, justLoggedIn);
|
||||
}
|
||||
onShowLogin: function (username) {
|
||||
root.showLogin(username);
|
||||
}
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
color: "#ff9900"
|
||||
}
|
||||
GeneralSettings {
|
||||
// 2
|
||||
colorScheme: root.colorScheme
|
||||
|
||||
@ -36,8 +36,7 @@ SettingsView {
|
||||
type: SettingsItem.PrimaryButton
|
||||
|
||||
onClicked: {
|
||||
Backend.notifyKBArticleClicked("https://proton.me/support/bridge");
|
||||
Qt.openUrlExternally("https://proton.me/support/bridge");
|
||||
Backend.openKBArticle();
|
||||
}
|
||||
}
|
||||
SettingsItem {
|
||||
@ -104,7 +103,7 @@ SettingsView {
|
||||
type: Label.Caption
|
||||
|
||||
onLinkActivated: function (link) {
|
||||
Qt.openUrlExternally(link);
|
||||
Qt.openUrlExternally(link)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,14 +17,29 @@ import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import Proton
|
||||
import Notifications
|
||||
import "SetupWizard"
|
||||
|
||||
ApplicationWindow {
|
||||
id: root
|
||||
|
||||
property int _defaultHeight: 780
|
||||
property int _defaultWidth: 1080
|
||||
property var notifications
|
||||
|
||||
function layoutForUserCount(userCount) {
|
||||
if (userCount === 0) {
|
||||
contentLayout.currentIndex = 1;
|
||||
setupWizard.showOnboarding();
|
||||
return;
|
||||
}
|
||||
const u = Backend.users.get(0);
|
||||
if (!u) {
|
||||
console.trace();
|
||||
return;
|
||||
}
|
||||
if ((userCount === 1) && (u.state === EUserState.SignedOut)) {
|
||||
contentLayout.currentIndex = 1;
|
||||
setupWizard.showLogin(u.primaryEmailOrUsername());
|
||||
}
|
||||
}
|
||||
function selectUser(userID) {
|
||||
contentWrapper.selectUser(userID);
|
||||
}
|
||||
@ -35,42 +50,42 @@ ApplicationWindow {
|
||||
root.requestActivate();
|
||||
}
|
||||
}
|
||||
function showClientConfigurator(user, address, justLoggedIn) {
|
||||
contentLayout.currentIndex = 1;
|
||||
setupWizard.showClientConfig(user, address, justLoggedIn);
|
||||
}
|
||||
function showHelp() {
|
||||
contentWrapper.showHelp();
|
||||
}
|
||||
function showLocalCacheSettings() {
|
||||
contentWrapper.showLocalCacheSettings();
|
||||
}
|
||||
function showLogin(username = "") {
|
||||
contentLayout.currentIndex = 1;
|
||||
setupWizard.showLogin(username);
|
||||
}
|
||||
function showSettings() {
|
||||
contentWrapper.showSettings();
|
||||
}
|
||||
function showSetup(user, address) {
|
||||
setupGuide.user = user;
|
||||
setupGuide.address = address;
|
||||
setupGuide.reset();
|
||||
contentLayout._showSetup = !!setupGuide.user;
|
||||
}
|
||||
function showSignIn(username) {
|
||||
if (contentLayout.currentIndex === 1)
|
||||
return;
|
||||
contentWrapper.showSignIn(username);
|
||||
}
|
||||
|
||||
colorScheme: ProtonStyle.currentStyle
|
||||
height: _defaultHeight
|
||||
minimumWidth: _defaultWidth
|
||||
height: screen.height < ProtonStyle.window_default_height + 100 ? ProtonStyle.window_minimum_height : ProtonStyle.window_default_height
|
||||
minimumHeight: ProtonStyle.window_minimum_height
|
||||
minimumWidth: ProtonStyle.window_minimum_width
|
||||
visible: true
|
||||
width: _defaultWidth
|
||||
width: ProtonStyle.window_default_width
|
||||
|
||||
Component.onCompleted: {
|
||||
layoutForUserCount(Backend.users.count);
|
||||
}
|
||||
|
||||
// show Setup Guide on every new user
|
||||
Connections {
|
||||
function onRowsAboutToBeRemoved(parent, first, last) {
|
||||
for (let i = first; i <= last; i++) {
|
||||
const user = Backend.users.get(i);
|
||||
if (setupGuide.user === user) {
|
||||
setupGuide.user = null;
|
||||
contentLayout._showSetup = false;
|
||||
return;
|
||||
if (setupWizard.user === user) {
|
||||
setupWizard.closeWizard();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -83,65 +98,53 @@ ApplicationWindow {
|
||||
if (user.setupGuideSeen) {
|
||||
return;
|
||||
}
|
||||
root.showSetup(user, user.addresses[0]);
|
||||
root.showClientConfigurator(user, user.addresses[0], false);
|
||||
}
|
||||
|
||||
target: Backend.users
|
||||
}
|
||||
Connections {
|
||||
function onLoginFinished(index, wasSignedOut) {
|
||||
const user = Backend.users.get(index);
|
||||
if (user && !wasSignedOut) {
|
||||
root.showSetup(user, user.addresses[0]);
|
||||
}
|
||||
console.debug("Login finished", index);
|
||||
}
|
||||
function onSelectUser(userID, forceShowWindow) {
|
||||
contentWrapper.selectUser(userID);
|
||||
if (setupWizard.visible) {
|
||||
setupWizard.closeWizard()
|
||||
}
|
||||
if (forceShowWindow) {
|
||||
root.showAndRise();
|
||||
}
|
||||
}
|
||||
function onShowHelp() {
|
||||
root.showHelp();
|
||||
if (setupWizard.visible) {
|
||||
setupWizard.closeWizard()
|
||||
}
|
||||
|
||||
root.showAndRise();
|
||||
}
|
||||
function onShowMainWindow() {
|
||||
root.showAndRise();
|
||||
}
|
||||
function onShowSettings() {
|
||||
if (setupWizard.visible) {
|
||||
setupWizard.closeWizard()
|
||||
}
|
||||
root.showSettings();
|
||||
root.showAndRise();
|
||||
}
|
||||
|
||||
target: Backend
|
||||
}
|
||||
Connections {
|
||||
function onCountChanged(count) {
|
||||
layoutForUserCount(count);
|
||||
}
|
||||
|
||||
target: Backend.users
|
||||
}
|
||||
StackLayout {
|
||||
id: contentLayout
|
||||
|
||||
property bool _showSetup: false
|
||||
|
||||
anchors.fill: parent
|
||||
currentIndex: {
|
||||
// show welcome when there are no users
|
||||
if (Backend.users.count === 0) {
|
||||
return 1;
|
||||
}
|
||||
const u = Backend.users.get(0);
|
||||
if (!u) {
|
||||
console.trace();
|
||||
console.log("empty user");
|
||||
return 1;
|
||||
}
|
||||
if ((Backend.users.count === 1) && (u.state === EUserState.SignedOut)) {
|
||||
showSignIn(u.primaryEmailOrUsername());
|
||||
return 0;
|
||||
}
|
||||
if (contentLayout._showSetup) {
|
||||
return 2;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
currentIndex: 0
|
||||
|
||||
ContentWrapper {
|
||||
// 0
|
||||
@ -160,30 +163,24 @@ ApplicationWindow {
|
||||
root.close();
|
||||
Backend.quit();
|
||||
}
|
||||
onShowSetupGuide: function (user, address) {
|
||||
root.showSetup(user, address);
|
||||
onShowClientConfigurator: function (user, address, justLoggedIn) {
|
||||
root.showClientConfigurator(user, address, justLoggedIn);
|
||||
}
|
||||
onShowLogin: function (username) {
|
||||
root.showLogin(username);
|
||||
}
|
||||
}
|
||||
WelcomeGuide {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true // 1
|
||||
colorScheme: root.colorScheme
|
||||
}
|
||||
SetupGuide {
|
||||
// 2
|
||||
id: setupGuide
|
||||
SetupWizard {
|
||||
id: setupWizard
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
colorScheme: root.colorScheme
|
||||
|
||||
onDismissed: {
|
||||
root.showSetup(null, "");
|
||||
onBugReportRequested: {
|
||||
contentWrapper.showBugReport();
|
||||
}
|
||||
onFinished: {
|
||||
// TODO: Do not close window. Trigger Backend to check that
|
||||
// there is a successfully connected client. Then Backend
|
||||
// should send another signal to close the setup guide.
|
||||
root.showSetup(null, "");
|
||||
onWizardEnded: {
|
||||
contentLayout.currentIndex = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,58 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
// This file is part of Proton Mail Bridge.
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import Proton
|
||||
import "SetupWizard"
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property ColorScheme colorScheme
|
||||
|
||||
color: root.colorScheme.background_norm
|
||||
|
||||
signal startSetup()
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
// we use the setup wizard left pane (onboarding version)
|
||||
LeftPane {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillHeight: true
|
||||
Layout.preferredWidth: ProtonStyle.wizard_pane_width
|
||||
colorScheme: root.colorScheme
|
||||
wizard: setupWizard
|
||||
|
||||
Component.onCompleted: {
|
||||
showNoAccount();
|
||||
}
|
||||
onStartSetup: {
|
||||
root.startSetup();
|
||||
}
|
||||
}
|
||||
Image {
|
||||
id: mailLogoWithWordmark
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.bottomMargin: ProtonStyle.wizard_window_margin
|
||||
height: sourceSize.height
|
||||
source: root.colorScheme.mail_logo_with_wordmark
|
||||
sourceSize.height: 36
|
||||
sourceSize.width: 134
|
||||
width: sourceSize.width
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -380,7 +380,7 @@ QtObject {
|
||||
}
|
||||
property Notification diskFull: Notification {
|
||||
brief: title
|
||||
description: qsTr("Quit Bridge and free disk space or disable the local cache (not recommended).")
|
||||
description: qsTr("Quit Bridge and free disk space or move the local cache to another disk.")
|
||||
group: Notifications.Group.Configuration | Notifications.Group.Dialogs
|
||||
icon: "./icons/ic-exclamation-circle-filled.svg"
|
||||
title: qsTr("Your disk is almost full")
|
||||
@ -453,7 +453,7 @@ QtObject {
|
||||
brief: title
|
||||
description: qsTr("Changing between split and combined address mode will require you to delete your account(s) from your email client and begin the setup process from scratch.")
|
||||
group: Notifications.Group.Configuration | Notifications.Group.Dialogs
|
||||
icon: "/qml/icons/ic-question-circle.svg"
|
||||
icon: "./icons/ic-question-circle.svg"
|
||||
title: qsTr("Enable split mode?")
|
||||
type: Notification.NotificationType.Warning
|
||||
|
||||
@ -728,10 +728,12 @@ QtObject {
|
||||
}
|
||||
property Notification noKeychain: Notification {
|
||||
brief: title
|
||||
description: qsTr("Bridge is not able to detect a supported password manager (pass or secret-service). Please install and setup supported password manager and restart the application.")
|
||||
description: Backend.goos === "darwin" ?
|
||||
qsTr("Bridge is not able to access your keychain. Please make sure your keychain is not locked and restart the application.") :
|
||||
qsTr("Bridge is not able to detect a supported password manager (pass or secret-service). Please install and setup supported password manager and restart the application.")
|
||||
group: Notifications.Group.Dialogs | Notifications.Group.Configuration
|
||||
icon: "./icons/ic-exclamation-circle-filled.svg"
|
||||
title: qsTr("No keychain available")
|
||||
title: Backend.goos === "darwin" ? qsTr("Cannot access keychain") : qsTr("No keychain available")
|
||||
type: Notification.NotificationType.Danger
|
||||
|
||||
action: [
|
||||
@ -788,8 +790,6 @@ QtObject {
|
||||
}
|
||||
}
|
||||
property Notification rebuildKeychain: Notification {
|
||||
property var supportLink: "https://proton.me/support/bridge"
|
||||
|
||||
brief: title
|
||||
description: qsTr("Bridge is not able to access your macOS keychain. Please consult the instructions on our support page.")
|
||||
group: Notifications.Group.Dialogs | Notifications.Group.Configuration
|
||||
@ -802,8 +802,7 @@ QtObject {
|
||||
text: qsTr("Open the support page")
|
||||
|
||||
onTriggered: {
|
||||
Backend.notifyKBArticleClicked(root.rebuildKeychain.supportLink);
|
||||
Qt.openUrlExternally(root.rebuildKeychain.supportLink);
|
||||
Backend.openKBArticle();
|
||||
Backend.quit();
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,11 +23,13 @@ T.Button {
|
||||
property bool borderless: false
|
||||
property ColorScheme colorScheme
|
||||
readonly property bool hasTextAndIcon: (control.text !== "") && (iconImage.source.toString().length > 0)
|
||||
property bool iconOnTheLeft: false
|
||||
readonly property bool isIcon: control.text === ""
|
||||
property int labelType: Proton.Label.LabelType.Body
|
||||
property bool loading: false
|
||||
readonly property bool primary: !secondary
|
||||
property alias secondary: control.flat
|
||||
property bool secondaryIsOpaque: false
|
||||
property alias textHorizontalAlignment: label.horizontalAlignment
|
||||
property alias textVerticalAlignment: label.verticalAlignment
|
||||
|
||||
@ -77,7 +79,7 @@ T.Button {
|
||||
if (control.loading) {
|
||||
return control.colorScheme.interaction_default_hover;
|
||||
}
|
||||
return control.colorScheme.interaction_default;
|
||||
return secondaryIsOpaque ? control.colorScheme.background_norm : control.colorScheme.interaction_default;
|
||||
}
|
||||
} else {
|
||||
if (primary) {
|
||||
@ -103,7 +105,7 @@ T.Button {
|
||||
if (control.loading) {
|
||||
return control.colorScheme.interaction_default_hover;
|
||||
}
|
||||
return control.colorScheme.interaction_default;
|
||||
return secondaryIsOpaque ? control.colorScheme.background_norm : control.colorScheme.interaction_default;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -115,6 +117,7 @@ T.Button {
|
||||
}
|
||||
contentItem: RowLayout {
|
||||
id: _contentItem
|
||||
layoutDirection: iconOnTheLeft ? Qt.RightToLeft : Qt.LeftToRight
|
||||
spacing: control.hasTextAndIcon ? control.spacing : 0
|
||||
|
||||
Proton.Label {
|
||||
@ -128,12 +131,13 @@ T.Button {
|
||||
return control.colorScheme.text_norm;
|
||||
}
|
||||
}
|
||||
colorScheme: root.colorScheme
|
||||
colorScheme: control.colorScheme
|
||||
elide: Text.ElideRight
|
||||
horizontalAlignment: Qt.AlignHCenter
|
||||
opacity: control.enabled || control.loading ? 1.0 : 0.5
|
||||
text: control.text
|
||||
type: labelType
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
visible: !control.isIcon
|
||||
}
|
||||
ColorImage {
|
||||
|
||||
@ -48,6 +48,7 @@ QtObject {
|
||||
property color interaction_weak_active
|
||||
property color interaction_weak_hover
|
||||
property string logo_img
|
||||
property string mail_logo_with_wordmark
|
||||
|
||||
// Primary
|
||||
property color primary_norm
|
||||
@ -82,7 +83,4 @@ QtObject {
|
||||
// Text
|
||||
property color text_norm
|
||||
property color text_weak
|
||||
|
||||
// Images
|
||||
property string welcome_img
|
||||
}
|
||||
|
||||
@ -0,0 +1,88 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
// This file is part of Proton Mail Bridge.
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
property var callback: null
|
||||
property ColorScheme colorScheme
|
||||
property bool external: false
|
||||
property string link: "#"
|
||||
property string text: ""
|
||||
|
||||
function clear() {
|
||||
root.callback = null;
|
||||
root.text = "";
|
||||
root.link = "";
|
||||
root.external = false;
|
||||
}
|
||||
function link(url, text) {
|
||||
return label.link(url, text);
|
||||
}
|
||||
function setCallback(callback, linkText, external) {
|
||||
root.callback = callback;
|
||||
root.text = linkText;
|
||||
root.link = "#"; // Cannot be empty, otherwise the text is not an hyperlink.
|
||||
root.external = external;
|
||||
}
|
||||
function setLink(linkURL, linkText, external) {
|
||||
root.callback = null;
|
||||
root.text = linkText;
|
||||
root.link = linkURL;
|
||||
root.external = external;
|
||||
}
|
||||
|
||||
Label {
|
||||
id: label
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
colorScheme: root.colorScheme
|
||||
text: label.link(root.link, root.text)
|
||||
type: Label.LabelType.Body
|
||||
|
||||
onLinkActivated: function (link) {
|
||||
if ((link !== "#") && (link.length > 0)) {
|
||||
Qt.openUrlExternally(link);
|
||||
}
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
}
|
||||
ColorImage {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
color: label.linkColor
|
||||
height: sourceSize.height
|
||||
source: "/qml/icons/ic-external-link.svg"
|
||||
sourceSize.height: 16
|
||||
sourceSize.width: 16
|
||||
visible: external
|
||||
width: sourceSize.width
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onClicked: {
|
||||
label.onLinkActivated(root.link);
|
||||
}
|
||||
}
|
||||
}
|
||||
HoverHandler {
|
||||
acceptedDevices: PointerDevice.Mouse
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
@ -20,21 +20,21 @@ import "."
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
property real account_hover_radius: 12 * root.px // px
|
||||
property real account_row_radius: 12 * root.px // px
|
||||
property real avatar_radius: 8 * root.px // px
|
||||
property real banner_radius: 12 * root.px // px
|
||||
property real big_avatar_radius: 12 * root.px // px
|
||||
property int account_hover_radius: 12
|
||||
property int account_row_radius: 12
|
||||
property int avatar_radius: 8
|
||||
property int banner_radius: 12
|
||||
property int big_avatar_radius: 12
|
||||
property int body_font_size: 14
|
||||
property real body_letter_spacing: 0.2 * root.px
|
||||
property real body_letter_spacing: 0.2
|
||||
property int body_line_height: 20
|
||||
property real button_radius: 8 * root.px // px
|
||||
property int button_radius: 8
|
||||
property int caption_font_size: 12
|
||||
property real caption_letter_spacing: 0.4 * root.px
|
||||
property real caption_letter_spacing: 0.4
|
||||
property int caption_line_height: 16
|
||||
property real card_radius: 12 * root.px // px
|
||||
property real checkbox_radius: 4 * root.px // px
|
||||
property real context_item_radius: 8 * root.px // px
|
||||
property int card_radius: 12
|
||||
property int checkbox_radius: 4
|
||||
property int context_item_radius: 8
|
||||
property ColorScheme currentStyle: lightStyle
|
||||
property ColorScheme darkProminentStyle: ColorScheme {
|
||||
id: _darkProminentStyle
|
||||
@ -72,6 +72,7 @@ QtObject {
|
||||
interaction_weak_active: "#6D697D"
|
||||
interaction_weak_hover: "#5B576B"
|
||||
logo_img: "/qml/icons/product_logos_dark.svg"
|
||||
mail_logo_with_wordmark: "/qml/icons/img-mail-logo-wordmark-dark.svg"
|
||||
|
||||
// Primary
|
||||
primary_norm: "#8A6EFF"
|
||||
@ -105,9 +106,6 @@ QtObject {
|
||||
// Text
|
||||
text_norm: "#FFFFFF"
|
||||
text_weak: "#A7A4B5"
|
||||
|
||||
// Images
|
||||
welcome_img: "/qml/icons/img-welcome-dark.png"
|
||||
}
|
||||
property ColorScheme darkStyle: ColorScheme {
|
||||
id: _darkStyle
|
||||
@ -145,6 +143,7 @@ QtObject {
|
||||
interaction_weak_active: "#6D697D"
|
||||
interaction_weak_hover: "#5B576B"
|
||||
logo_img: "/qml/icons/product_logos_dark.svg"
|
||||
mail_logo_with_wordmark: "/qml/icons/img-mail-logo-wordmark-dark.svg"
|
||||
|
||||
// Primary
|
||||
primary_norm: "#8A6EFF"
|
||||
@ -178,11 +177,8 @@ QtObject {
|
||||
// Text
|
||||
text_norm: "#FFFFFF"
|
||||
text_weak: "#A7A4B5"
|
||||
|
||||
// Images
|
||||
welcome_img: "/qml/icons/img-welcome-dark.png"
|
||||
}
|
||||
property real dialog_radius: 12 * root.px // px
|
||||
property int dialog_radius: 12
|
||||
property int fontWeight_100: Font.Thin
|
||||
property int fontWeight_200: Font.Light
|
||||
property int fontWeight_300: Font.ExtraLight
|
||||
@ -206,7 +202,7 @@ QtObject {
|
||||
}
|
||||
property int heading_font_size: 28
|
||||
property int heading_line_height: 36
|
||||
property real input_radius: 8 * root.px // px
|
||||
property int input_radius: 8
|
||||
property int lead_font_size: 18
|
||||
property int lead_line_height: 26
|
||||
property ColorScheme lightProminentStyle: ColorScheme {
|
||||
@ -245,6 +241,7 @@ QtObject {
|
||||
interaction_weak_active: "#8A6EFF"
|
||||
interaction_weak_hover: "#6D4AFF"
|
||||
logo_img: "/qml/icons/product_logos_dark.svg"
|
||||
mail_logo_with_wordmark: "/qml/icons/img-mail-logo-wordmark-dark.svg"
|
||||
|
||||
// Primary
|
||||
primary_norm: "#8A6EFF"
|
||||
@ -278,9 +275,6 @@ QtObject {
|
||||
// Text
|
||||
text_norm: "#FFFFFF"
|
||||
text_weak: "#9282D4"
|
||||
|
||||
// Images
|
||||
welcome_img: "/qml/icons/img-welcome-dark.png"
|
||||
}
|
||||
// TODO: Once we will use Qt >=5.15 this should be refactored with inline components as follows:
|
||||
// https://doc.qt.io/qt-5/qtqml-documents-definetypes.html#inline-components
|
||||
@ -325,6 +319,7 @@ QtObject {
|
||||
interaction_weak_active: "#A8A6A3"
|
||||
interaction_weak_hover: "#C2BFBC"
|
||||
logo_img: "/qml/icons/product_logos.svg"
|
||||
mail_logo_with_wordmark: "/qml/icons/img-mail-logo-wordmark.svg"
|
||||
|
||||
// Primary
|
||||
primary_norm: "#6D4AFF"
|
||||
@ -358,13 +353,35 @@ QtObject {
|
||||
// Text
|
||||
text_norm: "#0C0C14"
|
||||
text_weak: "#706D6B"
|
||||
|
||||
// Images
|
||||
welcome_img: "/qml/icons/img-welcome.png"
|
||||
}
|
||||
property real progress_bar_radius: 3 * root.px // px
|
||||
property real px: 1.00 // px
|
||||
property int progress_bar_radius: 3
|
||||
property int title_font_size: 20
|
||||
property int title_line_height: 24
|
||||
property real tooltip_radius: 8 * root.px // px
|
||||
property int tooltip_radius: 8
|
||||
|
||||
// WebView overlay styling
|
||||
property int web_view_button_width: 320
|
||||
property int web_view_corner_radius: 10
|
||||
property int web_view_overlay_button_vertical_margin: 10
|
||||
property int web_view_overlay_horizontal_padding: 10
|
||||
property int web_view_overlay_horizontal_margin: 250
|
||||
property int web_view_overlay_vertical_margin: 50
|
||||
property real web_view_overlay_opacity: 0.6
|
||||
property int web_view_overlay_vertical_padding: web_view_corner_radius
|
||||
property int web_view_overley_border_width: 1
|
||||
|
||||
property int window_default_height: 780
|
||||
property int window_default_width: 1080
|
||||
property int window_minimum_height: 650
|
||||
property int window_minimum_width: window_default_width
|
||||
|
||||
// setup wizard constant
|
||||
property int wizard_pane_bottomMargin: 92
|
||||
property int wizard_pane_width: 364
|
||||
property int wizard_window_margin: 40
|
||||
property int wizard_spacing_extra_large: 32
|
||||
property int wizard_spacing_extra_small: 4
|
||||
property int wizard_spacing_large: 24
|
||||
property int wizard_spacing_medium: 16
|
||||
property int wizard_spacing_small: 8
|
||||
}
|
||||
|
||||
@ -238,12 +238,12 @@ FocusScope {
|
||||
bottomPadding: 8
|
||||
color: {
|
||||
if (!control.enabled) {
|
||||
return root.colorScheme.text_disabled
|
||||
return root.colorScheme.text_disabled;
|
||||
}
|
||||
if (control.readOnly) {
|
||||
return root.colorScheme.text_hint
|
||||
return root.colorScheme.text_hint;
|
||||
}
|
||||
return root.colorScheme.text_norm
|
||||
return root.colorScheme.text_norm;
|
||||
}
|
||||
|
||||
// enforcing default focus here within component
|
||||
|
||||
@ -114,6 +114,9 @@ FocusScope {
|
||||
function getText(start, end) {
|
||||
control.getText(start, end);
|
||||
}
|
||||
function hidePassword() {
|
||||
eyeButton.checked = false;
|
||||
}
|
||||
function insert(position, text) {
|
||||
control.insert(position, text);
|
||||
}
|
||||
@ -147,6 +150,9 @@ FocusScope {
|
||||
function selectWord() {
|
||||
control.selectWord();
|
||||
}
|
||||
function showPassword() {
|
||||
eyeButton.checked = true;
|
||||
}
|
||||
function undo() {
|
||||
control.undo();
|
||||
}
|
||||
|
||||
@ -28,6 +28,7 @@ CheckBox 4.0 CheckBox.qml
|
||||
ComboBox 4.0 ComboBox.qml
|
||||
Dialog 4.0 Dialog.qml
|
||||
Label 4.0 Label.qml
|
||||
LinkLabel 4.0 LinkLabel.qml
|
||||
Menu 4.0 Menu.qml
|
||||
MenuItem 4.0 MenuItem.qml
|
||||
Popup 4.0 Popup.qml
|
||||
@ -36,3 +37,4 @@ Switch 4.0 Switch.qml
|
||||
TextArea 4.0 TextArea.qml
|
||||
TextField 4.0 TextField.qml
|
||||
Toggle 4.0 Toggle.qml
|
||||
WebFrame 4.0 WebFrame.qml
|
||||
|
||||
@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title></title>
|
||||
<style>
|
||||
body {font-family: sans-serif}
|
||||
h1 { font-size: 1.5em; text-align: center; margin-bottom: 2em;}
|
||||
p {text-align: justify; margin-bottom: 2em; }
|
||||
p.standfirst { font-weight: bold;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
%1
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,19 @@
|
||||
<h1>Why do I need bridge?</h1>
|
||||
<p class="standfirst">
|
||||
Proton does not have access to the content of your messages, so it cannot share your unencrypted messages with your email client from the
|
||||
Proton servers.
|
||||
</p>
|
||||
<p>
|
||||
Email clients such as Microsoft Outlook, Mozilla Thunderbird and Apple Mail use standard protocols named IMAP and SMTP to receive and send emails.
|
||||
</p>
|
||||
<p>
|
||||
Even though the IMAP and SMTP protocols can use secure channels (using SSL/TLS), they do not offer support for encrypted messages.
|
||||
Because Proton does not have access to the content of your messages, it is not possible to configure your email client to connect directly to
|
||||
Proton servers.
|
||||
</p>
|
||||
<p>
|
||||
The key to solving this problem is Bridge. Once installed on your computer and connected to your Proton account, Bridge can access your
|
||||
encrypted messages stored on the Proton servers. Bridge integrates an IMAP and a SMTP server that run on your computer and are accessible only
|
||||
to applications executing on your machine. Your email client connects to these local servers and Bridge is responsible for seamlessly encrypting
|
||||
and decrypting the messages that you send and receive.
|
||||
</p>
|
||||
@ -0,0 +1,19 @@
|
||||
<h1>Why do I need to install a certificate when configuring Apple Mail with Bridge?</h1>
|
||||
<p class="standfirst">
|
||||
Apple Mail requires a secure channel for communications with email servers, and the server needs to be acknowledged as trusted.
|
||||
</p>
|
||||
<p>
|
||||
In order to communicate with Bridge, Apple Mail requires secure connections using SSL/TLS. This cryptographic protocol includes an identity
|
||||
verification system using certificates. For publicly available servers, certificates are normally issued and digitally signed by a certificate
|
||||
authority, such as Let's Encrypt. This is not possible for Bridge, as the IMAP and SMTP servers are running on your own computer, and are not
|
||||
accessible from any network (local or internet).
|
||||
</p>
|
||||
<p>
|
||||
The solution is to use a self-signed certificate. When setting up an email account where the server provides a self-signed certificate, most
|
||||
email clients will issue a warning asking you whether you trust the server or not, because the certificate was not issued by a certificate
|
||||
authority.
|
||||
</p>
|
||||
<p>
|
||||
Apple Mail requires an extra step. It will simply refuse to connect if the certificate is not set as trusted. Bridge solves this by storing this
|
||||
certificate in the macOS keychain. This operation requires that you provide your macOS account password.
|
||||
</p>
|
||||
@ -0,0 +1,21 @@
|
||||
<h1>Why is there a warning sign when installing the Bridge profile on macOS?</h1>
|
||||
<p class="standfirst">
|
||||
This warning indicates that the certificate used to secure the communication channel between Bridge and your email client is not signed by a
|
||||
trusted third party.
|
||||
</p>
|
||||
<p>
|
||||
In order to communicate with Bridge, Apple Mail requires secure connections using SSL/TLS. This cryptographic protocol includes an identity
|
||||
verification system using certificates. For publicly available servers, certificates are normally issued and digitally signed by a certificate
|
||||
authority, such as Let's Encrypt. This is not possible for Bridge, as the IMAP and SMTP servers are running on your own computer, and are not
|
||||
accessible from any network (local or internet).
|
||||
</p>
|
||||
<p>
|
||||
The solution is to use a self-signed certificate. When setting up an email account where the server provides a self-signed certificate, most
|
||||
email clients will issue a warning asking you whether you trust the server or not, because the certificate was not issued by a certificate
|
||||
authority. The client has no way of verifying that the server is who it pretends to be.
|
||||
</p>
|
||||
<p>
|
||||
You can safely ignore this warning. The check concerns only the communication between your email client and Bridge, which occurs within your
|
||||
computer. On the other end, the communication between Bridge and the Proton servers uses the HTTPS protocol, and the identity of the remote
|
||||
server is verified by Bridge.
|
||||
</p>
|
||||
@ -90,7 +90,7 @@ Item {
|
||||
icon.source: root.actionIcon
|
||||
loading: root.loading
|
||||
secondary: root.type !== SettingsItem.PrimaryButton
|
||||
text: root.actionText + (root.actionIcon !== "" ? " " : "")
|
||||
text: root.actionText
|
||||
visible: root.type === SettingsItem.Button || root.type === SettingsItem.PrimaryButton
|
||||
|
||||
onClicked: {
|
||||
|
||||
@ -1,293 +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 QtQuick.Controls.impl
|
||||
import Proton
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string address
|
||||
property ColorScheme colorScheme
|
||||
property var user
|
||||
|
||||
signal dismissed
|
||||
signal finished
|
||||
|
||||
function reset() {
|
||||
guidePages.currentIndex = 0;
|
||||
clientList.currentIndex = -1;
|
||||
actionList.currentIndex = -1;
|
||||
}
|
||||
function setupAction(actionID, clientID) {
|
||||
if (user) {
|
||||
user.setupGuideSeen = true;
|
||||
}
|
||||
switch (actionID) {
|
||||
case -1:
|
||||
root.dismissed();
|
||||
break; // dismiss
|
||||
case 0 // automatic
|
||||
:
|
||||
if (user) {
|
||||
switch (clientID) {
|
||||
case 0:
|
||||
root.user.configureAppleMail(root.address);
|
||||
Backend.notifyAutoconfigClicked("AppleMail");
|
||||
break;
|
||||
}
|
||||
}
|
||||
root.finished();
|
||||
break;
|
||||
case 1 // manual
|
||||
:
|
||||
let clientObj = clients.get(clientID);
|
||||
if (clientObj !== undefined && clientObj.link !== "") {
|
||||
Qt.openUrlExternally(clientObj.link);
|
||||
Backend.notifyKBArticleClicked(clientObj.link);
|
||||
} else {
|
||||
console.log("unexpected client index", actionID, clientID);
|
||||
}
|
||||
root.finished();
|
||||
break;
|
||||
default:
|
||||
console.log("unexpected client setup action", actionID, clientID);
|
||||
}
|
||||
}
|
||||
|
||||
implicitHeight: children[0].implicitHeight
|
||||
implicitWidth: children[0].implicitWidth
|
||||
|
||||
ListModel {
|
||||
id: clients
|
||||
|
||||
property bool haveAutoSetup: true
|
||||
property string iconSource: "/qml/icons/ic-apple-mail.svg"
|
||||
property string link: "https://proton.me/support/protonmail-bridge-clients-apple-mail"
|
||||
property string name: "Apple Mail"
|
||||
|
||||
Component.onCompleted: {
|
||||
if (Backend.goos === "darwin") {
|
||||
append({
|
||||
"name": "Apple Mail",
|
||||
"iconSource": "/qml/icons/ic-apple-mail.svg",
|
||||
"haveAutoSetup": true,
|
||||
"link": "https://proton.me/support/protonmail-bridge-clients-apple-mail"
|
||||
});
|
||||
append({
|
||||
"name": "Microsoft Outlook",
|
||||
"iconSource": "/qml/icons/ic-microsoft-outlook.svg",
|
||||
"haveAutoSetup": false,
|
||||
"link": "https://proton.me/support/protonmail-bridge-clients-macos-outlook-2019"
|
||||
});
|
||||
}
|
||||
if (Backend.goos === "windows") {
|
||||
append({
|
||||
"name": "Microsoft Outlook",
|
||||
"iconSource": "/qml/icons/ic-microsoft-outlook.svg",
|
||||
"haveAutoSetup": false,
|
||||
"link": "https://proton.me/support/protonmail-bridge-clients-windows-outlook-2019"
|
||||
});
|
||||
}
|
||||
append({
|
||||
"name": "Mozilla Thunderbird",
|
||||
"iconSource": "/qml/icons/ic-mozilla-thunderbird.svg",
|
||||
"haveAutoSetup": false,
|
||||
"link": "https://proton.me/support/protonmail-bridge-clients-windows-thunderbird"
|
||||
});
|
||||
append({
|
||||
"name": "Other",
|
||||
"iconSource": "/qml/icons/ic-other-mail-clients.svg",
|
||||
"haveAutoSetup": false,
|
||||
"link": "https://proton.me/support/protonmail-bridge-configure-client"
|
||||
});
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
anchors.fill: root
|
||||
color: root.colorScheme.background_norm
|
||||
}
|
||||
StackLayout {
|
||||
id: guidePages
|
||||
anchors.bottomMargin: 70
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 80
|
||||
anchors.rightMargin: 80
|
||||
anchors.topMargin: 30
|
||||
|
||||
ColumnLayout {
|
||||
// 0: Client selection
|
||||
id: clientView
|
||||
|
||||
property int columnWidth: 268
|
||||
|
||||
Layout.fillHeight: true
|
||||
spacing: 8
|
||||
|
||||
Label {
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Setting up email client")
|
||||
type: Label.LabelType.Heading
|
||||
}
|
||||
Label {
|
||||
color: root.colorScheme.text_weak
|
||||
colorScheme: root.colorScheme
|
||||
text: address
|
||||
type: Label.LabelType.Lead
|
||||
}
|
||||
RowLayout {
|
||||
Layout.topMargin: 32 - clientView.spacing
|
||||
spacing: 24
|
||||
|
||||
ColumnLayout {
|
||||
id: clientColumn
|
||||
Layout.alignment: Qt.AlignTop
|
||||
|
||||
Label {
|
||||
id: labelA
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Choose an email client")
|
||||
type: Label.LabelType.Body_semibold
|
||||
}
|
||||
ListView {
|
||||
id: clientList
|
||||
Layout.fillHeight: true
|
||||
model: clients
|
||||
width: clientView.columnWidth
|
||||
|
||||
delegate: Item {
|
||||
implicitHeight: clientRow.height
|
||||
implicitWidth: clientRow.width
|
||||
|
||||
ColumnLayout {
|
||||
id: clientRow
|
||||
width: clientList.width
|
||||
|
||||
RowLayout {
|
||||
Layout.bottomMargin: 12
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 12
|
||||
|
||||
ColorImage {
|
||||
height: 36
|
||||
source: model.iconSource
|
||||
sourceSize.height: 36
|
||||
}
|
||||
Label {
|
||||
Layout.leftMargin: 12
|
||||
colorScheme: root.colorScheme
|
||||
text: model.name
|
||||
type: Label.LabelType.Body
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 1
|
||||
color: root.colorScheme.border_weak
|
||||
}
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onClicked: {
|
||||
clientList.currentIndex = index;
|
||||
if (!model.haveAutoSetup) {
|
||||
root.setupAction(1, index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
highlight: Rectangle {
|
||||
color: root.colorScheme.interaction_default_active
|
||||
radius: ProtonStyle.context_item_radius
|
||||
}
|
||||
}
|
||||
}
|
||||
ColumnLayout {
|
||||
id: actionColumn
|
||||
Layout.alignment: Qt.AlignTop
|
||||
visible: clientList.currentIndex >= 0 && clients.get(clientList.currentIndex).haveAutoSetup
|
||||
|
||||
Label {
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Choose configuration mode")
|
||||
type: Label.LabelType.Body_semibold
|
||||
}
|
||||
ListView {
|
||||
id: actionList
|
||||
Layout.fillHeight: true
|
||||
model: [qsTr("Configure automatically"), qsTr("Configure manually")]
|
||||
width: clientView.columnWidth
|
||||
|
||||
delegate: Item {
|
||||
implicitHeight: children[0].height
|
||||
implicitWidth: children[0].width
|
||||
|
||||
ColumnLayout {
|
||||
width: actionList.width
|
||||
|
||||
Label {
|
||||
Layout.bottomMargin: 20
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.topMargin: 20
|
||||
colorScheme: root.colorScheme
|
||||
text: modelData
|
||||
type: Label.LabelType.Body
|
||||
}
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 1
|
||||
color: root.colorScheme.border_weak
|
||||
}
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onClicked: {
|
||||
actionList.currentIndex = index;
|
||||
root.setupAction(index, clientList.currentIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
highlight: Rectangle {
|
||||
color: root.colorScheme.interaction_default_active
|
||||
radius: ProtonStyle.context_item_radius
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
flat: true
|
||||
text: qsTr("Set up later")
|
||||
|
||||
onClicked: {
|
||||
root.setupAction(-1, -1);
|
||||
if (user) {
|
||||
user.setupGuideSeen = true;
|
||||
}
|
||||
root.dismissed();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,274 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
// This file is part of Proton Mail Bridge.
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
import QtQml
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
|
||||
Item {
|
||||
id: root
|
||||
enum Screen {
|
||||
CertificateInstall,
|
||||
ProfileInstall
|
||||
}
|
||||
|
||||
property var wizard
|
||||
|
||||
signal appleMailAutoconfigCertificateInstallPageShown
|
||||
signal appleMailAutoconfigProfileInstallPageShow
|
||||
|
||||
function showAutoconfig() {
|
||||
if (Backend.isTLSCertificateInstalled()) {
|
||||
showProfileInstall();
|
||||
} else {
|
||||
showCertificateInstall();
|
||||
}
|
||||
}
|
||||
function showCertificateInstall() {
|
||||
certificateInstall.reset();
|
||||
stack.currentIndex = ClientConfigAppleMail.Screen.CertificateInstall;
|
||||
appleMailAutoconfigCertificateInstallPageShown();
|
||||
}
|
||||
function showProfileInstall() {
|
||||
profileInstall.reset();
|
||||
stack.currentIndex = ClientConfigAppleMail.Screen.ProfileInstall;
|
||||
appleMailAutoconfigProfileInstallPageShow();
|
||||
}
|
||||
|
||||
StackLayout {
|
||||
id: stack
|
||||
anchors.fill: parent
|
||||
|
||||
// stack index 0
|
||||
Item {
|
||||
id: certificateInstall
|
||||
|
||||
property string errorString: ""
|
||||
property bool showBugReportLink: false
|
||||
property bool waitingForCert: false
|
||||
|
||||
function clearError() {
|
||||
errorString = "";
|
||||
showBugReportLink = false;
|
||||
}
|
||||
function reset() {
|
||||
waitingForCert = false;
|
||||
clearError();
|
||||
}
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
|
||||
ColumnLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: ProtonStyle.wizard_spacing_large
|
||||
|
||||
Connections {
|
||||
function onCertificateInstallCanceled() {
|
||||
certificateInstall.waitingForCert = false;
|
||||
certificateInstall.errorString = qsTr("Apple Mail cannot be configured if you do not install the certificate. Please retry.");
|
||||
certificateInstall.showBugReportLink = false;
|
||||
}
|
||||
function onCertificateInstallFailed() {
|
||||
certificateInstall.waitingForCert = false;
|
||||
certificateInstall.errorString = qsTr("An error occurred while installing the certificate.");
|
||||
certificateInstall.showBugReportLink = true;
|
||||
}
|
||||
function onCertificateInstallSuccess() {
|
||||
certificateInstall.reset();
|
||||
root.showAutoconfig();
|
||||
}
|
||||
|
||||
target: Backend
|
||||
}
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: ProtonStyle.wizard_spacing_medium
|
||||
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
colorScheme: wizard.colorScheme
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: qsTr("Install the bridge certificate")
|
||||
type: Label.LabelType.Title
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
color: colorScheme.text_weak
|
||||
colorScheme: wizard.colorScheme
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: qsTr("After clicking on the button below, a system pop-up will ask you for your credentials, please enter your macOS user credentials (not your Proton account’s) and validate.")
|
||||
type: Label.LabelType.Body
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
Image {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
height: 182
|
||||
opacity: certificateInstall.waitingForCert ? 0.3 : 1.0
|
||||
source: "/qml/icons/img-macos-cert-screenshot.png"
|
||||
width: 140
|
||||
}
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: ProtonStyle.wizard_spacing_medium
|
||||
|
||||
Button {
|
||||
Layout.fillWidth: true
|
||||
colorScheme: wizard.colorScheme
|
||||
enabled: !certificateInstall.waitingForCert
|
||||
loading: certificateInstall.waitingForCert
|
||||
text: qsTr("Install the certificate")
|
||||
|
||||
onClicked: {
|
||||
certificateInstall.clearError();
|
||||
certificateInstall.waitingForCert = true;
|
||||
Backend.installTLSCertificate();
|
||||
}
|
||||
}
|
||||
Button {
|
||||
Layout.fillWidth: true
|
||||
colorScheme: wizard.colorScheme
|
||||
enabled: !certificateInstall.waitingForCert
|
||||
secondary: true
|
||||
text: qsTr("Cancel")
|
||||
|
||||
onClicked: {
|
||||
wizard.closeWizard();
|
||||
}
|
||||
}
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: ProtonStyle.wizard_spacing_small
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: ProtonStyle.wizard_spacing_extra_small
|
||||
|
||||
ColorImage {
|
||||
color: wizard.colorScheme.signal_danger
|
||||
height: errorLabel.lineHeight
|
||||
source: "/qml/icons/ic-exclamation-circle-filled.svg"
|
||||
sourceSize.height: errorLabel.lineHeight
|
||||
visible: certificateInstall.errorString.length > 0
|
||||
}
|
||||
Label {
|
||||
id: errorLabel
|
||||
Layout.fillWidth: true
|
||||
color: wizard.colorScheme.signal_danger
|
||||
colorScheme: wizard.colorScheme
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: certificateInstall.errorString
|
||||
type: Label.LabelType.Body_semibold
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
LinkLabel {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
callback: wizard.showBugReport
|
||||
colorScheme: wizard.colorScheme
|
||||
link: "#"
|
||||
text: qsTr("Report the problem")
|
||||
visible: certificateInstall.showBugReportLink
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// stack index 1
|
||||
Item {
|
||||
id: profileInstall
|
||||
|
||||
property bool profilePaneLaunched: false
|
||||
|
||||
function reset() {
|
||||
profilePaneLaunched = false;
|
||||
}
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
|
||||
ColumnLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: ProtonStyle.wizard_spacing_large
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: ProtonStyle.wizard_spacing_medium
|
||||
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
colorScheme: wizard.colorScheme
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: qsTr("Install the profile")
|
||||
type: Label.LabelType.Title
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
color: colorScheme.text_weak
|
||||
colorScheme: wizard.colorScheme
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: qsTr("A system pop-up will appear. Double click on the entry with your email, and click ’Install’ in the dialog that appears.")
|
||||
type: Label.LabelType.Body
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
Image {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
height: 102
|
||||
source: "/qml/icons/img-macos-profile-screenshot.png"
|
||||
width: 364
|
||||
}
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: ProtonStyle.wizard_spacing_medium
|
||||
|
||||
Button {
|
||||
Layout.fillWidth: true
|
||||
colorScheme: wizard.colorScheme
|
||||
text: profileInstall.profilePaneLaunched ? qsTr("I have installed the profile") : qsTr("Install the profile")
|
||||
|
||||
onClicked: {
|
||||
if (profileInstall.profilePaneLaunched) {
|
||||
wizard.showClientConfigEnd();
|
||||
} else {
|
||||
wizard.user.configureAppleMail(wizard.address);
|
||||
profileInstall.profilePaneLaunched = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
Button {
|
||||
Layout.fillWidth: true
|
||||
colorScheme: wizard.colorScheme
|
||||
secondary: true
|
||||
text: qsTr("Cancel")
|
||||
|
||||
onClicked: {
|
||||
wizard.closeWizard();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,99 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
// This file is part of Proton Mail Bridge.
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
import QtQml
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property ColorScheme colorScheme: wizard.colorScheme
|
||||
property var wizard
|
||||
|
||||
clip: true
|
||||
color: colorScheme.background_norm
|
||||
|
||||
Item {
|
||||
id: centeredContainer
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 84
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 32
|
||||
clip: true
|
||||
width: ProtonStyle.wizard_pane_width
|
||||
|
||||
ColumnLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: ProtonStyle.wizard_spacing_medium
|
||||
|
||||
Image {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.preferredHeight: sourceSize.height
|
||||
Layout.preferredWidth: sourceSize.width
|
||||
source: "/qml/icons/img-client-config-success.svg"
|
||||
sourceSize.height: 104
|
||||
sourceSize.width: 190
|
||||
}
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
colorScheme: wizard.colorScheme
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: qsTr("Congratulations! You're all setup")
|
||||
type: Label.LabelType.Title
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
color: colorScheme.text_weak
|
||||
colorScheme: wizard.colorScheme
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: wizard.address
|
||||
type: Label.LabelType.Body
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
colorScheme: wizard.colorScheme
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: qsTr("Your client has been configured. While complete synchronization might take some time, you can already send encrypted emails.")
|
||||
type: Label.LabelType.Body
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
Button {
|
||||
Layout.fillWidth: true
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Done")
|
||||
|
||||
onClicked: wizard.closeWizard()
|
||||
}
|
||||
}
|
||||
}
|
||||
Image {
|
||||
id: mailLogoWithWordmark
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 32
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
height: 36
|
||||
source: root.colorScheme.mail_logo_with_wordmark
|
||||
sourceSize.height: height
|
||||
sourceSize.width: width
|
||||
width: 134
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,163 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
// This file is part of Proton Mail Bridge.
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
import QtQml
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import ".."
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property ColorScheme colorScheme: wizard.colorScheme
|
||||
readonly property bool genericClient: SetupWizard.Client.Generic === wizard.client
|
||||
property var wizard
|
||||
|
||||
clip: true
|
||||
color: colorScheme.background_weak
|
||||
|
||||
Item {
|
||||
id: centeredContainer
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
width: 640
|
||||
|
||||
ColumnLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: ProtonStyle.wizard_spacing_medium
|
||||
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
colorScheme: wizard.colorScheme
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: qsTr("Configure %1").arg(wizard.clientName())
|
||||
type: Label.LabelType.Title
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
border.color: colorScheme.border_norm
|
||||
border.width: 1
|
||||
color: "transparent"
|
||||
height: childrenRect.height + 2 * ProtonStyle.wizard_spacing_medium
|
||||
radius: 12
|
||||
|
||||
RowLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.margins: ProtonStyle.wizard_spacing_medium
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
spacing: ProtonStyle.wizard_spacing_small
|
||||
|
||||
Label {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
colorScheme: wizard.colorScheme
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
text: (SetupWizard.Client.MicrosoftOutlook === wizard.client) ? qsTr("Are you unsure about your Outlook version or do you need assistance in configuring Outlook?") : qsTr("Do you need assistance in configuring %1?".arg(wizard.clientName()))
|
||||
type: Label.LabelType.Body
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
Button {
|
||||
colorScheme: root.colorScheme
|
||||
icon.source: "/qml/icons/ic-external-link.svg"
|
||||
text: qsTr("Open guide")
|
||||
|
||||
onClicked: function () {
|
||||
Backend.openKBArticle(wizard.setupGuideLink());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
border.color: colorScheme.signal_warning
|
||||
border.width: 1
|
||||
color: "transparent"
|
||||
height: childrenRect.height + 2 * ProtonStyle.wizard_spacing_medium
|
||||
radius: ProtonStyle.banner_radius
|
||||
|
||||
RowLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.margins: ProtonStyle.wizard_spacing_medium
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
spacing: ProtonStyle.wizard_spacing_medium
|
||||
|
||||
ColorImage {
|
||||
id: image
|
||||
height: 36
|
||||
source: "/qml/icons/ic-warning-orange.svg"
|
||||
sourceSize.height: height
|
||||
sourceSize.width: width
|
||||
width: height
|
||||
}
|
||||
Label {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
colorScheme: wizard.colorScheme
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
text: qsTr("Copy paste the provided configuration parameters. Use the password below (not your Proton password), when adding your Proton account to %1.".arg(wizard.clientName()))
|
||||
type: Label.LabelType.Body
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
}
|
||||
RowLayout {
|
||||
id: configuration
|
||||
Layout.fillWidth: true
|
||||
spacing: ProtonStyle.wizard_spacing_extra_large
|
||||
|
||||
Configuration {
|
||||
Layout.fillWidth: true
|
||||
colorScheme: wizard.colorScheme
|
||||
highlightPassword: true
|
||||
hostname: Backend.hostname
|
||||
password: wizard.user ? wizard.user.password : ""
|
||||
port: Backend.imapPort.toString()
|
||||
security: Backend.useSSLForIMAP ? "SSL" : "STARTTLS"
|
||||
title: "IMAP"
|
||||
username: wizard.address
|
||||
}
|
||||
Configuration {
|
||||
Layout.fillWidth: true
|
||||
colorScheme: wizard.colorScheme
|
||||
highlightPassword: true
|
||||
hostname: Backend.hostname
|
||||
password: wizard.user ? wizard.user.password : ""
|
||||
port: Backend.smtpPort.toString()
|
||||
security: Backend.useSSLForSMTP ? "SSL" : "STARTTLS"
|
||||
title: "SMTP"
|
||||
username: wizard.address
|
||||
}
|
||||
}
|
||||
Button {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.preferredWidth: 304
|
||||
colorScheme: root.colorScheme
|
||||
secondary: true
|
||||
secondaryIsOpaque: true
|
||||
text: qsTr("Continue")
|
||||
|
||||
onClicked: wizard.showClientConfigEnd()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,101 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
// This file is part of Proton Mail Bridge.
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
import QtQml
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
readonly property bool onMacOS: (Backend.goos === "darwin")
|
||||
readonly property bool onWindows: (Backend.goos === "windows")
|
||||
property var wizard
|
||||
|
||||
ColumnLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: ProtonStyle.wizard_spacing_medium
|
||||
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: ProtonStyle.wizard_spacing_medium
|
||||
colorScheme: wizard.colorScheme
|
||||
horizontalAlignment: Qt.AlignHCenter
|
||||
text: qsTr("Select your email client")
|
||||
type: Label.LabelType.Title
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
ClientListItem {
|
||||
Layout.fillWidth: true
|
||||
colorScheme: wizard.colorScheme
|
||||
iconSource: "/qml/icons/ic-apple-mail.svg"
|
||||
text: "Apple Mail"
|
||||
visible: root.onMacOS
|
||||
|
||||
onClicked: {
|
||||
wizard.client = SetupWizard.Client.AppleMail;
|
||||
wizard.showAppleMailAutoConfig();
|
||||
}
|
||||
}
|
||||
ClientListItem {
|
||||
Layout.fillWidth: true
|
||||
colorScheme: wizard.colorScheme
|
||||
iconSource: "/qml/icons/ic-microsoft-outlook.svg"
|
||||
text: "Microsoft Outlook"
|
||||
visible: root.onMacOS || root.onWindows
|
||||
|
||||
onClicked: {
|
||||
wizard.client = SetupWizard.Client.MicrosoftOutlook;
|
||||
wizard.showClientParams();
|
||||
}
|
||||
}
|
||||
ClientListItem {
|
||||
Layout.fillWidth: true
|
||||
colorScheme: wizard.colorScheme
|
||||
iconSource: "/qml/icons/ic-mozilla-thunderbird.svg"
|
||||
text: "Mozilla Thunderbird"
|
||||
|
||||
onClicked: {
|
||||
wizard.client = SetupWizard.Client.MozillaThunderbird;
|
||||
wizard.showClientParams();
|
||||
}
|
||||
}
|
||||
ClientListItem {
|
||||
Layout.fillWidth: true
|
||||
colorScheme: wizard.colorScheme
|
||||
iconSource: "/qml/icons/ic-other-mail-clients.svg"
|
||||
text: qsTr("Other")
|
||||
|
||||
onClicked: {
|
||||
wizard.client = SetupWizard.Client.Generic;
|
||||
wizard.showClientParams();
|
||||
}
|
||||
}
|
||||
Button {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 20
|
||||
colorScheme: wizard.colorScheme
|
||||
secondary: true
|
||||
secondaryIsOpaque: true
|
||||
text: qsTr("Setup later")
|
||||
|
||||
onClicked: {
|
||||
root.wizard.closeWizard();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,70 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
// This file is part of Proton Mail Bridge.
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
import QtQml
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property ColorScheme colorScheme
|
||||
property string iconSource
|
||||
property string text
|
||||
|
||||
signal clicked
|
||||
|
||||
border.color: colorScheme.border_norm
|
||||
border.width: 1
|
||||
color: {
|
||||
if (mouseArea.pressed) {
|
||||
return colorScheme.interaction_default_active;
|
||||
}
|
||||
if (mouseArea.containsMouse) {
|
||||
return colorScheme.interaction_default_hover;
|
||||
}
|
||||
return colorScheme.background_norm;
|
||||
}
|
||||
height: 68
|
||||
radius: ProtonStyle.banner_radius
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: ProtonStyle.wizard_spacing_medium
|
||||
|
||||
ColorImage {
|
||||
height: sourceSize.height
|
||||
source: iconSource
|
||||
sourceSize.height: 36
|
||||
}
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 12
|
||||
colorScheme: root.colorScheme
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
text: root.text
|
||||
type: Label.LabelType.Body
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
acceptedButtons: Qt.LeftButton
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
|
||||
onClicked: {
|
||||
root.clicked();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
// This file is part of Proton Mail Bridge.
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
import QtQml
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
|
||||
Button {
|
||||
id: root
|
||||
|
||||
property var wizard
|
||||
readonly property int _iconPadding: 8 // The SVG image we use has internal padding that we need to compensate for alignment.
|
||||
readonly property int _iconSize: 24
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: ProtonStyle.wizard_window_margin - _iconPadding
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: ProtonStyle.wizard_window_margin - _iconPadding
|
||||
colorScheme: wizard.colorScheme
|
||||
horizontalPadding: 0
|
||||
icon.color: wizard.colorScheme.text_weak
|
||||
icon.height: _iconSize
|
||||
icon.source: "/qml/icons/ic-question-circle.svg"
|
||||
icon.width: _iconSize
|
||||
verticalPadding: 0
|
||||
|
||||
onClicked: {
|
||||
menu.popup(-menu.width + root.width, -menu.height);
|
||||
}
|
||||
|
||||
Menu {
|
||||
id: menu
|
||||
colorScheme: root.colorScheme
|
||||
modal: true
|
||||
|
||||
MenuItem {
|
||||
id: getHelpItem
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Get help")
|
||||
|
||||
onClicked: {
|
||||
Backend.openKBArticle();
|
||||
}
|
||||
}
|
||||
MenuItem {
|
||||
id: reportAProblemItem
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Report a problem")
|
||||
|
||||
onClicked: {
|
||||
wizard.showBugReport();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,145 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
// This file is part of Proton Mail Bridge.
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
import QtQml
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
readonly property string addAccountTitle: qsTr("Add a Proton Mail account")
|
||||
readonly property string welcomeDescription: qsTr("Bridge is the gateway between your Proton account and your email client. It runs in the background and encrypts and decrypts your messages seamlessly. ");
|
||||
readonly property string welcomeTitle: qsTr("Welcome to\nProton Mail Bridge")
|
||||
readonly property string welcomeImage: "/qml/icons/img-welcome.svg"
|
||||
readonly property int welcomeImageHeight: 148;
|
||||
readonly property int welcomeImageWidth: 265;
|
||||
|
||||
property int iconHeight
|
||||
property string iconSource
|
||||
property int iconWidth
|
||||
property var wizard
|
||||
property ColorScheme colorScheme
|
||||
property var _colorScheme: wizard ? wizard.colorScheme : colorScheme
|
||||
|
||||
signal startSetup()
|
||||
|
||||
function showAppleMailAutoconfigCertificateInstall() {
|
||||
showAppleMailAutoconfigCommon();
|
||||
descriptionLabel.text = qsTr("Apple Mail configuration is mostly automated, but in order to work, Bridge needs to install a certificate in your keychain.");
|
||||
linkLabel1.setCallback(function() { Backend.openKBArticle("https://proton.me/support/apple-mail-certificate"); }, qsTr("Why is this certificate needed?"), true);
|
||||
linkLabel2.clear();
|
||||
}
|
||||
function showAppleMailAutoconfigCommon() {
|
||||
titleLabel.text = "";
|
||||
linkLabel1.clear();
|
||||
linkLabel2.clear();
|
||||
iconSource = wizard.clientIconSource();
|
||||
iconHeight = 80;
|
||||
iconWidth = 80;
|
||||
}
|
||||
function showAppleMailAutoconfigProfileInstall() {
|
||||
showAppleMailAutoconfigCommon();
|
||||
descriptionLabel.text = qsTr("The final step before you can start using Apple Mail is to install the Bridge server profile in the system preferences.\n\nAdding a server profile is necessary to ensure that your Mac can receive and send Proton Mails.");
|
||||
linkLabel1.setCallback(function() { Backend.openKBArticle("https://proton.me/support/macos-certificate-warning"); }, qsTr("Why is there a yellow warning sign?"), true);
|
||||
linkLabel2.setCallback(wizard.showClientParams, qsTr("Configure Apple Mail manually"), false);
|
||||
}
|
||||
function showClientSelector(newAccount = true) {
|
||||
titleLabel.text = "";
|
||||
descriptionLabel.text = newAccount ? qsTr("Bridge is now connected to Proton, and has already started downloading your messages. Let’s now connect your email client to Bridge.") : qsTr("Let’s connect your email client to Bridge.");
|
||||
linkLabel1.clear();
|
||||
linkLabel2.clear();
|
||||
iconSource = "/qml/icons/img-client-config-selector.svg";
|
||||
iconHeight = 104;
|
||||
iconWidth = 266;
|
||||
}
|
||||
function showLogin() {
|
||||
showOnboarding();
|
||||
}
|
||||
function showLogin2FA() {
|
||||
showOnboarding();
|
||||
}
|
||||
function showLoginMailboxPassword() {
|
||||
showOnboarding();
|
||||
}
|
||||
|
||||
function showNoAccount() {
|
||||
titleLabel.text = welcomeTitle;
|
||||
descriptionLabel.text = welcomeDescription;
|
||||
linkLabel1.setCallback(startSetup, "Start setup", false);
|
||||
linkLabel2.clear();
|
||||
root.iconSource = welcomeImage;
|
||||
root.iconHeight = welcomeImageHeight;
|
||||
root.iconWidth = welcomeImageWidth;
|
||||
}
|
||||
|
||||
function showOnboarding() {
|
||||
titleLabel.text = (Backend.users.count === 0) ? welcomeTitle : addAccountTitle;
|
||||
descriptionLabel.text = welcomeDescription
|
||||
linkLabel1.setCallback(function() { Backend.openKBArticle("https://proton.me/support/why-you-need-bridge"); }, qsTr("Why do I need Bridge?"), true);
|
||||
linkLabel2.clear();
|
||||
root.iconSource = welcomeImage;
|
||||
root.iconHeight = welcomeImageHeight;
|
||||
root.iconWidth = welcomeImageWidth;
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: ProtonStyle.wizard_spacing_medium
|
||||
|
||||
Image {
|
||||
id: icon
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignTop
|
||||
Layout.preferredHeight: root.iconHeight
|
||||
Layout.preferredWidth: root.iconWidth
|
||||
source: root.iconSource
|
||||
sourceSize.height: root.iconHeight
|
||||
sourceSize.width: root.iconWidth
|
||||
}
|
||||
Label {
|
||||
id: titleLabel
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
colorScheme: _colorScheme
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: ""
|
||||
type: Label.LabelType.Heading
|
||||
visible: text.length !== 0
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
Label {
|
||||
id: descriptionLabel
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
colorScheme: _colorScheme
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: ""
|
||||
type: Label.LabelType.Body
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
LinkLabel {
|
||||
id: linkLabel1
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
colorScheme: _colorScheme
|
||||
visible: (text !== "")
|
||||
}
|
||||
LinkLabel {
|
||||
id: linkLabel2
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
colorScheme: _colorScheme
|
||||
visible: (text !== "")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,479 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
// This file is part of Proton Mail Bridge.
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
import QtQml
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
|
||||
FocusScope {
|
||||
id: root
|
||||
enum RootStack {
|
||||
Login,
|
||||
TOTP,
|
||||
MailboxPassword
|
||||
}
|
||||
|
||||
property alias currentIndex: stackLayout.currentIndex
|
||||
property alias username: usernameTextField.text
|
||||
property var wizard
|
||||
|
||||
signal loginAbort(string username, bool wasSignedOut)
|
||||
|
||||
function abort() {
|
||||
root.reset();
|
||||
loginAbort(usernameTextField.text, false);
|
||||
Backend.loginAbort(usernameTextField.text);
|
||||
}
|
||||
function reset(clearUsername = false) {
|
||||
stackLayout.currentIndex = Login.RootStack.Login;
|
||||
loginLayout.reset(clearUsername);
|
||||
totpLayout.reset();
|
||||
mailboxPasswordLayout.reset();
|
||||
if (username.length === 0) {
|
||||
usernameTextField.forceActiveFocus();
|
||||
} else {
|
||||
passwordTextField.forceActiveFocus();
|
||||
}
|
||||
passwordTextField.hidePassword();
|
||||
secondPasswordTextField.hidePassword();
|
||||
}
|
||||
|
||||
StackLayout {
|
||||
id: stackLayout
|
||||
function loginFailed() {
|
||||
signInButton.loading = false;
|
||||
usernameTextField.enabled = true;
|
||||
usernameTextField.error = true;
|
||||
passwordTextField.enabled = true;
|
||||
passwordTextField.error = true;
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
Connections {
|
||||
function onLogin2FAError(_) {
|
||||
console.assert(stackLayout.currentIndex === Login.RootStack.TOTP, "Unexpected login2FAError");
|
||||
twoFAButton.loading = false;
|
||||
twoFactorPasswordTextField.enabled = true;
|
||||
twoFactorPasswordTextField.error = true;
|
||||
twoFactorPasswordTextField.errorString = qsTr("Your code is incorrect");
|
||||
twoFactorPasswordTextField.focus = true;
|
||||
}
|
||||
function onLogin2FAErrorAbort(_) {
|
||||
console.assert(stackLayout.currentIndex === Login.RootStack.TOTP, "Unexpected login2FAErrorAbort");
|
||||
root.reset();
|
||||
errorLabel.text = qsTr("Incorrect login credentials. Please try again.");
|
||||
}
|
||||
function onLogin2FARequested(username) {
|
||||
console.assert(stackLayout.currentIndex === Login.RootStack.Login, "Unexpected login2FARequested");
|
||||
twoFactorUsernameLabel.text = username;
|
||||
stackLayout.currentIndex = Login.RootStack.TOTP;
|
||||
twoFactorPasswordTextField.focus = true;
|
||||
}
|
||||
function onLogin2PasswordError(_) {
|
||||
console.assert(stackLayout.currentIndex === Login.RootStack.MailboxPassword, "Unexpected login2PasswordError");
|
||||
secondPasswordButton.loading = false;
|
||||
secondPasswordTextField.enabled = true;
|
||||
secondPasswordTextField.error = true;
|
||||
secondPasswordTextField.errorString = qsTr("Your mailbox password is incorrect");
|
||||
secondPasswordTextField.focus = true;
|
||||
}
|
||||
function onLogin2PasswordErrorAbort(_) {
|
||||
console.assert(stackLayout.currentIndex === Login.RootStack.MailboxPassword, "Unexpected login2PasswordErrorAbort");
|
||||
root.reset();
|
||||
errorLabel.text = qsTr("Incorrect login credentials. Please try again.");
|
||||
}
|
||||
function onLogin2PasswordRequested(username) {
|
||||
console.assert(stackLayout.currentIndex === Login.RootStack.Login || stackLayout.currentIndex === Login.RootStack.TOTP, "Unexpected login2PasswordRequested");
|
||||
stackLayout.currentIndex = Login.RootStack.MailboxPassword;
|
||||
mailboxPasswordUsernameLabel.text = username;
|
||||
secondPasswordTextField.focus = true;
|
||||
}
|
||||
function onLoginAlreadyLoggedIn(_) {
|
||||
stackLayout.currentIndex = Login.RootStack.Login;
|
||||
root.reset();
|
||||
}
|
||||
function onLoginConnectionError(_) {
|
||||
if (stackLayout.currentIndex === Login.RootStack.Login) {
|
||||
stackLayout.loginFailed();
|
||||
}
|
||||
}
|
||||
function onLoginFinished(_) {
|
||||
stackLayout.currentIndex = Login.RootStack.Login;
|
||||
root.reset();
|
||||
}
|
||||
function onLoginFreeUserError() {
|
||||
console.assert(stackLayout.currentIndex === Login.RootStack.Login, "Unexpected loginFreeUserError");
|
||||
stackLayout.loginFailed();
|
||||
}
|
||||
function onLoginUsernamePasswordError(errorMsg) {
|
||||
console.assert(stackLayout.currentIndex === Login.RootStack.Login, "Unexpected loginUsernamePasswordError");
|
||||
stackLayout.loginFailed();
|
||||
if (errorMsg !== "")
|
||||
errorLabel.text = errorMsg;
|
||||
else
|
||||
errorLabel.text = qsTr("Incorrect login credentials");
|
||||
}
|
||||
|
||||
target: Backend
|
||||
}
|
||||
Item {
|
||||
ColumnLayout {
|
||||
id: loginLayout
|
||||
function clearErrors() {
|
||||
usernameTextField.error = false;
|
||||
usernameTextField.errorString = "";
|
||||
passwordTextField.error = false;
|
||||
passwordTextField.errorString = "";
|
||||
errorLabel.text = "";
|
||||
}
|
||||
function reset(clearUsername = false) {
|
||||
signInButton.loading = false;
|
||||
errorLabel.text = "";
|
||||
usernameTextField.enabled = true;
|
||||
usernameTextField.focus = true;
|
||||
if (clearUsername) {
|
||||
usernameTextField.text = "";
|
||||
}
|
||||
passwordTextField.enabled = true;
|
||||
passwordTextField.text = "";
|
||||
clearErrors();
|
||||
}
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: ProtonStyle.wizard_spacing_medium
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: ProtonStyle.wizard_spacing_small
|
||||
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
colorScheme: wizard.colorScheme
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: qsTr("Sign in")
|
||||
type: Label.LabelType.Title
|
||||
}
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
color: wizard.colorScheme.text_weak
|
||||
colorScheme: wizard.colorScheme
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: qsTr("Enter your Proton Account details.")
|
||||
type: Label.LabelType.Body
|
||||
}
|
||||
}
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
ColorImage {
|
||||
color: wizard.colorScheme.signal_danger
|
||||
height: errorLabel.lineHeight
|
||||
source: "/qml/icons/ic-exclamation-circle-filled.svg"
|
||||
sourceSize.height: errorLabel.lineHeight
|
||||
visible: errorLabel.text.length > 0
|
||||
}
|
||||
Label {
|
||||
id: errorLabel
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 4
|
||||
color: wizard.colorScheme.signal_danger
|
||||
colorScheme: wizard.colorScheme
|
||||
type: Label.LabelType.Caption_semibold
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
TextField {
|
||||
id: usernameTextField
|
||||
Layout.fillWidth: true
|
||||
colorScheme: wizard.colorScheme
|
||||
focus: true
|
||||
label: qsTr("Email or username")
|
||||
validateOnEditingFinished: false
|
||||
validator: function (str) {
|
||||
if (str.length === 0) {
|
||||
return qsTr("Enter email or username");
|
||||
}
|
||||
}
|
||||
|
||||
onAccepted: passwordTextField.forceActiveFocus()
|
||||
onTextChanged: {
|
||||
loginLayout.clearErrors();
|
||||
}
|
||||
}
|
||||
TextField {
|
||||
id: passwordTextField
|
||||
Layout.fillWidth: true
|
||||
colorScheme: wizard.colorScheme
|
||||
echoMode: TextInput.Password
|
||||
label: qsTr("Password")
|
||||
validateOnEditingFinished: false
|
||||
validator: function (str) {
|
||||
if (str.length === 0) {
|
||||
return qsTr("Enter password");
|
||||
}
|
||||
}
|
||||
|
||||
onAccepted: signInButton.checkAndSignIn()
|
||||
onTextChanged: {
|
||||
loginLayout.clearErrors();
|
||||
}
|
||||
}
|
||||
Button {
|
||||
id: signInButton
|
||||
function checkAndSignIn() {
|
||||
usernameTextField.validate();
|
||||
passwordTextField.validate();
|
||||
if (usernameTextField.error || passwordTextField.error) {
|
||||
return;
|
||||
}
|
||||
usernameTextField.enabled = false;
|
||||
passwordTextField.enabled = false;
|
||||
loading = true;
|
||||
Backend.login(usernameTextField.text, Qt.btoa(passwordTextField.text));
|
||||
}
|
||||
|
||||
Layout.fillWidth: true
|
||||
colorScheme: wizard.colorScheme
|
||||
enabled: !loading
|
||||
text: loading ? qsTr("Signing in") : qsTr("Sign in")
|
||||
|
||||
onClicked: {
|
||||
checkAndSignIn();
|
||||
}
|
||||
}
|
||||
Button {
|
||||
Layout.fillWidth: true
|
||||
colorScheme: wizard.colorScheme
|
||||
enabled: !signInButton.loading
|
||||
secondary: true
|
||||
secondaryIsOpaque: true
|
||||
text: qsTr("Cancel")
|
||||
|
||||
onClicked: {
|
||||
root.abort();
|
||||
}
|
||||
}
|
||||
LinkLabel {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
colorScheme: wizard.colorScheme
|
||||
external: true
|
||||
link: "https://proton.me/mail/pricing"
|
||||
text: qsTr("Create or upgrade your account")
|
||||
}
|
||||
}
|
||||
}
|
||||
Item {
|
||||
ColumnLayout {
|
||||
id: totpLayout
|
||||
function reset() {
|
||||
twoFAButton.loading = false;
|
||||
twoFactorPasswordTextField.enabled = true;
|
||||
twoFactorPasswordTextField.error = false;
|
||||
twoFactorPasswordTextField.errorString = "";
|
||||
twoFactorPasswordTextField.text = "";
|
||||
}
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: ProtonStyle.wizard_spacing_medium
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: ProtonStyle.wizard_spacing_small
|
||||
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
colorScheme: wizard.colorScheme
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: qsTr("Two-factor authentication")
|
||||
type: Label.LabelType.Title
|
||||
}
|
||||
Label {
|
||||
id: twoFactorUsernameLabel
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
color: wizard.colorScheme.text_weak
|
||||
colorScheme: wizard.colorScheme
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: ""
|
||||
type: Label.LabelType.Body
|
||||
}
|
||||
}
|
||||
Label {
|
||||
id: descriptionLabel
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
colorScheme: wizard.colorScheme
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: qsTr("You have enabled two-factor authentication. Please enter the 6-digit code provided by your authenticator application.")
|
||||
type: Label.LabelType.Body
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
TextField {
|
||||
id: twoFactorPasswordTextField
|
||||
Layout.fillWidth: true
|
||||
colorScheme: wizard.colorScheme
|
||||
label: qsTr("Two-factor code")
|
||||
validateOnEditingFinished: false
|
||||
validator: function (str) {
|
||||
if (str.length === 0) {
|
||||
return qsTr("Enter the 6-digit code");
|
||||
}
|
||||
}
|
||||
|
||||
onAccepted: {
|
||||
twoFAButton.onClicked();
|
||||
}
|
||||
onTextChanged: {
|
||||
if (text.length >= 6) {
|
||||
twoFAButton.onClicked();
|
||||
}
|
||||
}
|
||||
}
|
||||
Button {
|
||||
id: twoFAButton
|
||||
Layout.fillWidth: true
|
||||
colorScheme: wizard.colorScheme
|
||||
enabled: !loading
|
||||
text: loading ? qsTr("Authenticating") : qsTr("Authenticate")
|
||||
|
||||
onClicked: {
|
||||
twoFactorPasswordTextField.validate();
|
||||
if (twoFactorPasswordTextField.error) {
|
||||
return;
|
||||
}
|
||||
twoFactorPasswordTextField.enabled = false;
|
||||
loading = true;
|
||||
Backend.login2FA(usernameTextField.text, Qt.btoa(twoFactorPasswordTextField.text));
|
||||
}
|
||||
}
|
||||
Button {
|
||||
Layout.fillWidth: true
|
||||
colorScheme: wizard.colorScheme
|
||||
enabled: !twoFAButton.loading
|
||||
secondary: true
|
||||
secondaryIsOpaque: true
|
||||
text: qsTr("Cancel")
|
||||
|
||||
onClicked: {
|
||||
root.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Item {
|
||||
ColumnLayout {
|
||||
id: mailboxPasswordLayout
|
||||
function reset() {
|
||||
secondPasswordButton.loading = false;
|
||||
secondPasswordTextField.enabled = true;
|
||||
secondPasswordTextField.error = false;
|
||||
secondPasswordTextField.errorString = "";
|
||||
secondPasswordTextField.text = "";
|
||||
}
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: ProtonStyle.wizard_spacing_medium
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: ProtonStyle.wizard_spacing_small
|
||||
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
colorScheme: wizard.colorScheme
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: qsTr("Unlock your mailbox")
|
||||
type: Label.LabelType.Title
|
||||
}
|
||||
Label {
|
||||
id: mailboxPasswordUsernameLabel
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
color: wizard.colorScheme.text_weak
|
||||
colorScheme: wizard.colorScheme
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: ""
|
||||
type: Label.LabelType.Body
|
||||
}
|
||||
}
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
colorScheme: wizard.colorScheme
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: qsTr("You have secured your account with a separate mailbox password.")
|
||||
type: Label.LabelType.Body
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
TextField {
|
||||
id: secondPasswordTextField
|
||||
Layout.fillWidth: true
|
||||
colorScheme: wizard.colorScheme
|
||||
echoMode: TextInput.Password
|
||||
label: qsTr("Mailbox password")
|
||||
validateOnEditingFinished: false
|
||||
validator: function (str) {
|
||||
if (str.length === 0) {
|
||||
return qsTr("Enter password");
|
||||
}
|
||||
}
|
||||
|
||||
onAccepted: {
|
||||
secondPasswordButton.onClicked();
|
||||
}
|
||||
}
|
||||
Button {
|
||||
id: secondPasswordButton
|
||||
Layout.fillWidth: true
|
||||
colorScheme: wizard.colorScheme
|
||||
enabled: !loading
|
||||
text: loading ? qsTr("Unlocking") : qsTr("Unlock")
|
||||
|
||||
onClicked: {
|
||||
secondPasswordTextField.validate();
|
||||
if (secondPasswordTextField.error) {
|
||||
return;
|
||||
}
|
||||
secondPasswordTextField.enabled = false;
|
||||
loading = true;
|
||||
Backend.login2Password(usernameTextField.text, Qt.btoa(secondPasswordTextField.text));
|
||||
}
|
||||
}
|
||||
Button {
|
||||
Layout.fillWidth: true
|
||||
colorScheme: wizard.colorScheme
|
||||
enabled: !secondPasswordButton.loading
|
||||
secondary: true
|
||||
secondaryIsOpaque: true
|
||||
text: qsTr("Cancel")
|
||||
|
||||
onClicked: {
|
||||
root.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,52 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
// This file is part of Proton Mail Bridge.
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
import QtQml
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var wizard
|
||||
|
||||
ColumnLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: ProtonStyle.wizard_spacing_large
|
||||
|
||||
StepDescriptionBox {
|
||||
colorScheme: wizard.colorScheme
|
||||
description: qsTr("Connect Bridge to your Proton account")
|
||||
icon: "/qml/icons/ic-bridge.svg"
|
||||
iconSize: 48
|
||||
title: qsTr("Step 1")
|
||||
}
|
||||
StepDescriptionBox {
|
||||
colorScheme: wizard.colorScheme
|
||||
description: qsTr("Connect your email client to Bridge")
|
||||
icon: "/qml/icons/img-mail-clients.svg"
|
||||
iconSize: 48
|
||||
title: qsTr("Step 2")
|
||||
}
|
||||
Button {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
colorScheme: wizard.colorScheme
|
||||
text: qsTr("Start setup")
|
||||
|
||||
onClicked: wizard.showLogin()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,300 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
// This file is part of Proton Mail Bridge.
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
import QtQml
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
|
||||
Item {
|
||||
id: root
|
||||
enum Client {
|
||||
AppleMail,
|
||||
MicrosoftOutlook,
|
||||
MozillaThunderbird,
|
||||
Generic
|
||||
}
|
||||
enum ContentStack {
|
||||
Onboarding,
|
||||
Login,
|
||||
ClientConfigSelector,
|
||||
ClientConfigAppleMail
|
||||
}
|
||||
enum RootStack {
|
||||
TwoPanesView,
|
||||
ClientConfigParameters,
|
||||
ClientConfigEnd
|
||||
}
|
||||
|
||||
property string address
|
||||
property var backAction: null
|
||||
property int client
|
||||
property ColorScheme colorScheme
|
||||
property var user
|
||||
|
||||
signal bugReportRequested
|
||||
signal wizardEnded
|
||||
|
||||
function _showClientConfig() {
|
||||
showClientConfig(root.user, root.address, false);
|
||||
}
|
||||
function clientIconSource() {
|
||||
switch (client) {
|
||||
case SetupWizard.Client.AppleMail:
|
||||
return "/qml/icons/ic-apple-mail.svg";
|
||||
case SetupWizard.Client.MicrosoftOutlook:
|
||||
return "/qml/icons/ic-microsoft-outlook.svg";
|
||||
case SetupWizard.Client.MozillaThunderbird:
|
||||
return "/qml/icons/ic-mozilla-thunderbird.svg";
|
||||
case SetupWizard.Client.Generic:
|
||||
return "/qml/icons/ic-other-mail-clients.svg";
|
||||
default:
|
||||
console.error("Unknown mail client " + client);
|
||||
return "/qml/icons/ic-other-mail-clients.svg";
|
||||
}
|
||||
}
|
||||
function clientName() {
|
||||
switch (client) {
|
||||
case SetupWizard.Client.AppleMail:
|
||||
return "Apple Mail";
|
||||
case SetupWizard.Client.MicrosoftOutlook:
|
||||
return "Outlook";
|
||||
case SetupWizard.Client.MozillaThunderbird:
|
||||
return "Thunderbird";
|
||||
case SetupWizard.Client.Generic:
|
||||
return qsTr("your email client");
|
||||
default:
|
||||
console.error("Unknown mail client " + client);
|
||||
return qsTr("your email client");
|
||||
}
|
||||
}
|
||||
function closeWizard() {
|
||||
wizardEnded();
|
||||
}
|
||||
function setupGuideLink() {
|
||||
switch (client) {
|
||||
case SetupWizard.Client.AppleMail:
|
||||
return "https://proton.me/support/protonmail-bridge-clients-apple-mail";
|
||||
case SetupWizard.Client.MicrosoftOutlook:
|
||||
return (Backend.goos === "darwin") ? "https://proton.me/support/protonmail-bridge-clients-macos-outlook-2019" : "https://proton.me/support/protonmail-bridge-clients-windows-outlook-2019";
|
||||
case SetupWizard.Client.MozillaThunderbird:
|
||||
return "https://proton.me/support/protonmail-bridge-clients-windows-thunderbird";
|
||||
default:
|
||||
return "https://proton.me/support/protonmail-bridge-configure-client";
|
||||
}
|
||||
}
|
||||
function showAppleMailAutoConfig() {
|
||||
backAction = _showClientConfig;
|
||||
rootStackLayout.currentIndex = SetupWizard.RootStack.TwoPanesView;
|
||||
rightContent.currentIndex = SetupWizard.ContentStack.ClientConfigAppleMail;
|
||||
clientConfigAppleMail.showAutoconfig(); // This will trigger signals that will display the appropriate left content.
|
||||
}
|
||||
function showBugReport() {
|
||||
closeWizard();
|
||||
bugReportRequested();
|
||||
}
|
||||
function showClientConfig(user, address, justLoggedIn) {
|
||||
backAction = null;
|
||||
root.user = user;
|
||||
root.address = address;
|
||||
rootStackLayout.currentIndex = SetupWizard.RootStack.TwoPanesView;
|
||||
leftContent.showClientSelector(justLoggedIn);
|
||||
rightContent.currentIndex = SetupWizard.ContentStack.ClientConfigSelector;
|
||||
}
|
||||
function showClientConfigEnd() {
|
||||
backAction = null;
|
||||
rootStackLayout.currentIndex = SetupWizard.RootStack.ClientConfigEnd;
|
||||
}
|
||||
function showClientParams() {
|
||||
backAction = _showClientConfig;
|
||||
rootStackLayout.currentIndex = SetupWizard.RootStack.ClientConfigParameters;
|
||||
}
|
||||
function showLogin(username = "") {
|
||||
backAction = null;
|
||||
rootStackLayout.currentIndex = SetupWizard.RootStack.TwoPanesView;
|
||||
root.address = "";
|
||||
leftContent.showLogin();
|
||||
rightContent.currentIndex = SetupWizard.ContentStack.Login;
|
||||
login.username = username;
|
||||
login.reset(false);
|
||||
}
|
||||
function showOnboarding() {
|
||||
backAction = null;
|
||||
rootStackLayout.currentIndex = SetupWizard.RootStack.TwoPanesView;
|
||||
root.address = "";
|
||||
root.user = null;
|
||||
leftContent.showOnboarding();
|
||||
rightContent.currentIndex = SetupWizard.ContentStack.Onboarding;
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onLoginFinished(userIndex, wasSignedOut) {
|
||||
if (wasSignedOut) {
|
||||
closeWizard();
|
||||
return;
|
||||
}
|
||||
let user = Backend.users.get(userIndex);
|
||||
let address = user ? user.addresses[0] : "";
|
||||
showClientConfig(user, address, true);
|
||||
}
|
||||
|
||||
target: Backend
|
||||
}
|
||||
StackLayout {
|
||||
id: rootStackLayout
|
||||
anchors.fill: parent
|
||||
|
||||
// rootStackLayout index 0
|
||||
RowLayout {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
Rectangle {
|
||||
id: leftHalf
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
color: root.colorScheme.background_norm
|
||||
|
||||
LeftPane {
|
||||
id: leftContent
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: ProtonStyle.wizard_pane_bottomMargin
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: ProtonStyle.wizard_window_margin
|
||||
clip: true
|
||||
width: ProtonStyle.wizard_pane_width
|
||||
wizard: root
|
||||
|
||||
Connections {
|
||||
function onAppleMailAutoconfigCertificateInstallPageShown() {
|
||||
leftContent.showAppleMailAutoconfigCertificateInstall();
|
||||
}
|
||||
function onAppleMailAutoconfigProfileInstallPageShow() {
|
||||
leftContent.showAppleMailAutoconfigProfileInstall();
|
||||
}
|
||||
|
||||
target: clientConfigAppleMail
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onLogin2FARequested() {
|
||||
leftContent.showLogin2FA();
|
||||
}
|
||||
function onLogin2PasswordRequested() {
|
||||
leftContent.showLoginMailboxPassword();
|
||||
}
|
||||
|
||||
target: Backend
|
||||
}
|
||||
}
|
||||
Image {
|
||||
id: mailLogoWithWordmark
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: ProtonStyle.wizard_window_margin
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
height: sourceSize.height
|
||||
source: root.colorScheme.mail_logo_with_wordmark
|
||||
sourceSize.height: 36
|
||||
sourceSize.width: 134
|
||||
width: sourceSize.width
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
id: rightHalf
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
color: root.colorScheme.background_weak
|
||||
|
||||
StackLayout {
|
||||
id: rightContent
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: ProtonStyle.wizard_pane_bottomMargin
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: ProtonStyle.wizard_window_margin
|
||||
clip: true
|
||||
currentIndex: 0
|
||||
width: ProtonStyle.wizard_pane_width
|
||||
|
||||
// rightContent stack index 0
|
||||
Onboarding {
|
||||
wizard: root
|
||||
}
|
||||
|
||||
// rightContent tack index 1
|
||||
Login {
|
||||
id: login
|
||||
wizard: root
|
||||
|
||||
onLoginAbort: {
|
||||
root.closeWizard();
|
||||
}
|
||||
}
|
||||
|
||||
// rightContent stack index 2
|
||||
ClientConfigSelector {
|
||||
id: clientConfigSelector
|
||||
wizard: root
|
||||
}
|
||||
// rightContent stack index 3
|
||||
ClientConfigAppleMail {
|
||||
id: clientConfigAppleMail
|
||||
wizard: root
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// rootStackLayout index 1
|
||||
ClientConfigParameters {
|
||||
id: clientConfigParameters
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
wizard: root
|
||||
}
|
||||
|
||||
// rootStackLayout index 2
|
||||
ClientConfigEnd {
|
||||
id: clientConfigEnd
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
wizard: root
|
||||
}
|
||||
}
|
||||
HelpButton {
|
||||
wizard: root
|
||||
}
|
||||
Button {
|
||||
id: backButton
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: ProtonStyle.wizard_window_margin
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: ProtonStyle.wizard_window_margin
|
||||
colorScheme: root.colorScheme
|
||||
icon.source: "/qml/icons/ic-chevron-left.svg"
|
||||
iconOnTheLeft: true
|
||||
secondary: true
|
||||
secondaryIsOpaque: true
|
||||
spacing: ProtonStyle.wizard_spacing_small
|
||||
text: qsTr("Back")
|
||||
visible: backAction != null
|
||||
|
||||
onClicked: {
|
||||
if (backAction) {
|
||||
backAction();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,59 @@
|
||||
// Copyright (c) 2023 Proton AG
|
||||
// This file is part of Proton Mail Bridge.
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
import QtQml
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
property ColorScheme colorScheme
|
||||
property string description
|
||||
property string icon
|
||||
property int iconSize: 64
|
||||
property string title
|
||||
|
||||
spacing: ProtonStyle.wizard_spacing_large
|
||||
|
||||
Image {
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
Layout.preferredHeight: iconSize
|
||||
Layout.preferredWidth: iconSize
|
||||
mipmap: true
|
||||
source: root.icon
|
||||
}
|
||||
ColumnLayout {
|
||||
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
|
||||
Layout.fillWidth: true
|
||||
spacing: ProtonStyle.wizard_spacing_small
|
||||
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
|
||||
Layout.fillHeight: false
|
||||
Layout.fillWidth: true
|
||||
colorScheme: root.colorScheme
|
||||
text: root.title
|
||||
type: Label.LabelType.Body_bold
|
||||
}
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
colorScheme: root.colorScheme
|
||||
text: root.description
|
||||
type: Label.LabelType.Body
|
||||
verticalAlignment: Text.AlignTop
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,413 +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 QtQml
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Controls.impl
|
||||
import Proton
|
||||
|
||||
FocusScope {
|
||||
id: root
|
||||
|
||||
property ColorScheme colorScheme
|
||||
property alias currentIndex: stackLayout.currentIndex
|
||||
property alias username: usernameTextField.text
|
||||
|
||||
function abort() {
|
||||
root.reset();
|
||||
Backend.loginAbort(usernameTextField.text);
|
||||
}
|
||||
function reset() {
|
||||
stackLayout.currentIndex = 0;
|
||||
loginNormalLayout.reset();
|
||||
login2FALayout.reset();
|
||||
login2PasswordLayout.reset();
|
||||
}
|
||||
|
||||
implicitHeight: children[0].implicitHeight
|
||||
implicitWidth: children[0].implicitWidth
|
||||
state: "Page 1"
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: "Page 1"
|
||||
|
||||
PropertyChanges {
|
||||
currentIndex: 0
|
||||
target: stackLayout
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "Page 2"
|
||||
|
||||
PropertyChanges {
|
||||
currentIndex: 1
|
||||
target: stackLayout
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "Page 3"
|
||||
|
||||
PropertyChanges {
|
||||
currentIndex: 2
|
||||
target: stackLayout
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
StackLayout {
|
||||
id: stackLayout
|
||||
function loginFailed() {
|
||||
signInButton.loading = false;
|
||||
usernameTextField.enabled = true;
|
||||
usernameTextField.error = true;
|
||||
passwordTextField.enabled = true;
|
||||
passwordTextField.error = true;
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
Connections {
|
||||
function onLogin2FAError(_) {
|
||||
console.assert(stackLayout.currentIndex === 1, "Unexpected login2FAError");
|
||||
twoFAButton.loading = false;
|
||||
twoFactorPasswordTextField.enabled = true;
|
||||
twoFactorPasswordTextField.error = true;
|
||||
twoFactorPasswordTextField.errorString = qsTr("Your code is incorrect");
|
||||
twoFactorPasswordTextField.focus = true;
|
||||
}
|
||||
function onLogin2FAErrorAbort(_) {
|
||||
console.assert(stackLayout.currentIndex === 1, "Unexpected login2FAErrorAbort");
|
||||
root.reset();
|
||||
errorLabel.text = qsTr("Incorrect login credentials. Please try again.");
|
||||
}
|
||||
function onLogin2FARequested(username) {
|
||||
console.assert(stackLayout.currentIndex === 0, "Unexpected login2FARequested");
|
||||
twoFactorUsernameLabel.text = username;
|
||||
stackLayout.currentIndex = 1;
|
||||
twoFactorPasswordTextField.focus = true;
|
||||
}
|
||||
function onLogin2PasswordError(_) {
|
||||
console.assert(stackLayout.currentIndex === 2, "Unexpected login2PasswordError");
|
||||
secondPasswordButton.loading = false;
|
||||
secondPasswordTextField.enabled = true;
|
||||
secondPasswordTextField.error = true;
|
||||
secondPasswordTextField.errorString = qsTr("Your mailbox password is incorrect");
|
||||
secondPasswordTextField.focus = true;
|
||||
}
|
||||
function onLogin2PasswordErrorAbort(_) {
|
||||
console.assert(stackLayout.currentIndex === 2, "Unexpected login2PasswordErrorAbort");
|
||||
root.reset();
|
||||
errorLabel.text = qsTr("Incorrect login credentials. Please try again.");
|
||||
}
|
||||
function onLogin2PasswordRequested() {
|
||||
console.assert(stackLayout.currentIndex === 0 || stackLayout.currentIndex === 1, "Unexpected login2PasswordRequested");
|
||||
stackLayout.currentIndex = 2;
|
||||
secondPasswordTextField.focus = true;
|
||||
}
|
||||
function onLoginAlreadyLoggedIn(_) {
|
||||
stackLayout.currentIndex = 0;
|
||||
root.reset();
|
||||
}
|
||||
function onLoginConnectionError(_) {
|
||||
if (stackLayout.currentIndex === 0) {
|
||||
stackLayout.loginFailed();
|
||||
}
|
||||
}
|
||||
function onLoginFinished(_) {
|
||||
stackLayout.currentIndex = 0;
|
||||
root.reset();
|
||||
}
|
||||
function onLoginFreeUserError() {
|
||||
console.assert(stackLayout.currentIndex === 0, "Unexpected loginFreeUserError");
|
||||
stackLayout.loginFailed();
|
||||
}
|
||||
function onLoginUsernamePasswordError(errorMsg) {
|
||||
console.assert(stackLayout.currentIndex === 0, "Unexpected loginUsernamePasswordError");
|
||||
stackLayout.loginFailed();
|
||||
if (errorMsg !== "")
|
||||
errorLabel.text = errorMsg;
|
||||
else
|
||||
errorLabel.text = qsTr("Incorrect login credentials");
|
||||
}
|
||||
|
||||
target: Backend
|
||||
}
|
||||
ColumnLayout {
|
||||
id: loginNormalLayout
|
||||
function reset() {
|
||||
signInButton.loading = false;
|
||||
errorLabel.text = "";
|
||||
usernameTextField.enabled = true;
|
||||
usernameTextField.error = false;
|
||||
usernameTextField.errorString = "";
|
||||
usernameTextField.focus = true;
|
||||
passwordTextField.enabled = true;
|
||||
passwordTextField.error = false;
|
||||
passwordTextField.errorString = "";
|
||||
passwordTextField.text = "";
|
||||
}
|
||||
|
||||
spacing: 0
|
||||
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.topMargin: 16
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Sign in")
|
||||
type: Label.LabelType.Title
|
||||
}
|
||||
Label {
|
||||
id: subTitle
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.topMargin: 8
|
||||
color: root.colorScheme.text_weak
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Enter your Proton Account details.")
|
||||
type: Label.LabelType.Body
|
||||
}
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 36
|
||||
spacing: 0
|
||||
visible: errorLabel.text.length > 0
|
||||
|
||||
ColorImage {
|
||||
color: root.colorScheme.signal_danger
|
||||
height: errorLabel.lineHeight
|
||||
source: "/qml/icons/ic-exclamation-circle-filled.svg"
|
||||
sourceSize.height: errorLabel.lineHeight
|
||||
}
|
||||
Label {
|
||||
id: errorLabel
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 4
|
||||
color: root.colorScheme.signal_danger
|
||||
colorScheme: root.colorScheme
|
||||
type: root.error ? Label.LabelType.Caption_semibold : Label.LabelType.Caption
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
TextField {
|
||||
id: usernameTextField
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 24
|
||||
colorScheme: root.colorScheme
|
||||
focus: true
|
||||
label: qsTr("Email or username")
|
||||
validateOnEditingFinished: false
|
||||
validator: function (str) {
|
||||
if (str.length === 0) {
|
||||
return qsTr("Enter email or username");
|
||||
}
|
||||
}
|
||||
|
||||
onAccepted: passwordTextField.forceActiveFocus()
|
||||
onTextChanged: {
|
||||
// remove "invalid username / password error"
|
||||
if (error || errorLabel.text.length > 0) {
|
||||
errorLabel.text = "";
|
||||
usernameTextField.error = false;
|
||||
passwordTextField.error = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
TextField {
|
||||
id: passwordTextField
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
colorScheme: root.colorScheme
|
||||
echoMode: TextInput.Password
|
||||
label: qsTr("Password")
|
||||
validateOnEditingFinished: false
|
||||
validator: function (str) {
|
||||
if (str.length === 0) {
|
||||
return qsTr("Enter password");
|
||||
}
|
||||
}
|
||||
|
||||
onAccepted: signInButton.checkAndSignIn()
|
||||
onTextChanged: {
|
||||
// remove "invalid username / password error"
|
||||
if (error || errorLabel.text.length > 0) {
|
||||
errorLabel.text = "";
|
||||
usernameTextField.error = false;
|
||||
passwordTextField.error = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Button {
|
||||
id: signInButton
|
||||
function checkAndSignIn() {
|
||||
usernameTextField.validate();
|
||||
passwordTextField.validate();
|
||||
if (usernameTextField.error || passwordTextField.error) {
|
||||
return;
|
||||
}
|
||||
usernameTextField.enabled = false;
|
||||
passwordTextField.enabled = false;
|
||||
loading = true;
|
||||
Backend.login(usernameTextField.text, Qt.btoa(passwordTextField.text));
|
||||
}
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 24
|
||||
colorScheme: root.colorScheme
|
||||
enabled: !loading
|
||||
text: loading ? qsTr("Signing in") : qsTr("Sign in")
|
||||
|
||||
onClicked: {
|
||||
checkAndSignIn();
|
||||
}
|
||||
}
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.topMargin: 24
|
||||
colorScheme: root.colorScheme
|
||||
text: link("https://proton.me/mail/pricing", qsTr("Create or upgrade your account"))
|
||||
textFormat: Text.StyledText
|
||||
type: Label.LabelType.Body
|
||||
|
||||
onLinkActivated: {
|
||||
Qt.openUrlExternally(link);
|
||||
}
|
||||
}
|
||||
}
|
||||
ColumnLayout {
|
||||
id: login2FALayout
|
||||
function reset() {
|
||||
twoFAButton.loading = false;
|
||||
twoFactorPasswordTextField.enabled = true;
|
||||
twoFactorPasswordTextField.error = false;
|
||||
twoFactorPasswordTextField.errorString = "";
|
||||
twoFactorPasswordTextField.text = "";
|
||||
}
|
||||
|
||||
spacing: 0
|
||||
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.topMargin: 16
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Two-factor authentication")
|
||||
type: Label.LabelType.Heading
|
||||
}
|
||||
Label {
|
||||
id: twoFactorUsernameLabel
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.topMargin: 8
|
||||
color: root.colorScheme.text_weak
|
||||
colorScheme: root.colorScheme
|
||||
type: Label.LabelType.Lead
|
||||
}
|
||||
TextField {
|
||||
id: twoFactorPasswordTextField
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 32
|
||||
assistiveText: qsTr("Enter the 6-digit code")
|
||||
colorScheme: root.colorScheme
|
||||
label: qsTr("Two-factor code")
|
||||
validateOnEditingFinished: false
|
||||
validator: function (str) {
|
||||
if (str.length === 0) {
|
||||
return qsTr("Enter the 6-digit code");
|
||||
}
|
||||
}
|
||||
|
||||
onAccepted: {
|
||||
twoFAButton.onClicked();
|
||||
}
|
||||
onTextChanged: {
|
||||
if (text.length >= 6) {
|
||||
twoFAButton.onClicked();
|
||||
}
|
||||
}
|
||||
}
|
||||
Button {
|
||||
id: twoFAButton
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 24
|
||||
colorScheme: root.colorScheme
|
||||
enabled: !loading
|
||||
text: loading ? qsTr("Authenticating") : qsTr("Authenticate")
|
||||
|
||||
onClicked: {
|
||||
twoFactorPasswordTextField.validate();
|
||||
if (twoFactorPasswordTextField.error) {
|
||||
return;
|
||||
}
|
||||
twoFactorPasswordTextField.enabled = false;
|
||||
loading = true;
|
||||
Backend.login2FA(usernameTextField.text, Qt.btoa(twoFactorPasswordTextField.text));
|
||||
}
|
||||
}
|
||||
}
|
||||
ColumnLayout {
|
||||
id: login2PasswordLayout
|
||||
function reset() {
|
||||
secondPasswordButton.loading = false;
|
||||
secondPasswordTextField.enabled = true;
|
||||
secondPasswordTextField.error = false;
|
||||
secondPasswordTextField.errorString = "";
|
||||
secondPasswordTextField.text = "";
|
||||
}
|
||||
|
||||
spacing: 0
|
||||
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.topMargin: 16
|
||||
colorScheme: root.colorScheme
|
||||
text: qsTr("Unlock your mailbox")
|
||||
type: Label.LabelType.Heading
|
||||
}
|
||||
TextField {
|
||||
id: secondPasswordTextField
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8 + implicitHeight + 24 + subTitle.implicitHeight
|
||||
colorScheme: root.colorScheme
|
||||
echoMode: TextInput.Password
|
||||
label: qsTr("Mailbox password")
|
||||
validateOnEditingFinished: false
|
||||
validator: function (str) {
|
||||
if (str.length === 0) {
|
||||
return qsTr("Enter password");
|
||||
}
|
||||
}
|
||||
|
||||
onAccepted: {
|
||||
secondPasswordButton.onClicked();
|
||||
}
|
||||
}
|
||||
Button {
|
||||
id: secondPasswordButton
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 24
|
||||
colorScheme: root.colorScheme
|
||||
enabled: !loading
|
||||
text: loading ? qsTr("Unlocking") : qsTr("Unlock")
|
||||
|
||||
onClicked: {
|
||||
secondPasswordTextField.validate();
|
||||
if (secondPasswordTextField.error) {
|
||||
return;
|
||||
}
|
||||
secondPasswordTextField.enabled = false;
|
||||
loading = true;
|
||||
Backend.login2Password(usernameTextField.text, Qt.btoa(secondPasswordTextField.text));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,245 +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 QtQml
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import Proton
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property ColorScheme colorScheme
|
||||
|
||||
implicitHeight: children[0].implicitHeight
|
||||
implicitWidth: children[0].implicitWidth
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: "Page 1"
|
||||
|
||||
PropertyChanges {
|
||||
currentIndex: 0
|
||||
target: signInItem
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "Page 2"
|
||||
|
||||
PropertyChanges {
|
||||
currentIndex: 1
|
||||
target: signInItem
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "Page 3"
|
||||
|
||||
PropertyChanges {
|
||||
currentIndex: 2
|
||||
target: signInItem
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Rectangle {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
color: root.colorScheme.background_norm
|
||||
implicitHeight: children[0].implicitHeight
|
||||
implicitWidth: children[0].implicitWidth
|
||||
visible: signInItem.currentIndex === 0
|
||||
|
||||
GridLayout {
|
||||
anchors.fill: parent
|
||||
columnSpacing: 0
|
||||
columns: 3
|
||||
rowSpacing: 0
|
||||
|
||||
// top margin
|
||||
Item {
|
||||
Layout.columnSpan: 3
|
||||
Layout.fillWidth: true
|
||||
|
||||
// Using binding component here instead of direct binding to avoid binding loop during construction of element
|
||||
Binding on Layout.preferredHeight {
|
||||
value: (parent.height - welcomeContentItem.height) / 4
|
||||
}
|
||||
}
|
||||
|
||||
// left margin
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: 80
|
||||
Layout.minimumWidth: 48
|
||||
Layout.preferredHeight: welcomeContentItem.height
|
||||
}
|
||||
ColumnLayout {
|
||||
id: welcomeContentItem
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
Image {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.topMargin: 16
|
||||
source: colorScheme.welcome_img
|
||||
sourceSize.height: 148
|
||||
sourceSize.width: 264
|
||||
}
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
colorScheme: root.colorScheme
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: qsTr("Welcome to\nProton Mail Bridge")
|
||||
type: Label.LabelType.Heading
|
||||
}
|
||||
Label {
|
||||
id: longTextLabel
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: 320
|
||||
Layout.topMargin: 16
|
||||
colorScheme: root.colorScheme
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: qsTr("Add your Proton Mail account to securely access and manage your messages in your favorite email client. Bridge runs in the background and encrypts and decrypts your messages seamlessly.")
|
||||
type: Label.LabelType.Body
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
|
||||
// Right margin
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: 80
|
||||
Layout.minimumWidth: 48
|
||||
Layout.preferredHeight: welcomeContentItem.height
|
||||
}
|
||||
|
||||
// bottom margin
|
||||
Item {
|
||||
Layout.columnSpan: 3
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: children[0].implicitHeight + children[0].anchors.bottomMargin + children[0].anchors.topMargin
|
||||
implicitWidth: children[0].implicitWidth
|
||||
|
||||
Image {
|
||||
id: logoImage
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 48
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.topMargin: 48
|
||||
source: colorScheme.logo_img
|
||||
sourceSize.height: 25
|
||||
sourceSize.width: 200
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
color: (signInItem.currentIndex == 0) ? root.colorScheme.background_weak : root.colorScheme.background_norm
|
||||
implicitHeight: children[0].implicitHeight
|
||||
implicitWidth: children[0].implicitWidth
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: signInItem.currentIndex == 0 ? 0 : parent.width / 4
|
||||
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
|
||||
implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
|
||||
|
||||
Button {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 80
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 80
|
||||
anchors.rightMargin: 80
|
||||
anchors.topMargin: 80
|
||||
colorScheme: root.colorScheme
|
||||
secondary: true
|
||||
text: qsTr("Back")
|
||||
visible: signInItem.currentIndex != 0
|
||||
|
||||
onClicked: {
|
||||
signInItem.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
GridLayout {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
columnSpacing: 0
|
||||
columns: 3
|
||||
rowSpacing: 0
|
||||
|
||||
// top margin
|
||||
Item {
|
||||
Layout.columnSpan: 3
|
||||
Layout.fillWidth: true
|
||||
|
||||
// Using binding component here instead of direct binding to avoid binding loop during construction of element
|
||||
Binding on Layout.preferredHeight {
|
||||
value: (parent.height - signInItem.height) / 4
|
||||
}
|
||||
}
|
||||
|
||||
// left margin
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: 80
|
||||
Layout.minimumWidth: 48
|
||||
Layout.preferredHeight: signInItem.height
|
||||
}
|
||||
SignIn {
|
||||
id: signInItem
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: 320
|
||||
colorScheme: root.colorScheme
|
||||
focus: true
|
||||
username: Backend.users.count === 1 && Backend.users.get(0) && (Backend.users.get(0).state === EUserState.SignedOut) ? Backend.users.get(0).username : ""
|
||||
}
|
||||
|
||||
// Right margin
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: 80
|
||||
Layout.minimumWidth: 48
|
||||
Layout.preferredHeight: signInItem.height
|
||||
}
|
||||
|
||||
// bottom margin
|
||||
Item {
|
||||
Layout.columnSpan: 3
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
Layout.preferredWidth: signInItem.currentIndex === 0 ? 0 : parent.width / 4
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 128 128" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(1.6,0,0,1.6,9.6,5.6)">
|
||||
<path d="M34,0C15.497,0 0.5,14.852 0.5,33.176L0.5,66.572C0.5,70.122 3.406,73 6.991,73L61.702,73C64.903,73 67.5,70.428 67.5,67.258L67.5,33.176C67.5,14.855 52.503,0 34,0ZM53.283,32.992L38.641,45.207C35.998,47.413 32.133,47.413 29.49,45.207L14.848,32.992C14.848,22.625 23.334,14.221 33.802,14.221L34.329,14.221C44.796,14.221 53.283,22.625 53.283,32.992Z" style="fill:rgb(109,74,255);fill-rule:nonzero;"/>
|
||||
<path d="M34,0C15.497,0 0.5,14.852 0.5,33.176L0.5,66.572C0.5,70.122 3.406,73 6.991,73L61.702,73C64.903,73 67.5,70.428 67.5,67.258L67.5,33.176C67.5,14.855 52.503,0 34,0ZM53.283,32.992L38.641,45.207C35.998,47.413 32.133,47.413 29.49,45.207L14.848,32.992C14.848,22.625 23.334,14.221 33.802,14.221L34.329,14.221C44.796,14.221 53.283,22.625 53.283,32.992Z" style="fill:url(#_Linear1);fill-rule:nonzero;"/>
|
||||
<path d="M38.664,45.355C37.138,46.579 33.168,48.292 29.504,45.355C25.841,42.418 18.237,35.924 14.893,33.045L14.911,33.045L14.848,32.992C14.848,22.625 23.334,14.221 33.802,14.221L34.329,14.221C44.796,14.221 53.283,22.625 53.283,32.992L53.219,33.045L53.276,33.045L53.276,73L61.702,73C64.903,73 67.5,70.428 67.5,67.258L67.5,33.176C67.5,14.855 52.503,0 34,0C15.497,0 0.5,14.852 0.5,33.176L0.5,35.274L21.217,52.914C22.744,54.354 26.757,56.37 30.595,52.914C34.433,49.459 37.574,46.435 38.664,45.355Z" style="fill:url(#_Radial2);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(9.85984,-28.1373,28.1373,9.85984,2.89896,81.4231)"><stop offset="0" style="stop-color:rgb(40,176,232);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(197,183,255);stop-opacity:0"/></linearGradient>
|
||||
<radialGradient id="_Radial2" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-63.0498,-56.1536,45.8629,-51.4953,63.5497,79.0473)"><stop offset="0" style="stop-color:rgb(226,219,255);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(109,74,255);stop-opacity:1"/></radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.8363 13.87C11.0407 13.6842 11.0557 13.368 10.87 13.1637L6.17573 8L10.87 2.83633C11.0557 2.632 11.0407 2.31578 10.8363 2.13003C10.632 1.94427 10.3158 1.95933 10.13 2.16366L5.13003 7.66366C4.95666 7.85437 4.95666 8.14562 5.13003 8.33633L10.13 13.8363C10.3158 14.0407 10.632 14.0557 10.8363 13.87Z" fill="#0C0C14"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 469 B |
@ -1,3 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14 2L13.2427 2L13.2427 1.99998L13.2426 2L13 2L9 2V3H12.2426L5.76613 9.47651L6.47324 10.1836L13 3.65686V7H14V3V2ZM2 2H5V3H3L3 13L13 13V11H14V14H13H3H2V13V3V2Z" fill="white"/>
|
||||
<path d="M12 4.72447L12 8.5C12 8.77614 11.7761 9 11.5 9C11.2239 9 11 8.77614 11 8.5V5.70711L5.35355 11.3536C5.15829 11.5488 4.84171 11.5488 4.64645 11.3536C4.45118 11.1583 4.45118 10.8417 4.64645 10.6464L10.2929 5H7.50002C7.22387 5 7.00002 4.77614 7.00002 4.5C7.00002 4.22386 7.22387 4 7.50002 4L11.2761 3.99999C11.3175 3.99994 11.3769 3.99987 11.4328 4.00452C11.5821 3.98436 11.7388 4.03167 11.8536 4.14645C11.9683 4.26124 12.0157 4.41796 11.9955 4.56731C12.0001 4.62322 12.0001 4.68283 12 4.72447Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3 1C1.89543 1 1 1.89543 1 3V13C1 14.1046 1.89543 15 3 15H13C14.1046 15 15 14.1046 15 13V3C15 1.89543 14.1046 1 13 1H3ZM13 2H3C2.44772 2 2 2.44772 2 3V13C2 13.5523 2.44772 14 3 14H13C13.5523 14 14 13.5523 14 13V3C14 2.44772 13.5523 2 13 2Z" fill="white"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 327 B After Width: | Height: | Size: 924 B |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 436 KiB |
@ -0,0 +1,11 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.9864 4.0315C14.3445 1.75773 17.6381 1.75773 18.9961 4.0315L31.232 24.5182C32.6254 26.8512 30.9445 29.8129 28.2272 29.8129H3.75539C1.03801 29.8129 -0.642844 26.8512 0.75053 24.5182L12.9864 4.0315Z" fill="url(#paint0_linear_4081_29778)"/>
|
||||
<path d="M14.5387 12.1944C14.5432 11.3954 15.1922 10.75 15.9912 10.75C16.7903 10.75 17.4392 11.3954 17.4437 12.1944L17.4829 19.25C17.4875 20.0771 16.8183 20.75 15.9912 20.75C15.1641 20.75 14.4949 20.0771 14.4995 19.25L14.5387 12.1944Z" fill="white"/>
|
||||
<path d="M17.4912 23.75C17.4912 24.5784 16.8196 25.25 15.9912 25.25C15.1628 25.25 14.4912 24.5784 14.4912 23.75C14.4912 22.9216 15.1628 22.25 15.9912 22.25C16.8196 22.25 17.4912 22.9216 17.4912 23.75Z" fill="white"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_4081_29778" x1="15.9913" y1="2.09253" x2="15.9913" y2="42.104" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFB800"/>
|
||||
<stop offset="1" stop-color="#FF8419"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@ -0,0 +1,151 @@
|
||||
<svg width="270" height="104" viewBox="0 0 270 104" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M179.366 102.5H3.05083C1.33939 102.5 0 101.161 0 99.4493V97.7378C0 96.0264 1.33939 94.687 3.05083 94.687H179.366C181.077 94.687 182.417 96.0264 182.417 97.7378V99.4493C182.417 101.161 181.003 102.5 179.366 102.5Z" fill="#B0D4E5"/>
|
||||
<path d="M164.229 94.6867H16.6963V9.42924C16.6963 4.51816 20.7144 0.5 25.6255 0.5H155.3C160.211 0.5 164.229 4.51816 164.229 9.42924V94.6867Z" fill="url(#paint0_radial_4147_15491)"/>
|
||||
<path d="M157.311 94.6862H24.3628V10.8426C24.3628 9.65203 25.3301 8.68469 26.5207 8.68469H155.079C156.269 8.68469 157.237 9.65203 157.237 10.8426V94.6862H157.311Z" fill="url(#paint1_linear_4147_15491)"/>
|
||||
<rect opacity="0.5" x="29" y="66" width="123" height="24" rx="3" fill="white"/>
|
||||
<rect x="29" y="14" width="123" height="48" rx="3" fill="white"/>
|
||||
<path d="M46.686 70.0266H55.3141C57.3414 70.0266 58.9735 71.6587 58.9735 73.686V82.3141C58.9735 84.3414 57.3414 85.9734 55.3141 85.9734H46.686C44.6587 85.9734 43.0266 84.3414 43.0266 82.3141V73.686C43.0266 71.6587 44.6587 70.0266 46.686 70.0266Z" fill="url(#paint2_linear_4147_15491)"/>
|
||||
<path d="M46.5205 74.5615C46.4365 74.5615 46.3573 74.576 46.2824 74.6058L47.783 76.1506L49.3001 77.7232L49.3278 77.7564L49.4164 77.845L49.505 77.9391L50.8062 79.2735C50.8279 79.287 50.8907 79.3452 50.9397 79.3697C51.0029 79.4013 51.0714 79.4304 51.142 79.4329C51.2182 79.4356 51.296 79.4138 51.3646 79.3805C51.4159 79.3555 51.4387 79.3197 51.4984 79.2735L53.0045 77.7176L55.9945 74.639C55.9003 74.5879 55.796 74.5615 55.6844 74.5615H46.5205ZM46.0609 74.7497C45.9009 74.9014 45.8007 75.1293 45.8007 75.3865V80.4585C45.8007 80.6667 45.8676 80.8559 45.9779 81.0011L46.1883 80.8018L47.7553 79.2791L49.1451 77.9336L49.1174 77.9003L47.5947 76.3333L46.072 74.7608L46.0609 74.7497ZM56.1938 74.7996L53.1927 77.9003L53.165 77.928L54.6102 79.3289L56.1772 80.8516L56.2714 80.9402C56.3557 80.8048 56.4043 80.6381 56.4043 80.4585V75.3865C56.4043 75.1572 56.3248 74.9492 56.1938 74.7996ZM49.3223 78.1163L47.938 79.4618L46.3655 80.9845L46.1661 81.1783C46.2712 81.246 46.3908 81.2891 46.5205 81.2891H55.6844C55.8403 81.2891 55.9818 81.2291 56.0997 81.134L56 81.0343L54.4275 79.5116L52.9823 78.1163L51.6811 79.4563C51.6107 79.503 51.5636 79.5547 51.4949 79.5865C51.3842 79.6377 51.2629 79.681 51.141 79.6791C51.0187 79.6773 50.8988 79.6294 50.7891 79.5755C50.7341 79.5484 50.7047 79.5215 50.6401 79.4673L49.3223 78.1163Z" fill="white"/>
|
||||
<g clip-path="url(#clip0_4147_15491)">
|
||||
<path d="M126.645 73.4143H126.646C127.138 71.6949 129.281 70.8555 131.465 70.8555C132.974 70.8555 134.329 71.3334 135.258 72.0918C134.682 72.1203 134.135 72.2329 133.634 72.4146C134.385 72.6933 135.029 73.1225 135.507 73.6535C135.19 73.5987 134.861 73.5699 134.524 73.5699C134.488 73.5699 134.452 73.5702 134.415 73.5709C135.283 74.8295 135.792 76.3554 135.792 78C135.792 82.3135 132.295 85.8103 127.981 85.8103C123.734 85.8103 120.171 82.2543 120.171 78C120.171 77.3281 120.261 76.6341 120.434 75.9837C120.479 75.8471 120.543 75.7161 120.627 75.6681C120.732 75.6081 120.828 75.787 120.844 75.8452C120.958 76.275 121.112 76.6906 121.302 77.0887C121.286 76.1969 121.667 75.3848 122.191 74.6821C122.54 74.2138 122.864 73.7796 123.014 72.5271C123.024 72.443 123.103 72.3825 123.184 72.4089C124.321 72.7812 124.929 74.6748 124.834 76.2583C125.463 76.3481 125.46 75.6919 125.46 75.6919C125.259 75.0747 125.393 73.9275 126.643 73.4143H126.645Z" fill="url(#paint3_linear_4147_15491)"/>
|
||||
<path opacity="0.9" d="M135.536 76.011C135.726 80.3067 132.195 84.0103 127.888 84.0103C123.857 84.0103 120.554 80.8945 120.255 76.9398C120.203 77.3052 120.174 77.6783 120.171 78.0576C120.202 82.2895 123.757 85.8103 127.981 85.8103C132.295 85.8103 135.792 82.3135 135.792 78C135.792 77.3126 135.703 76.646 135.536 76.011Z" fill="url(#paint4_radial_4147_15491)"/>
|
||||
<g style="mix-blend-mode:screen">
|
||||
<path d="M127.792 73.9835C127.707 73.8346 127.319 73.6144 127.149 73.576C127.792 71.5174 131.066 70.8854 133.07 71.2497C133.904 71.4012 134.942 71.8559 135.258 72.0918C134.33 71.3334 132.975 70.8555 131.466 70.8555C129.282 70.8555 127.138 71.6949 126.647 73.4143H126.645H126.643C125.393 73.9275 125.26 75.0751 125.46 75.6922C125.653 74.9561 126.571 74.0514 127.792 73.9835Z" fill="url(#paint5_radial_4147_15491)"/>
|
||||
</g>
|
||||
<path d="M130.061 72.5619C128.306 72.9071 127.733 73.0199 127.146 73.5781C127.805 71.8322 129.489 71.4785 131.494 72.2742C130.942 72.3884 130.47 72.4814 130.061 72.5619Z" fill="url(#paint6_linear_4147_15491)"/>
|
||||
<path d="M120.595 75.7446C120.116 77.7069 120.486 80.0134 122.664 81.949C122.015 81.2402 121.224 78.6226 122.971 76.7525C123.088 76.6265 123.291 76.7192 123.297 76.8914C123.441 80.7778 126.577 83.1514 130.192 82.7076C129.072 82.6446 125.367 81.3471 128.123 80.8337C129.564 80.5654 131.822 80.1446 131.822 78.1182C131.822 74.8328 129.282 73.8722 127.742 74.0151C126.688 74.113 125.749 74.7819 125.46 75.6916C125.571 76.0503 125.129 76.3014 124.834 76.2593C124.929 74.6758 124.321 72.7812 123.184 72.4088C123.103 72.3825 123.024 72.443 123.014 72.5271C122.864 73.7796 122.54 74.2137 122.191 74.6821C121.667 75.3848 121.286 76.1968 121.302 77.0887C121.112 76.6906 120.958 76.2749 120.844 75.8452C120.831 75.797 120.761 75.6626 120.677 75.6562C120.631 75.6528 120.607 75.6974 120.595 75.7446Z" fill="url(#paint7_radial_4147_15491)"/>
|
||||
<g style="mix-blend-mode:screen">
|
||||
<path d="M127.156 81.0327C129.277 82.7547 133.542 81.4637 133.542 77.2765C131.82 79.8867 129.627 81.687 127.156 81.0327Z" fill="url(#paint8_linear_4147_15491)"/>
|
||||
</g>
|
||||
<g style="mix-blend-mode:screen">
|
||||
<path d="M122.971 76.7525C123.015 76.7045 123.072 76.6881 123.126 76.6956C121.563 78.6023 122.824 81.9512 123.689 82.7739C123.738 82.9109 122.868 82.1985 122.749 82.0316C122.09 81.4731 121.147 78.7053 122.971 76.7525Z" fill="url(#paint9_linear_4147_15491)"/>
|
||||
</g>
|
||||
<path d="M127.982 81.1904C130.103 81.1904 131.823 79.7872 131.823 78.0562C131.823 76.3252 130.103 74.922 127.982 74.922C126.172 74.922 124.14 76.0994 124.14 78.102C124.141 81.1965 127.411 82.9768 130.197 82.7073C129.988 82.683 128.682 82.6136 127.799 81.6152C127.72 81.5252 127.582 81.3682 127.644 81.2661C127.707 81.1641 127.879 81.1904 127.982 81.1904Z" fill="url(#paint10_linear_4147_15491)"/>
|
||||
<path opacity="0.6" d="M131.404 76.6322L128.369 79.5368C128.099 79.7282 127.812 79.7423 127.529 79.5685L124.552 76.6427C124.636 76.5075 124.732 76.3779 124.838 76.2549C124.945 76.3552 125.049 76.4525 125.15 76.5474C125.932 77.28 126.564 77.8721 127.455 78.6325C127.858 78.9758 127.983 78.969 128.377 78.6325C129.396 77.7623 130.142 77.1038 131.111 76.2385C131.22 76.3633 131.318 76.4948 131.404 76.6322Z" fill="white"/>
|
||||
<mask id="mask0_4147_15491" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="124" y="76" width="8" height="7">
|
||||
<path d="M131.823 78.0561C131.823 79.7871 130.103 81.1903 127.982 81.1903C127.88 81.1903 127.707 81.164 127.645 81.266C127.582 81.3681 127.72 81.5251 127.8 81.6151C128.629 82.5532 129.832 82.6711 130.148 82.7021C130.168 82.7041 130.185 82.7057 130.198 82.7072C127.411 82.9767 124.142 81.1964 124.141 78.1019C124.141 77.5512 124.294 77.0629 124.554 76.6443L127.546 79.3617C127.759 79.5551 128.118 79.5551 128.331 79.3617L131.379 76.5928C131.663 77.0296 131.823 77.5276 131.823 78.0561Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_4147_15491)">
|
||||
<path opacity="0.7" d="M132.96 74.4413H123.251V83.244H132.96V74.4413Z" fill="url(#paint11_linear_4147_15491)"/>
|
||||
<g filter="url(#filter0_f_4147_15491)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M127.091 79.3802C126.525 78.7914 124.831 76.8357 124.831 76.8357L124.962 76.8413L127.614 78.8223C127.813 78.9667 128.096 78.9652 128.293 78.8189L130.894 76.8439L131.032 76.8331C131.032 76.8331 129.393 78.7426 128.765 79.3717C128.138 80.0007 127.656 79.969 127.091 79.3802Z" fill="#458FCD"/>
|
||||
</g>
|
||||
</g>
|
||||
<path d="M128.52 73.3543C128.931 73.2248 128.895 72.8179 128.895 72.8179C128.895 72.8179 128.69 72.5759 128.282 72.71C127.901 72.8356 127.842 73.107 127.842 73.107C127.842 73.107 128.05 73.5023 128.52 73.3543Z" fill="white"/>
|
||||
</g>
|
||||
<path d="M96.2971 71H87.7017C87.3166 71 86.9998 71.3169 86.9998 71.702V72.5L91.8444 74L96.9991 72.5V71.702C96.9991 71.3169 96.6822 71 96.2971 71Z" fill="#0364B8"/>
|
||||
<path d="M97.8241 78.7025C97.8973 78.4723 97.9558 78.2377 97.9991 78C97.9991 77.8811 97.9356 77.7709 97.8326 77.7115L97.8261 77.708L97.8241 77.7071L92.4054 74.62C92.382 74.6049 92.3578 74.591 92.3329 74.5785C92.1231 74.4745 91.8763 74.4745 91.6665 74.5785C91.6416 74.591 91.6174 74.6048 91.594 74.62L86.1753 77.7071L86.1733 77.708L86.1668 77.7115C86.0639 77.7709 86.0003 77.8811 86.0004 78C86.0436 78.2377 86.1021 78.4723 86.1753 78.7025L91.9209 82.905L97.8241 78.7025Z" fill="#0A2767"/>
|
||||
<path d="M93.9993 72.5H90.4995L89.489 74.0001L90.4995 75.5L93.9993 78.5H96.999V75.5L93.9993 72.5Z" fill="#28A8EA"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M86.9998 72.5H90.4995V75.5H86.9998V72.5Z" fill="#0078D4"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M93.9993 72.5H96.9991V75.5H93.9993V72.5Z" fill="#50D9FF"/>
|
||||
<path d="M93.9993 78.5L90.4995 75.5H86.9998V78.5L90.4995 81.5L95.9152 82.384L93.9993 78.5Z" fill="#0364B8"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M90.4995 75.5H93.9993V78.5H90.4995V75.5Z" fill="#0078D4"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M86.9998 78.5H90.4995V81.5H86.9998V78.5Z" fill="#064A8C"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M93.9993 78.5H96.9991V81.5H93.9993V78.5Z" fill="#0078D4"/>
|
||||
<path d="M92.0949 82.6091L86.1982 78.3091L86.4452 77.8746C86.4452 77.8746 91.8174 80.9346 91.8993 80.9806C91.9673 81.0079 92.0436 81.0057 92.1099 80.9746C92.1863 80.9316 97.5755 77.8596 97.5755 77.8596L97.8235 78.2941L92.0949 82.6091Z" fill="#0A2767" fill-opacity="0.498039"/>
|
||||
<path d="M97.8325 78.2886L97.826 78.2926L97.8245 78.2936L92.4059 81.3806C92.1872 81.5216 91.9103 81.5388 91.6759 81.4261L93.5632 83.9566L97.69 84.8551V84.8571C97.8846 84.7164 98 84.4903 98 84.2501V78.0001C98 78.119 97.9364 78.2292 97.8335 78.2886H97.8325Z" fill="#1490DF"/>
|
||||
<path d="M97.999 84.2502V83.8813L93.0078 81.0372L92.4053 81.3802C92.1868 81.5212 91.9098 81.5385 91.6754 81.4257L93.5628 83.9563L97.6895 84.8547V84.8567C97.8841 84.7159 97.9995 84.4899 97.9995 84.2497L97.999 84.2502Z" fill="black" fill-opacity="0.047059"/>
|
||||
<path d="M97.974 84.4417L92.5028 81.3247L92.4053 81.3802C92.1868 81.5212 91.9098 81.5385 91.6754 81.4257L93.5628 83.9562L97.6895 84.8547V84.8567C97.8295 84.7552 97.9303 84.6083 97.9745 84.4412L97.974 84.4417Z" fill="black" fill-opacity="0.098039"/>
|
||||
<path d="M86.1746 78.2951V78.2901H86.1697L86.1546 78.2801C86.0576 78.2204 85.9987 78.114 85.9996 78.0001V84.2511C85.9996 84.662 86.3378 85.0001 86.7487 85.0001H97.249C97.3114 84.9995 97.3736 84.9911 97.434 84.9751C97.4653 84.9697 97.4956 84.9595 97.524 84.9451C97.5346 84.9441 97.5448 84.9406 97.554 84.9351C97.5948 84.9184 97.6335 84.8966 97.669 84.8701L97.689 84.8551L86.1746 78.2951Z" fill="#28A8EA"/>
|
||||
<path d="M90.9995 82.3337V74.1672C90.9984 73.802 90.6982 73.5018 90.333 73.5007H87.0152V77.2287L86.1747 77.7077L86.1728 77.7087L86.1663 77.7122C86.0633 77.7717 85.9998 77.8819 85.9998 78.0007V83.0022V83.0002H90.333C90.6982 82.9992 90.9984 82.6989 90.9995 82.3337Z" fill="black" fill-opacity="0.098039"/>
|
||||
<path d="M90.4995 82.8336V74.6671C90.4984 74.3019 90.1982 74.0018 89.8331 74.0006H87.0152V77.2286L86.1747 77.7076L86.1728 77.7086L86.1663 77.7121C86.0633 77.7716 85.9998 77.8818 85.9998 78.0006V83.5022V83.5001H89.8331C90.1982 83.499 90.4984 83.1989 90.4995 82.8336ZM90.4995 81.8336V74.6671C90.4984 74.3019 90.1982 74.0018 89.8331 74.0006H87.0152V77.2286L86.1747 77.7076L86.1728 77.7086L86.1663 77.7121C86.0633 77.7716 85.9998 77.8818 85.9998 78.0006V82.5021V82.5001H89.8331C90.1982 82.499 90.4984 82.1989 90.4995 81.8336ZM89.9995 81.8336V74.6671C89.9984 74.3019 89.6983 74.0018 89.3331 74.0006H87.0152V77.2286L86.1747 77.7076L86.1728 77.7086L86.1663 77.7121C86.0633 77.7716 85.9998 77.8818 85.9998 78.0006V82.5021V82.5001H89.3331C89.6983 82.499 89.9984 82.1989 89.9995 81.8336Z" fill="black" fill-opacity="0.2"/>
|
||||
<path d="M82.6665 74.0002H89.3325C89.6981 74.0002 89.999 74.3011 89.999 74.6667V81.3332C89.999 81.6988 89.6981 81.9997 89.3325 81.9997H82.6665C82.3009 81.9997 82 81.6988 82 81.3332V74.6667C82 74.3011 82.3009 74.0002 82.6665 74.0002Z" fill="#0078D4"/>
|
||||
<path d="M83.9334 76.7341C84.1105 76.3567 84.3964 76.0409 84.7543 75.8272C85.1509 75.6001 85.6025 75.487 86.0593 75.5002C86.4823 75.4909 86.8999 75.5981 87.2662 75.8102C87.6108 76.0154 87.8884 76.3165 88.0651 76.6767C88.2575 77.0735 88.3535 77.5102 88.3451 77.9512C88.3545 78.4121 88.2556 78.8688 88.0567 79.2847C87.8761 79.6576 87.5901 79.9696 87.2342 80.1817C86.8537 80.4004 86.4205 80.5106 85.9817 80.5002C85.5495 80.5105 85.1227 80.402 84.7479 80.1867C84.4005 79.981 84.1196 79.6796 83.9389 79.3187C83.7452 78.9275 83.6481 78.4956 83.6559 78.0592C83.6473 77.6022 83.7424 77.1492 83.9339 76.7341H83.9334ZM84.8083 78.8632C84.9028 79.1018 85.063 79.3089 85.2703 79.4601C85.4813 79.6079 85.7343 79.6842 85.9917 79.6777C86.266 79.6885 86.5365 79.6099 86.7622 79.4537C86.967 79.3024 87.123 79.0942 87.2107 78.8552C87.3093 78.5887 87.3579 78.3062 87.3542 78.0222C87.3572 77.7356 87.3116 77.4505 87.2192 77.1792C87.1377 76.935 86.9873 76.7194 86.7862 76.5587C86.5665 76.3946 86.2967 76.3113 86.0228 76.3227C85.7597 76.3159 85.5011 76.3925 85.2843 76.5417C85.0731 76.6935 84.9093 76.9023 84.8123 77.1437C84.5977 77.6964 84.5965 78.3097 84.8088 78.8632H84.8083Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M37 24C37 22.3431 38.3431 21 40 21H42C43.6569 21 45 22.3431 45 24V26C45 27.6569 43.6569 29 42 29H40C38.3431 29 37 27.6569 37 26V24ZM47 22.5C47 21.6716 47.6716 21 48.5 21H89.5C90.3284 21 91 21.6716 91 22.5C91 23.3284 90.3284 24 89.5 24H48.5C47.6716 24 47 23.3284 47 22.5ZM48.5 26C47.6716 26 47 26.6716 47 27.5C47 28.3284 47.6716 29 48.5 29H81.5C82.3284 29 83 28.3284 83 27.5C83 26.6716 82.3284 26 81.5 26H48.5ZM37 36C37 34.3431 38.3431 33 40 33H42C43.6569 33 45 34.3431 45 36V38C45 39.6569 43.6569 41 42 41H40C38.3431 41 37 39.6569 37 38V36ZM40 45C38.3431 45 37 46.3431 37 48V50C37 51.6569 38.3431 53 40 53H42C43.6569 53 45 51.6569 45 50V48C45 46.3431 43.6569 45 42 45H40ZM47 34.5C47 33.6716 47.6716 33 48.5 33H89.5C90.3284 33 91 33.6716 91 34.5C91 35.3284 90.3284 36 89.5 36H48.5C47.6716 36 47 35.3284 47 34.5ZM48.5 45C47.6716 45 47 45.6716 47 46.5C47 47.3284 47.6716 48 48.5 48H89.5C90.3284 48 91 47.3284 91 46.5C91 45.6716 90.3284 45 89.5 45H48.5ZM47 39.5C47 38.6716 47.6716 38 48.5 38H81.5C82.3284 38 83 38.6716 83 39.5C83 40.3284 82.3284 41 81.5 41H48.5C47.6716 41 47 40.3284 47 39.5ZM48.5 50C47.6716 50 47 50.6716 47 51.5C47 52.3284 47.6716 53 48.5 53H81.5C82.3284 53 83 52.3284 83 51.5C83 50.6716 82.3284 50 81.5 50H48.5Z" fill="#B0D4E5"/>
|
||||
<circle cx="176" cy="53.5" r="2" fill="#B0D4E5"/>
|
||||
<circle cx="184" cy="53.5" r="2" fill="#B0D4E5"/>
|
||||
<circle cx="192" cy="53.5" r="2" fill="#B0D4E5"/>
|
||||
<circle cx="200" cy="53.5" r="2" fill="#B0D4E5"/>
|
||||
<path d="M233.817 30.5C223.274 30.5 214.729 39.0451 214.729 49.5874V68.8014C214.729 70.8441 216.385 72.5 218.428 72.5H249.601C251.424 72.5 252.904 71.0203 252.904 69.1965V49.5874C252.904 39.0465 244.359 30.5 233.817 30.5ZM244.804 49.4814L236.461 56.5096C234.955 57.7787 232.753 57.7787 231.247 56.5096L222.904 49.4814C222.904 43.5172 227.74 38.6817 233.704 38.6817H234.004C239.968 38.6817 244.804 43.5172 244.804 49.4814Z" fill="#6D4AFF"/>
|
||||
<path d="M233.817 30.5C223.274 30.5 214.729 39.0451 214.729 49.5874V68.8014C214.729 70.8441 216.385 72.5 218.428 72.5H249.601C251.424 72.5 252.904 71.0203 252.904 69.1965V49.5874C252.904 39.0465 244.359 30.5 233.817 30.5ZM244.804 49.4814L236.461 56.5096C234.955 57.7787 232.753 57.7787 231.247 56.5096L222.904 49.4814C222.904 43.5172 227.74 38.6817 233.704 38.6817H234.004C239.968 38.6817 244.804 43.5172 244.804 49.4814Z" fill="url(#paint12_linear_4147_15491)"/>
|
||||
<g filter="url(#filter1_i_4147_15491)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M236.474 56.5948C235.604 57.2989 233.343 58.2847 231.255 56.5948C229.168 54.9048 224.835 51.1687 222.93 49.5119H222.94L222.904 49.4814C222.904 43.5172 227.74 38.6817 233.704 38.6817H234.004C239.968 38.6817 244.804 43.5172 244.804 49.4814L244.767 49.5119H244.8V72.5H249.601C251.424 72.5 252.904 71.0203 252.904 69.1965V49.5874C252.904 39.0465 244.359 30.5 233.817 30.5C223.274 30.5 214.729 39.0451 214.729 49.5874V50.7946L226.533 60.9439C227.403 61.7723 229.69 62.9321 231.877 60.9439C234.064 58.9557 235.853 57.2161 236.474 56.5948Z" fill="url(#paint13_radial_4147_15491)"/>
|
||||
</g>
|
||||
<circle cx="254.278" cy="27.7778" r="15.2778" fill="url(#paint14_linear_4147_15491)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M260.702 24.1676C261.113 24.5761 261.116 25.2412 260.707 25.653L253.128 33.2918C252.93 33.4906 252.662 33.6024 252.382 33.6024C252.102 33.6024 251.834 33.4906 251.636 33.2918L247.758 29.383C247.349 28.9713 247.352 28.3062 247.764 27.8976C248.175 27.489 248.84 27.4916 249.249 27.9034L252.382 31.0608L259.216 24.1733C259.625 23.7615 260.29 23.759 260.702 24.1676Z" fill="white"/>
|
||||
<defs>
|
||||
<filter id="filter0_f_4147_15491" x="111.934" y="63.936" width="31.9953" height="28.7937" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="6.44854" result="effect1_foregroundBlur_4147_15491"/>
|
||||
</filter>
|
||||
<filter id="filter1_i_4147_15491" x="214.137" y="30.5" width="38.7676" height="43.6537" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="-0.592742" dy="1.65375"/>
|
||||
<feGaussianBlur stdDeviation="4.44556"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.462745 0 0 0 0 0.337255 0 0 0 0 1 0 0 0 0.24 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_4147_15491"/>
|
||||
</filter>
|
||||
<radialGradient id="paint0_radial_4147_15491" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(160.195 7) rotate(145.641) scale(59.357 92.9759)">
|
||||
<stop stop-color="#292842"/>
|
||||
<stop offset="1" stop-color="#38385F"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="paint1_linear_4147_15491" x1="21.9037" y1="100.487" x2="165.819" y2="14.2507" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#35168C"/>
|
||||
<stop offset="0.317708" stop-color="#FF5454"/>
|
||||
<stop offset="0.46875" stop-color="#FFDD64"/>
|
||||
<stop offset="0.677083" stop-color="#BCE6FF"/>
|
||||
<stop offset="0.911458" stop-color="#6983EF"/>
|
||||
<stop offset="1" stop-color="#2395FF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_4147_15491" x1="51.1117" y1="85.9094" x2="51.1272" y2="70.2191" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#70EFFF"/>
|
||||
<stop offset="1" stop-color="#5770FF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_4147_15491" x1="122.882" y1="73.4298" x2="133.892" y2="83.6998" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#1B91F3"/>
|
||||
<stop offset="1" stop-color="#0B68CB"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="paint4_radial_4147_15491" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(122.862 76.9635) rotate(66.5179) scale(6.60435 6.32678)">
|
||||
<stop offset="0.525579" stop-color="#0B4186" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="#0B4186" stop-opacity="0.45"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint5_radial_4147_15491" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(128.183 74.2511) rotate(-127.994) scale(1.41191 2.33636)">
|
||||
<stop stop-color="#EF3ACC" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="#EF3ACC" stop-opacity="0.64"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="paint6_linear_4147_15491" x1="125.946" y1="76.0588" x2="129.907" y2="71.6484" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0F5DB0"/>
|
||||
<stop offset="1" stop-color="#0F5DB0" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="paint7_radial_4147_15491" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(125.036 82.2554) rotate(-64.2627) scale(10.3034 12.7276)">
|
||||
<stop offset="0.0160882" stop-color="#094188"/>
|
||||
<stop offset="0.967387" stop-color="#0B4186" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="paint8_linear_4147_15491" x1="132.89" y1="79.2522" x2="131.048" y2="83.7752" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#E247C4" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="#E247C4" stop-opacity="0.64"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint9_linear_4147_15491" x1="121.466" y1="75.1958" x2="123.089" y2="81.7279" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.104632" stop-color="#EF3ACC"/>
|
||||
<stop offset="1" stop-color="#EF3ACC" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint10_linear_4147_15491" x1="127.982" y1="76.0698" x2="127.982" y2="82.6751" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="0.90535" stop-color="#BEE1FE"/>
|
||||
<stop offset="1" stop-color="#96CEFD"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint11_linear_4147_15491" x1="128.105" y1="79.8075" x2="128.105" y2="82.5745" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#BCE0FD"/>
|
||||
<stop offset="1" stop-color="#88CCFC"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint12_linear_4147_15491" x1="216.096" y1="77.3462" x2="221.812" y2="61.1923" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#28B0E8"/>
|
||||
<stop offset="1" stop-color="#C5B7FF" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="paint13_radial_4147_15491" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(250.653 75.9793) rotate(-138.034) scale(48.3148 39.5031)">
|
||||
<stop stop-color="#E2DBFF"/>
|
||||
<stop offset="1" stop-color="#6D4AFF"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="paint14_linear_4147_15491" x1="255.861" y1="10.272" x2="256.004" y2="43.0557" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#2AF091"/>
|
||||
<stop offset="1" stop-color="#00C5A1"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_4147_15491">
|
||||
<rect width="16" height="16" fill="white" transform="translate(120 70)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 22 KiB |
@ -0,0 +1,33 @@
|
||||
<svg width="190" height="104" viewBox="0 0 190 104" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M95.6666 29C78.0961 29 63.8542 43.2418 63.8542 60.8123V92.8357C63.8542 96.2402 66.6141 99 70.0185 99H121.973C125.013 99 127.479 96.5338 127.479 93.4941V60.8123C127.479 43.2441 113.237 29 95.6666 29ZM113.978 60.6357L100.074 72.3494C97.5638 74.4646 93.8933 74.4646 91.3835 72.3494L77.4789 60.6357C77.4789 50.6953 85.5381 42.6362 95.4785 42.6362H95.9786C105.919 42.6362 113.978 50.6953 113.978 60.6357Z" fill="#6D4AFF"/>
|
||||
<path d="M95.6666 29C78.0961 29 63.8542 43.2418 63.8542 60.8123V92.8357C63.8542 96.2402 66.6141 99 70.0185 99H121.973C125.013 99 127.479 96.5338 127.479 93.4941V60.8123C127.479 43.2441 113.237 29 95.6666 29ZM113.978 60.6357L100.074 72.3494C97.5638 74.4646 93.8933 74.4646 91.3835 72.3494L77.4789 60.6357C77.4789 50.6953 85.5381 42.6362 95.4785 42.6362H95.9786C105.919 42.6362 113.978 50.6953 113.978 60.6357Z" fill="url(#paint0_linear_4081_30418)"/>
|
||||
<g filter="url(#filter0_i_4081_30418)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M100.096 72.4913C98.6461 73.6649 94.8768 75.3079 91.3975 72.4913C87.9183 69.6747 80.6973 63.4479 77.5218 60.6866H77.5393L77.479 60.6357C77.479 50.6953 85.5382 42.6362 95.4785 42.6362H95.9786C105.919 42.6362 113.978 50.6953 113.978 60.6357L113.918 60.6866H113.972V99H121.973C125.013 99 127.479 96.5338 127.479 93.4941V60.8123C127.479 43.2441 113.237 29 95.6666 29C78.0961 29 63.8542 43.2418 63.8542 60.8123V62.8243L83.5277 79.7398C84.9774 81.1205 88.7881 83.0534 92.4331 79.7398C96.078 76.4262 99.0603 73.5268 100.096 72.4913Z" fill="url(#paint1_radial_4081_30418)"/>
|
||||
</g>
|
||||
<ellipse cx="128.5" cy="35.9999" rx="16" ry="15.9999" fill="url(#paint2_linear_4081_30418)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M135.227 32.2191C135.659 32.647 135.661 33.3434 135.233 33.7747L127.295 41.7746C127.089 41.9828 126.808 42.0998 126.515 42.0998C126.221 42.0998 125.94 41.9828 125.734 41.7746L121.672 37.6811C121.244 37.2499 121.247 36.5534 121.678 36.1255C122.109 35.6976 122.806 35.7003 123.233 36.1315L126.515 39.4381L133.672 32.2251C134.1 31.7939 134.796 31.7912 135.227 32.2191Z" fill="white"/>
|
||||
<defs>
|
||||
<filter id="filter0_i_4081_30418" x="62.8663" y="29" width="64.6127" height="72.7562" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="-0.987903" dy="2.75625"/>
|
||||
<feGaussianBlur stdDeviation="7.40927"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.462745 0 0 0 0 0.337255 0 0 0 0 1 0 0 0 0.24 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_4081_30418"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_4081_30418" x1="66.1324" y1="107.077" x2="75.659" y2="80.1539" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#28B0E8"/>
|
||||
<stop offset="1" stop-color="#C5B7FF" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="paint1_radial_4081_30418" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(123.728 104.799) rotate(-138.034) scale(80.5247 65.8384)">
|
||||
<stop stop-color="#E2DBFF"/>
|
||||
<stop offset="1" stop-color="#6D4AFF"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="paint2_linear_4081_30418" x1="130.158" y1="17.6667" x2="130.308" y2="52" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#2AF091"/>
|
||||
<stop offset="1" stop-color="#00C5A1"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |