Compare commits

...

152 Commits

Author SHA1 Message Date
9230596037 chore: merge Laviolette to master 2026-01-28 16:14:32 +00:00
87bba395d0 chore: Laviolette Bridge 3.22.0 changelog. 2026-01-22 16:47:19 +01:00
4ab5bcdf65 fix(BRIDGE-449): IMAP OK heartbeat; gluon bump 2026-01-21 15:54:03 +00:00
6194a1a125 chore: bump badssl pin 2026-01-21 12:43:51 +01:00
c6ec3cb53f fix(BRIDGE-447): bump GPA 2026-01-09 12:27:25 +01:00
16e1177ec2 chore(BRIDGE-455): bump go toolchain to 1.24.11; logrus package and cloudflare/circl bump; bump GPA and Gluon; remove ignored vulns 2026-01-08 13:00:18 +01:00
194942e895 chore: Happy New Year (2026) 2026-01-08 13:00:18 +01:00
fee4570a3a ci: DEVEX-54 migrating win runners. 2025-12-15 12:48:48 +01:00
63ea76d757 chore(BRIDGE-440): supress govulnechk findings x2 2025-12-09 12:29:09 +01:00
3db547d187 feat(BRIDGE-409): update gluon and go-proton-api to the latest version. 2025-11-27 12:04:21 +01:00
4378adab1a chore: bump badssl pin 2025-11-26 15:50:57 +01:00
726eb72026 chore: Bump VCPKG on Windows; resolve build issues 2025-11-10 12:35:12 +01:00
5a28fbf2df feat(BRIDGE-409): update gluon to fix max message size issue. 2025-11-05 11:09:19 +01:00
131a663578 chore(BRIDGE-440): supress govulnechk findings 2025-10-30 14:48:40 +01:00
6ab8558e17 chore: Attempt at stabilizing Bridge launch for e2e tests 2025-10-24 14:57:25 +02:00
ac3d7375f7 fix(BRIDGE-424): patch second password prompt 2025-10-01 11:42:48 +02:00
cd375c81fa chore(BRIDGE-429): migrate GitHub forks to ProtonMail organization 2025-09-30 12:06:44 +02:00
7fce966a65 fix(BRIDGE-424): resolved wrong layout ordering; u2f pin option is now treated as a 'password' 2025-09-29 11:58:09 +02:00
edf903fd21 feat(BRIDGE-424): FIDO2 GUI support. 2025-09-25 17:09:00 +02:00
e091e58be1 feat(BRIDGE-424): implement FIDO2 support 2025-09-24 14:42:15 +00:00
2fb5b751b6 chore(BRIDGE-428): suppress vulnerability finding - GO-2025-3956 2025-09-24 16:09:05 +02:00
e442b67ac0 chore: bump badssl pin 2025-09-24 15:57:35 +02:00
8baa7e3b6e chore: Got rid of something that is not needed, but was failing the pipeline. 2025-08-15 13:31:29 +00:00
5bb71575c3 chore: Changes made so that the e2e tests can be run on a scheduled nightly pipeline. 2025-08-15 10:24:03 +02:00
fd709b0d08 test(BRIDGE-136): Download Bridge 2025-08-15 07:25:10 +00:00
cf9b35163a chore: let's see 2025-08-13 12:44:21 +00:00
f3b3fd960b chore: BRIDGE-416 silence database/sql vuln 2025-08-12 16:30:50 +02:00
02684e27fb chore: Kanmon Bridge 3.21.2 changelog. 2025-07-21 16:46:06 +02:00
f6d4fc0c29 chore: Kanmon Bridge 3.21.1 changelog. 2025-07-21 16:46:00 +02:00
054a51ba15 chore: Kanmon Bridge 3.21.0 changelog. 2025-07-21 16:45:53 +02:00
7b2ea635ee chore(BRIDGE-409): Bump GPA 2025-07-18 15:39:36 +02:00
7faf32d0ff feat(BRIDGE-396): Observability metrics for vault issues; Extension to observability service to support caching 2025-07-17 14:06:23 +02:00
c32c431640 chore: merge Kanmon to master 2025-07-10 13:36:03 +00:00
de3fd34998 feat(BRIDGE-356): Added retry logic for unavailable preferred keychain on Linux; Feature flag support before bridge initialization; Refactored some bits of the code; 2025-07-10 13:23:26 +00:00
20183bf984 fix(BRIDGE-374): Tweaked MBOX header sanitization logic, ensures that RFC822 headers are present. 2025-07-10 12:08:47 +02:00
47316a1843 ci: DEVOPS-3481 adding back cache (reduces build time by 1h) 2025-07-08 10:31:55 +02:00
4cc2ded001 chore: Kanmon Bridge 3.21.2 changelog. 2025-07-07 11:34:42 +02:00
366f7eb4e0 feat(BRIDGE-358): hover tooltip on copy to clipboard UI button 2025-07-07 08:49:02 +00: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
5ea45790fe fix(BRIDGE-406): fixed faulty certificate chain validation logic; made certificate pin checks exclusive to leaf certs; 2025-07-04 15:19:05 +02:00
7e8d16e883 feat(BRIDGE-151): Additional sentry report for auto-update error case 2025-07-02 09:35:21 +00:00
e9b1befae4 chore: refactor; unify vault mutex usage with wrappers; 2025-07-02 08:59:44 +00:00
26cc6169fa feat(BRIDGE-361): log the utilized keychain helper 2025-07-01 10:56:50 +02:00
be9e03d917 feat(BRIDGE-278): Rollout Feature Flag stickiness support; a new UUID 'sticky' value has been added to the vault" 2025-06-27 16:21:40 +02:00
2669bb4df9 fix(BRIDGE-395): skip saving the last used keychain helper as user preference on Windows & macOS 2025-06-27 13:10:41 +00:00
bffad05933 fix(BRIDGE-394): bump Gluon 2025-06-27 12:29:47 +02:00
7269e5ece0 fix(BRIDGE-355): bump GPA 2025-06-27 12:27:50 +02:00
6ee9e6e38b chore(BRIDGE-369): bump GPA & gopenpg to 2.9.0 2025-06-24 12:41:02 +02:00
2f2a8a200b chore(BRIDGE-392): bump go to 1.24.4 2025-06-24 10:29:20 +02:00
5832d48a5b chore: remove windows env vcpkg cache 2025-06-23 15:07:42 +00:00
6b1d203225 feat(BRIDGE-391): Simplified internal label conflict resolver 2025-06-17 13:09:39 +02:00
dbef40cfc5 chore: merge Kanmon to master 2025-06-12 16:54:19 +00:00
b699a53b12 chore(BRIDGE-388): disable particular vuln reporting 2025-06-12 12:17:09 +02:00
4ee149ecd7 fix(BRIDGE-387): use gluon ID for getting the message count instead of the address ID 2025-06-12 11:47:33 +02: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
7b533d5951 feat(BRIDGE-383): Internal mailbox conflict resolution extended; Minor alterations to mailbox conflict pre-checker 2025-06-11 14:09:53 +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
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
731 changed files with 14471 additions and 7187 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 -->

1
.gitlab/CODEOWNERS Normal file
View File

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

3
.gitmodules vendored
View File

@ -1,3 +1,6 @@
[submodule "submodules/vcpkg"] [submodule "submodules/vcpkg"]
path = extern/vcpkg path = extern/vcpkg
url = https://github.com/Microsoft/vcpkg.git url = https://github.com/Microsoft/vcpkg.git
[submodule "extern/vcpkg-windows"]
path = extern/vcpkg-windows
url = https://github.com/microsoft/vcpkg.git

View File

@ -87,7 +87,6 @@ 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. - 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]

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.21.9 * 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

@ -42,7 +42,10 @@ Proton Mail Bridge includes the following 3rd party software:
* [go-smtp](https://github.com/emersion/go-smtp) available under [license](https://github.com/emersion/go-smtp/blob/master/LICENSE) * [go-smtp](https://github.com/emersion/go-smtp) available under [license](https://github.com/emersion/go-smtp/blob/master/LICENSE)
* [go-vcard](https://github.com/emersion/go-vcard) available under [license](https://github.com/emersion/go-vcard/blob/master/LICENSE) * [go-vcard](https://github.com/emersion/go-vcard) available under [license](https://github.com/emersion/go-vcard/blob/master/LICENSE)
* [color](https://github.com/fatih/color) available under [license](https://github.com/fatih/color/blob/master/LICENSE) * [color](https://github.com/fatih/color) available under [license](https://github.com/fatih/color/blob/master/LICENSE)
* [cbor](https://github.com/fxamacker/cbor/v2) available under [license](https://github.com/fxamacker/cbor/v2/blob/master/LICENSE)
* [sentry-go](https://github.com/getsentry/sentry-go) available under [license](https://github.com/getsentry/sentry-go/blob/master/LICENSE) * [sentry-go](https://github.com/getsentry/sentry-go) available under [license](https://github.com/getsentry/sentry-go/blob/master/LICENSE)
* [ctaphid](https://github.com/go-ctap/ctaphid) available under [license](https://github.com/go-ctap/ctaphid/blob/master/LICENSE)
* [winhello](https://github.com/go-ctap/winhello) available under [license](https://github.com/go-ctap/winhello/blob/master/LICENSE)
* [resty](https://github.com/go-resty/resty/v2) available under [license](https://github.com/go-resty/resty/v2/blob/master/LICENSE) * [resty](https://github.com/go-resty/resty/v2) available under [license](https://github.com/go-resty/resty/v2/blob/master/LICENSE)
* [dbus](https://github.com/godbus/dbus) available under [license](https://github.com/godbus/dbus/blob/master/LICENSE) * [dbus](https://github.com/godbus/dbus) available under [license](https://github.com/godbus/dbus/blob/master/LICENSE)
* [mock](https://github.com/golang/mock) available under [license](https://github.com/golang/mock/blob/master/LICENSE) * [mock](https://github.com/golang/mock) available under [license](https://github.com/golang/mock/blob/master/LICENSE)
@ -52,6 +55,7 @@ Proton Mail Bridge includes the following 3rd party software:
* [html2text](https://github.com/jaytaylor/html2text) available under [license](https://github.com/jaytaylor/html2text/blob/master/LICENSE) * [html2text](https://github.com/jaytaylor/html2text) available under [license](https://github.com/jaytaylor/html2text/blob/master/LICENSE)
* [go-locale](https://github.com/jeandeaual/go-locale) available under [license](https://github.com/jeandeaual/go-locale/blob/master/LICENSE) * [go-locale](https://github.com/jeandeaual/go-locale) available under [license](https://github.com/jeandeaual/go-locale/blob/master/LICENSE)
* [go-keychain](https://github.com/keybase/go-keychain) available under [license](https://github.com/keybase/go-keychain/blob/master/LICENSE) * [go-keychain](https://github.com/keybase/go-keychain) available under [license](https://github.com/keybase/go-keychain/blob/master/LICENSE)
* [go-libfido2](https://github.com/keys-pub/go-libfido2) available under [license](https://github.com/keys-pub/go-libfido2/blob/master/LICENSE)
* [dns](https://github.com/miekg/dns) available under [license](https://github.com/miekg/dns/blob/master/LICENSE) * [dns](https://github.com/miekg/dns) available under [license](https://github.com/miekg/dns/blob/master/LICENSE)
* [memory](https://github.com/pbnjay/memory) available under [license](https://github.com/pbnjay/memory/blob/master/LICENSE) * [memory](https://github.com/pbnjay/memory) available under [license](https://github.com/pbnjay/memory/blob/master/LICENSE)
* [errors](https://github.com/pkg/errors) available under [license](https://github.com/pkg/errors/blob/master/LICENSE) * [errors](https://github.com/pkg/errors) available under [license](https://github.com/pkg/errors/blob/master/LICENSE)
@ -70,7 +74,6 @@ Proton Mail Bridge includes the following 3rd party software:
* [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) * [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)
@ -111,6 +114,7 @@ Proton Mail Bridge includes the following 3rd party software:
* [multierror](https://github.com/joeshaw/multierror) available under [license](https://github.com/joeshaw/multierror/blob/master/LICENSE) * [multierror](https://github.com/joeshaw/multierror) available under [license](https://github.com/joeshaw/multierror/blob/master/LICENSE)
* [go](https://github.com/json-iterator/go) available under [license](https://github.com/json-iterator/go/blob/master/LICENSE) * [go](https://github.com/json-iterator/go) available under [license](https://github.com/json-iterator/go/blob/master/LICENSE)
* [cpuid](https://github.com/klauspost/cpuid/v2) available under [license](https://github.com/klauspost/cpuid/v2/blob/master/LICENSE) * [cpuid](https://github.com/klauspost/cpuid/v2) available under [license](https://github.com/klauspost/cpuid/v2/blob/master/LICENSE)
* [cose](https://github.com/ldclabs/cose) available under [license](https://github.com/ldclabs/cose/blob/master/LICENSE)
* [go-urn](https://github.com/leodido/go-urn) available under [license](https://github.com/leodido/go-urn/blob/master/LICENSE) * [go-urn](https://github.com/leodido/go-urn) available under [license](https://github.com/leodido/go-urn/blob/master/LICENSE)
* [go-colorable](https://github.com/mattn/go-colorable) available under [license](https://github.com/mattn/go-colorable/blob/master/LICENSE) * [go-colorable](https://github.com/mattn/go-colorable) available under [license](https://github.com/mattn/go-colorable/blob/master/LICENSE)
* [go-isatty](https://github.com/mattn/go-isatty) available under [license](https://github.com/mattn/go-isatty/blob/master/LICENSE) * [go-isatty](https://github.com/mattn/go-isatty) available under [license](https://github.com/mattn/go-isatty/blob/master/LICENSE)
@ -127,9 +131,11 @@ 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)
* [float16](https://github.com/x448/float16) available under [license](https://github.com/x448/float16/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) available under [license](https://gitlab.com/c0b/go-ordered-json/blob/master/LICENSE) * [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) * [go.opencensus.io](https://pkg.go.dev/go.opencensus.io?tab=licenses) available under [license](https://pkg.go.dev/go.opencensus.io?tab=licenses)
@ -141,8 +147,11 @@ Proton Mail Bridge includes the following 3rd party software:
* [appengine](https://google.golang.org/appengine) available under [license](https://pkg.go.dev/google.golang.org/appengine?tab=licenses) * [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) 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)
* [go-autostart](https://github.com/ProtonMail/go-autostart) available under [license](https://github.com/ProtonMail/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) * [winhello](https://github.com/ProtonMail/winhello) available under [license](https://github.com/ProtonMail/winhello/blob/master/LICENSE)
* [go-keychain](https://github.com/cuthix/go-keychain) available under [license](https://github.com/cuthix/go-keychain/blob/master/LICENSE) * [resty](https://github.com/ProtonMail/resty/v2) available under [license](https://github.com/ProtonMail/resty/v2/blob/master/LICENSE)
* [go-keychain](https://github.com/ProtonMail/go-keychain) available under [license](https://github.com/ProtonMail/go-keychain/blob/master/LICENSE)
* [go-libfido2](https://github.com/ProtonMail/go-libfido2) available under [license](https://github.com/ProtonMail/go-libfido2/blob/master/LICENSE)
<!-- END AUTOGEN --> <!-- END AUTOGEN -->

View File

@ -3,6 +3,147 @@
Changelog [format](http://keepachangelog.com/en/1.0.0/) Changelog [format](http://keepachangelog.com/en/1.0.0/)
## Laviolette Bridge 3.22.0
### Added
* BRIDGE-358: Hover tooltip for IMAP/SMTP settings clipboard UI actions.
* BRIDGE-356: Added support for unavailable keychain retries on Linux, such that we don't wipe the vault. Feature flag support before Bridge initialization.
* BRIDGE-278: Rollout feature flag support.
* BRIDGE-151: Additional sentry reporting related to auto-update failures.
* BRIDGE-361: Debug information on the utilized keychain helper.
* BRIDGE-396: Observability caching and metrics for vault related issues.
* BRIDGE-449: Generic IMAP OK heartbeat for long lasting commands.
### Changed
* BRIDGE-374: Modified MBOX header sanitization logic, it now ensures that RFC822 headers are present before stripping content.
* BRIDGE-391: Simplified internal label conflict resolver.
* BRIDGE-409: Increased the import size limit to 55MB.
* BRIDGE-369: Bumped gopenpgp to v2.9.0.
* BRIDGE-455: Bumped Go to 1.24.11.
* BRIDGE-424: FIDO2 support.
### Fixed
* BRIDGE-395: Don't store the last utilized keychain as the user preference on Windows & macOS.
* BRIDGE-387: Use the address gluon ID, instead of the address ID to fetch message counts.
* BRIDGE-394: Prevent the RFC822 parser from mutating the message literal.
* BRIDGE-355: Prevent Bridge from crashing when an unknown message charset is detected on Import.
* BRIDGE-447: Adjusted error message for message import size limitation.
## 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 ## Erasmus Bridge 3.15.0
### Added ### Added

View File

@ -9,10 +9,10 @@ TARGET_OS?=${GOOS}
ROOT_DIR:=$(realpath .) 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 install-libfido2
# 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.15.0+git BRIDGE_APP_VERSION?=3.22.0+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
@ -32,6 +32,24 @@ BUILD_FLAGS_LAUNCHER:=${BUILD_FLAGS}
GO_LDFLAGS:=$(addprefix -X github.com/ProtonMail/proton-bridge/v3/internal/constants., Version=${APP_VERSION} Revision=${REVISION} Tag=${TAG} BuildTime=${BUILD_TIME}) GO_LDFLAGS:=$(addprefix -X github.com/ProtonMail/proton-bridge/v3/internal/constants., Version=${APP_VERSION} Revision=${REVISION} Tag=${TAG} BuildTime=${BUILD_TIME})
GO_LDFLAGS+=-X "github.com/ProtonMail/proton-bridge/v3/internal/constants.FullAppName=${APP_FULL_NAME}" GO_LDFLAGS+=-X "github.com/ProtonMail/proton-bridge/v3/internal/constants.FullAppName=${APP_FULL_NAME}"
## Libfido2 set-up.
# We use vcpkg for libfido2 on *nix systems.
VCPKG_ROOT_NIX := $(ROOT_DIR)/extern/vcpkg
ifeq "${TARGET_OS}" "darwin"
VCPKG_INSTALLED_ARM := $(VCPKG_ROOT_NIX)/installed/arm64-osx
VCPKG_INSTALLED_X64 := $(VCPKG_ROOT_NIX)/installed/x64-osx
LIBFIDO2_CFLAGS_ARM64 := -I$(VCPKG_INSTALLED_ARM)/include
LIBFIDO2_LDFLAGS_ARM64 := -L$(VCPKG_INSTALLED_ARM)/lib -lfido2 -lcbor -lssl -lcrypto
LIBFIDO2_CFLAGS_X64 := -I$(VCPKG_INSTALLED_X64)/include
LIBFIDO2_LDFLAGS_X64 := -L$(VCPKG_INSTALLED_X64)/lib -lfido2 -lcbor -lssl -lcrypto
endif
ifeq "${TARGET_OS}" "linux"
LIBFIDO2_LDFLAGS := -lfido2 -lcbor -lssl -lcrypto
endif
ifneq "${DSN_SENTRY}" "" ifneq "${DSN_SENTRY}" ""
GO_LDFLAGS+=-X github.com/ProtonMail/proton-bridge/v3/internal/constants.DSNSentry=${DSN_SENTRY} GO_LDFLAGS+=-X github.com/ProtonMail/proton-bridge/v3/internal/constants.DSNSentry=${DSN_SENTRY}
endif endif
@ -95,8 +113,14 @@ go-build=go build $(1) -o $(2) $(3)
go-build-finalize=${go-build} go-build-finalize=${go-build}
ifeq "${GOOS}-$(shell uname -m)" "darwin-arm64" ifeq "${GOOS}-$(shell uname -m)" "darwin-arm64"
go-build-finalize= \ go-build-finalize= \
MACOSX_DEPLOYMENT_TARGET=${MACOS_MIN_VERSION_ARM64} CGO_ENABLED=1 CGO_CFLAGS="-mmacosx-version-min=${MACOS_MIN_VERSION_ARM64}" GOARCH=arm64 $(call go-build,$(1),$(2)_arm,$(3)) && \ MACOSX_DEPLOYMENT_TARGET=${MACOS_MIN_VERSION_ARM64} CGO_ENABLED=1 \
MACOSX_DEPLOYMENT_TARGET=${MACOS_MIN_VERSION_AMD64} CGO_ENABLED=1 CGO_CFLAGS="-mmacosx-version-min=${MACOS_MIN_VERSION_AMD64}" GOARCH=amd64 $(call go-build,$(1),$(2)_amd,$(3)) && \ CGO_CFLAGS="-mmacosx-version-min=${MACOS_MIN_VERSION_ARM64} ${LIBFIDO2_CFLAGS_ARM64}" \
CGO_LDFLAGS="${LIBFIDO2_LDFLAGS_ARM64}" \
GOARCH=arm64 $(call go-build,$(1),$(2)_arm,$(3)) && \
MACOSX_DEPLOYMENT_TARGET=${MACOS_MIN_VERSION_AMD64} CGO_ENABLED=1 \
CGO_CFLAGS="-mmacosx-version-min=${MACOS_MIN_VERSION_AMD64} ${LIBFIDO2_CFLAGS_X64}" \
CGO_LDFLAGS="${LIBFIDO2_LDFLAGS_X64}" \
GOARCH=amd64 $(call go-build,$(1),$(2)_amd,$(3)) && \
lipo -create -output $(2) $(2)_arm $(2)_amd && rm -f $(2)_arm $(2)_amd lipo -create -output $(2) $(2)_arm $(2)_amd && rm -f $(2)_arm $(2)_amd
endif endif
@ -107,6 +131,14 @@ ifeq "${GOOS}" "windows"
$(if $(4), && rm -f ${4},) $(if $(4), && rm -f ${4},)
endif endif
ifneq "${GOOS}" "darwin"
ifneq "${GOOS}" "windows"
go-build-finalize= \
CGO_LDFLAGS="${LIBFIDO2_LDFLAGS}" \
$(call go-build,$(1),$(2),$(3))
endif
endif
${EXE_NAME}: gofiles ${RESOURCE_FILE} ${EXE_NAME}: gofiles ${RESOURCE_FILE}
$(call go-build-finalize,${BUILD_FLAGS},"${LAUNCHER_EXE}","./cmd/${TARGET_CMD}/","${ROOT_DIR}/cmd/${TARGET_CMD}/${RESOURCE_FILE}") $(call go-build-finalize,${BUILD_FLAGS},"${LAUNCHER_EXE}","./cmd/${TARGET_CMD}/","${ROOT_DIR}/cmd/${TARGET_CMD}/${RESOURCE_FILE}")
mv ${LAUNCHER_EXE} ${BRIDGE_EXE} mv ${LAUNCHER_EXE} ${BRIDGE_EXE}
@ -137,7 +169,7 @@ ${DEPLOY_DIR}/linux: ${EXE_TARGET} build-launcher
cp -pf ./dist/${EXE_NAME}.desktop ${DEPLOY_DIR}/linux/ cp -pf ./dist/${EXE_NAME}.desktop ${DEPLOY_DIR}/linux/
mv ${LAUNCHER_EXE} ${DEPLOY_DIR}/linux/ mv ${LAUNCHER_EXE} ${DEPLOY_DIR}/linux/
${DEPLOY_DIR}/darwin: ${EXE_TARGET} build-launcher ${DEPLOY_DIR}/darwin: install-libfido2 ${EXE_TARGET} build-launcher
mv ${EXE_GUI_TARGET} ${EXE_TARGET_DARWIN} mv ${EXE_GUI_TARGET} ${EXE_TARGET_DARWIN}
mv ${EXE_TARGET} ${DARWINAPP_CONTENTS}/MacOS/${BRIDGE_EXE_NAME} mv ${EXE_TARGET} ${DARWINAPP_CONTENTS}/MacOS/${BRIDGE_EXE_NAME}
perl -i -pe"s/>${BRIDGE_GUI_EXE_NAME}/>${LAUNCHER_EXE}/g" ${DARWINAPP_CONTENTS}/Info.plist perl -i -pe"s/>${BRIDGE_GUI_EXE_NAME}/>${LAUNCHER_EXE}/g" ${DARWINAPP_CONTENTS}/Info.plist
@ -189,7 +221,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.61.0" 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
@ -411,6 +443,10 @@ clean-vcpkg:
rm -rf ./.git/submodule/vcpkg rm -rf ./.git/submodule/vcpkg
rm -rf ./extern/vcpkg rm -rf ./extern/vcpkg
git checkout -- extern/vcpkg git checkout -- extern/vcpkg
git submodule deinit -f ./extern/vcpkg-windows
rm -rf ./.git/submodule/vcpkg-windows
rm -rf ./extern/vcpkg-windows
git checkout -- extern/vcpkg-windows
clean: clean-vendor clean-gui clean-vcpkg clean: clean-vendor clean-gui clean-vcpkg
rm -rf vendor-cache rm -rf vendor-cache
@ -423,6 +459,15 @@ clean: clean-vendor clean-gui clean-vcpkg
rm -f ${LAUNCHER_EXE} ${BRIDGE_EXE} ${BRIDGE_EXE_NAME} rm -f ${LAUNCHER_EXE} ${BRIDGE_EXE} ${BRIDGE_EXE_NAME}
install-libfido2:
ifeq "${TARGET_OS}" "darwin"
git submodule update --init --recursive ${VCPKG_ROOT_NIX} || \
{ echo "Failed to init vcpkg submodule"; exit 1; }
${VCPKG_ROOT_NIX}/bootstrap-vcpkg.sh -disableMetrics
cd extern/vcpkg && ./vcpkg install libfido2:arm64-osx libfido2:x64-osx
endif
.PHONY: generate .PHONY: generate
generate: generate:
go generate ./... go generate ./...

View File

@ -1,5 +1,5 @@
# Proton Mail Bridge # Proton Mail Bridge
Copyright (c) 2024 Proton AG Copyright (c) 2026 Proton AG
This repository holds the Proton Mail Bridge application. 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).

View File

@ -1,5 +1,4 @@
--- ---
.script-build: .script-build:
stage: build stage: build
needs: ["lint"] needs: ["lint"]
@ -22,6 +21,7 @@
- bridge_*.tgz - bridge_*.tgz
- vault-editor - vault-editor
- bridge-rollout - bridge-rollout
build-linux: build-linux:
extends: extends:
- .script-build - .script-build
@ -33,6 +33,7 @@ build-linux-qa:
- .rules-branch-manual-MR-and-devel-always - .rules-branch-manual-MR-and-devel-always
variables: variables:
BUILD_TAGS: "build_qa" BUILD_TAGS: "build_qa"
VCPKG_MAX_CONCURRENCY: 1
build-darwin: build-darwin:
extends: extends:

View File

@ -1,6 +1,4 @@
--- ---
.env-windows: .env-windows:
extends: extends:
- .image-windows-virt-build - .image-windows-virt-build
@ -15,10 +13,10 @@
BRIDGE_SYNC_FORCE_MINIMUM_SPEC: 1 BRIDGE_SYNC_FORCE_MINIMUM_SPEC: 1
VCPKG_DEFAULT_BINARY_CACHE: ${CI_PROJECT_DIR}/.cache VCPKG_DEFAULT_BINARY_CACHE: ${CI_PROJECT_DIR}/.cache
cache: cache:
key: windows-vcpkg-go-0 key: windows-vcpkg-go-1
paths: paths:
- .cache - .cache
when: 'always' when: "always"
.env-darwin: .env-darwin:
extends: extends:
@ -36,7 +34,7 @@
key: darwin-go-and-vcpkg key: darwin-go-and-vcpkg
paths: paths:
- .cache - .cache
when: 'always' when: "always"
.env-linux-build: .env-linux-build:
extends: extends:
@ -47,7 +45,7 @@
key: linux-vcpkg key: linux-vcpkg
paths: paths:
- .cache - .cache
when: 'always' when: "always"
before_script: before_script:
- export BRIDGE_SYNC_FORCE_MINIMUM_SPEC=1 - export BRIDGE_SYNC_FORCE_MINIMUM_SPEC=1
- !reference [.before-script-git-config, before_script] - !reference [.before-script-git-config, before_script]
@ -56,4 +54,3 @@
- export GOPATH="$CI_PROJECT_DIR/.cache" - export GOPATH="$CI_PROJECT_DIR/.cache"
tags: tags:
- shared-large - shared-large

View File

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

View File

@ -1,6 +1,4 @@
--- ---
lint: lint:
stage: test stage: test
extends: extends:
@ -33,8 +31,6 @@ lint-bug-report-preview:
paths: paths:
- coverage/** - coverage/**
test-linux: test-linux:
extends: extends:
- .image-linux-test - .image-linux-test
@ -93,7 +89,6 @@ test-integration-race:
paths: paths:
- integration-race-job.log - integration-race-job.log
test-integration-nightly: test-integration-nightly:
extends: extends:
- test-integration - test-integration
@ -131,12 +126,43 @@ test-coverage:
paths: paths:
- coverage* - coverage*
- coverage/** - coverage/**
when: 'always' when: "always"
reports: reports:
coverage_report: coverage_report:
coverage_format: cobertura coverage_format: cobertura
path: coverage.xml path: coverage.xml
test-e2e-ui:
stage: test
extends:
- .rules-branch-manual-scheduled-and-test-branch-always
tags:
- inbox-hyperv-windows-v1
image: windows-2022-inbox-gui-1.0.0
variables:
REQUIRES_GRAPHICAL_CONSOLE: true
before_script:
- echo "Downloading dotnet dependencies"
- cd ./tests/e2e/ui_tests/windows_os/
- dotnet restore ./ProtonMailBridge.UI.Tests.csproj
- dotnet list package
script:
- |
pwsh "$Env:CI_PROJECT_DIR/tests/e2e/ui_tests/windows_os/InstallerScripts/Get-BridgeInstaller.ps1"
- |
$Env:no_grpc_proxy="127.0.0.1"
$no_grpc_proxy="127.0.0.1"
dotnet test ./ProtonMailBridge.UI.Tests.csproj -- NUnit.Where="cat != TemporarilyExcluded"
after_script:
- |
cp "C:\Users\gitlab-runner\AppData\Roaming\protonmail\bridge-v3\logs\*" "$Env:CI_PROJECT_DIR\tests\e2e\ui_tests\windows_os\Results\artifacts\Logs\"
- |
pwsh "$Env:CI_PROJECT_DIR\tests\e2e\ui_tests\windows_os\InstallerScripts\Remove-Bridge.ps1"
artifacts:
paths:
- tests/e2e/ui_tests/windows_os/Results/artifacts/*
when: always
go-vuln-check: go-vuln-check:
extends: extends:
- .image-linux-test - .image-linux-test
@ -150,4 +176,3 @@ go-vuln-check:
when: always when: always
paths: paths:
- vulns* - vulns*

View File

@ -1,4 +1,4 @@
// Copyright (c) 2024 Proton AG // Copyright (c) 2026 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) 2024 Proton AG // Copyright (c) 2026 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) 2024 Proton AG // Copyright (c) 2026 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //
@ -31,6 +31,7 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/crash" "github.com/ProtonMail/proton-bridge/v3/internal/crash"
"github.com/ProtonMail/proton-bridge/v3/internal/locations" "github.com/ProtonMail/proton-bridge/v3/internal/locations"
"github.com/ProtonMail/proton-bridge/v3/internal/logging" "github.com/ProtonMail/proton-bridge/v3/internal/logging"
"github.com/ProtonMail/proton-bridge/v3/internal/platform"
"github.com/ProtonMail/proton-bridge/v3/internal/sentry" "github.com/ProtonMail/proton-bridge/v3/internal/sentry"
"github.com/ProtonMail/proton-bridge/v3/internal/updater" "github.com/ProtonMail/proton-bridge/v3/internal/updater"
"github.com/ProtonMail/proton-bridge/v3/internal/useragent" "github.com/ProtonMail/proton-bridge/v3/internal/useragent"
@ -164,7 +165,7 @@ func main() { //nolint:funlen
// On windows, if you use Run(), a terminal stays open; we don't want that. // On windows, if you use Run(), a terminal stays open; we don't want that.
if //goland:noinspection GoBoolExpressions if //goland:noinspection GoBoolExpressions
runtime.GOOS == "windows" { runtime.GOOS == platform.WINDOWS {
err = cmd.Start() err = cmd.Start()
} else { } else {
err = cmd.Run() err = cmd.Run()

View File

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

2
extern/vcpkg vendored

1
extern/vcpkg-windows vendored Submodule

Submodule extern/vcpkg-windows added at 120deac306

61
go.mod
View File

@ -1,16 +1,16 @@
module github.com/ProtonMail/proton-bridge/v3 module github.com/ProtonMail/proton-bridge/v3
go 1.21 go 1.24.4
toolchain go1.21.9 toolchain go1.24.11
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.20241018144126-31e040c2417e github.com/ProtonMail/gluon v0.17.1-0.20260112123503-2046c95ca745
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.20240918100656-b4860af56d47 github.com/ProtonMail/go-proton-api v0.4.1-0.20260109112619-daf7af47921d
github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton github.com/ProtonMail/gopenpgp/v2 v2.9.0-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
@ -26,41 +26,44 @@ require (
github.com/emersion/go-smtp v0.15.1-0.20221021114529-49b17434419d github.com/emersion/go-smtp v0.15.1-0.20221021114529-49b17434419d
github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3 github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3
github.com/fatih/color v1.13.0 github.com/fatih/color v1.13.0
github.com/fxamacker/cbor/v2 v2.9.0
github.com/getsentry/sentry-go v0.15.0 github.com/getsentry/sentry-go v0.15.0
github.com/go-ctap/ctaphid v0.8.1
github.com/go-ctap/winhello v0.1.0
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.7.0
github.com/google/uuid v1.3.0 github.com/google/uuid v1.6.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
github.com/jeandeaual/go-locale v0.0.0-20220711133428-7de61946b173 github.com/jeandeaual/go-locale v0.0.0-20220711133428-7de61946b173
github.com/keybase/go-keychain v0.0.0 github.com/keybase/go-keychain v0.0.0
github.com/keys-pub/go-libfido2 v1.5.4-0.20250104233141-2534349bd685
github.com/miekg/dns v1.1.50 github.com/miekg/dns v1.1.50
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58
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.3
github.com/stretchr/testify v1.8.4 github.com/stretchr/testify v1.11.1
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.24.0 golang.org/x/net v0.42.0
golang.org/x/oauth2 v0.7.0 golang.org/x/oauth2 v0.30.0
golang.org/x/sys v0.19.0 golang.org/x/sys v0.35.0
golang.org/x/text v0.14.0 golang.org/x/text v0.28.0
google.golang.org/api v0.114.0 google.golang.org/api v0.114.0
google.golang.org/grpc v1.56.3 google.golang.org/grpc v1.75.1
google.golang.org/protobuf v1.33.0 google.golang.org/protobuf v1.36.6
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.7.0 // 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.3.0-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
@ -68,7 +71,7 @@ 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.7 // indirect github.com/cloudflare/circl v1.6.1 // 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
@ -87,7 +90,7 @@ require (
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/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
github.com/golang/protobuf v1.5.3 // indirect github.com/golang/protobuf v1.5.4 // 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/enterprise-certificate-proxy v0.2.3 // indirect
github.com/googleapis/gax-go/v2 v2.7.1 // indirect github.com/googleapis/gax-go/v2 v2.7.1 // indirect
@ -98,6 +101,7 @@ require (
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 // indirect github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/ldclabs/cose v1.3.2 // indirect
github.com/leodido/go-urn v1.2.4 // indirect github.com/leodido/go-urn v1.2.4 // indirect
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
@ -114,25 +118,30 @@ require (
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.2 // 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/x448/float16 v0.8.4 // 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 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.22.0 // indirect golang.org/x/crypto v0.41.0 // indirect
golang.org/x/mod v0.8.0 // indirect golang.org/x/mod v0.26.0 // indirect
golang.org/x/sync v0.3.0 // indirect golang.org/x/sync v0.16.0 // indirect
golang.org/x/tools v0.6.0 // indirect golang.org/x/tools v0.35.0 // indirect
google.golang.org/appengine v1.6.7 // 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/ProtonMail/go-autostart => github.com/ProtonMail/go-autostart v0.0.0-20250402094843-326608c16033
github.com/emersion/go-message => github.com/ProtonMail/go-message v0.13.1-0.20240919135104-3bc88e6a9423 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-ctap/winhello => github.com/ProtonMail/winhello v0.0.0-20250918145518-a739b7dc2e56
github.com/keybase/go-keychain => github.com/cuthix/go-keychain v0.0.0-20240103134243-0b6a41580b77 github.com/go-resty/resty/v2 => github.com/ProtonMail/resty/v2 v2.0.0-20250929142426-e3dc6308c80b
github.com/keybase/go-keychain => github.com/ProtonMail/go-keychain v0.0.0-20250929142014-ea8548dff768
github.com/keys-pub/go-libfido2 => github.com/ProtonMail/go-libfido2 v0.0.0-20250916110427-df894d6d07a1
) )

168
go.sum
View File

@ -7,10 +7,8 @@ cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTj
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 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/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU=
cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE= cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo=
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 h1:v+yFJOfKC3yZdY6ZUI933pIYdhyhV8S3NpWrXWmg7jM=
@ -24,47 +22,49 @@ github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557/go.mod h1:sTrmvD/
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/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/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.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
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/gluon v0.17.1-0.20240923151549-d23b4bec3602 h1:EoMjWlC32tg46L/07hWoiZfLkqJyxVMcsq4Cyn+Ofqc= github.com/ProtonMail/gluon v0.17.1-0.20251127091939-17b9426ae8f7 h1:PaqJBeXv30G45LFglNMUxChxzGPg+V870BplSGrt0RM=
github.com/ProtonMail/gluon v0.17.1-0.20240923151549-d23b4bec3602/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8= github.com/ProtonMail/gluon v0.17.1-0.20251127091939-17b9426ae8f7/go.mod h1:OMwmLjgk6yJHX/P5KPck9WOcBVWIJLvuGZjj/8Ts/cw=
github.com/ProtonMail/gluon v0.17.1-0.20241002092751-3bbeea9053af h1:iMxTQUg2cB47cXqpMev3cZmQoGBOef3cSUjBbdEl33M= github.com/ProtonMail/gluon v0.17.1-0.20260108112233-b3e52866fa57 h1:aH0EeiBq/5c1rNI/1xzAmJWKgf+nFcqrKCUTUUV4/Sc=
github.com/ProtonMail/gluon v0.17.1-0.20241002092751-3bbeea9053af/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8= github.com/ProtonMail/gluon v0.17.1-0.20260108112233-b3e52866fa57/go.mod h1:YbW3CyxVxdbXiEGBwOxTW9nczPa8tA58HMkxosSf8bw=
github.com/ProtonMail/gluon v0.17.1-0.20241002111651-173859b80060 h1:dcu3tT84GjoXb++n7crv8UJeG8eRwogjTYdkoJ+MjQI= github.com/ProtonMail/gluon v0.17.1-0.20260112123503-2046c95ca745 h1:SHUpYnPoW78xQYAZCCcONiFMJDopk1a7vn7P42kYKMM=
github.com/ProtonMail/gluon v0.17.1-0.20241002111651-173859b80060/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8= github.com/ProtonMail/gluon v0.17.1-0.20260112123503-2046c95ca745/go.mod h1:YbW3CyxVxdbXiEGBwOxTW9nczPa8tA58HMkxosSf8bw=
github.com/ProtonMail/gluon v0.17.1-0.20241002142736-ef4153d156d8 h1:YxPHSJUA87i1hc6s1YrW89++V7HpcR7LSFQ6XM0TsAE= github.com/ProtonMail/go-autostart v0.0.0-20250402094843-326608c16033 h1:4r/ALoiixOOyjc1WhpwlkrcSFtRnc1GHWhk7ERELwbs=
github.com/ProtonMail/gluon v0.17.1-0.20241002142736-ef4153d156d8/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8= github.com/ProtonMail/go-autostart v0.0.0-20250402094843-326608c16033/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
github.com/ProtonMail/gluon v0.17.1-0.20241008123701-ddf4a459d0b4 h1:xE+V17O9HIttMpVymNCORQILk9OKpSekrrPbX7YGnF8=
github.com/ProtonMail/gluon v0.17.1-0.20241008123701-ddf4a459d0b4/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
github.com/ProtonMail/gluon v0.17.1-0.20241014082854-9d93627be032 h1:5bwI+mwF26c460xlq2Dw3/cVF1cU4Xo4kTKX1/pBXko=
github.com/ProtonMail/gluon v0.17.1-0.20241014082854-9d93627be032/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
github.com/ProtonMail/gluon v0.17.1-0.20241018144126-31e040c2417e h1:+UfdKOkF9JEiH9VXWBo+/nlXNVSJcxtuf4+SJTrk9fw=
github.com/ProtonMail/gluon v0.17.1-0.20241018144126-31e040c2417e/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
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.3.0-proton h1:tAQKQRZX/73VmzK6yHSCaRUOvS/3OYSQzhXQsrR7yUM=
github.com/ProtonMail/go-crypto v0.0.0-20230717121622-edf196117233/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= github.com/ProtonMail/go-crypto v1.3.0-proton/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
github.com/ProtonMail/go-keychain v0.0.0-20250929142014-ea8548dff768 h1:fTHlbASGfwJ4x4EeQ/JX7CX0YBfbFEcYm2nA6puPaWs=
github.com/ProtonMail/go-keychain v0.0.0-20250929142014-ea8548dff768/go.mod h1:ZoZU1fnBy3mOLWr3Pg+Y2+nTKtu6ypDte2kZg9HvSwY=
github.com/ProtonMail/go-libfido2 v0.0.0-20250916110427-df894d6d07a1 h1:MpPKmpti7MswJG5il3A+P24+iGxMj8V7/3JSMSRM1+c=
github.com/ProtonMail/go-libfido2 v0.0.0-20250916110427-df894d6d07a1/go.mod h1:92J9LtSBl0UyUWljElJpTbMMNhC6VeY8dshsu40qjjo=
github.com/ProtonMail/go-message v0.13.1-0.20240919135104-3bc88e6a9423 h1:p8nBDxvRnvDOyrcePKkPpErWGhDoTqpX8a1c54CcSu0= github.com/ProtonMail/go-message v0.13.1-0.20240919135104-3bc88e6a9423 h1:p8nBDxvRnvDOyrcePKkPpErWGhDoTqpX8a1c54CcSu0=
github.com/ProtonMail/go-message v0.13.1-0.20240919135104-3bc88e6a9423/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.20240918100656-b4860af56d47 h1:a+3dOyIxJEslN5HxyICM8flY9lnCyJupXNcv6fUaivA= github.com/ProtonMail/go-proton-api v0.4.1-0.20251127095056-9039cd6bf32a h1:g5A/1Jg7JR8MXucKDUJv48LnXq1mOSlI2yXo6/X4R/s=
github.com/ProtonMail/go-proton-api v0.4.1-0.20240918100656-b4860af56d47/go.mod h1:3A0cpdo0BIenIPjTG6u8EbzJ8uuJy7rVvM/NaynjCKA= github.com/ProtonMail/go-proton-api v0.4.1-0.20251127095056-9039cd6bf32a/go.mod h1:Xv7eeoGjaOLMZcjJj++yWNV99q5enByr0WcuF/ltTRA=
github.com/ProtonMail/go-proton-api v0.4.1-0.20260108112223-c9e6b92ad1fc h1:azlBBcGC5Y6FuEFRCY16pXh8vy268C9JBS6oU/AA33k=
github.com/ProtonMail/go-proton-api v0.4.1-0.20260108112223-c9e6b92ad1fc/go.mod h1:aVHyE5kG38rm99RQYuP3wWn8QuJpM5Me6KHaIDD92Qs=
github.com/ProtonMail/go-proton-api v0.4.1-0.20260109112619-daf7af47921d h1:q8y/G0qLRTxZr1xXk/kKFd2xwiyq44Yn2vI0xJJ6bhA=
github.com/ProtonMail/go-proton-api v0.4.1-0.20260109112619-daf7af47921d/go.mod h1:aVHyE5kG38rm99RQYuP3wWn8QuJpM5Me6KHaIDD92Qs=
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.9.0-proton h1:K3YRIBJo3YVObikaV9y1KWYGxFWRML+pFaiyh8ON2xA=
github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton/go.mod h1:omVkSsfPAhmptzPF/piMXb16wKIWUvVhZbVW7sJKh0A= github.com/ProtonMail/gopenpgp/v2 v2.9.0-proton/go.mod h1:NJ4RywdeD2sXCJyRRwb0ZYCx+QwGi14HUmlyNPegiwI=
github.com/ProtonMail/resty/v2 v2.0.0-20250929142426-e3dc6308c80b h1:0GYNP0odNPJFn1fbfwthcYPd3it0AVntvSaCjh2nlaE=
github.com/ProtonMail/resty/v2 v2.0.0-20250929142426-e3dc6308c80b/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A=
github.com/ProtonMail/winhello v0.0.0-20250918145518-a739b7dc2e56 h1:OFDKuwogje2Nor+2X81P0wWGcSZPXu2HKNUE71o3qZI=
github.com/ProtonMail/winhello v0.0.0-20250918145518-a739b7dc2e56/go.mod h1:kJnpbFRhpEatnRc05/CTeq4cWR2LUE7P6+KsPP/zRnE=
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=
@ -89,7 +89,6 @@ 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=
@ -107,9 +106,8 @@ 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/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 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=
@ -128,8 +126,6 @@ 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-20240103134243-0b6a41580b77 h1:sdB/yJMbubPQothFl6KYCOrMBRgy0pZbBXIWoJqSFLo=
github.com/cuthix/go-keychain v0.0.0-20240103134243-0b6a41580b77/go.mod h1:ZoZU1fnBy3mOLWr3Pg+Y2+nTKtu6ypDte2kZg9HvSwY=
github.com/danieljoos/wincred v1.2.1 h1:dl9cBrupW8+r5250DYkYxocLeZ1Y4vB1kxgtjxw8GQs= github.com/danieljoos/wincred v1.2.1 h1:dl9cBrupW8+r5250DYkYxocLeZ1Y4vB1kxgtjxw8GQs=
github.com/danieljoos/wincred v1.2.1/go.mod h1:uGaFL9fDn3OLTvzCGulzE+SzjEe5NGlh5FdCcyfPwps= github.com/danieljoos/wincred v1.2.1/go.mod h1:uGaFL9fDn3OLTvzCGulzE+SzjEe5NGlh5FdCcyfPwps=
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=
@ -175,6 +171,8 @@ github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BMXYYRWT
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:rZfgFAXFS/z/lEd6LJmf9HVZ1LkgYiHx5pHhV5DR16M= github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:rZfgFAXFS/z/lEd6LJmf9HVZ1LkgYiHx5pHhV5DR16M=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/fyne-io/mobile v0.1.2-0.20201127155338-06aeb98410cc/go.mod h1:/kOrWrZB6sasLbEy2JIvr4arEzQTXBTZGb3Y96yWbHY= github.com/fyne-io/mobile v0.1.2-0.20201127155338-06aeb98410cc/go.mod h1:/kOrWrZB6sasLbEy2JIvr4arEzQTXBTZGb3Y96yWbHY=
github.com/fyne-io/mobile v0.1.2/go.mod h1:/kOrWrZB6sasLbEy2JIvr4arEzQTXBTZGb3Y96yWbHY= github.com/fyne-io/mobile v0.1.2/go.mod h1:/kOrWrZB6sasLbEy2JIvr4arEzQTXBTZGb3Y96yWbHY=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
@ -186,6 +184,8 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
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-ctap/ctaphid v0.8.1 h1:HIDoSfqInkUIRBVPv61fVB2CNZ5nYxoaIgqmt8vzcs4=
github.com/go-ctap/ctaphid v0.8.1/go.mod h1:jRVrVfCs30jdZkSH2PoBopv9ry+tK99mpYumE4GIbb8=
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-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=
@ -194,6 +194,10 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200625191551-73d3c3675aa3/go.mod h1:tQ2
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
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-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
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/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=
@ -208,6 +212,8 @@ github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MG
github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4= github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4=
github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/goforj/godump v1.6.0 h1:3Dn8gaw5Xxxefr1ezTGTWrTKSr3ihK+eJ2xzRUoFfHQ=
github.com/goforj/godump v1.6.0/go.mod h1:/Vy+p50JtOkwsFN5dA1HQ7LS5gtPk3f61DaP4UR2o4s=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gofrs/uuid v4.3.0+incompatible h1:CaSVZxm5B+7o45rtab4jC2G37WGYX1zQfuU2i6DSvnc= github.com/gofrs/uuid v4.3.0+incompatible h1:CaSVZxm5B+7o45rtab4jC2G37WGYX1zQfuU2i6DSvnc=
github.com/gofrs/uuid v4.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
@ -235,9 +241,8 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 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.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 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.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
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=
@ -246,9 +251,8 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 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.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.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.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-cmp v0.5.9/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=
@ -257,8 +261,8 @@ github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8
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.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.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 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k=
github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= 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=
@ -339,6 +343,8 @@ 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/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/ldclabs/cose v1.3.2 h1:9M5l1zTvOyZONRsNj2PWJjmLdRqkcrsp80tyuNkOHdE=
github.com/ldclabs/cose v1.3.2/go.mod h1:X1srvv76GKudjv85VCUgka049gaK5aozbBhMDaCEbpc=
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=
@ -432,8 +438,8 @@ github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
@ -455,8 +461,9 @@ github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02n
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
@ -467,8 +474,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
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/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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
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=
@ -481,6 +488,8 @@ github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9
github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
@ -494,6 +503,18 @@ 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 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
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=
@ -510,11 +531,10 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
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-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/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
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=
@ -539,8 +559,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.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
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=
@ -563,20 +584,19 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
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/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
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.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
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=
@ -586,8 +606,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.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.16.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=
@ -621,18 +641,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/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.35.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=
@ -646,13 +663,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/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.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
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=
@ -681,12 +697,15 @@ 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.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
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=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
@ -719,8 +738,8 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 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.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 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.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 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-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 v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@ -730,10 +749,8 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
google.golang.org/protobuf v1.23.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.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.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.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
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=
@ -750,6 +767,7 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@ -1,4 +1,4 @@
// Copyright (c) 2024 Proton AG // Copyright (c) 2026 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //
@ -39,7 +39,10 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/frontend/theme" "github.com/ProtonMail/proton-bridge/v3/internal/frontend/theme"
"github.com/ProtonMail/proton-bridge/v3/internal/locations" "github.com/ProtonMail/proton-bridge/v3/internal/locations"
"github.com/ProtonMail/proton-bridge/v3/internal/logging" "github.com/ProtonMail/proton-bridge/v3/internal/logging"
"github.com/ProtonMail/proton-bridge/v3/internal/platform"
"github.com/ProtonMail/proton-bridge/v3/internal/sentry" "github.com/ProtonMail/proton-bridge/v3/internal/sentry"
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
"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/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"
@ -97,18 +100,21 @@ 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 var cliFlagEnableKeychainTest = &cli.BoolFlag{ //nolint:gochecknoglobals
Name: flagEnableKeychainTest, Name: flagEnableKeychainTest,
Usage: "Enable the keychain test for the current and future executions of the application", Usage: "This flag is deprecated and does nothing",
Value: false, Value: false,
DisableDefaultText: true, DisableDefaultText: true,
} //nolint:gochecknoglobals Hidden: true,
}
var cliFlagDisableKeychainTest = &cli.BoolFlag{ //nolint:gochecknoglobals var cliFlagDisableKeychainTest = &cli.BoolFlag{ //nolint:gochecknoglobals
Name: flagDisableKeychainTest, Name: flagDisableKeychainTest,
Usage: "Disable the keychain test for the current and future executions of the application", Usage: "This flag is deprecated and does nothing",
Value: false, Value: false,
DisableDefaultText: true, DisableDefaultText: true,
Hidden: true,
} }
func New() *cli.App { func New() *cli.App {
@ -205,12 +211,14 @@ func New() *cli.App {
// We override the default help value because we want "Show" to be capitalized // We override the default help value because we want "Show" to be capitalized
cli.HelpFlag = &cli.BoolFlag{ cli.HelpFlag = &cli.BoolFlag{
Name: "help", Name: "help",
Aliases: []string{"h"},
Usage: "Show help", Usage: "Show help",
DisableDefaultText: true, DisableDefaultText: true,
} }
if onMacOS() { if onMacOS() {
// The two flags below were introduced for BRIDGE-116, and are available only on macOS. // 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.Flags = append(app.Flags, cliFlagEnableKeychainTest, cliFlagDisableKeychainTest)
} }
@ -280,57 +288,61 @@ func run(c *cli.Context) error {
logrus.WithError(err).Error("Failed to get settings path") logrus.WithError(err).Error("Failed to get settings path")
} }
featureFlags := unleash.GetStartupFeatureFlagsAndStore(constants.APIHost, version, locations.ProvideUnleashStartupCachePath)
return withSingleInstance(settings, locations.GetLockFile(), version, func() error { return withSingleInstance(settings, locations.GetLockFile(), version, func() error {
// Look for available keychains // Look for available keychains
skipKeychainTest := checkSkipKeychainTest(c, settings) return WithKeychainList(crashHandler, func(keychains *keychain.List) error {
return WithKeychainList(crashHandler, skipKeychainTest, func(keychains *keychain.List) error { // Pre-init the observability service, load the cached metrics.
// Unlock the encrypted vault. return observability.WithObservability(locations, func(obsService *observability.Service) error {
return WithVault(reporter, locations, keychains, crashHandler, func(v *vault.Vault, insecure, corrupt bool) error { // Unlock the encrypted vault.
if !v.Migrated() { return WithVault(reporter, locations, keychains, obsService, featureFlags, crashHandler, func(v *vault.Vault, insecure, corrupt bool) error {
// Migrate old settings into the vault. if !v.Migrated() {
if err := migrateOldSettings(v); err != nil { // Migrate old settings into the vault.
logrus.WithError(err).Error("Failed to migrate old settings") if err := migrateOldSettings(v); err != nil {
} logrus.WithError(err).Error("Failed to migrate old settings")
// Migrate old accounts into the vault.
if err := migrateOldAccounts(locations, keychains, v); err != nil {
logrus.WithError(err).Error("Failed to migrate old accounts")
}
// The vault has been migrated.
if err := v.SetMigrated(); err != nil {
logrus.WithError(err).Error("Failed to mark vault as migrated")
}
}
logrus.WithFields(logrus.Fields{
"lastVersion": v.GetLastVersion().String(),
"showAllMail": v.GetShowAllMail(),
"updateCh": v.GetUpdateChannel(),
"autoUpdate": v.GetAutoUpdate(),
"rollout": v.GetUpdateRollout(),
"DoH": v.GetProxyAllowed(),
}).Info("Vault loaded")
// Load the cookies from the vault.
return withCookieJar(v, func(cookieJar http.CookieJar) error {
// Create a new bridge instance.
return withBridge(c, exe, locations, version, identifier, crashHandler, reporter, v, cookieJar, keychains, func(b *bridge.Bridge, eventCh <-chan events.Event) error {
if insecure {
logrus.Warn("The vault key could not be retrieved; the vault will not be encrypted")
b.PushError(bridge.ErrVaultInsecure)
} }
if corrupt { // Migrate old accounts into the vault.
logrus.Warn("The vault is corrupt and has been wiped") if err := migrateOldAccounts(locations, keychains, v); err != nil {
b.PushError(bridge.ErrVaultCorrupt) logrus.WithError(err).Error("Failed to migrate old accounts")
} }
// Remove old updates files // The vault has been migrated.
b.RemoveOldUpdates() if err := v.SetMigrated(); err != nil {
logrus.WithError(err).Error("Failed to mark vault as migrated")
}
}
// Run the frontend. logrus.WithFields(logrus.Fields{
return runFrontend(c, crashHandler, restarter, locations, b, eventCh, quitCh, c.Int(flagParentPID)) "lastVersion": v.GetLastVersion().String(),
"showAllMail": v.GetShowAllMail(),
"updateCh": v.GetUpdateChannel(),
"autoUpdate": v.GetAutoUpdate(),
"rollout": v.GetUpdateRollout(),
"DoH": v.GetProxyAllowed(),
}).Info("Vault loaded")
// Load the cookies from the vault.
return withCookieJar(v, func(cookieJar http.CookieJar) error {
// Create a new bridge instance.
return withBridge(c, exe, locations, version, identifier, obsService, crashHandler, reporter, v, cookieJar, keychains, func(b *bridge.Bridge, eventCh <-chan events.Event) error {
if insecure {
logrus.Warn("The vault key could not be retrieved; the vault will not be encrypted")
b.PushError(bridge.ErrVaultInsecure)
}
if corrupt {
logrus.Warn("The vault is corrupt and has been wiped")
b.PushError(bridge.ErrVaultCorrupt)
}
// Remove old updates files
b.RemoveOldUpdates()
// Run the frontend.
return runFrontend(c, crashHandler, restarter, locations, b, eventCh, quitCh, c.Int(flagParentPID))
})
}) })
}) })
}) })
@ -547,11 +559,11 @@ 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(panicHandler async.PanicHandler, skipKeychainTest bool, 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) defer async.HandlePanic(panicHandler)
return fn(keychain.NewList(skipKeychainTest)) return fn(keychain.NewList())
} }
func setDeviceCookies(jar *cookies.Jar) error { func setDeviceCookies(jar *cookies.Jar) error {
@ -572,38 +584,6 @@ func setDeviceCookies(jar *cookies.Jar) error {
return nil return nil
} }
func checkSkipKeychainTest(c *cli.Context, settingsDir string) bool {
if !onMacOS() {
return false
}
enable := c.Bool(flagEnableKeychainTest)
disable := c.Bool(flagDisableKeychainTest)
skip, err := vault.GetShouldSkipKeychainTest(settingsDir)
if err != nil {
logrus.WithError(err).Error("Could not load keychain settings.")
}
if (!enable) && (!disable) {
return skip
}
// if both switches are passed, 'enable' has priority
if disable {
skip = true
}
if enable {
skip = false
}
if err := vault.SetShouldSkipKeychainTest(settingsDir, skip); err != nil {
logrus.WithError(err).Error("Could not save keychain settings.")
}
return skip
}
func onMacOS() bool { func onMacOS() bool {
return runtime.GOOS == "darwin" return runtime.GOOS == platform.MACOS
} }

View File

@ -1,64 +0,0 @@
// Copyright (c) 2024 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 app
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
)
func TestCheckSkipKeychainTest(t *testing.T) {
var expectedResult bool
dir := t.TempDir()
app := cli.App{
Flags: []cli.Flag{
cliFlagEnableKeychainTest,
cliFlagDisableKeychainTest,
},
Action: func(c *cli.Context) error {
require.Equal(t, expectedResult, checkSkipKeychainTest(c, dir))
return nil
},
}
noArgs := []string{"appName"}
enableArgs := []string{"appName", "-" + flagEnableKeychainTest}
disableArgs := []string{"appName", "-" + flagDisableKeychainTest}
bothArgs := []string{"appName", "-" + flagDisableKeychainTest, "-" + flagEnableKeychainTest}
onMac := onMacOS()
expectedResult = false
require.NoError(t, app.Run(noArgs))
expectedResult = onMac
require.NoError(t, app.Run(disableArgs))
require.NoError(t, app.Run(noArgs))
expectedResult = false
require.NoError(t, app.Run(enableArgs))
require.NoError(t, app.Run(noArgs))
expectedResult = onMac
require.NoError(t, app.Run(disableArgs))
expectedResult = false
require.NoError(t, app.Run(bothArgs))
}

View File

@ -1,4 +1,4 @@
// Copyright (c) 2024 Proton AG // Copyright (c) 2026 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //
@ -33,6 +33,7 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/events" "github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/locations" "github.com/ProtonMail/proton-bridge/v3/internal/locations"
"github.com/ProtonMail/proton-bridge/v3/internal/sentry" "github.com/ProtonMail/proton-bridge/v3/internal/sentry"
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
"github.com/ProtonMail/proton-bridge/v3/internal/updater" "github.com/ProtonMail/proton-bridge/v3/internal/updater"
"github.com/ProtonMail/proton-bridge/v3/internal/useragent" "github.com/ProtonMail/proton-bridge/v3/internal/useragent"
"github.com/ProtonMail/proton-bridge/v3/internal/vault" "github.com/ProtonMail/proton-bridge/v3/internal/vault"
@ -52,6 +53,7 @@ func withBridge(
locations *locations.Locations, locations *locations.Locations,
version *semver.Version, version *semver.Version,
identifier *useragent.UserAgent, identifier *useragent.UserAgent,
obsService *observability.Service,
crashHandler *crash.Handler, crashHandler *crash.Handler,
reporter *sentry.Reporter, reporter *sentry.Reporter,
vault *vault.Vault, vault *vault.Vault,
@ -100,6 +102,7 @@ func withBridge(
updater, updater,
version, version,
keychains, keychains,
obsService,
// The API stuff. // The API stuff.
constants.APIHost, constants.APIHost,

View File

@ -1,4 +1,4 @@
// Copyright (c) 2024 Proton AG // Copyright (c) 2026 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) 2024 Proton AG // Copyright (c) 2026 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //
@ -138,7 +138,14 @@ 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(),
0,
make(map[string]bool),
)
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) 2024 Proton AG // Copyright (c) 2026 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //
@ -31,6 +31,7 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/cookies" "github.com/ProtonMail/proton-bridge/v3/internal/cookies"
"github.com/ProtonMail/proton-bridge/v3/internal/legacy/credentials" "github.com/ProtonMail/proton-bridge/v3/internal/legacy/credentials"
"github.com/ProtonMail/proton-bridge/v3/internal/locations" "github.com/ProtonMail/proton-bridge/v3/internal/locations"
"github.com/ProtonMail/proton-bridge/v3/internal/platform"
"github.com/ProtonMail/proton-bridge/v3/internal/updater" "github.com/ProtonMail/proton-bridge/v3/internal/updater"
"github.com/ProtonMail/proton-bridge/v3/internal/vault" "github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/ProtonMail/proton-bridge/v3/pkg/algo" "github.com/ProtonMail/proton-bridge/v3/pkg/algo"
@ -85,7 +86,7 @@ func TestMigratePrefsToVaultWithoutKeys(t *testing.T) {
func TestKeychainMigration(t *testing.T) { func TestKeychainMigration(t *testing.T) {
// Migration tested only for linux. // Migration tested only for linux.
if runtime.GOOS != "linux" { if runtime.GOOS != platform.LINUX {
return return
} }
@ -134,7 +135,13 @@ 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(),
0,
make(map[string]bool),
)
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) 2024 Proton AG // Copyright (c) 2026 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) 2024 Proton AG // Copyright (c) 2026 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //
@ -18,25 +18,33 @@
package app package app
import ( import (
"crypto/sha256"
"encoding/hex"
"errors"
"fmt" "fmt"
"path" "path"
"runtime"
"github.com/ProtonMail/gluon/async" "github.com/ProtonMail/gluon/async"
"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/platform"
"github.com/ProtonMail/proton-bridge/v3/internal/sentry" "github.com/ProtonMail/proton-bridge/v3/internal/sentry"
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
"github.com/ProtonMail/proton-bridge/v3/internal/unleash"
"github.com/ProtonMail/proton-bridge/v3/internal/vault" "github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/ProtonMail/proton-bridge/v3/internal/vault/observabilitymetrics"
"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(reporter *sentry.Reporter, 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, obsSender observability.BasicSender, featureFlags unleash.FeatureFlagStartupStore, 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(reporter, locations, keychains, panicHandler) encVault, insecure, corrupt, err := newVault(reporter, locations, keychains, obsSender, featureFlags, 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)
} }
@ -58,7 +66,7 @@ func WithVault(reporter *sentry.Reporter, locations *locations.Locations, keycha
return fn(encVault, insecure, corrupt != nil) return fn(encVault, insecure, corrupt != nil)
} }
func newVault(reporter *sentry.Reporter, 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, obsSender observability.BasicSender, featureFlags unleash.FeatureFlagStartupStore, 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)
@ -67,11 +75,19 @@ func newVault(reporter *sentry.Reporter, locations *locations.Locations, keychai
logrus.WithField("vaultDir", vaultDir).Debug("Loading vault from directory") logrus.WithField("vaultDir", vaultDir).Debug("Loading vault from directory")
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, featureFlags); err != nil {
if errors.Is(err, keychain.ErrPreferredKeychainNotAvailable) {
if err := vault.IncrementKeychainFailedAttemptCount(vaultDir); err != nil {
logrus.WithError(err).Error("Failed to increment failed keychain attempt count")
}
return &vault.Vault{}, false, nil, err
}
if reporter != nil { if reporter != nil {
if rerr := reporter.ReportMessageWithContext("Could not load/create vault key", map[string]any{ if rerr := reporter.ReportMessageWithContext("Could not load/create vault key", map[string]any{
"keychainDefaultHelper": keychains.GetDefaultHelper(), "keychainDefaultHelper": keychains.GetDefaultHelper(),
@ -87,8 +103,13 @@ func newVault(reporter *sentry.Reporter, locations *locations.Locations, keychai
// We store the insecure vault in a separate directory // We store the insecure vault in a separate directory
vaultDir = path.Join(vaultDir, "insecure") vaultDir = path.Join(vaultDir, "insecure")
// Schedule the relevant observability metric for sending.
obsSender.AddMetrics(observabilitymetrics.GenerateVaultKeyFetchGenericErrorMetric())
} 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()
@ -96,34 +117,77 @@ func newVault(reporter *sentry.Reporter, locations *locations.Locations, keychai
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 {
obsSender.AddMetrics(observabilitymetrics.GenerateVaultCreationGenericErrorMetric())
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 if corrupt != nil {
obsSender.AddMetrics(observabilitymetrics.GenerateVaultCreationCorruptErrorMetric())
}
// Remember the last successfully used keychain on Linux and store that as the user preference.
if runtime.GOOS == platform.LINUX {
if err := vault.SetHelper(vaultDir, lastUsedHelper); err != nil {
logrus.WithError(err).Error("Could not store last used keychain helper")
}
if err := vault.ResetFailedKeychainAttemptCount(vaultDir); err != nil {
logrus.WithError(err).Error("Could not reset and save failed keychain attempt count")
}
}
return userVault, insecure, corrupt, nil
} }
func loadVaultKey(vaultDir string, keychains *keychain.List) ([]byte, error) { // loadVaultKey - loads the key used to encrypt the vault alongside the keychain helper used to access it.
helper, err := vault.GetHelper(vaultDir) func loadVaultKey(vaultDir string, keychains *keychain.List, featureFlags unleash.FeatureFlagStartupStore) (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()) keychainFailedAttemptCount, err := vault.GetKeychainFailedAttemptCount(vaultDir)
if err != nil { if err != nil {
return nil, fmt.Errorf("could not create keychain: %w", err) return nil, keychainHelper, fmt.Errorf("could not get keychain failed attempt count: %w", err)
} }
key, err := vault.GetVaultKey(kc) kc, keychainHelper, err := keychain.NewKeychain(
keychainHelper, constants.KeyChainName,
keychains.GetHelpers(),
keychains.GetDefaultHelper(),
keychainFailedAttemptCount,
featureFlags,
)
if err != nil {
return nil, keychainHelper, fmt.Errorf("could not create keychain: %w", err)
}
logrus.WithField("keychainHelper", keychainHelper).Info("Initialized keychain helper")
key, err = vault.GetVaultKey(kc)
if err != nil { if err != nil {
if keychain.IsErrKeychainNoItem(err) { if keychain.IsErrKeychainNoItem(err) {
logrus.WithError(err).Warn("no vault key found, generating new") logrus.WithError(err).Warn("no vault key found, generating new")
return vault.NewVaultKey(kc) key, err := vault.NewVaultKey(kc)
return key, keychainHelper, err
} }
return nil, fmt.Errorf("could not check for vault key: %w", err) if keychain.ShouldRetryPreferredKeychain(featureFlags, keychainHelper) {
if keychainFailedAttemptCount < keychain.MaxFailedKeychainAttemptsLinux {
return nil, keychainHelper, keychain.PreferredKeychainRetryError(keychainFailedAttemptCount)
}
}
return nil, keychainHelper, fmt.Errorf("could not check for vault key: %w", err)
} }
return key, nil 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) 2024 Proton AG // Copyright (c) 2026 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) 2024 Proton AG // Copyright (c) 2026 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) 2024 Proton AG // Copyright (c) 2026 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) 2024 Proton AG // Copyright (c) 2026 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //
@ -42,6 +42,7 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/events" "github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/focus" "github.com/ProtonMail/proton-bridge/v3/internal/focus"
"github.com/ProtonMail/proton-bridge/v3/internal/identifier" "github.com/ProtonMail/proton-bridge/v3/internal/identifier"
"github.com/ProtonMail/proton-bridge/v3/internal/platform"
"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"
@ -50,11 +51,14 @@ import (
"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/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"
uuid "github.com/google/uuid"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -80,8 +84,9 @@ type Bridge struct {
imapEventCh chan imapEvents.Event imapEventCh chan imapEvents.Event
// updater is the bridge's updater. // updater is the bridge's updater.
updater Updater updater Updater
installCh chan installJob installChLegacy chan installJobLegacy
installCh chan installJob
// heartbeat is the telemetry heartbeat for metrics. // heartbeat is the telemetry heartbeat for metrics.
heartbeat *heartBeatState heartbeat *heartBeatState
@ -148,6 +153,9 @@ type Bridge struct {
// notificationStore is used for notification deduplication // notificationStore is used for notification deduplication
notificationStore *notifications.Store 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 var logPkg = logrus.WithField("pkg", "bridge") //nolint:gochecknoglobals
@ -160,6 +168,7 @@ func New(
updater Updater, // the updater to fetch and install updates updater Updater, // the updater to fetch and install updates
curVersion *semver.Version, // the current version of the bridge curVersion *semver.Version, // the current version of the bridge
keychains *keychain.List, // usable keychains keychains *keychain.List, // usable keychains
obsService *observability.Service,
apiURL string, // the URL of the API to use apiURL string, // the URL of the API to use
cookieJar http.CookieJar, // the cookie jar to use cookieJar http.CookieJar, // the cookie jar to use
@ -198,6 +207,7 @@ func New(
keychains, keychains,
panicHandler, panicHandler,
reporter, reporter,
obsService,
api, api,
identifier, identifier,
@ -234,6 +244,7 @@ func newBridge(
keychains *keychain.List, keychains *keychain.List,
panicHandler async.PanicHandler, panicHandler async.PanicHandler,
reporter reporter.Reporter, reporter reporter.Reporter,
obsService *observability.Service,
api *proton.Manager, api *proton.Manager,
identifier identifier.Identifier, identifier identifier.Identifier,
@ -265,9 +276,9 @@ 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) unleashService := unleash.NewBridgeService(ctx, api, locator, panicHandler, vault.GetFeatureFlagStickyKey())
observabilityService := observability.NewService(ctx, panicHandler) obsService.Initialize(ctx, panicHandler)
bridge := &Bridge{ bridge := &Bridge{
vault: vault, vault: vault,
@ -282,8 +293,9 @@ func newBridge(
tlsConfig: tlsConfig, tlsConfig: tlsConfig,
imapEventCh: imapEventCh, imapEventCh: imapEventCh,
updater: updater, updater: updater,
installCh: make(chan installJob), installChLegacy: make(chan installJobLegacy),
installCh: make(chan installJob),
curVersion: curVersion, curVersion: curVersion,
newVersion: curVersion, newVersion: curVersion,
@ -308,13 +320,15 @@ func newBridge(
lastVersion: lastVersion, lastVersion: lastVersion,
tasks: tasks, tasks: tasks,
syncService: syncservice.NewService(panicHandler, observabilityService), syncService: syncservice.NewService(panicHandler, obsService),
unleashService: unleashService, unleashService: unleashService,
observabilityService: observabilityService, observabilityService: obsService,
notificationStore: notifications.NewStore(locator.ProvideNotificationsCachePath), 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(),
@ -325,7 +339,8 @@ func newBridge(
reporter, reporter,
uidValidityGenerator, uidValidityGenerator,
&bridgeIMAPSMTPTelemetry{b: bridge}, &bridgeIMAPSMTPTelemetry{b: bridge},
observabilityService, obsService,
unleashService,
) )
// Check whether username has changed and correct (macOS only) // Check whether username has changed and correct (macOS only)
@ -435,17 +450,46 @@ 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) {
logPkg.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 { } else {
bridge.handleUpdate(version) if useOldUpdateLogic {
bridge.handleUpdateLegacy(versionLegacy)
} else {
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)
@ -636,14 +680,6 @@ func loadTLSConfig(vault *vault.Vault) (*tls.Config, error) {
}, nil }, nil
} }
func min(a, b time.Duration) time.Duration { //nolint:predeclared
if a < b {
return a
}
return b
}
func (bridge *Bridge) HasAPIConnection() bool { func (bridge *Bridge) HasAPIConnection() bool {
return bridge.api.GetStatus() == proton.StatusUp return bridge.api.GetStatus() == proton.StatusUp
} }
@ -655,7 +691,7 @@ func (bridge *Bridge) HasAPIConnection() bool {
// then we verify whether the gluon cache exists using the "new" username (provided by the DB path in this case) // 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. // if so we modify the cache directory in the user vault.
func (bridge *Bridge) verifyUsernameChange() { func (bridge *Bridge) verifyUsernameChange() {
if runtime.GOOS != "darwin" { if runtime.GOOS != platform.MACOS {
return return
} }
@ -690,13 +726,13 @@ func (bridge *Bridge) verifyUsernameChange() {
func GetUpdatedCachePath(gluonDBPath, gluonCachePath string) string { func GetUpdatedCachePath(gluonDBPath, gluonCachePath string) string {
// If gluon cache is moved to an external drive; regex find will fail; as is expected // If gluon cache is moved to an external drive; regex find will fail; as is expected
cachePathMatches := usernameChangeRegex.FindStringSubmatch(gluonCachePath) cachePathMatches := usernameChangeRegex.FindStringSubmatch(gluonCachePath)
if cachePathMatches == nil || len(cachePathMatches) < 2 { if len(cachePathMatches) < 2 {
return "" return ""
} }
cacheUsername := cachePathMatches[1] cacheUsername := cachePathMatches[1]
dbPathMatches := usernameChangeRegex.FindStringSubmatch(gluonDBPath) dbPathMatches := usernameChangeRegex.FindStringSubmatch(gluonDBPath)
if dbPathMatches == nil || len(dbPathMatches) < 2 { if len(dbPathMatches) < 2 {
return "" return ""
} }
@ -716,10 +752,45 @@ func (bridge *Bridge) PushObservabilityMetric(metric proton.ObservabilityMetric)
bridge.observabilityService.AddMetrics(metric) bridge.observabilityService.AddMetrics(metric)
} }
func (bridge *Bridge) PushDistinctObservabilityMetrics(errType observability.DistinctionErrorTypeEnum, metrics ...proton.ObservabilityMetric) { func (bridge *Bridge) PushDistinctObservabilityMetrics(errType observability.DistinctionMetricTypeEnum, metrics ...proton.ObservabilityMetric) {
bridge.observabilityService.AddDistinctMetrics(errType, metrics...) bridge.observabilityService.AddDistinctMetrics(errType, metrics...)
} }
func (bridge *Bridge) ModifyObservabilityHeartbeatInterval(duration time.Duration) { func (bridge *Bridge) ModifyObservabilityHeartbeatInterval(duration time.Duration) {
bridge.observabilityService.ModifyHeartbeatInterval(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)
}
func (bridge *Bridge) GetFeatureFlagStickyKey() uuid.UUID {
return bridge.vault.GetFeatureFlagStickyKey()
}

View File

@ -1,4 +1,4 @@
// Copyright (c) 2024 Proton AG // Copyright (c) 2026 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //
@ -45,6 +45,8 @@ 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/services/observability"
"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"
@ -55,6 +57,7 @@ import (
imapid "github.com/emersion/go-imap-id" imapid "github.com/emersion/go-imap-id"
"github.com/emersion/go-sasl" "github.com/emersion/go-sasl"
"github.com/emersion/go-smtp" "github.com/emersion/go-smtp"
"github.com/google/uuid"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"go.uber.org/goleak" "go.uber.org/goleak"
) )
@ -383,9 +386,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))
@ -400,7 +408,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{})
@ -411,7 +419,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,
@ -423,25 +431,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,
@ -452,9 +465,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))
@ -463,14 +481,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,
@ -484,7 +502,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) {
unleash.ModifyPollPeriodAndJitter(500*time.Millisecond, 0)
s.PushFeatureFlag(unleash.UpdateUseNewVersionFileStructureDisabled)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) { 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()
@ -597,7 +620,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.
@ -757,6 +780,30 @@ func TestBridge_ChangeCacheDirectory(t *testing.T) {
}) })
} }
func TestBridge_FeatureFlagStickyKey_Persistence(t *testing.T) {
var uuidOne uuid.UUID
var uuidTwo uuid.UUID
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, _ *bridge.Mocks) {
uuidOne = b.GetFeatureFlagStickyKey()
})
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
require.Equal(t, uuidOne, b.GetFeatureFlagStickyKey())
})
})
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, _ *bridge.Mocks) {
uuidTwo = b.GetFeatureFlagStickyKey()
require.NotEqual(t, uuidOne, uuidTwo)
})
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
require.Equal(t, uuidTwo, b.GetFeatureFlagStickyKey())
})
})
}
func TestBridge_ChangeAddressOrder(t *testing.T) { func TestBridge_ChangeAddressOrder(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) {
// Create a user. // Create a user.
@ -764,7 +811,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.
@ -898,6 +945,7 @@ func withBridgeNoMocks(
mocks.Updater, mocks.Updater,
v2_3_0, v2_3_0,
keychain.NewTestKeychainsList(), keychain.NewTestKeychainsList(),
observability.NewTestService(),
// The API stuff. // The API stuff.
apiURL, apiURL,

View File

@ -1,4 +1,4 @@
// Copyright (c) 2024 Proton AG // Copyright (c) 2026 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,46 +0,0 @@
// Copyright (c) 2024 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
import (
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
)
func (bridge *Bridge) ReportBugClicked() {
safe.RLock(func() {
for _, user := range bridge.users {
user.ReportBugClicked()
}
}, bridge.usersLock)
}
func (bridge *Bridge) AutoconfigUsed(client string) {
safe.RLock(func() {
for _, user := range bridge.users {
user.AutoconfigUsed(client)
}
}, bridge.usersLock)
}
func (bridge *Bridge) ExternalLinkClicked(article string) {
safe.RLock(func() {
for _, user := range bridge.users {
user.ExternalLinkClicked(article)
}
}, bridge.usersLock)
}

View File

@ -1,4 +1,4 @@
// Copyright (c) 2024 Proton AG // Copyright (c) 2026 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) 2024 Proton AG // Copyright (c) 2026 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) 2024 Proton AG // Copyright (c) 2026 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) 2024 Proton AG // Copyright (c) 2026 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) 2024 Proton AG // Copyright (c) 2026 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) 2024 Proton AG // Copyright (c) 2026 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)

View File

@ -1,4 +1,4 @@
// Copyright (c) 2024 Proton AG // Copyright (c) 2026 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) 2024 Proton AG // Copyright (c) 2026 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"
) )
@ -93,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) 2024 Proton AG // Copyright (c) 2026 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) 2024 Proton AG // Copyright (c) 2026 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) 2024 Proton AG // Copyright (c) 2026 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) 2024 Proton AG // Copyright (c) 2026 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
lock sync.RWMutex releases updater.VersionInfo
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) 2024 Proton AG // Copyright (c) 2026 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //

View File

@ -25,7 +25,7 @@ func NewMockObservabilitySender(ctrl *gomock.Controller) *MockObservabilitySende
func (m *MockObservabilitySender) EXPECT() *MockObservabilitySenderRecorder { return m.recorder } func (m *MockObservabilitySender) EXPECT() *MockObservabilitySenderRecorder { return m.recorder }
func (m *MockObservabilitySender) AddDistinctMetrics(errType observability.DistinctionErrorTypeEnum, _ ...proton.ObservabilityMetric) { func (m *MockObservabilitySender) AddDistinctMetrics(errType observability.DistinctionMetricTypeEnum, _ ...proton.ObservabilityMetric) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
m.ctrl.Call(m, "AddDistinctMetrics", errType) m.ctrl.Call(m, "AddDistinctMetrics", errType)
} }
@ -35,7 +35,18 @@ func (m *MockObservabilitySender) AddMetrics(metrics ...proton.ObservabilityMetr
m.ctrl.Call(m, "AddMetrics", metrics) m.ctrl.Call(m, "AddMetrics", metrics)
} }
func (mr *MockObservabilitySenderRecorder) AddDistinctMetrics(errType observability.DistinctionErrorTypeEnum, _ ...proton.ObservabilityMetric) *gomock.Call { 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() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, return mr.mock.ctrl.RecordCallWithMethodType(mr.mock,
"AddDistinctMetrics", "AddDistinctMetrics",
@ -47,3 +58,13 @@ func (mr *MockObservabilitySenderRecorder) AddMetrics(metrics ...proton.Observab
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddMetrics", reflect.TypeOf((*MockObservabilitySender)(nil).AddMetrics), metrics) 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

@ -1,4 +1,4 @@
// Copyright (c) 2024 Proton AG // Copyright (c) 2026 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //
@ -127,9 +127,9 @@ func TestBridge_Observability_UserMetric(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) {
userMetricPeriod := time.Millisecond * 200 userMetricPeriod := time.Millisecond * 600
heartbeatPeriod := time.Second * 10 heartbeatPeriod := time.Second * 10
throttlePeriod := time.Millisecond * 100 throttlePeriod := time.Millisecond * 300
observability.ModifyUserMetricInterval(userMetricPeriod) observability.ModifyUserMetricInterval(userMetricPeriod)
observability.ModifyThrottlePeriod(throttlePeriod) observability.ModifyThrottlePeriod(throttlePeriod)

View File

@ -1,4 +1,4 @@
// Copyright (c) 2024 Proton AG // Copyright (c) 2026 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) 2024 Proton AG // Copyright (c) 2026 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) 2024 Proton AG // Copyright (c) 2026 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) 2024 Proton AG // Copyright (c) 2026 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) 2024 Proton AG // Copyright (c) 2026 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //
@ -318,11 +318,10 @@ func (bridge *Bridge) GetKnowledgeBaseSuggestions(userInput string) (kb.ArticleL
// 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)

View File

@ -1,4 +1,4 @@
// Copyright (c) 2024 Proton AG // Copyright (c) 2026 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) 2024 Proton AG // Copyright (c) 2026 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) 2024 Proton AG // Copyright (c) 2026 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //
@ -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.
@ -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) 2024 Proton AG // Copyright (c) 2026 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) 2024 Proton AG // Copyright (c) 2026 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) 2024 Proton AG // Copyright (c) 2026 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //
@ -28,7 +28,6 @@ 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
@ -53,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

@ -1,4 +1,4 @@
// Copyright (c) 2024 Proton AG // Copyright (c) 2026 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) 2024 Proton AG // Copyright (c) 2026 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //
@ -21,22 +21,168 @@ import (
"context" "context"
"errors" "errors"
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/gluon/reporter" "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,
@ -44,7 +190,7 @@ func (bridge *Bridge) handleUpdate(version updater.VersionInfo) {
}) })
bridge.publish(events.UpdateLatest{ bridge.publish(events.UpdateLatest{
Version: version, VersionLegacy: version,
}) })
switch { switch {
@ -62,33 +208,33 @@ 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,
}) })
case !bridge.vault.GetAutoUpdate(): case !bridge.vault.GetAutoUpdate():
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,
@ -103,17 +249,12 @@ 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): case errors.Is(err, updater.ErrDownloadVerify):
@ -134,8 +275,84 @@ 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,
})
default:
log.Info("The update was installed successfully")
bridge.publish(events.UpdateInstalled{
VersionLegacy: job.version,
Silent: job.silent,
})
bridge.newVersion = job.version.Version
}
}, 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")
if reporterErr := bridge.reporter.ReportMessageWithContext(
"Cannot install update",
reporter.Context{"error": err},
); reporterErr != nil {
log.WithError(reporterErr).Error("Failed to report update error")
}
bridge.publish(events.UpdateFailed{
Release: job.Release,
Silent: job.Silent,
Error: err, Error: err,
}) })
@ -143,11 +360,11 @@ 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, Release: job.Release,
Silent: job.silent, Silent: job.Silent,
}) })
bridge.newVersion = job.version.Version bridge.newVersion = job.Release.Version
} }
}, bridge.newVersionLock) }, bridge.newVersionLock)
} }

View File

@ -0,0 +1,701 @@
// Copyright (c) 2026 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/platform"
"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 == platform.MACOS {
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 == platform.MACOS {
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) 2024 Proton AG // Copyright (c) 2026 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //
@ -33,6 +33,7 @@ import (
"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"
@ -255,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,
@ -280,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 {
@ -358,7 +359,7 @@ func (bridge *Bridge) SendBadEventUserFeedback(_ context.Context, userID string,
return user.BadEventFeedbackResync(ctx) return user.BadEventFeedbackResync(ctx)
} }
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,
@ -527,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)
@ -546,7 +542,6 @@ 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,
@ -556,7 +551,7 @@ func (bridge *Bridge) addUserWithVault(
syncSettingsPath, syncSettingsPath,
isNew, isNew,
bridge.notificationStore, bridge.notificationStore,
bridge.unleashService.GetFlagValue, 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)
@ -589,9 +584,12 @@ 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()
@ -610,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
if withData && withAPI {
user.SendConfigStatusAbort(ctx, withTelemetry)
}
logUser.WithFields(logrus.Fields{ logUser.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 {
logUser.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) 2024 Proton AG // Copyright (c) 2026 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"
@ -36,6 +37,7 @@ 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/ProtonMail/proton-bridge/v3/internal/platform"
"github.com/ProtonMail/proton-bridge/v3/internal/user" "github.com/ProtonMail/proton-bridge/v3/internal/user"
"github.com/bradenaw/juniper/stream" "github.com/bradenaw/juniper/stream"
"github.com/bradenaw/juniper/xslices" "github.com/bradenaw/juniper/xslices"
@ -76,6 +78,9 @@ func TestBridge_User_RefreshEvent(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) {
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 != platform.WINDOWS {
require.Equal(t, userID, (<-syncCh).UserID)
}
require.Equal(t, userID, (<-syncCh).UserID) require.Equal(t, userID, (<-syncCh).UserID)
closeCh() closeCh()
@ -304,7 +309,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)
@ -312,7 +317,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))
@ -328,6 +333,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.
@ -694,7 +780,7 @@ 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, _ *bridge.Mocks) { withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
@ -745,7 +831,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.

View File

@ -1,4 +1,4 @@
// Copyright (c) 2024 Proton AG // Copyright (c) 2026 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //
@ -43,8 +43,7 @@ func (bridge *Bridge) handleUserEvent(ctx context.Context, user *user.User, even
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)
} }

View File

@ -1,4 +1,4 @@
// Copyright (c) 2024 Proton AG // Copyright (c) 2026 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //
@ -658,7 +658,7 @@ func TestBridge_UserInfo_Alias(t *testing.T) {
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)))
@ -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) 2024 Proton AG // Copyright (c) 2026 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) 2024 Proton AG // Copyright (c) 2026 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) 2024 Proton AG // Copyright (c) 2026 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) 2024 Proton AG // Copyright (c) 2026 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) 2024 Proton AG // Copyright (c) 2026 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) 2024 Proton AG // Copyright (c) 2026 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) 2024 Proton AG // Copyright (c) 2026 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) 2024 Proton AG // Copyright (c) 2026 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) 2024 Proton AG // Copyright (c) 2026 Proton AG
// //
// This file is part of Proton Mail Bridge. // This file is part of Proton Mail Bridge.
// //

View File

@ -1,228 +0,0 @@
// Copyright (c) 2024 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 { //nolint:predeclared
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 uint64) 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 uint64) {
data.DataV1.ClickedLink |= 1 << pos
}
func (data *ConfigurationStatusData) hasLinkClicked(pos uint64) 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(uint64(i)) { //nolint:gosec // disable G115
if !first {
str += ","
} else {
first = false
str += "["
}
str += strconv.Itoa(i)
}
}
if str != "" {
str += "]"
}
return str
}

View File

@ -1,252 +0,0 @@
// Copyright (c) 2024 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) 2024 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) 2024 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,60 +0,0 @@
// Copyright (c) 2024 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() {
return (365 * (now.Year() - prev.Year())) + now.YearDay() - prev.YearDay()
} else if now.YearDay() > prev.YearDay() {
return now.YearDay() - prev.YearDay()
}
return 0
}

View File

@ -1,100 +0,0 @@
// Copyright (c) 2024 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)
}
func TestConfigurationProgress_fed_year_change(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(-1, 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.True(t, (req.Values.NbDay == 370) || (req.Values.NbDay == 371)) // leap year is accounted for in the simplest manner.
require.Equal(t, 2, req.Values.NbDaySinceLast)
}

View File

@ -1,63 +0,0 @@
// Copyright (c) 2024 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) 2024 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) 2024 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) 2024 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) 2024 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) 2024 Proton AG // Copyright (c) 2026 Proton AG
// //
// This file is part of Proton Mail Bridge.Bridge. // This file is part of Proton Mail Bridge.Bridge.
// //
@ -21,6 +21,8 @@ package constants
import ( import (
"fmt" "fmt"
"runtime" "runtime"
"github.com/ProtonMail/proton-bridge/v3/internal/platform"
) )
const VendorName = "protonmail" const VendorName = "protonmail"
@ -72,13 +74,13 @@ const (
// nolint:goconst // nolint:goconst
func getAPIOS() string { func getAPIOS() string {
switch runtime.GOOS { switch runtime.GOOS {
case "darwin": case platform.MACOS:
return "macos" return "macos"
case "linux": case platform.LINUX:
return "linux" return "linux"
case "windows": case platform.WINDOWS:
return "windows" return "windows"
default: default:

View File

@ -1,4 +1,4 @@
// Copyright (c) 2024 Proton AG // Copyright (c) 2026 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) 2024 Proton AG // Copyright (c) 2026 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) 2024 Proton AG // Copyright (c) 2026 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) 2024 Proton AG // Copyright (c) 2026 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) 2024 Proton AG // Copyright (c) 2026 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) 2024 Proton AG // Copyright (c) 2026 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) 2024 Proton AG // Copyright (c) 2026 Proton AG
// //
// This file is part of Proton Mail Bridge.Bridge. // This file is part of Proton Mail Bridge.Bridge.
// //

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