forked from Silverfish/proton-bridge
Compare commits
160 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 60df01eece | |||
| 4e7acd9091 | |||
| 3ca5d0af71 | |||
| 9425e091d8 | |||
| b1ad0ab6dc | |||
| b63b56960e | |||
| 7c232b1331 | |||
| 7be46a4740 | |||
| b57c7abe92 | |||
| c86c428718 | |||
| ed6e17a0ab | |||
| c3454360fc | |||
| 182dab18a6 | |||
| 13c8a98389 | |||
| 05a2c9d254 | |||
| d926dd3806 | |||
| 7cc2f3361d | |||
| c496d6c71c | |||
| 7fc907a874 | |||
| b953468af2 | |||
| 9f4caa4948 | |||
| 86630ce137 | |||
| 2c9477d65c | |||
| 34c002ff68 | |||
| f03688ba72 | |||
| 8c0bb22de3 | |||
| 53c2cbcaee | |||
| 3ca56cfab3 | |||
| 59cf5e890b | |||
| 70950e0048 | |||
| 2aa4e7c9da | |||
| 227bbf1c03 | |||
| 1d426e621c | |||
| 6e4dcdb93b | |||
| 20f35edc83 | |||
| 26cf684fb8 | |||
| 2a4cb6a916 | |||
| caa4a5cbdb | |||
| 91900a7942 | |||
| 04a7a81e27 | |||
| 53d5619c51 | |||
| 8aec11a634 | |||
| 69aa784d32 | |||
| 9e4310712c | |||
| 0b35b275d3 | |||
| 2db5a04e7a | |||
| c6f1f159f3 | |||
| 82af4e01bc | |||
| 9ad5f74409 | |||
| 10cf153678 | |||
| 5ba07db7e3 | |||
| ad0d4ebd36 | |||
| 9f3c14ab1e | |||
| 74cf5d422b | |||
| dcf694588c | |||
| 82c388a0dd | |||
| 94ed09b437 | |||
| 57962e5757 | |||
| 8a5c8eaf6e | |||
| 30029f489e | |||
| 2faeebe9e7 | |||
| f6727a56d2 | |||
| d7fd39503f | |||
| b4b66f94ec | |||
| cbd36184bd | |||
| 465f754803 | |||
| 2fa7c97f39 | |||
| 9048b14fdb | |||
| 43100d11bf | |||
| 4876314cf5 | |||
| 2f75131710 | |||
| 1e09fd6662 | |||
| 48f2c56caa | |||
| 20d83dd476 | |||
| 9c6be78b4c | |||
| 0a8e71771e | |||
| 29d1c7bccd | |||
| ca1996a670 | |||
| ab1c1c474a | |||
| d7cac8a8f0 | |||
| 63bc87cc86 | |||
| 232875d5cc | |||
| 5ea53ea5c0 | |||
| 367c505444 | |||
| 3bd39b3ea5 | |||
| e89dcb2cca | |||
| 2cb2ca15c7 | |||
| db41645159 | |||
| 4cf23bb2e6 | |||
| ea11c1046a | |||
| df40f27069 | |||
| 76d732f247 | |||
| b17bdad864 | |||
| 52daa165a2 | |||
| 4c5ba04822 | |||
| d7ff54d679 | |||
| 4aa1091f62 | |||
| 6d024d2055 | |||
| c86cdf737f | |||
| 7e36b215fe | |||
| badebbef9f | |||
| 5e072c3282 | |||
| 048c3a900c | |||
| 88a9fe410c | |||
| cf32b84257 | |||
| e7dea0a77f | |||
| 160489a771 | |||
| 996c6826b9 | |||
| 43b871a124 | |||
| d1bf186040 | |||
| 24c68f100e | |||
| 1bfabf9a83 | |||
| e8a778feca | |||
| 5d4c10c56e | |||
| 60b1c4d8f7 | |||
| f1404cd3ee | |||
| ee4da8a89c | |||
| 5a70a16149 | |||
| f019ba3713 | |||
| 4b966c4845 | |||
| 94703bcf37 | |||
| 40cc6b54c9 | |||
| 584ea7e9f8 | |||
| cbdbc124db | |||
| b9c3fa9401 | |||
| 0e4ec8a8b8 | |||
| c3e4bb80a8 | |||
| 6459840507 | |||
| 87abbe9396 | |||
| d26a8319b6 | |||
| c9c80fd861 | |||
| fea4cc7b3b | |||
| cba5da22ae | |||
| 59a29da054 | |||
| c70674471e | |||
| 7882324439 | |||
| 1f8866a48a | |||
| faf28a6d4e | |||
| 59745e6fb6 | |||
| c8925cd270 | |||
| e35f3b6056 | |||
| d68014ec7b | |||
| b63029054d | |||
| 849c8bee78 | |||
| 70f0384cc3 | |||
| 5459720523 | |||
| a00e2acb5c | |||
| 1d405076e6 | |||
| 7119c566ef | |||
| 0e9428aaae | |||
| fe009ca235 | |||
| a377384553 | |||
| 03c8c323bc | |||
| fdbc380421 | |||
| 7056134b24 | |||
| 93c7552a41 | |||
| 931ed119bb | |||
| 0580842ad2 | |||
| 8d9db83a87 | |||
| c3eb6b2dbf |
@ -54,17 +54,6 @@ stages:
|
|||||||
allow_failure: true
|
allow_failure: true
|
||||||
- when: never
|
- when: never
|
||||||
|
|
||||||
.rules-branch-manual-MR-always-allow-failure:
|
|
||||||
rules:
|
|
||||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
||||||
when: always
|
|
||||||
allow_failure: true
|
|
||||||
- if: $CI_COMMIT_BRANCH
|
|
||||||
when: manual
|
|
||||||
allow_failure: true
|
|
||||||
- when: never
|
|
||||||
|
|
||||||
|
|
||||||
# Stage: TEST
|
# Stage: TEST
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
@ -88,7 +77,7 @@ test-linux:
|
|||||||
test-linux-race:
|
test-linux-race:
|
||||||
stage: test
|
stage: test
|
||||||
extends:
|
extends:
|
||||||
- .rules-branch-manual-MR-always-allow-failure
|
- .rules-branch-and-MR-manual
|
||||||
script:
|
script:
|
||||||
- make test-race
|
- make test-race
|
||||||
tags:
|
tags:
|
||||||
@ -106,7 +95,7 @@ test-integration:
|
|||||||
test-integration-race:
|
test-integration-race:
|
||||||
stage: test
|
stage: test
|
||||||
extends:
|
extends:
|
||||||
- .rules-branch-manual-MR-always-allow-failure
|
- .rules-branch-and-MR-manual
|
||||||
script:
|
script:
|
||||||
- make test-integration-race
|
- make test-integration-race
|
||||||
tags:
|
tags:
|
||||||
|
|||||||
11
BUILDS.md
11
BUILDS.md
@ -5,13 +5,13 @@
|
|||||||
- the go-rfc5322 module cannot currently be compiled for 32-bit OSes
|
- the go-rfc5322 module cannot currently be compiled for 32-bit OSes
|
||||||
* Go 1.18
|
* Go 1.18
|
||||||
* Bash with basic build utils: make, gcc, sed, find, grep, ...
|
* Bash with basic build utils: make, gcc, sed, find, grep, ...
|
||||||
- For Windows it is recommended to use MinGW 64bit shell from [MSYS2](https://www.msys2.org/)
|
- For Windows, it is recommended to use MinGW 64bit shell from [MSYS2](https://www.msys2.org/)
|
||||||
* GCC (linux), msvc (windows) or Xcode (macOS)
|
* GCC (linux), msvc (windows) or Xcode (macOS)
|
||||||
* Windres (windows)
|
* Windres (windows)
|
||||||
* libglvnd and libsecret development files (linux)
|
* libglvnd and libsecret development files (linux)
|
||||||
|
|
||||||
To enable the sending of crash reports using Sentry please set the
|
To enable the sending of crash reports using Sentry please set the
|
||||||
`main.DSNSentry` value with the client key of your sentry project before build.
|
`DSN_SENTRY` environment variable with the client key of your sentry project before build.
|
||||||
Otherwise, the sending of crash reports will be disabled.
|
Otherwise, the sending of crash reports will be disabled.
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
@ -44,9 +44,10 @@ make build
|
|||||||
make build-nogui
|
make build-nogui
|
||||||
```
|
```
|
||||||
|
|
||||||
* Bridge without GUI will start by default without any interface (i.e., there is no way to add or remove client, get bridge password, etc)
|
* To launch Bridge without GUI, you can invoke the `bridge` executable with one the following command-line switches:
|
||||||
* Bridge always has the option (whether built with Qt or without) to use a CLI interface by starting it with the argument `-c`
|
* `--noninteractive` or `-n` to start Bridge without any interface (i.e., there is no way to add or remove client, get bridge password, etc.)
|
||||||
* NOTE: You still need to setup supported keychain on your system
|
* `--cli` or `-c` to start Bridge with an interactive terminal interface.
|
||||||
|
* NOTE: You still need to set up a supported keychain on your system.
|
||||||
|
|
||||||
## Launchers
|
## Launchers
|
||||||
Launchers are only included in official distributions and provide the public
|
Launchers are only included in official distributions and provide the public
|
||||||
|
|||||||
150
Changelog.md
150
Changelog.md
@ -2,6 +2,156 @@
|
|||||||
|
|
||||||
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||||
|
|
||||||
|
## [Bridge 3.0.20] Perth Narrows
|
||||||
|
|
||||||
|
### Added
|
||||||
|
* GODT-2442: Allow user to re-sync DB without logout.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
GODT-2419: Reduce sentry reports.
|
||||||
|
GODT-2458: Wait for both bridge and bridge-gui to be ended before restarting on crash.
|
||||||
|
GODT-2457: Include address if GetPublickKeys() error message.
|
||||||
|
GODT-2446: Attach logs to sentry reports for relevant bridge-gui exceptions.
|
||||||
|
GODT-2425: Out of sync messages and read status.
|
||||||
|
GODT-2435: Group report exception by message if exception message looks corrupted.
|
||||||
|
GODT-2356: Unify sentry release description and add more context to it.
|
||||||
|
GODT-2357: Hide DSN_SENTRY and use single setting point for DSN_SENTRY.
|
||||||
|
GODT-2444: Bad event info.
|
||||||
|
GODT-2447: Don't assume timestamp exists in log filename.
|
||||||
|
GODT-2333: Do not allow modifications to All Mail label.
|
||||||
|
GODT-2429: Do not report context cancel to sentry.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
GODT-2449: fix bug in Bridge-GUI's Exception::what().
|
||||||
|
GODT-2427: Parsing header issues.
|
||||||
|
GODT-2426: Fix crash on user delete.
|
||||||
|
GODT-2417: Do not request gluon recovered message from API.
|
||||||
|
|
||||||
|
|
||||||
|
## [Bridge 3.0.19] Perth Narrows
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* GODT-2364: Wait and retry once if the gRPC service config file exists but cannot be opened.
|
||||||
|
* GODT-2364: Added optional details to C++ exceptions.
|
||||||
|
* GODT-2413: Use qEnvironmentVariable() instead of qgetenv().
|
||||||
|
* GODT-2412: Don't treat context cancellation as BadEvent.
|
||||||
|
* GODT-2404: Handle unexpected EOF.
|
||||||
|
* GODT-2400: Allow state updates to be applied if command fails.
|
||||||
|
* GODT-2399: Fix immediate message deletion during updates.
|
||||||
|
* GODT-2390: Missing changes from pervious commit.
|
||||||
|
* GODT-2390: Add reports for uncaught json and net.opErr.
|
||||||
|
* GODT-2414: Multiple deletion bug in WriteControlledStore.
|
||||||
|
|
||||||
|
|
||||||
|
## [Bridge 3.0.18] Perth Narrows
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* GODT-2392: Create message if gluon updateMessage returns `no such message`.
|
||||||
|
* GODT-2391: Create draft if missing during message update on gluon side.
|
||||||
|
|
||||||
|
## [Bridge 3.0.16/17] Perth Narrows
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* GODT-2371: Continue, not return, when handling draft.
|
||||||
|
|
||||||
|
## [Bridge 3.0.15] Perth Narrows
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* GODT-2355: Improve wording and actions on bad event.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* GODT-2354: Report failed load users.
|
||||||
|
* GODT-2353: Show popup only after 3.0.16.
|
||||||
|
* GODT-2351: Bump GPA to better handle net.OpError.
|
||||||
|
|
||||||
|
|
||||||
|
## [Bridge 3.0.14] Perth Narrows
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* GODT-2323: Fix Expunge not issued for move.
|
||||||
|
* GODT-2341: Handle URL error.
|
||||||
|
* GODT-2340: Improve logging.
|
||||||
|
* GODT-2278: Improve sentry logs.
|
||||||
|
* GODT-2327: Sync issues when migrating DB.
|
||||||
|
* GODT-2318: Remove gluon DB if label sync was incomplete.
|
||||||
|
* GODT-1804: Only promote content headers if non-empty.
|
||||||
|
* GODT-2343: Only poll after send if sync is complete.
|
||||||
|
* GODT-2336: Recover from changed address order while bridge is down.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [Bridge 3.0.13] Perth Narrows
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
GODT-2328: Ignore labels that aren't part of user label set.
|
||||||
|
GODT-2326: Sync issue on missing fresh DB file.
|
||||||
|
GODT-2319: Seed the math/rand RNG on app startup.
|
||||||
|
GODT-1804: Preserve MIME parameters when uploading attachments.
|
||||||
|
|
||||||
|
|
||||||
|
## [Bridge 3.0.12] Perth Narrows
|
||||||
|
|
||||||
|
### Added
|
||||||
|
* GODT-2210: v3.0 splash screen.
|
||||||
|
* GODT-1770: handle UserBadEvent in CLI and gRPC.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* GODT-2311: Fix missing headers in re-downloaded Gluon messages.
|
||||||
|
* GODT-1453: clicking 'Sign in' from status window now selects the right account.
|
||||||
|
* GODT-2297: More significantly improve GPA's paging algorithm.
|
||||||
|
* GODT-2145: Fix button spacing w/ Qt 6.4.
|
||||||
|
* GODT-2223: Improve event handling.
|
||||||
|
* GODT-2305: Detect missing gluon DB.
|
||||||
|
* GODT-2291: Change gluon store default location from Cache to Data.
|
||||||
|
* Other: Disable dialer test until badssl cert is bumbed.
|
||||||
|
* GODT-2292: Updated BUILDS.md doc.
|
||||||
|
* GODT-2258: suggest email as login when signing in via status window.
|
||||||
|
* Other: Report corrupt and/or insecure vaults to sentry.
|
||||||
|
* Other: Better user load logs.
|
||||||
|
* GODT-2253: Restart Launcher from the gui when GUI crashes.
|
||||||
|
* Other(test): Make All Mail copy test more robust.
|
||||||
|
* Other(CI): Make race checks manual.
|
||||||
|
* Other: Remove old cert/key file location handling.
|
||||||
|
* GODT-2271: Update README with new system files path.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* GODT-2210: Fix splash screen always showing on CentOS and Ubuntu.
|
||||||
|
* GODT-2296: Log error rather than fail if cannot get parent ID.
|
||||||
|
* GODT-2266: Pause event stream while sending.
|
||||||
|
* GODT-2266: Add test for sent message flags.
|
||||||
|
* Other(test): Fix some more integration test placeholders.
|
||||||
|
* GODT-2177: Use correct attachment disposition when content ID is set.
|
||||||
|
* GODT-1556: If no references, use the in-reply-to header as ParentID.
|
||||||
|
* Other: make GUI Tester more resilient to Bridge abrupt termination.
|
||||||
|
* GODT-2275: fixed location of bridge-gui log files.
|
||||||
|
* Other: Ensure SMTP debug dump works on windows.
|
||||||
|
* Other: Fix MaxLogs off-by-one limit and bump limit to 10.
|
||||||
|
* Other: fix path of temp folder in README.
|
||||||
|
* Other(debug): Dump raw SMTP input to user's home dir.
|
||||||
|
|
||||||
|
|
||||||
|
## [Bridge 3.0.11] Perth Narrows
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* GODT-2252: Recover from deleted cached messages.
|
||||||
|
* GODT-2258: change login label and suggest email instead of username.
|
||||||
|
* Other: Don't clean settings path on teardown.
|
||||||
|
* Other: Bump GPA to v0.3.0.
|
||||||
|
* Other: added user's primary email address to the vault.
|
||||||
|
* GODT-2251: gluon store and DB separated.
|
||||||
|
* GODT-2093: use the primary email address in the account view and status view.
|
||||||
|
* GODT-2202: Report update errors from Gluon.
|
||||||
|
* GODT-2229: Own the full path for gluon and do not change Database path.
|
||||||
|
* GODT-1797: copyright notice shows a date range with the build year.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* GODT-2223: Handle bad events by logging user out.
|
||||||
|
* GODT-2165: Reduce UTF8 parsing errors from TLS header input.
|
||||||
|
* Others: chores fix a QML warning when no account is present* and a few typos in QML.
|
||||||
|
* Other(test): Fix integration test steps.
|
||||||
|
* GODT-2226: Fix moving drafts to trash.
|
||||||
|
* GODT-2246: do not report API error 422 when using an invalid email address.
|
||||||
|
|
||||||
|
|
||||||
## [Bridge 3.0.10] Perth Narrows
|
## [Bridge 3.0.10] Perth Narrows
|
||||||
|
|
||||||
|
|||||||
38
Makefile
38
Makefile
@ -11,7 +11,7 @@ ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
|
|||||||
.PHONY: build build-gui build-nogui build-launcher versioner hasher
|
.PHONY: build build-gui build-nogui build-launcher versioner hasher
|
||||||
|
|
||||||
# Keep version hardcoded so app build works also without Git repository.
|
# Keep version hardcoded so app build works also without Git repository.
|
||||||
BRIDGE_APP_VERSION?=3.0.10+git
|
BRIDGE_APP_VERSION?=3.0.20+git
|
||||||
APP_VERSION:=${BRIDGE_APP_VERSION}
|
APP_VERSION:=${BRIDGE_APP_VERSION}
|
||||||
APP_FULL_NAME:=Proton Mail Bridge
|
APP_FULL_NAME:=Proton Mail Bridge
|
||||||
APP_VENDOR:=Proton AG
|
APP_VENDOR:=Proton AG
|
||||||
@ -23,16 +23,21 @@ REVISION:=$(shell git rev-parse --short=10 HEAD)
|
|||||||
BUILD_TIME:=$(shell date +%FT%T%z)
|
BUILD_TIME:=$(shell date +%FT%T%z)
|
||||||
MACOS_MIN_VERSION_ARM64=11.0
|
MACOS_MIN_VERSION_ARM64=11.0
|
||||||
MACOS_MIN_VERSION_AMD64=10.15
|
MACOS_MIN_VERSION_AMD64=10.15
|
||||||
|
BUILD_ENV?=dev
|
||||||
|
|
||||||
BUILD_FLAGS:=-tags='${BUILD_TAGS}'
|
BUILD_FLAGS:=-tags='${BUILD_TAGS}'
|
||||||
BUILD_FLAGS_LAUNCHER:=${BUILD_FLAGS}
|
BUILD_FLAGS_LAUNCHER:=${BUILD_FLAGS}
|
||||||
BUILD_FLAGS_GUI:=-tags='${BUILD_TAGS} build_qt'
|
|
||||||
GO_LDFLAGS:=$(addprefix -X github.com/ProtonMail/proton-bridge/v3/internal/constants., Version=${APP_VERSION} Revision=${REVISION} BuildTime=${BUILD_TIME})
|
GO_LDFLAGS:=$(addprefix -X github.com/ProtonMail/proton-bridge/v3/internal/constants., Version=${APP_VERSION} Revision=${REVISION} BuildTime=${BUILD_TIME})
|
||||||
GO_LDFLAGS+=-X "github.com/ProtonMail/proton-bridge/v3/internal/constants.FullAppName=${APP_FULL_NAME}"
|
GO_LDFLAGS+=-X "github.com/ProtonMail/proton-bridge/v3/internal/constants.FullAppName=${APP_FULL_NAME}"
|
||||||
|
|
||||||
ifneq "${BUILD_LDFLAGS}" ""
|
ifneq "${DSN_SENTRY}" ""
|
||||||
GO_LDFLAGS+=${BUILD_LDFLAGS}
|
GO_LDFLAGS+=-X github.com/ProtonMail/proton-bridge/v3/internal/constants.DSNSentry=${DSN_SENTRY}
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
ifneq "${BUILD_ENV}" ""
|
||||||
|
GO_LDFLAGS+=-X github.com/ProtonMail/proton-bridge/v3/internal/constants.BuildEnv=${BUILD_ENV}
|
||||||
|
endif
|
||||||
|
|
||||||
GO_LDFLAGS_LAUNCHER:=${GO_LDFLAGS}
|
GO_LDFLAGS_LAUNCHER:=${GO_LDFLAGS}
|
||||||
ifeq "${TARGET_OS}" "windows"
|
ifeq "${TARGET_OS}" "windows"
|
||||||
#GO_LDFLAGS+=-H=windowsgui # Disabled so we can inspect trace logs from the bridge for debugging.
|
#GO_LDFLAGS+=-H=windowsgui # Disabled so we can inspect trace logs from the bridge for debugging.
|
||||||
@ -40,7 +45,6 @@ ifeq "${TARGET_OS}" "windows"
|
|||||||
endif
|
endif
|
||||||
|
|
||||||
BUILD_FLAGS+=-ldflags '${GO_LDFLAGS}'
|
BUILD_FLAGS+=-ldflags '${GO_LDFLAGS}'
|
||||||
BUILD_FLAGS_GUI+=-ldflags "${GO_LDFLAGS}"
|
|
||||||
BUILD_FLAGS_LAUNCHER+=-ldflags '${GO_LDFLAGS_LAUNCHER}'
|
BUILD_FLAGS_LAUNCHER+=-ldflags '${GO_LDFLAGS_LAUNCHER}'
|
||||||
DEPLOY_DIR:=cmd/${TARGET_CMD}/deploy
|
DEPLOY_DIR:=cmd/${TARGET_CMD}/deploy
|
||||||
DIRNAME:=$(shell basename ${CURDIR})
|
DIRNAME:=$(shell basename ${CURDIR})
|
||||||
@ -154,8 +158,10 @@ ${EXE_TARGET}: check-build-essentials ${EXE_NAME}
|
|||||||
BRIDGE_VENDOR="${APP_VENDOR}" \
|
BRIDGE_VENDOR="${APP_VENDOR}" \
|
||||||
BRIDGE_APP_VERSION=${APP_VERSION} \
|
BRIDGE_APP_VERSION=${APP_VERSION} \
|
||||||
BRIDGE_REVISION=${REVISION} \
|
BRIDGE_REVISION=${REVISION} \
|
||||||
BRIDGE_BUILD_TIME=${BUILD_TIME} \
|
BRIDGE_DSN_SENTRY=${DSN_SENTRY} \
|
||||||
|
BRIDGE_BUILD_TIME=${BUILD_TIME} \
|
||||||
BRIDGE_GUI_BUILD_CONFIG=Release \
|
BRIDGE_GUI_BUILD_CONFIG=Release \
|
||||||
|
BRIDGE_BUILD_ENV=${BUILD_ENV} \
|
||||||
BRIDGE_INSTALL_PATH=${ROOT_DIR}/${DEPLOY_DIR}/${GOOS} \
|
BRIDGE_INSTALL_PATH=${ROOT_DIR}/${DEPLOY_DIR}/${GOOS} \
|
||||||
./build.sh install
|
./build.sh install
|
||||||
mv "${ROOT_DIR}/${BRIDGE_EXE}" "$(ROOT_DIR)/${EXE_TARGET}"
|
mv "${ROOT_DIR}/${BRIDGE_EXE}" "$(ROOT_DIR)/${EXE_TARGET}"
|
||||||
@ -294,7 +300,7 @@ gofiles: ./internal/bridge/credits.go
|
|||||||
cd ./utils/ && ./credits.sh bridge
|
cd ./utils/ && ./credits.sh bridge
|
||||||
|
|
||||||
## Run and debug
|
## Run and debug
|
||||||
.PHONY: run run-qt run-qt-cli run-nogui run-cli run-noninteractive run-debug run-qml-preview clean-vendor clean-frontend-qt clean-frontend-qt-common clean
|
.PHONY: run run-qt run-qt-cli run-nogui run-cli run-noninteractive run-debug run-gui-tester clean-vendor clean-frontend-qt clean-frontend-qt-common clean
|
||||||
|
|
||||||
LOG?=debug
|
LOG?=debug
|
||||||
LOG_IMAP?=client # client/server/all, or empty to turn it off
|
LOG_IMAP?=client # client/server/all, or empty to turn it off
|
||||||
@ -321,12 +327,26 @@ run-nogui: build-nogui clean-vendor gofiles
|
|||||||
run-debug:
|
run-debug:
|
||||||
dlv debug ./cmd/Desktop-Bridge/main.go -- -l=debug
|
dlv debug ./cmd/Desktop-Bridge/main.go -- -l=debug
|
||||||
|
|
||||||
|
ifeq "${TARGET_OS}" "windows"
|
||||||
|
EXE_SUFFIX=.exe
|
||||||
|
endif
|
||||||
|
|
||||||
|
bridge-gui-tester: build-gui
|
||||||
|
cp ./cmd/Desktop-Bridge/deploy/${TARGET_OS}/bridge-gui${EXE_SUFFIX} .
|
||||||
|
cd ./internal/frontend/bridge-gui/bridge-gui-tester && cmake . && make
|
||||||
|
|
||||||
|
run-gui-tester: bridge-gui-tester
|
||||||
|
# copying tester as bridge so bridge-gui will start it and connect to it automatically
|
||||||
|
cp ./internal/frontend/bridge-gui/bridge-gui-tester/bridge-gui-tester${EXE_SUFFIX} bridge${EXE_SUFFIX}
|
||||||
|
./bridge-gui${EXE_SUFFIX}
|
||||||
|
|
||||||
|
|
||||||
clean-vendor:
|
clean-vendor:
|
||||||
rm -rf ./vendor
|
rm -rf ./vendor
|
||||||
|
|
||||||
clean-gui:
|
clean-gui:
|
||||||
cd internal/frontend/bridge-gui/ && \
|
cd internal/frontend/bridge-gui/ && \
|
||||||
rm -f Version.h && \
|
rm -f BuildConfig.h && \
|
||||||
rm -rf cmake-build-*/
|
rm -rf cmake-build-*/
|
||||||
|
|
||||||
clean-vcpkg:
|
clean-vcpkg:
|
||||||
@ -349,6 +369,6 @@ clean: clean-vendor clean-gui clean-vcpkg
|
|||||||
.PHONY: generate
|
.PHONY: generate
|
||||||
generate:
|
generate:
|
||||||
go generate ./...
|
go generate ./...
|
||||||
$(MAKE) add-license
|
$(MAKE) build
|
||||||
|
|
||||||
.FORCE:
|
.FORCE:
|
||||||
|
|||||||
52
README.md
52
README.md
@ -62,35 +62,33 @@ major problems.
|
|||||||
- `TAGS`: set build tags for tests
|
- `TAGS`: set build tags for tests
|
||||||
- `FEATURES`: set feature dir, file or scenario to test
|
- `FEATURES`: set feature dir, file or scenario to test
|
||||||
|
|
||||||
|
## Folders
|
||||||
|
|
||||||
|
There are now three types of system folders which Bridge recognises:
|
||||||
|
|
||||||
|
| | Windows | Mac | Linux | Linux (XDG) |
|
||||||
|
|--------|-------------------------------------|-----------------------------------------------------|-------------------------------------|---------------------------------------|
|
||||||
|
| config | %APPDATA%\protonmail\bridge-v3 | ~/Library/Application Support/protonmail/bridge-v3 | ~/.config/protonmail/bridge-v3 | $XDG_CONFIG_HOME/protonmail/bridge-v3 |
|
||||||
|
| cache | %LOCALAPPDATA%\protonmail\bridge-v3 | ~/Library/Caches/protonmail/bridge-v3 | ~/.cache/protonmail/bridge-v3 | $XDG_CACHE_HOME/protonmail/bridge-v3 |
|
||||||
|
| data | %APPDATA%\protonmail\bridge-v3 | ~/Library/Application Support/protonmail/bridge-v3 | ~/.local/share/protonmail/bridge-v3 | $XDG_DATA_HOME/protonmail/bridge-v3 |
|
||||||
|
| temp | %LOCALAPPDATA%\Temp | $TMPDIR if non-empty, else /tmp | $TMPDIR if non-empty, else /tmp | $TMPDIR if non-empty, else /tmp |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Files
|
## Files
|
||||||
### Database
|
|
||||||
The database stores metadata necessary for presenting messages and mailboxes to an email client:
|
|
||||||
- Linux: `~/.cache/protonmail/bridge/<cacheVersion>/mailbox-<userID>.db` (unless `XDG_CACHE_HOME` is set, in which case that is used as your `~`)
|
|
||||||
- macOS: `~/Library/Caches/protonmail/bridge/<cacheVersion>/mailbox-<userID>.db`
|
|
||||||
- Windows: `%LOCALAPPDATA%\protonmail\bridge\<cacheVersion>\mailbox-<userID>.db`
|
|
||||||
|
|
||||||
### Preferences
|
| | Base Dir | Path |
|
||||||
User preferences are stored in json at the following location:
|
|-----------------------|----------|----------------------------|
|
||||||
- Linux: `~/.config/protonmail/bridge/prefs.json`
|
| bridge lock file | cache | bridge.lock |
|
||||||
- macOS: `~/Library/ApplicationSupport/protonmail/bridge/prefs.json`
|
| bridge-gui lock file | cache | bridge-gui.lock |
|
||||||
- Windows: `%APPDATA%\protonmail\bridge\prefs.json`
|
| vault | config | vault.enc |
|
||||||
|
| gRPC server json | config | grpcServerConfig.json |
|
||||||
|
| gRPC client json | config | grpcClientConfig_<id>.json |
|
||||||
|
| Logs | data | logs |
|
||||||
|
| gluon DB | data | gluon/backend/db |
|
||||||
|
| gluon messages | sata | gluon/backend/store |
|
||||||
|
| Update files | data | updates |
|
||||||
|
| sentry cache | data | sentry_cache |
|
||||||
|
| Mac/Linux File Socket | temp | bridge_{RANDOM_UUID}.sock |
|
||||||
|
|
||||||
### IMAP Cache
|
|
||||||
The currently subscribed mailboxes are held in a json file:
|
|
||||||
- Linux: `~/.cache/protonmail/bridge/<cacheVersion>/user_info.json` (unless `XDG_CACHE_HOME` is set, in which case that is used as your `~`)
|
|
||||||
- macOS: `~/Library/Caches/protonmail/bridge/<cacheVersion>/user_info.json`
|
|
||||||
- Windows: `%LOCALAPPDATA%\protonmail\bridge\<cacheVersion>\user_info.json`
|
|
||||||
|
|
||||||
### Lock file
|
|
||||||
Bridge utilises an on-disk lock to ensure only one instance is run at once. The lock file is here:
|
|
||||||
- Linux: `~/.cache/protonmail/bridge/<cacheVersion>/bridge.lock` (unless `XDG_CACHE_HOME` is set, in which case that is used as your `~`)
|
|
||||||
- macOS: `~/Library/Caches/protonmail/bridge/<cacheVersion>/bridge.lock`
|
|
||||||
- Windows: `%LOCALAPPDATA%\protonmail\bridge\<cacheVersion>\bridge.lock`
|
|
||||||
|
|
||||||
### TLS Certificate and Key
|
|
||||||
When bridge first starts, it generates a unique TLS certificate and key file at the following locations:
|
|
||||||
- Linux: `~/.config/protonmail/bridge/{cert,key}.pem` (unless `XDG_CONFIG_HOME` is set, in which case that is used as your `~/.config`)
|
|
||||||
- macOS: `~/Library/ApplicationSupport/protonmail/bridge/{cert,key}.pem`
|
|
||||||
- Windows: `%APPDATA%\protonmail\bridge\{cert,key}.pem`
|
|
||||||
|
|
||||||
|
|||||||
@ -59,7 +59,7 @@ func main() { //nolint:funlen
|
|||||||
logrus.SetLevel(logrus.DebugLevel)
|
logrus.SetLevel(logrus.DebugLevel)
|
||||||
l := logrus.WithField("launcher_version", constants.Version)
|
l := logrus.WithField("launcher_version", constants.Version)
|
||||||
|
|
||||||
reporter := sentry.NewReporter(appName, constants.Version, useragent.New())
|
reporter := sentry.NewReporter(appName, useragent.New())
|
||||||
|
|
||||||
crashHandler := crash.NewHandler(reporter.ReportException)
|
crashHandler := crash.NewHandler(reporter.ReportException)
|
||||||
defer crashHandler.HandlePanic()
|
defer crashHandler.HandlePanic()
|
||||||
@ -127,9 +127,11 @@ func main() { //nolint:funlen
|
|||||||
|
|
||||||
l = l.WithField("exe_path", exe)
|
l = l.WithField("exe_path", exe)
|
||||||
|
|
||||||
args, wait, mainExe := findAndStripWait(args)
|
args, wait, mainExes := findAndStripWait(args)
|
||||||
if wait {
|
if wait {
|
||||||
waitForProcessToFinish(mainExe)
|
for _, mainExe := range mainExes {
|
||||||
|
waitForProcessToFinish(mainExe)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := execabs.Command(exe, appendLauncherPath(launcher, args)...) //nolint:gosec
|
cmd := execabs.Command(exe, appendLauncherPath(launcher, args)...) //nolint:gosec
|
||||||
@ -186,12 +188,11 @@ func findAndStrip[T comparable](slice []T, v T) (strippedList []T, found bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// findAndStripWait Check for waiter flag get its value and clean them both.
|
// findAndStripWait Check for waiter flag get its value and clean them both.
|
||||||
func findAndStripWait(args []string) ([]string, bool, string) {
|
func findAndStripWait(args []string) ([]string, bool, []string) {
|
||||||
res := append([]string{}, args...)
|
res := append([]string{}, args...)
|
||||||
|
|
||||||
hasFlag := false
|
hasFlag := false
|
||||||
var value string
|
values := make([]string, 0)
|
||||||
|
|
||||||
for k, v := range res {
|
for k, v := range res {
|
||||||
if v != FlagWait {
|
if v != FlagWait {
|
||||||
continue
|
continue
|
||||||
@ -200,14 +201,16 @@ func findAndStripWait(args []string) ([]string, bool, string) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
hasFlag = true
|
hasFlag = true
|
||||||
value = res[k+1]
|
values = append(values, res[k+1])
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasFlag {
|
if hasFlag {
|
||||||
res, _ = findAndStrip(res, FlagWait)
|
res, _ = findAndStrip(res, FlagWait)
|
||||||
res, _ = findAndStrip(res, value)
|
for _, v := range values {
|
||||||
|
res, _ = findAndStrip(res, v)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return res, hasFlag, value
|
return res, hasFlag, values
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPathToUpdatedExecutable(
|
func getPathToUpdatedExecutable(
|
||||||
|
|||||||
@ -56,3 +56,25 @@ func TestFindAndStrip(t *testing.T) {
|
|||||||
assert.False(t, found)
|
assert.False(t, found)
|
||||||
assert.True(t, xslices.Equal(result, []string{}))
|
assert.True(t, xslices.Equal(result, []string{}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFindAndStripWait(t *testing.T) {
|
||||||
|
result, found, values := findAndStripWait([]string{"a", "b", "c"})
|
||||||
|
assert.False(t, found)
|
||||||
|
assert.True(t, xslices.Equal(result, []string{"a", "b", "c"}))
|
||||||
|
assert.True(t, xslices.Equal(values, []string{}))
|
||||||
|
|
||||||
|
result, found, values = findAndStripWait([]string{"a", "--wait", "b"})
|
||||||
|
assert.True(t, found)
|
||||||
|
assert.True(t, xslices.Equal(result, []string{"a"}))
|
||||||
|
assert.True(t, xslices.Equal(values, []string{"b"}))
|
||||||
|
|
||||||
|
result, found, values = findAndStripWait([]string{"a", "--wait", "b", "--wait", "c"})
|
||||||
|
assert.True(t, found)
|
||||||
|
assert.True(t, xslices.Equal(result, []string{"a"}))
|
||||||
|
assert.True(t, xslices.Equal(values, []string{"b", "c"}))
|
||||||
|
|
||||||
|
result, found, values = findAndStripWait([]string{"a", "--wait", "b", "--wait", "c", "--wait", "d"})
|
||||||
|
assert.True(t, found)
|
||||||
|
assert.True(t, xslices.Equal(result, []string{"a"}))
|
||||||
|
assert.True(t, xslices.Equal(values, []string{"b", "c", "d"}))
|
||||||
|
}
|
||||||
|
|||||||
4
go.mod
4
go.mod
@ -5,9 +5,9 @@ go 1.18
|
|||||||
require (
|
require (
|
||||||
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
|
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
|
||||||
github.com/Masterminds/semver/v3 v3.1.1
|
github.com/Masterminds/semver/v3 v3.1.1
|
||||||
github.com/ProtonMail/gluon v0.14.2-0.20230106095250-7e99ea4da61e
|
github.com/ProtonMail/gluon v0.14.2-0.20230322121010-763723ee7bbc
|
||||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
|
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
|
||||||
github.com/ProtonMail/go-proton-api v0.2.4-0.20230109143101-f8fd857ee5b4
|
github.com/ProtonMail/go-proton-api v0.3.1-0.20230321105220-39e9131e1a68
|
||||||
github.com/ProtonMail/go-rfc5322 v0.11.0
|
github.com/ProtonMail/go-rfc5322 v0.11.0
|
||||||
github.com/ProtonMail/gopenpgp/v2 v2.4.10
|
github.com/ProtonMail/gopenpgp/v2 v2.4.10
|
||||||
github.com/PuerkitoBio/goquery v1.8.0
|
github.com/PuerkitoBio/goquery v1.8.0
|
||||||
|
|||||||
8
go.sum
8
go.sum
@ -28,8 +28,8 @@ github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs
|
|||||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
|
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
|
||||||
github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
|
github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
|
||||||
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
|
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
|
||||||
github.com/ProtonMail/gluon v0.14.2-0.20230106095250-7e99ea4da61e h1://xRNjGTAMXw2U91MtqPc4krUtxQmt2+4z1oYrBaOWU=
|
github.com/ProtonMail/gluon v0.14.2-0.20230322121010-763723ee7bbc h1:qLHEYjr7BJaZxeMyqhEBpenuAnduFNZqBA26gT9LXGo=
|
||||||
github.com/ProtonMail/gluon v0.14.2-0.20230106095250-7e99ea4da61e/go.mod h1:z2AxLIiBCT1K+0OBHyaDI7AEaO5qI6/BEC2TE42vs4Q=
|
github.com/ProtonMail/gluon v0.14.2-0.20230322121010-763723ee7bbc/go.mod h1:z2AxLIiBCT1K+0OBHyaDI7AEaO5qI6/BEC2TE42vs4Q=
|
||||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
|
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
|
||||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
|
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
|
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
|
||||||
@ -41,8 +41,8 @@ github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753/go.mod h1:NB
|
|||||||
github.com/ProtonMail/go-mime v0.0.0-20220302105931-303f85f7fe0f/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4=
|
github.com/ProtonMail/go-mime v0.0.0-20220302105931-303f85f7fe0f/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4=
|
||||||
github.com/ProtonMail/go-mime v0.0.0-20220429130430-2192574d760f h1:4IWzKjHzZxdrW9k4zl/qCwenOVHDbVDADPPHFLjs0Oc=
|
github.com/ProtonMail/go-mime v0.0.0-20220429130430-2192574d760f h1:4IWzKjHzZxdrW9k4zl/qCwenOVHDbVDADPPHFLjs0Oc=
|
||||||
github.com/ProtonMail/go-mime v0.0.0-20220429130430-2192574d760f/go.mod h1:qRZgbeASl2a9OwmsV85aWwRqic0NHPh+9ewGAzb4cgM=
|
github.com/ProtonMail/go-mime v0.0.0-20220429130430-2192574d760f/go.mod h1:qRZgbeASl2a9OwmsV85aWwRqic0NHPh+9ewGAzb4cgM=
|
||||||
github.com/ProtonMail/go-proton-api v0.2.4-0.20230109143101-f8fd857ee5b4 h1:xCot3copmyPz0cDOwl1XVmYQDRJGi6EgJUKJ58Vn58U=
|
github.com/ProtonMail/go-proton-api v0.3.1-0.20230321105220-39e9131e1a68 h1:CExt0Vd19dsUtf+IBSa/l96/DTHEmgXi4IbWG99Vs1E=
|
||||||
github.com/ProtonMail/go-proton-api v0.2.4-0.20230109143101-f8fd857ee5b4/go.mod h1:JUo5IQG0hNuPRuDpOUsCOvtee6UjTEHHF1QN2i8RSos=
|
github.com/ProtonMail/go-proton-api v0.3.1-0.20230321105220-39e9131e1a68/go.mod h1:JUo5IQG0hNuPRuDpOUsCOvtee6UjTEHHF1QN2i8RSos=
|
||||||
github.com/ProtonMail/go-rfc5322 v0.11.0 h1:o5Obrm4DpmQEffvgsVqG6S4BKwC1Wat+hYwjIp2YcCY=
|
github.com/ProtonMail/go-rfc5322 v0.11.0 h1:o5Obrm4DpmQEffvgsVqG6S4BKwC1Wat+hYwjIp2YcCY=
|
||||||
github.com/ProtonMail/go-rfc5322 v0.11.0/go.mod h1:6oOKr0jXvpoE6pwTx/HukigQpX2J9WUf6h0auplrFTw=
|
github.com/ProtonMail/go-rfc5322 v0.11.0/go.mod h1:6oOKr0jXvpoE6pwTx/HukigQpX2J9WUf6h0auplrFTw=
|
||||||
github.com/ProtonMail/go-srp v0.0.5 h1:xhUioxZgDbCnpo9JehyFhwwsn9JLWkUGfB0oiKXgiGg=
|
github.com/ProtonMail/go-srp v0.0.5 h1:xhUioxZgDbCnpo9JehyFhwwsn9JLWkUGfB0oiKXgiGg=
|
||||||
|
|||||||
@ -19,11 +19,13 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/cookiejar"
|
"net/http/cookiejar"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/Masterminds/semver/v3"
|
"github.com/Masterminds/semver/v3"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||||
@ -155,6 +157,9 @@ func New() *cli.App { //nolint:funlen
|
|||||||
}
|
}
|
||||||
|
|
||||||
func run(c *cli.Context) error { //nolint:funlen
|
func run(c *cli.Context) error { //nolint:funlen
|
||||||
|
// Seed the default RNG from the math/rand package.
|
||||||
|
rand.Seed(time.Now().UnixNano())
|
||||||
|
|
||||||
// Get the current bridge version.
|
// Get the current bridge version.
|
||||||
version, err := semver.NewVersion(constants.Version)
|
version, err := semver.NewVersion(constants.Version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -165,7 +170,7 @@ func run(c *cli.Context) error { //nolint:funlen
|
|||||||
identifier := useragent.New()
|
identifier := useragent.New()
|
||||||
|
|
||||||
// Create a new Sentry client that will be used to report crashes etc.
|
// Create a new Sentry client that will be used to report crashes etc.
|
||||||
reporter := sentry.NewReporter(constants.FullAppName, constants.Version, identifier)
|
reporter := sentry.NewReporter(constants.FullAppName, identifier)
|
||||||
|
|
||||||
// Determine the exe that should be used to restart/autostart the app.
|
// Determine the exe that should be used to restart/autostart the app.
|
||||||
// By default, this is the launcher, if used. Otherwise, we try to get
|
// By default, this is the launcher, if used. Otherwise, we try to get
|
||||||
@ -206,6 +211,16 @@ func run(c *cli.Context) error { //nolint:funlen
|
|||||||
return withSingleInstance(locations, version, func() error {
|
return withSingleInstance(locations, version, func() error {
|
||||||
// Unlock the encrypted vault.
|
// Unlock the encrypted vault.
|
||||||
return WithVault(locations, func(vault *vault.Vault, insecure, corrupt bool) error {
|
return WithVault(locations, func(vault *vault.Vault, insecure, corrupt bool) error {
|
||||||
|
// Report insecure vault.
|
||||||
|
if insecure {
|
||||||
|
_ = reporter.ReportMessageWithContext("Vault is insecure", map[string]interface{}{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Report corrupt vault.
|
||||||
|
if corrupt {
|
||||||
|
_ = reporter.ReportMessageWithContext("Vault is corrupt", map[string]interface{}{})
|
||||||
|
}
|
||||||
|
|
||||||
if !vault.Migrated() {
|
if !vault.Migrated() {
|
||||||
// Migrate old settings into the vault.
|
// Migrate old settings into the vault.
|
||||||
if err := migrateOldSettings(vault); err != nil {
|
if err := migrateOldSettings(vault); err != nil {
|
||||||
@ -322,14 +337,7 @@ func WithLocations(fn func(*locations.Locations) error) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create a new locations object that will be used to provide paths to store files.
|
// Create a new locations object that will be used to provide paths to store files.
|
||||||
locations := locations.New(provider, constants.ConfigName)
|
return fn(locations.New(provider, constants.ConfigName))
|
||||||
defer func() {
|
|
||||||
if err := locations.Clean(); err != nil {
|
|
||||||
logrus.WithError(err).Error("Failed to clean locations")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return fn(locations)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start profiling if requested.
|
// Start profiling if requested.
|
||||||
|
|||||||
@ -87,6 +87,11 @@ func migrateOldSettings(v *vault.Vault) error {
|
|||||||
return fmt.Errorf("failed to get user config dir: %w", err)
|
return fmt.Errorf("failed to get user config dir: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return migrateOldSettingsWithDir(configDir, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// nolint:gosec
|
||||||
|
func migrateOldSettingsWithDir(configDir string, v *vault.Vault) error {
|
||||||
b, err := os.ReadFile(filepath.Join(configDir, "protonmail", "bridge", "prefs.json"))
|
b, err := os.ReadFile(filepath.Join(configDir, "protonmail", "bridge", "prefs.json"))
|
||||||
if errors.Is(err, fs.ErrNotExist) {
|
if errors.Is(err, fs.ErrNotExist) {
|
||||||
return nil
|
return nil
|
||||||
@ -94,7 +99,27 @@ func migrateOldSettings(v *vault.Vault) error {
|
|||||||
return fmt.Errorf("failed to read old prefs file: %w", err)
|
return fmt.Errorf("failed to read old prefs file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return migratePrefsToVault(v, b)
|
if err := migratePrefsToVault(v, b); err != nil {
|
||||||
|
return fmt.Errorf("failed to migrate prefs to vault: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Info("Migrating TLS certificate")
|
||||||
|
|
||||||
|
certPEM, err := os.ReadFile(filepath.Join(configDir, "protonmail", "bridge", "cert.pem"))
|
||||||
|
if errors.Is(err, fs.ErrNotExist) {
|
||||||
|
return nil
|
||||||
|
} else if err != nil {
|
||||||
|
return fmt.Errorf("failed to read old cert file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
keyPEM, err := os.ReadFile(filepath.Join(configDir, "protonmail", "bridge", "key.pem"))
|
||||||
|
if errors.Is(err, fs.ErrNotExist) {
|
||||||
|
return nil
|
||||||
|
} else if err != nil {
|
||||||
|
return fmt.Errorf("failed to read old key file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return v.SetBridgeTLSCertKey(certPEM, keyPEM)
|
||||||
}
|
}
|
||||||
|
|
||||||
func migrateOldAccounts(locations *locations.Locations, v *vault.Vault) error {
|
func migrateOldAccounts(locations *locations.Locations, v *vault.Vault) error {
|
||||||
@ -147,7 +172,12 @@ func migrateOldAccount(userID string, store *credentials.Store, v *vault.Vault)
|
|||||||
return fmt.Errorf("failed to split api token for user %q: %w", userID, err)
|
return fmt.Errorf("failed to split api token for user %q: %w", userID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := v.AddUser(creds.UserID, creds.Name, authUID, authRef, creds.MailboxPassword)
|
var primaryEmail string
|
||||||
|
if len(creds.EmailList()) > 0 {
|
||||||
|
primaryEmail = creds.EmailList()[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := v.AddUser(creds.UserID, creds.Name, primaryEmail, authUID, authRef, creds.MailboxPassword)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to add user %q: %w", userID, err)
|
return fmt.Errorf("failed to add user %q: %w", userID, err)
|
||||||
}
|
}
|
||||||
@ -193,11 +223,10 @@ func migratePrefsToVault(vault *vault.Vault, b []byte) error {
|
|||||||
UpdateChannel updater.Channel `json:"update_channel"`
|
UpdateChannel updater.Channel `json:"update_channel"`
|
||||||
UpdateRollout float64 `json:"rollout,,string"`
|
UpdateRollout float64 `json:"rollout,,string"`
|
||||||
|
|
||||||
FirstStart bool `json:"first_time_start,,string"`
|
FirstStart bool `json:"first_time_start,,string"`
|
||||||
FirstStartGUI bool `json:"first_time_start_gui,,string"`
|
ColorScheme string `json:"color_scheme"`
|
||||||
ColorScheme string `json:"color_scheme"`
|
LastVersion *semver.Version `json:"last_used_version"`
|
||||||
LastVersion *semver.Version `json:"last_used_version"`
|
Autostart bool `json:"autostart,,string"`
|
||||||
Autostart bool `json:"autostart,,string"`
|
|
||||||
|
|
||||||
AllowProxy bool `json:"allow_proxy,,string"`
|
AllowProxy bool `json:"allow_proxy,,string"`
|
||||||
FetchWorkers int `json:"fetch_workers,,string"`
|
FetchWorkers int `json:"fetch_workers,,string"`
|
||||||
@ -241,10 +270,6 @@ func migratePrefsToVault(vault *vault.Vault, b []byte) error {
|
|||||||
errs = multierror.Append(errs, fmt.Errorf("failed to migrate first start: %w", err))
|
errs = multierror.Append(errs, fmt.Errorf("failed to migrate first start: %w", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := vault.SetFirstStartGUI(prefs.FirstStartGUI); err != nil {
|
|
||||||
errs = multierror.Append(errs, fmt.Errorf("failed to migrate first start GUI: %w", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := vault.SetColorScheme(prefs.ColorScheme); err != nil {
|
if err := vault.SetColorScheme(prefs.ColorScheme); err != nil {
|
||||||
errs = multierror.Append(errs, fmt.Errorf("failed to migrate color scheme: %w", err))
|
errs = multierror.Append(errs, fmt.Errorf("failed to migrate color scheme: %w", err))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,54 +38,44 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMigratePrefsToVault(t *testing.T) {
|
func TestMigratePrefsToVaultWithKeys(t *testing.T) {
|
||||||
// Create a new vault.
|
// Create a new vault.
|
||||||
vault, corrupt, err := vault.New(t.TempDir(), t.TempDir(), []byte("my secret key"))
|
vault, corrupt, err := vault.New(t.TempDir(), t.TempDir(), []byte("my secret key"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.False(t, corrupt)
|
require.False(t, corrupt)
|
||||||
|
|
||||||
// load the old prefs file.
|
// load the old prefs file.
|
||||||
b, err := os.ReadFile(filepath.Join("testdata", "prefs.json"))
|
configDir := filepath.Join("testdata", "with_keys")
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Migrate the old prefs file to the new vault.
|
// Migrate the old prefs file to the new vault.
|
||||||
require.NoError(t, migratePrefsToVault(vault, b))
|
require.NoError(t, migrateOldSettingsWithDir(configDir, vault))
|
||||||
|
|
||||||
// Check that the IMAP and SMTP prefs are migrated.
|
// Check Json Settings
|
||||||
require.Equal(t, 2143, vault.GetIMAPPort())
|
validateJSONPrefs(t, vault)
|
||||||
require.Equal(t, 2025, vault.GetSMTPPort())
|
|
||||||
require.True(t, vault.GetSMTPSSL())
|
|
||||||
|
|
||||||
// Check that the update channel is migrated.
|
// Check the keys were found and collected.
|
||||||
require.True(t, vault.GetAutoUpdate())
|
require.Equal(t, "-----BEGIN CERTIFICATE-----", string(vault.GetBridgeTLSCert()))
|
||||||
require.Equal(t, updater.EarlyChannel, vault.GetUpdateChannel())
|
require.Equal(t, "-----BEGIN RSA PRIVATE KEY-----", string(vault.GetBridgeTLSKey()))
|
||||||
require.Equal(t, 0.4849529004202015, vault.GetUpdateRollout())
|
}
|
||||||
|
|
||||||
// Check that the app settings have been migrated.
|
func TestMigratePrefsToVaultWithoutKeys(t *testing.T) {
|
||||||
require.False(t, vault.GetFirstStart())
|
// Create a new vault.
|
||||||
require.True(t, vault.GetFirstStartGUI())
|
vault, corrupt, err := vault.New(t.TempDir(), t.TempDir(), []byte("my secret key"))
|
||||||
require.Equal(t, "blablabla", vault.GetColorScheme())
|
|
||||||
require.Equal(t, "2.3.0+git", vault.GetLastVersion().String())
|
|
||||||
require.True(t, vault.GetAutostart())
|
|
||||||
|
|
||||||
// Check that the other app settings have been migrated.
|
|
||||||
require.Equal(t, 16, vault.SyncWorkers())
|
|
||||||
require.Equal(t, 16, vault.SyncAttPool())
|
|
||||||
require.False(t, vault.GetProxyAllowed())
|
|
||||||
require.False(t, vault.GetShowAllMail())
|
|
||||||
|
|
||||||
// Check that the cookies have been migrated.
|
|
||||||
jar, err := cookiejar.New(nil)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
require.False(t, corrupt)
|
||||||
|
|
||||||
cookies, err := cookies.NewCookieJar(jar, vault)
|
// load the old prefs file.
|
||||||
require.NoError(t, err)
|
configDir := filepath.Join("testdata", "without_keys")
|
||||||
|
|
||||||
url, err := url.Parse("https://api.protonmail.ch")
|
// Migrate the old prefs file to the new vault.
|
||||||
require.NoError(t, err)
|
require.NoError(t, migrateOldSettingsWithDir(configDir, vault))
|
||||||
|
|
||||||
// There should be a cookie for the API.
|
// Check Json Settings
|
||||||
require.NotEmpty(t, cookies.Cookies(url))
|
validateJSONPrefs(t, vault)
|
||||||
|
|
||||||
|
// Check the keys were found and collected.
|
||||||
|
require.NotEqual(t, []byte("-----BEGIN CERTIFICATE-----"), vault.GetBridgeTLSCert())
|
||||||
|
require.NotEqual(t, []byte("-----BEGIN RSA PRIVATE KEY-----"), vault.GetBridgeTLSKey())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestKeychainMigration(t *testing.T) {
|
func TestKeychainMigration(t *testing.T) {
|
||||||
@ -102,7 +92,7 @@ func TestKeychainMigration(t *testing.T) {
|
|||||||
oldCacheDir := filepath.Join(tmpDir, "protonmail", "bridge")
|
oldCacheDir := filepath.Join(tmpDir, "protonmail", "bridge")
|
||||||
require.NoError(t, os.MkdirAll(oldCacheDir, 0o700))
|
require.NoError(t, os.MkdirAll(oldCacheDir, 0o700))
|
||||||
|
|
||||||
oldPrefs, err := os.ReadFile(filepath.Join("testdata", "prefs.json"))
|
oldPrefs, err := os.ReadFile(filepath.Join("testdata", "without_keys", "protonmail", "bridge", "prefs.json"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
require.NoError(t, os.WriteFile(
|
require.NoError(t, os.WriteFile(
|
||||||
@ -197,3 +187,40 @@ func TestUserMigration(t *testing.T) {
|
|||||||
require.Equal(t, vault.CombinedMode, u.AddressMode())
|
require.Equal(t, vault.CombinedMode, u.AddressMode())
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateJSONPrefs(t *testing.T, vault *vault.Vault) {
|
||||||
|
// Check that the IMAP and SMTP prefs are migrated.
|
||||||
|
require.Equal(t, 2143, vault.GetIMAPPort())
|
||||||
|
require.Equal(t, 2025, vault.GetSMTPPort())
|
||||||
|
require.True(t, vault.GetSMTPSSL())
|
||||||
|
|
||||||
|
// Check that the update channel is migrated.
|
||||||
|
require.True(t, vault.GetAutoUpdate())
|
||||||
|
require.Equal(t, updater.EarlyChannel, vault.GetUpdateChannel())
|
||||||
|
require.Equal(t, 0.4849529004202015, vault.GetUpdateRollout())
|
||||||
|
|
||||||
|
// Check that the app settings have been migrated.
|
||||||
|
require.False(t, vault.GetFirstStart())
|
||||||
|
require.Equal(t, "blablabla", vault.GetColorScheme())
|
||||||
|
require.Equal(t, "2.3.0+git", vault.GetLastVersion().String())
|
||||||
|
require.True(t, vault.GetAutostart())
|
||||||
|
|
||||||
|
// Check that the other app settings have been migrated.
|
||||||
|
require.Equal(t, 16, vault.SyncWorkers())
|
||||||
|
require.Equal(t, 16, vault.SyncAttPool())
|
||||||
|
require.False(t, vault.GetProxyAllowed())
|
||||||
|
require.False(t, vault.GetShowAllMail())
|
||||||
|
|
||||||
|
// Check that the cookies have been migrated.
|
||||||
|
jar, err := cookiejar.New(nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cookies, err := cookies.NewCookieJar(jar, vault)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
url, err := url.Parse("https://api.protonmail.ch")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// There should be a cookie for the API.
|
||||||
|
require.NotEmpty(t, cookies.Cookies(url))
|
||||||
|
}
|
||||||
|
|||||||
1
internal/app/testdata/with_keys/protonmail/bridge/cert.pem
vendored
Normal file
1
internal/app/testdata/with_keys/protonmail/bridge/cert.pem
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
1
internal/app/testdata/with_keys/protonmail/bridge/key.pem
vendored
Normal file
1
internal/app/testdata/with_keys/protonmail/bridge/key.pem
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
-----BEGIN RSA PRIVATE KEY-----
|
||||||
31
internal/app/testdata/without_keys/protonmail/bridge/prefs.json
vendored
Normal file
31
internal/app/testdata/without_keys/protonmail/bridge/prefs.json
vendored
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"allow_proxy": "false",
|
||||||
|
"attachment_workers": "16",
|
||||||
|
"autostart": "true",
|
||||||
|
"autoupdate": "true",
|
||||||
|
"cache_compression": "true",
|
||||||
|
"cache_concurrent_read": "16",
|
||||||
|
"cache_concurrent_write": "16",
|
||||||
|
"cache_enabled": "true",
|
||||||
|
"cache_location": "/home/user/.config/protonmail/bridge/cache/c11/messages",
|
||||||
|
"cache_min_free_abs": "250000000",
|
||||||
|
"cache_min_free_rat": "",
|
||||||
|
"color_scheme": "blablabla",
|
||||||
|
"cookies": "{\"https://api.protonmail.ch\":[{\"Name\":\"Session-Id\",\"Value\":\"blablablablablablablablabla\",\"Path\":\"/\",\"Domain\":\"protonmail.ch\",\"Expires\":\"2023-02-19T00:20:40.269424437+01:00\",\"RawExpires\":\"\",\"MaxAge\":7776000,\"Secure\":true,\"HttpOnly\":true,\"SameSite\":0,\"Raw\":\"Session-Id=blablablablablablablablabla; Domain=protonmail.ch; Path=/; HttpOnly; Secure; Max-Age=7776000\",\"Unparsed\":null},{\"Name\":\"Tag\",\"Value\":\"default\",\"Path\":\"/\",\"Domain\":\"\",\"Expires\":\"2023-02-19T00:20:40.269428627+01:00\",\"RawExpires\":\"\",\"MaxAge\":7776000,\"Secure\":true,\"HttpOnly\":false,\"SameSite\":0,\"Raw\":\"Tag=default; Path=/; Secure; Max-Age=7776000\",\"Unparsed\":null}],\"https://protonmail.com\":[{\"Name\":\"Session-Id\",\"Value\":\"blablablablablablablablabla\",\"Path\":\"/\",\"Domain\":\"protonmail.com\",\"Expires\":\"2023-02-19T00:20:18.315084712+01:00\",\"RawExpires\":\"\",\"MaxAge\":7776000,\"Secure\":true,\"HttpOnly\":true,\"SameSite\":0,\"Raw\":\"Session-Id=Y3q2Mh-ClvqL6LWeYdfyPgAAABI; Domain=protonmail.com; Path=/; HttpOnly; Secure; Max-Age=7776000\",\"Unparsed\":null},{\"Name\":\"Tag\",\"Value\":\"redirect\",\"Path\":\"/\",\"Domain\":\"\",\"Expires\":\"2023-02-19T00:20:18.315087646+01:00\",\"RawExpires\":\"\",\"MaxAge\":7776000,\"Secure\":true,\"HttpOnly\":false,\"SameSite\":0,\"Raw\":\"Tag=redirect; Path=/; Secure; Max-Age=7776000\",\"Unparsed\":null}]}",
|
||||||
|
"fetch_workers": "16",
|
||||||
|
"first_time_start": "false",
|
||||||
|
"first_time_start_gui": "true",
|
||||||
|
"imap_workers": "16",
|
||||||
|
"is_all_mail_visible": "false",
|
||||||
|
"last_heartbeat": "325",
|
||||||
|
"last_used_version": "2.3.0+git",
|
||||||
|
"preferred_keychain": "secret-service",
|
||||||
|
"rebranding_migrated": "true",
|
||||||
|
"report_outgoing_email_without_encryption": "false",
|
||||||
|
"rollout": "0.4849529004202015",
|
||||||
|
"user_port_api": "1042",
|
||||||
|
"update_channel": "early",
|
||||||
|
"user_port_imap": "2143",
|
||||||
|
"user_port_smtp": "2025",
|
||||||
|
"user_ssl_smtp": "true"
|
||||||
|
}
|
||||||
@ -81,6 +81,7 @@ func newVault(locations *locations.Locations) (*vault.Vault, bool, bool, error)
|
|||||||
)
|
)
|
||||||
|
|
||||||
if key, err := getVaultKey(vaultDir); err != nil {
|
if key, err := getVaultKey(vaultDir); err != nil {
|
||||||
|
logrus.WithError(err).Error("Could not load/create vault key")
|
||||||
insecure = true
|
insecure = true
|
||||||
|
|
||||||
// We store the insecure vault in a separate directory
|
// We store the insecure vault in a separate directory
|
||||||
@ -89,12 +90,12 @@ func newVault(locations *locations.Locations) (*vault.Vault, bool, bool, error)
|
|||||||
vaultKey = key
|
vaultKey = key
|
||||||
}
|
}
|
||||||
|
|
||||||
gluonDir, err := locations.ProvideGluonPath()
|
gluonCacheDir, err := locations.ProvideGluonCachePath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, false, fmt.Errorf("could not provide gluon path: %w", err)
|
return nil, false, false, fmt.Errorf("could not provide gluon path: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
vault, corrupt, err := vault.New(vaultDir, gluonDir, vaultKey)
|
vault, corrupt, err := vault.New(vaultDir, gluonCacheDir, vaultKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, false, fmt.Errorf("could not create vault: %w", err)
|
return nil, false, false, fmt.Errorf("could not create vault: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,6 +21,7 @@ package bridge
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -38,6 +39,7 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/focus"
|
"github.com/ProtonMail/proton-bridge/v3/internal/focus"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/sentry"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||||
"github.com/bradenaw/juniper/xslices"
|
"github.com/bradenaw/juniper/xslices"
|
||||||
@ -108,6 +110,12 @@ type Bridge struct {
|
|||||||
logIMAPServer bool
|
logIMAPServer bool
|
||||||
logSMTP bool
|
logSMTP bool
|
||||||
|
|
||||||
|
// These two variables keep track of the startup values for the two settings of the same name.
|
||||||
|
// They are updated in the vault on startup so that we're sure they're updated in case of kill/crash,
|
||||||
|
// but we need to keep their initial value for the current instance of bridge.
|
||||||
|
firstStart bool
|
||||||
|
lastVersion *semver.Version
|
||||||
|
|
||||||
// tasks manages the bridge's goroutines.
|
// tasks manages the bridge's goroutines.
|
||||||
tasks *async.Group
|
tasks *async.Group
|
||||||
|
|
||||||
@ -216,13 +224,29 @@ func newBridge(
|
|||||||
return nil, fmt.Errorf("failed to load TLS config: %w", err)
|
return nil, fmt.Errorf("failed to load TLS config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
gluonDir, err := getGluonDir(vault)
|
gluonCacheDir, err := getGluonDir(vault)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get Gluon directory: %w", err)
|
return nil, fmt.Errorf("failed to get Gluon directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gluonDataDir, err := locator.ProvideGluonDataPath()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get Gluon Database directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
firstStart := vault.GetFirstStart()
|
||||||
|
if err := vault.SetFirstStart(false); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to save first start indicator: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
lastVersion := vault.GetLastVersion()
|
||||||
|
if err := vault.SetLastVersion(curVersion); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to save last version indicator: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
imapServer, err := newIMAPServer(
|
imapServer, err := newIMAPServer(
|
||||||
gluonDir,
|
gluonCacheDir,
|
||||||
|
gluonDataDir,
|
||||||
curVersion,
|
curVersion,
|
||||||
tlsConfig,
|
tlsConfig,
|
||||||
reporter,
|
reporter,
|
||||||
@ -272,6 +296,9 @@ func newBridge(
|
|||||||
logIMAPServer: logIMAPServer,
|
logIMAPServer: logIMAPServer,
|
||||||
logSMTP: logSMTP,
|
logSMTP: logSMTP,
|
||||||
|
|
||||||
|
firstStart: firstStart,
|
||||||
|
lastVersion: lastVersion,
|
||||||
|
|
||||||
tasks: tasks,
|
tasks: tasks,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -351,10 +378,11 @@ func (bridge *Bridge) init(tlsReporter TLSReporter) error {
|
|||||||
|
|
||||||
// Attempt to lazy load users when triggered.
|
// Attempt to lazy load users when triggered.
|
||||||
bridge.goLoad = bridge.tasks.Trigger(func(ctx context.Context) {
|
bridge.goLoad = bridge.tasks.Trigger(func(ctx context.Context) {
|
||||||
logrus.Info("Loading users")
|
|
||||||
|
|
||||||
if err := bridge.loadUsers(ctx); err != nil {
|
if err := bridge.loadUsers(ctx); err != nil {
|
||||||
logrus.WithError(err).Error("Failed to load users")
|
logrus.WithError(err).Error("Failed to load users")
|
||||||
|
if netErr := new(proton.NetError); !errors.As(err, &netErr) {
|
||||||
|
sentry.ReportError(bridge.reporter, "Failed to load users", err)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
bridge.publish(events.AllUsersLoaded{})
|
bridge.publish(events.AllUsersLoaded{})
|
||||||
}
|
}
|
||||||
@ -435,11 +463,6 @@ func (bridge *Bridge) Close(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bridge.watchers = nil
|
bridge.watchers = nil
|
||||||
|
|
||||||
// Save the last version of bridge that was run.
|
|
||||||
if err := bridge.vault.SetLastVersion(bridge.curVersion); err != nil {
|
|
||||||
logrus.WithError(err).Error("Failed to save last version")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) publish(event events.Event) {
|
func (bridge *Bridge) publish(event events.Event) {
|
||||||
|
|||||||
@ -23,7 +23,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@ -35,6 +35,7 @@ import (
|
|||||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/certs"
|
"github.com/ProtonMail/proton-bridge/v3/internal/certs"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/cookies"
|
"github.com/ProtonMail/proton-bridge/v3/internal/cookies"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/focus"
|
"github.com/ProtonMail/proton-bridge/v3/internal/focus"
|
||||||
@ -45,6 +46,7 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/tests"
|
"github.com/ProtonMail/proton-bridge/v3/tests"
|
||||||
"github.com/bradenaw/juniper/xslices"
|
"github.com/bradenaw/juniper/xslices"
|
||||||
|
"github.com/emersion/go-imap/client"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -349,7 +351,7 @@ func TestBridge_BadVaultKey(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBridge_MissingGluonDir(t *testing.T) {
|
func TestBridge_MissingGluonStore(t *testing.T) {
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||||
var gluonDir string
|
var gluonDir string
|
||||||
|
|
||||||
@ -361,13 +363,36 @@ func TestBridge_MissingGluonDir(t *testing.T) {
|
|||||||
require.NoError(t, bridge.SetGluonDir(ctx, t.TempDir()))
|
require.NoError(t, bridge.SetGluonDir(ctx, t.TempDir()))
|
||||||
|
|
||||||
// Get the gluon dir.
|
// Get the gluon dir.
|
||||||
gluonDir = bridge.GetGluonDir()
|
gluonDir = bridge.GetGluonCacheDir()
|
||||||
})
|
})
|
||||||
|
|
||||||
// The user removes the gluon dir while bridge is not running.
|
// The user removes the gluon dir while bridge is not running.
|
||||||
require.NoError(t, os.RemoveAll(gluonDir))
|
require.NoError(t, os.RemoveAll(gluonDir))
|
||||||
|
|
||||||
// Bridge starts but can't find the gluon dir; there should be no error.
|
// Bridge starts but can't find the gluon store dir; there should be no error.
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
|
// ...
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBridge_MissingGluonDatabase(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||||
|
var gluonDir string
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
|
_, err := bridge.LoginFull(context.Background(), username, password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Get the gluon dir.
|
||||||
|
gluonDir, err = bridge.GetGluonDataDir()
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
// The user removes the gluon dir while bridge is not running.
|
||||||
|
require.NoError(t, os.RemoveAll(gluonDir))
|
||||||
|
|
||||||
|
// Bridge starts but can't find the gluon database dir; there should be no error.
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
// ...
|
// ...
|
||||||
})
|
})
|
||||||
@ -456,41 +481,143 @@ func TestBridge_FactoryReset(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBridge_ChangeCacheDirectoryFailsBetweenDifferentVolumes(t *testing.T) {
|
func TestBridge_InitGluonDirectory(t *testing.T) {
|
||||||
if runtime.GOOS != "windows" {
|
|
||||||
t.Skip("Test only necessary on windows")
|
|
||||||
}
|
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
// Change directory
|
configDir, err := b.GetGluonDataDir()
|
||||||
err := bridge.SetGluonDir(ctx, "XX:\\")
|
require.NoError(t, err)
|
||||||
require.Error(t, err)
|
|
||||||
|
_, err = os.ReadDir(bridge.ApplyGluonCachePathSuffix(b.GetGluonCacheDir()))
|
||||||
|
require.False(t, os.IsNotExist(err))
|
||||||
|
|
||||||
|
_, err = os.ReadDir(bridge.ApplyGluonConfigPathSuffix(configDir))
|
||||||
|
require.False(t, os.IsNotExist(err))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBridge_ChangeCacheDirectory(t *testing.T) {
|
func TestBridge_ChangeCacheDirectory(t *testing.T) {
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
userID, addrID, err := s.CreateUser("imap", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
createNumMessages(ctx, t, c, addrID, labelID, 10)
|
||||||
|
})
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
newCacheDir := t.TempDir()
|
newCacheDir := t.TempDir()
|
||||||
currentCacheDir := bridge.GetGluonDir()
|
currentCacheDir := b.GetGluonCacheDir()
|
||||||
|
configDir, err := b.GetGluonDataDir()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Login the user.
|
// Login the user.
|
||||||
userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
|
||||||
|
defer done()
|
||||||
|
userID, err := b.LoginFull(ctx, "imap", password, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, userID, (<-syncCh).UserID)
|
||||||
|
|
||||||
// The user is now connected.
|
// The user is now connected.
|
||||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
require.Equal(t, []string{userID}, b.GetUserIDs())
|
||||||
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
|
require.Equal(t, []string{userID}, getConnectedUserIDs(t, b))
|
||||||
|
|
||||||
// Change directory
|
// Change directory
|
||||||
err = bridge.SetGluonDir(ctx, newCacheDir)
|
err = b.SetGluonDir(ctx, newCacheDir)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
_, err = os.ReadDir(currentCacheDir)
|
// Old store should no more exists.
|
||||||
|
_, err = os.ReadDir(bridge.ApplyGluonCachePathSuffix(currentCacheDir))
|
||||||
require.True(t, os.IsNotExist(err))
|
require.True(t, os.IsNotExist(err))
|
||||||
|
// Database should not have changed.
|
||||||
|
_, err = os.ReadDir(bridge.ApplyGluonConfigPathSuffix(configDir))
|
||||||
|
require.False(t, os.IsNotExist(err))
|
||||||
|
|
||||||
require.Equal(t, newCacheDir, bridge.GetGluonDir())
|
// New path should have Gluon sub-folder.
|
||||||
|
require.Equal(t, filepath.Join(newCacheDir, "gluon"), b.GetGluonCacheDir())
|
||||||
|
// And store should be inside it.
|
||||||
|
_, err = os.ReadDir(bridge.ApplyGluonCachePathSuffix(b.GetGluonCacheDir()))
|
||||||
|
require.False(t, os.IsNotExist(err))
|
||||||
|
|
||||||
|
// We should be able to fetch.
|
||||||
|
info, err := b.GetUserInfo(userID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, info.State == bridge.Connected)
|
||||||
|
|
||||||
|
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
||||||
|
defer func() { _ = client.Logout() }()
|
||||||
|
|
||||||
|
status, err := client.Select(`Folders/folder`, false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, uint32(10), status.Messages)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBridge_ChangeAddressOrder(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||||
|
// Create a user.
|
||||||
|
userID, addrID, err := s.CreateUser("imap", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create a second address for the user.
|
||||||
|
aliasID, err := s.CreateAddress(userID, "alias@"+s.GetDomain(), password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create 10 messages for the user.
|
||||||
|
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 10)
|
||||||
|
})
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
|
// Log the user in with its first address.
|
||||||
|
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
|
||||||
|
defer done()
|
||||||
|
userID, err := b.LoginFull(ctx, "imap", password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, userID, (<-syncCh).UserID)
|
||||||
|
|
||||||
|
// We should see 10 messages in the inbox.
|
||||||
|
info, err := b.GetUserInfo(userID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, info.State == bridge.Connected)
|
||||||
|
|
||||||
|
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
||||||
|
defer func() { _ = client.Logout() }()
|
||||||
|
|
||||||
|
status, err := client.Select(`Inbox`, false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, uint32(10), status.Messages)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Make the second address the primary one.
|
||||||
|
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
require.NoError(t, c.OrderAddresses(ctx, proton.OrderAddressesReq{AddressIDs: []string{aliasID, addrID}}))
|
||||||
|
})
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
|
// We should still see 10 messages in the inbox.
|
||||||
|
info, err := b.GetUserInfo(userID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, info.State == bridge.Connected)
|
||||||
|
|
||||||
|
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
||||||
|
defer func() { _ = client.Logout() }()
|
||||||
|
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
status, err := client.Select(`Inbox`, false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return status.Messages == 10
|
||||||
|
}, 5*time.Second, 100*time.Millisecond)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -522,20 +649,25 @@ func withEnv(t *testing.T, tests func(context.Context, *server.Server, *proton.N
|
|||||||
tests(ctx, server, netCtl, locations, vaultKey)
|
tests(ctx, server, netCtl, locations, vaultKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// withMocks creates the mock objects used in the tests.
|
||||||
|
func withMocks(t *testing.T, tests func(*bridge.Mocks)) {
|
||||||
|
mocks := bridge.NewMocks(t, v2_3_0, v2_3_0)
|
||||||
|
defer mocks.Close()
|
||||||
|
|
||||||
|
tests(mocks)
|
||||||
|
}
|
||||||
|
|
||||||
// withBridge creates a new bridge which points to the given API URL and uses the given keychain, and closes it when done.
|
// withBridge creates a new bridge which points to the given API URL and uses the given keychain, and closes it when done.
|
||||||
func withBridge(
|
func withBridgeNoMocks(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
t *testing.T,
|
t *testing.T,
|
||||||
|
mocks *bridge.Mocks,
|
||||||
apiURL string,
|
apiURL string,
|
||||||
netCtl *proton.NetCtl,
|
netCtl *proton.NetCtl,
|
||||||
locator bridge.Locator,
|
locator bridge.Locator,
|
||||||
vaultKey []byte,
|
vaultKey []byte,
|
||||||
tests func(*bridge.Bridge, *bridge.Mocks),
|
tests func(*bridge.Bridge),
|
||||||
) {
|
) {
|
||||||
// Create the mock objects used in the tests.
|
|
||||||
mocks := bridge.NewMocks(t, v2_3_0, v2_3_0)
|
|
||||||
defer mocks.Close()
|
|
||||||
|
|
||||||
// Bridge will disable the proxy by default at startup.
|
// Bridge will disable the proxy by default at startup.
|
||||||
mocks.ProxyCtl.EXPECT().DisallowProxy()
|
mocks.ProxyCtl.EXPECT().DisallowProxy()
|
||||||
|
|
||||||
@ -590,7 +722,24 @@ func withBridge(
|
|||||||
defer bridge.Close(ctx)
|
defer bridge.Close(ctx)
|
||||||
|
|
||||||
// Use the bridge.
|
// Use the bridge.
|
||||||
tests(bridge, mocks)
|
tests(bridge)
|
||||||
|
}
|
||||||
|
|
||||||
|
// withBridge creates a new bridge which points to the given API URL and uses the given keychain, and closes it when done.
|
||||||
|
func withBridge(
|
||||||
|
ctx context.Context,
|
||||||
|
t *testing.T,
|
||||||
|
apiURL string,
|
||||||
|
netCtl *proton.NetCtl,
|
||||||
|
locator bridge.Locator,
|
||||||
|
vaultKey []byte,
|
||||||
|
tests func(*bridge.Bridge, *bridge.Mocks),
|
||||||
|
) {
|
||||||
|
withMocks(t, func(mocks *bridge.Mocks) {
|
||||||
|
withBridgeNoMocks(ctx, t, mocks, apiURL, netCtl, locator, vaultKey, func(bridge *bridge.Bridge) {
|
||||||
|
tests(bridge, mocks)
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func waitForEvent[T any](t *testing.T, eventCh <-chan events.Event, wantEvent T) {
|
func waitForEvent[T any](t *testing.T, eventCh <-chan events.Event, wantEvent T) {
|
||||||
|
|||||||
@ -18,6 +18,8 @@
|
|||||||
package bridge
|
package bridge
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
@ -62,3 +64,75 @@ func moveFile(from, to string) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func copyDir(from, to string) error {
|
||||||
|
entries, err := os.ReadDir(from)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := createIfNotExists(to, 0o700); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, entry := range entries {
|
||||||
|
sourcePath := filepath.Join(from, entry.Name())
|
||||||
|
destPath := filepath.Join(to, entry.Name())
|
||||||
|
|
||||||
|
if entry.IsDir() {
|
||||||
|
if err := copyDir(sourcePath, destPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := copyFile(sourcePath, destPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyFile(srcFile, dstFile string) error {
|
||||||
|
out, err := os.Create(filepath.Clean(dstFile))
|
||||||
|
defer func(out *os.File) {
|
||||||
|
_ = out.Close()
|
||||||
|
}(out)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
in, err := os.Open(filepath.Clean(srcFile))
|
||||||
|
defer func(in *os.File) {
|
||||||
|
_ = in.Close()
|
||||||
|
}(in)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy(out, in)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func exists(filePath string) bool {
|
||||||
|
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func createIfNotExists(dir string, perm os.FileMode) error {
|
||||||
|
if exists(dir) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(dir, perm); err != nil {
|
||||||
|
return fmt.Errorf("failed to create directory: '%s', error: '%s'", dir, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@ -20,13 +20,10 @@ package bridge
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
|
||||||
|
|
||||||
"github.com/Masterminds/semver/v3"
|
"github.com/Masterminds/semver/v3"
|
||||||
"github.com/ProtonMail/gluon"
|
"github.com/ProtonMail/gluon"
|
||||||
@ -103,6 +100,8 @@ func (bridge *Bridge) closeIMAP(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// addIMAPUser connects the given user to gluon.
|
// addIMAPUser connects the given user to gluon.
|
||||||
|
//
|
||||||
|
//nolint:funlen
|
||||||
func (bridge *Bridge) addIMAPUser(ctx context.Context, user *user.User) error {
|
func (bridge *Bridge) addIMAPUser(ctx context.Context, user *user.User) error {
|
||||||
if bridge.imapServer == nil {
|
if bridge.imapServer == nil {
|
||||||
return fmt.Errorf("no imap server instance running")
|
return fmt.Errorf("no imap server instance running")
|
||||||
@ -122,9 +121,53 @@ func (bridge *Bridge) addIMAPUser(ctx context.Context, user *user.User) error {
|
|||||||
if gluonID, ok := user.GetGluonID(addrID); ok {
|
if gluonID, ok := user.GetGluonID(addrID); ok {
|
||||||
log.WithField("gluonID", gluonID).Info("Loading existing IMAP user")
|
log.WithField("gluonID", gluonID).Info("Loading existing IMAP user")
|
||||||
|
|
||||||
if err := bridge.imapServer.LoadUser(ctx, imapConn, gluonID, user.GluonKey()); err != nil {
|
// Load the user, checking whether the DB was newly created.
|
||||||
|
isNew, err := bridge.imapServer.LoadUser(ctx, imapConn, gluonID, user.GluonKey())
|
||||||
|
if err != nil {
|
||||||
return fmt.Errorf("failed to load IMAP user: %w", err)
|
return fmt.Errorf("failed to load IMAP user: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isNew {
|
||||||
|
// If the DB was newly created, clear the sync status; gluon's DB was not found.
|
||||||
|
logrus.Warn("IMAP user DB was newly created, clearing sync status")
|
||||||
|
|
||||||
|
// Remove the user from IMAP so we can clear the sync status.
|
||||||
|
if err := bridge.imapServer.RemoveUser(ctx, gluonID, false); err != nil {
|
||||||
|
return fmt.Errorf("failed to remove IMAP user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the sync status -- we need to resync all messages.
|
||||||
|
if err := user.ClearSyncStatus(); err != nil {
|
||||||
|
return fmt.Errorf("failed to clear sync status: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the user back to the IMAP server.
|
||||||
|
if isNew, err := bridge.imapServer.LoadUser(ctx, imapConn, gluonID, user.GluonKey()); err != nil {
|
||||||
|
return fmt.Errorf("failed to add IMAP user: %w", err)
|
||||||
|
} else if isNew {
|
||||||
|
panic("IMAP user should already have a database")
|
||||||
|
}
|
||||||
|
} else if status := user.GetSyncStatus(); !status.HasLabels {
|
||||||
|
// Otherwise, the DB already exists -- if the labels are not yet synced, we need to re-create the DB.
|
||||||
|
if err := bridge.imapServer.RemoveUser(ctx, gluonID, true); err != nil {
|
||||||
|
return fmt.Errorf("failed to remove old IMAP user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := user.RemoveGluonID(addrID, gluonID); err != nil {
|
||||||
|
return fmt.Errorf("failed to remove old IMAP user ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gluonID, err := bridge.imapServer.AddUser(ctx, imapConn, user.GluonKey())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to add IMAP user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := user.SetGluonID(addrID, gluonID); err != nil {
|
||||||
|
return fmt.Errorf("failed to set IMAP user ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.WithField("gluonID", gluonID).Info("Re-created IMAP user")
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Info("Creating new IMAP user")
|
log.Info("Creating new IMAP user")
|
||||||
|
|
||||||
@ -141,6 +184,9 @@ func (bridge *Bridge) addIMAPUser(ctx context.Context, user *user.User) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Trigger a sync for the user, if needed.
|
||||||
|
user.TriggerSync()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,6 +195,7 @@ func (bridge *Bridge) removeIMAPUser(ctx context.Context, user *user.User, withD
|
|||||||
if bridge.imapServer == nil {
|
if bridge.imapServer == nil {
|
||||||
return fmt.Errorf("no imap server instance running")
|
return fmt.Errorf("no imap server instance running")
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.WithFields(logrus.Fields{
|
logrus.WithFields(logrus.Fields{
|
||||||
"userID": user.ID(),
|
"userID": user.ID(),
|
||||||
"withData": withData,
|
"withData": withData,
|
||||||
@ -199,31 +246,24 @@ func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getGluonDir(encVault *vault.Vault) (string, error) {
|
func getGluonDir(encVault *vault.Vault) (string, error) {
|
||||||
empty, exists, err := isEmpty(encVault.GetGluonDir())
|
if err := os.MkdirAll(encVault.GetGluonCacheDir(), 0o700); err != nil {
|
||||||
if err != nil {
|
return "", fmt.Errorf("failed to create gluon dir: %w", err)
|
||||||
return "", fmt.Errorf("failed to check if gluon dir is empty: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !exists {
|
return encVault.GetGluonCacheDir(), nil
|
||||||
if err := os.MkdirAll(encVault.GetGluonDir(), 0o700); err != nil {
|
}
|
||||||
return "", fmt.Errorf("failed to create gluon dir: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if empty {
|
func ApplyGluonCachePathSuffix(basePath string) string {
|
||||||
if err := encVault.ForUser(runtime.NumCPU(), func(user *vault.User) error {
|
return filepath.Join(basePath, "backend", "store")
|
||||||
return user.ClearSyncStatus()
|
}
|
||||||
}); err != nil {
|
|
||||||
return "", fmt.Errorf("failed to reset user sync status: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return encVault.GetGluonDir(), nil
|
func ApplyGluonConfigPathSuffix(basePath string) string {
|
||||||
|
return filepath.Join(basePath, "backend", "db")
|
||||||
}
|
}
|
||||||
|
|
||||||
// nolint:funlen
|
// nolint:funlen
|
||||||
func newIMAPServer(
|
func newIMAPServer(
|
||||||
gluonDir string,
|
gluonCacheDir, gluonConfigDir string,
|
||||||
version *semver.Version,
|
version *semver.Version,
|
||||||
tlsConfig *tls.Config,
|
tlsConfig *tls.Config,
|
||||||
reporter reporter.Reporter,
|
reporter reporter.Reporter,
|
||||||
@ -231,11 +271,15 @@ func newIMAPServer(
|
|||||||
eventCh chan<- imapEvents.Event,
|
eventCh chan<- imapEvents.Event,
|
||||||
tasks *async.Group,
|
tasks *async.Group,
|
||||||
) (*gluon.Server, error) {
|
) (*gluon.Server, error) {
|
||||||
|
gluonCacheDir = ApplyGluonCachePathSuffix(gluonCacheDir)
|
||||||
|
gluonConfigDir = ApplyGluonConfigPathSuffix(gluonConfigDir)
|
||||||
|
|
||||||
logrus.WithFields(logrus.Fields{
|
logrus.WithFields(logrus.Fields{
|
||||||
"gluonDir": gluonDir,
|
"gluonStore": gluonCacheDir,
|
||||||
"version": version,
|
"gluonDB": gluonConfigDir,
|
||||||
"logClient": logClient,
|
"version": version,
|
||||||
"logServer": logServer,
|
"logClient": logClient,
|
||||||
|
"logServer": logServer,
|
||||||
}).Info("Creating IMAP server")
|
}).Info("Creating IMAP server")
|
||||||
|
|
||||||
if logClient || logServer {
|
if logClient || logServer {
|
||||||
@ -263,7 +307,8 @@ func newIMAPServer(
|
|||||||
|
|
||||||
imapServer, err := gluon.New(
|
imapServer, err := gluon.New(
|
||||||
gluon.WithTLS(tlsConfig),
|
gluon.WithTLS(tlsConfig),
|
||||||
gluon.WithDataDir(gluonDir),
|
gluon.WithDataDir(gluonCacheDir),
|
||||||
|
gluon.WithDatabaseDir(gluonConfigDir),
|
||||||
gluon.WithStoreBuilder(new(storeBuilder)),
|
gluon.WithStoreBuilder(new(storeBuilder)),
|
||||||
gluon.WithLogger(imapClientLog, imapServerLog),
|
gluon.WithLogger(imapClientLog, imapServerLog),
|
||||||
getGluonVersionInfo(version),
|
getGluonVersionInfo(version),
|
||||||
@ -297,25 +342,6 @@ func getGluonVersionInfo(version *semver.Version) gluon.Option {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// isEmpty returns whether the given directory is empty.
|
|
||||||
// If the directory does not exist, the second return value is false.
|
|
||||||
func isEmpty(dir string) (bool, bool, error) {
|
|
||||||
if _, err := os.Stat(dir); err != nil {
|
|
||||||
if !errors.Is(err, fs.ErrNotExist) {
|
|
||||||
return false, false, fmt.Errorf("failed to stat %s: %w", dir, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return true, false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
entries, err := os.ReadDir(dir)
|
|
||||||
if err != nil {
|
|
||||||
return false, false, fmt.Errorf("failed to read dir %s: %w", dir, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return len(entries) == 0, true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type storeBuilder struct{}
|
type storeBuilder struct{}
|
||||||
|
|
||||||
func (*storeBuilder) New(path, userID string, passphrase []byte) (store.Store, error) {
|
func (*storeBuilder) New(path, userID string, passphrase []byte) (store.Store, error) {
|
||||||
|
|||||||
@ -18,10 +18,12 @@
|
|||||||
package bridge_test
|
package bridge_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@ -30,6 +32,7 @@ import (
|
|||||||
"github.com/ProtonMail/go-proton-api/server"
|
"github.com/ProtonMail/go-proton-api/server"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
"github.com/emersion/go-imap"
|
"github.com/emersion/go-imap"
|
||||||
"github.com/emersion/go-imap/client"
|
"github.com/emersion/go-imap/client"
|
||||||
"github.com/emersion/go-sasl"
|
"github.com/emersion/go-sasl"
|
||||||
@ -42,7 +45,7 @@ func TestBridge_Send(t *testing.T) {
|
|||||||
_, _, err := s.CreateUser("recipient", password)
|
_, _, err := s.CreateUser("recipient", password)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
senderUserID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
senderUserID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@ -100,7 +103,7 @@ func TestBridge_Send(t *testing.T) {
|
|||||||
defer recipientIMAPClient.Logout() //nolint:errcheck
|
defer recipientIMAPClient.Logout() //nolint:errcheck
|
||||||
|
|
||||||
// Sender should have 10 messages in the sent folder.
|
// Sender should have 10 messages in the sent folder.
|
||||||
// Recipient should have 0 messages in inbox.
|
// Recipient should have 10 messages in inbox.
|
||||||
require.Eventually(t, func() bool {
|
require.Eventually(t, func() bool {
|
||||||
sent, err := senderIMAPClient.Status(`Sent`, []imap.StatusItem{imap.StatusMessages})
|
sent, err := senderIMAPClient.Status(`Sent`, []imap.StatusItem{imap.StatusMessages})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -113,3 +116,217 @@ func TestBridge_Send(t *testing.T) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBridge_SendDraftFlags(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
|
// Create a recipient user.
|
||||||
|
_, _, err := s.CreateUser("recipient", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// The sender should be fully synced.
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
||||||
|
defer done()
|
||||||
|
|
||||||
|
userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, userID, (<-syncCh).UserID)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Start the bridge.
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
// Get the sender user info.
|
||||||
|
userInfo, err := bridge.QueryUserInfo(username)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Connect the sender IMAP client.
|
||||||
|
imapClient, err := client.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, imapClient.Login(userInfo.Addresses[0], string(userInfo.BridgePass)))
|
||||||
|
defer imapClient.Logout() //nolint:errcheck
|
||||||
|
|
||||||
|
// The message to send.
|
||||||
|
const message = `Subject: Test\r\n\r\nHello world!`
|
||||||
|
|
||||||
|
// Save a draft.
|
||||||
|
require.NoError(t, imapClient.Append("Drafts", []string{imap.DraftFlag}, time.Now(), strings.NewReader(message)))
|
||||||
|
|
||||||
|
// Assert that the draft exists and is marked as a draft.
|
||||||
|
{
|
||||||
|
messages, err := clientFetch(imapClient, "Drafts")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, messages, 1)
|
||||||
|
require.Contains(t, messages[0].Flags, imap.DraftFlag)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect the SMTP client.
|
||||||
|
smtpClient, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort())))
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer smtpClient.Close() //nolint:errcheck
|
||||||
|
|
||||||
|
// Upgrade to TLS.
|
||||||
|
require.NoError(t, smtpClient.StartTLS(&tls.Config{InsecureSkipVerify: true}))
|
||||||
|
|
||||||
|
// Authorize with SASL PLAIN.
|
||||||
|
require.NoError(t, smtpClient.Auth(sasl.NewPlainClient(
|
||||||
|
userInfo.Addresses[0],
|
||||||
|
userInfo.Addresses[0],
|
||||||
|
string(userInfo.BridgePass)),
|
||||||
|
))
|
||||||
|
|
||||||
|
// Send the message.
|
||||||
|
require.NoError(t, smtpClient.SendMail(
|
||||||
|
userInfo.Addresses[0],
|
||||||
|
[]string{"recipient@" + s.GetDomain()},
|
||||||
|
strings.NewReader(message),
|
||||||
|
))
|
||||||
|
|
||||||
|
// Delete the draft: add the \Deleted flag and expunge.
|
||||||
|
{
|
||||||
|
status, err := imapClient.Select("Drafts", false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, uint32(1), status.Messages)
|
||||||
|
|
||||||
|
// Add the \Deleted flag.
|
||||||
|
require.NoError(t, clientStore(imapClient, 1, 1, true, imap.FormatFlagsOp(imap.AddFlags, true), imap.DeletedFlag))
|
||||||
|
|
||||||
|
// Expunge.
|
||||||
|
require.NoError(t, imapClient.Expunge(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert that the draft is eventually gone.
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
status, err := imapClient.Select("Drafts", false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return status.Messages == 0
|
||||||
|
}, 10*time.Second, 100*time.Millisecond)
|
||||||
|
|
||||||
|
// Assert that the message is eventually in the sent folder.
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
messages, err := clientFetch(imapClient, "Sent")
|
||||||
|
require.NoError(t, err)
|
||||||
|
return len(messages) == 1
|
||||||
|
}, 10*time.Second, 100*time.Millisecond)
|
||||||
|
|
||||||
|
// Assert that the message is not marked as a draft.
|
||||||
|
{
|
||||||
|
messages, err := clientFetch(imapClient, "Sent")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, messages, 1)
|
||||||
|
require.NotContains(t, messages[0].Flags, imap.DraftFlag)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBridge_SendInvite(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
|
// Create a recipient user.
|
||||||
|
_, _, err := s.CreateUser("recipient", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Set "attach public keys" to true for the user.
|
||||||
|
withClient(ctx, t, s, username, password, func(ctx context.Context, client *proton.Client) {
|
||||||
|
settings, err := client.SetAttachPublicKey(ctx, proton.SetAttachPublicKeyReq{AttachPublicKey: true})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, proton.Bool(true), settings.AttachPublicKey)
|
||||||
|
})
|
||||||
|
|
||||||
|
// The sender should be fully synced.
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
||||||
|
defer done()
|
||||||
|
|
||||||
|
userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, userID, (<-syncCh).UserID)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Start the bridge.
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
// Get the sender user info.
|
||||||
|
userInfo, err := bridge.QueryUserInfo(username)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Connect the sender IMAP client.
|
||||||
|
imapClient, err := client.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, imapClient.Login(userInfo.Addresses[0], string(userInfo.BridgePass)))
|
||||||
|
defer imapClient.Logout() //nolint:errcheck
|
||||||
|
|
||||||
|
// The message to send.
|
||||||
|
b, err := os.ReadFile("testdata/invite.eml")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Save a draft.
|
||||||
|
require.NoError(t, imapClient.Append("Drafts", []string{imap.DraftFlag}, time.Now(), bytes.NewReader(b)))
|
||||||
|
|
||||||
|
// Assert that the draft exists and is marked as a draft.
|
||||||
|
{
|
||||||
|
messages, err := clientFetch(imapClient, "Drafts")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, messages, 1)
|
||||||
|
require.Contains(t, messages[0].Flags, imap.DraftFlag)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect the SMTP client.
|
||||||
|
smtpClient, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort())))
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer smtpClient.Close() //nolint:errcheck
|
||||||
|
|
||||||
|
// Upgrade to TLS.
|
||||||
|
require.NoError(t, smtpClient.StartTLS(&tls.Config{InsecureSkipVerify: true}))
|
||||||
|
|
||||||
|
// Authorize with SASL PLAIN.
|
||||||
|
require.NoError(t, smtpClient.Auth(sasl.NewPlainClient(
|
||||||
|
userInfo.Addresses[0],
|
||||||
|
userInfo.Addresses[0],
|
||||||
|
string(userInfo.BridgePass)),
|
||||||
|
))
|
||||||
|
|
||||||
|
// Send the message.
|
||||||
|
require.NoError(t, smtpClient.SendMail(
|
||||||
|
userInfo.Addresses[0],
|
||||||
|
[]string{"recipient@" + s.GetDomain()},
|
||||||
|
bytes.NewReader(b),
|
||||||
|
))
|
||||||
|
|
||||||
|
// Delete the draft: add the \Deleted flag and expunge.
|
||||||
|
{
|
||||||
|
status, err := imapClient.Select("Drafts", false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, uint32(1), status.Messages)
|
||||||
|
|
||||||
|
// Add the \Deleted flag.
|
||||||
|
require.NoError(t, clientStore(imapClient, 1, 1, true, imap.FormatFlagsOp(imap.AddFlags, true), imap.DeletedFlag))
|
||||||
|
|
||||||
|
// Expunge.
|
||||||
|
require.NoError(t, imapClient.Expunge(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert that the draft is eventually gone.
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
status, err := imapClient.Select("Drafts", false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return status.Messages == 0
|
||||||
|
}, 10*time.Second, 100*time.Millisecond)
|
||||||
|
|
||||||
|
// Assert that the message is eventually in the sent folder.
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
messages, err := clientFetch(imapClient, "Sent")
|
||||||
|
require.NoError(t, err)
|
||||||
|
return len(messages) == 1
|
||||||
|
}, 10*time.Second, 100*time.Millisecond)
|
||||||
|
|
||||||
|
// Assert that the message is not marked as a draft.
|
||||||
|
{
|
||||||
|
messages, err := clientFetch(imapClient, "Sent")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, messages, 1)
|
||||||
|
require.NotContains(t, messages[0].Flags, imap.DraftFlag)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/Masterminds/semver/v3"
|
"github.com/Masterminds/semver/v3"
|
||||||
@ -114,38 +115,47 @@ func (bridge *Bridge) SetSMTPSSL(newSSL bool) error {
|
|||||||
return bridge.restartSMTP()
|
return bridge.restartSMTP()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) GetGluonDir() string {
|
func (bridge *Bridge) GetGluonCacheDir() string {
|
||||||
return bridge.vault.GetGluonDir()
|
return bridge.vault.GetGluonCacheDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) GetGluonDataDir() (string, error) {
|
||||||
|
return bridge.locator.ProvideGluonDataPath()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) SetGluonDir(ctx context.Context, newGluonDir string) error {
|
func (bridge *Bridge) SetGluonDir(ctx context.Context, newGluonDir string) error {
|
||||||
return safe.RLockRet(func() error {
|
return safe.RLockRet(func() error {
|
||||||
currentGluonDir := bridge.GetGluonDir()
|
currentGluonDir := bridge.GetGluonCacheDir()
|
||||||
|
newGluonDir = filepath.Join(newGluonDir, "gluon")
|
||||||
if newGluonDir == currentGluonDir {
|
if newGluonDir == currentGluonDir {
|
||||||
return fmt.Errorf("new gluon dir is the same as the old one")
|
return fmt.Errorf("new gluon dir is the same as the old one")
|
||||||
}
|
}
|
||||||
|
|
||||||
currentVolumeName := filepath.VolumeName(currentGluonDir)
|
if err := bridge.stopEventLoops(); err != nil {
|
||||||
newVolumeName := filepath.VolumeName(newGluonDir)
|
return err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
err := bridge.startEventLoops(ctx)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
if currentVolumeName != newVolumeName {
|
if err := bridge.moveGluonCacheDir(currentGluonDir, newGluonDir); err != nil {
|
||||||
return fmt.Errorf("it's currently not possible to move the cache between different volumes")
|
logrus.WithError(err).Error("failed to move GluonCacheDir")
|
||||||
|
if err := bridge.vault.SetGluonDir(currentGluonDir); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := bridge.closeIMAP(context.Background()); err != nil {
|
gluonDataDir, err := bridge.GetGluonDataDir()
|
||||||
return fmt.Errorf("failed to close IMAP: %w", err)
|
if err != nil {
|
||||||
}
|
panic(fmt.Errorf("failed to get Gluon Database directory: %w", err))
|
||||||
|
|
||||||
if err := moveDir(bridge.GetGluonDir(), newGluonDir); err != nil {
|
|
||||||
return fmt.Errorf("failed to move gluon dir: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := bridge.vault.SetGluonDir(newGluonDir); err != nil {
|
|
||||||
return fmt.Errorf("failed to set new gluon dir: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
imapServer, err := newIMAPServer(
|
imapServer, err := newIMAPServer(
|
||||||
bridge.vault.GetGluonDir(),
|
bridge.vault.GetGluonCacheDir(),
|
||||||
|
gluonDataDir,
|
||||||
bridge.curVersion,
|
bridge.curVersion,
|
||||||
bridge.tlsConfig,
|
bridge.tlsConfig,
|
||||||
bridge.reporter,
|
bridge.reporter,
|
||||||
@ -155,25 +165,60 @@ func (bridge *Bridge) SetGluonDir(ctx context.Context, newGluonDir string) error
|
|||||||
bridge.tasks,
|
bridge.tasks,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create new IMAP server: %w", err)
|
panic(fmt.Errorf("failed to create new IMAP server: %w", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
bridge.imapServer = imapServer
|
bridge.imapServer = imapServer
|
||||||
|
|
||||||
for _, user := range bridge.users {
|
|
||||||
if err := bridge.addIMAPUser(ctx, user); err != nil {
|
|
||||||
return fmt.Errorf("failed to add users to new IMAP server: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := bridge.serveIMAP(); err != nil {
|
|
||||||
return fmt.Errorf("failed to serve IMAP: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}, bridge.usersLock)
|
}, bridge.usersLock)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) moveGluonCacheDir(oldGluonDir, newGluonDir string) error {
|
||||||
|
logrus.Infof("gluon cache moving from %s to %s", oldGluonDir, newGluonDir)
|
||||||
|
oldCacheDir := ApplyGluonCachePathSuffix(oldGluonDir)
|
||||||
|
if err := copyDir(oldCacheDir, ApplyGluonCachePathSuffix(newGluonDir)); err != nil {
|
||||||
|
return fmt.Errorf("failed to copy gluon dir: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bridge.vault.SetGluonDir(newGluonDir); err != nil {
|
||||||
|
return fmt.Errorf("failed to set new gluon cache dir: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.RemoveAll(oldCacheDir); err != nil {
|
||||||
|
logrus.WithError(err).Error("failed to remove old gluon cache dir")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) stopEventLoops() error {
|
||||||
|
if err := bridge.closeIMAP(context.Background()); err != nil {
|
||||||
|
return fmt.Errorf("failed to close IMAP: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bridge.closeSMTP(); err != nil {
|
||||||
|
return fmt.Errorf("failed to close SMTP: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) startEventLoops(ctx context.Context) error {
|
||||||
|
for _, user := range bridge.users {
|
||||||
|
if err := bridge.addIMAPUser(ctx, user); err != nil {
|
||||||
|
return fmt.Errorf("failed to add users to new IMAP server: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bridge.serveIMAP(); err != nil {
|
||||||
|
panic(fmt.Errorf("failed to serve IMAP: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bridge.serveSMTP(); err != nil {
|
||||||
|
panic(fmt.Errorf("failed to serve SMTP: %w", err))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) GetProxyAllowed() bool {
|
func (bridge *Bridge) GetProxyAllowed() bool {
|
||||||
return bridge.vault.GetProxyAllowed()
|
return bridge.vault.GetProxyAllowed()
|
||||||
}
|
}
|
||||||
@ -272,23 +317,11 @@ func (bridge *Bridge) GetCurrentVersion() *semver.Version {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) GetLastVersion() *semver.Version {
|
func (bridge *Bridge) GetLastVersion() *semver.Version {
|
||||||
return bridge.vault.GetLastVersion()
|
return bridge.lastVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) GetFirstStart() bool {
|
func (bridge *Bridge) GetFirstStart() bool {
|
||||||
return bridge.vault.GetFirstStart()
|
return bridge.firstStart
|
||||||
}
|
|
||||||
|
|
||||||
func (bridge *Bridge) SetFirstStart(firstStart bool) error {
|
|
||||||
return bridge.vault.SetFirstStart(firstStart)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bridge *Bridge) GetFirstStartGUI() bool {
|
|
||||||
return bridge.vault.GetFirstStartGUI()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bridge *Bridge) SetFirstStartGUI(firstStart bool) error {
|
|
||||||
return bridge.vault.SetFirstStartGUI(firstStart)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) GetColorScheme() string {
|
func (bridge *Bridge) GetColorScheme() string {
|
||||||
@ -308,10 +341,10 @@ func (bridge *Bridge) FactoryReset(ctx context.Context) {
|
|||||||
}, bridge.usersLock)
|
}, bridge.usersLock)
|
||||||
|
|
||||||
// Wipe the vault.
|
// Wipe the vault.
|
||||||
gluonDir, err := bridge.locator.ProvideGluonPath()
|
gluonCacheDir, err := bridge.locator.ProvideGluonCachePath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.WithError(err).Error("Failed to provide gluon dir")
|
logrus.WithError(err).Error("Failed to provide gluon dir")
|
||||||
} else if err := bridge.vault.Reset(gluonDir); err != nil {
|
} else if err := bridge.vault.Reset(gluonCacheDir); err != nil {
|
||||||
logrus.WithError(err).Error("Failed to reset vault")
|
logrus.WithError(err).Error("Failed to reset vault")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -162,26 +162,7 @@ func TestBridge_Settings_FirstStart(t *testing.T) {
|
|||||||
// By default, first start is true.
|
// By default, first start is true.
|
||||||
require.True(t, bridge.GetFirstStart())
|
require.True(t, bridge.GetFirstStart())
|
||||||
|
|
||||||
// Set first start to false.
|
// the setting of the first start value is managed by bridge itself, so the setter is not exported.
|
||||||
require.NoError(t, bridge.SetFirstStart(false))
|
|
||||||
|
|
||||||
// Get the new setting.
|
|
||||||
require.False(t, bridge.GetFirstStart())
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBridge_Settings_FirstStartGUI(t *testing.T) {
|
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
|
||||||
// By default, first start is true.
|
|
||||||
require.True(t, bridge.GetFirstStartGUI())
|
|
||||||
|
|
||||||
// Set first start to false.
|
|
||||||
require.NoError(t, bridge.SetFirstStartGUI(false))
|
|
||||||
|
|
||||||
// Get the new setting.
|
|
||||||
require.False(t, bridge.GetFirstStartGUI())
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -351,7 +351,7 @@ func withClient(ctx context.Context, t *testing.T, s *server.Server, username st
|
|||||||
fn(ctx, c)
|
fn(ctx, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func clientFetch(client *client.Client, mailbox string) ([]*imap.Message, error) { //nolint:unused
|
func clientFetch(client *client.Client, mailbox string) ([]*imap.Message, error) {
|
||||||
status, err := client.Select(mailbox, false)
|
status, err := client.Select(mailbox, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -376,6 +376,35 @@ func clientFetch(client *client.Client, mailbox string) ([]*imap.Message, error)
|
|||||||
return iterator.Collect(iterator.Chan(resCh)), nil
|
return iterator.Collect(iterator.Chan(resCh)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func clientStore(client *client.Client, from, to int, isUID bool, item imap.StoreItem, flags ...string) error {
|
||||||
|
var storeFunc func(seqset *imap.SeqSet, item imap.StoreItem, value interface{}, ch chan *imap.Message) error
|
||||||
|
|
||||||
|
if isUID {
|
||||||
|
storeFunc = client.UidStore
|
||||||
|
} else {
|
||||||
|
storeFunc = client.Store
|
||||||
|
}
|
||||||
|
|
||||||
|
return storeFunc(
|
||||||
|
&imap.SeqSet{Set: []imap.Seq{{Start: uint32(from), Stop: uint32(to)}}},
|
||||||
|
item,
|
||||||
|
xslices.Map(flags, func(flag string) interface{} { return flag }),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func clientList(client *client.Client) []*imap.MailboxInfo {
|
||||||
|
resCh := make(chan *imap.MailboxInfo)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := client.List("", "*", resCh); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return iterator.Collect(iterator.Chan(resCh))
|
||||||
|
}
|
||||||
|
|
||||||
func createNumMessages(ctx context.Context, t *testing.T, c *proton.Client, addrID, labelID string, count int) []string {
|
func createNumMessages(ctx context.Context, t *testing.T, c *proton.Client, addrID, labelID string, count int) []string {
|
||||||
literal, err := os.ReadFile(filepath.Join("testdata", "text-plain.eml"))
|
literal, err := os.ReadFile(filepath.Join("testdata", "text-plain.eml"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -399,6 +428,9 @@ func createMessages(ctx context.Context, t *testing.T, c *proton.Client, addrID,
|
|||||||
_, addrKRs, err := proton.Unlock(user, addr, keyPass)
|
_, addrKRs, err := proton.Unlock(user, addr, keyPass)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, ok := addrKRs[addrID]
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
res, err := stream.Collect(ctx, c.ImportMessages(
|
res, err := stream.Collect(ctx, c.ImportMessages(
|
||||||
ctx,
|
ctx,
|
||||||
addrKRs[addrID],
|
addrKRs[addrID],
|
||||||
|
|||||||
85
internal/bridge/testdata/invite.eml
vendored
Normal file
85
internal/bridge/testdata/invite.eml
vendored
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
From: <username@proton.local>
|
||||||
|
To: <recipient@proton.local>
|
||||||
|
Subject: Testing calendar invite
|
||||||
|
Date: Fri, 3 Feb 2023 01:04:32 +0100
|
||||||
|
Message-ID: <000001d93763$183b74e0$48b25ea0$@proton.local>
|
||||||
|
MIME-Version: 1.0
|
||||||
|
Content-Type: text/calendar; method=REQUEST;
|
||||||
|
charset="utf-8"
|
||||||
|
Content-Transfer-Encoding: 7bit
|
||||||
|
X-Mailer: Microsoft Outlook 16.0
|
||||||
|
Thread-Index: Adk3Yw5pLdgwsT46RviXb/nfvQlesQAAAmGA
|
||||||
|
Content-Language: en-gb
|
||||||
|
|
||||||
|
BEGIN:VCALENDAR
|
||||||
|
PRODID:-//Microsoft Corporation//Outlook 16.0 MIMEDIR//EN
|
||||||
|
VERSION:2.0
|
||||||
|
METHOD:REQUEST
|
||||||
|
X-MS-OLK-FORCEINSPECTOROPEN:TRUE
|
||||||
|
BEGIN:VTIMEZONE
|
||||||
|
TZID:Central European Standard Time
|
||||||
|
BEGIN:STANDARD
|
||||||
|
DTSTART:16011028T030000
|
||||||
|
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
|
||||||
|
TZOFFSETFROM:+0200
|
||||||
|
TZOFFSETTO:+0100
|
||||||
|
END:STANDARD
|
||||||
|
BEGIN:DAYLIGHT
|
||||||
|
DTSTART:16010325T020000
|
||||||
|
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3
|
||||||
|
TZOFFSETFROM:+0100
|
||||||
|
TZOFFSETTO:+0200
|
||||||
|
END:DAYLIGHT
|
||||||
|
END:VTIMEZONE
|
||||||
|
BEGIN:VEVENT
|
||||||
|
ATTENDEE;CN=recipient@proton.local;RSVP=TRUE:mailto:recipient@proton.local
|
||||||
|
CLASS:PUBLIC
|
||||||
|
CREATED:20230203T000432Z
|
||||||
|
DESCRIPTION:qweqweqweqweqweqwe/gn\\n
|
||||||
|
DTEND;TZID="Central European Standard Time":20230203T020000
|
||||||
|
DTSTAMP:20230203T000432Z
|
||||||
|
DTSTART;TZID="Central European Standard Time":20230203T013000
|
||||||
|
LAST-MODIFIED:20230203T000432Z
|
||||||
|
LOCATION:qweqwe
|
||||||
|
ORGANIZER;CN=username@proton.local:mailto:username@proton.local
|
||||||
|
PRIORITY:5
|
||||||
|
SEQUENCE:0
|
||||||
|
SUMMARY;LANGUAGE=en-gb:Testing calendar invite
|
||||||
|
TRANSP:OPAQUE
|
||||||
|
UID:040000008200E00074C5B7101A82E008000000003080B2796B37D901000000000000000
|
||||||
|
0100000001236CD1CD93CA9449C6FF1AC4DEAC44E
|
||||||
|
X-ALT-DESC;FMTTYPE=text/html:<html xmlns:v="urn:schemas-microsoft-com:vml"
|
||||||
|
xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:w="urn:schemas-mic
|
||||||
|
rosoft-com:office:word" xmlns:m="http://schemas.microsoft.com/office/2004/
|
||||||
|
12/omml" xmlns="http://www.w3.org/TR/REC-html40"><head><meta http-equiv=Co
|
||||||
|
ntent-Type content="text/html/g; charset=us-ascii"><meta name=Generator con
|
||||||
|
tent="Microsoft Word 15 (filtered medium)"><style><!--/gn/* Font Definition
|
||||||
|
s *//gn@font-face\\n {font-family:"Cambria Math"\\;\\n panose-1:2 4 5 3 5 4 6
|
||||||
|
3 2 4/g;}\\n@font-face\\n {font-family:Calibri\\;\\n panose-1:2 15 5 2 2 2 4 3
|
||||||
|
2 4/g;}\\n/* Style Definitions */\\np.MsoNormal\\, li.MsoNormal\\, div.MsoNorma
|
||||||
|
l/gn {margin:0cm\\;\\n font-size:11.0pt\\;\\n font-family:"Calibri"\\,sans-serif
|
||||||
|
/g;\\n mso-fareast-language:EN-US\\;}\\nspan.EmailStyle18\\n {mso-style-type:pe
|
||||||
|
rsonal-compose/g;\\n font-family:"Calibri"\\,sans-serif\\;\\n color:windowtext\\
|
||||||
|
;}/gn.MsoChpDefault\\n {mso-style-type:export-only\\;\\n font-size:10.0pt\\;}\\n
|
||||||
|
@page WordSection1/gn {size:612.0pt 792.0pt\\;\\n margin:72.0pt 72.0pt 72.0pt
|
||||||
|
72.0pt/g;}\\ndiv.WordSection1\\n {page:WordSection1\\;}\\n--></style><!--[if g
|
||||||
|
te mso 9]><xml>/gn<o:shapedefaults v:ext="edit" spidmax="1026" />\\n</xml><!
|
||||||
|
[endif]--><!--[if gte mso 9]><xml>/gn<o:shapelayout v:ext="edit">\\n<o:idmap
|
||||||
|
v:ext="edit" data="1" />/gn</o:shapelayout></xml><![endif]--></head><body
|
||||||
|
lang=EN-GB link="#0563C1" vlink="#954F72" style='word-wrap:break-word'><di
|
||||||
|
v class=WordSection1><p class=MsoNormal><span lang=EN-US>qweqweqweqweqweqw
|
||||||
|
e<o:p></o:p></span></p></div></body></html>
|
||||||
|
X-MICROSOFT-CDO-BUSYSTATUS:TENTATIVE
|
||||||
|
X-MICROSOFT-CDO-IMPORTANCE:1
|
||||||
|
X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY
|
||||||
|
X-MICROSOFT-DISALLOW-COUNTER:FALSE
|
||||||
|
X-MS-OLK-AUTOSTARTCHECK:FALSE
|
||||||
|
X-MS-OLK-CONFTYPE:0
|
||||||
|
BEGIN:VALARM
|
||||||
|
TRIGGER:-PT15M
|
||||||
|
ACTION:DISPLAY
|
||||||
|
DESCRIPTION:Reminder
|
||||||
|
END:VALARM
|
||||||
|
END:VEVENT
|
||||||
|
END:VCALENDAR
|
||||||
|
|
||||||
@ -26,7 +26,8 @@ import (
|
|||||||
type Locator interface {
|
type Locator interface {
|
||||||
ProvideSettingsPath() (string, error)
|
ProvideSettingsPath() (string, error)
|
||||||
ProvideLogsPath() (string, error)
|
ProvideLogsPath() (string, error)
|
||||||
ProvideGluonPath() (string, error)
|
ProvideGluonCachePath() (string, error)
|
||||||
|
ProvideGluonDataPath() (string, error)
|
||||||
GetLicenseFilePath() string
|
GetLicenseFilePath() string
|
||||||
GetDependencyLicensesLink() string
|
GetDependencyLicensesLink() string
|
||||||
Clear() error
|
Clear() error
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
"github.com/ProtonMail/gluon/imap"
|
"github.com/ProtonMail/gluon/imap"
|
||||||
|
"github.com/ProtonMail/gluon/reporter"
|
||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/async"
|
"github.com/ProtonMail/proton-bridge/v3/internal/async"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
@ -94,7 +95,7 @@ func (bridge *Bridge) GetUserInfo(userID string) (UserInfo, error) {
|
|||||||
if len(user.AuthUID()) == 0 {
|
if len(user.AuthUID()) == 0 {
|
||||||
state = SignedOut
|
state = SignedOut
|
||||||
}
|
}
|
||||||
info = getUserInfo(user.UserID(), user.Username(), state, user.AddressMode())
|
info = getUserInfo(user.UserID(), user.Username(), user.PrimaryEmail(), state, user.AddressMode())
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return UserInfo{}, fmt.Errorf("failed to get user info: %w", err)
|
return UserInfo{}, fmt.Errorf("failed to get user info: %w", err)
|
||||||
}
|
}
|
||||||
@ -298,6 +299,59 @@ func (bridge *Bridge) SetAddressMode(ctx context.Context, userID string, mode va
|
|||||||
}, bridge.usersLock)
|
}, bridge.usersLock)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SendBadEventUserFeedback passes the feedback to the given user.
|
||||||
|
func (bridge *Bridge) SendBadEventUserFeedback(_ context.Context, userID string, doResync bool) error {
|
||||||
|
logrus.WithField("userID", userID).WithField("doResync", doResync).Info("Passing bad event feedback to user")
|
||||||
|
|
||||||
|
return safe.LockRet(func() error {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
user, ok := bridge.users[userID]
|
||||||
|
if !ok {
|
||||||
|
if rerr := bridge.reporter.ReportMessageWithContext(
|
||||||
|
"Failed to handle event: feedback failed: no such user",
|
||||||
|
reporter.Context{"user_id": userID},
|
||||||
|
); rerr != nil {
|
||||||
|
logrus.WithError(rerr).Error("Failed to report feedback failure")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ErrNoSuchUser
|
||||||
|
}
|
||||||
|
|
||||||
|
if doResync {
|
||||||
|
if rerr := bridge.reporter.ReportMessageWithContext(
|
||||||
|
"Failed to handle event: feedback resync",
|
||||||
|
reporter.Context{"user_id": userID},
|
||||||
|
); rerr != nil {
|
||||||
|
logrus.WithError(rerr).Error("Failed to report feedback failure")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bridge.addIMAPUser(ctx, user); err != nil {
|
||||||
|
return fmt.Errorf("failed to add IMAP user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
user.BadEventFeedbackResync(ctx)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if rerr := bridge.reporter.ReportMessageWithContext(
|
||||||
|
"Failed to handle event: feedback logout",
|
||||||
|
reporter.Context{"user_id": userID},
|
||||||
|
); rerr != nil {
|
||||||
|
logrus.WithError(rerr).Error("Failed to report feedback failure")
|
||||||
|
}
|
||||||
|
|
||||||
|
bridge.logoutUser(ctx, user, true, false)
|
||||||
|
|
||||||
|
bridge.publish(events.UserLoggedOut{
|
||||||
|
UserID: userID,
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}, bridge.usersLock)
|
||||||
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) loginUser(ctx context.Context, client *proton.Client, authUID, authRef string, keyPass []byte) (string, error) {
|
func (bridge *Bridge) loginUser(ctx context.Context, client *proton.Client, authUID, authRef string, keyPass []byte) (string, error) {
|
||||||
apiUser, err := client.GetUser(ctx)
|
apiUser, err := client.GetUser(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -329,30 +383,37 @@ func (bridge *Bridge) loginUser(ctx context.Context, client *proton.Client, auth
|
|||||||
|
|
||||||
// loadUsers tries to load each user in the vault that isn't already loaded.
|
// loadUsers tries to load each user in the vault that isn't already loaded.
|
||||||
func (bridge *Bridge) loadUsers(ctx context.Context) error {
|
func (bridge *Bridge) loadUsers(ctx context.Context) error {
|
||||||
|
logrus.WithField("count", len(bridge.vault.GetUserIDs())).Info("Loading users")
|
||||||
|
defer logrus.Info("Finished loading users")
|
||||||
|
|
||||||
return bridge.vault.ForUser(runtime.NumCPU(), func(user *vault.User) error {
|
return bridge.vault.ForUser(runtime.NumCPU(), func(user *vault.User) error {
|
||||||
|
log := logrus.WithField("userID", user.UserID())
|
||||||
|
|
||||||
if user.AuthUID() == "" {
|
if user.AuthUID() == "" {
|
||||||
|
log.Info("User is not connected (skipping)")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if safe.RLockRet(func() bool { return mapHas(bridge.users, user.UserID()) }, bridge.usersLock) {
|
if safe.RLockRet(func() bool { return mapHas(bridge.users, user.UserID()) }, bridge.usersLock) {
|
||||||
|
log.Info("User is already loaded (skipping)")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.WithField("userID", user.UserID()).Info("Loading connected user")
|
log.Info("Loading connected user")
|
||||||
|
|
||||||
bridge.publish(events.UserLoading{
|
bridge.publish(events.UserLoading{
|
||||||
UserID: user.UserID(),
|
UserID: user.UserID(),
|
||||||
})
|
})
|
||||||
|
|
||||||
if err := bridge.loadUser(ctx, user); err != nil {
|
if err := bridge.loadUser(ctx, user); err != nil {
|
||||||
logrus.WithError(err).Error("Failed to load connected user")
|
log.WithError(err).Error("Failed to load connected user")
|
||||||
|
|
||||||
bridge.publish(events.UserLoadFail{
|
bridge.publish(events.UserLoadFail{
|
||||||
UserID: user.UserID(),
|
UserID: user.UserID(),
|
||||||
Error: err,
|
Error: err,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
logrus.WithField("userID", user.UserID()).Info("Successfully loaded user")
|
log.Info("Successfully loaded connected user")
|
||||||
|
|
||||||
bridge.publish(events.UserLoadSuccess{
|
bridge.publish(events.UserLoadSuccess{
|
||||||
UserID: user.UserID(),
|
UserID: user.UserID(),
|
||||||
@ -367,7 +428,7 @@ func (bridge *Bridge) loadUsers(ctx context.Context) error {
|
|||||||
func (bridge *Bridge) loadUser(ctx context.Context, user *vault.User) error {
|
func (bridge *Bridge) loadUser(ctx context.Context, user *vault.User) error {
|
||||||
client, auth, err := bridge.api.NewClientWithRefresh(ctx, user.AuthUID(), user.AuthRef())
|
client, auth, err := bridge.api.NewClientWithRefresh(ctx, user.AuthUID(), user.AuthRef())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if apiErr := new(proton.Error); errors.As(err, &apiErr) && (apiErr.Code == proton.AuthRefreshTokenInvalid) {
|
if apiErr := new(proton.APIError); errors.As(err, &apiErr) && (apiErr.Code == proton.AuthRefreshTokenInvalid) {
|
||||||
// The session cannot be refreshed, we sign out the user by clearing his auth secrets.
|
// The session cannot be refreshed, we sign out the user by clearing his auth secrets.
|
||||||
if err := user.Clear(); err != nil {
|
if err := user.Clear(); err != nil {
|
||||||
logrus.WithError(err).Warn("Failed to clear user secrets")
|
logrus.WithError(err).Warn("Failed to clear user secrets")
|
||||||
@ -389,6 +450,12 @@ func (bridge *Bridge) loadUser(ctx context.Context, user *vault.User) error {
|
|||||||
return fmt.Errorf("failed to add user: %w", err)
|
return fmt.Errorf("failed to add user: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if user.PrimaryEmail() != apiUser.Email {
|
||||||
|
if err := user.SetPrimaryEmail(apiUser.Email); err != nil {
|
||||||
|
return fmt.Errorf("failed to modify user primary email: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -504,7 +571,7 @@ func (bridge *Bridge) newVaultUser(
|
|||||||
saltedKeyPass []byte,
|
saltedKeyPass []byte,
|
||||||
) (*vault.User, bool, error) {
|
) (*vault.User, bool, error) {
|
||||||
if !bridge.vault.HasUser(apiUser.ID) {
|
if !bridge.vault.HasUser(apiUser.ID) {
|
||||||
user, err := bridge.vault.AddUser(apiUser.ID, apiUser.Name, authUID, authRef, saltedKeyPass)
|
user, err := bridge.vault.AddUser(apiUser.ID, apiUser.Name, apiUser.Email, authUID, authRef, saltedKeyPass)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, fmt.Errorf("failed to add user to vault: %w", err)
|
return nil, false, fmt.Errorf("failed to add user to vault: %w", err)
|
||||||
}
|
}
|
||||||
@ -550,11 +617,17 @@ func (bridge *Bridge) logoutUser(ctx context.Context, user *user.User, withAPI,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// getUserInfo returns information about a disconnected user.
|
// getUserInfo returns information about a disconnected user.
|
||||||
func getUserInfo(userID, username string, state UserState, addressMode vault.AddressMode) UserInfo {
|
func getUserInfo(userID, username, primaryEmail string, state UserState, addressMode vault.AddressMode) UserInfo {
|
||||||
|
var addresses []string
|
||||||
|
if len(primaryEmail) > 0 {
|
||||||
|
addresses = []string{primaryEmail}
|
||||||
|
}
|
||||||
|
|
||||||
return UserInfo{
|
return UserInfo{
|
||||||
State: state,
|
State: state,
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
Username: username,
|
Username: username,
|
||||||
|
Addresses: addresses,
|
||||||
AddressMode: addressMode,
|
AddressMode: addressMode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
483
internal/bridge/user_event_test.go
Normal file
483
internal/bridge/user_event_test.go
Normal file
@ -0,0 +1,483 @@
|
|||||||
|
// 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_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/go-proton-api"
|
||||||
|
"github.com/ProtonMail/go-proton-api/server"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||||
|
"github.com/bradenaw/juniper/xslices"
|
||||||
|
"github.com/emersion/go-imap"
|
||||||
|
"github.com/emersion/go-imap/client"
|
||||||
|
"github.com/golang/mock/gomock"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBridge_User_RefreshEvent(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)
|
||||||
|
|
||||||
|
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var messageIDs []string
|
||||||
|
|
||||||
|
// Create 10 messages for the user.
|
||||||
|
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
messageIDs = createNumMessages(ctx, t, c, addrID, labelID, 10)
|
||||||
|
})
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
|
userLoginAndSync(ctx, t, bridge, "user", password)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Remove a message
|
||||||
|
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
require.NoError(t, c.DeleteMessage(ctx, messageIDs[0]))
|
||||||
|
})
|
||||||
|
|
||||||
|
require.NoError(t, s.RefreshUser(userID, proton.RefreshMail))
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
|
syncCh, closeCh := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
||||||
|
|
||||||
|
mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).MinTimes(1)
|
||||||
|
|
||||||
|
require.Equal(t, userID, (<-syncCh).UserID)
|
||||||
|
closeCh()
|
||||||
|
|
||||||
|
userContinueEventProcess(ctx, t, s, bridge)
|
||||||
|
})
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
|
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
createNumMessages(ctx, t, c, addrID, labelID, 10)
|
||||||
|
})
|
||||||
|
|
||||||
|
userContinueEventProcess(ctx, t, s, bridge)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBridge_User_BadMessage_BadEvent(t *testing.T) {
|
||||||
|
t.Run("Resync", test_badMessage_badEvent(func(t *testing.T, ctx context.Context, bridge *bridge.Bridge, badUserID string) {
|
||||||
|
// User feedback is resync
|
||||||
|
require.NoError(t, bridge.SendBadEventUserFeedback(ctx, badUserID, true))
|
||||||
|
|
||||||
|
// Wait for sync to finish
|
||||||
|
syncCh, closeCh := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
||||||
|
require.Equal(t, badUserID, (<-syncCh).UserID)
|
||||||
|
closeCh()
|
||||||
|
}))
|
||||||
|
|
||||||
|
t.Run("LogoutAndLogin", test_badMessage_badEvent(func(t *testing.T, ctx context.Context, bridge *bridge.Bridge, badUserID string) {
|
||||||
|
logoutCh, closeCh := chToType[events.Event, events.UserLoggedOut](bridge.GetEvents(events.UserLoggedOut{}))
|
||||||
|
|
||||||
|
// User feedback is logout
|
||||||
|
require.NoError(t, bridge.SendBadEventUserFeedback(ctx, badUserID, false))
|
||||||
|
|
||||||
|
require.Equal(t, badUserID, (<-logoutCh).UserID)
|
||||||
|
closeCh()
|
||||||
|
|
||||||
|
// The user will eventually be logged out due to the bad request errors.
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
return len(bridge.GetUserIDs()) == 1 && len(getConnectedUserIDs(t, bridge)) == 0
|
||||||
|
}, 100*user.EventPeriod, user.EventPeriod)
|
||||||
|
|
||||||
|
// Login again
|
||||||
|
_, err := bridge.LoginFull(ctx, "user", password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func test_badMessage_badEvent(userFeedback func(t *testing.T, ctx context.Context, bridge *bridge.Bridge, badUserID string)) func(t *testing.T) {
|
||||||
|
return func(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)
|
||||||
|
|
||||||
|
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create 10 messages for the user.
|
||||||
|
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
createNumMessages(ctx, t, c, addrID, labelID, 10)
|
||||||
|
})
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
|
userLoginAndSync(ctx, t, bridge, "user", password)
|
||||||
|
|
||||||
|
var messageIDs []string
|
||||||
|
|
||||||
|
// Create 10 more messages for the user, generating events.
|
||||||
|
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
messageIDs = createNumMessages(ctx, t, c, addrID, labelID, 10)
|
||||||
|
})
|
||||||
|
|
||||||
|
// If bridge attempts to sync the new messages, it should get a BadRequest error.
|
||||||
|
doBadRequest := true
|
||||||
|
s.AddStatusHook(func(req *http.Request) (int, bool) {
|
||||||
|
if !doBadRequest {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
if xslices.Index(xslices.Map(messageIDs[0:5], func(messageID string) string {
|
||||||
|
return "/mail/v4/messages/" + messageID
|
||||||
|
}), req.URL.Path) < 0 {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.StatusBadRequest, true
|
||||||
|
})
|
||||||
|
|
||||||
|
badUserID := userReceivesBadError(t, bridge, mocks)
|
||||||
|
|
||||||
|
// Remove messages, make response OK again
|
||||||
|
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
require.NoError(t, c.DeleteMessage(ctx, messageIDs[0:5]...))
|
||||||
|
})
|
||||||
|
doBadRequest = false
|
||||||
|
|
||||||
|
userFeedback(t, ctx, bridge, badUserID)
|
||||||
|
|
||||||
|
userContinueEventProcess(ctx, t, s, bridge)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBridge_User_BadMessage_NoBadEvent(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
|
// Create a user.
|
||||||
|
_, addrID, err := s.CreateUser("user", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create 10 messages for the user.
|
||||||
|
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 10)
|
||||||
|
})
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
|
userLoginAndSync(ctx, t, bridge, "user", password)
|
||||||
|
|
||||||
|
var messageIDs []string
|
||||||
|
|
||||||
|
// If bridge attempts to sync the new messages, it should get a BadRequest error.
|
||||||
|
s.AddStatusHook(func(req *http.Request) (int, bool) {
|
||||||
|
if len(messageIDs) < 3 {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(req.URL.Path, "/mail/v4/messages/"+messageIDs[2]) {
|
||||||
|
return http.StatusUnprocessableEntity, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create 10 more messages for the user, generating events.
|
||||||
|
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
messageIDs = createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 10)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Remove messages
|
||||||
|
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
require.NoError(t, c.DeleteMessage(ctx, messageIDs...))
|
||||||
|
})
|
||||||
|
|
||||||
|
userContinueEventProcess(ctx, t, s, bridge)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBridge_User_SameMessageLabelCreated_NoBadEvent(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)
|
||||||
|
|
||||||
|
var messageIDs []string
|
||||||
|
|
||||||
|
// Create 10 messages for the user.
|
||||||
|
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
messageIDs = createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 10)
|
||||||
|
})
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
userLoginAndSync(ctx, t, bridge, "user", password)
|
||||||
|
|
||||||
|
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Add NOOP events
|
||||||
|
require.NoError(t, s.AddLabelCreatedEvent(userID, labelID))
|
||||||
|
require.NoError(t, s.AddMessageCreatedEvent(userID, messageIDs[9]))
|
||||||
|
|
||||||
|
userContinueEventProcess(ctx, t, s, bridge)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBridge_User_MessageLabelDeleted_NoBadEvent(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)
|
||||||
|
|
||||||
|
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create 10 messages for the user.
|
||||||
|
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
createNumMessages(ctx, t, c, addrID, labelID, 10)
|
||||||
|
})
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
userLoginAndSync(ctx, t, bridge, "user", password)
|
||||||
|
|
||||||
|
// Create and delete 10 more messages for the user, generating delete events.
|
||||||
|
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
messageIDs := createNumMessages(ctx, t, c, addrID, labelID, 10)
|
||||||
|
require.NoError(t, c.DeleteMessage(ctx, messageIDs...))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create and delete 10 labels for the user, generating delete events.
|
||||||
|
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
label, err := c.CreateLabel(ctx, proton.CreateLabelReq{
|
||||||
|
Name: uuid.NewString(),
|
||||||
|
Color: "#f66",
|
||||||
|
Type: proton.LabelTypeLabel,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.NoError(t, c.DeleteLabel(ctx, label.ID))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
userContinueEventProcess(ctx, t, s, bridge)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBridge_User_AddressEvents_NoBadEvent(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)
|
||||||
|
|
||||||
|
// Create 10 messages for the user.
|
||||||
|
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 10)
|
||||||
|
})
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
userLoginAndSync(ctx, t, bridge, "user", password)
|
||||||
|
|
||||||
|
addrID, err = s.CreateAddress(userID, "other@pm.me", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
userContinueEventProcess(ctx, t, s, bridge)
|
||||||
|
|
||||||
|
require.NoError(t, s.AddAddressCreatedEvent(userID, addrID))
|
||||||
|
userContinueEventProcess(ctx, t, s, bridge)
|
||||||
|
})
|
||||||
|
|
||||||
|
otherID, err := s.CreateAddress(userID, "another@pm.me", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, s.RemoveAddress(userID, otherID))
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
userContinueEventProcess(ctx, t, s, bridge)
|
||||||
|
|
||||||
|
require.NoError(t, s.CreateAddressKey(userID, addrID, password))
|
||||||
|
userContinueEventProcess(ctx, t, s, bridge)
|
||||||
|
|
||||||
|
require.NoError(t, s.RemoveAddress(userID, addrID))
|
||||||
|
userContinueEventProcess(ctx, t, s, bridge)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBridge_User_Network_NoBadEvents(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
|
retVal := int32(0)
|
||||||
|
|
||||||
|
setResponseAndWait := func(status int32) {
|
||||||
|
atomic.StoreInt32(&retVal, status)
|
||||||
|
time.Sleep(user.EventPeriod)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.AddStatusHook(func(req *http.Request) (int, bool) {
|
||||||
|
status := atomic.LoadInt32(&retVal)
|
||||||
|
if strings.Contains(req.URL.Path, "/core/v4/events/") {
|
||||||
|
return int(status), status != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create a user.
|
||||||
|
_, addrID, err := s.CreateUser("user", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
|
userLoginAndSync(ctx, t, bridge, "user", password)
|
||||||
|
|
||||||
|
// Create 10 more messages for the user, generating events.
|
||||||
|
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 10)
|
||||||
|
|
||||||
|
setResponseAndWait(http.StatusInternalServerError)
|
||||||
|
setResponseAndWait(http.StatusServiceUnavailable)
|
||||||
|
setResponseAndWait(http.StatusPaymentRequired)
|
||||||
|
setResponseAndWait(http.StatusForbidden)
|
||||||
|
setResponseAndWait(http.StatusBadRequest)
|
||||||
|
setResponseAndWait(http.StatusUnprocessableEntity)
|
||||||
|
setResponseAndWait(http.StatusTooManyRequests)
|
||||||
|
time.Sleep(10 * time.Second) // needs minimum of 10 seconds to retry
|
||||||
|
})
|
||||||
|
|
||||||
|
setResponseAndWait(0)
|
||||||
|
time.Sleep(10 * time.Second) // needs up to 20 seconds to retry
|
||||||
|
userContinueEventProcess(ctx, t, s, bridge)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBridge503DuringEventDoesNotCauseBadEvent(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)
|
||||||
|
|
||||||
|
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create 10 messages for the user.
|
||||||
|
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
createNumMessages(ctx, t, c, addrID, labelID, 10)
|
||||||
|
})
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
|
userLoginAndSync(ctx, t, bridge, "user", password)
|
||||||
|
|
||||||
|
var messageIDs []string
|
||||||
|
|
||||||
|
// Create 10 more messages for the user, generating events.
|
||||||
|
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
messageIDs = createNumMessages(ctx, t, c, addrID, labelID, 10)
|
||||||
|
})
|
||||||
|
|
||||||
|
mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).MinTimes(1)
|
||||||
|
|
||||||
|
s.AddStatusHook(func(req *http.Request) (int, bool) {
|
||||||
|
if xslices.Index(xslices.Map(messageIDs[0:5], func(messageID string) string {
|
||||||
|
return "/mail/v4/messages/" + messageID
|
||||||
|
}), req.URL.Path) < 0 {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.StatusServiceUnavailable, true
|
||||||
|
})
|
||||||
|
|
||||||
|
userContinueEventProcess(ctx, t, s, bridge)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// userLoginAndSync logs in user and waits until user is fully synced.
|
||||||
|
func userLoginAndSync(
|
||||||
|
ctx context.Context,
|
||||||
|
t *testing.T,
|
||||||
|
bridge *bridge.Bridge,
|
||||||
|
username string, password []byte, //nolint:unparam
|
||||||
|
) {
|
||||||
|
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
||||||
|
defer done()
|
||||||
|
|
||||||
|
userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, userID, (<-syncCh).UserID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func userReceivesBadError(
|
||||||
|
t *testing.T,
|
||||||
|
bridge *bridge.Bridge,
|
||||||
|
mocks *bridge.Mocks,
|
||||||
|
) (userID string) {
|
||||||
|
badEventCh, closeCh := bridge.GetEvents(events.UserBadEvent{})
|
||||||
|
|
||||||
|
// The user will continue to process events and will receive bad request errors.
|
||||||
|
mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).MinTimes(1)
|
||||||
|
|
||||||
|
badEvent, ok := (<-badEventCh).(events.UserBadEvent)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
closeCh()
|
||||||
|
|
||||||
|
return badEvent.UserID
|
||||||
|
}
|
||||||
|
|
||||||
|
func userContinueEventProcess(
|
||||||
|
ctx context.Context,
|
||||||
|
t *testing.T,
|
||||||
|
s *server.Server,
|
||||||
|
bridge *bridge.Bridge,
|
||||||
|
) {
|
||||||
|
info, err := bridge.QueryUserInfo("user")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
||||||
|
defer func() { _ = client.Logout() }()
|
||||||
|
|
||||||
|
randomLabel := uuid.NewString()
|
||||||
|
|
||||||
|
// Create a new label.
|
||||||
|
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
require.NoError(t, getErr(c.CreateLabel(ctx, proton.CreateLabelReq{
|
||||||
|
Name: randomLabel,
|
||||||
|
Color: "#f66",
|
||||||
|
Type: proton.LabelTypeLabel,
|
||||||
|
})))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Wait for the label to be created.
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
return xslices.IndexFunc(clientList(client), func(mailbox *imap.MailboxInfo) bool {
|
||||||
|
return mailbox.Name == "Labels/"+randomLabel
|
||||||
|
}) >= 0
|
||||||
|
}, 100*user.EventPeriod, user.EventPeriod)
|
||||||
|
}
|
||||||
@ -21,10 +21,13 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/gluon/reporter"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (bridge *Bridge) handleUserEvent(ctx context.Context, user *user.User, event events.Event) error {
|
func (bridge *Bridge) handleUserEvent(ctx context.Context, user *user.User, event events.Event) error {
|
||||||
@ -45,12 +48,18 @@ func (bridge *Bridge) handleUserEvent(ctx context.Context, user *user.User, even
|
|||||||
}
|
}
|
||||||
|
|
||||||
case events.UserRefreshed:
|
case events.UserRefreshed:
|
||||||
if err := bridge.handleUserRefreshed(ctx, user); err != nil {
|
if err := bridge.handleUserRefreshed(ctx, user, event); err != nil {
|
||||||
return fmt.Errorf("failed to handle user refreshed event: %w", err)
|
return fmt.Errorf("failed to handle user refreshed event: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
case events.UserDeauth:
|
case events.UserDeauth:
|
||||||
bridge.handleUserDeauth(ctx, user)
|
bridge.handleUserDeauth(ctx, user)
|
||||||
|
|
||||||
|
case events.UserBadEvent:
|
||||||
|
bridge.handleUserBadEvent(ctx, user, event)
|
||||||
|
|
||||||
|
case events.UncategorizedEventError:
|
||||||
|
bridge.handleUncategorizedErrorEvent(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -111,8 +120,12 @@ func (bridge *Bridge) handleUserAddressDeleted(ctx context.Context, user *user.U
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) handleUserRefreshed(ctx context.Context, user *user.User) error {
|
func (bridge *Bridge) handleUserRefreshed(ctx context.Context, user *user.User, event events.UserRefreshed) error {
|
||||||
return safe.RLockRet(func() error {
|
return safe.RLockRet(func() error {
|
||||||
|
if event.CancelEventPool {
|
||||||
|
user.CancelSyncAndEventPoll()
|
||||||
|
}
|
||||||
|
|
||||||
if err := bridge.removeIMAPUser(ctx, user, true); err != nil {
|
if err := bridge.removeIMAPUser(ctx, user, true); err != nil {
|
||||||
return fmt.Errorf("failed to remove IMAP user: %w", err)
|
return fmt.Errorf("failed to remove IMAP user: %w", err)
|
||||||
}
|
}
|
||||||
@ -130,3 +143,34 @@ func (bridge *Bridge) handleUserDeauth(ctx context.Context, user *user.User) {
|
|||||||
bridge.logoutUser(ctx, user, false, false)
|
bridge.logoutUser(ctx, user, false, false)
|
||||||
}, bridge.usersLock)
|
}, bridge.usersLock)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) handleUserBadEvent(_ context.Context, user *user.User, event events.UserBadEvent) {
|
||||||
|
safe.Lock(func() {
|
||||||
|
if rerr := bridge.reporter.ReportMessageWithContext("Failed to handle event", reporter.Context{
|
||||||
|
"user_id": user.ID(),
|
||||||
|
"old_event_id": event.OldEventID,
|
||||||
|
"new_event_id": event.NewEventID,
|
||||||
|
"event_info": event.EventInfo,
|
||||||
|
"error": event.Error,
|
||||||
|
"error_type": internal.ErrCauseType(event.Error),
|
||||||
|
}); rerr != nil {
|
||||||
|
logrus.WithError(rerr).Error("Failed to report failed event handling")
|
||||||
|
}
|
||||||
|
|
||||||
|
user.CancelSyncAndEventPoll()
|
||||||
|
|
||||||
|
// Disable IMAP user
|
||||||
|
if err := bridge.removeIMAPUser(context.Background(), user, false); err != nil {
|
||||||
|
logrus.WithError(err).Error("Failed to remove IMAP user")
|
||||||
|
}
|
||||||
|
}, bridge.usersLock)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) handleUncategorizedErrorEvent(event events.UncategorizedEventError) {
|
||||||
|
if rerr := bridge.reporter.ReportMessageWithContext("Failed to handle due to uncategorized error", reporter.Context{
|
||||||
|
"error_type": internal.ErrCauseType(event.Error),
|
||||||
|
"error": event.Error,
|
||||||
|
}); rerr != nil {
|
||||||
|
logrus.WithError(rerr).Error("Failed to report failed event handling")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -44,6 +44,9 @@ var (
|
|||||||
|
|
||||||
// DSNSentry client keys to be able to report crashes to Sentry.
|
// DSNSentry client keys to be able to report crashes to Sentry.
|
||||||
DSNSentry = ""
|
DSNSentry = ""
|
||||||
|
|
||||||
|
// BuildEnv tags used at build time.
|
||||||
|
BuildEnv = ""
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@ -90,7 +90,8 @@ func TestTLSSignedCertWrongPublicKey(t *testing.T) {
|
|||||||
r.Error(t, err, "expected dial to fail because of wrong public key")
|
r.Error(t, err, "expected dial to fail because of wrong public key")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTLSSignedCertTrustedPublicKey(t *testing.T) {
|
// GODT-2293 bump badssl cert and re enable this.
|
||||||
|
func _TestTLSSignedCertTrustedPublicKey(t *testing.T) { //nolint:unused,deadcode
|
||||||
skipIfProxyIsSet(t)
|
skipIfProxyIsSet(t)
|
||||||
|
|
||||||
_, dialer, _, checker, _ := createClientWithPinningDialer("")
|
_, dialer, _, checker, _ := createClientWithPinningDialer("")
|
||||||
|
|||||||
38
internal/errors.go
Normal file
38
internal/errors.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
// 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 internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrCause returns the cause of the error, the inner-most error in the wrapped chain.
|
||||||
|
func ErrCause(err error) error {
|
||||||
|
cause := err
|
||||||
|
|
||||||
|
for errors.Unwrap(cause) != nil {
|
||||||
|
cause = errors.Unwrap(cause)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cause
|
||||||
|
}
|
||||||
|
|
||||||
|
func ErrCauseType(err error) string {
|
||||||
|
return fmt.Sprintf("%T", ErrCause(err))
|
||||||
|
}
|
||||||
@ -52,9 +52,8 @@ type UserLabelDeleted struct {
|
|||||||
|
|
||||||
UserID string
|
UserID string
|
||||||
LabelID string
|
LabelID string
|
||||||
Name string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (event UserLabelDeleted) String() string {
|
func (event UserLabelDeleted) String() string {
|
||||||
return fmt.Sprintf("UserLabelDeleted: UserID: %s, LabelID: %s, Name: %s", event.UserID, event.LabelID, logging.Sensitive(event.Name))
|
return fmt.Sprintf("UserLabelDeleted: UserID: %s, LabelID: %s", event.UserID, event.LabelID)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// AllUsersLoaded is emitted when all users have been loaded.
|
||||||
type AllUsersLoaded struct {
|
type AllUsersLoaded struct {
|
||||||
eventBase
|
eventBase
|
||||||
}
|
}
|
||||||
@ -31,6 +32,7 @@ func (event AllUsersLoaded) String() string {
|
|||||||
return "AllUsersLoaded"
|
return "AllUsersLoaded"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserLoading is emitted when a user is being loaded.
|
||||||
type UserLoading struct {
|
type UserLoading struct {
|
||||||
eventBase
|
eventBase
|
||||||
|
|
||||||
@ -41,6 +43,7 @@ func (event UserLoading) String() string {
|
|||||||
return fmt.Sprintf("UserLoading: UserID: %s", event.UserID)
|
return fmt.Sprintf("UserLoading: UserID: %s", event.UserID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserLoadSuccess is emitted when a user has been loaded successfully.
|
||||||
type UserLoadSuccess struct {
|
type UserLoadSuccess struct {
|
||||||
eventBase
|
eventBase
|
||||||
|
|
||||||
@ -51,6 +54,7 @@ func (event UserLoadSuccess) String() string {
|
|||||||
return fmt.Sprintf("UserLoadSuccess: UserID: %s", event.UserID)
|
return fmt.Sprintf("UserLoadSuccess: UserID: %s", event.UserID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserLoadFail is emitted when a user has failed to load.
|
||||||
type UserLoadFail struct {
|
type UserLoadFail struct {
|
||||||
eventBase
|
eventBase
|
||||||
|
|
||||||
@ -62,6 +66,7 @@ func (event UserLoadFail) String() string {
|
|||||||
return fmt.Sprintf("UserLoadFail: UserID: %s, Error: %s", event.UserID, event.Error)
|
return fmt.Sprintf("UserLoadFail: UserID: %s, Error: %s", event.UserID, event.Error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserLoggedIn is emitted when a user has logged in.
|
||||||
type UserLoggedIn struct {
|
type UserLoggedIn struct {
|
||||||
eventBase
|
eventBase
|
||||||
|
|
||||||
@ -72,6 +77,7 @@ func (event UserLoggedIn) String() string {
|
|||||||
return fmt.Sprintf("UserLoggedIn: UserID: %s", event.UserID)
|
return fmt.Sprintf("UserLoggedIn: UserID: %s", event.UserID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserLoggedOut is emitted when a user has logged out.
|
||||||
type UserLoggedOut struct {
|
type UserLoggedOut struct {
|
||||||
eventBase
|
eventBase
|
||||||
|
|
||||||
@ -82,6 +88,7 @@ func (event UserLoggedOut) String() string {
|
|||||||
return fmt.Sprintf("UserLoggedOut: UserID: %s", event.UserID)
|
return fmt.Sprintf("UserLoggedOut: UserID: %s", event.UserID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserDeauth is emitted when a user has lost its API authentication.
|
||||||
type UserDeauth struct {
|
type UserDeauth struct {
|
||||||
eventBase
|
eventBase
|
||||||
|
|
||||||
@ -92,6 +99,30 @@ func (event UserDeauth) String() string {
|
|||||||
return fmt.Sprintf("UserDeauth: UserID: %s", event.UserID)
|
return fmt.Sprintf("UserDeauth: UserID: %s", event.UserID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserBadEvent is emitted when a user cannot apply an event.
|
||||||
|
type UserBadEvent struct {
|
||||||
|
eventBase
|
||||||
|
|
||||||
|
UserID string
|
||||||
|
OldEventID string
|
||||||
|
NewEventID string
|
||||||
|
EventInfo string
|
||||||
|
|
||||||
|
Error error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (event UserBadEvent) String() string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"UserBadEvent: UserID: %s, OldEventID: %s, NewEventID: %s, EventInfo: %v, Error: %s",
|
||||||
|
event.UserID,
|
||||||
|
event.OldEventID,
|
||||||
|
event.NewEventID,
|
||||||
|
event.EventInfo,
|
||||||
|
event.Error,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserDeleted is emitted when a user has been deleted.
|
||||||
type UserDeleted struct {
|
type UserDeleted struct {
|
||||||
eventBase
|
eventBase
|
||||||
|
|
||||||
@ -102,6 +133,7 @@ func (event UserDeleted) String() string {
|
|||||||
return fmt.Sprintf("UserDeleted: UserID: %s", event.UserID)
|
return fmt.Sprintf("UserDeleted: UserID: %s", event.UserID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserChanged is emitted when a user's data has changed (name, email, etc.).
|
||||||
type UserChanged struct {
|
type UserChanged struct {
|
||||||
eventBase
|
eventBase
|
||||||
|
|
||||||
@ -112,16 +144,19 @@ func (event UserChanged) String() string {
|
|||||||
return fmt.Sprintf("UserChanged: UserID: %s", event.UserID)
|
return fmt.Sprintf("UserChanged: UserID: %s", event.UserID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserRefreshed is emitted when an API refresh was issued for a user.
|
||||||
type UserRefreshed struct {
|
type UserRefreshed struct {
|
||||||
eventBase
|
eventBase
|
||||||
|
|
||||||
UserID string
|
UserID string
|
||||||
|
CancelEventPool bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (event UserRefreshed) String() string {
|
func (event UserRefreshed) String() string {
|
||||||
return fmt.Sprintf("UserRefreshed: UserID: %s", event.UserID)
|
return fmt.Sprintf("UserRefreshed: UserID: %s", event.UserID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddressModeChanged is emitted when a user's address mode has changed.
|
||||||
type AddressModeChanged struct {
|
type AddressModeChanged struct {
|
||||||
eventBase
|
eventBase
|
||||||
|
|
||||||
@ -133,3 +168,14 @@ type AddressModeChanged struct {
|
|||||||
func (event AddressModeChanged) String() string {
|
func (event AddressModeChanged) String() string {
|
||||||
return fmt.Sprintf("AddressModeChanged: UserID: %s, AddressMode: %s", event.UserID, event.AddressMode)
|
return fmt.Sprintf("AddressModeChanged: UserID: %s, AddressMode: %s", event.UserID, event.AddressMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UncategorizedEventError struct {
|
||||||
|
eventBase
|
||||||
|
|
||||||
|
UserID string
|
||||||
|
Error error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (event UncategorizedEventError) String() string {
|
||||||
|
return fmt.Sprintf("UncategorizedEventError: UserID: %s, Source:%T, Error: %s", event.UserID, event.Error, event.Error)
|
||||||
|
}
|
||||||
|
|||||||
@ -24,12 +24,11 @@
|
|||||||
package proto
|
package proto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
reflect "reflect"
|
|
||||||
sync "sync"
|
|
||||||
|
|
||||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||||
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
||||||
|
reflect "reflect"
|
||||||
|
sync "sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -105,7 +104,7 @@ var file_focus_proto_rawDesc = []byte{
|
|||||||
0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x3d, 0x5a, 0x3b, 0x67,
|
0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x3d, 0x5a, 0x3b, 0x67,
|
||||||
0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x6e,
|
0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x6e,
|
||||||
0x4d, 0x61, 0x69, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x6e, 0x2d, 0x62, 0x72, 0x69, 0x64,
|
0x4d, 0x61, 0x69, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x6e, 0x2d, 0x62, 0x72, 0x69, 0x64,
|
||||||
0x67, 0x65, 0x2f, 0x76, 0x32, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x66,
|
0x67, 0x65, 0x2f, 0x76, 0x33, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x66,
|
||||||
0x6f, 0x63, 0x75, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
|
0x6f, 0x63, 0x75, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
|
||||||
0x6f, 0x33,
|
0x6f, 0x33,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,6 @@ package proto
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
context "context"
|
context "context"
|
||||||
|
|
||||||
grpc "google.golang.org/grpc"
|
grpc "google.golang.org/grpc"
|
||||||
codes "google.golang.org/grpc/codes"
|
codes "google.golang.org/grpc/codes"
|
||||||
status "google.golang.org/grpc/status"
|
status "google.golang.org/grpc/status"
|
||||||
|
|||||||
2
internal/frontend/.gitignore
vendored
2
internal/frontend/.gitignore
vendored
@ -10,5 +10,5 @@ rcc_cgo_*.go
|
|||||||
*.qmlc
|
*.qmlc
|
||||||
|
|
||||||
# Generated file
|
# Generated file
|
||||||
bridge-gui/bridge-gui/Version.h
|
bridge-gui/bridge-gui/BuildConfig.h
|
||||||
bridge-gui/bridge-gui/Resources.rc
|
bridge-gui/bridge-gui/Resources.rc
|
||||||
|
|||||||
@ -53,6 +53,7 @@ void GRPCQtProxy::connectSignals() {
|
|||||||
connect(this, &GRPCQtProxy::logoutUserReceived, &usersTab, &UsersTab::logoutUser);
|
connect(this, &GRPCQtProxy::logoutUserReceived, &usersTab, &UsersTab::logoutUser);
|
||||||
connect(this, &GRPCQtProxy::setUserSplitModeReceived, &usersTab, &UsersTab::setUserSplitMode);
|
connect(this, &GRPCQtProxy::setUserSplitModeReceived, &usersTab, &UsersTab::setUserSplitMode);
|
||||||
connect(this, &GRPCQtProxy::configureUserAppleMailReceived, &usersTab, &UsersTab::configureUserAppleMail);
|
connect(this, &GRPCQtProxy::configureUserAppleMailReceived, &usersTab, &UsersTab::configureUserAppleMail);
|
||||||
|
connect(this, &GRPCQtProxy::sendBadEventUserFeedbackReceived, &usersTab, &UsersTab::processBadEventUserFeedback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -178,6 +179,15 @@ void GRPCQtProxy::setUserSplitMode(QString const &userID, bool makeItActive) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \param[in] userID The userID.
|
||||||
|
/// \param[in] doResync Did the user request a resync?
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
void GRPCQtProxy::sendBadEventUserFeedback(QString const &userID, bool doResync) {
|
||||||
|
emit sendBadEventUserFeedbackReceived(userID, doResync);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \param[in] userID The userID.
|
/// \param[in] userID The userID.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
|
|||||||
@ -52,6 +52,7 @@ public: // member functions.
|
|||||||
void setDiskCachePath(QString const &path); ///< Forwards a setDiskCachePath 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.
|
void setIsAutomaticUpdateOn(bool on); ///< Forwards a SetIsAutomaticUpdateOn call via a Qt signal.
|
||||||
void setUserSplitMode(QString const &userID, bool makeItActive); ///< Forwards a setUserSplitMode call via a Qt signal.
|
void setUserSplitMode(QString const &userID, bool makeItActive); ///< Forwards a setUserSplitMode call via a Qt signal.
|
||||||
|
void sendBadEventUserFeedback(QString const &userID, bool doResync); ///< Forwards a sendBadEventUserFeedback call via a Qt signal.
|
||||||
void logoutUser(QString const &userID); ///< Forwards a logoutUser call via a Qt signal.
|
void logoutUser(QString const &userID); ///< Forwards a logoutUser call via a Qt signal.
|
||||||
void removeUser(QString const &userID); ///< Forwards a removeUser call via a Qt signal.
|
void removeUser(QString const &userID); ///< Forwards a removeUser call via a Qt signal.
|
||||||
void configureUserAppleMail(QString const &userID, QString const &address); ///< Forwards a configureUserAppleMail call via a Qt signal.
|
void configureUserAppleMail(QString const &userID, QString const &address); ///< Forwards a configureUserAppleMail call via a Qt signal.
|
||||||
@ -72,6 +73,7 @@ signals:
|
|||||||
void setDiskCachePathReceived(QString const &path); ///< Signal for the setDiskCachePath gRPC call.
|
void setDiskCachePathReceived(QString const &path); ///< Signal for the setDiskCachePath gRPC call.
|
||||||
void setIsAutomaticUpdateOnReceived(bool on); ///< Signal for the SetIsAutomaticUpdateOn gRPC call.
|
void setIsAutomaticUpdateOnReceived(bool on); ///< Signal for the SetIsAutomaticUpdateOn gRPC call.
|
||||||
void setUserSplitModeReceived(QString const &userID, bool makeItActive); ///< Signal for the SetUserSplitModeReceived gRPC call.
|
void setUserSplitModeReceived(QString const &userID, bool makeItActive); ///< Signal for the SetUserSplitModeReceived gRPC call.
|
||||||
|
void sendBadEventUserFeedbackReceived(QString const &userID, bool doResync); ///< Signal for the SendBadEventUserFeedback gRPC call.
|
||||||
void logoutUserReceived(QString const &userID); ///< Signal for the LogoutUserReceived gRPC call.
|
void logoutUserReceived(QString const &userID); ///< Signal for the LogoutUserReceived gRPC call.
|
||||||
void removeUserReceived(QString const &userID); ///< Signal for the RemoveUserReceived gRPC call.
|
void removeUserReceived(QString const &userID); ///< Signal for the RemoveUserReceived gRPC call.
|
||||||
void configureUserAppleMailReceived(QString const &userID, QString const &address); ///< Signal for the ConfigureAppleMail gRPC call.
|
void configureUserAppleMailReceived(QString const &userID, QString const &address); ///< Signal for the ConfigureAppleMail gRPC call.
|
||||||
|
|||||||
@ -86,9 +86,10 @@ Status GRPCService::AddLogEntry(ServerContext *, AddLogEntryRequest const *reque
|
|||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \return The status for the call.
|
/// \return The status for the call.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
Status GRPCService::GuiReady(ServerContext *, Empty const *, Empty *) {
|
Status GRPCService::GuiReady(ServerContext *, Empty const *, GuiReadyResponse *response) {
|
||||||
app().log().debug(__FUNCTION__);
|
app().log().debug(__FUNCTION__);
|
||||||
app().mainWindow().settingsTab().setGUIReady(true);
|
app().mainWindow().settingsTab().setGUIReady(true);
|
||||||
|
response->set_showsplashscreen(app().mainWindow().settingsTab().showSplashScreen());
|
||||||
return Status::OK;
|
return Status::OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,28 +125,6 @@ Status GRPCService::ShowOnStartup(ServerContext *, Empty const *, BoolValue *res
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
/// \param[out] response The response.
|
|
||||||
/// \return The status for the call.
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
Status GRPCService::ShowSplashScreen(ServerContext *, Empty const *, BoolValue *response) {
|
|
||||||
app().log().debug(__FUNCTION__);
|
|
||||||
response->set_value(app().mainWindow().settingsTab().showSplashScreen());
|
|
||||||
return Status::OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
/// \param[out] response The response.
|
|
||||||
/// \return The status for the call.
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
Status GRPCService::IsFirstGuiStart(ServerContext *, Empty const *, BoolValue *response) {
|
|
||||||
app().log().debug(__FUNCTION__);
|
|
||||||
response->set_value(app().mainWindow().settingsTab().isFirstGUIStart());
|
|
||||||
return Status::OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \param[in] request The request.
|
/// \param[in] request The request.
|
||||||
/// \return The status for the call.
|
/// \return The status for the call.
|
||||||
@ -715,6 +694,17 @@ Status GRPCService::SetUserSplitMode(ServerContext *, UserSplitModeRequest const
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \param[in] request The request.
|
||||||
|
/// \return The status for the call.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
Status GRPCService::SendBadEventUserFeedback(ServerContext *, UserBadEventFeedbackRequest const *request, Empty *) {
|
||||||
|
app().log().debug(__FUNCTION__);
|
||||||
|
qtProxy_.sendBadEventUserFeedback(QString::fromStdString(request->userid()), request->doresync());
|
||||||
|
return Status::OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \param[in] request The request.
|
/// \param[in] request The request.
|
||||||
/// \return The status for the call.
|
/// \return The status for the call.
|
||||||
@ -752,7 +742,7 @@ Status GRPCService::ConfigureUserAppleMail(ServerContext *, ConfigureAppleMailRe
|
|||||||
/// \param[in] writer The writer
|
/// \param[in] writer The writer
|
||||||
/// \return The status for the call.
|
/// \return The status for the call.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
Status GRPCService::RunEventStream(ServerContext *, EventStreamRequest const *request, ServerWriter<StreamEvent> *writer) {
|
Status GRPCService::RunEventStream(ServerContext *ctx, EventStreamRequest const *request, ServerWriter<StreamEvent> *writer) {
|
||||||
app().log().debug(__FUNCTION__);
|
app().log().debug(__FUNCTION__);
|
||||||
{
|
{
|
||||||
QMutexLocker locker(&eventStreamMutex_);
|
QMutexLocker locker(&eventStreamMutex_);
|
||||||
@ -767,19 +757,19 @@ Status GRPCService::RunEventStream(ServerContext *, EventStreamRequest const *re
|
|||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
QMutexLocker locker(&eventStreamMutex_);
|
QMutexLocker locker(&eventStreamMutex_);
|
||||||
if (eventStreamShouldStop_) {
|
if (eventStreamShouldStop_ || ctx->IsCancelled()) {
|
||||||
qtProxy_.setIsStreaming(false);
|
qtProxy_.setIsStreaming(false);
|
||||||
qtProxy_.setClientPlatform(QString());
|
qtProxy_.setClientPlatform(QString());
|
||||||
isStreaming_ = false;
|
isStreaming_ = false;
|
||||||
return Status::OK;
|
return Status::OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (eventQueue_.isEmpty()) {
|
if (eventQueue_.isEmpty()) {
|
||||||
locker.unlock();
|
locker.unlock();
|
||||||
QThread::msleep(100);
|
QThread::msleep(100);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
SPStreamEvent const event = eventQueue_.front();
|
SPStreamEvent const event = eventQueue_.front();
|
||||||
eventQueue_.pop_front();
|
eventQueue_.pop_front();
|
||||||
locker.unlock();
|
locker.unlock();
|
||||||
|
|||||||
@ -41,12 +41,10 @@ public: // member functions.
|
|||||||
bool isStreaming() const; ///< Check if the service is currently streaming events.
|
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 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;
|
grpc::Status AddLogEntry(::grpc::ServerContext *, ::grpc::AddLogEntryRequest const *request, ::google::protobuf::Empty *) override;
|
||||||
grpc::Status GuiReady(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::Empty *) override;
|
grpc::Status GuiReady(::grpc::ServerContext *, ::google::protobuf::Empty const *, grpc::GuiReadyResponse *response) override;
|
||||||
grpc::Status Quit(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::Empty *) override;
|
grpc::Status Quit(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::Empty *) override;
|
||||||
grpc::Status Restart(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::Empty *) override;
|
grpc::Status Restart(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::Empty *) override;
|
||||||
grpc::Status ShowOnStartup(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::BoolValue *response) override;
|
grpc::Status ShowOnStartup(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::BoolValue *response) override;
|
||||||
grpc::Status ShowSplashScreen(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::BoolValue *response) override;
|
|
||||||
grpc::Status IsFirstGuiStart(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::BoolValue *response) override;
|
|
||||||
grpc::Status SetIsAutostartOn(::grpc::ServerContext *, ::google::protobuf::BoolValue const *request, ::google::protobuf::Empty *) override;
|
grpc::Status SetIsAutostartOn(::grpc::ServerContext *, ::google::protobuf::BoolValue const *request, ::google::protobuf::Empty *) override;
|
||||||
grpc::Status IsAutostartOn(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::BoolValue *response) override;
|
grpc::Status IsAutostartOn(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::BoolValue *response) override;
|
||||||
grpc::Status SetIsBetaEnabled(::grpc::ServerContext *, ::google::protobuf::BoolValue const *request, ::google::protobuf::Empty *) override;
|
grpc::Status SetIsBetaEnabled(::grpc::ServerContext *, ::google::protobuf::BoolValue const *request, ::google::protobuf::Empty *) override;
|
||||||
@ -90,12 +88,12 @@ public: // member functions.
|
|||||||
grpc::Status GetUserList(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::grpc::UserListResponse *response) override;
|
grpc::Status GetUserList(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::grpc::UserListResponse *response) override;
|
||||||
grpc::Status GetUser(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::grpc::User *response) override;
|
grpc::Status GetUser(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::grpc::User *response) override;
|
||||||
grpc::Status SetUserSplitMode(::grpc::ServerContext *, ::grpc::UserSplitModeRequest const *request, ::google::protobuf::Empty *) override;
|
grpc::Status SetUserSplitMode(::grpc::ServerContext *, ::grpc::UserSplitModeRequest const *request, ::google::protobuf::Empty *) override;
|
||||||
|
grpc::Status SendBadEventUserFeedback(::grpc::ServerContext *context, ::grpc::UserBadEventFeedbackRequest const *request, ::google::protobuf::Empty *response) override;
|
||||||
grpc::Status LogoutUser(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
|
grpc::Status LogoutUser(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
|
||||||
grpc::Status RemoveUser(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
|
grpc::Status RemoveUser(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
|
||||||
grpc::Status ConfigureUserAppleMail(::grpc::ServerContext *, ::grpc::ConfigureAppleMailRequest const *request, ::google::protobuf::Empty *) override;
|
grpc::Status ConfigureUserAppleMail(::grpc::ServerContext *, ::grpc::ConfigureAppleMailRequest const *request, ::google::protobuf::Empty *) override;
|
||||||
grpc::Status RunEventStream(::grpc::ServerContext *, ::grpc::EventStreamRequest const *request, ::grpc::ServerWriter<::grpc::StreamEvent> *writer) override;
|
grpc::Status RunEventStream(::grpc::ServerContext *ctx, ::grpc::EventStreamRequest const *request, ::grpc::ServerWriter<::grpc::StreamEvent> *writer) override;
|
||||||
grpc::Status StopEventStream(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::Empty *) override;
|
grpc::Status StopEventStream(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::Empty *) override;
|
||||||
|
|
||||||
bool sendEvent(bridgepp::SPStreamEvent const &event); ///< Queue an event for sending through the event stream.
|
bool sendEvent(bridgepp::SPStreamEvent const &event); ///< Queue an event for sending through the event stream.
|
||||||
|
|
||||||
private: // member functions
|
private: // member functions
|
||||||
|
|||||||
@ -138,14 +138,6 @@ bool SettingsTab::showSplashScreen() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
/// \return true iff the 'Show Splash Screen' check box is checked.
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
bool SettingsTab::isFirstGUIStart() const {
|
|
||||||
return ui_.checkIsFirstGUIStart->isChecked();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \return true iff autosart is on.
|
/// \return true iff autosart is on.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
@ -450,7 +442,6 @@ void SettingsTab::resetUI() {
|
|||||||
ui_.editCurrentEmailClient->setText("Thunderbird/102.0.3");
|
ui_.editCurrentEmailClient->setText("Thunderbird/102.0.3");
|
||||||
ui_.checkShowOnStartup->setChecked(true);
|
ui_.checkShowOnStartup->setChecked(true);
|
||||||
ui_.checkShowSplashScreen->setChecked(false);
|
ui_.checkShowSplashScreen->setChecked(false);
|
||||||
ui_.checkIsFirstGUIStart->setChecked(false);
|
|
||||||
ui_.checkAutostart->setChecked(true);
|
ui_.checkAutostart->setChecked(true);
|
||||||
ui_.checkBetaEnabled->setChecked(true);
|
ui_.checkBetaEnabled->setChecked(true);
|
||||||
ui_.checkAllMailVisible->setChecked(true);
|
ui_.checkAllMailVisible->setChecked(true);
|
||||||
|
|||||||
@ -42,7 +42,6 @@ public: // member functions.
|
|||||||
void setGUIReady(bool ready); ///< Set the GUI as ready.
|
void setGUIReady(bool ready); ///< Set the GUI as ready.
|
||||||
bool showOnStartup() const; ///< Get the value for the 'Show On Startup' check.
|
bool showOnStartup() const; ///< Get the value for the 'Show On Startup' check.
|
||||||
bool showSplashScreen() const; ///< Get the value for the 'Show Splash Screen' check.
|
bool showSplashScreen() const; ///< Get the value for the 'Show Splash Screen' check.
|
||||||
bool isFirstGUIStart() const; ///< Get the value for the 'Is First GUI Start' check.
|
|
||||||
bool isAutostartOn() const; ///< Get the value for the 'Autostart' check.
|
bool isAutostartOn() const; ///< Get the value for the 'Autostart' check.
|
||||||
bool isBetaEnabled() const; ///< Get the value for the 'Beta Enabled' check.
|
bool isBetaEnabled() const; ///< Get the value for the 'Beta Enabled' check.
|
||||||
bool isAllMailVisible() const; ///< Get the value for the 'All Mail Visible' check.
|
bool isAllMailVisible() const; ///< Get the value for the 'All Mail Visible' check.
|
||||||
|
|||||||
@ -124,16 +124,6 @@
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="0" column="2">
|
<item row="0" column="2">
|
||||||
<widget class="QCheckBox" name="checkIsFirstGUIStart">
|
|
||||||
<property name="text">
|
|
||||||
<string>Is FIrst GUI Start</string>
|
|
||||||
</property>
|
|
||||||
<property name="checked">
|
|
||||||
<bool>false</bool>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="0">
|
|
||||||
<widget class="QCheckBox" name="checkAutostart">
|
<widget class="QCheckBox" name="checkAutostart">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Autostart</string>
|
<string>Autostart</string>
|
||||||
@ -143,7 +133,7 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="1">
|
<item row="1" column="0">
|
||||||
<widget class="QCheckBox" name="checkBetaEnabled">
|
<widget class="QCheckBox" name="checkBetaEnabled">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Beta Enabled</string>
|
<string>Beta Enabled</string>
|
||||||
@ -153,14 +143,17 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="2" column="1">
|
<item row="1" column="1">
|
||||||
<widget class="QCheckBox" name="checkAutomaticUpdate">
|
<widget class="QCheckBox" name="checkAllMailVisible">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Automatic Update</string>
|
<string>Show 'All Mail'</string>
|
||||||
|
</property>
|
||||||
|
<property name="checked">
|
||||||
|
<bool>false</bool>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="2" column="0">
|
<item row="1" column="2">
|
||||||
<widget class="QCheckBox" name="checkDarkTheme">
|
<widget class="QCheckBox" name="checkDarkTheme">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Dark Theme</string>
|
<string>Dark Theme</string>
|
||||||
@ -170,13 +163,10 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="2">
|
<item row="2" column="0">
|
||||||
<widget class="QCheckBox" name="checkAllMailVisible">
|
<widget class="QCheckBox" name="checkAutomaticUpdate">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Show 'All Mail'</string>
|
<string>Automatic Update</string>
|
||||||
</property>
|
|
||||||
<property name="checked">
|
|
||||||
<bool>false</bool>
|
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
@ -901,12 +891,6 @@
|
|||||||
<tabstop>editCurrentEmailClient</tabstop>
|
<tabstop>editCurrentEmailClient</tabstop>
|
||||||
<tabstop>checkShowOnStartup</tabstop>
|
<tabstop>checkShowOnStartup</tabstop>
|
||||||
<tabstop>checkShowSplashScreen</tabstop>
|
<tabstop>checkShowSplashScreen</tabstop>
|
||||||
<tabstop>checkIsFirstGUIStart</tabstop>
|
|
||||||
<tabstop>checkAutostart</tabstop>
|
|
||||||
<tabstop>checkBetaEnabled</tabstop>
|
|
||||||
<tabstop>checkAllMailVisible</tabstop>
|
|
||||||
<tabstop>checkDarkTheme</tabstop>
|
|
||||||
<tabstop>checkAutomaticUpdate</tabstop>
|
|
||||||
<tabstop>editHostname</tabstop>
|
<tabstop>editHostname</tabstop>
|
||||||
<tabstop>spinPortIMAP</tabstop>
|
<tabstop>spinPortIMAP</tabstop>
|
||||||
<tabstop>spinPortSMTP</tabstop>
|
<tabstop>spinPortSMTP</tabstop>
|
||||||
|
|||||||
@ -51,6 +51,7 @@ UsersTab::UsersTab(QWidget *parent)
|
|||||||
connect(ui_.buttonEditUser, &QPushButton::clicked, this, &UsersTab::onEditUserButton);
|
connect(ui_.buttonEditUser, &QPushButton::clicked, this, &UsersTab::onEditUserButton);
|
||||||
connect(ui_.tableUserList, &QTableView::doubleClicked, this, &UsersTab::onEditUserButton);
|
connect(ui_.tableUserList, &QTableView::doubleClicked, this, &UsersTab::onEditUserButton);
|
||||||
connect(ui_.buttonRemoveUser, &QPushButton::clicked, this, &UsersTab::onRemoveUserButton);
|
connect(ui_.buttonRemoveUser, &QPushButton::clicked, this, &UsersTab::onRemoveUserButton);
|
||||||
|
connect(ui_.buttonUserBadEvent, &QPushButton::clicked, this, &UsersTab::onSendUserBadEvent);
|
||||||
connect(ui_.checkUsernamePasswordError, &QCheckBox::toggled, this, &UsersTab::updateGUIState);
|
connect(ui_.checkUsernamePasswordError, &QCheckBox::toggled, this, &UsersTab::updateGUIState);
|
||||||
|
|
||||||
users_.append(randomUser());
|
users_.append(randomUser());
|
||||||
@ -96,6 +97,8 @@ void UsersTab::onEditUserButton() {
|
|||||||
if (grpc.isStreaming()) {
|
if (grpc.isStreaming()) {
|
||||||
grpc.sendEvent(newUserChangedEvent(user->id()));
|
grpc.sendEvent(newUserChangedEvent(user->id()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this->updateGUIState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -125,13 +128,42 @@ 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__));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (UserState::SignedOut == user->state()) {
|
||||||
|
app().log().error(QString("%1 failed. User is already signed out").arg(__FUNCTION__));
|
||||||
|
}
|
||||||
|
|
||||||
|
GRPCService &grpc = app().grpc();
|
||||||
|
if (grpc.isStreaming()) {
|
||||||
|
QString const userID = user->id();
|
||||||
|
grpc.sendEvent(newUserChangedEvent(userID));
|
||||||
|
grpc.sendEvent(newUserBadEvent(userID, ui_.editUserBadEvent->text()));
|
||||||
|
}
|
||||||
|
|
||||||
|
this->updateGUIState();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
//
|
//
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
void UsersTab::updateGUIState() {
|
void UsersTab::updateGUIState() {
|
||||||
bool const hasSelectedUser = ui_.tableUserList->selectionModel()->hasSelection();
|
SPUser const user = selectedUser();
|
||||||
|
bool const hasSelectedUser = user.get();
|
||||||
ui_.buttonEditUser->setEnabled(hasSelectedUser);
|
ui_.buttonEditUser->setEnabled(hasSelectedUser);
|
||||||
ui_.buttonRemoveUser->setEnabled(hasSelectedUser);
|
ui_.buttonRemoveUser->setEnabled(hasSelectedUser);
|
||||||
|
ui_.groupBoxBadEvent->setEnabled(hasSelectedUser && (UserState::SignedOut != user->state()));
|
||||||
ui_.editUsernamePasswordError->setEnabled(ui_.checkUsernamePasswordError->isChecked());
|
ui_.editUsernamePasswordError->setEnabled(ui_.checkUsernamePasswordError->isChecked());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -309,5 +341,30 @@ void UsersTab::removeUser(QString const &userID) {
|
|||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
void UsersTab::configureUserAppleMail(QString const &userID, QString const &address) {
|
void UsersTab::configureUserAppleMail(QString const &userID, QString const &address) {
|
||||||
app().log().info(QString("Apple mail configuration was requested for user %1, address %2").arg(userID, address));
|
app().log().info(QString("Apple mail configuration was requested for user %1, address %2").arg(userID, address));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \param[in] userID The userID.
|
||||||
|
/// \param[in] doResync Did the user request a resync?
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
void UsersTab::processBadEventUserFeedback(QString const &userID, bool doResync) {
|
||||||
|
app().log().info(QString("Feedback received for bad event: doResync = %1, userID = %2").arg(doResync ? "true" : "false", userID));
|
||||||
|
if (doResync) {
|
||||||
|
return; // we do not do any form of emulation for resync.
|
||||||
|
}
|
||||||
|
|
||||||
|
SPUser user = users_.userWithID(userID);
|
||||||
|
if (!user) {
|
||||||
|
app().log().error(QString("%1(): could not find user with id %1.").arg(__func__, userID));
|
||||||
|
}
|
||||||
|
|
||||||
|
user->setState(UserState::SignedOut);
|
||||||
|
users_.touch(userID);
|
||||||
|
GRPCService &grpc = app().grpc();
|
||||||
|
if (grpc.isStreaming()) {
|
||||||
|
grpc.sendEvent(newUserChangedEvent(userID));
|
||||||
|
}
|
||||||
|
|
||||||
|
this->updateGUIState();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -54,12 +54,14 @@ public slots:
|
|||||||
void logoutUser(QString const &userID); ///< slot for the logging out of a user.
|
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 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.
|
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:
|
private slots:
|
||||||
void onAddUserButton(); ///< Add a user to the user list.
|
void onAddUserButton(); ///< Add a user to the user list.
|
||||||
void onEditUserButton(); ///< Edit the currently selected user.
|
void onEditUserButton(); ///< Edit the currently selected user.
|
||||||
void onRemoveUserButton(); ///< Remove 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, QItemSelection); ///< Slot for the change of the selection.
|
||||||
|
void onSendUserBadEvent(); ///< Slot for the 'Send Bad Event Error' button.
|
||||||
void updateGUIState(); ///< Update the GUI state.
|
void updateGUIState(); ///< Update the GUI state.
|
||||||
|
|
||||||
private: // member functions.
|
private: // member functions.
|
||||||
|
|||||||
@ -67,7 +67,53 @@
|
|||||||
</spacer>
|
</spacer>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QGroupBox" name="groupBox">
|
<widget class="QGroupBox" name="groupBoxBadEvent">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>0</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="title">
|
||||||
|
<string>Bad Event</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="labelUserBadEvent">
|
||||||
|
<property name="text">
|
||||||
|
<string>Message: </string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="editUserBadEvent">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>200</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Bad event error.</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="buttonUserBadEvent">
|
||||||
|
<property name="text">
|
||||||
|
<string>Send Bad Event Error</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QGroupBox" name="groupBoxNextLogin">
|
||||||
<property name="minimumSize">
|
<property name="minimumSize">
|
||||||
<size>
|
<size>
|
||||||
<width>0</width>
|
<width>0</width>
|
||||||
|
|||||||
@ -186,6 +186,14 @@ void UserTable::touch(qint32 index) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \param[in] userID The userID.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
void UserTable::touch(QString const &userID) {
|
||||||
|
this->touch(this->indexOfUser(userID));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \param[in] index The index of the user in the list.
|
/// \param[in] index The index of the user in the list.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
|
|||||||
@ -43,6 +43,7 @@ public: // member functions.
|
|||||||
bridgepp::SPUser userWithUsername(QString const &username); ///< Return the user with a given username.
|
bridgepp::SPUser userWithUsername(QString const &username); ///< Return the user with a given username.
|
||||||
qint32 indexOfUser(QString const &userID); ///< Return the index of a given User.
|
qint32 indexOfUser(QString const &userID); ///< Return the index of a given User.
|
||||||
void touch(qint32 index); ///< touch the user at a given index (indicates it has been modified).
|
void touch(qint32 index); ///< touch the user at a given index (indicates it has been modified).
|
||||||
|
void touch(QString const& userID); ///< touch the user with the given userID (indicates it has been modified).
|
||||||
void remove(qint32 index); ///< Remove the user at a given index.
|
void remove(qint32 index); ///< Remove the user at a given index.
|
||||||
QList<bridgepp::SPUser> users() const; ///< Return a copy of the user list.
|
QList<bridgepp::SPUser> users() const; ///< Return a copy of the user list.
|
||||||
|
|
||||||
|
|||||||
@ -28,6 +28,10 @@
|
|||||||
|
|
||||||
using namespace bridgepp;
|
using namespace bridgepp;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
QString const noWindowFlag = "--no-window"; ///< The no-window command-line flag.
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \return The AppController instance.
|
/// \return The AppController instance.
|
||||||
@ -68,13 +72,34 @@ ProcessMonitor *AppController::bridgeMonitor() const {
|
|||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \param[in] function The function that caught the exception.
|
/// \param[in] exception The exception that triggered the fatal error.
|
||||||
/// \param[in] message The error message.
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
void AppController::onFatalError(QString const &function, QString const &message) {
|
void AppController::onFatalError(Exception const &exception) {
|
||||||
QString const fullMessage = QString("%1(): %2").arg(function, message);
|
sentry_uuid_t uuid = reportSentryException("AppController got notified of a fatal error", exception);
|
||||||
reportSentryException(SENTRY_LEVEL_ERROR, "AppController got notified of a fatal error", "Exception", fullMessage.toLocal8Bit());
|
|
||||||
QMessageBox::critical(nullptr, tr("Error"), message);
|
QMessageBox::critical(nullptr, tr("Error"), exception.what());
|
||||||
log().fatal(fullMessage);
|
restart(true);
|
||||||
|
log().fatal(QString("reportID: %1 Captured exception: %2").arg(QByteArray(uuid.bytes, 16).toHex(), exception.detailedWhat()));
|
||||||
qApp->exit(EXIT_FAILURE);
|
qApp->exit(EXIT_FAILURE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void AppController::restart(bool isCrashing) {
|
||||||
|
if (!launcher_.isEmpty()) {
|
||||||
|
QProcess p;
|
||||||
|
log_->info(QString("Restarting - App : %1 - Args : %2").arg(launcher_, launcherArgs_.join(" ")));
|
||||||
|
QStringList args = launcherArgs_;
|
||||||
|
if (isCrashing) {
|
||||||
|
args.append(noWindowFlag);
|
||||||
|
}
|
||||||
|
|
||||||
|
p.startDetached(launcher_, args);
|
||||||
|
p.waitForStarted();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void AppController::setLauncherArgs(const QString &launcher, const QStringList &args) {
|
||||||
|
launcher_ = launcher;
|
||||||
|
launcherArgs_ = args;
|
||||||
|
}
|
||||||
@ -20,21 +20,16 @@
|
|||||||
#define BRIDGE_GUI_APP_CONTROLLER_H
|
#define BRIDGE_GUI_APP_CONTROLLER_H
|
||||||
|
|
||||||
|
|
||||||
|
// @formatter:off
|
||||||
class QMLBackend;
|
class QMLBackend;
|
||||||
|
|
||||||
|
|
||||||
namespace bridgepp {
|
namespace bridgepp {
|
||||||
class Log;
|
class Log;
|
||||||
|
|
||||||
|
|
||||||
class Overseer;
|
class Overseer;
|
||||||
|
|
||||||
|
|
||||||
class GRPCClient;
|
class GRPCClient;
|
||||||
|
|
||||||
|
|
||||||
class ProcessMonitor;
|
class ProcessMonitor;
|
||||||
|
class Exception;
|
||||||
}
|
}
|
||||||
|
// @formatter:off
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
@ -55,18 +50,22 @@ public: // member functions.
|
|||||||
bridgepp::Log &log() { return *log_; } ///< Return a reference to the log.
|
bridgepp::Log &log() { return *log_; } ///< Return a reference to the log.
|
||||||
std::unique_ptr<bridgepp::Overseer> &bridgeOverseer() { return bridgeOverseer_; }; ///< Returns a reference the bridge overseer
|
std::unique_ptr<bridgepp::Overseer> &bridgeOverseer() { return bridgeOverseer_; }; ///< Returns a reference the bridge overseer
|
||||||
bridgepp::ProcessMonitor *bridgeMonitor() const; ///< Return the bridge worker.
|
bridgepp::ProcessMonitor *bridgeMonitor() const; ///< Return the bridge worker.
|
||||||
|
void setLauncherArgs(const QString& launcher, const QStringList& args);
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void onFatalError(QString const &function, QString const &message); ///< Handle fatal errors.
|
void onFatalError(bridgepp::Exception const& e); ///< Handle fatal errors.
|
||||||
|
|
||||||
private: // member functions
|
private: // member functions
|
||||||
AppController(); ///< Default constructor.
|
AppController(); ///< Default constructor.
|
||||||
|
void restart(bool isCrashing = false); ///< Restart the app.
|
||||||
|
|
||||||
private: // data members
|
private: // data members
|
||||||
std::unique_ptr<QMLBackend> backend_; ///< The backend.
|
std::unique_ptr<QMLBackend> backend_; ///< The backend.
|
||||||
std::unique_ptr<bridgepp::GRPCClient> grpc_; ///< The RPC client.
|
std::unique_ptr<bridgepp::GRPCClient> grpc_; ///< The RPC client.
|
||||||
std::unique_ptr<bridgepp::Log> log_; ///< The log.
|
std::unique_ptr<bridgepp::Log> log_; ///< The log.
|
||||||
std::unique_ptr<bridgepp::Overseer> bridgeOverseer_; ///< The overseer for the bridge monitor worker.
|
std::unique_ptr<bridgepp::Overseer> bridgeOverseer_; ///< The overseer for the bridge monitor worker.
|
||||||
|
QString launcher_;
|
||||||
|
QStringList launcherArgs_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -24,5 +24,7 @@
|
|||||||
#define PROJECT_VER "@BRIDGE_APP_VERSION@"
|
#define PROJECT_VER "@BRIDGE_APP_VERSION@"
|
||||||
#define PROJECT_REVISION "@BRIDGE_REVISION@"
|
#define PROJECT_REVISION "@BRIDGE_REVISION@"
|
||||||
#define PROJECT_BUILD_TIME "@BRIDGE_BUILD_TIME@"
|
#define PROJECT_BUILD_TIME "@BRIDGE_BUILD_TIME@"
|
||||||
|
#define PROJECT_DSN_SENTRY "@BRIDGE_DSN_SENTRY@"
|
||||||
|
#define PROJECT_BUILD_ENV "@BRIDGE_BUILD_ENV@"
|
||||||
|
|
||||||
#endif // BRIDGE_GUI_VERSION_H
|
#endif // BRIDGE_GUI_VERSION_H
|
||||||
@ -85,20 +85,12 @@ message(STATUS "Using Qt ${Qt6_VERSION}")
|
|||||||
#*****************************************************************************************************************************************************
|
#*****************************************************************************************************************************************************
|
||||||
find_package(sentry CONFIG REQUIRED)
|
find_package(sentry CONFIG REQUIRED)
|
||||||
|
|
||||||
set(DSN_SENTRY "https://ea31dfe8574849108fb8ba044fec3620@api.protonmail.ch/core/v4/reports/sentry/7")
|
|
||||||
set(SENTRY_CONFIG_GENERATED_FILE_DIR ${CMAKE_CURRENT_BINARY_DIR}/sentry-generated)
|
|
||||||
set(SENTRY_CONFIG_FILE ${SENTRY_CONFIG_GENERATED_FILE_DIR}/project_sentry_config.h)
|
|
||||||
file(GENERATE OUTPUT ${SENTRY_CONFIG_FILE} CONTENT
|
|
||||||
"// AUTO GENERATED FILE, DO NOT MODIFY\n#pragma once\nconst char* SentryDNS=\"${DSN_SENTRY}\";\nconst char* SentryProductID=\"bridge-mail@${BRIDGE_APP_VERSION}\";\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#*****************************************************************************************************************************************************
|
#*****************************************************************************************************************************************************
|
||||||
# Source files and output
|
# Source files and output
|
||||||
#*****************************************************************************************************************************************************
|
#*****************************************************************************************************************************************************
|
||||||
|
|
||||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/Version.h.in ${CMAKE_CURRENT_SOURCE_DIR}/Version.h)
|
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/BuildConfig.h.in ${CMAKE_CURRENT_SOURCE_DIR}/BuildConfig.h)
|
||||||
|
|
||||||
if (NOT TARGET bridgepp)
|
if (NOT TARGET bridgepp)
|
||||||
add_subdirectory(../bridgepp bridgepp)
|
add_subdirectory(../bridgepp bridgepp)
|
||||||
@ -120,9 +112,10 @@ add_executable(bridge-gui
|
|||||||
BridgeApp.cpp BridgeApp.h
|
BridgeApp.cpp BridgeApp.h
|
||||||
CommandLine.cpp CommandLine.h
|
CommandLine.cpp CommandLine.h
|
||||||
EventStreamWorker.cpp EventStreamWorker.h
|
EventStreamWorker.cpp EventStreamWorker.h
|
||||||
|
LogUtils.cpp LogUtils.h
|
||||||
main.cpp
|
main.cpp
|
||||||
Pch.h
|
Pch.h
|
||||||
Version.h
|
BuildConfig.h
|
||||||
QMLBackend.cpp QMLBackend.h
|
QMLBackend.cpp QMLBackend.h
|
||||||
UserList.cpp UserList.h
|
UserList.cpp UserList.h
|
||||||
SentryUtils.cpp SentryUtils.h
|
SentryUtils.cpp SentryUtils.h
|
||||||
|
|||||||
@ -98,6 +98,7 @@ CommandLineOptions parseCommandLine(int argc, char *argv[]) {
|
|||||||
// we can't use QCommandLineParser here since it will fail on unknown options.
|
// we can't use QCommandLineParser here since it will fail on unknown options.
|
||||||
// Arguments may contain some bridge flags.
|
// Arguments may contain some bridge flags.
|
||||||
if (arg == softwareRendererFlag) {
|
if (arg == softwareRendererFlag) {
|
||||||
|
options.bridgeGuiArgs.append(arg);
|
||||||
options.useSoftwareRenderer = true;
|
options.useSoftwareRenderer = true;
|
||||||
}
|
}
|
||||||
if (arg == noWindowFlag) {
|
if (arg == noWindowFlag) {
|
||||||
@ -113,10 +114,12 @@ CommandLineOptions parseCommandLine(int argc, char *argv[]) {
|
|||||||
else if (arg == "--attach" || arg == "-a") {
|
else if (arg == "--attach" || arg == "-a") {
|
||||||
// we don't keep the attach mode within the args since we don't need it for Bridge.
|
// we don't keep the attach mode within the args since we don't need it for Bridge.
|
||||||
options.attach = true;
|
options.attach = true;
|
||||||
|
options.bridgeGuiArgs.append(arg);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
else {
|
else {
|
||||||
options.bridgeArgs.append(arg);
|
options.bridgeArgs.append(arg);
|
||||||
|
options.bridgeGuiArgs.append(arg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!flagFound) {
|
if (!flagFound) {
|
||||||
|
|||||||
@ -28,6 +28,7 @@
|
|||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
struct CommandLineOptions {
|
struct CommandLineOptions {
|
||||||
QStringList bridgeArgs; ///< The command-line arguments we will pass to bridge when launching it.
|
QStringList bridgeArgs; ///< The command-line arguments we will pass to bridge when launching it.
|
||||||
|
QStringList bridgeGuiArgs; ///< The command-line arguments we will pass to bridge when launching it.
|
||||||
QString launcher; ///< The path to the launcher.
|
QString launcher; ///< The path to the launcher.
|
||||||
bool attach { false }; ///< Is the application running in attached mode?
|
bool attach { false }; ///< Is the application running in attached mode?
|
||||||
bridgepp::Log::Level logLevel { bridgepp::Log::defaultLevel }; ///< The log level
|
bridgepp::Log::Level logLevel { bridgepp::Log::defaultLevel }; ///< The log level
|
||||||
|
|||||||
@ -52,7 +52,7 @@ void EventStreamReader::run() {
|
|||||||
emit finished();
|
emit finished();
|
||||||
}
|
}
|
||||||
catch (Exception const &e) {
|
catch (Exception const &e) {
|
||||||
reportSentryException(SENTRY_LEVEL_ERROR, "Error during event stream read", "Exception", e.what());
|
reportSentryException("Error during event stream read", e);
|
||||||
emit error(e.qwhat());
|
emit error(e.qwhat());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
62
internal/frontend/bridge-gui/bridge-gui/LogUtils.cpp
Normal file
62
internal/frontend/bridge-gui/bridge-gui/LogUtils.cpp
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
// 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 "LogUtils.h"
|
||||||
|
#include <bridgepp/BridgeUtils.h>
|
||||||
|
|
||||||
|
|
||||||
|
using namespace bridgepp;
|
||||||
|
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
qsizetype const logFileTailMaxLength = 25 * 1024; ///< The maximum length of the portion of log returned by tailOfLatestBridgeLog()
|
||||||
|
}
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \brief Return the path of the latest bridge log.
|
||||||
|
/// \return The path of the latest bridge log file.
|
||||||
|
/// \return An empty string if no bridge log file was found.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
QString latestBridgeLogPath() {
|
||||||
|
QDir const logsDir(userLogsDir());
|
||||||
|
if (logsDir.isEmpty()) {
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
QFileInfoList files = logsDir.entryInfoList({ "v*.log" }, QDir::Files); // could do sorting, but only by last modification time. we want to sort by creation time.
|
||||||
|
std::sort(files.begin(), files.end(), [](QFileInfo const &lhs, QFileInfo const &rhs) -> bool {
|
||||||
|
return lhs.birthTime() < rhs.birthTime();
|
||||||
|
});
|
||||||
|
return files.back().absoluteFilePath();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// Return the maxSize last bytes of the latest bridge log.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
QByteArray tailOfLatestBridgeLog() {
|
||||||
|
QString path = latestBridgeLogPath();
|
||||||
|
if (path.isEmpty()) {
|
||||||
|
return QByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
QFile file(path);
|
||||||
|
return file.open(QIODevice::Text | QIODevice::ReadOnly) ? file.readAll().right(logFileTailMaxLength) : QByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
26
internal/frontend/bridge-gui/bridge-gui/LogUtils.h
Normal file
26
internal/frontend/bridge-gui/bridge-gui/LogUtils.h
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
// 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_LOG_UTILS_H
|
||||||
|
#define BRIDGE_GUI_LOG_UTILS_H
|
||||||
|
|
||||||
|
|
||||||
|
QByteArray tailOfLatestBridgeLog(); ///< Return the last bytes of the last bridge log.
|
||||||
|
|
||||||
|
|
||||||
|
#endif //BRIDGE_GUI_LOG_UTILS_H
|
||||||
@ -18,15 +18,17 @@
|
|||||||
|
|
||||||
#include "QMLBackend.h"
|
#include "QMLBackend.h"
|
||||||
#include "EventStreamWorker.h"
|
#include "EventStreamWorker.h"
|
||||||
#include "Version.h"
|
#include "BuildConfig.h"
|
||||||
|
#include "LogUtils.h"
|
||||||
#include <bridgepp/GRPC/GRPCClient.h>
|
#include <bridgepp/GRPC/GRPCClient.h>
|
||||||
#include <bridgepp/Exception/Exception.h>
|
#include <bridgepp/Exception/Exception.h>
|
||||||
#include <bridgepp/Worker/Overseer.h>
|
#include <bridgepp/Worker/Overseer.h>
|
||||||
|
#include <bridgepp/BridgeUtils.h>
|
||||||
|
|
||||||
|
|
||||||
#define HANDLE_EXCEPTION(x) try { x } \
|
#define HANDLE_EXCEPTION(x) try { x } \
|
||||||
catch (Exception const &e) { emit fatalError(__func__, e.qwhat()); } \
|
catch (Exception const &e) { emit fatalError(e); } \
|
||||||
catch (...) { emit fatalError(__func__, QString("An unknown exception occurred")); }
|
catch (...) { emit fatalError(Exception("An unknown exception occurred", QString(), __func__)); }
|
||||||
#define HANDLE_EXCEPTION_RETURN_BOOL(x) HANDLE_EXCEPTION(x) return false;
|
#define HANDLE_EXCEPTION_RETURN_BOOL(x) HANDLE_EXCEPTION(x) return false;
|
||||||
#define HANDLE_EXCEPTION_RETURN_QSTRING(x) HANDLE_EXCEPTION(x) return QString();
|
#define HANDLE_EXCEPTION_RETURN_QSTRING(x) HANDLE_EXCEPTION(x) return QString();
|
||||||
#define HANDLE_EXCEPTION_RETURN_ZERO(x) HANDLE_EXCEPTION(x) return 0;
|
#define HANDLE_EXCEPTION_RETURN_ZERO(x) HANDLE_EXCEPTION(x) return 0;
|
||||||
@ -56,12 +58,8 @@ void QMLBackend::init(GRPCConfig const &serviceConfig) {
|
|||||||
app().grpc().setLog(&log);
|
app().grpc().setLog(&log);
|
||||||
this->connectGrpcEvents();
|
this->connectGrpcEvents();
|
||||||
|
|
||||||
QString error;
|
app().grpc().connectToServer(serviceConfig, app().bridgeMonitor());
|
||||||
if (app().grpc().connectToServer(serviceConfig, app().bridgeMonitor(), error)) {
|
app().log().info("Connected to backend via gRPC service.");
|
||||||
app().log().info("Connected to backend via gRPC service.");
|
|
||||||
} else {
|
|
||||||
throw Exception(QString("Cannot connectToServer to go backend via gRPC: %1").arg(error));
|
|
||||||
}
|
|
||||||
|
|
||||||
QString bridgeVer;
|
QString bridgeVer;
|
||||||
app().grpc().version(bridgeVer);
|
app().grpc().version(bridgeVer);
|
||||||
@ -77,7 +75,6 @@ void QMLBackend::init(GRPCConfig const &serviceConfig) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Grab from bridge the value that will not change during the execution of this app (or that will only change locally).
|
// Grab from bridge the value that will not change during the execution of this app (or that will only change locally).
|
||||||
app().grpc().showSplashScreen(showSplashScreen_);
|
|
||||||
app().grpc().goos(goos_);
|
app().grpc().goos(goos_);
|
||||||
app().grpc().logsPath(logsPath_);
|
app().grpc().logsPath(logsPath_);
|
||||||
app().grpc().licensePath(licensePath_);
|
app().grpc().licensePath(licensePath_);
|
||||||
@ -102,6 +99,14 @@ bool QMLBackend::waitForEventStreamReaderToFinish(qint32 timeoutMs) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \return The build year as a string (e.g. 2023)
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
QString QMLBackend::buildYear() {
|
||||||
|
return QString(__DATE__).right(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \return The position of the cursor.
|
/// \return The position of the cursor.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
@ -460,19 +465,6 @@ bool QMLBackend::isDoHEnabled() const {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
/// \return The value for the 'isFirstGUIStart' property.
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
bool QMLBackend::isFirstGUIStart() const {
|
|
||||||
HANDLE_EXCEPTION_RETURN_BOOL(
|
|
||||||
bool v;
|
|
||||||
app().grpc().isFirstGUIStart(v);
|
|
||||||
return v;
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \return The value for the 'isAutomaticUpdateOn' property.
|
/// \return The value for the 'isAutomaticUpdateOn' property.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
@ -603,7 +595,8 @@ void QMLBackend::setDiskCachePath(QUrl const &path) const {
|
|||||||
void QMLBackend::login(QString const &username, QString const &password) const {
|
void QMLBackend::login(QString const &username, QString const &password) const {
|
||||||
HANDLE_EXCEPTION(
|
HANDLE_EXCEPTION(
|
||||||
if (username.compare("coco@bandicoot", Qt::CaseInsensitive) == 0) {
|
if (username.compare("coco@bandicoot", Qt::CaseInsensitive) == 0) {
|
||||||
throw Exception("User requested bridge-gui to crash by trying to log as coco@bandicoot");
|
throw Exception("User requested bridge-gui to crash by trying to log as coco@bandicoot",
|
||||||
|
"This error exists for test purposes and should be ignored.", __func__, tailOfLatestBridgeLog());
|
||||||
}
|
}
|
||||||
app().grpc().login(username, password);
|
app().grpc().login(username, password);
|
||||||
)
|
)
|
||||||
@ -691,9 +684,11 @@ void QMLBackend::changeKeychain(QString const &keychain) {
|
|||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
//
|
//
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
void QMLBackend::guiReady() const {
|
void QMLBackend::guiReady() {
|
||||||
HANDLE_EXCEPTION(
|
HANDLE_EXCEPTION(
|
||||||
app().grpc().guiReady();
|
bool showSplashScreen;
|
||||||
|
app().grpc().guiReady(showSplashScreen);
|
||||||
|
this->setShowSplashScreen(showSplashScreen);
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -821,6 +816,26 @@ void QMLBackend::setMailServerSettings(int imapPort, int smtpPort, bool useSSLFo
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \param[in] userID The userID.
|
||||||
|
/// \param[in] doResync Did the user request a resync.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
void QMLBackend::sendBadEventUserFeedback(QString const &userID, bool doResync) {
|
||||||
|
HANDLE_EXCEPTION(
|
||||||
|
app().grpc().sendBadEventUserFeedback(userID, doResync);
|
||||||
|
|
||||||
|
// Notification dialog has just been dismissed, we remove the userID from the queue, and if there are other events in the queue, we show
|
||||||
|
// the dialog again.
|
||||||
|
badEventDisplayQueue_.removeOne(userID);
|
||||||
|
if (!badEventDisplayQueue_.isEmpty()) {
|
||||||
|
// we introduce a small delay here, so that the user notices the dialog disappear and pops up again.
|
||||||
|
QTimer::singleShot(500, [&]() { this->displayBadEventDialog(badEventDisplayQueue_.front()); });
|
||||||
|
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \param[in] imapPort The IMAP port.
|
/// \param[in] imapPort The IMAP port.
|
||||||
/// \param[in] smtpPort The SMTP port.
|
/// \param[in] smtpPort The SMTP port.
|
||||||
@ -872,6 +887,29 @@ void QMLBackend::onLoginAlreadyLoggedIn(QString const &userID) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \param[in] userID The userID.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
void QMLBackend::onUserBadEvent(QString const &userID, QString const& ) {
|
||||||
|
HANDLE_EXCEPTION(
|
||||||
|
if (badEventDisplayQueue_.contains(userID)) {
|
||||||
|
app().log().error("Received 'bad event' for a user that is already in the queue.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SPUser const user = users_->getUserWithID(userID);
|
||||||
|
if (!user) {
|
||||||
|
app().log().error(QString("Received bad event for unknown user %1."));
|
||||||
|
}
|
||||||
|
|
||||||
|
badEventDisplayQueue_.append(userID);
|
||||||
|
if (badEventDisplayQueue_.size() == 1) { // there was no other item is the queue, we can display the dialog immediately.
|
||||||
|
this->displayBadEventDialog(userID);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
//
|
//
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
@ -979,5 +1017,25 @@ void QMLBackend::connectGrpcEvents() {
|
|||||||
|
|
||||||
// user events
|
// user events
|
||||||
connect(client, &GRPCClient::userDisconnected, this, &QMLBackend::userDisconnected);
|
connect(client, &GRPCClient::userDisconnected, this, &QMLBackend::userDisconnected);
|
||||||
|
connect(client, &GRPCClient::userBadEvent, this, &QMLBackend::onUserBadEvent);
|
||||||
users_->connectGRPCEvents();
|
users_->connectGRPCEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \param[in] userID The userID.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
void QMLBackend::displayBadEventDialog(QString const &userID) {
|
||||||
|
HANDLE_EXCEPTION(
|
||||||
|
SPUser const user = users_->getUserWithID(userID);
|
||||||
|
if (!user) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit userBadEvent(userID,
|
||||||
|
tr("Bridge ran into an internal error and it is not able to proceed with the account %1. Synchronize your local database now or logout"
|
||||||
|
" to do it later. Synchronization time depends on the size of your mailbox.").arg(elideLongString(user->primaryEmailOrUsername(), 30)));
|
||||||
|
emit selectUser(userID);
|
||||||
|
emit showMainWindow();
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
|
|
||||||
#include "MacOS/DockIcon.h"
|
#include "MacOS/DockIcon.h"
|
||||||
#include "Version.h"
|
#include "BuildConfig.h"
|
||||||
#include "UserList.h"
|
#include "UserList.h"
|
||||||
#include <bridgepp/GRPC/GRPCClient.h>
|
#include <bridgepp/GRPC/GRPCClient.h>
|
||||||
#include <bridgepp/GRPC/GRPCUtils.h>
|
#include <bridgepp/GRPC/GRPCUtils.h>
|
||||||
@ -45,6 +45,7 @@ public: // member functions.
|
|||||||
bool waitForEventStreamReaderToFinish(qint32 timeoutMs); ///< Wait for the event stream reader to finish.
|
bool waitForEventStreamReaderToFinish(qint32 timeoutMs); ///< Wait for the event stream reader to finish.
|
||||||
|
|
||||||
// invokable methods can be called from QML. They generally return a value, which slots cannot do.
|
// invokable methods can be called from QML. They generally return a value, which slots cannot do.
|
||||||
|
Q_INVOKABLE static QString buildYear(); ///< Return the application build year.
|
||||||
Q_INVOKABLE QPoint getCursorPos() const; ///< Retrieve the cursor position.
|
Q_INVOKABLE QPoint getCursorPos() const; ///< Retrieve the cursor position.
|
||||||
Q_INVOKABLE bool isPortFree(int port) const; ///< Check if a given network port is available.
|
Q_INVOKABLE bool isPortFree(int port) const; ///< Check if a given network port is available.
|
||||||
Q_INVOKABLE QString nativePath(QUrl const &url) const; ///< Retrieve the native path of a local URL.
|
Q_INVOKABLE QString nativePath(QUrl const &url) const; ///< Retrieve the native path of a local URL.
|
||||||
@ -73,7 +74,6 @@ public: // Qt/QML properties. Note that the NOTIFY-er signal is required even fo
|
|||||||
Q_PROPERTY(int imapPort READ imapPort WRITE setIMAPPort NOTIFY imapPortChanged)
|
Q_PROPERTY(int imapPort READ imapPort WRITE setIMAPPort NOTIFY imapPortChanged)
|
||||||
Q_PROPERTY(int smtpPort READ smtpPort WRITE setSMTPPort NOTIFY smtpPortChanged)
|
Q_PROPERTY(int smtpPort READ smtpPort WRITE setSMTPPort NOTIFY smtpPortChanged)
|
||||||
Q_PROPERTY(bool isDoHEnabled READ isDoHEnabled NOTIFY isDoHEnabledChanged)
|
Q_PROPERTY(bool isDoHEnabled READ isDoHEnabled NOTIFY isDoHEnabledChanged)
|
||||||
Q_PROPERTY(bool isFirstGUIStart READ isFirstGUIStart)
|
|
||||||
Q_PROPERTY(bool isAutomaticUpdateOn READ isAutomaticUpdateOn NOTIFY isAutomaticUpdateOnChanged)
|
Q_PROPERTY(bool isAutomaticUpdateOn READ isAutomaticUpdateOn NOTIFY isAutomaticUpdateOnChanged)
|
||||||
Q_PROPERTY(QString currentEmailClient READ currentEmailClient NOTIFY currentEmailClientChanged)
|
Q_PROPERTY(QString currentEmailClient READ currentEmailClient NOTIFY currentEmailClientChanged)
|
||||||
Q_PROPERTY(QStringList availableKeychain READ availableKeychain NOTIFY availableKeychainChanged)
|
Q_PROPERTY(QStringList availableKeychain READ availableKeychain NOTIFY availableKeychainChanged)
|
||||||
@ -110,7 +110,6 @@ public: // Qt/QML properties. Note that the NOTIFY-er signal is required even fo
|
|||||||
void setSMTPPort(int port); ///< Setter for the 'smtpPort' property.
|
void setSMTPPort(int port); ///< Setter for the 'smtpPort' property.
|
||||||
int smtpPort() const; ///< Getter for the 'smtpPort' property.
|
int smtpPort() const; ///< Getter for the 'smtpPort' property.
|
||||||
bool isDoHEnabled() const; ///< Getter for the 'isDoHEnabled' property.
|
bool isDoHEnabled() const; ///< Getter for the 'isDoHEnabled' property.
|
||||||
bool isFirstGUIStart() const; ///< Getter for the 'isFirstGUIStart' property.
|
|
||||||
bool isAutomaticUpdateOn() const; ///< Getter for the 'isAutomaticUpdateOn' property.
|
bool isAutomaticUpdateOn() const; ///< Getter for the 'isAutomaticUpdateOn' property.
|
||||||
QString currentEmailClient() const; ///< Getter for the 'currentEmail' property.
|
QString currentEmailClient() const; ///< Getter for the 'currentEmail' property.
|
||||||
QStringList availableKeychain() const; ///< Getter for the 'availableKeychain' property.
|
QStringList availableKeychain() const; ///< Getter for the 'availableKeychain' property.
|
||||||
@ -162,7 +161,7 @@ public slots: // slot for signals received from QML -> To be forwarded to Bridge
|
|||||||
void toggleAutomaticUpdate(bool makeItActive); ///< Slot for the automatic update toggle
|
void toggleAutomaticUpdate(bool makeItActive); ///< Slot for the automatic update toggle
|
||||||
void updateCurrentMailClient(); ///< Slot for the change of the current mail client.
|
void updateCurrentMailClient(); ///< Slot for the change of the current mail client.
|
||||||
void changeKeychain(QString const &keychain); ///< Slot for the change of keychain.
|
void changeKeychain(QString const &keychain); ///< Slot for the change of keychain.
|
||||||
void guiReady() const; ///< Slot for the GUI ready signal.
|
void guiReady(); ///< Slot for the GUI ready signal.
|
||||||
void quit() const; ///< Slot for the quit signal.
|
void quit() const; ///< Slot for the quit signal.
|
||||||
void restart() const; ///< Slot for the restart signal.
|
void restart() const; ///< Slot for the restart signal.
|
||||||
void forceLauncher(QString launcher) const; ///< Slot for the change of the launcher.
|
void forceLauncher(QString launcher) const; ///< Slot for the change of the launcher.
|
||||||
@ -174,12 +173,14 @@ public slots: // slot for signals received from QML -> To be forwarded to Bridge
|
|||||||
void onResetFinished(); ///< Slot for the reset finish signal.
|
void onResetFinished(); ///< Slot for the reset finish signal.
|
||||||
void onVersionChanged(); ///< Slot for the version change signal.
|
void onVersionChanged(); ///< Slot for the version change signal.
|
||||||
void setMailServerSettings(int imapPort, int smtpPort, bool useSSLForIMAP, bool useSSLForSMTP) const; ///< Forwards a connection mode change request from QML to gRPC
|
void setMailServerSettings(int imapPort, int smtpPort, bool useSSLForIMAP, bool useSSLForSMTP) const; ///< Forwards a connection mode change request from QML to gRPC
|
||||||
|
void sendBadEventUserFeedback(QString const &userID, bool doResync); ///< Slot the providing user feedback for a bad event.
|
||||||
|
|
||||||
public slots: // slot for signals received from gRPC that need transformation instead of simple forwarding
|
public slots: // slot for signals received from gRPC that need transformation instead of simple forwarding
|
||||||
void onMailServerSettingsChanged(int imapPort, int smtpPort, bool useSSLForIMAP, bool useSSLForSMTP); ///< Slot for the ConnectionModeChanged gRPC event.
|
void onMailServerSettingsChanged(int imapPort, int smtpPort, bool useSSLForIMAP, bool useSSLForSMTP); ///< Slot for the ConnectionModeChanged gRPC event.
|
||||||
void onGenericError(bridgepp::ErrorInfo const &info); ///< Slot for generic errors received from the gRPC service.
|
void onGenericError(bridgepp::ErrorInfo const &info); ///< Slot for generic errors received from the gRPC service.
|
||||||
void onLoginFinished(QString const &userID, bool wasSignedOut); ///< Slot for LoginFinished gRPC event.
|
void onLoginFinished(QString const &userID, bool wasSignedOut); ///< Slot for LoginFinished gRPC event.
|
||||||
void onLoginAlreadyLoggedIn(QString const &userID); ///< Slot for the LoginAlreadyLoggedIn gRPC event.
|
void onLoginAlreadyLoggedIn(QString const &userID); ///< Slot for the LoginAlreadyLoggedIn gRPC event.
|
||||||
|
void onUserBadEvent(QString const& userID, QString const& errorMessage); ///< Slot for the userBadEvent gRPC event.
|
||||||
|
|
||||||
signals: // Signals received from the Go backend, to be forwarded to QML
|
signals: // Signals received from the Go backend, to be forwarded to QML
|
||||||
void toggleAutostartFinished(); ///< Signal for the 'toggleAutostartFinished' gRPC stream event.
|
void toggleAutostartFinished(); ///< Signal for the 'toggleAutostartFinished' gRPC stream event.
|
||||||
@ -222,6 +223,7 @@ signals: // Signals received from the Go backend, to be forwarded to QML
|
|||||||
void addressChangedLogout(QString const &address); ///< Signal for the 'addressChangedLogout' gRPC stream event.
|
void addressChangedLogout(QString const &address); ///< Signal for the 'addressChangedLogout' gRPC stream event.
|
||||||
void apiCertIssue(); ///< Signal for the 'apiCertIssue' gRPC stream event.
|
void apiCertIssue(); ///< Signal for the 'apiCertIssue' gRPC stream event.
|
||||||
void userDisconnected(QString const &username); ///< Signal for the 'userDisconnected' gRPC stream event.
|
void userDisconnected(QString const &username); ///< Signal for the 'userDisconnected' gRPC stream event.
|
||||||
|
void userBadEvent(QString const &userID, QString const &description); ///< Signal for the 'userBadEvent' gRPC stream event.
|
||||||
void internetOff(); ///< Signal for the 'internetOff' gRPC stream event.
|
void internetOff(); ///< Signal for the 'internetOff' gRPC stream event.
|
||||||
void internetOn(); ///< Signal for the 'internetOn' gRPC stream event.
|
void internetOn(); ///< Signal for the 'internetOn' gRPC stream event.
|
||||||
void resetFinished(); ///< Signal for the 'resetFinished' gRPC stream event.
|
void resetFinished(); ///< Signal for the 'resetFinished' gRPC stream event.
|
||||||
@ -231,13 +233,15 @@ signals: // Signals received from the Go backend, to be forwarded to QML
|
|||||||
void showMainWindow(); ///< Signal for the 'showMainWindow' gRPC stream event.
|
void showMainWindow(); ///< Signal for the 'showMainWindow' gRPC stream event.
|
||||||
void hideMainWindow(); ///< Signal for the 'hideMainWindow' gRPC stream event.
|
void hideMainWindow(); ///< Signal for the 'hideMainWindow' gRPC stream event.
|
||||||
void genericError(QString const &title, QString const &description); ///< Signal for the 'genericError' gRPC stream event.
|
void genericError(QString const &title, QString const &description); ///< Signal for the 'genericError' gRPC stream event.
|
||||||
|
void selectUser(QString const); ///< Signal that request the given user account to be displayed.
|
||||||
|
|
||||||
// This signal is emitted when an exception is intercepted is calls triggered by QML. QML engine would intercept the exception otherwise.
|
// This signal is emitted when an exception is intercepted is calls triggered by QML. QML engine would intercept the exception otherwise.
|
||||||
void fatalError(QString const &function, QString const &message) const; ///< Signal emitted when an fatal error occurs.
|
void fatalError(bridgepp::Exception const& e) const; ///< Signal emitted when an fatal error occurs.
|
||||||
|
|
||||||
private: // member functions
|
private: // member functions
|
||||||
void retrieveUserList(); ///< Retrieve the list of users via gRPC.
|
void retrieveUserList(); ///< Retrieve the list of users via gRPC.
|
||||||
void connectGrpcEvents(); ///< Connect gRPC that need to be forwarded to QML via backend signals
|
void connectGrpcEvents(); ///< Connect gRPC that need to be forwarded to QML via backend signals
|
||||||
|
void displayBadEventDialog(QString const& userID); ///< Displays the bad event dialog for a user.
|
||||||
|
|
||||||
private: // data members
|
private: // data members
|
||||||
UserList *users_ { nullptr }; ///< The user list. Owned by backend.
|
UserList *users_ { nullptr }; ///< The user list. Owned by backend.
|
||||||
@ -250,6 +254,7 @@ private: // data members
|
|||||||
int smtpPort_ { 0 }; ///< The cached value for the SMTP port.
|
int smtpPort_ { 0 }; ///< The cached value for the SMTP port.
|
||||||
bool useSSLForIMAP_ { false }; ///< The cached value for useSSLForIMAP.
|
bool useSSLForIMAP_ { false }; ///< The cached value for useSSLForIMAP.
|
||||||
bool useSSLForSMTP_ { false }; ///< The cached value for useSSLForSMTP.
|
bool useSSLForSMTP_ { false }; ///< The cached value for useSSLForSMTP.
|
||||||
|
QList<QString> badEventDisplayQueue_; ///< THe queue for displaying 'bad event feedback request dialog'.
|
||||||
|
|
||||||
friend class AppController;
|
friend class AppController;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -42,6 +42,7 @@
|
|||||||
<file>qml/icons/ic-other-mail-clients.svg</file>
|
<file>qml/icons/ic-other-mail-clients.svg</file>
|
||||||
<file>qml/icons/ic-plus.svg</file>
|
<file>qml/icons/ic-plus.svg</file>
|
||||||
<file>qml/icons/ic-question-circle.svg</file>
|
<file>qml/icons/ic-question-circle.svg</file>
|
||||||
|
<file>qml/icons/ic-splash-check.svg</file>
|
||||||
<file>qml/icons/ic-success.svg</file>
|
<file>qml/icons/ic-success.svg</file>
|
||||||
<file>qml/icons/ic-three-dots-vertical.svg</file>
|
<file>qml/icons/ic-three-dots-vertical.svg</file>
|
||||||
<file>qml/icons/ic-trash.svg</file>
|
<file>qml/icons/ic-trash.svg</file>
|
||||||
|
|||||||
@ -16,19 +16,129 @@
|
|||||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
#include "SentryUtils.h"
|
#include "SentryUtils.h"
|
||||||
|
#include "BuildConfig.h"
|
||||||
|
#include <bridgepp/BridgeUtils.h>
|
||||||
|
#include <bridgepp/Exception/Exception.h>
|
||||||
|
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QCryptographicHash>
|
||||||
|
#include <QString>
|
||||||
|
#include <QSysInfo>
|
||||||
|
|
||||||
|
|
||||||
static constexpr const char *LoggerName = "bridge-gui";
|
static constexpr const char *LoggerName = "bridge-gui";
|
||||||
|
|
||||||
|
|
||||||
void reportSentryEvent(sentry_level_t level, const char *message) {
|
//****************************************************************************************************************************************************
|
||||||
auto event = sentry_value_new_message_event(level, LoggerName, message);
|
/// \return The temporary file used for sentry attachment.
|
||||||
sentry_capture_event(event);
|
//****************************************************************************************************************************************************
|
||||||
|
QString sentryAttachmentFilePath() {
|
||||||
|
static QString path;
|
||||||
|
if (!path.isEmpty()) {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
while (true) {
|
||||||
|
path = QDir::temp().absoluteFilePath(QUuid::createUuid().toString(QUuid::WithoutBraces) + ".txt"); // Sentry does not offer preview for .log files.
|
||||||
|
if (!QFileInfo::exists(path)) {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void reportSentryException(sentry_level_t level, const char *message, const char *exceptionType, const char *exception) {
|
QByteArray getProtectedHostname() {
|
||||||
|
QByteArray hostname = QCryptographicHash::hash(QSysInfo::machineHostName().toUtf8(), QCryptographicHash::Sha256);
|
||||||
|
return hostname.toHex();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString getApiOS() {
|
||||||
|
#if defined(Q_OS_DARWIN)
|
||||||
|
return "macos";
|
||||||
|
#elif defined(Q_OS_WINDOWS)
|
||||||
|
return "windows";
|
||||||
|
#else
|
||||||
|
return "linux";
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
QString appVersion(const QString& version) {
|
||||||
|
return QString("%1-bridge@%2").arg(getApiOS()).arg(version);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setSentryReportScope() {
|
||||||
|
sentry_set_tag("OS", bridgepp::goos().toUtf8());
|
||||||
|
sentry_set_tag("Client", PROJECT_FULL_NAME);
|
||||||
|
sentry_set_tag("Version", PROJECT_REVISION);
|
||||||
|
sentry_set_tag("HostArch", QSysInfo::currentCpuArchitecture().toUtf8());
|
||||||
|
sentry_set_tag("server_name", getProtectedHostname());
|
||||||
|
sentry_value_t user = sentry_value_new_object();
|
||||||
|
sentry_value_set_by_key(user, "id", sentry_value_new_string(getProtectedHostname()));
|
||||||
|
sentry_set_user(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
sentry_options_t* newSentryOptions(const char *sentryDNS, const char *cacheDir) {
|
||||||
|
sentry_options_t *sentryOptions = sentry_options_new();
|
||||||
|
sentry_options_set_dsn(sentryOptions, sentryDNS);
|
||||||
|
sentry_options_set_database_path(sentryOptions, cacheDir);
|
||||||
|
sentry_options_set_release(sentryOptions, appVersion(PROJECT_VER).toUtf8());
|
||||||
|
sentry_options_set_max_breadcrumbs(sentryOptions, 50);
|
||||||
|
sentry_options_set_environment(sentryOptions, PROJECT_BUILD_ENV);
|
||||||
|
QByteArray const array = sentryAttachmentFilePath().toLocal8Bit();
|
||||||
|
sentry_options_add_attachment(sentryOptions, array.constData());
|
||||||
|
// Enable this for debugging sentry.
|
||||||
|
// sentry_options_set_debug(sentryOptions, 1);
|
||||||
|
|
||||||
|
return sentryOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
sentry_uuid_t reportSentryEvent(sentry_level_t level, const char *message) {
|
||||||
|
auto event = sentry_value_new_message_event(level, LoggerName, message);
|
||||||
|
return sentry_capture_event(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
sentry_uuid_t reportSentryException(sentry_level_t level, const char *message, const char *exceptionType, const char *exception) {
|
||||||
auto event = sentry_value_new_message_event(level, LoggerName, message);
|
auto event = sentry_value_new_message_event(level, LoggerName, message);
|
||||||
sentry_event_add_exception(event, sentry_value_new_exception(exceptionType, exception));
|
sentry_event_add_exception(event, sentry_value_new_exception(exceptionType, exception));
|
||||||
sentry_capture_event(event);
|
|
||||||
|
// reject exception content from the fingerprint if there is not enough content
|
||||||
|
if ( strlen(exception) < 5) {
|
||||||
|
sentry_value_t fingerprint = sentry_value_new_list();
|
||||||
|
sentry_value_append(fingerprint, sentry_value_new_string("level"));
|
||||||
|
sentry_value_append(fingerprint, sentry_value_new_string(message));
|
||||||
|
sentry_value_append(fingerprint, sentry_value_new_string(LoggerName));
|
||||||
|
sentry_value_set_by_key(event, "fingerprint", fingerprint);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sentry_capture_event(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \param[in] message The message for the exception.
|
||||||
|
/// \param[in] function The name of the function that triggered the exception.
|
||||||
|
/// \param[in] exception The exception.
|
||||||
|
/// \return The Sentry exception UUID.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
sentry_uuid_t reportSentryException(QString const &message, bridgepp::Exception const exception) {
|
||||||
|
QByteArray const attachment = exception.attachment();
|
||||||
|
QFile file(sentryAttachmentFilePath());
|
||||||
|
bool const hasAttachment = !attachment.isEmpty();
|
||||||
|
if (hasAttachment) {
|
||||||
|
if (file.open(QIODevice::Text | QIODevice::WriteOnly)) {
|
||||||
|
file.write(attachment);
|
||||||
|
file.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sentry_uuid_t const uuid = reportSentryException(SENTRY_LEVEL_ERROR, message.toLocal8Bit(), "Exception",
|
||||||
|
exception.detailedWhat().toLocal8Bit());
|
||||||
|
|
||||||
|
if (hasAttachment) {
|
||||||
|
file.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
return uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@ -22,7 +22,10 @@
|
|||||||
#include <sentry.h>
|
#include <sentry.h>
|
||||||
|
|
||||||
|
|
||||||
void reportSentryEvent(sentry_level_t level, const char *message);
|
void setSentryReportScope();
|
||||||
void reportSentryException(sentry_level_t level, const char *message, const char *exceptionType, const char *exception);
|
sentry_options_t* newSentryOptions(const char * sentryDNS, const char * cacheDir);
|
||||||
|
sentry_uuid_t reportSentryEvent(sentry_level_t level, const char *message);
|
||||||
|
sentry_uuid_t reportSentryException(QString const& message, bridgepp::Exception const exception);
|
||||||
|
|
||||||
|
|
||||||
#endif //BRIDGE_GUI_SENTRYUTILS_H
|
#endif //BRIDGE_GUI_SENTRYUTILS_H
|
||||||
|
|||||||
@ -161,6 +161,17 @@ User *UserList::get(int row) const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \param[in] userID The userID.
|
||||||
|
/// \return The primary email address (or if unknown the username) of the user.
|
||||||
|
/// \return An empty string if the user cannot be found.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
QString UserList::primaryEmailOrUsername(QString const &userID) const {
|
||||||
|
SPUser const user = this->getUserWithID(userID);
|
||||||
|
return user ? user->primaryEmailOrUsername() : QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \param[in] userID The userID.
|
/// \param[in] userID The userID.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
|
|||||||
@ -54,6 +54,7 @@ signals:
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
Q_INVOKABLE bridgepp::User *get(int row) const;
|
Q_INVOKABLE bridgepp::User *get(int row) const;
|
||||||
|
Q_INVOKABLE QString primaryEmailOrUsername(QString const& userID) const; ///< Return the primary email or username of a user
|
||||||
|
|
||||||
public slots: ///< handler for signals coming from the gRPC service
|
public slots: ///< handler for signals coming from the gRPC service
|
||||||
void onUserChanged(QString const &userID);
|
void onUserChanged(QString const &userID);
|
||||||
|
|||||||
@ -75,6 +75,16 @@ function check_exit() {
|
|||||||
|
|
||||||
Write-host "Running build for version $bridgeVersion - $buildConfig in $buildDir"
|
Write-host "Running build for version $bridgeVersion - $buildConfig in $buildDir"
|
||||||
|
|
||||||
|
$REVISION_HASH = git rev-parse --short=10 HEAD
|
||||||
|
$bridgeDsnSentry = ($env:BRIDGE_DSN_SENTRY)
|
||||||
|
$bridgeBuidTime = ($env:BRIDGE_BUILD_TIME)
|
||||||
|
|
||||||
|
$bridgeBuildEnv = ($env:BRIDGE_BUILD_ENV)
|
||||||
|
if ($null -eq $bridgeBuildEnv)
|
||||||
|
{
|
||||||
|
$bridgeBuildEnv = "dev"
|
||||||
|
}
|
||||||
|
|
||||||
git submodule update --init --recursive $vcpkgRoot
|
git submodule update --init --recursive $vcpkgRoot
|
||||||
. $vcpkgBootstrap -disableMetrics
|
. $vcpkgBootstrap -disableMetrics
|
||||||
. $vcpkgExe install sentry-native:x64-windows grpc:x64-windows --clean-after-build
|
. $vcpkgExe install sentry-native:x64-windows grpc:x64-windows --clean-after-build
|
||||||
@ -82,7 +92,11 @@ git submodule update --init --recursive $vcpkgRoot
|
|||||||
. $cmakeExe -G "Visual Studio 17 2022" -DCMAKE_BUILD_TYPE="$buildConfig" `
|
. $cmakeExe -G "Visual Studio 17 2022" -DCMAKE_BUILD_TYPE="$buildConfig" `
|
||||||
-DBRIDGE_APP_FULL_NAME="$bridgeFullName" `
|
-DBRIDGE_APP_FULL_NAME="$bridgeFullName" `
|
||||||
-DBRIDGE_VENDOR="$bridgeVendor" `
|
-DBRIDGE_VENDOR="$bridgeVendor" `
|
||||||
|
-DBRIDGE_REVISION="$REVISION_HASH" `
|
||||||
-DBRIDGE_APP_VERSION="$bridgeVersion" `
|
-DBRIDGE_APP_VERSION="$bridgeVersion" `
|
||||||
|
-DBRIDGE_BUILD_TIME="$bridgeBuidTime" `
|
||||||
|
-DBRIDGE_DSN_SENTRY="$bridgeDsnSentry" `
|
||||||
|
-DBRIDGE_BUILD_ENV="$bridgeBuildEnv" `
|
||||||
-S . -B $buildDir
|
-S . -B $buildDir
|
||||||
|
|
||||||
check_exit "CMake failed"
|
check_exit "CMake failed"
|
||||||
|
|||||||
@ -55,7 +55,10 @@ BRIDGE_VENDOR=${BRIDGE_VENDOR:-"Proton AG"}
|
|||||||
BUILD_CONFIG=${BRIDGE_GUI_BUILD_CONFIG:-Debug}
|
BUILD_CONFIG=${BRIDGE_GUI_BUILD_CONFIG:-Debug}
|
||||||
BUILD_DIR=$(echo "./cmake-build-${BUILD_CONFIG}" | tr '[:upper:]' '[:lower:]')
|
BUILD_DIR=$(echo "./cmake-build-${BUILD_CONFIG}" | tr '[:upper:]' '[:lower:]')
|
||||||
VCPKG_ROOT="${BRIDGE_REPO_ROOT}/extern/vcpkg"
|
VCPKG_ROOT="${BRIDGE_REPO_ROOT}/extern/vcpkg"
|
||||||
|
BRIDGE_REVISION=$(git rev-parse --short=10 HEAD)
|
||||||
|
BRIDGE_DSN_SENTRY=${BRIDGE_DSN_SENTRY}
|
||||||
|
BRIDGE_BUILD_TIME=${BRIDGE_BUILD_TIME}
|
||||||
|
BRIDGE_BUILD_ENV= ${BRIDGE_BUILD_ENV:-"dev"}
|
||||||
git submodule update --init --recursive ${VCPKG_ROOT}
|
git submodule update --init --recursive ${VCPKG_ROOT}
|
||||||
check_exit "Failed to initialize vcpkg as a submodule."
|
check_exit "Failed to initialize vcpkg as a submodule."
|
||||||
|
|
||||||
@ -93,6 +96,10 @@ cmake \
|
|||||||
-DCMAKE_BUILD_TYPE="${BUILD_CONFIG}" \
|
-DCMAKE_BUILD_TYPE="${BUILD_CONFIG}" \
|
||||||
-DBRIDGE_APP_FULL_NAME="${BRIDGE_APP_FULL_NAME}" \
|
-DBRIDGE_APP_FULL_NAME="${BRIDGE_APP_FULL_NAME}" \
|
||||||
-DBRIDGE_VENDOR="${BRIDGE_VENDOR}" \
|
-DBRIDGE_VENDOR="${BRIDGE_VENDOR}" \
|
||||||
|
-DBRIDGE_REVISION="${BRIDGE_REVISION}" \
|
||||||
|
-DBRIDGE_DSN_SENTRY="${BRIDGE_DSN_SENTRY}" \
|
||||||
|
-DBRIDGE_BRIDGE_TIME="${BRIDGE_BRIDGE_TIME}" \
|
||||||
|
-DBRIDGE_BUILD_ENV="${BRIDGE_BUILD_ENV}" \
|
||||||
-DBRIDGE_APP_VERSION="${BRIDGE_APP_VERSION}" "${BRIDGE_CMAKE_MACOS_OPTS}" \
|
-DBRIDGE_APP_VERSION="${BRIDGE_APP_VERSION}" "${BRIDGE_CMAKE_MACOS_OPTS}" \
|
||||||
-G Ninja \
|
-G Ninja \
|
||||||
-S . \
|
-S . \
|
||||||
|
|||||||
@ -21,14 +21,15 @@
|
|||||||
#include "CommandLine.h"
|
#include "CommandLine.h"
|
||||||
#include "QMLBackend.h"
|
#include "QMLBackend.h"
|
||||||
#include "SentryUtils.h"
|
#include "SentryUtils.h"
|
||||||
#include "Version.h"
|
#include "BuildConfig.h"
|
||||||
|
#include "LogUtils.h"
|
||||||
#include <bridgepp/BridgeUtils.h>
|
#include <bridgepp/BridgeUtils.h>
|
||||||
#include <bridgepp/Exception/Exception.h>
|
#include <bridgepp/Exception/Exception.h>
|
||||||
#include <bridgepp/FocusGRPC/FocusGRPCClient.h>
|
#include <bridgepp/FocusGRPC/FocusGRPCClient.h>
|
||||||
#include <bridgepp/Log/Log.h>
|
#include <bridgepp/Log/Log.h>
|
||||||
#include <bridgepp/ProcessMonitor.h>
|
#include <bridgepp/ProcessMonitor.h>
|
||||||
#include <sentry.h>
|
#include <sentry.h>
|
||||||
#include <project_sentry_config.h>
|
#include <SentryUtils.h>
|
||||||
|
|
||||||
|
|
||||||
#ifdef Q_OS_MACOS
|
#ifdef Q_OS_MACOS
|
||||||
@ -55,6 +56,7 @@ QString const bridgeLock = "bridge-v3.lock"; ///< The file name used for the bri
|
|||||||
QString const bridgeGUILock = "bridge-v3-gui.lock"; ///< The file name used for the bridge-gui lock file.
|
QString const bridgeGUILock = "bridge-v3-gui.lock"; ///< The file name used for the bridge-gui lock file.
|
||||||
QString const exeName = "bridge" + exeSuffix; ///< The bridge executable file name.*
|
QString const exeName = "bridge" + exeSuffix; ///< The bridge executable file name.*
|
||||||
qint64 const grpcServiceConfigWaitDelayMs = 180000; ///< The wait delay for the gRPC config file in milliseconds.
|
qint64 const grpcServiceConfigWaitDelayMs = 180000; ///< The wait delay for the gRPC config file in milliseconds.
|
||||||
|
QString const waitFlag = "--wait"; ///< The wait command-line flag.
|
||||||
|
|
||||||
|
|
||||||
} // anonymous namespace
|
} // anonymous namespace
|
||||||
@ -237,7 +239,8 @@ void focusOtherInstance() {
|
|||||||
}
|
}
|
||||||
catch (Exception const &e) {
|
catch (Exception const &e) {
|
||||||
app().log().error(e.qwhat());
|
app().log().error(e.qwhat());
|
||||||
reportSentryException(SENTRY_LEVEL_ERROR, "Exception occurred during focusOtherInstance()", "Exception", e.what());
|
auto uuid = reportSentryException("Exception occurred during focusOtherInstance()", e);
|
||||||
|
app().log().fatal(QString("reportID: %1 Captured exception: %2").arg(QByteArray(uuid.bytes, 16).toHex()).arg(e.qwhat()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -245,7 +248,7 @@ void focusOtherInstance() {
|
|||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \param [in] args list of arguments to pass to bridge.
|
/// \param [in] args list of arguments to pass to bridge.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
void launchBridge(QStringList const &args) {
|
const QString launchBridge(QStringList const &args) {
|
||||||
UPOverseer &overseer = app().bridgeOverseer();
|
UPOverseer &overseer = app().bridgeOverseer();
|
||||||
overseer.reset();
|
overseer.reset();
|
||||||
|
|
||||||
@ -262,6 +265,7 @@ void launchBridge(QStringList const &args) {
|
|||||||
app().log().info(QString("Launching bridge process with command \"%1\" %2").arg(bridgeExePath, params.join(" ")));
|
app().log().info(QString("Launching bridge process with command \"%1\" %2").arg(bridgeExePath, params.join(" ")));
|
||||||
overseer = std::make_unique<Overseer>(new ProcessMonitor(bridgeExePath, params, nullptr), nullptr);
|
overseer = std::make_unique<Overseer>(new ProcessMonitor(bridgeExePath, params, nullptr), nullptr);
|
||||||
overseer->startWorker(true);
|
overseer->startWorker(true);
|
||||||
|
return bridgeExePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -289,19 +293,12 @@ void closeBridgeApp() {
|
|||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
int main(int argc, char *argv[]) {
|
int main(int argc, char *argv[]) {
|
||||||
// Init sentry.
|
// Init sentry.
|
||||||
sentry_options_t *sentryOptions = sentry_options_new();
|
sentry_options_t *sentryOptions = newSentryOptions(PROJECT_DSN_SENTRY, sentryCacheDir().toStdString().c_str());
|
||||||
sentry_options_set_dsn(sentryOptions, SentryDNS);
|
|
||||||
{
|
|
||||||
const QString sentryCachePath = sentryCacheDir();
|
|
||||||
sentry_options_set_database_path(sentryOptions, sentryCachePath.toStdString().c_str());
|
|
||||||
}
|
|
||||||
sentry_options_set_release(sentryOptions, SentryProductID);
|
|
||||||
// Enable this for debugging sentry.
|
|
||||||
// sentry_options_set_debug(sentryOptions, 1);
|
|
||||||
if (sentry_init(sentryOptions) != 0) {
|
if (sentry_init(sentryOptions) != 0) {
|
||||||
std::cerr << "Failed to initialize sentry" << std::endl;
|
std::cerr << "Failed to initialize sentry" << std::endl;
|
||||||
}
|
}
|
||||||
|
setSentryReportScope();
|
||||||
auto sentryClose = qScopeGuard([] { sentry_close(); });
|
auto sentryClose = qScopeGuard([] { sentry_close(); });
|
||||||
|
|
||||||
// The application instance is needed to display system message boxes. As we may have to do it in the exception handler,
|
// The application instance is needed to display system message boxes. As we may have to do it in the exception handler,
|
||||||
@ -334,15 +331,16 @@ int main(int argc, char *argv[]) {
|
|||||||
// When not in attached mode, log entries are forwarded to bridge, which output it on stdout/stderr. bridge-gui's process monitor intercept
|
// When not in attached mode, log entries are forwarded to bridge, which output it on stdout/stderr. bridge-gui's process monitor intercept
|
||||||
// these outputs and output them on the command-line.
|
// these outputs and output them on the command-line.
|
||||||
log.setLevel(cliOptions.logLevel);
|
log.setLevel(cliOptions.logLevel);
|
||||||
|
QString bridgeexec;
|
||||||
if (!cliOptions.attach) {
|
if (!cliOptions.attach) {
|
||||||
if (isBridgeRunning()) {
|
if (isBridgeRunning()) {
|
||||||
throw Exception("An orphan instance of bridge is already running. Please terminate it and relaunch the application.");
|
throw Exception("An orphan instance of bridge is already running. Please terminate it and relaunch the application.",
|
||||||
|
QString(), QString(), tailOfLatestBridgeLog());
|
||||||
}
|
}
|
||||||
|
|
||||||
// before launching bridge, we remove any trailing service config file, because we need to make sure we get a newly generated one.
|
// before launching bridge, we remove any trailing service config file, because we need to make sure we get a newly generated one.
|
||||||
GRPCClient::removeServiceConfigFile();
|
GRPCClient::removeServiceConfigFile();
|
||||||
launchBridge(cliOptions.bridgeArgs);
|
bridgeexec = launchBridge(cliOptions.bridgeArgs);
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info(QString("Retrieving gRPC service configuration from '%1'").arg(QDir::toNativeSeparators(grpcServerConfigPath())));
|
log.info(QString("Retrieving gRPC service configuration from '%1'").arg(QDir::toNativeSeparators(grpcServerConfigPath())));
|
||||||
@ -364,6 +362,7 @@ int main(int argc, char *argv[]) {
|
|||||||
QQuickWindow::setSceneGraphBackend(cliOptions.useSoftwareRenderer ? "software" : "rhi");
|
QQuickWindow::setSceneGraphBackend(cliOptions.useSoftwareRenderer ? "software" : "rhi");
|
||||||
log.info(QString("Qt Quick renderer: %1").arg(QQuickWindow::sceneGraphBackend()));
|
log.info(QString("Qt Quick renderer: %1").arg(QQuickWindow::sceneGraphBackend()));
|
||||||
|
|
||||||
|
|
||||||
QQmlApplicationEngine engine;
|
QQmlApplicationEngine engine;
|
||||||
std::unique_ptr<QQmlComponent> rootComponent(createRootQmlComponent(engine));
|
std::unique_ptr<QQmlComponent> rootComponent(createRootQmlComponent(engine));
|
||||||
std::unique_ptr<QObject> rootObject(rootComponent->create(engine.rootContext()));
|
std::unique_ptr<QObject> rootObject(rootComponent->create(engine.rootContext()));
|
||||||
@ -398,7 +397,16 @@ int main(int argc, char *argv[]) {
|
|||||||
int result = 0;
|
int result = 0;
|
||||||
if (!startError) {
|
if (!startError) {
|
||||||
// we succeeded in launching bridge, so we can be set as mainExecutable.
|
// we succeeded in launching bridge, so we can be set as mainExecutable.
|
||||||
app().grpc().setMainExecutable(QString::fromLocal8Bit(argv[0]));
|
QString mainexec = QString::fromLocal8Bit(argv[0]);
|
||||||
|
app().grpc().setMainExecutable(mainexec);
|
||||||
|
QStringList args = cliOptions.bridgeGuiArgs;
|
||||||
|
args.append(waitFlag);
|
||||||
|
args.append(mainexec);
|
||||||
|
if (!bridgeexec.isEmpty()) {
|
||||||
|
args.append(waitFlag);
|
||||||
|
args.append(bridgeexec);
|
||||||
|
}
|
||||||
|
app().setLauncherArgs(cliOptions.launcher, args);
|
||||||
result = QGuiApplication::exec();
|
result = QGuiApplication::exec();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -420,9 +428,9 @@ int main(int argc, char *argv[]) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
catch (Exception const &e) {
|
catch (Exception const &e) {
|
||||||
reportSentryException(SENTRY_LEVEL_ERROR, "Exception occurred during main", "Exception", e.what());
|
sentry_uuid_s const uuid = reportSentryException("Exception occurred during main", e);
|
||||||
QMessageBox::critical(nullptr, "Error", e.qwhat());
|
QMessageBox::critical(nullptr, "Error", e.qwhat());
|
||||||
QTextStream(stderr) << e.qwhat() << "\n";
|
QTextStream(stderr) << "reportID: " << QByteArray(uuid.bytes, 16).toHex() << " Captured exception :" << e.detailedWhat() << "\n";
|
||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,11 +36,11 @@ Item {
|
|||||||
if (root.usedFraction < .75) return root.colorScheme.signal_warning
|
if (root.usedFraction < .75) return root.colorScheme.signal_warning
|
||||||
return root.colorScheme.signal_danger
|
return root.colorScheme.signal_danger
|
||||||
}
|
}
|
||||||
property real usedFraction: root.user ? reasonableFracion(root.user.usedBytes, root.user.totalBytes) : 0
|
property real usedFraction: root.user ? reasonableFraction(root.user.usedBytes, root.user.totalBytes) : 0
|
||||||
property string totalSpace: root.spaceWithUnits(root.user ? root.reasonableBytes(root.user.totalBytes) : 0)
|
property string totalSpace: root.spaceWithUnits(root.user ? root.reasonableBytes(root.user.totalBytes) : 0)
|
||||||
property string usedSpace: root.spaceWithUnits(root.user ? root.reasonableBytes(root.user.usedBytes) : 0)
|
property string usedSpace: root.spaceWithUnits(root.user ? root.reasonableBytes(root.user.usedBytes) : 0)
|
||||||
|
|
||||||
function reasonableFracion(used, total){
|
function reasonableFraction(used, total){
|
||||||
var usedSafe = root.reasonableBytes(used)
|
var usedSafe = root.reasonableBytes(used)
|
||||||
var totalSafe = root.reasonableBytes(total)
|
var totalSafe = root.reasonableBytes(total)
|
||||||
if (totalSafe == 0 || usedSafe == 0) return 0
|
if (totalSafe == 0 || usedSafe == 0) return 0
|
||||||
@ -63,6 +63,10 @@ Item {
|
|||||||
return Math.round(bytes*10 / Math.pow(1024, i))/10 + " " + units[i]
|
return Math.round(bytes*10 / Math.pow(1024, i))/10 + " " + units[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function primaryEmail() {
|
||||||
|
return root.user ? root.user.primaryEmailOrUsername() : ""
|
||||||
|
}
|
||||||
|
|
||||||
// width expected to be set by parent object
|
// width expected to be set by parent object
|
||||||
implicitHeight : children[0].implicitHeight
|
implicitHeight : children[0].implicitHeight
|
||||||
|
|
||||||
@ -77,7 +81,7 @@ Item {
|
|||||||
anchors {
|
anchors {
|
||||||
top: root.top
|
top: root.top
|
||||||
left: root.left
|
left: root.left
|
||||||
right: root.rigth
|
right: root.right
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
@ -115,12 +119,10 @@ Item {
|
|||||||
spacing: 0
|
spacing: 0
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
Layout.maximumWidth: root.width - (
|
id: labelEmail
|
||||||
root._spacing + avatar.width
|
Layout.maximumWidth: root.width - (root._spacing + avatar.width)
|
||||||
)
|
|
||||||
|
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
text: root.user ? user.username : ""
|
text: primaryEmail()
|
||||||
type: {
|
type: {
|
||||||
switch (root.type) {
|
switch (root.type) {
|
||||||
case AccountDelegate.SmallView: return Label.Body
|
case AccountDelegate.SmallView: return Label.Body
|
||||||
@ -128,6 +130,29 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
elide: Text.ElideMiddle
|
elide: Text.ElideMiddle
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: labelArea
|
||||||
|
anchors.fill:parent
|
||||||
|
hoverEnabled: true
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolTip {
|
||||||
|
id: toolTipEmail
|
||||||
|
visible: labelArea.containsMouse && labelEmail.truncated
|
||||||
|
text: primaryEmail()
|
||||||
|
delay: 1000
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
border.color: root.colorScheme.background_strong
|
||||||
|
color: root.colorScheme.background_norm
|
||||||
|
}
|
||||||
|
|
||||||
|
contentItem: Text {
|
||||||
|
color: root.colorScheme.text_norm
|
||||||
|
text: toolTipEmail.text
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Item { implicitHeight: root.type == AccountDelegate.LargeView ? 6 * ProtonStyle.px : 0 }
|
Item { implicitHeight: root.type == AccountDelegate.LargeView ? 6 * ProtonStyle.px : 0 }
|
||||||
@ -155,7 +180,7 @@ Item {
|
|||||||
property string dots: ""
|
property string dots: ""
|
||||||
interval: 250;
|
interval: 250;
|
||||||
repeat: true;
|
repeat: true;
|
||||||
running: root.user && (root.user.state === EUserState.Locked)
|
running: (root.user != null) && (root.user.state === EUserState.Locked)
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
dots = dots + "."
|
dots = dots + "."
|
||||||
if (dots.length > 3)
|
if (dots.length > 3)
|
||||||
|
|||||||
@ -87,8 +87,8 @@ QtObject {
|
|||||||
mainWindow.showAndRise()
|
mainWindow.showAndRise()
|
||||||
}
|
}
|
||||||
|
|
||||||
onShowSignIn: {
|
onSelectUser: function(userID) {
|
||||||
mainWindow.showSignIn(username)
|
mainWindow.selectUser(userID)
|
||||||
mainWindow.showAndRise()
|
mainWindow.showAndRise()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,7 +117,7 @@ QtObject {
|
|||||||
// fit above
|
// fit above
|
||||||
_y = iconRect.top - height
|
_y = iconRect.top - height
|
||||||
if (isInInterval(_y, screenRect.top, screenRect.bottom - height)) {
|
if (isInInterval(_y, screenRect.top, screenRect.bottom - height)) {
|
||||||
// position preferebly in the horizontal center but bound to the screen rect
|
// position preferably in the horizontal center but bound to the screen rect
|
||||||
_x = bound(iconRect.left + (iconRect.width - width)/2, screenRect.left, screenRect.right - width)
|
_x = bound(iconRect.left + (iconRect.width - width)/2, screenRect.left, screenRect.right - width)
|
||||||
return Qt.point(_x, _y)
|
return Qt.point(_x, _y)
|
||||||
}
|
}
|
||||||
@ -125,7 +125,7 @@ QtObject {
|
|||||||
// fit below
|
// fit below
|
||||||
_y = iconRect.bottom
|
_y = iconRect.bottom
|
||||||
if (isInInterval(_y, screenRect.top, screenRect.bottom - height)) {
|
if (isInInterval(_y, screenRect.top, screenRect.bottom - height)) {
|
||||||
// position preferebly in the horizontal center but bound to the screen rect
|
// position preferably in the horizontal center but bound to the screen rect
|
||||||
_x = bound(iconRect.left + (iconRect.width - width)/2, screenRect.left, screenRect.right - width)
|
_x = bound(iconRect.left + (iconRect.width - width)/2, screenRect.left, screenRect.right - width)
|
||||||
return Qt.point(_x, _y)
|
return Qt.point(_x, _y)
|
||||||
}
|
}
|
||||||
@ -133,7 +133,7 @@ QtObject {
|
|||||||
// fit to the left
|
// fit to the left
|
||||||
_x = iconRect.left - width
|
_x = iconRect.left - width
|
||||||
if (isInInterval(_x, screenRect.left, screenRect.right - width)) {
|
if (isInInterval(_x, screenRect.left, screenRect.right - width)) {
|
||||||
// position preferebly in the vertical center but bound to the screen rect
|
// position preferably in the vertical center but bound to the screen rect
|
||||||
_y = bound(iconRect.top + (iconRect.height - height)/2, screenRect.top, screenRect.bottom - height)
|
_y = bound(iconRect.top + (iconRect.height - height)/2, screenRect.top, screenRect.bottom - height)
|
||||||
return Qt.point(_x, _y)
|
return Qt.point(_x, _y)
|
||||||
}
|
}
|
||||||
@ -141,12 +141,12 @@ QtObject {
|
|||||||
// fit to the right
|
// fit to the right
|
||||||
_x = iconRect.right
|
_x = iconRect.right
|
||||||
if (isInInterval(_x, screenRect.left, screenRect.right - width)) {
|
if (isInInterval(_x, screenRect.left, screenRect.right - width)) {
|
||||||
// position preferebly in the vertical center but bound to the screen rect
|
// position preferably in the vertical center but bound to the screen rect
|
||||||
_y = bound(iconRect.top + (iconRect.height - height)/2, screenRect.top, screenRect.bottom - height)
|
_y = bound(iconRect.top + (iconRect.height - height)/2, screenRect.top, screenRect.bottom - height)
|
||||||
return Qt.point(_x, _y)
|
return Qt.point(_x, _y)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: position satatus window right above icon and let window manager decide.
|
// Fallback: position status window right above icon and let window manager decide.
|
||||||
console.warn("Can't position status window: screenRect =", screenRect, "iconRect =", iconRect)
|
console.warn("Can't position status window: screenRect =", screenRect, "iconRect =", iconRect)
|
||||||
_x = bound(iconRect.left + (iconRect.width - width)/2, screenRect.left, screenRect.right - width)
|
_x = bound(iconRect.left + (iconRect.width - width)/2, screenRect.left, screenRect.right - width)
|
||||||
_y = bound(iconRect.top + (iconRect.height - height)/2, screenRect.top, screenRect.bottom - height)
|
_y = bound(iconRect.top + (iconRect.height - height)/2, screenRect.top, screenRect.bottom - height)
|
||||||
@ -281,11 +281,12 @@ QtObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Backend.showOnStartup) {
|
Backend.guiReady()
|
||||||
|
|
||||||
|
if (Backend.showOnStartup || Backend.showSplashScreen) {
|
||||||
mainWindow.showAndRise()
|
mainWindow.showAndRise()
|
||||||
}
|
}
|
||||||
|
|
||||||
Backend.guiReady()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setColorScheme() {
|
function setColorScheme() {
|
||||||
|
|||||||
@ -299,7 +299,7 @@ ColumnLayout {
|
|||||||
Button { colorScheme: root.colorScheme; text: "Toggle Finished"; onClicked: {user.toggleSplitModeFinished()}}
|
Button { colorScheme: root.colorScheme; text: "Toggle Finished"; onClicked: {user.toggleSplitModeFinished()}}
|
||||||
}
|
}
|
||||||
|
|
||||||
TextArea { // TODO: this is causing binding loop on imlicitWidth
|
TextArea { // TODO: this is causing binding loop on implicitWidth
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
text: user && user.addresses ? user.addresses.join("\n") : "user@protonmail.com"
|
text: user && user.addresses ? user.addresses.join("\n") : "user@protonmail.com"
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|||||||
@ -178,7 +178,7 @@ Window {
|
|||||||
signal toggleSplitModeFinished()
|
signal toggleSplitModeFinished()
|
||||||
|
|
||||||
function configureAppleMail(address){
|
function configureAppleMail(address){
|
||||||
userSignal("confugure apple mail "+address)
|
userSignal("configure apple mail "+address)
|
||||||
}
|
}
|
||||||
|
|
||||||
function logout(){
|
function logout(){
|
||||||
|
|||||||
@ -170,6 +170,10 @@ SettingsView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setDescription(message) {
|
||||||
|
description.text = message
|
||||||
|
}
|
||||||
|
|
||||||
function setDefaultValue() {
|
function setDefaultValue() {
|
||||||
description.text = ""
|
description.text = ""
|
||||||
address.text = root.selectedAddress
|
address.text = root.selectedAddress
|
||||||
|
|||||||
@ -188,7 +188,7 @@ Item {
|
|||||||
if (user.state !== EUserState.SignedOut) {
|
if (user.state !== EUserState.SignedOut) {
|
||||||
rightContent.showAccount()
|
rightContent.showAccount()
|
||||||
} else {
|
} else {
|
||||||
signIn.username = user.username
|
signIn.username = user.primaryEmailOrUsername()
|
||||||
rightContent.showSignIn()
|
rightContent.showSignIn()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -255,7 +255,8 @@ Item {
|
|||||||
return Backend.users.get(accounts.currentIndex)
|
return Backend.users.get(accounts.currentIndex)
|
||||||
}
|
}
|
||||||
onShowSignIn: {
|
onShowSignIn: {
|
||||||
signIn.username = this.user.username
|
var user = this.user
|
||||||
|
signIn.username = user ? user.primaryEmailOrUsername() : ""
|
||||||
rightContent.showSignIn()
|
rightContent.showSignIn()
|
||||||
}
|
}
|
||||||
onShowSetupGuide: function(user, address) {
|
onShowSetupGuide: function(user, address) {
|
||||||
@ -347,6 +348,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
BugReportView { // 8
|
BugReportView { // 8
|
||||||
|
id: bugReport
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
selectedAddress: {
|
selectedAddress: {
|
||||||
if (accounts.currentIndex < 0) return ""
|
if (accounts.currentIndex < 0) return ""
|
||||||
@ -398,4 +400,24 @@ Item {
|
|||||||
signIn.username = username
|
signIn.username = username
|
||||||
rightContent.showSignIn()
|
rightContent.showSignIn()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function selectUser(userID) {
|
||||||
|
var users = Backend.users;
|
||||||
|
for (var i = 0; i < users.count; i++) {
|
||||||
|
var user = users.get(i)
|
||||||
|
if (user.id !== userID) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
accounts.currentIndex = i;
|
||||||
|
if (user.state === EUserState.SignedOut)
|
||||||
|
showSignIn(user.primaryEmailOrUsername())
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.error("User with ID ", userID, " was not found in the account list")
|
||||||
|
}
|
||||||
|
|
||||||
|
function showBugReportAndPrefill(description) {
|
||||||
|
rightContent.showBugReport()
|
||||||
|
bugReport.setDescription(description)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -93,7 +93,7 @@ SettingsView {
|
|||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
// fill height so the footer label will be allways attached to the bottom
|
// fill height so the footer label will be always attached to the bottom
|
||||||
Item {
|
Item {
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
@ -108,9 +108,10 @@ SettingsView {
|
|||||||
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
|
||||||
text: qsTr("%1 v%2<br>© 2021 %3<br>%4 %5<br>%6").
|
text: qsTr("%1 v%2<br>© 2017-%3 %4<br>%5 %6<br>%7").
|
||||||
arg(Backend.appname).
|
arg(Backend.appname).
|
||||||
arg(Backend.version).
|
arg(Backend.version).
|
||||||
|
arg(Backend.buildYear()).
|
||||||
arg(Backend.vendor).
|
arg(Backend.vendor).
|
||||||
arg(link(Backend.licensePath, qsTr("License"))).
|
arg(link(Backend.licensePath, qsTr("License"))).
|
||||||
arg(link(Backend.dependencyLicensesLink, qsTr("Dependencies"))).
|
arg(link(Backend.dependencyLicensesLink, qsTr("Dependencies"))).
|
||||||
|
|||||||
@ -86,6 +86,10 @@ ApplicationWindow {
|
|||||||
root.showAndRise()
|
root.showAndRise()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onSelectUser(userID) {
|
||||||
|
root.selectUser(userID)
|
||||||
|
}
|
||||||
|
|
||||||
function onLoginFinished(index, wasSignedOut) {
|
function onLoginFinished(index, wasSignedOut) {
|
||||||
var user = Backend.users.get(index)
|
var user = Backend.users.get(index)
|
||||||
if (user && !wasSignedOut) {
|
if (user && !wasSignedOut) {
|
||||||
@ -116,7 +120,7 @@ ApplicationWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ((Backend.users.count === 1) && (u.state === EUserState.SignedOut)) {
|
if ((Backend.users.count === 1) && (u.state === EUserState.SignedOut)) {
|
||||||
showSignIn(u.username)
|
showSignIn(u.primaryEmailOrUsername())
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -165,7 +169,6 @@ ApplicationWindow {
|
|||||||
root.showSetup(null,"")
|
root.showSetup(null,"")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationPopups {
|
NotificationPopups {
|
||||||
@ -182,6 +185,11 @@ ApplicationWindow {
|
|||||||
function showLocalCacheSettings() { contentWrapper.showLocalCacheSettings() }
|
function showLocalCacheSettings() { contentWrapper.showLocalCacheSettings() }
|
||||||
function showSettings() { contentWrapper.showSettings() }
|
function showSettings() { contentWrapper.showSettings() }
|
||||||
function showHelp() { contentWrapper.showHelp() }
|
function showHelp() { contentWrapper.showHelp() }
|
||||||
|
function selectUser(userID) { contentWrapper.selectUser(userID) }
|
||||||
|
|
||||||
|
function showBugReportAndPrefill(message) {
|
||||||
|
contentWrapper.showBugReportAndPrefill(message)
|
||||||
|
}
|
||||||
|
|
||||||
function showSignIn(username) {
|
function showSignIn(username) {
|
||||||
if (contentLayout.currentIndex == 1) return
|
if (contentLayout.currentIndex == 1) return
|
||||||
|
|||||||
@ -129,6 +129,11 @@ Item {
|
|||||||
notification: root.notifications.noActiveKeyForRecipient
|
notification: root.notifications.noActiveKeyForRecipient
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NotificationDialog {
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
notification: root.notifications.userBadEvent
|
||||||
|
}
|
||||||
|
|
||||||
NotificationDialog {
|
NotificationDialog {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
notification: root.notifications.genericError
|
notification: root.notifications.genericError
|
||||||
|
|||||||
@ -80,6 +80,7 @@ QtObject {
|
|||||||
root.addressChanged,
|
root.addressChanged,
|
||||||
root.apiCertIssue,
|
root.apiCertIssue,
|
||||||
root.noActiveKeyForRecipient,
|
root.noActiveKeyForRecipient,
|
||||||
|
root.userBadEvent,
|
||||||
root.genericError
|
root.genericError
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -1045,8 +1046,8 @@ QtObject {
|
|||||||
property Notification apiCertIssue: Notification {
|
property Notification apiCertIssue: Notification {
|
||||||
title: qsTr("Unable to establish a \nsecure connection to \nProton servers")
|
title: qsTr("Unable to establish a \nsecure connection to \nProton servers")
|
||||||
description: qsTr("Bridge cannot verify the authenticity of Proton servers on your current network due to a TLS certificate error. " +
|
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 " +
|
"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>.")
|
"<a href=\"https://proton.me/blog/tls-ssl-certificate#Extra-security-precautions-taken-by-ProtonMail\">here</a>.")
|
||||||
|
|
||||||
brief: title
|
brief: title
|
||||||
icon: "./icons/ic-exclamation-circle-filled.svg"
|
icon: "./icons/ic-exclamation-circle-filled.svg"
|
||||||
@ -1085,7 +1086,7 @@ QtObject {
|
|||||||
|
|
||||||
function onNoActiveKeyForRecipient(email) {
|
function onNoActiveKeyForRecipient(email) {
|
||||||
root.noActiveKeyForRecipient.description = qsTr("There are no active keys to encrypt your message to %1. "+
|
root.noActiveKeyForRecipient.description = qsTr("There are no active keys to encrypt your message to %1. "+
|
||||||
"Please update the setting for this contact.").arg(email)
|
"Please update the setting for this contact.").arg(email)
|
||||||
root.noActiveKeyForRecipient.active = true
|
root.noActiveKeyForRecipient.active = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1101,20 +1102,61 @@ QtObject {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
property Notification userBadEvent: Notification {
|
||||||
|
title: qsTr("Internal error")
|
||||||
|
brief: title
|
||||||
|
description: "#PlaceHolderText"
|
||||||
|
icon: "./icons/ic-exclamation-circle-filled.svg"
|
||||||
|
type: Notification.NotificationType.Danger
|
||||||
|
group: Notifications.Group.Connection | Notifications.Group.Dialogs
|
||||||
|
|
||||||
|
property var userID: ""
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: Backend
|
||||||
|
function onUserBadEvent(userID, errorMessage) {
|
||||||
|
root.userBadEvent.userID = userID
|
||||||
|
root.userBadEvent.description = errorMessage
|
||||||
|
root.userBadEvent.active = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
action: [
|
||||||
|
Action {
|
||||||
|
text: qsTr("Synchronize")
|
||||||
|
|
||||||
|
onTriggered: {
|
||||||
|
root.userBadEvent.active = false
|
||||||
|
Backend.sendBadEventUserFeedback(root.userBadEvent.userID, true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
Action {
|
||||||
|
text: qsTr("Logout")
|
||||||
|
|
||||||
|
onTriggered: {
|
||||||
|
root.userBadEvent.active = false
|
||||||
|
Backend.sendBadEventUserFeedback(root.userBadEvent.userID, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
property Notification genericError: Notification {
|
property Notification genericError: Notification {
|
||||||
title: "#PlaceholderText#"
|
title: "#PlaceholderText#"
|
||||||
description: "#PlaceholderText#"
|
description: "#PlaceholderText#"
|
||||||
icon: "./icons/ic-exclamation-circle-filled.svg"
|
icon: "./icons/ic-exclamation-circle-filled.svg"
|
||||||
type: Notification.NotificationType.Danger
|
type: Notification.NotificationType.Danger
|
||||||
group: Notifications.Group.Dialogs
|
group: Notifications.Group.Dialogs
|
||||||
Connections {
|
Connections {
|
||||||
target: Backend
|
target: Backend
|
||||||
function onGenericError(title, description) {
|
function onGenericError(title, description) {
|
||||||
root.genericError.title = title
|
root.genericError.title = title
|
||||||
root.genericError.description = description
|
root.genericError.description = description
|
||||||
root.genericError.active = true;
|
root.genericError.active = true;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
action: [
|
action: [
|
||||||
Action {
|
Action {
|
||||||
|
|||||||
@ -28,7 +28,7 @@ T.Button {
|
|||||||
property alias secondary: control.flat
|
property alias secondary: control.flat
|
||||||
readonly property bool primary: !secondary
|
readonly property bool primary: !secondary
|
||||||
readonly property bool isIcon: control.text === ""
|
readonly property bool isIcon: control.text === ""
|
||||||
|
readonly property bool hasTextAndIcon: (control.text !== "") && (iconImage.source.toString().length > 0)
|
||||||
property bool loading: false
|
property bool loading: false
|
||||||
|
|
||||||
property bool borderless: false
|
property bool borderless: false
|
||||||
@ -67,7 +67,7 @@ T.Button {
|
|||||||
|
|
||||||
contentItem: RowLayout {
|
contentItem: RowLayout {
|
||||||
id: _contentItem
|
id: _contentItem
|
||||||
spacing: control.spacing
|
spacing: control.hasTextAndIcon ? control.spacing : 0
|
||||||
|
|
||||||
Proton.Label {
|
Proton.Label {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
|
|||||||
@ -341,7 +341,7 @@ QtObject {
|
|||||||
case "windows":
|
case "windows":
|
||||||
return "Segoe UI"
|
return "Segoe UI"
|
||||||
case "osx":
|
case "osx":
|
||||||
return ".AppleSystemUIFont" // should be SF Pro for the foreseeable future. Using "SF Pro Display" direcly here is not allowed by the font's license.
|
return ".AppleSystemUIFont" // should be SF Pro for the foreseeable future. Using "SF Pro Display" directly here is not allowed by the font's license.
|
||||||
case "linux":
|
case "linux":
|
||||||
return "Ubuntu"
|
return "Ubuntu"
|
||||||
default:
|
default:
|
||||||
|
|||||||
@ -204,7 +204,7 @@ FocusScope {
|
|||||||
TextField {
|
TextField {
|
||||||
colorScheme: root.colorScheme
|
colorScheme: root.colorScheme
|
||||||
id: usernameTextField
|
id: usernameTextField
|
||||||
label: qsTr("Username or email")
|
label: qsTr("Email or username")
|
||||||
focus: true
|
focus: true
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.topMargin: 24
|
Layout.topMargin: 24
|
||||||
@ -221,7 +221,7 @@ FocusScope {
|
|||||||
|
|
||||||
validator: function(str) {
|
validator: function(str) {
|
||||||
if (str.length === 0) {
|
if (str.length === 0) {
|
||||||
return qsTr("Enter username or email")
|
return qsTr("Enter email or username")
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,11 +39,11 @@ Dialog {
|
|||||||
Image {
|
Image {
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
|
||||||
sourceSize.width: 400
|
sourceSize.width: 384
|
||||||
sourceSize.height: 225
|
sourceSize.height: 144
|
||||||
|
|
||||||
Layout.preferredWidth: 400
|
Layout.preferredWidth: 384
|
||||||
Layout.preferredHeight: 225
|
Layout.preferredHeight: 144
|
||||||
|
|
||||||
source: "./icons/img-splash.png"
|
source: "./icons/img-splash.png"
|
||||||
}
|
}
|
||||||
@ -58,27 +58,110 @@ Dialog {
|
|||||||
|
|
||||||
type: Label.Title
|
type: Label.Title
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
text: qsTr("Updated Proton, unified protection")
|
text: qsTr("What's new in Bridge")
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
RowLayout {
|
||||||
colorScheme: root.colorScheme
|
width: root.width
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Item {
|
||||||
Layout.alignment: Qt.AlignHCenter;
|
Layout.fillHeight: true
|
||||||
Layout.preferredWidth: 336
|
width: 24
|
||||||
Layout.leftMargin: 24
|
Layout.leftMargin: 32
|
||||||
Layout.rightMargin: 24
|
Layout.rightMargin: 16
|
||||||
wrapMode: Text.WordWrap
|
Image {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
sourceSize.width: 24
|
||||||
|
sourceSize.height: 24
|
||||||
|
source: "./icons/ic-splash-check.svg"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type: Label.Body
|
Label {
|
||||||
horizontalAlignment: Text.AlignHCenter
|
colorScheme: root.colorScheme
|
||||||
textFormat: Text.StyledText
|
|
||||||
text: qsTr("Introducing Proton’s refreshed look.<br/>") +
|
|
||||||
qsTr("Many services, one mission. Welcome to an Internet where privacy is the default. ") +
|
|
||||||
link("https://proton.me/news/updated-proton",qsTr("Learn More"))
|
|
||||||
|
|
||||||
onLinkActivated: Qt.openUrlExternally(link)
|
Layout.fillWidth: true
|
||||||
|
Layout.alignment: Qt.AlignHCenter;
|
||||||
|
Layout.preferredWidth: 264
|
||||||
|
Layout.leftMargin: 0
|
||||||
|
Layout.rightMargin: 24
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
|
||||||
|
type: Label.Body
|
||||||
|
horizontalAlignment: Text.AlignLeft
|
||||||
|
textFormat: Text.StyledText
|
||||||
|
text: qsTr("<b>New IMAP engine</b><br/>For improved stability and performance.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
width: root.width
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillHeight: true
|
||||||
|
width: 24
|
||||||
|
Layout.leftMargin: 32
|
||||||
|
Layout.rightMargin: 16
|
||||||
|
Image {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
sourceSize.width: 24
|
||||||
|
sourceSize.height: 24
|
||||||
|
source: "./icons/ic-splash-check.svg"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.alignment: Qt.AlignHCenter;
|
||||||
|
Layout.preferredWidth: 264
|
||||||
|
Layout.leftMargin: 0
|
||||||
|
Layout.rightMargin: 24
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
|
||||||
|
type: Label.Body
|
||||||
|
horizontalAlignment: Text.AlignLeft
|
||||||
|
textFormat: Text.StyledText
|
||||||
|
text: qsTr("<b>Faster than ever</b><br/>Up to 10x faster syncing and receiving.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
width: root.width
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillHeight: true
|
||||||
|
width: 24
|
||||||
|
Layout.leftMargin: 32
|
||||||
|
Layout.rightMargin: 16
|
||||||
|
Image {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
sourceSize.width: 24
|
||||||
|
sourceSize.height: 24
|
||||||
|
source: "./icons/ic-splash-check.svg"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Label {
|
||||||
|
colorScheme: root.colorScheme
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.alignment: Qt.AlignHCenter;
|
||||||
|
Layout.preferredWidth: 264
|
||||||
|
Layout.leftMargin: 0
|
||||||
|
Layout.rightMargin: 24
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
|
||||||
|
type: Label.Body
|
||||||
|
horizontalAlignment: Text.AlignLeft
|
||||||
|
textFormat: Text.StyledText
|
||||||
|
text: qsTr("<b>Extra security</b><br/>New, encrypted local database and keychain improvements.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
@ -90,16 +173,21 @@ Dialog {
|
|||||||
onClicked: Backend.showSplashScreen = false
|
onClicked: Backend.showSplashScreen = false
|
||||||
}
|
}
|
||||||
|
|
||||||
Image {
|
Label {
|
||||||
Layout.alignment: Qt.AlignHCenter
|
colorScheme: root.colorScheme
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.alignment: Qt.AlignHCenter;
|
||||||
|
Layout.preferredWidth: 336
|
||||||
|
Layout.leftMargin: 24
|
||||||
|
Layout.rightMargin: 24
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
|
||||||
sourceSize.width: 164
|
type: Label.Body
|
||||||
sourceSize.height: 32
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
textFormat: Text.StyledText
|
||||||
|
text: qsTr("Note that your client will redownload all the emails.<br/>") + link("https://proton.me/blog/new-proton-mail-bridge", qsTr("Learn more about new Bridge."))
|
||||||
|
|
||||||
Layout.preferredWidth: 164
|
onLinkActivated: function(link) { Qt.openUrlExternally(link) }
|
||||||
Layout.preferredHeight: 32
|
|
||||||
|
|
||||||
source: "/qml/icons/img-proton-logos.svg"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,7 +40,7 @@ Window {
|
|||||||
signal showMainWindow()
|
signal showMainWindow()
|
||||||
signal showHelp()
|
signal showHelp()
|
||||||
signal showSettings()
|
signal showSettings()
|
||||||
signal showSignIn(string username)
|
signal selectUser(string userID)
|
||||||
signal quit()
|
signal quit()
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
@ -229,7 +229,7 @@ Window {
|
|||||||
visible: viewItem.user ? (viewItem.user.state === EUserState.SignedOut) : false
|
visible: viewItem.user ? (viewItem.user.state === EUserState.SignedOut) : false
|
||||||
text: qsTr("Sign in")
|
text: qsTr("Sign in")
|
||||||
onClicked: {
|
onClicked: {
|
||||||
root.showSignIn(viewItem.username)
|
root.selectUser(viewItem.user.id) // selectUser will show login screen if user is in SignedOut state.
|
||||||
root.close()
|
root.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||||
|
<g transform="matrix(1.21508,0,0,1.21508,-1.65989,-2.06087)">
|
||||||
|
<path d="M2,8.5L2.7,7.8L6.05,11.14L13.2,4L13.9,4.7L6.05,12.56L2,8.5Z" style="fill:rgb(101,126,228);"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 636 B |
Binary file not shown.
|
Before Width: | Height: | Size: 937 KiB After Width: | Height: | Size: 127 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 213 KiB After Width: | Height: | Size: 122 KiB |
@ -97,7 +97,7 @@ ColumnLayout {
|
|||||||
placeholderText: "Type 42 here"
|
placeholderText: "Type 42 here"
|
||||||
label: "42 Validator"
|
label: "42 Validator"
|
||||||
hint: "Accepts only \"42\""
|
hint: "Accepts only \"42\""
|
||||||
assistiveText: "Type sometihng here, preferably 42"
|
assistiveText: "Type something here, preferably 42"
|
||||||
|
|
||||||
wrapMode: TextInput.Wrap
|
wrapMode: TextInput.Wrap
|
||||||
|
|
||||||
|
|||||||
@ -149,7 +149,7 @@ RowLayout {
|
|||||||
placeholderText: "Type 42 here"
|
placeholderText: "Type 42 here"
|
||||||
label: "42 Validator"
|
label: "42 Validator"
|
||||||
hint: "Accepts only \"42\""
|
hint: "Accepts only \"42\""
|
||||||
assistiveText: "Type sometihng here, preferably 42"
|
assistiveText: "Type something here, preferably 42"
|
||||||
|
|
||||||
validator: function(str) {
|
validator: function(str) {
|
||||||
if (str === "42") {
|
if (str === "42") {
|
||||||
|
|||||||
@ -71,20 +71,20 @@ std::mt19937_64 &rng() {
|
|||||||
QString userConfigDir() {
|
QString userConfigDir() {
|
||||||
QString dir;
|
QString dir;
|
||||||
#ifdef Q_OS_WIN
|
#ifdef Q_OS_WIN
|
||||||
dir = qgetenv ("AppData");
|
dir = qEnvironmentVariable("AppData");
|
||||||
if (dir.isEmpty())
|
if (dir.isEmpty())
|
||||||
throw Exception("%AppData% is not defined.");
|
throw Exception("%AppData% is not defined.");
|
||||||
#elif defined(Q_OS_IOS) || defined(Q_OS_DARWIN)
|
#elif defined(Q_OS_IOS) || defined(Q_OS_DARWIN)
|
||||||
dir = qgetenv("HOME");
|
dir = qEnvironmentVariable("HOME");
|
||||||
if (dir.isEmpty()) {
|
if (dir.isEmpty()) {
|
||||||
throw Exception("$HOME is not defined.");
|
throw Exception("$HOME is not defined.");
|
||||||
}
|
}
|
||||||
dir += "/Library/Application Support";
|
dir += "/Library/Application Support";
|
||||||
#else
|
#else
|
||||||
dir = qgetenv ("XDG_CONFIG_HOME");
|
dir = qEnvironmentVariable("XDG_CONFIG_HOME");
|
||||||
if (dir.isEmpty())
|
if (dir.isEmpty())
|
||||||
{
|
{
|
||||||
dir = qgetenv ("HOME");
|
dir = qEnvironmentVariable("HOME");
|
||||||
if (dir.isEmpty())
|
if (dir.isEmpty())
|
||||||
throw Exception("neither $XDG_CONFIG_HOME nor $HOME are defined");
|
throw Exception("neither $XDG_CONFIG_HOME nor $HOME are defined");
|
||||||
dir += "/.config";
|
dir += "/.config";
|
||||||
@ -104,20 +104,20 @@ QString userCacheDir() {
|
|||||||
QString dir;
|
QString dir;
|
||||||
|
|
||||||
#ifdef Q_OS_WIN
|
#ifdef Q_OS_WIN
|
||||||
dir = qgetenv ("LocalAppData");
|
dir = qEnvironmentVariable("LocalAppData");
|
||||||
if (dir.isEmpty())
|
if (dir.isEmpty())
|
||||||
throw Exception("%LocalAppData% is not defined.");
|
throw Exception("%LocalAppData% is not defined.");
|
||||||
#elif defined(Q_OS_IOS) || defined(Q_OS_DARWIN)
|
#elif defined(Q_OS_IOS) || defined(Q_OS_DARWIN)
|
||||||
dir = qgetenv("HOME");
|
dir = qEnvironmentVariable("HOME");
|
||||||
if (dir.isEmpty()) {
|
if (dir.isEmpty()) {
|
||||||
throw Exception("$HOME is not defined.");
|
throw Exception("$HOME is not defined.");
|
||||||
}
|
}
|
||||||
dir += "/Library/Caches";
|
dir += "/Library/Caches";
|
||||||
#else
|
#else
|
||||||
dir = qgetenv ("XDG_CACHE_HOME");
|
dir = qEnvironmentVariable("XDG_CACHE_HOME");
|
||||||
if (dir.isEmpty())
|
if (dir.isEmpty())
|
||||||
{
|
{
|
||||||
dir = qgetenv ("HOME");
|
dir = qEnvironmentVariable("HOME");
|
||||||
if (dir.isEmpty())
|
if (dir.isEmpty())
|
||||||
throw Exception("neither $XDG_CACHE_HOME nor $HOME are defined");
|
throw Exception("neither $XDG_CACHE_HOME nor $HOME are defined");
|
||||||
dir += "/.cache";
|
dir += "/.cache";
|
||||||
@ -138,10 +138,10 @@ QString userDataDir() {
|
|||||||
QString folder;
|
QString folder;
|
||||||
|
|
||||||
#ifdef Q_OS_LINUX
|
#ifdef Q_OS_LINUX
|
||||||
QString dir = qgetenv ("XDG_DATA_HOME");
|
QString dir = qEnvironmentVariable("XDG_DATA_HOME");
|
||||||
if (dir.isEmpty())
|
if (dir.isEmpty())
|
||||||
{
|
{
|
||||||
dir = qgetenv ("HOME");
|
dir = qEnvironmentVariable("HOME");
|
||||||
if (dir.isEmpty())
|
if (dir.isEmpty())
|
||||||
throw Exception("neither $XDG_DATA_HOME nor $HOME are defined");
|
throw Exception("neither $XDG_DATA_HOME nor $HOME are defined");
|
||||||
dir += "/.local/share";
|
dir += "/.local/share";
|
||||||
@ -149,7 +149,7 @@ QString userDataDir() {
|
|||||||
folder = QDir(dir).absoluteFilePath(configFolder);
|
folder = QDir(dir).absoluteFilePath(configFolder);
|
||||||
QDir().mkpath(folder);
|
QDir().mkpath(folder);
|
||||||
#else
|
#else
|
||||||
folder = userCacheDir();
|
folder = userConfigDir();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
return folder;
|
return folder;
|
||||||
@ -280,4 +280,20 @@ bool onWindows() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// Elision is performed by inserting '...' around the (maxLen / 2) - 2 left-most and right-most characters of the string.
|
||||||
|
///
|
||||||
|
/// \return The elided string, or the original string if its length does not exceed maxLength.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
QString elideLongString(QString const &str, qint32 maxLength) {
|
||||||
|
qint32 const len = str.length();
|
||||||
|
if (len <= maxLength) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
qint32 const hLen = qMax(0, (maxLength / 2) - 2);
|
||||||
|
return str.left(hLen) + "..." + str.right(hLen);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
} // namespace bridgepp
|
} // namespace bridgepp
|
||||||
|
|||||||
@ -49,6 +49,7 @@ OS os(); ///< Return the operating system.
|
|||||||
bool onLinux(); ///< Check if the OS is Linux.
|
bool onLinux(); ///< Check if the OS is Linux.
|
||||||
bool onMacOS(); ///< Check if the OS is macOS.
|
bool onMacOS(); ///< Check if the OS is macOS.
|
||||||
bool onWindows(); ///< Check if the OS in Windows.
|
bool onWindows(); ///< Check if the OS in Windows.
|
||||||
|
QString elideLongString(QString const &str, qint32 maxLength); ///< Elide a string in the middle if its length exceed maxLength.
|
||||||
|
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|||||||
@ -23,11 +23,17 @@ namespace bridgepp {
|
|||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \param[in] what A description of the exception
|
/// \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.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
Exception::Exception(QString what) noexcept
|
Exception::Exception(QString qwhat, QString details, QString function, QByteArray attachment) noexcept
|
||||||
: std::exception()
|
: std::exception()
|
||||||
, what_(std::move(what)) {
|
, qwhat_(std::move(qwhat))
|
||||||
|
, what_(qwhat_.toLocal8Bit())
|
||||||
|
, details_(std::move(details))
|
||||||
|
, function_(std::move(function))
|
||||||
|
, attachment_(std::move(attachment)) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -36,7 +42,11 @@ Exception::Exception(QString what) noexcept
|
|||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
Exception::Exception(Exception const &ref) noexcept
|
Exception::Exception(Exception const &ref) noexcept
|
||||||
: std::exception(ref)
|
: std::exception(ref)
|
||||||
, what_(ref.what_) {
|
, qwhat_(ref.qwhat_)
|
||||||
|
, what_(ref.what_)
|
||||||
|
, details_(ref.details_)
|
||||||
|
, function_(ref.function_)
|
||||||
|
, attachment_(ref.attachment_) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -45,15 +55,19 @@ Exception::Exception(Exception const &ref) noexcept
|
|||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
Exception::Exception(Exception &&ref) noexcept
|
Exception::Exception(Exception &&ref) noexcept
|
||||||
: std::exception(ref)
|
: std::exception(ref)
|
||||||
, what_(ref.what_) {
|
, qwhat_(ref.qwhat_)
|
||||||
|
, what_(ref.what_)
|
||||||
|
, details_(ref.details_)
|
||||||
|
, function_(ref.function_)
|
||||||
|
, attachment_(ref.attachment_) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \return a string describing the exception
|
/// \return a string describing the exception
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
QString const &Exception::qwhat() const noexcept {
|
QString Exception::qwhat() const noexcept {
|
||||||
return what_;
|
return qwhat_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -61,8 +75,38 @@ QString const &Exception::qwhat() const noexcept {
|
|||||||
/// \return A pointer to the description string of the exception.
|
/// \return A pointer to the description string of the exception.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
const char *Exception::what() const noexcept {
|
const char *Exception::what() const noexcept {
|
||||||
return what_.toLocal8Bit().constData();
|
return what_.constData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \return The details for the exception.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
QString Exception::details() const noexcept {
|
||||||
|
return details_;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \return The attachment for the exception.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
QByteArray Exception::attachment() const noexcept {
|
||||||
|
return attachment_;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \return The details exception.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
QString Exception::detailedWhat() const {
|
||||||
|
QString result = qwhat_;
|
||||||
|
if (!function_.isEmpty()) {
|
||||||
|
result = QString("%1(): %2").arg(function_, result);
|
||||||
|
}
|
||||||
|
if (!details_.isEmpty()) {
|
||||||
|
result += "\n\nDetails:\n" + details_;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace bridgepp
|
} // namespace bridgepp
|
||||||
|
|||||||
@ -31,17 +31,25 @@ namespace bridgepp {
|
|||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
class Exception : public std::exception {
|
class Exception : public std::exception {
|
||||||
public: // member functions
|
public: // member functions
|
||||||
explicit Exception(QString what = QString()) noexcept; ///< Constructor
|
explicit Exception(QString qwhat = QString(), QString details = QString(), QString function = QString(),
|
||||||
|
QByteArray attachment = QByteArray()) noexcept; ///< Constructor
|
||||||
Exception(Exception const &ref) noexcept; ///< copy constructor
|
Exception(Exception const &ref) noexcept; ///< copy constructor
|
||||||
Exception(Exception &&ref) noexcept; ///< copy constructor
|
Exception(Exception &&ref) noexcept; ///< copy constructor
|
||||||
Exception &operator=(Exception const &) = delete; ///< Disabled assignment operator
|
Exception &operator=(Exception const &) = delete; ///< Disabled assignment operator
|
||||||
Exception &operator=(Exception &&) = delete; ///< Disabled assignment operator
|
Exception &operator=(Exception &&) = delete; ///< Disabled assignment operator
|
||||||
~Exception() noexcept override = default; ///< Destructor
|
~Exception() noexcept override = default; ///< Destructor
|
||||||
QString const &qwhat() const noexcept; ///< Return the description of the exception as a QString
|
QString qwhat() const noexcept; ///< Return the description of the exception as a QString
|
||||||
const char *what() const noexcept override; ///< Return the description of the exception as C style string
|
const char *what() const noexcept override; ///< Return the description of the exception as C style string
|
||||||
|
QString details() const noexcept; ///< Return the details for 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).
|
||||||
|
|
||||||
private: // data members
|
private: // data members
|
||||||
QString const what_; ///< The description of the exception
|
QString const qwhat_; ///< The description of the exception.
|
||||||
|
QByteArray const what_; ///< The c-string version of the qwhat message. Stored as a QByteArray for automatic lifetime management.
|
||||||
|
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.
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -560,6 +560,20 @@ SPStreamEvent newUserChangedEvent(QString const &userID) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \param[in] userID The userID.
|
||||||
|
/// \param[in] errorMessage The errorMessage
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
SPStreamEvent newUserBadEvent(QString const &userID, QString const &errorMessage) {
|
||||||
|
auto event = new grpc::UserBadEvent;
|
||||||
|
event->set_userid(userID.toStdString());
|
||||||
|
event->set_errormessage(errorMessage.toStdString());
|
||||||
|
auto userEvent = new grpc::UserEvent;
|
||||||
|
userEvent->set_allocated_userbadevent(event);
|
||||||
|
return wrapUserEvent(userEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \param[in] errorCode The error errorCode.
|
/// \param[in] errorCode The error errorCode.
|
||||||
/// \return The event.
|
/// \return The event.
|
||||||
|
|||||||
@ -77,6 +77,7 @@ SPStreamEvent newApiCertIssueEvent(); ///< Create a new ApiCertIssueEvent event.
|
|||||||
SPStreamEvent newToggleSplitModeFinishedEvent(QString const &userID); ///< Create a new ToggleSplitModeFinishedEvent event.
|
SPStreamEvent newToggleSplitModeFinishedEvent(QString const &userID); ///< Create a new ToggleSplitModeFinishedEvent event.
|
||||||
SPStreamEvent newUserDisconnectedEvent(QString const &username); ///< Create a new UserDisconnectedEvent event.
|
SPStreamEvent newUserDisconnectedEvent(QString const &username); ///< Create a new UserDisconnectedEvent event.
|
||||||
SPStreamEvent newUserChangedEvent(QString const &userID); ///< Create a new UserChangedEvent event.
|
SPStreamEvent newUserChangedEvent(QString const &userID); ///< Create a new UserChangedEvent event.
|
||||||
|
SPStreamEvent newUserBadEvent(QString const &userID, QString const& errorMessage); ///< Create a new UserBadEvent event.
|
||||||
|
|
||||||
// Generic error event
|
// Generic error event
|
||||||
SPStreamEvent newGenericErrorEvent(grpc::ErrorCode errorCode); ///< Create a new GenericErrrorEvent event.
|
SPStreamEvent newGenericErrorEvent(grpc::ErrorCode errorCode); ///< Create a new GenericErrrorEvent event.
|
||||||
|
|||||||
@ -88,8 +88,9 @@ GRPCConfig GRPCClient::waitAndRetrieveServiceConfig(qint64 timeoutMs, ProcessMon
|
|||||||
}
|
}
|
||||||
|
|
||||||
GRPCConfig sc;
|
GRPCConfig sc;
|
||||||
if (!sc.load(path)) {
|
QString err;
|
||||||
throw Exception("The gRPC service configuration file is invalid.");
|
if (!sc.load(path, &err)) {
|
||||||
|
throw Exception("The gRPC service configuration file is invalid.", err);
|
||||||
}
|
}
|
||||||
|
|
||||||
return sc;
|
return sc;
|
||||||
@ -105,11 +106,10 @@ void GRPCClient::setLog(Log *log) {
|
|||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \param[out] outError If the function returns false, this variable contains a description of the error.
|
|
||||||
/// \param[in] serverProcess An optional server process to monitor. If the process it, no need and retry, as connexion cannot be established. Ignored if null.
|
/// \param[in] serverProcess An optional server process to monitor. If the process it, no need and retry, as connexion cannot be established. Ignored if null.
|
||||||
/// \return true iff the connection was successful.
|
/// \return true iff the connection was successful.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
bool GRPCClient::connectToServer(GRPCConfig const &config, ProcessMonitor *serverProcess, QString &outError) {
|
void GRPCClient::connectToServer(GRPCConfig const &config, ProcessMonitor *serverProcess) {
|
||||||
try {
|
try {
|
||||||
serverToken_ = config.token.toStdString();
|
serverToken_ = config.token.toStdString();
|
||||||
QString address;
|
QString address;
|
||||||
@ -158,9 +158,10 @@ bool GRPCClient::connectToServer(GRPCConfig const &config, ProcessMonitor *serve
|
|||||||
this->logInfo("Successfully connected to gRPC server.");
|
this->logInfo("Successfully connected to gRPC server.");
|
||||||
|
|
||||||
QString const clientToken = QUuid::createUuid().toString();
|
QString const clientToken = QUuid::createUuid().toString();
|
||||||
QString clientConfigPath = createClientConfigFile(clientToken);
|
QString error;
|
||||||
|
QString clientConfigPath = createClientConfigFile(clientToken, &error);
|
||||||
if (clientConfigPath.isEmpty()) {
|
if (clientConfigPath.isEmpty()) {
|
||||||
throw Exception("gRPC client config could not be saved.");
|
throw Exception("gRPC client config could not be saved.", error);
|
||||||
}
|
}
|
||||||
this->logInfo(QString("Client config file was saved to '%1'").arg(QDir::toNativeSeparators(clientConfigPath)));
|
this->logInfo(QString("Client config file was saved to '%1'").arg(QDir::toNativeSeparators(clientConfigPath)));
|
||||||
|
|
||||||
@ -176,12 +177,9 @@ bool GRPCClient::connectToServer(GRPCConfig const &config, ProcessMonitor *serve
|
|||||||
}
|
}
|
||||||
|
|
||||||
log_->info("gRPC token was validated");
|
log_->info("gRPC token was validated");
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
catch (Exception const &e) {
|
catch (Exception const &e) {
|
||||||
outError = e.qwhat();
|
throw Exception("Cannot connect to Go backend via gRPC: " + e.qwhat(), e.details());
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -222,17 +220,12 @@ grpc::Status GRPCClient::addLogEntry(Log::Level level, QString const &package, Q
|
|||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \return The status for the gRPC call.
|
/// \return The status for the gRPC call.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
grpc::Status GRPCClient::guiReady() {
|
grpc::Status GRPCClient::guiReady(bool &outShowSplashScreen) {
|
||||||
return this->logGRPCCallStatus(stub_->GuiReady(this->clientContext().get(), empty, &empty), __FUNCTION__);
|
GuiReadyResponse response;
|
||||||
}
|
Status status = this->logGRPCCallStatus(stub_->GuiReady(this->clientContext().get(), empty, &response), __FUNCTION__);
|
||||||
|
if (status.ok())
|
||||||
|
outShowSplashScreen = response.showsplashscreen();
|
||||||
//****************************************************************************************************************************************************
|
return status;
|
||||||
/// \param[out] outIsFirst The value for the property.
|
|
||||||
/// \return The status for the gRPC call.
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
grpc::Status GRPCClient::isFirstGUIStart(bool &outIsFirst) {
|
|
||||||
return this->logGRPCCallStatus(this->getBool(&Bridge::Stub::IsFirstGuiStart, outIsFirst), __FUNCTION__);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -465,15 +458,6 @@ grpc::Status GRPCClient::showOnStartup(bool &outValue) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
/// \param[out] outValue The value for the property.
|
|
||||||
/// \return The status for the gRPC call.
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
grpc::Status GRPCClient::showSplashScreen(bool &outValue) {
|
|
||||||
return this->logGRPCCallStatus(this->getBool(&Bridge::Stub::ShowSplashScreen, outValue), __FUNCTION__);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \param[out] outGoos The value for the property.
|
/// \param[out] outGoos The value for the property.
|
||||||
/// \return The status for the gRPC call.
|
/// \return The status for the gRPC call.
|
||||||
@ -691,6 +675,18 @@ grpc::Status GRPCClient::setUserSplitMode(QString const &userID, bool active) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \param[in] userID The userID.
|
||||||
|
/// \param[in] doResync Did the user request a resync.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
grpc::Status GRPCClient::sendBadEventUserFeedback(QString const &userID, bool doResync) {
|
||||||
|
UserBadEventFeedbackRequest request;
|
||||||
|
request.set_userid(userID.toStdString());
|
||||||
|
request.set_doresync(doResync);
|
||||||
|
return this->logGRPCCallStatus(stub_->SendBadEventUserFeedback(this->clientContext().get(), request, &empty), __FUNCTION__);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \param[out] outUsers The user list.
|
/// \param[out] outUsers The user list.
|
||||||
/// \return The status code for the gRPC call.
|
/// \return The status code for the gRPC call.
|
||||||
@ -1380,6 +1376,14 @@ void GRPCClient::processUserEvent(UserEvent const &event) {
|
|||||||
emit userChanged(userID);
|
emit userChanged(userID);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case UserEvent::kUserBadEvent: {
|
||||||
|
UserBadEvent const& e = event.userbadevent();
|
||||||
|
QString const userID = QString::fromStdString(e.userid());
|
||||||
|
QString const errorMessage = QString::fromStdString(e.errormessage());
|
||||||
|
this->logTrace(QString("User event received: UserBadEvent (userID = %1, errorMessage = %2).").arg(userID, errorMessage));
|
||||||
|
emit userBadEvent(userID, errorMessage);
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
this->logError("Unknown User event received.");
|
this->logError("Unknown User event received.");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -59,12 +59,11 @@ public: // member functions.
|
|||||||
GRPCClient &operator=(GRPCClient const &) = delete; ///< Disabled assignment operator.
|
GRPCClient &operator=(GRPCClient const &) = delete; ///< Disabled assignment operator.
|
||||||
GRPCClient &operator=(GRPCClient &&) = delete; ///< Disabled move assignment operator.
|
GRPCClient &operator=(GRPCClient &&) = delete; ///< Disabled move assignment operator.
|
||||||
void setLog(Log *log); ///< Set the log for the client.
|
void setLog(Log *log); ///< Set the log for the client.
|
||||||
bool connectToServer(GRPCConfig const &config, class ProcessMonitor *serverProcess, QString &outError); ///< Establish connection to the gRPC server.
|
void connectToServer(GRPCConfig const &config, class ProcessMonitor *serverProcess); ///< Establish connection to the gRPC server.
|
||||||
|
|
||||||
grpc::Status checkTokens(QString const &clientConfigPath, QString &outReturnedClientToken); ///< Performs a token check.
|
grpc::Status checkTokens(QString const &clientConfigPath, QString &outReturnedClientToken); ///< Performs a token check.
|
||||||
grpc::Status addLogEntry(Log::Level level, QString const &package, QString const &message); ///< Performs the "AddLogEntry" gRPC call.
|
grpc::Status addLogEntry(Log::Level level, QString const &package, QString const &message); ///< Performs the "AddLogEntry" gRPC call.
|
||||||
grpc::Status guiReady(); ///< performs the "GuiReady" gRPC call.
|
grpc::Status guiReady(bool &outShowSplashScreen); ///< performs the "GuiReady" gRPC call.
|
||||||
grpc::Status isFirstGUIStart(bool &outIsFirst); ///< performs the "IsFirstGUIStart" gRPC call.
|
|
||||||
grpc::Status isAutostartOn(bool &outIsOn); ///< Performs the "isAutostartOn" gRPC call.
|
grpc::Status isAutostartOn(bool &outIsOn); ///< Performs the "isAutostartOn" gRPC call.
|
||||||
grpc::Status setIsAutostartOn(bool on); ///< Performs the "setIsAutostartOn" gRPC call.
|
grpc::Status setIsAutostartOn(bool on); ///< Performs the "setIsAutostartOn" gRPC call.
|
||||||
grpc::Status isBetaEnabled(bool &outEnabled); ///< Performs the "isBetaEnabled" gRPC call.
|
grpc::Status isBetaEnabled(bool &outEnabled); ///< Performs the "isBetaEnabled" gRPC call.
|
||||||
@ -83,7 +82,6 @@ public: // member functions.
|
|||||||
grpc::Status setMainExecutable(QString const &exe); ///< Performs the 'SetMainExecutable' call.
|
grpc::Status setMainExecutable(QString const &exe); ///< Performs the 'SetMainExecutable' call.
|
||||||
grpc::Status isPortFree(qint32 port, bool &outFree); ///< Performs the 'IsPortFree' call.
|
grpc::Status isPortFree(qint32 port, bool &outFree); ///< Performs the 'IsPortFree' call.
|
||||||
grpc::Status showOnStartup(bool &outValue); ///< Performs the 'ShowOnStartup' call.
|
grpc::Status showOnStartup(bool &outValue); ///< Performs the 'ShowOnStartup' call.
|
||||||
grpc::Status showSplashScreen(bool &outValue); ///< Performs the 'ShowSplashScreen' call.
|
|
||||||
grpc::Status goos(QString &outGoos); ///< Performs the 'GoOs' call.
|
grpc::Status goos(QString &outGoos); ///< Performs the 'GoOs' call.
|
||||||
grpc::Status logsPath(QUrl &outPath); ///< Performs the 'LogsPath' call.
|
grpc::Status logsPath(QUrl &outPath); ///< Performs the 'LogsPath' call.
|
||||||
grpc::Status licensePath(QUrl &outPath); ///< Performs the 'LicensePath' call.
|
grpc::Status licensePath(QUrl &outPath); ///< Performs the 'LicensePath' call.
|
||||||
@ -175,11 +173,13 @@ public: // user related calls
|
|||||||
grpc::Status removeUser(QString const &userID); ///< Performs the 'removeUser' call.
|
grpc::Status removeUser(QString const &userID); ///< Performs the 'removeUser' call.
|
||||||
grpc::Status configureAppleMail(QString const &userID, QString const &address); ///< Performs the 'configureAppleMail' call.
|
grpc::Status configureAppleMail(QString const &userID, QString const &address); ///< Performs the 'configureAppleMail' call.
|
||||||
grpc::Status setUserSplitMode(QString const &userID, bool active); ///< Performs the 'SetUserSplitMode' call.
|
grpc::Status setUserSplitMode(QString const &userID, bool active); ///< Performs the 'SetUserSplitMode' call.
|
||||||
|
grpc::Status sendBadEventUserFeedback(QString const& userID, bool doResync); ///< Performs the 'SendBadEventUserFeedback' call.
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void toggleSplitModeFinished(QString const &userID);
|
void toggleSplitModeFinished(QString const &userID);
|
||||||
void userDisconnected(QString const &username);
|
void userDisconnected(QString const &username);
|
||||||
void userChanged(QString const &userID);
|
void userChanged(QString const &userID);
|
||||||
|
void userBadEvent(QString const &userID, QString const& errorMessage);
|
||||||
|
|
||||||
public: // keychain related calls
|
public: // keychain related calls
|
||||||
grpc::Status availableKeychains(QStringList &outKeychains);
|
grpc::Status availableKeychains(QStringList &outKeychains);
|
||||||
|
|||||||
@ -25,8 +25,7 @@ using namespace bridgepp;
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
Exception const invalidFileException("The service configuration file is invalid"); // Exception for invalid config.
|
Exception const invalidFileException("The content of the service configuration file is invalid"); // Exception for invalid config.
|
||||||
Exception const couldNotSaveException("The service configuration file could not be saved"); ///< Exception for write errors.
|
|
||||||
QString const keyPort = "port"; ///< The JSON key for the port.
|
QString const keyPort = "port"; ///< The JSON key for the port.
|
||||||
QString const keyCert = "cert"; ///< The JSON key for the TLS certificate.
|
QString const keyCert = "cert"; ///< The JSON key for the TLS certificate.
|
||||||
QString const keyToken = "token"; ///< The JSON key for the identification token.
|
QString const keyToken = "token"; ///< The JSON key for the identification token.
|
||||||
@ -78,8 +77,14 @@ qint32 jsonIntValue(QJsonObject const &object, QString const &key) {
|
|||||||
bool GRPCConfig::load(QString const &path, QString *outError) {
|
bool GRPCConfig::load(QString const &path, QString *outError) {
|
||||||
try {
|
try {
|
||||||
QFile file(path);
|
QFile file(path);
|
||||||
|
if (!file.exists())
|
||||||
|
throw Exception("The gRPC service configuration file does not exist.");
|
||||||
|
|
||||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||||
throw Exception("Could not open gRPC service config file.");
|
QThread::msleep(500); // we wait a bit and retry once, just in case server is not done writing/moving the config file.
|
||||||
|
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||||
|
throw Exception("The gRPC service configuration file exists but cannot be opened.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QJsonDocument const doc = QJsonDocument::fromJson(file.readAll());
|
QJsonDocument const doc = QJsonDocument::fromJson(file.readAll());
|
||||||
@ -93,7 +98,7 @@ bool GRPCConfig::load(QString const &path, QString *outError) {
|
|||||||
}
|
}
|
||||||
catch (Exception const &e) {
|
catch (Exception const &e) {
|
||||||
if (outError) {
|
if (outError) {
|
||||||
*outError = e.qwhat();
|
*outError = QString("Error loading gRPC service configuration file '%1'.\n%2").arg(QFileInfo(path).absoluteFilePath(), e.qwhat());
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -115,19 +120,19 @@ bool GRPCConfig::save(QString const &path, QString *outError) {
|
|||||||
|
|
||||||
QFile file(path);
|
QFile file(path);
|
||||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||||
throw couldNotSaveException;
|
throw Exception("The file could not be opened for writing.");
|
||||||
}
|
}
|
||||||
|
|
||||||
QByteArray const array = QJsonDocument(object).toJson();
|
QByteArray const array = QJsonDocument(object).toJson();
|
||||||
if (array.size() != file.write(array)) {
|
if (array.size() != file.write(array)) {
|
||||||
throw couldNotSaveException;
|
throw Exception("An error occurred while writing to the file.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch (Exception const &e) {
|
catch (Exception const &e) {
|
||||||
if (outError) {
|
if (outError) {
|
||||||
*outError = e.qwhat();
|
*outError = QString("Error saving gRPC service configuration file '%1'.\n%2").arg(QFileInfo(path).absoluteFilePath(), e.qwhat());
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -47,22 +47,6 @@ QString grpcClientConfigBaseFilename() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
/// \return The server certificate file name
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
QString serverCertificateFilename() {
|
|
||||||
return "cert.pem";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
//
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
QString serverKeyFilename() {
|
|
||||||
return "key.pem";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
} // anonymous namespace
|
} // anonymous namespace
|
||||||
|
|
||||||
|
|
||||||
@ -90,29 +74,14 @@ QString grpcClientConfigBasePath() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
/// \return The absolute path of the server certificate.
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
QString serverCertificatePath() {
|
|
||||||
return QDir(userConfigDir()).absoluteFilePath(serverCertificateFilename());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
/// \return The absolute path of the server key.
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
QString serverKeyPath() {
|
|
||||||
|
|
||||||
return QDir(userConfigDir()).absoluteFilePath(serverKeyFilename());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \param[in] token The token to put in the file.
|
/// \param[in] token The token to put in the file.
|
||||||
|
/// \param[out] outError if the function returns an empty string and this pointer is not null, the pointer variable holds a description of the error
|
||||||
|
/// on exit.
|
||||||
/// \return The path of the created file.
|
/// \return The path of the created file.
|
||||||
/// \return A null string if the file could not be saved..
|
/// \return A null string if the file could not be saved.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
QString createClientConfigFile(QString const &token) {
|
QString createClientConfigFile(QString const &token, QString *outError) {
|
||||||
QString const basePath = grpcClientConfigBasePath();
|
QString const basePath = grpcClientConfigBasePath();
|
||||||
QString path, error;
|
QString path, error;
|
||||||
for (qint32 i = 0; i < 1000; ++i) // we try a decent amount of times
|
for (qint32 i = 0; i < 1000; ++i) // we try a decent amount of times
|
||||||
@ -121,13 +90,16 @@ QString createClientConfigFile(QString const &token) {
|
|||||||
if (!QFileInfo(path).exists()) {
|
if (!QFileInfo(path).exists()) {
|
||||||
GRPCConfig config;
|
GRPCConfig config;
|
||||||
config.token = token;
|
config.token = token;
|
||||||
if (!config.save(path)) {
|
|
||||||
|
if (!config.save(path, outError)) {
|
||||||
return QString();
|
return QString();
|
||||||
}
|
}
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (outError)
|
||||||
|
*outError = "no usable client configuration file name could be found.";
|
||||||
return QString();
|
return QString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -36,9 +36,7 @@ typedef std::shared_ptr<grpc::StreamEvent> SPStreamEvent; ///< Type definition f
|
|||||||
|
|
||||||
QString grpcServerConfigPath(); ///< Return the path of the gRPC server config file.
|
QString grpcServerConfigPath(); ///< Return the path of the gRPC server config file.
|
||||||
QString grpcClientConfigBasePath(); ///< Return the path of the gRPC client config file.
|
QString grpcClientConfigBasePath(); ///< Return the path of the gRPC client config file.
|
||||||
QString serverCertificatePath(); ///< Return the path of the server certificate.
|
QString createClientConfigFile(QString const &token, QString *outError); ///< Create the client config file the server will retrieve and return its path.
|
||||||
QString serverKeyPath(); ///< Return the path of the server key.
|
|
||||||
QString createClientConfigFile(QString const &token); ///< Create the client config file the server will retrieve and return its path.
|
|
||||||
grpc::LogLevel logLevelToGRPC(Log::Level level); ///< Convert a Log::Level to gRPC enum value.
|
grpc::LogLevel logLevelToGRPC(Log::Level level); ///< Convert a Log::Level to gRPC enum value.
|
||||||
Log::Level logLevelFromGRPC(grpc::LogLevel level); ///< Convert a grpc::LogLevel to a Log::Level.
|
Log::Level logLevelFromGRPC(grpc::LogLevel level); ///< Convert a grpc::LogLevel to a Log::Level.
|
||||||
grpc::UserState userStateToGRPC(UserState state); ///< Convert a bridgepp::UserState to a grpc::UserState.
|
grpc::UserState userStateToGRPC(UserState state); ///< Convert a bridgepp::UserState to a grpc::UserState.
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user