mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-10 12:46:46 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a8caec560e |
41
.github/ISSUE_TEMPLATE/general-issue-template.md
vendored
Normal file
41
.github/ISSUE_TEMPLATE/general-issue-template.md
vendored
Normal file
@ -0,0 +1,41 @@
|
||||
---
|
||||
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 +0,0 @@
|
||||
* inbox-desktop-approvers
|
||||
@ -87,6 +87,7 @@ linters:
|
||||
- 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]
|
||||
- exhaustive # check exhaustiveness of enum switch statements [fast: false, auto-fix: false]
|
||||
- exportloopref # checks for pointers to enclosing loop variables [fast: false, auto-fix: false]
|
||||
- copyloopvar # detects places where loop variables are copied.
|
||||
- forcetypeassert # finds forced type assertions [fast: true, auto-fix: false]
|
||||
- godot # Check if comments end in a period [fast: true, auto-fix: true]
|
||||
|
||||
@ -3,14 +3,14 @@
|
||||
## Prerequisites
|
||||
* 64-bit OS:
|
||||
- the go-rfc5322 module cannot currently be compiled for 32-bit OSes
|
||||
* Go 1.24.0
|
||||
* Go 1.21.9
|
||||
* 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/)
|
||||
* GCC (Linux), msvc (Windows) or Xcode (macOS)
|
||||
* Windres (Windows)
|
||||
* libglvnd and libsecret development files (Linux)
|
||||
* pkg-config (Linux)
|
||||
* cmake, ninja-build and Qt 6.8.2 are required to build the graphical user interface. On Linux,
|
||||
* cmake, ninja-build and Qt 6.4.3 are required to build the graphical user interface. On Linux,
|
||||
the Mesa OpenGL development files are also needed.
|
||||
|
||||
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
|
||||
In order to build Bridge app with Qt interface we are using
|
||||
[Qt 6.8.2](https://doc.qt.io/qt-6/gettingstarted.html).
|
||||
[Qt 6.4.3](https://doc.qt.io/qt-6/gettingstarted.html).
|
||||
|
||||
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
|
||||
|
||||
@ -1,78 +1,10 @@
|
||||
# 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
|
||||
# Contribution Policy
|
||||
|
||||
By making a contribution to this project:
|
||||
|
||||
1. You assign any and all copyright related to the contribution to Proton AG;
|
||||
2. You certify that the contribution was created in whole by you;
|
||||
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.
|
||||
1. I assign any and all copyright related to the contribution to Proton AG;
|
||||
2. I certify that the contribution was created in whole by me;
|
||||
3. I understand and agree that this project and the contribution are public
|
||||
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.
|
||||
|
||||
@ -127,7 +127,6 @@ 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)
|
||||
* [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)
|
||||
* [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)
|
||||
* [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)
|
||||
@ -142,7 +141,6 @@ 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)
|
||||
* [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)
|
||||
* [go-autostart](https://github.com/ElectroNafta/go-autostart) available under [license](https://github.com/ElectroNafta/go-autostart/blob/master/LICENSE)
|
||||
* [go-message](https://github.com/ProtonMail/go-message) available under [license](https://github.com/ProtonMail/go-message/blob/master/LICENSE)
|
||||
* [go-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)
|
||||
|
||||
108
Changelog.md
108
Changelog.md
@ -3,114 +3,6 @@
|
||||
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
|
||||
|
||||
## Kanmon Bridge 3.21.1
|
||||
|
||||
### Changed
|
||||
* BRIDGE-383: Extended internal mailbox conflict resolution logic and minor changes to the mailbox conflict pre-checker.
|
||||
|
||||
|
||||
## Kanmon Bridge 3.21.0
|
||||
|
||||
### Added
|
||||
* BRIDGE-379: Mailbox pre-check on Bridge startup & conflict resolver for Bridge internal mailboxes.
|
||||
|
||||
### Changed
|
||||
* BRIDGE-376: Explicitly catch Gluon DB mailbox name conflicts and report them to Sentry.
|
||||
* BRIDGE-373: Extend user mailbox conflict resolver logging & report sync errors to Sentry.
|
||||
* BRIDGE-366: Kill switch support for IMAP IDLE.
|
||||
* BRIDGE-363: Observability metric support for IMAP connections.
|
||||
|
||||
### Fixed
|
||||
* BRIDGE-377: Correct API label field usage on user label conflict resolver - update handler (event loop).
|
||||
* BRIDGE-378: Fix incorrect field usage for system mailbox names.
|
||||
|
||||
|
||||
## Jubilee Bridge 3.20.1
|
||||
|
||||
### Fixed
|
||||
* BRIDGE-362: Implemented logic for reconciling label conflicts.
|
||||
|
||||
|
||||
## Jubilee Bridge 3.20.0
|
||||
|
||||
### Added
|
||||
* BRIDGE-348: Enable display of BYOE addresses in Bridge.
|
||||
* BRIDGE-340: Added additional logging for label operations and related bad events.
|
||||
* BRIDGE-324: Log a hash of the vault key on Bridge start.
|
||||
|
||||
### Changed
|
||||
* BRIDGE-352: Chore: bump go to 1.24.2.
|
||||
* BRIDGE-353: Chore: update x/net package to 0.38.0.
|
||||
|
||||
### Fixed
|
||||
* BRIDGE-351: Allow draft creation and import to BYOE addresses in combined mode.
|
||||
* BRIDGE-301: Prevent imports into non-BYOE external addresses.
|
||||
* BRIDGE-341: Replaced go-autostart with a fork to support creating autostart shortcuts in directories with Unicode characters on Windows.
|
||||
* BRIDGE-332: Strip newline characters from username and password fields in the Bridge GUI.
|
||||
* BRIDGE-336: Ensure all remote labels are verified and created in Gluon at Bridge startup.
|
||||
* BRIDGE-335: Persist the last successfully used keychain helper as a user preference on Linux.
|
||||
* BRIDGE-333: Ignore unknown label IDs during Bridge synchronization.
|
||||
|
||||
|
||||
## Infinity Bridge 3.19.0
|
||||
|
||||
### Changed
|
||||
* BRIDGE-316: Update Qt to latest LTS version 6.8.2.
|
||||
|
||||
|
||||
## Helix Bridge 3.18.0
|
||||
|
||||
### Changed
|
||||
* BRIDGE-309: Revised update logic and structure.
|
||||
* BRIDGE-154: Added access token to expiry refresh request.
|
||||
|
||||
|
||||
## Grunwald Bridge 3.17.0
|
||||
|
||||
### Added
|
||||
* BRIDGE-271: Report version file check failure to Sentry.
|
||||
* BRIDGE-247: Test: Automate Bridge 0% update rollout.
|
||||
* BRIDGE-248: Test: Additional Bridge UI e2e automation tests.
|
||||
|
||||
### Changed
|
||||
* BRIDGE-73: Update goopenpgp.
|
||||
* BRIDGE-287: Update x/net and x/crypto dependencies.
|
||||
* BRIDGE-303: Update govulncheck to latest release.
|
||||
* BRIDGE-226: Bump Go version to 1.23.4.
|
||||
* BRIDGE-288: Extension to synchronization update handler, observability tweaks and gluon update.
|
||||
|
||||
### Fixed
|
||||
* BRIDGE-291: Use correct field for user plan type.
|
||||
* BRIDGE-143: Add missing QML component attribute, cut/paste disabled on read-only text areas.
|
||||
|
||||
|
||||
## Flavien Bridge 3.16.0
|
||||
|
||||
### Added
|
||||
* BRIDGE-205: Add support for the IMAP AUTHENTICATE command.
|
||||
* BRIDGE-268: Add kill switch feature flag for the IMAP AUTHENTICATE command.
|
||||
* BRIDGE-261: Delete gluon data during user deletion.
|
||||
* BRIDGE-246: Test: Add Settings Menu Bridge UI e2e automation tests.
|
||||
|
||||
### Changed
|
||||
* BRIDGE-107: Improved human verification UX.
|
||||
* BRIDGE-281: Disable keychain test on macOS.
|
||||
* BRIDGE-266: Heartbeat telemetry update.
|
||||
* BRIDGE-253: Removed unused telemetry (activation and troubleshooting).
|
||||
* BRIDGE-252: Restored the -h shortcut for the CLI --help switch.
|
||||
* BRIDGE-264: Ignore apple notes as UserAgent.
|
||||
|
||||
### Fixed
|
||||
* BRIDGE-256: Fix reversed order of headers with multiple values.
|
||||
* BRIDGE-258: Fixed issue with draft updates and sending during synchronization.
|
||||
|
||||
|
||||
## Erasmus Bridge 3.15.1
|
||||
|
||||
### Changed
|
||||
* BRIDGE-281: Disable keychain test on macOS.
|
||||
|
||||
|
||||
## Erasmus Bridge 3.15.0
|
||||
|
||||
### Added
|
||||
|
||||
4
Makefile
4
Makefile
@ -12,7 +12,7 @@ ROOT_DIR:=$(realpath .)
|
||||
.PHONY: build build-gui build-nogui build-launcher versioner hasher
|
||||
|
||||
# Keep version hardcoded so app build works also without Git repository.
|
||||
BRIDGE_APP_VERSION?=3.21.1+git
|
||||
BRIDGE_APP_VERSION?=3.15.0+git
|
||||
APP_VERSION:=${BRIDGE_APP_VERSION}
|
||||
APP_FULL_NAME:=Proton Mail Bridge
|
||||
APP_VENDOR:=Proton AG
|
||||
@ -189,7 +189,7 @@ ${RESOURCE_FILE}: ./dist/info.rc ./dist/${SRC_ICO} .FORCE
|
||||
|
||||
## Dev dependencies
|
||||
.PHONY: install-devel-tools install-linter install-go-mod-outdated install-git-hooks
|
||||
LINTVER:="v1.64.6"
|
||||
LINTVER:="v1.61.0"
|
||||
LINTSRC:="https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh"
|
||||
|
||||
install-dev-dependencies: install-devel-tools install-linter install-go-mod-outdated
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
# Proton Mail Bridge
|
||||
Copyright (c) 2025 Proton AG
|
||||
Copyright (c) 2024 Proton AG
|
||||
|
||||
This repository holds the Proton Mail Bridge application.
|
||||
For a detailed build information see [BUILDS](./BUILDS.md).
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
32
go.mod
32
go.mod
@ -1,16 +1,16 @@
|
||||
module github.com/ProtonMail/proton-bridge/v3
|
||||
|
||||
go 1.24
|
||||
go 1.21
|
||||
|
||||
toolchain go1.24.2
|
||||
toolchain go1.21.9
|
||||
|
||||
require (
|
||||
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
|
||||
github.com/Masterminds/semver/v3 v3.2.0
|
||||
github.com/ProtonMail/gluon v0.17.1-0.20250611120816-05167d499f8d
|
||||
github.com/ProtonMail/gluon v0.17.1-0.20241018144126-31e040c2417e
|
||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20250417134000-e624a080f7ba
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.8.2-proton
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20240918100656-b4860af56d47
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton
|
||||
github.com/PuerkitoBio/goquery v1.8.1
|
||||
github.com/abiosoft/ishell v2.0.0+incompatible
|
||||
github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37
|
||||
@ -30,7 +30,7 @@ require (
|
||||
github.com/go-resty/resty/v2 v2.7.0
|
||||
github.com/godbus/dbus v4.1.0+incompatible
|
||||
github.com/golang/mock v1.6.0
|
||||
github.com/google/go-cmp v0.6.0
|
||||
github.com/google/go-cmp v0.5.9
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/jaytaylor/html2text v0.0.0-20211105163654-bc68cce691ba
|
||||
@ -46,10 +46,10 @@ require (
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.5
|
||||
go.uber.org/goleak v1.2.1
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
|
||||
golang.org/x/net v0.38.0
|
||||
golang.org/x/net v0.24.0
|
||||
golang.org/x/oauth2 v0.7.0
|
||||
golang.org/x/sys v0.31.0
|
||||
golang.org/x/text v0.23.0
|
||||
golang.org/x/sys v0.19.0
|
||||
golang.org/x/text v0.14.0
|
||||
google.golang.org/api v0.114.0
|
||||
google.golang.org/grpc v1.56.3
|
||||
google.golang.org/protobuf v1.33.0
|
||||
@ -60,7 +60,7 @@ require (
|
||||
cloud.google.com/go/compute v1.19.1 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf // indirect
|
||||
github.com/ProtonMail/go-crypto v1.1.4-proton // indirect
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230717121622-edf196117233 // indirect
|
||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect
|
||||
github.com/ProtonMail/go-srp v0.0.7 // indirect
|
||||
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect
|
||||
@ -68,7 +68,7 @@ require (
|
||||
github.com/bytedance/sonic v1.9.1 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/chzyer/test v1.0.0 // indirect
|
||||
github.com/cloudflare/circl v1.5.0 // indirect
|
||||
github.com/cloudflare/circl v1.3.7 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
github.com/cronokirby/saferith v0.33.0 // indirect
|
||||
github.com/cucumber/gherkin-go/v19 v19.0.3 // indirect
|
||||
@ -114,7 +114,6 @@ require (
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||
github.com/stretchr/objx v0.5.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
@ -122,17 +121,16 @@ require (
|
||||
gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/crypto v0.36.0 // indirect
|
||||
golang.org/x/mod v0.17.0 // indirect
|
||||
golang.org/x/sync v0.12.0 // indirect
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
||||
golang.org/x/crypto v0.22.0 // indirect
|
||||
golang.org/x/mod v0.8.0 // indirect
|
||||
golang.org/x/sync v0.3.0 // indirect
|
||||
golang.org/x/tools v0.6.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
replace (
|
||||
github.com/ProtonMail/go-autostart => github.com/ElectroNafta/go-autostart v0.0.0-20250402094843-326608c16033
|
||||
github.com/emersion/go-message => github.com/ProtonMail/go-message v0.13.1-0.20240919135104-3bc88e6a9423
|
||||
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
|
||||
|
||||
74
go.sum
74
go.sum
@ -23,8 +23,6 @@ github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557 h1:l6surSnJ3RP4qA
|
||||
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557/go.mod h1:sTrmvD/TxuypdOERsDOS7SndZg0rzzcCi1b6wQMXUYM=
|
||||
github.com/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/ElectroNafta/go-autostart v0.0.0-20250402094843-326608c16033 h1:d2RB9rQmSusb0K+qSgB+DAY+8i+AXZ/o+oDHj2vAUaA=
|
||||
github.com/ElectroNafta/go-autostart v0.0.0-20250402094843-326608c16033/go.mod h1:o0nKiWcK0e2G/90uL6akWRkzOV4mFcZmvpBPpigJvdw=
|
||||
github.com/Kodeworks/golang-image-ico v0.0.0-20141118225523-73f0f4cfade9/go.mod h1:7uhhqiBaR4CpN0k9rMjOtjpcfGd6DG2m04zQxKnWQ0I=
|
||||
github.com/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=
|
||||
@ -36,23 +34,37 @@ github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAE
|
||||
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/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
|
||||
github.com/ProtonMail/gluon v0.17.1-0.20250611120816-05167d499f8d h1:45W7G+X0w7nzLzeB0eiFkGho5DTK1jNmmNbt3IhN524=
|
||||
github.com/ProtonMail/gluon v0.17.1-0.20250611120816-05167d499f8d/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
|
||||
github.com/ProtonMail/gluon v0.17.1-0.20240923151549-d23b4bec3602 h1:EoMjWlC32tg46L/07hWoiZfLkqJyxVMcsq4Cyn+Ofqc=
|
||||
github.com/ProtonMail/gluon v0.17.1-0.20240923151549-d23b4bec3602/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
|
||||
github.com/ProtonMail/gluon v0.17.1-0.20241002092751-3bbeea9053af h1:iMxTQUg2cB47cXqpMev3cZmQoGBOef3cSUjBbdEl33M=
|
||||
github.com/ProtonMail/gluon v0.17.1-0.20241002092751-3bbeea9053af/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
|
||||
github.com/ProtonMail/gluon v0.17.1-0.20241002111651-173859b80060 h1:dcu3tT84GjoXb++n7crv8UJeG8eRwogjTYdkoJ+MjQI=
|
||||
github.com/ProtonMail/gluon v0.17.1-0.20241002111651-173859b80060/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
|
||||
github.com/ProtonMail/gluon v0.17.1-0.20241002142736-ef4153d156d8 h1:YxPHSJUA87i1hc6s1YrW89++V7HpcR7LSFQ6XM0TsAE=
|
||||
github.com/ProtonMail/gluon v0.17.1-0.20241002142736-ef4153d156d8/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
|
||||
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 v1.1.4-proton h1:KIo9uNlk3vzlwI7o5VjhiEjI4Ld1TDixOMnoNZyfpFE=
|
||||
github.com/ProtonMail/go-crypto v1.1.4-proton/go.mod h1:zNoyBJW3p/yVWiHNZgfTF9VsjwqYof5YY0M9kt2QaX0=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230717121622-edf196117233 h1:bdoKdh0f66/lrgVfYlxw0aqISY/KOqXmFJyGt7rGmnc=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230717121622-edf196117233/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
||||
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-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-proton-api v0.4.1-0.20250417134000-e624a080f7ba h1:DFBngZ7u/f69flRFzPp6Ipo6PKEyflJlA5OCh52yDB4=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20250417134000-e624a080f7ba/go.mod h1:eXIoLyIHxvPo8Kd9e1ygYIrAwbeWJhLi3vgSz2crlK4=
|
||||
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.20240918100656-b4860af56d47/go.mod h1:3A0cpdo0BIenIPjTG6u8EbzJ8uuJy7rVvM/NaynjCKA=
|
||||
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-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
|
||||
github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.8.2-proton h1:MMVgE6nk5Ulh9Ud5L1Xc5iaPKE85FbfKQV17ZMucrR0=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.8.2-proton/go.mod h1:+PjybET6fgcLzldFy1hpy7s8VibZ0T1hLFbxnnMk0lo=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton h1:8tqHYM6IGsdEc6Vxf1TWiwpHNj8yIEQNACPhxsDagrk=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton/go.mod h1:omVkSsfPAhmptzPF/piMXb16wKIWUvVhZbVW7sJKh0A=
|
||||
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
|
||||
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
|
||||
github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw=
|
||||
@ -77,6 +89,7 @@ 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/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.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||
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/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||
@ -94,8 +107,9 @@ github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
|
||||
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
||||
github.com/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.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys=
|
||||
github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
||||
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
|
||||
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/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
@ -233,8 +247,8 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
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/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
@ -496,10 +510,11 @@ 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-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.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.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
|
||||
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
||||
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-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@ -524,9 +539,8 @@ 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.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.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@ -549,14 +563,15 @@ 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-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.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.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.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
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.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
|
||||
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||
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-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@ -571,8 +586,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-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.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
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-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@ -606,15 +621,18 @@ 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-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.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.6.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.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
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.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
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.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
@ -628,12 +646,13 @@ 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.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.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.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.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
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.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||
@ -662,9 +681,8 @@ 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.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.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
@ -97,21 +97,18 @@ const (
|
||||
appShortName = "bridge"
|
||||
)
|
||||
|
||||
// the two flags below have been deprecated by BRIDGE-281. We however keep them so that bridge does not error if they are passed on startup.
|
||||
var cliFlagEnableKeychainTest = &cli.BoolFlag{ //nolint:gochecknoglobals
|
||||
Name: flagEnableKeychainTest,
|
||||
Usage: "This flag is deprecated and does nothing",
|
||||
Usage: "Enable the keychain test for the current and future executions of the application",
|
||||
Value: false,
|
||||
DisableDefaultText: true,
|
||||
Hidden: true,
|
||||
}
|
||||
} //nolint:gochecknoglobals
|
||||
|
||||
var cliFlagDisableKeychainTest = &cli.BoolFlag{ //nolint:gochecknoglobals
|
||||
Name: flagDisableKeychainTest,
|
||||
Usage: "This flag is deprecated and does nothing",
|
||||
Usage: "Disable the keychain test for the current and future executions of the application",
|
||||
Value: false,
|
||||
DisableDefaultText: true,
|
||||
Hidden: true,
|
||||
}
|
||||
|
||||
func New() *cli.App {
|
||||
@ -208,14 +205,12 @@ func New() *cli.App {
|
||||
// We override the default help value because we want "Show" to be capitalized
|
||||
cli.HelpFlag = &cli.BoolFlag{
|
||||
Name: "help",
|
||||
Aliases: []string{"h"},
|
||||
Usage: "Show help",
|
||||
DisableDefaultText: true,
|
||||
}
|
||||
|
||||
if onMacOS() {
|
||||
// The two flags below were introduced for BRIDGE-116, and are available only on macOS.
|
||||
// They have been later removed fro BRIDGE-281.
|
||||
app.Flags = append(app.Flags, cliFlagEnableKeychainTest, cliFlagDisableKeychainTest)
|
||||
}
|
||||
|
||||
@ -287,7 +282,8 @@ func run(c *cli.Context) error {
|
||||
|
||||
return withSingleInstance(settings, locations.GetLockFile(), version, func() error {
|
||||
// Look for available keychains
|
||||
return WithKeychainList(crashHandler, func(keychains *keychain.List) error {
|
||||
skipKeychainTest := checkSkipKeychainTest(c, settings)
|
||||
return WithKeychainList(crashHandler, skipKeychainTest, func(keychains *keychain.List) error {
|
||||
// Unlock the encrypted vault.
|
||||
return WithVault(reporter, locations, keychains, crashHandler, func(v *vault.Vault, insecure, corrupt bool) error {
|
||||
if !v.Migrated() {
|
||||
@ -551,11 +547,11 @@ func withCookieJar(vault *vault.Vault, fn func(http.CookieJar) error) error {
|
||||
}
|
||||
|
||||
// WithKeychainList init the list of usable keychains.
|
||||
func WithKeychainList(panicHandler async.PanicHandler, fn func(*keychain.List) error) error {
|
||||
func WithKeychainList(panicHandler async.PanicHandler, skipKeychainTest bool, fn func(*keychain.List) error) error {
|
||||
logrus.Debug("Creating keychain list")
|
||||
defer logrus.Debug("Keychain list stop")
|
||||
defer async.HandlePanic(panicHandler)
|
||||
return fn(keychain.NewList())
|
||||
return fn(keychain.NewList(skipKeychainTest))
|
||||
}
|
||||
|
||||
func setDeviceCookies(jar *cookies.Jar) error {
|
||||
@ -576,6 +572,38 @@ func setDeviceCookies(jar *cookies.Jar) error {
|
||||
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 {
|
||||
return runtime.GOOS == "darwin"
|
||||
}
|
||||
|
||||
64
internal/app/app_test.go
Normal file
64
internal/app/app_test.go
Normal file
@ -0,0 +1,64 @@
|
||||
// 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))
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
@ -138,7 +138,7 @@ func migrateOldAccounts(locations *locations.Locations, keychains *keychain.List
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get helper: %w", err)
|
||||
}
|
||||
keychain, _, err := keychain.NewKeychain(helper, "bridge", keychains.GetHelpers(), keychains.GetDefaultHelper())
|
||||
keychain, err := keychain.NewKeychain(helper, "bridge", keychains.GetHelpers(), keychains.GetDefaultHelper())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create keychain: %w", err)
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
@ -134,7 +134,7 @@ func TestKeychainMigration(t *testing.T) {
|
||||
func TestUserMigration(t *testing.T) {
|
||||
kcl := keychain.NewTestKeychainsList()
|
||||
|
||||
kc, _, err := keychain.NewKeychain("mock", "bridge", kcl.GetHelpers(), kcl.GetDefaultHelper())
|
||||
kc, err := keychain.NewKeychain("mock", "bridge", kcl.GetHelpers(), kcl.GetDefaultHelper())
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, kc.Put("brokenID", "broken"))
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
@ -18,8 +18,6 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"path"
|
||||
|
||||
@ -69,12 +67,11 @@ func newVault(reporter *sentry.Reporter, locations *locations.Locations, keychai
|
||||
logrus.WithField("vaultDir", vaultDir).Debug("Loading vault from directory")
|
||||
|
||||
var (
|
||||
vaultKey []byte
|
||||
insecure bool
|
||||
lastUsedHelper string
|
||||
vaultKey []byte
|
||||
insecure bool
|
||||
)
|
||||
|
||||
if key, helper, err := loadVaultKey(vaultDir, keychains); err != nil {
|
||||
if key, err := loadVaultKey(vaultDir, keychains); err != nil {
|
||||
if reporter != nil {
|
||||
if rerr := reporter.ReportMessageWithContext("Could not load/create vault key", map[string]any{
|
||||
"keychainDefaultHelper": keychains.GetDefaultHelper(),
|
||||
@ -92,8 +89,6 @@ func newVault(reporter *sentry.Reporter, locations *locations.Locations, keychai
|
||||
vaultDir = path.Join(vaultDir, "insecure")
|
||||
} else {
|
||||
vaultKey = key
|
||||
lastUsedHelper = helper
|
||||
logHashedVaultKey(vaultKey) // Log a hash of the vault key.
|
||||
}
|
||||
|
||||
gluonCacheDir, err := locations.ProvideGluonCachePath()
|
||||
@ -101,47 +96,34 @@ func newVault(reporter *sentry.Reporter, locations *locations.Locations, keychai
|
||||
return nil, false, nil, fmt.Errorf("could not provide gluon path: %w", err)
|
||||
}
|
||||
|
||||
userVault, corrupt, err := vault.New(vaultDir, gluonCacheDir, vaultKey, panicHandler)
|
||||
vault, corrupt, err := vault.New(vaultDir, gluonCacheDir, vaultKey, panicHandler)
|
||||
if err != nil {
|
||||
return nil, false, corrupt, fmt.Errorf("could not create vault: %w", err)
|
||||
}
|
||||
|
||||
// Remember the last successfully used keychain and store that as the user preference.
|
||||
if err := vault.SetHelper(vaultDir, lastUsedHelper); err != nil {
|
||||
logrus.WithError(err).Error("Could not store last used keychain helper")
|
||||
}
|
||||
|
||||
return userVault, insecure, corrupt, nil
|
||||
return vault, insecure, corrupt, nil
|
||||
}
|
||||
|
||||
// loadVaultKey - loads the key used to encrypt the vault alongside the keychain helper used to access it.
|
||||
func loadVaultKey(vaultDir string, keychains *keychain.List) (key []byte, keychainHelper string, err error) {
|
||||
keychainHelper, err = vault.GetHelper(vaultDir)
|
||||
func loadVaultKey(vaultDir string, keychains *keychain.List) ([]byte, error) {
|
||||
helper, err := vault.GetHelper(vaultDir)
|
||||
if err != nil {
|
||||
return nil, keychainHelper, fmt.Errorf("could not get keychain helper: %w", err)
|
||||
return nil, fmt.Errorf("could not get keychain helper: %w", err)
|
||||
}
|
||||
|
||||
kc, keychainHelper, err := keychain.NewKeychain(keychainHelper, constants.KeyChainName, keychains.GetHelpers(), keychains.GetDefaultHelper())
|
||||
kc, err := keychain.NewKeychain(helper, constants.KeyChainName, keychains.GetHelpers(), keychains.GetDefaultHelper())
|
||||
if err != nil {
|
||||
return nil, keychainHelper, fmt.Errorf("could not create keychain: %w", err)
|
||||
return nil, fmt.Errorf("could not create keychain: %w", err)
|
||||
}
|
||||
|
||||
key, err = vault.GetVaultKey(kc)
|
||||
key, err := vault.GetVaultKey(kc)
|
||||
if err != nil {
|
||||
if keychain.IsErrKeychainNoItem(err) {
|
||||
logrus.WithError(err).Warn("no vault key found, generating new")
|
||||
key, err := vault.NewVaultKey(kc)
|
||||
return key, keychainHelper, err
|
||||
return vault.NewVaultKey(kc)
|
||||
}
|
||||
|
||||
return nil, keychainHelper, fmt.Errorf("could not check for vault key: %w", err)
|
||||
return nil, fmt.Errorf("could not check for vault key: %w", err)
|
||||
}
|
||||
|
||||
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")
|
||||
return key, nil
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
@ -50,12 +50,10 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/telemetry"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/unleash"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
|
||||
"github.com/bradenaw/juniper/xslices"
|
||||
"github.com/elastic/go-sysinfo/types"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
@ -82,9 +80,8 @@ type Bridge struct {
|
||||
imapEventCh chan imapEvents.Event
|
||||
|
||||
// updater is the bridge's updater.
|
||||
updater Updater
|
||||
installChLegacy chan installJobLegacy
|
||||
installCh chan installJob
|
||||
updater Updater
|
||||
installCh chan installJob
|
||||
|
||||
// heartbeat is the telemetry heartbeat for metrics.
|
||||
heartbeat *heartBeatState
|
||||
@ -151,9 +148,6 @@ type Bridge struct {
|
||||
|
||||
// notificationStore is used for notification deduplication
|
||||
notificationStore *notifications.Store
|
||||
|
||||
// getHostVersion primarily used for testing the update logic - it should return an OS version
|
||||
getHostVersion func(host types.Host) string
|
||||
}
|
||||
|
||||
var logPkg = logrus.WithField("pkg", "bridge") //nolint:gochecknoglobals
|
||||
@ -288,9 +282,8 @@ func newBridge(
|
||||
tlsConfig: tlsConfig,
|
||||
imapEventCh: imapEventCh,
|
||||
|
||||
updater: updater,
|
||||
installChLegacy: make(chan installJobLegacy),
|
||||
installCh: make(chan installJob),
|
||||
updater: updater,
|
||||
installCh: make(chan installJob),
|
||||
|
||||
curVersion: curVersion,
|
||||
newVersion: curVersion,
|
||||
@ -322,8 +315,6 @@ func newBridge(
|
||||
observabilityService: observabilityService,
|
||||
|
||||
notificationStore: notifications.NewStore(locator.ProvideNotificationsCachePath),
|
||||
|
||||
getHostVersion: func(host types.Host) string { return host.Info().OS.Version },
|
||||
}
|
||||
|
||||
bridge.serverManager = imapsmtpserver.NewService(context.Background(),
|
||||
@ -335,7 +326,6 @@ func newBridge(
|
||||
uidValidityGenerator,
|
||||
&bridgeIMAPSMTPTelemetry{b: bridge},
|
||||
observabilityService,
|
||||
unleashService,
|
||||
)
|
||||
|
||||
// Check whether username has changed and correct (macOS only)
|
||||
@ -445,46 +435,17 @@ func (bridge *Bridge) init(tlsReporter TLSReporter) error {
|
||||
// Check for updates when triggered.
|
||||
bridge.goUpdate = bridge.tasks.PeriodicOrTrigger(constants.UpdateCheckInterval, 0, func(ctx context.Context) {
|
||||
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 {
|
||||
bridge.publish(events.UpdateCheckFailed{Error: err})
|
||||
if errors.Is(err, updater.ErrVersionFileDownloadOrVerify) {
|
||||
logPkg.WithError(err).Error("Cannot download or verify the version file")
|
||||
if reporterErr := bridge.reporter.ReportMessageWithContext(
|
||||
"Cannot download or verify the version file",
|
||||
reporter.Context{"error": err},
|
||||
); reporterErr != nil {
|
||||
logPkg.WithError(reporterErr).Error("Failed to report version file check error")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if useOldUpdateLogic {
|
||||
bridge.handleUpdateLegacy(versionLegacy)
|
||||
} else {
|
||||
bridge.handleUpdate(version)
|
||||
}
|
||||
bridge.handleUpdate(version)
|
||||
}
|
||||
})
|
||||
defer bridge.goUpdate()
|
||||
|
||||
// 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
|
||||
// Install updates when available.
|
||||
bridge.tasks.Once(func(ctx context.Context) {
|
||||
async.RangeContext(ctx, bridge.installCh, func(job installJob) {
|
||||
bridge.installUpdate(ctx, job)
|
||||
@ -675,6 +636,14 @@ func loadTLSConfig(vault *vault.Vault) (*tls.Config, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func min(a, b time.Duration) time.Duration { //nolint:predeclared
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func (bridge *Bridge) HasAPIConnection() bool {
|
||||
return bridge.api.GetStatus() == proton.StatusUp
|
||||
}
|
||||
@ -721,13 +690,13 @@ func (bridge *Bridge) verifyUsernameChange() {
|
||||
func GetUpdatedCachePath(gluonDBPath, gluonCachePath string) string {
|
||||
// If gluon cache is moved to an external drive; regex find will fail; as is expected
|
||||
cachePathMatches := usernameChangeRegex.FindStringSubmatch(gluonCachePath)
|
||||
if len(cachePathMatches) < 2 {
|
||||
if cachePathMatches == nil || len(cachePathMatches) < 2 {
|
||||
return ""
|
||||
}
|
||||
|
||||
cacheUsername := cachePathMatches[1]
|
||||
dbPathMatches := usernameChangeRegex.FindStringSubmatch(gluonDBPath)
|
||||
if len(dbPathMatches) < 2 {
|
||||
if dbPathMatches == nil || len(dbPathMatches) < 2 {
|
||||
return ""
|
||||
}
|
||||
|
||||
@ -747,41 +716,10 @@ func (bridge *Bridge) PushObservabilityMetric(metric proton.ObservabilityMetric)
|
||||
bridge.observabilityService.AddMetrics(metric)
|
||||
}
|
||||
|
||||
func (bridge *Bridge) PushDistinctObservabilityMetrics(errType observability.DistinctionMetricTypeEnum, metrics ...proton.ObservabilityMetric) {
|
||||
func (bridge *Bridge) PushDistinctObservabilityMetrics(errType observability.DistinctionErrorTypeEnum, metrics ...proton.ObservabilityMetric) {
|
||||
bridge.observabilityService.AddDistinctMetrics(errType, metrics...)
|
||||
}
|
||||
|
||||
func (bridge *Bridge) ModifyObservabilityHeartbeatInterval(duration time.Duration) {
|
||||
bridge.observabilityService.ModifyHeartbeatInterval(duration)
|
||||
}
|
||||
|
||||
func (bridge *Bridge) ReportMessageWithContext(message string, messageCtx reporter.Context) {
|
||||
if err := bridge.reporter.ReportMessageWithContext(message, messageCtx); err != nil {
|
||||
logPkg.WithFields(logrus.Fields{
|
||||
"err": err,
|
||||
"sentryMessage": message,
|
||||
"messageCtx": messageCtx,
|
||||
}).Info("Error occurred when sending Report to Sentry")
|
||||
}
|
||||
}
|
||||
|
||||
// GetUsers is only used for testing purposes.
|
||||
func (bridge *Bridge) GetUsers() map[string]*user.User {
|
||||
return bridge.users
|
||||
}
|
||||
|
||||
// SetCurrentVersionTest - sets the current version of bridge; should only be used for tests.
|
||||
func (bridge *Bridge) SetCurrentVersionTest(version *semver.Version) {
|
||||
bridge.curVersion = version
|
||||
bridge.newVersion = version
|
||||
}
|
||||
|
||||
// SetHostVersionGetterTest - sets the OS version helper func; only used for testing.
|
||||
func (bridge *Bridge) SetHostVersionGetterTest(fn func(host types.Host) string) {
|
||||
bridge.getHostVersion = fn
|
||||
}
|
||||
|
||||
// SetRolloutPercentageTest - sets the rollout percentage; should only be used for testing.
|
||||
func (bridge *Bridge) SetRolloutPercentageTest(rollout float64) error {
|
||||
return bridge.vault.SetUpdateRollout(rollout)
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
@ -45,7 +45,6 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/focus"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapsmtpserver"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/unleash"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/user"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
|
||||
@ -384,14 +383,9 @@ func TestBridge_Cookies(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_CheckUpdate_Legacy(t *testing.T) {
|
||||
func TestBridge_CheckUpdate(t *testing.T) {
|
||||
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) {
|
||||
// Wait for FF poll.
|
||||
time.Sleep(600 * time.Millisecond)
|
||||
// Disable autoupdate for this test.
|
||||
require.NoError(t, bridge.SetAutoUpdate(false))
|
||||
|
||||
@ -406,7 +400,7 @@ func TestBridge_CheckUpdate_Legacy(t *testing.T) {
|
||||
require.Equal(t, events.UpdateNotAvailable{}, <-noUpdateCh)
|
||||
|
||||
// Simulate a new version being available.
|
||||
mocks.Updater.SetLatestVersionLegacy(v2_4_0, v2_3_0)
|
||||
mocks.Updater.SetLatestVersion(v2_4_0, v2_3_0)
|
||||
|
||||
// Get a stream of update available events.
|
||||
updateCh, done := bridge.GetEvents(events.UpdateAvailable{})
|
||||
@ -417,7 +411,7 @@ func TestBridge_CheckUpdate_Legacy(t *testing.T) {
|
||||
|
||||
// We should receive an event indicating that an update is available.
|
||||
require.Equal(t, events.UpdateAvailable{
|
||||
VersionLegacy: updater.VersionInfoLegacy{
|
||||
Version: updater.VersionInfo{
|
||||
Version: v2_4_0,
|
||||
MinAuto: v2_3_0,
|
||||
RolloutProportion: 1.0,
|
||||
@ -429,30 +423,25 @@ func TestBridge_CheckUpdate_Legacy(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_AutoUpdate_Legacy(t *testing.T) {
|
||||
func TestBridge_AutoUpdate(t *testing.T) {
|
||||
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(b *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Wait for FF poll.
|
||||
time.Sleep(600 * time.Millisecond)
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
// Enable autoupdate for this test.
|
||||
require.NoError(t, b.SetAutoUpdate(true))
|
||||
require.NoError(t, bridge.SetAutoUpdate(true))
|
||||
|
||||
// Get a stream of update events.
|
||||
updateCh, done := b.GetEvents(events.UpdateInstalled{})
|
||||
updateCh, done := bridge.GetEvents(events.UpdateInstalled{})
|
||||
defer done()
|
||||
|
||||
// Simulate a new version being available.
|
||||
mocks.Updater.SetLatestVersionLegacy(v2_4_0, v2_3_0)
|
||||
mocks.Updater.SetLatestVersion(v2_4_0, v2_3_0)
|
||||
|
||||
// Check for updates.
|
||||
b.CheckForUpdates()
|
||||
bridge.CheckForUpdates()
|
||||
|
||||
// We should receive an event indicating that the update was silently installed.
|
||||
require.Equal(t, events.UpdateInstalled{
|
||||
VersionLegacy: updater.VersionInfoLegacy{
|
||||
Version: updater.VersionInfo{
|
||||
Version: v2_4_0,
|
||||
MinAuto: v2_3_0,
|
||||
RolloutProportion: 1.0,
|
||||
@ -463,14 +452,9 @@ func TestBridge_AutoUpdate_Legacy(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_ManualUpdate_Legacy(t *testing.T) {
|
||||
func TestBridge_ManualUpdate(t *testing.T) {
|
||||
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) {
|
||||
// Wait for FF poll.
|
||||
time.Sleep(600 * time.Millisecond)
|
||||
// Disable autoupdate for this test.
|
||||
require.NoError(t, bridge.SetAutoUpdate(false))
|
||||
|
||||
@ -479,14 +463,14 @@ func TestBridge_ManualUpdate_Legacy(t *testing.T) {
|
||||
defer done()
|
||||
|
||||
// Simulate a new version being available, but it's too new for us.
|
||||
mocks.Updater.SetLatestVersionLegacy(v2_4_0, v2_4_0)
|
||||
mocks.Updater.SetLatestVersion(v2_4_0, v2_4_0)
|
||||
|
||||
// Check for updates.
|
||||
bridge.CheckForUpdates()
|
||||
|
||||
// We should receive an event indicating an update is available, but we can't install it.
|
||||
require.Equal(t, events.UpdateAvailable{
|
||||
VersionLegacy: updater.VersionInfoLegacy{
|
||||
Version: updater.VersionInfo{
|
||||
Version: v2_4_0,
|
||||
MinAuto: v2_4_0,
|
||||
RolloutProportion: 1.0,
|
||||
@ -500,12 +484,7 @@ func TestBridge_ManualUpdate_Legacy(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) {
|
||||
unleash.ModifyPollPeriodAndJitter(500*time.Millisecond, 0)
|
||||
s.PushFeatureFlag(unleash.UpdateUseNewVersionFileStructureDisabled)
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
// Wait for FF poll.
|
||||
time.Sleep(600 * time.Millisecond)
|
||||
// Get a stream of update events.
|
||||
updateCh, done := bridge.GetEvents(events.UpdateForced{})
|
||||
defer done()
|
||||
@ -618,7 +597,7 @@ func TestBridge_AddressWithoutKeys(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create an additional address for the user; it will not have keys.
|
||||
aliasAddrID, err := s.CreateAddress(userID, "alias@pm.me", []byte("password"), true)
|
||||
aliasAddrID, err := s.CreateAddress(userID, "alias@pm.me", []byte("password"))
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create an API client so we can remove the address keys.
|
||||
@ -785,7 +764,7 @@ func TestBridge_ChangeAddressOrder(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a second address for the user.
|
||||
aliasID, err := s.CreateAddress(userID, "alias@"+s.GetDomain(), password, true)
|
||||
aliasID, err := s.CreateAddress(userID, "alias@"+s.GetDomain(), password)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create 10 messages for the user.
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
@ -25,6 +25,7 @@ import (
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
)
|
||||
|
||||
@ -79,6 +80,12 @@ func (bridge *Bridge) ReportBug(ctx context.Context, report *ReportBugReq) error
|
||||
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
|
||||
for i, att := range attachments {
|
||||
if i == 0 && report.IncludeLogs {
|
||||
|
||||
46
internal/bridge/config_status.go
Normal file
46
internal/bridge/config_status.go
Normal file
@ -0,0 +1,46 @@
|
||||
// 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)
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// 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 {
|
||||
if user.GetAddressMode() == vault.SplitMode {
|
||||
splitMode = true
|
||||
break
|
||||
}
|
||||
h.SetUserPlan(user.GetUserPlanName())
|
||||
}
|
||||
var numberConnectedAccounts = len(bridge.users)
|
||||
h.SetNumberConnectedAccounts(numberConnectedAccounts)
|
||||
var nbAccount = len(bridge.users)
|
||||
h.SetNbAccount(nbAccount)
|
||||
h.SetSplitMode(splitMode)
|
||||
|
||||
// Do not try to send if there is no user yet.
|
||||
if numberConnectedAccounts > 0 {
|
||||
if nbAccount > 0 {
|
||||
defer h.start()
|
||||
}
|
||||
}, bridge.usersLock)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
@ -17,9 +17,7 @@
|
||||
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
import "github.com/sirupsen/logrus"
|
||||
|
||||
func (bridge *Bridge) GetCurrentUserAgent() string {
|
||||
return bridge.identifier.GetUserAgent()
|
||||
@ -32,8 +30,6 @@ func (bridge *Bridge) SetCurrentPlatform(platform string) {
|
||||
func (bridge *Bridge) setUserAgent(name, version string) {
|
||||
currentUserAgent := bridge.identifier.GetClientString()
|
||||
|
||||
bridge.heartbeat.SetContactedByAppleNotes(name)
|
||||
|
||||
bridge.identifier.SetClient(name, version)
|
||||
|
||||
newUserAgent := bridge.identifier.GetClientString()
|
||||
@ -58,7 +54,6 @@ func (b *bridgeUserAgentUpdater) HasClient() bool {
|
||||
}
|
||||
|
||||
func (b *bridgeUserAgentUpdater) SetClient(name, version string) {
|
||||
b.heartbeat.SetContactedByAppleNotes(name)
|
||||
b.identifier.SetClient(name, version)
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
@ -26,7 +26,6 @@ import (
|
||||
imapEvents "github.com/ProtonMail/gluon/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/unleash"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/useragent"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
@ -94,10 +93,6 @@ func (b *bridgeIMAPSettings) LogServer() bool {
|
||||
return b.b.logIMAPServer
|
||||
}
|
||||
|
||||
func (b *bridgeIMAPSettings) DisableIMAPAuthenticate() bool {
|
||||
return b.b.unleashService.GetFlagValue(unleash.IMAPAuthenticateCommandDisabled)
|
||||
}
|
||||
|
||||
func (b *bridgeIMAPSettings) Port() int {
|
||||
return b.b.vault.GetIMAPPort()
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -54,9 +54,6 @@ func NewMocks(tb testing.TB, version, minAuto *semver.Version) *Mocks {
|
||||
mocks.Heartbeat.EXPECT().IsTelemetryAvailable(gomock.Any()).AnyTimes()
|
||||
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
|
||||
}
|
||||
|
||||
@ -122,14 +119,13 @@ func (provider *TestLocationsProvider) UserCache() string {
|
||||
}
|
||||
|
||||
type TestUpdater struct {
|
||||
latest updater.VersionInfoLegacy
|
||||
releases updater.VersionInfo
|
||||
lock sync.RWMutex
|
||||
latest updater.VersionInfo
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
func NewTestUpdater(version, minAuto *semver.Version) *TestUpdater {
|
||||
return &TestUpdater{
|
||||
latest: updater.VersionInfoLegacy{
|
||||
latest: updater.VersionInfo{
|
||||
Version: version,
|
||||
MinAuto: minAuto,
|
||||
|
||||
@ -138,11 +134,11 @@ func NewTestUpdater(version, minAuto *semver.Version) *TestUpdater {
|
||||
}
|
||||
}
|
||||
|
||||
func (testUpdater *TestUpdater) SetLatestVersionLegacy(version, minAuto *semver.Version) {
|
||||
func (testUpdater *TestUpdater) SetLatestVersion(version, minAuto *semver.Version) {
|
||||
testUpdater.lock.Lock()
|
||||
defer testUpdater.lock.Unlock()
|
||||
|
||||
testUpdater.latest = updater.VersionInfoLegacy{
|
||||
testUpdater.latest = updater.VersionInfo{
|
||||
Version: version,
|
||||
MinAuto: minAuto,
|
||||
|
||||
@ -150,35 +146,17 @@ func (testUpdater *TestUpdater) SetLatestVersionLegacy(version, minAuto *semver.
|
||||
}
|
||||
}
|
||||
|
||||
func (testUpdater *TestUpdater) GetVersionInfoLegacy(_ context.Context, _ updater.Downloader, _ updater.Channel) (updater.VersionInfoLegacy, error) {
|
||||
func (testUpdater *TestUpdater) GetVersionInfo(_ context.Context, _ updater.Downloader, _ updater.Channel) (updater.VersionInfo, error) {
|
||||
testUpdater.lock.RLock()
|
||||
defer testUpdater.lock.RUnlock()
|
||||
|
||||
return testUpdater.latest, nil
|
||||
}
|
||||
|
||||
func (testUpdater *TestUpdater) InstallUpdateLegacy(_ context.Context, _ updater.Downloader, _ updater.VersionInfoLegacy) error {
|
||||
func (testUpdater *TestUpdater) InstallUpdate(_ context.Context, _ updater.Downloader, _ updater.VersionInfo) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (testUpdater *TestUpdater) RemoveOldUpdates() error {
|
||||
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
|
||||
}
|
||||
|
||||
@ -88,18 +88,3 @@ func (mr *MockReporterMockRecorder) ReportMessageWithContext(arg0, arg1 interfac
|
||||
mr.mock.ctrl.T.Helper()
|
||||
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)
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -25,7 +25,7 @@ func NewMockObservabilitySender(ctrl *gomock.Controller) *MockObservabilitySende
|
||||
|
||||
func (m *MockObservabilitySender) EXPECT() *MockObservabilitySenderRecorder { return m.recorder }
|
||||
|
||||
func (m *MockObservabilitySender) AddDistinctMetrics(errType observability.DistinctionMetricTypeEnum, _ ...proton.ObservabilityMetric) {
|
||||
func (m *MockObservabilitySender) AddDistinctMetrics(errType observability.DistinctionErrorTypeEnum, _ ...proton.ObservabilityMetric) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "AddDistinctMetrics", errType)
|
||||
}
|
||||
@ -35,18 +35,7 @@ func (m *MockObservabilitySender) AddMetrics(metrics ...proton.ObservabilityMetr
|
||||
m.ctrl.Call(m, "AddMetrics", metrics)
|
||||
}
|
||||
|
||||
func (m *MockObservabilitySender) AddTimeLimitedMetric(metricType observability.DistinctionMetricTypeEnum, metric proton.ObservabilityMetric) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "AddTimeLimitedMetric", metricType, metric)
|
||||
}
|
||||
|
||||
func (m *MockObservabilitySender) GetEmailClient() string {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "GetEmailClient")
|
||||
return ""
|
||||
}
|
||||
|
||||
func (mr *MockObservabilitySenderRecorder) AddDistinctMetrics(errType observability.DistinctionMetricTypeEnum, _ ...proton.ObservabilityMetric) *gomock.Call {
|
||||
func (mr *MockObservabilitySenderRecorder) AddDistinctMetrics(errType observability.DistinctionErrorTypeEnum, _ ...proton.ObservabilityMetric) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock,
|
||||
"AddDistinctMetrics",
|
||||
@ -58,13 +47,3 @@ func (mr *MockObservabilitySenderRecorder) AddMetrics(metrics ...proton.Observab
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddMetrics", reflect.TypeOf((*MockObservabilitySender)(nil).AddMetrics), metrics)
|
||||
}
|
||||
|
||||
func (mr *MockObservabilitySenderRecorder) AddTimeLimitedMetric(metricType observability.DistinctionMetricTypeEnum, metric proton.ObservabilityMetric) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddTimeLimitedMetric", reflect.TypeOf((*MockObservabilitySender)(nil).AddTimeLimitedMetric), metricType, metric)
|
||||
}
|
||||
|
||||
func (mr *MockObservabilitySenderRecorder) GetEmailClient() {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
mr.mock.ctrl.Call(mr.mock, "GetEmailClient", reflect.TypeOf((*MockObservabilitySender)(nil).GetEmailClient))
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// 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) {
|
||||
userMetricPeriod := time.Millisecond * 600
|
||||
userMetricPeriod := time.Millisecond * 200
|
||||
heartbeatPeriod := time.Second * 10
|
||||
throttlePeriod := time.Millisecond * 300
|
||||
throttlePeriod := time.Millisecond * 100
|
||||
observability.ModifyUserMetricInterval(userMetricPeriod)
|
||||
observability.ModifyThrottlePeriod(throttlePeriod)
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
@ -318,10 +318,11 @@ 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,
|
||||
// which we need at next startup to decrypt the vault.
|
||||
func (bridge *Bridge) FactoryReset(ctx context.Context) {
|
||||
useTelemetry := !bridge.GetTelemetryDisabled()
|
||||
// Delete all the users.
|
||||
safe.Lock(func() {
|
||||
for _, user := range bridge.users {
|
||||
bridge.logoutUser(ctx, user, true, true)
|
||||
bridge.logoutUser(ctx, user, true, true, useTelemetry)
|
||||
}
|
||||
}, bridge.usersLock)
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
@ -355,7 +355,7 @@ func TestBridge_CanProcessEventsDuringSync(t *testing.T) {
|
||||
|
||||
// Create a new address
|
||||
newAddress := "foo@proton.ch"
|
||||
addrID, err := s.CreateAddress(userID, newAddress, password, true)
|
||||
addrID, err := s.CreateAddress(userID, newAddress, password)
|
||||
require.NoError(t, err)
|
||||
|
||||
event := <-addressCreatedCh
|
||||
@ -430,7 +430,7 @@ func TestBridge_EventReplayAfterSyncHasFinished(t *testing.T) {
|
||||
createNumMessages(ctx, t, c, addrID, labelID, numMsg)
|
||||
})
|
||||
|
||||
addrID1, err := s.CreateAddress(userID, "foo@proton.ch", password, true)
|
||||
addrID1, err := s.CreateAddress(userID, "foo@proton.ch", password)
|
||||
require.NoError(t, err)
|
||||
|
||||
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.
|
||||
addrID2, err := s.CreateAddress(userID, "bar@proton.ch", password, true)
|
||||
addrID2, err := s.CreateAddress(userID, "bar@proton.ch", password)
|
||||
require.NoError(t, err)
|
||||
|
||||
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.
|
||||
addrID, err := s.CreateAddress(userID, "bar@proton.ch", password, true)
|
||||
addrID, err := s.CreateAddress(userID, "bar@proton.ch", password)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 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, info.Addresses[0], "user@proton.local")
|
||||
|
||||
addrID2, err := s.CreateAddress(userID, "foo@"+s.GetDomain(), password, true)
|
||||
addrID2, err := s.CreateAddress(userID, "foo@"+s.GetDomain(), password)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, s.SetAddressOrder(userID, []string{addrID2, addrID}))
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
@ -28,6 +28,7 @@ type Locator interface {
|
||||
ProvideLogsPath() (string, error)
|
||||
ProvideGluonCachePath() (string, error)
|
||||
ProvideGluonDataPath() (string, error)
|
||||
ProvideStatsPath() (string, error)
|
||||
GetLicenseFilePath() string
|
||||
GetDependencyLicensesLink() string
|
||||
Clear(...string) error
|
||||
@ -52,9 +53,7 @@ type Autostarter interface {
|
||||
}
|
||||
|
||||
type Updater interface {
|
||||
GetVersionInfoLegacy(context.Context, updater.Downloader, updater.Channel) (updater.VersionInfoLegacy, error)
|
||||
InstallUpdateLegacy(context.Context, updater.Downloader, updater.VersionInfoLegacy) error
|
||||
GetVersionInfo(context.Context, updater.Downloader, updater.Channel) (updater.VersionInfo, error)
|
||||
InstallUpdate(context.Context, updater.Downloader, updater.VersionInfo) error
|
||||
RemoveOldUpdates() error
|
||||
GetVersionInfo(context.Context, updater.Downloader) (updater.VersionInfo, error)
|
||||
InstallUpdate(context.Context, updater.Downloader, updater.Release) error
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
@ -21,168 +21,22 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/gluon/reporter"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
||||
"github.com/elastic/go-sysinfo"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
func (bridge *Bridge) CheckForUpdates() {
|
||||
bridge.goUpdate()
|
||||
}
|
||||
|
||||
func (bridge *Bridge) InstallUpdateLegacy(version updater.VersionInfoLegacy) {
|
||||
bridge.installChLegacy <- installJobLegacy{version: version, silent: false}
|
||||
}
|
||||
|
||||
func (bridge *Bridge) InstallUpdate(release updater.Release) {
|
||||
bridge.installCh <- installJob{Release: release, Silent: false}
|
||||
func (bridge *Bridge) InstallUpdate(version updater.VersionInfo) {
|
||||
bridge.installCh <- installJob{version: version, silent: false}
|
||||
}
|
||||
|
||||
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{
|
||||
"version": version.Version,
|
||||
"current": bridge.curVersion,
|
||||
@ -190,7 +44,7 @@ func (bridge *Bridge) handleUpdateLegacy(version updater.VersionInfoLegacy) {
|
||||
})
|
||||
|
||||
bridge.publish(events.UpdateLatest{
|
||||
VersionLegacy: version,
|
||||
Version: version,
|
||||
})
|
||||
|
||||
switch {
|
||||
@ -208,33 +62,33 @@ func (bridge *Bridge) handleUpdateLegacy(version updater.VersionInfoLegacy) {
|
||||
log.Info("An update is available but is incompatible with this version")
|
||||
|
||||
bridge.publish(events.UpdateAvailable{
|
||||
VersionLegacy: version,
|
||||
Compatible: false,
|
||||
Silent: false,
|
||||
Version: version,
|
||||
Compatible: false,
|
||||
Silent: false,
|
||||
})
|
||||
|
||||
case !bridge.vault.GetAutoUpdate():
|
||||
log.Info("An update is available but auto-update is disabled")
|
||||
|
||||
bridge.publish(events.UpdateAvailable{
|
||||
VersionLegacy: version,
|
||||
Compatible: true,
|
||||
Silent: false,
|
||||
Version: version,
|
||||
Compatible: true,
|
||||
Silent: false,
|
||||
})
|
||||
|
||||
default:
|
||||
safe.RLock(func() {
|
||||
bridge.installChLegacy <- installJobLegacy{version: version, silent: true}
|
||||
bridge.installCh <- installJob{version: version, silent: true}
|
||||
}, bridge.newVersionLock)
|
||||
}
|
||||
}
|
||||
|
||||
type installJobLegacy struct {
|
||||
version updater.VersionInfoLegacy
|
||||
type installJob struct {
|
||||
version updater.VersionInfo
|
||||
silent bool
|
||||
}
|
||||
|
||||
func (bridge *Bridge) installUpdateLegacy(ctx context.Context, job installJobLegacy) {
|
||||
func (bridge *Bridge) installUpdate(ctx context.Context, job installJob) {
|
||||
safe.Lock(func() {
|
||||
log := logrus.WithFields(logrus.Fields{
|
||||
"version": job.version.Version,
|
||||
@ -249,85 +103,19 @@ func (bridge *Bridge) installUpdateLegacy(ctx context.Context, job installJobLeg
|
||||
log.WithField("silent", job.silent).Info("An update is available")
|
||||
|
||||
bridge.publish(events.UpdateAvailable{
|
||||
VersionLegacy: job.version,
|
||||
Compatible: true,
|
||||
Silent: job.silent,
|
||||
})
|
||||
|
||||
err := bridge.updater.InstallUpdateLegacy(ctx, bridge.api, job.version)
|
||||
|
||||
switch {
|
||||
case errors.Is(err, updater.ErrDownloadVerify):
|
||||
// BRIDGE-207: if download or verification fails, we do not want to trigger a manual update. We report in the log and to Sentry
|
||||
// and we fail silently.
|
||||
log.WithError(err).Error("The update could not be installed, but we will fail silently")
|
||||
if reporterErr := bridge.reporter.ReportMessageWithContext(
|
||||
"Cannot download or verify update",
|
||||
reporter.Context{"error": err},
|
||||
); reporterErr != nil {
|
||||
log.WithError(reporterErr).Error("Failed to report update error")
|
||||
}
|
||||
|
||||
case errors.Is(err, updater.ErrUpdateAlreadyInstalled):
|
||||
log.Info("The update was already installed")
|
||||
|
||||
case err != nil:
|
||||
log.WithError(err).Error("The update could not be installed")
|
||||
|
||||
bridge.publish(events.UpdateFailed{
|
||||
VersionLegacy: job.version,
|
||||
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,
|
||||
Version: job.version,
|
||||
Compatible: true,
|
||||
Silent: job.Silent,
|
||||
Silent: job.silent,
|
||||
})
|
||||
|
||||
err := bridge.updater.InstallUpdate(ctx, bridge.api, job.Release)
|
||||
bridge.publish(events.UpdateInstalling{
|
||||
Version: job.version,
|
||||
Silent: job.silent,
|
||||
})
|
||||
|
||||
err := bridge.updater.InstallUpdate(ctx, bridge.api, job.version)
|
||||
|
||||
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.
|
||||
@ -346,8 +134,8 @@ func (bridge *Bridge) installUpdate(ctx context.Context, job installJob) {
|
||||
log.WithError(err).Error("The update could not be installed")
|
||||
|
||||
bridge.publish(events.UpdateFailed{
|
||||
Release: job.Release,
|
||||
Silent: job.Silent,
|
||||
Version: job.version,
|
||||
Silent: job.silent,
|
||||
Error: err,
|
||||
})
|
||||
|
||||
@ -355,11 +143,11 @@ func (bridge *Bridge) installUpdate(ctx context.Context, job installJob) {
|
||||
log.Info("The update was installed successfully")
|
||||
|
||||
bridge.publish(events.UpdateInstalled{
|
||||
Release: job.Release,
|
||||
Silent: job.Silent,
|
||||
Version: job.version,
|
||||
Silent: job.silent,
|
||||
})
|
||||
|
||||
bridge.newVersion = job.Release.Version
|
||||
bridge.newVersion = job.version.Version
|
||||
}
|
||||
}, bridge.newVersionLock)
|
||||
}
|
||||
|
||||
@ -1,700 +0,0 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package bridge_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/go-proton-api/server"
|
||||
bridgePkg "github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/updater/versioncompare"
|
||||
"github.com/elastic/go-sysinfo/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// NOTE: we always assume the highest version is always the first in the release json array
|
||||
|
||||
func Test_Update_BetaEligible(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridgePkg.Locator, vaultKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridgePkg.Bridge, mocks *bridgePkg.Mocks) {
|
||||
updateCh, done := bridge.GetEvents(events.UpdateInstalled{})
|
||||
defer done()
|
||||
|
||||
err := bridge.SetUpdateChannel(updater.EarlyChannel)
|
||||
require.NoError(t, err)
|
||||
|
||||
bridge.SetCurrentVersionTest(semver.MustParse("2.1.1"))
|
||||
|
||||
expectedRelease := updater.Release{
|
||||
ReleaseCategory: updater.EarlyAccessReleaseCategory,
|
||||
Version: semver.MustParse("2.1.2"),
|
||||
SystemVersion: versioncompare.SystemVersion{},
|
||||
RolloutProportion: 1.0,
|
||||
MinAuto: &semver.Version{},
|
||||
File: []updater.File{
|
||||
{
|
||||
URL: "RANDOM_INSTALLER_URL",
|
||||
Identifier: updater.InstallerIdentifier,
|
||||
},
|
||||
{
|
||||
URL: "RANDOM_PACKAGE_URL",
|
||||
Identifier: updater.PackageIdentifier,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
updaterData := updater.VersionInfo{Releases: []updater.Release{
|
||||
expectedRelease,
|
||||
}}
|
||||
|
||||
go func() {
|
||||
time.Sleep(1 * time.Second)
|
||||
mocks.Updater.SetLatestVersion(updaterData)
|
||||
bridge.CheckForUpdates()
|
||||
}()
|
||||
|
||||
select {
|
||||
case update := <-updateCh:
|
||||
require.Equal(t, events.UpdateInstalled{
|
||||
Release: expectedRelease,
|
||||
Silent: true,
|
||||
}, update)
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("timeout waiting for update")
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Update_Stable(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridgePkg.Locator, vaultKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridgePkg.Bridge, mocks *bridgePkg.Mocks) {
|
||||
updateCh, done := bridge.GetEvents(events.UpdateInstalled{})
|
||||
defer done()
|
||||
|
||||
err := bridge.SetUpdateChannel(updater.StableChannel)
|
||||
require.NoError(t, err)
|
||||
|
||||
bridge.SetCurrentVersionTest(semver.MustParse("2.1.1"))
|
||||
|
||||
expectedRelease := updater.Release{
|
||||
ReleaseCategory: updater.StableReleaseCategory,
|
||||
Version: semver.MustParse("2.1.3"),
|
||||
SystemVersion: versioncompare.SystemVersion{},
|
||||
RolloutProportion: 1.0,
|
||||
MinAuto: &semver.Version{},
|
||||
File: []updater.File{
|
||||
{
|
||||
URL: "RANDOM_INSTALLER_URL",
|
||||
Identifier: updater.InstallerIdentifier,
|
||||
},
|
||||
{
|
||||
URL: "RANDOM_PACKAGE_URL",
|
||||
Identifier: updater.PackageIdentifier,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
updaterData := updater.VersionInfo{Releases: []updater.Release{
|
||||
{
|
||||
ReleaseCategory: updater.EarlyAccessReleaseCategory,
|
||||
Version: semver.MustParse("2.1.4"),
|
||||
SystemVersion: versioncompare.SystemVersion{},
|
||||
RolloutProportion: 1.0,
|
||||
MinAuto: &semver.Version{},
|
||||
File: []updater.File{
|
||||
{
|
||||
URL: "RANDOM_INSTALLER_URL",
|
||||
Identifier: updater.InstallerIdentifier,
|
||||
},
|
||||
{
|
||||
URL: "RANDOM_PACKAGE_URL",
|
||||
Identifier: updater.PackageIdentifier,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedRelease,
|
||||
}}
|
||||
|
||||
mocks.Updater.SetLatestVersion(updaterData)
|
||||
|
||||
bridge.CheckForUpdates()
|
||||
|
||||
require.Equal(t, events.UpdateInstalled{
|
||||
Release: expectedRelease,
|
||||
Silent: true,
|
||||
}, <-updateCh)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Update_CurrentReleaseNewest(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridgePkg.Locator, vaultKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridgePkg.Bridge, mocks *bridgePkg.Mocks) {
|
||||
updateCh, done := bridge.GetEvents(events.UpdateNotAvailable{})
|
||||
defer done()
|
||||
|
||||
err := bridge.SetUpdateChannel(updater.StableChannel)
|
||||
require.NoError(t, err)
|
||||
|
||||
bridge.SetCurrentVersionTest(semver.MustParse("2.1.5"))
|
||||
|
||||
expectedRelease := updater.Release{
|
||||
ReleaseCategory: updater.StableReleaseCategory,
|
||||
Version: semver.MustParse("2.1.3"),
|
||||
SystemVersion: versioncompare.SystemVersion{},
|
||||
RolloutProportion: 1.0,
|
||||
MinAuto: &semver.Version{},
|
||||
File: []updater.File{
|
||||
{
|
||||
URL: "RANDOM_INSTALLER_URL",
|
||||
Identifier: updater.InstallerIdentifier,
|
||||
},
|
||||
{
|
||||
URL: "RANDOM_PACKAGE_URL",
|
||||
Identifier: updater.PackageIdentifier,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
updaterData := updater.VersionInfo{Releases: []updater.Release{
|
||||
{
|
||||
ReleaseCategory: updater.EarlyAccessReleaseCategory,
|
||||
Version: semver.MustParse("2.1.4"),
|
||||
SystemVersion: versioncompare.SystemVersion{},
|
||||
RolloutProportion: 1.0,
|
||||
MinAuto: &semver.Version{},
|
||||
File: []updater.File{
|
||||
{
|
||||
URL: "RANDOM_INSTALLER_URL",
|
||||
Identifier: updater.InstallerIdentifier,
|
||||
},
|
||||
{
|
||||
URL: "RANDOM_PACKAGE_URL",
|
||||
Identifier: updater.PackageIdentifier,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedRelease,
|
||||
}}
|
||||
|
||||
mocks.Updater.SetLatestVersion(updaterData)
|
||||
bridge.CheckForUpdates()
|
||||
|
||||
require.Equal(t, events.UpdateNotAvailable{}, <-updateCh)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Update_NotRolledOutYet(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridgePkg.Locator, vaultKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridgePkg.Bridge, mocks *bridgePkg.Mocks) {
|
||||
require.NoError(t, bridge.SetUpdateChannel(updater.EarlyChannel))
|
||||
bridge.SetCurrentVersionTest(semver.MustParse("2.0.0"))
|
||||
require.NoError(t, bridge.SetRolloutPercentageTest(1.0))
|
||||
|
||||
updaterData := updater.VersionInfo{Releases: []updater.Release{
|
||||
{
|
||||
ReleaseCategory: updater.StableReleaseCategory,
|
||||
Version: semver.MustParse("2.1.5"),
|
||||
SystemVersion: versioncompare.SystemVersion{},
|
||||
RolloutProportion: 0.5,
|
||||
MinAuto: &semver.Version{},
|
||||
File: []updater.File{
|
||||
{
|
||||
URL: "RANDOM_INSTALLER_URL",
|
||||
Identifier: updater.InstallerIdentifier,
|
||||
},
|
||||
{
|
||||
URL: "RANDOM_PACKAGE_URL",
|
||||
Identifier: updater.PackageIdentifier,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ReleaseCategory: updater.StableReleaseCategory,
|
||||
Version: semver.MustParse("2.1.4"),
|
||||
SystemVersion: versioncompare.SystemVersion{},
|
||||
RolloutProportion: 0.5,
|
||||
MinAuto: &semver.Version{},
|
||||
File: []updater.File{
|
||||
{
|
||||
URL: "RANDOM_INSTALLER_URL",
|
||||
Identifier: updater.InstallerIdentifier,
|
||||
},
|
||||
{
|
||||
URL: "RANDOM_PACKAGE_URL",
|
||||
Identifier: updater.PackageIdentifier,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
||||
mocks.Updater.SetLatestVersion(updaterData)
|
||||
|
||||
updateCh, done := bridge.GetEvents(events.UpdateNotAvailable{})
|
||||
defer done()
|
||||
|
||||
bridge.CheckForUpdates()
|
||||
|
||||
require.Equal(t, events.UpdateNotAvailable{}, <-updateCh)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Update_CheckOSVersion_NoUpdate(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridgePkg.Locator, vaultKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridgePkg.Bridge, mocks *bridgePkg.Mocks) {
|
||||
require.NoError(t, bridge.SetAutoUpdate(true))
|
||||
require.NoError(t, bridge.SetUpdateChannel(updater.StableChannel))
|
||||
|
||||
currentBridgeVersion := semver.MustParse("2.1.5")
|
||||
bridge.SetCurrentVersionTest(currentBridgeVersion)
|
||||
|
||||
// Override the OS version check
|
||||
bridge.SetHostVersionGetterTest(func(_ types.Host) string {
|
||||
return "10.0.0"
|
||||
})
|
||||
|
||||
updateNotAvailableCh, done := bridge.GetEvents(events.UpdateNotAvailable{})
|
||||
defer done()
|
||||
|
||||
updateCh, updateChDone := bridge.GetEvents(events.UpdateInstalled{})
|
||||
defer updateChDone()
|
||||
|
||||
expectedRelease := updater.Release{
|
||||
ReleaseCategory: updater.StableReleaseCategory,
|
||||
Version: semver.MustParse("2.4.0"),
|
||||
SystemVersion: versioncompare.SystemVersion{
|
||||
Minimum: "12.0.0",
|
||||
Maximum: "13.0.0",
|
||||
},
|
||||
RolloutProportion: 1.0,
|
||||
File: []updater.File{
|
||||
{
|
||||
URL: "RANDOM_INSTALLER_URL",
|
||||
Identifier: updater.InstallerIdentifier,
|
||||
},
|
||||
{
|
||||
URL: "RANDOM_PACKAGE_URL",
|
||||
Identifier: updater.PackageIdentifier,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
updaterData := updater.VersionInfo{Releases: []updater.Release{
|
||||
expectedRelease,
|
||||
{
|
||||
ReleaseCategory: updater.StableReleaseCategory,
|
||||
Version: semver.MustParse("2.3.0"),
|
||||
SystemVersion: versioncompare.SystemVersion{
|
||||
Minimum: "10.1.0",
|
||||
Maximum: "11.5",
|
||||
},
|
||||
RolloutProportion: 1.0,
|
||||
File: []updater.File{
|
||||
{
|
||||
URL: "RANDOM_INSTALLER_URL",
|
||||
Identifier: updater.InstallerIdentifier,
|
||||
},
|
||||
{
|
||||
URL: "RANDOM_PACKAGE_URL",
|
||||
Identifier: updater.PackageIdentifier,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
||||
mocks.Updater.SetLatestVersion(updaterData)
|
||||
|
||||
bridge.CheckForUpdates()
|
||||
|
||||
if runtime.GOOS == "darwin" {
|
||||
require.Equal(t, events.UpdateNotAvailable{}, <-updateNotAvailableCh)
|
||||
} else {
|
||||
require.Equal(t, events.UpdateInstalled{
|
||||
Release: expectedRelease,
|
||||
Silent: true,
|
||||
}, <-updateCh)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Update_CheckOSVersion_HasUpdate(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridgePkg.Locator, vaultKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridgePkg.Bridge, mocks *bridgePkg.Mocks) {
|
||||
require.NoError(t, bridge.SetAutoUpdate(true))
|
||||
require.NoError(t, bridge.SetUpdateChannel(updater.StableChannel))
|
||||
|
||||
updateCh, done := bridge.GetEvents(events.UpdateInstalled{})
|
||||
defer done()
|
||||
|
||||
currentBridgeVersion := semver.MustParse("2.1.5")
|
||||
bridge.SetCurrentVersionTest(currentBridgeVersion)
|
||||
|
||||
// Override the OS version check
|
||||
bridge.SetHostVersionGetterTest(func(_ types.Host) string {
|
||||
return "10.0.0"
|
||||
})
|
||||
|
||||
expectedUpdateRelease := updater.Release{
|
||||
ReleaseCategory: updater.StableReleaseCategory,
|
||||
Version: semver.MustParse("2.2.0"),
|
||||
SystemVersion: versioncompare.SystemVersion{
|
||||
Minimum: "10.0.0",
|
||||
Maximum: "10.1.12",
|
||||
},
|
||||
RolloutProportion: 1.0,
|
||||
File: []updater.File{
|
||||
{
|
||||
URL: "RANDOM_INSTALLER_URL",
|
||||
Identifier: updater.InstallerIdentifier,
|
||||
},
|
||||
{
|
||||
URL: "RANDOM_PACKAGE_URL",
|
||||
Identifier: updater.PackageIdentifier,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
expectedUpdateReleaseWindowsLinux := updater.Release{
|
||||
ReleaseCategory: updater.StableReleaseCategory,
|
||||
Version: semver.MustParse("2.4.0"),
|
||||
SystemVersion: versioncompare.SystemVersion{
|
||||
Minimum: "12.0.0",
|
||||
},
|
||||
RolloutProportion: 1.0,
|
||||
File: []updater.File{
|
||||
{
|
||||
URL: "RANDOM_INSTALLER_URL",
|
||||
Identifier: updater.InstallerIdentifier,
|
||||
},
|
||||
{
|
||||
URL: "RANDOM_PACKAGE_URL",
|
||||
Identifier: updater.PackageIdentifier,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
updaterData := updater.VersionInfo{Releases: []updater.Release{
|
||||
expectedUpdateReleaseWindowsLinux,
|
||||
{
|
||||
ReleaseCategory: updater.StableReleaseCategory,
|
||||
Version: semver.MustParse("2.3.0"),
|
||||
SystemVersion: versioncompare.SystemVersion{
|
||||
Minimum: "11.0.0",
|
||||
},
|
||||
RolloutProportion: 1.0,
|
||||
File: []updater.File{
|
||||
{
|
||||
URL: "RANDOM_INSTALLER_URL",
|
||||
Identifier: updater.InstallerIdentifier,
|
||||
},
|
||||
{
|
||||
URL: "RANDOM_PACKAGE_URL",
|
||||
Identifier: updater.PackageIdentifier,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedUpdateRelease,
|
||||
{
|
||||
ReleaseCategory: updater.StableReleaseCategory,
|
||||
Version: semver.MustParse("2.1.0"),
|
||||
SystemVersion: versioncompare.SystemVersion{},
|
||||
RolloutProportion: 1.0,
|
||||
File: []updater.File{
|
||||
{
|
||||
URL: "RANDOM_INSTALLER_URL",
|
||||
Identifier: updater.InstallerIdentifier,
|
||||
},
|
||||
{
|
||||
URL: "RANDOM_PACKAGE_URL",
|
||||
Identifier: updater.PackageIdentifier,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
||||
mocks.Updater.SetLatestVersion(updaterData)
|
||||
|
||||
bridge.CheckForUpdates()
|
||||
|
||||
if runtime.GOOS == "darwin" {
|
||||
require.Equal(t, events.UpdateInstalled{
|
||||
Release: expectedUpdateRelease,
|
||||
Silent: true,
|
||||
}, <-updateCh)
|
||||
} else {
|
||||
require.Equal(t, events.UpdateInstalled{
|
||||
Release: expectedUpdateReleaseWindowsLinux,
|
||||
Silent: true,
|
||||
}, <-updateCh)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Update_UpdateFromMinVer_UpdateAvailable(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridgePkg.Locator, vaultKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridgePkg.Bridge, mocks *bridgePkg.Mocks) {
|
||||
require.NoError(t, bridge.SetAutoUpdate(true))
|
||||
require.NoError(t, bridge.SetUpdateChannel(updater.StableChannel))
|
||||
|
||||
currentBridgeVersion := semver.MustParse("2.1.5")
|
||||
bridge.SetCurrentVersionTest(currentBridgeVersion)
|
||||
|
||||
updateCh, done := bridge.GetEvents(events.UpdateInstalled{})
|
||||
defer done()
|
||||
|
||||
expectedUpdateRelease := updater.Release{
|
||||
ReleaseCategory: updater.StableReleaseCategory,
|
||||
Version: semver.MustParse("2.2.0"),
|
||||
SystemVersion: versioncompare.SystemVersion{},
|
||||
RolloutProportion: 1.0,
|
||||
MinAuto: currentBridgeVersion,
|
||||
File: []updater.File{
|
||||
{
|
||||
URL: "RANDOM_INSTALLER_URL",
|
||||
Identifier: updater.InstallerIdentifier,
|
||||
},
|
||||
{
|
||||
URL: "RANDOM_PACKAGE_URL",
|
||||
Identifier: updater.PackageIdentifier,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
updaterData := updater.VersionInfo{Releases: []updater.Release{
|
||||
{
|
||||
ReleaseCategory: updater.StableReleaseCategory,
|
||||
Version: semver.MustParse("2.3.0"),
|
||||
SystemVersion: versioncompare.SystemVersion{},
|
||||
RolloutProportion: 1.0,
|
||||
MinAuto: semver.MustParse("2.2.1"),
|
||||
File: []updater.File{
|
||||
{
|
||||
URL: "RANDOM_INSTALLER_URL",
|
||||
Identifier: updater.InstallerIdentifier,
|
||||
},
|
||||
{
|
||||
URL: "RANDOM_PACKAGE_URL",
|
||||
Identifier: updater.PackageIdentifier,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ReleaseCategory: updater.StableReleaseCategory,
|
||||
Version: semver.MustParse("2.2.1"),
|
||||
SystemVersion: versioncompare.SystemVersion{},
|
||||
RolloutProportion: 1.0,
|
||||
MinAuto: semver.MustParse("2.2.0"),
|
||||
File: []updater.File{
|
||||
{
|
||||
URL: "RANDOM_INSTALLER_URL",
|
||||
Identifier: updater.InstallerIdentifier,
|
||||
},
|
||||
{
|
||||
URL: "RANDOM_PACKAGE_URL",
|
||||
Identifier: updater.PackageIdentifier,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedUpdateRelease,
|
||||
}}
|
||||
|
||||
mocks.Updater.SetLatestVersion(updaterData)
|
||||
|
||||
bridge.CheckForUpdates()
|
||||
|
||||
require.Equal(t, events.UpdateInstalled{
|
||||
Release: expectedUpdateRelease,
|
||||
Silent: true,
|
||||
}, <-updateCh)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Update_UpdateFromMinVer_NoCompatibleVersionForceManual -
|
||||
// if we have an update, but we don't satisfy minVersion, a manual update to the highest possible version should be performed.
|
||||
func Test_Update_UpdateFromMinVer_NoCompatibleVersionForceManual(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridgePkg.Locator, vaultKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridgePkg.Bridge, mocks *bridgePkg.Mocks) {
|
||||
require.NoError(t, bridge.SetAutoUpdate(true))
|
||||
require.NoError(t, bridge.SetUpdateChannel(updater.StableChannel))
|
||||
|
||||
currentBridgeVersion := semver.MustParse("2.1.5")
|
||||
bridge.SetCurrentVersionTest(currentBridgeVersion)
|
||||
|
||||
updateCh, done := bridge.GetEvents(events.UpdateAvailable{})
|
||||
defer done()
|
||||
|
||||
expectedUpdateRelease := updater.Release{
|
||||
ReleaseCategory: updater.StableReleaseCategory,
|
||||
Version: semver.MustParse("2.3.0"),
|
||||
SystemVersion: versioncompare.SystemVersion{},
|
||||
RolloutProportion: 1.0,
|
||||
MinAuto: semver.MustParse("2.2.1"),
|
||||
File: []updater.File{
|
||||
{
|
||||
URL: "RANDOM_INSTALLER_URL",
|
||||
Identifier: updater.InstallerIdentifier,
|
||||
},
|
||||
{
|
||||
URL: "RANDOM_PACKAGE_URL",
|
||||
Identifier: updater.PackageIdentifier,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
updaterData := updater.VersionInfo{Releases: []updater.Release{
|
||||
{
|
||||
ReleaseCategory: updater.StableReleaseCategory,
|
||||
Version: semver.MustParse("2.2.1"),
|
||||
SystemVersion: versioncompare.SystemVersion{},
|
||||
RolloutProportion: 1.0,
|
||||
MinAuto: semver.MustParse("2.2.0"),
|
||||
File: []updater.File{
|
||||
{
|
||||
URL: "RANDOM_INSTALLER_URL",
|
||||
Identifier: updater.InstallerIdentifier,
|
||||
},
|
||||
{
|
||||
URL: "RANDOM_PACKAGE_URL",
|
||||
Identifier: updater.PackageIdentifier,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ReleaseCategory: updater.StableReleaseCategory,
|
||||
Version: semver.MustParse("2.2.0"),
|
||||
SystemVersion: versioncompare.SystemVersion{},
|
||||
RolloutProportion: 1.0,
|
||||
MinAuto: semver.MustParse("2.1.6"),
|
||||
File: []updater.File{
|
||||
{
|
||||
URL: "RANDOM_INSTALLER_URL",
|
||||
Identifier: updater.InstallerIdentifier,
|
||||
},
|
||||
{
|
||||
URL: "RANDOM_PACKAGE_URL",
|
||||
Identifier: updater.PackageIdentifier,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedUpdateRelease,
|
||||
}}
|
||||
|
||||
mocks.Updater.SetLatestVersion(updaterData)
|
||||
|
||||
bridge.CheckForUpdates()
|
||||
|
||||
require.Equal(t, events.UpdateAvailable{
|
||||
Release: expectedUpdateRelease,
|
||||
Silent: false,
|
||||
Compatible: false,
|
||||
}, <-updateCh)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Test_Update_UpdateFromMinVer_NoCompatibleVersionForceManual_BetaMismatch - only Beta updates are available
|
||||
// nor do we satisfy the minVersion, we can't do anything in this case.
|
||||
func Test_Update_UpdateFromMinVer_NoCompatibleVersionForceManual_BetaMismatch(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridgePkg.Locator, vaultKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridgePkg.Bridge, mocks *bridgePkg.Mocks) {
|
||||
require.NoError(t, bridge.SetAutoUpdate(true))
|
||||
require.NoError(t, bridge.SetUpdateChannel(updater.StableChannel))
|
||||
|
||||
currentBridgeVersion := semver.MustParse("2.1.5")
|
||||
bridge.SetCurrentVersionTest(currentBridgeVersion)
|
||||
|
||||
updateCh, done := bridge.GetEvents(events.UpdateNotAvailable{})
|
||||
defer done()
|
||||
|
||||
expectedUpdateRelease := updater.Release{
|
||||
ReleaseCategory: updater.EarlyAccessReleaseCategory,
|
||||
Version: semver.MustParse("2.3.0"),
|
||||
SystemVersion: versioncompare.SystemVersion{},
|
||||
RolloutProportion: 1.0,
|
||||
MinAuto: semver.MustParse("2.2.1"),
|
||||
File: []updater.File{
|
||||
{
|
||||
URL: "RANDOM_INSTALLER_URL",
|
||||
Identifier: updater.InstallerIdentifier,
|
||||
},
|
||||
{
|
||||
URL: "RANDOM_PACKAGE_URL",
|
||||
Identifier: updater.PackageIdentifier,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
updaterData := updater.VersionInfo{Releases: []updater.Release{
|
||||
{
|
||||
ReleaseCategory: updater.EarlyAccessReleaseCategory,
|
||||
Version: semver.MustParse("2.2.1"),
|
||||
SystemVersion: versioncompare.SystemVersion{},
|
||||
RolloutProportion: 1.0,
|
||||
MinAuto: semver.MustParse("2.2.0"),
|
||||
File: []updater.File{
|
||||
{
|
||||
URL: "RANDOM_INSTALLER_URL",
|
||||
Identifier: updater.InstallerIdentifier,
|
||||
},
|
||||
{
|
||||
URL: "RANDOM_PACKAGE_URL",
|
||||
Identifier: updater.PackageIdentifier,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ReleaseCategory: updater.EarlyAccessReleaseCategory,
|
||||
Version: semver.MustParse("2.2.0"),
|
||||
SystemVersion: versioncompare.SystemVersion{},
|
||||
RolloutProportion: 1.0,
|
||||
MinAuto: semver.MustParse("2.1.6"),
|
||||
File: []updater.File{
|
||||
{
|
||||
URL: "RANDOM_INSTALLER_URL",
|
||||
Identifier: updater.InstallerIdentifier,
|
||||
},
|
||||
{
|
||||
URL: "RANDOM_PACKAGE_URL",
|
||||
Identifier: updater.PackageIdentifier,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedUpdateRelease,
|
||||
}}
|
||||
|
||||
mocks.Updater.SetLatestVersion(updaterData)
|
||||
|
||||
bridge.CheckForUpdates()
|
||||
|
||||
require.Equal(t, events.UpdateNotAvailable{}, <-updateCh)
|
||||
})
|
||||
})
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
@ -33,7 +33,6 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice"
|
||||
"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/vault"
|
||||
"github.com/go-resty/resty/v2"
|
||||
@ -256,7 +255,7 @@ func (bridge *Bridge) LogoutUser(ctx context.Context, userID string) error {
|
||||
return ErrNoSuchUser
|
||||
}
|
||||
|
||||
bridge.logoutUser(ctx, user, true, false)
|
||||
bridge.logoutUser(ctx, user, true, false, false)
|
||||
|
||||
bridge.publish(events.UserLoggedOut{
|
||||
UserID: userID,
|
||||
@ -281,7 +280,7 @@ func (bridge *Bridge) DeleteUser(ctx context.Context, userID string) error {
|
||||
}
|
||||
|
||||
if user, ok := bridge.users[userID]; ok {
|
||||
bridge.logoutUser(ctx, user, true, true)
|
||||
bridge.logoutUser(ctx, user, true, true, !bridge.GetTelemetryDisabled())
|
||||
}
|
||||
|
||||
if err := imapservice.DeleteSyncState(syncConfigDir, userID); err != nil {
|
||||
@ -359,7 +358,7 @@ func (bridge *Bridge) SendBadEventUserFeedback(_ context.Context, userID string,
|
||||
return user.BadEventFeedbackResync(ctx)
|
||||
}
|
||||
|
||||
bridge.logoutUser(ctx, user, true, false)
|
||||
bridge.logoutUser(ctx, user, true, false, false)
|
||||
|
||||
bridge.publish(events.UserLoggedOut{
|
||||
UserID: userID,
|
||||
@ -528,6 +527,11 @@ func (bridge *Bridge) addUserWithVault(
|
||||
vault *vault.User,
|
||||
isNew bool,
|
||||
) error {
|
||||
statsPath, err := bridge.locator.ProvideStatsPath()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get Statistics directory: %w", err)
|
||||
}
|
||||
|
||||
syncSettingsPath, err := bridge.locator.ProvideIMAPSyncConfigPath()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get IMAP sync config path: %w", err)
|
||||
@ -542,6 +546,7 @@ func (bridge *Bridge) addUserWithVault(
|
||||
bridge.panicHandler,
|
||||
bridge.vault.GetShowAllMail(),
|
||||
bridge.vault.GetMaxSyncMemory(),
|
||||
statsPath,
|
||||
bridge,
|
||||
bridge.serverManager,
|
||||
bridge.serverManager,
|
||||
@ -551,7 +556,7 @@ func (bridge *Bridge) addUserWithVault(
|
||||
syncSettingsPath,
|
||||
isNew,
|
||||
bridge.notificationStore,
|
||||
bridge.unleashService,
|
||||
bridge.unleashService.GetFlagValue,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create user: %w", err)
|
||||
@ -584,12 +589,9 @@ func (bridge *Bridge) addUserWithVault(
|
||||
// Finally, save the user in the bridge.
|
||||
safe.Lock(func() {
|
||||
bridge.users[apiUser.ID] = user
|
||||
bridge.heartbeat.SetNumberConnectedAccounts(len(bridge.users))
|
||||
bridge.heartbeat.SetNbAccount(len(bridge.users))
|
||||
}, 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.
|
||||
bridge.heartbeat.start()
|
||||
|
||||
@ -608,21 +610,26 @@ func (bridge *Bridge) newVaultUser(
|
||||
return bridge.vault.GetOrAddUser(apiUser.ID, apiUser.Name, apiUser.Email, authUID, authRef, saltedKeyPass)
|
||||
}
|
||||
|
||||
// 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 bool) {
|
||||
// logout logs out the given user, optionally logging them out from the API too.
|
||||
func (bridge *Bridge) logoutUser(ctx context.Context, user *user.User, withAPI, withData, withTelemetry bool) {
|
||||
defer delete(bridge.users, user.ID())
|
||||
|
||||
// if this is actually a remove account
|
||||
if withData && withAPI {
|
||||
user.SendConfigStatusAbort(ctx, withTelemetry)
|
||||
}
|
||||
|
||||
logUser.WithFields(logrus.Fields{
|
||||
"userID": user.ID(),
|
||||
"withAPI": withAPI,
|
||||
"withData": withData,
|
||||
}).Debug("Logging out user")
|
||||
|
||||
if err := user.Logout(ctx, withAPI, withData, bridge.unleashService.GetFlagValue(unleash.UserRemovalGluonDataCleanupDisabled)); err != nil {
|
||||
if err := user.Logout(ctx, withAPI); err != nil {
|
||||
logUser.WithError(err).Error("Failed to logout user")
|
||||
}
|
||||
|
||||
bridge.heartbeat.SetNumberConnectedAccounts(len(bridge.users) - 1)
|
||||
bridge.heartbeat.SetNbAccount(len(bridge.users))
|
||||
|
||||
user.Close()
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
@ -23,7 +23,6 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/mail"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
@ -77,9 +76,6 @@ func TestBridge_User_RefreshEvent(t *testing.T) {
|
||||
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{}))
|
||||
|
||||
if runtime.GOOS != "windows" {
|
||||
require.Equal(t, userID, (<-syncCh).UserID)
|
||||
}
|
||||
require.Equal(t, userID, (<-syncCh).UserID)
|
||||
closeCh()
|
||||
|
||||
@ -308,7 +304,7 @@ func TestBridge_User_AddressEvents_NoBadEvent(t *testing.T) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
userLoginAndSync(ctx, t, bridge, "user", password)
|
||||
|
||||
addrID, err = s.CreateAddress(userID, "other@pm.me", password, true)
|
||||
addrID, err = s.CreateAddress(userID, "other@pm.me", password)
|
||||
require.NoError(t, err)
|
||||
userContinueEventProcess(ctx, t, s, bridge)
|
||||
|
||||
@ -316,7 +312,7 @@ func TestBridge_User_AddressEvents_NoBadEvent(t *testing.T) {
|
||||
userContinueEventProcess(ctx, t, s, bridge)
|
||||
})
|
||||
|
||||
otherID, err := s.CreateAddress(userID, "another@pm.me", password, true)
|
||||
otherID, err := s.CreateAddress(userID, "another@pm.me", password)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, s.RemoveAddress(userID, otherID))
|
||||
|
||||
@ -332,87 +328,6 @@ 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) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
// Create a user.
|
||||
@ -779,7 +694,7 @@ func TestBridge_User_DisableEnableAddress(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create an additional address for the user.
|
||||
aliasID, err := s.CreateAddress(userID, "alias@"+s.GetDomain(), password, true)
|
||||
aliasID, err := s.CreateAddress(userID, "alias@"+s.GetDomain(), password)
|
||||
require.NoError(t, err)
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
@ -830,7 +745,7 @@ func TestBridge_User_CreateDisabledAddress(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create an additional address for the user.
|
||||
aliasID, err := s.CreateAddress(userID, "alias@"+s.GetDomain(), password, true)
|
||||
aliasID, err := s.CreateAddress(userID, "alias@"+s.GetDomain(), password)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Immediately disable the address.
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
@ -43,7 +43,8 @@ func (bridge *Bridge) handleUserEvent(ctx context.Context, user *user.User, even
|
||||
|
||||
func (bridge *Bridge) handleUserDeauth(ctx context.Context, user *user.User) {
|
||||
safe.Lock(func() {
|
||||
bridge.logoutUser(ctx, user, false, false)
|
||||
bridge.logoutUser(ctx, user, false, false, false)
|
||||
user.ReportConfigStatusFailure("User deauth.")
|
||||
}, bridge.usersLock)
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
@ -658,7 +658,7 @@ func TestBridge_UserInfo_Alias(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Give the new user an alias.
|
||||
require.NoError(t, getErr(s.CreateAddress(userID, "alias@pm.me", []byte("password"), true)))
|
||||
require.NoError(t, getErr(s.CreateAddress(userID, "alias@pm.me", []byte("password"))))
|
||||
|
||||
// Login the user.
|
||||
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.
|
||||
userID, _, err := s.CreateUser("user", password)
|
||||
require.NoError(t, err)
|
||||
addrID2, err := s.CreateAddress(userID, "user@external.com", password, false)
|
||||
addrID2, err := s.CreateAddress(userID, "user@external.com", []byte("password"))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, s.ChangeAddressType(userID, addrID2, proton.AddressTypeExternal))
|
||||
|
||||
@ -720,29 +720,6 @@ 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.
|
||||
func getErr[T any](_ T, err error) error {
|
||||
return err
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
228
internal/configstatus/config_status.go
Normal file
228
internal/configstatus/config_status.go
Normal file
@ -0,0 +1,228 @@
|
||||
// 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
|
||||
}
|
||||
252
internal/configstatus/config_status_test.go
Normal file
252
internal/configstatus/config_status_test.go
Normal file
@ -0,0 +1,252 @@
|
||||
// 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)
|
||||
}
|
||||
59
internal/configstatus/configuration_abort.go
Normal file
59
internal/configstatus/configuration_abort.go
Normal file
@ -0,0 +1,59 @@
|
||||
// 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(),
|
||||
},
|
||||
}
|
||||
}
|
||||
75
internal/configstatus/configuration_abort_test.go
Normal file
75
internal/configstatus/configuration_abort_test.go
Normal file
@ -0,0 +1,75 @@
|
||||
// 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)
|
||||
}
|
||||
60
internal/configstatus/configuration_progress.go
Normal file
60
internal/configstatus/configuration_progress.go
Normal file
@ -0,0 +1,60 @@
|
||||
// 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
|
||||
}
|
||||
100
internal/configstatus/configuration_progress_test.go
Normal file
100
internal/configstatus/configuration_progress_test.go
Normal file
@ -0,0 +1,100 @@
|
||||
// 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)
|
||||
}
|
||||
63
internal/configstatus/configuration_recovery.go
Normal file
63
internal/configstatus/configuration_recovery.go
Normal file
@ -0,0 +1,63 @@
|
||||
// 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,
|
||||
},
|
||||
}
|
||||
}
|
||||
79
internal/configstatus/configuration_recovery_test.go
Normal file
79
internal/configstatus/configuration_recovery_test.go
Normal file
@ -0,0 +1,79 @@
|
||||
// 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)
|
||||
}
|
||||
61
internal/configstatus/configuration_success.go
Normal file
61
internal/configstatus/configuration_success.go
Normal file
@ -0,0 +1,61 @@
|
||||
// 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(),
|
||||
},
|
||||
}
|
||||
}
|
||||
77
internal/configstatus/configuration_success_test.go
Normal file
77
internal/configstatus/configuration_success_test.go
Normal file
@ -0,0 +1,77 @@
|
||||
// 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)
|
||||
}
|
||||
56
internal/configstatus/types_config_status.go
Normal file
56
internal/configstatus/types_config_status.go
Normal file
@ -0,0 +1,56 @@
|
||||
// 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
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.Bridge.
|
||||
//
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user