Compare commits

...

339 Commits

Author SHA1 Message Date
c32c431640 chore: merge Kanmon to master 2025-07-10 13:36:03 +00:00
4cc2ded001 chore: Kanmon Bridge 3.21.2 changelog. 2025-07-07 11:34:42 +02:00
15880dfe19 fix(BRIDGE-406): fixed faulty certificate chain validation logic; made certificate pin checks exclusive to leaf certs; 2025-07-04 15:19:44 +02:00
dbef40cfc5 chore: merge Kanmon to master 2025-06-12 16:54:19 +00:00
e9ea976773 chore: Kanmon Bridge 3.21.1 changelog. 2025-06-11 16:15:53 +02:00
a00af3a398 feat(BRIDGE-383): Internal mailbox conflict resolution extended; Minor alterations to mailbox conflict pre-checker 2025-06-11 16:11:20 +02:00
8b891fb3e7 chore: merge Kanmon to master 2025-06-09 13:05:03 +00:00
50ab740b92 chore: Kanmon Bridge 3.21.0 changelog. 2025-06-05 15:45:27 +02:00
39f2362996 feat(BRIDGE-379): mailbox pre-checker on startup & conflict resolver for bridge internal mailboxes; TODO potentially add this for system mailboxes as well 2025-06-05 14:34:29 +02:00
d2742c81e5 feat(BRIDGE-376): catch gluon errors related to label uniqueness constrainst... 2025-06-05 14:34:29 +02:00
9cb914cf13 fix(BRIDGE-377): Correct label field usage on label update handler 2025-06-05 14:34:29 +02:00
4088cf18c3 feat(BRIDGE-373): extend label conflict resolver logging & report sync errors to sentry 2025-06-05 14:34:29 +02:00
c02bae5eb2 fix(BRIDGE-378): Fix incorrect field usage for system mailbox names 2025-06-03 17:36:58 +02:00
94125056ab chore: merge Jubilee to master 2025-05-29 14:02:18 +00:00
2aa8acfb5b chore: changes to reconcile release/jubilee with dev 2025-05-28 16:56:28 +02:00
8109b384c5 fix(BRIDGE-362): added label conflict reconciliation logic 2025-05-28 16:56:07 +02:00
6d79ad3e41 chore: Jubilee Bridge 3.20.1 changelog. 2025-05-28 16:53:23 +02:00
5d93ee0cfc chore: Jubilee Bridge 3.20.0 changelog. 2025-05-28 16:53:23 +02:00
675b37a2fa chore: Jubilee Bridge 3.20.1 changelog. 2025-05-28 10:20:12 +02:00
c3e2201945 feat(BRIDGE-366): Kill switch support for IMAP IDLE 2025-05-28 09:53:45 +02:00
9d4415d8cc fix(BRIDGE-362): added label conflict reconciliation logic 2025-05-28 09:27:57 +02:00
89da7335b6 feat(BRIDGE-363): Observability metrics for IMAP connections; minor unleash service refactor; 2025-05-16 15:28:53 +02:00
4557f54e2f chore: merge Jubilee to master 2025-05-06 14:51:31 +00:00
05623a9e49 chore: Jubilee Bridge 3.20.0 changelog. 2025-04-24 13:40:43 +02:00
a305ee1113 chore: Infinity Bridge 3.19.0 changelog. 2025-04-24 11:13:17 +02:00
e38f7748d0 chore: bump GPA 2025-04-22 12:41:13 +00:00
92b2024e3e chore(BRIDGE-352): bump go to 1.24.2 2025-04-17 14:06:54 +00:00
37a8fc95d2 chore(BRIDGE-353): update x/net package to 0.38.0 2025-04-17 10:48:31 +02:00
0c63533aa7 fix(BRIDGE-351): allow draft creation and import to BYOE addresses in combined mode 2025-04-15 17:26:14 +02:00
af98bc2273 fix(BRIDGE-301): don't use external, non-BYOE addresses for imports 2025-04-10 09:51:31 +00:00
b37f2d138a feat(BRIDGE-348): display BYOE addresses in Bridge 2025-04-10 10:06:40 +02:00
7831a98e6c chore(BRIDGE-346): silence http/net vulnerability 2025-04-09 10:49:34 +02:00
4d415675e0 fix(BRIDGE-341): replace go-autostart with fork; added ability to create shortcuts with unicode chars 2025-04-02 11:56:58 +00:00
291f44d1b5 fix(BRIDGE-332): filter new line characters from username and password fields in GUI 2025-04-01 14:37:18 +02:00
a4b315d67a fix(BRIDGE-336): check and create all labels in Gluon on Bridge start 2025-03-25 15:24:59 +01:00
a15d4eb3ef ci: update CODEOWNERS 2025-03-24 15:38:03 +01:00
4e764fe93d feat(BRIDGE-340): additional logging for label operations & bad events 2025-03-24 14:30:19 +01:00
df409925ec fix(BRIDGE-335): store last sucessfully used keychain helper as user preference 2025-03-19 15:10:09 +01:00
e68f3441d7 fix(BRIDGE-196): bump badssl public key 2025-03-19 10:00:23 +00:00
42605c1923 chore: merge Infinity to master 2025-03-18 15:03:14 +00:00
899d3293bc feat(BRIDGE-324): added a log entry for the vault key hash 2025-03-18 11:21:12 +00:00
c66f0b800a fix(BRIDGE-333): ignore unkown label IDs during synchronization 2025-03-17 10:43:26 +01:00
b9c75d02b2 chore: stabilize windows tests 2025-03-14 11:56:42 +01:00
9f4801b738 chore: Infinity Bridge 3.19.0 changelog. 2025-03-07 11:12:59 +01:00
4b91e66505 chore(BRIDGE-315): remove silenced vulns 2025-03-06 14:49:03 +00:00
0cbcd0bf13 fix(BRIDGE-329): fix menu bar icons not displayin on macOS 2025-03-06 15:10:52 +01:00
5c12b00e70 chore: Helix Bridge 3.18.0 changelog. 2025-03-06 10:37:52 +01:00
6e7cdfcd68 feat(BRIDGE-316): Changes required for Qt 6.8.2 bump; bumped go to 1.24.0; changes to OS bundler configs; golangci-lint bump; 2025-03-05 14:27:33 +01:00
4e6236611a chore: merge Helix to master 2025-02-27 10:36:11 +00:00
a75f84742b chore: remove redundant log entry 2025-02-24 10:58:16 +01:00
0800aeea50 chore: Helix Bridge 3.18.0 changelog. 2025-02-18 23:44:44 +01:00
f4ddf43ac7 chore: Grunwald Bridge 3.17.0 changelog. 2025-02-18 17:11:46 +01:00
da0f51ce5f feat(BRIDGE-309): Update to the bridge updater logic corresponding to the version file restructure 2025-02-17 15:43:15 +00:00
d711d9f562 feat(BRIDGE-154): include access token when refreshing 2025-02-17 15:10:05 +01:00
b230f2ece6 chore: merge XXX to master 2025-02-12 08:37:01 +00:00
d44c488ed5 chore: minor comment just so we have a new commit 2025-02-11 10:28:05 +01:00
fe39d23cf8 chore(BRIDGE-315): silence crypto/internal/nistec vuln 2025-02-10 12:53:07 +01:00
dbb84f2ae2 chore(BRIDGE-315): silence govulncheck vulns 2025-01-31 10:36:50 +01:00
8237129670 chore: merge Grunwald to master 2025-01-29 15:52:34 +00:00
8e634995c5 chore: Grunwald Bridge 3.17.0 changelog. 2025-01-21 14:42:52 +01:00
a2c1da9748 chore(BRIDGE-73): bumping GPA; includes changes from BRIDGE-93 and BRIDGE-73 (GPA); 2025-01-21 13:08:29 +01:00
b8cc71fdd8 chore(BRIDGE-287): bump x/net x/crypto dependencies; remove ignore statements from govulncheck 2025-01-20 16:56:25 +00:00
1949e89053 chore(BRIDGE-303): update govulncheck to use latest release 2025-01-20 16:09:32 +01:00
ae5469fc81 chore: remove export loop ref and loop-scope assignments (changed with go 1.22) 2025-01-20 14:54:34 +01:00
105ea4de0d feat(BRIDGE-226): bump version Go 1.23.4 Qt 6.4.3. 2025-01-20 11:12:35 +00:00
b21d126ab0 feat(BRIDGE-288): extended sync message update handler; observability tweaks; gluon bump; 2025-01-16 15:08:52 +01:00
7bc7a5e7b3 chore: downgraded govulncheck package 2025-01-16 14:43:44 +01:00
10a685a123 chore: Prepare for issue tracker removal 2025-01-14 10:48:03 +01:00
896f50c754 chore: FF devel into master 2025-01-14 10:35:25 +01:00
edbc7d0e3d chore: Flavien Bridge 3.16.0 changelog. 2025-01-09 14:49:17 +01:00
b7b1043b88 chore: Erasmus Bridge 3.15.1 changelog. 2025-01-09 14:48:38 +01:00
0641c63377 fix(BRIDGE-291): use correct field for user plan type 2025-01-08 10:21:10 +00:00
74a990c69a feat(BRIDGE-271): report to sentry when version file check fails 2025-01-08 08:59:29 +00:00
e340e9f845 fix(BRIDGE-143): added missing qml component attribute; cut/paste disabled on read only text areas 2025-01-08 08:29:21 +00:00
082849dc6c chore: year bump 2025-01-02 14:03:49 +01:00
6878b3b5e0 test(BRIDGE-247): Automate Bridge 0% update rollout 2024-12-30 16:01:10 +00:00
16245a372e test(BRIDGE-248): Additional Bridge UI e2e automation tests 2024-12-30 14:17:10 +00:00
28b0dbd051 chore: suppress vulnerability warnings in x/net package 2024-12-23 13:39:44 +01:00
60633fc09c chore: merge Flavien to master 2024-12-17 15:20:30 +00:00
9c5b5c2ac3 chore: FF devel into master 2024-12-16 12:22:45 +01:00
0e6df4ce73 chore: Prepare for issue tracker removal 2024-12-16 10:47:24 +00:00
a4772ee4e0 chore: added CODEOWNER group 2024-12-12 10:46:17 +01:00
ef779a23c1 chore: fix linter issues. 2024-12-05 15:06:20 +01:00
4f4a2c3fd8 chore: merge Erasmus to master 2024-12-05 11:35:19 +00:00
120a7b3626 chore: Erasmus Bridge 3.15.1 changelog. 2024-12-04 14:44:25 +01:00
7cf3b6fb7b feat(BRIDGE-281): disable keychain test on macOS.
(cherry picked from commit 3f78f4d672)
2024-12-04 14:09:50 +01:00
03c9455b0d chore: Flavien Bridge 3.16.0 changelog. 2024-12-04 10:03:12 +01:00
dd2448f35a fix(BRIDGE-266): changed heartbeat measurement group 2024-12-04 08:51:17 +01:00
3f78f4d672 feat(BRIDGE-281): disable keychain test on macOS. 2024-11-29 09:14:29 +01:00
5fbe94c559 chore: Erasmus Bridge 3.15.0 changelog. 2024-11-27 14:59:13 +01:00
80d556343e fix(BRIDGE-256): fix reversed order of headers with multiple values. 2024-11-26 17:08:43 +00:00
612d1054db test(BRIDGE-246): Add Settings Menu Bridge UI e2e automation tests 2024-11-25 13:25:20 +00:00
acf2fc32c4 fix(BRIDGE-264): ignore apple notes as User-Agentt 2024-11-25 08:55:27 +00:00
af01c63298 fix(BRIDGE-261): delete gluon data during user deletion; integration tests; FF kill switch; Sentry report if error; 2024-11-22 14:32:28 +00:00
2e98d64f94 feat(BRIDGE-266): heartbeat telemetry update; extra integration tests; 2024-11-22 14:09:48 +00:00
cdcdd45bcf feat(BRIDGE-268): add kill switch feature flag for the IMAP AUTHENTICATE command. 2024-11-22 12:32:33 +01:00
61ca604ace chore: merge Erasmus to master 2024-11-13 09:30:24 +00:00
b3e2a91f56 feat(BRIDGE-205): add support for the IMAP AUTHENTICATE command. 2024-11-12 15:28:22 +01:00
7d9753e2da fix(BRIDGE-107): improved human verification UX 2024-11-11 09:03:02 +00:00
f1aef383b7 fix(BRIDGE-258): fixed issue with draft updates and sending during synchronization 2024-11-07 17:47:36 +01:00
6647231278 chore: (BRIDGE-253) removing unused telemetry (activation and troubleshooting) 2024-10-30 15:13:41 +00:00
531368da86 feat(BRIDGE-252): restored the -h shortcut shortcut for the CLI --help switch. 2024-10-30 12:36:21 +01:00
a8caec560e chore: Erasmus Bridge 3.15.0 changelog. 2024-10-29 10:47:33 +01:00
0c21925939 chore: cherry pick changes from Dragon release branch.
(cherry picked from commit 6105f32c75)
2024-10-29 08:25:15 +01:00
19a445e73a fix(BRIDGE-240): fix user contribution to match our QML naming convention. 2024-10-28 16:06:19 +01:00
96e0070ed2 fix(BRIDGE-240): avoid name clash with 6.8-introduced popupType
(cherry picked from commit 834a2f910a)
2024-10-28 15:58:37 +01:00
516ff5206d fix(BRIDGE-240): fix ColorImage Qt crash
(cherry picked from commit 5615176ca9)
2024-10-28 15:58:37 +01:00
4d2b328589 feat(BRIDGE-238): Added host information to sentry events; new sentry event for keychain issues 2024-10-28 11:53:04 +00:00
810be2d423 feat(BRIDGE-215): tweak wording on macOS profile install page. 2024-10-28 10:58:52 +01:00
e3d0334b6f feat(BRIDGE-236): added SMTP observability metrics 2024-10-25 08:25:21 +00:00
fb523e5573 feat(BRIDGE-217): added missing parameter to the CLI help command. 2024-10-24 10:27:32 +02:00
cb8d1a2389 fix(BRIDGE-231): fix reversed header order in messages. 2024-10-23 11:25:49 +00:00
84f0a6722a chore: README.md update. 2024-10-23 08:57:03 +02:00
c3a495facd feat(BRIDGE-234): add accessibility name in QML for UI automation. 2024-10-21 16:53:05 +02:00
9cdc40ca05 test(BRIDGE-232): Add Home Menu Bridge UI e2e automation tests 2024-10-21 12:48:03 +00:00
7021b1c2ea feat(BRIDGE-228): removed sentry events: 2024-10-21 09:16:56 +00:00
607d9df8a9 fix(BRIDGE-235): fix compilation of Bridge GUI Tester on Windows. 2024-10-21 08:40:29 +02:00
93396145dc test(BRIDGE-220): Add Bridge E2E UI login/logout tests for Windows 2024-10-18 07:52:46 +00:00
7457fb06d2 feat(BRIDGE-120): use appropriate address key when importing / saving draft. 2024-10-11 12:50:21 +02:00
bee2642aec chore: update golangci-lint to 1.61.0. 2024-10-09 14:56:18 +02:00
b481ce2203 test(BRIDGE-131): Integration tests for messages from Proton <-> Gmail 2024-10-09 12:29:42 +00:00
040d887aae feat(BRIDGE-218): observability adapter; gluon observability metrics and tests; 2024-10-08 13:13:07 +00:00
3710dff0cd feat(BRIDGE-142): bridge icon can be removed from the menu bar on macOS. 2024-10-03 15:29:07 +02:00
df78e29234 chore: merge Dragon to master 2024-09-30 09:05:11 +00:00
6105f32c75 chore: Dragon Bridge 3.14.0 changelog. 2024-09-25 10:47:40 +02:00
f5bc6ad1f0 chore: Colorado Bridge 3.13.0 changelog.
(cherry picked from commit 43cbedafb8)
2024-09-25 07:42:38 +02:00
e8a95e26f6 feat(BRIDGE-207): failure to download or verify an update now fails silently. 2024-09-24 14:06:18 +00:00
ebe54ca92e fix(BRIDGE-210): reduced log level of cache events so they won't be printed to stdout 2024-09-24 13:58:26 +02:00
ff7e45f395 feat(BRIDGE-204): removed redundant Sentry events 2024-09-23 15:52:23 +00:00
79c63f5785 fix(BRIDGE-106): Fixed import of multipart-related messages; added relevant tests 2024-09-23 10:57:26 +00:00
3ca9e625f5 feat(BRIDGE-150): Observability service modification; user distinction utility & heartbeat; various observbility metrics & relevant integration tests 2024-09-23 10:13:05 +00:00
5b874657cb test(BRIDGE-133): Bridge E2E UI tests for Windows 2024-09-17 05:23:08 +00:00
da76784290 chore: merge Colorado to master 2024-09-10 12:05:30 +00:00
bfe67f3005 fix(BRIDGE-108): fixed GetInitials when empty username is passed; we now overwrite the username in the vault if its value changed, each time we refresh user auth 2024-09-05 08:14:30 +00:00
99e6f00aaa test(BRIDGE-124): Fixing failing nightly integration tests due to KB articles issues 2024-08-30 14:23:39 +00:00
43cbedafb8 chore: Colorado Bridge 3.13.0 changelog. 2024-08-30 15:35:30 +02:00
ac9ab8ab32 fix(BRIDGE-81): update KB suggestions. 2024-08-30 09:50:41 +02:00
f04350c046 feat(BRIDGE-37): Remote notification support 2024-08-29 13:31:37 +02:00
ed1b65731a feat(BRIDGE-81): kb article suggestion updates + more weight for long keywords. 2024-08-27 16:16:23 +02:00
d12928b31c feat(BRIDGE-122): Observability service implementation 2024-08-27 15:21:41 +02:00
1ea06a95b7 fix(BRIDGE-138): remove deprecated doc. 2024-08-27 08:49:15 +02:00
e290cd308b feat(BRIDGE-119): added support for Feature Flags 2024-08-21 14:54:27 +02:00
3d53bf7477 feat(BRIDGE-116): add command-line switches to enable/disable keychain check on macOS. 2024-08-09 09:53:46 +02:00
84c0b907d7 chore: Bastei Bridge 3.12.0 changelog.
(cherry picked from commit ed5adb18fb)
2024-08-09 08:32:13 +02:00
b30455b641 chore: Bastei Bridge 3.12.0 changelog.
(cherry picked from commit 48a75b0dd7)
2024-08-09 08:32:13 +02:00
db9902e70b feat(BRIDGE-97): added repair button telemetry
(cherry picked from commit 85a91c5572)
2024-08-09 08:26:56 +02:00
f1f63c1d03 feat(BRIDGE-79): update to the KB suggestion list.
(cherry picked from commit 56d4bfbb71)
2024-08-09 08:23:19 +02:00
81a3c2aba8 feat(BRIDGE-88): added context menu for quick actions on input labels: cut, copy, paste 2024-08-07 10:19:40 +00:00
bbfc9beb04 chore: update GPA. 2024-07-30 11:02:17 +02:00
c4dba09ee6 ci: updated devsecops kit to latest. 2024-07-26 08:54:37 +02:00
a5435eb1da ci: BRIDGE-113 adding kits CI/CD component 2024-07-24 12:45:47 +02:00
54c56efdfa test(GODT-3205): Report results from nightly integration tests to Testmo 2024-07-16 09:04:27 +00:00
fc64dbec59 chore: golangci-lint update. 2024-07-11 16:29:59 +02:00
5d3f084a2b test(BRIDGE-109): Update KB articles failing integration tests
- Change KB article suggestions based on description
2024-07-11 09:28:38 +00:00
606d6c0e3e chore: temporarily silence GO-2024-2963 in govulncheck 2024-07-11 07:34:51 +02:00
9fbb6b4ca5 fix(BRIDGE-67): added detection for username changes on macOS & automatic reconfiguration 2024-06-20 12:32:42 +00:00
0d33cc5000 chore: merge Bastei to master 2024-06-19 06:06:24 +00:00
ed5adb18fb chore: Bastei Bridge 3.12.0 changelog. 2024-06-17 11:19:49 +02:00
85a91c5572 feat(BRIDGE-97): added repair button telemetry 2024-06-14 13:01:07 +00:00
56d4bfbb71 feat(BRIDGE-79): update to the KB suggestion list. 2024-06-13 10:05:23 +02:00
48a75b0dd7 chore: Bastei Bridge 3.12.0 changelog. 2024-06-06 10:10:36 +02:00
8688277ee6 ci: supress govulncheck vulns 2024-06-05 12:36:43 +00:00
63eb67760e fix(BRIDGE-90): disable repair button when bridge cannot connect to proton servers; bump GPA 2024-06-05 12:36:43 +00:00
cffab028b2 chore: cherry picked changelog from 3.11 release branch.
chore: Alcantara Bridge 3.11.0 changelog.

(cherry picked from commit 2569e83e51)

chore: Alcantara Bridge 3.11.0 changelog.

(cherry picked from commit b574ccb6ea)

chore: Alcantara Bridge 3.11.0 changelog.

(cherry picked from commit 82607efe1c)

chore: Alcantara Bridge 3.11.1 changelog.

(cherry picked from commit cd8db6fd1c)
2024-06-04 14:01:16 +00:00
8ea712b052 fix(BRIDGE-15): Apple Mail profile install page was not properly reset before showing.
(cherry picked from commit 961dc9435f)
2024-06-04 11:11:02 +02:00
ff0615167b feat(BRIDGE-75): Bridge repair button/feature implemented 2024-06-03 12:37:23 +00:00
e2b361b9a6 feat(BRIDGE-79): Add New Outlook for Mac KB disclaimer.
Update submitted by Laze Dimitrovski
2024-05-30 17:49:22 +02:00
1c6bbf1fae chore: enable GO-2024-2687 in govulncheck 2024-05-29 16:48:02 +02:00
e7713fa785 fix(BRIDGE-69): explicitly handle semver panic for last bridge version from vault 2024-05-22 10:54:38 +00:00
b84663dd7a chore: merge Alcantara to master 2024-05-21 09:32:21 +00:00
28ae54b5ca feat(BRIDGE-16): bump version Go 1.21.9 Qt 6.4.3. 2024-05-21 08:51:28 +02:00
00aff40160 fix(BRIDGE-70): hotfix for blocked smtp/imap port causing bridge to quit 2024-05-17 12:35:07 +02:00
cd8db6fd1c chore: Alcantara Bridge 3.11.1 changelog. 2024-05-16 15:12:56 +02:00
a5e0f85a58 fix(BRIDGE-70): hotfix for blocked smtp/imap port causing bridge to quit 2024-05-16 09:51:32 +02:00
ab289e6e01 fix(BRIDGE-29): bump gluon version 2024-05-14 15:41:10 +02:00
a28dc9f2f3 fix(BRIDGE-49): Configure gitleaks baseline and grype config 2024-05-02 10:59:43 +00:00
6cbe51138a chore: merge Alcantara to master 2024-04-29 12:31:37 +00:00
8a859082cd ci: added gitleaks and grype 2024-04-29 13:58:48 +02:00
1d972835ff fix(BRIDGE-21): missing panic handling 2024-04-26 13:24:02 +02:00
8469e0a661 fix(BRIDGE-17): broken telemetry heartbeat test 2024-04-25 11:16:13 +00:00
82607efe1c chore: Alcantara Bridge 3.11.0 changelog. 2024-04-23 17:07:24 +02:00
961dc9435f fix(BRIDGE-15): Apple Mail profile install page was not properly reset before showing. 2024-04-23 15:58:22 +02:00
6ea970bf97 feat(BRIDGE-23): update gluon to go 1.21. 2024-04-23 14:52:31 +02:00
a05b90e803 feat(BRIDGE-22): update gpa to go 1.21. 2024-04-23 14:50:46 +02:00
239ad8b946 fix(BRIDGE-10): bumped gluon version 2024-04-23 12:42:08 +02:00
b574ccb6ea chore: Alcantara Bridge 3.11.0 changelog. 2024-04-22 10:37:47 +02:00
2569e83e51 chore: Alcantara Bridge 3.11.0 changelog. 2024-04-22 09:27:43 +02:00
d9fdbb35bc fix(GODT-3185): logic mistake. 2024-04-22 07:26:18 +00:00
5769fb9466 ci: windows build missing revision 2024-04-19 10:39:47 +02:00
a4020cebd4 chore: do not use C++ 20 std::ranges. 2024-04-19 08:03:18 +02:00
7a8760e2ef fix(BRIDGE-19): warning instead of error on logs for checksum validation... 2024-04-17 12:59:36 +00:00
9552e72ba8 feat(BRIDGE-14): HV3 implementation - GUI & CLI; ownership verification & CAPTCHA are supported 2024-04-12 13:07:22 +00:00
c692c21b87 fix(BRIDGE-8): more robust command-line args parser in bridge-gui.
fix(BRIDGE-8): add command-line invocation to log.
2024-04-12 11:16:59 +00:00
bb15efa711 fix(BRIDGE-8): launcher replace session-id if provided instead of adding another one. 2024-04-12 11:16:59 +00:00
e94d3be12d chore: bump testing context bridge version 2024-04-12 11:55:53 +02:00
66569f71a0 fix(BRIDGE-7): add timestamp to test credentials for keychain on macOS. 2024-04-09 10:43:31 +02:00
9bfa79455e fix(BRIDGE-7): modify keychain test on macOS. 2024-04-08 14:35:36 +02:00
67e802e3a0 feat(BRIDGE-15): fix a stack layout index in comment. 2024-04-08 11:27:28 +02:00
8a5e2007f6 feat(BRIDGE-15): certificate install is now also done during Outlook setup on macOS. 2024-04-04 08:57:30 +02:00
5b92945626 chore: disable GO-2024-2687 in govulncheck 2024-04-04 07:58:16 +02:00
4a8a7ef093 fix(BRIDGE-4): logs not being created when invalid flag is passed 2024-03-21 16:32:12 +00:00
2cfda14b1a fix(BRIDGE-5): add tooltip to tray icon. 2024-03-20 14:55:40 +01:00
312993e08e feat(GODT-3253): windows cache and paths. 2024-03-15 11:28:52 +01:00
b1110b04c9 feat(GODT-3253): make paths. 2024-03-15 11:27:33 +01:00
d2bc60d9cb ci: debug 2024-03-15 11:27:29 +01:00
1d8f6c75c8 feat(GODT-3253): use new virtual machine for windows jobs. bump vcpkg to 2024.02.14 2024-03-15 11:23:46 +01:00
06daaf8d9f feat(GODT-3146): don't need to wait for IMAP in tests. 2024-03-14 11:57:55 +01:00
cb436fff63 feat(GODT-3146): remove unused 2024-03-13 14:31:53 +01:00
921a44f1a3 feat(GODT-3146): keep imap/smtp server always on. 2024-03-13 14:22:23 +01:00
d35af6b686 chore: added bridge-rollout to CI. 2024-03-13 09:25:40 +00:00
4cb938c57f chore: added bridge-rollout cli tool. 2024-03-13 09:25:40 +00:00
232e98d812 chore: Zaehringen Bridge 3.10.0 changelog. 2024-03-13 10:21:52 +01:00
6fadbde4a6 feat(GODT-3185): report cases which leads to wrong address key used 2024-03-13 07:49:25 +00:00
f34a7ff0ed chore: merge Zaehringen to master 2024-03-12 12:27:21 +00:00
d2fbbc3e25 fix(GODT-3163): filter MBOX format delimiter. 2024-03-07 12:30:33 +00:00
1c7c342e19 ci(GODT-3304): ignore go vulncheck until go version bumped. 2024-03-07 13:00:16 +01:00
da069a0155 chore: Zaehringen Bridge 3.10.0 changelog. 2024-03-06 10:33:17 +01:00
8e49c84a12 chore: changelog update. 2024-03-06 08:19:13 +01:00
754d80d097 feat(GODT-3193): assume text content type on attachments. 2024-03-01 15:25:37 +00:00
63e272e270 feat(GODT-3193): preserve attachment encoding. 2024-03-01 15:25:37 +00:00
54859a34b2 fix(GODT-3290): fix test failing because of leap day. 2024-03-01 10:56:24 +01:00
9b1feed68b feat(GODT-3214): encrypt only with primary key. 2024-02-28 13:42:09 +00:00
c9b6cc162b feat(GODT-3199): add package log field. 2024-02-27 13:07:37 +01:00
bf3c90b8e9 test(GODT-1602): rebased GPA changes. 2024-02-26 16:56:52 +01:00
8d63fb2301 feat(GODT-2662): enable cache on darwin tart. 2024-02-23 10:33:26 +01:00
7953306cc8 feat(GODT-2662): use tart runner for darwin jobs. 2024-02-23 10:00:47 +01:00
37352d44d2 test(GODT-1602): run integration tests against black 🖤 2024-02-19 10:43:35 +00:00
2a1aeb208d test(GODT-3257): quad9 provider test not working on CI. 2024-02-19 10:06:02 +01:00
94fbe260e4 test(GODT-3220): Fix linting issues by deleting a function
-Deleted a function that was no longer used

GODT-3220
2024-02-14 08:57:48 +01:00
6d4937222e test(GODT-3220): Rollback to a test scenario for logging in with an alias address
-Added test scenario for logging in with an alias address

GODT-3220
2024-02-13 10:56:23 +00:00
e33bad7bf1 test(GODT-3220): Add test scenario for sending an HTML msg with public key and multiple attachments to Internal
-Added test scenario for sending an HTML msg with public key and multiple attachments to Internal
- Verified the message on receipient's side

GODT-3220
2024-02-13 10:56:23 +00:00
70fdc91aff test(GODT-3220): Add test scenario for sending a message to multiple bcc accounts
- Added test scenario for sending a message to two bcc accounts
- Verified on recipients' side that the message is received

GODT-3220
2024-02-13 10:56:23 +00:00
bde8e45b37 test(GODT-3220): Add test scenarios for loging in with an alias address
-Added test scenarios for logging in with an alias address and logging in with an alias address that no longer exists

GODT-3220
2024-02-13 10:56:23 +00:00
6cb2d944d0 test(GODT-3220): Add test scenarios for logining in with alias address and loging in with an alias address
-Added a test scenario for logging in with an alias address
-Added a test scenario for logging in with alias address that no longer exists

GODT-3220
2024-02-13 10:56:23 +00:00
cf0f59afc0 test(GODT-3220): Add scenario cannot login with deleted alias 2024-02-13 10:56:23 +00:00
65d8fbbf31 test: keep deleted address in test suite 2024-02-13 10:56:23 +00:00
d919c0accf test(GODT-3220): Add step definition for logging in with alias address
GODT-3220
2024-02-13 10:56:23 +00:00
0ca07066db test(GODT-3220): Create function for getting the test user by address
GODT-3220
2024-02-13 10:56:23 +00:00
384fa4eb4b chore: merge Ypsilon to master 2024-02-12 11:19:51 +00:00
0c6e4ffa35 chore: merge Xikou to master 2024-02-03 00:14:41 +01:00
7fa1948c21 chore: Ypsilon Bridge 3.9.1 changelog. 2024-02-02 22:20:43 +01:00
413ab1fc1e fix(GODT-3235): update bridge update key 2024-02-02 22:10:28 +01:00
4951244400 chore: Xikou Bridge 3.8.2 changelog. 2024-02-02 19:32:58 +01:00
d65d6ee2e5 fix(GODT-3235): use release xikou for trigger build 2024-02-02 18:37:38 +01:00
097d6f86d3 fix(GODT-3235): update bridge update key 2024-02-02 17:34:32 +01:00
9894cf9744 chore: merge Ypsilon to master 2024-01-31 11:00:11 +00:00
45c2102ff7 chore: Ypsilon Bridge 3.9.0 changelog. 2024-01-30 16:15:48 +01:00
97fc964467 fix(GODT-3229): escape reserved XML characters in Apple configuration profile. 2024-01-29 16:27:36 +01:00
bfde96dc88 feat(GODT-3230): Scripts for removing Bridge from device 2024-01-29 11:33:55 +00:00
fdb5c0cbee chore: Ypsilon Bridge 3.9.0 changelog. 2024-01-29 11:17:23 +01:00
5b4c6870b5 fix(GODT-3228): update COPYING_NOTES.md 2024-01-29 08:03:49 +01:00
a433da8782 fix(GODT-3228): get rid of fork of docker-credential-helpers. 2024-01-29 07:52:08 +01:00
5df95566b7 chore: Ypsilon Bridge 3.9.0 changelog. 2024-01-25 10:10:11 +01:00
164fb23653 feat(GODT-3160): no need to ignore vulns 2024-01-25 09:30:31 +01:00
374194c13b feat(GODT-3160): make linter happy 2024-01-24 10:28:08 +01:00
1cd35defe5 feat(GODT-3160): bump version Go 1.21.6 Qt 6.4.3. 2024-01-24 10:11:52 +01:00
56aa497b9d feat(GODT-3169): load pipeline env from bridge internal. 2024-01-24 09:36:43 +01:00
773a230d14 fix(GODT-3176): assume inline if content id is present. 2024-01-18 16:45:08 +00:00
76d257af21 fix(GODT-3160): ignore non-called vulnerabilities. 2024-01-17 14:56:24 +01:00
856efec886 fix(GODT-3160): updated external dependencies reported by govulncheck. 2024-01-17 09:47:18 +01:00
46fd1d5a76 test(GODT-3052): Replace attachments and inline content in feature tests with the smallest valid versions 2024-01-15 13:10:22 +00:00
f565fc4f69 fix(GODT-3203): Crash in chunkDivide
If for some reason all the message we are trying to sync in a chunk are
deleted from another client it is possible that the input to the build
stage will be empty. This case is now handled correctly.
2024-01-11 08:33:02 +01:00
2895f42a64 feat(GODT-3195): add OS info to the log. 2024-01-10 08:32:43 +01:00
5751166ebc feat(GODT-3155): customize log formatter for easier parsing. 2024-01-09 09:24:21 +01:00
e63afd3910 feat(GODT-3156): add time zone info to the bridge log. 2024-01-05 09:30:13 +01:00
9b1daa0373 feat(GODT-3172): detect missing keychain item 2024-01-04 11:30:26 +00:00
89bb7b6389 feat(GODT-3172): do not list, just retrieve vault key. 2024-01-04 11:30:26 +00:00
31670ad9eb chore: fix for SMTP connection mode toggle in bridge-gui-tester. 2024-01-04 09:30:24 +01:00
fb32d652bc fix(GODT-3183): Fix database indices
https://github.com/ProtonMail/gluon/pull/402
2024-01-03 08:09:27 +00:00
346988e604 chore: Also log the message received time when handling message creation event. 2024-01-02 15:18:56 +00:00
43df20c25d fix(GODT-3187): Fix numberOfDay computation when changing date. 2024-01-02 15:27:46 +01:00
25ebcffde3 fix(GODT-3187): Fix numberOfDay computation when changing year. 2024-01-02 15:27:46 +01:00
b8ae5be58c fix(GODT-3188): Happy new year. 2024-01-02 15:06:05 +01:00
26a3385f4e test(GODT-3162): Add test scenarios for KB article suggestions
-Added test scenarios to check relevant suggestion links to knowledge base articles in the in-app bug report form
2023-12-21 06:48:34 +00:00
dc002959eb test: Add scenarios for checking messages sent from Web Client 2023-12-18 14:53:13 +00:00
8703faf345 chore: set log as artefact for all integration test. 2023-12-18 09:42:48 +01:00
3ac59d6943 test(GODT-3162): Add step definition for checking KB article suggestions
* Add a step definition that takes input from a possible problem report description, and gets the suggested knowledge base articles
* Also, has input of what those knowledge base articles should be, just their title and url, and compares these two values.
* A sample integration test is added
2023-12-15 09:53:49 +00:00
8f5bd37aee chore: Get better logging arround keychain list initialisation. 2023-12-14 17:24:16 +01:00
f84067de3e chore: merge Xikou to master 2023-12-12 13:39:06 +01:00
f885bfbcf4 chore: merge Xikou to master 2023-12-11 17:04:00 +01:00
5c69af4418 chore: Xikou Bridge 3.8.1 changelog. 2023-12-11 11:49:01 +01:00
416f696863 feat(GODT-3121): added options to kb-tester CLI tool. 2023-12-08 11:04:48 +01:00
789c1cc816 feat(GODT-3121): kb suggestion first version of complete list. 2023-12-08 10:01:38 +01:00
58736dd254 chore: keep nighlty-job log as artifact. 2023-12-08 09:31:57 +01:00
a057138880 feat(GODT-3121): KB suggestion test tool now support multi-line input. 2023-12-07 10:48:33 +01:00
76087f1749 feat(GODT-3121): minimalist CLI tool to test KB suggestions. 2023-12-07 09:36:47 +01:00
83935f3a03 feat(GODT-3121): refactored retrieval kb article index lookup. 2023-12-07 09:35:05 +01:00
b93c10ad47 feat(GODT-3121): adds KB suggestion scoring. 2023-12-07 09:35:05 +01:00
3309137b80 feat(GODT-3121): forward user input to bridge. 2023-12-07 09:35:05 +01:00
88c4737ba4 feat(GODT-3121): reuse InfoTooltip. 2023-12-07 09:35:05 +01:00
e5db9b1ccc feat(GODT-3121): added display of bug report user input in bridge-gui-tester. 2023-12-07 09:35:05 +01:00
6e2e622a2f feat(GODT-3121): added tooltip for KB suggestions. 2023-12-07 09:35:05 +01:00
3a66063938 feat(GODT-3121): change log level of click on external link. 2023-12-07 09:35:05 +01:00
120ddbbcbb feat(GODT-3121): finalize UI for KB suggestions. 2023-12-07 09:35:05 +01:00
39b31abef8 feat(GODT-3121): fix issues reported by the resharper C++ engine. 2023-12-07 09:35:05 +01:00
ebeca394c7 feat(GODT-3121): implement suggestion list in bridge-gui. 2023-12-07 09:35:05 +01:00
2206cb3f12 feat(GODT-3121): suggestions links are in the final bug report page. 2023-12-07 09:35:05 +01:00
cfd07cf893 feat(GODT-3121): suggestions are transferred to QML. 2023-12-07 09:35:05 +01:00
2e2648fcd5 feat(GODT-3121): QML request suggestions. 2023-12-07 09:35:05 +01:00
3070912416 feat(GODT-3121): added gRPC call and event for KB suggestions. 2023-12-07 09:35:05 +01:00
51722eb1a4 feat(GODT-3121): introduced knowledgebase package. 2023-12-07 09:35:05 +01:00
5950eff083 chore(GODT-3160): silence vuln 2023-12-07 08:15:10 +01:00
5c67cc2e76 fix(GODT-3153): Do not take into account full address when hasing messages. 2023-12-06 16:14:38 +00:00
01db488caa feat(GODT-2001): add govulncheck to scan for vulnerabilities. 2023-12-06 15:29:21 +01:00
6cbef1d786 test: Improve TestMetadata_JobCorrectlyFinishesAfterCancel 2023-12-04 13:48:44 +00:00
f3aac09ecb chore: merge wakato release to master 2023-11-22 12:52:24 +01:00
38d692ebfb chore: merge wakato release to master 2023-11-14 11:32:39 +01:00
1acc7eb7db chore: merge release/vasco_da_gama to master 2023-11-03 17:10:42 +01:00
248fbf5e33 chore: Vasco da Gama Bridge 3.6.1 changelog. 2023-10-18 15:41:01 +02:00
8b12a454ea fix(GODT-3033): Unable to receive new mail
If the IMAP service happened to finish syncing and wanted to reset the
user event service at a time the latter was publishing an event a
deadlock would occur and the user would not receive any new messages.

This change puts the request to revert the event id in a separate
go-routine to avoid this situation from re-occurring. The operational
flow remains unchanged as the event service will only process this
request once the current set of events have been published.
2023-10-18 14:46:14 +02:00
310fcffc7b chore: merge release/vasco_da_gama to master 2023-10-17 11:54:05 +02:00
318ad16378 chore: merge Umshiang release to master 2023-10-13 08:40:01 +02:00
8be4246f7e chore: Vasco da Gama Bridge 3.6.0 changelog. 2023-10-11 16:09:55 +02:00
e580f89106 feat(GODT-3004): update gopenpgp and dependencies. 2023-10-11 15:29:52 +02:00
01043e033e chore: Umshiang Bridge 3.5.3 changelog. 2023-10-11 08:37:28 +02:00
94b44b383a feat(GODT-3004): update gopenpgp and dependencies. 2023-10-11 08:26:58 +02:00
a3b8fabb26 chore: merge Umshiang to master 2023-10-10 13:46:07 +02:00
275b30e518 chore: Vasco da Gama Bridge 3.6.0 changelog. 2023-10-10 11:29:36 +02:00
bf244e5c86 fix(GODT-3003): Ensure IMAP State is reset after vault corruption
After we detect that the user has suffered the GODT-3003 bug due the
vault corruption not ensuring that a previous sync state would be
erased, we patch the gluon db directly and then reset the sync state.

After the account is added, the sync is automatically triggered and the
account state fixes itself.
2023-10-10 11:24:06 +02:00
cf9651bb94 fix(GODT-3001): Only create system labels during system label sync 2023-10-10 11:23:32 +02:00
ba65ffdbc7 chore: Umshiang Bridge 3.5.2 changelog. 2023-10-10 11:22:41 +02:00
4b95ef4d82 chore: Umshiang Bridge 3.5.2 changelog. 2023-10-09 13:25:44 +02:00
951c7c27fb fix(GODT-3003): Ensure IMAP State is reset after vault corruption
After we detect that the user has suffered the GODT-3003 bug due the
vault corruption not ensuring that a previous sync state would be
erased, we patch the gluon db directly and then reset the sync state.

After the account is added, the sync is automatically triggered and the
account state fixes itself.
2023-10-09 11:19:36 +01:00
e7423a9519 fix(GODT-3001): Only create system labels during system label sync 2023-10-09 11:05:59 +01:00
d3582fa981 chore: Vasco da Gama Bridge 3.6.0 changelog. 2023-10-03 16:43:33 +02:00
80c852a5b2 fix(GODT-2992): fix link in 'no account view' in main window after 2FA or TOTP are cancelled.
(cherry picked from commit 1c344211d1)
2023-10-03 11:08:52 +02:00
51498e3e37 chore: merge master with release/umshiang 2023-09-28 14:19:45 +02:00
b7ef6e1486 chore: Umshiang Bridge 3.5.1 changelog. 2023-09-27 13:18:23 +02:00
0d03f84711 fix(GODT-2963): Use multi error to report file removal errors
Do not abort removing files on first error. Collect errors and try to
remove as many as possible. This would cause some state files to not be
removed on windows.
2023-09-27 12:34:07 +02:00
949666724d chore: Umshiang Bridge 3.5.1 changelog. 2023-09-27 10:54:50 +02:00
bbe19bf960 fix(GODT-2956): Restore old deletion rules
When unlabeling a message from trash we have to check if this message is
present in another folder before perma-deleting.
2023-09-26 14:06:31 +02:00
bfe25e3a46 fix(GODT-2951): Negative WaitGroup Counter
Do not defer call to `wg.Done()` in `job.onJobFinished`. If there is an
error it will also call `wg.Done()`.
2023-09-26 13:58:46 +02:00
236c958703 fix(GODT-2590): Fix send on closed channel
Ensure periodic user tasks are terminated before the other user
services. The panic triggered due to the fact that the telemetry service
was shutdown before this periodic task.
2023-09-26 13:58:18 +02:00
e6b312b437 fix(GODT-2949): Fix close of close channel in event service
This issue is triggered due to the `Service.Close()` call after the
go-routine for the event service exists. It is possible that during this
period a recently added subscriber with `pendingOpAdd` gets cancelled
and closed.

However, the subscriber later also enqueues a `pendingOpRemove` which
gets processed again with a call in `user.eventService.Close()` leading
to the double close panic.

This patch simply removes the `s.Close()` from the service, and leaves
the cleanup to called externally from user.Close() or user.Logout().
2023-09-26 13:58:07 +02:00
384154c767 chore: merge 'trift' into umshiang 2023-09-14 14:48:03 +02:00
45d2e9ea63 chore: update changelog. 2023-09-13 10:25:47 +02:00
86e8a566c7 chore: Umshiang Bridge 3.5.0 changelog. 2023-09-12 07:45:08 +02:00
a80fd92018 chore: Trift Bridge 3.4.2 changelog. 2023-09-01 15:12:34 +02:00
71063ac5ee fix(GODT-2902): do not check for changed values. Related to GODT-2857. 2023-09-01 14:44:27 +02:00
781 changed files with 32718 additions and 8200 deletions

View File

@ -1,41 +0,0 @@
---
name: General issue template
about: Template for detailed report of issues
title: ''
labels: ''
assignees: ''
---
Issue tracker is ONLY used for reporting bugs with technical details. "It doesn't work" or new features should be discussed with our customer support. Please use bug report function in Bridge or contact bridge@protonmail.ch.
<!--- Provide a general summary of the issue in the Title above -->
## Expected Behavior
<!--- Tell us what should happen -->
## Current Behavior
<!--- Tell us what happens instead of the expected behavior -->
## Possible Solution
<!--- Not obligatory, but suggest a fix/reason for the bug, -->
## Steps to Reproduce
<!--- Provide a link to a live example, or an unambiguous set of steps to -->
<!--- reproduce this bug. Include code to reproduce, if relevant -->
1.
2.
3.
4.
## Version Information
<!--- Which version of the app(s) were you using when you experienced this issue? -->
## Context (Environment)
<!--- How has this issue affected you? What are you trying to accomplish? -->
<!--- Providing context helps us come up with a solution that is most useful in the real world -->
## Detailed Description
<!--- Provide a detailed description of the change or addition you are proposing -->
## Possible Implementation
<!--- Not obligatory, but suggest an idea for implementing addition or change -->

3
.gitignore vendored
View File

@ -7,6 +7,7 @@
*~ *~
.idea .idea
.vscode .vscode
.vs
# Test files # Test files
godog.test godog.test
@ -35,6 +36,8 @@ cmd/Import-Export/deploy
proton-bridge proton-bridge
cmd/Desktop-Bridge/*.exe cmd/Desktop-Bridge/*.exe
cmd/launcher/*.exe cmd/launcher/*.exe
bin/
obj/
# Jetbrains (CLion, Golang) cmake build dirs # Jetbrains (CLion, Golang) cmake build dirs
cmake-build-*/ cmake-build-*/

View File

@ -16,8 +16,6 @@
# along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>. # along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
--- ---
image: gitlab.protontech.ch:4567/go/bridge-internal:test-go1.20
default: default:
tags: tags:
- shared-small - shared-small
@ -27,16 +25,26 @@ variables:
GOMAXPROCS: $(( ${CI_TAG_CPU} / 2 )) GOMAXPROCS: $(( ${CI_TAG_CPU} / 2 ))
before_script: before_script:
- apt update && apt-get -y install libsecret-1-dev - |
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST} if [ "$CI_JOB_NAME" != "grype-scan-code-dependencies" ]; then
apt update && apt-get -y install libsecret-1-dev
git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
fi
stages: stages:
- analyse
- test - test
- report
- build - build
include: include:
- local: ci/setup.yml
- local: ci/rules.yml - local: ci/rules.yml
- local: ci/env.yml - local: ci/env.yml
- local: ci/test.yml - local: ci/test.yml
- local: ci/report.yml
- local: ci/build.yml - local: ci/build.yml
- component: gitlab.protontech.ch/proton/devops/cicd-components/kits/devsecops/go@~latest
inputs:
stage: analyse

1
.gitlab/CODEOWNERS Normal file
View File

@ -0,0 +1 @@
* inbox-desktop-approvers

View File

@ -2,11 +2,12 @@
run: run:
timeout: 10m timeout: 10m
skip-dirs: skip-dirs:
- pkg/mime
- extern
issues: issues:
exclude-use-default: false exclude-use-default: false
exclude-dirs:
- pkg/mime
- extern
exclude: exclude:
- Using the variable on range scope `tt` in function literal - Using the variable on range scope `tt` in function literal
# For now we are missing a lot of comments. # For now we are missing a lot of comments.
@ -86,7 +87,7 @@ linters:
- asciicheck # Simple linter to check that your code does not contain non-ASCII identifiers [fast: true, auto-fix: false] - asciicheck # Simple linter to check that your code does not contain non-ASCII identifiers [fast: true, auto-fix: false]
- durationcheck # check for two durations multiplied together [fast: false, auto-fix: false] - durationcheck # check for two durations multiplied together [fast: false, auto-fix: false]
- exhaustive # check exhaustiveness of enum switch statements [fast: false, auto-fix: false] - exhaustive # check exhaustiveness of enum switch statements [fast: false, auto-fix: false]
- exportloopref # checks for pointers to enclosing loop variables [fast: false, auto-fix: false] - copyloopvar # detects places where loop variables are copied.
- forcetypeassert # finds forced type assertions [fast: true, auto-fix: false] - forcetypeassert # finds forced type assertions [fast: true, auto-fix: false]
- godot # Check if comments end in a period [fast: true, auto-fix: true] - godot # Check if comments end in a period [fast: true, auto-fix: true]
- goheader # Checks is file header matches to pattern [fast: true, auto-fix: false] - goheader # Checks is file header matches to pattern [fast: true, auto-fix: false]

2
.grype.yaml Normal file
View File

@ -0,0 +1,2 @@
# Check out for configuration details: https://github.com/anchore/grype?tab=readme-ov-file#configuration
fail-on-severity: "medium"

View File

@ -3,14 +3,14 @@
## Prerequisites ## Prerequisites
* 64-bit OS: * 64-bit OS:
- 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.20 * Go 1.24.0
* 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)
* pkg-config (Linux) * pkg-config (Linux)
* cmake, ninja-build and Qt 6.4.3 are required to build the graphical user interface. On Linux, * cmake, ninja-build and Qt 6.8.2 are required to build the graphical user interface. On Linux,
the Mesa OpenGL development files are also needed. the Mesa OpenGL development files are also needed.
To enable the sending of crash reports using Sentry please set the To enable the sending of crash reports using Sentry please set the
@ -19,7 +19,7 @@ Otherwise, the sending of crash reports will be disabled.
## Build ## Build
In order to build Bridge app with Qt interface we are using In order to build Bridge app with Qt interface we are using
[Qt 6.4.3](https://doc.qt.io/qt-6/gettingstarted.html). [Qt 6.8.2](https://doc.qt.io/qt-6/gettingstarted.html).
Please note that qmake path must be in your `PATH` to ensure Qt to be found. Please note that qmake path must be in your `PATH` to ensure Qt to be found.
Also, before you start build **on Windows**, please unset the `MSYSTEM` variable Also, before you start build **on Windows**, please unset the `MSYSTEM` variable

View File

@ -1,10 +1,78 @@
# Contribution Policy # Contributing guidelines
The following document describes how to contribute to the project. In this context, contribution does not only mean code contribution but also reporting issues, requesting new features, or just asking for help.
## Reporting issues
In case you experience issues while using the application, our request is to contact Proton customer support directly.
The benefits of using Proton customer support are
- Available 24/7/365.
- Provides priority support based on subscription type.
- Will escalate the issue to the developers every time it becomes too technical or they do not know the answer to a question.
- Easier to detect systematic issues by connecting similar reports.
- Possible to quickly derive frequency of an issue.
- Can assist you to transfer sensitive information safely to us.
To speed up the communication with customer support, consider the following:
- Whenever is possible, use the in-app bug report feature. It provides an application specific guide compared to using the generic report form on web.
- Whenever is possible, proactively attach logs to your report. Reporting an issue from the application can help you in that.
- Check whether your system is officially supported by Proton, including the source of the installer. We cannot provide help when the application is packaged by a third party or when the application is used on systems that we do not prepare to support.
- If your report is a feature request, see the Feature request section. In case it is an issue related to application security, see the Security vulnerabilities section.
In the past, we used GitHub issue tracker for more technical issues in parallel to Proton customer support, but we run into limitations with this approach:
- Monitoring GitHub issue tracker took development time as it was managed by the development team.
- It made issue frequency tracking challenging because we did not have a single point of entry for issues.
- Users were confused what technical issue means, and used the GitHub issue tracker for feature requests, or non-technical discussions.
- Users sometimes shared sensitive data through the GitHub issue tracker.
For the above reasons, we do not use GitHub issue tracker anymore but ask you to contact our customer support in case you run into a problem.
### Security vulnerabilities
Proton runs a bug bounty program for security vulnerabilities. They differ from normal bug reports in the following ways:
- These reports go directly to our security team.
- They expect deeper explanation of the issue.
- Depending on the finding, they may be financially rewarded.
More information about the program can be found [here](https://proton.me/security/bug-bounty).
## Feature requests
What someone considers as a bug is sometimes a feature, and sometimes, a missing feature is considered as a bug. Instead of reporting feature requests as bugs, we setup a UserVoice page to allow our users to share their preferences. UserVoice also makes it possible to vote on other feature requests, making the community preference public.
Our product team frequently monitors UserVoice, and the features listed there are taken into account in our planning.
Examples for UserVoice requests:
- Extending the officially supported environments (e.g., operating systems, clients, or computer architectures).
- Requesting new features.
- Integration with non-Proton services.
UserVoice is available [here](https://protonmail.uservoice.com/).
## Asking for help
The best ways to get answer for generic questions or to get help with setting up the system is to interact with our active community on [Reddit](https://reddit.com/r/ProtonMail/) or to contact customer support.
## Code contribution
We are grateful if you can contribute directly with code. In that case there is nothing else to do than to open a pull request.
The following is worthwhile noting
- The project is primarily developed on an internal repository, and the one on GitHub is only a mirror of it. For that reason, the merge request will not be merged on GitHub but added to the project internally. We are keeping the original author in the change set to respect the contribution.
- The application is used on numerous platforms and by many third party clients. To have higher chance your change to be accepted, consider all supported dependencies.
- Give detailed description of the issue, preferably with test steps to reproduce the original issue, and to verify the fix. It is even better if you also extend the automated tests.
### Contribution policy
By making a contribution to this project: By making a contribution to this project:
1. I assign any and all copyright related to the contribution to Proton AG; 1. You assign any and all copyright related to the contribution to Proton AG;
2. I certify that the contribution was created in whole by me; 2. You certify that the contribution was created in whole by you;
3. I understand and agree that this project and the contribution are public 3. You understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information you submit with it) is maintained indefinitely and may be redistributed with this project or the open source license(s) involved.
and that a record of the contribution (including all personal information I
submit with it) is maintained indefinitely and may be redistributed with
this project or the open source license(s) involved.

View File

@ -63,11 +63,15 @@ Proton Mail Bridge includes the following 3rd party software:
* [goleak](https://go.uber.org/goleak) available under [license](https://pkg.go.dev/go.uber.org/goleak?tab=licenses) * [goleak](https://go.uber.org/goleak) available under [license](https://pkg.go.dev/go.uber.org/goleak?tab=licenses)
* [exp](https://golang.org/x/exp) available under [license](https://cs.opensource.google/go/x/exp/+/master:LICENSE) * [exp](https://golang.org/x/exp) available under [license](https://cs.opensource.google/go/x/exp/+/master:LICENSE)
* [net](https://golang.org/x/net) available under [license](https://cs.opensource.google/go/x/net/+/master:LICENSE) * [net](https://golang.org/x/net) available under [license](https://cs.opensource.google/go/x/net/+/master:LICENSE)
* [oauth2](https://golang.org/x/oauth2) available under [license](https://cs.opensource.google/go/x/oauth2/+/master:LICENSE)
* [sys](https://golang.org/x/sys) available under [license](https://cs.opensource.google/go/x/sys/+/master:LICENSE) * [sys](https://golang.org/x/sys) available under [license](https://cs.opensource.google/go/x/sys/+/master:LICENSE)
* [text](https://golang.org/x/text) available under [license](https://cs.opensource.google/go/x/text/+/master:LICENSE) * [text](https://golang.org/x/text) available under [license](https://cs.opensource.google/go/x/text/+/master:LICENSE)
* [api](https://google.golang.org/api) available under [license](https://pkg.go.dev/google.golang.org/api?tab=licenses)
* [grpc](https://google.golang.org/grpc) available under [license](https://github.com/grpc/grpc-go/blob/master/LICENSE) * [grpc](https://google.golang.org/grpc) available under [license](https://github.com/grpc/grpc-go/blob/master/LICENSE)
* [protobuf](https://google.golang.org/protobuf) available under [license](https://github.com/protocolbuffers/protobuf/blob/main/LICENSE) * [protobuf](https://google.golang.org/protobuf) available under [license](https://github.com/protocolbuffers/protobuf/blob/main/LICENSE)
* [plist](https://howett.net/plist) available under [license](https://github.com/DHowett/go-plist/blob/main/LICENSE) * [plist](https://howett.net/plist) available under [license](https://github.com/DHowett/go-plist/blob/main/LICENSE)
* [compute](https://cloud.google.com/go/compute) available under [license](https://pkg.go.dev/cloud.google.com/go/compute?tab=licenses)
* [metadata](https://cloud.google.com/go/compute/metadata) available under [license](https://pkg.go.dev/cloud.google.com/go/compute/metadata?tab=licenses)
* [bcrypt](https://github.com/ProtonMail/bcrypt) available under [license](https://github.com/ProtonMail/bcrypt/blob/master/LICENSE) * [bcrypt](https://github.com/ProtonMail/bcrypt) available under [license](https://github.com/ProtonMail/bcrypt/blob/master/LICENSE)
* [go-crypto](https://github.com/ProtonMail/go-crypto) available under [license](https://github.com/ProtonMail/go-crypto/blob/master/LICENSE) * [go-crypto](https://github.com/ProtonMail/go-crypto) available under [license](https://github.com/ProtonMail/go-crypto/blob/master/LICENSE)
* [go-mime](https://github.com/ProtonMail/go-mime) available under [license](https://github.com/ProtonMail/go-mime/blob/master/LICENSE) * [go-mime](https://github.com/ProtonMail/go-mime) available under [license](https://github.com/ProtonMail/go-mime/blob/master/LICENSE)
@ -95,8 +99,11 @@ Proton Mail Bridge includes the following 3rd party software:
* [validator](https://github.com/go-playground/validator/v10) available under [license](https://github.com/go-playground/validator/v10/blob/master/LICENSE) * [validator](https://github.com/go-playground/validator/v10) available under [license](https://github.com/go-playground/validator/v10/blob/master/LICENSE)
* [go-json](https://github.com/goccy/go-json) available under [license](https://github.com/goccy/go-json/blob/master/LICENSE) * [go-json](https://github.com/goccy/go-json) available under [license](https://github.com/goccy/go-json/blob/master/LICENSE)
* [uuid](https://github.com/gofrs/uuid) available under [license](https://github.com/gofrs/uuid/blob/master/LICENSE) * [uuid](https://github.com/gofrs/uuid) available under [license](https://github.com/gofrs/uuid/blob/master/LICENSE)
* [groupcache](https://github.com/golang/groupcache) available under [license](https://github.com/golang/groupcache/blob/master/LICENSE)
* [protobuf](https://github.com/golang/protobuf) available under [license](https://github.com/golang/protobuf/blob/master/LICENSE) * [protobuf](https://github.com/golang/protobuf) available under [license](https://github.com/golang/protobuf/blob/master/LICENSE)
* [pprof](https://github.com/google/pprof) available under [license](https://github.com/google/pprof/blob/master/LICENSE) * [pprof](https://github.com/google/pprof) available under [license](https://github.com/google/pprof/blob/master/LICENSE)
* [enterprise-certificate-proxy](https://github.com/googleapis/enterprise-certificate-proxy) available under [license](https://github.com/googleapis/enterprise-certificate-proxy/blob/master/LICENSE)
* [gax-go](https://github.com/googleapis/gax-go/v2) available under [license](https://github.com/googleapis/gax-go/v2/blob/master/LICENSE)
* [errwrap](https://github.com/hashicorp/errwrap) available under [license](https://github.com/hashicorp/errwrap/blob/master/LICENSE) * [errwrap](https://github.com/hashicorp/errwrap) available under [license](https://github.com/hashicorp/errwrap/blob/master/LICENSE)
* [go-immutable-radix](https://github.com/hashicorp/go-immutable-radix) available under [license](https://github.com/hashicorp/go-immutable-radix/blob/master/LICENSE) * [go-immutable-radix](https://github.com/hashicorp/go-immutable-radix) available under [license](https://github.com/hashicorp/go-immutable-radix/blob/master/LICENSE)
* [go-memdb](https://github.com/hashicorp/go-memdb) available under [license](https://github.com/hashicorp/go-memdb/blob/master/LICENSE) * [go-memdb](https://github.com/hashicorp/go-memdb) available under [license](https://github.com/hashicorp/go-memdb/blob/master/LICENSE)
@ -120,19 +127,22 @@ Proton Mail Bridge includes the following 3rd party software:
* [blackfriday](https://github.com/russross/blackfriday/v2) available under [license](https://github.com/russross/blackfriday/v2/blob/master/LICENSE) * [blackfriday](https://github.com/russross/blackfriday/v2) available under [license](https://github.com/russross/blackfriday/v2/blob/master/LICENSE)
* [pflag](https://github.com/spf13/pflag) available under [license](https://github.com/spf13/pflag/blob/master/LICENSE) * [pflag](https://github.com/spf13/pflag) available under [license](https://github.com/spf13/pflag/blob/master/LICENSE)
* [bom](https://github.com/ssor/bom) available under [license](https://github.com/ssor/bom/blob/master/LICENSE) * [bom](https://github.com/ssor/bom) available under [license](https://github.com/ssor/bom/blob/master/LICENSE)
* [objx](https://github.com/stretchr/objx) available under [license](https://github.com/stretchr/objx/blob/master/LICENSE)
* [golang-asm](https://github.com/twitchyliquid64/golang-asm) available under [license](https://github.com/twitchyliquid64/golang-asm/blob/master/LICENSE) * [golang-asm](https://github.com/twitchyliquid64/golang-asm) available under [license](https://github.com/twitchyliquid64/golang-asm/blob/master/LICENSE)
* [codec](https://github.com/ugorji/go/codec) available under [license](https://github.com/ugorji/go/codec/blob/master/LICENSE) * [codec](https://github.com/ugorji/go/codec) available under [license](https://github.com/ugorji/go/codec/blob/master/LICENSE)
* [tagparser](https://github.com/vmihailenco/tagparser/v2) available under [license](https://github.com/vmihailenco/tagparser/v2/blob/master/LICENSE) * [tagparser](https://github.com/vmihailenco/tagparser/v2) available under [license](https://github.com/vmihailenco/tagparser/v2/blob/master/LICENSE)
* [smetrics](https://github.com/xrash/smetrics) available under [license](https://github.com/xrash/smetrics/blob/master/LICENSE) * [smetrics](https://github.com/xrash/smetrics) available under [license](https://github.com/xrash/smetrics/blob/master/LICENSE)
* [go-ordered-json](https://gitlab.com/c0b/go-ordered-json) * [go-ordered-json](https://gitlab.com/c0b/go-ordered-json) available under [license](https://gitlab.com/c0b/go-ordered-json/blob/master/LICENSE)
* [go.opencensus.io](https://pkg.go.dev/go.opencensus.io?tab=licenses) available under [license](https://pkg.go.dev/go.opencensus.io?tab=licenses)
* [arch](https://golang.org/x/arch) available under [license](https://cs.opensource.google/go/x/arch/+/master:LICENSE) * [arch](https://golang.org/x/arch) available under [license](https://cs.opensource.google/go/x/arch/+/master:LICENSE)
* [crypto](https://golang.org/x/crypto) available under [license](https://cs.opensource.google/go/x/crypto/+/master:LICENSE) * [crypto](https://golang.org/x/crypto) available under [license](https://cs.opensource.google/go/x/crypto/+/master:LICENSE)
* [mod](https://golang.org/x/mod) available under [license](https://cs.opensource.google/go/x/mod/+/master:LICENSE) * [mod](https://golang.org/x/mod) available under [license](https://cs.opensource.google/go/x/mod/+/master:LICENSE)
* [sync](https://golang.org/x/sync) available under [license](https://cs.opensource.google/go/x/sync/+/master:LICENSE) * [sync](https://golang.org/x/sync) available under [license](https://cs.opensource.google/go/x/sync/+/master:LICENSE)
* [tools](https://golang.org/x/tools) available under [license](https://cs.opensource.google/go/x/tools/+/master:LICENSE) * [tools](https://golang.org/x/tools) available under [license](https://cs.opensource.google/go/x/tools/+/master:LICENSE)
* [appengine](https://google.golang.org/appengine) available under [license](https://pkg.go.dev/google.golang.org/appengine?tab=licenses)
* [genproto](https://google.golang.org/genproto) available under [license](https://pkg.go.dev/google.golang.org/genproto?tab=licenses) * [genproto](https://google.golang.org/genproto) available under [license](https://pkg.go.dev/google.golang.org/genproto?tab=licenses)
* [yaml](https://gopkg.in/yaml.v3) available under [license](https://github.com/go-yaml/yaml/blob/v3.0.1/LICENSE) * [yaml](https://gopkg.in/yaml.v3) available under [license](https://github.com/go-yaml/yaml/blob/v3.0.1/LICENSE) available under [license](https://github.com/go-yaml/yaml/blob/v3.0.1/LICENSE)
* [docker-credential-helpers](https://github.com/ProtonMail/docker-credential-helpers) available under [license](https://github.com/ProtonMail/docker-credential-helpers/blob/master/LICENSE) * [go-autostart](https://github.com/ElectroNafta/go-autostart) available under [license](https://github.com/ElectroNafta/go-autostart/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-smtp](https://github.com/ProtonMail/go-smtp) available under [license](https://github.com/ProtonMail/go-smtp/blob/master/LICENSE) * [go-smtp](https://github.com/ProtonMail/go-smtp) available under [license](https://github.com/ProtonMail/go-smtp/blob/master/LICENSE)
* [resty](https://github.com/LBeernaertProton/resty/v2) available under [license](https://github.com/LBeernaertProton/resty/v2/blob/master/LICENSE) * [resty](https://github.com/LBeernaertProton/resty/v2) available under [license](https://github.com/LBeernaertProton/resty/v2/blob/master/LICENSE)

View File

@ -3,6 +3,297 @@
Changelog [format](http://keepachangelog.com/en/1.0.0/) Changelog [format](http://keepachangelog.com/en/1.0.0/)
## Kanmon Bridge 3.21.2
### Fixed
* BRIDGE-406: Fixed faulty certificate chain validation logic. Made certificate pin checks exclusive to leaf certificates.
## Kanmon Bridge 3.21.1
### Changed
* BRIDGE-383: Extended internal mailbox conflict resolution logic and minor changes to the mailbox conflict pre-checker.
## Kanmon Bridge 3.21.0
### Added
* BRIDGE-379: Mailbox pre-check on Bridge startup & conflict resolver for Bridge internal mailboxes.
### Changed
* BRIDGE-376: Explicitly catch Gluon DB mailbox name conflicts and report them to Sentry.
* BRIDGE-373: Extend user mailbox conflict resolver logging & report sync errors to Sentry.
* BRIDGE-366: Kill switch support for IMAP IDLE.
* BRIDGE-363: Observability metric support for IMAP connections.
### Fixed
* BRIDGE-377: Correct API label field usage on user label conflict resolver - update handler (event loop).
* BRIDGE-378: Fix incorrect field usage for system mailbox names.
## Jubilee Bridge 3.20.1
### Fixed
* BRIDGE-362: Implemented logic for reconciling label conflicts.
## Jubilee Bridge 3.20.0
### Added
* BRIDGE-348: Enable display of BYOE addresses in Bridge.
* BRIDGE-340: Added additional logging for label operations and related bad events.
* BRIDGE-324: Log a hash of the vault key on Bridge start.
### Changed
* BRIDGE-352: Chore: bump go to 1.24.2.
* BRIDGE-353: Chore: update x/net package to 0.38.0.
### Fixed
* BRIDGE-351: Allow draft creation and import to BYOE addresses in combined mode.
* BRIDGE-301: Prevent imports into non-BYOE external addresses.
* BRIDGE-341: Replaced go-autostart with a fork to support creating autostart shortcuts in directories with Unicode characters on Windows.
* BRIDGE-332: Strip newline characters from username and password fields in the Bridge GUI.
* BRIDGE-336: Ensure all remote labels are verified and created in Gluon at Bridge startup.
* BRIDGE-335: Persist the last successfully used keychain helper as a user preference on Linux.
* BRIDGE-333: Ignore unknown label IDs during Bridge synchronization.
## Infinity Bridge 3.19.0
### Changed
* BRIDGE-316: Update Qt to latest LTS version 6.8.2.
## Helix Bridge 3.18.0
### Changed
* BRIDGE-309: Revised update logic and structure.
* BRIDGE-154: Added access token to expiry refresh request.
## Grunwald Bridge 3.17.0
### Added
* BRIDGE-271: Report version file check failure to Sentry.
* BRIDGE-247: Test: Automate Bridge 0% update rollout.
* BRIDGE-248: Test: Additional Bridge UI e2e automation tests.
### Changed
* BRIDGE-73: Update goopenpgp.
* BRIDGE-287: Update x/net and x/crypto dependencies.
* BRIDGE-303: Update govulncheck to latest release.
* BRIDGE-226: Bump Go version to 1.23.4.
* BRIDGE-288: Extension to synchronization update handler, observability tweaks and gluon update.
### Fixed
* BRIDGE-291: Use correct field for user plan type.
* BRIDGE-143: Add missing QML component attribute, cut/paste disabled on read-only text areas.
## Flavien Bridge 3.16.0
### Added
* BRIDGE-205: Add support for the IMAP AUTHENTICATE command.
* BRIDGE-268: Add kill switch feature flag for the IMAP AUTHENTICATE command.
* BRIDGE-261: Delete gluon data during user deletion.
* BRIDGE-246: Test: Add Settings Menu Bridge UI e2e automation tests.
### Changed
* BRIDGE-107: Improved human verification UX.
* BRIDGE-281: Disable keychain test on macOS.
* BRIDGE-266: Heartbeat telemetry update.
* BRIDGE-253: Removed unused telemetry (activation and troubleshooting).
* BRIDGE-252: Restored the -h shortcut for the CLI --help switch.
* BRIDGE-264: Ignore apple notes as UserAgent.
### Fixed
* BRIDGE-256: Fix reversed order of headers with multiple values.
* BRIDGE-258: Fixed issue with draft updates and sending during synchronization.
## Erasmus Bridge 3.15.1
### Changed
* BRIDGE-281: Disable keychain test on macOS.
## Erasmus Bridge 3.15.0
### Added
* BRIDGE-238: Added host information to sentry events; new sentry event for keychain issues.
* BRIDGE-236: Added SMTP observability metrics.
* BRIDGE-217: Added missing parameter to the CLI help command.
* BRIDGE-234: Add accessibility name in QML for UI automation.
* BRIDGE-232: Test: Add Home Menu Bridge UI e2e automation tests.
* BRIDGE-220: Test: Add Bridge E2E UI login/logout tests for Windows.
### Changed
* BRIDGE-228: Removed sentry events.
* BRIDGE-218: Observability adapter; gluon observability metrics and tests.
* BRIDGE-215: Tweak wording on macOS profile install page.
* BRIDGE-131: Test: Integration tests for messages from Proton <-> Gmail.
* BRIDGE-142: Bridge icon can be removed from the menu bar on macOS.
### Fixed
* BRIDGE-240: Fix for running against Qt 6.8 (contribution of GitHub user Cimbali).
* BRIDGE-231: Fix reversed header order in messages.
* BRIDGE-235: Fix compilation of Bridge GUI Tester on Windows.
* BRIDGE-120: Use appropriate address key when importing / saving draft.
## Dragon Bridge 3.14.0
### Changed
* BRIDGE-207: Failure to download or verify an update now fails silently.
* BRIDGE-204: Removed redundant Sentry events.
* BRIDGE-150: Observability service modification.
* BRIDGE-210: Reduced log level of cache events so they won't be printed to stdout.
### Fixed
* BRIDGE-106: Fixed import of multipart-related messages.
* BRIDGE-108: Fixed GetInitials when empty username is passed.
## Colorado Bridge 3.13.0
### Added
* BRIDGE-37: added message broadcasting functionality.
* BRIDGE-122: added observability service.
* BRIDGE-119: added support for Feature Flags.
* BRIDGE-116: added command-line switches to enable/disable keychain check on macOS.
* BRIDGE-88: added context menu for quick actions on input labels: cut, copy, paste.
### Changed
* BRIDGE-81: KB article suggestion updates + more weight for long keywords.
### Fixed
* BRIDGE-67: Added detection for username changes on macOS & automatic reconfiguration.
* BRIDGE-138: Remove deprecated doc.
## Bastei Bridge 3.12.0
### Added
* BRIDGE-75: Bridge repair button.
* BRIDGE-79: Add New Outlook for Mac KB disclaimer.
### Changed
* BRIDGE-16: Bump version Go 1.21.9 Qt 6.4.3.
* BRIDGE-23: Update gluon to go 1.21.
* BRIDGE-22: Update gpa to go 1.21.
### Fixed
* BRIDGE-90: Disable repair button when bridge cannot connect to proton servers; bump GPA.
* BRIDGE-69: Explicitly handle semver panic for last bridge version from vault.
* BRIDGE-29: Bump gluon version.
* BRIDGE-49: Configure gitleaks baseline and grype config.
* BRIDGE-21: Missing panic handling.
* BRIDGE-17: Broken telemetry heartbeat test.
* BRIDGE-10: Bumped gluon version.
## Alcantara Bridge 3.11.1
### Fixed
* BRIDGE-70: Hotfix for blocked smtp/imap port causing bridge to quit.
## Alcantara Bridge 3.11.0
### Added
* GODT-3185: Report cases which leads to wrong address key used.
### Changed
* BRIDGE-14: HV3 implementation.
* BRIDGE-15: Certificate install is now also done during Outlook setup on macOS.
* GODT-3146: Start servers on startup, keep running even when no users are active.
* BRIDGE-19: Update checksum validation use warning instead of error on non-existing files.
### Fixed
* BRIDGE-8: Fix bridge double sessionID issue in logs.
* BRIDGE-7: Modify keychain test on macOS.
* BRIDGE-4: Logs not being created when invalid flag is passed.
* BRIDGE-5: Add tooltip to tray icon.
* GODT-3163: Filter MBOX format delimiter.
## Zaehringen Bridge 3.10.0
### Added
* GODT-3199: Add package log field.
* GODT-3220: Add more test scenarios.
### Changed
* GODT-3193: Preserve attachment encoding.
* GODT-3214: Encrypt only with primary key.
* GODT-2662: Use tart runner for darwin jobs.
* GODT-1602: Test: run integration tests against black 🖤.
* GODT-3257: Test: quad9 provider test not working on CI.
### Fixed
* GODT-3290: Fix test failing because of leap day.
## Ypsilon Bridge 3.9.1
### Fixed
* GODT-3235: Update bridge update key.
## Ypsilon Bridge 3.9.0
### Added
* GODT-3230: Scripts for removing Bridge from device.
* GODT-3195: Add OS info to the log.
* GODT-3156: Add time zone info to the bridge log.
* GODT-3162: Test: Add test scenarios for KB article suggestions.
* Test: Add scenarios for checking messages sent from Web Client.
* GODT-3162: Test: Add step definition for checking KB article suggestions.
### Changed
* GODT-3160: Bump version Go 1.21.6.
* GODT-3160: Load pipeline env from bridge internal.
* GODT-3052: Test: Replace attachments and inline content in feature tests with the smallest valid versions.
* GODT-3155: Customize log formatter for easier parsing.
* GODT-3172: Detect missing keychain item.
* GODT-3172: Do not list, just retrieve vault key.
* Log the message received time when handling message creation event.
* Set log as artefact for all integration test.
* Get better logging arround keychain list initialisation.
### Fixed
* GODT-3229: Escape reserved XML characters in Apple configuration profile.
* GODT-3228: Get rid of fork of docker-credential-helpers.
* GODT-3176: Assume inline if content id is present.
* GODT-3160: Ignore non-called vulnerabilities.
* GODT-3160: Updated external dependencies reported by govulncheck.
* GODT-3203: Crash in chunkDivide.
* Fix for SMTP connection mode toggle in bridge-gui-tester.
* GODT-3183: Fix database indices.
* GODT-3187: Fix numberOfDay computation when changing year and day.
* GODT-3188: Happy new year.
## Xikou Bridge 3.8.2
### Fixed
* GODT-3235: Update bridge update key.
## Xikou Bridge 3.8.1
### Added
* GODT-3121: Suggest relevant KB articles in the in-app bug report form.
* GODT-2001: Add govulncheck to scan for vulnerabilities.
### Changed
* Keep nighlty-job log as artifact.
* Test: Improve TestMetadata_JobCorrectlyFinishesAfterCancel.
### Fixed
* GODT-3153: Do not take into account full address when hasing messages.
## Xikou Bridge 3.8.0 ## Xikou Bridge 3.8.0
### Added ### Added

View File

@ -1,17 +1,18 @@
export GO111MODULE=on export GO111MODULE=on
export CGO_ENABLED=1
# By default, the target OS is the same as the host OS, # By default, the target OS is the same as the host OS,
# but this can be overridden by setting TARGET_OS to "windows"/"darwin"/"linux". # but this can be overridden by setting TARGET_OS to "windows"/"darwin"/"linux".
GOOS:=$(shell go env GOOS) GOOS:=$(shell go env GOOS)
TARGET_CMD?=Desktop-Bridge TARGET_CMD?=Desktop-Bridge
TARGET_OS?=${GOOS} TARGET_OS?=${GOOS}
ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) ROOT_DIR:=$(realpath .)
## Build ## Build
.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.8.0+git BRIDGE_APP_VERSION?=3.21.2+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
@ -19,8 +20,8 @@ SRC_ICO:=bridge.ico
SRC_ICNS:=Bridge.icns SRC_ICNS:=Bridge.icns
SRC_SVG:=bridge.svg SRC_SVG:=bridge.svg
EXE_NAME:=proton-bridge EXE_NAME:=proton-bridge
REVISION:=$(shell ./utils/get_revision.sh) REVISION:=$(shell "${ROOT_DIR}/utils/get_revision.sh" rev)
TAG:=$(shell ./utils/get_revision.sh tag) TAG:=$(shell "${ROOT_DIR}/utils/get_revision.sh" tag)
BUILD_TIME:=$(shell date +%FT%T%z) BUILD_TIME:=$(shell date +%FT%T%z)
MACOS_MIN_VERSION_ARM64=11.0 MACOS_MIN_VERSION_ARM64=11.0
MACOS_MIN_VERSION_AMD64=10.15 MACOS_MIN_VERSION_AMD64=10.15
@ -101,9 +102,9 @@ endif
ifeq "${GOOS}" "windows" ifeq "${GOOS}" "windows"
go-build-finalize= \ go-build-finalize= \
$(if $(4),powershell Copy-Item ${ROOT_DIR}/${RESOURCE_FILE} ${4} &&,) \ $(if $(4),cp "${ROOT_DIR}/${RESOURCE_FILE}" ${4} &&,) \
$(call go-build,$(1),$(2),$(3)) \ $(call go-build,$(1),$(2),$(3)) \
$(if $(4), && powershell Remove-Item ${4} -Force,) $(if $(4), && rm -f ${4},)
endif endif
${EXE_NAME}: gofiles ${RESOURCE_FILE} ${EXE_NAME}: gofiles ${RESOURCE_FILE}
@ -117,7 +118,10 @@ versioner:
go build ${BUILD_FLAGS} -o versioner utils/versioner/main.go go build ${BUILD_FLAGS} -o versioner utils/versioner/main.go
vault-editor: vault-editor:
$(call go-build-finalize,"-tags=debug","vault-editor","./utils/vault-editor/main.go") $(call go-build-finalize,-tags=debug,"vault-editor","./utils/vault-editor/main.go")
bridge-rollout:
$(call go-build-finalize,, "bridge-rollout","./utils/bridge-rollout/bridge-rollout.go")
hasher: hasher:
go build -o hasher utils/hasher/main.go go build -o hasher utils/hasher/main.go
@ -164,7 +168,7 @@ ${EXE_TARGET}: check-build-essentials ${EXE_NAME}
BRIDGE_BUILD_TIME=${BUILD_TIME} \ BRIDGE_BUILD_TIME=${BUILD_TIME} \
BRIDGE_GUI_BUILD_CONFIG=Release \ BRIDGE_GUI_BUILD_CONFIG=Release \
BRIDGE_BUILD_ENV=${BUILD_ENV} \ BRIDGE_BUILD_ENV=${BUILD_ENV} \
BRIDGE_INSTALL_PATH=${ROOT_DIR}/${DEPLOY_DIR}/${GOOS} \ BRIDGE_INSTALL_PATH="${ROOT_DIR}/${DEPLOY_DIR}/${GOOS}" \
./build.sh install ./build.sh install
mv "${ROOT_DIR}/${BRIDGE_EXE}" "$(ROOT_DIR)/${EXE_TARGET}" mv "${ROOT_DIR}/${BRIDGE_EXE}" "$(ROOT_DIR)/${EXE_TARGET}"
@ -185,7 +189,7 @@ ${RESOURCE_FILE}: ./dist/info.rc ./dist/${SRC_ICO} .FORCE
## Dev dependencies ## Dev dependencies
.PHONY: install-devel-tools install-linter install-go-mod-outdated install-git-hooks .PHONY: install-devel-tools install-linter install-go-mod-outdated install-git-hooks
LINTVER:="v1.52.2" LINTVER:="v1.64.6"
LINTSRC:="https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh" LINTSRC:="https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh"
install-dev-dependencies: install-devel-tools install-linter install-go-mod-outdated install-dev-dependencies: install-devel-tools install-linter install-go-mod-outdated
@ -260,7 +264,8 @@ test-integration-race: gofiles
test-integration-nightly: gofiles test-integration-nightly: gofiles
mkdir -p coverage/integration mkdir -p coverage/integration
go test \ gotestsum \
--junitfile tests/result/feature-tests.xml -- \
-v -timeout=90m -p=1 -count=1 -tags=test_integration \ -v -timeout=90m -p=1 -count=1 -tags=test_integration \
${GOCOVERAGE} \ ${GOCOVERAGE} \
github.com/ProtonMail/proton-bridge/v3/tests \ github.com/ProtonMail/proton-bridge/v3/tests \
@ -328,13 +333,6 @@ lint-bug-report:
lint-bug-report-preview: lint-bug-report-preview:
python3 utils/validate_bug_report_file.py --file "internal/frontend/bridge-gui/bridge-gui/qml/Resources/bug_report_flow.json" --preview python3 utils/validate_bug_report_file.py --file "internal/frontend/bridge-gui/bridge-gui/qml/Resources/bug_report_flow.json" --preview
gobinsec: gobinsec-cache.yml build
gobinsec -wait -cache -config utils/gobinsec_conf.yml ${EXE_TARGET} ${DEPLOY_DIR}/${TARGET_OS}/${LAUNCHER_EXE}
gobinsec-cache.yml:
./utils/gobinsec_update.sh
cp ./utils/gobinsec_update/gobinsec-cache-valid.yml ./gobinsec-cache.yml
updates: install-go-mod-outdated updates: install-go-mod-outdated
# Uncomment the "-ci" to fail the job if something can be updated. # Uncomment the "-ci" to fail the job if something can be updated.
go list -u -m -json all | go-mod-outdated -update -direct #-ci go list -u -m -json all | go-mod-outdated -update -direct #-ci

View File

@ -1,7 +1,7 @@
# Proton Mail Bridge and Import Export app # Proton Mail Bridge
Copyright (c) 2023 Proton AG Copyright (c) 2025 Proton AG
This repository holds the Proton Mail Bridge and the Proton Mail Import-Export applications. This repository holds the Proton Mail Bridge application.
For a detailed build information see [BUILDS](./BUILDS.md). For a detailed build information see [BUILDS](./BUILDS.md).
The license can be found in [LICENSE](./LICENSE) file, for more licensing information see [COPYING_NOTES](./COPYING_NOTES.md). The license can be found in [LICENSE](./LICENSE) file, for more licensing information see [COPYING_NOTES](./COPYING_NOTES.md).
For contribution policy see [CONTRIBUTING](./CONTRIBUTING.md). For contribution policy see [CONTRIBUTING](./CONTRIBUTING.md).
@ -13,7 +13,7 @@ Proton Mail Bridge for e-mail clients.
When launched, Bridge will initialize local IMAP/SMTP servers and render When launched, Bridge will initialize local IMAP/SMTP servers and render
its GUI. its GUI.
To configure an e-mail client, firstly log in using your Proton Mail credentials. To configure an e-mail client, first log in using your Proton Mail credentials.
Open your e-mail client and add a new account using the settings which are Open your e-mail client and add a new account using the settings which are
located in the Bridge GUI. The client will only be able to sync with located in the Bridge GUI. The client will only be able to sync with
your Proton Mail account when the Bridge is running, thus the option your Proton Mail account when the Bridge is running, thus the option
@ -24,10 +24,10 @@ background.
More details [on the public website](https://proton.me/mail/bridge). More details [on the public website](https://proton.me/mail/bridge).
## Launchers ## Launcher
Launchers are binaries used to run the Proton Mail Bridge or Import-Export apps. The launcher is a binary used to run the Proton Mail Bridge.
Official distributions of the Proton Mail Bridge and Import-Export apps contain The Official distribution of the Proton Mail Bridge application contains
both a launcher and the app itself. The launcher is installed in a protected both a launcher and the app itself. The launcher is installed in a protected
area of the system (i.e. an area accessible only with admin privileges) and is area of the system (i.e. an area accessible only with admin privileges) and is
used to run the app. The launcher ensures that nobody tampered with the app's used to run the app. The launcher ensures that nobody tampered with the app's
@ -37,7 +37,7 @@ feature enables the app to securely update itself automatically without asking
the user for a password. the user for a password.
## Keychain ## Keychain
You need to have a keychain in order to run the Proton Mail Bridge. On Mac or You need to have a keychain in order to run Proton Mail Bridge. On Mac or
Windows, Bridge uses native credential managers. On Linux, use `secret-service` freedesktop.org API Windows, Bridge uses native credential managers. On Linux, use `secret-service` freedesktop.org API
(e.g. [Gnome keyring](https://wiki.gnome.org/Projects/GnomeKeyring/)) (e.g. [Gnome keyring](https://wiki.gnome.org/Projects/GnomeKeyring/))
or or

View File

@ -1,4 +1,3 @@
--- ---
.script-build: .script-build:
@ -7,9 +6,14 @@
extends: extends:
- .rules-branch-and-MR-manual - .rules-branch-and-MR-manual
script: script:
- which go && go version
- which gcc && gcc --version
- which qmake && qmake --version
- git rev-parse --short=10 HEAD
- make build - make build
- git diff && git diff-index --quiet HEAD - git diff && git diff-index --quiet HEAD
- make vault-editor - make vault-editor
- make bridge-rollout
artifacts: artifacts:
expire_in: 1 day expire_in: 1 day
when: always when: always
@ -17,7 +21,7 @@
paths: paths:
- bridge_*.tgz - bridge_*.tgz
- vault-editor - vault-editor
- bridge-rollout
build-linux: build-linux:
extends: extends:
- .script-build - .script-build
@ -66,4 +70,3 @@ trigger-qa-installer:
trigger: trigger:
project: "jcuth/bridge-release" project: "jcuth/bridge-release"
branch: master branch: master

View File

@ -2,46 +2,45 @@
--- ---
.env-windows: .env-windows:
extends:
- .image-windows-virt-build
before_script: before_script:
- export BRIDGE_SYNC_FORCE_MINIMUM_SPEC=1 - !reference [.before-script-windows-virt-build, before_script]
- export GOROOT=/c/Go1.20/ - !reference [.before-script-git-config, before_script]
- export PATH=$GOROOT/bin:$PATH - mkdir -p .cache/bin
- export GOARCH=amd64 - export PATH=$(pwd)/.cache/bin:$PATH
- export GOPATH=~/go1.20 - export GOPATH="$CI_PROJECT_DIR/.cache"
- export GO111MODULE=on variables:
- export PATH="${GOPATH}/bin:${PATH}" GOARCH: amd64
- export MSYSTEM= BRIDGE_SYNC_FORCE_MINIMUM_SPEC: 1
- export QT6DIR=/c/grrrQt/6.4.3/msvc2019_64 VCPKG_DEFAULT_BINARY_CACHE: ${CI_PROJECT_DIR}/.cache
- export PATH=$PATH:${QT6DIR}/bin cache:
- export PATH="/c/Program Files/Microsoft Visual Studio/2022/Community/Common7/IDE/CommonExtensions/Microsoft/CMake/CMake/bin:$PATH" key: windows-vcpkg-go-0
- $(git config --global -l | grep -o 'url.*gitlab.protontech.ch.*insteadof' | xargs -L 1 git config --global --unset &> /dev/null) || echo "nothing to remove" paths:
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST} - .cache
- git config --global safe.directory '*' when: 'always'
- git status --porcelain
cache: {}
tags:
- windows-bridge
.env-darwin: .env-darwin:
extends:
- .image-darwin-build
before_script: before_script:
- export BRIDGE_SYNC_FORCE_MINIMUM_SPEC=1 - !reference [.before-script-darwin-tart-build, before_script]
- export PATH=/usr/local/bin:$PATH - !reference [.before-script-git-config, before_script]
- export PATH=/usr/local/opt/git/bin:$PATH - mkdir -p .cache/bin
- export PATH=/usr/local/opt/make/libexec/gnubin:$PATH - export PATH=$(pwd)/.cache/bin:$PATH
- export PATH=/usr/local/opt/gnu-sed/libexec/gnubin:$PATH - export GOPATH="$CI_PROJECT_DIR/.cache"
- export GOROOT=~/local/opt/go@1.20 variables:
- export PATH="${GOROOT}/bin:$PATH" BRIDGE_SYNC_FORCE_MINIMUM_SPEC: 1
- export GOPATH=~/go1.20 VCPKG_DEFAULT_BINARY_CACHE: ${CI_PROJECT_DIR}/.cache
- export PATH="${GOPATH}/bin:$PATH" cache:
- export QT6DIR=/opt/Qt/6.4.3/macos key: darwin-go-and-vcpkg
- export PATH="${QT6DIR}/bin:$PATH" paths:
- uname -a - .cache
cache: {} when: 'always'
tags:
- macos-m1-bridge
.env-linux-build: .env-linux-build:
image: gitlab.protontech.ch:4567/go/bridge-internal:build-go1.20-qt6.4.3 extends:
- .image-linux-build
variables: variables:
VCPKG_DEFAULT_BINARY_CACHE: ${CI_PROJECT_DIR}/.cache VCPKG_DEFAULT_BINARY_CACHE: ${CI_PROJECT_DIR}/.cache
cache: cache:
@ -50,13 +49,11 @@
- .cache - .cache
when: 'always' when: 'always'
before_script: before_script:
- mkdir -p .cache/bin
- export BRIDGE_SYNC_FORCE_MINIMUM_SPEC=1 - export BRIDGE_SYNC_FORCE_MINIMUM_SPEC=1
- !reference [.before-script-git-config, before_script]
- mkdir -p .cache/bin
- export PATH=$(pwd)/.cache/bin:$PATH - export PATH=$(pwd)/.cache/bin:$PATH
- export GOPATH="$CI_PROJECT_DIR/.cache" - export GOPATH="$CI_PROJECT_DIR/.cache"
- export PATH=$PATH:$QT6DIR/bin
- $(git config --global -l | grep -o 'url.*gitlab.protontech.ch.*insteadof' | xargs -L 1 git config --global --unset &> /dev/null) || echo "nothing to remove"
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
tags: tags:
- shared-large - shared-large

25
ci/report.yml Normal file
View File

@ -0,0 +1,25 @@
---
include:
- project: 'tpe/testmo-reporter'
ref: master
file: '/scenarios/testmo-script.yml'
testmo-upload:
stage: report
extends:
- .testmo-upload
- .rules-branch-manual-scheduled-and-test-branch-always
needs:
- test-integration-nightly
before_script: []
variables:
TESTMO_TOKEN: "$TESTMO_TOKEN"
TESTMO_URL: "https://proton.testmo.net"
PROJECT_ID: "9"
NAME: "Nightly integration tests"
MILESTONE: "Nightly integration tests"
SOURCE: "test-integration-nightly"
TAGS: "$CI_COMMIT_REF_SLUG"
RESULT_FOLDER: "tests/result/*.xml"

7
ci/setup.yml Normal file
View File

@ -0,0 +1,7 @@
---
include:
- project: 'go/bridge-internal'
ref: 'master'
file: 'ci/runners-setup.yml'

View File

@ -4,15 +4,17 @@
lint: lint:
stage: test stage: test
extends: extends:
- .image-linux-test
- .rules-branch-manual-br-tag-and-MR-and-devel-always - .rules-branch-manual-br-tag-and-MR-and-devel-always
script: script:
- make lint - make lint
tags: tags:
- shared-medium - shared-medium
bug-report-preview: lint-bug-report-preview:
stage: test stage: test
extends: extends:
- .image-linux-test
- .rules-branch-and-MR-manual - .rules-branch-and-MR-manual
script: script:
- make lint-bug-report-preview - make lint-bug-report-preview
@ -24,20 +26,36 @@ bug-report-preview:
extends: extends:
- .rules-branch-manual-MR-and-devel-always - .rules-branch-manual-MR-and-devel-always
script: script:
- which go && go version
- which gcc && gcc --version
- make test - make test
artifacts: artifacts:
paths: paths:
- coverage/** - coverage/**
test-linux: test-linux:
extends: extends:
- .image-linux-test
- .script-test - .script-test
tags: tags:
- shared-large - shared-large
test-windows:
extends:
- .env-windows
- .script-test
test-darwin:
extends:
- .env-darwin
- .script-test
fuzz-linux: fuzz-linux:
stage: test stage: test
extends: extends:
- .image-linux-test
- .rules-branch-manual-MR-and-devel-always - .rules-branch-manual-MR-and-devel-always
script: script:
- make fuzz - make fuzz
@ -55,14 +73,26 @@ test-integration:
extends: extends:
- test-linux - test-linux
script: script:
- make test-integration - make test-integration | tee -a integration-job.log
after_script:
- |
grep "Error: " integration-job.log
artifacts:
when: always
paths:
- integration-job.log
test-integration-race: test-integration-race:
extends: extends:
- test-integration - test-integration
- .rules-branch-and-MR-manual - .rules-branch-and-MR-manual
script: script:
- make test-integration-race - make test-integration-race | tee -a integration-race-job.log
artifacts:
when: always
paths:
- integration-race-job.log
test-integration-nightly: test-integration-nightly:
extends: extends:
@ -71,21 +101,20 @@ test-integration-nightly:
needs: needs:
- test-integration - test-integration
script: script:
- make test-integration-nightly - make test-integration-nightly | tee -a nightly-job.log
after_script:
test-windows: - |
extends: grep "Error: " nightly-job.log
- .env-windows artifacts:
- .script-test when: always
paths:
test-darwin: - tests/result/feature-tests.xml
extends: - nightly-job.log
- .env-darwin
- .script-test
test-coverage: test-coverage:
stage: test stage: test
extends: extends:
- .image-linux-test
- .rules-branch-manual-scheduled-and-test-branch-always - .rules-branch-manual-scheduled-and-test-branch-always
script: script:
- ./utils/coverage.sh - ./utils/coverage.sh
@ -107,3 +136,18 @@ test-coverage:
coverage_report: coverage_report:
coverage_format: cobertura coverage_format: cobertura
path: coverage.xml path: coverage.xml
go-vuln-check:
extends:
- .image-linux-test
- .rules-branch-manual-MR-and-devel-always
stage: test
tags:
- shared-medium
script:
- ./utils/govulncheck.sh
artifacts:
when: always
paths:
- vulns*

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //
@ -19,8 +19,15 @@ package main
import ( import (
"os" "os"
"runtime"
"strings" "strings"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
"github.com/ProtonMail/proton-bridge/v3/internal/sentry"
"github.com/sirupsen/logrus"
"github.com/ProtonMail/proton-bridge/v3/internal/app" "github.com/ProtonMail/proton-bridge/v3/internal/app"
"github.com/bradenaw/juniper/xslices" "github.com/bradenaw/juniper/xslices"
) )
@ -43,5 +50,72 @@ import (
*/ */
func main() { func main() {
_ = app.New().Run(xslices.Filter(os.Args, func(arg string) bool { return !strings.Contains(arg, "-psn_") })) appErr := app.New().Run(xslices.Filter(os.Args, func(arg string) bool { return !strings.Contains(arg, "-psn_") }))
if appErr != nil {
_ = app.WithLocations(func(l *locations.Locations) error {
logsPath, err := l.ProvideLogsPath()
if err != nil {
return err
}
// Get the session ID if its specified
var sessionID logging.SessionID
if flagVal, found := getFlagValue(os.Args, app.FlagSessionID); found {
sessionID = logging.SessionID(flagVal)
} else {
sessionID = logging.NewSessionID()
}
closer, err := logging.Init(
logsPath,
sessionID,
logging.BridgeShortAppName,
logging.DefaultMaxLogFileSize,
logging.DefaultPruningSize,
"",
)
if err != nil {
return err
}
defer func() {
_ = logging.Close(closer)
}()
logrus.
WithField("appName", constants.FullAppName).
WithField("version", constants.Version).
WithField("revision", constants.Revision).
WithField("tag", constants.Tag).
WithField("build", constants.BuildTime).
WithField("runtime", runtime.GOOS).
WithField("args", os.Args).
WithField("SentryID", sentry.GetProtectedHostname()).WithError(appErr).Error("Failed to initialize bridge")
return nil
})
}
}
// getFlagValue - obtains the value of a specified tag
// The flag can be of the following form `-flag value`, `--flag value`, `-flag=value` or `--flags=value`.
func getFlagValue(argList []string, flag string) (string, bool) {
eqPrefix1 := "-" + flag + "="
eqPrefix2 := "--" + flag + "="
for i := 0; i < len(argList); i++ {
arg := argList[i]
if strings.HasPrefix(arg, eqPrefix1) {
val := strings.TrimPrefix(arg, eqPrefix1)
return val, len(val) > 0
}
if strings.HasPrefix(arg, eqPrefix2) {
val := strings.TrimPrefix(arg, eqPrefix2)
return val, len(val) > 0
}
if (arg == "-"+flag || arg == "--"+flag) && i+1 < len(argList) {
return argList[i+1], true
}
}
return "", false
} }

View File

@ -0,0 +1,47 @@
// Copyright (c) 2025 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 main
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestGetFlagValue(t *testing.T) {
tests := []struct {
args []string
flag string
expected string
}{
{[]string{"session-id", ""}, "session-id", ""},
{[]string{"-session-id", ""}, "session-id", ""},
{[]string{"--session-id", ""}, "session-id", ""},
{[]string{"session-id", "test"}, "session-id", ""},
{[]string{"-session-id", "test"}, "session-id", "test"},
{[]string{"--session-id", "test"}, "session-id", "test"},
{[]string{"session-id=test"}, "session-id", ""},
{[]string{"-session-id=test"}, "session-id", "test"},
{[]string{"--session-id=test"}, "session-id", "test"},
}
for _, tt := range tests {
val, _ := getFlagValue(tt.args, tt.flag)
require.Equal(t, val, tt.expected)
}
}

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //
@ -40,6 +40,7 @@ import (
"github.com/elastic/go-sysinfo/types" "github.com/elastic/go-sysinfo/types"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"golang.org/x/exp/slices"
"golang.org/x/sys/execabs" "golang.org/x/sys/execabs"
) )
@ -53,9 +54,12 @@ const (
FlagCLIShort = "c" FlagCLIShort = "c"
FlagNonInteractive = "noninteractive" FlagNonInteractive = "noninteractive"
FlagNonInteractiveShort = "n" FlagNonInteractiveShort = "n"
FlagLauncher = "--launcher" FlagLauncher = "launcher"
FlagWait = "--wait" FlagWait = "wait"
FlagSessionID = "--session-id" FlagSessionID = "session-id"
HyphenatedFlagLauncher = "--" + FlagLauncher
HyphenatedFlagWait = "--" + FlagWait
HyphenatedFlagSessionID = "--" + FlagSessionID
) )
func main() { //nolint:funlen func main() { //nolint:funlen
@ -151,7 +155,7 @@ func main() { //nolint:funlen
} }
} }
cmd := execabs.Command(exe, appendLauncherPath(launcher, append(args, FlagSessionID, string(sessionID)))...) //nolint:gosec cmd := execabs.Command(exe, appendLauncherPath(launcher, appendOrModifySessionID(args, string(sessionID)))...) //nolint:gosec
cmd.Stdin = os.Stdin cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
@ -173,19 +177,14 @@ func main() { //nolint:funlen
// appendLauncherPath add launcher path if missing. // appendLauncherPath add launcher path if missing.
func appendLauncherPath(path string, args []string) []string { func appendLauncherPath(path string, args []string) []string {
if !sliceContains(args, FlagLauncher) { if !slices.Contains(args, HyphenatedFlagLauncher) {
res := append([]string{}, args...) res := append([]string{}, args...)
res = append(res, FlagLauncher, path) res = append(res, HyphenatedFlagLauncher, path)
return res return res
} }
return args return args
} }
// sliceContains checks if a value is present in a list.
func sliceContains[T comparable](list []T, s T) bool {
return xslices.Any(list, func(arg T) bool { return arg == s })
}
// inCLIMode detect if CLI mode is asked. // inCLIMode detect if CLI mode is asked.
func inCLIMode(args []string) bool { func inCLIMode(args []string) bool {
return hasFlag(args, FlagCLI) || hasFlag(args, FlagCLIShort) || hasFlag(args, FlagNonInteractive) || hasFlag(args, FlagNonInteractiveShort) return hasFlag(args, FlagCLI) || hasFlag(args, FlagCLIShort) || hasFlag(args, FlagNonInteractive) || hasFlag(args, FlagNonInteractiveShort)
@ -193,7 +192,12 @@ func inCLIMode(args []string) bool {
// hasFlag checks if a flag is present in a list. // hasFlag checks if a flag is present in a list.
func hasFlag(args []string, flag string) bool { func hasFlag(args []string, flag string) bool {
return xslices.Any(args, func(arg string) bool { return (arg == "-"+flag) || (arg == "--"+flag) }) return flagIndex(args, flag) >= 0
}
// flagIndex returns the position of the first occurrence of a flag int args, or -1 if the flag is not present.
func flagIndex(args []string, flag string) int {
return slices.IndexFunc(args, func(arg string) bool { return (arg == "-"+flag) || (arg == "--"+flag) })
} }
// findAndStrip check if a value is present in s list and remove all occurrences of the value from this list. // findAndStrip check if a value is present in s list and remove all occurrences of the value from this list.
@ -211,7 +215,7 @@ func findAndStripWait(args []string) ([]string, bool, []string) {
hasFlag := false hasFlag := false
values := make([]string, 0) values := make([]string, 0)
for k, v := range res { for k, v := range res {
if v != FlagWait { if v != HyphenatedFlagWait {
continue continue
} }
if k+1 >= len(res) { if k+1 >= len(res) {
@ -222,7 +226,7 @@ func findAndStripWait(args []string) ([]string, bool, []string) {
} }
if hasFlag { if hasFlag {
res, _ = findAndStrip(res, FlagWait) res, _ = findAndStrip(res, HyphenatedFlagWait)
for _, v := range values { for _, v := range values {
res, _ = findAndStrip(res, v) res, _ = findAndStrip(res, v)
} }
@ -230,6 +234,23 @@ func findAndStripWait(args []string) ([]string, bool, []string) {
return res, hasFlag, values return res, hasFlag, values
} }
// return args with the sessionID flag and value added or modified. The original slice is not modified.
func appendOrModifySessionID(args []string, sessionID string) []string {
index := flagIndex(args, FlagSessionID)
if index < 0 {
return append(args, HyphenatedFlagSessionID, sessionID)
}
if index == len(args)-1 {
return append(args, sessionID)
}
res := slices.Clone(args)
res[index+1] = sessionID
return res
}
func getPathToUpdatedExecutable( func getPathToUpdatedExecutable(
name string, name string,
ver *versioner.Versioner, ver *versioner.Versioner,

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //
@ -20,61 +20,62 @@ package main
import ( import (
"testing" "testing"
"github.com/bradenaw/juniper/xslices" "github.com/ProtonMail/proton-bridge/v3/internal/logging"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestSliceContains(t *testing.T) {
assert.True(t, sliceContains([]string{"a", "b", "c"}, "a"))
assert.True(t, sliceContains([]int{1, 2, 3}, 2))
assert.False(t, sliceContains([]string{"a", "b", "c"}, "A"))
assert.False(t, sliceContains([]int{1, 2, 3}, 4))
assert.False(t, sliceContains([]string{}, "a"))
assert.True(t, sliceContains([]string{"a", "a"}, "a"))
}
func TestFindAndStrip(t *testing.T) { func TestFindAndStrip(t *testing.T) {
list := []string{"a", "b", "c", "c", "b", "c"} list := []string{"a", "b", "c", "c", "b", "c"}
result, found := findAndStrip(list, "a") result, found := findAndStrip(list, "a")
assert.True(t, found) assert.True(t, found)
assert.True(t, xslices.Equal(result, []string{"b", "c", "c", "b", "c"})) assert.Equal(t, result, []string{"b", "c", "c", "b", "c"})
result, found = findAndStrip(list, "c") result, found = findAndStrip(list, "c")
assert.True(t, found) assert.True(t, found)
assert.True(t, xslices.Equal(result, []string{"a", "b", "b"})) assert.Equal(t, result, []string{"a", "b", "b"})
result, found = findAndStrip([]string{"c", "c", "c"}, "c") result, found = findAndStrip([]string{"c", "c", "c"}, "c")
assert.True(t, found) assert.True(t, found)
assert.True(t, xslices.Equal(result, []string{})) assert.Equal(t, result, []string{})
result, found = findAndStrip(list, "A") result, found = findAndStrip(list, "A")
assert.False(t, found) assert.False(t, found)
assert.True(t, xslices.Equal(result, list)) assert.Equal(t, result, list)
result, found = findAndStrip([]string{}, "a") result, found = findAndStrip([]string{}, "a")
assert.False(t, found) assert.False(t, found)
assert.True(t, xslices.Equal(result, []string{})) assert.Equal(t, result, []string{})
} }
func TestFindAndStripWait(t *testing.T) { func TestFindAndStripWait(t *testing.T) {
result, found, values := findAndStripWait([]string{"a", "b", "c"}) result, found, values := findAndStripWait([]string{"a", "b", "c"})
assert.False(t, found) assert.False(t, found)
assert.True(t, xslices.Equal(result, []string{"a", "b", "c"})) assert.Equal(t, result, []string{"a", "b", "c"})
assert.True(t, xslices.Equal(values, []string{})) assert.Equal(t, values, []string{})
result, found, values = findAndStripWait([]string{"a", "--wait", "b"}) result, found, values = findAndStripWait([]string{"a", "--wait", "b"})
assert.True(t, found) assert.True(t, found)
assert.True(t, xslices.Equal(result, []string{"a"})) assert.Equal(t, result, []string{"a"})
assert.True(t, xslices.Equal(values, []string{"b"})) assert.Equal(t, values, []string{"b"})
result, found, values = findAndStripWait([]string{"a", "--wait", "b", "--wait", "c"}) result, found, values = findAndStripWait([]string{"a", "--wait", "b", "--wait", "c"})
assert.True(t, found) assert.True(t, found)
assert.True(t, xslices.Equal(result, []string{"a"})) assert.Equal(t, result, []string{"a"})
assert.True(t, xslices.Equal(values, []string{"b", "c"})) assert.Equal(t, values, []string{"b", "c"})
result, found, values = findAndStripWait([]string{"a", "--wait", "b", "--wait", "c", "--wait", "d"}) result, found, values = findAndStripWait([]string{"a", "--wait", "b", "--wait", "c", "--wait", "d"})
assert.True(t, found) assert.True(t, found)
assert.True(t, xslices.Equal(result, []string{"a"})) assert.Equal(t, result, []string{"a"})
assert.True(t, xslices.Equal(values, []string{"b", "c", "d"})) assert.Equal(t, values, []string{"b", "c", "d"})
}
func TestAppendOrModifySessionID(t *testing.T) {
sessionID := string(logging.NewSessionID())
assert.Equal(t, appendOrModifySessionID(nil, sessionID), []string{"--session-id", sessionID})
assert.Equal(t, appendOrModifySessionID([]string{}, sessionID), []string{"--session-id", sessionID})
assert.Equal(t, appendOrModifySessionID([]string{"--cli"}, sessionID), []string{"--cli", "--session-id", sessionID})
assert.Equal(t, appendOrModifySessionID([]string{"--cli", "--session-id"}, sessionID), []string{"--cli", "--session-id", sessionID})
assert.Equal(t, appendOrModifySessionID([]string{"--cli", "--session-id"}, sessionID), []string{"--cli", "--session-id", sessionID})
assert.Equal(t, appendOrModifySessionID([]string{"--session-id", "<oldID>", "--cli"}, sessionID), []string{"--session-id", sessionID, "--cli"})
} }

View File

@ -1,135 +0,0 @@
# Bridge
## Main blocks
This is basic overview of the main bridge blocks.
Note connection between IMAP/SMTP and PMAPI. IMAP and SMTP packages are in the queue to be refactored
and we would like to try to have functionality in bridge core or bridge utilities (such as messages)
than direct usage of PMAPI from IMAP or SMTP. Also database (BoltDB) should be moved to bridge core.
```mermaid
graph LR
S[Server]
C[Client]
U[User]
subgraph "Bridge app"
Core[Bridge core]
API[PMAPI]
Store
DB[BoltDB]
Frontend["Qt / CLI"]
IMAP
SMTP
IMAP --> Store
IMAP --> Core
SMTP --> Core
SMTP --> API
Core --> API
Core --> Store
Store --> API
Store --> DB
Frontend --> Core
end
C --> IMAP
C --> SMTP
U --> Frontend
API --> S
```
## Code structure
More detailed graph of main types used in Bridge app and connection between them. Here is already
communication to PMAPI only from bridge core which is not true, yet. IMAP and SMTP are still calling
PMAPI directly.
```mermaid
graph TD
C["Client (e.g. Thunderbird)"]
PM[Proton Mail Server]
subgraph "Bridge app"
subgraph "Bridge core"
B[Bridge]
U[User]
B --> U
end
subgraph Store
StoreU[Store User]
StoreA[Address]
StoreM[Mailbox]
StoreU --> StoreA
StoreA --> StoreM
end
subgraph Credentials
CredStore[Store]
Creds[Credentials]
CredStore --> Creds
end
subgraph Frontend
CLI
Qt
end
subgraph IMAP
IB[IMAP backend]
IA[IMAP address]
IM[IMAP mailbox]
IB --> B
IB --> IA
IA --> IM
IA --> U
IA --> StoreA
IM --> StoreM
end
subgraph SMTP
SB[SMTP backend]
SS[SMTP session]
SB --> B
SB --> SS
SS --> U
end
end
subgraph PMAPI
AC[Client]
end
C --> IB
C --> SB
CLI --> B
Qt --> B
U --> CredStore
U --> Creds
U --> StoreU
StoreU --> AC
StoreA --> AC
StoreM --> AC
B --> AC
U --> AC
AC --> PM
```
## How to debug
Run `make run-debug` which starts [Delve](https://github.com/go-delve/delve).

View File

@ -1,114 +0,0 @@
# Communication
## First login and sync
When user logs in to the bridge for the first time, immediately starts the first sync.
First sync downloads all headers of all e-mails and creates database to have proper UIDs
and indexes for IMAP. See [database](database.md) for more information.
By default, whenever it's possible, sync downloads only all e-mails maiblox which already
have list of labels so we can construct all mailboxes (inbox, sent, trash, custom folders
and labels) without need to download each e-mail headers many times.
Note that we need to download also bodies to calculate size of the e-mail and set proper
content type (clients uses content type for guess if e-mail contains attachment)--but only
body, not attachment. Also it's downloaded only for the first time. After that we store
those information in our database so next time we only sync headers, labels and so on.
First sync takes some time. List of 150 messages takes about second and then we need to
download bodies for each message. We still need to do some optimalizations. Anyway, if
user has reasonable amount of e-mails, there is good chance user will see e-mails in the
client right after adding account.
When account is added to client, client start the sync. This sync will ask Bridge app
for all headers (done quickly) and then starts to download all bodies and attachment.
Unfortunately for some e-mail more than once if the same e-mail is in more mailboxes
(e.g. inbox and all mail)--there is no way to tell over IMAP it's the same message.
After successful login of client to IMAP, Bridge starts event loop. That periodicly ask
servers (each 30 seconds) for new updates (new message, keys, …).
```mermaid
sequenceDiagram
participant S as Server
participant B as Bridge
participant C as Client
Note right of B: Set up PM account<br/>by user
loop First sync
B ->> S: Fetch body and attachments
Note right of B: Build local database<br/>(e-mail UIDs)
end
Note right of C: Set up IMAP/SMTP<br/>by user
C ->> B: IMAP login
B ->> S: Authenticate user
Note right of B: Create IMAP user
loop Event loop, every 30 sec
B ->> S: Fetch e-mail headers
B ->> C: Send IMAP IDLE response
end
C ->> B: IMAP LIST directories
loop Client sync
C ->> B: IMAP SELECT directory
C ->> B: IMAP SEARCH e-mails UIDs
C ->> B: IMAP FETCH of e-mail UID
B ->> S: Fetch body and attachments
Note right of B: Decrypt message<br/>and attachment
B ->> C: IMAP response
end
```
## IMAP IDLE extension
IMAP IDLE is extension, it has to be supported by both client and server. IMAP server (in our case
the bridge) supports it so clients can use it. It works by issuing `IDLE` command by the client and
keeps the connection open. When the server has some update, server (the bridge) will respond to that
by `EXISTS` (new message), `APPEND` (imported message), `EXPUNGE` (deleted message) or `MOVE` response.
Even when there is connection with IDLE open, server can mark the client as inactive. Therefore,
it's recommended the client should reissue the connection after each 29 minutes. This is not the
real push and can fail!
Our event loop is also simple pull and it will trigger IMAP IDLE when we get some new update from
the server. Would be good to have push from the server, but we need to wait for the support on API.
RFC: https://tools.ietf.org/html/rfc2177
```mermaid
sequenceDiagram
participant S as Server
participant B as Bridge
participant C as Client
C ->> B: IMAP IDLE
loop Every 30 seconds
S ->> B: Checking events
B ->> C: IMAP response
end
```
## Sending e-mails
E-mail are sent over standard SMTP protocol. Our bridge takes the message, encrypts and sent it
further to our server which will then send the message to its final destination. The important
and tricky part is encryption. See [encryption](encryption.md) or [PMEL document](https://docs.google.com/document/d/1lEBkG0DC5FOWlumInKtu4a9Cc1Eszp48ZhFy9UpPQso/edit)
for more information.
```mermaid
sequenceDiagram
participant S as Server
participant B as Bridge
participant C as Client
C ->> B: SMTP send e-mail
Note right of B: Encrypt messages
B ->> S: Send encrypted e-mail
B ->> C: Respond OK
```

View File

@ -1,27 +0,0 @@
# Database
Bridge needs to have a small database to pair our IDs with IMAP UIDs and indexes. IMAP protocol
requires every message to have an unique UID in mailbox. In this context, mailbox is not an account,
but a folder or label. This means that one message can have more UIDs, one for each mailbox (folder),
and that two messages can have the same UID, but each for different mailbox (folder).
IMAP index is just an index. Look at it like to an array: `["UID1", "UID2", "UID3"]`. We can access
message by UID or index; for example index 2 and UID `UID2`. When this message is deleted, we need
to re-index all following messages. The array will look now like `["UID1", "UID3"]` and the last
message can be accessed by index 2 or UID `UID3`.
See RFCs for more information:
* https://tools.ietf.org/html/rfc822
* https://tools.ietf.org/html/rfc3501
Our database is currently built on BBolt and have those buckets (key-value storage):
* Message metadata bucket:
* `[metadataBucket][API_ID] -> pmapi.Message{subject, from, to, size, other headers...}` (without body or attachment)
* Mapping buckets
* `[mailboxesBucket][addressID-mailboxID][api_ids][API_ID] -> UID`
* `[mailboxesBucket][addressID-mailboxID][imap_ids][UID] -> API_ID`

View File

@ -1,12 +0,0 @@
# Encryption
Encryption is done in PMAPI, bridge utils and bridge itself. The best would be to keep encryption
in PMAPI and bridge utils (in package such as messages). All packages are using our high-level
GopenPGP library on top of OpenPGP.
## `gopenpgp.KeyRing`
We use one `KeyRing` per address. Our usage then contains all keys for specific address. Primary
key is always on the first position, then there old ones to be able to decrypt last e-mail.
OpenPGP encrypts given message with all available keys, so we need to first get first (primary)
key for encryption to have message encrypted only once with primary key.

View File

@ -1,9 +0,0 @@
# Bridge Documentation
Documentation pages in order to read for a novice:
* [Bridge code](bridge.md)
* [Internal Bridge database](database.md)
* [Communication between Bridge, Client and Server](communication.md)
* [Encryption](encryption.md)

View File

@ -1,103 +0,0 @@
# Update mechanism of Bridge
There are multiple options how to change version of application:
* Automatic in-app update
* Manual in-app update
* Manual install
In-app update ends with restarting bridge into new version. Automatic in-app
update is downloading, verifying and installing the new version immediately
without user confirmation. For manual in-app update user needs to confirm first.
Update is done from special update file published on website.
The manual installation requires user to download, verify and install manually
using installer for given OS.
The bridge is installed and executed differently for given OS:
* Windows and Linux apps are using launcher mechanism:
* There is system protected installation path which is created on first
install. It contains bridge exe and launcher exe. When users starts
bridge the launcher is executed first. It will check update path compare
version with installed one. The newer version then is then executed.
* Update mechanism means to replace files in update folder which is located
in user space.
* macOS app does not use launcher
* No launcher, only one executable
* In-App update replaces the bridge files in installation path directly
```mermaid
flowchart LR
subgraph Frontend
U[User requests<br>version check]
ManIns((Notify user about<br>manual install<br>is needed))
R((Notify user<br>about restart))
ManUp((Notify user about<br>manual update))
NF((Notify user about<br>force update))
ManUp -->|Install| InstFront[Install]
InstFront -->|Ok| R
InstFront -->|Error| ManIns
U --> CheckFront[Check online]
CheckFront -->|Ok| IAFront{Is new version<br>and applicable?}
CheckFront -->|Error| ManIns
IAFront -->|No| Latest((Notify user<br>has latest version))
IAFront -->|Yes| CanInstall{Can update?}
CanInstall -->|No| ManIns
CanInstall -->|Yes| NotifOrInstall{Is automatic<br>update enabled?}
NotifOrInstall -->|Manual| ManUp
end
subgraph Backend
W[Wait for next check]
W --> Check[Check online]
Check --> NV{Has new<br>version?}
Check -->|Error| W
NV -->|No new version| W
IA{Is install<br>applicable?}
NV -->|New version<br>available| IA
IA -->|Local rollout<br>not enough| W
IA -->|Yes| AU{Is automatic\nupdate enabled?}
AU -->|Yes| CanUp{Can update?}
CanUp -->|No| ManIns
CanUp -->|Yes| Ins[Install]
Ins -->|Error| ManIns
Ins -->|Ok| R
AU -->|No| ManUp
ManUp -->|Ignore| W
F[Force update]
F --> NF
end
ManIns --> Web[Open web page]
NF --> Web
ManUp --> Web
R --> Re[Restart]
NF --> Q[Quit bridge]
NotifOrInstall -->|Automatic| W
```
The non-trivial is to combine the update with setting change:
* turn off/on automatic in-app updates
* change from stable to beta or back
_TODO fill flow chart details_
We are not support downgrade functionality. Only some circumstances can lead to
downgrading the app version.
_TODO fill flow chart details_

2
extern/vcpkg vendored

58
go.mod
View File

@ -1,21 +1,23 @@
module github.com/ProtonMail/proton-bridge/v3 module github.com/ProtonMail/proton-bridge/v3
go 1.20 go 1.24
toolchain go1.24.2
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.2.0 github.com/Masterminds/semver/v3 v3.2.0
github.com/ProtonMail/gluon v0.17.1-0.20231114153341-2ecbdd2739f7 github.com/ProtonMail/gluon v0.17.1-0.20250611120816-05167d499f8d
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.4.1-0.20231130083229-e8aa47d7a366 github.com/ProtonMail/go-proton-api v0.4.1-0.20250417134000-e624a080f7ba
github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton github.com/ProtonMail/gopenpgp/v2 v2.8.2-proton
github.com/PuerkitoBio/goquery v1.8.1 github.com/PuerkitoBio/goquery v1.8.1
github.com/abiosoft/ishell v2.0.0+incompatible github.com/abiosoft/ishell v2.0.0+incompatible
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37 github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37
github.com/bradenaw/juniper v0.12.0 github.com/bradenaw/juniper v0.12.0
github.com/cucumber/godog v0.12.5 github.com/cucumber/godog v0.12.5
github.com/cucumber/messages-go/v16 v16.0.1 github.com/cucumber/messages-go/v16 v16.0.1
github.com/docker/docker-credential-helpers v0.6.3 github.com/docker/docker-credential-helpers v0.8.1
github.com/elastic/go-sysinfo v1.11.2-0.20231129083954-35e55cd2a542 github.com/elastic/go-sysinfo v1.11.2-0.20231129083954-35e55cd2a542
github.com/emersion/go-imap v1.2.1 github.com/emersion/go-imap v1.2.1
github.com/emersion/go-imap-id v0.0.0-20190926060100-f94a56b9ecde github.com/emersion/go-imap-id v0.0.0-20190926060100-f94a56b9ecde
@ -28,7 +30,7 @@ require (
github.com/go-resty/resty/v2 v2.7.0 github.com/go-resty/resty/v2 v2.7.0
github.com/godbus/dbus v4.1.0+incompatible github.com/godbus/dbus v4.1.0+incompatible
github.com/golang/mock v1.6.0 github.com/golang/mock v1.6.0
github.com/google/go-cmp v0.5.9 github.com/google/go-cmp v0.6.0
github.com/google/uuid v1.3.0 github.com/google/uuid v1.3.0
github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-multierror v1.1.1
github.com/jaytaylor/html2text v0.0.0-20211105163654-bc68cce691ba github.com/jaytaylor/html2text v0.0.0-20211105163654-bc68cce691ba
@ -39,22 +41,26 @@ require (
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/pkg/profile v1.7.0 github.com/pkg/profile v1.7.0
github.com/sirupsen/logrus v1.9.2 github.com/sirupsen/logrus v1.9.2
github.com/stretchr/testify v1.8.3 github.com/stretchr/testify v1.8.4
github.com/urfave/cli/v2 v2.24.4 github.com/urfave/cli/v2 v2.24.4
github.com/vmihailenco/msgpack/v5 v5.3.5 github.com/vmihailenco/msgpack/v5 v5.3.5
go.uber.org/goleak v1.2.1 go.uber.org/goleak v1.2.1
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
golang.org/x/net v0.17.0 golang.org/x/net v0.38.0
golang.org/x/sys v0.13.0 golang.org/x/oauth2 v0.7.0
golang.org/x/text v0.13.0 golang.org/x/sys v0.31.0
golang.org/x/text v0.23.0
google.golang.org/api v0.114.0
google.golang.org/grpc v1.56.3 google.golang.org/grpc v1.56.3
google.golang.org/protobuf v1.30.0 google.golang.org/protobuf v1.33.0
howett.net/plist v1.0.0 howett.net/plist v1.0.0
) )
require ( require (
cloud.google.com/go/compute v1.19.1 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf // indirect github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf // indirect
github.com/ProtonMail/go-crypto v0.0.0-20230717121622-edf196117233 // indirect github.com/ProtonMail/go-crypto v1.1.4-proton // indirect
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect
github.com/ProtonMail/go-srp v0.0.7 // indirect github.com/ProtonMail/go-srp v0.0.7 // indirect
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect
@ -62,11 +68,11 @@ require (
github.com/bytedance/sonic v1.9.1 // indirect github.com/bytedance/sonic v1.9.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/chzyer/test v1.0.0 // indirect github.com/chzyer/test v1.0.0 // indirect
github.com/cloudflare/circl v1.3.3 // indirect github.com/cloudflare/circl v1.5.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/cronokirby/saferith v0.33.0 // indirect github.com/cronokirby/saferith v0.33.0 // indirect
github.com/cucumber/gherkin-go/v19 v19.0.3 // indirect github.com/cucumber/gherkin-go/v19 v19.0.3 // indirect
github.com/danieljoos/wincred v1.1.2 // indirect github.com/danieljoos/wincred v1.2.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/elastic/go-windows v1.0.1 // indirect github.com/elastic/go-windows v1.0.1 // indirect
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect
@ -80,8 +86,11 @@ require (
github.com/go-playground/validator/v10 v10.14.0 // indirect github.com/go-playground/validator/v10 v10.14.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect github.com/goccy/go-json v0.10.2 // indirect
github.com/gofrs/uuid v4.3.0+incompatible // indirect github.com/gofrs/uuid v4.3.0+incompatible // indirect
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
github.com/golang/protobuf v1.5.3 // indirect github.com/golang/protobuf v1.5.3 // indirect
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
github.com/googleapis/gax-go/v2 v2.7.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/go-memdb v1.3.3 // indirect github.com/hashicorp/go-memdb v1.3.3 // indirect
@ -93,36 +102,39 @@ require (
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/mattn/go-sqlite3 v1.14.17 // indirect github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/pierrec/lz4/v4 v4.1.17 // indirect github.com/pierrec/lz4/v4 v4.1.17 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect
github.com/rivo/uniseg v0.4.2 // indirect github.com/rivo/uniseg v0.4.2 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/stretchr/objx v0.5.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect github.com/ugorji/go/codec v1.2.11 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a // indirect gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a // indirect
go.opencensus.io v0.24.0 // indirect
golang.org/x/arch v0.3.0 // indirect golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.14.0 // indirect golang.org/x/crypto v0.36.0 // indirect
golang.org/x/mod v0.8.0 // indirect golang.org/x/mod v0.17.0 // indirect
golang.org/x/sync v0.2.0 // indirect golang.org/x/sync v0.12.0 // indirect
golang.org/x/tools v0.6.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )
replace ( replace (
github.com/docker/docker-credential-helpers => github.com/ProtonMail/docker-credential-helpers v1.1.0 github.com/ProtonMail/go-autostart => github.com/ElectroNafta/go-autostart v0.0.0-20250402094843-326608c16033
github.com/emersion/go-message => github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7 github.com/emersion/go-message => github.com/ProtonMail/go-message v0.13.1-0.20240919135104-3bc88e6a9423
github.com/emersion/go-smtp => github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865 github.com/emersion/go-smtp => github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865
github.com/go-resty/resty/v2 => github.com/LBeernaertProton/resty/v2 v2.0.0-20231129100320-dddf8030d93a github.com/go-resty/resty/v2 => github.com/LBeernaertProton/resty/v2 v2.0.0-20231129100320-dddf8030d93a
github.com/keybase/go-keychain => github.com/cuthix/go-keychain v0.0.0-20230517073537-fc1740a83768 github.com/keybase/go-keychain => github.com/cuthix/go-keychain v0.0.0-20240103134243-0b6a41580b77
) )

163
go.sum
View File

@ -5,9 +5,16 @@ cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6A
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.110.0 h1:Zc8gqp3+a9/Eyph2KDmcGaPtbKRIoqq4YTlL4NMD0Ys=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/compute v1.19.1 h1:am86mquDUgjGNWxiGn+5PGLbmgiWXlE/yNWpIpNvuXY=
cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE=
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
cloud.google.com/go/longrunning v0.4.1 h1:v+yFJOfKC3yZdY6ZUI933pIYdhyhV8S3NpWrXWmg7jM=
cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
@ -16,37 +23,36 @@ github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557 h1:l6surSnJ3RP4qA
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557/go.mod h1:sTrmvD/TxuypdOERsDOS7SndZg0rzzcCi1b6wQMXUYM= github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557/go.mod h1:sTrmvD/TxuypdOERsDOS7SndZg0rzzcCi1b6wQMXUYM=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/ElectroNafta/go-autostart v0.0.0-20250402094843-326608c16033 h1:d2RB9rQmSusb0K+qSgB+DAY+8i+AXZ/o+oDHj2vAUaA=
github.com/ElectroNafta/go-autostart v0.0.0-20250402094843-326608c16033/go.mod h1:o0nKiWcK0e2G/90uL6akWRkzOV4mFcZmvpBPpigJvdw=
github.com/Kodeworks/golang-image-ico v0.0.0-20141118225523-73f0f4cfade9/go.mod h1:7uhhqiBaR4CpN0k9rMjOtjpcfGd6DG2m04zQxKnWQ0I= github.com/Kodeworks/golang-image-ico v0.0.0-20141118225523-73f0f4cfade9/go.mod h1:7uhhqiBaR4CpN0k9rMjOtjpcfGd6DG2m04zQxKnWQ0I=
github.com/LBeernaertProton/resty/v2 v2.0.0-20231129100320-dddf8030d93a h1:eQO/GF/+H8/9udc9QAgieFr+jr1tjXlJo35RAhsUbWY= github.com/LBeernaertProton/resty/v2 v2.0.0-20231129100320-dddf8030d93a h1:eQO/GF/+H8/9udc9QAgieFr+jr1tjXlJo35RAhsUbWY=
github.com/LBeernaertProton/resty/v2 v2.0.0-20231129100320-dddf8030d93a/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A= github.com/LBeernaertProton/resty/v2 v2.0.0-20231129100320-dddf8030d93a/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A=
github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g= github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg=
github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I= github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs69zUkSzubzjBbL+cmOXgnmt9Fyd9ug= github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs69zUkSzubzjBbL+cmOXgnmt9Fyd9ug=
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/gluon v0.17.1-0.20250611120816-05167d499f8d h1:45W7G+X0w7nzLzeB0eiFkGho5DTK1jNmmNbt3IhN524=
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g= github.com/ProtonMail/gluon v0.17.1-0.20250611120816-05167d499f8d/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
github.com/ProtonMail/gluon v0.17.1-0.20231114153341-2ecbdd2739f7 h1:w+VoSAq9FQvKMm3DlH1MIEZ1KGe7LJ+81EJFVwSV4VU=
github.com/ProtonMail/gluon v0.17.1-0.20231114153341-2ecbdd2739f7/go.mod h1:Og5/Dz1MiGpCJn51XujZwxiLG7WzvvjE5PRpZBQmAHo=
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE= github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
github.com/ProtonMail/go-crypto v0.0.0-20230717121622-edf196117233 h1:bdoKdh0f66/lrgVfYlxw0aqISY/KOqXmFJyGt7rGmnc= github.com/ProtonMail/go-crypto v1.1.4-proton h1:KIo9uNlk3vzlwI7o5VjhiEjI4Ld1TDixOMnoNZyfpFE=
github.com/ProtonMail/go-crypto v0.0.0-20230717121622-edf196117233/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= github.com/ProtonMail/go-crypto v1.1.4-proton/go.mod h1:zNoyBJW3p/yVWiHNZgfTF9VsjwqYof5YY0M9kt2QaX0=
github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7 h1:+j+Kd/DyZ/qGfMT9htAT7HxqIEbZHsatsx+m8AoV6fc= github.com/ProtonMail/go-message v0.13.1-0.20240919135104-3bc88e6a9423 h1:p8nBDxvRnvDOyrcePKkPpErWGhDoTqpX8a1c54CcSu0=
github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4= github.com/ProtonMail/go-message v0.13.1-0.20240919135104-3bc88e6a9423/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k= github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw= github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
github.com/ProtonMail/go-proton-api v0.4.1-0.20231130083229-e8aa47d7a366 h1:W9P5GdDnuGkB3tbzKnXmUrTjIs6zk/K+4lpPTWzsoRE= github.com/ProtonMail/go-proton-api v0.4.1-0.20250417134000-e624a080f7ba h1:DFBngZ7u/f69flRFzPp6Ipo6PKEyflJlA5OCh52yDB4=
github.com/ProtonMail/go-proton-api v0.4.1-0.20231130083229-e8aa47d7a366/go.mod h1:t+hb0BfkmZ9fpvzVRpHC7limoowym6ln/j0XL9a8DDw= github.com/ProtonMail/go-proton-api v0.4.1-0.20250417134000-e624a080f7ba/go.mod h1:eXIoLyIHxvPo8Kd9e1ygYIrAwbeWJhLi3vgSz2crlK4=
github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865 h1:EP1gnxLL5Z7xBSymE9nSTM27nRYINuvssAtDmG0suD8= github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865 h1:EP1gnxLL5Z7xBSymE9nSTM27nRYINuvssAtDmG0suD8=
github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ= github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI= github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk= github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk=
github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton h1:8tqHYM6IGsdEc6Vxf1TWiwpHNj8yIEQNACPhxsDagrk= github.com/ProtonMail/gopenpgp/v2 v2.8.2-proton h1:MMVgE6nk5Ulh9Ud5L1Xc5iaPKE85FbfKQV17ZMucrR0=
github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton/go.mod h1:omVkSsfPAhmptzPF/piMXb16wKIWUvVhZbVW7sJKh0A= github.com/ProtonMail/gopenpgp/v2 v2.8.2-proton/go.mod h1:+PjybET6fgcLzldFy1hpy7s8VibZ0T1hLFbxnnMk0lo=
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM= github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ= github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw= github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw=
@ -71,10 +77,10 @@ github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJm
github.com/bradenaw/juniper v0.12.0 h1:Q/7icpPQD1nH/La5DobQfNEtwyrBSiSu47jOQx7lJEM= github.com/bradenaw/juniper v0.12.0 h1:Q/7icpPQD1nH/La5DobQfNEtwyrBSiSu47jOQx7lJEM=
github.com/bradenaw/juniper v0.12.0/go.mod h1:Z2B7aJlQ7xbfWsnMLROj5t/5FQ94/MkIdKC30J4WvzI= github.com/bradenaw/juniper v0.12.0/go.mod h1:Z2B7aJlQ7xbfWsnMLROj5t/5FQ94/MkIdKC30J4WvzI=
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
@ -88,8 +94,9 @@ github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs= github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys=
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
@ -107,20 +114,25 @@ github.com/cucumber/godog v0.12.5/go.mod h1:u6SD7IXC49dLpPN35kal0oYEjsXZWee4pW6T
github.com/cucumber/messages-go/v16 v16.0.0/go.mod h1:EJcyR5Mm5ZuDsKJnT2N9KRnBK30BGjtYotDKpwQ0v6g= github.com/cucumber/messages-go/v16 v16.0.0/go.mod h1:EJcyR5Mm5ZuDsKJnT2N9KRnBK30BGjtYotDKpwQ0v6g=
github.com/cucumber/messages-go/v16 v16.0.1 h1:fvkpwsLgnIm0qugftrw2YwNlio+ABe2Iu94Ap8GMYIY= github.com/cucumber/messages-go/v16 v16.0.1 h1:fvkpwsLgnIm0qugftrw2YwNlio+ABe2Iu94Ap8GMYIY=
github.com/cucumber/messages-go/v16 v16.0.1/go.mod h1:EJcyR5Mm5ZuDsKJnT2N9KRnBK30BGjtYotDKpwQ0v6g= github.com/cucumber/messages-go/v16 v16.0.1/go.mod h1:EJcyR5Mm5ZuDsKJnT2N9KRnBK30BGjtYotDKpwQ0v6g=
github.com/cuthix/go-keychain v0.0.0-20230517073537-fc1740a83768 h1:Jrcoxtrk4qpuzKIYPlEkjIK0M+bABs0oW2QzrOuwlzk= github.com/cuthix/go-keychain v0.0.0-20240103134243-0b6a41580b77 h1:sdB/yJMbubPQothFl6KYCOrMBRgy0pZbBXIWoJqSFLo=
github.com/cuthix/go-keychain v0.0.0-20230517073537-fc1740a83768/go.mod h1:ZoZU1fnBy3mOLWr3Pg+Y2+nTKtu6ypDte2kZg9HvSwY= github.com/cuthix/go-keychain v0.0.0-20240103134243-0b6a41580b77/go.mod h1:ZoZU1fnBy3mOLWr3Pg+Y2+nTKtu6ypDte2kZg9HvSwY=
github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg= github.com/danieljoos/wincred v1.2.1 h1:dl9cBrupW8+r5250DYkYxocLeZ1Y4vB1kxgtjxw8GQs=
github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= github.com/danieljoos/wincred v1.2.1/go.mod h1:uGaFL9fDn3OLTvzCGulzE+SzjEe5NGlh5FdCcyfPwps=
github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM= github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM=
github.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.8.1 h1:j/eKUktUltBtMzKqmfLB0PAgqYyMHOp5vfsD1807oKo=
github.com/docker/docker-credential-helpers v0.8.1/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/elastic/go-sysinfo v1.11.2-0.20231129083954-35e55cd2a542 h1:IFTm6NBbfSgZCaeEzorQhH4T7ZERl4j+1u7oXWzmJcM= github.com/elastic/go-sysinfo v1.11.2-0.20231129083954-35e55cd2a542 h1:IFTm6NBbfSgZCaeEzorQhH4T7ZERl4j+1u7oXWzmJcM=
github.com/elastic/go-sysinfo v1.11.2-0.20231129083954-35e55cd2a542/go.mod h1:GKqR8bbMK/1ITnez9NIsIfXQr25aLhRJa7AfT8HpBFQ= github.com/elastic/go-sysinfo v1.11.2-0.20231129083954-35e55cd2a542/go.mod h1:GKqR8bbMK/1ITnez9NIsIfXQr25aLhRJa7AfT8HpBFQ=
github.com/elastic/go-windows v1.0.1 h1:AlYZOldA+UJ0/2nBuqWdo90GFCgG9xuyw9SYzGUtJm0= github.com/elastic/go-windows v1.0.1 h1:AlYZOldA+UJ0/2nBuqWdo90GFCgG9xuyw9SYzGUtJm0=
@ -136,6 +148,10 @@ github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwo
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3 h1:hQ1wTMaKcGfobYRT88RM8NFNyX+IQHvagkm/tqViU98= github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3 h1:hQ1wTMaKcGfobYRT88RM8NFNyX+IQHvagkm/tqViU98=
github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM= github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
@ -157,6 +173,7 @@ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7/go.mod h1:482civXOzJJCPzJ4ZOX/pwvXBWSnzD4OKMdH4ClKGbk= github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7/go.mod h1:482civXOzJJCPzJ4ZOX/pwvXBWSnzD4OKMdH4ClKGbk=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200625191551-73d3c3675aa3/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200625191551-73d3c3675aa3/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@ -164,6 +181,7 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
@ -182,9 +200,12 @@ github.com/gofrs/uuid v4.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRx
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff/go.mod h1:wfqRWLHRBsRgkp5dmbG56SA0DmVtwrF5N3oPdI8t+Aw= github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff/go.mod h1:wfqRWLHRBsRgkp5dmbG56SA0DmVtwrF5N3oPdI8t+Aw=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
@ -193,6 +214,13 @@ github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+Licev
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
@ -200,9 +228,13 @@ github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Z
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
@ -210,10 +242,15 @@ github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OI
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y= github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y=
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k=
github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gax-go/v2 v2.7.1 h1:gF4c0zjUP2H/s/hEGyLA3I0fA2ZWjzYiONAD6cvPr8A=
github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
@ -283,9 +320,11 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/lucor/goinfo v0.0.0-20200401173949-526b5363a13a/go.mod h1:ORP3/rB5IsulLEBwQZCJyyV6niqmI7P4EWSmkug+1Ng= github.com/lucor/goinfo v0.0.0-20200401173949-526b5363a13a/go.mod h1:ORP3/rB5IsulLEBwQZCJyyV6niqmI7P4EWSmkug+1Ng=
@ -303,8 +342,8 @@ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
@ -331,7 +370,9 @@ github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM=
github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
@ -341,6 +382,7 @@ github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNc
github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc= github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc=
github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@ -354,12 +396,13 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8= github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8=
@ -367,6 +410,7 @@ github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@ -408,8 +452,9 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
@ -433,6 +478,8 @@ gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a/go.mod h1:NREv
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
@ -447,11 +494,12 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -476,8 +524,9 @@ golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -494,22 +543,25 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g=
golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -519,8 +571,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -540,13 +592,13 @@ golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200720211630-cb9d2d5c5666/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200720211630-cb9d2d5c5666/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -554,17 +606,15 @@ golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
@ -578,12 +628,12 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
@ -598,6 +648,7 @@ golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
@ -611,8 +662,9 @@ golang.org/x/tools v0.0.0-20200328031815-3db5fc6bac03/go.mod h1:Sl4aGygMT6LrqrWc
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -622,10 +674,14 @@ google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.114.0 h1:1xQPji6cO2E2vLiI+C/XiFAnsn1WV3mjaEwGLhi3grE=
google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
@ -635,17 +691,31 @@ google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A= google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc= google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc=
google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@ -669,6 +739,7 @@ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //
@ -26,6 +26,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
"time"
"github.com/Masterminds/semver/v3" "github.com/Masterminds/semver/v3"
"github.com/ProtonMail/gluon/async" "github.com/ProtonMail/gluon/async"
@ -43,6 +44,7 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/vault" "github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain" "github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
"github.com/ProtonMail/proton-bridge/v3/pkg/restarter" "github.com/ProtonMail/proton-bridge/v3/pkg/restarter"
"github.com/elastic/go-sysinfo"
"github.com/pkg/profile" "github.com/pkg/profile"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
@ -73,6 +75,13 @@ const (
flagLogIMAP = "log-imap" flagLogIMAP = "log-imap"
flagLogSMTP = "log-smtp" flagLogSMTP = "log-smtp"
flagEnableKeychainTest = "enable-keychain-test"
flagDisableKeychainTest = "disable-keychain-test"
flagSoftwareRenderer = "software-renderer"
flagSetSoftwareRenderer = "set-software-renderer"
flagSetHardwareRenderer = "set-hardware-renderer"
) )
// Hidden flags. // Hidden flags.
@ -80,8 +89,7 @@ const (
flagLauncher = "launcher" flagLauncher = "launcher"
flagNoWindow = "no-window" flagNoWindow = "no-window"
flagParentPID = "parent-pid" flagParentPID = "parent-pid"
flagSoftwareRenderer = "software-renderer" FlagSessionID = "session-id"
flagSessionID = "session-id"
) )
const ( const (
@ -89,6 +97,23 @@ const (
appShortName = "bridge" appShortName = "bridge"
) )
// the two flags below have been deprecated by BRIDGE-281. We however keep them so that bridge does not error if they are passed on startup.
var cliFlagEnableKeychainTest = &cli.BoolFlag{ //nolint:gochecknoglobals
Name: flagEnableKeychainTest,
Usage: "This flag is deprecated and does nothing",
Value: false,
DisableDefaultText: true,
Hidden: true,
}
var cliFlagDisableKeychainTest = &cli.BoolFlag{ //nolint:gochecknoglobals
Name: flagDisableKeychainTest,
Usage: "This flag is deprecated and does nothing",
Value: false,
DisableDefaultText: true,
Hidden: true,
}
func New() *cli.App { func New() *cli.App {
app := cli.NewApp() app := cli.NewApp()
@ -138,6 +163,24 @@ func New() *cli.App {
Name: flagLogSMTP, Name: flagLogSMTP,
Usage: "Enable logging of SMTP communications (may contain decrypted data!)", Usage: "Enable logging of SMTP communications (may contain decrypted data!)",
}, },
&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: "Use software rendering of the GUI for the current execution of the application",
Value: false,
DisableDefaultText: true,
},
&cli.BoolFlag{
Name: flagSetSoftwareRenderer, // This flag is ignored by bridge, we just want it to be shown in the help (BRIDGE-217).
Usage: "Toggle software rendering of the GUI for the current and future executions of the application",
Value: false,
DisableDefaultText: true,
},
&cli.BoolFlag{
Name: flagSetHardwareRenderer, // This flag is ignored by bridge, we just want it to be shown in the help (BRIDGE-217).
Usage: "Toggle hardware rendering of the GUI for the current and future executions of the application",
Value: false,
DisableDefaultText: true,
},
// Hidden flags // Hidden flags
&cli.BoolFlag{ &cli.BoolFlag{
@ -156,18 +199,26 @@ func New() *cli.App {
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,
},
&cli.StringFlag{ &cli.StringFlag{
Name: flagSessionID, Name: FlagSessionID,
Hidden: true, Hidden: true,
}, },
} }
// We override the default help value because we want "Show" to be capitalized
cli.HelpFlag = &cli.BoolFlag{
Name: "help",
Aliases: []string{"h"},
Usage: "Show help",
DisableDefaultText: true,
}
if onMacOS() {
// The two flags below were introduced for BRIDGE-116, and are available only on macOS.
// They have been later removed fro BRIDGE-281.
app.Flags = append(app.Flags, cliFlagEnableKeychainTest, cliFlagDisableKeychainTest)
}
app.Action = run app.Action = run
return app return app
@ -236,9 +287,9 @@ func run(c *cli.Context) error {
return withSingleInstance(settings, locations.GetLockFile(), version, func() error { return withSingleInstance(settings, locations.GetLockFile(), version, func() error {
// Look for available keychains // Look for available keychains
return WithKeychainList(func(keychains *keychain.List) error { return WithKeychainList(crashHandler, func(keychains *keychain.List) error {
// Unlock the encrypted vault. // Unlock the encrypted vault.
return WithVault(locations, keychains, crashHandler, func(v *vault.Vault, insecure, corrupt bool) error { return WithVault(reporter, locations, keychains, crashHandler, func(v *vault.Vault, insecure, corrupt bool) error {
if !v.Migrated() { if !v.Migrated() {
// Migrate old settings into the vault. // Migrate old settings into the vault.
if err := migrateOldSettings(v); err != nil { if err := migrateOldSettings(v); err != nil {
@ -344,7 +395,7 @@ func withLogging(c *cli.Context, crashHandler *crash.Handler, locations *locatio
logrus.WithField("path", logsPath).Debug("Received logs path") logrus.WithField("path", logsPath).Debug("Received logs path")
// Initialize logging. // Initialize logging.
sessionID := logging.NewSessionIDFromString(c.String(flagSessionID)) sessionID := logging.NewSessionIDFromString(c.String(FlagSessionID))
var closer io.Closer var closer io.Closer
if closer, err = logging.Init( if closer, err = logging.Init(
logsPath, logsPath,
@ -371,6 +422,24 @@ func withLogging(c *cli.Context, crashHandler *crash.Handler, locations *locatio
WithField("SentryID", sentry.GetProtectedHostname()). WithField("SentryID", sentry.GetProtectedHostname()).
Info("Run app") Info("Run app")
now := time.Now()
logrus.
WithField("timeZone", now.Format("MST")).
WithField("offset", now.Format("-07:00:00")).
Info("Time zone info")
host, err := sysinfo.Host()
if err != nil {
logrus.WithError(err).Error("Could not retrieve operating system info")
} else {
osInfo := host.Info().OS
logrus.
WithField("name", osInfo.Name).
WithField("version", osInfo.Version).
WithField("build", osInfo.Build).
Info("Operating system info")
}
return fn(closer) return fn(closer)
} }
@ -482,9 +551,10 @@ func withCookieJar(vault *vault.Vault, fn func(http.CookieJar) error) error {
} }
// WithKeychainList init the list of usable keychains. // WithKeychainList init the list of usable keychains.
func WithKeychainList(fn func(*keychain.List) error) error { func WithKeychainList(panicHandler async.PanicHandler, fn func(*keychain.List) error) error {
logrus.Debug("Creating keychain list") logrus.Debug("Creating keychain list")
defer logrus.Debug("Keychain list stop") defer logrus.Debug("Keychain list stop")
defer async.HandlePanic(panicHandler)
return fn(keychain.NewList()) return fn(keychain.NewList())
} }
@ -505,3 +575,7 @@ func setDeviceCookies(jar *cookies.Jar) error {
return nil return nil
} }
func onMacOS() bool {
return runtime.GOOS == "darwin"
}

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //
@ -43,7 +43,7 @@ import (
// nolint:gosec // nolint:gosec
func migrateKeychainHelper(locations *locations.Locations) error { func migrateKeychainHelper(locations *locations.Locations) error {
logrus.Info("Migrating keychain helper") logrus.Trace("Checking if keychain helper needs to be migrated")
settings, err := locations.ProvideSettingsPath() settings, err := locations.ProvideSettingsPath()
if err != nil { if err != nil {
@ -75,7 +75,11 @@ func migrateKeychainHelper(locations *locations.Locations) error {
return fmt.Errorf("failed to unmarshal old prefs file: %w", err) return fmt.Errorf("failed to unmarshal old prefs file: %w", err)
} }
return vault.SetHelper(settings, prefs.Helper) err = vault.SetHelper(settings, prefs.Helper)
if err == nil {
logrus.Info("Keychain helper has been migrated")
}
return err
} }
// nolint:gosec // nolint:gosec
@ -134,7 +138,7 @@ func migrateOldAccounts(locations *locations.Locations, keychains *keychain.List
if err != nil { if err != nil {
return fmt.Errorf("failed to get helper: %w", err) return fmt.Errorf("failed to get helper: %w", err)
} }
keychain, err := keychain.NewKeychain(helper, "bridge", keychains.GetHelpers(), keychains.GetDefaultHelper()) keychain, _, err := keychain.NewKeychain(helper, "bridge", keychains.GetHelpers(), keychains.GetDefaultHelper())
if err != nil { if err != nil {
return fmt.Errorf("failed to create keychain: %w", err) return fmt.Errorf("failed to create keychain: %w", err)
} }

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //
@ -134,7 +134,7 @@ func TestKeychainMigration(t *testing.T) {
func TestUserMigration(t *testing.T) { func TestUserMigration(t *testing.T) {
kcl := keychain.NewTestKeychainsList() kcl := keychain.NewTestKeychainsList()
kc, err := keychain.NewKeychain("mock", "bridge", kcl.GetHelpers(), kcl.GetDefaultHelper()) kc, _, err := keychain.NewKeychain("mock", "bridge", kcl.GetHelpers(), kcl.GetDefaultHelper())
require.NoError(t, err) require.NoError(t, err)
require.NoError(t, kc.Put("brokenID", "broken")) require.NoError(t, kc.Put("brokenID", "broken"))

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //
@ -18,6 +18,8 @@
package app package app
import ( import (
"crypto/sha256"
"encoding/hex"
"fmt" "fmt"
"path" "path"
@ -25,17 +27,18 @@ import (
"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/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/locations" "github.com/ProtonMail/proton-bridge/v3/internal/locations"
"github.com/ProtonMail/proton-bridge/v3/internal/sentry"
"github.com/ProtonMail/proton-bridge/v3/internal/vault" "github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain" "github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
func WithVault(locations *locations.Locations, keychains *keychain.List, panicHandler async.PanicHandler, fn func(*vault.Vault, bool, bool) error) error { func WithVault(reporter *sentry.Reporter, locations *locations.Locations, keychains *keychain.List, panicHandler async.PanicHandler, fn func(*vault.Vault, bool, bool) error) error {
logrus.Debug("Creating vault") logrus.Debug("Creating vault")
defer logrus.Debug("Vault stopped") defer logrus.Debug("Vault stopped")
// Create the encVault. // Create the encVault.
encVault, insecure, corrupt, err := newVault(locations, keychains, panicHandler) encVault, insecure, corrupt, err := newVault(reporter, locations, keychains, panicHandler)
if err != nil { if err != nil {
return fmt.Errorf("could not create vault: %w", err) return fmt.Errorf("could not create vault: %w", err)
} }
@ -57,7 +60,7 @@ func WithVault(locations *locations.Locations, keychains *keychain.List, panicHa
return fn(encVault, insecure, corrupt != nil) return fn(encVault, insecure, corrupt != nil)
} }
func newVault(locations *locations.Locations, keychains *keychain.List, panicHandler async.PanicHandler) (*vault.Vault, bool, error, error) { func newVault(reporter *sentry.Reporter, locations *locations.Locations, keychains *keychain.List, panicHandler async.PanicHandler) (*vault.Vault, bool, error, error) {
vaultDir, err := locations.ProvideSettingsPath() vaultDir, err := locations.ProvideSettingsPath()
if err != nil { if err != nil {
return nil, false, nil, fmt.Errorf("could not get vault dir: %w", err) return nil, false, nil, fmt.Errorf("could not get vault dir: %w", err)
@ -68,9 +71,20 @@ func newVault(locations *locations.Locations, keychains *keychain.List, panicHan
var ( var (
vaultKey []byte vaultKey []byte
insecure bool insecure bool
lastUsedHelper string
) )
if key, err := loadVaultKey(vaultDir, keychains); err != nil { if key, helper, err := loadVaultKey(vaultDir, keychains); err != nil {
if reporter != nil {
if rerr := reporter.ReportMessageWithContext("Could not load/create vault key", map[string]any{
"keychainDefaultHelper": keychains.GetDefaultHelper(),
"keychainUsableHelpersLength": len(keychains.GetHelpers()),
"error": err.Error(),
}); rerr != nil {
logrus.WithError(err).Info("Failed to report keychain issue to Sentry")
}
}
logrus.WithError(err).Error("Could not load/create vault key") logrus.WithError(err).Error("Could not load/create vault key")
insecure = true insecure = true
@ -78,6 +92,8 @@ func newVault(locations *locations.Locations, keychains *keychain.List, panicHan
vaultDir = path.Join(vaultDir, "insecure") vaultDir = path.Join(vaultDir, "insecure")
} else { } else {
vaultKey = key vaultKey = key
lastUsedHelper = helper
logHashedVaultKey(vaultKey) // Log a hash of the vault key.
} }
gluonCacheDir, err := locations.ProvideGluonCachePath() gluonCacheDir, err := locations.ProvideGluonCachePath()
@ -85,33 +101,47 @@ func newVault(locations *locations.Locations, keychains *keychain.List, panicHan
return nil, false, nil, fmt.Errorf("could not provide gluon path: %w", err) return nil, false, nil, fmt.Errorf("could not provide gluon path: %w", err)
} }
vault, corrupt, err := vault.New(vaultDir, gluonCacheDir, vaultKey, panicHandler) userVault, corrupt, err := vault.New(vaultDir, gluonCacheDir, vaultKey, panicHandler)
if err != nil { if err != nil {
return nil, false, corrupt, fmt.Errorf("could not create vault: %w", err) return nil, false, corrupt, fmt.Errorf("could not create vault: %w", err)
} }
return vault, insecure, corrupt, nil // Remember the last successfully used keychain and store that as the user preference.
if err := vault.SetHelper(vaultDir, lastUsedHelper); err != nil {
logrus.WithError(err).Error("Could not store last used keychain helper")
} }
func loadVaultKey(vaultDir string, keychains *keychain.List) ([]byte, error) { return userVault, insecure, corrupt, nil
helper, err := vault.GetHelper(vaultDir) }
// loadVaultKey - loads the key used to encrypt the vault alongside the keychain helper used to access it.
func loadVaultKey(vaultDir string, keychains *keychain.List) (key []byte, keychainHelper string, err error) {
keychainHelper, err = vault.GetHelper(vaultDir)
if err != nil { if err != nil {
return nil, fmt.Errorf("could not get keychain helper: %w", err) return nil, keychainHelper, fmt.Errorf("could not get keychain helper: %w", err)
} }
kc, err := keychain.NewKeychain(helper, constants.KeyChainName, keychains.GetHelpers(), keychains.GetDefaultHelper()) kc, keychainHelper, err := keychain.NewKeychain(keychainHelper, constants.KeyChainName, keychains.GetHelpers(), keychains.GetDefaultHelper())
if err != nil { if err != nil {
return nil, fmt.Errorf("could not create keychain: %w", err) return nil, keychainHelper, fmt.Errorf("could not create keychain: %w", err)
} }
has, err := vault.HasVaultKey(kc) key, err = vault.GetVaultKey(kc)
if err != nil { if err != nil {
return nil, fmt.Errorf("could not check for vault key: %w", err) if keychain.IsErrKeychainNoItem(err) {
logrus.WithError(err).Warn("no vault key found, generating new")
key, err := vault.NewVaultKey(kc)
return key, keychainHelper, err
} }
if has { return nil, keychainHelper, fmt.Errorf("could not check for vault key: %w", err)
return vault.GetVaultKey(kc)
} }
return vault.NewVaultKey(kc) return key, keychainHelper, nil
}
// logHashedVaultKey - computes a sha256 hash and encodes it to base 64. The resulting string is logged.
func logHashedVaultKey(vaultKey []byte) {
hashedKey := sha256.Sum256(vaultKey)
logrus.WithField("hashedKey", hex.EncodeToString(hashedKey[:])).Info("Found vault key")
} }

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //
@ -40,7 +40,7 @@ func defaultAPIOptions(
proton.WithAppVersion(constants.AppVersion(version.Original())), proton.WithAppVersion(constants.AppVersion(version.Original())),
proton.WithCookieJar(cookieJar), proton.WithCookieJar(cookieJar),
proton.WithTransport(transport), proton.WithTransport(transport),
proton.WithLogger(logrus.StandardLogger()), proton.WithLogger(logrus.WithField("pkg", "gpa/client")),
proton.WithPanicHandler(panicHandler), proton.WithPanicHandler(panicHandler),
} }
} }

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //
@ -24,6 +24,10 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"os"
"regexp"
"runtime"
"strings"
"sync" "sync"
"time" "time"
@ -41,16 +45,23 @@ import (
"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/sentry"
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapsmtpserver" "github.com/ProtonMail/proton-bridge/v3/internal/services/imapsmtpserver"
"github.com/ProtonMail/proton-bridge/v3/internal/services/notifications"
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
"github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice" "github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice"
"github.com/ProtonMail/proton-bridge/v3/internal/telemetry" "github.com/ProtonMail/proton-bridge/v3/internal/telemetry"
"github.com/ProtonMail/proton-bridge/v3/internal/unleash"
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
"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/ProtonMail/proton-bridge/v3/pkg/keychain" "github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
"github.com/bradenaw/juniper/xslices" "github.com/bradenaw/juniper/xslices"
"github.com/elastic/go-sysinfo/types"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
var usernameChangeRegex = regexp.MustCompile(`^/Users/([^/]+)/`)
type Bridge struct { type Bridge struct {
// vault holds bridge-specific data, such as preferences and known users (authorized or not). // vault holds bridge-specific data, such as preferences and known users (authorized or not).
vault *vault.Vault vault *vault.Vault
@ -72,6 +83,7 @@ type Bridge struct {
// updater is the bridge's updater. // updater is the bridge's updater.
updater Updater updater Updater
installChLegacy chan installJobLegacy
installCh chan installJob installCh chan installJob
// heartbeat is the telemetry heartbeat for metrics. // heartbeat is the telemetry heartbeat for metrics.
@ -130,8 +142,22 @@ type Bridge struct {
serverManager *imapsmtpserver.Service serverManager *imapsmtpserver.Service
syncService *syncservice.Service syncService *syncservice.Service
// unleashService is responsible for polling the feature flags and caching
unleashService *unleash.Service
// observabilityService is responsible for handling calls to the observability system
observabilityService *observability.Service
// notificationStore is used for notification deduplication
notificationStore *notifications.Store
// getHostVersion primarily used for testing the update logic - it should return an OS version
getHostVersion func(host types.Host) string
} }
var logPkg = logrus.WithField("pkg", "bridge") //nolint:gochecknoglobals
// New creates a new bridge. // New creates a new bridge.
func New( func New(
locator Locator, // the locator to provide paths to store data locator Locator, // the locator to provide paths to store data
@ -245,6 +271,10 @@ func newBridge(
return nil, fmt.Errorf("failed to create focus service: %w", err) return nil, fmt.Errorf("failed to create focus service: %w", err)
} }
unleashService := unleash.NewBridgeService(ctx, api, locator, panicHandler)
observabilityService := observability.NewService(ctx, panicHandler)
bridge := &Bridge{ bridge := &Bridge{
vault: vault, vault: vault,
@ -259,6 +289,7 @@ func newBridge(
imapEventCh: imapEventCh, imapEventCh: imapEventCh,
updater: updater, updater: updater,
installChLegacy: make(chan installJobLegacy),
installCh: make(chan installJob), installCh: make(chan installJob),
curVersion: curVersion, curVersion: curVersion,
@ -284,7 +315,15 @@ func newBridge(
lastVersion: lastVersion, lastVersion: lastVersion,
tasks: tasks, tasks: tasks,
syncService: syncservice.NewService(reporter, panicHandler), syncService: syncservice.NewService(panicHandler, observabilityService),
unleashService: unleashService,
observabilityService: observabilityService,
notificationStore: notifications.NewStore(locator.ProvideNotificationsCachePath),
getHostVersion: func(host types.Host) string { return host.Info().OS.Version },
} }
bridge.serverManager = imapsmtpserver.NewService(context.Background(), bridge.serverManager = imapsmtpserver.NewService(context.Background(),
@ -295,8 +334,13 @@ func newBridge(
reporter, reporter,
uidValidityGenerator, uidValidityGenerator,
&bridgeIMAPSMTPTelemetry{b: bridge}, &bridgeIMAPSMTPTelemetry{b: bridge},
observabilityService,
unleashService,
) )
// Check whether username has changed and correct (macOS only)
bridge.verifyUsernameChange()
if err := bridge.serverManager.Init(context.Background(), bridge.tasks, &bridgeEventSubscription{b: bridge}); err != nil { if err := bridge.serverManager.Init(context.Background(), bridge.tasks, &bridgeEventSubscription{b: bridge}); err != nil {
return nil, err return nil, err
} }
@ -309,6 +353,10 @@ func newBridge(
bridge.syncService.Run() bridge.syncService.Run()
bridge.unleashService.Run()
bridge.observabilityService.Run(bridge)
return bridge, nil return bridge, nil
} }
@ -322,7 +370,7 @@ func (bridge *Bridge) init(tlsReporter TLSReporter) error {
// Handle connection up/down events. // Handle connection up/down events.
bridge.api.AddStatusObserver(func(status proton.Status) { bridge.api.AddStatusObserver(func(status proton.Status) {
logrus.Info("API status changed: ", status) logPkg.Info("API status changed: ", status)
switch { switch {
case status == proton.StatusUp: case status == proton.StatusUp:
@ -337,7 +385,7 @@ func (bridge *Bridge) init(tlsReporter TLSReporter) error {
// If any call returns a bad version code, we need to update. // If any call returns a bad version code, we need to update.
bridge.api.AddErrorHandler(proton.AppVersionBadCode, func() { bridge.api.AddErrorHandler(proton.AppVersionBadCode, func() {
logrus.Warn("App version is bad") logPkg.Warn("App version is bad")
bridge.publish(events.UpdateForced{}) bridge.publish(events.UpdateForced{})
}) })
@ -350,7 +398,7 @@ func (bridge *Bridge) init(tlsReporter TLSReporter) error {
// Log all manager API requests (client requests are logged separately). // Log all manager API requests (client requests are logged separately).
bridge.api.AddPostRequestHook(func(_ *resty.Client, r *resty.Response) error { bridge.api.AddPostRequestHook(func(_ *resty.Client, r *resty.Response) error {
if _, ok := proton.ClientIDFromContext(r.Request.Context()); !ok { if _, ok := proton.ClientIDFromContext(r.Request.Context()); !ok {
logrus.Infof("[MANAGER] %v: %v %v", r.Status(), r.Request.Method, r.Request.URL) logrus.WithField("pkg", "gpa/manager").Infof("%v: %v %v", r.Status(), r.Request.Method, r.Request.URL)
} }
return nil return nil
@ -359,7 +407,7 @@ func (bridge *Bridge) init(tlsReporter TLSReporter) error {
// Publish a TLS issue event if a TLS issue is encountered. // Publish a TLS issue event if a TLS issue is encountered.
bridge.tasks.Once(func(ctx context.Context) { bridge.tasks.Once(func(ctx context.Context) {
async.RangeContext(ctx, tlsReporter.GetTLSIssueCh(), func(struct{}) { async.RangeContext(ctx, tlsReporter.GetTLSIssueCh(), func(struct{}) {
logrus.Warn("TLS issue encountered") logPkg.Warn("TLS issue encountered")
bridge.publish(events.TLSIssue{}) bridge.publish(events.TLSIssue{})
}) })
}) })
@ -367,7 +415,7 @@ func (bridge *Bridge) init(tlsReporter TLSReporter) error {
// Publish a raise event if the focus service is called. // Publish a raise event if the focus service is called.
bridge.tasks.Once(func(ctx context.Context) { bridge.tasks.Once(func(ctx context.Context) {
async.RangeContext(ctx, bridge.focusService.GetRaiseCh(), func(struct{}) { async.RangeContext(ctx, bridge.focusService.GetRaiseCh(), func(struct{}) {
logrus.Info("Focus service requested raise") logPkg.Info("Focus service requested raise")
bridge.publish(events.Raise{}) bridge.publish(events.Raise{})
}) })
}) })
@ -375,7 +423,7 @@ func (bridge *Bridge) init(tlsReporter TLSReporter) error {
// Handle any IMAP events that are forwarded to the bridge from gluon. // Handle any IMAP events that are forwarded to the bridge from gluon.
bridge.tasks.Once(func(ctx context.Context) { bridge.tasks.Once(func(ctx context.Context) {
async.RangeContext(ctx, bridge.imapEventCh, func(event imapEvents.Event) { async.RangeContext(ctx, bridge.imapEventCh, func(event imapEvents.Event) {
logrus.WithField("event", fmt.Sprintf("%T", event)).Debug("Received IMAP event") logPkg.WithField("event", fmt.Sprintf("%T", event)).Debug("Received IMAP event")
bridge.handleIMAPEvent(event) bridge.handleIMAPEvent(event)
}) })
}) })
@ -383,7 +431,7 @@ func (bridge *Bridge) init(tlsReporter TLSReporter) error {
// Attempt to load users from the vault when triggered. // Attempt to load users from the vault when triggered.
bridge.goLoad = bridge.tasks.Trigger(func(ctx context.Context) { bridge.goLoad = bridge.tasks.Trigger(func(ctx context.Context) {
if err := bridge.loadUsers(ctx); err != nil { if err := bridge.loadUsers(ctx); err != nil {
logrus.WithError(err).Error("Failed to load users") logPkg.WithError(err).Error("Failed to load users")
if netErr := new(proton.NetError); !errors.As(err, &netErr) { if netErr := new(proton.NetError); !errors.As(err, &netErr) {
sentry.ReportError(bridge.reporter, "Failed to load users", err) sentry.ReportError(bridge.reporter, "Failed to load users", err)
} }
@ -396,18 +444,47 @@ func (bridge *Bridge) init(tlsReporter TLSReporter) error {
// Check for updates when triggered. // Check for updates when triggered.
bridge.goUpdate = bridge.tasks.PeriodicOrTrigger(constants.UpdateCheckInterval, 0, func(ctx context.Context) { bridge.goUpdate = bridge.tasks.PeriodicOrTrigger(constants.UpdateCheckInterval, 0, func(ctx context.Context) {
logrus.Info("Checking for updates") logPkg.Info("Checking for updates")
var versionLegacy updater.VersionInfoLegacy
var version updater.VersionInfo
var err error
useOldUpdateLogic := bridge.GetFeatureFlagValue(unleash.UpdateUseNewVersionFileStructureDisabled)
if useOldUpdateLogic {
versionLegacy, err = bridge.updater.GetVersionInfoLegacy(ctx, bridge.api, bridge.vault.GetUpdateChannel())
} else {
version, err = bridge.updater.GetVersionInfo(ctx, bridge.api)
}
version, err := bridge.updater.GetVersionInfo(ctx, bridge.api, bridge.vault.GetUpdateChannel())
if err != nil { if err != nil {
bridge.publish(events.UpdateCheckFailed{Error: err}) bridge.publish(events.UpdateCheckFailed{Error: err})
if errors.Is(err, updater.ErrVersionFileDownloadOrVerify) {
logPkg.WithError(err).Error("Cannot download or verify the version file")
if reporterErr := bridge.reporter.ReportMessageWithContext(
"Cannot download or verify the version file",
reporter.Context{"error": err},
); reporterErr != nil {
logPkg.WithError(reporterErr).Error("Failed to report version file check error")
}
}
} else {
if useOldUpdateLogic {
bridge.handleUpdateLegacy(versionLegacy)
} else { } else {
bridge.handleUpdate(version) bridge.handleUpdate(version)
} }
}
}) })
defer bridge.goUpdate() defer bridge.goUpdate()
// Install updates when available. // Install updates when available - based on old update logic
bridge.tasks.Once(func(ctx context.Context) {
async.RangeContext(ctx, bridge.installChLegacy, func(job installJobLegacy) {
bridge.installUpdateLegacy(ctx, job)
})
})
// Install updates when available - based on new update logic
bridge.tasks.Once(func(ctx context.Context) { bridge.tasks.Once(func(ctx context.Context) {
async.RangeContext(ctx, bridge.installCh, func(job installJob) { async.RangeContext(ctx, bridge.installCh, func(job installJob) {
bridge.installUpdate(ctx, job) bridge.installUpdate(ctx, job)
@ -434,7 +511,10 @@ func (bridge *Bridge) GetErrors() []error {
} }
func (bridge *Bridge) Close(ctx context.Context) { func (bridge *Bridge) Close(ctx context.Context) {
logrus.Info("Closing bridge") logPkg.Info("Closing bridge")
// Stop observability service
bridge.observabilityService.Stop()
// Stop heart beat before closing users. // Stop heart beat before closing users.
bridge.heartbeat.stop() bridge.heartbeat.stop()
@ -448,7 +528,7 @@ func (bridge *Bridge) Close(ctx context.Context) {
// Close the servers // Close the servers
if err := bridge.serverManager.CloseServers(ctx); err != nil { if err := bridge.serverManager.CloseServers(ctx); err != nil {
logrus.WithError(err).Error("Failed to close servers") logPkg.WithError(err).Error("Failed to close servers")
} }
bridge.syncService.Close() bridge.syncService.Close()
@ -459,6 +539,9 @@ func (bridge *Bridge) Close(ctx context.Context) {
// Close the focus service. // Close the focus service.
bridge.focusService.Close() bridge.focusService.Close()
// Close the unleash service.
bridge.unleashService.Close()
// Close the watchers. // Close the watchers.
bridge.watchersLock.Lock() bridge.watchersLock.Lock()
defer bridge.watchersLock.Unlock() defer bridge.watchersLock.Unlock()
@ -474,12 +557,12 @@ func (bridge *Bridge) publish(event events.Event) {
bridge.watchersLock.RLock() bridge.watchersLock.RLock()
defer bridge.watchersLock.RUnlock() defer bridge.watchersLock.RUnlock()
logrus.WithField("event", event).Debug("Publishing event") logPkg.WithField("event", event).Debug("Publishing event")
for _, watcher := range bridge.watchers { for _, watcher := range bridge.watchers {
if watcher.IsWatching(event) { if watcher.IsWatching(event) {
if ok := watcher.Send(event); !ok { if ok := watcher.Send(event); !ok {
logrus.WithField("event", event).Warn("Failed to send event to watcher") logPkg.WithField("event", event).Warn("Failed to send event to watcher")
} }
} }
} }
@ -512,13 +595,13 @@ func (bridge *Bridge) remWatcher(watcher *watcher.Watcher[events.Event]) {
} }
func (bridge *Bridge) onStatusUp(_ context.Context) { func (bridge *Bridge) onStatusUp(_ context.Context) {
logrus.Info("Handling API status up") logPkg.Info("Handling API status up")
bridge.goLoad() bridge.goLoad()
} }
func (bridge *Bridge) onStatusDown(ctx context.Context) { func (bridge *Bridge) onStatusDown(ctx context.Context) {
logrus.Info("Handling API status down") logPkg.Info("Handling API status down")
for backoff := time.Second; ; backoff = min(backoff*2, 30*time.Second) { for backoff := time.Second; ; backoff = min(backoff*2, 30*time.Second) {
select { select {
@ -526,10 +609,10 @@ func (bridge *Bridge) onStatusDown(ctx context.Context) {
return return
case <-time.After(backoff): case <-time.After(backoff):
logrus.Info("Pinging API") logPkg.Info("Pinging API")
if err := bridge.api.Ping(ctx); err != nil { if err := bridge.api.Ping(ctx); err != nil {
logrus.WithError(err).Warn("Ping failed, API is still unreachable") logPkg.WithError(err).Warn("Ping failed, API is still unreachable")
} else { } else {
return return
} }
@ -537,6 +620,49 @@ func (bridge *Bridge) onStatusDown(ctx context.Context) {
} }
} }
func (bridge *Bridge) Repair() {
var wg sync.WaitGroup
userIDs := bridge.GetUserIDs()
for _, userID := range userIDs {
logPkg.Info("Initiating repair for userID:", userID)
userInfo, err := bridge.GetUserInfo(userID)
if err != nil {
logPkg.WithError(err).Error("Failed getting user info for repair; ID:", userID)
continue
}
if userInfo.State != Connected {
logPkg.Info("User is not connected. Repair will be executed on following successful log in.", userID)
if err := bridge.vault.GetUser(userID, func(user *vault.User) {
if err := user.SetShouldSync(true); err != nil {
logPkg.WithError(err).Error("Failed setting vault should sync for user:", userID)
}
}); err != nil {
logPkg.WithError(err).Error("Unable to get user vault when scheduling repair:", userID)
}
continue
}
bridgeUser, ok := bridge.users[userID]
if !ok {
logPkg.Info("UserID does not exist in bridge user map", userID)
continue
}
wg.Add(1)
go func(userID string) {
defer wg.Done()
if err = bridgeUser.TriggerRepair(); err != nil {
logPkg.WithError(err).Error("Failed re-syncing IMAP for userID", userID)
}
}(userID)
}
wg.Wait()
}
func loadTLSConfig(vault *vault.Vault) (*tls.Config, error) { func loadTLSConfig(vault *vault.Vault) (*tls.Config, error) {
cert, err := tls.X509KeyPair(vault.GetBridgeTLSCert()) cert, err := tls.X509KeyPair(vault.GetBridgeTLSCert())
if err != nil { if err != nil {
@ -549,10 +675,113 @@ func loadTLSConfig(vault *vault.Vault) (*tls.Config, error) {
}, nil }, nil
} }
func min(a, b time.Duration) time.Duration { func (bridge *Bridge) HasAPIConnection() bool {
if a < b { return bridge.api.GetStatus() == proton.StatusUp
return a
} }
return b // verifyUsernameChange - works only on macOS
// it attempts to check whether a username change has taken place by comparing the gluon DB path (which is static and provided by bridge)
// to the gluon Cache path - which can be modified by the user and is stored in the vault;
// if a username discrepancy is detected, and the cache folder does not exist with the "old" username
// then we verify whether the gluon cache exists using the "new" username (provided by the DB path in this case)
// if so we modify the cache directory in the user vault.
func (bridge *Bridge) verifyUsernameChange() {
if runtime.GOOS != "darwin" {
return
}
gluonDBPath, err := bridge.GetGluonDataDir()
if err != nil {
logPkg.WithError(err).Error("Failed to get gluon db path")
return
}
gluonCachePath := bridge.GetGluonCacheDir()
// If the cache folder exists even on another user account or is in `/Users/Shared` we would still be able to access it
// though it depends on the permissions; this is an edge-case.
if _, err := os.Stat(gluonCachePath); err == nil {
return
}
newCacheDir := GetUpdatedCachePath(gluonDBPath, gluonCachePath)
if newCacheDir == "" {
return
}
if _, err := os.Stat(newCacheDir); err == nil {
logPkg.Info("Username change detected. Trying to restore gluon cache directory")
if err = bridge.vault.SetGluonDir(newCacheDir); err != nil {
logPkg.WithError(err).Error("Failed to restore gluon cache directory")
return
}
logPkg.Info("Successfully restored gluon cache directory")
}
}
func GetUpdatedCachePath(gluonDBPath, gluonCachePath string) string {
// If gluon cache is moved to an external drive; regex find will fail; as is expected
cachePathMatches := usernameChangeRegex.FindStringSubmatch(gluonCachePath)
if len(cachePathMatches) < 2 {
return ""
}
cacheUsername := cachePathMatches[1]
dbPathMatches := usernameChangeRegex.FindStringSubmatch(gluonDBPath)
if len(dbPathMatches) < 2 {
return ""
}
dbUsername := dbPathMatches[1]
if cacheUsername == dbUsername {
return ""
}
return strings.Replace(gluonCachePath, "/Users/"+cacheUsername+"/", "/Users/"+dbUsername+"/", 1)
}
func (bridge *Bridge) GetFeatureFlagValue(key string) bool {
return bridge.unleashService.GetFlagValue(key)
}
func (bridge *Bridge) PushObservabilityMetric(metric proton.ObservabilityMetric) {
bridge.observabilityService.AddMetrics(metric)
}
func (bridge *Bridge) PushDistinctObservabilityMetrics(errType observability.DistinctionMetricTypeEnum, metrics ...proton.ObservabilityMetric) {
bridge.observabilityService.AddDistinctMetrics(errType, metrics...)
}
func (bridge *Bridge) ModifyObservabilityHeartbeatInterval(duration time.Duration) {
bridge.observabilityService.ModifyHeartbeatInterval(duration)
}
func (bridge *Bridge) ReportMessageWithContext(message string, messageCtx reporter.Context) {
if err := bridge.reporter.ReportMessageWithContext(message, messageCtx); err != nil {
logPkg.WithFields(logrus.Fields{
"err": err,
"sentryMessage": message,
"messageCtx": messageCtx,
}).Info("Error occurred when sending Report to Sentry")
}
}
// GetUsers is only used for testing purposes.
func (bridge *Bridge) GetUsers() map[string]*user.User {
return bridge.users
}
// SetCurrentVersionTest - sets the current version of bridge; should only be used for tests.
func (bridge *Bridge) SetCurrentVersionTest(version *semver.Version) {
bridge.curVersion = version
bridge.newVersion = version
}
// SetHostVersionGetterTest - sets the OS version helper func; only used for testing.
func (bridge *Bridge) SetHostVersionGetterTest(fn func(host types.Host) string) {
bridge.getHostVersion = fn
}
// SetRolloutPercentageTest - sets the rollout percentage; should only be used for testing.
func (bridge *Bridge) SetRolloutPercentageTest(rollout float64) error {
return bridge.vault.SetUpdateRollout(rollout)
} }

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //
@ -45,6 +45,7 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/focus" "github.com/ProtonMail/proton-bridge/v3/internal/focus"
"github.com/ProtonMail/proton-bridge/v3/internal/locations" "github.com/ProtonMail/proton-bridge/v3/internal/locations"
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapsmtpserver" "github.com/ProtonMail/proton-bridge/v3/internal/services/imapsmtpserver"
"github.com/ProtonMail/proton-bridge/v3/internal/unleash"
"github.com/ProtonMail/proton-bridge/v3/internal/updater" "github.com/ProtonMail/proton-bridge/v3/internal/updater"
"github.com/ProtonMail/proton-bridge/v3/internal/user" "github.com/ProtonMail/proton-bridge/v3/internal/user"
"github.com/ProtonMail/proton-bridge/v3/internal/useragent" "github.com/ProtonMail/proton-bridge/v3/internal/useragent"
@ -76,7 +77,7 @@ func init() {
func TestBridge_ConnStatus(t *testing.T) { func TestBridge_ConnStatus(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, _ *bridge.Mocks) {
// Get a stream of connection status events. // Get a stream of connection status events.
eventCh, done := bridge.GetEvents(events.ConnStatusUp{}, events.ConnStatusDown{}) eventCh, done := bridge.GetEvents(events.ConnStatusUp{}, events.ConnStatusDown{})
defer done() defer done()
@ -125,7 +126,7 @@ func TestBridge_TLSIssue(t *testing.T) {
func TestBridge_Focus(t *testing.T) { func TestBridge_Focus(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, _ *bridge.Mocks) {
// Get a stream of TLS issue events. // Get a stream of TLS issue events.
raiseCh, done := bridge.GetEvents(events.Raise{}) raiseCh, done := bridge.GetEvents(events.Raise{})
defer done() defer done()
@ -156,7 +157,7 @@ func TestBridge_UserAgent(t *testing.T) {
calls = append(calls, call) calls = append(calls, call)
}) })
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, _ *bridge.Mocks) {
// Set the platform to something other than the default. // Set the platform to something other than the default.
bridge.SetCurrentPlatform("platform") bridge.SetCurrentPlatform("platform")
@ -183,21 +184,12 @@ func TestBridge_UserAgent_Persistence(t *testing.T) {
_, _, err := s.CreateUser(otherUser, otherPassword) _, _, err := s.CreateUser(otherUser, otherPassword)
require.NoError(t, err) require.NoError(t, err)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) { withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
imapWaiter := waitForIMAPServerReady(b)
defer imapWaiter.Done()
smtpWaiter := waitForSMTPServerReady(b)
defer smtpWaiter.Done()
currentUserAgent := b.GetCurrentUserAgent() currentUserAgent := b.GetCurrentUserAgent()
require.Contains(t, currentUserAgent, useragent.DefaultUserAgent) require.Contains(t, currentUserAgent, useragent.DefaultUserAgent)
require.NoError(t, getErr(b.LoginFull(ctx, otherUser, otherPassword, nil, nil))) require.NoError(t, getErr(b.LoginFull(ctx, otherUser, otherPassword, nil, nil)))
imapWaiter.Wait()
smtpWaiter.Wait()
imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort())) imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
require.NoError(t, err) require.NoError(t, err)
defer func() { _ = imapClient.Logout() }() defer func() { _ = imapClient.Logout() }()
@ -220,7 +212,7 @@ func TestBridge_UserAgent_Persistence(t *testing.T) {
require.Contains(t, b.GetCurrentUserAgent(), "MyFancyClient/0.1.2") require.Contains(t, b.GetCurrentUserAgent(), "MyFancyClient/0.1.2")
}) })
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, _ *bridge.Mocks) {
currentUserAgent := bridge.GetCurrentUserAgent() currentUserAgent := bridge.GetCurrentUserAgent()
require.Contains(t, currentUserAgent, "MyFancyClient/0.1.2") require.Contains(t, currentUserAgent, "MyFancyClient/0.1.2")
}) })
@ -234,22 +226,13 @@ func TestBridge_UserAgentFromUnknownClient(t *testing.T) {
_, _, err := s.CreateUser(otherUser, otherPassword) _, _, err := s.CreateUser(otherUser, otherPassword)
require.NoError(t, err) require.NoError(t, err)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) { withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
imapWaiter := waitForIMAPServerReady(b)
defer imapWaiter.Done()
smtpWaiter := waitForSMTPServerReady(b)
defer smtpWaiter.Done()
currentUserAgent := b.GetCurrentUserAgent() currentUserAgent := b.GetCurrentUserAgent()
require.Contains(t, currentUserAgent, useragent.DefaultUserAgent) require.Contains(t, currentUserAgent, useragent.DefaultUserAgent)
userID, err := b.LoginFull(context.Background(), username, password, nil, nil) userID, err := b.LoginFull(context.Background(), username, password, nil, nil)
require.NoError(t, err) require.NoError(t, err)
imapWaiter.Wait()
smtpWaiter.Wait()
imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort())) imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
require.NoError(t, err) require.NoError(t, err)
defer func() { _ = imapClient.Logout() }() defer func() { _ = imapClient.Logout() }()
@ -273,22 +256,13 @@ func TestBridge_UserAgentFromSMTPClient(t *testing.T) {
_, _, err := s.CreateUser(otherUser, otherPassword) _, _, err := s.CreateUser(otherUser, otherPassword)
require.NoError(t, err) require.NoError(t, err)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) { withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
imapWaiter := waitForIMAPServerReady(b)
defer imapWaiter.Done()
smtpWaiter := waitForSMTPServerReady(b)
defer smtpWaiter.Done()
currentUserAgent := b.GetCurrentUserAgent() currentUserAgent := b.GetCurrentUserAgent()
require.Contains(t, currentUserAgent, useragent.DefaultUserAgent) require.Contains(t, currentUserAgent, useragent.DefaultUserAgent)
userID, err := b.LoginFull(context.Background(), username, password, nil, nil) userID, err := b.LoginFull(context.Background(), username, password, nil, nil)
require.NoError(t, err) require.NoError(t, err)
imapWaiter.Wait()
smtpWaiter.Wait()
client, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(b.GetSMTPPort()))) client, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(b.GetSMTPPort())))
require.NoError(t, err) require.NoError(t, err)
defer client.Close() //nolint:errcheck defer client.Close() //nolint:errcheck
@ -332,18 +306,9 @@ func TestBridge_UserAgentFromIMAPID(t *testing.T) {
_, _, err := s.CreateUser(otherUser, otherPassword) _, _, err := s.CreateUser(otherUser, otherPassword)
require.NoError(t, err) require.NoError(t, err)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) { withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
imapWaiter := waitForIMAPServerReady(b)
defer imapWaiter.Done()
smtpWaiter := waitForSMTPServerReady(b)
defer smtpWaiter.Done()
require.NoError(t, getErr(b.LoginFull(ctx, otherUser, otherPassword, nil, nil))) require.NoError(t, getErr(b.LoginFull(ctx, otherUser, otherPassword, nil, nil)))
imapWaiter.Wait()
smtpWaiter.Wait()
imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort())) imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
require.NoError(t, err) require.NoError(t, err)
defer func() { _ = imapClient.Logout() }() defer func() { _ = imapClient.Logout() }()
@ -401,13 +366,13 @@ func TestBridge_Cookies(t *testing.T) {
}) })
// Start bridge and add a user so that API assigns us a session ID via cookie. // Start bridge and add a user so that API assigns us a session ID via cookie.
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, _ *bridge.Mocks) {
_, err := bridge.LoginFull(context.Background(), username, password, nil, nil) _, err := bridge.LoginFull(context.Background(), username, password, nil, nil)
require.NoError(t, err) require.NoError(t, err)
}) })
// Start bridge again and check that it uses the same session ID. // Start bridge again and check that it uses the same session ID.
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) {
// ... // ...
}) })
@ -419,9 +384,14 @@ func TestBridge_Cookies(t *testing.T) {
}) })
} }
func TestBridge_CheckUpdate(t *testing.T) { func TestBridge_CheckUpdate_Legacy(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) {
unleash.ModifyPollPeriodAndJitter(500*time.Millisecond, 0)
s.PushFeatureFlag(unleash.UpdateUseNewVersionFileStructureDisabled)
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) {
// Wait for FF poll.
time.Sleep(600 * time.Millisecond)
// Disable autoupdate for this test. // Disable autoupdate for this test.
require.NoError(t, bridge.SetAutoUpdate(false)) require.NoError(t, bridge.SetAutoUpdate(false))
@ -436,7 +406,7 @@ func TestBridge_CheckUpdate(t *testing.T) {
require.Equal(t, events.UpdateNotAvailable{}, <-noUpdateCh) require.Equal(t, events.UpdateNotAvailable{}, <-noUpdateCh)
// Simulate a new version being available. // Simulate a new version being available.
mocks.Updater.SetLatestVersion(v2_4_0, v2_3_0) mocks.Updater.SetLatestVersionLegacy(v2_4_0, v2_3_0)
// Get a stream of update available events. // Get a stream of update available events.
updateCh, done := bridge.GetEvents(events.UpdateAvailable{}) updateCh, done := bridge.GetEvents(events.UpdateAvailable{})
@ -447,7 +417,7 @@ func TestBridge_CheckUpdate(t *testing.T) {
// We should receive an event indicating that an update is available. // We should receive an event indicating that an update is available.
require.Equal(t, events.UpdateAvailable{ require.Equal(t, events.UpdateAvailable{
Version: updater.VersionInfo{ VersionLegacy: updater.VersionInfoLegacy{
Version: v2_4_0, Version: v2_4_0,
MinAuto: v2_3_0, MinAuto: v2_3_0,
RolloutProportion: 1.0, RolloutProportion: 1.0,
@ -459,25 +429,30 @@ func TestBridge_CheckUpdate(t *testing.T) {
}) })
} }
func TestBridge_AutoUpdate(t *testing.T) { func TestBridge_AutoUpdate_Legacy(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) { unleash.ModifyPollPeriodAndJitter(500*time.Millisecond, 0)
s.PushFeatureFlag(unleash.UpdateUseNewVersionFileStructureDisabled)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
// Wait for FF poll.
time.Sleep(600 * time.Millisecond)
// Enable autoupdate for this test. // Enable autoupdate for this test.
require.NoError(t, bridge.SetAutoUpdate(true)) require.NoError(t, b.SetAutoUpdate(true))
// Get a stream of update events. // Get a stream of update events.
updateCh, done := bridge.GetEvents(events.UpdateInstalled{}) updateCh, done := b.GetEvents(events.UpdateInstalled{})
defer done() defer done()
// Simulate a new version being available. // Simulate a new version being available.
mocks.Updater.SetLatestVersion(v2_4_0, v2_3_0) mocks.Updater.SetLatestVersionLegacy(v2_4_0, v2_3_0)
// Check for updates. // Check for updates.
bridge.CheckForUpdates() b.CheckForUpdates()
// We should receive an event indicating that the update was silently installed. // We should receive an event indicating that the update was silently installed.
require.Equal(t, events.UpdateInstalled{ require.Equal(t, events.UpdateInstalled{
Version: updater.VersionInfo{ VersionLegacy: updater.VersionInfoLegacy{
Version: v2_4_0, Version: v2_4_0,
MinAuto: v2_3_0, MinAuto: v2_3_0,
RolloutProportion: 1.0, RolloutProportion: 1.0,
@ -488,9 +463,14 @@ func TestBridge_AutoUpdate(t *testing.T) {
}) })
} }
func TestBridge_ManualUpdate(t *testing.T) { func TestBridge_ManualUpdate_Legacy(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) {
unleash.ModifyPollPeriodAndJitter(500*time.Millisecond, 0)
s.PushFeatureFlag(unleash.UpdateUseNewVersionFileStructureDisabled)
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) {
// Wait for FF poll.
time.Sleep(600 * time.Millisecond)
// Disable autoupdate for this test. // Disable autoupdate for this test.
require.NoError(t, bridge.SetAutoUpdate(false)) require.NoError(t, bridge.SetAutoUpdate(false))
@ -499,14 +479,14 @@ func TestBridge_ManualUpdate(t *testing.T) {
defer done() defer done()
// Simulate a new version being available, but it's too new for us. // Simulate a new version being available, but it's too new for us.
mocks.Updater.SetLatestVersion(v2_4_0, v2_4_0) mocks.Updater.SetLatestVersionLegacy(v2_4_0, v2_4_0)
// Check for updates. // Check for updates.
bridge.CheckForUpdates() bridge.CheckForUpdates()
// We should receive an event indicating an update is available, but we can't install it. // We should receive an event indicating an update is available, but we can't install it.
require.Equal(t, events.UpdateAvailable{ require.Equal(t, events.UpdateAvailable{
Version: updater.VersionInfo{ VersionLegacy: updater.VersionInfoLegacy{
Version: v2_4_0, Version: v2_4_0,
MinAuto: v2_4_0, MinAuto: v2_4_0,
RolloutProportion: 1.0, RolloutProportion: 1.0,
@ -520,7 +500,12 @@ func TestBridge_ManualUpdate(t *testing.T) {
func TestBridge_ForceUpdate(t *testing.T) { func TestBridge_ForceUpdate(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) { unleash.ModifyPollPeriodAndJitter(500*time.Millisecond, 0)
s.PushFeatureFlag(unleash.UpdateUseNewVersionFileStructureDisabled)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
// Wait for FF poll.
time.Sleep(600 * time.Millisecond)
// Get a stream of update events. // Get a stream of update events.
updateCh, done := bridge.GetEvents(events.UpdateForced{}) updateCh, done := bridge.GetEvents(events.UpdateForced{})
defer done() defer done()
@ -543,7 +528,7 @@ func TestBridge_BadVaultKey(t *testing.T) {
var userID string var userID string
// Login a user. // Login a user.
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, _ *bridge.Mocks) {
newUserID, err := bridge.LoginFull(context.Background(), username, password, nil, nil) newUserID, err := bridge.LoginFull(context.Background(), username, password, nil, nil)
require.NoError(t, err) require.NoError(t, err)
@ -551,17 +536,17 @@ func TestBridge_BadVaultKey(t *testing.T) {
}) })
// Start bridge with the correct vault key -- it should load the users correctly. // Start bridge with the correct vault key -- it should load the users correctly.
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, _ *bridge.Mocks) {
require.ElementsMatch(t, []string{userID}, bridge.GetUserIDs()) require.ElementsMatch(t, []string{userID}, bridge.GetUserIDs())
}) })
// Start bridge with a bad vault key, the vault will be wiped and bridge will show no users. // Start bridge with a bad vault key, the vault will be wiped and bridge will show no users.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, []byte("bad"), func(bridge *bridge.Bridge, mocks *bridge.Mocks) { withBridge(ctx, t, s.GetHostURL(), netCtl, locator, []byte("bad"), func(bridge *bridge.Bridge, _ *bridge.Mocks) {
require.Empty(t, bridge.GetUserIDs()) require.Empty(t, bridge.GetUserIDs())
}) })
// Start bridge with a nil vault key, the vault will be wiped and bridge will show no users. // Start bridge with a nil vault key, the vault will be wiped and bridge will show no users.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, nil, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { withBridge(ctx, t, s.GetHostURL(), netCtl, locator, nil, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
require.Empty(t, bridge.GetUserIDs()) require.Empty(t, bridge.GetUserIDs())
}) })
}) })
@ -571,7 +556,7 @@ 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
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, _ *bridge.Mocks) {
_, err := bridge.LoginFull(context.Background(), username, password, nil, nil) _, err := bridge.LoginFull(context.Background(), username, password, nil, nil)
require.NoError(t, err) require.NoError(t, err)
@ -586,7 +571,7 @@ func TestBridge_MissingGluonStore(t *testing.T) {
require.NoError(t, os.RemoveAll(gluonDir)) require.NoError(t, os.RemoveAll(gluonDir))
// Bridge starts but can't find the gluon store dir; there should be no error. // Bridge starts but can't find the gluon store dir; there should be no error.
withBridgeWaitForServers(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { withBridgeWaitForServers(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(_ *bridge.Bridge, _ *bridge.Mocks) {
// ... // ...
}) })
}) })
@ -596,7 +581,7 @@ func TestBridge_MissingGluonDatabase(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
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, _ *bridge.Mocks) {
_, err := bridge.LoginFull(context.Background(), username, password, nil, nil) _, err := bridge.LoginFull(context.Background(), username, password, nil, nil)
require.NoError(t, err) require.NoError(t, err)
@ -609,7 +594,7 @@ func TestBridge_MissingGluonDatabase(t *testing.T) {
require.NoError(t, os.RemoveAll(gluonDir)) require.NoError(t, os.RemoveAll(gluonDir))
// Bridge starts but can't find the gluon database dir; there should be no error. // 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) {
// ... // ...
}) })
}) })
@ -623,7 +608,7 @@ func TestBridge_AddressWithoutKeys(t *testing.T) {
) )
defer m.Close() defer m.Close()
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, _ *bridge.Mocks) {
// Watch for sync finished event. // Watch for sync finished event.
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{})) syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
defer done() defer done()
@ -633,7 +618,7 @@ func TestBridge_AddressWithoutKeys(t *testing.T) {
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.
aliasAddrID, err := s.CreateAddress(userID, "alias@pm.me", []byte("password")) aliasAddrID, err := s.CreateAddress(userID, "alias@pm.me", []byte("password"), true)
require.NoError(t, err) require.NoError(t, err)
// Create an API client so we can remove the address keys. // Create an API client so we can remove the address keys.
@ -699,7 +684,7 @@ func TestBridge_FactoryReset(t *testing.T) {
func TestBridge_InitGluonDirectory(t *testing.T) { func TestBridge_InitGluonDirectory(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(b *bridge.Bridge, mocks *bridge.Mocks) { withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
configDir, err := b.GetGluonDataDir() configDir, err := b.GetGluonDataDir()
require.NoError(t, err) require.NoError(t, err)
@ -714,22 +699,13 @@ func TestBridge_InitGluonDirectory(t *testing.T) {
func TestBridge_LoginFailed(t *testing.T) { func TestBridge_LoginFailed(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, _ *bridge.Mocks) {
imapWaiter := waitForIMAPServerReady(bridge)
defer imapWaiter.Done()
smtpWaiter := waitForSMTPServerReady(bridge)
defer smtpWaiter.Done()
failCh, done := chToType[events.Event, events.IMAPLoginFailed](bridge.GetEvents(events.IMAPLoginFailed{})) failCh, done := chToType[events.Event, events.IMAPLoginFailed](bridge.GetEvents(events.IMAPLoginFailed{}))
defer done() defer done()
_, err := bridge.LoginFull(ctx, username, password, nil, nil) _, err := bridge.LoginFull(ctx, username, password, nil, nil)
require.NoError(t, err) require.NoError(t, err)
imapWaiter.Wait()
smtpWaiter.Wait()
imapClient, err := eventuallyDial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort()))) imapClient, err := eventuallyDial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetIMAPPort())))
require.NoError(t, err) require.NoError(t, err)
@ -751,18 +727,12 @@ func TestBridge_ChangeCacheDirectory(t *testing.T) {
createNumMessages(ctx, t, c, addrID, labelID, 10) createNumMessages(ctx, t, c, addrID, labelID, 10)
}) })
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) { withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
newCacheDir := t.TempDir() newCacheDir := t.TempDir()
currentCacheDir := b.GetGluonCacheDir() currentCacheDir := b.GetGluonCacheDir()
configDir, err := b.GetGluonDataDir() configDir, err := b.GetGluonDataDir()
require.NoError(t, err) require.NoError(t, err)
imapWaiter := waitForIMAPServerReady(b)
defer imapWaiter.Done()
smtpWaiter := waitForSMTPServerReady(b)
defer smtpWaiter.Done()
// Login the user. // Login the user.
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{})) syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
defer done() defer done()
@ -796,9 +766,6 @@ func TestBridge_ChangeCacheDirectory(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.True(t, info.State == bridge.Connected) require.True(t, info.State == bridge.Connected)
imapWaiter.Wait()
smtpWaiter.Wait()
client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort())) client, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
require.NoError(t, err) require.NoError(t, err)
require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass))) require.NoError(t, client.Login(info.Addresses[0], string(info.BridgePass)))
@ -818,7 +785,7 @@ func TestBridge_ChangeAddressOrder(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
// Create a second address for the user. // Create a second address for the user.
aliasID, err := s.CreateAddress(userID, "alias@"+s.GetDomain(), password) aliasID, err := s.CreateAddress(userID, "alias@"+s.GetDomain(), password, true)
require.NoError(t, err) require.NoError(t, err)
// Create 10 messages for the user. // Create 10 messages for the user.
@ -826,7 +793,7 @@ func TestBridge_ChangeAddressOrder(t *testing.T) {
createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 10) createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 10)
}) })
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) { withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
// Log the user in with its first address. // Log the user in with its first address.
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{})) syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
defer done() defer done()
@ -854,7 +821,7 @@ func TestBridge_ChangeAddressOrder(t *testing.T) {
require.NoError(t, c.OrderAddresses(ctx, proton.OrderAddressesReq{AddressIDs: []string{aliasID, addrID}})) 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) { withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
// We should still see 10 messages in the inbox. // We should still see 10 messages in the inbox.
info, err := b.GetUserInfo(userID) info, err := b.GetUserInfo(userID)
require.NoError(t, err) require.NoError(t, err)
@ -1131,3 +1098,57 @@ func waitForIMAPServerStopped(b *bridge.Bridge) *eventWaiter {
cancel: cancel, cancel: cancel,
} }
} }
func TestBridge_GetUpdatedCachePath(t *testing.T) {
type TestData struct {
gluonDBPath string
gluonCachePath string
shouldChange bool
}
dataArr := []TestData{
{
gluonDBPath: "/Users/test/",
gluonCachePath: "/Users/test/gluon",
shouldChange: false,
}, {
gluonDBPath: "/Users/test/",
gluonCachePath: "/Users/tester/gluon",
shouldChange: true,
}, {
gluonDBPath: "/Users/testing/",
gluonCachePath: "/Users/test/gluon",
shouldChange: true,
},
{
gluonDBPath: "/Users/testing/",
gluonCachePath: "/Users/test/gluon",
shouldChange: true,
},
{
gluonDBPath: "/Users/testing/",
gluonCachePath: "/Volumes/test/gluon",
shouldChange: false,
},
{
gluonDBPath: "/Volumes/test/",
gluonCachePath: "/Users/test/gluon",
shouldChange: false,
},
{
gluonDBPath: "/XXX/test/",
gluonCachePath: "/Users/test/gluon",
shouldChange: false,
},
{
gluonDBPath: "/XXX/test/",
gluonCachePath: "/YYY/test/gluon",
shouldChange: false,
},
}
for _, el := range dataArr {
newCachePath := bridge.GetUpdatedCachePath(el.gluonDBPath, el.gluonCachePath)
require.Equal(t, el.shouldChange, newCachePath != "" && newCachePath != el.gluonCachePath)
}
}

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //
@ -25,7 +25,6 @@ import (
"github.com/ProtonMail/go-proton-api" "github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/proton-bridge/v3/internal/constants" "github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/logging" "github.com/ProtonMail/proton-bridge/v3/internal/logging"
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/vault" "github.com/ProtonMail/proton-bridge/v3/internal/vault"
) )
@ -80,12 +79,6 @@ func (bridge *Bridge) ReportBug(ctx context.Context, report *ReportBugReq) error
return err return err
} }
safe.RLock(func() {
for _, user := range bridge.users {
user.ReportBugSent()
}
}, bridge.usersLock)
// if we have a token we can append more attachment to the bugReport // if we have a token we can append more attachment to the bugReport
for i, att := range attachments { for i, att := range attachments {
if i == 0 && report.IncludeLogs { if i == 0 && report.IncludeLogs {

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //
@ -34,7 +34,7 @@ import (
// ConfigureAppleMail configures Apple Mail for the given userID and address. // ConfigureAppleMail configures Apple Mail for the given userID and address.
// If configuring Apple Mail for Catalina or newer, it ensures Bridge is using SSL. // If configuring Apple Mail for Catalina or newer, it ensures Bridge is using SSL.
func (bridge *Bridge) ConfigureAppleMail(ctx context.Context, userID, address string) error { func (bridge *Bridge) ConfigureAppleMail(ctx context.Context, userID, address string) error {
logrus.WithFields(logrus.Fields{ logPkg.WithFields(logrus.Fields{
"userID": userID, "userID": userID,
"address": logging.Sensitive(address), "address": logging.Sensitive(address),
}).Info("Configuring Apple Mail") }).Info("Configuring Apple Mail")

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //
@ -65,7 +65,11 @@ func (bridge *Bridge) CheckClientState(ctx context.Context, checkFlags bool, pro
if progressCB != nil { if progressCB != nil {
progressCB(fmt.Sprintf("Checking state for user %v", usr.Name())) progressCB(fmt.Sprintf("Checking state for user %v", usr.Name()))
} }
log := logrus.WithField("user", usr.Name()).WithField("diag", "state-check") log := logrus.WithFields(logrus.Fields{
"pkg": "bridge/debug",
"user": usr.Name(),
"diag": "state-check",
})
log.Debug("Retrieving all server metadata") log.Debug("Retrieving all server metadata")
meta, err := usr.GetDiagnosticMetadata(ctx) meta, err := usr.GetDiagnosticMetadata(ctx)
if err != nil { if err != nil {
@ -280,7 +284,7 @@ func clientGetMessageIDs(client *goimapclient.Client, mailbox string) (map[strin
internalID, ok := header.GetChecked("X-Pm-Internal-Id") internalID, ok := header.GetChecked("X-Pm-Internal-Id")
if !ok { if !ok {
logrus.Errorf("Message %v does not have internal id", internalID) logrus.WithField("pkg", "bridge/debug").Errorf("Message %v does not have internal id", internalID)
continue continue
} }

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //
@ -64,9 +64,6 @@ func TestBridge_HandleDraftsSendFromOtherClient(t *testing.T) {
// The initial user should be fully synced. // The initial user should be fully synced.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, _ *bridge.Mocks) { withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
waiter := waitForIMAPServerReady(b)
defer waiter.Done()
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{})) syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
defer done() defer done()
@ -74,7 +71,6 @@ func TestBridge_HandleDraftsSendFromOtherClient(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, userID, (<-syncCh).UserID) require.Equal(t, userID, (<-syncCh).UserID)
waiter.Wait()
info, err := b.GetUserInfo(userID) info, err := b.GetUserInfo(userID)
require.NoError(t, err) require.NoError(t, err)

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //
@ -73,15 +73,15 @@ func (h *heartBeatState) init(bridge *Bridge, manager telemetry.HeartbeatManager
for _, user := range bridge.users { for _, user := range bridge.users {
if user.GetAddressMode() == vault.SplitMode { if user.GetAddressMode() == vault.SplitMode {
splitMode = true splitMode = true
break
} }
h.SetUserPlan(user.GetUserPlanName())
} }
var nbAccount = len(bridge.users) var numberConnectedAccounts = len(bridge.users)
h.SetNbAccount(nbAccount) h.SetNumberConnectedAccounts(numberConnectedAccounts)
h.SetSplitMode(splitMode) h.SetSplitMode(splitMode)
// Do not try to send if there is no user yet. // Do not try to send if there is no user yet.
if nbAccount > 0 { if numberConnectedAccounts > 0 {
defer h.start() defer h.start()
} }
}, bridge.usersLock) }, bridge.usersLock)
@ -97,7 +97,7 @@ func (h *heartBeatState) start() {
h.taskStarted = true h.taskStarted = true
h.task.PeriodicOrTrigger(h.taskInterval, 0, func(ctx context.Context) { h.task.PeriodicOrTrigger(h.taskInterval, 0, func(ctx context.Context) {
logrus.Debug("Checking for heartbeat") logrus.WithField("pkg", "bridge/heartbeat").Debug("Checking for heartbeat")
h.TrySending(ctx) h.TrySending(ctx)
}) })
@ -135,7 +135,7 @@ func (bridge *Bridge) SendHeartbeat(ctx context.Context, heartbeat *telemetry.He
if err := bridge.reporter.ReportMessageWithContext("Cannot parse heartbeat data.", reporter.Context{ if err := bridge.reporter.ReportMessageWithContext("Cannot parse heartbeat data.", reporter.Context{
"error": err, "error": err,
}); err != nil { }); err != nil {
logrus.WithError(err).Error("Failed to parse heartbeat data.") logrus.WithField("pkg", "bridge/heartbeat").WithError(err).Error("Failed to parse heartbeat data.")
} }
return false return false
} }

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //
@ -17,7 +17,9 @@
package bridge package bridge
import "github.com/sirupsen/logrus" import (
"github.com/sirupsen/logrus"
)
func (bridge *Bridge) GetCurrentUserAgent() string { func (bridge *Bridge) GetCurrentUserAgent() string {
return bridge.identifier.GetUserAgent() return bridge.identifier.GetUserAgent()
@ -30,6 +32,8 @@ func (bridge *Bridge) SetCurrentPlatform(platform string) {
func (bridge *Bridge) setUserAgent(name, version string) { func (bridge *Bridge) setUserAgent(name, version string) {
currentUserAgent := bridge.identifier.GetClientString() currentUserAgent := bridge.identifier.GetClientString()
bridge.heartbeat.SetContactedByAppleNotes(name)
bridge.identifier.SetClient(name, version) bridge.identifier.SetClient(name, version)
newUserAgent := bridge.identifier.GetClientString() newUserAgent := bridge.identifier.GetClientString()
@ -54,6 +58,7 @@ func (b *bridgeUserAgentUpdater) HasClient() bool {
} }
func (b *bridgeUserAgentUpdater) SetClient(name, version string) { func (b *bridgeUserAgentUpdater) SetClient(name, version string) {
b.heartbeat.SetContactedByAppleNotes(name)
b.identifier.SetClient(name, version) b.identifier.SetClient(name, version)
} }

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //
@ -26,6 +26,7 @@ import (
imapEvents "github.com/ProtonMail/gluon/events" imapEvents "github.com/ProtonMail/gluon/events"
"github.com/ProtonMail/proton-bridge/v3/internal/events" "github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapsmtpserver" "github.com/ProtonMail/proton-bridge/v3/internal/services/imapsmtpserver"
"github.com/ProtonMail/proton-bridge/v3/internal/unleash"
"github.com/ProtonMail/proton-bridge/v3/internal/useragent" "github.com/ProtonMail/proton-bridge/v3/internal/useragent"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -35,10 +36,12 @@ func (bridge *Bridge) restartIMAP(ctx context.Context) error {
} }
func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) { func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) {
log := logrus.WithField("pkg", "bridge/event/imap")
switch event := event.(type) { switch event := event.(type) {
case imapEvents.UserAdded: case imapEvents.UserAdded:
for labelID, count := range event.Counts { for labelID, count := range event.Counts {
logrus.WithFields(logrus.Fields{ log.WithFields(logrus.Fields{
"gluonID": event.UserID, "gluonID": event.UserID,
"labelID": labelID, "labelID": labelID,
"count": count, "count": count,
@ -46,7 +49,7 @@ func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) {
} }
case imapEvents.IMAPID: case imapEvents.IMAPID:
logrus.WithFields(logrus.Fields{ log.WithFields(logrus.Fields{
"sessionID": event.SessionID, "sessionID": event.SessionID,
"name": event.IMAPID.Name, "name": event.IMAPID.Name,
"version": event.IMAPID.Version, "version": event.IMAPID.Version,
@ -57,7 +60,7 @@ func (bridge *Bridge) handleIMAPEvent(event imapEvents.Event) {
} }
case imapEvents.LoginFailed: case imapEvents.LoginFailed:
logrus.WithFields(logrus.Fields{ log.WithFields(logrus.Fields{
"sessionID": event.SessionID, "sessionID": event.SessionID,
"username": event.Username, "username": event.Username,
"pkg": "imap", "pkg": "imap",
@ -91,6 +94,10 @@ func (b *bridgeIMAPSettings) LogServer() bool {
return b.b.logIMAPServer return b.b.logIMAPServer
} }
func (b *bridgeIMAPSettings) DisableIMAPAuthenticate() bool {
return b.b.unleashService.GetFlagValue(unleash.IMAPAuthenticateCommandDisabled)
}
func (b *bridgeIMAPSettings) Port() int { func (b *bridgeIMAPSettings) Port() int {
return b.b.vault.GetIMAPPort() return b.b.vault.GetIMAPPort()
} }

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //

View File

@ -54,6 +54,9 @@ func NewMocks(tb testing.TB, version, minAuto *semver.Version) *Mocks {
mocks.Heartbeat.EXPECT().IsTelemetryAvailable(gomock.Any()).AnyTimes() mocks.Heartbeat.EXPECT().IsTelemetryAvailable(gomock.Any()).AnyTimes()
mocks.Heartbeat.EXPECT().GetHeartbeatPeriodicInterval().AnyTimes().Return(500 * time.Millisecond) mocks.Heartbeat.EXPECT().GetHeartbeatPeriodicInterval().AnyTimes().Return(500 * time.Millisecond)
// It's called whenever a context is cancelled during sync. We should ought to remove this and make it more granular in the future.
mocks.Reporter.EXPECT().ReportMessageWithContext("Failed to sync, will retry later", gomock.Any()).AnyTimes()
return mocks return mocks
} }
@ -119,13 +122,14 @@ func (provider *TestLocationsProvider) UserCache() string {
} }
type TestUpdater struct { type TestUpdater struct {
latest updater.VersionInfo latest updater.VersionInfoLegacy
releases updater.VersionInfo
lock sync.RWMutex lock sync.RWMutex
} }
func NewTestUpdater(version, minAuto *semver.Version) *TestUpdater { func NewTestUpdater(version, minAuto *semver.Version) *TestUpdater {
return &TestUpdater{ return &TestUpdater{
latest: updater.VersionInfo{ latest: updater.VersionInfoLegacy{
Version: version, Version: version,
MinAuto: minAuto, MinAuto: minAuto,
@ -134,11 +138,11 @@ func NewTestUpdater(version, minAuto *semver.Version) *TestUpdater {
} }
} }
func (testUpdater *TestUpdater) SetLatestVersion(version, minAuto *semver.Version) { func (testUpdater *TestUpdater) SetLatestVersionLegacy(version, minAuto *semver.Version) {
testUpdater.lock.Lock() testUpdater.lock.Lock()
defer testUpdater.lock.Unlock() defer testUpdater.lock.Unlock()
testUpdater.latest = updater.VersionInfo{ testUpdater.latest = updater.VersionInfoLegacy{
Version: version, Version: version,
MinAuto: minAuto, MinAuto: minAuto,
@ -146,17 +150,35 @@ func (testUpdater *TestUpdater) SetLatestVersion(version, minAuto *semver.Versio
} }
} }
func (testUpdater *TestUpdater) GetVersionInfo(_ context.Context, _ updater.Downloader, _ updater.Channel) (updater.VersionInfo, error) { func (testUpdater *TestUpdater) GetVersionInfoLegacy(_ context.Context, _ updater.Downloader, _ updater.Channel) (updater.VersionInfoLegacy, error) {
testUpdater.lock.RLock() testUpdater.lock.RLock()
defer testUpdater.lock.RUnlock() defer testUpdater.lock.RUnlock()
return testUpdater.latest, nil return testUpdater.latest, nil
} }
func (testUpdater *TestUpdater) InstallUpdate(_ context.Context, _ updater.Downloader, _ updater.VersionInfo) error { func (testUpdater *TestUpdater) InstallUpdateLegacy(_ context.Context, _ updater.Downloader, _ updater.VersionInfoLegacy) error {
return nil return nil
} }
func (testUpdater *TestUpdater) RemoveOldUpdates() error { func (testUpdater *TestUpdater) RemoveOldUpdates() error {
return nil return nil
} }
func (testUpdater *TestUpdater) SetLatestVersion(releases updater.VersionInfo) {
testUpdater.lock.Lock()
defer testUpdater.lock.Unlock()
testUpdater.releases = releases
}
func (testUpdater *TestUpdater) GetVersionInfo(_ context.Context, _ updater.Downloader) (updater.VersionInfo, error) {
testUpdater.lock.RLock()
defer testUpdater.lock.RUnlock()
return testUpdater.releases, nil
}
func (testUpdater *TestUpdater) InstallUpdate(_ context.Context, _ updater.Downloader, _ updater.Release) error {
return nil
}

View File

@ -88,3 +88,18 @@ func (mr *MockReporterMockRecorder) ReportMessageWithContext(arg0, arg1 interfac
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportMessageWithContext", reflect.TypeOf((*MockReporter)(nil).ReportMessageWithContext), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportMessageWithContext", reflect.TypeOf((*MockReporter)(nil).ReportMessageWithContext), arg0, arg1)
} }
// ReportWarningWithContext mocks base method.
func (m *MockReporter) ReportWarningWithContext(arg0 string, arg1 map[string]interface{}) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReportWarningWithContext", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// ReportWarningWithContext indicates an expected call of ReportWarningWithContext.
func (mr *MockReporterMockRecorder) ReportWarningWithContext(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportWarningWithContext", reflect.TypeOf((*MockReporter)(nil).ReportMessageWithContext), arg0, arg1)
}

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //

View File

@ -0,0 +1,70 @@
package mocks
import (
reflect "reflect"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
"github.com/golang/mock/gomock"
)
type MockObservabilitySender struct {
ctrl *gomock.Controller
recorder *MockObservabilitySenderRecorder
}
type MockObservabilitySenderRecorder struct {
mock *MockObservabilitySender
}
func NewMockObservabilitySender(ctrl *gomock.Controller) *MockObservabilitySender {
mock := &MockObservabilitySender{ctrl: ctrl}
mock.recorder = &MockObservabilitySenderRecorder{mock: mock}
return mock
}
func (m *MockObservabilitySender) EXPECT() *MockObservabilitySenderRecorder { return m.recorder }
func (m *MockObservabilitySender) AddDistinctMetrics(errType observability.DistinctionMetricTypeEnum, _ ...proton.ObservabilityMetric) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "AddDistinctMetrics", errType)
}
func (m *MockObservabilitySender) AddMetrics(metrics ...proton.ObservabilityMetric) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "AddMetrics", metrics)
}
func (m *MockObservabilitySender) AddTimeLimitedMetric(metricType observability.DistinctionMetricTypeEnum, metric proton.ObservabilityMetric) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "AddTimeLimitedMetric", metricType, metric)
}
func (m *MockObservabilitySender) GetEmailClient() string {
m.ctrl.T.Helper()
m.ctrl.Call(m, "GetEmailClient")
return ""
}
func (mr *MockObservabilitySenderRecorder) AddDistinctMetrics(errType observability.DistinctionMetricTypeEnum, _ ...proton.ObservabilityMetric) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock,
"AddDistinctMetrics",
reflect.TypeOf((*MockObservabilitySender)(nil).AddDistinctMetrics),
errType)
}
func (mr *MockObservabilitySenderRecorder) AddMetrics(metrics ...proton.ObservabilityMetric) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddMetrics", reflect.TypeOf((*MockObservabilitySender)(nil).AddMetrics), metrics)
}
func (mr *MockObservabilitySenderRecorder) AddTimeLimitedMetric(metricType observability.DistinctionMetricTypeEnum, metric proton.ObservabilityMetric) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddTimeLimitedMetric", reflect.TypeOf((*MockObservabilitySender)(nil).AddTimeLimitedMetric), metricType, metric)
}
func (mr *MockObservabilitySenderRecorder) GetEmailClient() {
mr.mock.ctrl.T.Helper()
mr.mock.ctrl.Call(mr.mock, "GetEmailClient", reflect.TypeOf((*MockObservabilitySender)(nil).GetEmailClient))
}

View File

@ -0,0 +1,164 @@
// Copyright (c) 2025 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 (
"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/services/observability"
"github.com/stretchr/testify/require"
"golang.org/x/net/context"
)
func TestBridge_Observability(t *testing.T) {
testMetric := proton.ObservabilityMetric{
Name: "test1",
Version: 1,
Timestamp: time.Now().Unix(),
Data: nil,
}
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
throttlePeriod := time.Millisecond * 500
observability.ModifyThrottlePeriod(throttlePeriod)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
require.NoError(t, getErr(bridge.LoginFull(ctx, username, password, nil, nil)))
bridge.PushObservabilityMetric(testMetric)
time.Sleep(time.Millisecond * 50) // Wait for the metric to be sent
require.Equal(t, 1, len(s.GetObservabilityStatistics().Metrics))
for i := 0; i < 10; i++ {
time.Sleep(time.Millisecond * 5) // Minor delay between each so our tests aren't flaky
bridge.PushObservabilityMetric(testMetric)
}
// We should still have only 1 metric sent as the throttleDuration has not passed
require.Equal(t, 1, len(s.GetObservabilityStatistics().Metrics))
// Wait for throttle duration to pass; we should have our remaining metrics posted
time.Sleep(throttlePeriod)
require.Equal(t, 11, len(s.GetObservabilityStatistics().Metrics))
// Wait for the throttle duration to reset; i.e. so we have enough time to send a request immediately
time.Sleep(throttlePeriod)
for i := 0; i < 10; i++ {
time.Sleep(time.Millisecond * 5)
bridge.PushObservabilityMetric(testMetric)
}
// We should only have one additional metric sent immediately
require.Equal(t, 12, len(s.GetObservabilityStatistics().Metrics))
// Wait for the others to be sent
time.Sleep(throttlePeriod)
require.Equal(t, 21, len(s.GetObservabilityStatistics().Metrics))
// Spam the endpoint a bit
for i := 0; i < 300; i++ {
if i < 200 {
time.Sleep(time.Millisecond * 10)
}
bridge.PushObservabilityMetric(testMetric)
}
// Ensure we've sent all metrics
time.Sleep(throttlePeriod)
observabilityStats := s.GetObservabilityStatistics()
require.Equal(t, 321, len(observabilityStats.Metrics))
// Verify that each request had a throttleDuration time difference between each request
for i := 0; i < len(observabilityStats.RequestTime)-1; i++ {
tOne := observabilityStats.RequestTime[i]
tTwo := observabilityStats.RequestTime[i+1]
require.True(t, tTwo.Sub(tOne).Abs() > throttlePeriod)
}
})
})
}
func TestBridge_Observability_Heartbeat(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
throttlePeriod := time.Millisecond * 300
observability.ModifyThrottlePeriod(throttlePeriod)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
require.NoError(t, getErr(bridge.LoginFull(ctx, username, password, nil, nil)))
bridge.ModifyObservabilityHeartbeatInterval(throttlePeriod)
require.Equal(t, 0, len(s.GetObservabilityStatistics().Metrics))
time.Sleep(time.Millisecond * 150)
require.Equal(t, 0, len(s.GetObservabilityStatistics().Metrics))
time.Sleep(time.Millisecond * 200)
require.Equal(t, 1, len(s.GetObservabilityStatistics().Metrics))
time.Sleep(time.Millisecond * 350)
require.Equal(t, 2, len(s.GetObservabilityStatistics().Metrics))
time.Sleep(time.Millisecond * 350)
require.Equal(t, 3, len(s.GetObservabilityStatistics().Metrics))
})
})
}
func TestBridge_Observability_UserMetric(t *testing.T) {
testMetric := proton.ObservabilityMetric{
Name: "test1",
Version: 1,
Timestamp: time.Now().Unix(),
Data: nil,
}
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
userMetricPeriod := time.Millisecond * 600
heartbeatPeriod := time.Second * 10
throttlePeriod := time.Millisecond * 300
observability.ModifyUserMetricInterval(userMetricPeriod)
observability.ModifyThrottlePeriod(throttlePeriod)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
require.NoError(t, getErr(bridge.LoginFull(ctx, username, password, nil, nil)))
bridge.ModifyObservabilityHeartbeatInterval(heartbeatPeriod)
time.Sleep(throttlePeriod)
require.Equal(t, 0, len(s.GetObservabilityStatistics().Metrics))
bridge.PushDistinctObservabilityMetrics(observability.SyncError, testMetric)
time.Sleep(throttlePeriod)
// We're expecting two observability metrics to be sent, the actual metric + the user metric.
require.Equal(t, 2, len(s.GetObservabilityStatistics().Metrics))
bridge.PushDistinctObservabilityMetrics(observability.SyncError, testMetric)
time.Sleep(throttlePeriod)
// We're expecting only a single metric to be sent, since the user metric update has been sent already within the predefined period.
require.Equal(t, 3, len(s.GetObservabilityStatistics().Metrics))
bridge.PushDistinctObservabilityMetrics(observability.SyncError, testMetric)
time.Sleep(throttlePeriod)
// Two metric updates should be sent again.
require.Equal(t, 5, len(s.GetObservabilityStatistics().Metrics))
bridge.PushDistinctObservabilityMetrics(observability.SyncError, testMetric)
time.Sleep(throttlePeriod)
// Only a single one should be sent.
require.Equal(t, 6, len(s.GetObservabilityStatistics().Metrics))
})
})
}

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //
@ -46,17 +46,12 @@ func TestBridge_Send(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) { withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
smtpWaiter := waitForSMTPServerReady(bridge)
defer smtpWaiter.Done()
senderUserID, err := bridge.LoginFull(ctx, username, password, nil, nil) senderUserID, err := bridge.LoginFull(ctx, username, password, nil, nil)
require.NoError(t, err) require.NoError(t, err)
recipientUserID, err := bridge.LoginFull(ctx, "recipient", password, nil, nil) recipientUserID, err := bridge.LoginFull(ctx, "recipient", password, nil, nil)
require.NoError(t, err) require.NoError(t, err)
smtpWaiter.Wait()
senderInfo, err := bridge.GetUserInfo(senderUserID) senderInfo, err := bridge.GetUserInfo(senderUserID)
require.NoError(t, err) require.NoError(t, err)
@ -409,9 +404,6 @@ SGVsbG8gd29ybGQK
require.NoError(t, err) require.NoError(t, err)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) { withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
smtpWaiter := waitForSMTPServerReady(bridge)
defer smtpWaiter.Done()
senderUserID, err := bridge.LoginFull(ctx, username, password, nil, nil) senderUserID, err := bridge.LoginFull(ctx, username, password, nil, nil)
require.NoError(t, err) require.NoError(t, err)
@ -431,8 +423,6 @@ SGVsbG8gd29ybGQK
messageMultipartWithoutTextWithTextAttachment, messageMultipartWithoutTextWithTextAttachment,
} }
smtpWaiter.Wait()
for _, m := range messages { for _, m := range messages {
// Dial the server. // Dial the server.
client, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort()))) client, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort())))
@ -617,9 +607,6 @@ Hello world
require.NoError(t, err) require.NoError(t, err)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) { withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
smtpWaiter := waitForSMTPServerReady(bridge)
defer smtpWaiter.Done()
senderUserID, err := bridge.LoginFull(ctx, username, password, nil, nil) senderUserID, err := bridge.LoginFull(ctx, username, password, nil, nil)
require.NoError(t, err) require.NoError(t, err)
@ -639,8 +626,6 @@ Hello world
messageInlineImageFollowedByText, messageInlineImageFollowedByText,
} }
smtpWaiter.Wait()
for _, m := range messages { for _, m := range messages {
// Dial the server. // Dial the server.
client, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort()))) client, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort())))
@ -714,17 +699,12 @@ func TestBridge_SendAddressDisabled(t *testing.T) {
require.NoError(t, s.ChangeAddressAllowSend(senderUserID, addrID, false)) require.NoError(t, s.ChangeAddressAllowSend(senderUserID, addrID, false))
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) { withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
smtpWaiter := waitForSMTPServerReady(bridge)
defer smtpWaiter.Done()
senderUserID, err := bridge.LoginFull(ctx, "sender", password, nil, nil) senderUserID, err := bridge.LoginFull(ctx, "sender", password, nil, nil)
require.NoError(t, err) require.NoError(t, err)
_, err = bridge.LoginFull(ctx, "recipient", password, nil, nil) _, err = bridge.LoginFull(ctx, "recipient", password, nil, nil)
require.NoError(t, err) require.NoError(t, err)
smtpWaiter.Wait()
recipientInfo, err := bridge.GetUserInfo(recipientUserID) recipientInfo, err := bridge.GetUserInfo(recipientUserID)
require.NoError(t, err) require.NoError(t, err)
@ -750,7 +730,7 @@ func TestBridge_SendAddressDisabled(t *testing.T) {
strings.NewReader("Subject: Test 1\r\n\r\nHello world!"), strings.NewReader("Subject: Test 1\r\n\r\nHello world!"),
) )
smtpErr := smtpservice.NewErrCanNotSendOnAddress(senderInfo.Addresses[0]) smtpErr := smtpservice.NewErrCannotSendFromAddress(senderInfo.Addresses[0])
require.Equal(t, fmt.Sprintf("Error: %v", smtpErr.Error()), err.Error()) require.Equal(t, fmt.Sprintf("Error: %v", smtpErr.Error()), err.Error())
}) })
}) })

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //
@ -29,16 +29,12 @@ import (
"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/ProtonMail/proton-bridge/v3/internal/events"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestBridge_Report(t *testing.T) { func TestBridge_Report(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(b *bridge.Bridge, mocks *bridge.Mocks) { withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
imapWaiter := waitForIMAPServerReady(b)
defer imapWaiter.Done()
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{})) syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
defer done() defer done()
@ -54,19 +50,11 @@ func TestBridge_Report(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.True(t, info.State == bridge.Connected) require.True(t, info.State == bridge.Connected)
imapWaiter.Wait()
// Dial the IMAP port. // Dial the IMAP port.
conn, err := net.Dial("tcp", fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort())) conn, err := net.Dial("tcp", fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
require.NoError(t, err) require.NoError(t, err)
defer func() { require.NoError(t, conn.Close()) }() defer func() { require.NoError(t, conn.Close()) }()
// Sending garbage to the IMAP port should cause the bridge to report it.
mocks.Reporter.EXPECT().ReportMessageWithContext(
gomock.Eq("Failed to parse IMAP command"),
gomock.Any(),
).Return(nil)
// Read lines from the IMAP port. // Read lines from the IMAP port.
lineCh := liner.New(conn).Lines(func() error { return nil }) lineCh := liner.New(conn).Lines(func() error { return nil })

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //
@ -20,6 +20,7 @@ package bridge_test
import ( import (
"context" "context"
"fmt" "fmt"
"net"
"testing" "testing"
"github.com/ProtonMail/go-proton-api" "github.com/ProtonMail/go-proton-api"
@ -27,57 +28,39 @@ import (
"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/ProtonMail/proton-bridge/v3/internal/events"
"github.com/emersion/go-smtp"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestServerManager_NoLoadedUsersNoServers(t *testing.T) { func TestServerManager_ServersStartWithBridge(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, _ *bridge.Mocks) {
_, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort())) imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
require.Error(t, err)
})
})
}
func TestServerManager_ServersStartAfterFirstConnectedUser(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) {
imapWaiter := waitForIMAPServerReady(bridge)
defer imapWaiter.Done()
smtpWaiter := waitForSMTPServerReady(bridge)
defer smtpWaiter.Done()
_, err := bridge.LoginFull(ctx, username, password, nil, nil)
require.NoError(t, err) require.NoError(t, err)
require.NoError(t, imapClient.Logout())
imapWaiter.Wait() smtpClient, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort())))
smtpWaiter.Wait() require.NoError(t, err)
smtpClient.Close() //nolint:errcheck
}) })
}) })
} }
func TestServerManager_ServersStopsAfterUserLogsOut(t *testing.T) { func TestServerManager_ServersKeepsRunningfterUserLogsOut(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, _ *bridge.Mocks) {
imapWaiter := waitForIMAPServerReady(bridge)
defer imapWaiter.Done()
smtpWaiter := waitForSMTPServerReady(bridge)
defer smtpWaiter.Done()
userID, err := bridge.LoginFull(ctx, username, password, nil, nil) userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
require.NoError(t, err) require.NoError(t, err)
imapWaiter.Wait()
smtpWaiter.Wait()
imapWaiterStopped := waitForIMAPServerStopped(bridge)
defer imapWaiterStopped.Done()
require.NoError(t, bridge.LogoutUser(ctx, userID)) require.NoError(t, bridge.LogoutUser(ctx, userID))
imapWaiterStopped.Wait() imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, imapClient.Logout())
smtpClient, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort())))
require.NoError(t, err)
smtpClient.Close() //nolint:errcheck
}) })
}) })
} }
@ -89,22 +72,13 @@ func TestServerManager_ServersDoNotStopWhenThereIsStillOneActiveUser(t *testing.
_, _, err := s.CreateUser(otherUser, otherPassword) _, _, err := s.CreateUser(otherUser, otherPassword)
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) {
imapWaiter := waitForIMAPServerReady(bridge)
defer imapWaiter.Done()
smtpWaiter := waitForSMTPServerReady(bridge)
defer smtpWaiter.Done()
_, err := bridge.LoginFull(ctx, username, password, nil, nil) _, err := bridge.LoginFull(ctx, username, password, nil, nil)
require.NoError(t, err) require.NoError(t, err)
userIDOther, err := bridge.LoginFull(ctx, otherUser, otherPassword, nil, nil) userIDOther, err := bridge.LoginFull(ctx, otherUser, otherPassword, nil, nil)
require.NoError(t, err) require.NoError(t, err)
imapWaiter.Wait()
smtpWaiter.Wait()
evtCh, cancel := bridge.GetEvents(events.UserDeauth{}) evtCh, cancel := bridge.GetEvents(events.UserDeauth{})
defer cancel() defer cancel()
@ -115,38 +89,17 @@ func TestServerManager_ServersDoNotStopWhenThereIsStillOneActiveUser(t *testing.
imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort())) imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
require.NoError(t, err) require.NoError(t, err)
require.NoError(t, imapClient.Logout()) require.NoError(t, imapClient.Logout())
})
})
}
func TestServerManager_ServersStartIfAtLeastOneUserIsLoggedIn(t *testing.T) { smtpClient, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort())))
otherPassword := []byte("bar")
otherUser := "foo"
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
userIDOther, _, err := s.CreateUser(otherUser, otherPassword)
require.NoError(t, err) require.NoError(t, err)
smtpClient.Close() //nolint:errcheck
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
_, err := bridge.LoginFull(ctx, username, password, nil, nil)
require.NoError(t, err)
_, err = bridge.LoginFull(ctx, otherUser, otherPassword, nil, nil)
require.NoError(t, err)
})
require.NoError(t, s.RevokeUser(userIDOther))
withBridgeWaitForServers(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, imapClient.Logout())
}) })
}) })
} }
func TestServerManager_NetworkLossStopsServers(t *testing.T) { func TestServerManager_NetworkLossStopsServers(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, _ *bridge.Mocks) {
imapWaiter := waitForIMAPServerReady(bridge) imapWaiter := waitForIMAPServerReady(bridge)
defer imapWaiter.Done() defer imapWaiter.Done()
@ -162,8 +115,13 @@ func TestServerManager_NetworkLossStopsServers(t *testing.T) {
_, err := bridge.LoginFull(ctx, username, password, nil, nil) _, err := bridge.LoginFull(ctx, username, password, nil, nil)
require.NoError(t, err) require.NoError(t, err)
imapWaiter.Wait() imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
smtpWaiter.Wait() require.NoError(t, err)
require.NoError(t, imapClient.Logout())
smtpClient, err := smtp.Dial(net.JoinHostPort(constants.Host, fmt.Sprint(bridge.GetSMTPPort())))
require.NoError(t, err)
smtpClient.Close() //nolint:errcheck
netCtl.Disable() netCtl.Disable()

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //
@ -22,11 +22,11 @@ import (
"fmt" "fmt"
"github.com/Masterminds/semver/v3" "github.com/Masterminds/semver/v3"
"github.com/ProtonMail/proton-bridge/v3/internal/kb"
"github.com/ProtonMail/proton-bridge/v3/internal/safe" "github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/services/userevents" "github.com/ProtonMail/proton-bridge/v3/internal/services/userevents"
"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/sirupsen/logrus"
) )
func (bridge *Bridge) GetKeychainApp() (string, error) { func (bridge *Bridge) GetKeychainApp() (string, error) {
@ -133,7 +133,7 @@ func (bridge *Bridge) SetGluonDir(ctx context.Context, newGluonDir string) error
bridge.usersLock.RLock() bridge.usersLock.RLock()
defer func() { defer func() {
logrus.Info("Restarting user event loops") logPkg.Info("Restarting user event loops")
for _, u := range bridge.users { for _, u := range bridge.users {
u.ResumeEventLoop() u.ResumeEventLoop()
} }
@ -148,20 +148,20 @@ func (bridge *Bridge) SetGluonDir(ctx context.Context, newGluonDir string) error
waiters := make([]waiter, 0, len(bridge.users)) waiters := make([]waiter, 0, len(bridge.users))
logrus.Info("Pausing user event loops for gluon dir change") logPkg.Info("Pausing user event loops for gluon dir change")
for id, u := range bridge.users { for id, u := range bridge.users {
waiters = append(waiters, waiter{w: u.PauseEventLoopWithWaiter(), id: id}) waiters = append(waiters, waiter{w: u.PauseEventLoopWithWaiter(), id: id})
} }
logrus.Info("Waiting on user event loop completion") logPkg.Info("Waiting on user event loop completion")
for _, waiter := range waiters { for _, waiter := range waiters {
if err := waiter.w.WaitPollFinished(ctx); err != nil { if err := waiter.w.WaitPollFinished(ctx); err != nil {
logrus.WithError(err).Errorf("Failed to wait on event loop pause for user %v", waiter.id) logPkg.WithError(err).Errorf("Failed to wait on event loop pause for user %v", waiter.id)
return fmt.Errorf("failed on event loop pause: %w", err) return fmt.Errorf("failed on event loop pause: %w", err)
} }
} }
logrus.Info("Changing gluon directory") logPkg.Info("Changing gluon directory")
return bridge.serverManager.SetGluonDir(ctx, newGluonDir) return bridge.serverManager.SetGluonDir(ctx, newGluonDir)
} }
@ -310,28 +310,31 @@ func (bridge *Bridge) SetColorScheme(colorScheme string) error {
return bridge.vault.SetColorScheme(colorScheme) return bridge.vault.SetColorScheme(colorScheme)
} }
func (bridge *Bridge) GetKnowledgeBaseSuggestions(userInput string) (kb.ArticleList, error) {
return kb.GetSuggestions(userInput)
}
// FactoryReset deletes all users, wipes the vault, and deletes all files. // FactoryReset deletes all users, wipes the vault, and deletes all files.
// Note: it does not clear the keychain. The only entry in the keychain is the vault password, // Note: it does not clear the keychain. The only entry in the keychain is the vault password,
// which we need at next startup to decrypt the vault. // which we need at next startup to decrypt the vault.
func (bridge *Bridge) FactoryReset(ctx context.Context) { func (bridge *Bridge) FactoryReset(ctx context.Context) {
useTelemetry := !bridge.GetTelemetryDisabled()
// Delete all the users. // Delete all the users.
safe.Lock(func() { safe.Lock(func() {
for _, user := range bridge.users { for _, user := range bridge.users {
bridge.logoutUser(ctx, user, true, true, useTelemetry) bridge.logoutUser(ctx, user, true, true)
} }
}, bridge.usersLock) }, bridge.usersLock)
// Wipe the vault. // Wipe the vault.
gluonCacheDir, err := bridge.locator.ProvideGluonCachePath() gluonCacheDir, err := bridge.locator.ProvideGluonCachePath()
if err != nil { if err != nil {
logrus.WithError(err).Error("Failed to provide gluon dir") logPkg.WithError(err).Error("Failed to provide gluon dir")
} else if err := bridge.vault.Reset(gluonCacheDir); err != nil { } else if err := bridge.vault.Reset(gluonCacheDir); err != nil {
logrus.WithError(err).Error("Failed to reset vault") logPkg.WithError(err).Error("Failed to reset vault")
} }
// Lastly, delete all files except the vault. // Lastly, delete all files except the vault.
if err := bridge.locator.Clear(bridge.vault.Path()); err != nil { if err := bridge.locator.Clear(bridge.vault.Path()); err != nil {
logrus.WithError(err).Error("Failed to clear data paths") logPkg.WithError(err).Error("Failed to clear data paths")
} }
} }

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //
@ -31,7 +31,7 @@ import (
func TestBridge_Settings_GluonDir(t *testing.T) { func TestBridge_Settings_GluonDir(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, _ *bridge.Mocks) {
// Create a user. // Create a user.
_, err := bridge.LoginFull(context.Background(), username, password, nil, nil) _, err := bridge.LoginFull(context.Background(), username, password, nil, nil)
require.NoError(t, err) require.NoError(t, err)
@ -57,7 +57,7 @@ func TestBridge_Settings_GluonDirWithOnGoingEvents(t *testing.T) {
userID, addrID, err := s.CreateUser("imap", password) userID, addrID, err := s.CreateUser("imap", 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) {
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{})) syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
defer done() defer done()
@ -74,7 +74,7 @@ func TestBridge_Settings_GluonDirWithOnGoingEvents(t *testing.T) {
createNumMessages(ctx, t, c, addrID, labelID, 200) createNumMessages(ctx, t, c, addrID, labelID, 200)
}) })
withBridgeWaitForServers(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { withBridgeWaitForServers(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
// Create a new location for the Gluon data. // Create a new location for the Gluon data.
newGluonDir := t.TempDir() newGluonDir := t.TempDir()
@ -93,7 +93,7 @@ func TestBridge_Settings_GluonDirWithOnGoingEvents(t *testing.T) {
func TestBridge_Settings_IMAPPort(t *testing.T) { func TestBridge_Settings_IMAPPort(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, _ *bridge.Mocks) {
curPort := bridge.GetIMAPPort() curPort := bridge.GetIMAPPort()
// Set the port to 1144. // Set the port to 1144.
@ -110,7 +110,7 @@ func TestBridge_Settings_IMAPPort(t *testing.T) {
func TestBridge_Settings_IMAPSSL(t *testing.T) { func TestBridge_Settings_IMAPSSL(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, _ *bridge.Mocks) {
// By default, IMAP SSL is disabled. // By default, IMAP SSL is disabled.
require.False(t, bridge.GetIMAPSSL()) require.False(t, bridge.GetIMAPSSL())
@ -125,7 +125,7 @@ func TestBridge_Settings_IMAPSSL(t *testing.T) {
func TestBridge_Settings_SMTPPort(t *testing.T) { func TestBridge_Settings_SMTPPort(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, _ *bridge.Mocks) {
curPort := bridge.GetSMTPPort() curPort := bridge.GetSMTPPort()
// Set the port to 1024. // Set the port to 1024.
@ -142,7 +142,7 @@ func TestBridge_Settings_SMTPPort(t *testing.T) {
func TestBridge_Settings_SMTPSSL(t *testing.T) { func TestBridge_Settings_SMTPSSL(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, _ *bridge.Mocks) {
// By default, SMTP SSL is disabled. // By default, SMTP SSL is disabled.
require.False(t, bridge.GetSMTPSSL()) require.False(t, bridge.GetSMTPSSL())
@ -198,7 +198,7 @@ func TestBridge_Settings_Autostart(t *testing.T) {
func TestBridge_Settings_FirstStart(t *testing.T) { func TestBridge_Settings_FirstStart(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, _ *bridge.Mocks) {
// By default, first start is true. // By default, first start is true.
require.True(t, bridge.GetFirstStart()) require.True(t, bridge.GetFirstStart())

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //
@ -232,7 +232,7 @@ func TestBridge_SyncWithOngoingEvents(t *testing.T) {
var total uint64 var total uint64
// The initial user should be fully synced. // The initial user should be fully synced.
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) {
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{})) syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
defer done() defer done()
@ -246,7 +246,7 @@ func TestBridge_SyncWithOngoingEvents(t *testing.T) {
}) })
// Now let's remove the user and stop the network at 2/3 of the data. // Now let's remove the user and stop the network at 2/3 of the data.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) { withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
require.NoError(t, bridge.DeleteUser(ctx, userID)) require.NoError(t, bridge.DeleteUser(ctx, userID))
}) })
@ -254,7 +254,7 @@ func TestBridge_SyncWithOngoingEvents(t *testing.T) {
netCtl.SetReadLimit(2 * total / 3) netCtl.SetReadLimit(2 * total / 3)
// Login the user; its sync should fail. // Login the user; its sync should fail.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, mocks *bridge.Mocks) { withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{})) syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
defer done() defer done()
@ -355,7 +355,7 @@ func TestBridge_CanProcessEventsDuringSync(t *testing.T) {
// Create a new address // Create a new address
newAddress := "foo@proton.ch" newAddress := "foo@proton.ch"
addrID, err := s.CreateAddress(userID, newAddress, password) addrID, err := s.CreateAddress(userID, newAddress, password, true)
require.NoError(t, err) require.NoError(t, err)
event := <-addressCreatedCh event := <-addressCreatedCh
@ -430,7 +430,7 @@ func TestBridge_EventReplayAfterSyncHasFinished(t *testing.T) {
createNumMessages(ctx, t, c, addrID, labelID, numMsg) createNumMessages(ctx, t, c, addrID, labelID, numMsg)
}) })
addrID1, err := s.CreateAddress(userID, "foo@proton.ch", password) addrID1, err := s.CreateAddress(userID, "foo@proton.ch", password, true)
require.NoError(t, err) require.NoError(t, err)
var allowSyncToProgress atomic.Bool var allowSyncToProgress atomic.Bool
@ -469,7 +469,7 @@ func TestBridge_EventReplayAfterSyncHasFinished(t *testing.T) {
}) })
// User AddrID2 event as a check point to see when the new address was created. // User AddrID2 event as a check point to see when the new address was created.
addrID2, err := s.CreateAddress(userID, "bar@proton.ch", password) addrID2, err := s.CreateAddress(userID, "bar@proton.ch", password, true)
require.NoError(t, err) require.NoError(t, err)
allowSyncToProgress.Store(true) allowSyncToProgress.Store(true)
@ -552,7 +552,7 @@ func TestBridge_MessageCreateDuringSync(t *testing.T) {
}) })
// User AddrID2 event as a check point to see when the new address was created. // User AddrID2 event as a check point to see when the new address was created.
addrID, err := s.CreateAddress(userID, "bar@proton.ch", password) addrID, err := s.CreateAddress(userID, "bar@proton.ch", password, true)
require.NoError(t, err) require.NoError(t, err)
// At most two events can be published, one for the first address, then for the second. // At most two events can be published, one for the first address, then for the second.
@ -592,7 +592,7 @@ func TestBridge_CorruptedVaultClearsPreviousIMAPSyncState(t *testing.T) {
createNumMessages(ctx, t, c, addrID, labelID, 100) createNumMessages(ctx, t, c, addrID, labelID, 100)
}) })
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, _ *bridge.Mocks) {
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{})) syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
defer done() defer done()
@ -625,7 +625,7 @@ func TestBridge_CorruptedVaultClearsPreviousIMAPSyncState(t *testing.T) {
require.NoError(t, os.WriteFile(filepath.Join(settingsPath, "vault.enc"), []byte("Trash!"), 0o600)) require.NoError(t, os.WriteFile(filepath.Join(settingsPath, "vault.enc"), []byte("Trash!"), 0o600))
// Bridge starts but can't find the gluon database dir; there should be no error. // 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, _ *bridge.Mocks) {
_, err := bridge.LoginFull(context.Background(), "imap", password, nil, nil) _, err := bridge.LoginFull(context.Background(), "imap", password, nil, nil)
require.NoError(t, err) require.NoError(t, err)
}) })
@ -663,7 +663,7 @@ func TestBridge_AddressOrderChangeDuringSyncInCombinedModeDoesNotTriggerBadEvent
require.Equal(t, 1, len(info.Addresses)) require.Equal(t, 1, len(info.Addresses))
require.Equal(t, info.Addresses[0], "user@proton.local") require.Equal(t, info.Addresses[0], "user@proton.local")
addrID2, err := s.CreateAddress(userID, "foo@"+s.GetDomain(), password) addrID2, err := s.CreateAddress(userID, "foo@"+s.GetDomain(), password, true)
require.NoError(t, err) require.NoError(t, err)
require.NoError(t, s.SetAddressOrder(userID, []string{addrID2, addrID})) require.NoError(t, s.SetAddressOrder(userID, []string{addrID2, addrID}))

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //
@ -28,11 +28,12 @@ type Locator interface {
ProvideLogsPath() (string, error) ProvideLogsPath() (string, error)
ProvideGluonCachePath() (string, error) ProvideGluonCachePath() (string, error)
ProvideGluonDataPath() (string, error) ProvideGluonDataPath() (string, error)
ProvideStatsPath() (string, error)
GetLicenseFilePath() string GetLicenseFilePath() string
GetDependencyLicensesLink() string GetDependencyLicensesLink() string
Clear(...string) error Clear(...string) error
ProvideIMAPSyncConfigPath() (string, error) ProvideIMAPSyncConfigPath() (string, error)
ProvideUnleashCachePath() (string, error)
ProvideNotificationsCachePath() (string, error)
} }
type ProxyController interface { type ProxyController interface {
@ -51,7 +52,9 @@ type Autostarter interface {
} }
type Updater interface { type Updater interface {
GetVersionInfo(context.Context, updater.Downloader, updater.Channel) (updater.VersionInfo, error) GetVersionInfoLegacy(context.Context, updater.Downloader, updater.Channel) (updater.VersionInfoLegacy, error)
InstallUpdate(context.Context, updater.Downloader, updater.VersionInfo) error InstallUpdateLegacy(context.Context, updater.Downloader, updater.VersionInfoLegacy) error
RemoveOldUpdates() error RemoveOldUpdates() error
GetVersionInfo(context.Context, updater.Downloader) (updater.VersionInfo, error)
InstallUpdate(context.Context, updater.Downloader, updater.Release) error
} }

View File

@ -0,0 +1,90 @@
// Copyright (c) 2025 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"
"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/unleash"
"github.com/stretchr/testify/require"
)
func Test_UnleashService(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
unleash.ModifyPollPeriodAndJitter(500*time.Millisecond, 0)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
// Initial startup assumes there is no cached feature flags.
require.Equal(t, b.GetFeatureFlagValue("test-1"), false)
require.Equal(t, b.GetFeatureFlagValue("test-2"), false)
require.Equal(t, b.GetFeatureFlagValue("test-3"), false)
s.PushFeatureFlag("test-1")
s.PushFeatureFlag("test-2")
// Wait for poll.
time.Sleep(time.Millisecond * 700)
require.Equal(t, b.GetFeatureFlagValue("test-1"), true)
require.Equal(t, b.GetFeatureFlagValue("test-2"), true)
require.Equal(t, b.GetFeatureFlagValue("test-3"), false)
s.PushFeatureFlag("test-3")
time.Sleep(time.Millisecond * 700) // Wait for poll again
require.Equal(t, b.GetFeatureFlagValue("test-1"), true)
require.Equal(t, b.GetFeatureFlagValue("test-2"), true)
require.Equal(t, b.GetFeatureFlagValue("test-3"), true)
})
// Wait for Bridge to close.
time.Sleep(time.Millisecond * 500)
// Second instance should have a feature flag cache file available. Therefore, all of the flags should evaluate to true on startup.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
require.Equal(t, b.GetFeatureFlagValue("test-1"), true)
require.Equal(t, b.GetFeatureFlagValue("test-2"), true)
require.Equal(t, b.GetFeatureFlagValue("test-3"), true)
s.DeleteFeatureFlags()
require.Equal(t, b.GetFeatureFlagValue("test-1"), true)
require.Equal(t, b.GetFeatureFlagValue("test-2"), true)
require.Equal(t, b.GetFeatureFlagValue("test-3"), true)
time.Sleep(time.Millisecond * 700)
require.Equal(t, b.GetFeatureFlagValue("test-1"), false)
require.Equal(t, b.GetFeatureFlagValue("test-2"), false)
require.Equal(t, b.GetFeatureFlagValue("test-3"), false)
s.PushFeatureFlag("test-3")
require.Equal(t, b.GetFeatureFlagValue("test-1"), false)
require.Equal(t, b.GetFeatureFlagValue("test-2"), false)
require.Equal(t, b.GetFeatureFlagValue("test-3"), false)
time.Sleep(time.Millisecond * 700)
require.Equal(t, b.GetFeatureFlagValue("test-1"), false)
require.Equal(t, b.GetFeatureFlagValue("test-2"), false)
require.Equal(t, b.GetFeatureFlagValue("test-3"), true)
})
})
}

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //
@ -21,21 +21,168 @@ import (
"context" "context"
"errors" "errors"
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/gluon/reporter"
"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/updater" "github.com/ProtonMail/proton-bridge/v3/internal/updater"
"github.com/elastic/go-sysinfo"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"golang.org/x/exp/slices"
) )
func (bridge *Bridge) CheckForUpdates() { func (bridge *Bridge) CheckForUpdates() {
bridge.goUpdate() bridge.goUpdate()
} }
func (bridge *Bridge) InstallUpdate(version updater.VersionInfo) { func (bridge *Bridge) InstallUpdateLegacy(version updater.VersionInfoLegacy) {
bridge.installCh <- installJob{version: version, silent: false} bridge.installChLegacy <- installJobLegacy{version: version, silent: false}
}
func (bridge *Bridge) InstallUpdate(release updater.Release) {
bridge.installCh <- installJob{Release: release, Silent: false}
} }
func (bridge *Bridge) handleUpdate(version updater.VersionInfo) { func (bridge *Bridge) handleUpdate(version updater.VersionInfo) {
updateChannel := bridge.vault.GetUpdateChannel()
updateRollout := bridge.vault.GetUpdateRollout()
autoUpdateEnabled := bridge.vault.GetAutoUpdate()
checkSystemVersion := true
hostInfo, err := sysinfo.Host()
// If we're unable to get host system information we skip the update's minimum/maximum OS version checks
if err != nil {
checkSystemVersion = false
logrus.WithError(err).Error("Failed to obtain host system info while handling updates")
if reporterErr := bridge.reporter.ReportMessageWithContext(
"Failed to obtain host system info while handling updates",
reporter.Context{"error": err},
); reporterErr != nil {
logrus.WithError(reporterErr).Error("Failed to report update error")
}
}
if len(version.Releases) > 0 {
// Update latest is only used to update the release notes and landing page URL
bridge.publish(events.UpdateLatest{Release: version.Releases[0]})
}
// minAutoUpdateEvent - used to determine the highest compatible update that satisfies the Minimum Bridge version
minAutoUpdateEvent := events.UpdateAvailable{
Release: updater.Release{Version: &semver.Version{}},
Compatible: false,
Silent: false,
}
// We assume that the version file is always created in descending order
// where newer versions are prepended to the top of the releases
// The logic for checking update eligibility is as follows:
// 1. Check release channel.
// 2. Check whether release version is greater.
// 3. Check if rollout is larger.
// 4. Check OS Version restrictions (provided that restrictions are provided, and we can extract the OS version).
// 5. Check Minimum Compatible Bridge Version.
// 6. Check if an update package is provided.
// 7. Check auto-update.
for _, release := range version.Releases {
log := logrus.WithFields(logrus.Fields{
"current": bridge.curVersion,
"channel": updateChannel,
"update_version": release.Version,
"update_channel": release.ReleaseCategory,
"update_min_auto": release.MinAuto,
"update_rollout": release.RolloutProportion,
"update_min_os_version": release.SystemVersion.Minimum,
"update_max_os_version": release.SystemVersion.Maximum,
})
log.Debug("Checking update release")
if !release.ReleaseCategory.UpdateEligible(updateChannel) {
log.Debug("Update does not satisfy update channel requirement")
continue
}
if !release.Version.GreaterThan(bridge.curVersion) {
log.Debug("Update version is not greater than current version")
continue
}
if release.RolloutProportion < updateRollout {
log.Debug("Update has not been rolled out yet")
continue
}
if checkSystemVersion {
shouldContinue, err := release.SystemVersion.IsHostVersionEligible(log, hostInfo, bridge.getHostVersion)
if err != nil && shouldContinue {
log.WithError(err).Error(
"Failed to verify host system version compatibility during release check." +
"Error is non-fatal continuing with checks",
)
} else if err != nil {
log.WithError(err).Error("Failed to verify host system version compatibility during update check")
continue
}
if !shouldContinue {
log.Debug("Host version does not satisfy system requirements for update")
continue
}
}
if release.MinAuto != nil && bridge.curVersion.LessThan(release.MinAuto) {
log.Debug("Update is available but is incompatible with this Bridge version")
if release.Version.GreaterThan(minAutoUpdateEvent.Release.Version) {
minAutoUpdateEvent.Release = release
}
continue
}
// Check if we have a provided installer package
if found := slices.IndexFunc(release.File, func(file updater.File) bool {
return file.Identifier == updater.PackageIdentifier
}); found == -1 {
log.Error("Update is available but does not contain update package")
if reporterErr := bridge.reporter.ReportMessageWithContext(
"Available update does not contain update package",
reporter.Context{"update_version": release.Version},
); reporterErr != nil {
log.WithError(reporterErr).Error("Failed to report update error")
}
continue
}
if !autoUpdateEnabled {
log.Info("An update is available but auto-update is disabled")
bridge.publish(events.UpdateAvailable{
Release: release,
Compatible: true,
Silent: false,
})
return
}
// If we've gotten to this point that means an automatic update is available and we should install it
safe.RLock(func() {
bridge.installCh <- installJob{Release: release, Silent: true}
}, bridge.newVersionLock)
return
}
// If there's a release with a minAuto requirement that we satisfy (alongside all other checks)
// then notify the user that a manual update is needed
if !minAutoUpdateEvent.Release.Version.Equal(&semver.Version{}) {
bridge.publish(minAutoUpdateEvent)
}
bridge.publish(events.UpdateNotAvailable{})
}
func (bridge *Bridge) handleUpdateLegacy(version updater.VersionInfoLegacy) {
log := logrus.WithFields(logrus.Fields{ log := logrus.WithFields(logrus.Fields{
"version": version.Version, "version": version.Version,
"current": bridge.curVersion, "current": bridge.curVersion,
@ -43,7 +190,7 @@ func (bridge *Bridge) handleUpdate(version updater.VersionInfo) {
}) })
bridge.publish(events.UpdateLatest{ bridge.publish(events.UpdateLatest{
Version: version, VersionLegacy: version,
}) })
switch { switch {
@ -61,7 +208,7 @@ func (bridge *Bridge) handleUpdate(version updater.VersionInfo) {
log.Info("An update is available but is incompatible with this version") log.Info("An update is available but is incompatible with this version")
bridge.publish(events.UpdateAvailable{ bridge.publish(events.UpdateAvailable{
Version: version, VersionLegacy: version,
Compatible: false, Compatible: false,
Silent: false, Silent: false,
}) })
@ -70,24 +217,24 @@ func (bridge *Bridge) handleUpdate(version updater.VersionInfo) {
log.Info("An update is available but auto-update is disabled") log.Info("An update is available but auto-update is disabled")
bridge.publish(events.UpdateAvailable{ bridge.publish(events.UpdateAvailable{
Version: version, VersionLegacy: version,
Compatible: true, Compatible: true,
Silent: false, Silent: false,
}) })
default: default:
safe.RLock(func() { safe.RLock(func() {
bridge.installCh <- installJob{version: version, silent: true} bridge.installChLegacy <- installJobLegacy{version: version, silent: true}
}, bridge.newVersionLock) }, bridge.newVersionLock)
} }
} }
type installJob struct { type installJobLegacy struct {
version updater.VersionInfo version updater.VersionInfoLegacy
silent bool silent bool
} }
func (bridge *Bridge) installUpdate(ctx context.Context, job installJob) { func (bridge *Bridge) installUpdateLegacy(ctx context.Context, job installJobLegacy) {
safe.Lock(func() { safe.Lock(func() {
log := logrus.WithFields(logrus.Fields{ log := logrus.WithFields(logrus.Fields{
"version": job.version.Version, "version": job.version.Version,
@ -102,19 +249,25 @@ func (bridge *Bridge) installUpdate(ctx context.Context, job installJob) {
log.WithField("silent", job.silent).Info("An update is available") log.WithField("silent", job.silent).Info("An update is available")
bridge.publish(events.UpdateAvailable{ bridge.publish(events.UpdateAvailable{
Version: job.version, VersionLegacy: job.version,
Compatible: true, Compatible: true,
Silent: job.silent, Silent: job.silent,
}) })
bridge.publish(events.UpdateInstalling{ err := bridge.updater.InstallUpdateLegacy(ctx, bridge.api, job.version)
Version: job.version,
Silent: job.silent,
})
err := bridge.updater.InstallUpdate(ctx, bridge.api, job.version)
switch { switch {
case errors.Is(err, updater.ErrDownloadVerify):
// BRIDGE-207: if download or verification fails, we do not want to trigger a manual update. We report in the log and to Sentry
// and we fail silently.
log.WithError(err).Error("The update could not be installed, but we will fail silently")
if reporterErr := bridge.reporter.ReportMessageWithContext(
"Cannot download or verify update",
reporter.Context{"error": err},
); reporterErr != nil {
log.WithError(reporterErr).Error("Failed to report update error")
}
case errors.Is(err, updater.ErrUpdateAlreadyInstalled): case errors.Is(err, updater.ErrUpdateAlreadyInstalled):
log.Info("The update was already installed") log.Info("The update was already installed")
@ -122,7 +275,7 @@ func (bridge *Bridge) installUpdate(ctx context.Context, job installJob) {
log.WithError(err).Error("The update could not be installed") log.WithError(err).Error("The update could not be installed")
bridge.publish(events.UpdateFailed{ bridge.publish(events.UpdateFailed{
Version: job.version, VersionLegacy: job.version,
Silent: job.silent, Silent: job.silent,
Error: err, Error: err,
}) })
@ -131,7 +284,7 @@ func (bridge *Bridge) installUpdate(ctx context.Context, job installJob) {
log.Info("The update was installed successfully") log.Info("The update was installed successfully")
bridge.publish(events.UpdateInstalled{ bridge.publish(events.UpdateInstalled{
Version: job.version, VersionLegacy: job.version,
Silent: job.silent, Silent: job.silent,
}) })
@ -140,6 +293,77 @@ func (bridge *Bridge) installUpdate(ctx context.Context, job installJob) {
}, bridge.newVersionLock) }, bridge.newVersionLock)
} }
type installJob struct {
Release updater.Release
Silent bool
}
func (bridge *Bridge) installUpdate(ctx context.Context, job installJob) {
safe.Lock(func() {
log := logrus.WithFields(logrus.Fields{
"version": job.Release.Version,
"current": bridge.curVersion,
"channel": bridge.vault.GetUpdateChannel(),
})
if !job.Release.Version.GreaterThan(bridge.newVersion) {
return
}
log.WithField("silent", job.Silent).Info("An update is available")
bridge.publish(events.UpdateAvailable{
Release: job.Release,
Compatible: true,
Silent: job.Silent,
})
err := bridge.updater.InstallUpdate(ctx, bridge.api, job.Release)
switch {
case errors.Is(err, updater.ErrReleaseUpdatePackageMissing):
log.WithError(err).Error("The update could not be installed but we will fail silently")
if reporterErr := bridge.reporter.ReportExceptionWithContext(
"Cannot download update, update package is missing",
reporter.Context{"error": err},
); reporterErr != nil {
log.WithError(reporterErr).Error("Failed to report update error")
}
case errors.Is(err, updater.ErrDownloadVerify):
// BRIDGE-207: if download or verification fails, we do not want to trigger a manual update. We report in the log and to Sentry
// and we fail silently.
log.WithError(err).Error("The update could not be installed, but we will fail silently")
if reporterErr := bridge.reporter.ReportMessageWithContext(
"Cannot download or verify update",
reporter.Context{"error": err},
); reporterErr != nil {
log.WithError(reporterErr).Error("Failed to report update error")
}
case errors.Is(err, updater.ErrUpdateAlreadyInstalled):
log.Info("The update was already installed")
case err != nil:
log.WithError(err).Error("The update could not be installed")
bridge.publish(events.UpdateFailed{
Release: job.Release,
Silent: job.Silent,
Error: err,
})
default:
log.Info("The update was installed successfully")
bridge.publish(events.UpdateInstalled{
Release: job.Release,
Silent: job.Silent,
})
bridge.newVersion = job.Release.Version
}
}, bridge.newVersionLock)
}
func (bridge *Bridge) RemoveOldUpdates() { func (bridge *Bridge) RemoveOldUpdates() {
if err := bridge.updater.RemoveOldUpdates(); err != nil { if err := bridge.updater.RemoveOldUpdates(); err != nil {
logrus.WithError(err).Error("Remove old updates fails") logrus.WithError(err).Error("Remove old updates fails")

View File

@ -0,0 +1,700 @@
// Copyright (c) 2025 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"
"runtime"
"testing"
"time"
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/go-proton-api/server"
bridgePkg "github.com/ProtonMail/proton-bridge/v3/internal/bridge"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
"github.com/ProtonMail/proton-bridge/v3/internal/updater/versioncompare"
"github.com/elastic/go-sysinfo/types"
"github.com/stretchr/testify/require"
)
// NOTE: we always assume the highest version is always the first in the release json array
func Test_Update_BetaEligible(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridgePkg.Locator, vaultKey []byte) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridgePkg.Bridge, mocks *bridgePkg.Mocks) {
updateCh, done := bridge.GetEvents(events.UpdateInstalled{})
defer done()
err := bridge.SetUpdateChannel(updater.EarlyChannel)
require.NoError(t, err)
bridge.SetCurrentVersionTest(semver.MustParse("2.1.1"))
expectedRelease := updater.Release{
ReleaseCategory: updater.EarlyAccessReleaseCategory,
Version: semver.MustParse("2.1.2"),
SystemVersion: versioncompare.SystemVersion{},
RolloutProportion: 1.0,
MinAuto: &semver.Version{},
File: []updater.File{
{
URL: "RANDOM_INSTALLER_URL",
Identifier: updater.InstallerIdentifier,
},
{
URL: "RANDOM_PACKAGE_URL",
Identifier: updater.PackageIdentifier,
},
},
}
updaterData := updater.VersionInfo{Releases: []updater.Release{
expectedRelease,
}}
go func() {
time.Sleep(1 * time.Second)
mocks.Updater.SetLatestVersion(updaterData)
bridge.CheckForUpdates()
}()
select {
case update := <-updateCh:
require.Equal(t, events.UpdateInstalled{
Release: expectedRelease,
Silent: true,
}, update)
case <-time.After(2 * time.Second):
t.Fatal("timeout waiting for update")
}
})
})
}
func Test_Update_Stable(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridgePkg.Locator, vaultKey []byte) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridgePkg.Bridge, mocks *bridgePkg.Mocks) {
updateCh, done := bridge.GetEvents(events.UpdateInstalled{})
defer done()
err := bridge.SetUpdateChannel(updater.StableChannel)
require.NoError(t, err)
bridge.SetCurrentVersionTest(semver.MustParse("2.1.1"))
expectedRelease := updater.Release{
ReleaseCategory: updater.StableReleaseCategory,
Version: semver.MustParse("2.1.3"),
SystemVersion: versioncompare.SystemVersion{},
RolloutProportion: 1.0,
MinAuto: &semver.Version{},
File: []updater.File{
{
URL: "RANDOM_INSTALLER_URL",
Identifier: updater.InstallerIdentifier,
},
{
URL: "RANDOM_PACKAGE_URL",
Identifier: updater.PackageIdentifier,
},
},
}
updaterData := updater.VersionInfo{Releases: []updater.Release{
{
ReleaseCategory: updater.EarlyAccessReleaseCategory,
Version: semver.MustParse("2.1.4"),
SystemVersion: versioncompare.SystemVersion{},
RolloutProportion: 1.0,
MinAuto: &semver.Version{},
File: []updater.File{
{
URL: "RANDOM_INSTALLER_URL",
Identifier: updater.InstallerIdentifier,
},
{
URL: "RANDOM_PACKAGE_URL",
Identifier: updater.PackageIdentifier,
},
},
},
expectedRelease,
}}
mocks.Updater.SetLatestVersion(updaterData)
bridge.CheckForUpdates()
require.Equal(t, events.UpdateInstalled{
Release: expectedRelease,
Silent: true,
}, <-updateCh)
})
})
}
func Test_Update_CurrentReleaseNewest(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridgePkg.Locator, vaultKey []byte) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridgePkg.Bridge, mocks *bridgePkg.Mocks) {
updateCh, done := bridge.GetEvents(events.UpdateNotAvailable{})
defer done()
err := bridge.SetUpdateChannel(updater.StableChannel)
require.NoError(t, err)
bridge.SetCurrentVersionTest(semver.MustParse("2.1.5"))
expectedRelease := updater.Release{
ReleaseCategory: updater.StableReleaseCategory,
Version: semver.MustParse("2.1.3"),
SystemVersion: versioncompare.SystemVersion{},
RolloutProportion: 1.0,
MinAuto: &semver.Version{},
File: []updater.File{
{
URL: "RANDOM_INSTALLER_URL",
Identifier: updater.InstallerIdentifier,
},
{
URL: "RANDOM_PACKAGE_URL",
Identifier: updater.PackageIdentifier,
},
},
}
updaterData := updater.VersionInfo{Releases: []updater.Release{
{
ReleaseCategory: updater.EarlyAccessReleaseCategory,
Version: semver.MustParse("2.1.4"),
SystemVersion: versioncompare.SystemVersion{},
RolloutProportion: 1.0,
MinAuto: &semver.Version{},
File: []updater.File{
{
URL: "RANDOM_INSTALLER_URL",
Identifier: updater.InstallerIdentifier,
},
{
URL: "RANDOM_PACKAGE_URL",
Identifier: updater.PackageIdentifier,
},
},
},
expectedRelease,
}}
mocks.Updater.SetLatestVersion(updaterData)
bridge.CheckForUpdates()
require.Equal(t, events.UpdateNotAvailable{}, <-updateCh)
})
})
}
func Test_Update_NotRolledOutYet(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridgePkg.Locator, vaultKey []byte) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridgePkg.Bridge, mocks *bridgePkg.Mocks) {
require.NoError(t, bridge.SetUpdateChannel(updater.EarlyChannel))
bridge.SetCurrentVersionTest(semver.MustParse("2.0.0"))
require.NoError(t, bridge.SetRolloutPercentageTest(1.0))
updaterData := updater.VersionInfo{Releases: []updater.Release{
{
ReleaseCategory: updater.StableReleaseCategory,
Version: semver.MustParse("2.1.5"),
SystemVersion: versioncompare.SystemVersion{},
RolloutProportion: 0.5,
MinAuto: &semver.Version{},
File: []updater.File{
{
URL: "RANDOM_INSTALLER_URL",
Identifier: updater.InstallerIdentifier,
},
{
URL: "RANDOM_PACKAGE_URL",
Identifier: updater.PackageIdentifier,
},
},
},
{
ReleaseCategory: updater.StableReleaseCategory,
Version: semver.MustParse("2.1.4"),
SystemVersion: versioncompare.SystemVersion{},
RolloutProportion: 0.5,
MinAuto: &semver.Version{},
File: []updater.File{
{
URL: "RANDOM_INSTALLER_URL",
Identifier: updater.InstallerIdentifier,
},
{
URL: "RANDOM_PACKAGE_URL",
Identifier: updater.PackageIdentifier,
},
},
},
}}
mocks.Updater.SetLatestVersion(updaterData)
updateCh, done := bridge.GetEvents(events.UpdateNotAvailable{})
defer done()
bridge.CheckForUpdates()
require.Equal(t, events.UpdateNotAvailable{}, <-updateCh)
})
})
}
func Test_Update_CheckOSVersion_NoUpdate(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridgePkg.Locator, vaultKey []byte) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridgePkg.Bridge, mocks *bridgePkg.Mocks) {
require.NoError(t, bridge.SetAutoUpdate(true))
require.NoError(t, bridge.SetUpdateChannel(updater.StableChannel))
currentBridgeVersion := semver.MustParse("2.1.5")
bridge.SetCurrentVersionTest(currentBridgeVersion)
// Override the OS version check
bridge.SetHostVersionGetterTest(func(_ types.Host) string {
return "10.0.0"
})
updateNotAvailableCh, done := bridge.GetEvents(events.UpdateNotAvailable{})
defer done()
updateCh, updateChDone := bridge.GetEvents(events.UpdateInstalled{})
defer updateChDone()
expectedRelease := updater.Release{
ReleaseCategory: updater.StableReleaseCategory,
Version: semver.MustParse("2.4.0"),
SystemVersion: versioncompare.SystemVersion{
Minimum: "12.0.0",
Maximum: "13.0.0",
},
RolloutProportion: 1.0,
File: []updater.File{
{
URL: "RANDOM_INSTALLER_URL",
Identifier: updater.InstallerIdentifier,
},
{
URL: "RANDOM_PACKAGE_URL",
Identifier: updater.PackageIdentifier,
},
},
}
updaterData := updater.VersionInfo{Releases: []updater.Release{
expectedRelease,
{
ReleaseCategory: updater.StableReleaseCategory,
Version: semver.MustParse("2.3.0"),
SystemVersion: versioncompare.SystemVersion{
Minimum: "10.1.0",
Maximum: "11.5",
},
RolloutProportion: 1.0,
File: []updater.File{
{
URL: "RANDOM_INSTALLER_URL",
Identifier: updater.InstallerIdentifier,
},
{
URL: "RANDOM_PACKAGE_URL",
Identifier: updater.PackageIdentifier,
},
},
},
}}
mocks.Updater.SetLatestVersion(updaterData)
bridge.CheckForUpdates()
if runtime.GOOS == "darwin" {
require.Equal(t, events.UpdateNotAvailable{}, <-updateNotAvailableCh)
} else {
require.Equal(t, events.UpdateInstalled{
Release: expectedRelease,
Silent: true,
}, <-updateCh)
}
})
})
}
func Test_Update_CheckOSVersion_HasUpdate(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridgePkg.Locator, vaultKey []byte) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridgePkg.Bridge, mocks *bridgePkg.Mocks) {
require.NoError(t, bridge.SetAutoUpdate(true))
require.NoError(t, bridge.SetUpdateChannel(updater.StableChannel))
updateCh, done := bridge.GetEvents(events.UpdateInstalled{})
defer done()
currentBridgeVersion := semver.MustParse("2.1.5")
bridge.SetCurrentVersionTest(currentBridgeVersion)
// Override the OS version check
bridge.SetHostVersionGetterTest(func(_ types.Host) string {
return "10.0.0"
})
expectedUpdateRelease := updater.Release{
ReleaseCategory: updater.StableReleaseCategory,
Version: semver.MustParse("2.2.0"),
SystemVersion: versioncompare.SystemVersion{
Minimum: "10.0.0",
Maximum: "10.1.12",
},
RolloutProportion: 1.0,
File: []updater.File{
{
URL: "RANDOM_INSTALLER_URL",
Identifier: updater.InstallerIdentifier,
},
{
URL: "RANDOM_PACKAGE_URL",
Identifier: updater.PackageIdentifier,
},
},
}
expectedUpdateReleaseWindowsLinux := updater.Release{
ReleaseCategory: updater.StableReleaseCategory,
Version: semver.MustParse("2.4.0"),
SystemVersion: versioncompare.SystemVersion{
Minimum: "12.0.0",
},
RolloutProportion: 1.0,
File: []updater.File{
{
URL: "RANDOM_INSTALLER_URL",
Identifier: updater.InstallerIdentifier,
},
{
URL: "RANDOM_PACKAGE_URL",
Identifier: updater.PackageIdentifier,
},
},
}
updaterData := updater.VersionInfo{Releases: []updater.Release{
expectedUpdateReleaseWindowsLinux,
{
ReleaseCategory: updater.StableReleaseCategory,
Version: semver.MustParse("2.3.0"),
SystemVersion: versioncompare.SystemVersion{
Minimum: "11.0.0",
},
RolloutProportion: 1.0,
File: []updater.File{
{
URL: "RANDOM_INSTALLER_URL",
Identifier: updater.InstallerIdentifier,
},
{
URL: "RANDOM_PACKAGE_URL",
Identifier: updater.PackageIdentifier,
},
},
},
expectedUpdateRelease,
{
ReleaseCategory: updater.StableReleaseCategory,
Version: semver.MustParse("2.1.0"),
SystemVersion: versioncompare.SystemVersion{},
RolloutProportion: 1.0,
File: []updater.File{
{
URL: "RANDOM_INSTALLER_URL",
Identifier: updater.InstallerIdentifier,
},
{
URL: "RANDOM_PACKAGE_URL",
Identifier: updater.PackageIdentifier,
},
},
},
}}
mocks.Updater.SetLatestVersion(updaterData)
bridge.CheckForUpdates()
if runtime.GOOS == "darwin" {
require.Equal(t, events.UpdateInstalled{
Release: expectedUpdateRelease,
Silent: true,
}, <-updateCh)
} else {
require.Equal(t, events.UpdateInstalled{
Release: expectedUpdateReleaseWindowsLinux,
Silent: true,
}, <-updateCh)
}
})
})
}
func Test_Update_UpdateFromMinVer_UpdateAvailable(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridgePkg.Locator, vaultKey []byte) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridgePkg.Bridge, mocks *bridgePkg.Mocks) {
require.NoError(t, bridge.SetAutoUpdate(true))
require.NoError(t, bridge.SetUpdateChannel(updater.StableChannel))
currentBridgeVersion := semver.MustParse("2.1.5")
bridge.SetCurrentVersionTest(currentBridgeVersion)
updateCh, done := bridge.GetEvents(events.UpdateInstalled{})
defer done()
expectedUpdateRelease := updater.Release{
ReleaseCategory: updater.StableReleaseCategory,
Version: semver.MustParse("2.2.0"),
SystemVersion: versioncompare.SystemVersion{},
RolloutProportion: 1.0,
MinAuto: currentBridgeVersion,
File: []updater.File{
{
URL: "RANDOM_INSTALLER_URL",
Identifier: updater.InstallerIdentifier,
},
{
URL: "RANDOM_PACKAGE_URL",
Identifier: updater.PackageIdentifier,
},
},
}
updaterData := updater.VersionInfo{Releases: []updater.Release{
{
ReleaseCategory: updater.StableReleaseCategory,
Version: semver.MustParse("2.3.0"),
SystemVersion: versioncompare.SystemVersion{},
RolloutProportion: 1.0,
MinAuto: semver.MustParse("2.2.1"),
File: []updater.File{
{
URL: "RANDOM_INSTALLER_URL",
Identifier: updater.InstallerIdentifier,
},
{
URL: "RANDOM_PACKAGE_URL",
Identifier: updater.PackageIdentifier,
},
},
},
{
ReleaseCategory: updater.StableReleaseCategory,
Version: semver.MustParse("2.2.1"),
SystemVersion: versioncompare.SystemVersion{},
RolloutProportion: 1.0,
MinAuto: semver.MustParse("2.2.0"),
File: []updater.File{
{
URL: "RANDOM_INSTALLER_URL",
Identifier: updater.InstallerIdentifier,
},
{
URL: "RANDOM_PACKAGE_URL",
Identifier: updater.PackageIdentifier,
},
},
},
expectedUpdateRelease,
}}
mocks.Updater.SetLatestVersion(updaterData)
bridge.CheckForUpdates()
require.Equal(t, events.UpdateInstalled{
Release: expectedUpdateRelease,
Silent: true,
}, <-updateCh)
})
})
}
// Test_Update_UpdateFromMinVer_NoCompatibleVersionForceManual -
// if we have an update, but we don't satisfy minVersion, a manual update to the highest possible version should be performed.
func Test_Update_UpdateFromMinVer_NoCompatibleVersionForceManual(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridgePkg.Locator, vaultKey []byte) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridgePkg.Bridge, mocks *bridgePkg.Mocks) {
require.NoError(t, bridge.SetAutoUpdate(true))
require.NoError(t, bridge.SetUpdateChannel(updater.StableChannel))
currentBridgeVersion := semver.MustParse("2.1.5")
bridge.SetCurrentVersionTest(currentBridgeVersion)
updateCh, done := bridge.GetEvents(events.UpdateAvailable{})
defer done()
expectedUpdateRelease := updater.Release{
ReleaseCategory: updater.StableReleaseCategory,
Version: semver.MustParse("2.3.0"),
SystemVersion: versioncompare.SystemVersion{},
RolloutProportion: 1.0,
MinAuto: semver.MustParse("2.2.1"),
File: []updater.File{
{
URL: "RANDOM_INSTALLER_URL",
Identifier: updater.InstallerIdentifier,
},
{
URL: "RANDOM_PACKAGE_URL",
Identifier: updater.PackageIdentifier,
},
},
}
updaterData := updater.VersionInfo{Releases: []updater.Release{
{
ReleaseCategory: updater.StableReleaseCategory,
Version: semver.MustParse("2.2.1"),
SystemVersion: versioncompare.SystemVersion{},
RolloutProportion: 1.0,
MinAuto: semver.MustParse("2.2.0"),
File: []updater.File{
{
URL: "RANDOM_INSTALLER_URL",
Identifier: updater.InstallerIdentifier,
},
{
URL: "RANDOM_PACKAGE_URL",
Identifier: updater.PackageIdentifier,
},
},
},
{
ReleaseCategory: updater.StableReleaseCategory,
Version: semver.MustParse("2.2.0"),
SystemVersion: versioncompare.SystemVersion{},
RolloutProportion: 1.0,
MinAuto: semver.MustParse("2.1.6"),
File: []updater.File{
{
URL: "RANDOM_INSTALLER_URL",
Identifier: updater.InstallerIdentifier,
},
{
URL: "RANDOM_PACKAGE_URL",
Identifier: updater.PackageIdentifier,
},
},
},
expectedUpdateRelease,
}}
mocks.Updater.SetLatestVersion(updaterData)
bridge.CheckForUpdates()
require.Equal(t, events.UpdateAvailable{
Release: expectedUpdateRelease,
Silent: false,
Compatible: false,
}, <-updateCh)
})
})
}
// Test_Update_UpdateFromMinVer_NoCompatibleVersionForceManual_BetaMismatch - only Beta updates are available
// nor do we satisfy the minVersion, we can't do anything in this case.
func Test_Update_UpdateFromMinVer_NoCompatibleVersionForceManual_BetaMismatch(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridgePkg.Locator, vaultKey []byte) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridgePkg.Bridge, mocks *bridgePkg.Mocks) {
require.NoError(t, bridge.SetAutoUpdate(true))
require.NoError(t, bridge.SetUpdateChannel(updater.StableChannel))
currentBridgeVersion := semver.MustParse("2.1.5")
bridge.SetCurrentVersionTest(currentBridgeVersion)
updateCh, done := bridge.GetEvents(events.UpdateNotAvailable{})
defer done()
expectedUpdateRelease := updater.Release{
ReleaseCategory: updater.EarlyAccessReleaseCategory,
Version: semver.MustParse("2.3.0"),
SystemVersion: versioncompare.SystemVersion{},
RolloutProportion: 1.0,
MinAuto: semver.MustParse("2.2.1"),
File: []updater.File{
{
URL: "RANDOM_INSTALLER_URL",
Identifier: updater.InstallerIdentifier,
},
{
URL: "RANDOM_PACKAGE_URL",
Identifier: updater.PackageIdentifier,
},
},
}
updaterData := updater.VersionInfo{Releases: []updater.Release{
{
ReleaseCategory: updater.EarlyAccessReleaseCategory,
Version: semver.MustParse("2.2.1"),
SystemVersion: versioncompare.SystemVersion{},
RolloutProportion: 1.0,
MinAuto: semver.MustParse("2.2.0"),
File: []updater.File{
{
URL: "RANDOM_INSTALLER_URL",
Identifier: updater.InstallerIdentifier,
},
{
URL: "RANDOM_PACKAGE_URL",
Identifier: updater.PackageIdentifier,
},
},
},
{
ReleaseCategory: updater.EarlyAccessReleaseCategory,
Version: semver.MustParse("2.2.0"),
SystemVersion: versioncompare.SystemVersion{},
RolloutProportion: 1.0,
MinAuto: semver.MustParse("2.1.6"),
File: []updater.File{
{
URL: "RANDOM_INSTALLER_URL",
Identifier: updater.InstallerIdentifier,
},
{
URL: "RANDOM_PACKAGE_URL",
Identifier: updater.PackageIdentifier,
},
},
},
expectedUpdateRelease,
}}
mocks.Updater.SetLatestVersion(updaterData)
bridge.CheckForUpdates()
require.Equal(t, events.UpdateNotAvailable{}, <-updateCh)
})
})
}

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //
@ -28,16 +28,20 @@ import (
"github.com/ProtonMail/gluon/reporter" "github.com/ProtonMail/gluon/reporter"
"github.com/ProtonMail/go-proton-api" "github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/proton-bridge/v3/internal/events" "github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/hv"
"github.com/ProtonMail/proton-bridge/v3/internal/logging" "github.com/ProtonMail/proton-bridge/v3/internal/logging"
"github.com/ProtonMail/proton-bridge/v3/internal/safe" "github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice" "github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice"
"github.com/ProtonMail/proton-bridge/v3/internal/try" "github.com/ProtonMail/proton-bridge/v3/internal/try"
"github.com/ProtonMail/proton-bridge/v3/internal/unleash"
"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/go-resty/resty/v2" "github.com/go-resty/resty/v2"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
var logUser = logrus.WithField("pkg", "bridge/user") //nolint:gochecknoglobals
type UserState int type UserState int
const ( const (
@ -121,23 +125,28 @@ func (bridge *Bridge) QueryUserInfo(query string) (UserInfo, error) {
} }
// LoginAuth begins the login process. It returns an authorized client that might need 2FA. // LoginAuth begins the login process. It returns an authorized client that might need 2FA.
func (bridge *Bridge) LoginAuth(ctx context.Context, username string, password []byte) (*proton.Client, proton.Auth, error) { func (bridge *Bridge) LoginAuth(ctx context.Context, username string, password []byte, hvDetails *proton.APIHVDetails) (*proton.Client, proton.Auth, error) {
logrus.WithField("username", logging.Sensitive(username)).Info("Authorizing user for login") logUser.WithField("username", logging.Sensitive(username)).Info("Authorizing user for login")
if username == "crash@bandicoot" { if username == "crash@bandicoot" {
panic("Your wish is my command.. I crash!") panic("Your wish is my command.. I crash!")
} }
client, auth, err := bridge.api.NewClientWithLoginWithHVToken(ctx, username, password, hvDetails)
client, auth, err := bridge.api.NewClientWithLogin(ctx, username, password)
if err != nil { if err != nil {
if hv.IsHvRequest(err) {
logUser.WithFields(logrus.Fields{"username": logging.Sensitive(username),
"loginError": err.Error()}).Info("Human Verification requested for login")
return nil, proton.Auth{}, err
}
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.UserID) }, 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") logUser.WithField("userID", auth.UserID).Warn("User already logged in")
if err := client.AuthDelete(ctx); err != nil { if err := client.AuthDelete(ctx); err != nil {
logrus.WithError(err).Warn("Failed to delete auth") logUser.WithError(err).Warn("Failed to delete auth")
} }
return nil, proton.Auth{}, ErrUserAlreadyLoggedIn return nil, proton.Auth{}, ErrUserAlreadyLoggedIn
@ -152,12 +161,13 @@ func (bridge *Bridge) LoginUser(
client *proton.Client, client *proton.Client,
auth proton.Auth, auth proton.Auth,
keyPass []byte, keyPass []byte,
hvDetails *proton.APIHVDetails,
) (string, error) { ) (string, error) {
logrus.WithField("userID", auth.UserID).Info("Logging in authorized user") logUser.WithField("userID", auth.UserID).Info("Logging in authorized user")
userID, err := try.CatchVal( userID, err := try.CatchVal(
func() (string, error) { func() (string, error) {
return bridge.loginUser(ctx, client, auth.UID, auth.RefreshToken, keyPass) return bridge.loginUser(ctx, client, auth.UID, auth.RefreshToken, keyPass, hvDetails)
}, },
) )
@ -165,7 +175,7 @@ func (bridge *Bridge) LoginUser(
// Failure to unlock will allow retries, so we do not delete auth. // Failure to unlock will allow retries, so we do not delete auth.
if !errors.Is(err, ErrFailedToUnlock) { if !errors.Is(err, ErrFailedToUnlock) {
if deleteErr := client.AuthDelete(ctx); deleteErr != nil { if deleteErr := client.AuthDelete(ctx); deleteErr != nil {
logrus.WithError(deleteErr).Error("Failed to delete auth") logUser.WithError(deleteErr).Error("Failed to delete auth")
} }
} }
return "", fmt.Errorf("failed to login user: %w", err) return "", fmt.Errorf("failed to login user: %w", err)
@ -188,15 +198,16 @@ func (bridge *Bridge) LoginFull(
getTOTP func() (string, error), getTOTP func() (string, error),
getKeyPass func() ([]byte, error), getKeyPass func() ([]byte, error),
) (string, error) { ) (string, error) {
logrus.WithField("username", logging.Sensitive(username)).Info("Performing full user login") logUser.WithField("username", logging.Sensitive(username)).Info("Performing full user login")
client, auth, err := bridge.LoginAuth(ctx, username, password) // (atanas) the following may need to be modified once HV is merged (its used only for testing; and depends on whether we will test HV related logic)
client, auth, err := bridge.LoginAuth(ctx, username, password, nil)
if err != nil { if err != nil {
return "", fmt.Errorf("failed to begin login process: %w", err) return "", fmt.Errorf("failed to begin login process: %w", err)
} }
if auth.TwoFA.Enabled&proton.HasTOTP != 0 { if auth.TwoFA.Enabled&proton.HasTOTP != 0 {
logrus.WithField("userID", auth.UserID).Info("Requesting TOTP") logUser.WithField("userID", auth.UserID).Info("Requesting TOTP")
totp, err := getTOTP() totp, err := getTOTP()
if err != nil { if err != nil {
@ -211,7 +222,7 @@ func (bridge *Bridge) LoginFull(
var keyPass []byte var keyPass []byte
if auth.PasswordMode == proton.TwoPasswordMode { if auth.PasswordMode == proton.TwoPasswordMode {
logrus.WithField("userID", auth.UserID).Info("Requesting mailbox password") logUser.WithField("userID", auth.UserID).Info("Requesting mailbox password")
userKeyPass, err := getKeyPass() userKeyPass, err := getKeyPass()
if err != nil { if err != nil {
@ -223,10 +234,10 @@ func (bridge *Bridge) LoginFull(
keyPass = password keyPass = password
} }
userID, err := bridge.LoginUser(ctx, client, auth, keyPass) userID, err := bridge.LoginUser(ctx, client, auth, keyPass, nil)
if err != nil { if err != nil {
if deleteErr := client.AuthDelete(ctx); deleteErr != nil { if deleteErr := client.AuthDelete(ctx); deleteErr != nil {
logrus.WithError(err).Error("Failed to delete auth") logUser.WithError(err).Error("Failed to delete auth")
} }
return "", err return "", err
@ -237,7 +248,7 @@ func (bridge *Bridge) LoginFull(
// LogoutUser logs out the given user. // LogoutUser logs out the given user.
func (bridge *Bridge) LogoutUser(ctx context.Context, userID string) error { func (bridge *Bridge) LogoutUser(ctx context.Context, userID string) error {
logrus.WithField("userID", userID).Info("Logging out user") logUser.WithField("userID", userID).Info("Logging out user")
return safe.LockRet(func() error { return safe.LockRet(func() error {
user, ok := bridge.users[userID] user, ok := bridge.users[userID]
@ -245,7 +256,7 @@ func (bridge *Bridge) LogoutUser(ctx context.Context, userID string) error {
return ErrNoSuchUser return ErrNoSuchUser
} }
bridge.logoutUser(ctx, user, true, false, false) bridge.logoutUser(ctx, user, true, false)
bridge.publish(events.UserLoggedOut{ bridge.publish(events.UserLoggedOut{
UserID: userID, UserID: userID,
@ -257,7 +268,7 @@ func (bridge *Bridge) LogoutUser(ctx context.Context, userID string) error {
// DeleteUser deletes the given user. // DeleteUser deletes the given user.
func (bridge *Bridge) DeleteUser(ctx context.Context, userID string) error { func (bridge *Bridge) DeleteUser(ctx context.Context, userID string) error {
logrus.WithField("userID", userID).Info("Deleting user") logUser.WithField("userID", userID).Info("Deleting user")
syncConfigDir, err := bridge.locator.ProvideIMAPSyncConfigPath() syncConfigDir, err := bridge.locator.ProvideIMAPSyncConfigPath()
if err != nil { if err != nil {
@ -270,7 +281,7 @@ func (bridge *Bridge) DeleteUser(ctx context.Context, userID string) error {
} }
if user, ok := bridge.users[userID]; ok { if user, ok := bridge.users[userID]; ok {
bridge.logoutUser(ctx, user, true, true, !bridge.GetTelemetryDisabled()) bridge.logoutUser(ctx, user, true, true)
} }
if err := imapservice.DeleteSyncState(syncConfigDir, userID); err != nil { if err := imapservice.DeleteSyncState(syncConfigDir, userID); err != nil {
@ -278,7 +289,7 @@ func (bridge *Bridge) DeleteUser(ctx context.Context, userID string) error {
} }
if err := bridge.vault.DeleteUser(userID); err != nil { if err := bridge.vault.DeleteUser(userID); err != nil {
logrus.WithError(err).Error("Failed to delete vault user") logUser.WithError(err).Error("Failed to delete vault user")
} }
bridge.publish(events.UserDeleted{ bridge.publish(events.UserDeleted{
@ -291,7 +302,7 @@ func (bridge *Bridge) DeleteUser(ctx context.Context, userID string) error {
// SetAddressMode sets the address mode for the given user. // SetAddressMode sets the address mode for the given user.
func (bridge *Bridge) SetAddressMode(ctx context.Context, userID string, mode vault.AddressMode) error { func (bridge *Bridge) SetAddressMode(ctx context.Context, userID string, mode vault.AddressMode) error {
logrus.WithField("userID", userID).WithField("mode", mode).Info("Setting address mode") logUser.WithField("userID", userID).WithField("mode", mode).Info("Setting address mode")
return safe.RLockRet(func() error { return safe.RLockRet(func() error {
user, ok := bridge.users[userID] user, ok := bridge.users[userID]
@ -327,7 +338,7 @@ func (bridge *Bridge) SetAddressMode(ctx context.Context, userID string, mode va
// SendBadEventUserFeedback passes the feedback to the given user. // SendBadEventUserFeedback passes the feedback to the given user.
func (bridge *Bridge) SendBadEventUserFeedback(_ context.Context, userID string, doResync bool) error { func (bridge *Bridge) SendBadEventUserFeedback(_ context.Context, userID string, doResync bool) error {
logrus.WithField("userID", userID).WithField("doResync", doResync).Info("Passing bad event feedback to user") logUser.WithField("userID", userID).WithField("doResync", doResync).Info("Passing bad event feedback to user")
return safe.RLockRet(func() error { return safe.RLockRet(func() error {
ctx := context.Background() ctx := context.Background()
@ -338,31 +349,17 @@ func (bridge *Bridge) SendBadEventUserFeedback(_ context.Context, userID string,
"Failed to handle event: feedback failed: no such user", "Failed to handle event: feedback failed: no such user",
reporter.Context{"user_id": userID}, reporter.Context{"user_id": userID},
); rerr != nil { ); rerr != nil {
logrus.WithError(rerr).Error("Failed to report feedback failure") logUser.WithError(rerr).Error("Failed to report feedback failure")
} }
return ErrNoSuchUser return ErrNoSuchUser
} }
if doResync { if doResync {
if rerr := bridge.reporter.ReportMessageWithContext(
"Failed to handle event: feedback resync",
reporter.Context{"user_id": userID},
); rerr != nil {
logrus.WithError(rerr).Error("Failed to report feedback failure")
}
return user.BadEventFeedbackResync(ctx) return user.BadEventFeedbackResync(ctx)
} }
if rerr := bridge.reporter.ReportMessageWithContext( bridge.logoutUser(ctx, user, true, false)
"Failed to handle event: feedback logout",
reporter.Context{"user_id": userID},
); rerr != nil {
logrus.WithError(rerr).Error("Failed to report feedback failure")
}
bridge.logoutUser(ctx, user, true, false, false)
bridge.publish(events.UserLoggedOut{ bridge.publish(events.UserLoggedOut{
UserID: userID, UserID: userID,
@ -372,8 +369,8 @@ func (bridge *Bridge) SendBadEventUserFeedback(_ context.Context, userID string,
}, bridge.usersLock) }, bridge.usersLock)
} }
func (bridge *Bridge) loginUser(ctx context.Context, client *proton.Client, authUID, authRef string, keyPass []byte) (string, error) { func (bridge *Bridge) loginUser(ctx context.Context, client *proton.Client, authUID, authRef string, keyPass []byte, hvDetails *proton.APIHVDetails) (string, error) {
apiUser, err := client.GetUser(ctx) apiUser, err := client.GetUserWithHV(ctx, hvDetails)
if err != nil { if err != nil {
return "", fmt.Errorf("failed to get API user: %w", err) return "", fmt.Errorf("failed to get API user: %w", err)
} }
@ -403,11 +400,11 @@ 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") logUser.WithField("count", len(bridge.vault.GetUserIDs())).Info("Loading users")
defer logrus.Info("Finished loading users") defer logUser.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()) log := logUser.WithField("userID", user.UserID())
if user.AuthUID() == "" { if user.AuthUID() == "" {
log.Info("User is not connected (skipping)") log.Info("User is not connected (skipping)")
@ -451,7 +448,7 @@ func (bridge *Bridge) loadUser(ctx context.Context, user *vault.User) error {
if apiErr := new(proton.APIError); 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") logUser.WithError(err).Warn("Failed to clear user secrets")
} }
} }
@ -496,24 +493,24 @@ func (bridge *Bridge) addUser(
if err := bridge.addUserWithVault(ctx, client, apiUser, vaultUser, isNew); err != nil { if err := bridge.addUserWithVault(ctx, client, apiUser, vaultUser, isNew); err != nil {
if _, ok := err.(*resty.ResponseError); ok || isLogin { if _, ok := err.(*resty.ResponseError); ok || isLogin {
logrus.WithError(err).Error("Failed to add user, clearing its secrets from vault") logUser.WithError(err).Error("Failed to add user, clearing its secrets from vault")
if err := vaultUser.Clear(); err != nil { if err := vaultUser.Clear(); err != nil {
logrus.WithError(err).Error("Failed to clear user secrets") logUser.WithError(err).Error("Failed to clear user secrets")
} }
} else { } else {
logrus.WithError(err).Error("Failed to add user") logUser.WithError(err).Error("Failed to add user")
} }
if err := vaultUser.Close(); err != nil { if err := vaultUser.Close(); err != nil {
logrus.WithError(err).Error("Failed to close vault user") logUser.WithError(err).Error("Failed to close vault user")
} }
if isNew { if isNew {
logrus.Warn("Deleting newly added vault user") logUser.Warn("Deleting newly added vault user")
if err := bridge.vault.DeleteUser(apiUser.ID); err != nil { if err := bridge.vault.DeleteUser(apiUser.ID); err != nil {
logrus.WithError(err).Error("Failed to delete vault user") logUser.WithError(err).Error("Failed to delete vault user")
} }
} }
@ -531,11 +528,6 @@ func (bridge *Bridge) addUserWithVault(
vault *vault.User, vault *vault.User,
isNew bool, isNew bool,
) error { ) error {
statsPath, err := bridge.locator.ProvideStatsPath()
if err != nil {
return fmt.Errorf("failed to get Statistics directory: %w", err)
}
syncSettingsPath, err := bridge.locator.ProvideIMAPSyncConfigPath() syncSettingsPath, err := bridge.locator.ProvideIMAPSyncConfigPath()
if err != nil { if err != nil {
return fmt.Errorf("failed to get IMAP sync config path: %w", err) return fmt.Errorf("failed to get IMAP sync config path: %w", err)
@ -550,14 +542,16 @@ func (bridge *Bridge) addUserWithVault(
bridge.panicHandler, bridge.panicHandler,
bridge.vault.GetShowAllMail(), bridge.vault.GetShowAllMail(),
bridge.vault.GetMaxSyncMemory(), bridge.vault.GetMaxSyncMemory(),
statsPath,
bridge, bridge,
bridge.serverManager, bridge.serverManager,
bridge.serverManager, bridge.serverManager,
&bridgeEventSubscription{b: bridge}, &bridgeEventSubscription{b: bridge},
bridge.syncService, bridge.syncService,
bridge.observabilityService,
syncSettingsPath, syncSettingsPath,
isNew, isNew,
bridge.notificationStore,
bridge.unleashService,
) )
if err != nil { if err != nil {
return fmt.Errorf("failed to create user: %w", err) return fmt.Errorf("failed to create user: %w", err)
@ -567,7 +561,7 @@ func (bridge *Bridge) addUserWithVault(
// For example, if the user's addresses change, we need to update them in gluon. // For example, if the user's addresses change, we need to update them in gluon.
bridge.tasks.Once(func(ctx context.Context) { bridge.tasks.Once(func(ctx context.Context) {
async.RangeContext(ctx, user.GetEventCh(), func(event events.Event) { async.RangeContext(ctx, user.GetEventCh(), func(event events.Event) {
logrus.WithFields(logrus.Fields{ logUser.WithFields(logrus.Fields{
"userID": apiUser.ID, "userID": apiUser.ID,
"event": event, "event": event,
}).Debug("Received user event") }).Debug("Received user event")
@ -590,12 +584,17 @@ func (bridge *Bridge) addUserWithVault(
// Finally, save the user in the bridge. // Finally, save the user in the bridge.
safe.Lock(func() { safe.Lock(func() {
bridge.users[apiUser.ID] = user bridge.users[apiUser.ID] = user
bridge.heartbeat.SetNbAccount(len(bridge.users)) bridge.heartbeat.SetNumberConnectedAccounts(len(bridge.users))
}, bridge.usersLock) }, bridge.usersLock)
// Set user plan if its of a higher rank.
bridge.heartbeat.SetUserPlan(user.GetUserPlanName())
// As we need at least one user to send heartbeat, try to send it. // As we need at least one user to send heartbeat, try to send it.
bridge.heartbeat.start() bridge.heartbeat.start()
user.PublishEvent(ctx, events.UserLoadedCheckResync{UserID: user.ID()})
return nil return nil
} }
@ -609,26 +608,21 @@ func (bridge *Bridge) newVaultUser(
return bridge.vault.GetOrAddUser(apiUser.ID, apiUser.Name, apiUser.Email, authUID, authRef, saltedKeyPass) return bridge.vault.GetOrAddUser(apiUser.ID, apiUser.Name, apiUser.Email, authUID, authRef, saltedKeyPass)
} }
// logout logs out the given user, optionally logging them out from the API too. // logoutUser logs out the given user, optionally logging them out from the API and deleting user related gluon data.
func (bridge *Bridge) logoutUser(ctx context.Context, user *user.User, withAPI, withData, withTelemetry bool) { func (bridge *Bridge) logoutUser(ctx context.Context, user *user.User, withAPI, withData bool) {
defer delete(bridge.users, user.ID()) defer delete(bridge.users, user.ID())
// if this is actually a remove account logUser.WithFields(logrus.Fields{
if withData && withAPI {
user.SendConfigStatusAbort(ctx, withTelemetry)
}
logrus.WithFields(logrus.Fields{
"userID": user.ID(), "userID": user.ID(),
"withAPI": withAPI, "withAPI": withAPI,
"withData": withData, "withData": withData,
}).Debug("Logging out user") }).Debug("Logging out user")
if err := user.Logout(ctx, withAPI); err != nil { if err := user.Logout(ctx, withAPI, withData, bridge.unleashService.GetFlagValue(unleash.UserRemovalGluonDataCleanupDisabled)); err != nil {
logrus.WithError(err).Error("Failed to logout user") logUser.WithError(err).Error("Failed to logout user")
} }
bridge.heartbeat.SetNbAccount(len(bridge.users)) bridge.heartbeat.SetNumberConnectedAccounts(len(bridge.users) - 1)
user.Close() user.Close()
} }

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //
@ -23,6 +23,7 @@ import (
"net" "net"
"net/http" "net/http"
"net/mail" "net/mail"
"runtime"
"strings" "strings"
"sync/atomic" "sync/atomic"
"testing" "testing"
@ -62,7 +63,7 @@ func TestBridge_User_RefreshEvent(t *testing.T) {
messageIDs = createNumMessages(ctx, t, c, addrID, labelID, 10) messageIDs = createNumMessages(ctx, t, c, addrID, labelID, 10)
}) })
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) {
userLoginAndSync(ctx, t, bridge, "user", password) userLoginAndSync(ctx, t, bridge, "user", password)
}) })
@ -73,16 +74,19 @@ func TestBridge_User_RefreshEvent(t *testing.T) {
require.NoError(t, s.RefreshUser(userID, proton.RefreshMail)) require.NoError(t, s.RefreshUser(userID, proton.RefreshMail))
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) {
syncCh, closeCh := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{})) syncCh, closeCh := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
if runtime.GOOS != "windows" {
require.Equal(t, userID, (<-syncCh).UserID)
}
require.Equal(t, userID, (<-syncCh).UserID) require.Equal(t, userID, (<-syncCh).UserID)
closeCh() closeCh()
userContinueEventProcess(ctx, t, s, bridge) userContinueEventProcess(ctx, t, s, bridge)
}) })
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) {
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) { withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
createNumMessages(ctx, t, c, addrID, labelID, 10) createNumMessages(ctx, t, c, addrID, labelID, 10)
}) })
@ -139,9 +143,6 @@ func test_badMessage_badEvent(userFeedback func(t *testing.T, ctx context.Contex
}) })
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) {
smtpWaiter := waitForSMTPServerReady(bridge)
defer smtpWaiter.Done()
userLoginAndSync(ctx, t, bridge, "user", password) userLoginAndSync(ctx, t, bridge, "user", password)
var messageIDs []string var messageIDs []string
@ -177,8 +178,6 @@ func test_badMessage_badEvent(userFeedback func(t *testing.T, ctx context.Contex
userFeedback(t, ctx, bridge, badUserID) userFeedback(t, ctx, bridge, badUserID)
smtpWaiter.Wait()
userContinueEventProcess(ctx, t, s, bridge) userContinueEventProcess(ctx, t, s, bridge)
}) })
}) })
@ -196,10 +195,7 @@ func TestBridge_User_BadMessage_NoBadEvent(t *testing.T) {
createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 10) createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 10)
}) })
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) {
smtpWaiter := waitForSMTPServerReady(bridge)
defer smtpWaiter.Done()
userLoginAndSync(ctx, t, bridge, "user", password) userLoginAndSync(ctx, t, bridge, "user", password)
var messageIDs []string var messageIDs []string
@ -223,7 +219,6 @@ func TestBridge_User_BadMessage_NoBadEvent(t *testing.T) {
require.NoError(t, c.DeleteMessage(ctx, messageIDs...)) require.NoError(t, c.DeleteMessage(ctx, messageIDs...))
}) })
smtpWaiter.Wait()
userContinueEventProcess(ctx, t, s, bridge) userContinueEventProcess(ctx, t, s, bridge)
}) })
}) })
@ -313,7 +308,7 @@ func TestBridge_User_AddressEvents_NoBadEvent(t *testing.T) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) { withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
userLoginAndSync(ctx, t, bridge, "user", password) userLoginAndSync(ctx, t, bridge, "user", password)
addrID, err = s.CreateAddress(userID, "other@pm.me", password) addrID, err = s.CreateAddress(userID, "other@pm.me", password, true)
require.NoError(t, err) require.NoError(t, err)
userContinueEventProcess(ctx, t, s, bridge) userContinueEventProcess(ctx, t, s, bridge)
@ -321,7 +316,7 @@ func TestBridge_User_AddressEvents_NoBadEvent(t *testing.T) {
userContinueEventProcess(ctx, t, s, bridge) userContinueEventProcess(ctx, t, s, bridge)
}) })
otherID, err := s.CreateAddress(userID, "another@pm.me", password) otherID, err := s.CreateAddress(userID, "another@pm.me", password, true)
require.NoError(t, err) require.NoError(t, err)
require.NoError(t, s.RemoveAddress(userID, otherID)) require.NoError(t, s.RemoveAddress(userID, otherID))
@ -337,6 +332,87 @@ func TestBridge_User_AddressEvents_NoBadEvent(t *testing.T) {
}) })
} }
func TestBridge_User_AddressEvents_BYOEAddressAdded(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
// Create a user.
userID, addrID, err := s.CreateUser("user", password)
require.NoError(t, err)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
userLoginAndSync(ctx, t, bridge, "user", password)
// Create an additional proton address
addrID, err = s.CreateAddress(userID, "other@pm.me", password, true)
require.NoError(t, err)
userContinueEventProcess(ctx, t, s, bridge)
require.NoError(t, s.AddAddressCreatedEvent(userID, addrID))
userContinueEventProcess(ctx, t, s, bridge)
userInfo, err := bridge.GetUserInfo(userID)
require.NoError(t, err)
require.Equal(t, 2, len(userInfo.Addresses))
// Create an external address with sending disabled.
externalID, err := s.CreateExternalAddress(userID, "another@yahoo.com", password, false)
require.NoError(t, err)
userContinueEventProcess(ctx, t, s, bridge)
require.NoError(t, s.AddAddressCreatedEvent(userID, externalID))
userContinueEventProcess(ctx, t, s, bridge)
// User addresses should still return 2, as we ignore the external address.
userInfo, err = bridge.GetUserInfo(userID)
require.NoError(t, err)
require.Equal(t, 2, len(userInfo.Addresses))
// Create an external address w. sending enabled. This is considered a BYOE address.
BYOEAddrID, err := s.CreateExternalAddress(userID, "other@yahoo.com", password, true)
require.NoError(t, err)
userContinueEventProcess(ctx, t, s, bridge)
require.NoError(t, s.AddAddressCreatedEvent(userID, BYOEAddrID))
userContinueEventProcess(ctx, t, s, bridge)
userInfo, err = bridge.GetUserInfo(userID)
require.NoError(t, err)
require.Equal(t, 3, len(userInfo.Addresses))
})
})
}
func TestBridge_User_AddressEvents_ExternalAddressSendChanged(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
userID, _, err := s.CreateUser("user", password)
require.NoError(t, err)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
userLoginAndSync(ctx, t, bridge, "user", password)
// Create an additional external address.
externalID, err := s.CreateExternalAddress(userID, "other@yahoo.me", password, false)
require.NoError(t, err)
userContinueEventProcess(ctx, t, s, bridge)
require.NoError(t, s.AddAddressCreatedEvent(userID, externalID))
userContinueEventProcess(ctx, t, s, bridge)
// We expect only one address, the external one without sending should not be considered a valid address.
userInfo, err := bridge.GetUserInfo(userID)
require.NoError(t, err)
require.Equal(t, 1, len(userInfo.Addresses))
// Change it to allow sending such that it becomes a BYOE address.
err = s.ChangeAddressAllowSend(userID, externalID, true)
require.NoError(t, err)
userContinueEventProcess(ctx, t, s, bridge)
require.NoError(t, s.AddAddressUpdatedEvent(userID, externalID))
userContinueEventProcess(ctx, t, s, bridge)
// We should now have 2 usable addresses listed.
userInfo, err = bridge.GetUserInfo(userID)
require.NoError(t, err)
require.Equal(t, 2, len(userInfo.Addresses))
})
})
}
func TestBridge_User_AddressEventUpdatedForAddressThatDoesNotExist_NoBadEvent(t *testing.T) { func TestBridge_User_AddressEventUpdatedForAddressThatDoesNotExist_NoBadEvent(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) {
// Create a user. // Create a user.
@ -377,7 +453,7 @@ func TestBridge_User_Network_NoBadEvents(t *testing.T) {
_, addrID, err := s.CreateUser("user", password) _, addrID, err := s.CreateUser("user", 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) {
userLoginAndSync(ctx, t, bridge, "user", password) userLoginAndSync(ctx, t, bridge, "user", password)
// Create 10 more messages for the user, generating events. // Create 10 more messages for the user, generating events.
@ -463,7 +539,7 @@ func TestBridge_User_UpdateDraft(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
// Initially sync the user. // Initially sync the user.
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) {
userLoginAndSync(ctx, t, bridge, "user", password) userLoginAndSync(ctx, t, bridge, "user", password)
}) })
@ -496,7 +572,7 @@ func TestBridge_User_UpdateDraft(t *testing.T) {
require.Empty(t, draft.ReplyTos) require.Empty(t, draft.ReplyTos)
// Process those events // Process those events
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) {
userContinueEventProcess(ctx, t, s, bridge) userContinueEventProcess(ctx, t, s, bridge)
}) })
@ -522,7 +598,7 @@ func TestBridge_User_UpdateDraftAndCreateOtherMessage(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
// Initially sync the user. // Initially sync the user.
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) {
userLoginAndSync(ctx, t, bridge, "user", password) userLoginAndSync(ctx, t, bridge, "user", password)
}) })
@ -554,7 +630,7 @@ func TestBridge_User_UpdateDraftAndCreateOtherMessage(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
// Process those events // Process those events
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) {
userContinueEventProcess(ctx, t, s, bridge) userContinueEventProcess(ctx, t, s, bridge)
}) })
@ -582,7 +658,7 @@ func TestBridge_User_UpdateDraftAndCreateOtherMessage(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
// Process those events. // Process those events.
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) {
userContinueEventProcess(ctx, t, s, bridge) userContinueEventProcess(ctx, t, s, bridge)
}) })
@ -590,7 +666,7 @@ func TestBridge_User_UpdateDraftAndCreateOtherMessage(t *testing.T) {
require.NoError(t, c.MarkMessagesUnread(ctx, res[0].MessageID)) require.NoError(t, c.MarkMessagesUnread(ctx, res[0].MessageID))
// Process those events. // Process those events.
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) {
userContinueEventProcess(ctx, t, s, bridge) userContinueEventProcess(ctx, t, s, bridge)
}) })
}) })
@ -604,7 +680,7 @@ func TestBridge_User_SendDraftRemoveDraftFlag(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
// Initially sync the user. // Initially sync the user.
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) {
userLoginAndSync(ctx, t, bridge, "user", password) userLoginAndSync(ctx, t, bridge, "user", password)
}) })
@ -637,7 +713,7 @@ func TestBridge_User_SendDraftRemoveDraftFlag(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
// Process those events // Process those events
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) {
userContinueEventProcess(ctx, t, s, bridge) userContinueEventProcess(ctx, t, s, bridge)
info, err := bridge.QueryUserInfo("user") info, err := bridge.QueryUserInfo("user")
@ -676,7 +752,7 @@ func TestBridge_User_SendDraftRemoveDraftFlag(t *testing.T) {
} }
// Process those events; the draft will move to the sent folder and lose the draft flag. // Process those events; the draft will move to the sent folder and lose the draft flag.
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) {
userContinueEventProcess(ctx, t, s, bridge) userContinueEventProcess(ctx, t, s, bridge)
info, err := bridge.QueryUserInfo("user") info, err := bridge.QueryUserInfo("user")
@ -703,10 +779,10 @@ func TestBridge_User_DisableEnableAddress(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
// Create an additional address for the user. // Create an additional address for the user.
aliasID, err := s.CreateAddress(userID, "alias@"+s.GetDomain(), password) aliasID, err := s.CreateAddress(userID, "alias@"+s.GetDomain(), password, true)
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) {
require.NoError(t, getErr(bridge.LoginFull(ctx, "user", password, nil, nil))) require.NoError(t, getErr(bridge.LoginFull(ctx, "user", password, nil, nil)))
// Initially we should list the address. // Initially we should list the address.
@ -720,7 +796,7 @@ func TestBridge_User_DisableEnableAddress(t *testing.T) {
require.NoError(t, c.DisableAddress(ctx, aliasID)) require.NoError(t, c.DisableAddress(ctx, aliasID))
}) })
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) {
// Eventually we shouldn't list the address. // Eventually we shouldn't list the address.
require.Eventually(t, func() bool { require.Eventually(t, func() bool {
info, err := bridge.QueryUserInfo("user") info, err := bridge.QueryUserInfo("user")
@ -735,7 +811,7 @@ func TestBridge_User_DisableEnableAddress(t *testing.T) {
require.NoError(t, c.EnableAddress(ctx, aliasID)) require.NoError(t, c.EnableAddress(ctx, aliasID))
}) })
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) {
// Eventually we should list the address. // Eventually we should list the address.
require.Eventually(t, func() bool { require.Eventually(t, func() bool {
info, err := bridge.QueryUserInfo("user") info, err := bridge.QueryUserInfo("user")
@ -754,7 +830,7 @@ func TestBridge_User_CreateDisabledAddress(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
// Create an additional address for the user. // Create an additional address for the user.
aliasID, err := s.CreateAddress(userID, "alias@"+s.GetDomain(), password) aliasID, err := s.CreateAddress(userID, "alias@"+s.GetDomain(), password, true)
require.NoError(t, err) require.NoError(t, err)
// Immediately disable the address. // Immediately disable the address.
@ -762,7 +838,7 @@ func TestBridge_User_CreateDisabledAddress(t *testing.T) {
require.NoError(t, c.DisableAddress(ctx, aliasID)) require.NoError(t, c.DisableAddress(ctx, aliasID))
}) })
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) {
require.NoError(t, getErr(bridge.LoginFull(ctx, "user", password, nil, nil))) require.NoError(t, getErr(bridge.LoginFull(ctx, "user", password, nil, nil)))
// Initially we shouldn't list the address. // Initially we shouldn't list the address.
@ -775,21 +851,12 @@ func TestBridge_User_CreateDisabledAddress(t *testing.T) {
func TestBridge_User_HandleParentLabelRename(t *testing.T) { func TestBridge_User_HandleParentLabelRename(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, _ *bridge.Mocks) {
imapWaiter := waitForIMAPServerReady(bridge)
defer imapWaiter.Done()
smtpWaiter := waitForSMTPServerReady(bridge)
defer smtpWaiter.Done()
require.NoError(t, getErr(bridge.LoginFull(ctx, username, password, nil, nil))) require.NoError(t, getErr(bridge.LoginFull(ctx, username, password, nil, nil)))
info, err := bridge.QueryUserInfo(username) info, err := bridge.QueryUserInfo(username)
require.NoError(t, err) require.NoError(t, err)
imapWaiter.Wait()
smtpWaiter.Wait()
cli, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort())) cli, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
require.NoError(t, err) require.NoError(t, err)
require.NoError(t, cli.Login(info.Addresses[0], string(info.BridgePass))) require.NoError(t, cli.Login(info.Addresses[0], string(info.BridgePass)))

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //
@ -36,15 +36,14 @@ func (bridge *Bridge) handleUserEvent(ctx context.Context, user *user.User, even
case events.UserBadEvent: case events.UserBadEvent:
bridge.handleUserBadEvent(ctx, user, event) bridge.handleUserBadEvent(ctx, user, event)
case events.UncategorizedEventError: case events.UserLoadedCheckResync:
bridge.handleUncategorizedErrorEvent(event) user.VerifyResyncAndExecute()
} }
} }
func (bridge *Bridge) handleUserDeauth(ctx context.Context, user *user.User) { func (bridge *Bridge) handleUserDeauth(ctx context.Context, user *user.User) {
safe.Lock(func() { safe.Lock(func() {
bridge.logoutUser(ctx, user, false, false, false) bridge.logoutUser(ctx, user, false, false)
user.ReportConfigStatusFailure("User deauth.")
}, bridge.usersLock) }, bridge.usersLock)
} }
@ -58,18 +57,9 @@ func (bridge *Bridge) handleUserBadEvent(ctx context.Context, user *user.User, e
"error": event.Error, "error": event.Error,
"error_type": internal.ErrCauseType(event.Error), "error_type": internal.ErrCauseType(event.Error),
}); rerr != nil { }); rerr != nil {
logrus.WithError(rerr).Error("Failed to report failed event handling") logrus.WithField("pkg", "bridge/event").WithError(rerr).Error("Failed to report failed event handling")
} }
user.OnBadEvent(ctx) user.OnBadEvent(ctx)
}, bridge.usersLock) }, bridge.usersLock)
} }
func (bridge *Bridge) handleUncategorizedErrorEvent(event events.UncategorizedEventError) {
if rerr := bridge.reporter.ReportMessageWithContext("Failed to handle due to uncategorized error", reporter.Context{
"error_type": internal.ErrCauseType(event.Error),
"error": event.Error,
}); rerr != nil {
logrus.WithError(rerr).Error("Failed to report failed event handling")
}
}

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //
@ -35,12 +35,12 @@ import (
func TestBridge_WithoutUsers(t *testing.T) { func TestBridge_WithoutUsers(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, _ *bridge.Mocks) {
require.Empty(t, bridge.GetUserIDs()) require.Empty(t, bridge.GetUserIDs())
require.Empty(t, getConnectedUserIDs(t, bridge)) require.Empty(t, getConnectedUserIDs(t, bridge))
}) })
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) {
require.Empty(t, bridge.GetUserIDs()) require.Empty(t, bridge.GetUserIDs())
require.Empty(t, getConnectedUserIDs(t, bridge)) require.Empty(t, getConnectedUserIDs(t, bridge))
}) })
@ -49,7 +49,7 @@ func TestBridge_WithoutUsers(t *testing.T) {
func TestBridge_Login(t *testing.T) { func TestBridge_Login(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, _ *bridge.Mocks) {
// Login the user. // Login the user.
userID, err := bridge.LoginFull(ctx, username, password, nil, nil) userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
require.NoError(t, err) require.NoError(t, err)
@ -69,7 +69,7 @@ func TestBridge_Login_DropConn(t *testing.T) {
defer func() { _ = dropListener.Close() }() defer func() { _ = dropListener.Close() }()
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, _ *bridge.Mocks) {
// Login the user. // Login the user.
userID, err := bridge.LoginFull(ctx, username, password, nil, nil) userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
require.NoError(t, err) require.NoError(t, err)
@ -96,7 +96,7 @@ func TestBridge_Login_DropConn(t *testing.T) {
return 0, false return 0, false
}) })
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) {
// The user is eventually connected. // The user is eventually connected.
require.Eventually(t, func() bool { require.Eventually(t, func() bool {
return len(bridge.GetUserIDs()) == 1 && len(getConnectedUserIDs(t, bridge)) == 1 return len(bridge.GetUserIDs()) == 1 && len(getConnectedUserIDs(t, bridge)) == 1
@ -107,7 +107,7 @@ func TestBridge_Login_DropConn(t *testing.T) {
func TestBridge_LoginTwice(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) { 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, _ *bridge.Mocks) {
// Login the user. // Login the user.
userID, err := bridge.LoginFull(ctx, username, password, nil, nil) userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
require.NoError(t, err) require.NoError(t, err)
@ -125,7 +125,7 @@ func TestBridge_LoginTwice(t *testing.T) {
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, _ *bridge.Mocks) {
// Login the user. // Login the user.
userID := must(bridge.LoginFull(ctx, username, password, nil, nil)) userID := must(bridge.LoginFull(ctx, username, password, nil, nil))
@ -153,7 +153,7 @@ func TestBridge_LoginLogoutLogin(t *testing.T) {
func TestBridge_LoginDeleteLogin(t *testing.T) { func TestBridge_LoginDeleteLogin(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, _ *bridge.Mocks) {
// Login the user. // Login the user.
userID := must(bridge.LoginFull(ctx, username, password, nil, nil)) userID := must(bridge.LoginFull(ctx, username, password, nil, nil))
@ -181,7 +181,7 @@ func TestBridge_LoginDeleteLogin(t *testing.T) {
func TestBridge_LoginDeauthLogin(t *testing.T) { func TestBridge_LoginDeauthLogin(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, _ *bridge.Mocks) {
// Login the user. // Login the user.
userID := must(bridge.LoginFull(ctx, username, password, nil, nil)) userID := must(bridge.LoginFull(ctx, username, password, nil, nil))
@ -215,7 +215,7 @@ func TestBridge_LoginDeauthRestartLogin(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) {
var userID string var userID string
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) {
// Login the user. // Login the user.
userID = must(bridge.LoginFull(ctx, username, password, nil, nil)) userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
@ -235,7 +235,7 @@ func TestBridge_LoginDeauthRestartLogin(t *testing.T) {
require.IsType(t, events.UserDeauth{}, <-eventCh) require.IsType(t, events.UserDeauth{}, <-eventCh)
}) })
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) {
// The user should be disconnected at startup. // The user should be disconnected at startup.
require.Equal(t, []string{userID}, bridge.GetUserIDs()) require.Equal(t, []string{userID}, bridge.GetUserIDs())
require.Empty(t, getConnectedUserIDs(t, bridge)) require.Empty(t, getConnectedUserIDs(t, bridge))
@ -257,7 +257,7 @@ func TestBridge_LoginExpireLogin(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) {
s.SetAuthLife(authLife) s.SetAuthLife(authLife)
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) {
// Login the user. Its auth will only be valid for a short time. // Login the user. Its auth will only be valid for a short time.
userID := must(bridge.LoginFull(ctx, username, password, nil, nil)) userID := must(bridge.LoginFull(ctx, username, password, nil, nil))
@ -275,7 +275,7 @@ func TestBridge_FailToLoad(t *testing.T) {
var userID string var userID string
// Login the user. // Login the user.
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) {
userID = must(bridge.LoginFull(ctx, username, password, nil, nil)) userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
}) })
@ -283,7 +283,7 @@ func TestBridge_FailToLoad(t *testing.T) {
require.NoError(t, s.RevokeUser(userID)) require.NoError(t, s.RevokeUser(userID))
// When bridge starts, the user will not be logged in. // When bridge starts, the user will not be logged in.
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) {
require.Equal(t, []string{userID}, bridge.GetUserIDs()) require.Equal(t, []string{userID}, bridge.GetUserIDs())
require.Empty(t, getConnectedUserIDs(t, bridge)) require.Empty(t, getConnectedUserIDs(t, bridge))
}) })
@ -295,7 +295,7 @@ func TestBridge_LoadWithoutInternet(t *testing.T) {
var userID string var userID string
// Login the user. // Login the user.
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) {
userID = must(bridge.LoginFull(ctx, username, password, nil, nil)) userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
}) })
@ -303,7 +303,7 @@ func TestBridge_LoadWithoutInternet(t *testing.T) {
netCtl.Disable() netCtl.Disable()
// Start bridge without internet. // Start bridge without internet.
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) {
// Initially, users are not connected. // Initially, users are not connected.
require.Equal(t, []string{userID}, bridge.GetUserIDs()) require.Equal(t, []string{userID}, bridge.GetUserIDs())
require.Empty(t, getConnectedUserIDs(t, bridge)) require.Empty(t, getConnectedUserIDs(t, bridge))
@ -325,11 +325,11 @@ func TestBridge_LoginRestart(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) {
var userID string var userID string
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) {
userID = must(bridge.LoginFull(ctx, username, password, nil, nil)) userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
}) })
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) {
require.Equal(t, []string{userID}, bridge.GetUserIDs()) require.Equal(t, []string{userID}, bridge.GetUserIDs())
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge)) require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
}) })
@ -340,7 +340,7 @@ func TestBridge_LoginLogoutRestart(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) {
var userID string var userID string
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) {
// Login the user. // Login the user.
userID = must(bridge.LoginFull(ctx, username, password, nil, nil)) userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
@ -348,7 +348,7 @@ func TestBridge_LoginLogoutRestart(t *testing.T) {
require.NoError(t, bridge.LogoutUser(ctx, userID)) require.NoError(t, bridge.LogoutUser(ctx, userID))
}) })
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) {
// The user is still disconnected. // The user is still disconnected.
require.Equal(t, []string{userID}, bridge.GetUserIDs()) require.Equal(t, []string{userID}, bridge.GetUserIDs())
require.Empty(t, getConnectedUserIDs(t, bridge)) require.Empty(t, getConnectedUserIDs(t, bridge))
@ -360,7 +360,7 @@ func TestBridge_LoginDeleteRestart(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) {
var userID string var userID string
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) {
// Login the user. // Login the user.
userID = must(bridge.LoginFull(ctx, username, password, nil, nil)) userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
@ -368,7 +368,7 @@ func TestBridge_LoginDeleteRestart(t *testing.T) {
require.NoError(t, bridge.DeleteUser(ctx, userID)) require.NoError(t, bridge.DeleteUser(ctx, userID))
}) })
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) {
// The user is still gone. // The user is still gone.
require.Empty(t, bridge.GetUserIDs()) require.Empty(t, bridge.GetUserIDs())
require.Empty(t, getConnectedUserIDs(t, bridge)) require.Empty(t, getConnectedUserIDs(t, bridge))
@ -384,7 +384,7 @@ func TestBridge_FailLoginRecover(t *testing.T) {
// Log the user in, wait for it to sync, then log it out. // Log the user in, wait for it to sync, then log it out.
// (We don't want to count message sync data in the test.) // (We don't want to count message sync data in the test.)
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) {
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{})) syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
defer done() defer done()
@ -396,7 +396,7 @@ func TestBridge_FailLoginRecover(t *testing.T) {
var total uint64 var total uint64
// Now that the user is synced, we can measure exactly how much data is needed during login. // Now that the user is synced, we can measure exactly how much data is needed during login.
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) {
total = countBytesRead(netCtl, func() { total = countBytesRead(netCtl, func() {
must(bridge.LoginFull(ctx, username, password, nil, nil)) must(bridge.LoginFull(ctx, username, password, nil, nil))
}) })
@ -405,7 +405,7 @@ func TestBridge_FailLoginRecover(t *testing.T) {
}) })
// Now simulate failing to login. // Now simulate failing to login.
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) {
// Simulate a partial read. // Simulate a partial read.
netCtl.SetReadLimit(i * total / 10) netCtl.SetReadLimit(i * total / 10)
@ -421,7 +421,7 @@ func TestBridge_FailLoginRecover(t *testing.T) {
netCtl.SetReadLimit(0) netCtl.SetReadLimit(0)
// We should now be able to log the user in. // We should now be able to log the user in.
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) {
require.NoError(t, getErr(bridge.LoginFull(ctx, username, password, nil, nil))) require.NoError(t, getErr(bridge.LoginFull(ctx, username, password, nil, nil)))
// The user should be there, now connected. // The user should be there, now connected.
@ -441,7 +441,7 @@ func TestBridge_FailLoadRecover(t *testing.T) {
// Log the user in and wait for it to sync. // Log the user in and wait for it to sync.
// (We don't want to count message sync data in the test.) // (We don't want to count message sync data in the test.)
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) {
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{})) syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
defer done() defer done()
@ -451,7 +451,7 @@ func TestBridge_FailLoadRecover(t *testing.T) {
// See how much data it takes to load the user at startup. // See how much data it takes to load the user at startup.
total := countBytesRead(netCtl, func() { total := countBytesRead(netCtl, func() {
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) {
// ... // ...
}) })
}) })
@ -460,7 +460,7 @@ func TestBridge_FailLoadRecover(t *testing.T) {
netCtl.SetReadLimit(i * total / 10) netCtl.SetReadLimit(i * total / 10)
// We should fail to load the user; it should be listed but disconnected. // We should fail to load the user; it should be listed but disconnected.
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) {
require.Equal(t, []string{userID}, bridge.GetUserIDs()) require.Equal(t, []string{userID}, bridge.GetUserIDs())
require.Empty(t, getConnectedUserIDs(t, bridge)) require.Empty(t, getConnectedUserIDs(t, bridge))
}) })
@ -469,7 +469,7 @@ func TestBridge_FailLoadRecover(t *testing.T) {
netCtl.SetReadLimit(0) netCtl.SetReadLimit(0)
// We should now be able to load the user. // We should now be able to load the user.
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) {
require.Equal(t, []string{userID}, bridge.GetUserIDs()) require.Equal(t, []string{userID}, bridge.GetUserIDs())
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge)) require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
}) })
@ -484,7 +484,7 @@ func TestBridge_BridgePass(t *testing.T) {
var pass []byte var pass []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, _ *bridge.Mocks) {
// Login the user. // Login the user.
userID = must(bridge.LoginFull(ctx, username, password, nil, nil)) userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
@ -501,7 +501,7 @@ func TestBridge_BridgePass(t *testing.T) {
require.Equal(t, pass, pass) require.Equal(t, pass, pass)
}) })
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) {
// The bridge should load the user. // The bridge should load the user.
require.Equal(t, []string{userID}, bridge.GetUserIDs()) require.Equal(t, []string{userID}, bridge.GetUserIDs())
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge)) require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
@ -514,7 +514,7 @@ func TestBridge_BridgePass(t *testing.T) {
func TestBridge_AddressMode(t *testing.T) { func TestBridge_AddressMode(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, _ *bridge.Mocks) {
// Login the user. // Login the user.
userID, err := bridge.LoginFull(ctx, username, password, nil, nil) userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
require.NoError(t, err) require.NoError(t, err)
@ -552,7 +552,7 @@ func TestBridge_AddressMode(t *testing.T) {
func TestBridge_LoginLogoutRepeated(t *testing.T) { func TestBridge_LoginLogoutRepeated(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, _ *bridge.Mocks) {
for i := 0; i < 10; i++ { for i := 0; i < 10; i++ {
// Log the user in. // Log the user in.
userID := must(bridge.LoginFull(ctx, username, password, nil, nil)) userID := must(bridge.LoginFull(ctx, username, password, nil, nil))
@ -568,7 +568,7 @@ func TestBridge_LogoutOffline(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) {
var userID string var userID string
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) {
// Login the user. // Login the user.
userID = must(bridge.LoginFull(ctx, username, password, nil, nil)) userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
@ -590,7 +590,7 @@ func TestBridge_LogoutOffline(t *testing.T) {
// Go back online. // Go back online.
netCtl.Enable() netCtl.Enable()
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) {
// The user is still disconnected. // The user is still disconnected.
require.Equal(t, []string{userID}, bridge.GetUserIDs()) require.Equal(t, []string{userID}, bridge.GetUserIDs())
require.Empty(t, getConnectedUserIDs(t, bridge)) require.Empty(t, getConnectedUserIDs(t, bridge))
@ -600,7 +600,7 @@ func TestBridge_LogoutOffline(t *testing.T) {
func TestBridge_DeleteDisconnected(t *testing.T) { func TestBridge_DeleteDisconnected(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, _ *bridge.Mocks) {
// Login the user. // Login the user.
userID, err := bridge.LoginFull(ctx, username, password, nil, nil) userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
require.NoError(t, err) require.NoError(t, err)
@ -628,7 +628,7 @@ func TestBridge_DeleteDisconnected(t *testing.T) {
func TestBridge_DeleteOffline(t *testing.T) { func TestBridge_DeleteOffline(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, _ *bridge.Mocks) {
// Login the user. // Login the user.
userID, err := bridge.LoginFull(ctx, username, password, nil, nil) userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
require.NoError(t, err) require.NoError(t, err)
@ -652,13 +652,13 @@ func TestBridge_DeleteOffline(t *testing.T) {
func TestBridge_UserInfo_Alias(t *testing.T) { 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, _ *bridge.Mocks) {
// Create a new user. // Create a new user.
userID, _, err := s.CreateUser("primary", []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.
require.NoError(t, getErr(s.CreateAddress(userID, "alias@pm.me", []byte("password")))) require.NoError(t, getErr(s.CreateAddress(userID, "alias@pm.me", []byte("password"), true)))
// Login the user. // Login the user.
require.NoError(t, getErr(bridge.LoginFull(ctx, "primary", []byte("password"), nil, nil))) require.NoError(t, getErr(bridge.LoginFull(ctx, "primary", []byte("password"), nil, nil)))
@ -675,7 +675,7 @@ func TestBridge_UserInfo_Alias(t *testing.T) {
func TestBridge_User_Refresh(t *testing.T) { func TestBridge_User_Refresh(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, _ *bridge.Mocks) {
// Get a channel of sync started events. // Get a channel of sync started events.
syncStartCh, done := chToType[events.Event, events.SyncStarted](bridge.GetEvents(events.SyncStarted{})) syncStartCh, done := chToType[events.Event, events.SyncStarted](bridge.GetEvents(events.SyncStarted{}))
defer done() defer done()
@ -706,7 +706,7 @@ func TestBridge_User_GetAddresses(t *testing.T) {
// Create a user. // Create a user.
userID, _, err := s.CreateUser("user", password) userID, _, err := s.CreateUser("user", password)
require.NoError(t, err) require.NoError(t, err)
addrID2, err := s.CreateAddress(userID, "user@external.com", []byte("password")) addrID2, err := s.CreateAddress(userID, "user@external.com", password, false)
require.NoError(t, err) require.NoError(t, err)
require.NoError(t, s.ChangeAddressType(userID, addrID2, proton.AddressTypeExternal)) require.NoError(t, s.ChangeAddressType(userID, addrID2, proton.AddressTypeExternal))
@ -720,6 +720,29 @@ func TestBridge_User_GetAddresses(t *testing.T) {
}) })
} }
func TestBridge_User_GetAddresses_BYOE(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
// Create a user.
userID, _, err := s.CreateUser("user", password)
require.NoError(t, err)
// Add a non-sending external address.
_, err = s.CreateExternalAddress(userID, "user@external.com", password, false)
require.NoError(t, err)
// Add a BYOE address.
_, err = s.CreateExternalAddress(userID, "user2@external.com", password, true)
require.NoError(t, err)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
userLoginAndSync(ctx, t, bridge, "user", password)
info, err := bridge.GetUserInfo(userID)
require.NoError(t, err)
require.Equal(t, 2, len(info.Addresses))
require.Equal(t, info.Addresses[0], "user@proton.local")
require.Equal(t, info.Addresses[1], "user2@external.com")
})
})
}
// getErr returns the error that was passed to it. // getErr returns the error that was passed to it.
func getErr[T any](_ T, err error) error { func getErr[T any](_ T, err error) error {
return err return err

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //
@ -21,6 +21,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/ProtonMail/proton-bridge/v3/internal/useragent" "github.com/ProtonMail/proton-bridge/v3/internal/useragent"
@ -70,24 +71,24 @@ func prepareMobileConfig(
password []byte, password []byte,
) *mobileconfig.Config { ) *mobileconfig.Config {
return &mobileconfig.Config{ return &mobileconfig.Config{
DisplayName: username, DisplayName: escapeXMLString(username),
EmailAddress: addresses, EmailAddress: escapeXMLString(addresses),
AccountName: displayName, AccountName: escapeXMLString(displayName),
AccountDescription: username, AccountDescription: escapeXMLString(username),
Identifier: "protonmail " + username + strconv.FormatInt(time.Now().Unix(), 10), Identifier: escapeXMLString("protonmail " + username + strconv.FormatInt(time.Now().Unix(), 10)),
IMAP: &mobileconfig.IMAP{ IMAP: &mobileconfig.IMAP{
Hostname: hostname, Hostname: escapeXMLString(hostname),
Port: imapPort, Port: imapPort,
TLS: imapSSL, TLS: imapSSL,
Username: username, Username: escapeXMLString(username),
Password: string(password), Password: escapeXMLString(string(password)),
}, },
SMTP: &mobileconfig.SMTP{ SMTP: &mobileconfig.SMTP{
Hostname: hostname, Hostname: escapeXMLString(hostname),
Port: smtpPort, Port: smtpPort,
TLS: smtpSSL, TLS: smtpSSL,
Username: username, Username: escapeXMLString(username),
Password: string(password), Password: escapeXMLString(string(password)),
}, },
} }
} }
@ -121,3 +122,13 @@ func saveConfigTemporarily(mc *mobileconfig.Config) (fname string, err error) {
return return
} }
// escapeXMLString replace all occurrences of the 5 characters `&`, `<`, `>`, `"` and `'` by their respective escaped version as per the XML spec.
// https://www.w3.org/TR/xml/#syntax
func escapeXMLString(input string) string {
result := strings.ReplaceAll(input, `&`, `&amp;`)
result = strings.ReplaceAll(result, `<`, `&lt;`)
result = strings.ReplaceAll(result, `>`, `&gt;`)
result = strings.ReplaceAll(result, `"`, `&quot;`)
return strings.ReplaceAll(result, `'`, `&apos;`)
}

View File

@ -0,0 +1,38 @@
// Copyright (c) 2025 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/>.
//go:build darwin
package clientconfig
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestEscapeXMLString(t *testing.T) {
require.Equal(t, escapeXMLString(`abc&&''""<<>>def`), `abc&amp;&amp;&apos;&apos;&quot;&quot;&lt;&lt;&gt;&gt;def`)
}
// This test requires human interaction (user configuration profile installation prompt). It is for debugging purpose and is disabled by default.
func _TestInstallCert(t *testing.T) { //nolint:unused
require.NoError(
t,
(&AppleMail{}).Configure(`127.0.0.1`, 1143, 1025, true, false, `user&>>`, `<<abc&&'"def>>`, `user&a`, []byte(`ir8R9vhdNXyB7isWzhyEkQ`)),
)
}

View File

@ -1,228 +0,0 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package configstatus
import (
"encoding/json"
"fmt"
"os"
"strconv"
"time"
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/sirupsen/logrus"
)
const version = "1.0.0"
func LoadConfigurationStatus(filepath string) (*ConfigurationStatus, error) {
status := ConfigurationStatus{
FilePath: filepath,
DataLock: safe.NewRWMutex(),
Data: &ConfigurationStatusData{},
}
if _, err := os.Stat(filepath); err == nil {
if err := status.Load(); err == nil {
return &status, nil
}
logrus.WithError(err).Warn("Cannot load configuration status file. Reset it.")
}
status.Data.init()
if err := status.Save(); err != nil {
return &status, err
}
return &status, nil
}
func (status *ConfigurationStatus) Load() error {
bytes, err := os.ReadFile(status.FilePath)
if err != nil {
return err
}
var metadata MetadataOnly
if err := json.Unmarshal(bytes, &metadata); err != nil {
return err
}
if metadata.Metadata.Version != version {
return fmt.Errorf("unsupported configstatus file version %s", metadata.Metadata.Version)
}
return json.Unmarshal(bytes, status.Data)
}
func (status *ConfigurationStatus) Save() error {
temp := status.FilePath + "_temp"
f, err := os.Create(temp) //nolint:gosec
if err != nil {
return err
}
enc := json.NewEncoder(f)
enc.SetIndent("", " ")
err = enc.Encode(status.Data)
if err := f.Close(); err != nil {
logrus.WithError(err).Error("Error while closing configstatus file.")
}
if err != nil {
return err
}
return os.Rename(temp, status.FilePath)
}
func (status *ConfigurationStatus) IsPending() bool {
status.DataLock.RLock()
defer status.DataLock.RUnlock()
return !status.Data.DataV1.PendingSince.IsZero()
}
func (status *ConfigurationStatus) isPendingSinceMin() int {
if min := int(time.Since(status.Data.DataV1.PendingSince).Minutes()); min > 0 {
return min
}
return 0
}
func (status *ConfigurationStatus) IsFromFailure() bool {
status.DataLock.RLock()
defer status.DataLock.RUnlock()
return status.Data.DataV1.FailureDetails != ""
}
func (status *ConfigurationStatus) ApplySuccess() error {
status.DataLock.Lock()
defer status.DataLock.Unlock()
status.Data.init()
status.Data.DataV1.PendingSince = time.Time{}
return status.Save()
}
func (status *ConfigurationStatus) ApplyFailure(err string) error {
status.DataLock.Lock()
defer status.DataLock.Unlock()
status.Data.init()
status.Data.DataV1.FailureDetails = err
return status.Save()
}
func (status *ConfigurationStatus) ApplyProgress() error {
status.DataLock.Lock()
defer status.DataLock.Unlock()
status.Data.DataV1.LastProgress = time.Now()
return status.Save()
}
func (status *ConfigurationStatus) RecordLinkClicked(link uint) error {
status.DataLock.Lock()
defer status.DataLock.Unlock()
if !status.Data.hasLinkClicked(link) {
status.Data.setClickedLink(link)
return status.Save()
}
return nil
}
func (status *ConfigurationStatus) ReportClicked() error {
status.DataLock.Lock()
defer status.DataLock.Unlock()
if !status.Data.DataV1.ReportClick {
status.Data.DataV1.ReportClick = true
return status.Save()
}
return nil
}
func (status *ConfigurationStatus) ReportSent() error {
status.DataLock.Lock()
defer status.DataLock.Unlock()
if !status.Data.DataV1.ReportSent {
status.Data.DataV1.ReportSent = true
return status.Save()
}
return nil
}
func (status *ConfigurationStatus) AutoconfigUsed(client string) error {
status.DataLock.Lock()
defer status.DataLock.Unlock()
if client != status.Data.DataV1.Autoconf {
status.Data.DataV1.Autoconf = client
return status.Save()
}
return nil
}
func (status *ConfigurationStatus) Remove() error {
status.DataLock.Lock()
defer status.DataLock.Unlock()
return os.Remove(status.FilePath)
}
func (data *ConfigurationStatusData) init() {
data.Metadata = Metadata{
Version: version,
}
data.DataV1.PendingSince = time.Now()
data.DataV1.LastProgress = time.Time{}
data.DataV1.Autoconf = ""
data.DataV1.ClickedLink = 0
data.DataV1.ReportSent = false
data.DataV1.ReportClick = false
data.DataV1.FailureDetails = ""
}
func (data *ConfigurationStatusData) setClickedLink(pos uint) {
data.DataV1.ClickedLink |= 1 << pos
}
func (data *ConfigurationStatusData) hasLinkClicked(pos uint) bool {
val := data.DataV1.ClickedLink & (1 << pos)
return val > 0
}
func (data *ConfigurationStatusData) clickedLinkToString() string {
var str = ""
var first = true
for i := 0; i < 64; i++ {
if data.hasLinkClicked(uint(i)) {
if !first {
str += ","
} else {
first = false
str += "["
}
str += strconv.Itoa(i)
}
}
if str != "" {
str += "]"
}
return str
}

View File

@ -1,252 +0,0 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package configstatus_test
import (
"encoding/json"
"os"
"path/filepath"
"testing"
"time"
"github.com/ProtonMail/proton-bridge/v3/internal/configstatus"
"github.com/stretchr/testify/require"
)
func TestConfigStatus_init_virgin(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
require.Equal(t, "1.0.0", config.Data.Metadata.Version)
require.Equal(t, false, config.Data.DataV1.PendingSince.IsZero())
require.Equal(t, true, config.Data.DataV1.LastProgress.IsZero())
require.Equal(t, "", config.Data.DataV1.Autoconf)
require.Equal(t, uint64(0), config.Data.DataV1.ClickedLink)
require.Equal(t, false, config.Data.DataV1.ReportSent)
require.Equal(t, false, config.Data.DataV1.ReportClick)
require.Equal(t, "", config.Data.DataV1.FailureDetails)
}
func TestConfigStatus_init_existing(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
var data = configstatus.ConfigurationStatusData{
Metadata: configstatus.Metadata{Version: "1.0.0"},
DataV1: configstatus.DataV1{Autoconf: "Mr TBird"},
}
require.NoError(t, dumpConfigStatusInFile(&data, file))
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
require.Equal(t, "1.0.0", config.Data.Metadata.Version)
require.Equal(t, "Mr TBird", config.Data.DataV1.Autoconf)
}
func TestConfigStatus_init_bad_version(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
var data = configstatus.ConfigurationStatusData{
Metadata: configstatus.Metadata{Version: "2.0.0"},
DataV1: configstatus.DataV1{Autoconf: "Mr TBird"},
}
require.NoError(t, dumpConfigStatusInFile(&data, file))
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
require.Equal(t, "1.0.0", config.Data.Metadata.Version)
require.Equal(t, "", config.Data.DataV1.Autoconf)
}
func TestConfigStatus_IsPending(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
require.Equal(t, true, config.IsPending())
config.Data.DataV1.PendingSince = time.Time{}
require.Equal(t, false, config.IsPending())
}
func TestConfigStatus_IsFromFailure(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
require.Equal(t, false, config.IsFromFailure())
config.Data.DataV1.FailureDetails = "test"
require.Equal(t, true, config.IsFromFailure())
}
func TestConfigStatus_ApplySuccess(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
require.Equal(t, true, config.IsPending())
require.NoError(t, config.ApplySuccess())
require.Equal(t, false, config.IsPending())
config2, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
require.Equal(t, "1.0.0", config2.Data.Metadata.Version)
require.Equal(t, true, config2.Data.DataV1.PendingSince.IsZero())
require.Equal(t, true, config2.Data.DataV1.LastProgress.IsZero())
require.Equal(t, "", config2.Data.DataV1.Autoconf)
require.Equal(t, uint64(0), config2.Data.DataV1.ClickedLink)
require.Equal(t, false, config2.Data.DataV1.ReportSent)
require.Equal(t, false, config2.Data.DataV1.ReportClick)
require.Equal(t, "", config2.Data.DataV1.FailureDetails)
}
func TestConfigStatus_ApplyFailure(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
require.NoError(t, config.ApplySuccess())
require.NoError(t, config.ApplyFailure("Big Failure"))
require.Equal(t, true, config.IsFromFailure())
require.Equal(t, true, config.IsPending())
config2, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
require.Equal(t, "1.0.0", config2.Data.Metadata.Version)
require.Equal(t, false, config2.Data.DataV1.PendingSince.IsZero())
require.Equal(t, true, config2.Data.DataV1.LastProgress.IsZero())
require.Equal(t, "", config2.Data.DataV1.Autoconf)
require.Equal(t, uint64(0), config2.Data.DataV1.ClickedLink)
require.Equal(t, false, config2.Data.DataV1.ReportSent)
require.Equal(t, false, config2.Data.DataV1.ReportClick)
require.Equal(t, "Big Failure", config2.Data.DataV1.FailureDetails)
}
func TestConfigStatus_ApplyProgress(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
require.Equal(t, true, config.IsPending())
require.Equal(t, true, config.Data.DataV1.LastProgress.IsZero())
require.NoError(t, config.ApplyProgress())
config2, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
require.Equal(t, "1.0.0", config2.Data.Metadata.Version)
require.Equal(t, false, config2.Data.DataV1.PendingSince.IsZero())
require.Equal(t, false, config2.Data.DataV1.LastProgress.IsZero())
require.Equal(t, "", config2.Data.DataV1.Autoconf)
require.Equal(t, uint64(0), config2.Data.DataV1.ClickedLink)
require.Equal(t, false, config2.Data.DataV1.ReportSent)
require.Equal(t, false, config2.Data.DataV1.ReportClick)
require.Equal(t, "", config2.Data.DataV1.FailureDetails)
}
func TestConfigStatus_RecordLinkClicked(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
require.Equal(t, uint64(0), config.Data.DataV1.ClickedLink)
require.NoError(t, config.RecordLinkClicked(0))
require.Equal(t, uint64(1), config.Data.DataV1.ClickedLink)
require.NoError(t, config.RecordLinkClicked(1))
require.Equal(t, uint64(3), config.Data.DataV1.ClickedLink)
config2, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
require.Equal(t, "1.0.0", config2.Data.Metadata.Version)
require.Equal(t, false, config2.Data.DataV1.PendingSince.IsZero())
require.Equal(t, true, config2.Data.DataV1.LastProgress.IsZero())
require.Equal(t, "", config2.Data.DataV1.Autoconf)
require.Equal(t, uint64(3), config2.Data.DataV1.ClickedLink)
require.Equal(t, false, config2.Data.DataV1.ReportSent)
require.Equal(t, false, config2.Data.DataV1.ReportClick)
require.Equal(t, "", config2.Data.DataV1.FailureDetails)
}
func TestConfigStatus_ReportClicked(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
require.Equal(t, false, config.Data.DataV1.ReportClick)
require.NoError(t, config.ReportClicked())
require.Equal(t, true, config.Data.DataV1.ReportClick)
config2, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
require.Equal(t, "1.0.0", config2.Data.Metadata.Version)
require.Equal(t, false, config2.Data.DataV1.PendingSince.IsZero())
require.Equal(t, true, config2.Data.DataV1.LastProgress.IsZero())
require.Equal(t, "", config2.Data.DataV1.Autoconf)
require.Equal(t, uint64(0), config2.Data.DataV1.ClickedLink)
require.Equal(t, false, config2.Data.DataV1.ReportSent)
require.Equal(t, true, config2.Data.DataV1.ReportClick)
require.Equal(t, "", config2.Data.DataV1.FailureDetails)
}
func TestConfigStatus_ReportSent(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
require.Equal(t, false, config.Data.DataV1.ReportSent)
require.NoError(t, config.ReportSent())
require.Equal(t, true, config.Data.DataV1.ReportSent)
config2, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
require.Equal(t, "1.0.0", config2.Data.Metadata.Version)
require.Equal(t, false, config2.Data.DataV1.PendingSince.IsZero())
require.Equal(t, true, config2.Data.DataV1.LastProgress.IsZero())
require.Equal(t, "", config2.Data.DataV1.Autoconf)
require.Equal(t, uint64(0), config2.Data.DataV1.ClickedLink)
require.Equal(t, true, config2.Data.DataV1.ReportSent)
require.Equal(t, false, config2.Data.DataV1.ReportClick)
require.Equal(t, "", config2.Data.DataV1.FailureDetails)
}
func dumpConfigStatusInFile(data *configstatus.ConfigurationStatusData, file string) error {
f, err := os.Create(file)
if err != nil {
return err
}
defer func() { _ = f.Close() }()
return json.NewEncoder(f).Encode(data)
}

View File

@ -1,59 +0,0 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package configstatus
import (
"strconv"
)
type ConfigAbortValues struct {
Duration int `json:"duration"`
}
type ConfigAbortDimensions struct {
ReportClick string `json:"report_click"`
ReportSent string `json:"report_sent"`
ClickedLink string `json:"clicked_link"`
}
type ConfigAbortData struct {
MeasurementGroup string
Event string
Values ConfigSuccessValues
Dimensions ConfigSuccessDimensions
}
type ConfigAbortBuilder struct{}
func (*ConfigAbortBuilder) New(config *ConfigurationStatus) ConfigAbortData {
config.DataLock.RLock()
defer config.DataLock.RUnlock()
return ConfigAbortData{
MeasurementGroup: "bridge.any.configuration",
Event: "bridge_config_abort",
Values: ConfigSuccessValues{
Duration: config.isPendingSinceMin(),
},
Dimensions: ConfigSuccessDimensions{
ReportClick: strconv.FormatBool(config.Data.DataV1.ReportClick),
ReportSent: strconv.FormatBool(config.Data.DataV1.ReportSent),
ClickedLink: config.Data.clickedLinkToString(),
},
}
}

View File

@ -1,75 +0,0 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package configstatus_test
import (
"path/filepath"
"testing"
"time"
"github.com/ProtonMail/proton-bridge/v3/internal/configstatus"
"github.com/stretchr/testify/require"
)
func TestConfigurationAbort_default(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
var builder = configstatus.ConfigAbortBuilder{}
req := builder.New(config)
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_abort", req.Event)
require.Equal(t, 0, req.Values.Duration)
require.Equal(t, "false", req.Dimensions.ReportClick)
require.Equal(t, "false", req.Dimensions.ReportSent)
require.Equal(t, "", req.Dimensions.ClickedLink)
}
func TestConfigurationAbort_fed(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
var data = configstatus.ConfigurationStatusData{
Metadata: configstatus.Metadata{Version: "1.0.0"},
DataV1: configstatus.DataV1{
PendingSince: time.Now().Add(-10 * time.Minute),
LastProgress: time.Time{},
Autoconf: "Mr TBird",
ClickedLink: 42,
ReportSent: false,
ReportClick: true,
FailureDetails: "Not an error",
},
}
require.NoError(t, dumpConfigStatusInFile(&data, file))
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
var builder = configstatus.ConfigAbortBuilder{}
req := builder.New(config)
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_abort", req.Event)
require.Equal(t, 10, req.Values.Duration)
require.Equal(t, "true", req.Dimensions.ReportClick)
require.Equal(t, "false", req.Dimensions.ReportSent)
require.Equal(t, "[1,3,5]", req.Dimensions.ClickedLink)
}

View File

@ -1,63 +0,0 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package configstatus
import "time"
type ConfigProgressValues struct {
NbDay int `json:"nb_day"`
NbDaySinceLast int `json:"nb_day_since_last"`
}
type ConfigProgressData struct {
MeasurementGroup string
Event string
Values ConfigProgressValues
Dimensions struct{}
}
type ConfigProgressBuilder struct{}
func (*ConfigProgressBuilder) New(config *ConfigurationStatus) ConfigProgressData {
config.DataLock.RLock()
defer config.DataLock.RUnlock()
return ConfigProgressData{
MeasurementGroup: "bridge.any.configuration",
Event: "bridge_config_progress",
Values: ConfigProgressValues{
NbDay: numberOfDay(time.Now(), config.Data.DataV1.PendingSince),
NbDaySinceLast: numberOfDay(time.Now(), config.Data.DataV1.LastProgress),
},
}
}
func numberOfDay(now, prev time.Time) int {
if now.IsZero() || prev.IsZero() {
return 1
}
if now.Year() > prev.Year() {
if now.YearDay() > prev.YearDay() {
return 365 + (now.YearDay() - prev.YearDay())
}
return (prev.YearDay() + now.YearDay()) - 365
} else if now.YearDay() > prev.YearDay() {
return now.YearDay() - prev.YearDay()
}
return 0
}

View File

@ -1,71 +0,0 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package configstatus_test
import (
"path/filepath"
"testing"
"time"
"github.com/ProtonMail/proton-bridge/v3/internal/configstatus"
"github.com/stretchr/testify/require"
)
func TestConfigurationProgress_default(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
var builder = configstatus.ConfigProgressBuilder{}
req := builder.New(config)
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_progress", req.Event)
require.Equal(t, 0, req.Values.NbDay)
require.Equal(t, 1, req.Values.NbDaySinceLast)
}
func TestConfigurationProgress_fed(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
var data = configstatus.ConfigurationStatusData{
Metadata: configstatus.Metadata{Version: "1.0.0"},
DataV1: configstatus.DataV1{
PendingSince: time.Now().AddDate(0, 0, -5),
LastProgress: time.Now().AddDate(0, 0, -2),
Autoconf: "Mr TBird",
ClickedLink: 42,
ReportSent: false,
ReportClick: true,
FailureDetails: "Not an error",
},
}
require.NoError(t, dumpConfigStatusInFile(&data, file))
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
var builder = configstatus.ConfigProgressBuilder{}
req := builder.New(config)
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_progress", req.Event)
require.Equal(t, 5, req.Values.NbDay)
require.Equal(t, 2, req.Values.NbDaySinceLast)
}

View File

@ -1,63 +0,0 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package configstatus
import (
"strconv"
)
type ConfigRecoveryValues struct {
Duration int `json:"duration"`
}
type ConfigRecoveryDimensions struct {
Autoconf string `json:"autoconf"`
ReportClick string `json:"report_click"`
ReportSent string `json:"report_sent"`
ClickedLink string `json:"clicked_link"`
FailureDetails string `json:"failure_details"`
}
type ConfigRecoveryData struct {
MeasurementGroup string
Event string
Values ConfigRecoveryValues
Dimensions ConfigRecoveryDimensions
}
type ConfigRecoveryBuilder struct{}
func (*ConfigRecoveryBuilder) New(config *ConfigurationStatus) ConfigRecoveryData {
config.DataLock.RLock()
defer config.DataLock.RUnlock()
return ConfigRecoveryData{
MeasurementGroup: "bridge.any.configuration",
Event: "bridge_config_recovery",
Values: ConfigRecoveryValues{
Duration: config.isPendingSinceMin(),
},
Dimensions: ConfigRecoveryDimensions{
Autoconf: config.Data.DataV1.Autoconf,
ReportClick: strconv.FormatBool(config.Data.DataV1.ReportClick),
ReportSent: strconv.FormatBool(config.Data.DataV1.ReportSent),
ClickedLink: config.Data.clickedLinkToString(),
FailureDetails: config.Data.DataV1.FailureDetails,
},
}
}

View File

@ -1,79 +0,0 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package configstatus_test
import (
"path/filepath"
"testing"
"time"
"github.com/ProtonMail/proton-bridge/v3/internal/configstatus"
"github.com/stretchr/testify/require"
)
func TestConfigurationRecovery_default(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
var builder = configstatus.ConfigRecoveryBuilder{}
req := builder.New(config)
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_recovery", req.Event)
require.Equal(t, 0, req.Values.Duration)
require.Equal(t, "", req.Dimensions.Autoconf)
require.Equal(t, "false", req.Dimensions.ReportClick)
require.Equal(t, "false", req.Dimensions.ReportSent)
require.Equal(t, "", req.Dimensions.ClickedLink)
require.Equal(t, "", req.Dimensions.FailureDetails)
}
func TestConfigurationRecovery_fed(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
var data = configstatus.ConfigurationStatusData{
Metadata: configstatus.Metadata{Version: "1.0.0"},
DataV1: configstatus.DataV1{
PendingSince: time.Now().Add(-10 * time.Minute),
LastProgress: time.Time{},
Autoconf: "Mr TBird",
ClickedLink: 42,
ReportSent: false,
ReportClick: true,
FailureDetails: "Not an error",
},
}
require.NoError(t, dumpConfigStatusInFile(&data, file))
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
var builder = configstatus.ConfigRecoveryBuilder{}
req := builder.New(config)
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_recovery", req.Event)
require.Equal(t, 10, req.Values.Duration)
require.Equal(t, "Mr TBird", req.Dimensions.Autoconf)
require.Equal(t, "true", req.Dimensions.ReportClick)
require.Equal(t, "false", req.Dimensions.ReportSent)
require.Equal(t, "[1,3,5]", req.Dimensions.ClickedLink)
require.Equal(t, "Not an error", req.Dimensions.FailureDetails)
}

View File

@ -1,61 +0,0 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package configstatus
import (
"strconv"
)
type ConfigSuccessValues struct {
Duration int `json:"duration"`
}
type ConfigSuccessDimensions struct {
Autoconf string `json:"autoconf"`
ReportClick string `json:"report_click"`
ReportSent string `json:"report_sent"`
ClickedLink string `json:"clicked_link"`
}
type ConfigSuccessData struct {
MeasurementGroup string
Event string
Values ConfigSuccessValues
Dimensions ConfigSuccessDimensions
}
type ConfigSuccessBuilder struct{}
func (*ConfigSuccessBuilder) New(config *ConfigurationStatus) ConfigSuccessData {
config.DataLock.RLock()
defer config.DataLock.RUnlock()
return ConfigSuccessData{
MeasurementGroup: "bridge.any.configuration",
Event: "bridge_config_success",
Values: ConfigSuccessValues{
Duration: config.isPendingSinceMin(),
},
Dimensions: ConfigSuccessDimensions{
Autoconf: config.Data.DataV1.Autoconf,
ReportClick: strconv.FormatBool(config.Data.DataV1.ReportClick),
ReportSent: strconv.FormatBool(config.Data.DataV1.ReportSent),
ClickedLink: config.Data.clickedLinkToString(),
},
}
}

View File

@ -1,77 +0,0 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package configstatus_test
import (
"path/filepath"
"testing"
"time"
"github.com/ProtonMail/proton-bridge/v3/internal/configstatus"
"github.com/stretchr/testify/require"
)
func TestConfigurationSuccess_default(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
var builder = configstatus.ConfigSuccessBuilder{}
req := builder.New(config)
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_success", req.Event)
require.Equal(t, 0, req.Values.Duration)
require.Equal(t, "", req.Dimensions.Autoconf)
require.Equal(t, "false", req.Dimensions.ReportClick)
require.Equal(t, "false", req.Dimensions.ReportSent)
require.Equal(t, "", req.Dimensions.ClickedLink)
}
func TestConfigurationSuccess_fed(t *testing.T) {
dir := t.TempDir()
file := filepath.Join(dir, "dummy.json")
var data = configstatus.ConfigurationStatusData{
Metadata: configstatus.Metadata{Version: "1.0.0"},
DataV1: configstatus.DataV1{
PendingSince: time.Now().Add(-10 * time.Minute),
LastProgress: time.Time{},
Autoconf: "Mr TBird",
ClickedLink: 42,
ReportSent: false,
ReportClick: true,
FailureDetails: "Not an error",
},
}
require.NoError(t, dumpConfigStatusInFile(&data, file))
config, err := configstatus.LoadConfigurationStatus(file)
require.NoError(t, err)
var builder = configstatus.ConfigSuccessBuilder{}
req := builder.New(config)
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
require.Equal(t, "bridge_config_success", req.Event)
require.Equal(t, 10, req.Values.Duration)
require.Equal(t, "Mr TBird", req.Dimensions.Autoconf)
require.Equal(t, "true", req.Dimensions.ReportClick)
require.Equal(t, "false", req.Dimensions.ReportSent)
require.Equal(t, "[1,3,5]", req.Dimensions.ClickedLink)
}

View File

@ -1,56 +0,0 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package configstatus
import (
"time"
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
)
const ProgressCheckInterval = time.Hour
type Metadata struct {
Version string `json:"version"`
}
type MetadataOnly struct {
Metadata Metadata `json:"metadata"`
}
type DataV1 struct {
PendingSince time.Time `json:"pending_since"`
LastProgress time.Time `json:"last_progress"`
Autoconf string `json:"auto_conf"`
ClickedLink uint64 `json:"clicked_link"`
ReportSent bool `json:"report_sent"`
ReportClick bool `json:"report_click"`
FailureDetails string `json:"failure_details"`
}
type ConfigurationStatusData struct {
Metadata Metadata `json:"metadata"`
DataV1 DataV1 `json:"dataV1"`
}
type ConfigurationStatus struct {
FilePath string
DataLock safe.RWMutex
Data *ConfigurationStatusData
}

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge.Bridge. // This file is part of Proton Mail Bridge.Bridge.
// //

View File

@ -1,4 +1,4 @@
// Copyright (c) 2023 Proton AG // Copyright (c) 2025 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //

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