Compare commits

...

96 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
3210709810 chore: Wakato Bridge 3.7.1 changelog. 2023-11-20 11:56:03 +01:00
8fd988d7c5 fix(GODT-3054): Only delete drafts after message has been Sent
When editing a draft created by Apple Mail on the web client and then
later sending the draft with Apple Mail, we need to delete the draft
ourselves, or it will remain in the Draft folder.

This patch makes sure that the deletion of said draft only occurs after
the message was successfully sent.
2023-11-20 10:37:04 +01:00
bf89d548d3 fix(GODT-2576): Correctly handle Forwarded messages from Thunderbird
Thunderbird uses `In-Reply-To` with `X-Forwarded-Message-Id` to signal
to the SMTP server that it is forwarding a message.
2023-11-16 16:17:54 +01:00
51229cbb68 feat(GODT-3122): added test, changed interface for accessing display name. 2023-11-16 10:44:59 +00:00
36c5c37dac fix(GODT-3122): use display name as 'Email Account Name' in macOS profile. 2023-11-16 10:44:59 +00:00
5a434fafbc fix(GODT-3125): Heartbeat crash on exit
Ensure that the heartbeat background task is stopped before we close
the users as it accesses data within these instances.

Additionally, we also make sure that when telemetry is disabled, we stop
the background task.

Finally, `HeartbeatManager` now specifies what the desired interval is
so we can better configure the test cases.
2023-11-16 11:05:40 +01:00
ea1c2534df fix(GODT-2617): Validate user can send from the SMTP sender address
https://github.com/ProtonMail/go-proton-api/pull/126
2023-11-15 14:13:21 +01:00
1cafbfcaaa chore: Wakato Bridge 3.7.1 changelog. 2023-11-15 12:54:18 +01:00
2d44ccaee0 fix(GODT-3123): Trigger bad event on empty EventID on existing accounts
See `checkIrrecoverableEventID` for more details.
2023-11-15 11:06:51 +01:00
96517b7fb1 chore: Remove debug prints 2023-11-15 09:09:07 +01:00
bc381407a7 feat(GODT-2576): Forward and $Forward Flag Support
When an IMAP client stores the `Forward` or `$Forward` flags on a
message, the forwarded state is now correctly represented on the Proton
servers.

https://github.com/ProtonMail/go-proton-api/pull/125
https://github.com/ProtonMail/gluon/pull/400
2023-11-15 07:51:00 +01:00
ddc5e775b9 fix(GODT-3118): Do not reset EventID when migrating sync settings 2023-11-14 07:03:28 +00:00
ea26188dc0 fix(GODT-2277): Fix keychains initialisation in vault-editor (for write as well). 2023-11-13 15:37:32 +01:00
159e1cee7d fix(GODT-2277): Fix keychains initialisation in vault-editor. 2023-11-13 13:58:03 +00:00
4394ad0e9b feat(GODT-3053): use smaller bridge window on small screens. 2023-11-10 14:23:41 +00:00
856bdd1321 fix(GODT-3116): Panic on closed channel
If sync finishes during shutdown, check if there is a context error in
the deferred go routine before rewinding the event.
2023-11-10 14:47:03 +01:00
ff288145df fix(GODT-1623): Throttle SMTP failed requests
If a SMPT client keeps hammering bridge and triggers multiple successive
errors in quick succession, force that client to wait 20 seconds before
trying again.
2023-11-10 12:54:38 +00:00
83bbdbd63e feat(GODT-3113): Only force UTF-8 charset for HTML part when needed. 2023-11-10 12:50:15 +00:00
fa430ee0fb fix(GODT-3047): fixed 'disk full' error message. 2023-11-10 08:57:53 +00:00
0303ba38e8 feat(GODT-3113): Do not render HTML for attachment. 2023-11-10 08:36:46 +00:00
2a78b5c144 feat(GODT-3112): replaced error message when bridge exists prematurely. Added a link to support form. 2023-11-09 12:52:31 +00:00
a00b3cdb92 fix(GODT-3054): Delete draft create from reply
If an IMAP client creates a new message as a reply/forward from an
existing draft, that draft will be deleted once the message has been
sent.

Other than not being the correct behavior, the original reason for which
this line of code was added (carried over from v2), seems to be no longer
necessary as in all tests, the message is correctly removed from the
drafts folder after sent.
2023-11-09 13:24:38 +01:00
8d3e04679f feat(GODT-3010): Do not log error when no MimeType provided to lower the noise. 2023-11-09 09:45:40 +00:00
21ff7b4b97 feat(GODT-2947): Remove 'blame it on the weather' error part from go-smtp. 2023-11-09 09:45:02 +00:00
4ea161f7ad chore(GODT-3010): Log MimeType parsing issue. 2023-11-08 16:21:19 +00:00
dc584ea29b feat(GODT-3104): added log entry for cert install status on startup on macOS. 2023-11-08 16:30:50 +01:00
4a01c46aed fix(GODT-3048): WKD Policy behavior
Ensure Bridge respects the no encrypt setting on a contact which has a
WKD key.
2023-11-08 14:23:36 +01:00
e8d9534b9c feat(GODT-2277): Move Keychain helpers creation in main. 2023-11-08 13:05:57 +00:00
96904b160f test(GODT-2740): Sending Plain text messages to internal recipient 2023-11-07 10:02:26 +00:00
b535be72f8 test(GODT-2892): Create fake log file 2023-11-07 07:21:26 +00:00
199 changed files with 8579 additions and 3724 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)
@ -133,6 +134,7 @@ Proton Mail Bridge includes the following 3rd party software:
* [yaml](https://gopkg.in/yaml.v3) available under [license](https://github.com/go-yaml/yaml/blob/v3.0.1/LICENSE)
* [docker-credential-helpers](https://github.com/ProtonMail/docker-credential-helpers) available under [license](https://github.com/ProtonMail/docker-credential-helpers/blob/master/LICENSE)
* [go-message](https://github.com/ProtonMail/go-message) available under [license](https://github.com/ProtonMail/go-message/blob/master/LICENSE)
* [go-smtp](https://github.com/ProtonMail/go-smtp) available under [license](https://github.com/ProtonMail/go-smtp/blob/master/LICENSE)
* [resty](https://github.com/LBeernaertProton/resty/v2) available under [license](https://github.com/LBeernaertProton/resty/v2/blob/master/LICENSE)
* [go-keychain](https://github.com/cuthix/go-keychain) available under [license](https://github.com/cuthix/go-keychain/blob/master/LICENSE)
<!-- END AUTOGEN -->

View File

@ -3,6 +3,93 @@
Changelog [format](http://keepachangelog.com/en/1.0.0/)
## Xikou Bridge 3.8.2
### Fixed
* GODT-3235: Update bridge update key.
## Xikou Bridge 3.8.1
### Added
* GODT-3121: Suggest relevant KB articles in the in-app bug report form.
* GODT-2001: Add govulncheck to scan for vulnerabilities.
### Changed
* Keep nighlty-job log as artifact.
* Test: Improve TestMetadata_JobCorrectlyFinishesAfterCancel.
### Fixed
* GODT-3153: Do not take into account full address when hasing messages.
## Xikou Bridge 3.8.0
### Added
* Test: Add test scenarios to add an /Answered flag to a replied message and revert.
* GODT-3046: Added links to KB in error messages.
* Test(GODT-3113): Inline HTML message and HTML attachment is getting altered.
* Test(GODT-3124): Attempt to fix 401 during login.
### Changed
* GODT-3134: Br tag triggers installer.
* Added update events to bridge GUI tester.
### Fixed
* GODT-3142: Pass br tag if available.
* GODT-3151: Fix feature test with non modified HTML part.
* GODT-3151: Only modify HTML Meta content if UTF-8 charset override is needed.
* GODT-2851: Add empty text part if no text part when importing multipart.
* GODT-3102: Distinguish Vault Decryption from Serialization Errors.
* GODT-3124: Handling of sync child jobs.
* GODT-3148: Bump go-sysinfo to get rid of linker warning on macOS Sonoma.
* GODT-3124: Flaky tests.
* GODT-3022: Handle multipart/related on fake server.
* GODT-3133: Fix GetSystemLanguage.
* GODT-3124: Race condition in sync task waiter.
* GODT-3124: Race conditions reported by race check.
* GODT-2797: Encode attached key name and use same pubkey name as web-app.
* Fix case of IMAP login error.
* GODT-3132: Do not allow sending on disabled accounts.
* GODT-3046: fix typo spotted during KB article review.
* GODT-3129: Bad Event during after address order change.
* GODT-3117: Improve GetAllContacts and GetAllContactsEmail.
## Wakato Bridge 3.7.1
### Added
* Test(GODT-2740): Sending Plain text messages to internal recipient.
* Test(GODT-2892): Create fake log file.
* GODT-3122: Added test, changed interface for accessing display name.
### Changed
* Remove debug prints.
* GODT-2576: Forward and $Forward Flag Support.
* GODT-3053: Use smaller bridge window on small screens.
* GODT-3113: Only force UTF-8 charset for HTML part when needed.
* GODT-3113: Do not render HTML for attachment.
* GODT-3112: Replaced error message when bridge exists prematurely. Added a link to support form.
* GODT-2947: Remove 'blame it on the weather' error part from go-smtp.
* GODT-3010: Log MimeType parsing issue.
* GODT-3104: Added log entry for cert install status on startup on macOS.
* GODT-2277: Move Keychain helpers creation in main.
### Fixed
* GODT-3054: Only delete drafts after message has been Sent.
* GODT-2576: Correctly handle Forwarded messages from Thunderbird.
* GODT-3122: Use display name as 'Email Account Name' in macOS profile.
* GODT-3125: Heartbeat crash on exit.
* GODT-2617: Validate user can send from the SMTP sender address.
* GODT-3123: Trigger bad event on empty EventID on existing accounts.
* GODT-3118: Do not reset EventID when migrating sync settings.
* GODT-3116: Panic on closed channel.
* GODT-1623: Throttle SMTP failed requests.
* GODT-3047: Fixed 'disk full' error message.
* GODT-3054: Delete draft create from reply.
* GODT-3048: WKD Policy behavior.
## Wakato Bridge 3.7.0
### Added

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.0+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*

10
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.20231025125916-5c7941465df8
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.20231106093533-5f248dfc820d
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
@ -121,6 +122,7 @@ require (
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/go-resty/resty/v2 => github.com/LBeernaertProton/resty/v2 v2.0.0-20231030122409-92db8bee3605
github.com/emersion/go-smtp => github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865
github.com/go-resty/resty/v2 => github.com/LBeernaertProton/resty/v2 v2.0.0-20231129100320-dddf8030d93a
github.com/keybase/go-keychain => github.com/cuthix/go-keychain v0.0.0-20230517073537-fc1740a83768
)

63
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.20231025125916-5c7941465df8 h1:sG0o5pEoS2z2jNR9zK7Juq5Tr3X+GfHmQ8L99RPowaE=
github.com/ProtonMail/gluon v0.17.1-0.20231025125916-5c7941465df8/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,10 @@ 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.20231106093533-5f248dfc820d h1:LI2kvxBisX19f7lyMh0H6NcAHHg/Y7/x/xZWtxVrXOc=
github.com/ProtonMail/go-proton-api v0.4.1-0.20231106093533-5f248dfc820d/go.mod h1:WEXJqj5DSc2YI77SgXdpMY0nk33Qy92Vu2r4tOEazA8=
github.com/ProtonMail/go-proton-api v0.4.1-0.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=
github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk=
github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton h1:8tqHYM6IGsdEc6Vxf1TWiwpHNj8yIEQNACPhxsDagrk=
@ -48,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=
@ -111,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=
@ -122,8 +132,6 @@ github.com/emersion/go-imap-id v0.0.0-20190926060100-f94a56b9ecde/go.mod h1:sPwp
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead h1:fI1Jck0vUrXT8bnphprS1EoVRe2Q5CKCX8iDlpqjQ/Y=
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-smtp v0.15.1-0.20221021114529-49b17434419d h1:hFRM6zCBSc+Xa0rBOqSlG6Qe9dKC/2vLhGAuZlWxTsc=
github.com/emersion/go-smtp v0.15.1-0.20221021114529-49b17434419d/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3 h1:hQ1wTMaKcGfobYRT88RM8NFNyX+IQHvagkm/tqViU98=
@ -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

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

View File

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

View File

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

View File

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

View File

@ -22,6 +22,7 @@ import (
"path"
"github.com/ProtonMail/gluon/async"
"github.com/ProtonMail/proton-bridge/v3/internal/certs"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
@ -29,30 +30,37 @@ import (
"github.com/sirupsen/logrus"
)
func WithVault(locations *locations.Locations, panicHandler async.PanicHandler, fn func(*vault.Vault, bool, bool) error) error {
func WithVault(locations *locations.Locations, keychains *keychain.List, panicHandler async.PanicHandler, fn func(*vault.Vault, bool, bool) error) error {
logrus.Debug("Creating vault")
defer logrus.Debug("Vault stopped")
// Create the encVault.
encVault, insecure, corrupt, err := newVault(locations, panicHandler)
encVault, insecure, corrupt, err := newVault(locations, keychains, panicHandler)
if err != nil {
return fmt.Errorf("could not create vault: %w", err)
}
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, 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")
@ -62,7 +70,7 @@ func newVault(locations *locations.Locations, panicHandler async.PanicHandler) (
insecure bool
)
if key, err := loadVaultKey(vaultDir); err != nil {
if key, err := loadVaultKey(vaultDir, keychains); err != nil {
logrus.WithError(err).Error("Could not load/create vault key")
insecure = true
@ -74,24 +82,24 @@ func newVault(locations *locations.Locations, panicHandler async.PanicHandler) (
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
}
func loadVaultKey(vaultDir string) ([]byte, error) {
func loadVaultKey(vaultDir string, keychains *keychain.List) ([]byte, error) {
helper, err := vault.GetHelper(vaultDir)
if err != nil {
return nil, fmt.Errorf("could not get keychain helper: %w", err)
}
kc, err := keychain.NewKeychain(helper, constants.KeyChainName)
kc, err := keychain.NewKeychain(helper, constants.KeyChainName, keychains.GetHelpers(), keychains.GetDefaultHelper())
if err != nil {
return nil, fmt.Errorf("could not create keychain: %w", err)
}

View File

@ -45,6 +45,7 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/telemetry"
"github.com/ProtonMail/proton-bridge/v3/internal/user"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
"github.com/bradenaw/juniper/xslices"
"github.com/go-resty/resty/v2"
"github.com/sirupsen/logrus"
@ -74,7 +75,7 @@ type Bridge struct {
installCh chan installJob
// heartbeat is the telemetry heartbeat for metrics.
heartbeat telemetry.Heartbeat
heartbeat *heartBeatState
// curVersion is the current version of the bridge,
// newVersion is the version that was installed by the updater.
@ -82,6 +83,9 @@ type Bridge struct {
newVersion *semver.Version
newVersionLock safe.RWMutex
// keychains is the utils that own usable keychains found in the OS.
keychains *keychain.List
// focusService is used to raise the bridge window when needed.
focusService *focus.Service
@ -124,9 +128,6 @@ type Bridge struct {
// goUpdate triggers a check/install of updates.
goUpdate func()
// goHeartbeat triggers a check/sending if heartbeat is needed.
goHeartbeat func()
serverManager *imapsmtpserver.Service
syncService *syncservice.Service
}
@ -138,6 +139,7 @@ func New(
autostarter Autostarter, // the autostarter to manage autostart settings
updater Updater, // the updater to fetch and install updates
curVersion *semver.Version, // the current version of the bridge
keychains *keychain.List, // usable keychains
apiURL string, // the URL of the API to use
cookieJar http.CookieJar, // the cookie jar to use
@ -148,6 +150,7 @@ func New(
panicHandler async.PanicHandler,
reporter reporter.Reporter,
uidValidityGenerator imap.UIDValidityGenerator,
heartBeatManager telemetry.HeartbeatManager,
logIMAPClient, logIMAPServer bool, // whether to log IMAP client/server activity
logSMTP bool, // whether to log SMTP activity
@ -163,6 +166,7 @@ func New(
// bridge is the bridge.
bridge, err := newBridge(
context.Background(),
tasks,
imapEventCh,
@ -171,6 +175,7 @@ func New(
autostarter,
updater,
curVersion,
keychains,
panicHandler,
reporter,
@ -178,6 +183,7 @@ func New(
identifier,
proxyCtl,
uidValidityGenerator,
heartBeatManager,
logIMAPClient, logIMAPServer, logSMTP,
)
if err != nil {
@ -196,6 +202,7 @@ func New(
}
func newBridge(
ctx context.Context,
tasks *async.Group,
imapEventCh chan imapEvents.Event,
@ -204,6 +211,7 @@ func newBridge(
autostarter Autostarter,
updater Updater,
curVersion *semver.Version,
keychains *keychain.List,
panicHandler async.PanicHandler,
reporter reporter.Reporter,
@ -211,6 +219,7 @@ func newBridge(
identifier identifier.Identifier,
proxyCtl ProxyController,
uidValidityGenerator imap.UIDValidityGenerator,
heartbeatManager telemetry.HeartbeatManager,
logIMAPClient, logIMAPServer, logSMTP bool,
) (*Bridge, error) {
@ -256,9 +265,13 @@ func newBridge(
newVersion: curVersion,
newVersionLock: safe.NewRWMutex(),
keychains: keychains,
panicHandler: panicHandler,
reporter: reporter,
heartbeat: newHeartBeatState(ctx, panicHandler),
focusService: focusService,
autostarter: autostarter,
locator: locator,
@ -288,7 +301,13 @@ func newBridge(
return nil, err
}
bridge.syncService.Run(bridge.tasks)
if heartbeatManager == nil {
bridge.heartbeat.init(bridge, bridge)
} else {
bridge.heartbeat.init(bridge, heartbeatManager)
}
bridge.syncService.Run()
return bridge, nil
}
@ -417,6 +436,9 @@ func (bridge *Bridge) GetErrors() []error {
func (bridge *Bridge) Close(ctx context.Context) {
logrus.Info("Closing bridge")
// Stop heart beat before closing users.
bridge.heartbeat.stop()
// Close all users.
safe.Lock(func() {
for _, user := range bridge.users {
@ -429,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

@ -49,6 +49,7 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/user"
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
"github.com/ProtonMail/proton-bridge/v3/tests"
"github.com/bradenaw/juniper/xslices"
imapid "github.com/emersion/go-imap-id"
@ -950,6 +951,7 @@ func withBridgeNoMocks(
mocks.Autostarter,
mocks.Updater,
v2_3_0,
keychain.NewTestKeychainsList(),
// The API stuff.
apiURL,
@ -961,6 +963,7 @@ func withBridgeNoMocks(
mocks.CrashHandler,
mocks.Reporter,
testUIDValidityGenerator,
mocks.Heartbeat,
// The logging stuff.
os.Getenv("BRIDGE_LOG_IMAP_CLIENT") == "1",
@ -970,9 +973,6 @@ func withBridgeNoMocks(
require.NoError(t, err)
require.Empty(t, bridge.GetErrors())
// Start the Heartbeat process.
bridge.StartHeartbeat(mocks.Heartbeat)
// Wait for bridge to finish loading users.
waitForEvent(t, eventCh, events.AllUsersLoaded{})

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -33,6 +33,7 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
smtpservice "github.com/ProtonMail/proton-bridge/v3/internal/services/smtp"
"github.com/emersion/go-imap"
"github.com/emersion/go-sasl"
"github.com/emersion/go-smtp"
@ -701,3 +702,56 @@ Hello world
})
})
}
func TestBridge_SendAddressDisabled(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
recipientUserID, _, err := s.CreateUser("recipient", password)
require.NoError(t, err)
senderUserID, addrID, err := s.CreateUser("sender", password)
require.NoError(t, err)
require.NoError(t, s.ChangeAddressAllowSend(senderUserID, addrID, false))
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
smtpWaiter := waitForSMTPServerReady(bridge)
defer smtpWaiter.Done()
senderUserID, err := bridge.LoginFull(ctx, "sender", password, nil, nil)
require.NoError(t, err)
_, err = bridge.LoginFull(ctx, "recipient", password, nil, nil)
require.NoError(t, err)
smtpWaiter.Wait()
recipientInfo, err := bridge.GetUserInfo(recipientUserID)
require.NoError(t, err)
senderInfo, err := bridge.GetUserInfo(senderUserID)
require.NoError(t, err)
// Dial the server.
client, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort())))
require.NoError(t, err)
defer client.Close() //nolint:errcheck
// Upgrade to TLS.
require.NoError(t, client.StartTLS(&tls.Config{InsecureSkipVerify: true}))
require.NoError(t, client.Auth(sasl.NewLoginClient(
senderInfo.Addresses[0],
string(senderInfo.BridgePass)),
))
// Send the message.
err = client.SendMail(
senderInfo.Addresses[0],
[]string{recipientInfo.Addresses[0]},
strings.NewReader("Subject: Test 1\r\n\r\nHello world!"),
)
smtpErr := smtpservice.NewErrCanNotSendOnAddress(senderInfo.Addresses[0])
require.Equal(t, fmt.Sprintf("Error: %v", smtpErr.Error()), err.Error())
})
})
}

View File

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

@ -494,7 +494,7 @@ func (bridge *Bridge) addUser(
return fmt.Errorf("failed to add vault user: %w", err)
}
if err := bridge.addUserWithVault(ctx, client, apiUser, vaultUser); err != nil {
if err := bridge.addUserWithVault(ctx, client, apiUser, vaultUser, isNew); err != nil {
if _, ok := err.(*resty.ResponseError); ok || isLogin {
logrus.WithError(err).Error("Failed to add user, clearing its secrets from vault")
@ -529,6 +529,7 @@ func (bridge *Bridge) addUserWithVault(
client *proton.Client,
apiUser proton.User,
vault *vault.User,
isNew bool,
) error {
statsPath, err := bridge.locator.ProvideStatsPath()
if err != nil {
@ -556,6 +557,7 @@ func (bridge *Bridge) addUserWithVault(
&bridgeEventSubscription{b: bridge},
bridge.syncService,
syncSettingsPath,
isNew,
)
if err != nil {
return fmt.Errorf("failed to create user: %w", err)
@ -592,7 +594,7 @@ func (bridge *Bridge) addUserWithVault(
}, bridge.usersLock)
// As we need at least one user to send heartbeat, try to send it.
defer bridge.goHeartbeat()
bridge.heartbeat.start()
return nil
}

View File

@ -356,6 +356,10 @@ func removeCertTrustCGo(buffer *C.char, size C.ulonglong) error {
}
}
func osSupportCertInstall() bool {
return true
}
// installCert installs a certificate in the keychain. The certificate is added to the keychain and it is set as trusted.
// This function will trigger a security prompt from the system, unless the certificate is already trusted in the user keychain.
func installCert(certPEM []byte) error {

View File

@ -28,6 +28,7 @@ import (
func TestCertInKeychain(t *testing.T) {
// no trust settings change is performed, so this test will not trigger an OS security prompt.
certPEM := generatePEMCertificate(t)
require.True(t, osSupportCertInstall())
require.False(t, isCertInKeychain(certPEM))
require.NoError(t, addCertToKeychain(certPEM))
require.True(t, isCertInKeychain(certPEM))

View File

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

View File

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

View File

@ -37,6 +37,10 @@ func NewInstaller() *Installer {
}
}
func (installer *Installer) OSSupportCertInstall() bool {
return osSupportCertInstall()
}
func (installer *Installer) InstallCert(certPEM []byte) error {
installer.log.Info("Installing the Bridge TLS certificate in the OS keychain")
@ -64,3 +68,15 @@ func (installer *Installer) UninstallCert(certPEM []byte) error {
func (installer *Installer) IsCertInstalled(certPEM []byte) bool {
return isCertInstalled(certPEM)
}
// LogCertInstallStatus reports the current status of the certificate installation in the log.
// If certificate installation is not supported on the platform, this function does nothing.
func (installer *Installer) LogCertInstallStatus(certPEM []byte) {
if installer.OSSupportCertInstall() {
if installer.IsCertInstalled(certPEM) {
installer.log.Info("The Bridge TLS certificate is installed in the OS keychain")
} else {
installer.log.Info("The Bridge TLS certificate is not installed in the OS keychain")
}
}
}

View File

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

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

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

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
@ -69,8 +70,8 @@ ApplicationWindow {
}
colorScheme: ProtonStyle.currentStyle
height: ProtonStyle.window_default_height
minimumHeight:ProtonStyle.window_minimum_height
height: screen.height < ProtonStyle.window_default_height + 100 ? ProtonStyle.window_minimum_height : ProtonStyle.window_default_height
minimumHeight: ProtonStyle.window_minimum_height
minimumWidth: ProtonStyle.window_minimum_width
visible: true
width: ProtonStyle.window_default_width

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 disable the local cache (not recommended).")
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

@ -26,14 +26,16 @@ namespace bridgepp {
/// \param[in] what A description of the exception.
/// \param[in] details The optional details for the exception.
/// \param[in] function The name of the calling function.
/// \param[in] showSupportLink Should a link to the support web form be included in GUI message.
//****************************************************************************************************************************************************
Exception::Exception(QString qwhat, QString details, QString function, QByteArray attachment) noexcept
Exception::Exception(QString qwhat, QString details, QString function, QByteArray attachment, bool showSupportLink) noexcept
: std::exception()
, qwhat_(std::move(qwhat))
, what_(qwhat_.toLocal8Bit())
, details_(std::move(details))
, function_(std::move(function))
, attachment_(std::move(attachment)) {
, attachment_(std::move(attachment))
, showSupportLink_(showSupportLink) {
}
@ -46,7 +48,8 @@ Exception::Exception(Exception const &ref) noexcept
, what_(ref.what_)
, details_(ref.details_)
, function_(ref.function_)
, attachment_(ref.attachment_) {
, attachment_(ref.attachment_)
, showSupportLink_(ref.showSupportLink_) {
}
@ -59,7 +62,8 @@ Exception::Exception(Exception &&ref) noexcept
, what_(ref.what_)
, details_(ref.details_)
, function_(ref.function_)
, attachment_(ref.attachment_) {
, attachment_(ref.attachment_)
, showSupportLink_(ref.showSupportLink_) {
}
@ -118,4 +122,12 @@ QString Exception::detailedWhat() const {
}
//****************************************************************************************************************************************************
/// \return true iff A link to the support page should shown in the GUI message box.
//****************************************************************************************************************************************************
bool Exception::showSupportLink() const {
return showSupportLink_;
}
} // namespace bridgepp

View File

@ -33,7 +33,7 @@ namespace bridgepp {
class Exception : public std::exception {
public: // member functions
explicit Exception(QString qwhat = QString(), QString details = QString(), QString function = QString(),
QByteArray attachment = QByteArray()) noexcept; ///< Constructor
QByteArray attachment = QByteArray(), bool showSupportLink = false) noexcept; ///< Constructor
Exception(Exception const &ref) noexcept; ///< copy constructor
Exception(Exception &&ref) noexcept; ///< copy constructor
Exception &operator=(Exception const &) = delete; ///< Disabled assignment operator
@ -45,6 +45,7 @@ public: // member functions
QString function() const noexcept; ///< Return the function that threw the exception.
QByteArray attachment() const noexcept; ///< Return the attachment for the exception.
QString detailedWhat() const; ///< Return the detailed description of the message (i.e. including the function name and the details).
bool showSupportLink() const; ///< Return the value for the 'Show support link' option.
public: // static data members
static qsizetype const attachmentMaxLength {25 * 1024}; ///< The maximum length text attachment sent in Sentry reports, in bytes.
@ -55,6 +56,7 @@ private: // data members
QString const details_; ///< The optional details for the exception.
QString const function_; ///< The name of the function that created the exception.
QByteArray const attachment_; ///< The attachment to add to the exception.
bool const showSupportLink_; ///< Should the GUI feedback include a link to support.
};

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