Compare commits

..

1 Commits

Author SHA1 Message Date
86e8a566c7 chore: Umshiang Bridge 3.5.0 changelog. 2023-09-12 07:45:08 +02:00
315 changed files with 6429 additions and 20021 deletions

View File

@ -18,10 +18,6 @@
---
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 ))
@ -34,9 +30,256 @@ stages:
- test
- build
include:
- local: ci/rules.yml
- local: ci/env.yml
- local: ci/test.yml
- local: ci/build.yml
.rules-branch-and-MR-manual:
rules:
- if: $CI_COMMIT_BRANCH || $CI_PIPELINE_SOURCE == "merge_request_event"
when: manual
allow_failure: true
- when: never
.rules-branch-manual-MR-and-devel-always:
rules:
- if: $CI_COMMIT_BRANCH == "devel" || $CI_PIPELINE_SOURCE == "merge_request_event"
when: always
allow_failure: false
- if: $CI_COMMIT_BRANCH
when: manual
allow_failure: true
- when: never
.rules-branch-manual-scheduled-and-test-branch-always:
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
when: always
allow_failure: false
- if: $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME=~ /^test/
when: always
allow_failure: false
- if: $CI_COMMIT_BRANCH
when: manual
allow_failure: true
- when: never
# ENV
.env-windows:
before_script:
- export BRIDGE_SYNC_FORCE_MINIMUM_SPEC=1
- export GOROOT=/c/Go1.20/
- export PATH=$GOROOT/bin:$PATH
- export GOARCH=amd64
- export GOPATH=~/go1.20
- export GO111MODULE=on
- export PATH="${GOPATH}/bin:${PATH}"
- export MSYSTEM=
- export QT6DIR=/c/grrrQt/6.3.2/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"
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
- git config --global safe.directory '*'
- git status --porcelain
cache: {}
tags:
- windows-bridge
.env-darwin:
before_script:
- export BRIDGE_SYNC_FORCE_MINIMUM_SPEC=1
- export PATH=/usr/local/bin:$PATH
- export PATH=/usr/local/opt/git/bin:$PATH
- export PATH=/usr/local/opt/make/libexec/gnubin:$PATH
- export PATH=/usr/local/opt/gnu-sed/libexec/gnubin:$PATH
- export GOROOT=~/local/opt/go@1.20
- export PATH="${GOROOT}/bin:$PATH"
- export GOPATH=~/go1.20
- export PATH="${GOPATH}/bin:$PATH"
- export QT6DIR=/opt/Qt/6.3.2/macos
- export PATH="${QT6DIR}/bin:$PATH"
- uname -a
cache: {}
tags:
- macos-m1-bridge
.env-linux-build:
image: gitlab.protontech.ch:4567/go/bridge-internal:build-go1.20-qt6.3.2
variables:
VCPKG_DEFAULT_BINARY_CACHE: ${CI_PROJECT_DIR}/.cache
cache:
key: linux-vcpkg
paths:
- .cache
when: 'always'
before_script:
- mkdir -p .cache/bin
- export BRIDGE_SYNC_FORCE_MINIMUM_SPEC=1
- export PATH=$(pwd)/.cache/bin:$PATH
- export GOPATH="$CI_PROJECT_DIR/.cache"
- export PATH=$PATH:$QT6DIR/bin
- $(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
# Stage: TEST
lint:
stage: test
extends:
- .rules-branch-manual-MR-and-devel-always
script:
- make lint
tags:
- medium
bug-report-preview:
stage: test
extends:
- .rules-branch-and-MR-manual
script:
- make lint-bug-report-preview
tags:
- medium
.script-test:
stage: test
extends:
- .rules-branch-manual-MR-and-devel-always
script:
- make test
artifacts:
paths:
- coverage/**
test-linux:
extends:
- .script-test
tags:
- large
fuzz-linux:
stage: test
extends:
- .rules-branch-manual-MR-and-devel-always
script:
- make fuzz
tags:
- large
test-linux-race:
extends:
- test-linux
- .rules-branch-and-MR-manual
script:
- make test-race
test-integration:
extends:
- test-linux
script:
- make test-integration
test-integration-race:
extends:
- test-integration
- .rules-branch-and-MR-manual
script:
- make test-integration-race
test-integration-nightly:
extends:
- test-integration
- .rules-branch-manual-scheduled-and-test-branch-always
needs:
- test-integration
script:
- make test-integration-nightly
test-windows:
extends:
- .env-windows
- .script-test
test-darwin:
extends:
- .env-darwin
- .script-test
test-coverage:
stage: test
extends:
- .rules-branch-manual-scheduled-and-test-branch-always
script:
- ./utils/coverage.sh
coverage: '/total:.*\(statements\).*\d+\.\d+%/'
needs:
- test-linux
- test-windows
- test-darwin
- test-integration
- test-integration-nightly
tags:
- small
artifacts:
paths:
- coverage*
- coverage/**
when: 'always'
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
# Stage: BUILD
.script-build:
stage: build
needs: ["lint"]
extends:
- .rules-branch-and-MR-manual
script:
- make build
- git diff && git diff-index --quiet HEAD
- make vault-editor
artifacts:
expire_in: 1 day
when: always
name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"
paths:
- bridge_*.tgz
- vault-editor
build-linux:
extends:
- .script-build
- .env-linux-build
build-linux-qa:
extends:
- build-linux
- .rules-branch-manual-MR-and-devel-always
variables:
BUILD_TAGS: "build_qa"
build-darwin:
extends:
- .script-build
- .env-darwin
build-darwin-qa:
extends:
- build-darwin
variables:
BUILD_TAGS: "build_qa"
build-windows:
extends:
- .script-build
- .env-windows
build-windows-qa:
extends:
- build-windows
variables:
BUILD_TAGS: "build_qa"
# TODO: PUT BACK ALL THE JOBS! JUST DID THIS FOR NOW TO GET CI WORKING AGAIN...

View File

@ -10,7 +10,7 @@
* Windres (Windows)
* libglvnd and libsecret development files (Linux)
* pkg-config (Linux)
* cmake, ninja-build and Qt 6.4.3 are required to build the graphical user interface. On Linux,
* cmake, ninja-build and Qt 6 are required to build the graphical user interface. On Linux,
the Mesa OpenGL development files are also needed.
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.4.3](https://doc.qt.io/qt-6/gettingstarted.html).
[Qt 6.3](https://doc.qt.io/qt-6/gettingstarted.html).
Please note that qmake path must be in your `PATH` to ensure Qt to be found.
Also, before you start build **on Windows**, please unset the `MSYSTEM` variable

View File

@ -40,7 +40,6 @@ Proton Mail Bridge includes the following 3rd party software:
* [go-message](https://github.com/emersion/go-message) available under [license](https://github.com/emersion/go-message/blob/master/LICENSE)
* [go-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)
@ -50,7 +49,6 @@ Proton Mail Bridge includes the following 3rd party software:
* [uuid](https://github.com/google/uuid) available under [license](https://github.com/google/uuid/blob/master/LICENSE)
* [go-multierror](https://github.com/hashicorp/go-multierror) available under [license](https://github.com/hashicorp/go-multierror/blob/master/LICENSE)
* [html2text](https://github.com/jaytaylor/html2text) available under [license](https://github.com/jaytaylor/html2text/blob/master/LICENSE)
* [go-locale](https://github.com/jeandeaual/go-locale) available under [license](https://github.com/jeandeaual/go-locale/blob/master/LICENSE)
* [go-keychain](https://github.com/keybase/go-keychain) available under [license](https://github.com/keybase/go-keychain/blob/master/LICENSE)
* [dns](https://github.com/miekg/dns) available under [license](https://github.com/miekg/dns/blob/master/LICENSE)
* [memory](https://github.com/pbnjay/memory) available under [license](https://github.com/pbnjay/memory/blob/master/LICENSE)
@ -85,6 +83,7 @@ Proton Mail Bridge includes the following 3rd party software:
* [go-spew](https://github.com/davecgh/go-spew) available under [license](https://github.com/davecgh/go-spew/blob/master/LICENSE)
* [go-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)
@ -124,7 +123,6 @@ Proton Mail Bridge includes the following 3rd party software:
* [codec](https://github.com/ugorji/go/codec) available under [license](https://github.com/ugorji/go/codec/blob/master/LICENSE)
* [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)
@ -134,7 +132,5 @@ Proton Mail Bridge includes the following 3rd party software:
* [yaml](https://gopkg.in/yaml.v3) available under [license](https://github.com/go-yaml/yaml/blob/v3.0.1/LICENSE)
* [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 -->

View File

@ -3,203 +3,6 @@
Changelog [format](http://keepachangelog.com/en/1.0.0/)
## Xikou Bridge 3.8.2
### Fixed
* GODT-3235: Update bridge update key.
## Xikou Bridge 3.8.1
### Added
* GODT-3121: Suggest relevant KB articles in the in-app bug report form.
* GODT-2001: Add govulncheck to scan for vulnerabilities.
### Changed
* Keep nighlty-job log as artifact.
* Test: Improve TestMetadata_JobCorrectlyFinishesAfterCancel.
### Fixed
* GODT-3153: Do not take into account full address when hasing messages.
## Xikou Bridge 3.8.0
### Added
* Test: Add test scenarios to add an /Answered flag to a replied message and revert.
* GODT-3046: Added links to KB in error messages.
* Test(GODT-3113): Inline HTML message and HTML attachment is getting altered.
* Test(GODT-3124): Attempt to fix 401 during login.
### Changed
* GODT-3134: Br tag triggers installer.
* Added update events to bridge GUI tester.
### Fixed
* GODT-3142: Pass br tag if available.
* GODT-3151: Fix feature test with non modified HTML part.
* GODT-3151: Only modify HTML Meta content if UTF-8 charset override is needed.
* GODT-2851: Add empty text part if no text part when importing multipart.
* GODT-3102: Distinguish Vault Decryption from Serialization Errors.
* GODT-3124: Handling of sync child jobs.
* GODT-3148: Bump go-sysinfo to get rid of linker warning on macOS Sonoma.
* GODT-3124: Flaky tests.
* GODT-3022: Handle multipart/related on fake server.
* GODT-3133: Fix GetSystemLanguage.
* GODT-3124: Race condition in sync task waiter.
* GODT-3124: Race conditions reported by race check.
* GODT-2797: Encode attached key name and use same pubkey name as web-app.
* Fix case of IMAP login error.
* GODT-3132: Do not allow sending on disabled accounts.
* GODT-3046: fix typo spotted during KB article review.
* GODT-3129: Bad Event during after address order change.
* GODT-3117: Improve GetAllContacts and GetAllContactsEmail.
## 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
@ -272,7 +75,6 @@ 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

View File

@ -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.8.2+git
BRIDGE_APP_VERSION?=3.5.0+git
APP_VERSION:=${BRIDGE_APP_VERSION}
APP_FULL_NAME:=Proton Mail Bridge
APP_VENDOR:=Proton AG
@ -304,7 +304,6 @@ 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
@ -328,6 +327,13 @@ lint-bug-report:
lint-bug-report-preview:
python3 utils/validate_bug_report_file.py --file "internal/frontend/bridge-gui/bridge-gui/qml/Resources/bug_report_flow.json" --preview
gobinsec: gobinsec-cache.yml build
gobinsec -wait -cache -config utils/gobinsec_conf.yml ${EXE_TARGET} ${DEPLOY_DIR}/${TARGET_OS}/${LAUNCHER_EXE}
gobinsec-cache.yml:
./utils/gobinsec_update.sh
cp ./utils/gobinsec_update/gobinsec-cache-valid.yml ./gobinsec-cache.yml
updates: install-go-mod-outdated
# Uncomment the "-ci" to fail the job if something can be updated.
go list -u -m -json all | go-mod-outdated -update -direct #-ci

View File

@ -1,69 +0,0 @@
---
.script-build:
stage: build
needs: ["lint"]
extends:
- .rules-branch-and-MR-manual
script:
- make build
- git diff && git diff-index --quiet HEAD
- make vault-editor
artifacts:
expire_in: 1 day
when: always
name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"
paths:
- bridge_*.tgz
- vault-editor
build-linux:
extends:
- .script-build
- .env-linux-build
build-linux-qa:
extends:
- build-linux
- .rules-branch-manual-MR-and-devel-always
variables:
BUILD_TAGS: "build_qa"
build-darwin:
extends:
- .script-build
- .env-darwin
build-darwin-qa:
extends:
- build-darwin
variables:
BUILD_TAGS: "build_qa"
build-windows:
extends:
- .script-build
- .env-windows
build-windows-qa:
extends:
- build-windows
variables:
BUILD_TAGS: "build_qa"
trigger-qa-installer:
stage: build
needs: ["lint"]
extends:
- .rules-br-tag-always-branch-and-MR-manual
variables:
APP: bridge
WORKFLOW: build-all
SRC_TAG: $CI_COMMIT_BRANCH
TAG: $CI_COMMIT_TAG
SRC_HASH: $CI_COMMIT_SHA
trigger:
project: "jcuth/bridge-release"
branch: release/xikou

View File

@ -1,62 +0,0 @@
---
.env-windows:
before_script:
- export BRIDGE_SYNC_FORCE_MINIMUM_SPEC=1
- export GOROOT=/c/Go1.20/
- export PATH=$GOROOT/bin:$PATH
- export GOARCH=amd64
- export GOPATH=~/go1.20
- export GO111MODULE=on
- export PATH="${GOPATH}/bin:${PATH}"
- export MSYSTEM=
- 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"
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
- git config --global safe.directory '*'
- git status --porcelain
cache: {}
tags:
- windows-bridge
.env-darwin:
before_script:
- export BRIDGE_SYNC_FORCE_MINIMUM_SPEC=1
- export PATH=/usr/local/bin:$PATH
- export PATH=/usr/local/opt/git/bin:$PATH
- export PATH=/usr/local/opt/make/libexec/gnubin:$PATH
- export PATH=/usr/local/opt/gnu-sed/libexec/gnubin:$PATH
- export GOROOT=~/local/opt/go@1.20
- export PATH="${GOROOT}/bin:$PATH"
- export GOPATH=~/go1.20
- export PATH="${GOPATH}/bin:$PATH"
- export QT6DIR=/opt/Qt/6.4.3/macos
- export PATH="${QT6DIR}/bin:$PATH"
- uname -a
cache: {}
tags:
- macos-m1-bridge
.env-linux-build:
image: gitlab.protontech.ch:4567/go/bridge-internal:build-go1.20-qt6.4.3
variables:
VCPKG_DEFAULT_BINARY_CACHE: ${CI_PROJECT_DIR}/.cache
cache:
key: linux-vcpkg
paths:
- .cache
when: 'always'
before_script:
- mkdir -p .cache/bin
- export BRIDGE_SYNC_FORCE_MINIMUM_SPEC=1
- export PATH=$(pwd)/.cache/bin:$PATH
- export GOPATH="$CI_PROJECT_DIR/.cache"
- export PATH=$PATH:$QT6DIR/bin
- $(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:
- shared-large

View File

@ -1,58 +0,0 @@
---
.rules-branch-and-MR-manual:
rules:
- if: $CI_COMMIT_BRANCH || $CI_PIPELINE_SOURCE == "merge_request_event"
when: manual
allow_failure: true
- when: never
.rules-branch-manual-MR-and-devel-always:
rules:
- if: $CI_COMMIT_BRANCH == "devel" || $CI_PIPELINE_SOURCE == "merge_request_event"
when: always
allow_failure: false
- if: $CI_COMMIT_BRANCH
when: manual
allow_failure: true
- when: never
.rules-branch-manual-br-tag-and-MR-and-devel-always:
rules:
- if: $CI_COMMIT_BRANCH == "devel" || $CI_PIPELINE_SOURCE == "merge_request_event"
when: always
allow_failure: false
- if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_TAG =~ /^br-\d+/
when: always
allow_failure: false
- if: $CI_COMMIT_BRANCH
when: manual
allow_failure: true
- when: never
.rules-branch-manual-scheduled-and-test-branch-always:
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
when: always
allow_failure: false
- if: $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME=~ /^test/
when: always
allow_failure: false
- if: $CI_COMMIT_BRANCH
when: manual
allow_failure: true
- when: never
.rules-br-tag-always-branch-and-MR-manual:
rules:
- if: $CI_PIPELINE_SOURCE == 'push' && $CI_COMMIT_BRANCH
when: manual
allow_failure: true
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
when: manual
allow_failure: true
- if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_TAG =~ /^br-\d+/
when: always
- when: never

View File

@ -1,128 +0,0 @@
---
lint:
stage: test
extends:
- .rules-branch-manual-br-tag-and-MR-and-devel-always
script:
- make lint
tags:
- shared-medium
bug-report-preview:
stage: test
extends:
- .rules-branch-and-MR-manual
script:
- make lint-bug-report-preview
tags:
- shared-medium
.script-test:
stage: test
extends:
- .rules-branch-manual-MR-and-devel-always
script:
- make test
artifacts:
paths:
- coverage/**
test-linux:
extends:
- .script-test
tags:
- shared-large
fuzz-linux:
stage: test
extends:
- .rules-branch-manual-MR-and-devel-always
script:
- make fuzz
tags:
- shared-large
test-linux-race:
extends:
- test-linux
- .rules-branch-and-MR-manual
script:
- make test-race
test-integration:
extends:
- test-linux
script:
- make test-integration
test-integration-race:
extends:
- test-integration
- .rules-branch-and-MR-manual
script:
- make test-integration-race
test-integration-nightly:
extends:
- test-integration
- .rules-branch-manual-scheduled-and-test-branch-always
needs:
- test-integration
script:
- make test-integration-nightly | tee -a nightly-job.log
artifacts:
when: always
paths:
- nightly-job.log
test-windows:
extends:
- .env-windows
- .script-test
test-darwin:
extends:
- .env-darwin
- .script-test
test-coverage:
stage: test
extends:
- .rules-branch-manual-scheduled-and-test-branch-always
script:
- ./utils/coverage.sh
coverage: '/total:.*\(statements\).*\d+\.\d+%/'
needs:
- test-linux
- test-windows
- test-darwin
- test-integration
- test-integration-nightly
tags:
- shared-small
artifacts:
paths:
- coverage*
- coverage/**
when: 'always'
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
go-vuln-check:
extends:
- .rules-branch-manual-MR-and-devel-always
stage: test
tags:
- shared-medium
script:
- apt-get -y install jq
- ./utils/govulncheck.sh
artifacts:
when: always
paths:
- vulns*

View File

@ -23,6 +23,7 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/app"
"github.com/bradenaw/juniper/xslices"
"github.com/sirupsen/logrus"
)
/*
@ -43,5 +44,7 @@ import (
*/
func main() {
_ = app.New().Run(xslices.Filter(os.Args, func(arg string) bool { return !strings.Contains(arg, "-psn_") }))
if err := app.New().Run(xslices.Filter(os.Args, func(arg string) bool { return !strings.Contains(arg, "-psn_") })); err != nil {
logrus.Fatal(err)
}
}

30
go.mod
View File

@ -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.20231206152152-caaf10897f9e
github.com/ProtonMail/gluon v0.17.1-0.20230829112217-5d5c25c504b5
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
github.com/ProtonMail/go-proton-api v0.4.1-0.20231130083229-e8aa47d7a366
github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton
github.com/ProtonMail/go-proton-api v0.4.1-0.20230831064234-0e3a549b3f36
github.com/ProtonMail/gopenpgp/v2 v2.7.1-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
@ -16,13 +16,12 @@ require (
github.com/cucumber/godog v0.12.5
github.com/cucumber/messages-go/v16 v16.0.1
github.com/docker/docker-credential-helpers v0.6.3
github.com/elastic/go-sysinfo v1.11.2-0.20231129083954-35e55cd2a542
github.com/elastic/go-sysinfo v1.8.1
github.com/emersion/go-imap v1.2.1
github.com/emersion/go-imap-id v0.0.0-20190926060100-f94a56b9ecde
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
@ -32,7 +31,6 @@ require (
github.com/google/uuid v1.3.0
github.com/hashicorp/go-multierror v1.1.1
github.com/jaytaylor/html2text v0.0.0-20211105163654-bc68cce691ba
github.com/jeandeaual/go-locale v0.0.0-20220711133428-7de61946b173
github.com/keybase/go-keychain v0.0.0
github.com/miekg/dns v1.1.50
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58
@ -44,17 +42,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.17.0
golang.org/x/sys v0.13.0
golang.org/x/text v0.13.0
google.golang.org/grpc v1.56.3
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
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-20230717121622-edf196117233 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903 // indirect
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect
github.com/ProtonMail/go-srp v0.0.7 // indirect
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect
@ -70,6 +68,7 @@ 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
@ -80,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.3 // indirect
github.com/golang/protobuf v1.5.2 // 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
@ -109,20 +108,17 @@ 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.14.0 // indirect
golang.org/x/crypto v0.9.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-20230410155749-daa745c078e1 // indirect
google.golang.org/genproto v0.0.0-20230221151758-ace64dc21148 // 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-20231129100320-dddf8030d93a
github.com/keybase/go-keychain => github.com/cuthix/go-keychain v0.0.0-20230517073537-fc1740a83768
)

111
go.sum
View File

@ -11,49 +11,42 @@ cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqCl
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
fyne.io/fyne v1.4.2/go.mod h1:xL4c3WmpE/Tvz5CEm5vqsaizU/EeOCm9DYlL2GtTSiM=
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557 h1:l6surSnJ3RP4qA1qmKJ+hQn3UjytosdoG27WGjrDlVs=
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/Kodeworks/golang-image-ico v0.0.0-20141118225523-73f0f4cfade9/go.mod h1:7uhhqiBaR4CpN0k9rMjOtjpcfGd6DG2m04zQxKnWQ0I=
github.com/LBeernaertProton/resty/v2 v2.0.0-20231129100320-dddf8030d93a h1:eQO/GF/+H8/9udc9QAgieFr+jr1tjXlJo35RAhsUbWY=
github.com/LBeernaertProton/resty/v2 v2.0.0-20231129100320-dddf8030d93a/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/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs69zUkSzubzjBbL+cmOXgnmt9Fyd9ug=
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.20231206152152-caaf10897f9e h1:kHmSOTxynSip1WJvwZTFOGJPVfI42e/I8bDzDjLK7aM=
github.com/ProtonMail/gluon v0.17.1-0.20231206152152-caaf10897f9e/go.mod h1:Og5/Dz1MiGpCJn51XujZwxiLG7WzvvjE5PRpZBQmAHo=
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/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-20230717121622-edf196117233 h1:bdoKdh0f66/lrgVfYlxw0aqISY/KOqXmFJyGt7rGmnc=
github.com/ProtonMail/go-crypto v0.0.0-20230717121622-edf196117233/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
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-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.20231130083229-e8aa47d7a366 h1:W9P5GdDnuGkB3tbzKnXmUrTjIs6zk/K+4lpPTWzsoRE=
github.com/ProtonMail/go-proton-api v0.4.1-0.20231130083229-e8aa47d7a366/go.mod h1:t+hb0BfkmZ9fpvzVRpHC7limoowym6ln/j0XL9a8DDw=
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-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-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.4-proton h1:8tqHYM6IGsdEc6Vxf1TWiwpHNj8yIEQNACPhxsDagrk=
github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton/go.mod h1:omVkSsfPAhmptzPF/piMXb16wKIWUvVhZbVW7sJKh0A=
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/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=
github.com/abiosoft/ishell v2.0.0+incompatible/go.mod h1:HQR9AqF2R3P4XXpMpI0NAzgHf/aS6+zVXRj14cVk9qg=
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db h1:CjPUSXOiYptLbTdr1RceuZgSFDQ7U15ITERUGrUORx8=
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db/go.mod h1:rB3B4rKii8V21ydCbIzH5hZiCQE7f5E9SzUb/ZZx530=
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37 h1:28uU3TtuvQ6KRndxg9TrC868jBWmSKgh0GTXkACCXmA=
@ -71,7 +64,6 @@ github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJm
github.com/bradenaw/juniper v0.12.0 h1:Q/7icpPQD1nH/La5DobQfNEtwyrBSiSu47jOQx7lJEM=
github.com/bradenaw/juniper v0.12.0/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=
@ -117,12 +109,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/elastic/go-sysinfo v1.11.2-0.20231129083954-35e55cd2a542 h1:IFTm6NBbfSgZCaeEzorQhH4T7ZERl4j+1u7oXWzmJcM=
github.com/elastic/go-sysinfo v1.11.2-0.20231129083954-35e55cd2a542/go.mod h1:GKqR8bbMK/1ITnez9NIsIfXQr25aLhRJa7AfT8HpBFQ=
github.com/elastic/go-sysinfo v1.8.1 h1:4Yhj+HdV6WjbCRgGdZpPJ8lZQlXZLKDAeIkmQ/VRvi4=
github.com/elastic/go-sysinfo v1.8.1/go.mod h1:JfllUnzoQV/JRYymbH3dO1yggI3mV2oTKSXsDHM+uIM=
github.com/elastic/go-windows v1.0.1 h1:AlYZOldA+UJ0/2nBuqWdo90GFCgG9xuyw9SYzGUtJm0=
github.com/elastic/go-windows v1.0.1/go.mod h1:FoVvqWSun28vaDQPbj2Elfc0JahhPB7WQEGa3c814Ss=
github.com/emersion/go-imap v1.2.1 h1:+s9ZjMEjOB8NzZMVTM3cCenz2JrQIGGo5j1df19WjTA=
@ -132,6 +120,8 @@ github.com/emersion/go-imap-id v0.0.0-20190926060100-f94a56b9ecde/go.mod h1:sPwp
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-sasl v0.0.0-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=
@ -144,9 +134,6 @@ github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNu
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BMXYYRWTLOJKlh+lOBt6nUQgXAfB7oVIQt5cNreqSLI=
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:rZfgFAXFS/z/lEd6LJmf9HVZ1LkgYiHx5pHhV5DR16M=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fyne-io/mobile v0.1.2-0.20201127155338-06aeb98410cc/go.mod h1:/kOrWrZB6sasLbEy2JIvr4arEzQTXBTZGb3Y96yWbHY=
github.com/fyne-io/mobile v0.1.2/go.mod h1:/kOrWrZB6sasLbEy2JIvr4arEzQTXBTZGb3Y96yWbHY=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/getsentry/sentry-go v0.15.0 h1:CP9bmA7pralrVUedYZsmIHWpq/pBtXTSew7xvVpfLaA=
@ -157,9 +144,7 @@ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7/go.mod h1:482civXOzJJCPzJ4ZOX/pwvXBWSnzD4OKMdH4ClKGbk=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200625191551-73d3c3675aa3/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
@ -170,19 +155,18 @@ 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=
github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4=
github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gofrs/uuid v4.3.0+incompatible h1:CaSVZxm5B+7o45rtab4jC2G37WGYX1zQfuU2i6DSvnc=
github.com/gofrs/uuid v4.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff/go.mod h1:wfqRWLHRBsRgkp5dmbG56SA0DmVtwrF5N3oPdI8t+Aw=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
@ -194,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.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
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/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=
@ -256,16 +240,12 @@ github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2p
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jackmordaunt/icns v0.0.0-20181231085925-4f16af745526/go.mod h1:UQkeMHVoNcyXYq9otUupF7/h/2tmHlhrS2zw7ZVvUqc=
github.com/jaytaylor/html2text v0.0.0-20211105163654-bc68cce691ba h1:QFQpJdgbON7I0jr2hYW7Bs+XV0qjc3d5tZoDnRFnqTg=
github.com/jaytaylor/html2text v0.0.0-20211105163654-bc68cce691ba/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
github.com/jeandeaual/go-locale v0.0.0-20220711133428-7de61946b173 h1:jOONCXyzHWM+ukp+weX77o//U3pMeOj62CNxChJLxIU=
github.com/jeandeaual/go-locale v0.0.0-20220711133428-7de61946b173/go.mod h1:uO/uctjf8AcWhNfp5Ili6oPtyFrAoQXEtVY3N798VkQ=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 h1:rp+c0RAYOWj8l6qbCUTSiRLG/iKnW3K3/QfPPuSsBt4=
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901/go.mod h1:Z86h9688Y0wesXCyonoVr47MasHilkuLMqGhRZ4Hpak=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/josephspurrier/goversioninfo v0.0.0-20200309025242-14b0ab84c6ca/go.mod h1:eJTEwMjXb7kZ633hO3Ln9mBUCOjX2+FlTljvpl9SYdE=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
@ -288,7 +268,6 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/lucor/goinfo v0.0.0-20200401173949-526b5363a13a/go.mod h1:ORP3/rB5IsulLEBwQZCJyyV6niqmI7P4EWSmkug+1Ng=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
@ -324,14 +303,9 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/nicksnyder/go-i18n/v2 v2.1.1/go.mod h1:d++QJC9ZVf7pa48qrsRWhMJ5pSHIPmS3OLqK1niyLxs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
@ -390,8 +364,6 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/srwiley/oksvg v0.0.0-20200311192757-870daf9aa564/go.mod h1:afMbS0qvv1m5tfENCwnOdZGOF8RGR/FsZ7bvBxQGZG4=
github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9/go.mod h1:mvWM0+15UqyrFKqdRjY6LuAVJR0HOVhJlEgZ5JWtSWU=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@ -425,11 +397,8 @@ github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
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=
@ -448,10 +417,9 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-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.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
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/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=
@ -461,7 +429,6 @@ golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERs
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@ -473,7 +440,6 @@ golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
@ -492,21 +458,18 @@ golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
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=
@ -515,7 +478,6 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -535,11 +497,8 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200720211630-cb9d2d5c5666/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -548,46 +507,34 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-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=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/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=
@ -601,13 +548,11 @@ golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBn
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190808195139-e713427fea3f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200328031815-3db5fc6bac03/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
@ -635,13 +580,13 @@ google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-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-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
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/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.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc=
google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
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/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=
@ -649,7 +594,6 @@ google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
@ -661,7 +605,6 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@ -41,7 +41,6 @@ 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"
@ -205,7 +204,7 @@ func run(c *cli.Context) error {
}()
// Restart the app if requested.
err = withRestarter(exe, func(restarter *restarter.Restarter) error {
return withRestarter(exe, func(restarter *restarter.Restarter) error {
// Handle crashes with various actions.
return withCrashHandler(restarter, reporter, func(crashHandler *crash.Handler, quitCh <-chan struct{}) error {
migrationErr := migrateOldVersions()
@ -235,10 +234,8 @@ func run(c *cli.Context) error {
}
return withSingleInstance(settings, locations.GetLockFile(), version, func() error {
// 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 {
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 {
@ -246,7 +243,7 @@ func run(c *cli.Context) error {
}
// Migrate old accounts into the vault.
if err := migrateOldAccounts(locations, keychains, v); err != nil {
if err := migrateOldAccounts(locations, v); err != nil {
logrus.WithError(err).Error("Failed to migrate old accounts")
}
@ -268,7 +265,7 @@ func run(c *cli.Context) error {
// 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 {
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)
@ -279,8 +276,8 @@ func run(c *cli.Context) error {
b.PushError(bridge.ErrVaultCorrupt)
}
// Remove old updates files
b.RemoveOldUpdates()
// Start telemetry heartbeat process
b.StartHeartbeat(b)
// Run the frontend.
return runFrontend(c, crashHandler, restarter, locations, b, eventCh, quitCh, c.Int(flagParentPID))
@ -293,14 +290,6 @@ func run(c *cli.Context) error {
})
})
})
})
// if an error occurs, it must be logged now because we're about to close the log file.
if err != nil {
logrus.Fatal(err)
}
return err
}
// If there's another instance already running, try to raise it and exit.
@ -481,13 +470,6 @@ 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 {

View File

@ -37,7 +37,6 @@ 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"
)
@ -56,7 +55,6 @@ 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")
@ -99,7 +97,6 @@ func withBridge(
autostarter,
updater,
version,
keychains,
// The API stuff.
constants.APIHost,
@ -113,7 +110,6 @@ func withBridge(
crashHandler,
reporter,
imap.DefaultEpochUIDValidityGenerator(),
nil,
// The logging stuff.
c.String(flagLogIMAP) == "client" || c.String(flagLogIMAP) == "all",
@ -159,7 +155,7 @@ func newUpdater(locations *locations.Locations) (*updater.Updater, error) {
}
return updater.NewUpdater(
versioner.New(updatesDir),
updater.NewInstaller(versioner.New(updatesDir)),
verifier,
constants.UpdateName,
runtime.GOOS,

View File

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

View File

@ -35,6 +35,7 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/ProtonMail/proton-bridge/v3/pkg/algo"
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
dockerCredentials "github.com/docker/docker-credential-helpers/credentials"
"github.com/stretchr/testify/require"
)
@ -42,7 +43,7 @@ func TestMigratePrefsToVaultWithKeys(t *testing.T) {
// Create a new vault.
vault, corrupt, err := vault.New(t.TempDir(), t.TempDir(), []byte("my secret key"), async.NoopPanicHandler{})
require.NoError(t, err)
require.NoError(t, corrupt)
require.False(t, corrupt)
// load the old prefs file.
configDir := filepath.Join("testdata", "with_keys")
@ -63,7 +64,7 @@ func TestMigratePrefsToVaultWithoutKeys(t *testing.T) {
// Create a new vault.
vault, corrupt, err := vault.New(t.TempDir(), t.TempDir(), []byte("my secret key"), async.NoopPanicHandler{})
require.NoError(t, err)
require.NoError(t, corrupt)
require.False(t, corrupt)
// load the old prefs file.
configDir := filepath.Join("testdata", "without_keys")
@ -132,9 +133,11 @@ func TestKeychainMigration(t *testing.T) {
}
func TestUserMigration(t *testing.T) {
kcl := keychain.NewTestKeychainsList()
keychainHelper := keychain.NewTestHelper()
kc, err := keychain.NewKeychain("mock", "bridge", kcl.GetHelpers(), kcl.GetDefaultHelper())
keychain.Helpers["mock"] = func(string) (dockerCredentials.Helper, error) { return keychainHelper, nil }
kc, err := keychain.NewKeychain("mock", "bridge")
require.NoError(t, err)
require.NoError(t, kc.Put("brokenID", "broken"))
@ -173,9 +176,9 @@ func TestUserMigration(t *testing.T) {
v, corrupt, err := vault.New(settingsFolder, settingsFolder, token, async.NoopPanicHandler{})
require.NoError(t, err)
require.NoError(t, corrupt)
require.False(t, corrupt)
require.NoError(t, migrateOldAccounts(locations, kcl, v))
require.NoError(t, migrateOldAccounts(locations, v))
require.Equal(t, []string{wantCredentials.UserID}, v.GetUserIDs())
require.NoError(t, v.GetUser(wantCredentials.UserID, func(u *vault.User) {

View File

@ -30,37 +30,47 @@ import (
"github.com/sirupsen/logrus"
)
func WithVault(locations *locations.Locations, keychains *keychain.List, panicHandler async.PanicHandler, fn func(*vault.Vault, bool, bool) error) error {
func WithVault(locations *locations.Locations, panicHandler async.PanicHandler, fn func(*vault.Vault, bool, bool) error) error {
logrus.Debug("Creating vault")
defer logrus.Debug("Vault stopped")
// Create the encVault.
encVault, insecure, corrupt, err := newVault(locations, keychains, panicHandler)
encVault, insecure, corrupt, err := newVault(locations, panicHandler)
if err != nil {
return fmt.Errorf("could not create vault: %w", err)
}
logrus.WithFields(logrus.Fields{
"insecure": insecure,
"corrupt": corrupt != nil,
"corrupt": corrupt,
}).Debug("Vault created")
if corrupt != nil {
logrus.WithError(corrupt).Warn("Failed to load existing vault, vault has been reset")
// 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)
}
cert, _ := encVault.GetBridgeTLSCert()
certs.NewInstaller().LogCertInstallStatus(cert)
if err := encVault.SetCertsInstalled(true); err != nil {
return fmt.Errorf("failed to set certs installed: %w", err)
}
logrus.Debug("Certificates successfully installed")
}
// GODT-1950: Add teardown actions (e.g. to close the vault).
return fn(encVault, insecure, corrupt != nil)
return fn(encVault, insecure, corrupt)
}
func newVault(locations *locations.Locations, keychains *keychain.List, panicHandler async.PanicHandler) (*vault.Vault, bool, error, error) {
func newVault(locations *locations.Locations, panicHandler async.PanicHandler) (*vault.Vault, bool, bool, error) {
vaultDir, err := locations.ProvideSettingsPath()
if err != nil {
return nil, false, nil, fmt.Errorf("could not get vault dir: %w", err)
return nil, false, false, fmt.Errorf("could not get vault dir: %w", err)
}
logrus.WithField("vaultDir", vaultDir).Debug("Loading vault from directory")
@ -70,7 +80,7 @@ func newVault(locations *locations.Locations, keychains *keychain.List, panicHan
insecure bool
)
if key, err := loadVaultKey(vaultDir, keychains); err != nil {
if key, err := loadVaultKey(vaultDir); err != nil {
logrus.WithError(err).Error("Could not load/create vault key")
insecure = true
@ -82,24 +92,24 @@ func newVault(locations *locations.Locations, keychains *keychain.List, panicHan
gluonCacheDir, err := locations.ProvideGluonCachePath()
if err != nil {
return nil, false, nil, fmt.Errorf("could not provide gluon path: %w", err)
return nil, false, false, fmt.Errorf("could not provide gluon path: %w", err)
}
vault, corrupt, err := vault.New(vaultDir, gluonCacheDir, vaultKey, panicHandler)
if err != nil {
return nil, false, corrupt, fmt.Errorf("could not create vault: %w", err)
return nil, false, false, fmt.Errorf("could not create vault: %w", err)
}
return vault, insecure, corrupt, nil
}
func loadVaultKey(vaultDir string, keychains *keychain.List) ([]byte, error) {
func loadVaultKey(vaultDir string) ([]byte, error) {
helper, err := vault.GetHelper(vaultDir)
if err != nil {
return nil, fmt.Errorf("could not get keychain helper: %w", err)
}
kc, err := keychain.NewKeychain(helper, constants.KeyChainName, keychains.GetHelpers(), keychains.GetDefaultHelper())
kc, err := keychain.NewKeychain(helper, constants.KeyChainName)
if err != nil {
return nil, fmt.Errorf("could not create keychain: %w", err)
}

View File

@ -45,7 +45,6 @@ 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"
@ -75,7 +74,7 @@ type Bridge struct {
installCh chan installJob
// heartbeat is the telemetry heartbeat for metrics.
heartbeat *heartBeatState
heartbeat telemetry.Heartbeat
// curVersion is the current version of the bridge,
// newVersion is the version that was installed by the updater.
@ -83,9 +82,6 @@ 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
@ -128,6 +124,9 @@ 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
}
@ -139,7 +138,6 @@ 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
@ -150,7 +148,6 @@ 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
@ -166,7 +163,6 @@ func New(
// bridge is the bridge.
bridge, err := newBridge(
context.Background(),
tasks,
imapEventCh,
@ -175,7 +171,6 @@ func New(
autostarter,
updater,
curVersion,
keychains,
panicHandler,
reporter,
@ -183,7 +178,6 @@ func New(
identifier,
proxyCtl,
uidValidityGenerator,
heartBeatManager,
logIMAPClient, logIMAPServer, logSMTP,
)
if err != nil {
@ -202,7 +196,6 @@ func New(
}
func newBridge(
ctx context.Context,
tasks *async.Group,
imapEventCh chan imapEvents.Event,
@ -211,7 +204,6 @@ func newBridge(
autostarter Autostarter,
updater Updater,
curVersion *semver.Version,
keychains *keychain.List,
panicHandler async.PanicHandler,
reporter reporter.Reporter,
@ -219,7 +211,6 @@ func newBridge(
identifier identifier.Identifier,
proxyCtl ProxyController,
uidValidityGenerator imap.UIDValidityGenerator,
heartbeatManager telemetry.HeartbeatManager,
logIMAPClient, logIMAPServer, logSMTP bool,
) (*Bridge, error) {
@ -265,13 +256,9 @@ func newBridge(
newVersion: curVersion,
newVersionLock: safe.NewRWMutex(),
keychains: keychains,
panicHandler: panicHandler,
reporter: reporter,
heartbeat: newHeartBeatState(ctx, panicHandler),
focusService: focusService,
autostarter: autostarter,
locator: locator,
@ -301,13 +288,7 @@ func newBridge(
return nil, err
}
if heartbeatManager == nil {
bridge.heartbeat.init(bridge, bridge)
} else {
bridge.heartbeat.init(bridge, heartbeatManager)
}
bridge.syncService.Run()
bridge.syncService.Run(bridge.tasks)
return bridge, nil
}
@ -436,9 +417,6 @@ 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 {
@ -451,8 +429,6 @@ func (bridge *Bridge) Close(ctx context.Context) {
logrus.WithError(err).Error("Failed to close servers")
}
bridge.syncService.Close()
// Stop all ongoing tasks.
bridge.tasks.CancelAndWait()
@ -511,15 +487,27 @@ func (bridge *Bridge) remWatcher(watcher *watcher.Watcher[events.Event]) {
watcher.Close()
}
func (bridge *Bridge) onStatusUp(_ context.Context) {
func (bridge *Bridge) onStatusUp(ctx 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():

View File

@ -49,7 +49,6 @@ 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"
@ -586,7 +585,7 @@ func TestBridge_MissingGluonStore(t *testing.T) {
require.NoError(t, os.RemoveAll(gluonDir))
// Bridge starts but can't find the gluon store dir; there should be no error.
withBridgeWaitForServers(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
// ...
})
})
@ -951,7 +950,6 @@ func withBridgeNoMocks(
mocks.Autostarter,
mocks.Updater,
v2_3_0,
keychain.NewTestKeychainsList(),
// The API stuff.
apiURL,
@ -963,7 +961,6 @@ func withBridgeNoMocks(
mocks.CrashHandler,
mocks.Reporter,
testUIDValidityGenerator,
mocks.Heartbeat,
// The logging stuff.
os.Getenv("BRIDGE_LOG_IMAP_CLIENT") == "1",
@ -973,6 +970,9 @@ 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{})

View File

@ -19,7 +19,6 @@ package bridge
import (
"context"
"errors"
"io"
"github.com/ProtonMail/go-proton-api"
@ -34,133 +33,63 @@ const (
DefaultMaxSessionCountForBugReport = 10
)
type ReportBugReq struct {
OSType string
OSVersion string
Title string
Description string
Username string
Email string
EmailClient string
IncludeLogs bool
}
func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, title, description, username, email, client string, attachLogs bool) error {
var account string
func (bridge *Bridge) ReportBug(ctx context.Context, report *ReportBugReq) error {
if info, err := bridge.QueryUserInfo(report.Username); err == nil {
report.Username = info.Username
if info, err := bridge.QueryUserInfo(username); err == nil {
account = info.Username
} else if userIDs := bridge.GetUserIDs(); len(userIDs) > 0 {
if err := bridge.vault.GetUser(userIDs[0], func(user *vault.User) {
report.Username = user.Username()
account = user.Username()
}); err != nil {
return err
}
}
var attachments []proton.ReportBugAttachment
if report.IncludeLogs {
logs, err := bridge.CollectLogs()
var attachment []proton.ReportBugAttachment
if attachLogs {
logsPath, err := bridge.locator.ProvideLogsPath()
if err != nil {
return err
}
attachments = append(attachments, logs)
}
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 == "" {
buffer, err := logging.ZipLogsForBugReport(logsPath, DefaultMaxSessionCountForBugReport, DefaultMaxBugReportZipSize)
if err != nil {
return err
}
safe.RLock(func() {
body, err := io.ReadAll(buffer)
if err != nil {
return err
}
attachment = append(attachment, proton.ReportBugAttachment{
Name: "logs.zip",
Filename: "logs.zip",
MIMEType: "application/zip",
Body: body,
})
}
safe.Lock(func() {
for _, user := range bridge.users {
user.ReportBugSent()
}
}, bridge.usersLock)
// 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
}
return bridge.api.ReportBug(ctx, proton.ReportBugReq{
OS: osType,
OSVersion: osVersion,
func (bridge *Bridge) CollectLogs() (proton.ReportBugAttachment, error) {
logsPath, err := bridge.locator.ProvideLogsPath()
if err != nil {
return proton.ReportBugAttachment{}, err
}
Title: "[Bridge] Bug - " + title,
Description: description,
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,
Client: client,
ClientType: proton.ClientTypeEmail,
ClientVersion: constants.AppVersion(bridge.curVersion.Original()),
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...)
Username: account,
Email: email,
}, attachment...)
}

View File

@ -22,7 +22,7 @@ import (
)
func (bridge *Bridge) ReportBugClicked() {
safe.RLock(func() {
safe.Lock(func() {
for _, user := range bridge.users {
user.ReportBugClicked()
}
@ -30,17 +30,17 @@ func (bridge *Bridge) ReportBugClicked() {
}
func (bridge *Bridge) AutoconfigUsed(client string) {
safe.RLock(func() {
safe.Lock(func() {
for _, user := range bridge.users {
user.AutoconfigUsed(client)
}
}, bridge.usersLock)
}
func (bridge *Bridge) ExternalLinkClicked(article string) {
safe.RLock(func() {
func (bridge *Bridge) KBArticleOpened(article string) {
safe.Lock(func() {
for _, user := range bridge.users {
user.ExternalLinkClicked(article)
user.KBArticleOpened(article)
}
}, bridge.usersLock)
}

View File

@ -19,7 +19,6 @@ package bridge
import (
"context"
"errors"
"strings"
"github.com/ProtonMail/proton-bridge/v3/internal/clientconfig"
@ -31,8 +30,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,
@ -45,28 +44,16 @@ func (bridge *Bridge) ConfigureAppleMail(ctx context.Context, userID, address st
return ErrNoSuchUser
}
emails := user.Emails()
displayNames := user.DisplayNames()
if (len(emails) == 0) || (len(displayNames) == 0) {
return errors.New("could not retrieve user address info")
}
if address == "" {
address = emails[0]
address = user.Emails()[0]
}
var username, displayName, addresses string
username := address
addresses := address
if user.GetAddressMode() == vault.CombinedMode {
username = address
displayName = displayNames[username]
addresses = strings.Join(emails, ",")
} else {
username = address
addresses = address
displayName = displayNames[address]
if len(displayName) == 0 {
displayName = address
}
username = user.Emails()[0]
addresses = strings.Join(user.Emails(), ",")
}
if useragent.IsCatalinaOrNewer() && !bridge.vault.GetSMTPSSL() {
@ -82,7 +69,6 @@ func (bridge *Bridge) ConfigureAppleMail(ctx context.Context, userID, address st
bridge.vault.GetIMAPSSL(),
bridge.vault.GetSMTPSSL(),
username,
displayName,
addresses,
user.BridgePass(),
)

View File

@ -20,100 +20,18 @@ 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() {
@ -162,6 +80,49 @@ func (bridge *Bridge) SetLastHeartbeatSent(timestamp time.Time) error {
return bridge.vault.SetLastHeartbeatSent(timestamp)
}
func (bridge *Bridge) GetHeartbeatPeriodicInterval() time.Duration {
return HeartbeatCheckInterval
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)
}

View File

@ -1,24 +0,0 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge
import "golang.org/x/exp/maps"
func (bridge *Bridge) GetHelpersNames() []string {
return maps.Keys(bridge.keychains.GetHelpers())
}

View File

@ -7,7 +7,6 @@ import (
"os"
"sync"
"testing"
"time"
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/proton-bridge/v3/internal/bridge/mocks"
@ -52,7 +51,6 @@ func NewMocks(tb testing.TB, version, minAuto *semver.Version) *Mocks {
// this is called at start of heartbeat process.
mocks.Heartbeat.EXPECT().IsTelemetryAvailable(gomock.Any()).AnyTimes()
mocks.Heartbeat.EXPECT().GetHeartbeatPeriodicInterval().AnyTimes().Return(500 * time.Millisecond)
return mocks
}
@ -156,7 +154,3 @@ func (testUpdater *TestUpdater) GetVersionInfo(_ context.Context, _ updater.Down
func (testUpdater *TestUpdater) InstallUpdate(_ context.Context, _ updater.Downloader, _ updater.VersionInfo) error {
return nil
}
func (testUpdater *TestUpdater) RemoveOldUpdates() error {
return nil
}

View File

@ -36,20 +36,6 @@ 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()

View File

@ -33,7 +33,6 @@ 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"
@ -337,9 +336,6 @@ 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
@ -347,7 +343,7 @@ Date: Mon, 13 Mar 2023 16:06:16 +0100
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
Content-Disposition: attachment;
Content-Disposition: inline;
filename=Cat_August_2010-4.jpeg
Content-Type: image/jpeg;
name="Cat_August_2010-4.jpeg"
@ -364,7 +360,7 @@ Subject: A new message Part2
Date: Mon, 13 Mar 2023 16:06:16 +0100
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
Content-Disposition: attachment;
Content-Disposition: inline;
filename=Cat_August_2010-4.jpeg
Content-Type: image/jpeg;
name="Cat_August_2010-4.jpeg"
@ -524,234 +520,3 @@ SGVsbG8gd29ybGQK
})
})
}
func TestBridge_SendInlineImage(t *testing.T) {
const messageInlineImageOnly = `Content-Type: multipart/mixed;
boundary="Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84"
Subject: A new message
Date: Mon, 13 Mar 2023 16:06:16 +0100
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
Content-Disposition: inline;
filename=Cat_August_2010-4.jpeg
Content-Type: image/jpeg;
name="Cat_August_2010-4.jpeg"
Content-Transfer-Encoding: base64
SGVsbG8gd29ybGQ=
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84--
`
const messageInlineImageWithHTML = `Content-Type: multipart/mixed;
boundary="Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84"
Subject: A new message Part2
Date: Mon, 13 Mar 2023 16:06:16 +0100
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
Content-Type: text/html;charset=utf8
Content-Transfer-Encoding: quoted-printable
Hello world
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
Content-Disposition: inline;
filename=Cat_August_2010-4.jpeg
Content-Type: image/jpeg;
name="Cat_August_2010-4.jpeg"
Content-Transfer-Encoding: base64
SGVsbG8gd29ybGQ=
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84--
`
const messageInlineImageWithText = `Content-Type: multipart/mixed;
boundary="Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84"
Subject: A new message Part3
Date: Mon, 13 Mar 2023 16:06:16 +0100
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
Content-Type: text/plain;charset=utf8
Content-Transfer-Encoding: quoted-printable
Hello world
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
Content-Disposition: inline;
filename=Cat_August_2010-4.jpeg
Content-Type: image/jpeg;
name="Cat_August_2010-4.jpeg"
Content-Transfer-Encoding: base64
SGVsbG8gd29ybGQ=
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84--
`
const messageInlineImageFollowedByText = `Content-Type: multipart/mixed;
boundary="Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84"
Subject: A new message Part4
Date: Mon, 13 Mar 2023 16:06:16 +0100
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
Content-Disposition: inline;
filename=Cat_August_2010-4.jpeg
Content-Type: image/jpeg;
name="Cat_August_2010-4.jpeg"
Content-Transfer-Encoding: base64
SGVsbG8gd29ybGQ=
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84
Content-Type: text/plain;charset=utf8
Content-Transfer-Encoding: quoted-printable
Hello world
--Apple-Mail=_E7AC06C7-4EB2-4453-8CBB-80F4412A7C84--
`
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
_, _, err := s.CreateUser("recipient", password)
require.NoError(t, err)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
smtpWaiter := waitForSMTPServerReady(bridge)
defer smtpWaiter.Done()
senderUserID, err := bridge.LoginFull(ctx, username, password, nil, nil)
require.NoError(t, err)
recipientUserID, err := bridge.LoginFull(ctx, "recipient", password, nil, nil)
require.NoError(t, err)
senderInfo, err := bridge.GetUserInfo(senderUserID)
require.NoError(t, err)
recipientInfo, err := bridge.GetUserInfo(recipientUserID)
require.NoError(t, err)
messages := []string{
messageInlineImageOnly,
messageInlineImageWithHTML,
messageInlineImageWithText,
messageInlineImageFollowedByText,
}
smtpWaiter.Wait()
for _, m := range messages {
// Dial the server.
client, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort())))
require.NoError(t, err)
defer client.Close() //nolint:errcheck
// Upgrade to TLS.
require.NoError(t, client.StartTLS(&tls.Config{InsecureSkipVerify: true}))
// Authorize with SASL LOGIN.
require.NoError(t, client.Auth(sasl.NewLoginClient(
senderInfo.Addresses[0],
string(senderInfo.BridgePass)),
))
// Send the message.
require.NoError(t, client.SendMail(
senderInfo.Addresses[0],
[]string{recipientInfo.Addresses[0]},
strings.NewReader(m),
))
}
// Connect the sender IMAP client.
senderIMAPClient, err := eventuallyDial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
require.NoError(t, err)
require.NoError(t, senderIMAPClient.Login(senderInfo.Addresses[0], string(senderInfo.BridgePass)))
defer senderIMAPClient.Logout() //nolint:errcheck
// Connect the recipient IMAP client.
recipientIMAPClient, err := eventuallyDial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
require.NoError(t, err)
require.NoError(t, recipientIMAPClient.Login(recipientInfo.Addresses[0], string(recipientInfo.BridgePass)))
defer recipientIMAPClient.Logout() //nolint:errcheck
require.Eventually(t, func() bool {
messages, err := clientFetch(senderIMAPClient, `Sent`, imap.FetchBodyStructure)
require.NoError(t, err)
if len(messages) != 4 {
return false
}
// messages may not be in order
for _, message := range messages {
require.Equal(t, 1, len(message.BodyStructure.Parts))
require.Equal(t, "multipart", message.BodyStructure.MIMEType)
require.Equal(t, "mixed", message.BodyStructure.MIMESubType)
require.Equal(t, "multipart", message.BodyStructure.Parts[0].MIMEType)
require.Equal(t, "related", message.BodyStructure.Parts[0].MIMESubType)
require.Len(t, message.BodyStructure.Parts[0].Parts, 2)
require.Equal(t, "text", message.BodyStructure.Parts[0].Parts[0].MIMEType)
require.Equal(t, "html", message.BodyStructure.Parts[0].Parts[0].MIMESubType)
require.Equal(t, "image", message.BodyStructure.Parts[0].Parts[1].MIMEType)
require.Equal(t, "jpeg", message.BodyStructure.Parts[0].Parts[1].MIMESubType)
}
return true
}, 10*time.Second, 100*time.Millisecond)
})
})
}
func TestBridge_SendAddressDisabled(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
recipientUserID, _, err := s.CreateUser("recipient", password)
require.NoError(t, err)
senderUserID, addrID, err := s.CreateUser("sender", password)
require.NoError(t, err)
require.NoError(t, s.ChangeAddressAllowSend(senderUserID, addrID, false))
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
smtpWaiter := waitForSMTPServerReady(bridge)
defer smtpWaiter.Done()
senderUserID, err := bridge.LoginFull(ctx, "sender", password, nil, nil)
require.NoError(t, err)
_, err = bridge.LoginFull(ctx, "recipient", password, nil, nil)
require.NoError(t, err)
smtpWaiter.Wait()
recipientInfo, err := bridge.GetUserInfo(recipientUserID)
require.NoError(t, err)
senderInfo, err := bridge.GetUserInfo(senderUserID)
require.NoError(t, err)
// Dial the server.
client, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort())))
require.NoError(t, err)
defer client.Close() //nolint:errcheck
// Upgrade to TLS.
require.NoError(t, client.StartTLS(&tls.Config{InsecureSkipVerify: true}))
require.NoError(t, client.Auth(sasl.NewLoginClient(
senderInfo.Addresses[0],
string(senderInfo.BridgePass)),
))
// Send the message.
err = client.SendMail(
senderInfo.Addresses[0],
[]string{recipientInfo.Addresses[0]},
strings.NewReader("Subject: Test 1\r\n\r\nHello world!"),
)
smtpErr := smtpservice.NewErrCanNotSendOnAddress(senderInfo.Addresses[0])
require.Equal(t, fmt.Sprintf("Error: %v", smtpErr.Error()), err.Error())
})
})
}

View File

@ -22,7 +22,6 @@ import (
"fmt"
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/proton-bridge/v3/internal/kb"
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/services/userevents"
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
@ -262,12 +261,9 @@ func (bridge *Bridge) SetTelemetryDisabled(isDisabled bool) error {
return err
}
// If telemetry is re-enabled locally, try to send the heartbeat.
if isDisabled {
bridge.heartbeat.stop()
} else {
bridge.heartbeat.start()
if !isDisabled {
defer bridge.goHeartbeat()
}
return nil
}
@ -311,10 +307,6 @@ func (bridge *Bridge) SetColorScheme(colorScheme string) error {
return bridge.vault.SetColorScheme(colorScheme)
}
func (bridge *Bridge) GetKnowledgeBaseSuggestions(userInput string) (kb.ArticleList, error) {
return kb.GetSuggestions(userInput)
}
// FactoryReset deletes all users, wipes the vault, and deletes all files.
// Note: it does not clear the keychain. The only entry in the keychain is the vault password,
// which we need at next startup to decrypt the vault.

View File

@ -37,7 +37,6 @@ 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"
@ -580,116 +579,6 @@ 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 TestBridge_AddressOrderChangeDuringSyncInCombinedModeDoesNotTriggerBadEventOnNewMessage(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
// Create a user.
userID, addrID, err := s.CreateUser("user", password)
require.NoError(t, err)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
userInfoChanged, done := chToType[events.Event, events.UserChanged](bridge.GetEvents(events.UserChanged{}))
defer done()
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 300)
})
_, err := bridge.LoginFull(ctx, "user", password, nil, nil)
require.NoError(t, err)
info, err := bridge.GetUserInfo(userID)
require.NoError(t, err)
require.Equal(t, 1, len(info.Addresses))
require.Equal(t, info.Addresses[0], "user@proton.local")
addrID2, err := s.CreateAddress(userID, "foo@"+s.GetDomain(), password)
require.NoError(t, err)
require.NoError(t, s.SetAddressOrder(userID, []string{addrID2, addrID}))
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
createNumMessages(ctx, t, c, addrID2, proton.InboxLabel, 1)
})
// Since we can't intercept events at this time, we sleep for a bit to make sure the
// new message does not get combined into the event below. This ensures the newly created
// goes through the full code flow which triggered the original bad event.
time.Sleep(time.Second)
require.NoError(t, s.SetAddressOrder(userID, []string{addrID, addrID2}))
for i := 0; i < 2; i++ {
select {
case <-ctx.Done():
return
case e := <-userInfoChanged:
require.Equal(t, userID, e.UserID)
}
}
})
})
}
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()),

View File

@ -53,5 +53,4 @@ 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
}

View File

@ -139,9 +139,3 @@ 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")
}
}

View File

@ -46,8 +46,6 @@ const (
Connected
)
var ErrFailedToUnlock = errors.New("failed to unlock user keys")
type UserInfo struct {
// UserID is the user's API ID.
UserID string
@ -68,10 +66,10 @@ type UserInfo struct {
BridgePass []byte
// UsedSpace is the amount of space used by the user.
UsedSpace uint64
UsedSpace int
// MaxSpace is the total amount of space available to the user.
MaxSpace uint64
MaxSpace int
}
// GetUserIDs returns the IDs of all known users (authorized or not).
@ -159,15 +157,11 @@ 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)
}
@ -223,16 +217,7 @@ func (bridge *Bridge) LoginFull(
keyPass = password
}
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
return bridge.LoginUser(ctx, client, auth, keyPass)
}
// LogoutUser logs out the given user.
@ -329,7 +314,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.RLockRet(func() error {
return safe.LockRet(func() error {
ctx := context.Background()
user, ok := bridge.users[userID]
@ -389,9 +374,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("%w: %w", ErrFailedToUnlock, err)
return "", fmt.Errorf("failed to unlock user keys: %w", err)
} else if userKR.CountDecryptionEntities() == 0 {
return "", ErrFailedToUnlock
return "", fmt.Errorf("failed to unlock user keys")
}
if err := bridge.addUser(ctx, client, apiUser, authUID, authRef, saltedKeyPass, true); err != nil {
@ -494,7 +479,7 @@ func (bridge *Bridge) addUser(
return fmt.Errorf("failed to add vault user: %w", err)
}
if err := bridge.addUserWithVault(ctx, client, apiUser, vaultUser, isNew); err != nil {
if err := bridge.addUserWithVault(ctx, client, apiUser, vaultUser); err != nil {
if _, ok := err.(*resty.ResponseError); ok || isLogin {
logrus.WithError(err).Error("Failed to add user, clearing its secrets from vault")
@ -529,7 +514,6 @@ func (bridge *Bridge) addUserWithVault(
client *proton.Client,
apiUser proton.User,
vault *vault.User,
isNew bool,
) error {
statsPath, err := bridge.locator.ProvideStatsPath()
if err != nil {
@ -557,7 +541,6 @@ func (bridge *Bridge) addUserWithVault(
&bridgeEventSubscription{b: bridge},
bridge.syncService,
syncSettingsPath,
isNew,
)
if err != nil {
return fmt.Errorf("failed to create user: %w", err)
@ -594,7 +577,7 @@ func (bridge *Bridge) addUserWithVault(
}, bridge.usersLock)
// As we need at least one user to send heartbeat, try to send it.
bridge.heartbeat.start()
defer bridge.goHeartbeat()
return nil
}

View File

@ -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.RLock(func() {
safe.Lock(func() {
if rerr := bridge.reporter.ReportMessageWithContext("Failed to handle event", reporter.Context{
"user_id": user.ID(),
"old_event_id": event.OldEventID,

View File

@ -23,200 +23,71 @@ 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).
//****************************************************************************************************************************************************
/// \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 installTrustedCert(char const *bytes, unsigned long long length) {
if (length == 0) {
return errSecInvalidData;
}
NSData *der = [NSData dataWithBytes:bytes length:length];
//****************************************************************************************************************************************************
/// \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);
}
//****************************************************************************************************************************************************
/// \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));
}
//****************************************************************************************************************************************************
/// \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) {
// 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,
};
return SecItemAdd((__bridge CFDictionaryRef) addQuery, NULL);
OSStatus status = SecItemAdd((__bridge CFDictionaryRef) addQuery, NULL);
if ((errSecSuccess != status) && (errSecDuplicateItem != status)) {
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) {
// 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:trustLevel],
(id)kSecTrustSettingsResult: [NSNumber numberWithInt:kSecTrustSettingsResultTrustRoot],
(id)kSecTrustSettingsPolicy: (__bridge id) policy,
};
status = SecTrustSettingsSetTrustSettings(cert, kSecTrustSettingsDomainUser, (__bridge CFTypeRef)(trustSettings));
CFRelease(policy);
CFRelease(cert);
return status;
}
int removeTrustedCert(char const *bytes, unsigned long long length) {
if (0 == length) {
return errSecInvalidData;
}
NSData *der = [NSData dataWithBytes: bytes length: length];
SecCertificateRef cert = SecCertificateCreateWithData(NULL, (CFDataRef) der);
// 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;
}
//****************************************************************************************************************************************************
/// \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);
}
// 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 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));
CFRelease(cert);
return status;
}
*/
import "C"
@ -248,120 +119,6 @@ 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 {
@ -370,24 +127,18 @@ 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))
if !isCertInKeychainCGo(buffer, size) {
if err := addCertToKeychainCGo(buffer, size); err != nil {
return err
}
}
if !isCertTrustedCGo(buffer, size) {
return setCertTrustedCGo(buffer, size)
}
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)
}
}
// 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 {
@ -396,32 +147,10 @@ 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 isCertTrustedCGo(buffer, size) {
if err := removeCertTrustCGo(buffer, size); err != nil {
return err
}
}
if isCertInKeychainCGo(buffer, size) {
return removeCertFromKeychainCGo(buffer, size)
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)
}
return nil
}
func isCertInstalled(certPEM []byte) bool {
certDER, err := certPEMToDER(certPEM)
if err != nil {
return false
}
p := C.CBytes(certDER)
defer C.free(unsafe.Pointer(p)) //nolint:unconvert
buffer := (*C.char)(p)
size := C.ulonglong(len(certDER))
return isCertInKeychainCGo(buffer, size) && isCertTrustedCGo(buffer, size)
}

View File

@ -25,74 +25,20 @@ import (
"github.com/stretchr/testify/require"
)
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 {
// This test implies human interactions to enter password and is disabled by default.
func _TestTrustedCertsDarwin(t *testing.T) { //nolint:unused
template, err := NewTLSTemplate()
require.NoError(t, err)
certPEM, _, err := GenerateCert(template)
require.NoError(t, err)
return certPEM
require.Error(t, installCert([]byte{0})) // Cannot install an invalid cert.
require.Error(t, uninstallCert(certPEM)) // Cannot uninstall a cert that is not installed.
require.NoError(t, installCert(certPEM)) // Can install a valid cert.
require.NoError(t, installCert(certPEM)) // Can install an already installed cert.
require.NoError(t, uninstallCert(certPEM)) // Can uninstall an installed cert.
require.Error(t, uninstallCert(certPEM)) // Cannot uninstall an already uninstalled cert.
require.NoError(t, installCert(certPEM)) // Can reinstall an uninstalled cert.
require.NoError(t, uninstallCert(certPEM)) // Can uninstall a reinstalled cert.
}

View File

@ -17,10 +17,6 @@
package certs
func osSupportCertInstall() bool {
return false
}
func installCert([]byte) error {
return nil // Linux doesn't have a root cert store.
}
@ -28,7 +24,3 @@ func installCert([]byte) error {
func uninstallCert([]byte) error {
return nil // Linux doesn't have a root cert store.
}
func isCertInstalled([]byte) bool {
return false
}

View File

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

View File

@ -17,66 +17,16 @@
package certs
import (
"errors"
"github.com/sirupsen/logrus"
)
var (
ErrUserCanceledCertificateInstall = errors.New("the user cancelled the authorization dialog")
)
type Installer struct {
log *logrus.Entry
}
type Installer struct{}
func NewInstaller() *Installer {
return &Installer{
log: logrus.WithField("pkg", "certs"),
}
}
func (installer *Installer) OSSupportCertInstall() bool {
return osSupportCertInstall()
return &Installer{}
}
func (installer *Installer) InstallCert(certPEM []byte) error {
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
return installCert(certPEM)
}
func (installer *Installer) UninstallCert(certPEM []byte) error {
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")
}
}
return uninstallCert(certPEM)
}

View File

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

View File

@ -95,13 +95,6 @@ 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()
@ -135,7 +128,7 @@ func (status *ConfigurationStatus) ApplyProgress() error {
return status.Save()
}
func (status *ConfigurationStatus) RecordLinkClicked(link uint64) error {
func (status *ConfigurationStatus) RecordLinkClicked(link uint) error {
status.DataLock.Lock()
defer status.DataLock.Unlock()
@ -198,11 +191,11 @@ func (data *ConfigurationStatusData) init() {
data.DataV1.FailureDetails = ""
}
func (data *ConfigurationStatusData) setClickedLink(pos uint64) {
func (data *ConfigurationStatusData) setClickedLink(pos uint) {
data.DataV1.ClickedLink |= 1 << pos
}
func (data *ConfigurationStatusData) hasLinkClicked(pos uint64) bool {
func (data *ConfigurationStatusData) hasLinkClicked(pos uint) bool {
val := data.DataV1.ClickedLink & (1 << pos)
return val > 0
}
@ -211,7 +204,7 @@ func (data *ConfigurationStatusData) clickedLinkToString() string {
var str = ""
var first = true
for i := 0; i < 64; i++ {
if data.hasLinkClicked(uint64(i)) {
if data.hasLinkClicked(uint(i)) {
if !first {
str += ","
} else {

View File

@ -19,6 +19,7 @@ package configstatus
import (
"strconv"
"time"
)
type ConfigAbortValues struct {
@ -40,20 +41,17 @@ type ConfigAbortData struct {
type ConfigAbortBuilder struct{}
func (*ConfigAbortBuilder) New(config *ConfigurationStatus) ConfigAbortData {
config.DataLock.RLock()
defer config.DataLock.RUnlock()
func (*ConfigAbortBuilder) New(data *ConfigurationStatusData) ConfigAbortData {
return ConfigAbortData{
MeasurementGroup: "bridge.any.configuration",
Event: "bridge_config_abort",
Values: ConfigSuccessValues{
Duration: config.isPendingSinceMin(),
Duration: int(time.Since(data.DataV1.PendingSince).Minutes()),
},
Dimensions: ConfigSuccessDimensions{
ReportClick: strconv.FormatBool(config.Data.DataV1.ReportClick),
ReportSent: strconv.FormatBool(config.Data.DataV1.ReportSent),
ClickedLink: config.Data.clickedLinkToString(),
ReportClick: strconv.FormatBool(data.DataV1.ReportClick),
ReportSent: strconv.FormatBool(data.DataV1.ReportSent),
ClickedLink: data.clickedLinkToString(),
},
}
}

View File

@ -33,7 +33,7 @@ func TestConfigurationAbort_default(t *testing.T) {
require.NoError(t, err)
var builder = configstatus.ConfigAbortBuilder{}
req := builder.New(config)
req := builder.New(config.Data)
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_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)
req := builder.New(config.Data)
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_abort", req.Event)

View File

@ -33,16 +33,13 @@ type ConfigProgressData struct {
type ConfigProgressBuilder struct{}
func (*ConfigProgressBuilder) New(config *ConfigurationStatus) ConfigProgressData {
config.DataLock.RLock()
defer config.DataLock.RUnlock()
func (*ConfigProgressBuilder) New(data *ConfigurationStatusData) ConfigProgressData {
return ConfigProgressData{
MeasurementGroup: "bridge.any.configuration",
Event: "bridge_config_progress",
Values: ConfigProgressValues{
NbDay: numberOfDay(time.Now(), config.Data.DataV1.PendingSince),
NbDaySinceLast: numberOfDay(time.Now(), config.Data.DataV1.LastProgress),
NbDay: numberOfDay(time.Now(), data.DataV1.PendingSince),
NbDaySinceLast: numberOfDay(time.Now(), data.DataV1.LastProgress),
},
}
}

View File

@ -33,7 +33,7 @@ func TestConfigurationProgress_default(t *testing.T) {
require.NoError(t, err)
var builder = configstatus.ConfigProgressBuilder{}
req := builder.New(config)
req := builder.New(config.Data)
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_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)
req := builder.New(config.Data)
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_progress", req.Event)

View File

@ -19,6 +19,7 @@ package configstatus
import (
"strconv"
"time"
)
type ConfigRecoveryValues struct {
@ -42,22 +43,19 @@ type ConfigRecoveryData struct {
type ConfigRecoveryBuilder struct{}
func (*ConfigRecoveryBuilder) New(config *ConfigurationStatus) ConfigRecoveryData {
config.DataLock.RLock()
defer config.DataLock.RUnlock()
func (*ConfigRecoveryBuilder) New(data *ConfigurationStatusData) ConfigRecoveryData {
return ConfigRecoveryData{
MeasurementGroup: "bridge.any.configuration",
Event: "bridge_config_recovery",
Values: ConfigRecoveryValues{
Duration: config.isPendingSinceMin(),
Duration: int(time.Since(data.DataV1.PendingSince).Minutes()),
},
Dimensions: ConfigRecoveryDimensions{
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,
Autoconf: data.DataV1.Autoconf,
ReportClick: strconv.FormatBool(data.DataV1.ReportClick),
ReportSent: strconv.FormatBool(data.DataV1.ReportSent),
ClickedLink: data.clickedLinkToString(),
FailureDetails: data.DataV1.FailureDetails,
},
}
}

View File

@ -33,7 +33,7 @@ func TestConfigurationRecovery_default(t *testing.T) {
require.NoError(t, err)
var builder = configstatus.ConfigRecoveryBuilder{}
req := builder.New(config)
req := builder.New(config.Data)
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_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)
req := builder.New(config.Data)
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_recovery", req.Event)

View File

@ -19,6 +19,7 @@ package configstatus
import (
"strconv"
"time"
)
type ConfigSuccessValues struct {
@ -41,21 +42,18 @@ type ConfigSuccessData struct {
type ConfigSuccessBuilder struct{}
func (*ConfigSuccessBuilder) New(config *ConfigurationStatus) ConfigSuccessData {
config.DataLock.RLock()
defer config.DataLock.RUnlock()
func (*ConfigSuccessBuilder) New(data *ConfigurationStatusData) ConfigSuccessData {
return ConfigSuccessData{
MeasurementGroup: "bridge.any.configuration",
Event: "bridge_config_success",
Values: ConfigSuccessValues{
Duration: config.isPendingSinceMin(),
Duration: int(time.Since(data.DataV1.PendingSince).Minutes()),
},
Dimensions: ConfigSuccessDimensions{
Autoconf: config.Data.DataV1.Autoconf,
ReportClick: strconv.FormatBool(config.Data.DataV1.ReportClick),
ReportSent: strconv.FormatBool(config.Data.DataV1.ReportSent),
ClickedLink: config.Data.clickedLinkToString(),
Autoconf: data.DataV1.Autoconf,
ReportClick: strconv.FormatBool(data.DataV1.ReportClick),
ReportSent: strconv.FormatBool(data.DataV1.ReportSent),
ClickedLink: data.clickedLinkToString(),
},
}
}

View File

@ -33,7 +33,7 @@ func TestConfigurationSuccess_default(t *testing.T) {
require.NoError(t, err)
var builder = configstatus.ConfigSuccessBuilder{}
req := builder.New(config)
req := builder.New(config.Data)
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_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)
req := builder.New(config.Data)
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_success", req.Event)

View File

@ -175,7 +175,7 @@ type UsedSpaceChanged struct {
UserID string
UsedSpace uint64
UsedSpace int
}
func (event UsedSpaceChanged) String() string {

View File

@ -17,7 +17,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.31.0
// protoc-gen-go v1.28.0
// protoc v3.21.12
// source: focus.proto

View File

@ -1,23 +1,6 @@
// Copyright (c) 2022 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail 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.
//
// ProtonMail 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 ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.3.0
// - protoc-gen-go-grpc v1.2.0
// - protoc v3.21.12
// source: focus.proto
@ -37,11 +20,6 @@ import (
// Requires gRPC-Go v1.32.0 or later.
const _ = grpc.SupportPackageIsVersion7
const (
Focus_Raise_FullMethodName = "/focus.Focus/Raise"
Focus_Version_FullMethodName = "/focus.Focus/Version"
)
// FocusClient is the client API for Focus service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
@ -60,7 +38,7 @@ func NewFocusClient(cc grpc.ClientConnInterface) FocusClient {
func (c *focusClient) Raise(ctx context.Context, in *wrapperspb.StringValue, opts ...grpc.CallOption) (*emptypb.Empty, error) {
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, Focus_Raise_FullMethodName, in, out, opts...)
err := c.cc.Invoke(ctx, "/focus.Focus/Raise", in, out, opts...)
if err != nil {
return nil, err
}
@ -69,7 +47,7 @@ func (c *focusClient) Raise(ctx context.Context, in *wrapperspb.StringValue, opt
func (c *focusClient) Version(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*VersionResponse, error) {
out := new(VersionResponse)
err := c.cc.Invoke(ctx, Focus_Version_FullMethodName, in, out, opts...)
err := c.cc.Invoke(ctx, "/focus.Focus/Version", in, out, opts...)
if err != nil {
return nil, err
}
@ -118,7 +96,7 @@ func _Focus_Raise_Handler(srv interface{}, ctx context.Context, dec func(interfa
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Focus_Raise_FullMethodName,
FullMethod: "/focus.Focus/Raise",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(FocusServer).Raise(ctx, req.(*wrapperspb.StringValue))
@ -136,7 +114,7 @@ func _Focus_Version_Handler(srv interface{}, ctx context.Context, dec func(inter
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Focus_Version_FullMethodName,
FullMethod: "/focus.Focus/Version",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(FocusServer).Version(ctx, req.(*emptypb.Empty))

View File

@ -18,6 +18,7 @@
#include "AppController.h"
#include "GRPCService.h"
#include <bridgepp/GRPC/GRPCUtils.h>
#include <bridgepp/Exception/Exception.h>
#include "MainWindow.h"
#include <bridgepp/Log/Log.h>
@ -67,7 +68,7 @@ void AppController::setMainWindow(MainWindow *mainWindow) {
//****************************************************************************************************************************************************
/// \return The main window.
//****************************************************************************************************************************************************
MainWindow &AppController::mainWindow() const {
MainWindow &AppController::mainWindow() {
if (!mainWindow_) {
throw Exception("mainWindow has not yet been registered.");
}
@ -78,7 +79,7 @@ MainWindow &AppController::mainWindow() const {
//****************************************************************************************************************************************************
/// \return A reference to the log.
//****************************************************************************************************************************************************
bridgepp::Log &AppController::log() const {
bridgepp::Log &AppController::log() {
return *log_;
}
@ -86,7 +87,7 @@ bridgepp::Log &AppController::log() const {
//****************************************************************************************************************************************************
/// \return A reference to the bridge-gui log.
//****************************************************************************************************************************************************
bridgepp::Log &AppController::bridgeGUILog() const {
bridgepp::Log &AppController::bridgeGUILog() {
return *bridgeGUILog_;
}
@ -94,6 +95,6 @@ bridgepp::Log &AppController::bridgeGUILog() const {
//****************************************************************************************************************************************************
/// \return A reference to the gRPC service.
//****************************************************************************************************************************************************
GRPCService &AppController::grpc() const {
GRPCService &AppController::grpc() {
return *grpc_;
}

View File

@ -42,10 +42,10 @@ public: // member functions.
AppController &operator=(AppController const &) = delete; ///< Disabled assignment operator.
AppController &operator=(AppController &&) = delete; ///< Disabled move assignment operator.
void setMainWindow(MainWindow *mainWindow); ///< Set the main window.
MainWindow &mainWindow() const; ///< Return the main window.
bridgepp::Log &log() const; ///< Return a reference to the log.
bridgepp::Log &bridgeGUILog() const; ///< Return a reference to the bridge-gui log.
GRPCService &grpc() const; ///< Return a reference to the gRPC service.
MainWindow &mainWindow(); ///< Return the main window.
bridgepp::Log &log(); ///< Return a reference to the log.
bridgepp::Log &bridgeGUILog(); ///< Return a reference to the bridge-gui log.
GRPCService &grpc(); ///< Return a reference to the gRPC service.
private: // member functions.
AppController(); ///< Default constructor.

View File

@ -66,8 +66,6 @@ add_executable(bridge-gui-tester
GRPCQtProxy.cpp GRPCQtProxy.h
GRPCService.cpp GRPCService.h
GRPCServerWorker.cpp GRPCServerWorker.h
Tabs/EventsTab.cpp Tabs/EventsTab.h
Tabs/KnowledgeBaseTab.cpp Tabs/KnowledgeBaseTab.h
Tabs/SettingsTab.cpp Tabs/SettingsTab.h
Tabs/UsersTab.cpp Tabs/UsersTab.h
UserDialog.cpp UserDialog.h

View File

@ -26,7 +26,7 @@ using namespace grpc;
//****************************************************************************************************************************************************
/// \param[in] serverToken The server token expected from gRPC calls
/// \param[in] The server token expected from gRPC calls
//****************************************************************************************************************************************************
GRPCMetadataProcessor::GRPCMetadataProcessor(QString const &serverToken)
: serverToken_(serverToken.toStdString()) {
@ -43,16 +43,16 @@ bool GRPCMetadataProcessor::IsBlocking() const {
//****************************************************************************************************************************************************
/// \param authMetadata The authentication metadata.
/// \param[in] inputMeta
/// \return the result of the metadata processing.
//****************************************************************************************************************************************************
Status GRPCMetadataProcessor::Process(InputMetadata const &authMetadata, AuthContext *,
OutputMetadata *, OutputMetadata *) {
Status GRPCMetadataProcessor::Process(AuthMetadataProcessor::InputMetadata const &auth_metadata, AuthContext *,
AuthMetadataProcessor::OutputMetadata *, AuthMetadataProcessor::OutputMetadata *) {
try {
const InputMetadata::const_iterator pathIt = authMetadata.find(":path");
QString const callName = (pathIt == authMetadata.end()) ? ("unkown gRPC call") : QString::fromLocal8Bit(pathIt->second);
AuthMetadataProcessor::InputMetadata::const_iterator pathIt = auth_metadata.find(":path");
QString const callName = (pathIt == auth_metadata.end()) ? ("unkown gRPC call") : QString::fromLocal8Bit(pathIt->second);
AuthMetadataProcessor::InputMetadata::size_type const count = authMetadata.count(grpcMetadataServerTokenKey);
AuthMetadataProcessor::InputMetadata::size_type const count = auth_metadata.count(grpcMetadataServerTokenKey);
if (count == 0) {
throw Exception(QString("Missing server token in gRPC client call '%1'.").arg(callName));
}
@ -61,7 +61,7 @@ Status GRPCMetadataProcessor::Process(InputMetadata const &authMetadata, AuthCon
throw Exception(QString("Several server tokens were provided in gRPC client call '%1'.").arg(callName));
}
if (authMetadata.find(grpcMetadataServerTokenKey)->second != serverToken_) {
if (auth_metadata.find(grpcMetadataServerTokenKey)->second != serverToken_) {
throw Exception(QString("Invalid server token provided by gRPC client call '%1'.").arg(callName));
}
@ -70,7 +70,7 @@ Status GRPCMetadataProcessor::Process(InputMetadata const &authMetadata, AuthCon
}
catch (Exception const &e) {
app().log().error(e.qwhat());
return Status(UNAUTHENTICATED, e.qwhat().toStdString());
return Status(StatusCode::UNAUTHENTICATED, e.qwhat().toStdString());
}
}

View File

@ -35,7 +35,7 @@ public: // member functions.
GRPCMetadataProcessor &operator=(GRPCMetadataProcessor const &) = delete; ///< Disabled assignment operator.
GRPCMetadataProcessor &operator=(GRPCMetadataProcessor &&) = delete; ///< Disabled move assignment operator.
bool IsBlocking() const override; ///< Is the processor blocking?
grpc::Status Process(InputMetadata const &authMetadata, grpc::AuthContext *context, OutputMetadata *consumed_auth_metadata,
grpc::Status Process(InputMetadata const &auth_metadata, grpc::AuthContext *context, OutputMetadata *consumed_auth_metadata,
OutputMetadata *response_metadata) override; ///< Process the metadata
private:

View File

@ -31,11 +31,10 @@ GRPCQtProxy::GRPCQtProxy()
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void GRPCQtProxy::connectSignals() const {
void GRPCQtProxy::connectSignals() {
MainWindow &mainWindow = app().mainWindow();
SettingsTab const &settingsTab = mainWindow.settingsTab();
UsersTab const &usersTab = mainWindow.usersTab();
KnowledgeBaseTab const &kbTab = mainWindow.knowledgeBaseTab();
SettingsTab &settingsTab = mainWindow.settingsTab();
UsersTab &usersTab = mainWindow.usersTab();
connect(this, &GRPCQtProxy::delayedEventRequested, &mainWindow, &MainWindow::sendDelayedEvent);
connect(this, &GRPCQtProxy::setIsAutostartOnReceived, &settingsTab, &SettingsTab::setIsAutostartOn);
connect(this, &GRPCQtProxy::setIsBetaEnabledReceived, &settingsTab, &SettingsTab::setIsBetaEnabled);
@ -43,7 +42,6 @@ void GRPCQtProxy::connectSignals() const {
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);
@ -57,7 +55,6 @@ void GRPCQtProxy::connectSignals() const {
connect(this, &GRPCQtProxy::setUserSplitModeReceived, &usersTab, &UsersTab::setUserSplitMode);
connect(this, &GRPCQtProxy::configureUserAppleMailReceived, &usersTab, &UsersTab::configureUserAppleMail);
connect(this, &GRPCQtProxy::sendBadEventUserFeedbackReceived, &usersTab, &UsersTab::processBadEventUserFeedback);
connect(this, &GRPCQtProxy::requestKnowledgeBaseSuggestionsReceived, &kbTab, &KnowledgeBaseTab::requestKnowledgeBaseSuggestions);
}
@ -121,20 +118,6 @@ void GRPCQtProxy::reportBug(QString const &osType, QString const &osVersion, QSt
emit reportBugReceived(osType, osVersion, emailClient, address, description, includeLogs);
}
//****************************************************************************************************************************************************
/// \param[in] userInput The user input.
//****************************************************************************************************************************************************
void GRPCQtProxy::requestKnowledgeBaseSuggestions(QString const& userInput) {
emit requestKnowledgeBaseSuggestionsReceived(userInput);
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void GRPCQtProxy::installTLSCertificate() {
emit installTLSCertificateReceived();
}
//****************************************************************************************************************************************************
/// \param[in] folderPath The folder path.
@ -166,8 +149,8 @@ void GRPCQtProxy::setClientPlatform(QString const &clientPlatform) {
/// \param[in] useSSLForIMAP The IMAP connexion mode.
/// \param[in] useSSLForSMTP The IMAP connexion mode.
//****************************************************************************************************************************************************
void GRPCQtProxy::setMailServerSettings(qint32 imapPort, qint32 smtpPort, bool useSSLForIMAP, bool useSSLForSMTP) {
emit setMailServerSettingsReceived(imapPort, smtpPort, useSSLForIMAP, useSSLForSMTP);
void GRPCQtProxy::setMailServerSettings(qint32 imapPort, qint32 smtpPort, bool useSSLForIMAP, bool userSSLForSMTP) {
emit setMailServerSettingsReceived(imapPort, smtpPort, useSSLForIMAP, userSSLForSMTP);
}

View File

@ -36,7 +36,7 @@ public: // member functions.
GRPCQtProxy &operator=(GRPCQtProxy const &) = delete; ///< Disabled assignment operator.
GRPCQtProxy &operator=(GRPCQtProxy &&) = delete; ///< Disabled move assignment operator.
void connectSignals() const; // connect the signals to the main window.
void connectSignals(); // connect the signals to the main window.
void sendDelayedEvent(bridgepp::SPStreamEvent const &event); ///< Sends a delayed stream event.
void setIsAutostartOn(bool on); ///< Forwards a SetIsAutostartOn call via a Qt signal.
void setIsBetaEnabled(bool enabled); ///< Forwards a SetIsBetaEnabled call via a Qt signal.
@ -45,12 +45,10 @@ 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 requestKnowledgeBaseSuggestions(QString const &userInput); ///< Forwards a RequestKnowledgeBaseSuggestions 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.
void setMailServerSettings(qint32 imapPort, qint32 smtpPort, bool useSSLForIMAP, bool useSSLForSMTP); ///< Forwards a setMailServerSettings' call via a Qt signal.
void setMailServerSettings(qint32 imapPort, qint32 smtpPort, bool useSSLForIMAP, bool userSSLForSMTP); ///< Forwards a setMailServerSettings' call via a Qt signal.
void setIsDoHEnabled(bool enabled); ///< Forwards a setIsDoHEnabled call via a Qt signal.
void setDiskCachePath(QString const &path); ///< Forwards a setDiskCachePath call via a Qt signal.
void setIsAutomaticUpdateOn(bool on); ///< Forwards a SetIsAutomaticUpdateOn call via a Qt signal.
@ -69,8 +67,6 @@ 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 requestKnowledgeBaseSuggestionsReceived(QString const &userInput); ///< Signal for the RequestKnowledgeBaseSuggestions 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.

View File

@ -50,7 +50,7 @@ void GRPCServerWorker::run() {
SslServerCredentialsOptions ssl_opts;
ssl_opts.pem_root_certs = "";
ssl_opts.pem_key_cert_pairs.push_back(pair);
std::shared_ptr<ServerCredentials> const credentials = SslServerCredentials(ssl_opts);
std::shared_ptr<ServerCredentials> credentials = grpc::SslServerCredentials(ssl_opts);
GRPCConfig config;
config.cert = testTLSCert;
@ -59,7 +59,8 @@ void GRPCServerWorker::run() {
credentials->SetAuthMetadataProcessor(processor_); // gRPC interceptors are still experimental in C++, so we use AuthMetadataProcessor
ServerBuilder builder;
int port = 0; // Port will not be known until ServerBuilder::BuildAndStart() is called
if (useFileSocketForGRPC()) {
bool const useFileSocket = useFileSocketForGRPC();
if (useFileSocket) {
QString const fileSocketPath = getAvailableFileSocketPath();
if (fileSocketPath.isEmpty()) {
throw Exception("Could not get an available file socket.");
@ -101,7 +102,7 @@ void GRPCServerWorker::run() {
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void GRPCServerWorker::stop() const {
void GRPCServerWorker::stop() {
if (server_) {
server_->Shutdown();
}

View File

@ -40,7 +40,7 @@ public: // member functions.
GRPCServerWorker &operator=(GRPCServerWorker &&) = delete; ///< Disabled move assignment operator.
void run() override; ///< Run the worker.
void stop() const; ///< Stop the gRPC service.
void stop(); ///< Stop the gRPC service.
private: // data members
std::unique_ptr<grpc::Server> server_ { nullptr }; ///< The gRPC server.

View File

@ -39,7 +39,7 @@ QString const defaultKeychain = "defaultKeychain"; ///< The default keychain.
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void GRPCService::connectProxySignals() const {
void GRPCService::connectProxySignals() {
qtProxy_.connectSignals();
}
@ -185,7 +185,7 @@ Status GRPCService::SetIsAllMailVisible(ServerContext *, BoolValue const *reques
/// \param[out] response The response.
/// \return The status for the call.
//****************************************************************************************************************************************************
Status GRPCService::IsAllMailVisible(ServerContext *, Empty const *, BoolValue *response) {
Status GRPCService::IsAllMailVisible(ServerContext *, Empty const *request, BoolValue *response) {
app().log().debug(__FUNCTION__);
response->set_value(app().mainWindow().settingsTab().isAllMailVisible());
return Status::OK;
@ -214,16 +214,6 @@ grpc::Status GRPCService::IsTelemetryDisabled(::grpc::ServerContext *, ::google:
}
//****************************************************************************************************************************************************
/// \param[out] response The response.
/// \return The status for the call.
//****************************************************************************************************************************************************
Status GRPCService::GoOs(ServerContext *, Empty const*, StringValue *response) {
response->set_value(app().mainWindow().settingsTab().os().toStdString());
return Status::OK;
}
//****************************************************************************************************************************************************
/// \return The status for the call.
//****************************************************************************************************************************************************
@ -354,23 +344,19 @@ Status GRPCService::SetMainExecutable(ServerContext *, StringValue const *reques
return Status::OK;
}
//****************************************************************************************************************************************************
/// \param[in] request The request.
/// \return The status for the call.
//****************************************************************************************************************************************************
grpc::Status GRPCService::RequestKnowledgeBaseSuggestions(ServerContext*, StringValue const* request, Empty*) {
QString const userInput = QString::fromUtf8(request->value());
app().log().info(QString("RequestKnowledgeBaseSuggestions: %1").arg(userInput.left(10) + "..."));
qtProxy_.requestKnowledgeBaseSuggestionsReceived(userInput);
QList<bridgepp::KnowledgeBaseSuggestion> suggestions;
for (qsizetype i = 1; i <= 3; ++i) {
suggestions.push_back( {
.title = QString("Suggested link %1").arg(i),
.url = QString("https://proton.me/support/bridge#%1").arg(i),
});
}
qtProxy_.sendDelayedEvent(newKnowledgeBaseSuggestionsEvent(app().mainWindow().knowledgeBaseTab().getSuggestions()));
//****************************************************************************************************************************************************
/// \param[in] request The request
//****************************************************************************************************************************************************
Status GRPCService::ReportBug(ServerContext *, ReportBugRequest const *request, Empty *) {
app().log().debug(__FUNCTION__);
SettingsTab &tab = app().mainWindow().settingsTab();
qtProxy_.reportBug(QString::fromStdString(request->ostype()), QString::fromStdString(request->osversion()),
QString::fromStdString(request->emailclient()), QString::fromStdString(request->address()), QString::fromStdString(request->description()),
request->includelogs());
qtProxy_.sendDelayedEvent(tab.nextBugReportWillSucceed() ? newReportBugSuccessEvent() : newReportBugErrorEvent());
qtProxy_.sendDelayedEvent(newReportBugFinishedEvent());
return Status::OK;
}
@ -378,27 +364,15 @@ grpc::Status GRPCService::RequestKnowledgeBaseSuggestions(ServerContext*, String
//****************************************************************************************************************************************************
/// \param[in] request The request
//****************************************************************************************************************************************************
Status GRPCService::ReportBug(ServerContext *, ReportBugRequest const *request, Empty *) {
app().log().debug(__FUNCTION__);
EventsTab const&eventsTab = app().mainWindow().eventsTab();
qtProxy_.reportBug(QString::fromStdString(request->ostype()), QString::fromStdString(request->osversion()),
QString::fromStdString(request->emailclient()), QString::fromStdString(request->address()), QString::fromStdString(request->description()),
request->includelogs());
SPStreamEvent event;
switch (eventsTab.nextBugReportResult()) {
case EventsTab::BugReportResult::Success:
event = newReportBugSuccessEvent();
break;
case EventsTab::BugReportResult::Error:
event = newReportBugErrorEvent();
break;
case EventsTab::BugReportResult::DataSharingError:
event = newReportBugFallbackEvent();
break;
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));
}
qtProxy_.sendDelayedEvent(event);
qtProxy_.sendDelayedEvent(newReportBugFinishedEvent());
if (!tab.nextTLSKeyExportWillSucceed()) {
qtProxy_.sendDelayedEvent(newGenericErrorEvent(grpc::TLS_KEY_EXPORT_ERROR));
}
qtProxy_.exportTLSCertificates(QString::fromStdString(request->value()));
return Status::OK;
}
@ -432,7 +406,7 @@ Status GRPCService::Login(ServerContext *, LoginRequest const *request, Empty *)
return Status::OK;
}
if (usersTab.nextUserTwoPasswordsRequired()) {
qtProxy_.sendDelayedEvent(newLoginTwoPasswordsRequestedEvent(loginUsername_));
qtProxy_.sendDelayedEvent(newLoginTwoPasswordsRequestedEvent());
return Status::OK;
}
@ -447,7 +421,7 @@ Status GRPCService::Login(ServerContext *, LoginRequest const *request, Empty *)
//****************************************************************************************************************************************************
Status GRPCService::Login2FA(ServerContext *, LoginRequest const *request, Empty *) {
app().log().debug(__FUNCTION__);
UsersTab const &usersTab = app().mainWindow().usersTab();
UsersTab &usersTab = app().mainWindow().usersTab();
if (usersTab.nextUserTFAError()) {
qtProxy_.sendDelayedEvent(newLoginError(LoginErrorType::TFA_ERROR, "2FA Error."));
return Status::OK;
@ -457,7 +431,7 @@ Status GRPCService::Login2FA(ServerContext *, LoginRequest const *request, Empty
return Status::OK;
}
if (usersTab.nextUserTwoPasswordsRequired()) {
qtProxy_.sendDelayedEvent(newLoginTwoPasswordsRequestedEvent(loginUsername_));
qtProxy_.sendDelayedEvent(newLoginTwoPasswordsRequestedEvent());
return Status::OK;
}
@ -472,7 +446,7 @@ Status GRPCService::Login2FA(ServerContext *, LoginRequest const *request, Empty
//****************************************************************************************************************************************************
Status GRPCService::Login2Passwords(ServerContext *, LoginRequest const *request, Empty *) {
app().log().debug(__FUNCTION__);
UsersTab const &usersTab = app().mainWindow().usersTab();
UsersTab &usersTab = app().mainWindow().usersTab();
if (usersTab.nextUserTwoPasswordsError()) {
qtProxy_.sendDelayedEvent(newLoginError(LoginErrorType::TWO_PASSWORDS_ERROR, "Two Passwords error."));
@ -562,12 +536,12 @@ Status GRPCService::DiskCachePath(ServerContext *, Empty const *, StringValue *r
Status GRPCService::SetDiskCachePath(ServerContext *, StringValue const *path, Empty *) {
app().log().debug(__FUNCTION__);
EventsTab const &eventsTab = app().mainWindow().eventsTab();
SettingsTab &tab = app().mainWindow().settingsTab();
QString const qPath = QString::fromStdString(path->value());
// we mimic the behaviour of Bridge
if (!eventsTab.nextCacheChangeWillSucceed()) {
qtProxy_.sendDelayedEvent(newDiskCacheErrorEvent(static_cast<DiskCacheErrorType>(CANT_MOVE_DISK_CACHE_ERROR)));
if (!tab.nextCacheChangeWillSucceed()) {
qtProxy_.sendDelayedEvent(newDiskCacheErrorEvent(grpc::DiskCacheErrorType(tab.cacheError())));
} else {
qtProxy_.setDiskCachePath(qPath);
qtProxy_.sendDelayedEvent(newDiskCachePathChangedEvent(qPath));
@ -604,7 +578,7 @@ Status GRPCService::IsDoHEnabled(ServerContext *, Empty const *, BoolValue *resp
/// \param[in] settings The IMAP/SMTP settings.
/// \return The status for the call.
//****************************************************************************************************************************************************
Status GRPCService::SetMailServerSettings(ServerContext *, ImapSmtpSettings const *settings, Empty *) {
Status GRPCService::SetMailServerSettings(::grpc::ServerContext *context, ImapSmtpSettings const *settings, Empty *) {
app().log().debug(__FUNCTION__);
qtProxy_.setMailServerSettings(settings->imapport(), settings->smtpport(), settings->usesslforimap(), settings->usesslforsmtp());
qtProxy_.sendDelayedEvent(newMailServerSettingsChanged(*settings));
@ -617,9 +591,9 @@ Status GRPCService::SetMailServerSettings(ServerContext *, ImapSmtpSettings cons
/// \param[out] outSettings The settings
/// \return The status for the call.
//****************************************************************************************************************************************************
Status GRPCService::MailServerSettings(ServerContext *, Empty const *, ImapSmtpSettings *outSettings) {
Status GRPCService::MailServerSettings(::grpc::ServerContext *, Empty const *, ImapSmtpSettings *outSettings) {
app().log().debug(__FUNCTION__);
SettingsTab const &tab = app().mainWindow().settingsTab();
SettingsTab &tab = app().mainWindow().settingsTab();
outSettings->set_imapport(tab.imapPort());
outSettings->set_smtpport(tab.smtpPort());
outSettings->set_usesslforimap(tab.useSSLForIMAP());
@ -646,7 +620,7 @@ Status GRPCService::Hostname(ServerContext *, Empty const *, StringValue *respon
//****************************************************************************************************************************************************
Status GRPCService::IsPortFree(ServerContext *, Int32Value const *request, BoolValue *response) {
app().log().debug(__FUNCTION__);
response->set_value(app().mainWindow().eventsTab().isPortFree());
response->set_value(app().mainWindow().settingsTab().isPortFree());
return Status::OK;
}
@ -717,8 +691,8 @@ Status GRPCService::GetUserList(ServerContext *, Empty const *, UserListResponse
//****************************************************************************************************************************************************
Status GRPCService::GetUser(ServerContext *, StringValue const *request, grpc::User *response) {
app().log().debug(__FUNCTION__);
QString const userID = QString::fromStdString(request->value());
SPUser const user = app().mainWindow().usersTab().userWithID(userID);
QString userID = QString::fromStdString(request->value());
SPUser user = app().mainWindow().usersTab().userWithID(userID);
if (!user) {
return Status(NOT_FOUND, QString("user not found %1").arg(userID).toStdString());
}
@ -782,87 +756,10 @@ Status GRPCService::ConfigureUserAppleMail(ServerContext *, ConfigureAppleMailRe
}
//****************************************************************************************************************************************************
/// \param[in] request The request
/// \return The status for the call.
//****************************************************************************************************************************************************
Status GRPCService::ExportTLSCertificates(ServerContext *, StringValue const *request, Empty *) {
app().log().debug(__FUNCTION__);
SettingsTab const &tab = app().mainWindow().settingsTab();
if (!tab.nextTLSCertExportWillSucceed()) {
qtProxy_.sendDelayedEvent(newGenericErrorEvent(TLS_CERT_EXPORT_ERROR));
}
if (!tab.nextTLSKeyExportWillSucceed()) {
qtProxy_.sendDelayedEvent(newGenericErrorEvent(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().nextTLSCertInstallResult()) {
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::ExternalLinkClicked(::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
/// \return The status for the call.
//****************************************************************************************************************************************************
Status GRPCService::RunEventStream(ServerContext *ctx, EventStreamRequest const *request, ServerWriter<StreamEvent> *writer) {
app().log().debug(__FUNCTION__);
@ -953,3 +850,4 @@ void GRPCService::finishLogin() {
qtProxy_.sendDelayedEvent(newLoginFinishedEvent(user->id(), alreadyExist));
}

View File

@ -37,7 +37,7 @@ public: // member functions.
~GRPCService() override = default; ///< Destructor.
GRPCService &operator=(GRPCService const &) = delete; ///< Disabled assignment operator.
GRPCService &operator=(GRPCService &&) = delete; ///< Disabled move assignment operator.
void connectProxySignals() const; ///< Connect the signals of the Qt Proxy to the GUI components
void connectProxySignals(); ///< Connect the signals of the Qt Proxy to the GUI components
bool isStreaming() const; ///< Check if the service is currently streaming events.
grpc::Status CheckTokens(::grpc::ServerContext *context, ::google::protobuf::StringValue const *request, ::google::protobuf::StringValue *response) override;
grpc::Status AddLogEntry(::grpc::ServerContext *, ::grpc::AddLogEntryRequest const *request, ::google::protobuf::Empty *) override;
@ -53,7 +53,6 @@ public: // member functions.
grpc::Status IsAllMailVisible(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::google::protobuf::BoolValue *response) override;
grpc::Status 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;
@ -65,9 +64,9 @@ 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 RequestKnowledgeBaseSuggestions(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
grpc::Status Login(::grpc::ServerContext *, ::grpc::LoginRequest const *request, ::google::protobuf::Empty *) override;
grpc::Status Login2FA(::grpc::ServerContext *, ::grpc::LoginRequest const *request, ::google::protobuf::Empty *) override;
grpc::Status Login2Passwords(::grpc::ServerContext *, ::grpc::LoginRequest const *request, ::google::protobuf::Empty *) override;
@ -94,12 +93,6 @@ 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 ExternalLinkClicked(::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.

View File

@ -63,7 +63,7 @@ MainWindow::MainWindow(QWidget *parent)
//****************************************************************************************************************************************************
/// \return A reference to the 'General' tab.
//****************************************************************************************************************************************************
SettingsTab &MainWindow::settingsTab() const {
SettingsTab &MainWindow::settingsTab() {
return *ui_.settingsTab;
}
@ -71,32 +71,16 @@ SettingsTab &MainWindow::settingsTab() const {
//****************************************************************************************************************************************************
/// \return A reference to the users tab.
//****************************************************************************************************************************************************
UsersTab &MainWindow::usersTab() const {
UsersTab &MainWindow::usersTab() {
return *ui_.usersTab;
}
//****************************************************************************************************************************************************
/// \return A reference to the events tab.
//****************************************************************************************************************************************************
EventsTab& MainWindow::eventsTab() const {
return *ui_.eventsTab;
}
//****************************************************************************************************************************************************
/// \return A reference to the knowledge base tab.
//****************************************************************************************************************************************************
KnowledgeBaseTab& MainWindow::knowledgeBaseTab() const {
return *ui_.knowledgeBaseTab;
}
//****************************************************************************************************************************************************
/// \param[in] level The log level.
/// \param[in] message The log message
//****************************************************************************************************************************************************
void MainWindow::addLogEntry(bridgepp::Log::Level level, const QString &message) const {
void MainWindow::addLogEntry(bridgepp::Log::Level level, const QString &message) {
addEntryToLogEdit(level, message, *ui_.editLog);
}
@ -105,7 +89,7 @@ void MainWindow::addLogEntry(bridgepp::Log::Level level, const QString &message)
/// \param[in] level The log level.
/// \param[in] message The log message
//****************************************************************************************************************************************************
void MainWindow::addBridgeGUILogEntry(bridgepp::Log::Level level, const QString &message) const {
void MainWindow::addBridgeGUILogEntry(bridgepp::Log::Level level, const QString &message) {
addEntryToLogEdit(level, message, *ui_.editBridgeGUILog);
}
@ -113,8 +97,8 @@ void MainWindow::addBridgeGUILogEntry(bridgepp::Log::Level level, const QString
//****************************************************************************************************************************************************
/// \param[in] event The event.
//****************************************************************************************************************************************************
void MainWindow::sendDelayedEvent(SPStreamEvent const &event) const {
QTimer::singleShot(this->eventsTab().eventDelayMs(), [event] { app().grpc().sendEvent(event); });
void MainWindow::sendDelayedEvent(SPStreamEvent const &event) {
QTimer::singleShot(this->settingsTab().eventDelayMs(), [event] { app().grpc().sendEvent(event); });
}

View File

@ -38,17 +38,15 @@ public: // member functions.
MainWindow &operator=(MainWindow const &) = delete; ///< Disabled assignment operator.
MainWindow &operator=(MainWindow &&) = delete; ///< Disabled move assignment operator.
SettingsTab &settingsTab() const; ///< Returns a reference the 'Settings' tab.
UsersTab &usersTab() const; ///< Returns a reference to the 'Users' tab.
EventsTab &eventsTab() const; ///< Returns a reference to the 'Events' tab.
KnowledgeBaseTab &knowledgeBaseTab() const; ///< Returns a reference to the 'Knowledge Base' tab.
SettingsTab &settingsTab(); ///< Returns a reference the 'Settings' tab.
UsersTab &usersTab(); ///< Returns a reference to the 'Users' tab.
public slots:
void sendDelayedEvent(bridgepp::SPStreamEvent const &event) const; ///< Sends a gRPC event after the delay specified in the UI. The call is non blocking.
void sendDelayedEvent(bridgepp::SPStreamEvent const &event); ///< Sends a gRPC event after the delay specified in the UI. The call is non blocking.
private slots:
void addLogEntry(bridgepp::Log::Level level, QString const &message) const; ///< Add an entry to the log.
void addBridgeGUILogEntry(bridgepp::Log::Level level, const QString &message) const; ///< Add an entry to the log.
void addLogEntry(bridgepp::Log::Level level, QString const &message); ///< Add an entry to the log.
void addBridgeGUILogEntry(bridgepp::Log::Level level, const QString &message); ///< Add an entry to the log.
private:
Ui::MainWindow ui_ {}; ///< The GUI for the window.

View File

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>1096</width>
<height>876</height>
<width>1226</width>
<height>1086</height>
</rect>
</property>
<property name="windowTitle">
@ -22,7 +22,7 @@
</property>
<widget class="QTabWidget" name="tabTop">
<property name="currentIndex">
<number>3</number>
<number>0</number>
</property>
<widget class="SettingsTab" name="settingsTab">
<attribute name="title">
@ -34,16 +34,6 @@
<string>Users</string>
</attribute>
</widget>
<widget class="EventsTab" name="eventsTab">
<attribute name="title">
<string>Events &amp;&amp; Errors</string>
</attribute>
</widget>
<widget class="KnowledgeBaseTab" name="knowledgeBaseTab">
<attribute name="title">
<string>Knowledge Base</string>
</attribute>
</widget>
</widget>
<widget class="QTabWidget" name="tabBottom">
<property name="currentIndex">
@ -106,18 +96,6 @@
<header>Tabs/UsersTab.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>EventsTab</class>
<extends>QWidget</extends>
<header>Tabs/EventsTab.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>KnowledgeBaseTab</class>
<extends>QWidget</extends>
<header>Tabs/KnowledgeBaseTab.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>

View File

@ -1,109 +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/>.
#include "EventsTab.h"
#include "GRPCService.h"
#include <bridgepp/GRPC/EventFactory.h>
using namespace bridgepp;
//****************************************************************************************************************************************************
/// \brief Connect an address error button to the generation of an address error event.
///
/// \param[in] button The error button.
/// \param[in] edit The edit containing the address.
/// \param[in] eventGenerator The factory function creating the event.
//****************************************************************************************************************************************************
void connectAddressError(QPushButton const* button, QLineEdit* edit, SPStreamEvent (*eventGenerator)(QString const&)) {
QObject::connect(button, &QPushButton::clicked, [edit, eventGenerator]() { app().grpc().sendEvent(eventGenerator(edit->text())); });
}
//****************************************************************************************************************************************************
/// \param[in] parent The parent widget.
//****************************************************************************************************************************************************
EventsTab::EventsTab(QWidget* parent)
: QWidget(parent) {
ui_.setupUi(this);
this->resetUI();
connect(ui_.buttonInternetOn, &QPushButton::clicked, []() { app().grpc().sendEvent(newInternetStatusEvent(true)); });
connect(ui_.buttonInternetOff, &QPushButton::clicked, []() { app().grpc().sendEvent(newInternetStatusEvent(false)); });
connect(ui_.buttonShowMainWindow, &QPushButton::clicked, []() { app().grpc().sendEvent(newShowMainWindowEvent()); });
connect(ui_.buttonNoKeychain, &QPushButton::clicked, []() { app().grpc().sendEvent(newHasNoKeychainEvent()); });
connect(ui_.buttonAPICertIssue, &QPushButton::clicked, []() { app().grpc().sendEvent(newApiCertIssueEvent()); });
connectAddressError(ui_.buttonAddressChanged, ui_.editAddressErrors, newAddressChangedEvent);
connectAddressError(ui_.buttonAddressChangedLogout, ui_.editAddressErrors, newAddressChangedLogoutEvent);
//connect(ui_.checkNextCacheChangeWillSucceed, &QCheckBox::toggled, this, &SettingsTab::updateGUIState);
connect(ui_.buttonUpdateError, &QPushButton::clicked, [&]() {
app().grpc().sendEvent(newUpdateErrorEvent(static_cast<grpc::UpdateErrorType>(ui_.comboUpdateError->currentIndex())));
});
connect(ui_.buttonUpdateManualReady, &QPushButton::clicked, [&] {
app().grpc().sendEvent(newUpdateManualReadyEvent(ui_.editUpdateVersion->text()));
});
connect(ui_.buttonUpdateForce, &QPushButton::clicked, [&] {
app().grpc().sendEvent(newUpdateForceEvent(ui_.editUpdateVersion->text()));
});
connect(ui_.buttonUpdateManualRestart, &QPushButton::clicked, []() { app().grpc().sendEvent(newUpdateManualRestartNeededEvent()); });
connect(ui_.buttonUpdateSilentRestart, &QPushButton::clicked, []() { app().grpc().sendEvent(newUpdateSilentRestartNeededEvent()); });
connect(ui_.buttonUpdateIsLatest, &QPushButton::clicked, []() { app().grpc().sendEvent(newUpdateIsLatestVersionEvent()); });
connect(ui_.buttonUpdateCheckFinished, &QPushButton::clicked, []() { app().grpc().sendEvent(newUpdateCheckFinishedEvent()); });
connect(ui_.buttonUpdateVersionChanged, &QPushButton::clicked, []() { app().grpc().sendEvent(newUpdateVersionChangedEvent()); });
}
//****************************************************************************************************************************************************
/// \return The delay to apply before sending automatically generated events.
//****************************************************************************************************************************************************
qint32 EventsTab::eventDelayMs() const {
return ui_.spinEventDelay->value();
}
//****************************************************************************************************************************************************
/// \return The bug report results
//****************************************************************************************************************************************************
EventsTab::BugReportResult EventsTab::nextBugReportResult() const {
return static_cast<BugReportResult>(ui_.comboBugReportResult->currentIndex());
}
//****************************************************************************************************************************************************
/// \return The reply for the next IsPortFree gRPC call.
//****************************************************************************************************************************************************
bool EventsTab::isPortFree() const {
return ui_.checkIsPortFree->isChecked();
}
//****************************************************************************************************************************************************
/// \return The value for the 'Next Cache Change Will Succeed' check box.
//****************************************************************************************************************************************************
bool EventsTab::nextCacheChangeWillSucceed() const {
return ui_.checkNextCacheChangeWillSucceed->isChecked();
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void EventsTab::resetUI() const {
ui_.comboBugReportResult->setCurrentIndex(0);
ui_.checkIsPortFree->setChecked(true);
ui_.checkNextCacheChangeWillSucceed->setChecked(true);
}

View File

@ -1,59 +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/>.
#ifndef BRIDGE_GUI_TESTER_EVENTS_TAB_H
#define BRIDGE_GUI_TESTER_EVENTS_TAB_H
#include "ui_EventsTab.h"
//****************************************************************************************************************************************************
/// \brief Events tabs
//****************************************************************************************************************************************************
class EventsTab: public QWidget {
Q_OBJECT
public: // data types
enum class BugReportResult {
Success = 0,
Error = 1,
DataSharingError = 2,
}; ///< Enumeration for the result of bug report sending
public: // member functions.
explicit EventsTab(QWidget *parent = nullptr); ///< Default constructor.
EventsTab(EventsTab const&) = delete; ///< Disabled copy-constructor.
EventsTab(EventsTab&&) = delete; ///< Disabled assignment copy-constructor.
~EventsTab() override = default; ///< Destructor.
EventsTab& operator=(EventsTab const&) = delete; ///< Disabled assignment operator.
EventsTab& operator=(EventsTab&&) = delete; ///< Disabled move assignment operator.
qint32 eventDelayMs() const; ///< Get the delay for sending automatically generated events.
BugReportResult nextBugReportResult() const; ///< Get the value of the 'Next bug report result' combo box.
bool isPortFree() const; ///< Get the value for the "Is Port Free" check box.
bool nextCacheChangeWillSucceed() const; ///< Get the value for the 'Next Cache Change will succeed' edit.
void resetUI() const; ///< Resets the UI.
private: // data members
Ui::EventsTab ui_ {}; ///< The UI for the widget.
};
#endif

View File

@ -1,359 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>EventsTab</class>
<widget class="QWidget" name="EventsTab">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>563</width>
<height>571</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_11">
<item>
<widget class="QLabel" name="labelEventDelay">
<property name="toolTip">
<string>Delay applied before sending automatically generated events</string>
</property>
<property name="text">
<string>Delay for asynchronous events</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinEventDelay">
<property name="suffix">
<string> ms</string>
</property>
<property name="minimum">
<number>-1</number>
</property>
<property name="maximum">
<number>3600000</number>
</property>
<property name="singleStep">
<number>100</number>
</property>
<property name="value">
<number>0</number>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_5">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>1</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="title">
<string>Address related errors</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_9">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_12">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Address</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="editAddressErrors">
<property name="text">
<string>dummy.user@proton.me</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_15">
<item>
<widget class="QPushButton" name="buttonAddressChanged">
<property name="text">
<string>Address Changed</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="buttonAddressChangedLogout">
<property name="text">
<string>Address Changed Logout</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="2">
<widget class="QCheckBox" name="checkNextCacheChangeWillSucceed">
<property name="text">
<string>Next Cache Change will succeed</string>
</property>
</widget>
</item>
<item row="0" column="3">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>1</height>
</size>
</property>
</spacer>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="checkIsPortFree">
<property name="text">
<string>Reply true to the next 'Is Port Free' request.</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_13">
<item>
<widget class="QLabel" name="labelNextBugReportResult">
<property name="text">
<string>Next bug report result</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBugReportResult">
<item>
<property name="text">
<string>Success</string>
</property>
</item>
<item>
<property name="text">
<string>Error</string>
</property>
</item>
<item>
<property name="text">
<string>Data sharing error</string>
</property>
</item>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>1</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QGridLayout" name="gridLayout_6">
<property name="verticalSpacing">
<number>8</number>
</property>
<item row="0" column="0">
<widget class="QPushButton" name="buttonUpdateManualReady">
<property name="text">
<string>Update Manual Ready</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QLineEdit" name="editUpdateVersion">
<property name="text">
<string>4.0</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QPushButton" name="buttonUpdateVersionChanged">
<property name="text">
<string>Update version changed</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QPushButton" name="buttonUpdateForce">
<property name="text">
<string>Update Force</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QPushButton" name="buttonUpdateManualRestart">
<property name="text">
<string>Update manual restart</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QPushButton" name="buttonUpdateCheckFinished">
<property name="text">
<string>Update check finished</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="buttonUpdateSilentRestart">
<property name="text">
<string>Update silent restart</string>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QComboBox" name="comboUpdateError">
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToContents</enum>
</property>
<item>
<property name="text">
<string>Update manual error</string>
</property>
</item>
<item>
<property name="text">
<string>Update force error</string>
</property>
</item>
<item>
<property name="text">
<string>Update silent error</string>
</property>
</item>
</widget>
</item>
<item row="2" column="1">
<widget class="QPushButton" name="buttonUpdateError">
<property name="text">
<string>Update error</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QPushButton" name="buttonUpdateIsLatest">
<property name="text">
<string>Update is latest</string>
</property>
</widget>
</item>
<item row="4" column="1" colspan="2">
<spacer name="horizontalSpacer_6">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>1</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QGridLayout" name="gridLayout_5">
<property name="horizontalSpacing">
<number>-1</number>
</property>
<property name="verticalSpacing">
<number>8</number>
</property>
<item row="0" column="1">
<widget class="QPushButton" name="buttonInternetOn">
<property name="text">
<string>Internet On</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QPushButton" name="buttonShowMainWindow">
<property name="text">
<string>Show Main Window</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QPushButton" name="buttonInternetOff">
<property name="text">
<string>Internet Off</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="buttonAPICertIssue">
<property name="text">
<string>API Certficate Issue</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QPushButton" name="buttonNoKeychain">
<property name="text">
<string>No Keychain</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>200</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -1,96 +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/>.
#include "KnowledgeBaseTab.h"
#include "GRPCService.h"
#include "bridgepp/GRPC/EventFactory.h"
using namespace bridgepp;
//****************************************************************************************************************************************************
/// \param[in] parent The parent widget of the tab.
//****************************************************************************************************************************************************
KnowledgeBaseTab::KnowledgeBaseTab(QWidget* parent)
: QWidget(parent) {
ui_.setupUi(this);
connect(ui_.checkSuggestion1, &QCheckBox::stateChanged, this, &KnowledgeBaseTab::updateGuiState);
connect(ui_.checkSuggestion2, &QCheckBox::stateChanged, this, &KnowledgeBaseTab::updateGuiState);
connect(ui_.checkSuggestion3, &QCheckBox::stateChanged, this, &KnowledgeBaseTab::updateGuiState);
connect(ui_.buttonSend, &QCheckBox::clicked, this, &KnowledgeBaseTab::sendKnowledgeBaseSuggestions);
}
//****************************************************************************************************************************************************
/// \param[in] checkbox The check box.
/// \param[in] widgets The widgets to conditionally enable.
//****************************************************************************************************************************************************
void enableWidgetsIfChecked(QCheckBox const* checkbox, QWidgetList const& widgets) {
bool const checked = checkbox->isChecked();
for (QWidget *const widget: widgets) {
widget->setEnabled(checked);
}
}
//****************************************************************************************************************************************************
/// \return The suggestions.
//****************************************************************************************************************************************************
QList<KnowledgeBaseSuggestion> KnowledgeBaseTab::getSuggestions() const {
QList<KnowledgeBaseSuggestion> result;
if (ui_.checkSuggestion1->isChecked()) {
result.push_back({ .url = ui_.editUrl1->text(), .title = ui_.editTitle1->text() });
}
if (ui_.checkSuggestion2->isChecked()) {
result.push_back({ .url = ui_.editUrl2->text(), .title = ui_.editTitle2->text() });
}
if (ui_.checkSuggestion3->isChecked()) {
result.push_back({ .url = ui_.editUrl3->text(), .title = ui_.editTitle3->text() });
}
return result;
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void KnowledgeBaseTab::sendKnowledgeBaseSuggestions() const {
app().grpc().sendEvent(newKnowledgeBaseSuggestionsEvent(this->getSuggestions()));
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void KnowledgeBaseTab::updateGuiState() {
enableWidgetsIfChecked(ui_.checkSuggestion1, { ui_.labelTitle1, ui_.editTitle1, ui_.labelUrl1, ui_.editUrl1});
enableWidgetsIfChecked(ui_.checkSuggestion2, { ui_.labelTitle2, ui_.editTitle2, ui_.labelUrl2, ui_.editUrl2});
enableWidgetsIfChecked(ui_.checkSuggestion3, { ui_.labelTitle3, ui_.editTitle3, ui_.labelUrl3, ui_.editUrl3});
}
//****************************************************************************************************************************************************
/// \param[in] userInput The user input.
//****************************************************************************************************************************************************
void KnowledgeBaseTab::requestKnowledgeBaseSuggestions(QString const& userInput) const {
ui_.editUserInput->setPlainText(userInput);
ui_.labelLastReceived->setText(tr("Last received: %1").arg(QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm:ss.zzz")));
}

View File

@ -1,53 +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/>.
#ifndef BRIDGE_GUI_TESTER_KNOWLEDGE_BASE_TAB_H
#define BRIDGE_GUI_TESTER_KNOWLEDGE_BASE_TAB_H
#include "ui_KnowledgeBaseTab.h"
#include <bridgepp/GRPC/GRPCClient.h>
//****************************************************************************************************************************************************
/// \brief Knowledge base table.
//****************************************************************************************************************************************************
class KnowledgeBaseTab: public QWidget {
public: // member functions.
explicit KnowledgeBaseTab(QWidget *parent = nullptr); ///< Default constructor.
KnowledgeBaseTab(KnowledgeBaseTab const&) = delete; ///< Disabled copy-constructor.
KnowledgeBaseTab(KnowledgeBaseTab&&) = delete; ///< Disabled assignment copy-constructor.
~KnowledgeBaseTab() override = default; ///< Destructor.
KnowledgeBaseTab& operator=(KnowledgeBaseTab const&) = delete; ///< Disabled assignment operator.
KnowledgeBaseTab& operator=(KnowledgeBaseTab&&) = delete; ///< Disabled move assignment operator.
QList<bridgepp::KnowledgeBaseSuggestion> getSuggestions() const; ///< Returns the suggestions.
public slots:
void requestKnowledgeBaseSuggestions(QString const &userInput) const; ///< Slot for the 'RequestKnowledgeBaseSuggestions' gRPC call.
private slots:
void sendKnowledgeBaseSuggestions() const; ///< Send a KnowledgeBaseSuggestions event.
void updateGuiState(); ///< Update the GUI state.
private: // data members
Ui::KnowledgeBaseTab ui_ {}; ///< The UI for the widget.
};
#endif //BRIDGE_GUI_TESTER_KNOWLEDGE_BASE_TAB_H

View File

@ -1,239 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>KnowledgeBaseTab</class>
<widget class="QWidget" name="KnowledgeBaseTab">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>857</width>
<height>905</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QGroupBox" name="groupUserInput">
<property name="title">
<string>User Input</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QPlainTextEdit" name="editUserInput">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="labelLastReceived">
<property name="text">
<string>Last received: never</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="groupBoxSuggestion1">
<property name="title">
<string>Suggestion 1</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0" colspan="2">
<widget class="QCheckBox" name="checkSuggestion1">
<property name="text">
<string>Enabled</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="labelTitle1">
<property name="text">
<string>Title</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="editTitle1">
<property name="text">
<string>Automatically start Bridge</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="labelUrl1">
<property name="text">
<string>URL</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="editUrl1">
<property name="text">
<string>https://proton.me/support/automatically-start-bridge</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBoxSuggestion2">
<property name="title">
<string>Suggestion 2</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0" colspan="2">
<widget class="QCheckBox" name="checkSuggestion2">
<property name="text">
<string>Enabled</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="labelTitle2">
<property name="text">
<string>Title</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="editTitle2">
<property name="text">
<string>Proton Mail Bridge connection issues with Thunderbird, Outlook, and Apple Mail</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="labelUrl2">
<property name="text">
<string>URL</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="editUrl2">
<property name="text">
<string>https://proton.me/support/bridge-ssl-connection-issue</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBoxSuggestion3">
<property name="title">
<string>Suggestion 3</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0" colspan="2">
<widget class="QCheckBox" name="checkSuggestion3">
<property name="text">
<string>Enabled</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="labelTitle3">
<property name="text">
<string>Title</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="editTitle3">
<property name="text">
<string>Difference between combined addresses mode and split addresses mode</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="labelUrl3">
<property name="text">
<string>URL</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="editUrl3">
<property name="text">
<string>https://proton.me/support/difference-combined-addresses-mode-split-addresses-mode</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="buttonSend">
<property name="text">
<string>Send Knowledge Base Suggestions</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>208</width>
<height>289</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -18,6 +18,7 @@
#include "SettingsTab.h"
#include "GRPCService.h"
#include <bridgepp/GRPC/EventFactory.h>
#include <bridgepp/BridgeUtils.h>
@ -37,6 +38,23 @@ SettingsTab::SettingsTab(QWidget *parent)
: QWidget(parent) {
ui_.setupUi(this);
connect(ui_.buttonInternetOn, &QPushButton::clicked, []() { app().grpc().sendEvent(newInternetStatusEvent(true)); });
connect(ui_.buttonInternetOff, &QPushButton::clicked, []() { app().grpc().sendEvent(newInternetStatusEvent(false)); });
connect(ui_.buttonShowMainWindow, &QPushButton::clicked, []() { app().grpc().sendEvent(newShowMainWindowEvent()); });
connect(ui_.buttonAPICertIssue, &QPushButton::clicked, []() { app().grpc().sendEvent(newApiCertIssueEvent()); });
connect(ui_.buttonDiskCacheUnavailable, &QPushButton::clicked, []() {
app().grpc().sendEvent(
newDiskCacheErrorEvent(grpc::DiskCacheErrorType::DISK_CACHE_UNAVAILABLE_ERROR));
});
connect(ui_.buttonDiskFull, &QPushButton::clicked, []() {
app().grpc().sendEvent(
newDiskCacheErrorEvent(grpc::DiskCacheErrorType::DISK_FULL_ERROR));
});
connect(ui_.buttonNoActiveKeyForRecipient, &QPushButton::clicked, [&]() {
app().grpc().sendEvent(
newNoActiveKeyForRecipientEvent(ui_.editNoActiveKeyForRecipient->text()));
});
connect(ui_.checkNextCacheChangeWillSucceed, &QCheckBox::toggled, this, &SettingsTab::updateGUIState);
this->resetUI();
this->updateGUIState();
}
@ -46,10 +64,11 @@ SettingsTab::SettingsTab(QWidget *parent)
//
//****************************************************************************************************************************************************
void SettingsTab::updateGUIState() {
bool const connected = app().grpc().isStreaming();
bool connected = app().grpc().isStreaming();
for (QWidget *widget: { ui_.groupVersion, ui_.groupGeneral, ui_.groupMail, ui_.groupPaths, ui_.groupCache }) {
widget->setEnabled(!connected);
}
ui_.comboCacheError->setEnabled(!ui_.checkNextCacheChangeWillSucceed->isChecked());
}
@ -65,7 +84,7 @@ void SettingsTab::setIsStreaming(bool isStreaming) {
//****************************************************************************************************************************************************
/// \param[in] clientPlatform The client platform.
//****************************************************************************************************************************************************
void SettingsTab::setClientPlatform(QString const &clientPlatform) const {
void SettingsTab::setClientPlatform(QString const &clientPlatform) {
ui_.labelClientPlatformValue->setText(clientPlatform);
}
@ -120,7 +139,7 @@ bool SettingsTab::showSplashScreen() const {
//****************************************************************************************************************************************************
/// \return true iff autostart is on.
/// \return true iff autosart is on.
//****************************************************************************************************************************************************
bool SettingsTab::isAutostartOn() const {
return ui_.checkAutostart->isChecked();
@ -130,7 +149,7 @@ bool SettingsTab::isAutostartOn() const {
//****************************************************************************************************************************************************
/// \param[in] on Should autostart be turned on?
//****************************************************************************************************************************************************
void SettingsTab::setIsAutostartOn(bool on) const {
void SettingsTab::setIsAutostartOn(bool on) {
ui_.checkAutostart->setChecked(on);
}
@ -146,7 +165,7 @@ QString SettingsTab::colorSchemeName() const {
//****************************************************************************************************************************************************
/// \param[in] name True if the 'Use Dark Theme' check box should be checked.
//****************************************************************************************************************************************************
void SettingsTab::setColorSchemeName(QString const &name) const {
void SettingsTab::setColorSchemeName(QString const &name) {
ui_.checkDarkTheme->setChecked(name == colorSchemeDark);
}
@ -162,7 +181,7 @@ bool SettingsTab::isBetaEnabled() const {
//****************************************************************************************************************************************************
/// \param[in] enabled The new state for the 'Beta Enabled' check box.
//****************************************************************************************************************************************************
void SettingsTab::setIsBetaEnabled(bool enabled) const {
void SettingsTab::setIsBetaEnabled(bool enabled) {
ui_.checkBetaEnabled->setChecked(enabled);
}
@ -178,7 +197,7 @@ bool SettingsTab::isAllMailVisible() const {
//****************************************************************************************************************************************************
/// \param[in] visible The new value for the 'All Mail Visible' check box.
//****************************************************************************************************************************************************
void SettingsTab::setIsAllMailVisible(bool visible) const {
void SettingsTab::setIsAllMailVisible(bool visible) {
ui_.checkAllMailVisible->setChecked(visible);
}
@ -194,11 +213,19 @@ bool SettingsTab::isTelemetryDisabled() const {
//****************************************************************************************************************************************************
/// \param[in] isDisabled The new value for the 'Disable Telemetry' check box.
//****************************************************************************************************************************************************
void SettingsTab::setIsTelemetryDisabled(bool isDisabled) const {
void SettingsTab::setIsTelemetryDisabled(bool isDisabled) {
ui_.checkIsTelemetryDisabled->setChecked(isDisabled);
}
//****************************************************************************************************************************************************
/// \return The delay to apply before sending automatically generated events.
//****************************************************************************************************************************************************
qint32 SettingsTab::eventDelayMs() const {
return ui_.spinEventDelay->value();
}
//****************************************************************************************************************************************************
/// \return The path
//****************************************************************************************************************************************************
@ -248,7 +275,7 @@ QString SettingsTab::landingPageLink() const {
/// \param[in] includeLogs Are the log included.
//****************************************************************************************************************************************************
void SettingsTab::setBugReport(QString const &osType, QString const &osVersion, QString const &emailClient, QString const &address,
QString const &description, bool includeLogs) const {
QString const &description, bool includeLogs) {
ui_.editOSType->setText(osType);
ui_.editOSVersion->setText(osVersion);
ui_.editEmailClient->setText(emailClient);
@ -258,36 +285,21 @@ void SettingsTab::setBugReport(QString const &osType, QString const &osVersion,
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void SettingsTab::installTLSCertificate() const {
ui_.labelLastTLSCertInstall->setText(QString("Last install: %1").arg(QDateTime::currentDateTime().toString(Qt::ISODateWithMs)));
ui_.checkTLSCertIsInstalled->setChecked(this->nextTLSCertInstallResult() == TLSCertInstallResult::Success);
}
//****************************************************************************************************************************************************
/// \param[in] folderPath The folder path.
//****************************************************************************************************************************************************
void SettingsTab::exportTLSCertificates(QString const &folderPath) const {
ui_.labeLastTLSCertExport->setText(QString("%1 Export to %2").arg(QDateTime::currentDateTime().toString(Qt::ISODateWithMs),folderPath));
void SettingsTab::exportTLSCertificates(QString const &folderPath) {
ui_.labeLastTLSCertsExport->setText(QString("%1 Export to %2")
.arg(QDateTime::currentDateTime().toString(Qt::ISODateWithMs))
.arg(folderPath));
}
//****************************************************************************************************************************************************
/// \return the state of the 'TLS Certificate is installed' check box.
/// \return The state of the check box.
//****************************************************************************************************************************************************
bool SettingsTab::isTLSCertificateInstalled() const {
return ui_.checkTLSCertIsInstalled->isChecked();
}
//****************************************************************************************************************************************************
/// \return The value for the 'Next TLS cert install result'.
//****************************************************************************************************************************************************
SettingsTab::TLSCertInstallResult SettingsTab::nextTLSCertInstallResult() const {
return static_cast<TLSCertInstallResult>(ui_.comboNextTLSCertInstallResult->currentIndex());
bool SettingsTab::nextBugReportWillSucceed() const {
return ui_.checkNextBugReportWillSucceed->isChecked();
}
@ -318,7 +330,7 @@ QString SettingsTab::hostname() const {
//****************************************************************************************************************************************************
/// \return The value of the IMAP port spin box.
//****************************************************************************************************************************************************
qint32 SettingsTab::imapPort() const {
qint32 SettingsTab::imapPort() {
return ui_.spinPortIMAP->value();
}
@ -326,7 +338,7 @@ qint32 SettingsTab::imapPort() const {
//****************************************************************************************************************************************************
/// \return The value of the SMTP port spin box.
//****************************************************************************************************************************************************
qint32 SettingsTab::smtpPort() const {
qint32 SettingsTab::smtpPort() {
return ui_.spinPortSMTP->value();
}
@ -337,7 +349,7 @@ qint32 SettingsTab::smtpPort() const {
/// \param[in] useSSLForIMAP The IMAP connexion mode.
/// \param[in] useSSLForSMTP The IMAP connexion mode.
//****************************************************************************************************************************************************
void SettingsTab::setMailServerSettings(qint32 imapPort, qint32 smtpPort, bool useSSLForIMAP, bool useSSLForSMTP) const {
void SettingsTab::setMailServerSettings(qint32 imapPort, qint32 smtpPort, bool useSSLForIMAP, bool useSSLForSMTP) {
ui_.spinPortIMAP->setValue(imapPort);
ui_.spinPortSMTP->setValue(smtpPort);
ui_.checkUseSSLForIMAP->setChecked(useSSLForIMAP);
@ -372,15 +384,23 @@ bool SettingsTab::isDoHEnabled() const {
//****************************************************************************************************************************************************
/// \param[in] enabled The state of the 'DoH enabled' check box.
//****************************************************************************************************************************************************
void SettingsTab::setIsDoHEnabled(bool enabled) const {
void SettingsTab::setIsDoHEnabled(bool enabled) {
ui_.checkDoHEnabled->setChecked(enabled);
}
//****************************************************************************************************************************************************
/// \return The reply for the next IsPortFree gRPC call.
//****************************************************************************************************************************************************
bool SettingsTab::isPortFree() const {
return ui_.checkIsPortFree->isChecked();
}
//****************************************************************************************************************************************************
/// \param[in] path The path of the local cache.
//****************************************************************************************************************************************************
void SettingsTab::setDiskCachePath(const QString &path) const {
void SettingsTab::setDiskCachePath(const QString &path) {
ui_.editDiskCachePath->setText(path);
}
@ -393,6 +413,22 @@ QString SettingsTab::diskCachePath() const {
}
//****************************************************************************************************************************************************
/// \return The value for the 'Next Cache Change Will Succeed' check box.
//****************************************************************************************************************************************************
bool SettingsTab::nextCacheChangeWillSucceed() const {
return ui_.checkNextCacheChangeWillSucceed->isChecked();
}
//****************************************************************************************************************************************************
/// \return The index of the selected cache error.
//****************************************************************************************************************************************************
qint32 SettingsTab::cacheError() const {
return ui_.comboCacheError->currentIndex();
}
//****************************************************************************************************************************************************
/// \return the value for the 'Automatic Update' check.
//****************************************************************************************************************************************************
@ -404,7 +440,7 @@ bool SettingsTab::isAutomaticUpdateOn() const {
//****************************************************************************************************************************************************
/// \param[in] on The value for the 'Automatic Update' check.
//****************************************************************************************************************************************************
void SettingsTab::setIsAutomaticUpdateOn(bool on) const {
void SettingsTab::setIsAutomaticUpdateOn(bool on) {
ui_.checkAutomaticUpdate->setChecked(on);
}
@ -453,23 +489,20 @@ void SettingsTab::resetUI() {
ui_.editAddress->setText(QString());
ui_.editDescription->setPlainText(QString());
ui_.labelIncludeLogsValue->setText(QString());
ui_.checkNextBugReportWillSucceed->setChecked(true);
ui_.editHostname->setText("localhost");
ui_.spinPortIMAP->setValue(1143);
ui_.spinPortSMTP->setValue(1025);
ui_.checkUseSSLForSMTP->setChecked(false);
ui_.checkDoHEnabled->setChecked(true);
ui_.checkIsPortFree->setChecked(true);
QString const cacheDir = QDir(tmpDir).absoluteFilePath("cache");
QDir().mkpath(cacheDir);
ui_.editDiskCachePath->setText(QDir::toNativeSeparators(cacheDir));
ui_.checkNextCacheChangeWillSucceed->setChecked(true);
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");
}

View File

@ -28,18 +28,11 @@
//****************************************************************************************************************************************************
class SettingsTab : public QWidget {
Q_OBJECT
public: // data types.
enum class TLSCertInstallResult {
Success = 0,
Canceled = 1,
Failure = 2
}; ///< Enumeration 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.
SettingsTab(SettingsTab &&) = delete; ///< Disabled assignment copy-constructor.
~SettingsTab() override = default; ///< Destructor.
~SettingsTab() = default; ///< Destructor.
SettingsTab &operator=(SettingsTab const &) = delete; ///< Disabled assignment operator.
SettingsTab &operator=(SettingsTab &&) = delete; ///< Disabled move assignment operator.
@ -54,47 +47,49 @@ public: // member functions.
bool isAllMailVisible() const; ///< Get the value for the 'All Mail Visible' check.
bool isTelemetryDisabled() const; ///< Get the value for the 'Disable Telemetry' check box.
QString colorSchemeName() const; ///< Get the value of the 'Use Dark Theme' checkbox.
qint32 eventDelayMs() const; ///< Get the delay for sending automatically generated events.
QString logsPath() const; ///< Get the content of the 'Logs Path' edit.
QString licensePath() const; ///< Get the content of the 'License Path' edit.
QString releaseNotesPageLink() const; ///< Get the content of the 'Release Notes Page Link' edit.
QString dependencyLicenseLink() const; ///< Get the content of the 'Dependency License Link' edit.
QString landingPageLink() const; ///< Get the content of the 'Landing Page Link' edit.
bool isTLSCertificateInstalled() const; ///< Get the status of the 'TLS Certificate is installed' check box.
TLSCertInstallResult nextTLSCertInstallResult() const; ///< Get the value of the 'Next TLS Certificate install result' combo box.
bool nextBugReportWillSucceed() const; ///< Get the status of the 'Next Bug Report Will Fail' check box.
bool nextTLSCertExportWillSucceed() const; ///< Get the status of the 'Next TLS Cert export will succeed' check box.
bool nextTLSKeyExportWillSucceed() const; ///< Get the status of the 'Next TLS Key export will succeed' check box.
QString hostname() const; ///< Get the value of the 'Hostname' edit.
qint32 imapPort() const; ///< Get the value of the IMAP port spin.
qint32 smtpPort() const; ///< Get the value of the SMTP port spin.
qint32 imapPort(); ///< Get the value of the IMAP port spin.
qint32 smtpPort(); ///< Get the value of the SMTP port spin.
bool useSSLForSMTP() const; ///< Get the value for the 'Use SSL for SMTP' check box.
bool useSSLForIMAP() const; ///< Get the value for the 'Use SSL for IMAP' check box.
bool isDoHEnabled() const; ///< Get the value for the 'DoH Enabled' check box.
bool isPortFree() const; ///< Get the value for the "Is Port Free" check box.
QString diskCachePath() const; ///< Get the value for the 'Disk Cache Path' edit.
bool nextCacheChangeWillSucceed() const; ///< Get the value for the 'Next Cache Change will succeed' edit.
qint32 cacheError() const; ///< Return the index of the selected cache error.
bool isAutomaticUpdateOn() const; ///<Get the value for the 'Automatic Update' check box.
public slots:
void updateGUIState(); ///< Update the GUI state.
void setIsStreaming(bool isStreaming); ///< Set the isStreamingEvents value.
void setClientPlatform(QString const &clientPlatform) const; ///< Set the client platform.
void setIsAutostartOn(bool on) const; ///< Set the value for the 'Autostart' check box.
void setIsBetaEnabled(bool enabled) const; ///< Set the value for the 'Beta Enabled' check box.
void setIsAllMailVisible(bool visible) const; ///< Set the value for the 'All Mail Visible' check box.
void setIsTelemetryDisabled(bool isDisabled) const; ///< Set the value for the 'Disable Telemetry' check box.
void setColorSchemeName(QString const &name) const; ///< Set the value for the 'Use Dark Theme' check box.
void setClientPlatform(QString const &clientPlatform); ///< Set the client platform.
void setIsAutostartOn(bool on); ///< Set the value for the 'Autostart' check box.
void setIsBetaEnabled(bool enabled); ///< Set the value for the 'Beta Enabled' check box.
void setIsAllMailVisible(bool visible); ///< Set the value for the 'All Mail Visible' check box.
void setIsTelemetryDisabled(bool isDisabled); ///< Set the value for the 'Disable Telemetry' check box.
void setColorSchemeName(QString const &name); ///< Set the value for the 'Use Dark Theme' check box.
void setBugReport(QString const &osType, QString const &osVersion, QString const &emailClient, QString const &address, QString const &description,
bool includeLogs) const; ///< Set the content of the bug report box.
void installTLSCertificate() const; ///< Install the TLS certificate.
void exportTLSCertificates(QString const &folderPath) const; ///< Export the TLS certificates.
void setMailServerSettings(qint32 imapPort, qint32 smtpPort, bool useSSLForIMAP, bool useSSLForSMTP) const; ///< Change the mail server settings.
void setIsDoHEnabled(bool enabled) const; ///< Set the value for the 'DoH Enabled' check box.
void setDiskCachePath(QString const &path) const; ///< Set the value for the 'Cache On Disk Enabled' check box.
void setIsAutomaticUpdateOn(bool on) const; ///< Set the value for the 'Automatic Update' check box.
bool includeLogs); ///< Set the content of the bug report box.
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.
void setDiskCachePath(QString const &path); ///< Set the value for the 'Cache On Disk Enabled' check box.
void setIsAutomaticUpdateOn(bool on); ///< Set the value for the 'Automatic Update' check box.
private: // member functions.
void resetUI(); ///< Reset the widget.
private: // data members.
Ui::SettingsTab ui_ {}; ///< The GUI for the tab
Ui::SettingsTab ui_; ///< The GUI for the tab
};

View File

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>1146</width>
<height>716</height>
<width>1127</width>
<height>808</height>
</rect>
</property>
<property name="windowTitle">
@ -18,9 +18,6 @@
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>4</number>
</property>
<item>
<widget class="QGroupBox" name="groupVersion">
<property name="minimumSize">
@ -106,9 +103,6 @@
<string>General Settings</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<property name="verticalSpacing">
<number>4</number>
</property>
<item row="0" column="0">
<widget class="QCheckBox" name="checkShowOnStartup">
<property name="text">
@ -192,9 +186,6 @@
<string>Mail</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_6">
<property name="spacing">
<number>4</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_6" stretch="0,0">
<item>
@ -208,7 +199,7 @@
<widget class="QLineEdit" name="editHostname">
<property name="minimumSize">
<size>
<width>0</width>
<width>200</width>
<height>0</height>
</size>
</property>
@ -296,9 +287,6 @@
<string>Paths &amp;&amp; Links</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<property name="verticalSpacing">
<number>4</number>
</property>
<item row="0" column="0">
<widget class="QLabel" name="labelLogsPath">
<property name="text">
@ -382,7 +370,7 @@
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupCert">
<widget class="QGroupBox" name="groupCache_2">
<property name="minimumSize">
<size>
<width>0</width>
@ -392,84 +380,34 @@
<property name="title">
<string>TLS Certficates</string>
</property>
<layout class="QGridLayout" name="gridLayout_4" columnstretch="1,1">
<property name="verticalSpacing">
<number>4</number>
</property>
<item row="0" column="0">
<widget class="QCheckBox" name="checkTLSCertIsInstalled">
<layout class="QVBoxLayout" name="verticalLayout_9">
<item>
<widget class="QLabel" name="labeLastTLSCertsExport">
<property name="text">
<string>Certificate is installed</string>
</property>
<property name="checked">
<bool>false</bool>
<string>Last Export: Never</string>
</property>
</widget>
</item>
<item row="0" column="1">
<item>
<widget class="QCheckBox" name="checkTLSCertExportWillSucceed">
<property name="text">
<string>Certificate export will succeed</string>
<string>TLS certificate export will succeed</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="1">
<item>
<widget class="QCheckBox" name="checkTLSKeyExportWillSucceed">
<property name="text">
<string>Key export will succeed</string>
<string>TLS private 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>
@ -502,9 +440,6 @@
<string>Status</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="spacing">
<number>5</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
@ -614,8 +549,8 @@
<string>Bug Report</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="1" column="1">
<widget class="QLineEdit" name="editEmailClient">
<item row="0" column="3">
<widget class="QLineEdit" name="editOSVersion">
<property name="minimumSize">
<size>
<width>0</width>
@ -628,37 +563,6 @@
<height>0</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="labelOSType">
<property name="text">
<string>OS Type</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QLabel" name="labelAddress">
<property name="text">
<string>Address</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="labelEmailClient">
<property name="text">
<string>Email Cient</string>
</property>
</widget>
</item>
<item row="2" column="1" colspan="3">
<widget class="QPlainTextEdit" name="editDescription">
<property name="readOnly">
<bool>true</bool>
</property>
@ -677,6 +581,13 @@
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="labelOSType">
<property name="text">
<string>OS Type</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="labelIncludeLogs">
<property name="text">
@ -684,18 +595,22 @@
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="labelDescription">
<item row="1" column="2">
<widget class="QLabel" name="labelAddress">
<property name="text">
<string>Description</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
<string>Address</string>
</property>
</widget>
</item>
<item row="0" column="3">
<widget class="QLineEdit" name="editOSVersion">
<item row="0" column="2">
<widget class="QLabel" name="labelOSVersion">
<property name="text">
<string>OS Version</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="editOSType">
<property name="minimumSize">
<size>
<width>0</width>
@ -720,29 +635,251 @@
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="editOSType">
<item row="2" column="0">
<widget class="QLabel" name="labelDescription">
<property name="text">
<string>Description</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="editEmailClient">
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="baseSize">
<size>
<width>250</width>
<height>0</height>
</size>
<property name="text">
<string/>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QLabel" name="labelOSVersion">
<item row="1" column="0">
<widget class="QLabel" name="labelEmailClient">
<property name="text">
<string>OS Version</string>
<string>Email Cient</string>
</property>
</widget>
</item>
<item row="2" column="1" colspan="4">
<widget class="QPlainTextEdit" name="editDescription">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupApp">
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="title">
<string>Events &amp;&amp; Errors</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_8">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_11">
<item>
<widget class="QLabel" name="labelEventDelay">
<property name="toolTip">
<string>Delay applied before sending automatically generated events</string>
</property>
<property name="text">
<string>Delay for asynchronous events</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinEventDelay">
<property name="suffix">
<string> ms</string>
</property>
<property name="maximum">
<number>3600000</number>
</property>
<property name="singleStep">
<number>100</number>
</property>
<property name="value">
<number>1000</number>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_5">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>1</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_9">
<item>
<widget class="QPushButton" name="buttonInternetOff">
<property name="text">
<string>Internet Off</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="buttonInternetOn">
<property name="text">
<string>Internet On</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="buttonShowMainWindow">
<property name="text">
<string>Show Main Window</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="buttonAPICertIssue">
<property name="text">
<string>API cert. Issue</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_13">
<item>
<widget class="QPushButton" name="buttonDiskCacheUnavailable">
<property name="text">
<string>Disk Cache Unavailable</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="buttonDiskFull">
<property name="text">
<string>Disk Full</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>1</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_12">
<item>
<widget class="QPushButton" name="buttonNoActiveKeyForRecipient">
<property name="text">
<string>No Active Key For Recipient</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="editNoActiveKeyForRecipient">
<property name="text">
<string>dummy.user@proton.me</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="checkIsPortFree">
<property name="text">
<string>Reply true to the next 'Is Port Free' request.</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_8">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QCheckBox" name="checkNextCacheChangeWillSucceed">
<property name="text">
<string>Next Cache Change will succeed</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_6">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>1</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QComboBox" name="comboCacheError">
<item>
<property name="text">
<string>Disk Cache Unavailable</string>
</property>
</item>
<item>
<property name="text">
<string>Can't Move Disk Cache</string>
</property>
</item>
<item>
<property name="text">
<string>Disk Full</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="checkNextBugReportWillSucceed">
<property name="text">
<string>Next Bug Report Will Succeed</string>
</property>
</widget>
</item>
@ -772,8 +909,19 @@
<tabstop>editDependencyLicenseLink</tabstop>
<tabstop>editLandingPageLink</tabstop>
<tabstop>editDiskCachePath</tabstop>
<tabstop>editOSType</tabstop>
<tabstop>editOSVersion</tabstop>
<tabstop>editEmailClient</tabstop>
<tabstop>editAddress</tabstop>
<tabstop>editDescription</tabstop>
<tabstop>spinEventDelay</tabstop>
<tabstop>buttonInternetOff</tabstop>
<tabstop>buttonInternetOn</tabstop>
<tabstop>buttonShowMainWindow</tabstop>
<tabstop>checkIsPortFree</tabstop>
<tabstop>checkNextCacheChangeWillSucceed</tabstop>
<tabstop>comboCacheError</tabstop>
<tabstop>checkNextBugReportWillSucceed</tabstop>
</tabstops>
<resources/>
<connections/>

View File

@ -58,7 +58,7 @@ UsersTab::UsersTab(QWidget *parent)
connect(ui_.checkSync, &QCheckBox::toggled, this, &UsersTab::onCheckSyncToggled);
connect(ui_.sliderSync, &QSlider::valueChanged, this, &UsersTab::onSliderSyncValueChanged);
users_.append(defaultUser());
users_.append(randomUser());
this->updateGUIState();
}
@ -85,7 +85,7 @@ void UsersTab::onAddUserButton() {
//
//****************************************************************************************************************************************************
void UsersTab::onEditUserButton() {
const int index = selectedIndex();
int index = selectedIndex();
if ((index < 0) || (index >= users_.userCount())) {
return;
}
@ -110,7 +110,7 @@ void UsersTab::onEditUserButton() {
//
//****************************************************************************************************************************************************
void UsersTab::onRemoveUserButton() {
const int index = selectedIndex();
int index = selectedIndex();
if ((index < 0) || (index >= users_.userCount())) {
return;
}
@ -127,7 +127,7 @@ void UsersTab::onRemoveUserButton() {
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void UsersTab::onSelectionChanged(QItemSelection const&, QItemSelection const&) {
void UsersTab::onSelectionChanged(QItemSelection, QItemSelection) {
this->updateGUIState();
}
@ -137,6 +137,7 @@ void UsersTab::onSelectionChanged(QItemSelection const&, QItemSelection const&)
//****************************************************************************************************************************************************
void UsersTab::onSendUserBadEvent() {
SPUser const user = selectedUser();
int const index = this->selectedIndex();
if (!user) {
app().log().error(QString("%1 failed. Unkown user.").arg(__FUNCTION__));
@ -174,8 +175,8 @@ void UsersTab::onSendUsedBytesChangedEvent() {
app().log().error(QString("%1 failed. User is not connected").arg(__FUNCTION__));
}
auto const usedBytes = static_cast<qint64>(ui_.spinUsedBytes->value());
user->setUsedBytes(static_cast<float>(usedBytes));
qint64 const usedBytes = qint64(ui_.spinUsedBytes->value());
user->setUsedBytes(usedBytes);
users_.touch(index);
GRPCService &grpc = app().grpc();
@ -223,10 +224,9 @@ void UsersTab::updateGUIState() {
QSignalBlocker b(ui_.checkSync);
bool const syncing = user && user->isSyncing();
ui_.checkSync->setChecked(syncing);
// ReSharper disable once CppDFAUnusedValue
b = QSignalBlocker(ui_.sliderSync);
ui_.sliderSync->setEnabled(syncing);
qint32 const progressPercent = syncing ? static_cast<qint32>(user->syncProgress() * 100.0f) : 0;
qint32 const progressPercent = syncing ? qint32(user->syncProgress() * 100.0f) : 0;
ui_.sliderSync->setValue(progressPercent);
ui_.labelSync->setText(syncing ? QString("%1%").arg(progressPercent) : "" );
}
@ -418,7 +418,7 @@ void UsersTab::processBadEventUserFeedback(QString const &userID, bool doResync)
return; // we do not do any form of emulation for resync.
}
SPUser const user = users_.userWithID(userID);
SPUser user = users_.userWithID(userID);
if (!user) {
app().log().error(QString("%1(): could not find user with id %1.").arg(__func__, userID));
}
@ -464,12 +464,12 @@ void UsersTab::onCheckSyncToggled(bool checked) {
//****************************************************************************************************************************************************
void UsersTab::onSliderSyncValueChanged(int value) {
SPUser const user = this->selectedUser();
if ((!user) || (!user->isSyncing()) || user->syncProgress() == static_cast<float>(value)) {
if ((!user) || (!user->isSyncing()) || user->syncProgress() == value) {
return;
}
double const progress = value / 100.0;
user->setSyncProgress(static_cast<float>(progress));
user->setSyncProgress(progress);
app().grpc().sendEvent(newSyncProgressEvent(user->id(), progress, 1, 1)); // we do not simulate elapsed & remaining.
this->updateGUIState();
}

View File

@ -53,14 +53,14 @@ public slots:
void setUserSplitMode(QString const &userID, bool makeItActive); ///< Slot for the split mode.
void logoutUser(QString const &userID); ///< slot for the logging out of a user.
void removeUser(QString const &userID); ///< Slot for the removal of a user.
static void configureUserAppleMail(QString const &userID, QString const &address); ///< Slot for the configuration of Apple mail.
void configureUserAppleMail(QString const &userID, QString const &address); ///< Slot for the configuration of Apple mail.
void processBadEventUserFeedback(QString const& userID, bool doResync); ///< Slot for the reception of a bad event user feedback.
private slots:
void onAddUserButton(); ///< Add a user to the user list.
void onEditUserButton(); ///< Edit the currently selected user.
void onRemoveUserButton(); ///< Remove the currently selected user.
void onSelectionChanged(QItemSelection const&, QItemSelection const&); ///< Slot for the change of the selection.
void onSelectionChanged(QItemSelection, QItemSelection); ///< Slot for the change of the selection.
void onSendUserBadEvent(); ///< Slot for the 'Send Bad Event Error' button.
void onSendUsedBytesChangedEvent(); ///< Slot for the 'Send Used Bytes Changed Event' button.
void onSendIMAPLoginFailedEvent(); ///< Slot for the 'Send IMAP Login failure Event' button.

View File

@ -26,7 +26,7 @@ using namespace bridgepp;
/// \param[in] user The user.
/// \param[in] parent The parent widget of the dialog.
//****************************************************************************************************************************************************
UserDialog::UserDialog(const bridgepp::SPUser &user, QWidget *parent)
UserDialog::UserDialog(bridgepp::SPUser &user, QWidget *parent)
: QDialog(parent)
, user_(user) {
ui_.setupUi(this);
@ -57,8 +57,8 @@ void UserDialog::onOK() {
user_->setAvatarText(ui_.editAvatarText->text());
user_->setState(this->state());
user_->setSplitMode(ui_.checkSplitMode->isChecked());
user_->setUsedBytes(static_cast<float>(ui_.spinUsedBytes->value()));
user_->setTotalBytes(static_cast<float>(ui_.spinTotalBytes->value()));
user_->setUsedBytes(float(ui_.spinUsedBytes->value()));
user_->setTotalBytes(float(ui_.spinTotalBytes->value()));
this->accept();
}
@ -67,14 +67,14 @@ void UserDialog::onOK() {
//****************************************************************************************************************************************************
/// \return The user state that is currently selected in the dialog.
//****************************************************************************************************************************************************
UserState UserDialog::state() const {
return static_cast<UserState>(ui_.comboState->currentIndex());
UserState UserDialog::state() {
return UserState(ui_.comboState->currentIndex());
}
//****************************************************************************************************************************************************
/// \param[in] state The user state to select in the dialog.
//****************************************************************************************************************************************************
void UserDialog::setState(UserState state) const {
ui_.comboState->setCurrentIndex(static_cast<qint32>(state));
void UserDialog::setState(UserState state) {
ui_.comboState->setCurrentIndex(qint32(state));
}

View File

@ -30,7 +30,7 @@
class UserDialog : public QDialog {
Q_OBJECT
public: // member functions.
UserDialog(const bridgepp::SPUser &user, QWidget *parent); ///< Default constructor.
UserDialog(bridgepp::SPUser &user, QWidget *parent); ///< Default constructor.
UserDialog(UserDialog const &) = delete; ///< Disabled copy-constructor.
UserDialog(UserDialog &&) = delete; ///< Disabled assignment copy-constructor.
~UserDialog() override = default; ///< Destructor.
@ -38,8 +38,8 @@ public: // member functions.
UserDialog &operator=(UserDialog &&) = delete; ///< Disabled move assignment operator.
private: // member functions
bridgepp::UserState state() const; ///< Get the user state selected in the dialog.
void setState(bridgepp::UserState state) const; ///< Set the user state selected in the dialog
bridgepp::UserState state(); ///< Get the user state selected in the dialog.
void setState(bridgepp::UserState state); ///< Set the user state selected in the dialog
private slots:
void onOK(); ///< Slot for the OK button.

View File

@ -35,7 +35,7 @@ UserTable::UserTable(QObject *parent)
/// \return The number of rows in the table.
//****************************************************************************************************************************************************
int UserTable::rowCount(QModelIndex const &) const {
return static_cast<int>(users_.size());
return users_.size();
}
@ -111,7 +111,7 @@ QVariant UserTable::headerData(int section, Qt::Orientation orientation, int rol
/// \param[in] user The user to add.
//****************************************************************************************************************************************************
void UserTable::append(SPUser const &user) {
qint32 const count = static_cast<int>(users_.size());
qint32 const count = users_.size();
this->beginInsertRows(QModelIndex(), count, count);
users_.append(user);
this->endInsertRows();
@ -122,7 +122,7 @@ void UserTable::append(SPUser const &user) {
/// \return The number of users in the table.
//****************************************************************************************************************************************************
qint32 UserTable::userCount() const {
return static_cast<qint32>(users_.count());
return users_.count();
}
@ -141,7 +141,7 @@ bridgepp::SPUser UserTable::userAtIndex(qint32 index) {
/// \return A null pointer if the user is not in the list.
//****************************************************************************************************************************************************
bridgepp::SPUser UserTable::userWithID(QString const &userID) {
QList<SPUser>::const_iterator const it = std::find_if(users_.constBegin(), users_.constEnd(), [&userID](SPUser const &user) -> bool {
QList<SPUser>::const_iterator it = std::find_if(users_.constBegin(), users_.constEnd(), [&userID](SPUser const &user) -> bool {
return user->id() == userID;
});
@ -155,7 +155,7 @@ bridgepp::SPUser UserTable::userWithID(QString const &userID) {
/// \return A null pointer if the user is not in the list.
//****************************************************************************************************************************************************
bridgepp::SPUser UserTable::userWithUsernameOrEmail(QString const &username) {
QList<SPUser>::const_iterator const it = std::find_if(users_.constBegin(), users_.constEnd(), [&username](SPUser const &user) -> bool {
QList<SPUser>::const_iterator it = std::find_if(users_.constBegin(), users_.constEnd(), [&username](SPUser const &user) -> bool {
if (user->username().compare(username, Qt::CaseInsensitive) == 0) {
return true;
}
@ -172,7 +172,7 @@ bridgepp::SPUser UserTable::userWithUsernameOrEmail(QString const &username) {
/// \return -1 if the user could not be found.
//****************************************************************************************************************************************************
qint32 UserTable::indexOfUser(QString const &userID) {
QList<SPUser>::const_iterator const it = std::find_if(users_.constBegin(), users_.constEnd(), [&userID](SPUser const &user) -> bool {
QList<SPUser>::const_iterator it = std::find_if(users_.constBegin(), users_.constEnd(), [&userID](SPUser const &user) -> bool {
return user->id() == userID;
});

View File

@ -33,7 +33,7 @@ public: // member functions.
explicit UserTable(QObject *parent); ///< Default constructor.
UserTable(UserTable const &) = delete; ///< Disabled copy-constructor.
UserTable(UserTable &&) = delete; ///< Disabled assignment copy-constructor.
~UserTable() override = default; ///< Destructor.
~UserTable() = default; ///< Destructor.
UserTable &operator=(UserTable const &) = delete; ///< Disabled assignment operator.
UserTable &operator=(UserTable &&) = delete; ///< Disabled move assignment operator.
qint32 userCount() const; ///< Return the number of users in the table.

View File

@ -71,7 +71,7 @@ int main(int argc, char **argv) {
app().log().error(message);
qApp->exit(EXIT_FAILURE);
});
UPOverseer const overseer = std::make_unique<Overseer>(serverWorker, nullptr);
UPOverseer overseer = std::make_unique<Overseer>(serverWorker, nullptr);
overseer->startWorker(true);
qint32 const exitCode = QApplication::exec();

View File

@ -41,7 +41,6 @@ namespace {
QString const bugReportFile = ":qml/Resources/bug_report_flow.json";
QString const bridgeKBUrl = "https://proton.me/support/bridge"; ///< The URL for the root of the bridge knowledge base.
}
@ -279,40 +278,6 @@ 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::openExternalLink(QString const &url) {
HANDLE_EXCEPTION(
QString const u = url.isEmpty() ? bridgeKBUrl : url;
QDesktopServices::openUrl(u);
emit notifyExternalLinkClicked(u);
)
}
//****************************************************************************************************************************************************
/// \param[in] categoryID The ID of the bug report category.
//****************************************************************************************************************************************************
void QMLBackend::requestKnowledgeBaseSuggestions(qint8 categoryID) const {
HANDLE_EXCEPTION(
app().grpc().requestKnowledgeBaseSuggestions(reportFlow_.collectUserInput(categoryID));
)
}
//****************************************************************************************************************************************************
/// \return The value for the 'showOnStartup' property.
//****************************************************************************************************************************************************
@ -976,15 +941,6 @@ void QMLBackend::reportBug(QString const &category, QString const &description,
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void QMLBackend::installTLSCertificate() {
HANDLE_EXCEPTION(
app().grpc().installTLSCertificate();
)
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
@ -1072,9 +1028,9 @@ void QMLBackend::notifyAutoconfigClicked(QString const &client) const {
//****************************************************************************************************************************************************
/// \param[in] article The url of the KB article.
//****************************************************************************************************************************************************
void QMLBackend::notifyExternalLinkClicked(QString const &article) const {
void QMLBackend::notifyKBArticleClicked(QString const &article) const {
HANDLE_EXCEPTION(
app().grpc().externalLinkClicked(article);
app().grpc().KBArticleClicked(article);
)
}
@ -1311,14 +1267,12 @@ 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"); });
connect(client, &GRPCClient::knowledgeBasSuggestionsReceived, this, &QMLBackend::receivedKnowledgeBaseSuggestions);
// cache events
connect(client, &GRPCClient::diskCacheUnavailable, this, &QMLBackend::diskCacheUnavailable);
connect(client, &GRPCClient::cantMoveDiskCache, this, &QMLBackend::cantMoveDiskCache);
connect(client, &GRPCClient::diskFull, this, &QMLBackend::diskFull);
connect(client, &GRPCClient::diskCachePathChanged, this, &QMLBackend::diskCachePathChanged);
connect(client, &GRPCClient::diskCachePathChangeFinished, this, &QMLBackend::diskCachePathChangeFinished);
@ -1363,6 +1317,7 @@ void QMLBackend::connectGrpcEvents() {
connect(client, &GRPCClient::rebuildKeychain, this, &QMLBackend::notifyRebuildKeychain);
// mail events
connect(client, &GRPCClient::noActiveKeyForRecipient, this, &QMLBackend::noActiveKeyForRecipient);
connect(client, &GRPCClient::addressChanged, this, &QMLBackend::addressChanged);
connect(client, &GRPCClient::addressChangedLogout, this, &QMLBackend::addressChangedLogout);
connect(client, &GRPCClient::apiCertIssue, this, &QMLBackend::apiCertIssue);

View File

@ -64,9 +64,6 @@ 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 openExternalLink(QString const & url = QString()); ///< Open a knowledge base article.
Q_INVOKABLE void requestKnowledgeBaseSuggestions(qint8 categoryID) const; ///< Request knowledgebase article suggestions.
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)
@ -198,7 +195,6 @@ 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.
@ -206,7 +202,7 @@ public slots: // slot for signals received from QML -> To be forwarded to Bridge
void sendBadEventUserFeedback(QString const &userID, bool doResync); ///< Slot the providing user feedback for a bad event.
void notifyReportBugClicked() const; ///< Slot for the ReportBugClicked gRPC event.
void notifyAutoconfigClicked(QString const &client) const; ///< Slot for gAutoconfigClicked gRPC event.
void notifyExternalLinkClicked(QString const &article) const; ///< Slot for KBArticleClicked gRPC event.
void notifyKBArticleClicked(QString const &article) const; ///< Slot for KBArticleClicked gRPC event.
public slots: // slots for functions that need to be processed locally.
void setNormalTrayIcon(); ///< Set the tray icon to normal.
@ -225,15 +221,17 @@ public slots: // slot for signals received from gRPC that need transformation in
signals: // Signals received from the Go backend, to be forwarded to QML
void toggleAutostartFinished(); ///< Signal for the 'toggleAutostartFinished' gRPC stream event.
void diskCacheUnavailable(); ///< Signal for the 'diskCacheUnavailable' gRPC stream event.
void cantMoveDiskCache(); ///< Signal for the 'cantMoveDiskCache' gRPC stream event.
void diskCachePathChangeFinished(); ///< Signal for the 'diskCachePathChangeFinished' gRPC stream event.
void diskFull(); ///< Signal for the 'diskFull' gRPC stream event.
void loginUsernamePasswordError(QString const &errorMsg); ///< Signal for the 'loginUsernamePasswordError' gRPC stream event.
void loginFreeUserError(); ///< Signal for the 'loginFreeUserError' gRPC stream event.
void loginConnectionError(QString const &errorMsg); ///< Signal for the 'loginConnectionError' gRPC stream event.
void login2FARequested(QString const &username); ///< Signal for the 'login2FARequested' gRPC stream event.
void login2FAError(QString const &errorMsg); ///< Signal for the 'login2FAError' gRPC stream event.
void login2FAErrorAbort(QString const &errorMsg); ///< Signal for the 'login2FAErrorAbort' gRPC stream event.
void login2PasswordRequested(QString const &username); ///< Signal for the 'login2PasswordRequested' gRPC stream event.
void login2PasswordRequested(); ///< Signal for the 'login2PasswordRequested' gRPC stream event.
void login2PasswordError(QString const &errorMsg); ///< Signal for the 'login2PasswordError' gRPC stream event.
void login2PasswordErrorAbort(QString const &errorMsg); ///< Signal for the 'login2PasswordErrorAbort' gRPC stream event.
void loginFinished(int index, bool wasSignedOut); ///< Signal for the 'loginFinished' gRPC stream event.
@ -257,6 +255,7 @@ signals: // Signals received from the Go backend, to be forwarded to QML
void changeKeychainFinished(); ///< Signal for the 'changeKeychainFinished' gRPC stream event.
void notifyHasNoKeychain(); ///< Signal for the 'notifyHasNoKeychain' gRPC stream event.
void notifyRebuildKeychain(); ///< Signal for the 'notifyRebuildKeychain' gRPC stream event.
void noActiveKeyForRecipient(QString const &email); ///< Signal for the 'noActiveKeyForRecipient' gRPC stream event.
void addressChanged(QString const &address); ///< Signal for the 'addressChanged' gRPC stream event.
void addressChangedLogout(QString const &address); ///< Signal for the 'addressChangedLogout' gRPC stream event.
void apiCertIssue(); ///< Signal for the 'apiCertIssue' gRPC stream event.
@ -269,17 +268,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 'showSettings' event (from the context menu).
void showSettings(); ///< Signal for the 'showHelp' event (from the context menu).
void selectUser(QString const& userID, bool forceShowWindow); ///< Signal emitted in order to selected a user with a given ID in the list.
void genericError(QString const &title, QString const &description); ///< Signal for the 'genericError' gRPC stream event.
void imapLoginWhileSignedOut(QString const& username); ///< Signal for the notification of IMAP login attempt on a signed out account.
void receivedKnowledgeBaseSuggestions(QList<bridgepp::KnowledgeBaseSuggestion> const& suggestions); ///< Signal for the reception of knowledgebase article suggestions.
// This signal is emitted when an exception is intercepted is calls triggered by QML. QML engine would intercept the exception otherwise.
void fatalError(bridgepp::Exception const& e) const; ///< Signal emitted when an fatal error occurs.

View File

@ -5,6 +5,12 @@
<file>qml/AccountView.qml</file>
<file>qml/Banner.qml</file>
<file>qml/Bridge.qml</file>
<file>qml/bridgeqml.qmlproject</file>
<file>qml/BugCategoryView.qml</file>
<file>qml/BugQuestionView.qml</file>
<file>qml/BugReportFlow.qml</file>
<file>qml/BugReportView.qml</file>
<file>qml/CategoryItem.qml</file>
<file>qml/Configuration.qml</file>
<file>qml/ConfigurationItem.qml</file>
<file>qml/ContentWrapper.qml</file>
@ -14,11 +20,9 @@
<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>
@ -45,18 +49,13 @@
<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>
@ -76,7 +75,6 @@
<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>
@ -84,12 +82,6 @@
<file>qml/Notifications/Notifications.qml</file>
<file>qml/Notifications/qmldir</file>
<file>qml/PortSettings.qml</file>
<file>qml/BugReport/BugCategoryView.qml</file>
<file>qml/BugReport/BugQuestionView.qml</file>
<file>qml/BugReport/BugReportFlow.qml</file>
<file>qml/BugReport/BugReportView.qml</file>
<file>qml/BugReport/CategoryItem.qml</file>
<file>qml/BugReport/QuestionItem.qml</file>
<file>qml/Proton/Action.qml</file>
<file>qml/Proton/ApplicationWindow.qml</file>
<file>qml/Proton/Button.qml</file>
@ -97,9 +89,7 @@
<file>qml/Proton/ColorScheme.qml</file>
<file>qml/Proton/ComboBox.qml</file>
<file>qml/Proton/Dialog.qml</file>
<file>qml/Proton/InfoTooltip.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>
@ -110,26 +100,15 @@
<file>qml/Proton/TextArea.qml</file>
<file>qml/Proton/TextField.qml</file>
<file>qml/Proton/Toggle.qml</file>
<file>qml/QuestionItem.qml</file>
<file>qml/Resources/bug_report_flow.json</file>
<file>qml/Resources/Help/Template.html</file>
<file>qml/Resources/Help/WhyBridge.html</file>
<file>qml/Resources/Help/WhyCertificate.html</file>
<file>qml/Resources/Help/WhyProfileWarning.html</file>
<file>qml/SettingsItem.qml</file>
<file>qml/SettingsView.qml</file>
<file>qml/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/SetupGuide.qml</file>
<file>qml/SignIn.qml</file>
<file>qml/ConnectionModeSettings.qml</file>
<file>qml/SplashScreen.qml</file>
<file>qml/Status.qml</file>
<file>qml/WelcomeGuide.qml</file>
</qresource>
</RCC>

View File

@ -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 syncStarted event for unknown userID %1").arg(userID));
app().log().error(QString("Received onSyncStarted 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 syncFinished event for unknown userID %1").arg(userID));
app().log().error(QString("Received onSyncFinished 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 syncProgress event for unknown userID %1").arg(userID));
app().log().error(QString("Received onSyncFinished event for unknown userID %1").arg(userID));
return;
}
users_[index]->setSyncProgress(progress);

View File

@ -415,11 +415,7 @@ int main(int argc, char *argv[]) {
}
catch (Exception const &e) {
sentry_uuid_s const uuid = reportSentryException("Exception occurred during main", e);
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);
QMessageBox::critical(nullptr, "Error", e.qwhat());
QTextStream(stderr) << "reportID: " << QByteArray(uuid.bytes, 16).toHex() << " Captured exception :" << e.detailedWhat() << "\n";
return EXIT_FAILURE;
}

View File

@ -22,7 +22,7 @@ Item {
LargeView
}
property var _spacing: 12
property var _spacing: 12 * ProtonStyle.px
property ColorScheme colorScheme
property color progressColor: {
if (!root.enabled)
@ -154,7 +154,7 @@ Item {
}
}
Item {
implicitHeight: root.type === AccountDelegate.LargeView ? 6 : 0
implicitHeight: root.type === AccountDelegate.LargeView ? 6 * ProtonStyle.px : 0
}
RowLayout {
spacing: 0
@ -222,15 +222,15 @@ Item {
}
}
Item {
implicitHeight: root.type === AccountDelegate.LargeView ? 3 : 0
implicitHeight: root.type === AccountDelegate.LargeView ? 3 * ProtonStyle.px : 0
}
Rectangle {
id: progress_bar
color: root.colorScheme.border_weak
height: 4
height: 4 * ProtonStyle.px
radius: ProtonStyle.progress_bar_radius
visible: root.user ? root.type === AccountDelegate.LargeView : false
width: 140
width: 140 * ProtonStyle.px
Rectangle {
id: progress_bar_filled

View File

@ -23,14 +23,13 @@ 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 showClientConfigurator(var user, string address, bool justLoggedIn)
signal showLogin(var username)
signal showSetupGuide(var user, string address)
signal showSignIn
Rectangle {
anchors.fill: parent
@ -64,7 +63,7 @@ Item {
// account delegate with action buttons
Layout.fillWidth: true
Layout.topMargin: _topMargin
spacing: _buttonSpacing
AccountDelegate {
Layout.fillWidth: true
colorScheme: root.colorScheme
@ -93,9 +92,9 @@ Item {
visible: root.user ? (root.user.state === EUserState.SignedOut) : false
onClicked: {
if (user) {
root.showLogin(user.primaryEmailOrUsername());
}
if (!root.user)
return;
root.showSignIn();
}
}
Button {
@ -119,18 +118,18 @@ Item {
}
SettingsItem {
Layout.fillWidth: true
actionText: qsTr("Configure email client")
actionText: qsTr("Configure")
colorScheme: root.colorScheme
description: qsTr("Using the mailbox details below (re)configure your client.")
showSeparator: splitMode.visible
text: qsTr("Email clients")
type: SettingsItem.PrimaryButton
visible: _connected && ((!root.user.splitMode) || (root.user.addresses.length === 1))
type: SettingsItem.Button
visible: _connected && (!root.user.splitMode) || (root.user.addresses.length === 1)
onClicked: {
if (!root.user)
return;
root.showClientConfigurator(root.user, user.addresses[0], false);
root.showSetupGuide(root.user, user.addresses[0]);
}
}
SettingsItem {
@ -166,13 +165,13 @@ Item {
}
Button {
colorScheme: root.colorScheme
secondary: false
text: qsTr("Configure email client")
secondary: true
text: qsTr("Configure")
onClicked: {
if (!root.user)
return;
root.showClientConfigurator(root.user, addressSelector.displayText, false);
root.showSetupGuide(root.user, addressSelector.displayText);
}
}
}

View File

@ -84,7 +84,6 @@ Popup {
anchors.topMargin: 14
spacing: 8
ColorImage {
Layout.preferredHeight: 24
Layout.preferredWidth: 24
@ -109,30 +108,15 @@ Popup {
sourceSize.width: 24
width: 24
}
ColumnLayout {
Layout.alignment: Qt.AlignTop
Label {
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
Layout.leftMargin: 16
Label {
id: messageLabel
Layout.alignment: Qt.AlignTop
Layout.fillWidth: true
color: root.colorScheme.text_invert
colorScheme: root.colorScheme
text: root.notification ? root.notification.description : ""
wrapMode: Text.WordWrap
}
LinkLabel {
Layout.alignment: Qt.AlignTop | Qt.AlignLeft
Layout.fillWidth: true
colorScheme: root.colorScheme
color: messageLabel.color
external: true
link: root.notification ? root.notification.linkUrl : ""
text: root.notification ? root.notification.linkText : ""
visible: root.notification && root.notification.linkUrl.length > 0
}
}
}
}
Rectangle {

View File

@ -34,9 +34,13 @@ QtObject {
function onColorSchemeNameChanged(scheme) {
root.setColorScheme();
}
function onDiskCacheUnavailable() {
mainWindow.showAndRise();
}
function onHideMainWindow() {
mainWindow.hide();
}
target: Backend
}
}

View File

@ -14,7 +14,6 @@ import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Proton
import ".."
SettingsView {
id: root

View File

@ -14,7 +14,6 @@ import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Proton
import ".."
SettingsView {
id: root

View File

@ -1,201 +0,0 @@
// Copyright (c) 2023 Proton AG
// This file is part of Proton Mail Bridge.
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Proton
import ".."
SettingsView {
id: root
property var selectedAddress
property var categoryId: -1
property string category: Backend.getBugCategory(root.categoryId)
property var suggestions: null
signal bugReportWasSent
function isValidEmail(text) {
const reEmail = /^[^@]+@[^@]+\.[A-Za-z]+\s*$/;
return reEmail.test(text);
}
function setCategoryId(catId) {
root.categoryId = catId;
}
function setDefaultValue() {
description.text = Backend.collectAnswers(root.categoryId);
address.text = root.selectedAddress;
emailClient.text = Backend.currentEmailClient;
includeLogs.checked = true;
}
function submit() {
sendButton.loading = true;
Backend.reportBug(root.category, description.text, address.text, emailClient.text, includeLogs.checked);
}
fillHeight: true
onVisibleChanged: {
root.setDefaultValue();
}
ColumnLayout {
spacing: 32
Label {
colorScheme: root.colorScheme
text: qsTr("Send report")
type: Label.Heading
}
TextArea {
id: description
KeyNavigation.priority: KeyNavigation.BeforeItem
KeyNavigation.tab: address
Layout.fillHeight: true
Layout.fillWidth: true
Layout.minimumHeight: heightForLinesVisible(4)
colorScheme: root.colorScheme
textFormat: Text.MarkdownText
// set implicitHeight to explicit height because se don't
// want TextArea implicitHeight (which is height of all text)
// to be considered in SettingsView internal scroll view
implicitHeight: height
label: "Your answers to: " + qsTr(root.category);
readOnly: true
}
ColumnLayout {
id: suggestionBox
visible: suggestions && suggestions.length > 0
spacing: 8
RowLayout {
Label {
colorScheme: root.colorScheme
text: qsTr("We believe these links may help you solve your problem")
type: Label.Body_semibold
}
InfoTooltip {
colorScheme: root.colorScheme
text: qsTr("The links will open in an external browser. If you cancel the report, your input will be preserved until you restart Bridge.")
Layout.bottomMargin: -4
}
}
Repeater {
model: suggestions
LinkLabel {
required property var modelData
colorScheme: root.colorScheme
text: modelData.title
link: modelData.url
external: true
}
}
}
RowLayout {
spacing: 12
TextField {
id: address
Layout.preferredWidth: 1
Layout.fillWidth: true
colorScheme: root.colorScheme
label: qsTr("Your contact email")
placeholderText: qsTr("e.g. jane.doe@protonmail.com")
validator: function (str) {
if (!isValidEmail(str)) {
return qsTr("Enter valid email address");
}
}
}
TextField {
id: emailClient
Layout.preferredWidth: 1
Layout.fillWidth: true
colorScheme: root.colorScheme
label: qsTr("Your email client (including version)")
placeholderText: qsTr("e.g. Apple Mail 14.0")
validator: function (str) {
if (str.length === 0) {
return qsTr("Enter an email client name and version");
}
}
}
}
RowLayout {
spacing: 12
CheckBox {
id: includeLogs
checked: true
colorScheme: root.colorScheme
text: qsTr("Include my recent logs")
}
Button {
colorScheme: root.colorScheme
secondary: true
text: qsTr("View logs")
onClicked: Backend.openExternalLink(Backend.logsPath)
}
Label {
Layout.fillWidth: true
verticalAlignment: Qt.AlignVCenter
colorScheme: root.colorScheme
type: Label.Caption
color: root.colorScheme.text_weak
text: qsTr("Reports are not end-to-end encrypted, please do not send any sensitive information.")
wrapMode: Text.WordWrap
}
}
Button {
id: sendButton
colorScheme: root.colorScheme
enabled: !loading
text: qsTr("Send")
onClicked: {
description.validate();
address.validate();
emailClient.validate();
if (description.error || address.error || emailClient.error) {
return;
}
submit();
}
Connections {
function onBugReportSendSuccess() {
root.bugReportWasSent();
}
function onReportBugFinished() {
sendButton.loading = false;
}
function onReceivedKnowledgeBaseSuggestions(suggestions) {
root.suggestions = suggestions
}
target: Backend
}
}
}
}

View File

@ -15,7 +15,6 @@ import QtQuick.Layouts
import QtQuick.Controls
import Proton
import Notifications
import ".."
Item {
id: root
@ -36,7 +35,7 @@ Item {
}
function showBugQuestion() {
bugQuestion.setCategoryId(root.categoryId);
bugQuestion.positionViewAtBeginning();
bugQuestion.positionViewAtBegining();
bugReportFlow.currentIndex = 1;
}
function showBugReport() {
@ -78,7 +77,6 @@ Item {
root.showBugCategory();
}
onQuestionAnswered: {
Backend.requestKnowledgeBaseSuggestions(categoryId);
root.showBugReport();
}
}

View File

@ -0,0 +1,161 @@
// Copyright (c) 2023 Proton AG
// This file is part of Proton Mail Bridge.
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Proton
SettingsView {
id: root
property var selectedAddress
property var categoryId:-1
property string category: Backend.getBugCategory(root.categoryId)
signal bugReportWasSent
function isValidEmail(text) {
const reEmail = /^[^@]+@[^@]+\.[A-Za-z]+\s*$/;
return reEmail.test(text);
}
function setCategoryId(catId) {
root.categoryId = catId;
}
function setDefaultValue() {
description.text = Backend.collectAnswers(root.categoryId);
address.text = root.selectedAddress;
emailClient.text = Backend.currentEmailClient;
includeLogs.checked = true;
}
function submit() {
sendButton.loading = true;
Backend.reportBug(root.category, description.text, address.text, emailClient.text, includeLogs.checked);
}
fillHeight: true
onVisibleChanged: {
root.setDefaultValue();
}
Label {
colorScheme: root.colorScheme
text: qsTr("Send report")
type: Label.Heading
}
TextArea {
id: description
KeyNavigation.priority: KeyNavigation.BeforeItem
KeyNavigation.tab: address
Layout.fillHeight: true
Layout.fillWidth: true
Layout.minimumHeight: heightForLinesVisible(4)
colorScheme: root.colorScheme
textFormat: Text.MarkdownText
// set implicitHeight to explicit height because se don't
// want TextArea implicitHeight (which is height of all text)
// to be considered in SettingsView internal scroll view
implicitHeight: height
label: "Your answers to: " + qsTr(root.category);
readOnly : true
}
TextField {
id: address
Layout.fillWidth: true
colorScheme: root.colorScheme
label: qsTr("Your contact email")
placeholderText: qsTr("e.g. jane.doe@protonmail.com")
validator: function (str) {
if (!isValidEmail(str)) {
return qsTr("Enter valid email address");
}
return;
}
}
TextField {
id: emailClient
Layout.fillWidth: true
colorScheme: root.colorScheme
label: qsTr("Your email client (including version)")
placeholderText: qsTr("e.g. Apple Mail 14.0")
validator: function (str) {
if (str.length === 0) {
return qsTr("Enter an email client name and version");
}
return;
}
}
RowLayout {
CheckBox {
id: includeLogs
checked: true
colorScheme: root.colorScheme
text: qsTr("Include my recent logs")
}
Button {
Layout.leftMargin: 12
colorScheme: root.colorScheme
secondary: true
text: qsTr("View logs")
onClicked: Qt.openUrlExternally(Backend.logsPath)
}
}
TextEdit {
Layout.fillWidth: true
color: root.colorScheme.text_weak
font.family: ProtonStyle.font_family
font.letterSpacing: ProtonStyle.caption_letter_spacing
font.pixelSize: ProtonStyle.caption_font_size
font.weight: ProtonStyle.fontWeight_400
readOnly: true
selectByMouse: true
selectedTextColor: root.colorScheme.text_invert
// No way to set lineHeight: ProtonStyle.caption_line_height
selectionColor: root.colorScheme.interaction_norm
text: qsTr("Reports are not end-to-end encrypted, please do not send any sensitive information.")
wrapMode: Text.WordWrap
}
Button {
id: sendButton
colorScheme: root.colorScheme
enabled: !loading
text: qsTr("Send")
onClicked: {
description.validate();
address.validate();
emailClient.validate();
if (description.error || address.error || emailClient.error) {
return;
}
submit();
}
Connections {
function onBugReportSendSuccess() {
root.bugReportWasSent();
}
function onReportBugFinished() {
sendButton.loading = false;
}
target: Backend
}
}
}

View File

@ -14,7 +14,6 @@ import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Proton
import ".."
Item {
id: root
@ -33,7 +32,7 @@ Item {
RowLayout {
anchors.fill: parent
spacing: 12
spacing: 16
Label {
id: mainLabel
@ -45,13 +44,42 @@ Item {
wrapMode: Text.WordWrap
}
InfoTooltip {
ColorImage {
id: infoImage
Layout.alignment: Qt.AlignVCenter
Layout.topMargin: 4
Layout.bottomMargin: root._bottomMargin
colorScheme: root.colorScheme
color: root.colorScheme.interaction_norm
height: 21
width: 21
source: "/qml/icons/ic-info-circle.svg"
sourceSize.height: 21
sourceSize.width: 21
visible: root.hint !== ""
MouseArea {
id: imageArea
anchors.fill: infoImage
hoverEnabled: true
}
ToolTip {
id: toolTipinfo
text: root.hint
size: 16
visible: imageArea.containsMouse
implicitWidth: Math.min(400, tooltipText.implicitWidth)
background: Rectangle {
radius: 4
border.color: root.colorScheme.border_weak
color: root.colorScheme.background_weak
}
contentItem: Text {
id: tooltipText
color: root.colorScheme.text_hint
text: toolTipinfo.text
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
}
// fill height so the footer label will always be attached to the bottom

View File

@ -21,7 +21,6 @@ Rectangle {
property int _margin: 24
property ColorScheme colorScheme
property bool highlightPassword
property string hostname
property string password
property string port
@ -69,8 +68,7 @@ Rectangle {
}
ConfigurationItem {
colorScheme: root.colorScheme
label: highlightPassword ? qsTr("Use this password") : qsTr("Password")
labelColor: highlightPassword ? colorScheme.signal_warning_active : colorScheme.text_norm
label: qsTr("Password")
value: root.password
}
ConfigurationItem {

View File

@ -21,7 +21,6 @@ Item {
property var colorScheme
property string label
property string labelColor: root.colorScheme.text_norm
property string value
Layout.fillWidth: true
@ -36,10 +35,9 @@ Item {
ColumnLayout {
Label {
color: labelColor
colorScheme: root.colorScheme
text: root.label
type: Label.Body_semibold
type: Label.Body
}
TextEdit {
id: valueText

View File

@ -15,7 +15,6 @@ import QtQuick.Layouts
import QtQuick.Controls
import Proton
import Notifications
import "BugReport"
Item {
id: root
@ -25,8 +24,7 @@ Item {
signal closeWindow
signal quitBridge
signal showClientConfigurator(var user, string address, bool justLoggedIn)
signal showLogin(var username)
signal showSetupGuide(var user, string address)
function selectUser(userID) {
const users = Backend.users;
@ -37,14 +35,11 @@ Item {
}
accounts.currentIndex = i;
if (user.state === EUserState.SignedOut)
showLogin(user.primaryEmailOrUsername());
showSignIn(user.primaryEmailOrUsername());
return;
}
console.error("User with ID ", userID, " was not found in the account list");
}
function showBugReport() {
rightContent.showBugReport();
}
function showHelp() {
rightContent.showHelpView();
}
@ -54,9 +49,9 @@ Item {
function showSettings() {
rightContent.showGeneralSettings();
}
function hasAccount() {
return Backend.users.count > 0
function showSignIn(username) {
signIn.username = username;
rightContent.showSignIn();
}
RowLayout {
@ -195,41 +190,6 @@ 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
@ -246,7 +206,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
@ -273,7 +233,8 @@ Item {
if (user.state !== EUserState.SignedOut) {
rightContent.showAccount();
} else {
showLogin(user.primaryEmailOrUsername());
signIn.username = user.primaryEmailOrUsername();
rightContent.showSignIn();
}
}
}
@ -321,7 +282,8 @@ Item {
width: 36
onClicked: {
root.showLogin("");
signIn.username = "";
rightContent.showSignIn();
}
}
}
@ -361,19 +323,15 @@ Item {
function showPortSettings() {
rightContent.currentIndex = 4;
}
function showSignIn() {
rightContent.currentIndex = 1;
signIn.focus = true;
}
anchors.fill: parent
StackLayout {
// 0
currentIndex: hasAccount() ? 1 : 0
NoAccountView {
colorScheme: root.colorScheme
onStartSetup: {
root.showLogin("")
}
}
AccountView {
// 0
colorScheme: root.colorScheme
notifications: root.notifications
user: {
@ -384,18 +342,45 @@ Item {
return Backend.users.get(accounts.currentIndex);
}
onShowClientConfigurator: function (user, address, justLoggedIn) {
root.showClientConfigurator(user, address, justLoggedIn);
onShowSetupGuide: function (user, address) {
root.showSetupGuide(user, address);
}
onShowLogin: function (username) {
root.showLogin(username);
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
colorScheme: root.colorScheme
horizontalPadding: 8
icon.source: "/qml/icons/ic-arrow-left.svg"
secondary: true
onClicked: {
signIn.abort();
rightContent.showAccount();
}
Rectangle {
}
SignIn {
id: signIn
Layout.bottomMargin: 68
Layout.fillHeight: true
Layout.fillWidth: true
color: "#ff9900"
Layout.leftMargin: 80 - backButton.width - 18
Layout.preferredWidth: 320
Layout.rightMargin: 80
Layout.topMargin: 68
colorScheme: root.colorScheme
}
}
GeneralSettings {
// 2

View File

@ -36,7 +36,8 @@ SettingsView {
type: SettingsItem.PrimaryButton
onClicked: {
Backend.openExternalLink();
Backend.notifyKBArticleClicked("https://proton.me/support/bridge");
Qt.openUrlExternally("https://proton.me/support/bridge");
}
}
SettingsItem {
@ -70,7 +71,7 @@ SettingsView {
text: qsTr("Logs")
type: SettingsItem.Button
onClicked: Backend.openExternalLink(Backend.logsPath)
onClicked: Qt.openUrlExternally(Backend.logsPath)
}
SettingsItem {
id: reportBug
@ -103,7 +104,7 @@ SettingsView {
type: Label.Caption
onLinkActivated: function (link) {
Backend.openExternalLink(link)
Qt.openUrlExternally(link);
}
}
}

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