mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-10 12:46:46 +00:00
Compare commits
164 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 777ad369a2 | |||
| 715efaa087 | |||
| 606a8f134d | |||
| 84e92ca69f | |||
| 0f0f8b3461 | |||
| 761b98f02f | |||
| b19e16e4b8 | |||
| 407c9fe1a6 | |||
| 0b61f8f146 | |||
| 06eee89479 | |||
| e3a43e4ca8 | |||
| f876ffab52 | |||
| 0dcd4ca133 | |||
| 2562d1e77d | |||
| e1531c200c | |||
| c09bc742d8 | |||
| 29e8d07693 | |||
| 4fd4e8a16e | |||
| 30d627c2be | |||
| 9390cb64b4 | |||
| d720feaa6d | |||
| 9f7cda3b69 | |||
| 878f67a051 | |||
| 7fb8550c97 | |||
| 700836aea0 | |||
| 16aaa1b050 | |||
| 8790d3cfcf | |||
| bb07138fb0 | |||
| 37c650e490 | |||
| 272e3895fd | |||
| 6e7f374b0d | |||
| 3743e45566 | |||
| b10e8abde0 | |||
| 5dab4422e9 | |||
| 82b6037a00 | |||
| 1bdb8b2724 | |||
| 8c905e4f42 | |||
| e9e59a2704 | |||
| e3a1482b8f | |||
| 9539b24d64 | |||
| 87caeef0af | |||
| 757e8a02ec | |||
| 6d0a128111 | |||
| 28b36d379b | |||
| 038b5d1437 | |||
| 038e1794eb | |||
| 663b2cd888 | |||
| 23f14e5799 | |||
| 55572acdc8 | |||
| 08125e9281 | |||
| e8ee9de5b9 | |||
| 91aea0e968 | |||
| 4cba009ac8 | |||
| 47ea4b226a | |||
| 00059e6754 | |||
| e4b81063cb | |||
| 3499fbd758 | |||
| 4b3d4690e8 | |||
| 48480bc839 | |||
| 031ed9c203 | |||
| f551732a17 | |||
| 7a814faed2 | |||
| 792317e945 | |||
| 9c10e06aac | |||
| c39108043b | |||
| 30bf941979 | |||
| 55ee6a9d13 | |||
| 2b25fe1fa4 | |||
| 57d563d488 | |||
| 2ca9ca3cb6 | |||
| ebb04d8a14 | |||
| 3c24ac26d5 | |||
| 87ce5a6d82 | |||
| 9623e2de6f | |||
| b9b4c1c38d | |||
| 688cb30d4a | |||
| 1aca2cde71 | |||
| 49fa451cc3 | |||
| 5f1389f824 | |||
| a90693e488 | |||
| ebeec056cd | |||
| 49d65292c0 | |||
| 6c30a04ac0 | |||
| f070314524 | |||
| 75c88eaa55 | |||
| bd6ae2ac2b | |||
| 58d04f9693 | |||
| 01c12655b8 | |||
| 4003e0a2ab | |||
| e87db5b2ab | |||
| 5b9c28e6f0 | |||
| 4375d77a98 | |||
| 842c9c8ecd | |||
| f3cc19b09c | |||
| 6b8faf0ecf | |||
| 71ad1e9939 | |||
| f355cb4d38 | |||
| 5ae8d274c0 | |||
| 6402894096 |
@ -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:
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
- 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)
|
||||
@ -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
|
||||
|
||||
@ -132,7 +132,6 @@ Proton Mail Bridge includes the following 3rd party software:
|
||||
gopkg.in/yaml.v2
|
||||
gopkg.in/yaml.v3
|
||||
* [docker-credential-helpers](https://github.com/ProtonMail/docker-credential-helpers) available under [license](https://github.com/ProtonMail/docker-credential-helpers/blob/master/LICENSE)
|
||||
* [go-imap](https://github.com/ProtonMail/go-imap) available under [license](https://github.com/ProtonMail/go-imap/blob/master/LICENSE)
|
||||
* [go-message](https://github.com/ProtonMail/go-message) available under [license](https://github.com/ProtonMail/go-message/blob/master/LICENSE)
|
||||
* [go-keychain](https://github.com/cuthix/go-keychain) available under [license](https://github.com/cuthix/go-keychain/blob/master/LICENSE)
|
||||
<!-- END AUTOGEN -->
|
||||
|
||||
144
Changelog.md
144
Changelog.md
@ -2,12 +2,154 @@
|
||||
|
||||
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
|
||||
## [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
|
||||
|
||||
### Changed
|
||||
* GODT-2205: use lock file in bridge-gui to detect orphan bridge.
|
||||
* GODT-2242: Bump GPA - Don't send any 2fa information if not needed.
|
||||
* GODT-2179: added handler for exceptions in QML backend methods.
|
||||
* GODT-2181: Match live API behaviour.
|
||||
* GODT-2221: Set DOH off by default.
|
||||
* GODT-1817: Re-enable all integration tests.
|
||||
* Other: C++ Code reformat.
|
||||
* GODT-2234: added command-line switch to force Qt to use software rendering for QML.
|
||||
* Other: added C/C++ header template file (*.h.in) type to missing_license.sh script.
|
||||
* GODT-2236: add log entry when SMTP / IMAP serve method fails.
|
||||
* Other: reorganised QMLBackend class code.
|
||||
|
||||
### Fixed
|
||||
* Other: Flag messages imported into "Sent" mailbox as Sent.
|
||||
* Other: Fix testCtx.getMBoxID().
|
||||
* Other: Fixed GUI Tester to comply with latest gRPC changes.
|
||||
* GODT-2010: add Cocoa app delegate handler for second application instance.
|
||||
* Other: Fix double close on event channels.
|
||||
* GODT-2233: Fix sub folder creation bug.
|
||||
* GODT-2222: Dot not error on unknown Address Events.
|
||||
* GODT-2218: Fix invalid UID ranges.
|
||||
|
||||
|
||||
## [Bridge 3.0.9] Perth Narrows
|
||||
|
||||
### Changed
|
||||
* GODT-2181(test): Refactor integration test setup a bit.
|
||||
* Other: Updated GUI tester for new gRPC calls.
|
||||
* GODT-1847: Add option to export TLS Certificates in GUI.
|
||||
|
||||
### Fixed
|
||||
* Other: Fix TOTP login (bump go-proton-api).
|
||||
* GODT-2188: Do not fail append with invalid mime-type.
|
||||
* GODT-2213: Don't unnecessarily enable/disable autostart.
|
||||
* Other: Do not decode message body during send record hashing.
|
||||
* GODT-2196: Do not generate message updates for unknown labels.
|
||||
* Other: Prevent double login.
|
||||
* Other: Improve migration logging prefers username over primary address.
|
||||
* Other(test): Prefer native API revoke rather than fake server method.
|
||||
* GODT-2190: Unify crashpad_handler for darwin.
|
||||
* Other(test): Add test that we skip and report bad messages during sync.
|
||||
* Other: Catalina build.
|
||||
* GODT-2042: Fix setup guide not always showing on first login.
|
||||
* GODT-2152: Sign-in dialog validate email and password only when button is pressed.
|
||||
* GODT-1556: Add unit test for in-reply-to header without references.
|
||||
* GODT-2150: Fixed initial implementation that filtered --no-window in gui instead of bridge.
|
||||
* GODT-2167: Bind sign-in buttons availability to loading state.
|
||||
* Other: Only send to necessary update channel.
|
||||
* GODT-1804: Add parsing ics attachment test.
|
||||
* Other: Fix Warning introduced by connecting check timer.
|
||||
|
||||
|
||||
## [Bridge 3.0.8] Perth Narrows
|
||||
|
||||
### Fixed
|
||||
* Other: Add sentry reports for event processing failures.
|
||||
* Other: Do not fail on label events.
|
||||
|
||||
|
||||
## [Bridge 3.0.7] Perth Narrows
|
||||
|
||||
### Fixed
|
||||
* Other: Increase default UIDVALIDITY.
|
||||
* GODT-2173: fix: Migrate Bridge passwords from v2.X.
|
||||
* GODT-2207: Fix encoding of non utf7 mailbox names.
|
||||
* Other: Increase worker count (2 -> 4).
|
||||
|
||||
|
||||
## [Bridge 3.0.6] Perth Narrows
|
||||
|
||||
### Fixed
|
||||
* GODT-2187: Skip messages during sync that fail to build/parse.
|
||||
|
||||
|
||||
## [Bridge 3.0.5] Perth Narrows
|
||||
|
||||
### Fixed
|
||||
* GODT-2178: Bump go-proton-api to fix drafts.
|
||||
* GODT-2180: Allow login with FIDO2.
|
||||
|
||||
|
||||
## [Bridge 3.0.4] Perth Narrows
|
||||
|
||||
### Changed
|
||||
@ -21,6 +163,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
* GODT-2170: Update draft event means delete old and create new message.
|
||||
* GODT-2170: User create draft route: first steps.
|
||||
|
||||
|
||||
## [Bridge 3.0.3] Perth Narrows
|
||||
|
||||
### Fixed
|
||||
@ -51,6 +194,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
* Other: Ensure context is string in sentry reports.
|
||||
* GODT-2160: Ensure we can safely move cache file.
|
||||
|
||||
|
||||
## [Bridge 3.0.1] Perth Narrows
|
||||
|
||||
### Changed
|
||||
|
||||
11
Makefile
11
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.5+git
|
||||
BRIDGE_APP_VERSION?=3.0.12+git
|
||||
APP_VERSION:=${BRIDGE_APP_VERSION}
|
||||
APP_FULL_NAME:=Proton Mail Bridge
|
||||
APP_VENDOR:=Proton AG
|
||||
@ -21,7 +21,8 @@ SRC_SVG:=bridge.svg
|
||||
EXE_NAME:=proton-bridge
|
||||
REVISION:=$(shell git rev-parse --short=10 HEAD)
|
||||
BUILD_TIME:=$(shell date +%FT%T%z)
|
||||
MACOS_MIN_VERSION=11.0
|
||||
MACOS_MIN_VERSION_ARM64=11.0
|
||||
MACOS_MIN_VERSION_AMD64=10.15
|
||||
|
||||
BUILD_FLAGS:=-tags='${BUILD_TAGS}'
|
||||
BUILD_FLAGS_LAUNCHER:=${BUILD_FLAGS}
|
||||
@ -88,8 +89,8 @@ go-build=go build $(1) -o $(2) $(3)
|
||||
go-build-finalize=${go-build}
|
||||
ifeq "${GOOS}-$(shell uname -m)" "darwin-arm64"
|
||||
go-build-finalize= \
|
||||
MACOSX_DEPLOYMENT_TARGET=${MACOS_MIN_VERSION} CGO_ENABLED=1 CGO_CFLAGS="-mmacosx-version-min=${MACOS_MIN_VERSION}" GOARCH=arm64 $(call go-build,$(1),$(2)_arm,$(3)) && \
|
||||
MACOSX_DEPLOYMENT_TARGET=${MACOS_MIN_VERSION} CGO_ENABLED=1 CGO_CFLAGS="-mmacosx-version-min=${MACOS_MIN_VERSION}" GOARCH=amd64 $(call go-build,$(1),$(2)_amd,$(3)) && \
|
||||
MACOSX_DEPLOYMENT_TARGET=${MACOS_MIN_VERSION_ARM64} CGO_ENABLED=1 CGO_CFLAGS="-mmacosx-version-min=${MACOS_MIN_VERSION_ARM64}" GOARCH=arm64 $(call go-build,$(1),$(2)_arm,$(3)) && \
|
||||
MACOSX_DEPLOYMENT_TARGET=${MACOS_MIN_VERSION_AMD64} CGO_ENABLED=1 CGO_CFLAGS="-mmacosx-version-min=${MACOS_MIN_VERSION_AMD64}" GOARCH=amd64 $(call go-build,$(1),$(2)_amd,$(3)) && \
|
||||
lipo -create -output $(2) $(2)_arm $(2)_amd && rm -f $(2)_arm $(2)_amd
|
||||
endif
|
||||
|
||||
@ -280,7 +281,7 @@ updates: install-go-mod-outdated
|
||||
doc:
|
||||
godoc -http=:6060
|
||||
|
||||
release-notes: release-notes/bridge_stable.html release-notes/bridge_early.html
|
||||
release-notes: release-notes/bridge_stable.html release-notes/bridge_early.html utils/release_notes.sh
|
||||
|
||||
release-notes/%.html: release-notes/%.md
|
||||
./utils/release_notes.sh $^
|
||||
|
||||
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`
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
5
go.mod
5
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.20221202093012-ad1570c49c8c
|
||||
github.com/ProtonMail/gluon v0.14.2-0.20230127085305-bc2d818d9d13
|
||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
|
||||
github.com/ProtonMail/go-proton-api v0.2.1
|
||||
github.com/ProtonMail/go-proton-api v0.3.1-0.20230126112849-3c1ac277855e
|
||||
github.com/ProtonMail/go-rfc5322 v0.11.0
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.4.10
|
||||
github.com/PuerkitoBio/goquery v1.8.0
|
||||
@ -120,7 +120,6 @@ require (
|
||||
|
||||
replace (
|
||||
github.com/docker/docker-credential-helpers => github.com/ProtonMail/docker-credential-helpers v1.1.0
|
||||
github.com/emersion/go-imap => github.com/ProtonMail/go-imap v0.0.0-20201228133358-4db68cea0cac
|
||||
github.com/emersion/go-message => github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753
|
||||
github.com/keybase/go-keychain => github.com/cuthix/go-keychain v0.0.0-20220405075754-31e7cee908fe
|
||||
)
|
||||
|
||||
13
go.sum
13
go.sum
@ -28,23 +28,21 @@ 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.20221202093012-ad1570c49c8c h1:DzVlJERHOHDQjYz/P12VlORS4rF2Ii83cWcYHsXGdng=
|
||||
github.com/ProtonMail/gluon v0.14.2-0.20221202093012-ad1570c49c8c/go.mod h1:z2AxLIiBCT1K+0OBHyaDI7AEaO5qI6/BEC2TE42vs4Q=
|
||||
github.com/ProtonMail/gluon v0.14.2-0.20230127085305-bc2d818d9d13 h1:rljNZVgfq/F1LLyJ4NmCfEzWayC/rk+l9QgJjtQTLKI=
|
||||
github.com/ProtonMail/gluon v0.14.2-0.20230127085305-bc2d818d9d13/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=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20220822140716-1678d6eb0cbe/go.mod h1:UBYPn8k0D56RtnR8RFQMjmh4KrZzWJ5o7Z9SYjossQ8=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20220824120805-4b6e5c587895 h1:NsReiLpErIPzRrnogAXYwSoU7txA977LjDGrbkewJbg=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20220824120805-4b6e5c587895/go.mod h1:UBYPn8k0D56RtnR8RFQMjmh4KrZzWJ5o7Z9SYjossQ8=
|
||||
github.com/ProtonMail/go-imap v0.0.0-20201228133358-4db68cea0cac h1:2xU3QncAiS/W3UlWZTkbNKW5WkLzk6Egl1T0xX+sbjs=
|
||||
github.com/ProtonMail/go-imap v0.0.0-20201228133358-4db68cea0cac/go.mod h1:yKASt+C3ZiDAiCSssxg9caIckWF/JG7ZQTO7GAmvicU=
|
||||
github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753 h1:I8IsYA297x0QLU80G5I6aLYUu3JYNSpo8j5fkXtFDW0=
|
||||
github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
|
||||
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.1 h1:M15/zzfx6EPiskv2+gogUkmvx7Y1SmRRtLT6GiBh5T0=
|
||||
github.com/ProtonMail/go-proton-api v0.2.1/go.mod h1:jqvJ2HqLHqiPJoEb+BTIB1IF7wvr6p+8ZfA6PO2NRNk=
|
||||
github.com/ProtonMail/go-proton-api v0.3.1-0.20230126112849-3c1ac277855e h1:UkfLQc44UvknNCLoBEZb1qg7zfVWVLMvCE/LtdVEcAw=
|
||||
github.com/ProtonMail/go-proton-api v0.3.1-0.20230126112849-3c1ac277855e/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=
|
||||
@ -122,9 +120,10 @@ github.com/elastic/go-sysinfo v1.8.1 h1:4Yhj+HdV6WjbCRgGdZpPJ8lZQlXZLKDAeIkmQ/VR
|
||||
github.com/elastic/go-sysinfo v1.8.1/go.mod h1:JfllUnzoQV/JRYymbH3dO1yggI3mV2oTKSXsDHM+uIM=
|
||||
github.com/elastic/go-windows v1.0.1 h1:AlYZOldA+UJ0/2nBuqWdo90GFCgG9xuyw9SYzGUtJm0=
|
||||
github.com/elastic/go-windows v1.0.1/go.mod h1:FoVvqWSun28vaDQPbj2Elfc0JahhPB7WQEGa3c814Ss=
|
||||
github.com/emersion/go-imap v1.2.1-0.20220429085312-746087b7a317 h1:i0cBrdFLm8A/3hWEjn/BwdXLBplFJoZtu63p7bjrmaI=
|
||||
github.com/emersion/go-imap v1.2.1-0.20220429085312-746087b7a317/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY=
|
||||
github.com/emersion/go-imap-id v0.0.0-20190926060100-f94a56b9ecde h1:43mBoVwooyLm1+1YVf5nvn1pSFWhw7rOpcrp1Jg/qk0=
|
||||
github.com/emersion/go-imap-id v0.0.0-20190926060100-f94a56b9ecde/go.mod h1:sPwp0FFboaK/bxsrUz1lNrDMUCsZUsKC5YuM4uRVRVs=
|
||||
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
|
||||
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead h1:fI1Jck0vUrXT8bnphprS1EoVRe2Q5CKCX8iDlpqjQ/Y=
|
||||
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
@ -69,9 +69,10 @@ const (
|
||||
|
||||
// Hidden flags.
|
||||
const (
|
||||
flagLauncher = "launcher"
|
||||
flagNoWindow = "no-window"
|
||||
flagParentPID = "parent-pid"
|
||||
flagLauncher = "launcher"
|
||||
flagNoWindow = "no-window"
|
||||
flagParentPID = "parent-pid"
|
||||
flagSoftwareRenderer = "software-renderer"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -140,6 +141,12 @@ func New() *cli.App { //nolint:funlen
|
||||
Hidden: true,
|
||||
Value: -1,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flagSoftwareRenderer, // This flag is ignored by bridge, but should be passed to launcher in case of restart, so it need to be accepted by the CLI parser.
|
||||
Usage: "GUI is using software renderer",
|
||||
Hidden: true,
|
||||
Value: false,
|
||||
},
|
||||
}
|
||||
|
||||
app.Action = run
|
||||
@ -199,6 +206,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 {
|
||||
@ -315,14 +332,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.
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
@ -30,8 +30,10 @@ import (
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/legacy/credentials"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
"github.com/ProtonMail/proton-bridge/v3/pkg/algo"
|
||||
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
|
||||
"github.com/allan-simon/go-singleinstance"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
@ -43,6 +45,16 @@ import (
|
||||
func migrateKeychainHelper(locations *locations.Locations) error {
|
||||
logrus.Info("Migrating keychain helper")
|
||||
|
||||
settings, err := locations.ProvideSettingsPath()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get settings path: %w", err)
|
||||
}
|
||||
|
||||
// If keychain helper file is already there do not migrate again.
|
||||
if keychainName, _ := vault.GetHelper(settings); keychainName != "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
configDir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get user config dir: %w", err)
|
||||
@ -63,11 +75,6 @@ func migrateKeychainHelper(locations *locations.Locations) error {
|
||||
return fmt.Errorf("failed to unmarshal old prefs file: %w", err)
|
||||
}
|
||||
|
||||
settings, err := locations.ProvideSettingsPath()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get settings path: %w", err)
|
||||
}
|
||||
|
||||
return vault.SetHelper(settings, prefs.Helper)
|
||||
}
|
||||
|
||||
@ -115,26 +122,65 @@ func migrateOldAccounts(locations *locations.Locations, v *vault.Vault) error {
|
||||
return fmt.Errorf("failed to create credentials store: %w", err)
|
||||
}
|
||||
|
||||
var migrationErrors error
|
||||
|
||||
for _, userID := range users {
|
||||
logrus.WithField("userID", userID).Info("Migrating account")
|
||||
|
||||
creds, err := store.Get(userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get user: %w", err)
|
||||
if err := migrateOldAccount(userID, store, v); err != nil {
|
||||
migrationErrors = multierror.Append(migrationErrors, err)
|
||||
}
|
||||
}
|
||||
|
||||
authUID, authRef, err := creds.SplitAPIToken()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to split api token: %w", err)
|
||||
}
|
||||
return migrationErrors
|
||||
}
|
||||
|
||||
user, err := v.AddUser(creds.UserID, creds.EmailList()[0], authUID, authRef, creds.MailboxPassword)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add user: %w", err)
|
||||
}
|
||||
func migrateOldAccount(userID string, store *credentials.Store, v *vault.Vault) error {
|
||||
l := logrus.WithField("userID", userID)
|
||||
l.Info("Migrating account")
|
||||
|
||||
creds, err := store.Get(userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get user %q: %w", userID, err)
|
||||
}
|
||||
|
||||
authUID, authRef, err := creds.SplitAPIToken()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to split api token for user %q: %w", userID, err)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
l = l.WithField("username", logging.Sensitive(user.Username()))
|
||||
l.Info("Migrated account with random bridge password")
|
||||
|
||||
defer func() {
|
||||
if err := user.Close(); err != nil {
|
||||
return fmt.Errorf("failed to close user: %w", err)
|
||||
logrus.WithField("userID", userID).WithError(err).Error("Failed to close vault user after migration")
|
||||
}
|
||||
}()
|
||||
|
||||
dec, err := algo.B64RawDecode([]byte(creds.BridgePassword))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode bridge password for user %q: %w", userID, err)
|
||||
}
|
||||
|
||||
if err := user.SetBridgePass(dec); err != nil {
|
||||
return fmt.Errorf("failed to set bridge password for user %q: %w", userID, err)
|
||||
}
|
||||
|
||||
l = l.WithField("password", logging.Sensitive(string(algo.B64RawEncode(dec))))
|
||||
l.Info("Migrated existing bridge password")
|
||||
|
||||
if !creds.IsCombinedAddressMode {
|
||||
if err := user.SetAddressMode(vault.SplitMode); err != nil {
|
||||
return fmt.Errorf("failed to set split address mode to user %q: %w", userID, err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -152,11 +198,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"`
|
||||
@ -200,10 +245,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))
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
@ -22,11 +22,19 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/cookies"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/legacy/credentials"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
"github.com/ProtonMail/proton-bridge/v3/pkg/algo"
|
||||
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
|
||||
dockerCredentials "github.com/docker/docker-credential-helpers/credentials"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@ -55,7 +63,6 @@ func TestMigratePrefsToVault(t *testing.T) {
|
||||
|
||||
// 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())
|
||||
@ -79,3 +86,113 @@ func TestMigratePrefsToVault(t *testing.T) {
|
||||
// There should be a cookie for the API.
|
||||
require.NotEmpty(t, cookies.Cookies(url))
|
||||
}
|
||||
|
||||
func TestKeychainMigration(t *testing.T) {
|
||||
// Migration tested only for linux.
|
||||
if runtime.GOOS != "linux" {
|
||||
return
|
||||
}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Prepare for keychain migration test
|
||||
{
|
||||
require.NoError(t, os.Setenv("XDG_CONFIG_HOME", tmpDir))
|
||||
oldCacheDir := filepath.Join(tmpDir, "protonmail", "bridge")
|
||||
require.NoError(t, os.MkdirAll(oldCacheDir, 0o700))
|
||||
|
||||
oldPrefs, err := os.ReadFile(filepath.Join("testdata", "prefs.json"))
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, os.WriteFile(
|
||||
filepath.Join(oldCacheDir, "prefs.json"),
|
||||
oldPrefs, 0o600,
|
||||
))
|
||||
}
|
||||
|
||||
locations := locations.New(bridge.NewTestLocationsProvider(tmpDir), "config-name")
|
||||
settingsFolder, err := locations.ProvideSettingsPath()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Check that there is nothing yet
|
||||
keychainName, err := vault.GetHelper(settingsFolder)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "", keychainName)
|
||||
|
||||
// Check migration
|
||||
require.NoError(t, migrateKeychainHelper(locations))
|
||||
keychainName, err = vault.GetHelper(settingsFolder)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "secret-service", keychainName)
|
||||
|
||||
// Change the migrated value
|
||||
require.NoError(t, vault.SetHelper(settingsFolder, "different"))
|
||||
|
||||
// Calling migration again will not overwrite existing prefs
|
||||
require.NoError(t, migrateKeychainHelper(locations))
|
||||
keychainName, err = vault.GetHelper(settingsFolder)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "different", keychainName)
|
||||
}
|
||||
|
||||
func TestUserMigration(t *testing.T) {
|
||||
keychainHelper := keychain.NewTestHelper()
|
||||
|
||||
keychain.Helpers["mock"] = func(string) (dockerCredentials.Helper, error) { return keychainHelper, nil }
|
||||
|
||||
kc, err := keychain.NewKeychain("mock", "bridge")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, kc.Put("brokenID", "broken"))
|
||||
require.NoError(t, kc.Put(
|
||||
"emptyID",
|
||||
(&credentials.Credentials{}).Marshal(),
|
||||
))
|
||||
|
||||
wantUID := "uidtoken"
|
||||
wantRefresh := "refreshtoken"
|
||||
|
||||
wantCredentials := credentials.Credentials{
|
||||
UserID: "validID",
|
||||
Name: "user@pm.me",
|
||||
Emails: "user@pm.me;alias@pm.me",
|
||||
APIToken: wantUID + ":" + wantRefresh,
|
||||
MailboxPassword: []byte("secret"),
|
||||
BridgePassword: "bElu2Q1Vusy28J3Wf56cIg",
|
||||
Version: "v2.3.X",
|
||||
Timestamp: 100,
|
||||
IsCombinedAddressMode: true,
|
||||
}
|
||||
require.NoError(t, kc.Put(
|
||||
wantCredentials.UserID,
|
||||
wantCredentials.Marshal(),
|
||||
))
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
locations := locations.New(bridge.NewTestLocationsProvider(tmpDir), "config-name")
|
||||
settingsFolder, err := locations.ProvideSettingsPath()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, vault.SetHelper(settingsFolder, "mock"))
|
||||
|
||||
token, err := crypto.RandomToken(32)
|
||||
require.NoError(t, err)
|
||||
|
||||
v, corrupt, err := vault.New(settingsFolder, settingsFolder, token)
|
||||
require.NoError(t, err)
|
||||
require.False(t, corrupt)
|
||||
|
||||
require.NoError(t, migrateOldAccounts(locations, v))
|
||||
require.Equal(t, []string{wantCredentials.UserID}, v.GetUserIDs())
|
||||
|
||||
require.NoError(t, v.GetUser(wantCredentials.UserID, func(u *vault.User) {
|
||||
require.Equal(t, wantCredentials.UserID, u.UserID())
|
||||
require.Equal(t, wantUID, u.AuthUID())
|
||||
require.Equal(t, wantRefresh, u.AuthRef())
|
||||
require.Equal(t, wantCredentials.MailboxPassword, u.KeyPass())
|
||||
require.Equal(t,
|
||||
[]byte(wantCredentials.BridgePassword),
|
||||
algo.B64RawEncode(u.BridgePass()),
|
||||
)
|
||||
require.Equal(t, vault.CombinedMode, u.AddressMode())
|
||||
}))
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
@ -89,12 +89,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)
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
@ -108,6 +108,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
|
||||
|
||||
@ -179,11 +185,13 @@ func New( //nolint:funlen
|
||||
|
||||
// Start serving IMAP.
|
||||
if err := bridge.serveIMAP(); err != nil {
|
||||
logrus.WithError(err).Error("IMAP error")
|
||||
bridge.PushError(ErrServeIMAP)
|
||||
}
|
||||
|
||||
// Start serving SMTP.
|
||||
if err := bridge.serveSMTP(); err != nil {
|
||||
logrus.WithError(err).Error("SMTP error")
|
||||
bridge.PushError(ErrServeSMTP)
|
||||
}
|
||||
|
||||
@ -214,13 +222,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,
|
||||
@ -270,6 +294,9 @@ func newBridge(
|
||||
logIMAPServer: logIMAPServer,
|
||||
logSMTP: logSMTP,
|
||||
|
||||
firstStart: firstStart,
|
||||
lastVersion: lastVersion,
|
||||
|
||||
tasks: tasks,
|
||||
}
|
||||
|
||||
@ -433,11 +460,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) {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
@ -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"
|
||||
)
|
||||
|
||||
@ -59,7 +61,7 @@ var (
|
||||
func init() {
|
||||
user.EventPeriod = 100 * time.Millisecond
|
||||
user.EventJitter = 0
|
||||
backend.GenerateKey = tests.FastGenerateKey
|
||||
backend.GenerateKey = backend.FastGenerateKey
|
||||
certs.GenerateCert = tests.FastGenerateCert
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
// ...
|
||||
})
|
||||
@ -384,7 +409,7 @@ func TestBridge_AddressWithoutKeys(t *testing.T) {
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Create a user which will have an address without keys.
|
||||
userID, _, err := s.CreateUser("nokeys", "nokeys@pm.me", []byte("password"))
|
||||
userID, _, err := s.CreateUser("nokeys", []byte("password"))
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create an additional address for the user; it will not have keys.
|
||||
@ -456,41 +481,80 @@ 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)
|
||||
})
|
||||
})
|
||||
}
|
||||
@ -501,7 +565,7 @@ func withEnv(t *testing.T, tests func(context.Context, *server.Server, *proton.N
|
||||
defer server.Close()
|
||||
|
||||
// Add test user.
|
||||
_, _, err := server.CreateUser(username, username+"@pm.me", password)
|
||||
_, _, err := server.CreateUser(username, password)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Generate a random vault key.
|
||||
@ -522,22 +586,27 @@ 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 enable the proxy by default at startup.
|
||||
mocks.ProxyCtl.EXPECT().AllowProxy()
|
||||
// Bridge will disable the proxy by default at startup.
|
||||
mocks.ProxyCtl.EXPECT().DisallowProxy()
|
||||
|
||||
// Get the path to the vault.
|
||||
vaultDir, err := locator.ProvideSettingsPath()
|
||||
@ -566,7 +635,7 @@ func withBridge(
|
||||
cookieJar,
|
||||
useragent.New(),
|
||||
mocks.TLSReporter,
|
||||
proton.NewDialer(netCtl, &tls.Config{InsecureSkipVerify: true}).GetRoundTripper(),
|
||||
netCtl.NewRoundTripper(&tls.Config{InsecureSkipVerify: true}),
|
||||
mocks.ProxyCtl,
|
||||
mocks.CrashHandler,
|
||||
mocks.Reporter,
|
||||
@ -590,7 +659,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) {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
@ -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"
|
||||
@ -65,7 +62,7 @@ func (bridge *Bridge) serveIMAP() error {
|
||||
}
|
||||
|
||||
if err := bridge.vault.SetIMAPPort(getPort(imapListener.Addr())); err != nil {
|
||||
return fmt.Errorf("failed to set IMAP port: %w", err)
|
||||
return fmt.Errorf("failed to store IMAP port in vault: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -122,9 +119,20 @@ 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 the DB was newly created, clear the sync status; gluon's DB was not found.
|
||||
if isNew {
|
||||
logrus.Warn("IMAP user DB was newly created, clearing sync status")
|
||||
|
||||
if err := user.ClearSyncStatus(); err != nil {
|
||||
return fmt.Errorf("failed to clear sync status: %w", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Info("Creating new IMAP user")
|
||||
|
||||
@ -149,6 +157,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 +208,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 +233,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 +269,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 +304,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) {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -144,3 +144,17 @@ func (mr *MockAutostarterMockRecorder) Enable() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Enable", reflect.TypeOf((*MockAutostarter)(nil).Enable))
|
||||
}
|
||||
|
||||
// IsEnabled mocks base method.
|
||||
func (m *MockAutostarter) IsEnabled() bool {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "IsEnabled")
|
||||
ret0, _ := ret[0].(bool)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// IsEnabled indicates an expected call of IsEnabled.
|
||||
func (mr *MockAutostarterMockRecorder) IsEnabled() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsEnabled", reflect.TypeOf((*MockAutostarter)(nil).IsEnabled))
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
@ -35,7 +35,7 @@ import (
|
||||
|
||||
func TestBridge_Refresh(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
userID, _, err := s.CreateUser("imap", "imap@pm.me", password)
|
||||
userID, _, err := s.CreateUser("imap", password)
|
||||
require.NoError(t, err)
|
||||
|
||||
names := iterator.Collect(iterator.Map(iterator.Counter(10), func(i int) string {
|
||||
@ -67,13 +67,13 @@ func TestBridge_Refresh(t *testing.T) {
|
||||
|
||||
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.Login("imap@pm.me", string(info.BridgePass)))
|
||||
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
||||
defer func() { _ = client.Logout() }()
|
||||
|
||||
for _, name := range names {
|
||||
status, err := client.Select("Folders/"+name, false)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint32(1), status.UidValidity)
|
||||
require.Equal(t, uint32(1000), status.UidValidity)
|
||||
}
|
||||
})
|
||||
|
||||
@ -100,13 +100,13 @@ func TestBridge_Refresh(t *testing.T) {
|
||||
|
||||
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.Login("imap@pm.me", string(info.BridgePass)))
|
||||
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
||||
defer func() { _ = client.Logout() }()
|
||||
|
||||
for _, name := range names {
|
||||
status, err := client.Select("Folders/"+name, false)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint32(2), status.UidValidity)
|
||||
require.Equal(t, uint32(1001), status.UidValidity)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
@ -30,6 +30,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"
|
||||
@ -39,10 +40,10 @@ import (
|
||||
|
||||
func TestBridge_Send(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
_, _, err := s.CreateUser("recipient", "recipient@pm.me", password)
|
||||
_, _, 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 +101,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 +114,106 @@ 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)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
@ -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()
|
||||
}
|
||||
@ -207,15 +252,24 @@ func (bridge *Bridge) GetAutostart() bool {
|
||||
}
|
||||
|
||||
func (bridge *Bridge) SetAutostart(autostart bool) error {
|
||||
if err := bridge.vault.SetAutostart(autostart); err != nil {
|
||||
return err
|
||||
if autostart != bridge.vault.GetAutostart() {
|
||||
if err := bridge.vault.SetAutostart(autostart); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
if autostart {
|
||||
// do nothing if already enabled
|
||||
if bridge.autostarter.IsEnabled() {
|
||||
return nil
|
||||
}
|
||||
err = bridge.autostarter.Enable()
|
||||
} else {
|
||||
// do nothing if already disabled
|
||||
if !bridge.autostarter.IsEnabled() {
|
||||
return nil
|
||||
}
|
||||
err = bridge.autostarter.Disable()
|
||||
}
|
||||
|
||||
@ -263,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 {
|
||||
@ -299,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")
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
@ -119,14 +119,14 @@ func TestBridge_Settings_Proxy(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, proxy is allowed.
|
||||
require.True(t, bridge.GetProxyAllowed())
|
||||
require.False(t, bridge.GetProxyAllowed())
|
||||
|
||||
// Disallow proxy.
|
||||
mocks.ProxyCtl.EXPECT().DisallowProxy()
|
||||
require.NoError(t, bridge.SetProxyAllowed(false))
|
||||
mocks.ProxyCtl.EXPECT().AllowProxy()
|
||||
require.NoError(t, bridge.SetProxyAllowed(true))
|
||||
|
||||
// Get the new setting.
|
||||
require.False(t, bridge.GetProxyAllowed())
|
||||
require.True(t, bridge.GetProxyAllowed())
|
||||
})
|
||||
})
|
||||
}
|
||||
@ -134,10 +134,19 @@ func TestBridge_Settings_Proxy(t *testing.T) {
|
||||
func TestBridge_Settings_Autostart(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, autostart is disabled.
|
||||
// By default, autostart is enabled.
|
||||
require.True(t, bridge.GetAutostart())
|
||||
|
||||
// Disable autostart.
|
||||
mocks.Autostarter.EXPECT().IsEnabled().Return(true)
|
||||
mocks.Autostarter.EXPECT().Disable().Return(nil)
|
||||
require.NoError(t, bridge.SetAutostart(false))
|
||||
|
||||
// Get the new setting.
|
||||
require.False(t, bridge.GetAutostart())
|
||||
|
||||
// Enable autostart.
|
||||
// Re Enable autostart.
|
||||
mocks.Autostarter.EXPECT().IsEnabled().Return(false)
|
||||
mocks.Autostarter.EXPECT().Enable().Return(nil)
|
||||
require.NoError(t, bridge.SetAutostart(true))
|
||||
|
||||
@ -153,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.
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
@ -47,7 +47,7 @@ func (bridge *Bridge) serveSMTP() error {
|
||||
})
|
||||
|
||||
if err := bridge.vault.SetSMTPPort(getPort(smtpListener.Addr())); err != nil {
|
||||
return fmt.Errorf("failed to set IMAP port: %w", err)
|
||||
return fmt.Errorf("failed to store SMTP port in vault: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
@ -20,12 +20,15 @@ package bridge_test
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/gluon/rfc822"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/go-proton-api/server"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||
@ -33,7 +36,10 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"github.com/bradenaw/juniper/iterator"
|
||||
"github.com/bradenaw/juniper/stream"
|
||||
"github.com/bradenaw/juniper/xslices"
|
||||
"github.com/emersion/go-imap"
|
||||
"github.com/emersion/go-imap/client"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@ -41,14 +47,191 @@ func TestBridge_Sync(t *testing.T) {
|
||||
numMsg := 1 << 8
|
||||
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
userID, addrID, err := s.CreateUser("imap", "imap@pm.me", password)
|
||||
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) {
|
||||
createMessages(ctx, t, c, addrID, labelID, numMsg)
|
||||
createNumMessages(ctx, t, c, addrID, labelID, numMsg)
|
||||
})
|
||||
|
||||
var total uint64
|
||||
|
||||
// The initial user 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()
|
||||
|
||||
// Count how many bytes it takes to fully sync the user.
|
||||
total = countBytesRead(netCtl, func() {
|
||||
userID, err := bridge.LoginFull(ctx, "imap", password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, userID, (<-syncCh).UserID)
|
||||
})
|
||||
})
|
||||
|
||||
// If we then connect an IMAP client, it should see all the messages.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
|
||||
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(numMsg), status.Messages)
|
||||
})
|
||||
|
||||
// Now let's remove the user and simulate a network error.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
require.NoError(t, bridge.DeleteUser(ctx, userID))
|
||||
})
|
||||
|
||||
// Pretend we can only sync 2/3 of the original messages.
|
||||
netCtl.SetReadLimit(2 * total / 3)
|
||||
|
||||
// Login the user; its sync should fail.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
|
||||
{
|
||||
syncCh, done := chToType[events.Event, events.SyncFailed](b.GetEvents(events.SyncFailed{}))
|
||||
defer done()
|
||||
|
||||
userID, err := b.LoginFull(ctx, "imap", password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, userID, (<-syncCh).UserID)
|
||||
|
||||
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.Less(t, status.Messages, uint32(numMsg))
|
||||
}
|
||||
|
||||
// Remove the network limit, allowing the sync to finish.
|
||||
netCtl.SetReadLimit(0)
|
||||
|
||||
{
|
||||
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
|
||||
defer done()
|
||||
|
||||
require.Equal(t, userID, (<-syncCh).UserID)
|
||||
|
||||
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(numMsg), status.Messages)
|
||||
}
|
||||
})
|
||||
}, server.WithTLS(false))
|
||||
}
|
||||
|
||||
// GODT-2215: This test no longer works since it's now possible to import messages into Gluon with bad ContentType header.
|
||||
func _TestBridge_Sync_BadMessage(t *testing.T) { //nolint:unused,deadcode
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
userID, addrID, err := s.CreateUser("imap", password)
|
||||
require.NoError(t, err)
|
||||
|
||||
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
|
||||
require.NoError(t, err)
|
||||
|
||||
var messageIDs []string
|
||||
|
||||
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
|
||||
messageIDs = createMessages(ctx, t, c, addrID, labelID,
|
||||
[]byte("To: someone@pm.me\r\nSubject: Good message\r\n\r\nHello!"),
|
||||
[]byte("To: someone@pm.me\r\nSubject: Bad message\r\nContentType: this is not a valid content type\r\n\r\nHello!"),
|
||||
)
|
||||
})
|
||||
|
||||
// The initial user should be fully synced and should skip the bad message.
|
||||
// We should report the bad message to sentry.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
mocks.Reporter.EXPECT().ReportMessageWithContext("Failed to build message (sync)", gomock.Any())
|
||||
|
||||
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
||||
defer done()
|
||||
|
||||
userID, err := bridge.LoginFull(ctx, "imap", password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, userID, (<-syncCh).UserID)
|
||||
})
|
||||
|
||||
// If we then connect an IMAP client, it should see the good message but not the bad one.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
|
||||
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(1), status.Messages)
|
||||
|
||||
messages, err := clientFetch(client, `Folders/folder`)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, messages, 1)
|
||||
|
||||
// The bad message should have been skipped.
|
||||
literal, err := io.ReadAll(messages[0].GetBody(must(imap.ParseBodySectionName("BODY[]"))))
|
||||
require.NoError(t, err)
|
||||
|
||||
header, err := rfc822.Parse(literal).ParseHeader()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "Good message", header.Get("Subject"))
|
||||
require.Equal(t, messageIDs[0], header.Get("X-Pm-Internal-Id"))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_SyncWithOngoingEvents(t *testing.T) {
|
||||
numMsg := 1 << 8
|
||||
messageSplitIndex := numMsg * 2 / 3
|
||||
renmainingMessageCount := numMsg - messageSplitIndex
|
||||
|
||||
messages := make([]string, 0, numMsg)
|
||||
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
userID, addrID, err := s.CreateUser("imap", password)
|
||||
require.NoError(t, err)
|
||||
|
||||
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
|
||||
require.NoError(t, err)
|
||||
|
||||
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
|
||||
importResults := createNumMessages(ctx, t, c, addrID, labelID, numMsg)
|
||||
for _, v := range importResults {
|
||||
if len(v) != 0 {
|
||||
messages = append(messages, v)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
var total uint64
|
||||
@ -67,23 +250,7 @@ func TestBridge_Sync(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
// If we then connect an IMAP client, it should see all the messages.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
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("imap@pm.me", string(info.BridgePass)))
|
||||
defer func() { _ = client.Logout() }()
|
||||
|
||||
status, err := client.Select(`Folders/folder`, false)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint32(numMsg), status.Messages)
|
||||
})
|
||||
|
||||
// Now let's remove the user and simulate a network error.
|
||||
// Now let's remove the user and stop the network at 2/3 of the data.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
require.NoError(t, bridge.DeleteUser(ctx, userID))
|
||||
})
|
||||
@ -108,7 +275,7 @@ func TestBridge_Sync(t *testing.T) {
|
||||
|
||||
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.Login("imap@pm.me", string(info.BridgePass)))
|
||||
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
||||
defer func() { _ = client.Logout() }()
|
||||
|
||||
status, err := client.Select(`Folders/folder`, false)
|
||||
@ -116,6 +283,20 @@ func TestBridge_Sync(t *testing.T) {
|
||||
require.Less(t, status.Messages, uint32(numMsg))
|
||||
}
|
||||
|
||||
// Create a new mailbox and move that last 1/3 of the messages into it to simulate user
|
||||
// actions during sync.
|
||||
{
|
||||
newLabelID, err := s.CreateLabel(userID, "folder2", "", proton.LabelTypeFolder)
|
||||
require.NoError(t, err)
|
||||
|
||||
messages := messages[messageSplitIndex:]
|
||||
|
||||
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
|
||||
require.NoError(t, c.UnlabelMessages(ctx, messages, labelID))
|
||||
require.NoError(t, c.LabelMessages(ctx, messages, newLabelID))
|
||||
})
|
||||
}
|
||||
|
||||
// Remove the network limit, allowing the sync to finish.
|
||||
netCtl.SetReadLimit(0)
|
||||
|
||||
@ -131,18 +312,33 @@ func TestBridge_Sync(t *testing.T) {
|
||||
|
||||
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.Login("imap@pm.me", string(info.BridgePass)))
|
||||
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(numMsg), status.Messages)
|
||||
// Original folder should have more than 0 messages and less than the total.
|
||||
require.Greater(t, status.Messages, uint32(0))
|
||||
require.Less(t, status.Messages, uint32(numMsg))
|
||||
|
||||
// Check that the new messages arrive in the right location.
|
||||
require.Eventually(t, func() bool {
|
||||
status, err := client.Select(`Folders/folder2`, true)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if status.Messages != uint32(renmainingMessageCount) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}, 10*time.Second, 500*time.Millisecond)
|
||||
}
|
||||
})
|
||||
}, server.WithTLS(false))
|
||||
}
|
||||
|
||||
func withClient(ctx context.Context, t *testing.T, s *server.Server, username string, password []byte, fn func(context.Context, *proton.Client)) {
|
||||
func withClient(ctx context.Context, t *testing.T, s *server.Server, username string, password []byte, fn func(context.Context, *proton.Client)) { //nolint:unparam
|
||||
m := proton.New(
|
||||
proton.WithHostURL(s.GetHostURL()),
|
||||
proton.WithTransport(proton.InsecureTransport()),
|
||||
@ -155,10 +351,68 @@ func withClient(ctx context.Context, t *testing.T, s *server.Server, username st
|
||||
fn(ctx, c)
|
||||
}
|
||||
|
||||
func createMessages(ctx context.Context, t *testing.T, c *proton.Client, addrID, labelID string, count int) {
|
||||
func clientFetch(client *client.Client, mailbox string) ([]*imap.Message, error) {
|
||||
status, err := client.Select(mailbox, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if status.Messages == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
resCh := make(chan *imap.Message)
|
||||
|
||||
go func() {
|
||||
if err := client.Fetch(
|
||||
&imap.SeqSet{Set: []imap.Seq{{Start: 1, Stop: status.Messages}}},
|
||||
[]imap.FetchItem{imap.FetchFlags, imap.FetchEnvelope, imap.FetchUid, "BODY.PEEK[]"},
|
||||
resCh,
|
||||
); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
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)
|
||||
|
||||
return createMessages(ctx, t, c, addrID, labelID, xslices.Repeat(literal, count)...)
|
||||
}
|
||||
|
||||
func createMessages(ctx context.Context, t *testing.T, c *proton.Client, addrID, labelID string, messages ...[]byte) []string {
|
||||
user, err := c.GetUser(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
@ -174,22 +428,30 @@ func createMessages(ctx context.Context, t *testing.T, c *proton.Client, addrID,
|
||||
_, addrKRs, err := proton.Unlock(user, addr, keyPass)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, getErr(stream.Collect(ctx, c.ImportMessages(
|
||||
_, ok := addrKRs[addrID]
|
||||
require.True(t, ok)
|
||||
|
||||
res, err := stream.Collect(ctx, c.ImportMessages(
|
||||
ctx,
|
||||
addrKRs[addrID],
|
||||
runtime.NumCPU(),
|
||||
runtime.NumCPU(),
|
||||
iterator.Collect(iterator.Map(iterator.Counter(count), func(i int) proton.ImportReq {
|
||||
xslices.Map(messages, func(message []byte) proton.ImportReq {
|
||||
return proton.ImportReq{
|
||||
Metadata: proton.ImportMetadata{
|
||||
AddressID: addrID,
|
||||
LabelIDs: []string{labelID},
|
||||
Flags: proton.MessageFlagReceived,
|
||||
},
|
||||
Message: literal,
|
||||
Message: message,
|
||||
}
|
||||
}))...,
|
||||
))))
|
||||
})...,
|
||||
))
|
||||
require.NoError(t, err)
|
||||
|
||||
return xslices.Map(res, func(res proton.ImportRes) string {
|
||||
return res.MessageID
|
||||
})
|
||||
}
|
||||
|
||||
func countBytesRead(ctl *proton.NetCtl, fn func()) uint64 {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
@ -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
|
||||
@ -51,6 +52,7 @@ type TLSReporter interface {
|
||||
type Autostarter interface {
|
||||
Enable() error
|
||||
Disable() error
|
||||
IsEnabled() bool
|
||||
}
|
||||
|
||||
type Updater interface {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
@ -75,6 +75,11 @@ func (bridge *Bridge) GetUserIDs() []string {
|
||||
return bridge.vault.GetUserIDs()
|
||||
}
|
||||
|
||||
// HasUser returns true iff the given user is known (authorized or not).
|
||||
func (bridge *Bridge) HasUser(userID string) bool {
|
||||
return bridge.vault.HasUser(userID)
|
||||
}
|
||||
|
||||
// GetUserInfo returns info about the given user.
|
||||
func (bridge *Bridge) GetUserInfo(userID string) (UserInfo, error) {
|
||||
return safe.RLockRetErr(func() (UserInfo, error) {
|
||||
@ -89,7 +94,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)
|
||||
}
|
||||
@ -124,7 +129,7 @@ func (bridge *Bridge) LoginAuth(ctx context.Context, username string, password [
|
||||
return nil, proton.Auth{}, fmt.Errorf("failed to create new API client: %w", err)
|
||||
}
|
||||
|
||||
if ok := safe.RLockRet(func() bool { return mapHas(bridge.users, auth.UID) }, bridge.usersLock); ok {
|
||||
if ok := safe.RLockRet(func() bool { return mapHas(bridge.users, auth.UserID) }, bridge.usersLock); ok {
|
||||
logrus.WithField("userID", auth.UserID).Warn("User already logged in")
|
||||
|
||||
if err := client.AuthDelete(ctx); err != nil {
|
||||
@ -324,30 +329,36 @@ 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")
|
||||
|
||||
return bridge.vault.ForUser(runtime.NumCPU(), func(user *vault.User) error {
|
||||
log := logrus.WithField("userID", user.UserID())
|
||||
|
||||
if user.AuthUID() == "" {
|
||||
log.Info("Not loading disconnected user")
|
||||
return nil
|
||||
}
|
||||
|
||||
if safe.RLockRet(func() bool { return mapHas(bridge.users, user.UserID()) }, bridge.usersLock) {
|
||||
log.Debug("Not loading already-loaded user")
|
||||
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(),
|
||||
@ -362,7 +373,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")
|
||||
@ -384,6 +395,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
|
||||
}
|
||||
|
||||
@ -441,9 +458,9 @@ func (bridge *Bridge) addUserWithVault(
|
||||
ctx,
|
||||
vault,
|
||||
client,
|
||||
bridge.reporter,
|
||||
apiUser,
|
||||
bridge.crashHandler,
|
||||
bridge.reporter,
|
||||
bridge.vault.SyncWorkers(),
|
||||
bridge.vault.GetShowAllMail(),
|
||||
)
|
||||
@ -499,7 +516,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)
|
||||
}
|
||||
@ -545,11 +562,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,
|
||||
}
|
||||
}
|
||||
|
||||
360
internal/bridge/user_event_test.go
Normal file
360
internal/bridge/user_event_test.go
Normal file
@ -0,0 +1,360 @@
|
||||
// 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_BadMessage_BadEvent(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, func(messageID string) string {
|
||||
return "/mail/v4/messages/" + messageID
|
||||
}), req.URL.Path) < 0 {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return http.StatusBadRequest, true
|
||||
})
|
||||
|
||||
userReceiveBadErrorAndLogout(t, bridge, mocks)
|
||||
|
||||
// Remove messages
|
||||
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||
require.NoError(t, c.DeleteMessage(ctx, messageIDs...))
|
||||
})
|
||||
doBadRequest = false
|
||||
|
||||
// Login again
|
||||
_, err := bridge.LoginFull(ctx, "user", password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
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)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 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 userReceiveBadErrorAndLogout(
|
||||
t *testing.T,
|
||||
bridge *bridge.Bridge,
|
||||
mocks *bridge.Mocks,
|
||||
) {
|
||||
// The user will continue to process events and will receive bad request errors.
|
||||
mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).MinTimes(1)
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// userContinueEventProcess checks that user will continue to process events and will not receive any bad request errors.
|
||||
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)
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
@ -21,10 +21,12 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/ProtonMail/gluon/reporter"
|
||||
"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 {
|
||||
@ -51,6 +53,9 @@ func (bridge *Bridge) handleUserEvent(ctx context.Context, user *user.User, even
|
||||
|
||||
case events.UserDeauth:
|
||||
bridge.handleUserDeauth(ctx, user)
|
||||
|
||||
case events.UserBadEvent:
|
||||
bridge.handleUserBadEvent(ctx, user, event.Error)
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -130,3 +135,15 @@ func (bridge *Bridge) handleUserDeauth(ctx context.Context, user *user.User) {
|
||||
bridge.logoutUser(ctx, user, false, false)
|
||||
}, bridge.usersLock)
|
||||
}
|
||||
|
||||
func (bridge *Bridge) handleUserBadEvent(ctx context.Context, user *user.User, err error) {
|
||||
safe.Lock(func() {
|
||||
if rerr := bridge.reporter.ReportMessageWithContext("Failed to handle event", reporter.Context{
|
||||
"error": err,
|
||||
}); rerr != nil {
|
||||
logrus.WithError(rerr).Error("Failed to report failed event handling")
|
||||
}
|
||||
|
||||
bridge.logoutUser(ctx, user, true, false)
|
||||
}, bridge.usersLock)
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
@ -61,6 +61,24 @@ func TestBridge_Login(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_LoginTwice(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) {
|
||||
// Login the user.
|
||||
userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// The user is now connected.
|
||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
|
||||
|
||||
// Additional login should fail.
|
||||
_, err = bridge.LoginFull(ctx, username, password, nil, nil)
|
||||
require.Error(t, err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_LoginLogoutLogin(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) {
|
||||
@ -592,7 +610,7 @@ func TestBridge_UserInfo_Alias(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) {
|
||||
// Create a new user.
|
||||
userID, _, err := s.CreateUser("primary", "primary@pm.me", []byte("password"))
|
||||
userID, _, err := s.CreateUser("primary", []byte("password"))
|
||||
require.NoError(t, err)
|
||||
|
||||
// Give the new user an alias.
|
||||
@ -606,7 +624,7 @@ func TestBridge_UserInfo_Alias(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// The user should have two addresses, the primary should be first.
|
||||
require.Equal(t, []string{"primary@pm.me", "alias@pm.me"}, info.Addresses)
|
||||
require.Equal(t, []string{"primary@" + s.GetDomain(), "alias@pm.me"}, info.Addresses)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
@ -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("")
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
@ -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,19 @@ 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
|
||||
Error error
|
||||
}
|
||||
|
||||
func (event UserBadEvent) String() string {
|
||||
return fmt.Sprintf("UserBadEvent: UserID: %s, Error: %s", event.UserID, event.Error)
|
||||
}
|
||||
|
||||
// UserDeleted is emitted when a user has been deleted.
|
||||
type UserDeleted struct {
|
||||
eventBase
|
||||
|
||||
@ -102,6 +122,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,6 +133,7 @@ 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
|
||||
|
||||
@ -122,6 +144,7 @@ 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
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
// Copyright (c) 2023 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user