mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-10 04:36:43 +00:00
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
|
||||
- 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
|
||||
|
||||
lint:
|
||||
@ -88,7 +77,7 @@ test-linux:
|
||||
test-linux-race:
|
||||
stage: test
|
||||
extends:
|
||||
- .rules-branch-manual-MR-always-allow-failure
|
||||
- .rules-branch-and-MR-manual
|
||||
script:
|
||||
- make test-race
|
||||
tags:
|
||||
@ -106,7 +95,7 @@ test-integration:
|
||||
test-integration-race:
|
||||
stage: test
|
||||
extends:
|
||||
- .rules-branch-manual-MR-always-allow-failure
|
||||
- .rules-branch-and-MR-manual
|
||||
script:
|
||||
- make test-integration-race
|
||||
tags:
|
||||
|
||||
11
BUILDS.md
11
BUILDS.md
@ -5,13 +5,13 @@
|
||||
- the go-rfc5322 module cannot currently be compiled for 32-bit OSes
|
||||
* Go 1.18
|
||||
* 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)
|
||||
* Windres (windows)
|
||||
* libglvnd and libsecret development files (linux)
|
||||
|
||||
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.
|
||||
|
||||
## Build
|
||||
@ -44,9 +44,10 @@ make build
|
||||
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)
|
||||
* Bridge always has the option (whether built with Qt or without) to use a CLI interface by starting it with the argument `-c`
|
||||
* NOTE: You still need to setup supported keychain on your system
|
||||
* To launch Bridge without GUI, you can invoke the `bridge` executable with one the following command-line switches:
|
||||
* `--noninteractive` or `-n` to start Bridge without any interface (i.e., there is no way to add or remove client, get bridge password, etc.)
|
||||
* `--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 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/)
|
||||
|
||||
## [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
|
||||
|
||||
|
||||
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
|
||||
|
||||
# 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_FULL_NAME:=Proton Mail Bridge
|
||||
APP_VENDOR:=Proton AG
|
||||
@ -23,16 +23,21 @@ REVISION:=$(shell git rev-parse --short=10 HEAD)
|
||||
BUILD_TIME:=$(shell date +%FT%T%z)
|
||||
MACOS_MIN_VERSION_ARM64=11.0
|
||||
MACOS_MIN_VERSION_AMD64=10.15
|
||||
BUILD_ENV?=dev
|
||||
|
||||
BUILD_FLAGS:=-tags='${BUILD_TAGS}'
|
||||
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+=-X "github.com/ProtonMail/proton-bridge/v3/internal/constants.FullAppName=${APP_FULL_NAME}"
|
||||
|
||||
ifneq "${BUILD_LDFLAGS}" ""
|
||||
GO_LDFLAGS+=${BUILD_LDFLAGS}
|
||||
ifneq "${DSN_SENTRY}" ""
|
||||
GO_LDFLAGS+=-X github.com/ProtonMail/proton-bridge/v3/internal/constants.DSNSentry=${DSN_SENTRY}
|
||||
endif
|
||||
|
||||
ifneq "${BUILD_ENV}" ""
|
||||
GO_LDFLAGS+=-X github.com/ProtonMail/proton-bridge/v3/internal/constants.BuildEnv=${BUILD_ENV}
|
||||
endif
|
||||
|
||||
GO_LDFLAGS_LAUNCHER:=${GO_LDFLAGS}
|
||||
ifeq "${TARGET_OS}" "windows"
|
||||
#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
|
||||
|
||||
BUILD_FLAGS+=-ldflags '${GO_LDFLAGS}'
|
||||
BUILD_FLAGS_GUI+=-ldflags "${GO_LDFLAGS}"
|
||||
BUILD_FLAGS_LAUNCHER+=-ldflags '${GO_LDFLAGS_LAUNCHER}'
|
||||
DEPLOY_DIR:=cmd/${TARGET_CMD}/deploy
|
||||
DIRNAME:=$(shell basename ${CURDIR})
|
||||
@ -154,8 +158,10 @@ ${EXE_TARGET}: check-build-essentials ${EXE_NAME}
|
||||
BRIDGE_VENDOR="${APP_VENDOR}" \
|
||||
BRIDGE_APP_VERSION=${APP_VERSION} \
|
||||
BRIDGE_REVISION=${REVISION} \
|
||||
BRIDGE_BUILD_TIME=${BUILD_TIME} \
|
||||
BRIDGE_DSN_SENTRY=${DSN_SENTRY} \
|
||||
BRIDGE_BUILD_TIME=${BUILD_TIME} \
|
||||
BRIDGE_GUI_BUILD_CONFIG=Release \
|
||||
BRIDGE_BUILD_ENV=${BUILD_ENV} \
|
||||
BRIDGE_INSTALL_PATH=${ROOT_DIR}/${DEPLOY_DIR}/${GOOS} \
|
||||
./build.sh install
|
||||
mv "${ROOT_DIR}/${BRIDGE_EXE}" "$(ROOT_DIR)/${EXE_TARGET}"
|
||||
@ -294,7 +300,7 @@ gofiles: ./internal/bridge/credits.go
|
||||
cd ./utils/ && ./credits.sh bridge
|
||||
|
||||
## 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_IMAP?=client # client/server/all, or empty to turn it off
|
||||
@ -321,12 +327,26 @@ run-nogui: build-nogui clean-vendor gofiles
|
||||
run-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:
|
||||
rm -rf ./vendor
|
||||
|
||||
clean-gui:
|
||||
cd internal/frontend/bridge-gui/ && \
|
||||
rm -f Version.h && \
|
||||
rm -f BuildConfig.h && \
|
||||
rm -rf cmake-build-*/
|
||||
|
||||
clean-vcpkg:
|
||||
@ -349,6 +369,6 @@ clean: clean-vendor clean-gui clean-vcpkg
|
||||
.PHONY: generate
|
||||
generate:
|
||||
go generate ./...
|
||||
$(MAKE) add-license
|
||||
$(MAKE) build
|
||||
|
||||
.FORCE:
|
||||
|
||||
52
README.md
52
README.md
@ -62,35 +62,33 @@ major problems.
|
||||
- `TAGS`: set build tags for tests
|
||||
- `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
|
||||
### 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
|
||||
User preferences are stored in json at the following location:
|
||||
- Linux: `~/.config/protonmail/bridge/prefs.json`
|
||||
- macOS: `~/Library/ApplicationSupport/protonmail/bridge/prefs.json`
|
||||
- Windows: `%APPDATA%\protonmail\bridge\prefs.json`
|
||||
| | Base Dir | Path |
|
||||
|-----------------------|----------|----------------------------|
|
||||
| bridge lock file | cache | bridge.lock |
|
||||
| bridge-gui lock file | cache | bridge-gui.lock |
|
||||
| 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)
|
||||
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)
|
||||
defer crashHandler.HandlePanic()
|
||||
@ -127,9 +127,11 @@ func main() { //nolint:funlen
|
||||
|
||||
l = l.WithField("exe_path", exe)
|
||||
|
||||
args, wait, mainExe := findAndStripWait(args)
|
||||
args, wait, mainExes := findAndStripWait(args)
|
||||
if wait {
|
||||
waitForProcessToFinish(mainExe)
|
||||
for _, mainExe := range mainExes {
|
||||
waitForProcessToFinish(mainExe)
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
func findAndStripWait(args []string) ([]string, bool, string) {
|
||||
func findAndStripWait(args []string) ([]string, bool, []string) {
|
||||
res := append([]string{}, args...)
|
||||
|
||||
hasFlag := false
|
||||
var value string
|
||||
|
||||
values := make([]string, 0)
|
||||
for k, v := range res {
|
||||
if v != FlagWait {
|
||||
continue
|
||||
@ -200,14 +201,16 @@ func findAndStripWait(args []string) ([]string, bool, string) {
|
||||
continue
|
||||
}
|
||||
hasFlag = true
|
||||
value = res[k+1]
|
||||
values = append(values, res[k+1])
|
||||
}
|
||||
|
||||
if hasFlag {
|
||||
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(
|
||||
|
||||
@ -56,3 +56,25 @@ func TestFindAndStrip(t *testing.T) {
|
||||
assert.False(t, found)
|
||||
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 (
|
||||
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
|
||||
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-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/gopenpgp/v2 v2.4.10
|
||||
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/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
|
||||
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
|
||||
github.com/ProtonMail/gluon v0.14.2-0.20230106095250-7e99ea4da61e h1://xRNjGTAMXw2U91MtqPc4krUtxQmt2+4z1oYrBaOWU=
|
||||
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 h1:qLHEYjr7BJaZxeMyqhEBpenuAnduFNZqBA26gT9LXGo=
|
||||
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/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
|
||||
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-20220429130430-2192574d760f h1:4IWzKjHzZxdrW9k4zl/qCwenOVHDbVDADPPHFLjs0Oc=
|
||||
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.2.4-0.20230109143101-f8fd857ee5b4/go.mod h1:JUo5IQG0hNuPRuDpOUsCOvtee6UjTEHHF1QN2i8RSos=
|
||||
github.com/ProtonMail/go-proton-api v0.3.1-0.20230321105220-39e9131e1a68 h1:CExt0Vd19dsUtf+IBSa/l96/DTHEmgXi4IbWG99Vs1E=
|
||||
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/go.mod h1:6oOKr0jXvpoE6pwTx/HukigQpX2J9WUf6h0auplrFTw=
|
||||
github.com/ProtonMail/go-srp v0.0.5 h1:xhUioxZgDbCnpo9JehyFhwwsn9JLWkUGfB0oiKXgiGg=
|
||||
|
||||
@ -19,11 +19,13 @@ package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"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
|
||||
// Seed the default RNG from the math/rand package.
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
|
||||
// Get the current bridge version.
|
||||
version, err := semver.NewVersion(constants.Version)
|
||||
if err != nil {
|
||||
@ -165,7 +170,7 @@ func run(c *cli.Context) error { //nolint:funlen
|
||||
identifier := useragent.New()
|
||||
|
||||
// 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.
|
||||
// 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 {
|
||||
// Unlock the encrypted vault.
|
||||
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() {
|
||||
// Migrate old settings into the vault.
|
||||
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.
|
||||
locations := locations.New(provider, constants.ConfigName)
|
||||
defer func() {
|
||||
if err := locations.Clean(); err != nil {
|
||||
logrus.WithError(err).Error("Failed to clean locations")
|
||||
}
|
||||
}()
|
||||
|
||||
return fn(locations)
|
||||
return fn(locations.New(provider, constants.ConfigName))
|
||||
}
|
||||
|
||||
// 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 migrateOldSettingsWithDir(configDir, v)
|
||||
}
|
||||
|
||||
// nolint:gosec
|
||||
func migrateOldSettingsWithDir(configDir string, v *vault.Vault) error {
|
||||
b, err := os.ReadFile(filepath.Join(configDir, "protonmail", "bridge", "prefs.json"))
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return nil
|
||||
@ -94,7 +99,27 @@ func migrateOldSettings(v *vault.Vault) error {
|
||||
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 {
|
||||
@ -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)
|
||||
}
|
||||
|
||||
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 {
|
||||
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"`
|
||||
UpdateRollout float64 `json:"rollout,,string"`
|
||||
|
||||
FirstStart bool `json:"first_time_start,,string"`
|
||||
FirstStartGUI bool `json:"first_time_start_gui,,string"`
|
||||
ColorScheme string `json:"color_scheme"`
|
||||
LastVersion *semver.Version `json:"last_used_version"`
|
||||
Autostart bool `json:"autostart,,string"`
|
||||
FirstStart bool `json:"first_time_start,,string"`
|
||||
ColorScheme string `json:"color_scheme"`
|
||||
LastVersion *semver.Version `json:"last_used_version"`
|
||||
Autostart bool `json:"autostart,,string"`
|
||||
|
||||
AllowProxy bool `json:"allow_proxy,,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))
|
||||
}
|
||||
|
||||
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 {
|
||||
errs = multierror.Append(errs, fmt.Errorf("failed to migrate color scheme: %w", err))
|
||||
}
|
||||
|
||||
@ -38,54 +38,44 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMigratePrefsToVault(t *testing.T) {
|
||||
func TestMigratePrefsToVaultWithKeys(t *testing.T) {
|
||||
// Create a new vault.
|
||||
vault, corrupt, err := vault.New(t.TempDir(), t.TempDir(), []byte("my secret key"))
|
||||
require.NoError(t, err)
|
||||
require.False(t, corrupt)
|
||||
|
||||
// load the old prefs file.
|
||||
b, err := os.ReadFile(filepath.Join("testdata", "prefs.json"))
|
||||
require.NoError(t, err)
|
||||
configDir := filepath.Join("testdata", "with_keys")
|
||||
|
||||
// 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.
|
||||
require.Equal(t, 2143, vault.GetIMAPPort())
|
||||
require.Equal(t, 2025, vault.GetSMTPPort())
|
||||
require.True(t, vault.GetSMTPSSL())
|
||||
// Check Json Settings
|
||||
validateJSONPrefs(t, vault)
|
||||
|
||||
// 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 the keys were found and collected.
|
||||
require.Equal(t, "-----BEGIN CERTIFICATE-----", string(vault.GetBridgeTLSCert()))
|
||||
require.Equal(t, "-----BEGIN RSA PRIVATE KEY-----", string(vault.GetBridgeTLSKey()))
|
||||
}
|
||||
|
||||
// Check that the app settings have been migrated.
|
||||
require.False(t, vault.GetFirstStart())
|
||||
require.True(t, vault.GetFirstStartGUI())
|
||||
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)
|
||||
func TestMigratePrefsToVaultWithoutKeys(t *testing.T) {
|
||||
// Create a new vault.
|
||||
vault, corrupt, err := vault.New(t.TempDir(), t.TempDir(), []byte("my secret key"))
|
||||
require.NoError(t, err)
|
||||
require.False(t, corrupt)
|
||||
|
||||
cookies, err := cookies.NewCookieJar(jar, vault)
|
||||
require.NoError(t, err)
|
||||
// load the old prefs file.
|
||||
configDir := filepath.Join("testdata", "without_keys")
|
||||
|
||||
url, err := url.Parse("https://api.protonmail.ch")
|
||||
require.NoError(t, err)
|
||||
// Migrate the old prefs file to the new vault.
|
||||
require.NoError(t, migrateOldSettingsWithDir(configDir, vault))
|
||||
|
||||
// There should be a cookie for the API.
|
||||
require.NotEmpty(t, cookies.Cookies(url))
|
||||
// Check Json Settings
|
||||
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) {
|
||||
@ -102,7 +92,7 @@ func TestKeychainMigration(t *testing.T) {
|
||||
oldCacheDir := filepath.Join(tmpDir, "protonmail", "bridge")
|
||||
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, os.WriteFile(
|
||||
@ -197,3 +187,40 @@ func TestUserMigration(t *testing.T) {
|
||||
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 {
|
||||
logrus.WithError(err).Error("Could not load/create vault key")
|
||||
insecure = true
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
gluonDir, err := locations.ProvideGluonPath()
|
||||
gluonCacheDir, err := locations.ProvideGluonCachePath()
|
||||
if err != nil {
|
||||
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 {
|
||||
return nil, false, false, fmt.Errorf("could not create vault: %w", err)
|
||||
}
|
||||
|
||||
@ -21,6 +21,7 @@ package bridge
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
@ -38,6 +39,7 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/focus"
|
||||
"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/vault"
|
||||
"github.com/bradenaw/juniper/xslices"
|
||||
@ -108,6 +110,12 @@ type Bridge struct {
|
||||
logIMAPServer 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 *async.Group
|
||||
|
||||
@ -216,13 +224,29 @@ func newBridge(
|
||||
return nil, fmt.Errorf("failed to load TLS config: %w", err)
|
||||
}
|
||||
|
||||
gluonDir, err := getGluonDir(vault)
|
||||
gluonCacheDir, err := getGluonDir(vault)
|
||||
if err != nil {
|
||||
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(
|
||||
gluonDir,
|
||||
gluonCacheDir,
|
||||
gluonDataDir,
|
||||
curVersion,
|
||||
tlsConfig,
|
||||
reporter,
|
||||
@ -272,6 +296,9 @@ func newBridge(
|
||||
logIMAPServer: logIMAPServer,
|
||||
logSMTP: logSMTP,
|
||||
|
||||
firstStart: firstStart,
|
||||
lastVersion: lastVersion,
|
||||
|
||||
tasks: tasks,
|
||||
}
|
||||
|
||||
@ -351,10 +378,11 @@ func (bridge *Bridge) init(tlsReporter TLSReporter) error {
|
||||
|
||||
// Attempt to lazy load users when triggered.
|
||||
bridge.goLoad = bridge.tasks.Trigger(func(ctx context.Context) {
|
||||
logrus.Info("Loading users")
|
||||
|
||||
if err := bridge.loadUsers(ctx); err != nil {
|
||||
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 {
|
||||
bridge.publish(events.AllUsersLoaded{})
|
||||
}
|
||||
@ -435,11 +463,6 @@ func (bridge *Bridge) Close(ctx context.Context) {
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@ -23,7 +23,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
@ -35,6 +35,7 @@ import (
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||
"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/events"
|
||||
"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/tests"
|
||||
"github.com/bradenaw/juniper/xslices"
|
||||
"github.com/emersion/go-imap/client"
|
||||
"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) {
|
||||
var gluonDir string
|
||||
|
||||
@ -361,13 +363,36 @@ func TestBridge_MissingGluonDir(t *testing.T) {
|
||||
require.NoError(t, bridge.SetGluonDir(ctx, t.TempDir()))
|
||||
|
||||
// Get the gluon dir.
|
||||
gluonDir = bridge.GetGluonDir()
|
||||
gluonDir = bridge.GetGluonCacheDir()
|
||||
})
|
||||
|
||||
// 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 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) {
|
||||
// ...
|
||||
})
|
||||
@ -456,41 +481,143 @@ func TestBridge_FactoryReset(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_ChangeCacheDirectoryFailsBetweenDifferentVolumes(t *testing.T) {
|
||||
if runtime.GOOS != "windows" {
|
||||
t.Skip("Test only necessary on windows")
|
||||
}
|
||||
func TestBridge_InitGluonDirectory(t *testing.T) {
|
||||
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) {
|
||||
// Change directory
|
||||
err := bridge.SetGluonDir(ctx, "XX:\\")
|
||||
require.Error(t, err)
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
configDir, err := b.GetGluonDataDir()
|
||||
require.NoError(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) {
|
||||
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()
|
||||
currentCacheDir := bridge.GetGluonDir()
|
||||
currentCacheDir := b.GetGluonCacheDir()
|
||||
configDir, err := b.GetGluonDataDir()
|
||||
require.NoError(t, err)
|
||||
|
||||
// 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.Equal(t, userID, (<-syncCh).UserID)
|
||||
|
||||
// The user is now connected.
|
||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
|
||||
require.Equal(t, []string{userID}, b.GetUserIDs())
|
||||
require.Equal(t, []string{userID}, getConnectedUserIDs(t, b))
|
||||
|
||||
// Change directory
|
||||
err = bridge.SetGluonDir(ctx, newCacheDir)
|
||||
err = b.SetGluonDir(ctx, newCacheDir)
|
||||
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))
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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.
|
||||
func withBridge(
|
||||
func withBridgeNoMocks(
|
||||
ctx context.Context,
|
||||
t *testing.T,
|
||||
mocks *bridge.Mocks,
|
||||
apiURL string,
|
||||
netCtl *proton.NetCtl,
|
||||
locator bridge.Locator,
|
||||
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.
|
||||
mocks.ProxyCtl.EXPECT().DisallowProxy()
|
||||
|
||||
@ -590,7 +722,24 @@ func withBridge(
|
||||
defer bridge.Close(ctx)
|
||||
|
||||
// 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) {
|
||||
|
||||
@ -18,6 +18,8 @@
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
@ -62,3 +64,75 @@ func moveFile(from, to string) error {
|
||||
|
||||
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 (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/gluon"
|
||||
@ -103,6 +100,8 @@ func (bridge *Bridge) closeIMAP(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// addIMAPUser connects the given user to gluon.
|
||||
//
|
||||
//nolint:funlen
|
||||
func (bridge *Bridge) addIMAPUser(ctx context.Context, user *user.User) error {
|
||||
if bridge.imapServer == nil {
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
@ -149,6 +195,7 @@ func (bridge *Bridge) removeIMAPUser(ctx context.Context, user *user.User, withD
|
||||
if bridge.imapServer == nil {
|
||||
return fmt.Errorf("no imap server instance running")
|
||||
}
|
||||
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"userID": user.ID(),
|
||||
"withData": withData,
|
||||
@ -199,31 +246,24 @@ func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) {
|
||||
}
|
||||
|
||||
func getGluonDir(encVault *vault.Vault) (string, error) {
|
||||
empty, exists, err := isEmpty(encVault.GetGluonDir())
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to check if gluon dir is empty: %w", err)
|
||||
if err := os.MkdirAll(encVault.GetGluonCacheDir(), 0o700); err != nil {
|
||||
return "", fmt.Errorf("failed to create gluon dir: %w", err)
|
||||
}
|
||||
|
||||
if !exists {
|
||||
if err := os.MkdirAll(encVault.GetGluonDir(), 0o700); err != nil {
|
||||
return "", fmt.Errorf("failed to create gluon dir: %w", err)
|
||||
}
|
||||
}
|
||||
return encVault.GetGluonCacheDir(), nil
|
||||
}
|
||||
|
||||
if empty {
|
||||
if err := encVault.ForUser(runtime.NumCPU(), func(user *vault.User) error {
|
||||
return user.ClearSyncStatus()
|
||||
}); err != nil {
|
||||
return "", fmt.Errorf("failed to reset user sync status: %w", err)
|
||||
}
|
||||
}
|
||||
func ApplyGluonCachePathSuffix(basePath string) string {
|
||||
return filepath.Join(basePath, "backend", "store")
|
||||
}
|
||||
|
||||
return encVault.GetGluonDir(), nil
|
||||
func ApplyGluonConfigPathSuffix(basePath string) string {
|
||||
return filepath.Join(basePath, "backend", "db")
|
||||
}
|
||||
|
||||
// nolint:funlen
|
||||
func newIMAPServer(
|
||||
gluonDir string,
|
||||
gluonCacheDir, gluonConfigDir string,
|
||||
version *semver.Version,
|
||||
tlsConfig *tls.Config,
|
||||
reporter reporter.Reporter,
|
||||
@ -231,11 +271,15 @@ func newIMAPServer(
|
||||
eventCh chan<- imapEvents.Event,
|
||||
tasks *async.Group,
|
||||
) (*gluon.Server, error) {
|
||||
gluonCacheDir = ApplyGluonCachePathSuffix(gluonCacheDir)
|
||||
gluonConfigDir = ApplyGluonConfigPathSuffix(gluonConfigDir)
|
||||
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"gluonDir": gluonDir,
|
||||
"version": version,
|
||||
"logClient": logClient,
|
||||
"logServer": logServer,
|
||||
"gluonStore": gluonCacheDir,
|
||||
"gluonDB": gluonConfigDir,
|
||||
"version": version,
|
||||
"logClient": logClient,
|
||||
"logServer": logServer,
|
||||
}).Info("Creating IMAP server")
|
||||
|
||||
if logClient || logServer {
|
||||
@ -263,7 +307,8 @@ func newIMAPServer(
|
||||
|
||||
imapServer, err := gluon.New(
|
||||
gluon.WithTLS(tlsConfig),
|
||||
gluon.WithDataDir(gluonDir),
|
||||
gluon.WithDataDir(gluonCacheDir),
|
||||
gluon.WithDatabaseDir(gluonConfigDir),
|
||||
gluon.WithStoreBuilder(new(storeBuilder)),
|
||||
gluon.WithLogger(imapClientLog, imapServerLog),
|
||||
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{}
|
||||
|
||||
func (*storeBuilder) New(path, userID string, passphrase []byte) (store.Store, error) {
|
||||
|
||||
@ -18,10 +18,12 @@
|
||||
package bridge_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@ -30,6 +32,7 @@ import (
|
||||
"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/emersion/go-imap"
|
||||
"github.com/emersion/go-imap/client"
|
||||
"github.com/emersion/go-sasl"
|
||||
@ -42,7 +45,7 @@ func TestBridge_Send(t *testing.T) {
|
||||
_, _, err := s.CreateUser("recipient", password)
|
||||
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)
|
||||
require.NoError(t, err)
|
||||
|
||||
@ -100,7 +103,7 @@ func TestBridge_Send(t *testing.T) {
|
||||
defer recipientIMAPClient.Logout() //nolint:errcheck
|
||||
|
||||
// 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 {
|
||||
sent, err := senderIMAPClient.Status(`Sent`, []imap.StatusItem{imap.StatusMessages})
|
||||
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"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
@ -114,38 +115,47 @@ func (bridge *Bridge) SetSMTPSSL(newSSL bool) error {
|
||||
return bridge.restartSMTP()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) GetGluonDir() string {
|
||||
return bridge.vault.GetGluonDir()
|
||||
func (bridge *Bridge) GetGluonCacheDir() string {
|
||||
return bridge.vault.GetGluonCacheDir()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) GetGluonDataDir() (string, error) {
|
||||
return bridge.locator.ProvideGluonDataPath()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) SetGluonDir(ctx context.Context, newGluonDir string) error {
|
||||
return safe.RLockRet(func() error {
|
||||
currentGluonDir := bridge.GetGluonDir()
|
||||
currentGluonDir := bridge.GetGluonCacheDir()
|
||||
newGluonDir = filepath.Join(newGluonDir, "gluon")
|
||||
if newGluonDir == currentGluonDir {
|
||||
return fmt.Errorf("new gluon dir is the same as the old one")
|
||||
}
|
||||
|
||||
currentVolumeName := filepath.VolumeName(currentGluonDir)
|
||||
newVolumeName := filepath.VolumeName(newGluonDir)
|
||||
if err := bridge.stopEventLoops(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
err := bridge.startEventLoops(ctx)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
if currentVolumeName != newVolumeName {
|
||||
return fmt.Errorf("it's currently not possible to move the cache between different volumes")
|
||||
if err := bridge.moveGluonCacheDir(currentGluonDir, newGluonDir); err != nil {
|
||||
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 {
|
||||
return fmt.Errorf("failed to close IMAP: %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)
|
||||
gluonDataDir, err := bridge.GetGluonDataDir()
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to get Gluon Database directory: %w", err))
|
||||
}
|
||||
|
||||
imapServer, err := newIMAPServer(
|
||||
bridge.vault.GetGluonDir(),
|
||||
bridge.vault.GetGluonCacheDir(),
|
||||
gluonDataDir,
|
||||
bridge.curVersion,
|
||||
bridge.tlsConfig,
|
||||
bridge.reporter,
|
||||
@ -155,25 +165,60 @@ func (bridge *Bridge) SetGluonDir(ctx context.Context, newGluonDir string) error
|
||||
bridge.tasks,
|
||||
)
|
||||
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
|
||||
|
||||
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
|
||||
}, 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 {
|
||||
return bridge.vault.GetProxyAllowed()
|
||||
}
|
||||
@ -272,23 +317,11 @@ func (bridge *Bridge) GetCurrentVersion() *semver.Version {
|
||||
}
|
||||
|
||||
func (bridge *Bridge) GetLastVersion() *semver.Version {
|
||||
return bridge.vault.GetLastVersion()
|
||||
return bridge.lastVersion
|
||||
}
|
||||
|
||||
func (bridge *Bridge) GetFirstStart() bool {
|
||||
return bridge.vault.GetFirstStart()
|
||||
}
|
||||
|
||||
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)
|
||||
return bridge.firstStart
|
||||
}
|
||||
|
||||
func (bridge *Bridge) GetColorScheme() string {
|
||||
@ -308,10 +341,10 @@ func (bridge *Bridge) FactoryReset(ctx context.Context) {
|
||||
}, bridge.usersLock)
|
||||
|
||||
// Wipe the vault.
|
||||
gluonDir, err := bridge.locator.ProvideGluonPath()
|
||||
gluonCacheDir, err := bridge.locator.ProvideGluonCachePath()
|
||||
if err != nil {
|
||||
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")
|
||||
}
|
||||
|
||||
|
||||
@ -162,26 +162,7 @@ func TestBridge_Settings_FirstStart(t *testing.T) {
|
||||
// By default, first start is true.
|
||||
require.True(t, bridge.GetFirstStart())
|
||||
|
||||
// Set first start to false.
|
||||
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())
|
||||
// the setting of the first start value is managed by bridge itself, so the setter is not exported.
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@ -351,7 +351,7 @@ func withClient(ctx context.Context, t *testing.T, s *server.Server, username st
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -376,6 +376,35 @@ func clientFetch(client *client.Client, mailbox string) ([]*imap.Message, error)
|
||||
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 {
|
||||
literal, err := os.ReadFile(filepath.Join("testdata", "text-plain.eml"))
|
||||
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)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, ok := addrKRs[addrID]
|
||||
require.True(t, ok)
|
||||
|
||||
res, err := stream.Collect(ctx, c.ImportMessages(
|
||||
ctx,
|
||||
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 {
|
||||
ProvideSettingsPath() (string, error)
|
||||
ProvideLogsPath() (string, error)
|
||||
ProvideGluonPath() (string, error)
|
||||
ProvideGluonCachePath() (string, error)
|
||||
ProvideGluonDataPath() (string, error)
|
||||
GetLicenseFilePath() string
|
||||
GetDependencyLicensesLink() string
|
||||
Clear() error
|
||||
|
||||
@ -24,6 +24,7 @@ import (
|
||||
"runtime"
|
||||
|
||||
"github.com/ProtonMail/gluon/imap"
|
||||
"github.com/ProtonMail/gluon/reporter"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/async"
|
||||
"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 {
|
||||
state = SignedOut
|
||||
}
|
||||
info = getUserInfo(user.UserID(), user.Username(), state, user.AddressMode())
|
||||
info = getUserInfo(user.UserID(), user.Username(), user.PrimaryEmail(), state, user.AddressMode())
|
||||
}); err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
apiUser, err := client.GetUser(ctx)
|
||||
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.
|
||||
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 {
|
||||
log := logrus.WithField("userID", user.UserID())
|
||||
|
||||
if user.AuthUID() == "" {
|
||||
log.Info("User is not connected (skipping)")
|
||||
return nil
|
||||
}
|
||||
|
||||
if safe.RLockRet(func() bool { return mapHas(bridge.users, user.UserID()) }, bridge.usersLock) {
|
||||
log.Info("User is already loaded (skipping)")
|
||||
return nil
|
||||
}
|
||||
|
||||
logrus.WithField("userID", user.UserID()).Info("Loading connected user")
|
||||
log.Info("Loading connected user")
|
||||
|
||||
bridge.publish(events.UserLoading{
|
||||
UserID: user.UserID(),
|
||||
})
|
||||
|
||||
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{
|
||||
UserID: user.UserID(),
|
||||
Error: err,
|
||||
})
|
||||
} else {
|
||||
logrus.WithField("userID", user.UserID()).Info("Successfully loaded user")
|
||||
log.Info("Successfully loaded connected user")
|
||||
|
||||
bridge.publish(events.UserLoadSuccess{
|
||||
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 {
|
||||
client, auth, err := bridge.api.NewClientWithRefresh(ctx, user.AuthUID(), user.AuthRef())
|
||||
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.
|
||||
if err := user.Clear(); err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@ -504,7 +571,7 @@ func (bridge *Bridge) newVaultUser(
|
||||
saltedKeyPass []byte,
|
||||
) (*vault.User, bool, error) {
|
||||
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 {
|
||||
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.
|
||||
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{
|
||||
State: state,
|
||||
UserID: userID,
|
||||
Username: username,
|
||||
Addresses: addresses,
|
||||
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"
|
||||
"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/safe"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||
"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 {
|
||||
@ -45,12 +48,18 @@ func (bridge *Bridge) handleUserEvent(ctx context.Context, user *user.User, even
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
case events.UserDeauth:
|
||||
bridge.handleUserDeauth(ctx, user)
|
||||
|
||||
case events.UserBadEvent:
|
||||
bridge.handleUserBadEvent(ctx, user, event)
|
||||
|
||||
case events.UncategorizedEventError:
|
||||
bridge.handleUncategorizedErrorEvent(event)
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -111,8 +120,12 @@ func (bridge *Bridge) handleUserAddressDeleted(ctx context.Context, user *user.U
|
||||
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 {
|
||||
if event.CancelEventPool {
|
||||
user.CancelSyncAndEventPoll()
|
||||
}
|
||||
|
||||
if err := bridge.removeIMAPUser(ctx, user, true); err != nil {
|
||||
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.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 = ""
|
||||
|
||||
// BuildEnv tags used at build time.
|
||||
BuildEnv = ""
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@ -90,7 +90,8 @@ func TestTLSSignedCertWrongPublicKey(t *testing.T) {
|
||||
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)
|
||||
|
||||
_, 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
|
||||
LabelID string
|
||||
Name 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"
|
||||
)
|
||||
|
||||
// AllUsersLoaded is emitted when all users have been loaded.
|
||||
type AllUsersLoaded struct {
|
||||
eventBase
|
||||
}
|
||||
@ -31,6 +32,7 @@ func (event AllUsersLoaded) String() string {
|
||||
return "AllUsersLoaded"
|
||||
}
|
||||
|
||||
// UserLoading is emitted when a user is being loaded.
|
||||
type UserLoading struct {
|
||||
eventBase
|
||||
|
||||
@ -41,6 +43,7 @@ func (event UserLoading) String() string {
|
||||
return fmt.Sprintf("UserLoading: UserID: %s", event.UserID)
|
||||
}
|
||||
|
||||
// UserLoadSuccess is emitted when a user has been loaded successfully.
|
||||
type UserLoadSuccess struct {
|
||||
eventBase
|
||||
|
||||
@ -51,6 +54,7 @@ func (event UserLoadSuccess) String() string {
|
||||
return fmt.Sprintf("UserLoadSuccess: UserID: %s", event.UserID)
|
||||
}
|
||||
|
||||
// UserLoadFail is emitted when a user has failed to load.
|
||||
type UserLoadFail struct {
|
||||
eventBase
|
||||
|
||||
@ -62,6 +66,7 @@ func (event UserLoadFail) String() string {
|
||||
return fmt.Sprintf("UserLoadFail: UserID: %s, Error: %s", event.UserID, event.Error)
|
||||
}
|
||||
|
||||
// UserLoggedIn is emitted when a user has logged in.
|
||||
type UserLoggedIn struct {
|
||||
eventBase
|
||||
|
||||
@ -72,6 +77,7 @@ func (event UserLoggedIn) String() string {
|
||||
return fmt.Sprintf("UserLoggedIn: UserID: %s", event.UserID)
|
||||
}
|
||||
|
||||
// UserLoggedOut is emitted when a user has logged out.
|
||||
type UserLoggedOut struct {
|
||||
eventBase
|
||||
|
||||
@ -82,6 +88,7 @@ func (event UserLoggedOut) String() string {
|
||||
return fmt.Sprintf("UserLoggedOut: UserID: %s", event.UserID)
|
||||
}
|
||||
|
||||
// UserDeauth is emitted when a user has lost its API authentication.
|
||||
type UserDeauth struct {
|
||||
eventBase
|
||||
|
||||
@ -92,6 +99,30 @@ func (event UserDeauth) String() string {
|
||||
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 {
|
||||
eventBase
|
||||
|
||||
@ -102,6 +133,7 @@ func (event UserDeleted) String() string {
|
||||
return fmt.Sprintf("UserDeleted: UserID: %s", event.UserID)
|
||||
}
|
||||
|
||||
// UserChanged is emitted when a user's data has changed (name, email, etc.).
|
||||
type UserChanged struct {
|
||||
eventBase
|
||||
|
||||
@ -112,16 +144,19 @@ func (event UserChanged) String() string {
|
||||
return fmt.Sprintf("UserChanged: UserID: %s", event.UserID)
|
||||
}
|
||||
|
||||
// UserRefreshed is emitted when an API refresh was issued for a user.
|
||||
type UserRefreshed struct {
|
||||
eventBase
|
||||
|
||||
UserID string
|
||||
UserID string
|
||||
CancelEventPool bool
|
||||
}
|
||||
|
||||
func (event UserRefreshed) String() string {
|
||||
return fmt.Sprintf("UserRefreshed: UserID: %s", event.UserID)
|
||||
}
|
||||
|
||||
// AddressModeChanged is emitted when a user's address mode has changed.
|
||||
type AddressModeChanged struct {
|
||||
eventBase
|
||||
|
||||
@ -133,3 +168,14 @@ type AddressModeChanged struct {
|
||||
func (event AddressModeChanged) String() string {
|
||||
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
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
)
|
||||
|
||||
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, 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,
|
||||
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, 0x33,
|
||||
}
|
||||
|
||||
@ -8,7 +8,6 @@ package proto
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
grpc "google.golang.org/grpc"
|
||||
codes "google.golang.org/grpc/codes"
|
||||
status "google.golang.org/grpc/status"
|
||||
|
||||
2
internal/frontend/.gitignore
vendored
2
internal/frontend/.gitignore
vendored
@ -10,5 +10,5 @@ rcc_cgo_*.go
|
||||
*.qmlc
|
||||
|
||||
# Generated file
|
||||
bridge-gui/bridge-gui/Version.h
|
||||
bridge-gui/bridge-gui/BuildConfig.h
|
||||
bridge-gui/bridge-gui/Resources.rc
|
||||
|
||||
@ -53,6 +53,7 @@ void GRPCQtProxy::connectSignals() {
|
||||
connect(this, &GRPCQtProxy::logoutUserReceived, &usersTab, &UsersTab::logoutUser);
|
||||
connect(this, &GRPCQtProxy::setUserSplitModeReceived, &usersTab, &UsersTab::setUserSplitMode);
|
||||
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.
|
||||
//****************************************************************************************************************************************************
|
||||
|
||||
@ -52,6 +52,7 @@ public: // member functions.
|
||||
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 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 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.
|
||||
@ -72,6 +73,7 @@ signals:
|
||||
void setDiskCachePathReceived(QString const &path); ///< Signal for the setDiskCachePath 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 sendBadEventUserFeedbackReceived(QString const &userID, bool doResync); ///< Signal for the SendBadEventUserFeedback 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 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.
|
||||
//****************************************************************************************************************************************************
|
||||
Status GRPCService::GuiReady(ServerContext *, Empty const *, Empty *) {
|
||||
Status GRPCService::GuiReady(ServerContext *, Empty const *, GuiReadyResponse *response) {
|
||||
app().log().debug(__FUNCTION__);
|
||||
app().mainWindow().settingsTab().setGUIReady(true);
|
||||
response->set_showsplashscreen(app().mainWindow().settingsTab().showSplashScreen());
|
||||
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.
|
||||
/// \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.
|
||||
/// \return The status for the call.
|
||||
@ -752,7 +742,7 @@ Status GRPCService::ConfigureUserAppleMail(ServerContext *, ConfigureAppleMailRe
|
||||
/// \param[in] writer The writer
|
||||
/// \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__);
|
||||
{
|
||||
QMutexLocker locker(&eventStreamMutex_);
|
||||
@ -767,19 +757,19 @@ Status GRPCService::RunEventStream(ServerContext *, EventStreamRequest const *re
|
||||
|
||||
while (true) {
|
||||
QMutexLocker locker(&eventStreamMutex_);
|
||||
if (eventStreamShouldStop_) {
|
||||
if (eventStreamShouldStop_ || ctx->IsCancelled()) {
|
||||
qtProxy_.setIsStreaming(false);
|
||||
qtProxy_.setClientPlatform(QString());
|
||||
isStreaming_ = false;
|
||||
return Status::OK;
|
||||
}
|
||||
|
||||
|
||||
if (eventQueue_.isEmpty()) {
|
||||
locker.unlock();
|
||||
QThread::msleep(100);
|
||||
continue;
|
||||
}
|
||||
|
||||
SPStreamEvent const event = eventQueue_.front();
|
||||
eventQueue_.pop_front();
|
||||
locker.unlock();
|
||||
|
||||
@ -41,12 +41,10 @@ public: // member functions.
|
||||
bool isStreaming() const; ///< Check if the service is currently streaming events.
|
||||
grpc::Status CheckTokens(::grpc::ServerContext *context, ::google::protobuf::StringValue const *request, ::google::protobuf::StringValue *response) override;
|
||||
grpc::Status AddLogEntry(::grpc::ServerContext *, ::grpc::AddLogEntryRequest const *request, ::google::protobuf::Empty *) override;
|
||||
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 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 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 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;
|
||||
@ -90,12 +88,12 @@ public: // member functions.
|
||||
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 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 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 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;
|
||||
|
||||
bool sendEvent(bridgepp::SPStreamEvent const &event); ///< Queue an event for sending through the event stream.
|
||||
|
||||
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.
|
||||
//****************************************************************************************************************************************************
|
||||
@ -450,7 +442,6 @@ void SettingsTab::resetUI() {
|
||||
ui_.editCurrentEmailClient->setText("Thunderbird/102.0.3");
|
||||
ui_.checkShowOnStartup->setChecked(true);
|
||||
ui_.checkShowSplashScreen->setChecked(false);
|
||||
ui_.checkIsFirstGUIStart->setChecked(false);
|
||||
ui_.checkAutostart->setChecked(true);
|
||||
ui_.checkBetaEnabled->setChecked(true);
|
||||
ui_.checkAllMailVisible->setChecked(true);
|
||||
|
||||
@ -42,7 +42,6 @@ public: // member functions.
|
||||
void setGUIReady(bool ready); ///< Set the GUI as ready.
|
||||
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 isFirstGUIStart() const; ///< Get the value for the 'Is First GUI Start' check.
|
||||
bool isAutostartOn() const; ///< Get the value for the 'Autostart' check.
|
||||
bool isBetaEnabled() const; ///< Get the value for the 'Beta Enabled' check.
|
||||
bool isAllMailVisible() const; ///< Get the value for the 'All Mail Visible' check.
|
||||
|
||||
@ -124,16 +124,6 @@
|
||||
</widget>
|
||||
</item>
|
||||
<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">
|
||||
<property name="text">
|
||||
<string>Autostart</string>
|
||||
@ -143,7 +133,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<item row="1" column="0">
|
||||
<widget class="QCheckBox" name="checkBetaEnabled">
|
||||
<property name="text">
|
||||
<string>Beta Enabled</string>
|
||||
@ -153,14 +143,17 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QCheckBox" name="checkAutomaticUpdate">
|
||||
<item row="1" column="1">
|
||||
<widget class="QCheckBox" name="checkAllMailVisible">
|
||||
<property name="text">
|
||||
<string>Automatic Update</string>
|
||||
<string>Show 'All Mail'</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<item row="1" column="2">
|
||||
<widget class="QCheckBox" name="checkDarkTheme">
|
||||
<property name="text">
|
||||
<string>Dark Theme</string>
|
||||
@ -170,13 +163,10 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QCheckBox" name="checkAllMailVisible">
|
||||
<item row="2" column="0">
|
||||
<widget class="QCheckBox" name="checkAutomaticUpdate">
|
||||
<property name="text">
|
||||
<string>Show 'All Mail'</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>false</bool>
|
||||
<string>Automatic Update</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@ -901,12 +891,6 @@
|
||||
<tabstop>editCurrentEmailClient</tabstop>
|
||||
<tabstop>checkShowOnStartup</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>spinPortIMAP</tabstop>
|
||||
<tabstop>spinPortSMTP</tabstop>
|
||||
|
||||
@ -51,6 +51,7 @@ UsersTab::UsersTab(QWidget *parent)
|
||||
connect(ui_.buttonEditUser, &QPushButton::clicked, this, &UsersTab::onEditUserButton);
|
||||
connect(ui_.tableUserList, &QTableView::doubleClicked, this, &UsersTab::onEditUserButton);
|
||||
connect(ui_.buttonRemoveUser, &QPushButton::clicked, this, &UsersTab::onRemoveUserButton);
|
||||
connect(ui_.buttonUserBadEvent, &QPushButton::clicked, this, &UsersTab::onSendUserBadEvent);
|
||||
connect(ui_.checkUsernamePasswordError, &QCheckBox::toggled, this, &UsersTab::updateGUIState);
|
||||
|
||||
users_.append(randomUser());
|
||||
@ -96,6 +97,8 @@ void UsersTab::onEditUserButton() {
|
||||
if (grpc.isStreaming()) {
|
||||
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() {
|
||||
bool const hasSelectedUser = ui_.tableUserList->selectionModel()->hasSelection();
|
||||
SPUser const user = selectedUser();
|
||||
bool const hasSelectedUser = user.get();
|
||||
ui_.buttonEditUser->setEnabled(hasSelectedUser);
|
||||
ui_.buttonRemoveUser->setEnabled(hasSelectedUser);
|
||||
ui_.groupBoxBadEvent->setEnabled(hasSelectedUser && (UserState::SignedOut != user->state()));
|
||||
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) {
|
||||
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 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 processBadEventUserFeedback(QString const& userID, bool doResync); ///< Slot for the reception of a bad event user feedback.
|
||||
|
||||
private slots:
|
||||
void onAddUserButton(); ///< Add a user to the user list.
|
||||
void onEditUserButton(); ///< Edit the currently selected user.
|
||||
void onRemoveUserButton(); ///< Remove the currently selected user.
|
||||
void onSelectionChanged(QItemSelection, QItemSelection); ///< Slot for the change of the selection.
|
||||
void onSendUserBadEvent(); ///< Slot for the 'Send Bad Event Error' button.
|
||||
void updateGUIState(); ///< Update the GUI state.
|
||||
|
||||
private: // member functions.
|
||||
|
||||
@ -67,7 +67,53 @@
|
||||
</spacer>
|
||||
</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">
|
||||
<size>
|
||||
<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.
|
||||
//****************************************************************************************************************************************************
|
||||
|
||||
@ -43,6 +43,7 @@ public: // member functions.
|
||||
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.
|
||||
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.
|
||||
QList<bridgepp::SPUser> users() const; ///< Return a copy of the user list.
|
||||
|
||||
|
||||
@ -28,6 +28,10 @@
|
||||
|
||||
using namespace bridgepp;
|
||||
|
||||
namespace {
|
||||
QString const noWindowFlag = "--no-window"; ///< The no-window command-line flag.
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The AppController instance.
|
||||
@ -68,13 +72,34 @@ ProcessMonitor *AppController::bridgeMonitor() const {
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] function The function that caught the exception.
|
||||
/// \param[in] message The error message.
|
||||
/// \param[in] exception The exception that triggered the fatal error.
|
||||
//****************************************************************************************************************************************************
|
||||
void AppController::onFatalError(QString const &function, QString const &message) {
|
||||
QString const fullMessage = QString("%1(): %2").arg(function, message);
|
||||
reportSentryException(SENTRY_LEVEL_ERROR, "AppController got notified of a fatal error", "Exception", fullMessage.toLocal8Bit());
|
||||
QMessageBox::critical(nullptr, tr("Error"), message);
|
||||
log().fatal(fullMessage);
|
||||
void AppController::onFatalError(Exception const &exception) {
|
||||
sentry_uuid_t uuid = reportSentryException("AppController got notified of a fatal error", exception);
|
||||
|
||||
QMessageBox::critical(nullptr, tr("Error"), exception.what());
|
||||
restart(true);
|
||||
log().fatal(QString("reportID: %1 Captured exception: %2").arg(QByteArray(uuid.bytes, 16).toHex(), exception.detailedWhat()));
|
||||
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
|
||||
|
||||
|
||||
// @formatter:off
|
||||
class QMLBackend;
|
||||
|
||||
|
||||
namespace bridgepp {
|
||||
class Log;
|
||||
|
||||
|
||||
class Overseer;
|
||||
|
||||
|
||||
class GRPCClient;
|
||||
|
||||
|
||||
class ProcessMonitor;
|
||||
class Exception;
|
||||
}
|
||||
// @formatter:off
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
@ -55,18 +50,22 @@ public: // member functions.
|
||||
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
|
||||
bridgepp::ProcessMonitor *bridgeMonitor() const; ///< Return the bridge worker.
|
||||
void setLauncherArgs(const QString& launcher, const QStringList& args);
|
||||
|
||||
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
|
||||
AppController(); ///< Default constructor.
|
||||
void restart(bool isCrashing = false); ///< Restart the app.
|
||||
|
||||
private: // data members
|
||||
std::unique_ptr<QMLBackend> backend_; ///< The backend.
|
||||
std::unique_ptr<bridgepp::GRPCClient> grpc_; ///< The RPC client.
|
||||
std::unique_ptr<bridgepp::Log> log_; ///< The log.
|
||||
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_REVISION "@BRIDGE_REVISION@"
|
||||
#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
|
||||
@ -85,20 +85,12 @@ message(STATUS "Using Qt ${Qt6_VERSION}")
|
||||
#*****************************************************************************************************************************************************
|
||||
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
|
||||
#*****************************************************************************************************************************************************
|
||||
|
||||
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)
|
||||
add_subdirectory(../bridgepp bridgepp)
|
||||
@ -120,9 +112,10 @@ add_executable(bridge-gui
|
||||
BridgeApp.cpp BridgeApp.h
|
||||
CommandLine.cpp CommandLine.h
|
||||
EventStreamWorker.cpp EventStreamWorker.h
|
||||
LogUtils.cpp LogUtils.h
|
||||
main.cpp
|
||||
Pch.h
|
||||
Version.h
|
||||
BuildConfig.h
|
||||
QMLBackend.cpp QMLBackend.h
|
||||
UserList.cpp UserList.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.
|
||||
// Arguments may contain some bridge flags.
|
||||
if (arg == softwareRendererFlag) {
|
||||
options.bridgeGuiArgs.append(arg);
|
||||
options.useSoftwareRenderer = true;
|
||||
}
|
||||
if (arg == noWindowFlag) {
|
||||
@ -113,10 +114,12 @@ CommandLineOptions parseCommandLine(int argc, char *argv[]) {
|
||||
else if (arg == "--attach" || arg == "-a") {
|
||||
// we don't keep the attach mode within the args since we don't need it for Bridge.
|
||||
options.attach = true;
|
||||
options.bridgeGuiArgs.append(arg);
|
||||
}
|
||||
#endif
|
||||
else {
|
||||
options.bridgeArgs.append(arg);
|
||||
options.bridgeGuiArgs.append(arg);
|
||||
}
|
||||
}
|
||||
if (!flagFound) {
|
||||
|
||||
@ -28,6 +28,7 @@
|
||||
//****************************************************************************************************************************************************
|
||||
struct CommandLineOptions {
|
||||
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.
|
||||
bool attach { false }; ///< Is the application running in attached mode?
|
||||
bridgepp::Log::Level logLevel { bridgepp::Log::defaultLevel }; ///< The log level
|
||||
|
||||
@ -52,7 +52,7 @@ void EventStreamReader::run() {
|
||||
emit finished();
|
||||
}
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
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 "EventStreamWorker.h"
|
||||
#include "Version.h"
|
||||
#include "BuildConfig.h"
|
||||
#include "LogUtils.h"
|
||||
#include <bridgepp/GRPC/GRPCClient.h>
|
||||
#include <bridgepp/Exception/Exception.h>
|
||||
#include <bridgepp/Worker/Overseer.h>
|
||||
#include <bridgepp/BridgeUtils.h>
|
||||
|
||||
|
||||
#define HANDLE_EXCEPTION(x) try { x } \
|
||||
catch (Exception const &e) { emit fatalError(__func__, e.qwhat()); } \
|
||||
catch (...) { emit fatalError(__func__, QString("An unknown exception occurred")); }
|
||||
catch (Exception const &e) { emit fatalError(e); } \
|
||||
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_QSTRING(x) HANDLE_EXCEPTION(x) return QString();
|
||||
#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);
|
||||
this->connectGrpcEvents();
|
||||
|
||||
QString error;
|
||||
if (app().grpc().connectToServer(serviceConfig, app().bridgeMonitor(), error)) {
|
||||
app().log().info("Connected to backend via gRPC service.");
|
||||
} else {
|
||||
throw Exception(QString("Cannot connectToServer to go backend via gRPC: %1").arg(error));
|
||||
}
|
||||
app().grpc().connectToServer(serviceConfig, app().bridgeMonitor());
|
||||
app().log().info("Connected to backend via gRPC service.");
|
||||
|
||||
QString 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).
|
||||
app().grpc().showSplashScreen(showSplashScreen_);
|
||||
app().grpc().goos(goos_);
|
||||
app().grpc().logsPath(logsPath_);
|
||||
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.
|
||||
//****************************************************************************************************************************************************
|
||||
@ -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.
|
||||
//****************************************************************************************************************************************************
|
||||
@ -603,7 +595,8 @@ void QMLBackend::setDiskCachePath(QUrl const &path) const {
|
||||
void QMLBackend::login(QString const &username, QString const &password) const {
|
||||
HANDLE_EXCEPTION(
|
||||
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);
|
||||
)
|
||||
@ -691,9 +684,11 @@ void QMLBackend::changeKeychain(QString const &keychain) {
|
||||
//****************************************************************************************************************************************************
|
||||
//
|
||||
//****************************************************************************************************************************************************
|
||||
void QMLBackend::guiReady() const {
|
||||
void QMLBackend::guiReady() {
|
||||
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] 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
|
||||
connect(client, &GRPCClient::userDisconnected, this, &QMLBackend::userDisconnected);
|
||||
connect(client, &GRPCClient::userBadEvent, this, &QMLBackend::onUserBadEvent);
|
||||
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 "Version.h"
|
||||
#include "BuildConfig.h"
|
||||
#include "UserList.h"
|
||||
#include <bridgepp/GRPC/GRPCClient.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.
|
||||
|
||||
// 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 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.
|
||||
@ -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 smtpPort READ smtpPort WRITE setSMTPPort NOTIFY smtpPortChanged)
|
||||
Q_PROPERTY(bool isDoHEnabled READ isDoHEnabled NOTIFY isDoHEnabledChanged)
|
||||
Q_PROPERTY(bool isFirstGUIStart READ isFirstGUIStart)
|
||||
Q_PROPERTY(bool isAutomaticUpdateOn READ isAutomaticUpdateOn NOTIFY isAutomaticUpdateOnChanged)
|
||||
Q_PROPERTY(QString currentEmailClient READ currentEmailClient NOTIFY currentEmailClientChanged)
|
||||
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.
|
||||
int smtpPort() const; ///< Getter for the 'smtpPort' 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.
|
||||
QString currentEmailClient() const; ///< Getter for the 'currentEmail' 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 updateCurrentMailClient(); ///< Slot for the change of the current mail client.
|
||||
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 restart() const; ///< Slot for the restart signal.
|
||||
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 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 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
|
||||
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 onLoginFinished(QString const &userID, bool wasSignedOut); ///< Slot for LoginFinished 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
|
||||
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 apiCertIssue(); ///< Signal for the 'apiCertIssue' 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 internetOn(); ///< Signal for the 'internetOn' 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 hideMainWindow(); ///< Signal for the 'hideMainWindow' 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.
|
||||
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
|
||||
void retrieveUserList(); ///< Retrieve the list of users via gRPC.
|
||||
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
|
||||
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.
|
||||
bool useSSLForIMAP_ { false }; ///< The cached value for useSSLForIMAP.
|
||||
bool useSSLForSMTP_ { false }; ///< The cached value for useSSLForSMTP.
|
||||
QList<QString> badEventDisplayQueue_; ///< THe queue for displaying 'bad event feedback request dialog'.
|
||||
|
||||
friend class AppController;
|
||||
};
|
||||
|
||||
@ -42,6 +42,7 @@
|
||||
<file>qml/icons/ic-other-mail-clients.svg</file>
|
||||
<file>qml/icons/ic-plus.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-three-dots-vertical.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/>.
|
||||
|
||||
#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";
|
||||
|
||||
|
||||
void reportSentryEvent(sentry_level_t level, const char *message) {
|
||||
auto event = sentry_value_new_message_event(level, LoggerName, message);
|
||||
sentry_capture_event(event);
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The temporary file used for sentry attachment.
|
||||
//****************************************************************************************************************************************************
|
||||
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);
|
||||
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>
|
||||
|
||||
|
||||
void reportSentryEvent(sentry_level_t level, const char *message);
|
||||
void reportSentryException(sentry_level_t level, const char *message, const char *exceptionType, const char *exception);
|
||||
void setSentryReportScope();
|
||||
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
|
||||
|
||||
@ -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.
|
||||
//****************************************************************************************************************************************************
|
||||
|
||||
@ -54,6 +54,7 @@ signals:
|
||||
|
||||
public:
|
||||
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
|
||||
void onUserChanged(QString const &userID);
|
||||
|
||||
@ -75,6 +75,16 @@ function check_exit() {
|
||||
|
||||
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
|
||||
. $vcpkgBootstrap -disableMetrics
|
||||
. $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" `
|
||||
-DBRIDGE_APP_FULL_NAME="$bridgeFullName" `
|
||||
-DBRIDGE_VENDOR="$bridgeVendor" `
|
||||
-DBRIDGE_REVISION="$REVISION_HASH" `
|
||||
-DBRIDGE_APP_VERSION="$bridgeVersion" `
|
||||
-DBRIDGE_BUILD_TIME="$bridgeBuidTime" `
|
||||
-DBRIDGE_DSN_SENTRY="$bridgeDsnSentry" `
|
||||
-DBRIDGE_BUILD_ENV="$bridgeBuildEnv" `
|
||||
-S . -B $buildDir
|
||||
|
||||
check_exit "CMake failed"
|
||||
|
||||
@ -55,7 +55,10 @@ BRIDGE_VENDOR=${BRIDGE_VENDOR:-"Proton AG"}
|
||||
BUILD_CONFIG=${BRIDGE_GUI_BUILD_CONFIG:-Debug}
|
||||
BUILD_DIR=$(echo "./cmake-build-${BUILD_CONFIG}" | tr '[:upper:]' '[:lower:]')
|
||||
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}
|
||||
check_exit "Failed to initialize vcpkg as a submodule."
|
||||
|
||||
@ -93,6 +96,10 @@ cmake \
|
||||
-DCMAKE_BUILD_TYPE="${BUILD_CONFIG}" \
|
||||
-DBRIDGE_APP_FULL_NAME="${BRIDGE_APP_FULL_NAME}" \
|
||||
-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}" \
|
||||
-G Ninja \
|
||||
-S . \
|
||||
|
||||
@ -21,14 +21,15 @@
|
||||
#include "CommandLine.h"
|
||||
#include "QMLBackend.h"
|
||||
#include "SentryUtils.h"
|
||||
#include "Version.h"
|
||||
#include "BuildConfig.h"
|
||||
#include "LogUtils.h"
|
||||
#include <bridgepp/BridgeUtils.h>
|
||||
#include <bridgepp/Exception/Exception.h>
|
||||
#include <bridgepp/FocusGRPC/FocusGRPCClient.h>
|
||||
#include <bridgepp/Log/Log.h>
|
||||
#include <bridgepp/ProcessMonitor.h>
|
||||
#include <sentry.h>
|
||||
#include <project_sentry_config.h>
|
||||
#include <SentryUtils.h>
|
||||
|
||||
|
||||
#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 exeName = "bridge" + exeSuffix; ///< The bridge executable file name.*
|
||||
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
|
||||
@ -237,7 +239,8 @@ void focusOtherInstance() {
|
||||
}
|
||||
catch (Exception const &e) {
|
||||
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.
|
||||
//****************************************************************************************************************************************************
|
||||
void launchBridge(QStringList const &args) {
|
||||
const QString launchBridge(QStringList const &args) {
|
||||
UPOverseer &overseer = app().bridgeOverseer();
|
||||
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(" ")));
|
||||
overseer = std::make_unique<Overseer>(new ProcessMonitor(bridgeExePath, params, nullptr), nullptr);
|
||||
overseer->startWorker(true);
|
||||
return bridgeExePath;
|
||||
}
|
||||
|
||||
|
||||
@ -289,19 +293,12 @@ void closeBridgeApp() {
|
||||
//****************************************************************************************************************************************************
|
||||
int main(int argc, char *argv[]) {
|
||||
// Init sentry.
|
||||
sentry_options_t *sentryOptions = sentry_options_new();
|
||||
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);
|
||||
sentry_options_t *sentryOptions = newSentryOptions(PROJECT_DSN_SENTRY, sentryCacheDir().toStdString().c_str());
|
||||
|
||||
if (sentry_init(sentryOptions) != 0) {
|
||||
std::cerr << "Failed to initialize sentry" << std::endl;
|
||||
}
|
||||
|
||||
setSentryReportScope();
|
||||
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,
|
||||
@ -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
|
||||
// these outputs and output them on the command-line.
|
||||
log.setLevel(cliOptions.logLevel);
|
||||
|
||||
QString bridgeexec;
|
||||
if (!cliOptions.attach) {
|
||||
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.
|
||||
GRPCClient::removeServiceConfigFile();
|
||||
launchBridge(cliOptions.bridgeArgs);
|
||||
bridgeexec = launchBridge(cliOptions.bridgeArgs);
|
||||
}
|
||||
|
||||
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");
|
||||
log.info(QString("Qt Quick renderer: %1").arg(QQuickWindow::sceneGraphBackend()));
|
||||
|
||||
|
||||
QQmlApplicationEngine engine;
|
||||
std::unique_ptr<QQmlComponent> rootComponent(createRootQmlComponent(engine));
|
||||
std::unique_ptr<QObject> rootObject(rootComponent->create(engine.rootContext()));
|
||||
@ -398,7 +397,16 @@ int main(int argc, char *argv[]) {
|
||||
int result = 0;
|
||||
if (!startError) {
|
||||
// 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();
|
||||
}
|
||||
|
||||
@ -420,9 +428,9 @@ int main(int argc, char *argv[]) {
|
||||
return result;
|
||||
}
|
||||
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());
|
||||
QTextStream(stderr) << e.qwhat() << "\n";
|
||||
QTextStream(stderr) << "reportID: " << QByteArray(uuid.bytes, 16).toHex() << " Captured exception :" << e.detailedWhat() << "\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,11 +36,11 @@ Item {
|
||||
if (root.usedFraction < .75) return root.colorScheme.signal_warning
|
||||
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 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 totalSafe = root.reasonableBytes(total)
|
||||
if (totalSafe == 0 || usedSafe == 0) return 0
|
||||
@ -63,6 +63,10 @@ Item {
|
||||
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
|
||||
implicitHeight : children[0].implicitHeight
|
||||
|
||||
@ -77,7 +81,7 @@ Item {
|
||||
anchors {
|
||||
top: root.top
|
||||
left: root.left
|
||||
right: root.rigth
|
||||
right: root.right
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
@ -115,12 +119,10 @@ Item {
|
||||
spacing: 0
|
||||
|
||||
Label {
|
||||
Layout.maximumWidth: root.width - (
|
||||
root._spacing + avatar.width
|
||||
)
|
||||
|
||||
id: labelEmail
|
||||
Layout.maximumWidth: root.width - (root._spacing + avatar.width)
|
||||
colorScheme: root.colorScheme
|
||||
text: root.user ? user.username : ""
|
||||
text: primaryEmail()
|
||||
type: {
|
||||
switch (root.type) {
|
||||
case AccountDelegate.SmallView: return Label.Body
|
||||
@ -128,6 +130,29 @@ Item {
|
||||
}
|
||||
}
|
||||
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 }
|
||||
@ -155,7 +180,7 @@ Item {
|
||||
property string dots: ""
|
||||
interval: 250;
|
||||
repeat: true;
|
||||
running: root.user && (root.user.state === EUserState.Locked)
|
||||
running: (root.user != null) && (root.user.state === EUserState.Locked)
|
||||
onTriggered: {
|
||||
dots = dots + "."
|
||||
if (dots.length > 3)
|
||||
|
||||
@ -87,8 +87,8 @@ QtObject {
|
||||
mainWindow.showAndRise()
|
||||
}
|
||||
|
||||
onShowSignIn: {
|
||||
mainWindow.showSignIn(username)
|
||||
onSelectUser: function(userID) {
|
||||
mainWindow.selectUser(userID)
|
||||
mainWindow.showAndRise()
|
||||
}
|
||||
|
||||
@ -117,7 +117,7 @@ QtObject {
|
||||
// fit above
|
||||
_y = iconRect.top - 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)
|
||||
return Qt.point(_x, _y)
|
||||
}
|
||||
@ -125,7 +125,7 @@ QtObject {
|
||||
// fit below
|
||||
_y = iconRect.bottom
|
||||
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)
|
||||
return Qt.point(_x, _y)
|
||||
}
|
||||
@ -133,7 +133,7 @@ QtObject {
|
||||
// fit to the left
|
||||
_x = iconRect.left - 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)
|
||||
return Qt.point(_x, _y)
|
||||
}
|
||||
@ -141,12 +141,12 @@ QtObject {
|
||||
// fit to the right
|
||||
_x = iconRect.right
|
||||
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)
|
||||
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)
|
||||
_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)
|
||||
@ -281,11 +281,12 @@ QtObject {
|
||||
}
|
||||
}
|
||||
|
||||
if (Backend.showOnStartup) {
|
||||
Backend.guiReady()
|
||||
|
||||
if (Backend.showOnStartup || Backend.showSplashScreen) {
|
||||
mainWindow.showAndRise()
|
||||
}
|
||||
|
||||
Backend.guiReady()
|
||||
}
|
||||
|
||||
function setColorScheme() {
|
||||
|
||||
@ -299,7 +299,7 @@ ColumnLayout {
|
||||
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
|
||||
text: user && user.addresses ? user.addresses.join("\n") : "user@protonmail.com"
|
||||
Layout.fillWidth: true
|
||||
|
||||
@ -178,7 +178,7 @@ Window {
|
||||
signal toggleSplitModeFinished()
|
||||
|
||||
function configureAppleMail(address){
|
||||
userSignal("confugure apple mail "+address)
|
||||
userSignal("configure apple mail "+address)
|
||||
}
|
||||
|
||||
function logout(){
|
||||
|
||||
@ -170,6 +170,10 @@ SettingsView {
|
||||
}
|
||||
}
|
||||
|
||||
function setDescription(message) {
|
||||
description.text = message
|
||||
}
|
||||
|
||||
function setDefaultValue() {
|
||||
description.text = ""
|
||||
address.text = root.selectedAddress
|
||||
|
||||
@ -188,7 +188,7 @@ Item {
|
||||
if (user.state !== EUserState.SignedOut) {
|
||||
rightContent.showAccount()
|
||||
} else {
|
||||
signIn.username = user.username
|
||||
signIn.username = user.primaryEmailOrUsername()
|
||||
rightContent.showSignIn()
|
||||
}
|
||||
}
|
||||
@ -255,7 +255,8 @@ Item {
|
||||
return Backend.users.get(accounts.currentIndex)
|
||||
}
|
||||
onShowSignIn: {
|
||||
signIn.username = this.user.username
|
||||
var user = this.user
|
||||
signIn.username = user ? user.primaryEmailOrUsername() : ""
|
||||
rightContent.showSignIn()
|
||||
}
|
||||
onShowSetupGuide: function(user, address) {
|
||||
@ -347,6 +348,7 @@ Item {
|
||||
}
|
||||
|
||||
BugReportView { // 8
|
||||
id: bugReport
|
||||
colorScheme: root.colorScheme
|
||||
selectedAddress: {
|
||||
if (accounts.currentIndex < 0) return ""
|
||||
@ -398,4 +400,24 @@ Item {
|
||||
signIn.username = username
|
||||
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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
@ -108,9 +108,10 @@ SettingsView {
|
||||
|
||||
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.version).
|
||||
arg(Backend.buildYear()).
|
||||
arg(Backend.vendor).
|
||||
arg(link(Backend.licensePath, qsTr("License"))).
|
||||
arg(link(Backend.dependencyLicensesLink, qsTr("Dependencies"))).
|
||||
|
||||
@ -86,6 +86,10 @@ ApplicationWindow {
|
||||
root.showAndRise()
|
||||
}
|
||||
|
||||
function onSelectUser(userID) {
|
||||
root.selectUser(userID)
|
||||
}
|
||||
|
||||
function onLoginFinished(index, wasSignedOut) {
|
||||
var user = Backend.users.get(index)
|
||||
if (user && !wasSignedOut) {
|
||||
@ -116,7 +120,7 @@ ApplicationWindow {
|
||||
}
|
||||
|
||||
if ((Backend.users.count === 1) && (u.state === EUserState.SignedOut)) {
|
||||
showSignIn(u.username)
|
||||
showSignIn(u.primaryEmailOrUsername())
|
||||
return 0
|
||||
}
|
||||
|
||||
@ -165,7 +169,6 @@ ApplicationWindow {
|
||||
root.showSetup(null,"")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
NotificationPopups {
|
||||
@ -182,6 +185,11 @@ ApplicationWindow {
|
||||
function showLocalCacheSettings() { contentWrapper.showLocalCacheSettings() }
|
||||
function showSettings() { contentWrapper.showSettings() }
|
||||
function showHelp() { contentWrapper.showHelp() }
|
||||
function selectUser(userID) { contentWrapper.selectUser(userID) }
|
||||
|
||||
function showBugReportAndPrefill(message) {
|
||||
contentWrapper.showBugReportAndPrefill(message)
|
||||
}
|
||||
|
||||
function showSignIn(username) {
|
||||
if (contentLayout.currentIndex == 1) return
|
||||
|
||||
@ -129,6 +129,11 @@ Item {
|
||||
notification: root.notifications.noActiveKeyForRecipient
|
||||
}
|
||||
|
||||
NotificationDialog {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.userBadEvent
|
||||
}
|
||||
|
||||
NotificationDialog {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.genericError
|
||||
|
||||
@ -80,6 +80,7 @@ QtObject {
|
||||
root.addressChanged,
|
||||
root.apiCertIssue,
|
||||
root.noActiveKeyForRecipient,
|
||||
root.userBadEvent,
|
||||
root.genericError
|
||||
]
|
||||
|
||||
@ -1045,8 +1046,8 @@ QtObject {
|
||||
property Notification apiCertIssue: Notification {
|
||||
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. " +
|
||||
"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>.")
|
||||
"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>.")
|
||||
|
||||
brief: title
|
||||
icon: "./icons/ic-exclamation-circle-filled.svg"
|
||||
@ -1085,7 +1086,7 @@ QtObject {
|
||||
|
||||
function onNoActiveKeyForRecipient(email) {
|
||||
root.noActiveKeyForRecipient.description = qsTr("There are no active keys to encrypt your message to %1. "+
|
||||
"Please update the setting for this contact.").arg(email)
|
||||
"Please update the setting for this contact.").arg(email)
|
||||
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 {
|
||||
title: "#PlaceholderText#"
|
||||
description: "#PlaceholderText#"
|
||||
icon: "./icons/ic-exclamation-circle-filled.svg"
|
||||
type: Notification.NotificationType.Danger
|
||||
group: Notifications.Group.Dialogs
|
||||
Connections {
|
||||
target: Backend
|
||||
function onGenericError(title, description) {
|
||||
root.genericError.title = title
|
||||
root.genericError.description = description
|
||||
root.genericError.active = true;
|
||||
}
|
||||
Connections {
|
||||
target: Backend
|
||||
function onGenericError(title, description) {
|
||||
root.genericError.title = title
|
||||
root.genericError.description = description
|
||||
root.genericError.active = true;
|
||||
}
|
||||
}
|
||||
|
||||
action: [
|
||||
Action {
|
||||
|
||||
@ -28,7 +28,7 @@ T.Button {
|
||||
property alias secondary: control.flat
|
||||
readonly property bool primary: !secondary
|
||||
readonly property bool isIcon: control.text === ""
|
||||
|
||||
readonly property bool hasTextAndIcon: (control.text !== "") && (iconImage.source.toString().length > 0)
|
||||
property bool loading: false
|
||||
|
||||
property bool borderless: false
|
||||
@ -67,7 +67,7 @@ T.Button {
|
||||
|
||||
contentItem: RowLayout {
|
||||
id: _contentItem
|
||||
spacing: control.spacing
|
||||
spacing: control.hasTextAndIcon ? control.spacing : 0
|
||||
|
||||
Proton.Label {
|
||||
colorScheme: root.colorScheme
|
||||
|
||||
@ -341,7 +341,7 @@ QtObject {
|
||||
case "windows":
|
||||
return "Segoe UI"
|
||||
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":
|
||||
return "Ubuntu"
|
||||
default:
|
||||
|
||||
@ -204,7 +204,7 @@ FocusScope {
|
||||
TextField {
|
||||
colorScheme: root.colorScheme
|
||||
id: usernameTextField
|
||||
label: qsTr("Username or email")
|
||||
label: qsTr("Email or username")
|
||||
focus: true
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 24
|
||||
@ -221,7 +221,7 @@ FocusScope {
|
||||
|
||||
validator: function(str) {
|
||||
if (str.length === 0) {
|
||||
return qsTr("Enter username or email")
|
||||
return qsTr("Enter email or username")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@ -39,11 +39,11 @@ Dialog {
|
||||
Image {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
sourceSize.width: 400
|
||||
sourceSize.height: 225
|
||||
sourceSize.width: 384
|
||||
sourceSize.height: 144
|
||||
|
||||
Layout.preferredWidth: 400
|
||||
Layout.preferredHeight: 225
|
||||
Layout.preferredWidth: 384
|
||||
Layout.preferredHeight: 144
|
||||
|
||||
source: "./icons/img-splash.png"
|
||||
}
|
||||
@ -58,27 +58,110 @@ Dialog {
|
||||
|
||||
type: Label.Title
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: qsTr("Updated Proton, unified protection")
|
||||
text: qsTr("What's new in Bridge")
|
||||
}
|
||||
|
||||
Label {
|
||||
colorScheme: root.colorScheme
|
||||
RowLayout {
|
||||
width: root.width
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignHCenter;
|
||||
Layout.preferredWidth: 336
|
||||
Layout.leftMargin: 24
|
||||
Layout.rightMargin: 24
|
||||
wrapMode: Text.WordWrap
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
type: Label.Body
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
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"))
|
||||
Label {
|
||||
colorScheme: root.colorScheme
|
||||
|
||||
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 {
|
||||
@ -90,16 +173,21 @@ Dialog {
|
||||
onClicked: Backend.showSplashScreen = false
|
||||
}
|
||||
|
||||
Image {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Label {
|
||||
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
|
||||
sourceSize.height: 32
|
||||
type: Label.Body
|
||||
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
|
||||
Layout.preferredHeight: 32
|
||||
|
||||
source: "/qml/icons/img-proton-logos.svg"
|
||||
onLinkActivated: function(link) { Qt.openUrlExternally(link) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -40,7 +40,7 @@ Window {
|
||||
signal showMainWindow()
|
||||
signal showHelp()
|
||||
signal showSettings()
|
||||
signal showSignIn(string username)
|
||||
signal selectUser(string userID)
|
||||
signal quit()
|
||||
|
||||
MouseArea {
|
||||
@ -229,7 +229,7 @@ Window {
|
||||
visible: viewItem.user ? (viewItem.user.state === EUserState.SignedOut) : false
|
||||
text: qsTr("Sign in")
|
||||
onClicked: {
|
||||
root.showSignIn(viewItem.username)
|
||||
root.selectUser(viewItem.user.id) // selectUser will show login screen if user is in SignedOut state.
|
||||
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"
|
||||
label: "42 Validator"
|
||||
hint: "Accepts only \"42\""
|
||||
assistiveText: "Type sometihng here, preferably 42"
|
||||
assistiveText: "Type something here, preferably 42"
|
||||
|
||||
wrapMode: TextInput.Wrap
|
||||
|
||||
|
||||
@ -149,7 +149,7 @@ RowLayout {
|
||||
placeholderText: "Type 42 here"
|
||||
label: "42 Validator"
|
||||
hint: "Accepts only \"42\""
|
||||
assistiveText: "Type sometihng here, preferably 42"
|
||||
assistiveText: "Type something here, preferably 42"
|
||||
|
||||
validator: function(str) {
|
||||
if (str === "42") {
|
||||
|
||||
@ -71,20 +71,20 @@ std::mt19937_64 &rng() {
|
||||
QString userConfigDir() {
|
||||
QString dir;
|
||||
#ifdef Q_OS_WIN
|
||||
dir = qgetenv ("AppData");
|
||||
dir = qEnvironmentVariable("AppData");
|
||||
if (dir.isEmpty())
|
||||
throw Exception("%AppData% is not defined.");
|
||||
#elif defined(Q_OS_IOS) || defined(Q_OS_DARWIN)
|
||||
dir = qgetenv("HOME");
|
||||
dir = qEnvironmentVariable("HOME");
|
||||
if (dir.isEmpty()) {
|
||||
throw Exception("$HOME is not defined.");
|
||||
}
|
||||
dir += "/Library/Application Support";
|
||||
#else
|
||||
dir = qgetenv ("XDG_CONFIG_HOME");
|
||||
dir = qEnvironmentVariable("XDG_CONFIG_HOME");
|
||||
if (dir.isEmpty())
|
||||
{
|
||||
dir = qgetenv ("HOME");
|
||||
dir = qEnvironmentVariable("HOME");
|
||||
if (dir.isEmpty())
|
||||
throw Exception("neither $XDG_CONFIG_HOME nor $HOME are defined");
|
||||
dir += "/.config";
|
||||
@ -104,20 +104,20 @@ QString userCacheDir() {
|
||||
QString dir;
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
dir = qgetenv ("LocalAppData");
|
||||
dir = qEnvironmentVariable("LocalAppData");
|
||||
if (dir.isEmpty())
|
||||
throw Exception("%LocalAppData% is not defined.");
|
||||
#elif defined(Q_OS_IOS) || defined(Q_OS_DARWIN)
|
||||
dir = qgetenv("HOME");
|
||||
dir = qEnvironmentVariable("HOME");
|
||||
if (dir.isEmpty()) {
|
||||
throw Exception("$HOME is not defined.");
|
||||
}
|
||||
dir += "/Library/Caches";
|
||||
#else
|
||||
dir = qgetenv ("XDG_CACHE_HOME");
|
||||
dir = qEnvironmentVariable("XDG_CACHE_HOME");
|
||||
if (dir.isEmpty())
|
||||
{
|
||||
dir = qgetenv ("HOME");
|
||||
dir = qEnvironmentVariable("HOME");
|
||||
if (dir.isEmpty())
|
||||
throw Exception("neither $XDG_CACHE_HOME nor $HOME are defined");
|
||||
dir += "/.cache";
|
||||
@ -138,10 +138,10 @@ QString userDataDir() {
|
||||
QString folder;
|
||||
|
||||
#ifdef Q_OS_LINUX
|
||||
QString dir = qgetenv ("XDG_DATA_HOME");
|
||||
QString dir = qEnvironmentVariable("XDG_DATA_HOME");
|
||||
if (dir.isEmpty())
|
||||
{
|
||||
dir = qgetenv ("HOME");
|
||||
dir = qEnvironmentVariable("HOME");
|
||||
if (dir.isEmpty())
|
||||
throw Exception("neither $XDG_DATA_HOME nor $HOME are defined");
|
||||
dir += "/.local/share";
|
||||
@ -149,7 +149,7 @@ QString userDataDir() {
|
||||
folder = QDir(dir).absoluteFilePath(configFolder);
|
||||
QDir().mkpath(folder);
|
||||
#else
|
||||
folder = userCacheDir();
|
||||
folder = userConfigDir();
|
||||
#endif
|
||||
|
||||
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
|
||||
|
||||
@ -49,6 +49,7 @@ OS os(); ///< Return the operating system.
|
||||
bool onLinux(); ///< Check if the OS is Linux.
|
||||
bool onMacOS(); ///< Check if the OS is macOS.
|
||||
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
|
||||
|
||||
@ -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()
|
||||
, 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
|
||||
: 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
|
||||
: 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
|
||||
//****************************************************************************************************************************************************
|
||||
QString const &Exception::qwhat() const noexcept {
|
||||
return what_;
|
||||
QString Exception::qwhat() const noexcept {
|
||||
return qwhat_;
|
||||
}
|
||||
|
||||
|
||||
@ -61,8 +75,38 @@ QString const &Exception::qwhat() const noexcept {
|
||||
/// \return A pointer to the description string of the exception.
|
||||
//****************************************************************************************************************************************************
|
||||
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
|
||||
|
||||
@ -31,17 +31,25 @@ namespace bridgepp {
|
||||
//****************************************************************************************************************************************************
|
||||
class Exception : public std::exception {
|
||||
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 &&ref) noexcept; ///< copy constructor
|
||||
Exception &operator=(Exception const &) = delete; ///< Disabled assignment operator
|
||||
Exception &operator=(Exception &&) = delete; ///< Disabled assignment operator
|
||||
~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
|
||||
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
|
||||
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.
|
||||
/// \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 newUserDisconnectedEvent(QString const &username); ///< Create a new UserDisconnectedEvent 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
|
||||
SPStreamEvent newGenericErrorEvent(grpc::ErrorCode errorCode); ///< Create a new GenericErrrorEvent event.
|
||||
|
||||
@ -88,8 +88,9 @@ GRPCConfig GRPCClient::waitAndRetrieveServiceConfig(qint64 timeoutMs, ProcessMon
|
||||
}
|
||||
|
||||
GRPCConfig sc;
|
||||
if (!sc.load(path)) {
|
||||
throw Exception("The gRPC service configuration file is invalid.");
|
||||
QString err;
|
||||
if (!sc.load(path, &err)) {
|
||||
throw Exception("The gRPC service configuration file is invalid.", err);
|
||||
}
|
||||
|
||||
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.
|
||||
/// \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 {
|
||||
serverToken_ = config.token.toStdString();
|
||||
QString address;
|
||||
@ -158,9 +158,10 @@ bool GRPCClient::connectToServer(GRPCConfig const &config, ProcessMonitor *serve
|
||||
this->logInfo("Successfully connected to gRPC server.");
|
||||
|
||||
QString const clientToken = QUuid::createUuid().toString();
|
||||
QString clientConfigPath = createClientConfigFile(clientToken);
|
||||
QString error;
|
||||
QString clientConfigPath = createClientConfigFile(clientToken, &error);
|
||||
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)));
|
||||
|
||||
@ -176,12 +177,9 @@ bool GRPCClient::connectToServer(GRPCConfig const &config, ProcessMonitor *serve
|
||||
}
|
||||
|
||||
log_->info("gRPC token was validated");
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception const &e) {
|
||||
outError = e.qwhat();
|
||||
return false;
|
||||
throw Exception("Cannot connect to Go backend via gRPC: " + e.qwhat(), e.details());
|
||||
}
|
||||
}
|
||||
|
||||
@ -222,17 +220,12 @@ grpc::Status GRPCClient::addLogEntry(Log::Level level, QString const &package, Q
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The status for the gRPC call.
|
||||
//****************************************************************************************************************************************************
|
||||
grpc::Status GRPCClient::guiReady() {
|
||||
return this->logGRPCCallStatus(stub_->GuiReady(this->clientContext().get(), empty, &empty), __FUNCTION__);
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \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__);
|
||||
grpc::Status GRPCClient::guiReady(bool &outShowSplashScreen) {
|
||||
GuiReadyResponse response;
|
||||
Status status = this->logGRPCCallStatus(stub_->GuiReady(this->clientContext().get(), empty, &response), __FUNCTION__);
|
||||
if (status.ok())
|
||||
outShowSplashScreen = response.showsplashscreen();
|
||||
return status;
|
||||
}
|
||||
|
||||
|
||||
@ -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.
|
||||
/// \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.
|
||||
/// \return The status code for the gRPC call.
|
||||
@ -1380,6 +1376,14 @@ void GRPCClient::processUserEvent(UserEvent const &event) {
|
||||
emit userChanged(userID);
|
||||
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:
|
||||
this->logError("Unknown User event received.");
|
||||
}
|
||||
|
||||
@ -59,12 +59,11 @@ public: // member functions.
|
||||
GRPCClient &operator=(GRPCClient const &) = delete; ///< Disabled assignment operator.
|
||||
GRPCClient &operator=(GRPCClient &&) = delete; ///< Disabled move assignment operator.
|
||||
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 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 isFirstGUIStart(bool &outIsFirst); ///< performs the "IsFirstGUIStart" gRPC call.
|
||||
grpc::Status guiReady(bool &outShowSplashScreen); ///< performs the "GuiReady" gRPC call.
|
||||
grpc::Status isAutostartOn(bool &outIsOn); ///< Performs the "isAutostartOn" gRPC call.
|
||||
grpc::Status setIsAutostartOn(bool on); ///< Performs the "setIsAutostartOn" 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 isPortFree(qint32 port, bool &outFree); ///< Performs the 'IsPortFree' 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 logsPath(QUrl &outPath); ///< Performs the 'LogsPath' 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 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 sendBadEventUserFeedback(QString const& userID, bool doResync); ///< Performs the 'SendBadEventUserFeedback' call.
|
||||
|
||||
signals:
|
||||
void toggleSplitModeFinished(QString const &userID);
|
||||
void userDisconnected(QString const &username);
|
||||
void userChanged(QString const &userID);
|
||||
void userBadEvent(QString const &userID, QString const& errorMessage);
|
||||
|
||||
public: // keychain related calls
|
||||
grpc::Status availableKeychains(QStringList &outKeychains);
|
||||
|
||||
@ -25,8 +25,7 @@ using namespace bridgepp;
|
||||
|
||||
namespace {
|
||||
|
||||
Exception const invalidFileException("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.
|
||||
Exception const invalidFileException("The content of the service configuration file is invalid"); // Exception for invalid config.
|
||||
QString const keyPort = "port"; ///< The JSON key for the port.
|
||||
QString const keyCert = "cert"; ///< The JSON key for the TLS certificate.
|
||||
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) {
|
||||
try {
|
||||
QFile file(path);
|
||||
if (!file.exists())
|
||||
throw Exception("The gRPC service configuration file does not exist.");
|
||||
|
||||
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());
|
||||
@ -93,7 +98,7 @@ bool GRPCConfig::load(QString const &path, QString *outError) {
|
||||
}
|
||||
catch (Exception const &e) {
|
||||
if (outError) {
|
||||
*outError = e.qwhat();
|
||||
*outError = QString("Error loading gRPC service configuration file '%1'.\n%2").arg(QFileInfo(path).absoluteFilePath(), e.qwhat());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@ -115,19 +120,19 @@ bool GRPCConfig::save(QString const &path, QString *outError) {
|
||||
|
||||
QFile file(path);
|
||||
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();
|
||||
if (array.size() != file.write(array)) {
|
||||
throw couldNotSaveException;
|
||||
throw Exception("An error occurred while writing to the file.");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception const &e) {
|
||||
if (outError) {
|
||||
*outError = e.qwhat();
|
||||
*outError = QString("Error saving gRPC service configuration file '%1'.\n%2").arg(QFileInfo(path).absoluteFilePath(), e.qwhat());
|
||||
}
|
||||
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
|
||||
|
||||
|
||||
@ -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[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 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 path, error;
|
||||
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()) {
|
||||
GRPCConfig config;
|
||||
config.token = token;
|
||||
if (!config.save(path)) {
|
||||
|
||||
if (!config.save(path, outError)) {
|
||||
return QString();
|
||||
}
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
if (outError)
|
||||
*outError = "no usable client configuration file name could be found.";
|
||||
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 grpcClientConfigBasePath(); ///< Return the path of the gRPC client config file.
|
||||
QString serverCertificatePath(); ///< Return the path of the server certificate.
|
||||
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.
|
||||
QString createClientConfigFile(QString const &token, QString *outError); ///< 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.
|
||||
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.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user