Compare commits

..

66 Commits

Author SHA1 Message Date
4951244400 chore: Xikou Bridge 3.8.2 changelog. 2024-02-02 19:32:58 +01:00
d65d6ee2e5 fix(GODT-3235): use release xikou for trigger build 2024-02-02 18:37:38 +01:00
097d6f86d3 fix(GODT-3235): update bridge update key 2024-02-02 17:34:32 +01:00
5c69af4418 chore: Xikou Bridge 3.8.1 changelog. 2023-12-11 11:49:01 +01:00
416f696863 feat(GODT-3121): added options to kb-tester CLI tool. 2023-12-08 11:04:48 +01:00
789c1cc816 feat(GODT-3121): kb suggestion first version of complete list. 2023-12-08 10:01:38 +01:00
58736dd254 chore: keep nighlty-job log as artifact. 2023-12-08 09:31:57 +01:00
a057138880 feat(GODT-3121): KB suggestion test tool now support multi-line input. 2023-12-07 10:48:33 +01:00
76087f1749 feat(GODT-3121): minimalist CLI tool to test KB suggestions. 2023-12-07 09:36:47 +01:00
83935f3a03 feat(GODT-3121): refactored retrieval kb article index lookup. 2023-12-07 09:35:05 +01:00
b93c10ad47 feat(GODT-3121): adds KB suggestion scoring. 2023-12-07 09:35:05 +01:00
3309137b80 feat(GODT-3121): forward user input to bridge. 2023-12-07 09:35:05 +01:00
88c4737ba4 feat(GODT-3121): reuse InfoTooltip. 2023-12-07 09:35:05 +01:00
e5db9b1ccc feat(GODT-3121): added display of bug report user input in bridge-gui-tester. 2023-12-07 09:35:05 +01:00
6e2e622a2f feat(GODT-3121): added tooltip for KB suggestions. 2023-12-07 09:35:05 +01:00
3a66063938 feat(GODT-3121): change log level of click on external link. 2023-12-07 09:35:05 +01:00
120ddbbcbb feat(GODT-3121): finalize UI for KB suggestions. 2023-12-07 09:35:05 +01:00
39b31abef8 feat(GODT-3121): fix issues reported by the resharper C++ engine. 2023-12-07 09:35:05 +01:00
ebeca394c7 feat(GODT-3121): implement suggestion list in bridge-gui. 2023-12-07 09:35:05 +01:00
2206cb3f12 feat(GODT-3121): suggestions links are in the final bug report page. 2023-12-07 09:35:05 +01:00
cfd07cf893 feat(GODT-3121): suggestions are transferred to QML. 2023-12-07 09:35:05 +01:00
2e2648fcd5 feat(GODT-3121): QML request suggestions. 2023-12-07 09:35:05 +01:00
3070912416 feat(GODT-3121): added gRPC call and event for KB suggestions. 2023-12-07 09:35:05 +01:00
51722eb1a4 feat(GODT-3121): introduced knowledgebase package. 2023-12-07 09:35:05 +01:00
5950eff083 chore(GODT-3160): silence vuln 2023-12-07 08:15:10 +01:00
5c67cc2e76 fix(GODT-3153): Do not take into account full address when hasing messages. 2023-12-06 16:14:38 +00:00
01db488caa feat(GODT-2001): add govulncheck to scan for vulnerabilities. 2023-12-06 15:29:21 +01:00
6cbef1d786 test: Improve TestMetadata_JobCorrectlyFinishesAfterCancel 2023-12-04 13:48:44 +00:00
dd9a819ea2 chore: Xikou Bridge 3.8.0 changelog. 2023-12-04 14:42:09 +01:00
401e56224b fix(GODT-3142): pass br tag if available 2023-12-04 14:14:52 +01:00
1ee52f0f55 fix(GODT-3151): Fix feature test with non modified HTML part. 2023-12-04 13:11:33 +01:00
9efaf9184c fix(GODT-3151): Only modify HTML Meta content if UTF-8 charset override is needed. 2023-12-04 11:45:47 +01:00
a8f270405f chore: Xikou Bridge 3.8.0 changelog. 2023-11-30 13:59:58 +01:00
38606888fe fix(GODT-2851): Add empty text part if no text part when importing multipart. 2023-11-30 11:03:31 +01:00
1b22c32ef9 fix(GODT-3102): Distinguish Vault Decryption from Serialization Errors
Rather than returning whether the vault was corrupt or not return the
error which caused the vault to be considered as corrupt.
2023-11-30 08:31:14 +01:00
7a1c7e8743 fix(GODT-3124): Handling of sync child jobs
Improve the handling of sync child jobs to ensure it behaves correctly
in all scenarios.

The sync service now uses a isolated context to avoid all the pipeline
stages shutting down before all the sync tasks have had the opportunity
to run their course.

The job waiter now immediately starts with a counter of 1 and waits
until all the child and the parent job finish before considering the
work to be finished.

Finally, we also handle the case where a sync job can't be queued
because the calling context has been cancelled.
2023-11-29 18:04:22 +00:00
9449177553 fix(GODT-3148): bump go-sysinfo to get rid of linker warning on macOS Sonoma. 2023-11-29 14:49:13 +01:00
bbcedc655a fix(GODT-3124): Flaky tests
Bump GPA to include fix for flacky tests.

https://github.com/ProtonMail/go-proton-api/pull/137
2023-11-29 12:02:06 +01:00
40c97ab19e fix(GODT-3022): Handle multipart/related on fake server. 2023-11-28 15:07:26 +00:00
50dd046b82 fix(GODT-3133): Fix GetSystemLanguage 2023-11-28 09:32:40 +01:00
7d13c99710 fix(GODT-3124): Race condition in sync task waiter
Fix incorrect use of `sync.WaitGroup` use to wait on sync jobs that
fail. After calling `WaitGroup.Wait()` it is advised to call
`WaitGroup.Add` until the existing counter has reached 0.

The code has been updated with a different mechanism that achieves the
same behavior which was previously available.
2023-11-28 09:15:28 +01:00
6d7c21b2c9 fix(GODT-3135): fix br tag pipeline rules. 2023-11-27 16:25:49 +00:00
f7434109be fix(GODT-3124): Race conditions reported by race check 2023-11-27 16:30:27 +01:00
414d74d06a test(GODT-3124): Attempt to fix 401 during login
Update GPA to use the simplified locking model and hope that the problem
solves itself. As far as I could tell, this might be a lock acquisition
issue.

https://github.com/ProtonMail/go-proton-api/pull/132
https://github.com/ProtonMail/go-proton-api/pull/133
2023-11-27 13:31:35 +01:00
110cdbf3ae feat(GODT-3046): report all clicked external links to bridge. 2023-11-27 10:41:46 +01:00
ec4ceb4552 feat(GODT-3134): br tag triggers installer 2023-11-24 12:32:01 +01:00
ef62704030 feat(GODT-31134): re-organize pipeline config files: no change 2023-11-24 11:56:19 +01:00
eaba6b6363 fix(GODT-2797): encode attached key name and use same pubkey name as web-app. 2023-11-23 15:24:08 +01:00
e1723fc24b test: Add test scenarios to add an /Answered flag to a replied message and revert 2023-11-23 07:52:05 +00:00
2073513d5e chore: fix case of IMAP login error. 2023-11-22 15:43:47 +01:00
36f7d9672f fix(GODT-3132): Do not allow sending on disabled accounts 2023-11-22 13:07:20 +00:00
ef183e0758 feat(GODT-3046): tester UI cleanup. 2023-11-22 11:01:59 +01:00
0d2a803711 feat(GODT-3046): added all links to KB in error messages. 2023-11-22 09:26:40 +01:00
06b5276981 feat(GODT-3046): fix typo spotted during KB article review. 2023-11-22 08:29:58 +01:00
b2d61da41f feat(GODT-3046): removed 'No active key for recipient. 2023-11-22 08:29:58 +01:00
e51c81fc03 feat(GODT-3046): added ReportBugFallback event support in bridge-gui. 2023-11-22 08:29:58 +01:00
26897f06c4 feat(GODT-3046): added 'no keychain' event to bridge-gui-tester. 2023-11-22 08:29:58 +01:00
5ca9a7db37 feat(GODT-3046): removed unused error notifications, and added default user to bridge-gui-tester. 2023-11-22 08:29:58 +01:00
b34f5d072f feat(GODT-3046): added addressChanged and addressChangedLogout to gui-tester. 2023-11-22 08:29:58 +01:00
eeb514cc81 feat(GODT-3046): removed unused notification. 2023-11-22 08:29:58 +01:00
650ad49ab0 feat(GODT-3046): link in pop-up banner. 2023-11-22 08:29:58 +01:00
0e5715c4e3 feat(GODT-3046): LinkLabel in notification dialog. 2023-11-22 08:29:58 +01:00
b0f1c3d4c5 test(GODT-3113): Inline HTML message and HTML attachment is getting altered 2023-11-21 15:15:02 +00:00
ba935a6cce fix(GODT-3129): Bad Event during after address order change
When syncing an account, if the user creates a new address and then
changes it to be the default address in combined address mode we need
to update the connector maps so that the new primary address ID can be
found in that map.

Includes https://github.com/ProtonMail/go-proton-api/pull/130
2023-11-21 12:24:24 +00:00
1370ff78c5 chore: added update events to bridge GUI tester. 2023-11-21 11:59:02 +01:00
109c15410a fix(GODT-3117): Improve GetAllContacts and GetAllContactsEmail
https://github.com/ProtonMail/go-proton-api/pull/129
2023-11-20 16:02:21 +01:00
147 changed files with 5772 additions and 3183 deletions

View File

@ -34,270 +34,9 @@ stages:
- test
- build
.rules-branch-and-MR-manual:
rules:
- if: $CI_COMMIT_BRANCH || $CI_PIPELINE_SOURCE == "merge_request_event"
when: manual
allow_failure: true
- when: never
include:
- local: ci/rules.yml
- local: ci/env.yml
- local: ci/test.yml
- local: ci/build.yml
.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.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
# Stage: TEST
lint:
stage: test
extends:
- .rules-branch-manual-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
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
# 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"
trigger-qa-installer:
stage: build
needs: ["lint"]
extends:
- .rules-branch-and-MR-manual
variables:
APP: bridge
WORKFLOW: build-all
SRC_TAG: $CI_COMMIT_BRANCH
SRC_HASH: $CI_COMMIT_SHA
trigger:
project: "jcuth/bridge-release"
branch: master
# TODO: PUT BACK ALL THE JOBS! JUST DID THIS FOR NOW TO GET CI WORKING AGAIN...

View File

@ -50,6 +50,7 @@ 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)

View File

@ -3,6 +3,59 @@
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

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.7.1+git
BRIDGE_APP_VERSION?=3.8.2+git
APP_VERSION:=${BRIDGE_APP_VERSION}
APP_FULL_NAME:=Proton Mail Bridge
APP_VENDOR:=Proton AG
@ -328,13 +328,6 @@ 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

69
ci/build.yml Normal file
View File

@ -0,0 +1,69 @@
---
.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

62
ci/env.yml Normal file
View File

@ -0,0 +1,62 @@
---
.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

58
ci/rules.yml Normal file
View File

@ -0,0 +1,58 @@
---
.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

128
ci/test.yml Normal file
View File

@ -0,0 +1,128 @@
---
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*

9
go.mod
View File

@ -5,9 +5,9 @@ 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.20231114153341-2ecbdd2739f7
github.com/ProtonMail/gluon v0.17.1-0.20231206152152-caaf10897f9e
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
github.com/ProtonMail/go-proton-api v0.4.1-0.20231116144214-8a47c8d92fbc
github.com/ProtonMail/go-proton-api v0.4.1-0.20231130083229-e8aa47d7a366
github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton
github.com/PuerkitoBio/goquery v1.8.1
github.com/abiosoft/ishell v2.0.0+incompatible
@ -16,7 +16,7 @@ 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.8.1
github.com/elastic/go-sysinfo v1.11.2-0.20231129083954-35e55cd2a542
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
@ -32,6 +32,7 @@ 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
@ -122,6 +123,6 @@ replace (
github.com/docker/docker-credential-helpers => github.com/ProtonMail/docker-credential-helpers v1.1.0
github.com/emersion/go-message => github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7
github.com/emersion/go-smtp => github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865
github.com/go-resty/resty/v2 => github.com/LBeernaertProton/resty/v2 v2.0.0-20231030122409-92db8bee3605
github.com/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
)

59
go.sum
View File

@ -11,22 +11,25 @@ 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/LBeernaertProton/resty/v2 v2.0.0-20231030122409-92db8bee3605 h1:54Fh3JS6s2Tjy6ZIRLtt1amZOqfYDcjErdye45z8fkQ=
github.com/LBeernaertProton/resty/v2 v2.0.0-20231030122409-92db8bee3605/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A=
github.com/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.20231114153341-2ecbdd2739f7 h1:w+VoSAq9FQvKMm3DlH1MIEZ1KGe7LJ+81EJFVwSV4VU=
github.com/ProtonMail/gluon v0.17.1-0.20231114153341-2ecbdd2739f7/go.mod h1:Og5/Dz1MiGpCJn51XujZwxiLG7WzvvjE5PRpZBQmAHo=
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/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=
@ -36,8 +39,8 @@ github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7 h1:+j+Kd/
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.20231116144214-8a47c8d92fbc h1:GBRKoFAldApEMkMrsFN1ZxG0eG797w6LTv/dFMDcsqQ=
github.com/ProtonMail/go-proton-api v0.4.1-0.20231116144214-8a47c8d92fbc/go.mod h1:WEXJqj5DSc2YI77SgXdpMY0nk33Qy92Vu2r4tOEazA8=
github.com/ProtonMail/go-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-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
@ -50,6 +53,7 @@ github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma
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=
@ -113,8 +117,12 @@ 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/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/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-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=
@ -136,6 +144,9 @@ 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=
@ -146,7 +157,9 @@ 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=
@ -162,11 +175,14 @@ 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=
@ -240,12 +256,16 @@ 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=
@ -268,6 +288,7 @@ 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=
@ -303,9 +324,14 @@ 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=
@ -364,6 +390,8 @@ 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=
@ -397,6 +425,7 @@ 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=
@ -432,6 +461,7 @@ 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=
@ -443,6 +473,7 @@ 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=
@ -461,6 +492,8 @@ 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=
@ -482,6 +515,7 @@ 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=
@ -501,8 +535,11 @@ 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=
@ -511,6 +548,7 @@ 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=
@ -536,6 +574,7 @@ 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=
@ -562,11 +601,13 @@ 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=
@ -608,6 +649,7 @@ 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=
@ -619,6 +661,7 @@ 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

@ -42,7 +42,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.False(t, corrupt)
require.NoError(t, corrupt)
// load the old prefs file.
configDir := filepath.Join("testdata", "with_keys")
@ -63,7 +63,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.False(t, corrupt)
require.NoError(t, corrupt)
// load the old prefs file.
configDir := filepath.Join("testdata", "without_keys")
@ -173,7 +173,7 @@ func TestUserMigration(t *testing.T) {
v, corrupt, err := vault.New(settingsFolder, settingsFolder, token, async.NoopPanicHandler{})
require.NoError(t, err)
require.False(t, corrupt)
require.NoError(t, corrupt)
require.NoError(t, migrateOldAccounts(locations, kcl, v))
require.Equal(t, []string{wantCredentials.UserID}, v.GetUserIDs())

View File

@ -42,21 +42,25 @@ func WithVault(locations *locations.Locations, keychains *keychain.List, panicHa
logrus.WithFields(logrus.Fields{
"insecure": insecure,
"corrupt": corrupt,
"corrupt": corrupt != nil,
}).Debug("Vault created")
if corrupt != nil {
logrus.WithError(corrupt).Warn("Failed to load existing vault, vault has been reset")
}
cert, _ := encVault.GetBridgeTLSCert()
certs.NewInstaller().LogCertInstallStatus(cert)
// GODT-1950: Add teardown actions (e.g. to close the vault).
return fn(encVault, insecure, corrupt)
return fn(encVault, insecure, corrupt != nil)
}
func newVault(locations *locations.Locations, keychains *keychain.List, panicHandler async.PanicHandler) (*vault.Vault, bool, bool, error) {
func newVault(locations *locations.Locations, keychains *keychain.List, panicHandler async.PanicHandler) (*vault.Vault, bool, error, error) {
vaultDir, err := locations.ProvideSettingsPath()
if err != nil {
return nil, false, false, fmt.Errorf("could not get vault dir: %w", err)
return nil, false, nil, fmt.Errorf("could not get vault dir: %w", err)
}
logrus.WithField("vaultDir", vaultDir).Debug("Loading vault from directory")
@ -78,12 +82,12 @@ func newVault(locations *locations.Locations, keychains *keychain.List, panicHan
gluonCacheDir, err := locations.ProvideGluonCachePath()
if err != nil {
return nil, false, false, fmt.Errorf("could not provide gluon path: %w", err)
return nil, false, nil, fmt.Errorf("could not provide gluon path: %w", err)
}
vault, corrupt, err := vault.New(vaultDir, gluonCacheDir, vaultKey, panicHandler)
if err != nil {
return nil, false, false, fmt.Errorf("could not create vault: %w", err)
return nil, false, corrupt, fmt.Errorf("could not create vault: %w", err)
}
return vault, insecure, corrupt, nil

View File

@ -307,7 +307,7 @@ func newBridge(
bridge.heartbeat.init(bridge, heartbeatManager)
}
bridge.syncService.Run(bridge.tasks)
bridge.syncService.Run()
return bridge, nil
}
@ -451,6 +451,8 @@ 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()

View File

@ -37,10 +37,10 @@ func (bridge *Bridge) AutoconfigUsed(client string) {
}, bridge.usersLock)
}
func (bridge *Bridge) KBArticleOpened(article string) {
func (bridge *Bridge) ExternalLinkClicked(article string) {
safe.RLock(func() {
for _, user := range bridge.users {
user.KBArticleOpened(article)
user.ExternalLinkClicked(article)
}
}, bridge.usersLock)
}

View File

@ -22,6 +22,7 @@ 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"
@ -310,6 +311,10 @@ 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

@ -641,6 +641,55 @@ func TestBridge_CorruptedVaultClearsPreviousIMAPSyncState(t *testing.T) {
})
}
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

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

View File

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

View File

@ -1,6 +1,23 @@
// 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.2.0
// - protoc-gen-go-grpc v1.3.0
// - protoc v3.21.12
// source: focus.proto
@ -20,6 +37,11 @@ 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.
@ -38,7 +60,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.Focus/Raise", in, out, opts...)
err := c.cc.Invoke(ctx, Focus_Raise_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
@ -47,7 +69,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.Focus/Version", in, out, opts...)
err := c.cc.Invoke(ctx, Focus_Version_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
@ -96,7 +118,7 @@ func _Focus_Raise_Handler(srv interface{}, ctx context.Context, dec func(interfa
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/focus.Focus/Raise",
FullMethod: Focus_Raise_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(FocusServer).Raise(ctx, req.(*wrapperspb.StringValue))
@ -114,7 +136,7 @@ func _Focus_Version_Handler(srv interface{}, ctx context.Context, dec func(inter
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/focus.Focus/Version",
FullMethod: Focus_Version_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(FocusServer).Version(ctx, req.(*emptypb.Empty))

View File

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

View File

@ -66,11 +66,13 @@ 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
UserTable.cpp UserTable.h
)
)
target_precompile_headers(bridge-gui-tester PRIVATE Pch.h)
target_include_directories(bridge-gui-tester PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})

View File

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

View File

@ -31,10 +31,11 @@ GRPCQtProxy::GRPCQtProxy()
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void GRPCQtProxy::connectSignals() {
void GRPCQtProxy::connectSignals() const {
MainWindow &mainWindow = app().mainWindow();
SettingsTab &settingsTab = mainWindow.settingsTab();
UsersTab &usersTab = mainWindow.usersTab();
SettingsTab const &settingsTab = mainWindow.settingsTab();
UsersTab const &usersTab = mainWindow.usersTab();
KnowledgeBaseTab const &kbTab = mainWindow.knowledgeBaseTab();
connect(this, &GRPCQtProxy::delayedEventRequested, &mainWindow, &MainWindow::sendDelayedEvent);
connect(this, &GRPCQtProxy::setIsAutostartOnReceived, &settingsTab, &SettingsTab::setIsAutostartOn);
connect(this, &GRPCQtProxy::setIsBetaEnabledReceived, &settingsTab, &SettingsTab::setIsBetaEnabled);
@ -56,6 +57,7 @@ void GRPCQtProxy::connectSignals() {
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);
}
@ -119,6 +121,13 @@ 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);
}
//****************************************************************************************************************************************************
//
@ -157,8 +166,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 userSSLForSMTP) {
emit setMailServerSettingsReceived(imapPort, smtpPort, useSSLForIMAP, userSSLForSMTP);
void GRPCQtProxy::setMailServerSettings(qint32 imapPort, qint32 smtpPort, bool useSSLForIMAP, bool useSSLForSMTP) {
emit setMailServerSettingsReceived(imapPort, smtpPort, useSSLForIMAP, useSSLForSMTP);
}

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(); // connect the signals to the main window.
void connectSignals() const; // 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,11 +45,12 @@ 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 userSSLForSMTP); ///< Forwards a setMailServerSettings' call via a Qt signal.
void setMailServerSettings(qint32 imapPort, qint32 smtpPort, bool useSSLForIMAP, bool useSSLForSMTP); ///< 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.
@ -68,6 +69,7 @@ signals:
void setColorSchemeNameReceived(QString const &name); ///< Forward a SetColorScheme call via a Qt Signal
void reportBugReceived(QString const &osType, QString const &osVersion, QString const &emailClient, QString const &address,
QString const &description, bool includeLogs); ///< Signal for the ReportBug gRPC call
void 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.

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> credentials = grpc::SslServerCredentials(ssl_opts);
std::shared_ptr<ServerCredentials> const credentials = SslServerCredentials(ssl_opts);
GRPCConfig config;
config.cert = testTLSCert;
@ -59,8 +59,7 @@ 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
bool const useFileSocket = useFileSocketForGRPC();
if (useFileSocket) {
if (useFileSocketForGRPC()) {
QString const fileSocketPath = getAvailableFileSocketPath();
if (fileSocketPath.isEmpty()) {
throw Exception("Could not get an available file socket.");
@ -102,7 +101,7 @@ void GRPCServerWorker::run() {
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void GRPCServerWorker::stop() {
void GRPCServerWorker::stop() const {
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(); ///< Stop the gRPC service.
void stop() const; ///< 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() {
void GRPCService::connectProxySignals() const {
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 *request, BoolValue *response) {
Status GRPCService::IsAllMailVisible(ServerContext *, Empty const *, BoolValue *response) {
app().log().debug(__FUNCTION__);
response->set_value(app().mainWindow().settingsTab().isAllMailVisible());
return Status::OK;
@ -354,17 +354,49 @@ 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()));
return Status::OK;
}
//****************************************************************************************************************************************************
/// \param[in] request The request
//****************************************************************************************************************************************************
Status GRPCService::ReportBug(ServerContext *, ReportBugRequest const *request, Empty *) {
app().log().debug(__FUNCTION__);
SettingsTab &tab = app().mainWindow().settingsTab();
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());
qtProxy_.sendDelayedEvent(tab.nextBugReportWillSucceed() ? newReportBugSuccessEvent() : newReportBugErrorEvent());
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;
}
qtProxy_.sendDelayedEvent(event);
qtProxy_.sendDelayedEvent(newReportBugFinishedEvent());
return Status::OK;
@ -380,7 +412,7 @@ Status GRPCService::Login(ServerContext *, LoginRequest const *request, Empty *)
UsersTab &usersTab = app().mainWindow().usersTab();
loginUsername_ = QString::fromStdString(request->username());
SPUser const& user = usersTab.userTable().userWithUsernameOrEmail(QString::fromStdString(request->username()));
SPUser const &user = usersTab.userTable().userWithUsernameOrEmail(QString::fromStdString(request->username()));
if (user) {
qtProxy_.sendDelayedEvent(newLoginAlreadyLoggedInEvent(user->id()));
return Status::OK;
@ -415,7 +447,7 @@ Status GRPCService::Login(ServerContext *, LoginRequest const *request, Empty *)
//****************************************************************************************************************************************************
Status GRPCService::Login2FA(ServerContext *, LoginRequest const *request, Empty *) {
app().log().debug(__FUNCTION__);
UsersTab &usersTab = app().mainWindow().usersTab();
UsersTab const &usersTab = app().mainWindow().usersTab();
if (usersTab.nextUserTFAError()) {
qtProxy_.sendDelayedEvent(newLoginError(LoginErrorType::TFA_ERROR, "2FA Error."));
return Status::OK;
@ -440,7 +472,7 @@ Status GRPCService::Login2FA(ServerContext *, LoginRequest const *request, Empty
//****************************************************************************************************************************************************
Status GRPCService::Login2Passwords(ServerContext *, LoginRequest const *request, Empty *) {
app().log().debug(__FUNCTION__);
UsersTab &usersTab = app().mainWindow().usersTab();
UsersTab const &usersTab = app().mainWindow().usersTab();
if (usersTab.nextUserTwoPasswordsError()) {
qtProxy_.sendDelayedEvent(newLoginError(LoginErrorType::TWO_PASSWORDS_ERROR, "Two Passwords error."));
@ -530,12 +562,12 @@ Status GRPCService::DiskCachePath(ServerContext *, Empty const *, StringValue *r
Status GRPCService::SetDiskCachePath(ServerContext *, StringValue const *path, Empty *) {
app().log().debug(__FUNCTION__);
SettingsTab &tab = app().mainWindow().settingsTab();
EventsTab const &eventsTab = app().mainWindow().eventsTab();
QString const qPath = QString::fromStdString(path->value());
// we mimic the behaviour of Bridge
if (!tab.nextCacheChangeWillSucceed()) {
qtProxy_.sendDelayedEvent(newDiskCacheErrorEvent(grpc::DiskCacheErrorType(tab.cacheError())));
if (!eventsTab.nextCacheChangeWillSucceed()) {
qtProxy_.sendDelayedEvent(newDiskCacheErrorEvent(static_cast<DiskCacheErrorType>(CANT_MOVE_DISK_CACHE_ERROR)));
} else {
qtProxy_.setDiskCachePath(qPath);
qtProxy_.sendDelayedEvent(newDiskCachePathChangedEvent(qPath));
@ -572,7 +604,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(::grpc::ServerContext *context, ImapSmtpSettings const *settings, Empty *) {
Status GRPCService::SetMailServerSettings(ServerContext *, ImapSmtpSettings const *settings, Empty *) {
app().log().debug(__FUNCTION__);
qtProxy_.setMailServerSettings(settings->imapport(), settings->smtpport(), settings->usesslforimap(), settings->usesslforsmtp());
qtProxy_.sendDelayedEvent(newMailServerSettingsChanged(*settings));
@ -585,9 +617,9 @@ Status GRPCService::SetMailServerSettings(::grpc::ServerContext *context, ImapSm
/// \param[out] outSettings The settings
/// \return The status for the call.
//****************************************************************************************************************************************************
Status GRPCService::MailServerSettings(::grpc::ServerContext *, Empty const *, ImapSmtpSettings *outSettings) {
Status GRPCService::MailServerSettings(ServerContext *, Empty const *, ImapSmtpSettings *outSettings) {
app().log().debug(__FUNCTION__);
SettingsTab &tab = app().mainWindow().settingsTab();
SettingsTab const &tab = app().mainWindow().settingsTab();
outSettings->set_imapport(tab.imapPort());
outSettings->set_smtpport(tab.smtpPort());
outSettings->set_usesslforimap(tab.useSSLForIMAP());
@ -614,7 +646,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().settingsTab().isPortFree());
response->set_value(app().mainWindow().eventsTab().isPortFree());
return Status::OK;
}
@ -685,8 +717,8 @@ Status GRPCService::GetUserList(ServerContext *, Empty const *, UserListResponse
//****************************************************************************************************************************************************
Status GRPCService::GetUser(ServerContext *, StringValue const *request, grpc::User *response) {
app().log().debug(__FUNCTION__);
QString userID = QString::fromStdString(request->value());
SPUser user = app().mainWindow().usersTab().userWithID(userID);
QString const userID = QString::fromStdString(request->value());
SPUser const user = app().mainWindow().usersTab().userWithID(userID);
if (!user) {
return Status(NOT_FOUND, QString("user not found %1").arg(userID).toStdString());
}
@ -754,14 +786,14 @@ Status GRPCService::ConfigureUserAppleMail(ServerContext *, ConfigureAppleMailRe
/// \param[in] request The request
/// \return The status for the call.
//****************************************************************************************************************************************************
Status GRPCService::ExportTLSCertificates(ServerContext *, StringValue const *request, Empty *response) {
Status GRPCService::ExportTLSCertificates(ServerContext *, StringValue const *request, Empty *) {
app().log().debug(__FUNCTION__);
SettingsTab &tab = app().mainWindow().settingsTab();
SettingsTab const &tab = app().mainWindow().settingsTab();
if (!tab.nextTLSCertExportWillSucceed()) {
qtProxy_.sendDelayedEvent(newGenericErrorEvent(grpc::TLS_CERT_EXPORT_ERROR));
qtProxy_.sendDelayedEvent(newGenericErrorEvent(TLS_CERT_EXPORT_ERROR));
}
if (!tab.nextTLSKeyExportWillSucceed()) {
qtProxy_.sendDelayedEvent(newGenericErrorEvent(grpc::TLS_KEY_EXPORT_ERROR));
qtProxy_.sendDelayedEvent(newGenericErrorEvent(TLS_KEY_EXPORT_ERROR));
}
qtProxy_.exportTLSCertificates(QString::fromStdString(request->value()));
return Status::OK;
@ -786,7 +818,7 @@ Status GRPCService::InstallTLSCertificate(ServerContext *, Empty const *, Empty
app().log().debug(__FUNCTION__);
SPStreamEvent event;
qtProxy_.installTLSCertificate();
switch (app().mainWindow().settingsTab().nextTLSCertIntallResult()) {
switch (app().mainWindow().settingsTab().nextTLSCertInstallResult()) {
case SettingsTab::TLSCertInstallResult::Success:
event = newCertificateInstallSuccessEvent();
break;
@ -805,7 +837,7 @@ Status GRPCService::InstallTLSCertificate(ServerContext *, Empty const *, Empty
//****************************************************************************************************************************************************
/// \param[in] request The request.
//****************************************************************************************************************************************************
Status GRPCService::KBArticleClicked(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) {
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;
}

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(); ///< Connect the signals of the Qt Proxy to the GUI components
void connectProxySignals() const; ///< 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;
@ -67,6 +67,7 @@ public: // member functions.
grpc::Status ReportBug(::grpc::ServerContext *, ::grpc::ReportBugRequest const *request, ::google::protobuf::Empty *) override;
grpc::Status ForceLauncher(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
grpc::Status SetMainExecutable(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
grpc::Status 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;
@ -98,7 +99,7 @@ public: // member functions.
grpc::Status ExportTLSCertificates(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
grpc::Status ReportBugClicked(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::google::protobuf::Empty *) override;
grpc::Status AutoconfigClicked(::grpc::ServerContext *context, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
grpc::Status KBArticleClicked(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
grpc::Status 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() {
SettingsTab &MainWindow::settingsTab() const {
return *ui_.settingsTab;
}
@ -71,16 +71,32 @@ SettingsTab &MainWindow::settingsTab() {
//****************************************************************************************************************************************************
/// \return A reference to the users tab.
//****************************************************************************************************************************************************
UsersTab &MainWindow::usersTab() {
UsersTab &MainWindow::usersTab() const {
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) {
void MainWindow::addLogEntry(bridgepp::Log::Level level, const QString &message) const {
addEntryToLogEdit(level, message, *ui_.editLog);
}
@ -89,7 +105,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) {
void MainWindow::addBridgeGUILogEntry(bridgepp::Log::Level level, const QString &message) const {
addEntryToLogEdit(level, message, *ui_.editBridgeGUILog);
}
@ -97,8 +113,8 @@ void MainWindow::addBridgeGUILogEntry(bridgepp::Log::Level level, const QString
//****************************************************************************************************************************************************
/// \param[in] event The event.
//****************************************************************************************************************************************************
void MainWindow::sendDelayedEvent(SPStreamEvent const &event) {
QTimer::singleShot(this->settingsTab().eventDelayMs(), [event] { app().grpc().sendEvent(event); });
void MainWindow::sendDelayedEvent(SPStreamEvent const &event) const {
QTimer::singleShot(this->eventsTab().eventDelayMs(), [event] { app().grpc().sendEvent(event); });
}

View File

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

View File

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>1226</width>
<height>1086</height>
<width>1096</width>
<height>876</height>
</rect>
</property>
<property name="windowTitle">
@ -22,7 +22,7 @@
</property>
<widget class="QTabWidget" name="tabTop">
<property name="currentIndex">
<number>0</number>
<number>3</number>
</property>
<widget class="SettingsTab" name="settingsTab">
<attribute name="title">
@ -34,6 +34,16 @@
<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">
@ -96,6 +106,18 @@
<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

@ -0,0 +1,109 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
#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

@ -0,0 +1,59 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
#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

@ -0,0 +1,359 @@
<?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

@ -0,0 +1,96 @@
// 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

@ -0,0 +1,53 @@
// 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

@ -0,0 +1,239 @@
<?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,7 +18,6 @@
#include "SettingsTab.h"
#include "GRPCService.h"
#include <bridgepp/GRPC/EventFactory.h>
#include <bridgepp/BridgeUtils.h>
@ -38,23 +37,6 @@ 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();
}
@ -64,11 +46,10 @@ SettingsTab::SettingsTab(QWidget *parent)
//
//****************************************************************************************************************************************************
void SettingsTab::updateGUIState() {
bool connected = app().grpc().isStreaming();
bool const 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());
}
@ -84,7 +65,7 @@ void SettingsTab::setIsStreaming(bool isStreaming) {
//****************************************************************************************************************************************************
/// \param[in] clientPlatform The client platform.
//****************************************************************************************************************************************************
void SettingsTab::setClientPlatform(QString const &clientPlatform) {
void SettingsTab::setClientPlatform(QString const &clientPlatform) const {
ui_.labelClientPlatformValue->setText(clientPlatform);
}
@ -139,7 +120,7 @@ bool SettingsTab::showSplashScreen() const {
//****************************************************************************************************************************************************
/// \return true iff autosart is on.
/// \return true iff autostart is on.
//****************************************************************************************************************************************************
bool SettingsTab::isAutostartOn() const {
return ui_.checkAutostart->isChecked();
@ -149,7 +130,7 @@ bool SettingsTab::isAutostartOn() const {
//****************************************************************************************************************************************************
/// \param[in] on Should autostart be turned on?
//****************************************************************************************************************************************************
void SettingsTab::setIsAutostartOn(bool on) {
void SettingsTab::setIsAutostartOn(bool on) const {
ui_.checkAutostart->setChecked(on);
}
@ -165,7 +146,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) {
void SettingsTab::setColorSchemeName(QString const &name) const {
ui_.checkDarkTheme->setChecked(name == colorSchemeDark);
}
@ -181,7 +162,7 @@ bool SettingsTab::isBetaEnabled() const {
//****************************************************************************************************************************************************
/// \param[in] enabled The new state for the 'Beta Enabled' check box.
//****************************************************************************************************************************************************
void SettingsTab::setIsBetaEnabled(bool enabled) {
void SettingsTab::setIsBetaEnabled(bool enabled) const {
ui_.checkBetaEnabled->setChecked(enabled);
}
@ -197,7 +178,7 @@ bool SettingsTab::isAllMailVisible() const {
//****************************************************************************************************************************************************
/// \param[in] visible The new value for the 'All Mail Visible' check box.
//****************************************************************************************************************************************************
void SettingsTab::setIsAllMailVisible(bool visible) {
void SettingsTab::setIsAllMailVisible(bool visible) const {
ui_.checkAllMailVisible->setChecked(visible);
}
@ -213,19 +194,11 @@ bool SettingsTab::isTelemetryDisabled() const {
//****************************************************************************************************************************************************
/// \param[in] isDisabled The new value for the 'Disable Telemetry' check box.
//****************************************************************************************************************************************************
void SettingsTab::setIsTelemetryDisabled(bool isDisabled) {
void SettingsTab::setIsTelemetryDisabled(bool isDisabled) const {
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
//****************************************************************************************************************************************************
@ -275,7 +248,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) {
QString const &description, bool includeLogs) const {
ui_.editOSType->setText(osType);
ui_.editOSVersion->setText(osVersion);
ui_.editEmailClient->setText(emailClient);
@ -288,27 +261,17 @@ void SettingsTab::setBugReport(QString const &osType, QString const &osVersion,
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void SettingsTab::installTLSCertificate() {
void SettingsTab::installTLSCertificate() const {
ui_.labelLastTLSCertInstall->setText(QString("Last install: %1").arg(QDateTime::currentDateTime().toString(Qt::ISODateWithMs)));
ui_.checkTLSCertIsInstalled->setChecked(this->nextTLSCertIntallResult() == TLSCertInstallResult::Success);
ui_.checkTLSCertIsInstalled->setChecked(this->nextTLSCertInstallResult() == TLSCertInstallResult::Success);
}
//****************************************************************************************************************************************************
/// \param[in] folderPath The folder path.
//****************************************************************************************************************************************************
void SettingsTab::exportTLSCertificates(QString const &folderPath) {
ui_.labeLastTLSCertExport->setText(QString("%1 Export to %2")
.arg(QDateTime::currentDateTime().toString(Qt::ISODateWithMs))
.arg(folderPath));
}
//****************************************************************************************************************************************************
/// \return The state of the check box.
//****************************************************************************************************************************************************
bool SettingsTab::nextBugReportWillSucceed() const {
return ui_.checkNextBugReportWillSucceed->isChecked();
void SettingsTab::exportTLSCertificates(QString const &folderPath) const {
ui_.labeLastTLSCertExport->setText(QString("%1 Export to %2").arg(QDateTime::currentDateTime().toString(Qt::ISODateWithMs),folderPath));
}
@ -323,8 +286,8 @@ bool SettingsTab::isTLSCertificateInstalled() const {
//****************************************************************************************************************************************************
/// \return The value for the 'Next TLS cert install result'.
//****************************************************************************************************************************************************
SettingsTab::TLSCertInstallResult SettingsTab::nextTLSCertIntallResult() const {
return TLSCertInstallResult(ui_.comboNextTLSCertInstallResult->currentIndex());
SettingsTab::TLSCertInstallResult SettingsTab::nextTLSCertInstallResult() const {
return static_cast<TLSCertInstallResult>(ui_.comboNextTLSCertInstallResult->currentIndex());
}
@ -355,7 +318,7 @@ QString SettingsTab::hostname() const {
//****************************************************************************************************************************************************
/// \return The value of the IMAP port spin box.
//****************************************************************************************************************************************************
qint32 SettingsTab::imapPort() {
qint32 SettingsTab::imapPort() const {
return ui_.spinPortIMAP->value();
}
@ -363,7 +326,7 @@ qint32 SettingsTab::imapPort() {
//****************************************************************************************************************************************************
/// \return The value of the SMTP port spin box.
//****************************************************************************************************************************************************
qint32 SettingsTab::smtpPort() {
qint32 SettingsTab::smtpPort() const {
return ui_.spinPortSMTP->value();
}
@ -374,7 +337,7 @@ qint32 SettingsTab::smtpPort() {
/// \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) {
void SettingsTab::setMailServerSettings(qint32 imapPort, qint32 smtpPort, bool useSSLForIMAP, bool useSSLForSMTP) const {
ui_.spinPortIMAP->setValue(imapPort);
ui_.spinPortSMTP->setValue(smtpPort);
ui_.checkUseSSLForIMAP->setChecked(useSSLForIMAP);
@ -409,23 +372,15 @@ bool SettingsTab::isDoHEnabled() const {
//****************************************************************************************************************************************************
/// \param[in] enabled The state of the 'DoH enabled' check box.
//****************************************************************************************************************************************************
void SettingsTab::setIsDoHEnabled(bool enabled) {
void SettingsTab::setIsDoHEnabled(bool enabled) const {
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) {
void SettingsTab::setDiskCachePath(const QString &path) const {
ui_.editDiskCachePath->setText(path);
}
@ -438,22 +393,6 @@ 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.
//****************************************************************************************************************************************************
@ -465,7 +404,7 @@ bool SettingsTab::isAutomaticUpdateOn() const {
//****************************************************************************************************************************************************
/// \param[in] on The value for the 'Automatic Update' check.
//****************************************************************************************************************************************************
void SettingsTab::setIsAutomaticUpdateOn(bool on) {
void SettingsTab::setIsAutomaticUpdateOn(bool on) const {
ui_.checkAutomaticUpdate->setChecked(on);
}
@ -514,20 +453,16 @@ 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);

View File

@ -33,13 +33,13 @@ public: // data types.
Success = 0,
Canceled = 1,
Failure = 2
}; ///< Enumberation for the result of a TLS certificate installation.
}; ///< 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() = default; ///< Destructor.
~SettingsTab() override = default; ///< Destructor.
SettingsTab &operator=(SettingsTab const &) = delete; ///< Disabled assignment operator.
SettingsTab &operator=(SettingsTab &&) = delete; ///< Disabled move assignment operator.
@ -54,52 +54,47 @@ 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 nextBugReportWillSucceed() const; ///< Get the status of the 'Next Bug Report Will Fail' check box.
bool isTLSCertificateInstalled() const; ///< Get the status of the 'TLS Certificate is installed' check box.
TLSCertInstallResult nextTLSCertIntallResult() const; ///< Get the value of the 'Next TLS Certificate install result' combo box.
TLSCertInstallResult nextTLSCertInstallResult() const; ///< Get the value of the 'Next TLS Certificate install result' combo box.
bool nextTLSCertExportWillSucceed() const; ///< Get the status of the 'Next TLS Cert export will succeed' check box.
bool nextTLSKeyExportWillSucceed() const; ///< Get the status of the 'Next TLS Key export will succeed' check box.
QString hostname() const; ///< Get the value of the 'Hostname' edit.
qint32 imapPort(); ///< Get the value of the IMAP port spin.
qint32 smtpPort(); ///< Get the value of the SMTP port spin.
qint32 imapPort() const; ///< Get the value of the IMAP port spin.
qint32 smtpPort() const; ///< 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); ///< 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 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 setBugReport(QString const &osType, QString const &osVersion, QString const &emailClient, QString const &address, QString const &description,
bool includeLogs); ///< Set the content of the bug report box.
void installTLSCertificate(); ///< Install the TLS certificate.
void exportTLSCertificates(QString const &folderPath); ///< Export the TLS certificates.
void setMailServerSettings(qint32 imapPort, qint32 smtpPort, bool useSSLForIMAP, bool useSSLForSMTP); ///< Change the mail server settings.
void setIsDoHEnabled(bool enabled); ///< Set the value for the 'DoH Enabled' check box.
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.
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.
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>1127</width>
<height>808</height>
<width>1146</width>
<height>716</height>
</rect>
</property>
<property name="windowTitle">
@ -18,6 +18,9 @@
<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">
@ -103,6 +106,9 @@
<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">
@ -186,6 +192,9 @@
<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>
@ -199,7 +208,7 @@
<widget class="QLineEdit" name="editHostname">
<property name="minimumSize">
<size>
<width>200</width>
<width>0</width>
<height>0</height>
</size>
</property>
@ -287,6 +296,9 @@
<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">
@ -381,6 +393,9 @@
<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">
<property name="text">
@ -487,6 +502,9 @@
<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>
@ -596,8 +614,8 @@
<string>Bug Report</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="3">
<widget class="QLineEdit" name="editOSVersion">
<item row="1" column="1">
<widget class="QLineEdit" name="editEmailClient">
<property name="minimumSize">
<size>
<width>0</width>
@ -610,6 +628,37 @@
<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>
@ -628,13 +677,6 @@
</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">
@ -642,22 +684,18 @@
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QLabel" name="labelAddress">
<item row="2" column="0">
<widget class="QLabel" name="labelDescription">
<property name="text">
<string>Address</string>
<string>Description</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
</widget>
</item>
<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">
<item row="0" column="3">
<widget class="QLineEdit" name="editOSVersion">
<property name="minimumSize">
<size>
<width>0</width>
@ -682,251 +720,29 @@
</property>
</widget>
</item>
<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">
<item row="0" column="1">
<widget class="QLineEdit" name="editOSType">
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="text">
<string/>
<property name="baseSize">
<size>
<width>250</width>
<height>0</height>
</size>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="labelEmailClient">
<item row="0" column="2">
<widget class="QLabel" name="labelOSVersion">
<property name="text">
<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>
<string>OS Version</string>
</property>
</widget>
</item>
@ -956,19 +772,8 @@
<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(randomUser());
users_.append(defaultUser());
this->updateGUIState();
}
@ -85,7 +85,7 @@ void UsersTab::onAddUserButton() {
//
//****************************************************************************************************************************************************
void UsersTab::onEditUserButton() {
int index = selectedIndex();
const int index = selectedIndex();
if ((index < 0) || (index >= users_.userCount())) {
return;
}
@ -110,7 +110,7 @@ void UsersTab::onEditUserButton() {
//
//****************************************************************************************************************************************************
void UsersTab::onRemoveUserButton() {
int index = selectedIndex();
const int index = selectedIndex();
if ((index < 0) || (index >= users_.userCount())) {
return;
}
@ -127,7 +127,7 @@ void UsersTab::onRemoveUserButton() {
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void UsersTab::onSelectionChanged(QItemSelection, QItemSelection) {
void UsersTab::onSelectionChanged(QItemSelection const&, QItemSelection const&) {
this->updateGUIState();
}
@ -137,7 +137,6 @@ void UsersTab::onSelectionChanged(QItemSelection, QItemSelection) {
//****************************************************************************************************************************************************
void UsersTab::onSendUserBadEvent() {
SPUser const user = selectedUser();
int const index = this->selectedIndex();
if (!user) {
app().log().error(QString("%1 failed. Unkown user.").arg(__FUNCTION__));
@ -175,8 +174,8 @@ void UsersTab::onSendUsedBytesChangedEvent() {
app().log().error(QString("%1 failed. User is not connected").arg(__FUNCTION__));
}
qint64 const usedBytes = qint64(ui_.spinUsedBytes->value());
user->setUsedBytes(usedBytes);
auto const usedBytes = static_cast<qint64>(ui_.spinUsedBytes->value());
user->setUsedBytes(static_cast<float>(usedBytes));
users_.touch(index);
GRPCService &grpc = app().grpc();
@ -224,9 +223,10 @@ 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 ? qint32(user->syncProgress() * 100.0f) : 0;
qint32 const progressPercent = syncing ? static_cast<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 user = users_.userWithID(userID);
SPUser const 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() == value) {
if ((!user) || (!user->isSyncing()) || user->syncProgress() == static_cast<float>(value)) {
return;
}
double const progress = value / 100.0;
user->setSyncProgress(progress);
user->setSyncProgress(static_cast<float>(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.
void configureUserAppleMail(QString const &userID, QString const &address); ///< Slot for the configuration of Apple mail.
static 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, QItemSelection); ///< Slot for the change of the selection.
void onSelectionChanged(QItemSelection const&, QItemSelection const&); ///< 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(bridgepp::SPUser &user, QWidget *parent)
UserDialog::UserDialog(const 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(float(ui_.spinUsedBytes->value()));
user_->setTotalBytes(float(ui_.spinTotalBytes->value()));
user_->setUsedBytes(static_cast<float>(ui_.spinUsedBytes->value()));
user_->setTotalBytes(static_cast<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() {
return UserState(ui_.comboState->currentIndex());
UserState UserDialog::state() const {
return static_cast<UserState>(ui_.comboState->currentIndex());
}
//****************************************************************************************************************************************************
/// \param[in] state The user state to select in the dialog.
//****************************************************************************************************************************************************
void UserDialog::setState(UserState state) {
ui_.comboState->setCurrentIndex(qint32(state));
void UserDialog::setState(UserState state) const {
ui_.comboState->setCurrentIndex(static_cast<qint32>(state));
}

View File

@ -30,7 +30,7 @@
class UserDialog : public QDialog {
Q_OBJECT
public: // member functions.
UserDialog(bridgepp::SPUser &user, QWidget *parent); ///< Default constructor.
UserDialog(const 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(); ///< Get the user state selected in the dialog.
void setState(bridgepp::UserState state); ///< Set the user state selected in the dialog
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
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 users_.size();
return static_cast<int>(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 = users_.size();
qint32 const count = static_cast<int>(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 users_.count();
return static_cast<qint32>(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 it = std::find_if(users_.constBegin(), users_.constEnd(), [&userID](SPUser const &user) -> bool {
QList<SPUser>::const_iterator const 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 it = std::find_if(users_.constBegin(), users_.constEnd(), [&username](SPUser const &user) -> bool {
QList<SPUser>::const_iterator const 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 it = std::find_if(users_.constBegin(), users_.constEnd(), [&userID](SPUser const &user) -> bool {
QList<SPUser>::const_iterator const 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() = default; ///< Destructor.
~UserTable() override = 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 overseer = std::make_unique<Overseer>(serverWorker, nullptr);
UPOverseer const overseer = std::make_unique<Overseer>(serverWorker, nullptr);
overseer->startWorker(true);
qint32 const exitCode = QApplication::exec();

View File

@ -294,11 +294,21 @@ bool QMLBackend::isTLSCertificateInstalled() {
//****************************************************************************************************************************************************
/// \param[in] url The URL of the knowledge base article. If empty/invalid, the home page for the Bridge knowledge base is opened.
//****************************************************************************************************************************************************
void QMLBackend::openKBArticle(QString const &url) {
void QMLBackend::openExternalLink(QString const &url) {
HANDLE_EXCEPTION(
QString const u = url.isEmpty() ? bridgeKBUrl : url;
QDesktopServices::openUrl(u);
emit notifyKBArticleClicked(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));
)
}
@ -1062,9 +1072,9 @@ void QMLBackend::notifyAutoconfigClicked(QString const &client) const {
//****************************************************************************************************************************************************
/// \param[in] article The url of the KB article.
//****************************************************************************************************************************************************
void QMLBackend::notifyKBArticleClicked(QString const &article) const {
void QMLBackend::notifyExternalLinkClicked(QString const &article) const {
HANDLE_EXCEPTION(
app().grpc().KBArticleClicked(article);
app().grpc().externalLinkClicked(article);
)
}
@ -1305,11 +1315,10 @@ void QMLBackend::connectGrpcEvents() {
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);
@ -1354,7 +1363,6 @@ 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

@ -65,7 +65,8 @@ public: // member functions.
Q_INVOKABLE QString collectAnswers(quint8 categoryId) const; ///< Collect answer for a given set of questions.
Q_INVOKABLE void clearAnswers(); ///< Clear all collected answers.
Q_INVOKABLE bool isTLSCertificateInstalled(); ///< Check if the bridge certificate is installed in the OS keychain.
Q_INVOKABLE void openKBArticle(QString const & url = QString()); ///< Open a knowledge base article.
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)
@ -205,7 +206,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 notifyKBArticleClicked(QString const &article) const; ///< Slot for KBArticleClicked gRPC event.
void notifyExternalLinkClicked(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.
@ -224,10 +225,8 @@ 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.
@ -258,7 +257,6 @@ 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.
@ -281,6 +279,7 @@ signals: // Signals received from the Go backend, to be forwarded to QML
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,11 +5,6 @@
<file>qml/AccountView.qml</file>
<file>qml/Banner.qml</file>
<file>qml/Bridge.qml</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>
@ -89,6 +84,12 @@
<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>
@ -96,6 +97,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>
@ -108,7 +110,6 @@
<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>

View File

@ -84,6 +84,7 @@ Popup {
anchors.topMargin: 14
spacing: 8
ColorImage {
Layout.preferredHeight: 24
Layout.preferredWidth: 24
@ -108,14 +109,29 @@ Popup {
sourceSize.width: 24
width: 24
}
Label {
Layout.alignment: Qt.AlignVCenter
ColumnLayout {
Layout.alignment: Qt.AlignTop
Layout.fillWidth: true
Layout.leftMargin: 16
color: root.colorScheme.text_invert
colorScheme: root.colorScheme
text: root.notification ? root.notification.description : ""
wrapMode: Text.WordWrap
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
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,201 @@
// 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

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

View File

@ -1,161 +0,0 @@
// Copyright (c) 2023 Proton AG
// This file is part of Proton Mail Bridge.
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Proton
SettingsView {
id: root
property var 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

@ -15,6 +15,7 @@ import QtQuick.Layouts
import QtQuick.Controls
import Proton
import Notifications
import "BugReport"
Item {
id: root

View File

@ -36,7 +36,7 @@ SettingsView {
type: SettingsItem.PrimaryButton
onClicked: {
Backend.openKBArticle();
Backend.openExternalLink();
}
}
SettingsItem {
@ -70,7 +70,7 @@ SettingsView {
text: qsTr("Logs")
type: SettingsItem.Button
onClicked: Qt.openUrlExternally(Backend.logsPath)
onClicked: Backend.openExternalLink(Backend.logsPath)
}
SettingsItem {
id: reportBug
@ -103,7 +103,7 @@ SettingsView {
type: Label.Caption
onLinkActivated: function (link) {
Qt.openUrlExternally(link)
Backend.openExternalLink(link)
}
}
}

View File

@ -18,6 +18,7 @@ import QtQuick.Controls
import Proton
import Notifications
import "SetupWizard"
import "BugReport"
ApplicationWindow {
id: root

View File

@ -71,7 +71,7 @@ Dialog {
wrapMode: Text.WordWrap
onLinkActivated: function (link) {
Qt.openUrlExternally(link);
Backend.openExternalLink(link);
}
}
Item {
@ -82,6 +82,17 @@ Dialog {
implicitWidth: additionalChildrenContainer.childrenRect.width
visible: children.length > 0
}
LinkLabel {
Layout.alignment: Qt.AlignHCenter
Layout.bottomMargin: 32
colorScheme: root.colorScheme
external: true
link: notification.linkUrl
text: notification.linkText
visible: notification.linkUrl.length > 0
}
ColumnLayout {
spacing: 8

View File

@ -61,18 +61,10 @@ Item {
colorScheme: root.colorScheme
notification: root.notifications.enableBeta
}
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.cacheUnavailable
}
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.cacheCantMove
}
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.diskFull
}
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.enableSplitMode
@ -101,10 +93,6 @@ Item {
colorScheme: root.colorScheme
notification: root.notifications.apiCertIssue
}
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.noActiveKeyForRecipient
}
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.userBadEvent

View File

@ -24,19 +24,17 @@ QtObject {
property list<Action> action
property bool active: false
// brief is used in status view only
property string brief
property string brief // brief is used in status view only
default property var children
property var data
// description is used in banners and in dialogs as description
property string description
property string description // description is used in banners and in dialogs as description
property bool dismissed: false
property int group
property string icon
property string linkUrl: ""
property string linkText: ""
readonly property var occurred: active ? new Date() : undefined
// title is used in dialogs only
property string title
property string title // title is used in dialogs only
property int type
onActiveChanged: {

View File

@ -28,31 +28,15 @@ QtObject {
Dialogs = 64
}
// Other
property Notification accountChanged: Notification {
brief: qsTr("Address list changed")
description: qsTr("The address list for .... account has changed. You need to reconfigure your email client.")
group: Notifications.Group.Configuration
icon: "./icons/ic-exclamation-circle-filled.svg"
type: Notification.NotificationType.Danger
action: Action {
text: qsTr("Reconfigure")
onTriggered:
// TODO: open configuration window here
{
}
}
}
property Notification addressChanged: Notification {
brief: title
description: qsTr("The address list for your account has changed. You might need to reconfigure your email client.")
group: Notifications.Group.Configuration
icon: "./icons/ic-exclamation-circle-filled.svg"
linkText: qsTr("Learn more about address list changes")
linkUrl: "https://proton.me/support/bridge-address-list-has-changed"
title: qsTr("Address list changes")
type: Notification.NotificationType.Warning
action: [
Action {
text: qsTr("OK")
@ -76,7 +60,7 @@ QtObject {
target: Backend
}
}
property var all: [root.noInternet, root.imapPortStartupError, root.smtpPortStartupError, root.imapPortChangeError, root.smtpPortChangeError, root.imapConnectionModeChangeError, root.smtpConnectionModeChangeError, root.updateManualReady, root.updateManualRestartNeeded, root.updateManualError, root.updateForce, root.updateForceError, root.updateSilentRestartNeeded, root.updateSilentError, root.updateIsLatestVersion, root.loginConnectionError, root.onlyPaidUsers, root.alreadyLoggedIn, root.enableBeta, root.bugReportSendSuccess, root.bugReportSendError, root.bugReportSendFallback, root.cacheUnavailable, root.cacheCantMove, root.accountChanged, root.diskFull, root.cacheLocationChangeSuccess, root.enableSplitMode, root.resetBridge, root.changeAllMailVisibility, root.deleteAccount, root.noKeychain, root.rebuildKeychain, root.addressChanged, root.apiCertIssue, root.noActiveKeyForRecipient, root.userBadEvent, root.imapLoginWhileSignedOut, root.genericError, root.genericQuestion]
property var all: [root.noInternet, root.imapPortStartupError, root.smtpPortStartupError, root.imapPortChangeError, root.smtpPortChangeError, root.imapConnectionModeChangeError, root.smtpConnectionModeChangeError, root.updateManualReady, root.updateManualRestartNeeded, root.updateManualError, root.updateForce, root.updateForceError, root.updateSilentRestartNeeded, root.updateSilentError, root.updateIsLatestVersion, root.loginConnectionError, root.onlyPaidUsers, root.alreadyLoggedIn, root.enableBeta, root.bugReportSendSuccess, root.bugReportSendError, root.bugReportSendFallback, root.cacheCantMove, root.cacheLocationChangeSuccess, root.enableSplitMode, root.resetBridge, root.changeAllMailVisibility, root.deleteAccount, root.noKeychain, root.rebuildKeychain, root.addressChanged, root.apiCertIssue, root.userBadEvent, root.imapLoginWhileSignedOut, root.genericError, root.genericQuestion]
property Notification alreadyLoggedIn: Notification {
brief: qsTr("Already signed in")
description: qsTr("This account is already signed in.")
@ -104,9 +88,11 @@ QtObject {
}
property Notification apiCertIssue: Notification {
brief: qsTr("Cannot establish secure connection")
description: qsTr("Bridge cannot verify the authenticity of Proton servers on your current network due to a TLS certificate error. " + "Start Bridge again after ensuring your connection is secure and/or connecting to a VPN. Learn more about TLS pinning " + "<a href=\"https://proton.me/blog/tls-ssl-certificate#Extra-security-precautions-taken-by-ProtonMail\">here</a>.")
description: qsTr("Bridge cannot verify the authenticity of Proton servers on your current network due to a TLS certificate error. Start Bridge again after ensuring your connection is secure and/or connecting to a VPN.")
group: Notifications.Group.Dialogs | Notifications.Group.Connection
icon: "./icons/ic-exclamation-circle-filled.svg"
linkText: qsTr("Learn mode about TLS pinning")
linkUrl: "https://proton.me/blog/tls-ssl-certificate#Extra-security-precautions-taken-by-ProtonMail"
title: qsTr("Unable to establish a \nsecure connection to \nProton servers")
type: Notification.NotificationType.Danger
@ -208,6 +194,8 @@ QtObject {
description: qsTr("The location you have selected is not available. Make sure you have enough free space or choose another location.")
group: Notifications.Group.Configuration | Notifications.Group.Dialogs
icon: "./icons/ic-exclamation-circle-filled.svg"
linkText: qsTr("Learn more about cache relocation issues")
linkUrl: "https://proton.me/support/bridge-cant-move-cache"
title: qsTr("Cant move cache")
type: Notification.NotificationType.Warning
@ -263,43 +251,6 @@ QtObject {
target: Backend
}
}
// Cache
property Notification cacheUnavailable: Notification {
brief: title
description: qsTr("The current cache location is unavailable. Check the directory or change it in your settings.")
group: Notifications.Group.Configuration | Notifications.Group.Dialogs
icon: "./icons/ic-exclamation-circle-filled.svg"
title: qsTr("Cache location is unavailable")
type: Notification.NotificationType.Warning
action: [
Action {
text: qsTr("Quit Bridge")
onTriggered: {
Backend.quit();
root.cacheUnavailable.active = false;
}
},
Action {
text: qsTr("Change location")
onTriggered: {
root.cacheUnavailable.active = false;
root.frontendMain.showLocalCacheSettings();
}
}
]
Connections {
function onDiskCacheUnavailable() {
root.cacheUnavailable.active = true;
}
target: Backend
}
}
property Notification changeAllMailVisibility: Notification {
property var isVisibleNow
@ -378,41 +329,6 @@ QtObject {
target: root
}
}
property Notification diskFull: Notification {
brief: title
description: qsTr("Quit Bridge and free disk space or move the local cache to another disk.")
group: Notifications.Group.Configuration | Notifications.Group.Dialogs
icon: "./icons/ic-exclamation-circle-filled.svg"
title: qsTr("Your disk is almost full")
type: Notification.NotificationType.Warning
action: [
Action {
text: qsTr("Quit Bridge")
onTriggered: {
Backend.quit();
root.diskFull.active = false;
}
},
Action {
text: qsTr("Settings")
onTriggered: {
root.diskFull.active = false;
root.frontendMain.showLocalCacheSettings();
}
}
]
Connections {
function onDiskFull() {
root.diskFull.active = true;
}
target: Backend
}
}
property Notification enableBeta: Notification {
brief: title
description: qsTr("Be the first to get new updates and use new features. Bridge will update to the latest beta version.")
@ -454,6 +370,8 @@ QtObject {
description: qsTr("Changing between split and combined address mode will require you to delete your account(s) from your email client and begin the setup process from scratch.")
group: Notifications.Group.Configuration | Notifications.Group.Dialogs
icon: "./icons/ic-question-circle.svg"
linkText: qsTr("Learn more about split mode")
linkUrl: "https://proton.me/support/difference-combined-addresses-mode-split-addresses-mode"
title: qsTr("Enable split mode?")
type: Notification.NotificationType.Warning
@ -600,7 +518,9 @@ QtObject {
description: "#PlaceHolderText"
group: Notifications.Group.Connection
icon: "./icons/ic-exclamation-circle-filled.svg"
title: qsTr("IMAP Login failed")
linkText: qsTr("Learn more about IMAP login issues")
linkUrl: "https://proton.me/support/bridge-imap-login-failed"
title: qsTr("IMAP login failed")
type: Notification.NotificationType.Danger
action: [
@ -627,6 +547,8 @@ QtObject {
description: qsTr("The IMAP port could not be changed.")
group: Notifications.Group.Connection
icon: "./icons/ic-alert.svg"
linkText: qsTr("Learn more about IMAP port issues")
linkUrl: "https://proton.me/support/port-already-occupied-error"
type: Notification.NotificationType.Danger
Connections {
@ -642,6 +564,8 @@ QtObject {
description: qsTr("The IMAP server could not be started. Please check or change the IMAP port.")
group: Notifications.Group.Connection
icon: "./icons/ic-alert.svg"
linkText: qsTr("Learn more about IMAP port issues")
linkUrl: "https://proton.me/support/port-already-occupied-error"
type: Notification.NotificationType.Danger
Connections {
@ -679,33 +603,6 @@ QtObject {
target: Backend
}
}
property Notification noActiveKeyForRecipient: Notification {
brief: title
description: "#PlaceholderText#"
group: Notifications.Group.Dialogs | Notifications.Group.Connection
icon: "./icons/ic-exclamation-circle-filled.svg"
title: qsTr("Unable to send \nencrypted message")
type: Notification.NotificationType.Danger
action: [
Action {
text: qsTr("OK")
onTriggered: {
root.noActiveKeyForRecipient.active = false;
}
}
]
Connections {
function onNoActiveKeyForRecipient(email) {
root.noActiveKeyForRecipient.description = qsTr("There are no active keys to encrypt your message to %1. " + "Please update the setting for this contact.").arg(email);
root.noActiveKeyForRecipient.active = true;
}
target: Backend
}
}
// Connection
property Notification noInternet: Notification {
@ -714,7 +611,6 @@ QtObject {
group: Notifications.Group.Connection
icon: "./icons/ic-no-connection.svg"
type: Notification.NotificationType.Danger
Connections {
function onInternetOff() {
root.noInternet.active = true;
@ -730,9 +626,11 @@ QtObject {
brief: title
description: Backend.goos === "darwin" ?
qsTr("Bridge is not able to access your keychain. Please make sure your keychain is not locked and restart the application.") :
qsTr("Bridge is not able to detect a supported password manager (pass or secret-service). Please install and setup supported password manager and restart the application.")
qsTr("Bridge is not able to detect a supported password manager (pass or secret-service). Please install and setup a supported password manager and restart the application.")
group: Notifications.Group.Dialogs | Notifications.Group.Configuration
icon: "./icons/ic-exclamation-circle-filled.svg"
linkText: qsTr("Learn more about keychain issues")
linkUrl: "https://proton.me/support/bridge-cannot-access-keychain"
title: Backend.goos === "darwin" ? qsTr("Cannot access keychain") : qsTr("No keychain available")
type: Notification.NotificationType.Danger
@ -775,7 +673,7 @@ QtObject {
text: qsTr("Upgrade")
onTriggered: {
Qt.openUrlExternally(root.onlyPaidUsers.pricingLink);
Backend.openExternalLink(root.onlyPaidUsers.pricingLink);
root.onlyPaidUsers.active = false;
}
}
@ -802,7 +700,7 @@ QtObject {
text: qsTr("Open the support page")
onTriggered: {
Backend.openKBArticle();
Backend.openExternalLink();
Backend.quit();
}
}
@ -893,6 +791,8 @@ QtObject {
description: qsTr("The SMTP port could not be changed.")
group: Notifications.Group.Connection
icon: "./icons/ic-alert.svg"
linkText: qsTr("Learn more about SMTP port issues")
linkUrl: "https://proton.me/support/port-already-occupied-error"
type: Notification.NotificationType.Danger
Connections {
@ -908,6 +808,8 @@ QtObject {
description: qsTr("The SMTP server could not be started. Please check or change the SMTP port.")
group: Notifications.Group.Connection
icon: "./icons/ic-alert.svg"
linkText: qsTr("Learn more about SMTP port issues")
linkUrl: "https://proton.me/support/port-already-occupied-error"
type: Notification.NotificationType.Danger
Connections {
@ -939,7 +841,7 @@ QtObject {
text: qsTr("Update manually")
onTriggered: {
Qt.openUrlExternally(Backend.landingPageLink);
Backend.openExternalLink(Backend.landingPageLink);
root.updateForce.active = false;
}
},
@ -969,6 +871,8 @@ QtObject {
description: qsTr("You must update manually. Go to: https://proton.me/mail/bridge#download")
group: Notifications.Group.Update | Notifications.Group.Dialogs
icon: "./icons/ic-exclamation-circle-filled.svg"
linkText: qsTr("Learn more about Bridge updates")
linkUrl: "https://proton.me/support/protonmail-bridge-manual-update"
title: qsTr("Bridge couldn't update")
type: Notification.NotificationType.Danger
@ -977,7 +881,7 @@ QtObject {
text: qsTr("Update manually")
onTriggered: {
Qt.openUrlExternally(Backend.landingPageLink);
Backend.openExternalLink(Backend.landingPageLink);
root.updateForceError.active = false;
}
},
@ -1027,6 +931,8 @@ QtObject {
description: qsTr("Please follow manual installation in order to update Bridge.")
group: Notifications.Group.Update
icon: "./icons/ic-exclamation-circle-filled.svg"
linkText: qsTr("Learn more about Bridge updates")
linkUrl: "https://proton.me/support/protonmail-bridge-manual-update"
title: qsTr("Bridge couldnt update")
type: Notification.NotificationType.Warning
@ -1035,7 +941,7 @@ QtObject {
text: qsTr("Update manually")
onTriggered: {
Qt.openUrlExternally(Backend.landingPageLink);
Backend.openExternalLink(Backend.landingPageLink);
root.updateManualError.active = false;
Backend.quit();
}
@ -1085,7 +991,7 @@ QtObject {
text: qsTr("Update manually")
onTriggered: {
Qt.openUrlExternally(Backend.landingPageLink);
Backend.openExternalLink(Backend.landingPageLink);
root.updateManualReady.active = false;
}
},
@ -1138,13 +1044,15 @@ QtObject {
description: qsTr("Bridge couldn't update")
group: Notifications.Group.Update
icon: "./icons/ic-exclamation-circle-filled.svg"
linkText: qsTr("Learn more about Bridge updates")
linkUrl: "https://proton.me/support/protonmail-bridge-manual-update"
type: Notification.NotificationType.Warning
action: Action {
text: qsTr("Update manually")
onTriggered: {
Qt.openUrlExternally(Backend.landingPageLink);
Backend.openExternalLink(Backend.landingPageLink);
root.updateSilentError.active = false;
}
}
@ -1188,6 +1096,8 @@ QtObject {
description: "#PlaceHolderText"
group: Notifications.Group.Connection | Notifications.Group.Dialogs
icon: "./icons/ic-exclamation-circle-filled.svg"
linkText: qsTr("Learn more about internal errors")
linkUrl: "https://proton.me/support/bridge-internal-error"
title: qsTr("Internal error")
type: Notification.NotificationType.Danger

View File

@ -0,0 +1,60 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
ColorImage {
id: root
property var colorScheme
property string text
property int size: 16
color: root.colorScheme.interaction_norm
height: size
width: size
source: "/qml/icons/ic-info-circle.svg"
sourceSize.height: size
sourceSize.width: size
visible: root.hint !== ""
MouseArea {
id: imageArea
anchors.fill: parent
hoverEnabled: true
}
ToolTip {
id: toolTipinfo
text: root.text
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_norm
text: root.text
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
}

View File

@ -22,6 +22,7 @@ RowLayout {
property bool external: false
property string link: "#"
property string text: ""
property color color: colorScheme.interaction_norm
function clear() {
root.callback = null;
@ -49,12 +50,12 @@ RowLayout {
id: label
Layout.alignment: Qt.AlignVCenter
colorScheme: root.colorScheme
linkColor: root.color
text: label.link(root.link, root.text)
type: Label.LabelType.Body
onLinkActivated: function (link) {
if ((link !== "#") && (link.length > 0)) {
Qt.openUrlExternally(link);
Backend.openExternalLink(link);
}
if (callback) {
callback();
@ -63,11 +64,12 @@ RowLayout {
}
ColorImage {
Layout.alignment: Qt.AlignVCenter
Layout.bottomMargin: -6
color: label.linkColor
height: sourceSize.height
source: "/qml/icons/ic-external-link.svg"
sourceSize.height: 16
sourceSize.width: 16
sourceSize.height: 14
sourceSize.width: 14
visible: external
width: sourceSize.width

View File

@ -27,6 +27,7 @@ Button 4.0 Button.qml
CheckBox 4.0 CheckBox.qml
ComboBox 4.0 ComboBox.qml
Dialog 4.0 Dialog.qml
InfoTooltip 4.0 InfoTooltip.qml
Label 4.0 Label.qml
LinkLabel 4.0 LinkLabel.qml
Menu 4.0 Menu.qml

View File

@ -30,7 +30,7 @@ Item {
property bool fillHeight: false
default property alias items: content.children
function positionViewAtBegining() {
function positionViewAtBeginning() {
scrollView.ScrollBar.vertical.position = 0
}
signal back

View File

@ -79,7 +79,7 @@ Rectangle {
text: qsTr("Open guide")
onClicked: function () {
Backend.openKBArticle(wizard.setupGuideLink());
Backend.openExternalLink(wizard.setupGuideLink());
}
}
}

View File

@ -49,7 +49,7 @@ Button {
text: qsTr("Get help")
onClicked: {
Backend.openKBArticle();
Backend.openExternalLink();
}
}
MenuItem {

View File

@ -37,7 +37,7 @@ Item {
function showAppleMailAutoconfigCertificateInstall() {
showAppleMailAutoconfigCommon();
descriptionLabel.text = qsTr("Apple Mail configuration is mostly automated, but in order to work, Bridge needs to install a certificate in your keychain.");
linkLabel1.setCallback(function() { Backend.openKBArticle("https://proton.me/support/apple-mail-certificate"); }, qsTr("Why is this certificate needed?"), true);
linkLabel1.setCallback(function() { Backend.openExternalLink("https://proton.me/support/apple-mail-certificate"); }, qsTr("Why is this certificate needed?"), true);
linkLabel2.clear();
}
function showAppleMailAutoconfigCommon() {
@ -51,7 +51,7 @@ Item {
function showAppleMailAutoconfigProfileInstall() {
showAppleMailAutoconfigCommon();
descriptionLabel.text = qsTr("The final step before you can start using Apple Mail is to install the Bridge server profile in the system preferences.\n\nAdding a server profile is necessary to ensure that your Mac can receive and send Proton Mails.");
linkLabel1.setCallback(function() { Backend.openKBArticle("https://proton.me/support/macos-certificate-warning"); }, qsTr("Why is there a yellow warning sign?"), true);
linkLabel1.setCallback(function() { Backend.openExternalLink("https://proton.me/support/macos-certificate-warning"); }, qsTr("Why is there a yellow warning sign?"), true);
linkLabel2.setCallback(wizard.showClientParams, qsTr("Configure Apple Mail manually"), false);
}
function showClientSelector(newAccount = true) {
@ -86,7 +86,7 @@ Item {
function showOnboarding() {
titleLabel.text = (Backend.users.count === 0) ? welcomeTitle : addAccountTitle;
descriptionLabel.text = welcomeDescription
linkLabel1.setCallback(function() { Backend.openKBArticle("https://proton.me/support/why-you-need-bridge"); }, qsTr("Why do I need Bridge?"), true);
linkLabel1.setCallback(function() { Backend.openExternalLink("https://proton.me/support/why-you-need-bridge"); }, qsTr("Why do I need Bridge?"), true);
linkLabel2.clear();
root.iconSource = welcomeImage;
root.iconHeight = welcomeImageHeight;

View File

@ -162,7 +162,7 @@ Dialog {
wrapMode: Text.WordWrap
onLinkActivated: function (link) {
Qt.openUrlExternally(link);
Backend.openExternalLink(link);
}
}
}

View File

@ -208,17 +208,19 @@ QString randomLastName() {
//****************************************************************************************************************************************************
//
/// \param[in] firstName The user's first name. If empty, a random common US first name is used.
/// \param[in] lastName The user's last name. If empty, a random common US last name is used.
/// \return The user
//****************************************************************************************************************************************************
SPUser randomUser() {
SPUser randomUser(QString const &firstName, QString const &lastName) {
SPUser user = User::newUser(nullptr);
user->setID(QUuid::createUuid().toString());
QString const firstName = randomFirstName();
QString const lastName = randomLastName();
QString const username = QString("%1.%2").arg(firstName.toLower(), lastName.toLower());
QString const first = firstName.isEmpty() ? randomFirstName() : firstName;
QString const last = lastName.isEmpty() ? randomLastName() : lastName;
QString const username = QString("%1.%2").arg(first.toLower(), last.toLower());
user->setUsername(username);
user->setAddresses(QStringList() << (username + "@proton.me") << (username + "@protonmail.com"));
user->setPassword(QUuid().createUuid().toString(QUuid::StringFormat::WithoutBraces).left(20));
user->setPassword(QUuid::createUuid().toString(QUuid::StringFormat::WithoutBraces).left(20));
user->setAvatarText(firstName.left(1) + lastName.left(1));
user->setState(UserState::Connected);
user->setSplitMode(false);
@ -229,6 +231,16 @@ SPUser randomUser() {
}
//****************************************************************************************************************************************************
/// \return The default user. The name Eric Norbert is used on the proton.me website, and should be used for screenshots.
//****************************************************************************************************************************************************
SPUser defaultUser() {
SPUser user = randomUser("Eric", "Norbert");
user->setAddresses({"eric.norbert@proton.me", "eric_norbert_writes@protonmail.com"}); // we override the address list with addresses commonly used on screenshots proton.me
return user;
}
//****************************************************************************************************************************************************
/// \return The OS the application is running on.
//****************************************************************************************************************************************************

View File

@ -44,7 +44,8 @@ QString goos(); ///< return the value of Go's GOOS for the current platform ("d
qint64 randN(qint64 n); ///< return a random integer in the half open range [0,n)
QString randomFirstName(); ///< Get a random first name from a pre-determined list.
QString randomLastName(); ///< Get a random first name from a pre-determined list.
SPUser randomUser(); ///< Get a random user.
SPUser defaultUser(); ///< Return The default user, with the name and addresses used on screenshots on proton.me
SPUser randomUser(QString const &firstName = "", QString const &lastName = ""); ///< Get a random user.
OS os(); ///< Return the operating system.
bool onLinux(); ///< Check if the OS is Linux.
bool onMacOS(); ///< Check if the OS is macOS.

View File

@ -30,13 +30,6 @@ namespace {
namespace bridgepp {
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
BugReportFlow::BugReportFlow() {
}
//****************************************************************************************************************************************************
/// \param[in] filepath The path of the file to parse.
/// \return True iff the file can be properly parsed.
@ -92,7 +85,7 @@ bool BugReportFlow::setAnswer(quint8 questionId, QString const &answer) {
//****************************************************************************************************************************************************
/// \param[in] questionId The id of the question.
/// \param[in] categoryId The id of the question.
/// \return answer the given question.
//****************************************************************************************************************************************************
QString BugReportFlow::getCategory(quint8 categoryId) const {
@ -128,7 +121,7 @@ QString BugReportFlow::collectAnswers(quint8 categoryId) const {
QVariantList sets = this->questionSet(categoryId);
for (QVariant const &var: sets) {
const QString& answer = getAnswer(var.toInt());
const QString answer = getAnswer(var.toInt());
if (answer.isEmpty())
continue;
answers += "#### " + questions_[var.toInt()].toMap()["text"].toString() + "\n\r";
@ -139,6 +132,24 @@ QString BugReportFlow::collectAnswers(quint8 categoryId) const {
}
//****************************************************************************************************************************************************
/// \param[in] categoryId The id of the question set.
//****************************************************************************************************************************************************
QString BugReportFlow::collectUserInput(quint8 categoryId) const {
if (categoryId > categories_.count() - 1)
return {};
QString input = this->getCategory(categoryId);
for (QVariant const &var: this->questionSet(categoryId)) {
QString const answer = getAnswer(var.toInt());
if (!answer.isEmpty())
input += " " + answer;
}
return input;
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************

View File

@ -28,7 +28,7 @@ namespace bridgepp {
class BugReportFlow {
public: // member functions.
BugReportFlow(); ///< Default constructor.
BugReportFlow() = default; ///< Default constructor.
BugReportFlow(BugReportFlow const &) = delete; ///< Disabled copy-constructor.
BugReportFlow(BugReportFlow &&) = delete; ///< Disabled assignment copy-constructor.
~BugReportFlow() = default; ///< Destructor.
@ -42,6 +42,7 @@ public: // member functions.
[[nodiscard]] QString getCategory(quint8 categoryId) const; ///< Get category name.
[[nodiscard]] QString getAnswer(quint8 questionId) const; ///< Get answer for a given question.
[[nodiscard]] QString collectAnswers(quint8 categoryId) const; ///< Collect answer for a given set of questions.
[[nodiscard]] QString collectUserInput(quint8 categoryId) const; ///< Collect the user input (user answers without quesitons) for a given set of questions.
void clearAnswers(); ///< Clear all collected answers.

View File

@ -202,6 +202,18 @@ SPStreamEvent newReportBugErrorEvent() {
}
//****************************************************************************************************************************************************
/// \return The event.
//****************************************************************************************************************************************************
SPStreamEvent newReportBugFallbackEvent() {
auto event = new grpc::ReportBugFallbackEvent;
auto appEvent = new grpc::AppEvent;
appEvent->set_allocated_reportbugfallback(event);
return wrapAppEvent(appEvent);
}
//****************************************************************************************************************************************************
/// \return The event.
//****************************************************************************************************************************************************
@ -235,6 +247,22 @@ SPStreamEvent newCertificateInstallFailedEvent() {
}
//****************************************************************************************************************************************************
/// \param[in] suggestions the suggestions
/// \return The event.
//****************************************************************************************************************************************************
SPStreamEvent newKnowledgeBaseSuggestionsEvent(QList<KnowledgeBaseSuggestion> const& suggestions) {
auto event = new grpc::KnowledgeBaseSuggestionsEvent;
for (KnowledgeBaseSuggestion const &suggestion: suggestions) {
grpc::KnowledgeBaseSuggestion *s = event->add_suggestions();
s->set_url(suggestion.url.toStdString());
s->set_title(suggestion.title.toStdString());
}
auto appEvent = new grpc::AppEvent;
appEvent->set_allocated_knowledgebasesuggestions(event);
return wrapAppEvent(appEvent);
}
//****************************************************************************************************************************************************
/// \return The event.
//****************************************************************************************************************************************************
@ -368,7 +396,7 @@ SPStreamEvent newUpdateForceEvent(QString const &version) {
//****************************************************************************************************************************************************
/// \return the event.
//****************************************************************************************************************************************************
SPStreamEvent newUpdateSilentRestartNeeded() {
SPStreamEvent newUpdateSilentRestartNeededEvent() {
auto event = new grpc::UpdateSilentRestartNeeded;
auto updateEvent = new grpc::UpdateEvent;
updateEvent->set_allocated_silentrestartneeded(event);
@ -379,7 +407,7 @@ SPStreamEvent newUpdateSilentRestartNeeded() {
//****************************************************************************************************************************************************
/// \return The event.
//****************************************************************************************************************************************************
SPStreamEvent newUpdateIsLatestVersion() {
SPStreamEvent newUpdateIsLatestVersionEvent() {
auto event = new grpc::UpdateIsLatestVersion;
auto updateEvent = new grpc::UpdateEvent;
updateEvent->set_allocated_islatestversion(event);
@ -390,7 +418,7 @@ SPStreamEvent newUpdateIsLatestVersion() {
//****************************************************************************************************************************************************
/// \return The event.
//****************************************************************************************************************************************************
SPStreamEvent newUpdateCheckFinished() {
SPStreamEvent newUpdateCheckFinishedEvent() {
auto event = new grpc::UpdateCheckFinished;
auto updateEvent = new grpc::UpdateEvent;
updateEvent->set_allocated_checkfinished(event);
@ -398,6 +426,17 @@ SPStreamEvent newUpdateCheckFinished() {
}
//****************************************************************************************************************************************************
/// \return The event.
//****************************************************************************************************************************************************
SPStreamEvent newUpdateVersionChangedEvent() {
auto event = new grpc::UpdateVersionChanged;
auto updateEvent = new grpc::UpdateEvent;
updateEvent->set_allocated_versionchanged(event);
return wrapUpdateEvent(updateEvent);
}
//****************************************************************************************************************************************************
/// \param[in] errorType The error type.
/// \return The event.
@ -505,19 +544,6 @@ SPStreamEvent newRebuildKeychainEvent() {
}
//****************************************************************************************************************************************************
/// \param[in] email The email.
/// \return The event.
//****************************************************************************************************************************************************
SPStreamEvent newNoActiveKeyForRecipientEvent(QString const &email) {
auto event = new grpc::NoActiveKeyForRecipientEvent;
event->set_email(email.toStdString());
auto mailEvent = new grpc::MailEvent;
mailEvent->set_allocated_noactivekeyforrecipientevent(event);
return wrapMailEvent(mailEvent);
}
//****************************************************************************************************************************************************
/// \param[in] address The address.
/// /// \return The event.

View File

@ -22,6 +22,7 @@
#include "bridge.grpc.pb.h"
#include "GRPCUtils.h"
#include <bridgepp/GRPC/GRPCClient.h>
namespace bridgepp {
@ -34,10 +35,12 @@ SPStreamEvent newResetFinishedEvent(); ///< Create a new ResetFinishedEvent even
SPStreamEvent newReportBugFinishedEvent(); ///< Create a new ReportBugFinishedEvent event.
SPStreamEvent newReportBugSuccessEvent(); ///< Create a new ReportBugSuccessEvent event.
SPStreamEvent newReportBugErrorEvent(); ///< Create a new ReportBugErrorEvent event.
SPStreamEvent newReportBugFallbackEvent(); ///< Create a new ReportBugFallbackEvent event.
SPStreamEvent newCertificateInstallSuccessEvent(); ///< Create a new CertificateInstallSuccessEvent event.
SPStreamEvent newCertificateInstallCanceledEvent(); ///< Create a new CertificateInstallCanceledEvent event.
SPStreamEvent newCertificateInstallFailedEvent(); ///< Create anew CertificateInstallFailedEvent event.
SPStreamEvent newShowMainWindowEvent(); ///< Create a new ShowMainWindowEvent event.
SPStreamEvent newKnowledgeBaseSuggestionsEvent(QList<KnowledgeBaseSuggestion> const& suggestions); ///< Create a new KnowledgeBaseSuggestions event.
// Login events
SPStreamEvent newLoginError(grpc::LoginErrorType error, QString const &message); ///< Create a new LoginError event.
@ -51,9 +54,10 @@ SPStreamEvent newUpdateErrorEvent(grpc::UpdateErrorType errorType); ///< Create
SPStreamEvent newUpdateManualReadyEvent(QString const &version); ///< Create a new UpdateManualReadyEvent event.
SPStreamEvent newUpdateManualRestartNeededEvent(); ///< Create a new UpdateManualRestartNeededEvent event.
SPStreamEvent newUpdateForceEvent(QString const &version); ///< Create a new UpdateForceEvent event.
SPStreamEvent newUpdateSilentRestartNeeded(); ///< Create a new UpdateSilentRestartNeeded event.
SPStreamEvent newUpdateIsLatestVersion(); ///< Create a new UpdateIsLatestVersion event.
SPStreamEvent newUpdateCheckFinished(); ///< Create a new UpdateCheckFinished event.
SPStreamEvent newUpdateSilentRestartNeededEvent(); ///< Create a new UpdateSilentRestartNeeded event.
SPStreamEvent newUpdateIsLatestVersionEvent(); ///< Create a new UpdateIsLatestVersion event.
SPStreamEvent newUpdateCheckFinishedEvent(); ///< Create a new UpdateCheckFinished event.
SPStreamEvent newUpdateVersionChangedEvent(); ///< Create a new updateVersionChanged event.
// Cache on disk related events
SPStreamEvent newDiskCacheErrorEvent(grpc::DiskCacheErrorType errorType); ///< Create a new DiskCacheErrorEvent event.
@ -71,7 +75,6 @@ SPStreamEvent newHasNoKeychainEvent(); ///< Create a new HasNoKeychainEvent even
SPStreamEvent newRebuildKeychainEvent(); ///< Create a new RebuildKeychainEvent event.
// Mail related events
SPStreamEvent newNoActiveKeyForRecipientEvent(QString const &email); ///< Create a new NoActiveKeyForRecipientEvent event.
SPStreamEvent newAddressChangedEvent(QString const &address); ///< Create a new AddressChangedEvent event.
SPStreamEvent newAddressChangedLogoutEvent(QString const &address); ///< Create a new AddressChangedLogoutEvent event.
SPStreamEvent newApiCertIssueEvent(); ///< Create a new ApiCertIssueEvent event.

View File

@ -568,6 +568,14 @@ grpc::Status GRPCClient::hostname(QString &outHostname) {
}
//****************************************************************************************************************************************************
/// \param[in] input The user input to analyze.
//****************************************************************************************************************************************************
grpc::Status GRPCClient::requestKnowledgeBaseSuggestions(QString const &input) {
return this->logGRPCCallStatus(this->setString(&Bridge::Stub::RequestKnowledgeBaseSuggestions, input), __FUNCTION__);
}
//****************************************************************************************************************************************************
/// \param[out] outPath The value for the property.
/// \return The status for the gRPC call.
@ -1164,6 +1172,19 @@ void GRPCClient::processAppEvent(AppEvent const &event) {
this->logTrace("App event received: CertificateInstallFailed.");
emit certificateInstallFailed();
break;
case AppEvent::kKnowledgeBaseSuggestions:
{
this->logTrace("App event received: KnowledgeBaseSuggestions.");
QList<KnowledgeBaseSuggestion> suggestions;
for (grpc::KnowledgeBaseSuggestion const &suggestion: event.knowledgebasesuggestions().suggestions()) {
suggestions.push_back(KnowledgeBaseSuggestion{
.url = QString::fromUtf8(suggestion.url()),
.title = QString::fromUtf8(suggestion.title())
});
}
emit knowledgeBasSuggestionsReceived(suggestions);
break;
}
default:
this->logError("Unknown App event received.");
}
@ -1298,18 +1319,10 @@ void GRPCClient::processCacheEvent(DiskCacheEvent const &event) {
switch (event.event_case()) {
case DiskCacheEvent::kError: {
switch (event.error().type()) {
case DISK_CACHE_UNAVAILABLE_ERROR:
this->logError("Cache error received: diskCacheUnavailable.");
emit diskCacheUnavailable();
break;
case CANT_MOVE_DISK_CACHE_ERROR:
this->logError("Cache error received: cantMoveDiskCache.");
emit cantMoveDiskCache();
break;
case DISK_FULL_ERROR:
this->logError("Cache error received: diskFull.");
emit diskFull();
break;
default:
this->logError("Unknown cache error event received.");
break;
@ -1409,12 +1422,6 @@ void GRPCClient::processKeychainEvent(KeychainEvent const &event) {
//****************************************************************************************************************************************************
void GRPCClient::processMailEvent(MailEvent const &event) {
switch (event.event_case()) {
case MailEvent::kNoActiveKeyForRecipientEvent: {
QString const email = QString::fromStdString(event.noactivekeyforrecipientevent().email());
this->logTrace(QString("Mail event received: NoActiveKeyForRecipient (email = %1).").arg(email));
emit noActiveKeyForRecipient(email);
break;
}
case MailEvent::kAddressChanged:
this->logTrace("Mail event received: AddressChanged.");
emit addressChanged(QString::fromStdString(event.addresschanged().address()));
@ -1527,24 +1534,30 @@ UPClientContext GRPCClient::clientContext() const {
}
//****************************************************************************************************************************************************
/// \param[in] userID The user ID.
/// \param[in] address The email address.
/// \return the status for the gRPC call.
//****************************************************************************************************************************************************
grpc::Status GRPCClient::reportBugClicked() {
return this->logGRPCCallStatus(stub_->ReportBugClicked(this->clientContext().get(), empty, &empty), __FUNCTION__);
}
//****************************************************************************************************************************************************
/// \param[in] client The client string.
/// \return the status for the gRPC call.
//****************************************************************************************************************************************************
grpc::Status GRPCClient::autoconfigClicked(QString const &client) {
StringValue s;
s.set_value(client.toStdString());
return this->logGRPCCallStatus(stub_->AutoconfigClicked(this->clientContext().get(), s, &empty), __FUNCTION__);
}
grpc::Status GRPCClient::KBArticleClicked(QString const &article) {
//****************************************************************************************************************************************************
/// \param[in] link The clicked link.
/// \return the status for the gRPC call.
//****************************************************************************************************************************************************
grpc::Status GRPCClient::externalLinkClicked(QString const &link) {
StringValue s;
s.set_value(article.toStdString());
return this->logGRPCCallStatus(stub_->KBArticleClicked(this->clientContext().get(), s, &empty), __FUNCTION__);
s.set_value(link.toStdString());
return this->logGRPCCallStatus(stub_->ExternalLinkClicked(this->clientContext().get(), s, &empty), __FUNCTION__);
}

View File

@ -42,6 +42,20 @@ typedef grpc::Status (grpc::Bridge::Stub::*StringParamMethod)(grpc::ClientContex
typedef std::unique_ptr<grpc::ClientContext> UPClientContext;
//****************************************************************************************************************************************************
/// \brief A struct for knowledge base suggestion.
//****************************************************************************************************************************************************
struct KnowledgeBaseSuggestion {
// The following lines make the type transmissible to QML (but not instanciable there)
Q_GADGET
Q_PROPERTY(QString url MEMBER url)
Q_PROPERTY(QString title MEMBER title)
public:
QString url; ///< The URL of the knowledge base article
QString title; ///< The title of the knowledge base article.
};
//****************************************************************************************************************************************************
/// \brief gRPC client class. This class encapsulate the gRPC service, abstracting all data type conversions.
//****************************************************************************************************************************************************
@ -93,6 +107,7 @@ public: // member functions.
grpc::Status releaseNotesPageLink(QUrl &outUrl); ///< Performs the 'releaseNotesPageLink' call.
grpc::Status landingPageLink(QUrl &outUrl); ///< Performs the 'landingPageLink' call.
grpc::Status hostname(QString &outHostname); ///< Performs the 'Hostname' call.
grpc::Status requestKnowledgeBaseSuggestions(QString const &input); ///< Performs the 'RequestKnowledgeBaseSuggestions' call.
signals: // app related signals
void internetStatus(bool isOn);
@ -106,21 +121,20 @@ signals: // app related signals
void certificateInstallCanceled();
void certificateInstallFailed();
void showMainWindow();
void knowledgeBasSuggestionsReceived(QList<KnowledgeBaseSuggestion> const& suggestions);
// cache related calls
public:
public: // cache related calls
grpc::Status diskCachePath(QUrl &outPath); ///< Performs the 'diskCachePath' call.
grpc::Status setDiskCachePath(QUrl const &path); ///< Performs the 'setDiskCachePath' call
signals:
void diskCacheUnavailable();
void cantMoveDiskCache();
void diskFull();
void diskCachePathChanged(QUrl const &path);
void diskCachePathChangeFinished();
// mail settings related calls
public:
public: // mail settings related calls
grpc::Status mailServerSettings(qint32 &outIMAPPort, qint32 &outSMTPPort, bool &outUseSSLForIMAP, bool &outUseSSLForSMTP); ///< Performs the 'MailServerSettings' gRPC call.
grpc::Status setMailServerSettings(qint32 imapPort, qint32 smtpPort, bool useSSLForIMAP, bool useSSLForSMTP); ///< Performs the 'SetMailServerSettings' gRPC call.
grpc::Status isDoHEnabled(bool &outEnabled); ///< Performs the 'isDoHEnabled' gRPC call.
@ -196,7 +210,7 @@ signals:
public: // telemetry related calls
grpc::Status reportBugClicked(); ///< Performs the 'reportBugClicked' call.
grpc::Status autoconfigClicked(QString const &userID); ///< Performs the 'AutoconfigClicked' call.
grpc::Status KBArticleClicked(QString const &userID); ///< Performs the 'KBArticleClicked' call.
grpc::Status externalLinkClicked(QString const &userID); ///< Performs the 'KBArticleClicked' call.
public: // keychain related calls
grpc::Status availableKeychains(QStringList &outKeychains);
@ -215,7 +229,6 @@ signals:
void certIsReady();
signals: // mail related events
void noActiveKeyForRecipient(QString const &email);
void addressChanged(QString const &address);
void addressChangedLogout(QString const &address);
void apiCertIssue();

File diff suppressed because it is too large Load Diff

View File

@ -58,6 +58,7 @@ service Bridge {
rpc ReportBug(ReportBugRequest) returns (google.protobuf.Empty);
rpc ForceLauncher(google.protobuf.StringValue) returns (google.protobuf.Empty);
rpc SetMainExecutable(google.protobuf.StringValue) returns (google.protobuf.Empty);
rpc RequestKnowledgeBaseSuggestions(google.protobuf.StringValue) returns (google.protobuf.Empty);
// login
rpc Login(LoginRequest) returns (google.protobuf.Empty);
@ -100,7 +101,7 @@ service Bridge {
// Telemetry
rpc ReportBugClicked(google.protobuf.Empty) returns (google.protobuf.Empty);
rpc AutoconfigClicked(google.protobuf.StringValue) returns (google.protobuf.Empty);
rpc KBArticleClicked(google.protobuf.StringValue) returns (google.protobuf.Empty);
rpc ExternalLinkClicked(google.protobuf.StringValue) returns (google.protobuf.Empty);
// TLS certificate related calls
rpc IsTLSCertificateInstalled(google.protobuf.Empty) returns (google.protobuf.BoolValue);
@ -269,6 +270,7 @@ message AppEvent {
CertificateInstallSuccessEvent certificateInstallSuccess = 9;
CertificateInstallCanceledEvent certificateInstallCanceled = 10;
CertificateInstallFailedEvent certificateInstallFailed = 11;
KnowledgeBaseSuggestionsEvent knowledgeBaseSuggestions = 12;
}
}
@ -287,6 +289,15 @@ message CertificateInstallSuccessEvent {}
message CertificateInstallCanceledEvent {}
message CertificateInstallFailedEvent {}
message KnowledgeBaseSuggestion {
string url = 1;
string title = 2;
}
message KnowledgeBaseSuggestionsEvent {
repeated KnowledgeBaseSuggestion suggestions = 1;
}
//**********************************************************
// Login related events
//**********************************************************
@ -385,9 +396,7 @@ message DiskCacheEvent {
}
enum DiskCacheErrorType {
DISK_CACHE_UNAVAILABLE_ERROR = 0;
CANT_MOVE_DISK_CACHE_ERROR = 1;
DISK_FULL_ERROR = 2;
CANT_MOVE_DISK_CACHE_ERROR = 0;
};
message DiskCacheErrorEvent {
@ -446,17 +455,12 @@ message RebuildKeychainEvent {}
//**********************************************************
message MailEvent {
oneof event {
NoActiveKeyForRecipientEvent noActiveKeyForRecipientEvent = 1;
AddressChangedEvent addressChanged = 2;
AddressChangedLogoutEvent addressChangedLogout = 3;
ApiCertIssueEvent apiCertIssue = 6;
AddressChangedEvent addressChanged = 1;
AddressChangedLogoutEvent addressChangedLogout = 2;
ApiCertIssueEvent apiCertIssue = 3;
}
}
message NoActiveKeyForRecipientEvent {
string email = 1;
}
message AddressChangedEvent {
string address = 1;
}
@ -485,6 +489,7 @@ message UserEvent {
}
}
message ToggleSplitModeFinishedEvent {
string userID = 1;
}

File diff suppressed because it is too large Load Diff

View File

@ -17,6 +17,11 @@
package grpc
import (
"github.com/ProtonMail/proton-bridge/v3/internal/kb"
"github.com/bradenaw/juniper/xslices"
)
func NewInternetStatusEvent(connected bool) *StreamEvent {
return appEvent(&AppEvent{Event: &AppEvent_InternetStatus{InternetStatus: &InternetStatusEvent{Connected: connected}}})
}
@ -61,6 +66,20 @@ func NewShowMainWindowEvent() *StreamEvent {
return appEvent(&AppEvent{Event: &AppEvent_ShowMainWindow{ShowMainWindow: &ShowMainWindowEvent{}}})
}
func NewRequestKnowledgeBaseSuggestionsEvent(suggestions kb.ArticleList) *StreamEvent {
s := xslices.Map(
suggestions,
func(article *kb.Article) *KnowledgeBaseSuggestion {
return &KnowledgeBaseSuggestion{Url: article.URL, Title: article.Title}
},
)
return appEvent(&AppEvent{Event: &AppEvent_KnowledgeBaseSuggestions{
KnowledgeBaseSuggestions: &KnowledgeBaseSuggestionsEvent{
Suggestions: s,
},
}})
}
func NewLoginError(err LoginErrorType, message string) *StreamEvent {
return loginEvent(&LoginEvent{Event: &LoginEvent_Error{Error: &LoginErrorEvent{Type: err, Message: message}}})
}
@ -161,10 +180,6 @@ func NewKeychainRebuildKeychainEvent() *StreamEvent {
return keychainEvent(&KeychainEvent{Event: &KeychainEvent_RebuildKeychain{RebuildKeychain: &RebuildKeychainEvent{}}})
}
func NewMailNoActiveKeyForRecipientEvent(email string) *StreamEvent {
return mailEvent(&MailEvent{Event: &MailEvent_NoActiveKeyForRecipientEvent{NoActiveKeyForRecipientEvent: &NoActiveKeyForRecipientEvent{Email: email}}})
}
func NewMailAddressChangeEvent(email string) *StreamEvent {
return mailEvent(&MailEvent{Event: &MailEvent_AddressChanged{AddressChanged: &AddressChangedEvent{Address: email}}})
}

View File

@ -30,6 +30,7 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/frontend/theme"
"github.com/ProtonMail/proton-bridge/v3/internal/kb"
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/service"
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
@ -375,6 +376,23 @@ func (s *Service) SetMainExecutable(_ context.Context, exe *wrapperspb.StringVal
return &emptypb.Empty{}, nil
}
func (s *Service) RequestKnowledgeBaseSuggestions(_ context.Context, userInput *wrapperspb.StringValue) (*emptypb.Empty, error) {
s.log.Debug("RequestKnowledgeBaseSuggestions")
go func() {
defer async.HandlePanic(s.panicHandler)
articles, err := s.bridge.GetKnowledgeBaseSuggestions(userInput.Value)
if err != nil {
s.log.WithError(err).Error("Could not retrieve KB article suggestions")
articles = kb.ArticleList{}
}
_ = s.SendEvent(NewRequestKnowledgeBaseSuggestionsEvent(articles))
}()
return &emptypb.Empty{}, nil
}
func (s *Service) Login(_ context.Context, login *LoginRequest) (*emptypb.Empty, error) {
s.log.WithField("username", login.Username).Debug("Login")

View File

@ -21,6 +21,7 @@ import (
"context"
"github.com/ProtonMail/gluon/async"
"github.com/ProtonMail/proton-bridge/v3/internal/kb"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
@ -122,6 +123,7 @@ func (s *Service) StartEventTest() error {
NewReportBugSuccessEvent(),
NewReportBugErrorEvent(),
NewShowMainWindowEvent(),
NewRequestKnowledgeBaseSuggestionsEvent(kb.ArticleList{}),
// login
NewLoginError(LoginErrorType_FREE_USER, "error"),
@ -166,7 +168,6 @@ func (s *Service) StartEventTest() error {
NewKeychainRebuildKeychainEvent(),
// mail
NewMailNoActiveKeyForRecipientEvent(dummyAddress),
NewMailAddressChangeEvent(dummyAddress),
NewMailAddressChangeLogoutEvent(dummyAddress),
NewMailApiCertIssue(),

View File

@ -34,7 +34,7 @@ func (s *Service) AutoconfigClicked(_ context.Context, client *wrapperspb.String
return &emptypb.Empty{}, nil
}
func (s *Service) KBArticleClicked(_ context.Context, article *wrapperspb.StringValue) (*emptypb.Empty, error) {
s.bridge.KBArticleOpened(article.Value)
func (s *Service) ExternalLinkClicked(_ context.Context, article *wrapperspb.StringValue) (*emptypb.Empty, error) {
s.bridge.ExternalLinkClicked(article.Value)
return &emptypb.Empty{}, nil
}

View File

@ -0,0 +1,425 @@
[
{
"index": 0,
"url": "https://proton.me/support/automatically-start-bridge",
"title": "Automatically start Bridge",
"keywords": [
"automatic",
"login",
"start",
"boot"
]
},
{
"index": 1,
"url": "https://proton.me/support/bridge-automatic-update",
"title": "Automatic Update and Bridge",
"keywords": [
"update",
"upgrade",
"restart",
"automatic",
"manual"
]
},
{
"index": 2,
"url": "https://proton.me/support/messages-encrypted-via-bridge",
"title": "Are my messages encrypted via Proton Mail Bridge?",
"keywords": [
"encrypted",
"privacy",
"message",
"security",
"gpg",
"pgp",
"crypto"
]
},
{
"index": 3,
"url": "https://proton.me/support/labels-in-bridge",
"title": "Labels in Bridge",
"keywords": [
"labels",
"folders",
"directories"
]
},
{
"index": 4,
"url": "https://proton.me/support/bridge-ssl-connection-issue",
"title": "Proton Mail Bridge connection issues with Thunderbird, Outlook, and Apple Mail",
"keywords": [
"connect",
"SSL",
"STARTTLS",
"client",
"program",
"Outlook",
"Apple Mail",
"Thunderbird"
]
},
{
"index": 5,
"url": "https://proton.me/support/sending-pgp-emails-bridge",
"title": "Sending PGP emails in Proton Mail Bridge",
"keywords": [
"pgp",
"gpg",
"encrypt",
"crypto"
]
},
{
"index": 6,
"url": "https://proton.me/support/difference-combined-addresses-mode-split-addresses-mode",
"title": "Difference between combined addresses mode and split addresses mode",
"keywords": [
"combined",
"split",
"address",
"mode"
]
},
{
"index": 7,
"url": "https://proton.me/support/thunderbird-connection-server-timed-error",
"title": "Thunderbird: 'Connection to server timed out' error",
"keywords": [
"Thunderbird",
"Connection to server timed out",
"Connection",
"Timeout"
]
},
{
"index": 8,
"url": "https://proton.me/support/update-required",
"title": "Update required",
"keywords": [
"update required",
"update",
"upgrade",
"restart",
"reboot"
]
},
{
"index": 9,
"url": "https://proton.me/support/port-already-occupied-error",
"title": "Port already occupied error",
"keywords": [
"Port",
"occupied",
"1143",
"1025",
"SMTP",
"IMAP",
"error"
]
},
{
"index": 10,
"url": "https://proton.me/support/clients-supported-bridge",
"title": "Email clients supported by Proton Mail Bridge",
"keywords": [
"client",
"Outlook",
"Thunderbird",
"Apple Mail",
"EM Client",
"The Bat",
"Eudora",
"Postbox",
"Canary",
"Spark"
]
},
{
"index": 11,
"url": "https://proton.me/support/imap-smtp-and-pop3-setup",
"title": "IMAP, SMTP, and POP3 setup",
"keywords": [
"IMAP",
"SMTP",
"setup",
"set up",
"configure",
"configuration",
"parameters"
]
},
{
"index": 12,
"url": "https://proton.me/support/protonmail-bridge-install",
"title": "How to install Proton Mail Bridge",
"keywords": [
"install",
"installer",
"setup",
"download",
"windows",
"mac",
"macos",
"linux"
]
},
{
"index": 13,
"url": "https://proton.me/support/bridge-for-linux",
"title": "Proton Mail Bridge for Linux",
"keywords": [
"Linux",
"Ubuntu",
"Fedora",
"Debian",
"Unix",
"deb",
"rpm",
"CentOS",
"Arch",
"Mint"
]
},
{
"index": 14,
"url": "https://proton.me/support/operating-systems-supported-bridge",
"title": "System requirements for Proton Mail Bridge",
"keywords": [
"requirement",
"cpu",
"memory",
"Windows",
"macOS",
"linux"
]
},
{
"index": 15,
"url": "https://proton.me/support/protonmail-bridge-configure-client",
"title": "How to configure your email client for Proton Mail Bridge",
"keywords": [
"Client",
"Outlook",
"configure",
"setup",
"application",
"setup",
"IMAP",
"SMTP"
]
},
{
"index": 16,
"url": "https://proton.me/support/invalid-password-error-setting-email-client",
"title": "Invalid password error while setting up email client",
"keywords": [
"password",
"invalid",
"error"
]
},
{
"index": 17,
"url": "https://proton.me/support/protonmail-bridge-clients-windows-outlook-2019",
"title": "Proton Mail Bridge Microsoft Outlook for Windows 2019 setup guide",
"keywords": [
"Outlook",
"2019",
"setup",
"configuration"
]
},
{
"index": 18,
"url": "https://proton.me/support/protonmail-bridge-clients-windows-outlook-2016",
"title": "Proton Mail Bridge Microsoft Outlook 2016 for Windows setup guide",
"keywords": [
"Outlook",
"2019",
"setup",
"configuration"
]
},
{
"index": 19,
"url": "https://proton.me/support/protonmail-bridge-clients-apple-mail",
"title": "Proton Mail Bridge Apple Mail setup guide",
"keywords": [
"Apple Mail",
"setup",
"configuration"
]
},
{
"index": 20,
"url": "https://proton.me/support/protonmail-bridge-clients-macos-new-outlook",
"title": "Proton Mail Bridge new Outlook for macOS setup guide",
"keywords": [
"Outlook",
"setup",
"configuration"
]
},
{
"index": 21,
"url": "https://proton.me/support/protonmail-bridge-clients-windows-thunderbird",
"title": "Proton Mail Bridge Thunderbird setup guide for Windows, macOS, and Linux",
"keywords": [
"Thunderbird",
"setup",
"configuration"
]
},
{
"index": 22,
"url": "https://proton.me/support/protonmail-bridge-clients-macos-outlook-2016",
"title": "Proton Mail Bridge Microsoft Outlook 2016 for macOS setup guide",
"keywords": [
"Outlook 2016",
"macOS"
]
},
{
"index": 23,
"url": "https://proton.me/support/protonmail-bridge-clients-macos-outlook-2019",
"title": "Proton Mail Bridge Microsoft Outlook 2019 for macOS setup guide",
"keywords": [
"Outlook 2019",
"macOS"
]
},
{
"index": 24,
"url": "https://proton.me/support/protonmail-bridge-clients-windows-outlook-2013",
"title": "Proton Mail Bridge Microsoft Outlook 2013 for Windows setup guide",
"keywords": [
"Outlook 2013",
"macOS"
]
},
{
"index": 25,
"url": "https://proton.me/support/protonmail-bridge-clients-macos-outlook-2011",
"title": "Proton Mail Bridge Microsoft Outlook 2011 for macOS setup guide",
"keywords": [
"Outlook 2011",
"macOS"
]
},
{
"index": 26,
"url": "https://proton.me/support/install-bridge-linux-pkgbuild-file",
"title": "Installing Proton Mail Bridge for Linux using a PKGBUILD file",
"keywords": [
"Linux",
"pkgbuild"
]
},
{
"index": 27,
"url": "https://proton.me/support/installing-bridge-linux-deb-file",
"title": "Installing Proton Mail Bridge for Linux using a DEB file",
"keywords": [
"Linux",
"deb"
]
},
{
"index": 28,
"url": "https://proton.me/support/verifying-bridge-package",
"title": "Verifying the Proton Mail Bridge package for Linux",
"keywords": [
"Linux",
"Package",
"Verify"
]
},
{
"index": 29,
"url": "https://proton.me/support/bridge-cli-guide",
"title": "Bridge CLI (command line interface) guide",
"keywords": [
"CLI",
"Terminal",
"Command-line",
"Powershell"
]
},
{
"index": 30,
"url": "https://proton.me/support/install-bridge-linux-rpm-file",
"title": "Installing Proton Mail Bridge for Linux using an RPM file",
"keywords": [
"Linux",
"Install",
"RPM"
]
},
{
"index": 31,
"url": "https://proton.me/support/bridge-linux-login-error",
"title": "How to fix Proton Bridge login errors",
"keywords": [
"login",
"error",
"connection"
]
},
{
"index": 32,
"url": "https://proton.me/support/bridge-linux-tray-icon",
"title": "How to fix a missing system tray icon in Linux",
"keywords": [
"linux",
"tray",
"notification"
]
},
{
"index": 33,
"url": "https://proton.me/support/why-you-need-bridge",
"title": "Why you need Proton Mail Bridge",
"keywords": [
"Bridge",
"email",
"client"
]
},
{
"index": 34,
"url": "https://proton.me/support/protonmail-bridge-manual-update",
"title": "How to manually update Proton Mail Bridge",
"keywords": [
"update",
"upgrade",
"manual",
"download"
]
},
{
"index": 35,
"url": "https://proton.me/support/macos-certificate-warning",
"title": "Warning when installing Proton Mail Bridge on macOS",
"keywords": [
"install",
"macOS",
"warning",
"certificate"
]
},
{
"index": 36,
"url": "https://proton.me/support/apple-mail-certificate",
"title": "Why you need to install a certificate for Apple Mail with Proton Mail Bridge",
"keywords": [
"certificate",
"Apple Mail",
"macOS"
]
}
]

107
internal/kb/suggester.go Normal file
View File

@ -0,0 +1,107 @@
// 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 kb
import (
_ "embed"
"encoding/json"
"errors"
"regexp"
"strings"
"github.com/bradenaw/juniper/xslices"
"golang.org/x/exp/slices"
)
var ErrArticleNotFound = errors.New("KB article not found")
//go:embed kbArticleList.json
var articleListString []byte
// Article is a struct that holds information about a knowledge-base article.
type Article struct {
Index uint64 `json:"index"`
URL string `json:"url"`
Title string `json:"title"`
Keywords []string `json:"keywords"`
Score int
}
type ArticleList []*Article
// GetArticleList returns the list of KB articles.
func GetArticleList() (ArticleList, error) {
var articles ArticleList
err := json.Unmarshal(articleListString, &articles)
return articles, err
}
// GetSuggestions returns a list of up to 3 suggestions for the built-in list of KB articles matching the given user input.
func GetSuggestions(userInput string) (ArticleList, error) {
articles, err := GetArticleList()
if err != nil {
return ArticleList{}, err
}
return GetSuggestionsFromArticleList(userInput, articles)
}
// GetSuggestionsFromArticleList returns a list of up to 3 suggestions for the given list of KB articles matching the given user input.
func GetSuggestionsFromArticleList(userInput string, articles ArticleList) (ArticleList, error) {
userInput = strings.ToUpper(userInput)
for _, article := range articles {
for _, keyword := range article.Keywords {
if strings.Contains(userInput, strings.ToUpper(keyword)) {
article.Score++
}
}
}
articles = xslices.Filter(articles, func(article *Article) bool { return article.Score > 0 })
slices.SortFunc(articles, func(lhs, rhs *Article) bool { return lhs.Score > rhs.Score })
if len(articles) > 3 {
return articles[:3], nil
}
return articles, nil
}
// GetArticleIndex retrieves the index of an article from its url. if the article is not found, ErrArticleNotFound is returned.
func GetArticleIndex(url string) (uint64, error) {
articles, err := GetArticleList()
if err != nil {
return 0, err
}
index := xslices.IndexFunc(articles, func(article *Article) bool { return strings.EqualFold(article.URL, url) })
if index == -1 {
return 0, ErrArticleNotFound
}
return uint64(index), nil
}
func simplifyUserInput(input string) string {
// replace any sequence not matching of the following with a single space:
// - letters in any language (accentuated or not)
// - numbers
// - the apostrophe character '
return strings.TrimSpace(regexp.MustCompile(`[^\p{L}\p{N}']+`).ReplaceAllString(input, " "))
}

View File

@ -0,0 +1,91 @@
// 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 kb
import (
"testing"
"github.com/stretchr/testify/require"
)
func Test_ArticleList(t *testing.T) {
articles, err := GetArticleList()
require.NoError(t, err)
require.NotEmpty(t, articles)
var bits uint64
for _, article := range articles {
require.Truef(t, article.Index < 64, "Invalid KB article index %d, (must be < 64)", article.Index)
require.Zerof(t, bits&(1<<article.Index), "Duplicate index %d in knowledge base", article.Index)
bits |= bits | (1 << article.Index)
require.NotEmpty(t, article.URL, "KB article with index %d has no URL", article.Index)
require.NotEmpty(t, article.Title, "KB article with index %d has no title", article.Index)
require.NotEmpty(t, article.Keywords, "KB article with index %d has no keyword", article.Index)
}
}
func Test_GetSuggestions(t *testing.T) {
suggestions, err := GetSuggestions("Thunderbird is not working, error during password")
require.NoError(t, err)
count := len(suggestions)
require.True(t, (count > 0) && (count <= 3))
suggestions, err = GetSuggestions("Supercalifragilisticexpialidocious Sesquipedalian Worcestershire")
require.NoError(t, err)
require.Empty(t, suggestions)
}
func Test_GetSuggestionsFromArticleList(t *testing.T) {
articleList := ArticleList{}
suggestions, err := GetSuggestionsFromArticleList("Thunderbird", articleList)
require.NoError(t, err)
require.Empty(t, suggestions)
articleList = ArticleList{
&Article{
Index: 0,
URL: "https://proton.me",
Title: "Proton home page",
Keywords: []string{"proton"},
},
&Article{
Index: 1,
URL: "https://mozilla.org",
Title: "Mozilla home page",
Keywords: []string{"mozilla"},
},
}
suggestions, err = GetSuggestionsFromArticleList("PRoToN", articleList)
require.NoError(t, err)
require.Len(t, suggestions, 1)
require.Equal(t, suggestions[0].URL, "https://proton.me")
}
func Test_GetArticleIndex(t *testing.T) {
index1, err := GetArticleIndex("https://proton.me/support/bridge-for-linux")
require.NoError(t, err)
index2, err := GetArticleIndex("HTTPS://PROTON.ME/support/bridge-for-linux")
require.NoError(t, err)
require.Equal(t, index1, index2)
_, err = GetArticleIndex("https://proton.me")
require.ErrorIs(t, err, ErrArticleNotFound)
}
func Test_simplifyUserInput(t *testing.T) {
require.Equal(t, "word1 ñóÄ don't déjà 33 pizza", simplifyUserInput(" \nword1 \n\tñóÄ don't\n\n\ndéjà, 33 pizza=🍕\n,\n"))
}

View File

@ -15,18 +15,18 @@
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
//go:build !windows
// +build !windows
package sentry
import "os"
import (
"github.com/jeandeaual/go-locale"
"github.com/sirupsen/logrus"
)
func GetSystemLang() string {
lang := os.Getenv("LC_ALL")
if lang == "" {
lang = os.Getenv("LANG")
lang, err := locale.GetLanguage()
if err != nil {
logrus.WithError(err).Error("Failed to get system language")
lang = "Unknown"
}
return lang
}

View File

@ -1,67 +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/>.
//go:build windows
// +build windows
package sentry
import (
"syscall"
"unsafe"
)
const (
defaultLocaleUser = "GetUserDefaultLocaleName" // https://learn.microsoft.com/en-us/windows/win32/api/winnls/nf-winnls-getuserdefaultlocalename
defaultLocaleSystem = "GetSystemDefaultLocaleName" // https://learn.microsoft.com/en-us/windows/win32/api/winnls/nf-winnls-getsystemdefaultlocalename
localeNameMaxLength = 85 // https://learn.microsoft.com/en-us/windows/win32/intl/locale-name-constants
)
func getLocale(dll *syscall.DLL, procName string) (string, error) {
proc, err := dll.FindProc(procName)
if err != nil {
return "errProc", err
}
b := make([]uint16, localeNameMaxLength)
r, _, err := proc.Call(uintptr(unsafe.Pointer(&b[0])), uintptr(localeNameMaxLength))
if r == 0 || err != nil {
return "errCall", err
}
return syscall.UTF16ToString(b), nil
}
func GetSystemLang() string {
dll, err := syscall.LoadDLL("kernel32")
if err != nil {
return "errDll"
}
defer func() {
_ = dll.Release()
}()
if lang, err := getLocale(dll, defaultLocaleUser); err == nil {
return lang
}
lang, _ := getLocale(dll, defaultLocaleSystem)
return lang
}

View File

@ -678,17 +678,27 @@ func (s *Connector) importMessage(
if err := s.identityState.WithAddrKR(s.addrID, func(_, addrKR *crypto.KeyRing) error {
var messageID string
p, err2 := parser.New(bytes.NewReader(literal))
if err2 != nil {
return fmt.Errorf("failed to parse literal: %w", err2)
}
if slices.Contains(labelIDs, proton.DraftsLabel) {
msg, err := s.createDraft(ctx, literal, addrKR, addr)
msg, err := s.createDraftWithParser(ctx, p, addrKR, addr)
if err != nil {
return fmt.Errorf("failed to create draft: %w", err)
}
// apply labels
messageID = msg.ID
} else {
// multipart body requires at least one text part to be properly encrypted.
if p.AttachEmptyTextPartIfNoneExists() {
buf := new(bytes.Buffer)
if err := p.NewWriter().Write(buf); err != nil {
return fmt.Errorf("failed build new MIMEBody: %w", err)
}
literal = buf.Bytes()
}
str, err := s.client.ImportMessages(ctx, addrKR, 1, 1, []proton.ImportReq{{
Metadata: proton.ImportMetadata{
AddressID: s.addrID,
@ -728,13 +738,7 @@ func (s *Connector) importMessage(
return toIMAPMessage(full.MessageMetadata), literal, nil
}
func (s *Connector) createDraft(ctx context.Context, literal []byte, addrKR *crypto.KeyRing, sender proton.Address) (proton.Message, error) {
// Create a new message parser from the reader.
parser, err := parser.New(bytes.NewReader(literal))
if err != nil {
return proton.Message{}, fmt.Errorf("failed to create parser: %w", err)
}
func (s *Connector) createDraftWithParser(ctx context.Context, parser *parser.Parser, addrKR *crypto.KeyRing, sender proton.Address) (proton.Message, error) {
message, err := message.ParseWithParser(parser, true)
if err != nil {
return proton.Message{}, fmt.Errorf("failed to parse message: %w", err)

View File

@ -274,6 +274,10 @@ func (s *Service) HandleRefreshEvent(ctx context.Context, _ proton.RefreshFlag)
return err
}
if err := s.rebuildConnectors(); err != nil {
return err
}
if err := s.syncStateProvider.ClearSyncStatus(ctx); err != nil {
return fmt.Errorf("failed to clear sync status:%w", err)
}
@ -292,6 +296,7 @@ func (s *Service) HandleUserEvent(_ context.Context, user *proton.User) error {
return s.identityState.Write(func(identity *useridentity.State) error {
identity.OnUserEvent(*user)
return nil
})
}

View File

@ -24,12 +24,18 @@ import (
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/proton-bridge/v3/internal/services/useridentity"
"github.com/ProtonMail/proton-bridge/v3/internal/usertypes"
"github.com/sirupsen/logrus"
)
func (s *Service) HandleAddressEvents(ctx context.Context, events []proton.AddressEvent) error {
s.log.Debug("handling address event")
if s.addressMode == usertypes.AddressModeCombined {
oldPrimaryAddr, err := s.identityState.GetPrimaryAddress()
if err != nil {
return fmt.Errorf("failed to get primary addr: %w", err)
}
if err := s.identityState.Write(func(identity *useridentity.State) error {
identity.OnAddressEvents(events)
return nil
@ -38,6 +44,28 @@ func (s *Service) HandleAddressEvents(ctx context.Context, events []proton.Addre
return err
}
newPrimaryAddr, err := s.identityState.GetPrimaryAddress()
if err != nil {
return fmt.Errorf("failed to get primary addr after update: %w", err)
}
if oldPrimaryAddr.ID == newPrimaryAddr.ID {
return nil
}
connector, ok := s.connectors[oldPrimaryAddr.ID]
if !ok {
return fmt.Errorf("could not find old primary addr conncetor after default address change")
}
s.connectors[newPrimaryAddr.ID] = connector
delete(s.connectors, oldPrimaryAddr.ID)
s.log.WithFields(logrus.Fields{
"old": oldPrimaryAddr.Email,
"new": newPrimaryAddr.Email,
}).Debug("Primary address changed")
return nil
}

View File

@ -19,15 +19,13 @@ package imapservice
import (
"context"
"sync"
"time"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
)
type syncReporter struct {
userID string
eventPublisher events.EventPublisher
type syncData struct {
start time.Time
total int64
count int64
@ -36,8 +34,25 @@ type syncReporter struct {
freq time.Duration
}
type syncReporter struct {
userID string
eventPublisher events.EventPublisher
dataLock sync.Mutex
data syncData
}
func (rep *syncReporter) withData(f func(s *syncData)) {
rep.dataLock.Lock()
defer rep.dataLock.Unlock()
f(&rep.data)
}
func (rep *syncReporter) OnStart(ctx context.Context) {
rep.start = time.Now()
rep.withData(func(s *syncData) {
s.start = time.Now()
})
rep.eventPublisher.PublishEvent(ctx, events.SyncStarted{UserID: rep.userID})
}
@ -55,35 +70,38 @@ func (rep *syncReporter) OnError(ctx context.Context, err error) {
}
func (rep *syncReporter) OnProgress(ctx context.Context, delta int64) {
rep.count += delta
rep.withData(func(s *syncData) {
s.count += delta
var progress float64
var remaining time.Duration
var progress float64
var remaining time.Duration
// It's possible for count to be bigger or smaller than total depending on when the sync begins and whether new
// messages are added/removed during this period. When this happens just limited the progress to 100%.
if s.count > s.total {
progress = 1
} else {
progress = float64(s.count) / float64(s.total)
remaining = time.Since(s.start) * time.Duration(s.total-(s.count+1)) / time.Duration(s.count+1)
}
// It's possible for count to be bigger or smaller than total depending on when the sync begins and whether new
// messages are added/removed during this period. When this happens just limited the progress to 100%.
if rep.count > rep.total {
progress = 1
} else {
progress = float64(rep.count) / float64(rep.total)
remaining = time.Since(rep.start) * time.Duration(rep.total-(rep.count+1)) / time.Duration(rep.count+1)
}
if time.Since(s.last) > s.freq {
rep.eventPublisher.PublishEvent(ctx, events.SyncProgress{
UserID: rep.userID,
Progress: progress,
Elapsed: time.Since(s.start),
Remaining: remaining,
})
if time.Since(rep.last) > rep.freq {
rep.eventPublisher.PublishEvent(ctx, events.SyncProgress{
UserID: rep.userID,
Progress: progress,
Elapsed: time.Since(rep.start),
Remaining: remaining,
})
rep.last = time.Now()
}
s.last = time.Now()
}
})
}
func (rep *syncReporter) InitializeProgressCounter(_ context.Context, current int64, total int64) {
rep.count = current
rep.total = total
rep.withData(func(s *syncData) {
s.count = current
s.total = total
})
}
func newSyncReporter(userID string, eventsPublisher events.EventPublisher, freq time.Duration) *syncReporter {
@ -91,7 +109,9 @@ func newSyncReporter(userID string, eventsPublisher events.EventPublisher, freq
userID: userID,
eventPublisher: eventsPublisher,
start: time.Now(),
freq: freq,
data: syncData{
start: time.Now(),
freq: freq,
},
}
}

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