mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-10 12:46:46 +00:00
Compare commits
201 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c6f1f159f3 | |||
| 82af4e01bc | |||
| 9ad5f74409 | |||
| 10cf153678 | |||
| 5ba07db7e3 | |||
| ad0d4ebd36 | |||
| 9f3c14ab1e | |||
| 74cf5d422b | |||
| dcf694588c | |||
| 82c388a0dd | |||
| 94ed09b437 | |||
| 57962e5757 | |||
| 8a5c8eaf6e | |||
| 30029f489e | |||
| 2faeebe9e7 | |||
| f6727a56d2 | |||
| d7fd39503f | |||
| b4b66f94ec | |||
| cbd36184bd | |||
| 465f754803 | |||
| 2fa7c97f39 | |||
| 9048b14fdb | |||
| 43100d11bf | |||
| 4876314cf5 | |||
| 2f75131710 | |||
| 1e09fd6662 | |||
| 48f2c56caa | |||
| 20d83dd476 | |||
| 9c6be78b4c | |||
| 0a8e71771e | |||
| 29d1c7bccd | |||
| ca1996a670 | |||
| ab1c1c474a | |||
| d7cac8a8f0 | |||
| 63bc87cc86 | |||
| 232875d5cc | |||
| 5ea53ea5c0 | |||
| 367c505444 | |||
| 3bd39b3ea5 | |||
| e89dcb2cca | |||
| 2cb2ca15c7 | |||
| db41645159 | |||
| 4cf23bb2e6 | |||
| ea11c1046a | |||
| df40f27069 | |||
| 76d732f247 | |||
| b17bdad864 | |||
| 52daa165a2 | |||
| 4c5ba04822 | |||
| d7ff54d679 | |||
| 4aa1091f62 | |||
| 6d024d2055 | |||
| c86cdf737f | |||
| 7e36b215fe | |||
| badebbef9f | |||
| 5e072c3282 | |||
| 048c3a900c | |||
| 88a9fe410c | |||
| cf32b84257 | |||
| e7dea0a77f | |||
| 160489a771 | |||
| 996c6826b9 | |||
| 43b871a124 | |||
| d1bf186040 | |||
| 24c68f100e | |||
| 1bfabf9a83 | |||
| e8a778feca | |||
| 5d4c10c56e | |||
| 60b1c4d8f7 | |||
| f1404cd3ee | |||
| ee4da8a89c | |||
| 5a70a16149 | |||
| f019ba3713 | |||
| 4b966c4845 | |||
| 94703bcf37 | |||
| 40cc6b54c9 | |||
| 584ea7e9f8 | |||
| cbdbc124db | |||
| b9c3fa9401 | |||
| 0e4ec8a8b8 | |||
| c3e4bb80a8 | |||
| 6459840507 | |||
| 87abbe9396 | |||
| d26a8319b6 | |||
| c9c80fd861 | |||
| fea4cc7b3b | |||
| cba5da22ae | |||
| 59a29da054 | |||
| c70674471e | |||
| 7882324439 | |||
| 1f8866a48a | |||
| faf28a6d4e | |||
| 59745e6fb6 | |||
| c8925cd270 | |||
| e35f3b6056 | |||
| d68014ec7b | |||
| b63029054d | |||
| 849c8bee78 | |||
| 70f0384cc3 | |||
| 5459720523 | |||
| a00e2acb5c | |||
| 1d405076e6 | |||
| 7119c566ef | |||
| 0e9428aaae | |||
| fe009ca235 | |||
| a377384553 | |||
| 03c8c323bc | |||
| fdbc380421 | |||
| 7056134b24 | |||
| 93c7552a41 | |||
| 931ed119bb | |||
| 0580842ad2 | |||
| 8d9db83a87 | |||
| c3eb6b2dbf | |||
| 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 | |||
| f551732a17 | |||
| 7a814faed2 | |||
| 792317e945 | |||
| 9c10e06aac | |||
| c39108043b | |||
| 2ca9ca3cb6 | |||
| 87ce5a6d82 | |||
| 9623e2de6f | |||
| b9b4c1c38d | |||
| 688cb30d4a | |||
| 1aca2cde71 | |||
| 49fa451cc3 | |||
| 5f1389f824 | |||
| a90693e488 | |||
| ebeec056cd | |||
| 49d65292c0 | |||
| 6c30a04ac0 | |||
| 4003e0a2ab | |||
| e87db5b2ab | |||
| 5b9c28e6f0 | |||
| 4375d77a98 | |||
| 842c9c8ecd | |||
| f3cc19b09c | |||
| 6b8faf0ecf | |||
| 71ad1e9939 | |||
| f355cb4d38 | |||
| 5ae8d274c0 | |||
| 6402894096 |
@ -54,17 +54,6 @@ stages:
|
|||||||
allow_failure: true
|
allow_failure: true
|
||||||
- when: never
|
- when: never
|
||||||
|
|
||||||
.rules-branch-manual-MR-always-allow-failure:
|
|
||||||
rules:
|
|
||||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
||||||
when: always
|
|
||||||
allow_failure: true
|
|
||||||
- if: $CI_COMMIT_BRANCH
|
|
||||||
when: manual
|
|
||||||
allow_failure: true
|
|
||||||
- when: never
|
|
||||||
|
|
||||||
|
|
||||||
# Stage: TEST
|
# Stage: TEST
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
@ -88,7 +77,7 @@ test-linux:
|
|||||||
test-linux-race:
|
test-linux-race:
|
||||||
stage: test
|
stage: test
|
||||||
extends:
|
extends:
|
||||||
- .rules-branch-manual-MR-always-allow-failure
|
- .rules-branch-and-MR-manual
|
||||||
script:
|
script:
|
||||||
- make test-race
|
- make test-race
|
||||||
tags:
|
tags:
|
||||||
@ -106,7 +95,7 @@ test-integration:
|
|||||||
test-integration-race:
|
test-integration-race:
|
||||||
stage: test
|
stage: test
|
||||||
extends:
|
extends:
|
||||||
- .rules-branch-manual-MR-always-allow-failure
|
- .rules-branch-and-MR-manual
|
||||||
script:
|
script:
|
||||||
- make test-integration-race
|
- make test-integration-race
|
||||||
tags:
|
tags:
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
- the go-rfc5322 module cannot currently be compiled for 32-bit OSes
|
- the go-rfc5322 module cannot currently be compiled for 32-bit OSes
|
||||||
* Go 1.18
|
* Go 1.18
|
||||||
* Bash with basic build utils: make, gcc, sed, find, grep, ...
|
* Bash with basic build utils: make, gcc, sed, find, grep, ...
|
||||||
- For Windows it is recommended to use MinGW 64bit shell from [MSYS2](https://www.msys2.org/)
|
- For Windows, it is recommended to use MinGW 64bit shell from [MSYS2](https://www.msys2.org/)
|
||||||
* GCC (linux), msvc (windows) or Xcode (macOS)
|
* GCC (linux), msvc (windows) or Xcode (macOS)
|
||||||
* Windres (windows)
|
* Windres (windows)
|
||||||
* libglvnd and libsecret development files (linux)
|
* libglvnd and libsecret development files (linux)
|
||||||
@ -44,9 +44,10 @@ make build
|
|||||||
make build-nogui
|
make build-nogui
|
||||||
```
|
```
|
||||||
|
|
||||||
* Bridge without GUI will start by default without any interface (i.e., there is no way to add or remove client, get bridge password, etc)
|
* To launch Bridge without GUI, you can invoke the `bridge` executable with one the following command-line switches:
|
||||||
* Bridge always has the option (whether built with Qt or without) to use a CLI interface by starting it with the argument `-c`
|
* `--noninteractive` or `-n` to start Bridge without any interface (i.e., there is no way to add or remove client, get bridge password, etc.)
|
||||||
* NOTE: You still need to setup supported keychain on your system
|
* `--cli` or `-c` to start Bridge with an interactive terminal interface.
|
||||||
|
* NOTE: You still need to set up a supported keychain on your system.
|
||||||
|
|
||||||
## Launchers
|
## Launchers
|
||||||
Launchers are only included in official distributions and provide the public
|
Launchers are only included in official distributions and provide the public
|
||||||
|
|||||||
@ -132,7 +132,6 @@ Proton Mail Bridge includes the following 3rd party software:
|
|||||||
gopkg.in/yaml.v2
|
gopkg.in/yaml.v2
|
||||||
gopkg.in/yaml.v3
|
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)
|
* [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-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)
|
* [go-keychain](https://github.com/cuthix/go-keychain) available under [license](https://github.com/cuthix/go-keychain/blob/master/LICENSE)
|
||||||
<!-- END AUTOGEN -->
|
<!-- END AUTOGEN -->
|
||||||
|
|||||||
187
Changelog.md
187
Changelog.md
@ -2,6 +2,193 @@
|
|||||||
|
|
||||||
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||||
|
|
||||||
|
## [Bridge 3.0.18] Perth Narrows
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* GODT-2364: Wait and retry once if the gRPC service config file exists but cannot be opened.
|
||||||
|
* GODT-2364: Added optional details to C++ exceptions.
|
||||||
|
* GODT-2413: Use qEnvironmentVariable() instead of qgetenv().
|
||||||
|
* GODT-2412: Don't treat context cancellation as BadEvent.
|
||||||
|
* GODT-2404: Handle unexpected EOF.
|
||||||
|
* GODT-2400: Allow state updates to be applied if command fails.
|
||||||
|
* GODT-2399: Fix immediate message deletion during updates.
|
||||||
|
* GODT-2390: Missing changes from pervious commit.
|
||||||
|
* GODT-2390: Add reports for uncaught json and net.opErr.
|
||||||
|
* GODT-2414: Multiple deletion bug in WriteControlledStore.
|
||||||
|
|
||||||
|
|
||||||
|
## [Bridge 3.0.18] Perth Narrows
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* GODT-2392: Create message if gluon updateMessage returns `no such message`.
|
||||||
|
* GODT-2391: Create draft if missing during message update on gluon side.
|
||||||
|
|
||||||
|
## [Bridge 3.0.16/17] Perth Narrows
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* GODT-2371: Continue, not return, when handling draft.
|
||||||
|
|
||||||
|
## [Bridge 3.0.15] Perth Narrows
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* GODT-2355: Improve wording and actions on bad event.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* GODT-2354: Report failed load users.
|
||||||
|
* GODT-2353: Show popup only after 3.0.16.
|
||||||
|
* GODT-2351: Bump GPA to better handle net.OpError.
|
||||||
|
|
||||||
|
|
||||||
|
## [Bridge 3.0.14] Perth Narrows
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* GODT-2323: Fix Expunge not issued for move.
|
||||||
|
* GODT-2341: Handle URL error.
|
||||||
|
* GODT-2340: Improve logging.
|
||||||
|
* GODT-2278: Improve sentry logs.
|
||||||
|
* GODT-2327: Sync issues when migrating DB.
|
||||||
|
* GODT-2318: Remove gluon DB if label sync was incomplete.
|
||||||
|
* GODT-1804: Only promote content headers if non-empty.
|
||||||
|
* GODT-2343: Only poll after send if sync is complete.
|
||||||
|
* GODT-2336: Recover from changed address order while bridge is down.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [Bridge 3.0.13] Perth Narrows
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
GODT-2328: Ignore labels that aren't part of user label set.
|
||||||
|
GODT-2326: Sync issue on missing fresh DB file.
|
||||||
|
GODT-2319: Seed the math/rand RNG on app startup.
|
||||||
|
GODT-1804: Preserve MIME parameters when uploading attachments.
|
||||||
|
|
||||||
|
|
||||||
|
## [Bridge 3.0.12] Perth Narrows
|
||||||
|
|
||||||
|
### Added
|
||||||
|
* GODT-2210: v3.0 splash screen.
|
||||||
|
* GODT-1770: handle UserBadEvent in CLI and gRPC.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* GODT-2311: Fix missing headers in re-downloaded Gluon messages.
|
||||||
|
* GODT-1453: clicking 'Sign in' from status window now selects the right account.
|
||||||
|
* GODT-2297: More significantly improve GPA's paging algorithm.
|
||||||
|
* GODT-2145: Fix button spacing w/ Qt 6.4.
|
||||||
|
* GODT-2223: Improve event handling.
|
||||||
|
* GODT-2305: Detect missing gluon DB.
|
||||||
|
* GODT-2291: Change gluon store default location from Cache to Data.
|
||||||
|
* Other: Disable dialer test until badssl cert is bumbed.
|
||||||
|
* GODT-2292: Updated BUILDS.md doc.
|
||||||
|
* GODT-2258: suggest email as login when signing in via status window.
|
||||||
|
* Other: Report corrupt and/or insecure vaults to sentry.
|
||||||
|
* Other: Better user load logs.
|
||||||
|
* GODT-2253: Restart Launcher from the gui when GUI crashes.
|
||||||
|
* Other(test): Make All Mail copy test more robust.
|
||||||
|
* Other(CI): Make race checks manual.
|
||||||
|
* Other: Remove old cert/key file location handling.
|
||||||
|
* GODT-2271: Update README with new system files path.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* GODT-2210: Fix splash screen always showing on CentOS and Ubuntu.
|
||||||
|
* GODT-2296: Log error rather than fail if cannot get parent ID.
|
||||||
|
* GODT-2266: Pause event stream while sending.
|
||||||
|
* GODT-2266: Add test for sent message flags.
|
||||||
|
* Other(test): Fix some more integration test placeholders.
|
||||||
|
* GODT-2177: Use correct attachment disposition when content ID is set.
|
||||||
|
* GODT-1556: If no references, use the in-reply-to header as ParentID.
|
||||||
|
* Other: make GUI Tester more resilient to Bridge abrupt termination.
|
||||||
|
* GODT-2275: fixed location of bridge-gui log files.
|
||||||
|
* Other: Ensure SMTP debug dump works on windows.
|
||||||
|
* Other: Fix MaxLogs off-by-one limit and bump limit to 10.
|
||||||
|
* Other: fix path of temp folder in README.
|
||||||
|
* Other(debug): Dump raw SMTP input to user's home dir.
|
||||||
|
|
||||||
|
|
||||||
|
## [Bridge 3.0.11] Perth Narrows
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* GODT-2252: Recover from deleted cached messages.
|
||||||
|
* GODT-2258: change login label and suggest email instead of username.
|
||||||
|
* Other: Don't clean settings path on teardown.
|
||||||
|
* Other: Bump GPA to v0.3.0.
|
||||||
|
* Other: added user's primary email address to the vault.
|
||||||
|
* GODT-2251: gluon store and DB separated.
|
||||||
|
* GODT-2093: use the primary email address in the account view and status view.
|
||||||
|
* GODT-2202: Report update errors from Gluon.
|
||||||
|
* GODT-2229: Own the full path for gluon and do not change Database path.
|
||||||
|
* GODT-1797: copyright notice shows a date range with the build year.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* GODT-2223: Handle bad events by logging user out.
|
||||||
|
* GODT-2165: Reduce UTF8 parsing errors from TLS header input.
|
||||||
|
* Others: chores fix a QML warning when no account is present* and a few typos in QML.
|
||||||
|
* Other(test): Fix integration test steps.
|
||||||
|
* GODT-2226: Fix moving drafts to trash.
|
||||||
|
* GODT-2246: do not report API error 422 when using an invalid email address.
|
||||||
|
|
||||||
|
|
||||||
|
## [Bridge 3.0.10] Perth Narrows
|
||||||
|
|
||||||
|
### 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
|
## [Bridge 3.0.7] Perth Narrows
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
27
Makefile
27
Makefile
@ -11,7 +11,7 @@ ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
|
|||||||
.PHONY: build build-gui build-nogui build-launcher versioner hasher
|
.PHONY: build build-gui build-nogui build-launcher versioner hasher
|
||||||
|
|
||||||
# Keep version hardcoded so app build works also without Git repository.
|
# Keep version hardcoded so app build works also without Git repository.
|
||||||
BRIDGE_APP_VERSION?=3.0.7+git
|
BRIDGE_APP_VERSION?=3.0.19+git
|
||||||
APP_VERSION:=${BRIDGE_APP_VERSION}
|
APP_VERSION:=${BRIDGE_APP_VERSION}
|
||||||
APP_FULL_NAME:=Proton Mail Bridge
|
APP_FULL_NAME:=Proton Mail Bridge
|
||||||
APP_VENDOR:=Proton AG
|
APP_VENDOR:=Proton AG
|
||||||
@ -21,7 +21,8 @@ SRC_SVG:=bridge.svg
|
|||||||
EXE_NAME:=proton-bridge
|
EXE_NAME:=proton-bridge
|
||||||
REVISION:=$(shell git rev-parse --short=10 HEAD)
|
REVISION:=$(shell git rev-parse --short=10 HEAD)
|
||||||
BUILD_TIME:=$(shell date +%FT%T%z)
|
BUILD_TIME:=$(shell date +%FT%T%z)
|
||||||
MACOS_MIN_VERSION=11.0
|
MACOS_MIN_VERSION_ARM64=11.0
|
||||||
|
MACOS_MIN_VERSION_AMD64=10.15
|
||||||
|
|
||||||
BUILD_FLAGS:=-tags='${BUILD_TAGS}'
|
BUILD_FLAGS:=-tags='${BUILD_TAGS}'
|
||||||
BUILD_FLAGS_LAUNCHER:=${BUILD_FLAGS}
|
BUILD_FLAGS_LAUNCHER:=${BUILD_FLAGS}
|
||||||
@ -88,8 +89,8 @@ go-build=go build $(1) -o $(2) $(3)
|
|||||||
go-build-finalize=${go-build}
|
go-build-finalize=${go-build}
|
||||||
ifeq "${GOOS}-$(shell uname -m)" "darwin-arm64"
|
ifeq "${GOOS}-$(shell uname -m)" "darwin-arm64"
|
||||||
go-build-finalize= \
|
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_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} 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_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
|
lipo -create -output $(2) $(2)_arm $(2)_amd && rm -f $(2)_arm $(2)_amd
|
||||||
endif
|
endif
|
||||||
|
|
||||||
@ -280,7 +281,7 @@ updates: install-go-mod-outdated
|
|||||||
doc:
|
doc:
|
||||||
godoc -http=:6060
|
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
|
release-notes/%.html: release-notes/%.md
|
||||||
./utils/release_notes.sh $^
|
./utils/release_notes.sh $^
|
||||||
@ -293,7 +294,7 @@ gofiles: ./internal/bridge/credits.go
|
|||||||
cd ./utils/ && ./credits.sh bridge
|
cd ./utils/ && ./credits.sh bridge
|
||||||
|
|
||||||
## Run and debug
|
## Run and debug
|
||||||
.PHONY: run run-qt run-qt-cli run-nogui run-cli run-noninteractive run-debug run-qml-preview clean-vendor clean-frontend-qt clean-frontend-qt-common clean
|
.PHONY: run run-qt run-qt-cli run-nogui run-cli run-noninteractive run-debug run-gui-tester clean-vendor clean-frontend-qt clean-frontend-qt-common clean
|
||||||
|
|
||||||
LOG?=debug
|
LOG?=debug
|
||||||
LOG_IMAP?=client # client/server/all, or empty to turn it off
|
LOG_IMAP?=client # client/server/all, or empty to turn it off
|
||||||
@ -320,6 +321,20 @@ run-nogui: build-nogui clean-vendor gofiles
|
|||||||
run-debug:
|
run-debug:
|
||||||
dlv debug ./cmd/Desktop-Bridge/main.go -- -l=debug
|
dlv debug ./cmd/Desktop-Bridge/main.go -- -l=debug
|
||||||
|
|
||||||
|
ifeq "${TARGET_OS}" "windows"
|
||||||
|
EXE_SUFFIX=.exe
|
||||||
|
endif
|
||||||
|
|
||||||
|
bridge-gui-tester: build-gui
|
||||||
|
cp ./cmd/Desktop-Bridge/deploy/${TARGET_OS}/bridge-gui${EXE_SUFFIX} .
|
||||||
|
cd ./internal/frontend/bridge-gui/bridge-gui-tester && cmake . && make
|
||||||
|
|
||||||
|
run-gui-tester: bridge-gui-tester
|
||||||
|
# copying tester as bridge so bridge-gui will start it and connect to it automatically
|
||||||
|
cp ./internal/frontend/bridge-gui/bridge-gui-tester/bridge-gui-tester${EXE_SUFFIX} bridge${EXE_SUFFIX}
|
||||||
|
./bridge-gui${EXE_SUFFIX}
|
||||||
|
|
||||||
|
|
||||||
clean-vendor:
|
clean-vendor:
|
||||||
rm -rf ./vendor
|
rm -rf ./vendor
|
||||||
|
|
||||||
|
|||||||
52
README.md
52
README.md
@ -62,35 +62,33 @@ major problems.
|
|||||||
- `TAGS`: set build tags for tests
|
- `TAGS`: set build tags for tests
|
||||||
- `FEATURES`: set feature dir, file or scenario to test
|
- `FEATURES`: set feature dir, file or scenario to test
|
||||||
|
|
||||||
|
## Folders
|
||||||
|
|
||||||
|
There are now three types of system folders which Bridge recognises:
|
||||||
|
|
||||||
|
| | Windows | Mac | Linux | Linux (XDG) |
|
||||||
|
|--------|-------------------------------------|-----------------------------------------------------|-------------------------------------|---------------------------------------|
|
||||||
|
| config | %APPDATA%\protonmail\bridge-v3 | ~/Library/Application Support/protonmail/bridge-v3 | ~/.config/protonmail/bridge-v3 | $XDG_CONFIG_HOME/protonmail/bridge-v3 |
|
||||||
|
| cache | %LOCALAPPDATA%\protonmail\bridge-v3 | ~/Library/Caches/protonmail/bridge-v3 | ~/.cache/protonmail/bridge-v3 | $XDG_CACHE_HOME/protonmail/bridge-v3 |
|
||||||
|
| data | %APPDATA%\protonmail\bridge-v3 | ~/Library/Application Support/protonmail/bridge-v3 | ~/.local/share/protonmail/bridge-v3 | $XDG_DATA_HOME/protonmail/bridge-v3 |
|
||||||
|
| temp | %LOCALAPPDATA%\Temp | $TMPDIR if non-empty, else /tmp | $TMPDIR if non-empty, else /tmp | $TMPDIR if non-empty, else /tmp |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Files
|
## Files
|
||||||
### Database
|
|
||||||
The database stores metadata necessary for presenting messages and mailboxes to an email client:
|
|
||||||
- Linux: `~/.cache/protonmail/bridge/<cacheVersion>/mailbox-<userID>.db` (unless `XDG_CACHE_HOME` is set, in which case that is used as your `~`)
|
|
||||||
- macOS: `~/Library/Caches/protonmail/bridge/<cacheVersion>/mailbox-<userID>.db`
|
|
||||||
- Windows: `%LOCALAPPDATA%\protonmail\bridge\<cacheVersion>\mailbox-<userID>.db`
|
|
||||||
|
|
||||||
### Preferences
|
| | Base Dir | Path |
|
||||||
User preferences are stored in json at the following location:
|
|-----------------------|----------|----------------------------|
|
||||||
- Linux: `~/.config/protonmail/bridge/prefs.json`
|
| bridge lock file | cache | bridge.lock |
|
||||||
- macOS: `~/Library/ApplicationSupport/protonmail/bridge/prefs.json`
|
| bridge-gui lock file | cache | bridge-gui.lock |
|
||||||
- Windows: `%APPDATA%\protonmail\bridge\prefs.json`
|
| vault | config | vault.enc |
|
||||||
|
| gRPC server json | config | grpcServerConfig.json |
|
||||||
|
| gRPC client json | config | grpcClientConfig_<id>.json |
|
||||||
|
| Logs | data | logs |
|
||||||
|
| gluon DB | data | gluon/backend/db |
|
||||||
|
| gluon messages | sata | gluon/backend/store |
|
||||||
|
| Update files | data | updates |
|
||||||
|
| sentry cache | data | sentry_cache |
|
||||||
|
| Mac/Linux File Socket | temp | bridge_{RANDOM_UUID}.sock |
|
||||||
|
|
||||||
### IMAP Cache
|
|
||||||
The currently subscribed mailboxes are held in a json file:
|
|
||||||
- Linux: `~/.cache/protonmail/bridge/<cacheVersion>/user_info.json` (unless `XDG_CACHE_HOME` is set, in which case that is used as your `~`)
|
|
||||||
- macOS: `~/Library/Caches/protonmail/bridge/<cacheVersion>/user_info.json`
|
|
||||||
- Windows: `%LOCALAPPDATA%\protonmail\bridge\<cacheVersion>\user_info.json`
|
|
||||||
|
|
||||||
### Lock file
|
|
||||||
Bridge utilises an on-disk lock to ensure only one instance is run at once. The lock file is here:
|
|
||||||
- Linux: `~/.cache/protonmail/bridge/<cacheVersion>/bridge.lock` (unless `XDG_CACHE_HOME` is set, in which case that is used as your `~`)
|
|
||||||
- macOS: `~/Library/Caches/protonmail/bridge/<cacheVersion>/bridge.lock`
|
|
||||||
- Windows: `%LOCALAPPDATA%\protonmail\bridge\<cacheVersion>\bridge.lock`
|
|
||||||
|
|
||||||
### TLS Certificate and Key
|
|
||||||
When bridge first starts, it generates a unique TLS certificate and key file at the following locations:
|
|
||||||
- Linux: `~/.config/protonmail/bridge/{cert,key}.pem` (unless `XDG_CONFIG_HOME` is set, in which case that is used as your `~/.config`)
|
|
||||||
- macOS: `~/Library/ApplicationSupport/protonmail/bridge/{cert,key}.pem`
|
|
||||||
- Windows: `%APPDATA%\protonmail\bridge\{cert,key}.pem`
|
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2022 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// 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.
|
// 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.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
|
|||||||
5
go.mod
5
go.mod
@ -5,9 +5,9 @@ go 1.18
|
|||||||
require (
|
require (
|
||||||
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
|
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
|
||||||
github.com/Masterminds/semver/v3 v3.1.1
|
github.com/Masterminds/semver/v3 v3.1.1
|
||||||
github.com/ProtonMail/gluon v0.14.2-0.20221214122222-2ab5c92d3546
|
github.com/ProtonMail/gluon v0.14.2-0.20230227135029-cef8f5824680
|
||||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
|
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
|
||||||
github.com/ProtonMail/go-proton-api v0.2.1
|
github.com/ProtonMail/go-proton-api v0.3.1-0.20230209110241-fe7894c4931a
|
||||||
github.com/ProtonMail/go-rfc5322 v0.11.0
|
github.com/ProtonMail/go-rfc5322 v0.11.0
|
||||||
github.com/ProtonMail/gopenpgp/v2 v2.4.10
|
github.com/ProtonMail/gopenpgp/v2 v2.4.10
|
||||||
github.com/PuerkitoBio/goquery v1.8.0
|
github.com/PuerkitoBio/goquery v1.8.0
|
||||||
@ -120,7 +120,6 @@ require (
|
|||||||
|
|
||||||
replace (
|
replace (
|
||||||
github.com/docker/docker-credential-helpers => github.com/ProtonMail/docker-credential-helpers v1.1.0
|
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/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
|
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/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
|
||||||
github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
|
github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
|
||||||
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
|
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
|
||||||
github.com/ProtonMail/gluon v0.14.2-0.20221214122222-2ab5c92d3546 h1:iyN4eO1Z0N+inMukpoBCmfbI+ubAop4Op/sdzmmUcm4=
|
github.com/ProtonMail/gluon v0.14.2-0.20230227135029-cef8f5824680 h1:NGp7LfbsKePRHBgMcgquycHx3CSuS7255i0wanAiCuY=
|
||||||
github.com/ProtonMail/gluon v0.14.2-0.20221214122222-2ab5c92d3546/go.mod h1:z2AxLIiBCT1K+0OBHyaDI7AEaO5qI6/BEC2TE42vs4Q=
|
github.com/ProtonMail/gluon v0.14.2-0.20230227135029-cef8f5824680/go.mod h1:z2AxLIiBCT1K+0OBHyaDI7AEaO5qI6/BEC2TE42vs4Q=
|
||||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
|
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
|
||||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
|
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
|
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20220822140716-1678d6eb0cbe/go.mod h1:UBYPn8k0D56RtnR8RFQMjmh4KrZzWJ5o7Z9SYjossQ8=
|
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 h1:NsReiLpErIPzRrnogAXYwSoU7txA977LjDGrbkewJbg=
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20220824120805-4b6e5c587895/go.mod h1:UBYPn8k0D56RtnR8RFQMjmh4KrZzWJ5o7Z9SYjossQ8=
|
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 h1:I8IsYA297x0QLU80G5I6aLYUu3JYNSpo8j5fkXtFDW0=
|
||||||
github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
|
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-20220302105931-303f85f7fe0f/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4=
|
||||||
github.com/ProtonMail/go-mime v0.0.0-20220429130430-2192574d760f h1:4IWzKjHzZxdrW9k4zl/qCwenOVHDbVDADPPHFLjs0Oc=
|
github.com/ProtonMail/go-mime v0.0.0-20220429130430-2192574d760f h1:4IWzKjHzZxdrW9k4zl/qCwenOVHDbVDADPPHFLjs0Oc=
|
||||||
github.com/ProtonMail/go-mime v0.0.0-20220429130430-2192574d760f/go.mod h1:qRZgbeASl2a9OwmsV85aWwRqic0NHPh+9ewGAzb4cgM=
|
github.com/ProtonMail/go-mime v0.0.0-20220429130430-2192574d760f/go.mod h1:qRZgbeASl2a9OwmsV85aWwRqic0NHPh+9ewGAzb4cgM=
|
||||||
github.com/ProtonMail/go-proton-api v0.2.1 h1:M15/zzfx6EPiskv2+gogUkmvx7Y1SmRRtLT6GiBh5T0=
|
github.com/ProtonMail/go-proton-api v0.3.1-0.20230209110241-fe7894c4931a h1:h9KLPt0HTCJjILYHREWCYnZv+1xaYmOVx/rxiT/1dIg=
|
||||||
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.20230209110241-fe7894c4931a/go.mod h1:JUo5IQG0hNuPRuDpOUsCOvtee6UjTEHHF1QN2i8RSos=
|
||||||
github.com/ProtonMail/go-rfc5322 v0.11.0 h1:o5Obrm4DpmQEffvgsVqG6S4BKwC1Wat+hYwjIp2YcCY=
|
github.com/ProtonMail/go-rfc5322 v0.11.0 h1:o5Obrm4DpmQEffvgsVqG6S4BKwC1Wat+hYwjIp2YcCY=
|
||||||
github.com/ProtonMail/go-rfc5322 v0.11.0/go.mod h1:6oOKr0jXvpoE6pwTx/HukigQpX2J9WUf6h0auplrFTw=
|
github.com/ProtonMail/go-rfc5322 v0.11.0/go.mod h1:6oOKr0jXvpoE6pwTx/HukigQpX2J9WUf6h0auplrFTw=
|
||||||
github.com/ProtonMail/go-srp v0.0.5 h1:xhUioxZgDbCnpo9JehyFhwwsn9JLWkUGfB0oiKXgiGg=
|
github.com/ProtonMail/go-srp v0.0.5 h1:xhUioxZgDbCnpo9JehyFhwwsn9JLWkUGfB0oiKXgiGg=
|
||||||
@ -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-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 h1:AlYZOldA+UJ0/2nBuqWdo90GFCgG9xuyw9SYzGUtJm0=
|
||||||
github.com/elastic/go-windows v1.0.1/go.mod h1:FoVvqWSun28vaDQPbj2Elfc0JahhPB7WQEGa3c814Ss=
|
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 h1:43mBoVwooyLm1+1YVf5nvn1pSFWhw7rOpcrp1Jg/qk0=
|
||||||
github.com/emersion/go-imap-id v0.0.0-20190926060100-f94a56b9ecde/go.mod h1:sPwp0FFboaK/bxsrUz1lNrDMUCsZUsKC5YuM4uRVRVs=
|
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-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 h1:fI1Jck0vUrXT8bnphprS1EoVRe2Q5CKCX8iDlpqjQ/Y=
|
||||||
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
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.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -19,11 +19,13 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/cookiejar"
|
"net/http/cookiejar"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/Masterminds/semver/v3"
|
"github.com/Masterminds/semver/v3"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||||
@ -69,9 +71,10 @@ const (
|
|||||||
|
|
||||||
// Hidden flags.
|
// Hidden flags.
|
||||||
const (
|
const (
|
||||||
flagLauncher = "launcher"
|
flagLauncher = "launcher"
|
||||||
flagNoWindow = "no-window"
|
flagNoWindow = "no-window"
|
||||||
flagParentPID = "parent-pid"
|
flagParentPID = "parent-pid"
|
||||||
|
flagSoftwareRenderer = "software-renderer"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -140,6 +143,12 @@ func New() *cli.App { //nolint:funlen
|
|||||||
Hidden: true,
|
Hidden: true,
|
||||||
Value: -1,
|
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
|
app.Action = run
|
||||||
@ -148,6 +157,9 @@ func New() *cli.App { //nolint:funlen
|
|||||||
}
|
}
|
||||||
|
|
||||||
func run(c *cli.Context) error { //nolint:funlen
|
func run(c *cli.Context) error { //nolint:funlen
|
||||||
|
// Seed the default RNG from the math/rand package.
|
||||||
|
rand.Seed(time.Now().UnixNano())
|
||||||
|
|
||||||
// Get the current bridge version.
|
// Get the current bridge version.
|
||||||
version, err := semver.NewVersion(constants.Version)
|
version, err := semver.NewVersion(constants.Version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -199,6 +211,16 @@ func run(c *cli.Context) error { //nolint:funlen
|
|||||||
return withSingleInstance(locations, version, func() error {
|
return withSingleInstance(locations, version, func() error {
|
||||||
// Unlock the encrypted vault.
|
// Unlock the encrypted vault.
|
||||||
return WithVault(locations, func(vault *vault.Vault, insecure, corrupt bool) error {
|
return WithVault(locations, func(vault *vault.Vault, insecure, corrupt bool) error {
|
||||||
|
// Report insecure vault.
|
||||||
|
if insecure {
|
||||||
|
_ = reporter.ReportMessageWithContext("Vault is insecure", map[string]interface{}{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Report corrupt vault.
|
||||||
|
if corrupt {
|
||||||
|
_ = reporter.ReportMessageWithContext("Vault is corrupt", map[string]interface{}{})
|
||||||
|
}
|
||||||
|
|
||||||
if !vault.Migrated() {
|
if !vault.Migrated() {
|
||||||
// Migrate old settings into the vault.
|
// Migrate old settings into the vault.
|
||||||
if err := migrateOldSettings(vault); err != nil {
|
if err := migrateOldSettings(vault); err != nil {
|
||||||
@ -315,14 +337,7 @@ func WithLocations(fn func(*locations.Locations) error) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create a new locations object that will be used to provide paths to store files.
|
// Create a new locations object that will be used to provide paths to store files.
|
||||||
locations := locations.New(provider, constants.ConfigName)
|
return fn(locations.New(provider, constants.ConfigName))
|
||||||
defer func() {
|
|
||||||
if err := locations.Clean(); err != nil {
|
|
||||||
logrus.WithError(err).Error("Failed to clean locations")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return fn(locations)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start profiling if requested.
|
// Start profiling if requested.
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2022 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// 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.
|
// 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.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -30,6 +30,7 @@ import (
|
|||||||
"github.com/Masterminds/semver/v3"
|
"github.com/Masterminds/semver/v3"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/legacy/credentials"
|
"github.com/ProtonMail/proton-bridge/v3/internal/legacy/credentials"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
|
"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/updater"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/pkg/algo"
|
"github.com/ProtonMail/proton-bridge/v3/pkg/algo"
|
||||||
@ -49,8 +50,8 @@ func migrateKeychainHelper(locations *locations.Locations) error {
|
|||||||
return fmt.Errorf("failed to get settings path: %w", err)
|
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 != "" {
|
if keychainName, _ := vault.GetHelper(settings); keychainName != "" {
|
||||||
// If uncorupted keychain file is already there do not migrate again.
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,7 +125,6 @@ func migrateOldAccounts(locations *locations.Locations, v *vault.Vault) error {
|
|||||||
var migrationErrors error
|
var migrationErrors error
|
||||||
|
|
||||||
for _, userID := range users {
|
for _, userID := range users {
|
||||||
logrus.WithField("userID", userID).Info("Migrating account")
|
|
||||||
if err := migrateOldAccount(userID, store, v); err != nil {
|
if err := migrateOldAccount(userID, store, v); err != nil {
|
||||||
migrationErrors = multierror.Append(migrationErrors, err)
|
migrationErrors = multierror.Append(migrationErrors, err)
|
||||||
}
|
}
|
||||||
@ -134,6 +134,9 @@ func migrateOldAccounts(locations *locations.Locations, v *vault.Vault) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func migrateOldAccount(userID string, store *credentials.Store, v *vault.Vault) error {
|
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)
|
creds, err := store.Get(userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get user %q: %w", userID, err)
|
return fmt.Errorf("failed to get user %q: %w", userID, err)
|
||||||
@ -144,11 +147,19 @@ func migrateOldAccount(userID string, store *credentials.Store, v *vault.Vault)
|
|||||||
return fmt.Errorf("failed to split api token for user %q: %w", userID, err)
|
return fmt.Errorf("failed to split api token for user %q: %w", userID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := v.AddUser(creds.UserID, creds.EmailList()[0], authUID, authRef, creds.MailboxPassword)
|
var primaryEmail string
|
||||||
|
if len(creds.EmailList()) > 0 {
|
||||||
|
primaryEmail = creds.EmailList()[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := v.AddUser(creds.UserID, creds.Name, primaryEmail, authUID, authRef, creds.MailboxPassword)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to add user %q: %w", userID, err)
|
return fmt.Errorf("failed to add user %q: %w", userID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
l = l.WithField("username", logging.Sensitive(user.Username()))
|
||||||
|
l.Info("Migrated account with random bridge password")
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := user.Close(); err != nil {
|
if err := user.Close(); err != nil {
|
||||||
logrus.WithField("userID", userID).WithError(err).Error("Failed to close vault user after migration")
|
logrus.WithField("userID", userID).WithError(err).Error("Failed to close vault user after migration")
|
||||||
@ -161,9 +172,12 @@ func migrateOldAccount(userID string, store *credentials.Store, v *vault.Vault)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := user.SetBridgePass(dec); err != nil {
|
if err := user.SetBridgePass(dec); err != nil {
|
||||||
return fmt.Errorf("failed to set bridge password to user %q: %w", userID, err)
|
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 !creds.IsCombinedAddressMode {
|
||||||
if err := user.SetAddressMode(vault.SplitMode); err != nil {
|
if err := user.SetAddressMode(vault.SplitMode); err != nil {
|
||||||
return fmt.Errorf("failed to set split address mode to user %q: %w", userID, err)
|
return fmt.Errorf("failed to set split address mode to user %q: %w", userID, err)
|
||||||
@ -184,11 +198,10 @@ func migratePrefsToVault(vault *vault.Vault, b []byte) error {
|
|||||||
UpdateChannel updater.Channel `json:"update_channel"`
|
UpdateChannel updater.Channel `json:"update_channel"`
|
||||||
UpdateRollout float64 `json:"rollout,,string"`
|
UpdateRollout float64 `json:"rollout,,string"`
|
||||||
|
|
||||||
FirstStart bool `json:"first_time_start,,string"`
|
FirstStart bool `json:"first_time_start,,string"`
|
||||||
FirstStartGUI bool `json:"first_time_start_gui,,string"`
|
ColorScheme string `json:"color_scheme"`
|
||||||
ColorScheme string `json:"color_scheme"`
|
LastVersion *semver.Version `json:"last_used_version"`
|
||||||
LastVersion *semver.Version `json:"last_used_version"`
|
Autostart bool `json:"autostart,,string"`
|
||||||
Autostart bool `json:"autostart,,string"`
|
|
||||||
|
|
||||||
AllowProxy bool `json:"allow_proxy,,string"`
|
AllowProxy bool `json:"allow_proxy,,string"`
|
||||||
FetchWorkers int `json:"fetch_workers,,string"`
|
FetchWorkers int `json:"fetch_workers,,string"`
|
||||||
@ -232,10 +245,6 @@ func migratePrefsToVault(vault *vault.Vault, b []byte) error {
|
|||||||
errs = multierror.Append(errs, fmt.Errorf("failed to migrate first start: %w", err))
|
errs = multierror.Append(errs, fmt.Errorf("failed to migrate first start: %w", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := vault.SetFirstStartGUI(prefs.FirstStartGUI); err != nil {
|
|
||||||
errs = multierror.Append(errs, fmt.Errorf("failed to migrate first start GUI: %w", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := vault.SetColorScheme(prefs.ColorScheme); err != nil {
|
if err := vault.SetColorScheme(prefs.ColorScheme); err != nil {
|
||||||
errs = multierror.Append(errs, fmt.Errorf("failed to migrate color scheme: %w", err))
|
errs = multierror.Append(errs, fmt.Errorf("failed to migrate color scheme: %w", err))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2022 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -63,7 +63,6 @@ func TestMigratePrefsToVault(t *testing.T) {
|
|||||||
|
|
||||||
// Check that the app settings have been migrated.
|
// Check that the app settings have been migrated.
|
||||||
require.False(t, vault.GetFirstStart())
|
require.False(t, vault.GetFirstStart())
|
||||||
require.True(t, vault.GetFirstStartGUI())
|
|
||||||
require.Equal(t, "blablabla", vault.GetColorScheme())
|
require.Equal(t, "blablabla", vault.GetColorScheme())
|
||||||
require.Equal(t, "2.3.0+git", vault.GetLastVersion().String())
|
require.Equal(t, "2.3.0+git", vault.GetLastVersion().String())
|
||||||
require.True(t, vault.GetAutostart())
|
require.True(t, vault.GetAutostart())
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2022 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// 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.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -89,12 +89,12 @@ func newVault(locations *locations.Locations) (*vault.Vault, bool, bool, error)
|
|||||||
vaultKey = key
|
vaultKey = key
|
||||||
}
|
}
|
||||||
|
|
||||||
gluonDir, err := locations.ProvideGluonPath()
|
gluonCacheDir, err := locations.ProvideGluonCachePath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, false, fmt.Errorf("could not provide gluon path: %w", err)
|
return nil, false, false, fmt.Errorf("could not provide gluon path: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
vault, corrupt, err := vault.New(vaultDir, gluonDir, vaultKey)
|
vault, corrupt, err := vault.New(vaultDir, gluonCacheDir, vaultKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, false, fmt.Errorf("could not create vault: %w", err)
|
return nil, false, false, fmt.Errorf("could not create vault: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2022 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// 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.
|
// 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.
|
// 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.
|
// 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.
|
// 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.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -21,6 +21,7 @@ package bridge
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -38,6 +39,7 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/focus"
|
"github.com/ProtonMail/proton-bridge/v3/internal/focus"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/sentry"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||||
"github.com/bradenaw/juniper/xslices"
|
"github.com/bradenaw/juniper/xslices"
|
||||||
@ -108,6 +110,12 @@ type Bridge struct {
|
|||||||
logIMAPServer bool
|
logIMAPServer bool
|
||||||
logSMTP bool
|
logSMTP bool
|
||||||
|
|
||||||
|
// These two variables keep track of the startup values for the two settings of the same name.
|
||||||
|
// They are updated in the vault on startup so that we're sure they're updated in case of kill/crash,
|
||||||
|
// but we need to keep their initial value for the current instance of bridge.
|
||||||
|
firstStart bool
|
||||||
|
lastVersion *semver.Version
|
||||||
|
|
||||||
// tasks manages the bridge's goroutines.
|
// tasks manages the bridge's goroutines.
|
||||||
tasks *async.Group
|
tasks *async.Group
|
||||||
|
|
||||||
@ -179,11 +187,13 @@ func New( //nolint:funlen
|
|||||||
|
|
||||||
// Start serving IMAP.
|
// Start serving IMAP.
|
||||||
if err := bridge.serveIMAP(); err != nil {
|
if err := bridge.serveIMAP(); err != nil {
|
||||||
|
logrus.WithError(err).Error("IMAP error")
|
||||||
bridge.PushError(ErrServeIMAP)
|
bridge.PushError(ErrServeIMAP)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start serving SMTP.
|
// Start serving SMTP.
|
||||||
if err := bridge.serveSMTP(); err != nil {
|
if err := bridge.serveSMTP(); err != nil {
|
||||||
|
logrus.WithError(err).Error("SMTP error")
|
||||||
bridge.PushError(ErrServeSMTP)
|
bridge.PushError(ErrServeSMTP)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -214,13 +224,29 @@ func newBridge(
|
|||||||
return nil, fmt.Errorf("failed to load TLS config: %w", err)
|
return nil, fmt.Errorf("failed to load TLS config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
gluonDir, err := getGluonDir(vault)
|
gluonCacheDir, err := getGluonDir(vault)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get Gluon directory: %w", err)
|
return nil, fmt.Errorf("failed to get Gluon directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gluonDataDir, err := locator.ProvideGluonDataPath()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get Gluon Database directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
firstStart := vault.GetFirstStart()
|
||||||
|
if err := vault.SetFirstStart(false); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to save first start indicator: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
lastVersion := vault.GetLastVersion()
|
||||||
|
if err := vault.SetLastVersion(curVersion); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to save last version indicator: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
imapServer, err := newIMAPServer(
|
imapServer, err := newIMAPServer(
|
||||||
gluonDir,
|
gluonCacheDir,
|
||||||
|
gluonDataDir,
|
||||||
curVersion,
|
curVersion,
|
||||||
tlsConfig,
|
tlsConfig,
|
||||||
reporter,
|
reporter,
|
||||||
@ -270,6 +296,9 @@ func newBridge(
|
|||||||
logIMAPServer: logIMAPServer,
|
logIMAPServer: logIMAPServer,
|
||||||
logSMTP: logSMTP,
|
logSMTP: logSMTP,
|
||||||
|
|
||||||
|
firstStart: firstStart,
|
||||||
|
lastVersion: lastVersion,
|
||||||
|
|
||||||
tasks: tasks,
|
tasks: tasks,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -349,10 +378,11 @@ func (bridge *Bridge) init(tlsReporter TLSReporter) error {
|
|||||||
|
|
||||||
// Attempt to lazy load users when triggered.
|
// Attempt to lazy load users when triggered.
|
||||||
bridge.goLoad = bridge.tasks.Trigger(func(ctx context.Context) {
|
bridge.goLoad = bridge.tasks.Trigger(func(ctx context.Context) {
|
||||||
logrus.Info("Loading users")
|
|
||||||
|
|
||||||
if err := bridge.loadUsers(ctx); err != nil {
|
if err := bridge.loadUsers(ctx); err != nil {
|
||||||
logrus.WithError(err).Error("Failed to load users")
|
logrus.WithError(err).Error("Failed to load users")
|
||||||
|
if netErr := new(proton.NetError); !errors.As(err, &netErr) {
|
||||||
|
sentry.ReportError(bridge.reporter, "Failed to load users", err)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
bridge.publish(events.AllUsersLoaded{})
|
bridge.publish(events.AllUsersLoaded{})
|
||||||
}
|
}
|
||||||
@ -433,11 +463,6 @@ func (bridge *Bridge) Close(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bridge.watchers = nil
|
bridge.watchers = nil
|
||||||
|
|
||||||
// Save the last version of bridge that was run.
|
|
||||||
if err := bridge.vault.SetLastVersion(bridge.curVersion); err != nil {
|
|
||||||
logrus.WithError(err).Error("Failed to save last version")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) publish(event events.Event) {
|
func (bridge *Bridge) publish(event events.Event) {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2022 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -23,7 +23,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@ -35,6 +35,7 @@ import (
|
|||||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/certs"
|
"github.com/ProtonMail/proton-bridge/v3/internal/certs"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/cookies"
|
"github.com/ProtonMail/proton-bridge/v3/internal/cookies"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/focus"
|
"github.com/ProtonMail/proton-bridge/v3/internal/focus"
|
||||||
@ -45,6 +46,7 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/tests"
|
"github.com/ProtonMail/proton-bridge/v3/tests"
|
||||||
"github.com/bradenaw/juniper/xslices"
|
"github.com/bradenaw/juniper/xslices"
|
||||||
|
"github.com/emersion/go-imap/client"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -59,7 +61,7 @@ var (
|
|||||||
func init() {
|
func init() {
|
||||||
user.EventPeriod = 100 * time.Millisecond
|
user.EventPeriod = 100 * time.Millisecond
|
||||||
user.EventJitter = 0
|
user.EventJitter = 0
|
||||||
backend.GenerateKey = tests.FastGenerateKey
|
backend.GenerateKey = backend.FastGenerateKey
|
||||||
certs.GenerateCert = tests.FastGenerateCert
|
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) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||||
var gluonDir string
|
var gluonDir string
|
||||||
|
|
||||||
@ -361,13 +363,36 @@ func TestBridge_MissingGluonDir(t *testing.T) {
|
|||||||
require.NoError(t, bridge.SetGluonDir(ctx, t.TempDir()))
|
require.NoError(t, bridge.SetGluonDir(ctx, t.TempDir()))
|
||||||
|
|
||||||
// Get the gluon dir.
|
// Get the gluon dir.
|
||||||
gluonDir = bridge.GetGluonDir()
|
gluonDir = bridge.GetGluonCacheDir()
|
||||||
})
|
})
|
||||||
|
|
||||||
// The user removes the gluon dir while bridge is not running.
|
// The user removes the gluon dir while bridge is not running.
|
||||||
require.NoError(t, os.RemoveAll(gluonDir))
|
require.NoError(t, os.RemoveAll(gluonDir))
|
||||||
|
|
||||||
// Bridge starts but can't find the gluon dir; there should be no error.
|
// Bridge starts but can't find the gluon store dir; there should be no error.
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
|
// ...
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBridge_MissingGluonDatabase(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||||
|
var gluonDir string
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
|
_, err := bridge.LoginFull(context.Background(), username, password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Get the gluon dir.
|
||||||
|
gluonDir, err = bridge.GetGluonDataDir()
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
// The user removes the gluon dir while bridge is not running.
|
||||||
|
require.NoError(t, os.RemoveAll(gluonDir))
|
||||||
|
|
||||||
|
// Bridge starts but can't find the gluon database dir; there should be no error.
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
// ...
|
// ...
|
||||||
})
|
})
|
||||||
@ -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) {
|
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.
|
// 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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Create an additional address for the user; it will not have keys.
|
// Create an additional address for the user; it will not have keys.
|
||||||
@ -456,41 +481,143 @@ func TestBridge_FactoryReset(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBridge_ChangeCacheDirectoryFailsBetweenDifferentVolumes(t *testing.T) {
|
func TestBridge_InitGluonDirectory(t *testing.T) {
|
||||||
if runtime.GOOS != "windows" {
|
|
||||||
t.Skip("Test only necessary on windows")
|
|
||||||
}
|
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
// Change directory
|
configDir, err := b.GetGluonDataDir()
|
||||||
err := bridge.SetGluonDir(ctx, "XX:\\")
|
require.NoError(t, err)
|
||||||
require.Error(t, err)
|
|
||||||
|
_, err = os.ReadDir(bridge.ApplyGluonCachePathSuffix(b.GetGluonCacheDir()))
|
||||||
|
require.False(t, os.IsNotExist(err))
|
||||||
|
|
||||||
|
_, err = os.ReadDir(bridge.ApplyGluonConfigPathSuffix(configDir))
|
||||||
|
require.False(t, os.IsNotExist(err))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBridge_ChangeCacheDirectory(t *testing.T) {
|
func TestBridge_ChangeCacheDirectory(t *testing.T) {
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
userID, addrID, err := s.CreateUser("imap", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
createNumMessages(ctx, t, c, addrID, labelID, 10)
|
||||||
|
})
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
newCacheDir := t.TempDir()
|
newCacheDir := t.TempDir()
|
||||||
currentCacheDir := bridge.GetGluonDir()
|
currentCacheDir := b.GetGluonCacheDir()
|
||||||
|
configDir, err := b.GetGluonDataDir()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Login the user.
|
// Login the user.
|
||||||
userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
|
||||||
|
defer done()
|
||||||
|
userID, err := b.LoginFull(ctx, "imap", password, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, userID, (<-syncCh).UserID)
|
||||||
|
|
||||||
// The user is now connected.
|
// The user is now connected.
|
||||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
require.Equal(t, []string{userID}, b.GetUserIDs())
|
||||||
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
|
require.Equal(t, []string{userID}, getConnectedUserIDs(t, b))
|
||||||
|
|
||||||
// Change directory
|
// Change directory
|
||||||
err = bridge.SetGluonDir(ctx, newCacheDir)
|
err = b.SetGluonDir(ctx, newCacheDir)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
_, err = os.ReadDir(currentCacheDir)
|
// Old store should no more exists.
|
||||||
|
_, err = os.ReadDir(bridge.ApplyGluonCachePathSuffix(currentCacheDir))
|
||||||
require.True(t, os.IsNotExist(err))
|
require.True(t, os.IsNotExist(err))
|
||||||
|
// Database should not have changed.
|
||||||
|
_, err = os.ReadDir(bridge.ApplyGluonConfigPathSuffix(configDir))
|
||||||
|
require.False(t, os.IsNotExist(err))
|
||||||
|
|
||||||
require.Equal(t, newCacheDir, bridge.GetGluonDir())
|
// New path should have Gluon sub-folder.
|
||||||
|
require.Equal(t, filepath.Join(newCacheDir, "gluon"), b.GetGluonCacheDir())
|
||||||
|
// And store should be inside it.
|
||||||
|
_, err = os.ReadDir(bridge.ApplyGluonCachePathSuffix(b.GetGluonCacheDir()))
|
||||||
|
require.False(t, os.IsNotExist(err))
|
||||||
|
|
||||||
|
// We should be able to fetch.
|
||||||
|
info, err := b.GetUserInfo(userID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, info.State == bridge.Connected)
|
||||||
|
|
||||||
|
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
||||||
|
defer func() { _ = client.Logout() }()
|
||||||
|
|
||||||
|
status, err := client.Select(`Folders/folder`, false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, uint32(10), status.Messages)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBridge_ChangeAddressOrder(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||||
|
// Create a user.
|
||||||
|
userID, addrID, err := s.CreateUser("imap", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create a second address for the user.
|
||||||
|
aliasID, err := s.CreateAddress(userID, "alias@"+s.GetDomain(), password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create 10 messages for the user.
|
||||||
|
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 10)
|
||||||
|
})
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
|
// Log the user in with its first address.
|
||||||
|
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
|
||||||
|
defer done()
|
||||||
|
userID, err := b.LoginFull(ctx, "imap", password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, userID, (<-syncCh).UserID)
|
||||||
|
|
||||||
|
// We should see 10 messages in the inbox.
|
||||||
|
info, err := b.GetUserInfo(userID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, info.State == bridge.Connected)
|
||||||
|
|
||||||
|
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
||||||
|
defer func() { _ = client.Logout() }()
|
||||||
|
|
||||||
|
status, err := client.Select(`Inbox`, false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, uint32(10), status.Messages)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Make the second address the primary one.
|
||||||
|
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
|
||||||
|
require.NoError(t, c.OrderAddresses(ctx, proton.OrderAddressesReq{AddressIDs: []string{aliasID, addrID}}))
|
||||||
|
})
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
|
// We should still see 10 messages in the inbox.
|
||||||
|
info, err := b.GetUserInfo(userID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, info.State == bridge.Connected)
|
||||||
|
|
||||||
|
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
|
||||||
|
defer func() { _ = client.Logout() }()
|
||||||
|
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
status, err := client.Select(`Inbox`, false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return status.Messages == 10
|
||||||
|
}, 5*time.Second, 100*time.Millisecond)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -501,7 +628,7 @@ func withEnv(t *testing.T, tests func(context.Context, *server.Server, *proton.N
|
|||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
// Add test user.
|
// Add test user.
|
||||||
_, _, err := server.CreateUser(username, username+"@pm.me", password)
|
_, _, err := server.CreateUser(username, password)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Generate a random vault key.
|
// Generate a random vault key.
|
||||||
@ -522,22 +649,27 @@ func withEnv(t *testing.T, tests func(context.Context, *server.Server, *proton.N
|
|||||||
tests(ctx, server, netCtl, locations, vaultKey)
|
tests(ctx, server, netCtl, locations, vaultKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// withMocks creates the mock objects used in the tests.
|
||||||
|
func withMocks(t *testing.T, tests func(*bridge.Mocks)) {
|
||||||
|
mocks := bridge.NewMocks(t, v2_3_0, v2_3_0)
|
||||||
|
defer mocks.Close()
|
||||||
|
|
||||||
|
tests(mocks)
|
||||||
|
}
|
||||||
|
|
||||||
// withBridge creates a new bridge which points to the given API URL and uses the given keychain, and closes it when done.
|
// withBridge creates a new bridge which points to the given API URL and uses the given keychain, and closes it when done.
|
||||||
func withBridge(
|
func withBridgeNoMocks(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
t *testing.T,
|
t *testing.T,
|
||||||
|
mocks *bridge.Mocks,
|
||||||
apiURL string,
|
apiURL string,
|
||||||
netCtl *proton.NetCtl,
|
netCtl *proton.NetCtl,
|
||||||
locator bridge.Locator,
|
locator bridge.Locator,
|
||||||
vaultKey []byte,
|
vaultKey []byte,
|
||||||
tests func(*bridge.Bridge, *bridge.Mocks),
|
tests func(*bridge.Bridge),
|
||||||
) {
|
) {
|
||||||
// Create the mock objects used in the tests.
|
// Bridge will disable the proxy by default at startup.
|
||||||
mocks := bridge.NewMocks(t, v2_3_0, v2_3_0)
|
mocks.ProxyCtl.EXPECT().DisallowProxy()
|
||||||
defer mocks.Close()
|
|
||||||
|
|
||||||
// Bridge will enable the proxy by default at startup.
|
|
||||||
mocks.ProxyCtl.EXPECT().AllowProxy()
|
|
||||||
|
|
||||||
// Get the path to the vault.
|
// Get the path to the vault.
|
||||||
vaultDir, err := locator.ProvideSettingsPath()
|
vaultDir, err := locator.ProvideSettingsPath()
|
||||||
@ -566,7 +698,7 @@ func withBridge(
|
|||||||
cookieJar,
|
cookieJar,
|
||||||
useragent.New(),
|
useragent.New(),
|
||||||
mocks.TLSReporter,
|
mocks.TLSReporter,
|
||||||
proton.NewDialer(netCtl, &tls.Config{InsecureSkipVerify: true}).GetRoundTripper(),
|
netCtl.NewRoundTripper(&tls.Config{InsecureSkipVerify: true}),
|
||||||
mocks.ProxyCtl,
|
mocks.ProxyCtl,
|
||||||
mocks.CrashHandler,
|
mocks.CrashHandler,
|
||||||
mocks.Reporter,
|
mocks.Reporter,
|
||||||
@ -590,7 +722,24 @@ func withBridge(
|
|||||||
defer bridge.Close(ctx)
|
defer bridge.Close(ctx)
|
||||||
|
|
||||||
// Use the bridge.
|
// Use the bridge.
|
||||||
tests(bridge, mocks)
|
tests(bridge)
|
||||||
|
}
|
||||||
|
|
||||||
|
// withBridge creates a new bridge which points to the given API URL and uses the given keychain, and closes it when done.
|
||||||
|
func withBridge(
|
||||||
|
ctx context.Context,
|
||||||
|
t *testing.T,
|
||||||
|
apiURL string,
|
||||||
|
netCtl *proton.NetCtl,
|
||||||
|
locator bridge.Locator,
|
||||||
|
vaultKey []byte,
|
||||||
|
tests func(*bridge.Bridge, *bridge.Mocks),
|
||||||
|
) {
|
||||||
|
withMocks(t, func(mocks *bridge.Mocks) {
|
||||||
|
withBridgeNoMocks(ctx, t, mocks, apiURL, netCtl, locator, vaultKey, func(bridge *bridge.Bridge) {
|
||||||
|
tests(bridge, mocks)
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func waitForEvent[T any](t *testing.T, eventCh <-chan events.Event, wantEvent T) {
|
func waitForEvent[T any](t *testing.T, eventCh <-chan events.Event, wantEvent T) {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2022 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// 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.
|
// 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.
|
// 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.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -18,6 +18,8 @@
|
|||||||
package bridge
|
package bridge
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
@ -62,3 +64,75 @@ func moveFile(from, to string) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func copyDir(from, to string) error {
|
||||||
|
entries, err := os.ReadDir(from)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := createIfNotExists(to, 0o700); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, entry := range entries {
|
||||||
|
sourcePath := filepath.Join(from, entry.Name())
|
||||||
|
destPath := filepath.Join(to, entry.Name())
|
||||||
|
|
||||||
|
if entry.IsDir() {
|
||||||
|
if err := copyDir(sourcePath, destPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := copyFile(sourcePath, destPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyFile(srcFile, dstFile string) error {
|
||||||
|
out, err := os.Create(filepath.Clean(dstFile))
|
||||||
|
defer func(out *os.File) {
|
||||||
|
_ = out.Close()
|
||||||
|
}(out)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
in, err := os.Open(filepath.Clean(srcFile))
|
||||||
|
defer func(in *os.File) {
|
||||||
|
_ = in.Close()
|
||||||
|
}(in)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy(out, in)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func exists(filePath string) bool {
|
||||||
|
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func createIfNotExists(dir string, perm os.FileMode) error {
|
||||||
|
if exists(dir) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(dir, perm); err != nil {
|
||||||
|
return fmt.Errorf("failed to create directory: '%s', error: '%s'", dir, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2022 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// 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.
|
// 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.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -20,13 +20,10 @@ package bridge
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
|
||||||
|
|
||||||
"github.com/Masterminds/semver/v3"
|
"github.com/Masterminds/semver/v3"
|
||||||
"github.com/ProtonMail/gluon"
|
"github.com/ProtonMail/gluon"
|
||||||
@ -65,7 +62,7 @@ func (bridge *Bridge) serveIMAP() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := bridge.vault.SetIMAPPort(getPort(imapListener.Addr())); err != nil {
|
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
|
return nil
|
||||||
@ -103,6 +100,8 @@ func (bridge *Bridge) closeIMAP(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// addIMAPUser connects the given user to gluon.
|
// addIMAPUser connects the given user to gluon.
|
||||||
|
//
|
||||||
|
//nolint:funlen
|
||||||
func (bridge *Bridge) addIMAPUser(ctx context.Context, user *user.User) error {
|
func (bridge *Bridge) addIMAPUser(ctx context.Context, user *user.User) error {
|
||||||
if bridge.imapServer == nil {
|
if bridge.imapServer == nil {
|
||||||
return fmt.Errorf("no imap server instance running")
|
return fmt.Errorf("no imap server instance running")
|
||||||
@ -122,9 +121,53 @@ func (bridge *Bridge) addIMAPUser(ctx context.Context, user *user.User) error {
|
|||||||
if gluonID, ok := user.GetGluonID(addrID); ok {
|
if gluonID, ok := user.GetGluonID(addrID); ok {
|
||||||
log.WithField("gluonID", gluonID).Info("Loading existing IMAP user")
|
log.WithField("gluonID", gluonID).Info("Loading existing IMAP user")
|
||||||
|
|
||||||
if err := bridge.imapServer.LoadUser(ctx, imapConn, gluonID, user.GluonKey()); err != nil {
|
// Load the user, checking whether the DB was newly created.
|
||||||
|
isNew, err := bridge.imapServer.LoadUser(ctx, imapConn, gluonID, user.GluonKey())
|
||||||
|
if err != nil {
|
||||||
return fmt.Errorf("failed to load IMAP user: %w", err)
|
return fmt.Errorf("failed to load IMAP user: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isNew {
|
||||||
|
// If the DB was newly created, clear the sync status; gluon's DB was not found.
|
||||||
|
logrus.Warn("IMAP user DB was newly created, clearing sync status")
|
||||||
|
|
||||||
|
// Remove the user from IMAP so we can clear the sync status.
|
||||||
|
if err := bridge.imapServer.RemoveUser(ctx, gluonID, false); err != nil {
|
||||||
|
return fmt.Errorf("failed to remove IMAP user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the sync status -- we need to resync all messages.
|
||||||
|
if err := user.ClearSyncStatus(); err != nil {
|
||||||
|
return fmt.Errorf("failed to clear sync status: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the user back to the IMAP server.
|
||||||
|
if isNew, err := bridge.imapServer.LoadUser(ctx, imapConn, gluonID, user.GluonKey()); err != nil {
|
||||||
|
return fmt.Errorf("failed to add IMAP user: %w", err)
|
||||||
|
} else if isNew {
|
||||||
|
panic("IMAP user should already have a database")
|
||||||
|
}
|
||||||
|
} else if status := user.GetSyncStatus(); !status.HasLabels {
|
||||||
|
// Otherwise, the DB already exists -- if the labels are not yet synced, we need to re-create the DB.
|
||||||
|
if err := bridge.imapServer.RemoveUser(ctx, gluonID, true); err != nil {
|
||||||
|
return fmt.Errorf("failed to remove old IMAP user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := user.RemoveGluonID(addrID, gluonID); err != nil {
|
||||||
|
return fmt.Errorf("failed to remove old IMAP user ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gluonID, err := bridge.imapServer.AddUser(ctx, imapConn, user.GluonKey())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to add IMAP user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := user.SetGluonID(addrID, gluonID); err != nil {
|
||||||
|
return fmt.Errorf("failed to set IMAP user ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.WithField("gluonID", gluonID).Info("Re-created IMAP user")
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Info("Creating new IMAP user")
|
log.Info("Creating new IMAP user")
|
||||||
|
|
||||||
@ -141,6 +184,9 @@ func (bridge *Bridge) addIMAPUser(ctx context.Context, user *user.User) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Trigger a sync for the user, if needed.
|
||||||
|
user.TriggerSync()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,6 +195,7 @@ func (bridge *Bridge) removeIMAPUser(ctx context.Context, user *user.User, withD
|
|||||||
if bridge.imapServer == nil {
|
if bridge.imapServer == nil {
|
||||||
return fmt.Errorf("no imap server instance running")
|
return fmt.Errorf("no imap server instance running")
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.WithFields(logrus.Fields{
|
logrus.WithFields(logrus.Fields{
|
||||||
"userID": user.ID(),
|
"userID": user.ID(),
|
||||||
"withData": withData,
|
"withData": withData,
|
||||||
@ -199,31 +246,24 @@ func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getGluonDir(encVault *vault.Vault) (string, error) {
|
func getGluonDir(encVault *vault.Vault) (string, error) {
|
||||||
empty, exists, err := isEmpty(encVault.GetGluonDir())
|
if err := os.MkdirAll(encVault.GetGluonCacheDir(), 0o700); err != nil {
|
||||||
if err != nil {
|
return "", fmt.Errorf("failed to create gluon dir: %w", err)
|
||||||
return "", fmt.Errorf("failed to check if gluon dir is empty: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !exists {
|
return encVault.GetGluonCacheDir(), nil
|
||||||
if err := os.MkdirAll(encVault.GetGluonDir(), 0o700); err != nil {
|
}
|
||||||
return "", fmt.Errorf("failed to create gluon dir: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if empty {
|
func ApplyGluonCachePathSuffix(basePath string) string {
|
||||||
if err := encVault.ForUser(runtime.NumCPU(), func(user *vault.User) error {
|
return filepath.Join(basePath, "backend", "store")
|
||||||
return user.ClearSyncStatus()
|
}
|
||||||
}); err != nil {
|
|
||||||
return "", fmt.Errorf("failed to reset user sync status: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return encVault.GetGluonDir(), nil
|
func ApplyGluonConfigPathSuffix(basePath string) string {
|
||||||
|
return filepath.Join(basePath, "backend", "db")
|
||||||
}
|
}
|
||||||
|
|
||||||
// nolint:funlen
|
// nolint:funlen
|
||||||
func newIMAPServer(
|
func newIMAPServer(
|
||||||
gluonDir string,
|
gluonCacheDir, gluonConfigDir string,
|
||||||
version *semver.Version,
|
version *semver.Version,
|
||||||
tlsConfig *tls.Config,
|
tlsConfig *tls.Config,
|
||||||
reporter reporter.Reporter,
|
reporter reporter.Reporter,
|
||||||
@ -231,11 +271,15 @@ func newIMAPServer(
|
|||||||
eventCh chan<- imapEvents.Event,
|
eventCh chan<- imapEvents.Event,
|
||||||
tasks *async.Group,
|
tasks *async.Group,
|
||||||
) (*gluon.Server, error) {
|
) (*gluon.Server, error) {
|
||||||
|
gluonCacheDir = ApplyGluonCachePathSuffix(gluonCacheDir)
|
||||||
|
gluonConfigDir = ApplyGluonConfigPathSuffix(gluonConfigDir)
|
||||||
|
|
||||||
logrus.WithFields(logrus.Fields{
|
logrus.WithFields(logrus.Fields{
|
||||||
"gluonDir": gluonDir,
|
"gluonStore": gluonCacheDir,
|
||||||
"version": version,
|
"gluonDB": gluonConfigDir,
|
||||||
"logClient": logClient,
|
"version": version,
|
||||||
"logServer": logServer,
|
"logClient": logClient,
|
||||||
|
"logServer": logServer,
|
||||||
}).Info("Creating IMAP server")
|
}).Info("Creating IMAP server")
|
||||||
|
|
||||||
if logClient || logServer {
|
if logClient || logServer {
|
||||||
@ -263,7 +307,8 @@ func newIMAPServer(
|
|||||||
|
|
||||||
imapServer, err := gluon.New(
|
imapServer, err := gluon.New(
|
||||||
gluon.WithTLS(tlsConfig),
|
gluon.WithTLS(tlsConfig),
|
||||||
gluon.WithDataDir(gluonDir),
|
gluon.WithDataDir(gluonCacheDir),
|
||||||
|
gluon.WithDatabaseDir(gluonConfigDir),
|
||||||
gluon.WithStoreBuilder(new(storeBuilder)),
|
gluon.WithStoreBuilder(new(storeBuilder)),
|
||||||
gluon.WithLogger(imapClientLog, imapServerLog),
|
gluon.WithLogger(imapClientLog, imapServerLog),
|
||||||
getGluonVersionInfo(version),
|
getGluonVersionInfo(version),
|
||||||
@ -297,25 +342,6 @@ func getGluonVersionInfo(version *semver.Version) gluon.Option {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// isEmpty returns whether the given directory is empty.
|
|
||||||
// If the directory does not exist, the second return value is false.
|
|
||||||
func isEmpty(dir string) (bool, bool, error) {
|
|
||||||
if _, err := os.Stat(dir); err != nil {
|
|
||||||
if !errors.Is(err, fs.ErrNotExist) {
|
|
||||||
return false, false, fmt.Errorf("failed to stat %s: %w", dir, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return true, false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
entries, err := os.ReadDir(dir)
|
|
||||||
if err != nil {
|
|
||||||
return false, false, fmt.Errorf("failed to read dir %s: %w", dir, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return len(entries) == 0, true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type storeBuilder struct{}
|
type storeBuilder struct{}
|
||||||
|
|
||||||
func (*storeBuilder) New(path, userID string, passphrase []byte) (store.Store, error) {
|
func (*storeBuilder) New(path, userID string, passphrase []byte) (store.Store, error) {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2022 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// 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.
|
// 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.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
|
|||||||
@ -144,3 +144,17 @@ func (mr *MockAutostarterMockRecorder) Enable() *gomock.Call {
|
|||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Enable", reflect.TypeOf((*MockAutostarter)(nil).Enable))
|
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.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -35,7 +35,7 @@ import (
|
|||||||
|
|
||||||
func TestBridge_Refresh(t *testing.T) {
|
func TestBridge_Refresh(t *testing.T) {
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
names := iterator.Collect(iterator.Map(iterator.Counter(10), func(i int) string {
|
names := iterator.Collect(iterator.Map(iterator.Counter(10), func(i int) string {
|
||||||
@ -67,7 +67,7 @@ func TestBridge_Refresh(t *testing.T) {
|
|||||||
|
|
||||||
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||||
require.NoError(t, err)
|
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() }()
|
defer func() { _ = client.Logout() }()
|
||||||
|
|
||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
@ -100,7 +100,7 @@ func TestBridge_Refresh(t *testing.T) {
|
|||||||
|
|
||||||
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||||
require.NoError(t, err)
|
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() }()
|
defer func() { _ = client.Logout() }()
|
||||||
|
|
||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2022 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -18,10 +18,12 @@
|
|||||||
package bridge_test
|
package bridge_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@ -30,6 +32,7 @@ import (
|
|||||||
"github.com/ProtonMail/go-proton-api/server"
|
"github.com/ProtonMail/go-proton-api/server"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
"github.com/emersion/go-imap"
|
"github.com/emersion/go-imap"
|
||||||
"github.com/emersion/go-imap/client"
|
"github.com/emersion/go-imap/client"
|
||||||
"github.com/emersion/go-sasl"
|
"github.com/emersion/go-sasl"
|
||||||
@ -39,10 +42,10 @@ import (
|
|||||||
|
|
||||||
func TestBridge_Send(t *testing.T) {
|
func TestBridge_Send(t *testing.T) {
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
senderUserID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
senderUserID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@ -100,7 +103,7 @@ func TestBridge_Send(t *testing.T) {
|
|||||||
defer recipientIMAPClient.Logout() //nolint:errcheck
|
defer recipientIMAPClient.Logout() //nolint:errcheck
|
||||||
|
|
||||||
// Sender should have 10 messages in the sent folder.
|
// Sender should have 10 messages in the sent folder.
|
||||||
// Recipient should have 0 messages in inbox.
|
// Recipient should have 10 messages in inbox.
|
||||||
require.Eventually(t, func() bool {
|
require.Eventually(t, func() bool {
|
||||||
sent, err := senderIMAPClient.Status(`Sent`, []imap.StatusItem{imap.StatusMessages})
|
sent, err := senderIMAPClient.Status(`Sent`, []imap.StatusItem{imap.StatusMessages})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -113,3 +116,217 @@ func TestBridge_Send(t *testing.T) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBridge_SendDraftFlags(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
|
// Create a recipient user.
|
||||||
|
_, _, err := s.CreateUser("recipient", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// The sender should be fully synced.
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
||||||
|
defer done()
|
||||||
|
|
||||||
|
userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, userID, (<-syncCh).UserID)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Start the bridge.
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
// Get the sender user info.
|
||||||
|
userInfo, err := bridge.QueryUserInfo(username)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Connect the sender IMAP client.
|
||||||
|
imapClient, err := client.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, imapClient.Login(userInfo.Addresses[0], string(userInfo.BridgePass)))
|
||||||
|
defer imapClient.Logout() //nolint:errcheck
|
||||||
|
|
||||||
|
// The message to send.
|
||||||
|
const message = `Subject: Test\r\n\r\nHello world!`
|
||||||
|
|
||||||
|
// Save a draft.
|
||||||
|
require.NoError(t, imapClient.Append("Drafts", []string{imap.DraftFlag}, time.Now(), strings.NewReader(message)))
|
||||||
|
|
||||||
|
// Assert that the draft exists and is marked as a draft.
|
||||||
|
{
|
||||||
|
messages, err := clientFetch(imapClient, "Drafts")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, messages, 1)
|
||||||
|
require.Contains(t, messages[0].Flags, imap.DraftFlag)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect the SMTP client.
|
||||||
|
smtpClient, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort())))
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer smtpClient.Close() //nolint:errcheck
|
||||||
|
|
||||||
|
// Upgrade to TLS.
|
||||||
|
require.NoError(t, smtpClient.StartTLS(&tls.Config{InsecureSkipVerify: true}))
|
||||||
|
|
||||||
|
// Authorize with SASL PLAIN.
|
||||||
|
require.NoError(t, smtpClient.Auth(sasl.NewPlainClient(
|
||||||
|
userInfo.Addresses[0],
|
||||||
|
userInfo.Addresses[0],
|
||||||
|
string(userInfo.BridgePass)),
|
||||||
|
))
|
||||||
|
|
||||||
|
// Send the message.
|
||||||
|
require.NoError(t, smtpClient.SendMail(
|
||||||
|
userInfo.Addresses[0],
|
||||||
|
[]string{"recipient@" + s.GetDomain()},
|
||||||
|
strings.NewReader(message),
|
||||||
|
))
|
||||||
|
|
||||||
|
// Delete the draft: add the \Deleted flag and expunge.
|
||||||
|
{
|
||||||
|
status, err := imapClient.Select("Drafts", false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, uint32(1), status.Messages)
|
||||||
|
|
||||||
|
// Add the \Deleted flag.
|
||||||
|
require.NoError(t, clientStore(imapClient, 1, 1, true, imap.FormatFlagsOp(imap.AddFlags, true), imap.DeletedFlag))
|
||||||
|
|
||||||
|
// Expunge.
|
||||||
|
require.NoError(t, imapClient.Expunge(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert that the draft is eventually gone.
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
status, err := imapClient.Select("Drafts", false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return status.Messages == 0
|
||||||
|
}, 10*time.Second, 100*time.Millisecond)
|
||||||
|
|
||||||
|
// Assert that the message is eventually in the sent folder.
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
messages, err := clientFetch(imapClient, "Sent")
|
||||||
|
require.NoError(t, err)
|
||||||
|
return len(messages) == 1
|
||||||
|
}, 10*time.Second, 100*time.Millisecond)
|
||||||
|
|
||||||
|
// Assert that the message is not marked as a draft.
|
||||||
|
{
|
||||||
|
messages, err := clientFetch(imapClient, "Sent")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, messages, 1)
|
||||||
|
require.NotContains(t, messages[0].Flags, imap.DraftFlag)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBridge_SendInvite(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
|
// Create a recipient user.
|
||||||
|
_, _, err := s.CreateUser("recipient", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Set "attach public keys" to true for the user.
|
||||||
|
withClient(ctx, t, s, username, password, func(ctx context.Context, client *proton.Client) {
|
||||||
|
settings, err := client.SetAttachPublicKey(ctx, proton.SetAttachPublicKeyReq{AttachPublicKey: true})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, proton.Bool(true), settings.AttachPublicKey)
|
||||||
|
})
|
||||||
|
|
||||||
|
// The sender should be fully synced.
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
||||||
|
defer done()
|
||||||
|
|
||||||
|
userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, userID, (<-syncCh).UserID)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Start the bridge.
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
// Get the sender user info.
|
||||||
|
userInfo, err := bridge.QueryUserInfo(username)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Connect the sender IMAP client.
|
||||||
|
imapClient, err := client.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, imapClient.Login(userInfo.Addresses[0], string(userInfo.BridgePass)))
|
||||||
|
defer imapClient.Logout() //nolint:errcheck
|
||||||
|
|
||||||
|
// The message to send.
|
||||||
|
b, err := os.ReadFile("testdata/invite.eml")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Save a draft.
|
||||||
|
require.NoError(t, imapClient.Append("Drafts", []string{imap.DraftFlag}, time.Now(), bytes.NewReader(b)))
|
||||||
|
|
||||||
|
// Assert that the draft exists and is marked as a draft.
|
||||||
|
{
|
||||||
|
messages, err := clientFetch(imapClient, "Drafts")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, messages, 1)
|
||||||
|
require.Contains(t, messages[0].Flags, imap.DraftFlag)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect the SMTP client.
|
||||||
|
smtpClient, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort())))
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer smtpClient.Close() //nolint:errcheck
|
||||||
|
|
||||||
|
// Upgrade to TLS.
|
||||||
|
require.NoError(t, smtpClient.StartTLS(&tls.Config{InsecureSkipVerify: true}))
|
||||||
|
|
||||||
|
// Authorize with SASL PLAIN.
|
||||||
|
require.NoError(t, smtpClient.Auth(sasl.NewPlainClient(
|
||||||
|
userInfo.Addresses[0],
|
||||||
|
userInfo.Addresses[0],
|
||||||
|
string(userInfo.BridgePass)),
|
||||||
|
))
|
||||||
|
|
||||||
|
// Send the message.
|
||||||
|
require.NoError(t, smtpClient.SendMail(
|
||||||
|
userInfo.Addresses[0],
|
||||||
|
[]string{"recipient@" + s.GetDomain()},
|
||||||
|
bytes.NewReader(b),
|
||||||
|
))
|
||||||
|
|
||||||
|
// Delete the draft: add the \Deleted flag and expunge.
|
||||||
|
{
|
||||||
|
status, err := imapClient.Select("Drafts", false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, uint32(1), status.Messages)
|
||||||
|
|
||||||
|
// Add the \Deleted flag.
|
||||||
|
require.NoError(t, clientStore(imapClient, 1, 1, true, imap.FormatFlagsOp(imap.AddFlags, true), imap.DeletedFlag))
|
||||||
|
|
||||||
|
// Expunge.
|
||||||
|
require.NoError(t, imapClient.Expunge(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert that the draft is eventually gone.
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
status, err := imapClient.Select("Drafts", false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return status.Messages == 0
|
||||||
|
}, 10*time.Second, 100*time.Millisecond)
|
||||||
|
|
||||||
|
// Assert that the message is eventually in the sent folder.
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
messages, err := clientFetch(imapClient, "Sent")
|
||||||
|
require.NoError(t, err)
|
||||||
|
return len(messages) == 1
|
||||||
|
}, 10*time.Second, 100*time.Millisecond)
|
||||||
|
|
||||||
|
// Assert that the message is not marked as a draft.
|
||||||
|
{
|
||||||
|
messages, err := clientFetch(imapClient, "Sent")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, messages, 1)
|
||||||
|
require.NotContains(t, messages[0].Flags, imap.DraftFlag)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2022 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// 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.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -21,6 +21,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/Masterminds/semver/v3"
|
"github.com/Masterminds/semver/v3"
|
||||||
@ -114,38 +115,47 @@ func (bridge *Bridge) SetSMTPSSL(newSSL bool) error {
|
|||||||
return bridge.restartSMTP()
|
return bridge.restartSMTP()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) GetGluonDir() string {
|
func (bridge *Bridge) GetGluonCacheDir() string {
|
||||||
return bridge.vault.GetGluonDir()
|
return bridge.vault.GetGluonCacheDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) GetGluonDataDir() (string, error) {
|
||||||
|
return bridge.locator.ProvideGluonDataPath()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) SetGluonDir(ctx context.Context, newGluonDir string) error {
|
func (bridge *Bridge) SetGluonDir(ctx context.Context, newGluonDir string) error {
|
||||||
return safe.RLockRet(func() error {
|
return safe.RLockRet(func() error {
|
||||||
currentGluonDir := bridge.GetGluonDir()
|
currentGluonDir := bridge.GetGluonCacheDir()
|
||||||
|
newGluonDir = filepath.Join(newGluonDir, "gluon")
|
||||||
if newGluonDir == currentGluonDir {
|
if newGluonDir == currentGluonDir {
|
||||||
return fmt.Errorf("new gluon dir is the same as the old one")
|
return fmt.Errorf("new gluon dir is the same as the old one")
|
||||||
}
|
}
|
||||||
|
|
||||||
currentVolumeName := filepath.VolumeName(currentGluonDir)
|
if err := bridge.stopEventLoops(); err != nil {
|
||||||
newVolumeName := filepath.VolumeName(newGluonDir)
|
return err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
err := bridge.startEventLoops(ctx)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
if currentVolumeName != newVolumeName {
|
if err := bridge.moveGluonCacheDir(currentGluonDir, newGluonDir); err != nil {
|
||||||
return fmt.Errorf("it's currently not possible to move the cache between different volumes")
|
logrus.WithError(err).Error("failed to move GluonCacheDir")
|
||||||
|
if err := bridge.vault.SetGluonDir(currentGluonDir); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := bridge.closeIMAP(context.Background()); err != nil {
|
gluonDataDir, err := bridge.GetGluonDataDir()
|
||||||
return fmt.Errorf("failed to close IMAP: %w", err)
|
if err != nil {
|
||||||
}
|
panic(fmt.Errorf("failed to get Gluon Database directory: %w", err))
|
||||||
|
|
||||||
if err := moveDir(bridge.GetGluonDir(), newGluonDir); err != nil {
|
|
||||||
return fmt.Errorf("failed to move gluon dir: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := bridge.vault.SetGluonDir(newGluonDir); err != nil {
|
|
||||||
return fmt.Errorf("failed to set new gluon dir: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
imapServer, err := newIMAPServer(
|
imapServer, err := newIMAPServer(
|
||||||
bridge.vault.GetGluonDir(),
|
bridge.vault.GetGluonCacheDir(),
|
||||||
|
gluonDataDir,
|
||||||
bridge.curVersion,
|
bridge.curVersion,
|
||||||
bridge.tlsConfig,
|
bridge.tlsConfig,
|
||||||
bridge.reporter,
|
bridge.reporter,
|
||||||
@ -155,25 +165,60 @@ func (bridge *Bridge) SetGluonDir(ctx context.Context, newGluonDir string) error
|
|||||||
bridge.tasks,
|
bridge.tasks,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create new IMAP server: %w", err)
|
panic(fmt.Errorf("failed to create new IMAP server: %w", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
bridge.imapServer = imapServer
|
bridge.imapServer = imapServer
|
||||||
|
|
||||||
for _, user := range bridge.users {
|
|
||||||
if err := bridge.addIMAPUser(ctx, user); err != nil {
|
|
||||||
return fmt.Errorf("failed to add users to new IMAP server: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := bridge.serveIMAP(); err != nil {
|
|
||||||
return fmt.Errorf("failed to serve IMAP: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}, bridge.usersLock)
|
}, bridge.usersLock)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) moveGluonCacheDir(oldGluonDir, newGluonDir string) error {
|
||||||
|
logrus.Infof("gluon cache moving from %s to %s", oldGluonDir, newGluonDir)
|
||||||
|
oldCacheDir := ApplyGluonCachePathSuffix(oldGluonDir)
|
||||||
|
if err := copyDir(oldCacheDir, ApplyGluonCachePathSuffix(newGluonDir)); err != nil {
|
||||||
|
return fmt.Errorf("failed to copy gluon dir: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bridge.vault.SetGluonDir(newGluonDir); err != nil {
|
||||||
|
return fmt.Errorf("failed to set new gluon cache dir: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.RemoveAll(oldCacheDir); err != nil {
|
||||||
|
logrus.WithError(err).Error("failed to remove old gluon cache dir")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) stopEventLoops() error {
|
||||||
|
if err := bridge.closeIMAP(context.Background()); err != nil {
|
||||||
|
return fmt.Errorf("failed to close IMAP: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bridge.closeSMTP(); err != nil {
|
||||||
|
return fmt.Errorf("failed to close SMTP: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) startEventLoops(ctx context.Context) error {
|
||||||
|
for _, user := range bridge.users {
|
||||||
|
if err := bridge.addIMAPUser(ctx, user); err != nil {
|
||||||
|
return fmt.Errorf("failed to add users to new IMAP server: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bridge.serveIMAP(); err != nil {
|
||||||
|
panic(fmt.Errorf("failed to serve IMAP: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bridge.serveSMTP(); err != nil {
|
||||||
|
panic(fmt.Errorf("failed to serve SMTP: %w", err))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) GetProxyAllowed() bool {
|
func (bridge *Bridge) GetProxyAllowed() bool {
|
||||||
return bridge.vault.GetProxyAllowed()
|
return bridge.vault.GetProxyAllowed()
|
||||||
}
|
}
|
||||||
@ -207,15 +252,24 @@ func (bridge *Bridge) GetAutostart() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) SetAutostart(autostart bool) error {
|
func (bridge *Bridge) SetAutostart(autostart bool) error {
|
||||||
if err := bridge.vault.SetAutostart(autostart); err != nil {
|
if autostart != bridge.vault.GetAutostart() {
|
||||||
return err
|
if err := bridge.vault.SetAutostart(autostart); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
if autostart {
|
if autostart {
|
||||||
|
// do nothing if already enabled
|
||||||
|
if bridge.autostarter.IsEnabled() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
err = bridge.autostarter.Enable()
|
err = bridge.autostarter.Enable()
|
||||||
} else {
|
} else {
|
||||||
|
// do nothing if already disabled
|
||||||
|
if !bridge.autostarter.IsEnabled() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
err = bridge.autostarter.Disable()
|
err = bridge.autostarter.Disable()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -263,23 +317,11 @@ func (bridge *Bridge) GetCurrentVersion() *semver.Version {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) GetLastVersion() *semver.Version {
|
func (bridge *Bridge) GetLastVersion() *semver.Version {
|
||||||
return bridge.vault.GetLastVersion()
|
return bridge.lastVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) GetFirstStart() bool {
|
func (bridge *Bridge) GetFirstStart() bool {
|
||||||
return bridge.vault.GetFirstStart()
|
return bridge.firstStart
|
||||||
}
|
|
||||||
|
|
||||||
func (bridge *Bridge) SetFirstStart(firstStart bool) error {
|
|
||||||
return bridge.vault.SetFirstStart(firstStart)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bridge *Bridge) GetFirstStartGUI() bool {
|
|
||||||
return bridge.vault.GetFirstStartGUI()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bridge *Bridge) SetFirstStartGUI(firstStart bool) error {
|
|
||||||
return bridge.vault.SetFirstStartGUI(firstStart)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) GetColorScheme() string {
|
func (bridge *Bridge) GetColorScheme() string {
|
||||||
@ -299,10 +341,10 @@ func (bridge *Bridge) FactoryReset(ctx context.Context) {
|
|||||||
}, bridge.usersLock)
|
}, bridge.usersLock)
|
||||||
|
|
||||||
// Wipe the vault.
|
// Wipe the vault.
|
||||||
gluonDir, err := bridge.locator.ProvideGluonPath()
|
gluonCacheDir, err := bridge.locator.ProvideGluonCachePath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.WithError(err).Error("Failed to provide gluon dir")
|
logrus.WithError(err).Error("Failed to provide gluon dir")
|
||||||
} else if err := bridge.vault.Reset(gluonDir); err != nil {
|
} else if err := bridge.vault.Reset(gluonCacheDir); err != nil {
|
||||||
logrus.WithError(err).Error("Failed to reset vault")
|
logrus.WithError(err).Error("Failed to reset vault")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2022 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// 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) {
|
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) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
// By default, proxy is allowed.
|
// By default, proxy is allowed.
|
||||||
require.True(t, bridge.GetProxyAllowed())
|
require.False(t, bridge.GetProxyAllowed())
|
||||||
|
|
||||||
// Disallow proxy.
|
// Disallow proxy.
|
||||||
mocks.ProxyCtl.EXPECT().DisallowProxy()
|
mocks.ProxyCtl.EXPECT().AllowProxy()
|
||||||
require.NoError(t, bridge.SetProxyAllowed(false))
|
require.NoError(t, bridge.SetProxyAllowed(true))
|
||||||
|
|
||||||
// Get the new setting.
|
// 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) {
|
func TestBridge_Settings_Autostart(t *testing.T) {
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
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) {
|
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())
|
require.False(t, bridge.GetAutostart())
|
||||||
|
|
||||||
// Enable autostart.
|
// Re Enable autostart.
|
||||||
|
mocks.Autostarter.EXPECT().IsEnabled().Return(false)
|
||||||
mocks.Autostarter.EXPECT().Enable().Return(nil)
|
mocks.Autostarter.EXPECT().Enable().Return(nil)
|
||||||
require.NoError(t, bridge.SetAutostart(true))
|
require.NoError(t, bridge.SetAutostart(true))
|
||||||
|
|
||||||
@ -153,26 +162,7 @@ func TestBridge_Settings_FirstStart(t *testing.T) {
|
|||||||
// By default, first start is true.
|
// By default, first start is true.
|
||||||
require.True(t, bridge.GetFirstStart())
|
require.True(t, bridge.GetFirstStart())
|
||||||
|
|
||||||
// Set first start to false.
|
// the setting of the first start value is managed by bridge itself, so the setter is not exported.
|
||||||
require.NoError(t, bridge.SetFirstStart(false))
|
|
||||||
|
|
||||||
// Get the new setting.
|
|
||||||
require.False(t, bridge.GetFirstStart())
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBridge_Settings_FirstStartGUI(t *testing.T) {
|
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
|
||||||
// By default, first start is true.
|
|
||||||
require.True(t, bridge.GetFirstStartGUI())
|
|
||||||
|
|
||||||
// Set first start to false.
|
|
||||||
require.NoError(t, bridge.SetFirstStartGUI(false))
|
|
||||||
|
|
||||||
// Get the new setting.
|
|
||||||
require.False(t, bridge.GetFirstStartGUI())
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2022 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// 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 {
|
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
|
return nil
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2022 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// 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.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -20,12 +20,15 @@ package bridge_test
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/gluon/rfc822"
|
||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
"github.com/ProtonMail/go-proton-api/server"
|
"github.com/ProtonMail/go-proton-api/server"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||||
@ -33,7 +36,10 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
"github.com/bradenaw/juniper/iterator"
|
"github.com/bradenaw/juniper/iterator"
|
||||||
"github.com/bradenaw/juniper/stream"
|
"github.com/bradenaw/juniper/stream"
|
||||||
|
"github.com/bradenaw/juniper/xslices"
|
||||||
|
"github.com/emersion/go-imap"
|
||||||
"github.com/emersion/go-imap/client"
|
"github.com/emersion/go-imap/client"
|
||||||
|
"github.com/golang/mock/gomock"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -41,14 +47,191 @@ func TestBridge_Sync(t *testing.T) {
|
|||||||
numMsg := 1 << 8
|
numMsg := 1 << 8
|
||||||
|
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
|
labelID, err := s.CreateLabel(userID, "folder", "", proton.LabelTypeFolder)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
withClient(ctx, t, s, "imap", password, func(ctx context.Context, c *proton.Client) {
|
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
|
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.
|
// 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(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.
|
|
||||||
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, mocks *bridge.Mocks) {
|
||||||
require.NoError(t, bridge.DeleteUser(ctx, userID))
|
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()))
|
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||||
require.NoError(t, err)
|
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() }()
|
defer func() { _ = client.Logout() }()
|
||||||
|
|
||||||
status, err := client.Select(`Folders/folder`, false)
|
status, err := client.Select(`Folders/folder`, false)
|
||||||
@ -116,6 +283,20 @@ func TestBridge_Sync(t *testing.T) {
|
|||||||
require.Less(t, status.Messages, uint32(numMsg))
|
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.
|
// Remove the network limit, allowing the sync to finish.
|
||||||
netCtl.SetReadLimit(0)
|
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()))
|
client, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||||
require.NoError(t, err)
|
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() }()
|
defer func() { _ = client.Logout() }()
|
||||||
|
|
||||||
status, err := client.Select(`Folders/folder`, false)
|
status, err := client.Select(`Folders/folder`, false)
|
||||||
require.NoError(t, err)
|
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))
|
}, 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(
|
m := proton.New(
|
||||||
proton.WithHostURL(s.GetHostURL()),
|
proton.WithHostURL(s.GetHostURL()),
|
||||||
proton.WithTransport(proton.InsecureTransport()),
|
proton.WithTransport(proton.InsecureTransport()),
|
||||||
@ -155,10 +351,68 @@ func withClient(ctx context.Context, t *testing.T, s *server.Server, username st
|
|||||||
fn(ctx, c)
|
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"))
|
literal, err := os.ReadFile(filepath.Join("testdata", "text-plain.eml"))
|
||||||
require.NoError(t, err)
|
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)
|
user, err := c.GetUser(ctx)
|
||||||
require.NoError(t, err)
|
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)
|
_, addrKRs, err := proton.Unlock(user, addr, keyPass)
|
||||||
require.NoError(t, err)
|
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,
|
ctx,
|
||||||
addrKRs[addrID],
|
addrKRs[addrID],
|
||||||
runtime.NumCPU(),
|
runtime.NumCPU(),
|
||||||
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{
|
return proton.ImportReq{
|
||||||
Metadata: proton.ImportMetadata{
|
Metadata: proton.ImportMetadata{
|
||||||
AddressID: addrID,
|
AddressID: addrID,
|
||||||
LabelIDs: []string{labelID},
|
LabelIDs: []string{labelID},
|
||||||
Flags: proton.MessageFlagReceived,
|
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 {
|
func countBytesRead(ctl *proton.NetCtl, fn func()) uint64 {
|
||||||
|
|||||||
85
internal/bridge/testdata/invite.eml
vendored
Normal file
85
internal/bridge/testdata/invite.eml
vendored
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
From: <username@proton.local>
|
||||||
|
To: <recipient@proton.local>
|
||||||
|
Subject: Testing calendar invite
|
||||||
|
Date: Fri, 3 Feb 2023 01:04:32 +0100
|
||||||
|
Message-ID: <000001d93763$183b74e0$48b25ea0$@proton.local>
|
||||||
|
MIME-Version: 1.0
|
||||||
|
Content-Type: text/calendar; method=REQUEST;
|
||||||
|
charset="utf-8"
|
||||||
|
Content-Transfer-Encoding: 7bit
|
||||||
|
X-Mailer: Microsoft Outlook 16.0
|
||||||
|
Thread-Index: Adk3Yw5pLdgwsT46RviXb/nfvQlesQAAAmGA
|
||||||
|
Content-Language: en-gb
|
||||||
|
|
||||||
|
BEGIN:VCALENDAR
|
||||||
|
PRODID:-//Microsoft Corporation//Outlook 16.0 MIMEDIR//EN
|
||||||
|
VERSION:2.0
|
||||||
|
METHOD:REQUEST
|
||||||
|
X-MS-OLK-FORCEINSPECTOROPEN:TRUE
|
||||||
|
BEGIN:VTIMEZONE
|
||||||
|
TZID:Central European Standard Time
|
||||||
|
BEGIN:STANDARD
|
||||||
|
DTSTART:16011028T030000
|
||||||
|
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
|
||||||
|
TZOFFSETFROM:+0200
|
||||||
|
TZOFFSETTO:+0100
|
||||||
|
END:STANDARD
|
||||||
|
BEGIN:DAYLIGHT
|
||||||
|
DTSTART:16010325T020000
|
||||||
|
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3
|
||||||
|
TZOFFSETFROM:+0100
|
||||||
|
TZOFFSETTO:+0200
|
||||||
|
END:DAYLIGHT
|
||||||
|
END:VTIMEZONE
|
||||||
|
BEGIN:VEVENT
|
||||||
|
ATTENDEE;CN=recipient@proton.local;RSVP=TRUE:mailto:recipient@proton.local
|
||||||
|
CLASS:PUBLIC
|
||||||
|
CREATED:20230203T000432Z
|
||||||
|
DESCRIPTION:qweqweqweqweqweqwe/gn\\n
|
||||||
|
DTEND;TZID="Central European Standard Time":20230203T020000
|
||||||
|
DTSTAMP:20230203T000432Z
|
||||||
|
DTSTART;TZID="Central European Standard Time":20230203T013000
|
||||||
|
LAST-MODIFIED:20230203T000432Z
|
||||||
|
LOCATION:qweqwe
|
||||||
|
ORGANIZER;CN=username@proton.local:mailto:username@proton.local
|
||||||
|
PRIORITY:5
|
||||||
|
SEQUENCE:0
|
||||||
|
SUMMARY;LANGUAGE=en-gb:Testing calendar invite
|
||||||
|
TRANSP:OPAQUE
|
||||||
|
UID:040000008200E00074C5B7101A82E008000000003080B2796B37D901000000000000000
|
||||||
|
0100000001236CD1CD93CA9449C6FF1AC4DEAC44E
|
||||||
|
X-ALT-DESC;FMTTYPE=text/html:<html xmlns:v="urn:schemas-microsoft-com:vml"
|
||||||
|
xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:w="urn:schemas-mic
|
||||||
|
rosoft-com:office:word" xmlns:m="http://schemas.microsoft.com/office/2004/
|
||||||
|
12/omml" xmlns="http://www.w3.org/TR/REC-html40"><head><meta http-equiv=Co
|
||||||
|
ntent-Type content="text/html/g; charset=us-ascii"><meta name=Generator con
|
||||||
|
tent="Microsoft Word 15 (filtered medium)"><style><!--/gn/* Font Definition
|
||||||
|
s *//gn@font-face\\n {font-family:"Cambria Math"\\;\\n panose-1:2 4 5 3 5 4 6
|
||||||
|
3 2 4/g;}\\n@font-face\\n {font-family:Calibri\\;\\n panose-1:2 15 5 2 2 2 4 3
|
||||||
|
2 4/g;}\\n/* Style Definitions */\\np.MsoNormal\\, li.MsoNormal\\, div.MsoNorma
|
||||||
|
l/gn {margin:0cm\\;\\n font-size:11.0pt\\;\\n font-family:"Calibri"\\,sans-serif
|
||||||
|
/g;\\n mso-fareast-language:EN-US\\;}\\nspan.EmailStyle18\\n {mso-style-type:pe
|
||||||
|
rsonal-compose/g;\\n font-family:"Calibri"\\,sans-serif\\;\\n color:windowtext\\
|
||||||
|
;}/gn.MsoChpDefault\\n {mso-style-type:export-only\\;\\n font-size:10.0pt\\;}\\n
|
||||||
|
@page WordSection1/gn {size:612.0pt 792.0pt\\;\\n margin:72.0pt 72.0pt 72.0pt
|
||||||
|
72.0pt/g;}\\ndiv.WordSection1\\n {page:WordSection1\\;}\\n--></style><!--[if g
|
||||||
|
te mso 9]><xml>/gn<o:shapedefaults v:ext="edit" spidmax="1026" />\\n</xml><!
|
||||||
|
[endif]--><!--[if gte mso 9]><xml>/gn<o:shapelayout v:ext="edit">\\n<o:idmap
|
||||||
|
v:ext="edit" data="1" />/gn</o:shapelayout></xml><![endif]--></head><body
|
||||||
|
lang=EN-GB link="#0563C1" vlink="#954F72" style='word-wrap:break-word'><di
|
||||||
|
v class=WordSection1><p class=MsoNormal><span lang=EN-US>qweqweqweqweqweqw
|
||||||
|
e<o:p></o:p></span></p></div></body></html>
|
||||||
|
X-MICROSOFT-CDO-BUSYSTATUS:TENTATIVE
|
||||||
|
X-MICROSOFT-CDO-IMPORTANCE:1
|
||||||
|
X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY
|
||||||
|
X-MICROSOFT-DISALLOW-COUNTER:FALSE
|
||||||
|
X-MS-OLK-AUTOSTARTCHECK:FALSE
|
||||||
|
X-MS-OLK-CONFTYPE:0
|
||||||
|
BEGIN:VALARM
|
||||||
|
TRIGGER:-PT15M
|
||||||
|
ACTION:DISPLAY
|
||||||
|
DESCRIPTION:Reminder
|
||||||
|
END:VALARM
|
||||||
|
END:VEVENT
|
||||||
|
END:VCALENDAR
|
||||||
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2022 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// 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.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -26,7 +26,8 @@ import (
|
|||||||
type Locator interface {
|
type Locator interface {
|
||||||
ProvideSettingsPath() (string, error)
|
ProvideSettingsPath() (string, error)
|
||||||
ProvideLogsPath() (string, error)
|
ProvideLogsPath() (string, error)
|
||||||
ProvideGluonPath() (string, error)
|
ProvideGluonCachePath() (string, error)
|
||||||
|
ProvideGluonDataPath() (string, error)
|
||||||
GetLicenseFilePath() string
|
GetLicenseFilePath() string
|
||||||
GetDependencyLicensesLink() string
|
GetDependencyLicensesLink() string
|
||||||
Clear() error
|
Clear() error
|
||||||
@ -51,6 +52,7 @@ type TLSReporter interface {
|
|||||||
type Autostarter interface {
|
type Autostarter interface {
|
||||||
Enable() error
|
Enable() error
|
||||||
Disable() error
|
Disable() error
|
||||||
|
IsEnabled() bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type Updater interface {
|
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.
|
// 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.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -75,6 +75,11 @@ func (bridge *Bridge) GetUserIDs() []string {
|
|||||||
return bridge.vault.GetUserIDs()
|
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.
|
// GetUserInfo returns info about the given user.
|
||||||
func (bridge *Bridge) GetUserInfo(userID string) (UserInfo, error) {
|
func (bridge *Bridge) GetUserInfo(userID string) (UserInfo, error) {
|
||||||
return safe.RLockRetErr(func() (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 {
|
if len(user.AuthUID()) == 0 {
|
||||||
state = SignedOut
|
state = SignedOut
|
||||||
}
|
}
|
||||||
info = getUserInfo(user.UserID(), user.Username(), state, user.AddressMode())
|
info = getUserInfo(user.UserID(), user.Username(), user.PrimaryEmail(), state, user.AddressMode())
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return UserInfo{}, fmt.Errorf("failed to get user info: %w", err)
|
return UserInfo{}, fmt.Errorf("failed to get user info: %w", err)
|
||||||
}
|
}
|
||||||
@ -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)
|
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")
|
logrus.WithField("userID", auth.UserID).Warn("User already logged in")
|
||||||
|
|
||||||
if err := client.AuthDelete(ctx); err != nil {
|
if err := client.AuthDelete(ctx); err != nil {
|
||||||
@ -324,30 +329,37 @@ func (bridge *Bridge) loginUser(ctx context.Context, client *proton.Client, auth
|
|||||||
|
|
||||||
// loadUsers tries to load each user in the vault that isn't already loaded.
|
// loadUsers tries to load each user in the vault that isn't already loaded.
|
||||||
func (bridge *Bridge) loadUsers(ctx context.Context) error {
|
func (bridge *Bridge) loadUsers(ctx context.Context) error {
|
||||||
|
logrus.WithField("count", len(bridge.vault.GetUserIDs())).Info("Loading users")
|
||||||
|
defer logrus.Info("Finished loading users")
|
||||||
|
|
||||||
return bridge.vault.ForUser(runtime.NumCPU(), func(user *vault.User) error {
|
return bridge.vault.ForUser(runtime.NumCPU(), func(user *vault.User) error {
|
||||||
|
log := logrus.WithField("userID", user.UserID())
|
||||||
|
|
||||||
if user.AuthUID() == "" {
|
if user.AuthUID() == "" {
|
||||||
|
log.Info("User is not connected (skipping)")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if safe.RLockRet(func() bool { return mapHas(bridge.users, user.UserID()) }, bridge.usersLock) {
|
if safe.RLockRet(func() bool { return mapHas(bridge.users, user.UserID()) }, bridge.usersLock) {
|
||||||
|
log.Info("User is already loaded (skipping)")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.WithField("userID", user.UserID()).Info("Loading connected user")
|
log.Info("Loading connected user")
|
||||||
|
|
||||||
bridge.publish(events.UserLoading{
|
bridge.publish(events.UserLoading{
|
||||||
UserID: user.UserID(),
|
UserID: user.UserID(),
|
||||||
})
|
})
|
||||||
|
|
||||||
if err := bridge.loadUser(ctx, user); err != nil {
|
if err := bridge.loadUser(ctx, user); err != nil {
|
||||||
logrus.WithError(err).Error("Failed to load connected user")
|
log.WithError(err).Error("Failed to load connected user")
|
||||||
|
|
||||||
bridge.publish(events.UserLoadFail{
|
bridge.publish(events.UserLoadFail{
|
||||||
UserID: user.UserID(),
|
UserID: user.UserID(),
|
||||||
Error: err,
|
Error: err,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
logrus.WithField("userID", user.UserID()).Info("Successfully loaded user")
|
log.Info("Successfully loaded connected user")
|
||||||
|
|
||||||
bridge.publish(events.UserLoadSuccess{
|
bridge.publish(events.UserLoadSuccess{
|
||||||
UserID: user.UserID(),
|
UserID: user.UserID(),
|
||||||
@ -362,7 +374,7 @@ func (bridge *Bridge) loadUsers(ctx context.Context) error {
|
|||||||
func (bridge *Bridge) loadUser(ctx context.Context, user *vault.User) error {
|
func (bridge *Bridge) loadUser(ctx context.Context, user *vault.User) error {
|
||||||
client, auth, err := bridge.api.NewClientWithRefresh(ctx, user.AuthUID(), user.AuthRef())
|
client, auth, err := bridge.api.NewClientWithRefresh(ctx, user.AuthUID(), user.AuthRef())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if apiErr := new(proton.Error); errors.As(err, &apiErr) && (apiErr.Code == proton.AuthRefreshTokenInvalid) {
|
if apiErr := new(proton.APIError); errors.As(err, &apiErr) && (apiErr.Code == proton.AuthRefreshTokenInvalid) {
|
||||||
// The session cannot be refreshed, we sign out the user by clearing his auth secrets.
|
// The session cannot be refreshed, we sign out the user by clearing his auth secrets.
|
||||||
if err := user.Clear(); err != nil {
|
if err := user.Clear(); err != nil {
|
||||||
logrus.WithError(err).Warn("Failed to clear user secrets")
|
logrus.WithError(err).Warn("Failed to clear user secrets")
|
||||||
@ -384,6 +396,12 @@ func (bridge *Bridge) loadUser(ctx context.Context, user *vault.User) error {
|
|||||||
return fmt.Errorf("failed to add user: %w", err)
|
return fmt.Errorf("failed to add user: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if user.PrimaryEmail() != apiUser.Email {
|
||||||
|
if err := user.SetPrimaryEmail(apiUser.Email); err != nil {
|
||||||
|
return fmt.Errorf("failed to modify user primary email: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -499,7 +517,7 @@ func (bridge *Bridge) newVaultUser(
|
|||||||
saltedKeyPass []byte,
|
saltedKeyPass []byte,
|
||||||
) (*vault.User, bool, error) {
|
) (*vault.User, bool, error) {
|
||||||
if !bridge.vault.HasUser(apiUser.ID) {
|
if !bridge.vault.HasUser(apiUser.ID) {
|
||||||
user, err := bridge.vault.AddUser(apiUser.ID, apiUser.Name, authUID, authRef, saltedKeyPass)
|
user, err := bridge.vault.AddUser(apiUser.ID, apiUser.Name, apiUser.Email, authUID, authRef, saltedKeyPass)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, fmt.Errorf("failed to add user to vault: %w", err)
|
return nil, false, fmt.Errorf("failed to add user to vault: %w", err)
|
||||||
}
|
}
|
||||||
@ -545,11 +563,17 @@ func (bridge *Bridge) logoutUser(ctx context.Context, user *user.User, withAPI,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// getUserInfo returns information about a disconnected user.
|
// getUserInfo returns information about a disconnected user.
|
||||||
func getUserInfo(userID, username string, state UserState, addressMode vault.AddressMode) UserInfo {
|
func getUserInfo(userID, username, primaryEmail string, state UserState, addressMode vault.AddressMode) UserInfo {
|
||||||
|
var addresses []string
|
||||||
|
if len(primaryEmail) > 0 {
|
||||||
|
addresses = []string{primaryEmail}
|
||||||
|
}
|
||||||
|
|
||||||
return UserInfo{
|
return UserInfo{
|
||||||
State: state,
|
State: state,
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
Username: username,
|
Username: username,
|
||||||
|
Addresses: addresses,
|
||||||
AddressMode: addressMode,
|
AddressMode: addressMode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -21,10 +21,13 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/gluon/reporter"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (bridge *Bridge) handleUserEvent(ctx context.Context, user *user.User, event events.Event) error {
|
func (bridge *Bridge) handleUserEvent(ctx context.Context, user *user.User, event events.Event) error {
|
||||||
@ -51,6 +54,12 @@ func (bridge *Bridge) handleUserEvent(ctx context.Context, user *user.User, even
|
|||||||
|
|
||||||
case events.UserDeauth:
|
case events.UserDeauth:
|
||||||
bridge.handleUserDeauth(ctx, user)
|
bridge.handleUserDeauth(ctx, user)
|
||||||
|
|
||||||
|
case events.UserBadEvent:
|
||||||
|
bridge.handleUserBadEvent(ctx, user, event.Error)
|
||||||
|
|
||||||
|
case events.UncategorizedEventError:
|
||||||
|
bridge.handleUncategorizedErrorEvent(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -130,3 +139,25 @@ func (bridge *Bridge) handleUserDeauth(ctx context.Context, user *user.User) {
|
|||||||
bridge.logoutUser(ctx, user, false, false)
|
bridge.logoutUser(ctx, user, false, false)
|
||||||
}, bridge.usersLock)
|
}, bridge.usersLock)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) handleUserBadEvent(ctx context.Context, user *user.User, err error) {
|
||||||
|
safe.Lock(func() {
|
||||||
|
if rerr := bridge.reporter.ReportMessageWithContext("Failed to handle event", reporter.Context{
|
||||||
|
"error_type": fmt.Sprintf("%T", internal.ErrCause(err)),
|
||||||
|
"error": err,
|
||||||
|
}); rerr != nil {
|
||||||
|
logrus.WithError(rerr).Error("Failed to report failed event handling")
|
||||||
|
}
|
||||||
|
|
||||||
|
bridge.logoutUser(ctx, user, true, false)
|
||||||
|
}, bridge.usersLock)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) handleUncategorizedErrorEvent(event events.UncategorizedEventError) {
|
||||||
|
if rerr := bridge.reporter.ReportMessageWithContext("Failed to handle due to uncategorized error", reporter.Context{
|
||||||
|
"error_type": fmt.Sprintf("%T", internal.ErrCause(event.Error)),
|
||||||
|
"error": event.Error,
|
||||||
|
}); rerr != nil {
|
||||||
|
logrus.WithError(rerr).Error("Failed to report failed event handling")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2022 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// 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) {
|
func TestBridge_LoginLogoutLogin(t *testing.T) {
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
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) {
|
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) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
// Create a new user.
|
// 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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Give the new user an alias.
|
// Give the new user an alias.
|
||||||
@ -606,7 +624,7 @@ func TestBridge_UserInfo_Alias(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// The user should have two addresses, the primary should be first.
|
// 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.
|
// 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.
|
// 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.
|
// 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.
|
// 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.
|
// 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.
|
// 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.
|
// 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.
|
// 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.
|
// 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.
|
// 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.
|
// 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.
|
// 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.
|
// 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.
|
// 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.
|
// 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.
|
// 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.
|
// 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.
|
// 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.
|
// 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.
|
// 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.
|
// 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.
|
// 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.
|
// 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.
|
// 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.
|
// 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.
|
// 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.
|
// 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.
|
// 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.
|
// 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")
|
r.Error(t, err, "expected dial to fail because of wrong public key")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTLSSignedCertTrustedPublicKey(t *testing.T) {
|
// GODT-2293 bump badssl cert and re enable this.
|
||||||
|
func _TestTLSSignedCertTrustedPublicKey(t *testing.T) { //nolint:unused,deadcode
|
||||||
skipIfProxyIsSet(t)
|
skipIfProxyIsSet(t)
|
||||||
|
|
||||||
_, dialer, _, checker, _ := createClientWithPinningDialer("")
|
_, dialer, _, checker, _ := createClientWithPinningDialer("")
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2022 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.Bridge.
|
// 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.
|
// 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.
|
// 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.
|
// 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.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
|
|||||||
31
internal/errors.go
Normal file
31
internal/errors.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
// Copyright (c) 2023 Proton AG
|
||||||
|
//
|
||||||
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
// ErrCause returns the cause of the error, the inner-most error in the wrapped chain.
|
||||||
|
func ErrCause(err error) error {
|
||||||
|
cause := err
|
||||||
|
|
||||||
|
for errors.Unwrap(cause) != nil {
|
||||||
|
cause = errors.Unwrap(cause)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cause
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2022 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// 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.
|
// 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.
|
// 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.
|
// 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.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -52,9 +52,8 @@ type UserLabelDeleted struct {
|
|||||||
|
|
||||||
UserID string
|
UserID string
|
||||||
LabelID string
|
LabelID string
|
||||||
Name string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (event UserLabelDeleted) String() string {
|
func (event UserLabelDeleted) String() string {
|
||||||
return fmt.Sprintf("UserLabelDeleted: UserID: %s, LabelID: %s, Name: %s", event.UserID, event.LabelID, logging.Sensitive(event.Name))
|
return fmt.Sprintf("UserLabelDeleted: UserID: %s, LabelID: %s", event.UserID, event.LabelID)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2022 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// 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.
|
// 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.
|
// 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.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
@ -23,6 +23,7 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// AllUsersLoaded is emitted when all users have been loaded.
|
||||||
type AllUsersLoaded struct {
|
type AllUsersLoaded struct {
|
||||||
eventBase
|
eventBase
|
||||||
}
|
}
|
||||||
@ -31,6 +32,7 @@ func (event AllUsersLoaded) String() string {
|
|||||||
return "AllUsersLoaded"
|
return "AllUsersLoaded"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserLoading is emitted when a user is being loaded.
|
||||||
type UserLoading struct {
|
type UserLoading struct {
|
||||||
eventBase
|
eventBase
|
||||||
|
|
||||||
@ -41,6 +43,7 @@ func (event UserLoading) String() string {
|
|||||||
return fmt.Sprintf("UserLoading: UserID: %s", event.UserID)
|
return fmt.Sprintf("UserLoading: UserID: %s", event.UserID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserLoadSuccess is emitted when a user has been loaded successfully.
|
||||||
type UserLoadSuccess struct {
|
type UserLoadSuccess struct {
|
||||||
eventBase
|
eventBase
|
||||||
|
|
||||||
@ -51,6 +54,7 @@ func (event UserLoadSuccess) String() string {
|
|||||||
return fmt.Sprintf("UserLoadSuccess: UserID: %s", event.UserID)
|
return fmt.Sprintf("UserLoadSuccess: UserID: %s", event.UserID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserLoadFail is emitted when a user has failed to load.
|
||||||
type UserLoadFail struct {
|
type UserLoadFail struct {
|
||||||
eventBase
|
eventBase
|
||||||
|
|
||||||
@ -62,6 +66,7 @@ func (event UserLoadFail) String() string {
|
|||||||
return fmt.Sprintf("UserLoadFail: UserID: %s, Error: %s", event.UserID, event.Error)
|
return fmt.Sprintf("UserLoadFail: UserID: %s, Error: %s", event.UserID, event.Error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserLoggedIn is emitted when a user has logged in.
|
||||||
type UserLoggedIn struct {
|
type UserLoggedIn struct {
|
||||||
eventBase
|
eventBase
|
||||||
|
|
||||||
@ -72,6 +77,7 @@ func (event UserLoggedIn) String() string {
|
|||||||
return fmt.Sprintf("UserLoggedIn: UserID: %s", event.UserID)
|
return fmt.Sprintf("UserLoggedIn: UserID: %s", event.UserID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserLoggedOut is emitted when a user has logged out.
|
||||||
type UserLoggedOut struct {
|
type UserLoggedOut struct {
|
||||||
eventBase
|
eventBase
|
||||||
|
|
||||||
@ -82,6 +88,7 @@ func (event UserLoggedOut) String() string {
|
|||||||
return fmt.Sprintf("UserLoggedOut: UserID: %s", event.UserID)
|
return fmt.Sprintf("UserLoggedOut: UserID: %s", event.UserID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserDeauth is emitted when a user has lost its API authentication.
|
||||||
type UserDeauth struct {
|
type UserDeauth struct {
|
||||||
eventBase
|
eventBase
|
||||||
|
|
||||||
@ -92,6 +99,19 @@ func (event UserDeauth) String() string {
|
|||||||
return fmt.Sprintf("UserDeauth: UserID: %s", event.UserID)
|
return fmt.Sprintf("UserDeauth: UserID: %s", event.UserID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserBadEvent is emitted when a user cannot apply an event.
|
||||||
|
type UserBadEvent struct {
|
||||||
|
eventBase
|
||||||
|
|
||||||
|
UserID string
|
||||||
|
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 {
|
type UserDeleted struct {
|
||||||
eventBase
|
eventBase
|
||||||
|
|
||||||
@ -102,6 +122,7 @@ func (event UserDeleted) String() string {
|
|||||||
return fmt.Sprintf("UserDeleted: UserID: %s", event.UserID)
|
return fmt.Sprintf("UserDeleted: UserID: %s", event.UserID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserChanged is emitted when a user's data has changed (name, email, etc.).
|
||||||
type UserChanged struct {
|
type UserChanged struct {
|
||||||
eventBase
|
eventBase
|
||||||
|
|
||||||
@ -112,6 +133,7 @@ func (event UserChanged) String() string {
|
|||||||
return fmt.Sprintf("UserChanged: UserID: %s", event.UserID)
|
return fmt.Sprintf("UserChanged: UserID: %s", event.UserID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserRefreshed is emitted when an API refresh was issued for a user.
|
||||||
type UserRefreshed struct {
|
type UserRefreshed struct {
|
||||||
eventBase
|
eventBase
|
||||||
|
|
||||||
@ -122,6 +144,7 @@ func (event UserRefreshed) String() string {
|
|||||||
return fmt.Sprintf("UserRefreshed: UserID: %s", event.UserID)
|
return fmt.Sprintf("UserRefreshed: UserID: %s", event.UserID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddressModeChanged is emitted when a user's address mode has changed.
|
||||||
type AddressModeChanged struct {
|
type AddressModeChanged struct {
|
||||||
eventBase
|
eventBase
|
||||||
|
|
||||||
@ -133,3 +156,14 @@ type AddressModeChanged struct {
|
|||||||
func (event AddressModeChanged) String() string {
|
func (event AddressModeChanged) String() string {
|
||||||
return fmt.Sprintf("AddressModeChanged: UserID: %s, AddressMode: %s", event.UserID, event.AddressMode)
|
return fmt.Sprintf("AddressModeChanged: UserID: %s, AddressMode: %s", event.UserID, event.AddressMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UncategorizedEventError struct {
|
||||||
|
eventBase
|
||||||
|
|
||||||
|
UserID string
|
||||||
|
Error error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (event UncategorizedEventError) String() string {
|
||||||
|
return fmt.Sprintf("UncategorizedEventError: UserID: %s, Source:%T, Error: %s", event.UserID, event.Error, event.Error)
|
||||||
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2022 Proton AG
|
// Copyright (c) 2023 Proton AG
|
||||||
//
|
//
|
||||||
// This file is part of Proton Mail Bridge.
|
// 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.
|
// 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.
|
// This file is part of Proton Mail Bridge.
|
||||||
//
|
//
|
||||||
|
|||||||
@ -24,12 +24,11 @@
|
|||||||
package proto
|
package proto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
reflect "reflect"
|
|
||||||
sync "sync"
|
|
||||||
|
|
||||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||||
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
||||||
|
reflect "reflect"
|
||||||
|
sync "sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -105,7 +104,7 @@ var file_focus_proto_rawDesc = []byte{
|
|||||||
0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x3d, 0x5a, 0x3b, 0x67,
|
0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x3d, 0x5a, 0x3b, 0x67,
|
||||||
0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x6e,
|
0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x6e,
|
||||||
0x4d, 0x61, 0x69, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x6e, 0x2d, 0x62, 0x72, 0x69, 0x64,
|
0x4d, 0x61, 0x69, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x6e, 0x2d, 0x62, 0x72, 0x69, 0x64,
|
||||||
0x67, 0x65, 0x2f, 0x76, 0x32, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x66,
|
0x67, 0x65, 0x2f, 0x76, 0x33, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x66,
|
||||||
0x6f, 0x63, 0x75, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
|
0x6f, 0x63, 0x75, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
|
||||||
0x6f, 0x33,
|
0x6f, 0x33,
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user