mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-10 04:36:43 +00:00
Compare commits
142 Commits
v3.12.0
...
5e136df557
| Author | SHA1 | Date | |
|---|---|---|---|
| 5e136df557 | |||
| 9c5b5c2ac3 | |||
| 0e6df4ce73 | |||
| a4772ee4e0 | |||
| ef779a23c1 | |||
| 4f4a2c3fd8 | |||
| 120a7b3626 | |||
| 7cf3b6fb7b | |||
| dd2448f35a | |||
| 917cf3fd51 | |||
| 98c3a01e08 | |||
| ecb74dbb29 | |||
| fa9836e9cf | |||
| 29a10bc4df | |||
| 6ad1e89919 | |||
| 55a0f27ca9 | |||
| c8291c2d35 | |||
| 4d8d00a62f | |||
| f0c76b1114 | |||
| cd69a712e1 | |||
| 21029825c9 | |||
| 3f78f4d672 | |||
| 5fbe94c559 | |||
| 80d556343e | |||
| 612d1054db | |||
| acf2fc32c4 | |||
| af01c63298 | |||
| 2e98d64f94 | |||
| cdcdd45bcf | |||
| 61ca604ace | |||
| b3e2a91f56 | |||
| 7d9753e2da | |||
| f1aef383b7 | |||
| 6647231278 | |||
| 531368da86 | |||
| a8caec560e | |||
| 0c21925939 | |||
| 19a445e73a | |||
| 96e0070ed2 | |||
| 516ff5206d | |||
| 4d2b328589 | |||
| 810be2d423 | |||
| e3d0334b6f | |||
| fb523e5573 | |||
| cb8d1a2389 | |||
| 84f0a6722a | |||
| c3a495facd | |||
| 9cdc40ca05 | |||
| 7021b1c2ea | |||
| 607d9df8a9 | |||
| 93396145dc | |||
| 7457fb06d2 | |||
| bee2642aec | |||
| b481ce2203 | |||
| 040d887aae | |||
| 3710dff0cd | |||
| df78e29234 | |||
| 6105f32c75 | |||
| f5bc6ad1f0 | |||
| e8a95e26f6 | |||
| ebe54ca92e | |||
| ff7e45f395 | |||
| 79c63f5785 | |||
| 3ca9e625f5 | |||
| 5b874657cb | |||
| da76784290 | |||
| bfe67f3005 | |||
| 99e6f00aaa | |||
| 43cbedafb8 | |||
| ac9ab8ab32 | |||
| f04350c046 | |||
| ed1b65731a | |||
| d12928b31c | |||
| 1ea06a95b7 | |||
| e290cd308b | |||
| 3d53bf7477 | |||
| 84c0b907d7 | |||
| b30455b641 | |||
| db9902e70b | |||
| f1f63c1d03 | |||
| 81a3c2aba8 | |||
| bbfc9beb04 | |||
| c4dba09ee6 | |||
| a5435eb1da | |||
| 54c56efdfa | |||
| fc64dbec59 | |||
| 5d3f084a2b | |||
| 606d6c0e3e | |||
| 9fbb6b4ca5 | |||
| 0d33cc5000 | |||
| b84663dd7a | |||
| cd8db6fd1c | |||
| a5e0f85a58 | |||
| 6cbe51138a | |||
| 82607efe1c | |||
| 961dc9435f | |||
| b574ccb6ea | |||
| 2569e83e51 | |||
| f34a7ff0ed | |||
| da069a0155 | |||
| 384fa4eb4b | |||
| 0c6e4ffa35 | |||
| 4951244400 | |||
| d65d6ee2e5 | |||
| 097d6f86d3 | |||
| 9894cf9744 | |||
| f84067de3e | |||
| f885bfbcf4 | |||
| f3aac09ecb | |||
| 38d692ebfb | |||
| 1acc7eb7db | |||
| 248fbf5e33 | |||
| 8b12a454ea | |||
| 310fcffc7b | |||
| 318ad16378 | |||
| 8be4246f7e | |||
| e580f89106 | |||
| 01043e033e | |||
| 94b44b383a | |||
| a3b8fabb26 | |||
| 275b30e518 | |||
| bf244e5c86 | |||
| cf9651bb94 | |||
| ba65ffdbc7 | |||
| 4b95ef4d82 | |||
| 951c7c27fb | |||
| e7423a9519 | |||
| d3582fa981 | |||
| 80c852a5b2 | |||
| 51498e3e37 | |||
| b7ef6e1486 | |||
| 0d03f84711 | |||
| 949666724d | |||
| bbe19bf960 | |||
| bfe25e3a46 | |||
| 236c958703 | |||
| e6b312b437 | |||
| 384154c767 | |||
| 45d2e9ea63 | |||
| 86e8a566c7 | |||
| a80fd92018 | |||
| 71063ac5ee |
30
.gitea/workflows/ci.yml
Normal file
30
.gitea/workflows/ci.yml
Normal file
@ -0,0 +1,30 @@
|
||||
name: nogui build
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
try:
|
||||
name: try & test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.23.3'
|
||||
|
||||
- name: Install build-essentials
|
||||
run: apt update -y && apt install build-essential libsecret-1-dev -y
|
||||
|
||||
- name: build nogui
|
||||
run: make build-nogui
|
||||
|
||||
- uses: christopherhx/gitea-upload-artifact@v4
|
||||
with:
|
||||
name: nogui.zip
|
||||
path: |
|
||||
proton-bridge
|
||||
bridge
|
||||
41
.github/ISSUE_TEMPLATE/general-issue-template.md
vendored
41
.github/ISSUE_TEMPLATE/general-issue-template.md
vendored
@ -1,41 +0,0 @@
|
||||
---
|
||||
name: General issue template
|
||||
about: Template for detailed report of issues
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Issue tracker is ONLY used for reporting bugs with technical details. "It doesn't work" or new features should be discussed with our customer support. Please use bug report function in Bridge or contact bridge@protonmail.ch.
|
||||
<!--- Provide a general summary of the issue in the Title above -->
|
||||
|
||||
## Expected Behavior
|
||||
<!--- Tell us what should happen -->
|
||||
|
||||
## Current Behavior
|
||||
<!--- Tell us what happens instead of the expected behavior -->
|
||||
|
||||
## Possible Solution
|
||||
<!--- Not obligatory, but suggest a fix/reason for the bug, -->
|
||||
|
||||
## Steps to Reproduce
|
||||
<!--- Provide a link to a live example, or an unambiguous set of steps to -->
|
||||
<!--- reproduce this bug. Include code to reproduce, if relevant -->
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
4.
|
||||
|
||||
## Version Information
|
||||
<!--- Which version of the app(s) were you using when you experienced this issue? -->
|
||||
|
||||
## Context (Environment)
|
||||
<!--- How has this issue affected you? What are you trying to accomplish? -->
|
||||
<!--- Providing context helps us come up with a solution that is most useful in the real world -->
|
||||
|
||||
## Detailed Description
|
||||
<!--- Provide a detailed description of the change or addition you are proposing -->
|
||||
|
||||
## Possible Implementation
|
||||
<!--- Not obligatory, but suggest an idea for implementing addition or change -->
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -7,6 +7,7 @@
|
||||
*~
|
||||
.idea
|
||||
.vscode
|
||||
.vs
|
||||
|
||||
# Test files
|
||||
godog.test
|
||||
@ -35,6 +36,8 @@ cmd/Import-Export/deploy
|
||||
proton-bridge
|
||||
cmd/Desktop-Bridge/*.exe
|
||||
cmd/launcher/*.exe
|
||||
bin/
|
||||
obj/
|
||||
|
||||
# Jetbrains (CLion, Golang) cmake build dirs
|
||||
cmake-build-*/
|
||||
|
||||
@ -34,6 +34,7 @@ before_script:
|
||||
stages:
|
||||
- analyse
|
||||
- test
|
||||
- report
|
||||
- build
|
||||
|
||||
include:
|
||||
@ -41,12 +42,9 @@ include:
|
||||
- local: ci/rules.yml
|
||||
- local: ci/env.yml
|
||||
- local: ci/test.yml
|
||||
- local: ci/report.yml
|
||||
- local: ci/build.yml
|
||||
- component: gitlab.protontech.ch/proton/devops/cicd-components/devsecops/gitleaks/scan-repository@~latest
|
||||
inputs:
|
||||
stage: analyse
|
||||
cli-args: "--baseline-path $GITLEAKS_BASELINE"
|
||||
- component: gitlab.protontech.ch/proton/devops/cicd-components/devsecops/grype/scan-code@~latest
|
||||
- component: gitlab.protontech.ch/proton/devops/cicd-components/kits/devsecops/go@~latest
|
||||
inputs:
|
||||
stage: analyse
|
||||
|
||||
|
||||
1
.gitlab/CODEOWNERS
Normal file
1
.gitlab/CODEOWNERS
Normal file
@ -0,0 +1 @@
|
||||
* @go/bridge-ppl/devs
|
||||
@ -2,11 +2,12 @@
|
||||
run:
|
||||
timeout: 10m
|
||||
skip-dirs:
|
||||
- pkg/mime
|
||||
- extern
|
||||
|
||||
issues:
|
||||
exclude-use-default: false
|
||||
exclude-dirs:
|
||||
- pkg/mime
|
||||
- extern
|
||||
exclude:
|
||||
- Using the variable on range scope `tt` in function literal
|
||||
# For now we are missing a lot of comments.
|
||||
@ -87,6 +88,7 @@ linters:
|
||||
- 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]
|
||||
- goheader # Checks is file header matches to pattern [fast: true, auto-fix: false]
|
||||
|
||||
@ -1,10 +1,78 @@
|
||||
# Contribution Policy
|
||||
# Contributing guidelines
|
||||
|
||||
The following document describes how to contribute to the project. In this context, contribution does not only mean code contribution but also reporting issues, requesting new features, or just asking for help.
|
||||
|
||||
## Reporting issues
|
||||
|
||||
In case you experience issues while using the application, our request is to contact Proton customer support directly.
|
||||
|
||||
The benefits of using Proton customer support are
|
||||
|
||||
- Available 24/7/365.
|
||||
- Provides priority support based on subscription type.
|
||||
- Will escalate the issue to the developers every time it becomes too technical or they do not know the answer to a question.
|
||||
- Easier to detect systematic issues by connecting similar reports.
|
||||
- Possible to quickly derive frequency of an issue.
|
||||
- Can assist you to transfer sensitive information safely to us.
|
||||
|
||||
To speed up the communication with customer support, consider the following:
|
||||
|
||||
- Whenever is possible, use the in-app bug report feature. It provides an application specific guide compared to using the generic report form on web.
|
||||
- Whenever is possible, proactively attach logs to your report. Reporting an issue from the application can help you in that.
|
||||
- Check whether your system is officially supported by Proton, including the source of the installer. We cannot provide help when the application is packaged by a third party or when the application is used on systems that we do not prepare to support.
|
||||
- If your report is a feature request, see the Feature request section. In case it is an issue related to application security, see the Security vulnerabilities section.
|
||||
|
||||
In the past, we used GitHub issue tracker for more technical issues in parallel to Proton customer support, but we run into limitations with this approach:
|
||||
|
||||
- Monitoring GitHub issue tracker took development time as it was managed by the development team.
|
||||
- It made issue frequency tracking challenging because we did not have a single point of entry for issues.
|
||||
- Users were confused what technical issue means, and used the GitHub issue tracker for feature requests, or non-technical discussions.
|
||||
- Users sometimes shared sensitive data through the GitHub issue tracker.
|
||||
|
||||
For the above reasons, we do not use GitHub issue tracker anymore but ask you to contact our customer support in case you run into a problem.
|
||||
|
||||
### Security vulnerabilities
|
||||
|
||||
Proton runs a bug bounty program for security vulnerabilities. They differ from normal bug reports in the following ways:
|
||||
|
||||
- These reports go directly to our security team.
|
||||
- They expect deeper explanation of the issue.
|
||||
- Depending on the finding, they may be financially rewarded.
|
||||
|
||||
More information about the program can be found [here](https://proton.me/security/bug-bounty).
|
||||
|
||||
## Feature requests
|
||||
|
||||
What someone considers as a bug is sometimes a feature, and sometimes, a missing feature is considered as a bug. Instead of reporting feature requests as bugs, we setup a UserVoice page to allow our users to share their preferences. UserVoice also makes it possible to vote on other feature requests, making the community preference public.
|
||||
|
||||
Our product team frequently monitors UserVoice, and the features listed there are taken into account in our planning.
|
||||
|
||||
Examples for UserVoice requests:
|
||||
|
||||
- Extending the officially supported environments (e.g., operating systems, clients, or computer architectures).
|
||||
- Requesting new features.
|
||||
- Integration with non-Proton services.
|
||||
|
||||
UserVoice is available [here](https://protonmail.uservoice.com/).
|
||||
|
||||
## Asking for help
|
||||
|
||||
The best ways to get answer for generic questions or to get help with setting up the system is to interact with our active community on [Reddit](https://reddit.com/r/ProtonMail/) or to contact customer support.
|
||||
|
||||
## Code contribution
|
||||
|
||||
We are grateful if you can contribute directly with code. In that case there is nothing else to do than to open a pull request.
|
||||
|
||||
The following is worthwhile noting
|
||||
|
||||
- The project is primarily developed on an internal repository, and the one on GitHub is only a mirror of it. For that reason, the merge request will not be merged on GitHub but added to the project internally. We are keeping the original author in the change set to respect the contribution.
|
||||
- The application is used on numerous platforms and by many third party clients. To have higher chance your change to be accepted, consider all supported dependencies.
|
||||
- Give detailed description of the issue, preferably with test steps to reproduce the original issue, and to verify the fix. It is even better if you also extend the automated tests.
|
||||
|
||||
### Contribution policy
|
||||
|
||||
By making a contribution to this project:
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
@ -63,11 +63,15 @@ Proton Mail Bridge includes the following 3rd party software:
|
||||
* [goleak](https://go.uber.org/goleak) available under [license](https://pkg.go.dev/go.uber.org/goleak?tab=licenses)
|
||||
* [exp](https://golang.org/x/exp) available under [license](https://cs.opensource.google/go/x/exp/+/master:LICENSE)
|
||||
* [net](https://golang.org/x/net) available under [license](https://cs.opensource.google/go/x/net/+/master:LICENSE)
|
||||
* [oauth2](https://golang.org/x/oauth2) available under [license](https://cs.opensource.google/go/x/oauth2/+/master:LICENSE)
|
||||
* [sys](https://golang.org/x/sys) available under [license](https://cs.opensource.google/go/x/sys/+/master:LICENSE)
|
||||
* [text](https://golang.org/x/text) available under [license](https://cs.opensource.google/go/x/text/+/master:LICENSE)
|
||||
* [api](https://google.golang.org/api) available under [license](https://pkg.go.dev/google.golang.org/api?tab=licenses)
|
||||
* [grpc](https://google.golang.org/grpc) available under [license](https://github.com/grpc/grpc-go/blob/master/LICENSE)
|
||||
* [protobuf](https://google.golang.org/protobuf) available under [license](https://github.com/protocolbuffers/protobuf/blob/main/LICENSE)
|
||||
* [plist](https://howett.net/plist) available under [license](https://github.com/DHowett/go-plist/blob/main/LICENSE)
|
||||
* [compute](https://cloud.google.com/go/compute) available under [license](https://pkg.go.dev/cloud.google.com/go/compute?tab=licenses)
|
||||
* [metadata](https://cloud.google.com/go/compute/metadata) available under [license](https://pkg.go.dev/cloud.google.com/go/compute/metadata?tab=licenses)
|
||||
* [bcrypt](https://github.com/ProtonMail/bcrypt) available under [license](https://github.com/ProtonMail/bcrypt/blob/master/LICENSE)
|
||||
* [go-crypto](https://github.com/ProtonMail/go-crypto) available under [license](https://github.com/ProtonMail/go-crypto/blob/master/LICENSE)
|
||||
* [go-mime](https://github.com/ProtonMail/go-mime) available under [license](https://github.com/ProtonMail/go-mime/blob/master/LICENSE)
|
||||
@ -95,8 +99,11 @@ Proton Mail Bridge includes the following 3rd party software:
|
||||
* [validator](https://github.com/go-playground/validator/v10) available under [license](https://github.com/go-playground/validator/v10/blob/master/LICENSE)
|
||||
* [go-json](https://github.com/goccy/go-json) available under [license](https://github.com/goccy/go-json/blob/master/LICENSE)
|
||||
* [uuid](https://github.com/gofrs/uuid) available under [license](https://github.com/gofrs/uuid/blob/master/LICENSE)
|
||||
* [groupcache](https://github.com/golang/groupcache) available under [license](https://github.com/golang/groupcache/blob/master/LICENSE)
|
||||
* [protobuf](https://github.com/golang/protobuf) available under [license](https://github.com/golang/protobuf/blob/master/LICENSE)
|
||||
* [pprof](https://github.com/google/pprof) available under [license](https://github.com/google/pprof/blob/master/LICENSE)
|
||||
* [enterprise-certificate-proxy](https://github.com/googleapis/enterprise-certificate-proxy) available under [license](https://github.com/googleapis/enterprise-certificate-proxy/blob/master/LICENSE)
|
||||
* [gax-go](https://github.com/googleapis/gax-go/v2) available under [license](https://github.com/googleapis/gax-go/v2/blob/master/LICENSE)
|
||||
* [errwrap](https://github.com/hashicorp/errwrap) available under [license](https://github.com/hashicorp/errwrap/blob/master/LICENSE)
|
||||
* [go-immutable-radix](https://github.com/hashicorp/go-immutable-radix) available under [license](https://github.com/hashicorp/go-immutable-radix/blob/master/LICENSE)
|
||||
* [go-memdb](https://github.com/hashicorp/go-memdb) available under [license](https://github.com/hashicorp/go-memdb/blob/master/LICENSE)
|
||||
@ -124,14 +131,16 @@ Proton Mail Bridge includes the following 3rd party software:
|
||||
* [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)
|
||||
* [smetrics](https://github.com/xrash/smetrics) available under [license](https://github.com/xrash/smetrics/blob/master/LICENSE)
|
||||
* [go-ordered-json](https://gitlab.com/c0b/go-ordered-json)
|
||||
* [go-ordered-json](https://gitlab.com/c0b/go-ordered-json) available under [license](https://gitlab.com/c0b/go-ordered-json/blob/master/LICENSE)
|
||||
* [go.opencensus.io](https://pkg.go.dev/go.opencensus.io?tab=licenses) available under [license](https://pkg.go.dev/go.opencensus.io?tab=licenses)
|
||||
* [arch](https://golang.org/x/arch) available under [license](https://cs.opensource.google/go/x/arch/+/master:LICENSE)
|
||||
* [crypto](https://golang.org/x/crypto) available under [license](https://cs.opensource.google/go/x/crypto/+/master:LICENSE)
|
||||
* [mod](https://golang.org/x/mod) available under [license](https://cs.opensource.google/go/x/mod/+/master:LICENSE)
|
||||
* [sync](https://golang.org/x/sync) available under [license](https://cs.opensource.google/go/x/sync/+/master:LICENSE)
|
||||
* [tools](https://golang.org/x/tools) available under [license](https://cs.opensource.google/go/x/tools/+/master:LICENSE)
|
||||
* [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)
|
||||
* [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-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)
|
||||
|
||||
60
Changelog.md
60
Changelog.md
@ -3,6 +3,66 @@
|
||||
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
|
||||
|
||||
## Erasmus Bridge 3.15.1
|
||||
|
||||
### Changed
|
||||
* BRIDGE-281: Disable keychain test on macOS.
|
||||
|
||||
|
||||
## Erasmus Bridge 3.15.0
|
||||
|
||||
### Added
|
||||
* BRIDGE-238: Added host information to sentry events; new sentry event for keychain issues.
|
||||
* BRIDGE-236: Added SMTP observability metrics.
|
||||
* BRIDGE-217: Added missing parameter to the CLI help command.
|
||||
* BRIDGE-234: Add accessibility name in QML for UI automation.
|
||||
* BRIDGE-232: Test: Add Home Menu Bridge UI e2e automation tests.
|
||||
* BRIDGE-220: Test: Add Bridge E2E UI login/logout tests for Windows.
|
||||
|
||||
### Changed
|
||||
* BRIDGE-228: Removed sentry events.
|
||||
* BRIDGE-218: Observability adapter; gluon observability metrics and tests.
|
||||
* BRIDGE-215: Tweak wording on macOS profile install page.
|
||||
* BRIDGE-131: Test: Integration tests for messages from Proton <-> Gmail.
|
||||
* BRIDGE-142: Bridge icon can be removed from the menu bar on macOS.
|
||||
|
||||
### Fixed
|
||||
* BRIDGE-240: Fix for running against Qt 6.8 (contribution of GitHub user Cimbali).
|
||||
* BRIDGE-231: Fix reversed header order in messages.
|
||||
* BRIDGE-235: Fix compilation of Bridge GUI Tester on Windows.
|
||||
* BRIDGE-120: Use appropriate address key when importing / saving draft.
|
||||
|
||||
|
||||
## Dragon Bridge 3.14.0
|
||||
|
||||
### Changed
|
||||
* BRIDGE-207: Failure to download or verify an update now fails silently.
|
||||
* BRIDGE-204: Removed redundant Sentry events.
|
||||
* BRIDGE-150: Observability service modification.
|
||||
* BRIDGE-210: Reduced log level of cache events so they won't be printed to stdout.
|
||||
|
||||
### Fixed
|
||||
* BRIDGE-106: Fixed import of multipart-related messages.
|
||||
* BRIDGE-108: Fixed GetInitials when empty username is passed.
|
||||
|
||||
|
||||
## Colorado Bridge 3.13.0
|
||||
|
||||
### Added
|
||||
* BRIDGE-37: added message broadcasting functionality.
|
||||
* BRIDGE-122: added observability service.
|
||||
* BRIDGE-119: added support for Feature Flags.
|
||||
* BRIDGE-116: added command-line switches to enable/disable keychain check on macOS.
|
||||
* BRIDGE-88: added context menu for quick actions on input labels: cut, copy, paste.
|
||||
|
||||
### Changed
|
||||
* BRIDGE-81: KB article suggestion updates + more weight for long keywords.
|
||||
|
||||
### Fixed
|
||||
* BRIDGE-67: Added detection for username changes on macOS & automatic reconfiguration.
|
||||
* BRIDGE-138: Remove deprecated doc.
|
||||
|
||||
|
||||
## Bastei Bridge 3.12.0
|
||||
|
||||
### Added
|
||||
|
||||
7
Makefile
7
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.12.0+git
|
||||
BRIDGE_APP_VERSION?=3.15.1+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.55.2"
|
||||
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
|
||||
@ -264,7 +264,8 @@ test-integration-race: gofiles
|
||||
|
||||
test-integration-nightly: gofiles
|
||||
mkdir -p coverage/integration
|
||||
go test \
|
||||
gotestsum \
|
||||
--junitfile tests/result/feature-tests.xml -- \
|
||||
-v -timeout=90m -p=1 -count=1 -tags=test_integration \
|
||||
${GOCOVERAGE} \
|
||||
github.com/ProtonMail/proton-bridge/v3/tests \
|
||||
|
||||
14
README.md
14
README.md
@ -1,7 +1,7 @@
|
||||
# Proton Mail Bridge and Import Export app
|
||||
# Proton Mail Bridge
|
||||
Copyright (c) 2024 Proton AG
|
||||
|
||||
This repository holds the Proton Mail Bridge and the Proton Mail Import-Export applications.
|
||||
This repository holds the Proton Mail Bridge application.
|
||||
For a detailed build information see [BUILDS](./BUILDS.md).
|
||||
The license can be found in [LICENSE](./LICENSE) file, for more licensing information see [COPYING_NOTES](./COPYING_NOTES.md).
|
||||
For contribution policy see [CONTRIBUTING](./CONTRIBUTING.md).
|
||||
@ -13,7 +13,7 @@ Proton Mail Bridge for e-mail clients.
|
||||
When launched, Bridge will initialize local IMAP/SMTP servers and render
|
||||
its GUI.
|
||||
|
||||
To configure an e-mail client, firstly log in using your Proton Mail credentials.
|
||||
To configure an e-mail client, first log in using your Proton Mail credentials.
|
||||
Open your e-mail client and add a new account using the settings which are
|
||||
located in the Bridge GUI. The client will only be able to sync with
|
||||
your Proton Mail account when the Bridge is running, thus the option
|
||||
@ -24,10 +24,10 @@ background.
|
||||
|
||||
More details [on the public website](https://proton.me/mail/bridge).
|
||||
|
||||
## Launchers
|
||||
Launchers are binaries used to run the Proton Mail Bridge or Import-Export apps.
|
||||
## Launcher
|
||||
The launcher is a binary used to run the Proton Mail Bridge.
|
||||
|
||||
Official distributions of the Proton Mail Bridge and Import-Export apps contain
|
||||
The Official distribution of the Proton Mail Bridge application contains
|
||||
both a launcher and the app itself. The launcher is installed in a protected
|
||||
area of the system (i.e. an area accessible only with admin privileges) and is
|
||||
used to run the app. The launcher ensures that nobody tampered with the app's
|
||||
@ -37,7 +37,7 @@ feature enables the app to securely update itself automatically without asking
|
||||
the user for a password.
|
||||
|
||||
## Keychain
|
||||
You need to have a keychain in order to run the Proton Mail Bridge. On Mac or
|
||||
You need to have a keychain in order to run Proton Mail Bridge. On Mac or
|
||||
Windows, Bridge uses native credential managers. On Linux, use `secret-service` freedesktop.org API
|
||||
(e.g. [Gnome keyring](https://wiki.gnome.org/Projects/GnomeKeyring/))
|
||||
or
|
||||
|
||||
25
ci/report.yml
Normal file
25
ci/report.yml
Normal file
@ -0,0 +1,25 @@
|
||||
|
||||
---
|
||||
|
||||
include:
|
||||
- project: 'tpe/testmo-reporter'
|
||||
ref: master
|
||||
file: '/scenarios/testmo-script.yml'
|
||||
|
||||
testmo-upload:
|
||||
stage: report
|
||||
extends:
|
||||
- .testmo-upload
|
||||
- .rules-branch-manual-scheduled-and-test-branch-always
|
||||
needs:
|
||||
- test-integration-nightly
|
||||
before_script: []
|
||||
variables:
|
||||
TESTMO_TOKEN: "$TESTMO_TOKEN"
|
||||
TESTMO_URL: "https://proton.testmo.net"
|
||||
PROJECT_ID: "9"
|
||||
NAME: "Nightly integration tests"
|
||||
MILESTONE: "Nightly integration tests"
|
||||
SOURCE: "test-integration-nightly"
|
||||
TAGS: "$CI_COMMIT_REF_SLUG"
|
||||
RESULT_FOLDER: "tests/result/*.xml"
|
||||
@ -108,6 +108,7 @@ test-integration-nightly:
|
||||
artifacts:
|
||||
when: always
|
||||
paths:
|
||||
- tests/result/feature-tests.xml
|
||||
- nightly-job.log
|
||||
|
||||
test-coverage:
|
||||
|
||||
135
doc/bridge.md
135
doc/bridge.md
@ -1,135 +0,0 @@
|
||||
# Bridge
|
||||
|
||||
## Main blocks
|
||||
|
||||
This is basic overview of the main bridge blocks.
|
||||
|
||||
Note connection between IMAP/SMTP and PMAPI. IMAP and SMTP packages are in the queue to be refactored
|
||||
and we would like to try to have functionality in bridge core or bridge utilities (such as messages)
|
||||
than direct usage of PMAPI from IMAP or SMTP. Also database (BoltDB) should be moved to bridge core.
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
S[Server]
|
||||
C[Client]
|
||||
U[User]
|
||||
|
||||
subgraph "Bridge app"
|
||||
Core[Bridge core]
|
||||
API[PMAPI]
|
||||
Store
|
||||
DB[BoltDB]
|
||||
Frontend["Qt / CLI"]
|
||||
IMAP
|
||||
SMTP
|
||||
|
||||
IMAP --> Store
|
||||
IMAP --> Core
|
||||
SMTP --> Core
|
||||
SMTP --> API
|
||||
Core --> API
|
||||
Core --> Store
|
||||
Store --> API
|
||||
Store --> DB
|
||||
Frontend --> Core
|
||||
|
||||
end
|
||||
|
||||
C --> IMAP
|
||||
C --> SMTP
|
||||
U --> Frontend
|
||||
API --> S
|
||||
```
|
||||
|
||||
## Code structure
|
||||
|
||||
More detailed graph of main types used in Bridge app and connection between them. Here is already
|
||||
communication to PMAPI only from bridge core which is not true, yet. IMAP and SMTP are still calling
|
||||
PMAPI directly.
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
|
||||
C["Client (e.g. Thunderbird)"]
|
||||
PM[Proton Mail Server]
|
||||
|
||||
subgraph "Bridge app"
|
||||
subgraph "Bridge core"
|
||||
B[Bridge]
|
||||
U[User]
|
||||
|
||||
B --> U
|
||||
end
|
||||
|
||||
subgraph Store
|
||||
StoreU[Store User]
|
||||
StoreA[Address]
|
||||
StoreM[Mailbox]
|
||||
|
||||
StoreU --> StoreA
|
||||
StoreA --> StoreM
|
||||
end
|
||||
|
||||
subgraph Credentials
|
||||
CredStore[Store]
|
||||
Creds[Credentials]
|
||||
|
||||
CredStore --> Creds
|
||||
end
|
||||
|
||||
subgraph Frontend
|
||||
CLI
|
||||
Qt
|
||||
end
|
||||
|
||||
subgraph IMAP
|
||||
IB[IMAP backend]
|
||||
IA[IMAP address]
|
||||
IM[IMAP mailbox]
|
||||
|
||||
IB --> B
|
||||
IB --> IA
|
||||
IA --> IM
|
||||
IA --> U
|
||||
IA --> StoreA
|
||||
IM --> StoreM
|
||||
end
|
||||
|
||||
subgraph SMTP
|
||||
SB[SMTP backend]
|
||||
SS[SMTP session]
|
||||
|
||||
SB --> B
|
||||
SB --> SS
|
||||
SS --> U
|
||||
end
|
||||
end
|
||||
|
||||
subgraph PMAPI
|
||||
AC[Client]
|
||||
end
|
||||
|
||||
C --> IB
|
||||
C --> SB
|
||||
|
||||
CLI --> B
|
||||
Qt --> B
|
||||
|
||||
U --> CredStore
|
||||
U --> Creds
|
||||
|
||||
U --> StoreU
|
||||
|
||||
StoreU --> AC
|
||||
StoreA --> AC
|
||||
StoreM --> AC
|
||||
|
||||
B --> AC
|
||||
U --> AC
|
||||
|
||||
AC --> PM
|
||||
```
|
||||
|
||||
## How to debug
|
||||
|
||||
Run `make run-debug` which starts [Delve](https://github.com/go-delve/delve).
|
||||
@ -1,114 +0,0 @@
|
||||
# Communication
|
||||
|
||||
## First login and sync
|
||||
|
||||
When user logs in to the bridge for the first time, immediately starts the first sync.
|
||||
First sync downloads all headers of all e-mails and creates database to have proper UIDs
|
||||
and indexes for IMAP. See [database](database.md) for more information.
|
||||
|
||||
By default, whenever it's possible, sync downloads only all e-mails maiblox which already
|
||||
have list of labels so we can construct all mailboxes (inbox, sent, trash, custom folders
|
||||
and labels) without need to download each e-mail headers many times.
|
||||
|
||||
Note that we need to download also bodies to calculate size of the e-mail and set proper
|
||||
content type (clients uses content type for guess if e-mail contains attachment)--but only
|
||||
body, not attachment. Also it's downloaded only for the first time. After that we store
|
||||
those information in our database so next time we only sync headers, labels and so on.
|
||||
|
||||
First sync takes some time. List of 150 messages takes about second and then we need to
|
||||
download bodies for each message. We still need to do some optimalizations. Anyway, if
|
||||
user has reasonable amount of e-mails, there is good chance user will see e-mails in the
|
||||
client right after adding account.
|
||||
|
||||
When account is added to client, client start the sync. This sync will ask Bridge app
|
||||
for all headers (done quickly) and then starts to download all bodies and attachment.
|
||||
Unfortunately for some e-mail more than once if the same e-mail is in more mailboxes
|
||||
(e.g. inbox and all mail)--there is no way to tell over IMAP it's the same message.
|
||||
|
||||
After successful login of client to IMAP, Bridge starts event loop. That periodicly ask
|
||||
servers (each 30 seconds) for new updates (new message, keys, …).
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant S as Server
|
||||
participant B as Bridge
|
||||
participant C as Client
|
||||
|
||||
Note right of B: Set up PM account<br/>by user
|
||||
|
||||
loop First sync
|
||||
B ->> S: Fetch body and attachments
|
||||
Note right of B: Build local database<br/>(e-mail UIDs)
|
||||
end
|
||||
|
||||
Note right of C: Set up IMAP/SMTP<br/>by user
|
||||
|
||||
C ->> B: IMAP login
|
||||
B ->> S: Authenticate user
|
||||
Note right of B: Create IMAP user
|
||||
|
||||
loop Event loop, every 30 sec
|
||||
B ->> S: Fetch e-mail headers
|
||||
B ->> C: Send IMAP IDLE response
|
||||
end
|
||||
|
||||
C ->> B: IMAP LIST directories
|
||||
|
||||
loop Client sync
|
||||
C ->> B: IMAP SELECT directory
|
||||
C ->> B: IMAP SEARCH e-mails UIDs
|
||||
C ->> B: IMAP FETCH of e-mail UID
|
||||
B ->> S: Fetch body and attachments
|
||||
Note right of B: Decrypt message<br/>and attachment
|
||||
B ->> C: IMAP response
|
||||
end
|
||||
```
|
||||
|
||||
## IMAP IDLE extension
|
||||
|
||||
IMAP IDLE is extension, it has to be supported by both client and server. IMAP server (in our case
|
||||
the bridge) supports it so clients can use it. It works by issuing `IDLE` command by the client and
|
||||
keeps the connection open. When the server has some update, server (the bridge) will respond to that
|
||||
by `EXISTS` (new message), `APPEND` (imported message), `EXPUNGE` (deleted message) or `MOVE` response.
|
||||
|
||||
Even when there is connection with IDLE open, server can mark the client as inactive. Therefore,
|
||||
it's recommended the client should reissue the connection after each 29 minutes. This is not the
|
||||
real push and can fail!
|
||||
|
||||
Our event loop is also simple pull and it will trigger IMAP IDLE when we get some new update from
|
||||
the server. Would be good to have push from the server, but we need to wait for the support on API.
|
||||
|
||||
RFC: https://tools.ietf.org/html/rfc2177
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant S as Server
|
||||
participant B as Bridge
|
||||
participant C as Client
|
||||
|
||||
C ->> B: IMAP IDLE
|
||||
|
||||
loop Every 30 seconds
|
||||
S ->> B: Checking events
|
||||
B ->> C: IMAP response
|
||||
end
|
||||
```
|
||||
|
||||
## Sending e-mails
|
||||
|
||||
E-mail are sent over standard SMTP protocol. Our bridge takes the message, encrypts and sent it
|
||||
further to our server which will then send the message to its final destination. The important
|
||||
and tricky part is encryption. See [encryption](encryption.md) or [PMEL document](https://docs.google.com/document/d/1lEBkG0DC5FOWlumInKtu4a9Cc1Eszp48ZhFy9UpPQso/edit)
|
||||
for more information.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant S as Server
|
||||
participant B as Bridge
|
||||
participant C as Client
|
||||
|
||||
C ->> B: SMTP send e-mail
|
||||
Note right of B: Encrypt messages
|
||||
B ->> S: Send encrypted e-mail
|
||||
B ->> C: Respond OK
|
||||
```
|
||||
@ -1,27 +0,0 @@
|
||||
# Database
|
||||
|
||||
Bridge needs to have a small database to pair our IDs with IMAP UIDs and indexes. IMAP protocol
|
||||
requires every message to have an unique UID in mailbox. In this context, mailbox is not an account,
|
||||
but a folder or label. This means that one message can have more UIDs, one for each mailbox (folder),
|
||||
and that two messages can have the same UID, but each for different mailbox (folder).
|
||||
|
||||
IMAP index is just an index. Look at it like to an array: `["UID1", "UID2", "UID3"]`. We can access
|
||||
message by UID or index; for example index 2 and UID `UID2`. When this message is deleted, we need
|
||||
to re-index all following messages. The array will look now like `["UID1", "UID3"]` and the last
|
||||
message can be accessed by index 2 or UID `UID3`.
|
||||
|
||||
See RFCs for more information:
|
||||
|
||||
* https://tools.ietf.org/html/rfc822
|
||||
* https://tools.ietf.org/html/rfc3501
|
||||
|
||||
Our database is currently built on BBolt and have those buckets (key-value storage):
|
||||
|
||||
* Message metadata bucket:
|
||||
|
||||
* `[metadataBucket][API_ID] -> pmapi.Message{subject, from, to, size, other headers...}` (without body or attachment)
|
||||
|
||||
* Mapping buckets
|
||||
|
||||
* `[mailboxesBucket][addressID-mailboxID][api_ids][API_ID] -> UID`
|
||||
* `[mailboxesBucket][addressID-mailboxID][imap_ids][UID] -> API_ID`
|
||||
@ -1,12 +0,0 @@
|
||||
# Encryption
|
||||
|
||||
Encryption is done in PMAPI, bridge utils and bridge itself. The best would be to keep encryption
|
||||
in PMAPI and bridge utils (in package such as messages). All packages are using our high-level
|
||||
GopenPGP library on top of OpenPGP.
|
||||
|
||||
## `gopenpgp.KeyRing`
|
||||
|
||||
We use one `KeyRing` per address. Our usage then contains all keys for specific address. Primary
|
||||
key is always on the first position, then there old ones to be able to decrypt last e-mail.
|
||||
OpenPGP encrypts given message with all available keys, so we need to first get first (primary)
|
||||
key for encryption to have message encrypted only once with primary key.
|
||||
@ -1,9 +0,0 @@
|
||||
# Bridge Documentation
|
||||
|
||||
Documentation pages in order to read for a novice:
|
||||
|
||||
* [Bridge code](bridge.md)
|
||||
* [Internal Bridge database](database.md)
|
||||
* [Communication between Bridge, Client and Server](communication.md)
|
||||
* [Encryption](encryption.md)
|
||||
|
||||
103
doc/updates.md
103
doc/updates.md
@ -1,103 +0,0 @@
|
||||
# Update mechanism of Bridge
|
||||
|
||||
There are multiple options how to change version of application:
|
||||
* Automatic in-app update
|
||||
* Manual in-app update
|
||||
* Manual install
|
||||
|
||||
In-app update ends with restarting bridge into new version. Automatic in-app
|
||||
update is downloading, verifying and installing the new version immediately
|
||||
without user confirmation. For manual in-app update user needs to confirm first.
|
||||
Update is done from special update file published on website.
|
||||
|
||||
The manual installation requires user to download, verify and install manually
|
||||
using installer for given OS.
|
||||
|
||||
The bridge is installed and executed differently for given OS:
|
||||
|
||||
* Windows and Linux apps are using launcher mechanism:
|
||||
* There is system protected installation path which is created on first
|
||||
install. It contains bridge exe and launcher exe. When users starts
|
||||
bridge the launcher is executed first. It will check update path compare
|
||||
version with installed one. The newer version then is then executed.
|
||||
* Update mechanism means to replace files in update folder which is located
|
||||
in user space.
|
||||
|
||||
* macOS app does not use launcher
|
||||
* No launcher, only one executable
|
||||
* In-App update replaces the bridge files in installation path directly
|
||||
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph Frontend
|
||||
U[User requests<br>version check]
|
||||
ManIns((Notify user about<br>manual install<br>is needed))
|
||||
R((Notify user<br>about restart))
|
||||
ManUp((Notify user about<br>manual update))
|
||||
NF((Notify user about<br>force update))
|
||||
|
||||
ManUp -->|Install| InstFront[Install]
|
||||
InstFront -->|Ok| R
|
||||
InstFront -->|Error| ManIns
|
||||
|
||||
U --> CheckFront[Check online]
|
||||
CheckFront -->|Ok| IAFront{Is new version<br>and applicable?}
|
||||
CheckFront -->|Error| ManIns
|
||||
|
||||
IAFront -->|No| Latest((Notify user<br>has latest version))
|
||||
IAFront -->|Yes| CanInstall{Can update?}
|
||||
CanInstall -->|No| ManIns
|
||||
CanInstall -->|Yes| NotifOrInstall{Is automatic<br>update enabled?}
|
||||
NotifOrInstall -->|Manual| ManUp
|
||||
end
|
||||
|
||||
|
||||
subgraph Backend
|
||||
W[Wait for next check]
|
||||
|
||||
W --> Check[Check online]
|
||||
|
||||
Check --> NV{Has new<br>version?}
|
||||
Check -->|Error| W
|
||||
NV -->|No new version| W
|
||||
IA{Is install<br>applicable?}
|
||||
NV -->|New version<br>available| IA
|
||||
IA -->|Local rollout<br>not enough| W
|
||||
IA -->|Yes| AU{Is automatic\nupdate enabled?}
|
||||
|
||||
AU -->|Yes| CanUp{Can update?}
|
||||
CanUp -->|No| ManIns
|
||||
|
||||
CanUp -->|Yes| Ins[Install]
|
||||
Ins -->|Error| ManIns
|
||||
Ins -->|Ok| R
|
||||
|
||||
AU -->|No| ManUp
|
||||
ManUp -->|Ignore| W
|
||||
|
||||
|
||||
F[Force update]
|
||||
F --> NF
|
||||
end
|
||||
|
||||
ManIns --> Web[Open web page]
|
||||
NF --> Web
|
||||
ManUp --> Web
|
||||
R --> Re[Restart]
|
||||
NF --> Q[Quit bridge]
|
||||
NotifOrInstall -->|Automatic| W
|
||||
```
|
||||
|
||||
|
||||
The non-trivial is to combine the update with setting change:
|
||||
* turn off/on automatic in-app updates
|
||||
* change from stable to beta or back
|
||||
|
||||
_TODO fill flow chart details_
|
||||
|
||||
|
||||
We are not support downgrade functionality. Only some circumstances can lead to
|
||||
downgrading the app version.
|
||||
|
||||
_TODO fill flow chart details_
|
||||
15
go.mod
15
go.mod
@ -7,9 +7,9 @@ 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.20240514133734-79cdd0fec41c
|
||||
github.com/ProtonMail/gluon v0.17.1-0.20241121121545-aa1cfd19b4b2
|
||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20240605113119-1a81ec7dc72d
|
||||
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
|
||||
@ -47,14 +47,18 @@ require (
|
||||
go.uber.org/goleak v1.2.1
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
|
||||
golang.org/x/net v0.24.0
|
||||
golang.org/x/oauth2 v0.7.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
|
||||
howett.net/plist v1.0.0
|
||||
)
|
||||
|
||||
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 v0.0.0-20230717121622-edf196117233 // indirect
|
||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect
|
||||
@ -82,8 +86,11 @@ require (
|
||||
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/gofrs/uuid v4.3.0+incompatible // indirect
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.7.1 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
||||
github.com/hashicorp/go-memdb v1.3.3 // indirect
|
||||
@ -112,17 +119,19 @@ require (
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||
gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/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/emersion/go-message => github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7
|
||||
github.com/emersion/go-message => github.com/ProtonMail/go-message v0.13.1-0.20240919135104-3bc88e6a9423
|
||||
github.com/emersion/go-smtp => github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865
|
||||
github.com/go-resty/resty/v2 => github.com/LBeernaertProton/resty/v2 v2.0.0-20231129100320-dddf8030d93a
|
||||
github.com/keybase/go-keychain => github.com/cuthix/go-keychain v0.0.0-20240103134243-0b6a41580b77
|
||||
|
||||
75
go.sum
75
go.sum
@ -5,9 +5,16 @@ cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6A
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go v0.110.0 h1:Zc8gqp3+a9/Eyph2KDmcGaPtbKRIoqq4YTlL4NMD0Ys=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/compute v1.19.1 h1:am86mquDUgjGNWxiGn+5PGLbmgiWXlE/yNWpIpNvuXY=
|
||||
cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE=
|
||||
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
|
||||
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
|
||||
cloud.google.com/go/longrunning v0.4.1 h1:v+yFJOfKC3yZdY6ZUI933pIYdhyhV8S3NpWrXWmg7jM=
|
||||
cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
@ -27,23 +34,19 @@ 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.20240423123310-0266b0f75d41 h1:Lu2hKO4fcHeMcbZOon129iM1dAy0ERwZkJtuNQCLlOQ=
|
||||
github.com/ProtonMail/gluon v0.17.1-0.20240423123310-0266b0f75d41/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
|
||||
github.com/ProtonMail/gluon v0.17.1-0.20240514133734-79cdd0fec41c h1:P3SvCACt13Zqdj0IRDB4bgwqI68+oMB2j0uVuPQyoTw=
|
||||
github.com/ProtonMail/gluon v0.17.1-0.20240514133734-79cdd0fec41c/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
|
||||
github.com/ProtonMail/gluon v0.17.1-0.20241121121545-aa1cfd19b4b2 h1:iZjKvjb6VkGb52ZaBBiXC1MGYJN4C/S97JfppdzpMHQ=
|
||||
github.com/ProtonMail/gluon v0.17.1-0.20241121121545-aa1cfd19b4b2/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
|
||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
|
||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-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.20230526094639-b62c999c85b7 h1:+j+Kd/DyZ/qGfMT9htAT7HxqIEbZHsatsx+m8AoV6fc=
|
||||
github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
|
||||
github.com/ProtonMail/go-message v0.13.1-0.20240919135104-3bc88e6a9423 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.20240423123404-a6163268401c h1:3U245DPGyL+LeAcJzFSg+E2lShXx+z/lBHM2v9P5mEg=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20240423123404-a6163268401c/go.mod h1:3A0cpdo0BIenIPjTG6u8EbzJ8uuJy7rVvM/NaynjCKA=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20240605113119-1a81ec7dc72d h1:B9/ZLubPWIY4uvATviFoCUoLauq98C3Bbt4v0A2VEdU=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20240605113119-1a81ec7dc72d/go.mod h1:3A0cpdo0BIenIPjTG6u8EbzJ8uuJy7rVvM/NaynjCKA=
|
||||
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=
|
||||
@ -78,6 +81,7 @@ github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7N
|
||||
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=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||
@ -94,6 +98,7 @@ github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtM
|
||||
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=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
@ -145,6 +150,10 @@ github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwo
|
||||
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
|
||||
github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3 h1:hQ1wTMaKcGfobYRT88RM8NFNyX+IQHvagkm/tqViU98=
|
||||
github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
@ -197,6 +206,8 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69
|
||||
github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff/go.mod h1:wfqRWLHRBsRgkp5dmbG56SA0DmVtwrF5N3oPdI8t+Aw=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
@ -205,6 +216,13 @@ github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+Licev
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
@ -212,6 +230,10 @@ github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Z
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
@ -222,10 +244,15 @@ github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OI
|
||||
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y=
|
||||
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/googleapis/gax-go/v2 v2.7.1 h1:gF4c0zjUP2H/s/hEGyLA3I0fA2ZWjzYiONAD6cvPr8A=
|
||||
github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
@ -371,6 +398,7 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP
|
||||
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
@ -452,6 +480,8 @@ gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a/go.mod h1:NREv
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
|
||||
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
|
||||
@ -466,6 +496,7 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-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=
|
||||
@ -514,6 +545,7 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
@ -531,6 +563,8 @@ 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=
|
||||
golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g=
|
||||
golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@ -561,6 +595,7 @@ golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200720211630-cb9d2d5c5666/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@ -620,6 +655,7 @@ golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
@ -644,10 +680,14 @@ google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.114.0 h1:1xQPji6cO2E2vLiI+C/XiFAnsn1WV3mjaEwGLhi3grE=
|
||||
google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
@ -657,13 +697,27 @@ google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
|
||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc=
|
||||
google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
@ -691,6 +745,7 @@ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
|
||||
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
||||
|
||||
@ -75,6 +75,13 @@ const (
|
||||
|
||||
flagLogIMAP = "log-imap"
|
||||
flagLogSMTP = "log-smtp"
|
||||
|
||||
flagEnableKeychainTest = "enable-keychain-test"
|
||||
flagDisableKeychainTest = "disable-keychain-test"
|
||||
|
||||
flagSoftwareRenderer = "software-renderer"
|
||||
flagSetSoftwareRenderer = "set-software-renderer"
|
||||
flagSetHardwareRenderer = "set-hardware-renderer"
|
||||
)
|
||||
|
||||
// Hidden flags.
|
||||
@ -82,7 +89,6 @@ const (
|
||||
flagLauncher = "launcher"
|
||||
flagNoWindow = "no-window"
|
||||
flagParentPID = "parent-pid"
|
||||
flagSoftwareRenderer = "software-renderer"
|
||||
FlagSessionID = "session-id"
|
||||
)
|
||||
|
||||
@ -91,6 +97,23 @@ 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",
|
||||
Value: false,
|
||||
DisableDefaultText: true,
|
||||
Hidden: true,
|
||||
}
|
||||
|
||||
var cliFlagDisableKeychainTest = &cli.BoolFlag{ //nolint:gochecknoglobals
|
||||
Name: flagDisableKeychainTest,
|
||||
Usage: "This flag is deprecated and does nothing",
|
||||
Value: false,
|
||||
DisableDefaultText: true,
|
||||
Hidden: true,
|
||||
}
|
||||
|
||||
func New() *cli.App {
|
||||
app := cli.NewApp()
|
||||
|
||||
@ -140,6 +163,24 @@ func New() *cli.App {
|
||||
Name: flagLogSMTP,
|
||||
Usage: "Enable logging of SMTP communications (may contain decrypted data!)",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flagSoftwareRenderer, // This flag is ignored by bridge, but should be passed to launcher in case of restart, so it need to be accepted by the CLI parser.
|
||||
Usage: "Use software rendering of the GUI for the current execution of the application",
|
||||
Value: false,
|
||||
DisableDefaultText: true,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flagSetSoftwareRenderer, // This flag is ignored by bridge, we just want it to be shown in the help (BRIDGE-217).
|
||||
Usage: "Toggle software rendering of the GUI for the current and future executions of the application",
|
||||
Value: false,
|
||||
DisableDefaultText: true,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flagSetHardwareRenderer, // This flag is ignored by bridge, we just want it to be shown in the help (BRIDGE-217).
|
||||
Usage: "Toggle hardware rendering of the GUI for the current and future executions of the application",
|
||||
Value: false,
|
||||
DisableDefaultText: true,
|
||||
},
|
||||
|
||||
// Hidden flags
|
||||
&cli.BoolFlag{
|
||||
@ -158,18 +199,26 @@ func New() *cli.App {
|
||||
Hidden: true,
|
||||
Value: -1,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: flagSoftwareRenderer, // This flag is ignored by bridge, but should be passed to launcher in case of restart, so it need to be accepted by the CLI parser.
|
||||
Usage: "GUI is using software renderer",
|
||||
Hidden: true,
|
||||
Value: false,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: FlagSessionID,
|
||||
Hidden: true,
|
||||
},
|
||||
}
|
||||
|
||||
// We override the default help value because we want "Show" to be capitalized
|
||||
cli.HelpFlag = &cli.BoolFlag{
|
||||
Name: "help",
|
||||
Aliases: []string{"h"},
|
||||
Usage: "Show help",
|
||||
DisableDefaultText: true,
|
||||
}
|
||||
|
||||
if onMacOS() {
|
||||
// The two flags below were introduced for BRIDGE-116, and are available only on macOS.
|
||||
// They have been later removed fro BRIDGE-281.
|
||||
app.Flags = append(app.Flags, cliFlagEnableKeychainTest, cliFlagDisableKeychainTest)
|
||||
}
|
||||
|
||||
app.Action = run
|
||||
|
||||
return app
|
||||
@ -240,7 +289,7 @@ func run(c *cli.Context) error {
|
||||
// Look for available keychains
|
||||
return WithKeychainList(crashHandler, func(keychains *keychain.List) error {
|
||||
// Unlock the encrypted vault.
|
||||
return WithVault(locations, keychains, crashHandler, func(v *vault.Vault, insecure, corrupt bool) error {
|
||||
return WithVault(reporter, locations, keychains, crashHandler, func(v *vault.Vault, insecure, corrupt bool) error {
|
||||
if !v.Migrated() {
|
||||
// Migrate old settings into the vault.
|
||||
if err := migrateOldSettings(v); err != nil {
|
||||
@ -526,3 +575,7 @@ func setDeviceCookies(jar *cookies.Jar) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func onMacOS() bool {
|
||||
return runtime.GOOS == "darwin"
|
||||
}
|
||||
|
||||
@ -25,17 +25,18 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/certs"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/sentry"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func WithVault(locations *locations.Locations, keychains *keychain.List, panicHandler async.PanicHandler, fn func(*vault.Vault, bool, bool) error) error {
|
||||
func WithVault(reporter *sentry.Reporter, locations *locations.Locations, keychains *keychain.List, panicHandler async.PanicHandler, fn func(*vault.Vault, bool, bool) error) error {
|
||||
logrus.Debug("Creating vault")
|
||||
defer logrus.Debug("Vault stopped")
|
||||
|
||||
// Create the encVault.
|
||||
encVault, insecure, corrupt, err := newVault(locations, keychains, panicHandler)
|
||||
encVault, insecure, corrupt, err := newVault(reporter, locations, keychains, panicHandler)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create vault: %w", err)
|
||||
}
|
||||
@ -57,7 +58,7 @@ func WithVault(locations *locations.Locations, keychains *keychain.List, panicHa
|
||||
return fn(encVault, insecure, corrupt != nil)
|
||||
}
|
||||
|
||||
func newVault(locations *locations.Locations, keychains *keychain.List, panicHandler async.PanicHandler) (*vault.Vault, bool, error, error) {
|
||||
func newVault(reporter *sentry.Reporter, locations *locations.Locations, keychains *keychain.List, panicHandler async.PanicHandler) (*vault.Vault, bool, error, error) {
|
||||
vaultDir, err := locations.ProvideSettingsPath()
|
||||
if err != nil {
|
||||
return nil, false, nil, fmt.Errorf("could not get vault dir: %w", err)
|
||||
@ -71,6 +72,16 @@ func newVault(locations *locations.Locations, keychains *keychain.List, panicHan
|
||||
)
|
||||
|
||||
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(),
|
||||
"keychainUsableHelpersLength": len(keychains.GetHelpers()),
|
||||
"error": err.Error(),
|
||||
}); rerr != nil {
|
||||
logrus.WithError(err).Info("Failed to report keychain issue to Sentry")
|
||||
}
|
||||
}
|
||||
|
||||
logrus.WithError(err).Error("Could not load/create vault key")
|
||||
insecure = true
|
||||
|
||||
|
||||
@ -24,6 +24,10 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@ -41,8 +45,11 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/sentry"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapsmtpserver"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/notifications"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/telemetry"
|
||||
"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/ProtonMail/proton-bridge/v3/pkg/keychain"
|
||||
@ -51,6 +58,8 @@ import (
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var usernameChangeRegex = regexp.MustCompile(`^/Users/([^/]+)/`)
|
||||
|
||||
type Bridge struct {
|
||||
// vault holds bridge-specific data, such as preferences and known users (authorized or not).
|
||||
vault *vault.Vault
|
||||
@ -130,6 +139,15 @@ type Bridge struct {
|
||||
|
||||
serverManager *imapsmtpserver.Service
|
||||
syncService *syncservice.Service
|
||||
|
||||
// unleashService is responsible for polling the feature flags and caching
|
||||
unleashService *unleash.Service
|
||||
|
||||
// observabilityService is responsible for handling calls to the observability system
|
||||
observabilityService *observability.Service
|
||||
|
||||
// notificationStore is used for notification deduplication
|
||||
notificationStore *notifications.Store
|
||||
}
|
||||
|
||||
var logPkg = logrus.WithField("pkg", "bridge") //nolint:gochecknoglobals
|
||||
@ -247,6 +265,10 @@ func newBridge(
|
||||
return nil, fmt.Errorf("failed to create focus service: %w", err)
|
||||
}
|
||||
|
||||
unleashService := unleash.NewBridgeService(ctx, api, locator, panicHandler)
|
||||
|
||||
observabilityService := observability.NewService(ctx, panicHandler)
|
||||
|
||||
bridge := &Bridge{
|
||||
vault: vault,
|
||||
|
||||
@ -286,7 +308,13 @@ func newBridge(
|
||||
lastVersion: lastVersion,
|
||||
|
||||
tasks: tasks,
|
||||
syncService: syncservice.NewService(reporter, panicHandler),
|
||||
syncService: syncservice.NewService(panicHandler, observabilityService),
|
||||
|
||||
unleashService: unleashService,
|
||||
|
||||
observabilityService: observabilityService,
|
||||
|
||||
notificationStore: notifications.NewStore(locator.ProvideNotificationsCachePath),
|
||||
}
|
||||
|
||||
bridge.serverManager = imapsmtpserver.NewService(context.Background(),
|
||||
@ -297,8 +325,12 @@ func newBridge(
|
||||
reporter,
|
||||
uidValidityGenerator,
|
||||
&bridgeIMAPSMTPTelemetry{b: bridge},
|
||||
observabilityService,
|
||||
)
|
||||
|
||||
// Check whether username has changed and correct (macOS only)
|
||||
bridge.verifyUsernameChange()
|
||||
|
||||
if err := bridge.serverManager.Init(context.Background(), bridge.tasks, &bridgeEventSubscription{b: bridge}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -311,6 +343,10 @@ func newBridge(
|
||||
|
||||
bridge.syncService.Run()
|
||||
|
||||
bridge.unleashService.Run()
|
||||
|
||||
bridge.observabilityService.Run(bridge)
|
||||
|
||||
return bridge, nil
|
||||
}
|
||||
|
||||
@ -438,6 +474,9 @@ func (bridge *Bridge) GetErrors() []error {
|
||||
func (bridge *Bridge) Close(ctx context.Context) {
|
||||
logPkg.Info("Closing bridge")
|
||||
|
||||
// Stop observability service
|
||||
bridge.observabilityService.Stop()
|
||||
|
||||
// Stop heart beat before closing users.
|
||||
bridge.heartbeat.stop()
|
||||
|
||||
@ -461,6 +500,9 @@ func (bridge *Bridge) Close(ctx context.Context) {
|
||||
// Close the focus service.
|
||||
bridge.focusService.Close()
|
||||
|
||||
// Close the unleash service.
|
||||
bridge.unleashService.Close()
|
||||
|
||||
// Close the watchers.
|
||||
bridge.watchersLock.Lock()
|
||||
defer bridge.watchersLock.Unlock()
|
||||
@ -541,9 +583,9 @@ func (bridge *Bridge) onStatusDown(ctx context.Context) {
|
||||
|
||||
func (bridge *Bridge) Repair() {
|
||||
var wg sync.WaitGroup
|
||||
userIDS := bridge.GetUserIDs()
|
||||
userIDs := bridge.GetUserIDs()
|
||||
|
||||
for _, userID := range userIDS {
|
||||
for _, userID := range userIDs {
|
||||
logPkg.Info("Initiating repair for userID:", userID)
|
||||
|
||||
userInfo, err := bridge.GetUserInfo(userID)
|
||||
@ -594,14 +636,97 @@ func loadTLSConfig(vault *vault.Vault) (*tls.Config, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func min(a, b time.Duration) time.Duration {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func (bridge *Bridge) HasAPIConnection() bool {
|
||||
return bridge.api.GetStatus() == proton.StatusUp
|
||||
}
|
||||
|
||||
// verifyUsernameChange - works only on macOS
|
||||
// it attempts to check whether a username change has taken place by comparing the gluon DB path (which is static and provided by bridge)
|
||||
// to the gluon Cache path - which can be modified by the user and is stored in the vault;
|
||||
// if a username discrepancy is detected, and the cache folder does not exist with the "old" username
|
||||
// then we verify whether the gluon cache exists using the "new" username (provided by the DB path in this case)
|
||||
// if so we modify the cache directory in the user vault.
|
||||
func (bridge *Bridge) verifyUsernameChange() {
|
||||
if runtime.GOOS != "darwin" {
|
||||
return
|
||||
}
|
||||
|
||||
gluonDBPath, err := bridge.GetGluonDataDir()
|
||||
if err != nil {
|
||||
logPkg.WithError(err).Error("Failed to get gluon db path")
|
||||
return
|
||||
}
|
||||
|
||||
gluonCachePath := bridge.GetGluonCacheDir()
|
||||
// If the cache folder exists even on another user account or is in `/Users/Shared` we would still be able to access it
|
||||
// though it depends on the permissions; this is an edge-case.
|
||||
if _, err := os.Stat(gluonCachePath); err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
newCacheDir := GetUpdatedCachePath(gluonDBPath, gluonCachePath)
|
||||
if newCacheDir == "" {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := os.Stat(newCacheDir); err == nil {
|
||||
logPkg.Info("Username change detected. Trying to restore gluon cache directory")
|
||||
if err = bridge.vault.SetGluonDir(newCacheDir); err != nil {
|
||||
logPkg.WithError(err).Error("Failed to restore gluon cache directory")
|
||||
return
|
||||
}
|
||||
logPkg.Info("Successfully restored gluon cache directory")
|
||||
}
|
||||
}
|
||||
|
||||
func GetUpdatedCachePath(gluonDBPath, gluonCachePath string) string {
|
||||
// If gluon cache is moved to an external drive; regex find will fail; as is expected
|
||||
cachePathMatches := usernameChangeRegex.FindStringSubmatch(gluonCachePath)
|
||||
if cachePathMatches == nil || len(cachePathMatches) < 2 {
|
||||
return ""
|
||||
}
|
||||
|
||||
cacheUsername := cachePathMatches[1]
|
||||
dbPathMatches := usernameChangeRegex.FindStringSubmatch(gluonDBPath)
|
||||
if dbPathMatches == nil || len(dbPathMatches) < 2 {
|
||||
return ""
|
||||
}
|
||||
|
||||
dbUsername := dbPathMatches[1]
|
||||
if cacheUsername == dbUsername {
|
||||
return ""
|
||||
}
|
||||
|
||||
return strings.Replace(gluonCachePath, "/Users/"+cacheUsername+"/", "/Users/"+dbUsername+"/", 1)
|
||||
}
|
||||
|
||||
func (bridge *Bridge) GetFeatureFlagValue(key string) bool {
|
||||
return bridge.unleashService.GetFlagValue(key)
|
||||
}
|
||||
|
||||
func (bridge *Bridge) PushObservabilityMetric(metric proton.ObservabilityMetric) {
|
||||
bridge.observabilityService.AddMetrics(metric)
|
||||
}
|
||||
|
||||
func (bridge *Bridge) PushDistinctObservabilityMetrics(errType observability.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
|
||||
}
|
||||
|
||||
@ -76,7 +76,7 @@ func init() {
|
||||
|
||||
func TestBridge_ConnStatus(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
// Get a stream of connection status events.
|
||||
eventCh, done := bridge.GetEvents(events.ConnStatusUp{}, events.ConnStatusDown{})
|
||||
defer done()
|
||||
@ -125,7 +125,7 @@ func TestBridge_TLSIssue(t *testing.T) {
|
||||
|
||||
func TestBridge_Focus(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
// Get a stream of TLS issue events.
|
||||
raiseCh, done := bridge.GetEvents(events.Raise{})
|
||||
defer done()
|
||||
@ -156,7 +156,7 @@ func TestBridge_UserAgent(t *testing.T) {
|
||||
calls = append(calls, call)
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
// Set the platform to something other than the default.
|
||||
bridge.SetCurrentPlatform("platform")
|
||||
|
||||
@ -183,7 +183,7 @@ func TestBridge_UserAgent_Persistence(t *testing.T) {
|
||||
_, _, err := s.CreateUser(otherUser, otherPassword)
|
||||
require.NoError(t, err)
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
|
||||
currentUserAgent := b.GetCurrentUserAgent()
|
||||
require.Contains(t, currentUserAgent, useragent.DefaultUserAgent)
|
||||
|
||||
@ -211,7 +211,7 @@ func TestBridge_UserAgent_Persistence(t *testing.T) {
|
||||
require.Contains(t, b.GetCurrentUserAgent(), "MyFancyClient/0.1.2")
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
currentUserAgent := bridge.GetCurrentUserAgent()
|
||||
require.Contains(t, currentUserAgent, "MyFancyClient/0.1.2")
|
||||
})
|
||||
@ -225,7 +225,7 @@ func TestBridge_UserAgentFromUnknownClient(t *testing.T) {
|
||||
_, _, err := s.CreateUser(otherUser, otherPassword)
|
||||
require.NoError(t, err)
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
|
||||
currentUserAgent := b.GetCurrentUserAgent()
|
||||
require.Contains(t, currentUserAgent, useragent.DefaultUserAgent)
|
||||
|
||||
@ -255,7 +255,7 @@ func TestBridge_UserAgentFromSMTPClient(t *testing.T) {
|
||||
_, _, err := s.CreateUser(otherUser, otherPassword)
|
||||
require.NoError(t, err)
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
|
||||
currentUserAgent := b.GetCurrentUserAgent()
|
||||
require.Contains(t, currentUserAgent, useragent.DefaultUserAgent)
|
||||
|
||||
@ -305,7 +305,7 @@ func TestBridge_UserAgentFromIMAPID(t *testing.T) {
|
||||
_, _, err := s.CreateUser(otherUser, otherPassword)
|
||||
require.NoError(t, err)
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
|
||||
require.NoError(t, getErr(b.LoginFull(ctx, otherUser, otherPassword, nil, nil)))
|
||||
|
||||
imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
|
||||
@ -365,13 +365,13 @@ func TestBridge_Cookies(t *testing.T) {
|
||||
})
|
||||
|
||||
// Start bridge and add a user so that API assigns us a session ID via cookie.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
_, err := bridge.LoginFull(context.Background(), username, password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
// Start bridge again and check that it uses the same session ID.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(_ *bridge.Bridge, _ *bridge.Mocks) {
|
||||
// ...
|
||||
})
|
||||
|
||||
@ -484,7 +484,7 @@ func TestBridge_ManualUpdate(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) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
// Get a stream of update events.
|
||||
updateCh, done := bridge.GetEvents(events.UpdateForced{})
|
||||
defer done()
|
||||
@ -507,7 +507,7 @@ func TestBridge_BadVaultKey(t *testing.T) {
|
||||
var userID string
|
||||
|
||||
// Login a user.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
newUserID, err := bridge.LoginFull(context.Background(), username, password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
@ -515,17 +515,17 @@ func TestBridge_BadVaultKey(t *testing.T) {
|
||||
})
|
||||
|
||||
// Start bridge with the correct vault key -- it should load the users correctly.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
require.ElementsMatch(t, []string{userID}, bridge.GetUserIDs())
|
||||
})
|
||||
|
||||
// Start bridge with a bad vault key, the vault will be wiped and bridge will show no users.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, []byte("bad"), func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, []byte("bad"), func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
require.Empty(t, bridge.GetUserIDs())
|
||||
})
|
||||
|
||||
// Start bridge with a nil vault key, the vault will be wiped and bridge will show no users.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, nil, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, nil, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
require.Empty(t, bridge.GetUserIDs())
|
||||
})
|
||||
})
|
||||
@ -535,7 +535,7 @@ func TestBridge_MissingGluonStore(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||
var gluonDir string
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
_, err := bridge.LoginFull(context.Background(), username, password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
@ -550,7 +550,7 @@ func TestBridge_MissingGluonStore(t *testing.T) {
|
||||
require.NoError(t, os.RemoveAll(gluonDir))
|
||||
|
||||
// Bridge starts but can't find the gluon store dir; there should be no error.
|
||||
withBridgeWaitForServers(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridgeWaitForServers(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(_ *bridge.Bridge, _ *bridge.Mocks) {
|
||||
// ...
|
||||
})
|
||||
})
|
||||
@ -560,7 +560,7 @@ func TestBridge_MissingGluonDatabase(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||
var gluonDir string
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
_, err := bridge.LoginFull(context.Background(), username, password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
@ -573,7 +573,7 @@ func TestBridge_MissingGluonDatabase(t *testing.T) {
|
||||
require.NoError(t, os.RemoveAll(gluonDir))
|
||||
|
||||
// Bridge starts but can't find the gluon database dir; there should be no error.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(_ *bridge.Bridge, _ *bridge.Mocks) {
|
||||
// ...
|
||||
})
|
||||
})
|
||||
@ -587,7 +587,7 @@ func TestBridge_AddressWithoutKeys(t *testing.T) {
|
||||
)
|
||||
defer m.Close()
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
// Watch for sync finished event.
|
||||
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
||||
defer done()
|
||||
@ -663,7 +663,7 @@ func TestBridge_FactoryReset(t *testing.T) {
|
||||
|
||||
func TestBridge_InitGluonDirectory(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
|
||||
configDir, err := b.GetGluonDataDir()
|
||||
require.NoError(t, err)
|
||||
|
||||
@ -678,7 +678,7 @@ func TestBridge_InitGluonDirectory(t *testing.T) {
|
||||
|
||||
func TestBridge_LoginFailed(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
failCh, done := chToType[events.Event, events.IMAPLoginFailed](bridge.GetEvents(events.IMAPLoginFailed{}))
|
||||
defer done()
|
||||
|
||||
@ -706,7 +706,7 @@ func TestBridge_ChangeCacheDirectory(t *testing.T) {
|
||||
createNumMessages(ctx, t, c, addrID, labelID, 10)
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
|
||||
newCacheDir := t.TempDir()
|
||||
currentCacheDir := b.GetGluonCacheDir()
|
||||
configDir, err := b.GetGluonDataDir()
|
||||
@ -772,7 +772,7 @@ func TestBridge_ChangeAddressOrder(t *testing.T) {
|
||||
createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 10)
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
|
||||
// Log the user in with its first address.
|
||||
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
|
||||
defer done()
|
||||
@ -800,7 +800,7 @@ func TestBridge_ChangeAddressOrder(t *testing.T) {
|
||||
require.NoError(t, c.OrderAddresses(ctx, proton.OrderAddressesReq{AddressIDs: []string{aliasID, addrID}}))
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
|
||||
// We should still see 10 messages in the inbox.
|
||||
info, err := b.GetUserInfo(userID)
|
||||
require.NoError(t, err)
|
||||
@ -1077,3 +1077,57 @@ func waitForIMAPServerStopped(b *bridge.Bridge) *eventWaiter {
|
||||
cancel: cancel,
|
||||
}
|
||||
}
|
||||
|
||||
func TestBridge_GetUpdatedCachePath(t *testing.T) {
|
||||
type TestData struct {
|
||||
gluonDBPath string
|
||||
gluonCachePath string
|
||||
shouldChange bool
|
||||
}
|
||||
|
||||
dataArr := []TestData{
|
||||
{
|
||||
gluonDBPath: "/Users/test/",
|
||||
gluonCachePath: "/Users/test/gluon",
|
||||
shouldChange: false,
|
||||
}, {
|
||||
gluonDBPath: "/Users/test/",
|
||||
gluonCachePath: "/Users/tester/gluon",
|
||||
shouldChange: true,
|
||||
}, {
|
||||
gluonDBPath: "/Users/testing/",
|
||||
gluonCachePath: "/Users/test/gluon",
|
||||
shouldChange: true,
|
||||
},
|
||||
{
|
||||
gluonDBPath: "/Users/testing/",
|
||||
gluonCachePath: "/Users/test/gluon",
|
||||
shouldChange: true,
|
||||
},
|
||||
{
|
||||
gluonDBPath: "/Users/testing/",
|
||||
gluonCachePath: "/Volumes/test/gluon",
|
||||
shouldChange: false,
|
||||
},
|
||||
{
|
||||
gluonDBPath: "/Volumes/test/",
|
||||
gluonCachePath: "/Users/test/gluon",
|
||||
shouldChange: false,
|
||||
},
|
||||
{
|
||||
gluonDBPath: "/XXX/test/",
|
||||
gluonCachePath: "/Users/test/gluon",
|
||||
shouldChange: false,
|
||||
},
|
||||
{
|
||||
gluonDBPath: "/XXX/test/",
|
||||
gluonCachePath: "/YYY/test/gluon",
|
||||
shouldChange: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, el := range dataArr {
|
||||
newCachePath := bridge.GetUpdatedCachePath(el.gluonDBPath, el.gluonCachePath)
|
||||
require.Equal(t, el.shouldChange, newCachePath != "" && newCachePath != el.gluonCachePath)
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,7 +25,6 @@ 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"
|
||||
)
|
||||
|
||||
@ -80,12 +79,6 @@ 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 {
|
||||
|
||||
@ -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 nbAccount = len(bridge.users)
|
||||
h.SetNbAccount(nbAccount)
|
||||
var numberConnectedAccounts = len(bridge.users)
|
||||
h.SetNumberConnectedAccounts(numberConnectedAccounts)
|
||||
h.SetSplitMode(splitMode)
|
||||
|
||||
// Do not try to send if there is no user yet.
|
||||
if nbAccount > 0 {
|
||||
if numberConnectedAccounts > 0 {
|
||||
defer h.start()
|
||||
}
|
||||
}, bridge.usersLock)
|
||||
|
||||
@ -17,7 +17,9 @@
|
||||
|
||||
package bridge
|
||||
|
||||
import "github.com/sirupsen/logrus"
|
||||
import (
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (bridge *Bridge) GetCurrentUserAgent() string {
|
||||
return bridge.identifier.GetUserAgent()
|
||||
@ -30,6 +32,8 @@ 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()
|
||||
@ -54,6 +58,7 @@ func (b *bridgeUserAgentUpdater) HasClient() bool {
|
||||
}
|
||||
|
||||
func (b *bridgeUserAgentUpdater) SetClient(name, version string) {
|
||||
b.heartbeat.SetContactedByAppleNotes(name)
|
||||
b.identifier.SetClient(name, version)
|
||||
}
|
||||
|
||||
|
||||
@ -26,6 +26,7 @@ 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"
|
||||
)
|
||||
@ -93,6 +94,10 @@ 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()
|
||||
}
|
||||
|
||||
49
internal/bridge/mocks/observability_mocks.go
Normal file
49
internal/bridge/mocks/observability_mocks.go
Normal file
@ -0,0 +1,49 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
|
||||
"github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
type MockObservabilitySender struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockObservabilitySenderRecorder
|
||||
}
|
||||
|
||||
type MockObservabilitySenderRecorder struct {
|
||||
mock *MockObservabilitySender
|
||||
}
|
||||
|
||||
func NewMockObservabilitySender(ctrl *gomock.Controller) *MockObservabilitySender {
|
||||
mock := &MockObservabilitySender{ctrl: ctrl}
|
||||
mock.recorder = &MockObservabilitySenderRecorder{mock: mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
func (m *MockObservabilitySender) EXPECT() *MockObservabilitySenderRecorder { return m.recorder }
|
||||
|
||||
func (m *MockObservabilitySender) AddDistinctMetrics(errType observability.DistinctionErrorTypeEnum, _ ...proton.ObservabilityMetric) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "AddDistinctMetrics", errType)
|
||||
}
|
||||
|
||||
func (m *MockObservabilitySender) AddMetrics(metrics ...proton.ObservabilityMetric) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "AddMetrics", metrics)
|
||||
}
|
||||
|
||||
func (mr *MockObservabilitySenderRecorder) AddDistinctMetrics(errType observability.DistinctionErrorTypeEnum, _ ...proton.ObservabilityMetric) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock,
|
||||
"AddDistinctMetrics",
|
||||
reflect.TypeOf((*MockObservabilitySender)(nil).AddDistinctMetrics),
|
||||
errType)
|
||||
}
|
||||
|
||||
func (mr *MockObservabilitySenderRecorder) AddMetrics(metrics ...proton.ObservabilityMetric) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddMetrics", reflect.TypeOf((*MockObservabilitySender)(nil).AddMetrics), metrics)
|
||||
}
|
||||
164
internal/bridge/observability_test.go
Normal file
164
internal/bridge/observability_test.go
Normal file
@ -0,0 +1,164 @@
|
||||
// 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_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/go-proton-api/server"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func TestBridge_Observability(t *testing.T) {
|
||||
testMetric := proton.ObservabilityMetric{
|
||||
Name: "test1",
|
||||
Version: 1,
|
||||
Timestamp: time.Now().Unix(),
|
||||
Data: nil,
|
||||
}
|
||||
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||
throttlePeriod := time.Millisecond * 500
|
||||
observability.ModifyThrottlePeriod(throttlePeriod)
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
require.NoError(t, getErr(bridge.LoginFull(ctx, username, password, nil, nil)))
|
||||
|
||||
bridge.PushObservabilityMetric(testMetric)
|
||||
time.Sleep(time.Millisecond * 50) // Wait for the metric to be sent
|
||||
require.Equal(t, 1, len(s.GetObservabilityStatistics().Metrics))
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
time.Sleep(time.Millisecond * 5) // Minor delay between each so our tests aren't flaky
|
||||
bridge.PushObservabilityMetric(testMetric)
|
||||
}
|
||||
// We should still have only 1 metric sent as the throttleDuration has not passed
|
||||
require.Equal(t, 1, len(s.GetObservabilityStatistics().Metrics))
|
||||
|
||||
// Wait for throttle duration to pass; we should have our remaining metrics posted
|
||||
time.Sleep(throttlePeriod)
|
||||
require.Equal(t, 11, len(s.GetObservabilityStatistics().Metrics))
|
||||
|
||||
// Wait for the throttle duration to reset; i.e. so we have enough time to send a request immediately
|
||||
time.Sleep(throttlePeriod)
|
||||
for i := 0; i < 10; i++ {
|
||||
time.Sleep(time.Millisecond * 5)
|
||||
bridge.PushObservabilityMetric(testMetric)
|
||||
}
|
||||
// We should only have one additional metric sent immediately
|
||||
require.Equal(t, 12, len(s.GetObservabilityStatistics().Metrics))
|
||||
|
||||
// Wait for the others to be sent
|
||||
time.Sleep(throttlePeriod)
|
||||
require.Equal(t, 21, len(s.GetObservabilityStatistics().Metrics))
|
||||
|
||||
// Spam the endpoint a bit
|
||||
for i := 0; i < 300; i++ {
|
||||
if i < 200 {
|
||||
time.Sleep(time.Millisecond * 10)
|
||||
}
|
||||
bridge.PushObservabilityMetric(testMetric)
|
||||
}
|
||||
|
||||
// Ensure we've sent all metrics
|
||||
time.Sleep(throttlePeriod)
|
||||
|
||||
observabilityStats := s.GetObservabilityStatistics()
|
||||
require.Equal(t, 321, len(observabilityStats.Metrics))
|
||||
|
||||
// Verify that each request had a throttleDuration time difference between each request
|
||||
for i := 0; i < len(observabilityStats.RequestTime)-1; i++ {
|
||||
tOne := observabilityStats.RequestTime[i]
|
||||
tTwo := observabilityStats.RequestTime[i+1]
|
||||
require.True(t, tTwo.Sub(tOne).Abs() > throttlePeriod)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_Observability_Heartbeat(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||
throttlePeriod := time.Millisecond * 300
|
||||
observability.ModifyThrottlePeriod(throttlePeriod)
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
require.NoError(t, getErr(bridge.LoginFull(ctx, username, password, nil, nil)))
|
||||
bridge.ModifyObservabilityHeartbeatInterval(throttlePeriod)
|
||||
|
||||
require.Equal(t, 0, len(s.GetObservabilityStatistics().Metrics))
|
||||
time.Sleep(time.Millisecond * 150)
|
||||
require.Equal(t, 0, len(s.GetObservabilityStatistics().Metrics))
|
||||
time.Sleep(time.Millisecond * 200)
|
||||
require.Equal(t, 1, len(s.GetObservabilityStatistics().Metrics))
|
||||
time.Sleep(time.Millisecond * 350)
|
||||
require.Equal(t, 2, len(s.GetObservabilityStatistics().Metrics))
|
||||
time.Sleep(time.Millisecond * 350)
|
||||
require.Equal(t, 3, len(s.GetObservabilityStatistics().Metrics))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBridge_Observability_UserMetric(t *testing.T) {
|
||||
testMetric := proton.ObservabilityMetric{
|
||||
Name: "test1",
|
||||
Version: 1,
|
||||
Timestamp: time.Now().Unix(),
|
||||
Data: nil,
|
||||
}
|
||||
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||
userMetricPeriod := time.Millisecond * 200
|
||||
heartbeatPeriod := time.Second * 10
|
||||
throttlePeriod := time.Millisecond * 100
|
||||
observability.ModifyUserMetricInterval(userMetricPeriod)
|
||||
observability.ModifyThrottlePeriod(throttlePeriod)
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
require.NoError(t, getErr(bridge.LoginFull(ctx, username, password, nil, nil)))
|
||||
bridge.ModifyObservabilityHeartbeatInterval(heartbeatPeriod)
|
||||
|
||||
time.Sleep(throttlePeriod)
|
||||
require.Equal(t, 0, len(s.GetObservabilityStatistics().Metrics))
|
||||
|
||||
bridge.PushDistinctObservabilityMetrics(observability.SyncError, testMetric)
|
||||
time.Sleep(throttlePeriod)
|
||||
// We're expecting two observability metrics to be sent, the actual metric + the user metric.
|
||||
require.Equal(t, 2, len(s.GetObservabilityStatistics().Metrics))
|
||||
|
||||
bridge.PushDistinctObservabilityMetrics(observability.SyncError, testMetric)
|
||||
time.Sleep(throttlePeriod)
|
||||
// We're expecting only a single metric to be sent, since the user metric update has been sent already within the predefined period.
|
||||
require.Equal(t, 3, len(s.GetObservabilityStatistics().Metrics))
|
||||
|
||||
bridge.PushDistinctObservabilityMetrics(observability.SyncError, testMetric)
|
||||
time.Sleep(throttlePeriod)
|
||||
// Two metric updates should be sent again.
|
||||
require.Equal(t, 5, len(s.GetObservabilityStatistics().Metrics))
|
||||
|
||||
bridge.PushDistinctObservabilityMetrics(observability.SyncError, testMetric)
|
||||
time.Sleep(throttlePeriod)
|
||||
// Only a single one should be sent.
|
||||
require.Equal(t, 6, len(s.GetObservabilityStatistics().Metrics))
|
||||
})
|
||||
})
|
||||
}
|
||||
@ -29,13 +29,12 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestBridge_Report(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
|
||||
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
|
||||
defer done()
|
||||
|
||||
@ -56,12 +55,6 @@ func TestBridge_Report(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
defer func() { require.NoError(t, conn.Close()) }()
|
||||
|
||||
// Sending garbage to the IMAP port should cause the bridge to report it.
|
||||
mocks.Reporter.EXPECT().ReportMessageWithContext(
|
||||
gomock.Eq("Failed to parse IMAP command"),
|
||||
gomock.Any(),
|
||||
).Return(nil)
|
||||
|
||||
// Read lines from the IMAP port.
|
||||
lineCh := liner.New(conn).Lines(func() error { return nil })
|
||||
|
||||
|
||||
@ -34,7 +34,7 @@ import (
|
||||
|
||||
func TestServerManager_ServersStartWithBridge(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, imapClient.Logout())
|
||||
@ -48,7 +48,7 @@ func TestServerManager_ServersStartWithBridge(t *testing.T) {
|
||||
|
||||
func TestServerManager_ServersKeepsRunningfterUserLogsOut(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
@ -72,7 +72,7 @@ func TestServerManager_ServersDoNotStopWhenThereIsStillOneActiveUser(t *testing.
|
||||
_, _, err := s.CreateUser(otherUser, otherPassword)
|
||||
require.NoError(t, err)
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
_, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
@ -99,7 +99,7 @@ func TestServerManager_ServersDoNotStopWhenThereIsStillOneActiveUser(t *testing.
|
||||
|
||||
func TestServerManager_NetworkLossStopsServers(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
imapWaiter := waitForIMAPServerReady(bridge)
|
||||
defer imapWaiter.Done()
|
||||
|
||||
|
||||
@ -318,11 +318,10 @@ func (bridge *Bridge) GetKnowledgeBaseSuggestions(userInput string) (kb.ArticleL
|
||||
// Note: it does not clear the keychain. The only entry in the keychain is the vault password,
|
||||
// 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, useTelemetry)
|
||||
bridge.logoutUser(ctx, user, true, true)
|
||||
}
|
||||
}, bridge.usersLock)
|
||||
|
||||
|
||||
@ -31,7 +31,7 @@ import (
|
||||
|
||||
func TestBridge_Settings_GluonDir(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
// Create a user.
|
||||
_, err := bridge.LoginFull(context.Background(), username, password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
@ -57,7 +57,7 @@ func TestBridge_Settings_GluonDirWithOnGoingEvents(t *testing.T) {
|
||||
userID, addrID, err := s.CreateUser("imap", password)
|
||||
require.NoError(t, err)
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
||||
defer done()
|
||||
|
||||
@ -74,7 +74,7 @@ func TestBridge_Settings_GluonDirWithOnGoingEvents(t *testing.T) {
|
||||
createNumMessages(ctx, t, c, addrID, labelID, 200)
|
||||
})
|
||||
|
||||
withBridgeWaitForServers(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridgeWaitForServers(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
// Create a new location for the Gluon data.
|
||||
newGluonDir := t.TempDir()
|
||||
|
||||
@ -93,7 +93,7 @@ func TestBridge_Settings_GluonDirWithOnGoingEvents(t *testing.T) {
|
||||
|
||||
func TestBridge_Settings_IMAPPort(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
curPort := bridge.GetIMAPPort()
|
||||
|
||||
// Set the port to 1144.
|
||||
@ -110,7 +110,7 @@ func TestBridge_Settings_IMAPPort(t *testing.T) {
|
||||
|
||||
func TestBridge_Settings_IMAPSSL(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
// By default, IMAP SSL is disabled.
|
||||
require.False(t, bridge.GetIMAPSSL())
|
||||
|
||||
@ -125,7 +125,7 @@ func TestBridge_Settings_IMAPSSL(t *testing.T) {
|
||||
|
||||
func TestBridge_Settings_SMTPPort(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
curPort := bridge.GetSMTPPort()
|
||||
|
||||
// Set the port to 1024.
|
||||
@ -142,7 +142,7 @@ func TestBridge_Settings_SMTPPort(t *testing.T) {
|
||||
|
||||
func TestBridge_Settings_SMTPSSL(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
// By default, SMTP SSL is disabled.
|
||||
require.False(t, bridge.GetSMTPSSL())
|
||||
|
||||
@ -198,7 +198,7 @@ func TestBridge_Settings_Autostart(t *testing.T) {
|
||||
|
||||
func TestBridge_Settings_FirstStart(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
// By default, first start is true.
|
||||
require.True(t, bridge.GetFirstStart())
|
||||
|
||||
|
||||
@ -232,7 +232,7 @@ func TestBridge_SyncWithOngoingEvents(t *testing.T) {
|
||||
var total uint64
|
||||
|
||||
// The initial user should be fully synced.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
||||
defer done()
|
||||
|
||||
@ -246,7 +246,7 @@ func TestBridge_SyncWithOngoingEvents(t *testing.T) {
|
||||
})
|
||||
|
||||
// Now let's remove the user and stop the network at 2/3 of the data.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
require.NoError(t, bridge.DeleteUser(ctx, userID))
|
||||
})
|
||||
|
||||
@ -254,7 +254,7 @@ func TestBridge_SyncWithOngoingEvents(t *testing.T) {
|
||||
netCtl.SetReadLimit(2 * total / 3)
|
||||
|
||||
// Login the user; its sync should fail.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
|
||||
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
|
||||
defer done()
|
||||
|
||||
@ -592,7 +592,7 @@ func TestBridge_CorruptedVaultClearsPreviousIMAPSyncState(t *testing.T) {
|
||||
createNumMessages(ctx, t, c, addrID, labelID, 100)
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
||||
defer done()
|
||||
|
||||
@ -625,7 +625,7 @@ func TestBridge_CorruptedVaultClearsPreviousIMAPSyncState(t *testing.T) {
|
||||
require.NoError(t, os.WriteFile(filepath.Join(settingsPath, "vault.enc"), []byte("Trash!"), 0o600))
|
||||
|
||||
// Bridge starts but can't find the gluon database dir; there should be no error.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
_, err := bridge.LoginFull(context.Background(), "imap", password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
@ -28,11 +28,12 @@ type Locator interface {
|
||||
ProvideLogsPath() (string, error)
|
||||
ProvideGluonCachePath() (string, error)
|
||||
ProvideGluonDataPath() (string, error)
|
||||
ProvideStatsPath() (string, error)
|
||||
GetLicenseFilePath() string
|
||||
GetDependencyLicensesLink() string
|
||||
Clear(...string) error
|
||||
ProvideIMAPSyncConfigPath() (string, error)
|
||||
ProvideUnleashCachePath() (string, error)
|
||||
ProvideNotificationsCachePath() (string, error)
|
||||
}
|
||||
|
||||
type ProxyController interface {
|
||||
|
||||
90
internal/bridge/unleash_test.go
Normal file
90
internal/bridge/unleash_test.go
Normal file
@ -0,0 +1,90 @@
|
||||
// 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_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/go-proton-api/server"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/unleash"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_UnleashService(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
unleash.ModifyPollPeriodAndJitter(500*time.Millisecond, 0)
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
|
||||
// Initial startup assumes there is no cached feature flags.
|
||||
require.Equal(t, b.GetFeatureFlagValue("test-1"), false)
|
||||
require.Equal(t, b.GetFeatureFlagValue("test-2"), false)
|
||||
require.Equal(t, b.GetFeatureFlagValue("test-3"), false)
|
||||
|
||||
s.PushFeatureFlag("test-1")
|
||||
s.PushFeatureFlag("test-2")
|
||||
|
||||
// Wait for poll.
|
||||
time.Sleep(time.Millisecond * 700)
|
||||
require.Equal(t, b.GetFeatureFlagValue("test-1"), true)
|
||||
require.Equal(t, b.GetFeatureFlagValue("test-2"), true)
|
||||
require.Equal(t, b.GetFeatureFlagValue("test-3"), false)
|
||||
|
||||
s.PushFeatureFlag("test-3")
|
||||
time.Sleep(time.Millisecond * 700) // Wait for poll again
|
||||
require.Equal(t, b.GetFeatureFlagValue("test-1"), true)
|
||||
require.Equal(t, b.GetFeatureFlagValue("test-2"), true)
|
||||
require.Equal(t, b.GetFeatureFlagValue("test-3"), true)
|
||||
})
|
||||
|
||||
// Wait for Bridge to close.
|
||||
time.Sleep(time.Millisecond * 500)
|
||||
|
||||
// Second instance should have a feature flag cache file available. Therefore, all of the flags should evaluate to true on startup.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
|
||||
require.Equal(t, b.GetFeatureFlagValue("test-1"), true)
|
||||
require.Equal(t, b.GetFeatureFlagValue("test-2"), true)
|
||||
require.Equal(t, b.GetFeatureFlagValue("test-3"), true)
|
||||
|
||||
s.DeleteFeatureFlags()
|
||||
|
||||
require.Equal(t, b.GetFeatureFlagValue("test-1"), true)
|
||||
require.Equal(t, b.GetFeatureFlagValue("test-2"), true)
|
||||
require.Equal(t, b.GetFeatureFlagValue("test-3"), true)
|
||||
|
||||
time.Sleep(time.Millisecond * 700)
|
||||
|
||||
require.Equal(t, b.GetFeatureFlagValue("test-1"), false)
|
||||
require.Equal(t, b.GetFeatureFlagValue("test-2"), false)
|
||||
require.Equal(t, b.GetFeatureFlagValue("test-3"), false)
|
||||
|
||||
s.PushFeatureFlag("test-3")
|
||||
require.Equal(t, b.GetFeatureFlagValue("test-1"), false)
|
||||
require.Equal(t, b.GetFeatureFlagValue("test-2"), false)
|
||||
require.Equal(t, b.GetFeatureFlagValue("test-3"), false)
|
||||
|
||||
time.Sleep(time.Millisecond * 700)
|
||||
require.Equal(t, b.GetFeatureFlagValue("test-1"), false)
|
||||
require.Equal(t, b.GetFeatureFlagValue("test-2"), false)
|
||||
require.Equal(t, b.GetFeatureFlagValue("test-3"), true)
|
||||
})
|
||||
})
|
||||
}
|
||||
@ -21,6 +21,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"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"
|
||||
@ -115,6 +116,17 @@ func (bridge *Bridge) installUpdate(ctx context.Context, job installJob) {
|
||||
err := bridge.updater.InstallUpdate(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")
|
||||
|
||||
|
||||
@ -33,6 +33,7 @@ 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"
|
||||
@ -255,7 +256,7 @@ func (bridge *Bridge) LogoutUser(ctx context.Context, userID string) error {
|
||||
return ErrNoSuchUser
|
||||
}
|
||||
|
||||
bridge.logoutUser(ctx, user, true, false, false)
|
||||
bridge.logoutUser(ctx, user, true, false)
|
||||
|
||||
bridge.publish(events.UserLoggedOut{
|
||||
UserID: userID,
|
||||
@ -280,7 +281,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.GetTelemetryDisabled())
|
||||
bridge.logoutUser(ctx, user, true, true)
|
||||
}
|
||||
|
||||
if err := imapservice.DeleteSyncState(syncConfigDir, userID); err != nil {
|
||||
@ -355,24 +356,10 @@ func (bridge *Bridge) SendBadEventUserFeedback(_ context.Context, userID string,
|
||||
}
|
||||
|
||||
if doResync {
|
||||
if rerr := bridge.reporter.ReportMessageWithContext(
|
||||
"Failed to handle event: feedback resync",
|
||||
reporter.Context{"user_id": userID},
|
||||
); rerr != nil {
|
||||
logUser.WithError(rerr).Error("Failed to report feedback failure")
|
||||
}
|
||||
|
||||
return user.BadEventFeedbackResync(ctx)
|
||||
}
|
||||
|
||||
if rerr := bridge.reporter.ReportMessageWithContext(
|
||||
"Failed to handle event: feedback logout",
|
||||
reporter.Context{"user_id": userID},
|
||||
); rerr != nil {
|
||||
logUser.WithError(rerr).Error("Failed to report feedback failure")
|
||||
}
|
||||
|
||||
bridge.logoutUser(ctx, user, true, false, false)
|
||||
bridge.logoutUser(ctx, user, true, false)
|
||||
|
||||
bridge.publish(events.UserLoggedOut{
|
||||
UserID: userID,
|
||||
@ -541,11 +528,6 @@ 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)
|
||||
@ -560,14 +542,16 @@ func (bridge *Bridge) addUserWithVault(
|
||||
bridge.panicHandler,
|
||||
bridge.vault.GetShowAllMail(),
|
||||
bridge.vault.GetMaxSyncMemory(),
|
||||
statsPath,
|
||||
bridge,
|
||||
bridge.serverManager,
|
||||
bridge.serverManager,
|
||||
&bridgeEventSubscription{b: bridge},
|
||||
bridge.syncService,
|
||||
bridge.observabilityService,
|
||||
syncSettingsPath,
|
||||
isNew,
|
||||
bridge.notificationStore,
|
||||
bridge.unleashService.GetFlagValue,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create user: %w", err)
|
||||
@ -600,9 +584,12 @@ func (bridge *Bridge) addUserWithVault(
|
||||
// Finally, save the user in the bridge.
|
||||
safe.Lock(func() {
|
||||
bridge.users[apiUser.ID] = user
|
||||
bridge.heartbeat.SetNbAccount(len(bridge.users))
|
||||
bridge.heartbeat.SetNumberConnectedAccounts(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()
|
||||
|
||||
@ -621,26 +608,21 @@ func (bridge *Bridge) newVaultUser(
|
||||
return bridge.vault.GetOrAddUser(apiUser.ID, apiUser.Name, apiUser.Email, authUID, authRef, saltedKeyPass)
|
||||
}
|
||||
|
||||
// logout logs out the given user, optionally logging them out from the API too.
|
||||
func (bridge *Bridge) logoutUser(ctx context.Context, user *user.User, withAPI, withData, withTelemetry bool) {
|
||||
// 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) {
|
||||
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); err != nil {
|
||||
if err := user.Logout(ctx, withAPI, withData, bridge.unleashService.GetFlagValue(unleash.UserRemovalGluonDataCleanupDisabled)); err != nil {
|
||||
logUser.WithError(err).Error("Failed to logout user")
|
||||
}
|
||||
|
||||
bridge.heartbeat.SetNbAccount(len(bridge.users))
|
||||
bridge.heartbeat.SetNumberConnectedAccounts(len(bridge.users) - 1)
|
||||
|
||||
user.Close()
|
||||
}
|
||||
|
||||
@ -62,7 +62,7 @@ func TestBridge_User_RefreshEvent(t *testing.T) {
|
||||
messageIDs = createNumMessages(ctx, t, c, addrID, labelID, 10)
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
userLoginAndSync(ctx, t, bridge, "user", password)
|
||||
})
|
||||
|
||||
@ -73,7 +73,7 @@ func TestBridge_User_RefreshEvent(t *testing.T) {
|
||||
|
||||
require.NoError(t, s.RefreshUser(userID, proton.RefreshMail))
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
syncCh, closeCh := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
||||
|
||||
require.Equal(t, userID, (<-syncCh).UserID)
|
||||
@ -82,7 +82,7 @@ func TestBridge_User_RefreshEvent(t *testing.T) {
|
||||
userContinueEventProcess(ctx, t, s, bridge)
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
withClient(ctx, t, s, "user", password, func(ctx context.Context, c *proton.Client) {
|
||||
createNumMessages(ctx, t, c, addrID, labelID, 10)
|
||||
})
|
||||
@ -191,7 +191,7 @@ func TestBridge_User_BadMessage_NoBadEvent(t *testing.T) {
|
||||
createNumMessages(ctx, t, c, addrID, proton.InboxLabel, 10)
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
userLoginAndSync(ctx, t, bridge, "user", password)
|
||||
|
||||
var messageIDs []string
|
||||
@ -368,7 +368,7 @@ func TestBridge_User_Network_NoBadEvents(t *testing.T) {
|
||||
_, addrID, err := s.CreateUser("user", password)
|
||||
require.NoError(t, err)
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
userLoginAndSync(ctx, t, bridge, "user", password)
|
||||
|
||||
// Create 10 more messages for the user, generating events.
|
||||
@ -454,7 +454,7 @@ func TestBridge_User_UpdateDraft(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Initially sync the user.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
userLoginAndSync(ctx, t, bridge, "user", password)
|
||||
})
|
||||
|
||||
@ -487,7 +487,7 @@ func TestBridge_User_UpdateDraft(t *testing.T) {
|
||||
require.Empty(t, draft.ReplyTos)
|
||||
|
||||
// Process those events
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
userContinueEventProcess(ctx, t, s, bridge)
|
||||
})
|
||||
|
||||
@ -513,7 +513,7 @@ func TestBridge_User_UpdateDraftAndCreateOtherMessage(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Initially sync the user.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
userLoginAndSync(ctx, t, bridge, "user", password)
|
||||
})
|
||||
|
||||
@ -545,7 +545,7 @@ func TestBridge_User_UpdateDraftAndCreateOtherMessage(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Process those events
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
userContinueEventProcess(ctx, t, s, bridge)
|
||||
})
|
||||
|
||||
@ -573,7 +573,7 @@ func TestBridge_User_UpdateDraftAndCreateOtherMessage(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Process those events.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
userContinueEventProcess(ctx, t, s, bridge)
|
||||
})
|
||||
|
||||
@ -581,7 +581,7 @@ func TestBridge_User_UpdateDraftAndCreateOtherMessage(t *testing.T) {
|
||||
require.NoError(t, c.MarkMessagesUnread(ctx, res[0].MessageID))
|
||||
|
||||
// Process those events.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
userContinueEventProcess(ctx, t, s, bridge)
|
||||
})
|
||||
})
|
||||
@ -595,7 +595,7 @@ func TestBridge_User_SendDraftRemoveDraftFlag(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Initially sync the user.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
userLoginAndSync(ctx, t, bridge, "user", password)
|
||||
})
|
||||
|
||||
@ -628,7 +628,7 @@ func TestBridge_User_SendDraftRemoveDraftFlag(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Process those events
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
userContinueEventProcess(ctx, t, s, bridge)
|
||||
|
||||
info, err := bridge.QueryUserInfo("user")
|
||||
@ -667,7 +667,7 @@ func TestBridge_User_SendDraftRemoveDraftFlag(t *testing.T) {
|
||||
}
|
||||
|
||||
// Process those events; the draft will move to the sent folder and lose the draft flag.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
userContinueEventProcess(ctx, t, s, bridge)
|
||||
|
||||
info, err := bridge.QueryUserInfo("user")
|
||||
@ -697,7 +697,7 @@ func TestBridge_User_DisableEnableAddress(t *testing.T) {
|
||||
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, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
require.NoError(t, getErr(bridge.LoginFull(ctx, "user", password, nil, nil)))
|
||||
|
||||
// Initially we should list the address.
|
||||
@ -711,7 +711,7 @@ func TestBridge_User_DisableEnableAddress(t *testing.T) {
|
||||
require.NoError(t, c.DisableAddress(ctx, aliasID))
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
// Eventually we shouldn't list the address.
|
||||
require.Eventually(t, func() bool {
|
||||
info, err := bridge.QueryUserInfo("user")
|
||||
@ -726,7 +726,7 @@ func TestBridge_User_DisableEnableAddress(t *testing.T) {
|
||||
require.NoError(t, c.EnableAddress(ctx, aliasID))
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
// Eventually we should list the address.
|
||||
require.Eventually(t, func() bool {
|
||||
info, err := bridge.QueryUserInfo("user")
|
||||
@ -753,7 +753,7 @@ func TestBridge_User_CreateDisabledAddress(t *testing.T) {
|
||||
require.NoError(t, c.DisableAddress(ctx, aliasID))
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
require.NoError(t, getErr(bridge.LoginFull(ctx, "user", password, nil, nil)))
|
||||
|
||||
// Initially we shouldn't list the address.
|
||||
@ -766,7 +766,7 @@ func TestBridge_User_CreateDisabledAddress(t *testing.T) {
|
||||
|
||||
func TestBridge_User_HandleParentLabelRename(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
require.NoError(t, getErr(bridge.LoginFull(ctx, username, password, nil, nil)))
|
||||
|
||||
info, err := bridge.QueryUserInfo(username)
|
||||
|
||||
@ -38,16 +38,12 @@ func (bridge *Bridge) handleUserEvent(ctx context.Context, user *user.User, even
|
||||
|
||||
case events.UserLoadedCheckResync:
|
||||
user.VerifyResyncAndExecute()
|
||||
|
||||
case events.UncategorizedEventError:
|
||||
bridge.handleUncategorizedErrorEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
func (bridge *Bridge) handleUserDeauth(ctx context.Context, user *user.User) {
|
||||
safe.Lock(func() {
|
||||
bridge.logoutUser(ctx, user, false, false, false)
|
||||
user.ReportConfigStatusFailure("User deauth.")
|
||||
bridge.logoutUser(ctx, user, false, false)
|
||||
}, bridge.usersLock)
|
||||
}
|
||||
|
||||
@ -67,12 +63,3 @@ func (bridge *Bridge) handleUserBadEvent(ctx context.Context, user *user.User, e
|
||||
user.OnBadEvent(ctx)
|
||||
}, bridge.usersLock)
|
||||
}
|
||||
|
||||
func (bridge *Bridge) handleUncategorizedErrorEvent(event events.UncategorizedEventError) {
|
||||
if rerr := bridge.reporter.ReportMessageWithContext("Failed to handle due to uncategorized error", reporter.Context{
|
||||
"error_type": internal.ErrCauseType(event.Error),
|
||||
"error": event.Error,
|
||||
}); rerr != nil {
|
||||
logrus.WithField("pkg", "bridge/event").WithError(rerr).Error("Failed to report failed event handling")
|
||||
}
|
||||
}
|
||||
|
||||
@ -35,12 +35,12 @@ import (
|
||||
|
||||
func TestBridge_WithoutUsers(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
require.Empty(t, bridge.GetUserIDs())
|
||||
require.Empty(t, getConnectedUserIDs(t, bridge))
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
require.Empty(t, bridge.GetUserIDs())
|
||||
require.Empty(t, getConnectedUserIDs(t, bridge))
|
||||
})
|
||||
@ -49,7 +49,7 @@ func TestBridge_WithoutUsers(t *testing.T) {
|
||||
|
||||
func TestBridge_Login(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
// Login the user.
|
||||
userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
@ -69,7 +69,7 @@ func TestBridge_Login_DropConn(t *testing.T) {
|
||||
defer func() { _ = dropListener.Close() }()
|
||||
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
// Login the user.
|
||||
userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
@ -96,7 +96,7 @@ func TestBridge_Login_DropConn(t *testing.T) {
|
||||
return 0, false
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
// The user is eventually connected.
|
||||
require.Eventually(t, func() bool {
|
||||
return len(bridge.GetUserIDs()) == 1 && len(getConnectedUserIDs(t, bridge)) == 1
|
||||
@ -107,7 +107,7 @@ func TestBridge_Login_DropConn(t *testing.T) {
|
||||
|
||||
func TestBridge_LoginTwice(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
// Login the user.
|
||||
userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
@ -125,7 +125,7 @@ func TestBridge_LoginTwice(t *testing.T) {
|
||||
|
||||
func TestBridge_LoginLogoutLogin(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
// Login the user.
|
||||
userID := must(bridge.LoginFull(ctx, username, password, nil, nil))
|
||||
|
||||
@ -153,7 +153,7 @@ func TestBridge_LoginLogoutLogin(t *testing.T) {
|
||||
|
||||
func TestBridge_LoginDeleteLogin(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
// Login the user.
|
||||
userID := must(bridge.LoginFull(ctx, username, password, nil, nil))
|
||||
|
||||
@ -181,7 +181,7 @@ func TestBridge_LoginDeleteLogin(t *testing.T) {
|
||||
|
||||
func TestBridge_LoginDeauthLogin(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
// Login the user.
|
||||
userID := must(bridge.LoginFull(ctx, username, password, nil, nil))
|
||||
|
||||
@ -215,7 +215,7 @@ func TestBridge_LoginDeauthRestartLogin(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
var userID string
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
// Login the user.
|
||||
userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
|
||||
|
||||
@ -235,7 +235,7 @@ func TestBridge_LoginDeauthRestartLogin(t *testing.T) {
|
||||
require.IsType(t, events.UserDeauth{}, <-eventCh)
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
// The user should be disconnected at startup.
|
||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||
require.Empty(t, getConnectedUserIDs(t, bridge))
|
||||
@ -257,7 +257,7 @@ func TestBridge_LoginExpireLogin(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
s.SetAuthLife(authLife)
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
// Login the user. Its auth will only be valid for a short time.
|
||||
userID := must(bridge.LoginFull(ctx, username, password, nil, nil))
|
||||
|
||||
@ -275,7 +275,7 @@ func TestBridge_FailToLoad(t *testing.T) {
|
||||
var userID string
|
||||
|
||||
// Login the user.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
|
||||
})
|
||||
|
||||
@ -283,7 +283,7 @@ func TestBridge_FailToLoad(t *testing.T) {
|
||||
require.NoError(t, s.RevokeUser(userID))
|
||||
|
||||
// When bridge starts, the user will not be logged in.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||
require.Empty(t, getConnectedUserIDs(t, bridge))
|
||||
})
|
||||
@ -295,7 +295,7 @@ func TestBridge_LoadWithoutInternet(t *testing.T) {
|
||||
var userID string
|
||||
|
||||
// Login the user.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
|
||||
})
|
||||
|
||||
@ -303,7 +303,7 @@ func TestBridge_LoadWithoutInternet(t *testing.T) {
|
||||
netCtl.Disable()
|
||||
|
||||
// Start bridge without internet.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
// Initially, users are not connected.
|
||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||
require.Empty(t, getConnectedUserIDs(t, bridge))
|
||||
@ -325,11 +325,11 @@ func TestBridge_LoginRestart(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
var userID string
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
|
||||
})
|
||||
@ -340,7 +340,7 @@ func TestBridge_LoginLogoutRestart(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
var userID string
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
// Login the user.
|
||||
userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
|
||||
|
||||
@ -348,7 +348,7 @@ func TestBridge_LoginLogoutRestart(t *testing.T) {
|
||||
require.NoError(t, bridge.LogoutUser(ctx, userID))
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
// The user is still disconnected.
|
||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||
require.Empty(t, getConnectedUserIDs(t, bridge))
|
||||
@ -360,7 +360,7 @@ func TestBridge_LoginDeleteRestart(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
var userID string
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
// Login the user.
|
||||
userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
|
||||
|
||||
@ -368,7 +368,7 @@ func TestBridge_LoginDeleteRestart(t *testing.T) {
|
||||
require.NoError(t, bridge.DeleteUser(ctx, userID))
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
// The user is still gone.
|
||||
require.Empty(t, bridge.GetUserIDs())
|
||||
require.Empty(t, getConnectedUserIDs(t, bridge))
|
||||
@ -384,7 +384,7 @@ func TestBridge_FailLoginRecover(t *testing.T) {
|
||||
|
||||
// Log the user in, wait for it to sync, then log it out.
|
||||
// (We don't want to count message sync data in the test.)
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
||||
defer done()
|
||||
|
||||
@ -396,7 +396,7 @@ func TestBridge_FailLoginRecover(t *testing.T) {
|
||||
var total uint64
|
||||
|
||||
// Now that the user is synced, we can measure exactly how much data is needed during login.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
total = countBytesRead(netCtl, func() {
|
||||
must(bridge.LoginFull(ctx, username, password, nil, nil))
|
||||
})
|
||||
@ -405,7 +405,7 @@ func TestBridge_FailLoginRecover(t *testing.T) {
|
||||
})
|
||||
|
||||
// Now simulate failing to login.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
// Simulate a partial read.
|
||||
netCtl.SetReadLimit(i * total / 10)
|
||||
|
||||
@ -421,7 +421,7 @@ func TestBridge_FailLoginRecover(t *testing.T) {
|
||||
netCtl.SetReadLimit(0)
|
||||
|
||||
// We should now be able to log the user in.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
require.NoError(t, getErr(bridge.LoginFull(ctx, username, password, nil, nil)))
|
||||
|
||||
// The user should be there, now connected.
|
||||
@ -441,7 +441,7 @@ func TestBridge_FailLoadRecover(t *testing.T) {
|
||||
|
||||
// Log the user in and wait for it to sync.
|
||||
// (We don't want to count message sync data in the test.)
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
syncCh, done := chToType[events.Event, events.SyncFinished](bridge.GetEvents(events.SyncFinished{}))
|
||||
defer done()
|
||||
|
||||
@ -451,7 +451,7 @@ func TestBridge_FailLoadRecover(t *testing.T) {
|
||||
|
||||
// See how much data it takes to load the user at startup.
|
||||
total := countBytesRead(netCtl, func() {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(_ *bridge.Bridge, _ *bridge.Mocks) {
|
||||
// ...
|
||||
})
|
||||
})
|
||||
@ -460,7 +460,7 @@ func TestBridge_FailLoadRecover(t *testing.T) {
|
||||
netCtl.SetReadLimit(i * total / 10)
|
||||
|
||||
// We should fail to load the user; it should be listed but disconnected.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||
require.Empty(t, getConnectedUserIDs(t, bridge))
|
||||
})
|
||||
@ -469,7 +469,7 @@ func TestBridge_FailLoadRecover(t *testing.T) {
|
||||
netCtl.SetReadLimit(0)
|
||||
|
||||
// We should now be able to load the user.
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
|
||||
})
|
||||
@ -484,7 +484,7 @@ func TestBridge_BridgePass(t *testing.T) {
|
||||
|
||||
var pass []byte
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
// Login the user.
|
||||
userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
|
||||
|
||||
@ -501,7 +501,7 @@ func TestBridge_BridgePass(t *testing.T) {
|
||||
require.Equal(t, pass, pass)
|
||||
})
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
// The bridge should load the user.
|
||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||
require.Equal(t, []string{userID}, getConnectedUserIDs(t, bridge))
|
||||
@ -514,7 +514,7 @@ func TestBridge_BridgePass(t *testing.T) {
|
||||
|
||||
func TestBridge_AddressMode(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
// Login the user.
|
||||
userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
@ -552,7 +552,7 @@ func TestBridge_AddressMode(t *testing.T) {
|
||||
|
||||
func TestBridge_LoginLogoutRepeated(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
for i := 0; i < 10; i++ {
|
||||
// Log the user in.
|
||||
userID := must(bridge.LoginFull(ctx, username, password, nil, nil))
|
||||
@ -568,7 +568,7 @@ func TestBridge_LogoutOffline(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
var userID string
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
// Login the user.
|
||||
userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
|
||||
|
||||
@ -590,7 +590,7 @@ func TestBridge_LogoutOffline(t *testing.T) {
|
||||
// Go back online.
|
||||
netCtl.Enable()
|
||||
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
// The user is still disconnected.
|
||||
require.Equal(t, []string{userID}, bridge.GetUserIDs())
|
||||
require.Empty(t, getConnectedUserIDs(t, bridge))
|
||||
@ -600,7 +600,7 @@ func TestBridge_LogoutOffline(t *testing.T) {
|
||||
|
||||
func TestBridge_DeleteDisconnected(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
// Login the user.
|
||||
userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
@ -628,7 +628,7 @@ func TestBridge_DeleteDisconnected(t *testing.T) {
|
||||
|
||||
func TestBridge_DeleteOffline(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
// Login the user.
|
||||
userID, err := bridge.LoginFull(ctx, username, password, nil, nil)
|
||||
require.NoError(t, err)
|
||||
@ -652,7 +652,7 @@ func TestBridge_DeleteOffline(t *testing.T) {
|
||||
|
||||
func TestBridge_UserInfo_Alias(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
// Create a new user.
|
||||
userID, _, err := s.CreateUser("primary", []byte("password"))
|
||||
require.NoError(t, err)
|
||||
@ -675,7 +675,7 @@ func TestBridge_UserInfo_Alias(t *testing.T) {
|
||||
|
||||
func TestBridge_User_Refresh(t *testing.T) {
|
||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
|
||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||
// Get a channel of sync started events.
|
||||
syncStartCh, done := chToType[events.Event, events.SyncStarted](bridge.GetEvents(events.SyncStarted{}))
|
||||
defer done()
|
||||
|
||||
@ -1,228 +0,0 @@
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package configstatus
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const version = "1.0.0"
|
||||
|
||||
func LoadConfigurationStatus(filepath string) (*ConfigurationStatus, error) {
|
||||
status := ConfigurationStatus{
|
||||
FilePath: filepath,
|
||||
DataLock: safe.NewRWMutex(),
|
||||
Data: &ConfigurationStatusData{},
|
||||
}
|
||||
|
||||
if _, err := os.Stat(filepath); err == nil {
|
||||
if err := status.Load(); err == nil {
|
||||
return &status, nil
|
||||
}
|
||||
logrus.WithError(err).Warn("Cannot load configuration status file. Reset it.")
|
||||
}
|
||||
|
||||
status.Data.init()
|
||||
if err := status.Save(); err != nil {
|
||||
return &status, err
|
||||
}
|
||||
return &status, nil
|
||||
}
|
||||
|
||||
func (status *ConfigurationStatus) Load() error {
|
||||
bytes, err := os.ReadFile(status.FilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var metadata MetadataOnly
|
||||
if err := json.Unmarshal(bytes, &metadata); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if metadata.Metadata.Version != version {
|
||||
return fmt.Errorf("unsupported configstatus file version %s", metadata.Metadata.Version)
|
||||
}
|
||||
|
||||
return json.Unmarshal(bytes, status.Data)
|
||||
}
|
||||
|
||||
func (status *ConfigurationStatus) Save() error {
|
||||
temp := status.FilePath + "_temp"
|
||||
f, err := os.Create(temp) //nolint:gosec
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
enc := json.NewEncoder(f)
|
||||
enc.SetIndent("", " ")
|
||||
err = enc.Encode(status.Data)
|
||||
if err := f.Close(); err != nil {
|
||||
logrus.WithError(err).Error("Error while closing configstatus file.")
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.Rename(temp, status.FilePath)
|
||||
}
|
||||
|
||||
func (status *ConfigurationStatus) IsPending() bool {
|
||||
status.DataLock.RLock()
|
||||
defer status.DataLock.RUnlock()
|
||||
|
||||
return !status.Data.DataV1.PendingSince.IsZero()
|
||||
}
|
||||
|
||||
func (status *ConfigurationStatus) isPendingSinceMin() int {
|
||||
if min := int(time.Since(status.Data.DataV1.PendingSince).Minutes()); min > 0 {
|
||||
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)) {
|
||||
if !first {
|
||||
str += ","
|
||||
} else {
|
||||
first = false
|
||||
str += "["
|
||||
}
|
||||
str += strconv.Itoa(i)
|
||||
}
|
||||
}
|
||||
if str != "" {
|
||||
str += "]"
|
||||
}
|
||||
return str
|
||||
}
|
||||
@ -1,252 +0,0 @@
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package configstatus_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/configstatus"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestConfigStatus_init_virgin(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "1.0.0", config.Data.Metadata.Version)
|
||||
|
||||
require.Equal(t, false, config.Data.DataV1.PendingSince.IsZero())
|
||||
require.Equal(t, true, config.Data.DataV1.LastProgress.IsZero())
|
||||
|
||||
require.Equal(t, "", config.Data.DataV1.Autoconf)
|
||||
require.Equal(t, uint64(0), config.Data.DataV1.ClickedLink)
|
||||
require.Equal(t, false, config.Data.DataV1.ReportSent)
|
||||
require.Equal(t, false, config.Data.DataV1.ReportClick)
|
||||
require.Equal(t, "", config.Data.DataV1.FailureDetails)
|
||||
}
|
||||
|
||||
func TestConfigStatus_init_existing(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
var data = configstatus.ConfigurationStatusData{
|
||||
Metadata: configstatus.Metadata{Version: "1.0.0"},
|
||||
DataV1: configstatus.DataV1{Autoconf: "Mr TBird"},
|
||||
}
|
||||
require.NoError(t, dumpConfigStatusInFile(&data, file))
|
||||
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "1.0.0", config.Data.Metadata.Version)
|
||||
require.Equal(t, "Mr TBird", config.Data.DataV1.Autoconf)
|
||||
}
|
||||
|
||||
func TestConfigStatus_init_bad_version(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
var data = configstatus.ConfigurationStatusData{
|
||||
Metadata: configstatus.Metadata{Version: "2.0.0"},
|
||||
DataV1: configstatus.DataV1{Autoconf: "Mr TBird"},
|
||||
}
|
||||
require.NoError(t, dumpConfigStatusInFile(&data, file))
|
||||
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "1.0.0", config.Data.Metadata.Version)
|
||||
require.Equal(t, "", config.Data.DataV1.Autoconf)
|
||||
}
|
||||
|
||||
func TestConfigStatus_IsPending(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, true, config.IsPending())
|
||||
config.Data.DataV1.PendingSince = time.Time{}
|
||||
require.Equal(t, false, config.IsPending())
|
||||
}
|
||||
|
||||
func TestConfigStatus_IsFromFailure(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, false, config.IsFromFailure())
|
||||
config.Data.DataV1.FailureDetails = "test"
|
||||
require.Equal(t, true, config.IsFromFailure())
|
||||
}
|
||||
|
||||
func TestConfigStatus_ApplySuccess(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, true, config.IsPending())
|
||||
require.NoError(t, config.ApplySuccess())
|
||||
require.Equal(t, false, config.IsPending())
|
||||
|
||||
config2, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "1.0.0", config2.Data.Metadata.Version)
|
||||
require.Equal(t, true, config2.Data.DataV1.PendingSince.IsZero())
|
||||
require.Equal(t, true, config2.Data.DataV1.LastProgress.IsZero())
|
||||
require.Equal(t, "", config2.Data.DataV1.Autoconf)
|
||||
require.Equal(t, uint64(0), config2.Data.DataV1.ClickedLink)
|
||||
require.Equal(t, false, config2.Data.DataV1.ReportSent)
|
||||
require.Equal(t, false, config2.Data.DataV1.ReportClick)
|
||||
require.Equal(t, "", config2.Data.DataV1.FailureDetails)
|
||||
}
|
||||
|
||||
func TestConfigStatus_ApplyFailure(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, config.ApplySuccess())
|
||||
|
||||
require.NoError(t, config.ApplyFailure("Big Failure"))
|
||||
require.Equal(t, true, config.IsFromFailure())
|
||||
require.Equal(t, true, config.IsPending())
|
||||
|
||||
config2, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "1.0.0", config2.Data.Metadata.Version)
|
||||
require.Equal(t, false, config2.Data.DataV1.PendingSince.IsZero())
|
||||
require.Equal(t, true, config2.Data.DataV1.LastProgress.IsZero())
|
||||
require.Equal(t, "", config2.Data.DataV1.Autoconf)
|
||||
require.Equal(t, uint64(0), config2.Data.DataV1.ClickedLink)
|
||||
require.Equal(t, false, config2.Data.DataV1.ReportSent)
|
||||
require.Equal(t, false, config2.Data.DataV1.ReportClick)
|
||||
require.Equal(t, "Big Failure", config2.Data.DataV1.FailureDetails)
|
||||
}
|
||||
|
||||
func TestConfigStatus_ApplyProgress(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, true, config.IsPending())
|
||||
require.Equal(t, true, config.Data.DataV1.LastProgress.IsZero())
|
||||
|
||||
require.NoError(t, config.ApplyProgress())
|
||||
|
||||
config2, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "1.0.0", config2.Data.Metadata.Version)
|
||||
require.Equal(t, false, config2.Data.DataV1.PendingSince.IsZero())
|
||||
require.Equal(t, false, config2.Data.DataV1.LastProgress.IsZero())
|
||||
require.Equal(t, "", config2.Data.DataV1.Autoconf)
|
||||
require.Equal(t, uint64(0), config2.Data.DataV1.ClickedLink)
|
||||
require.Equal(t, false, config2.Data.DataV1.ReportSent)
|
||||
require.Equal(t, false, config2.Data.DataV1.ReportClick)
|
||||
require.Equal(t, "", config2.Data.DataV1.FailureDetails)
|
||||
}
|
||||
|
||||
func TestConfigStatus_RecordLinkClicked(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, uint64(0), config.Data.DataV1.ClickedLink)
|
||||
require.NoError(t, config.RecordLinkClicked(0))
|
||||
require.Equal(t, uint64(1), config.Data.DataV1.ClickedLink)
|
||||
require.NoError(t, config.RecordLinkClicked(1))
|
||||
require.Equal(t, uint64(3), config.Data.DataV1.ClickedLink)
|
||||
|
||||
config2, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "1.0.0", config2.Data.Metadata.Version)
|
||||
require.Equal(t, false, config2.Data.DataV1.PendingSince.IsZero())
|
||||
require.Equal(t, true, config2.Data.DataV1.LastProgress.IsZero())
|
||||
require.Equal(t, "", config2.Data.DataV1.Autoconf)
|
||||
require.Equal(t, uint64(3), config2.Data.DataV1.ClickedLink)
|
||||
require.Equal(t, false, config2.Data.DataV1.ReportSent)
|
||||
require.Equal(t, false, config2.Data.DataV1.ReportClick)
|
||||
require.Equal(t, "", config2.Data.DataV1.FailureDetails)
|
||||
}
|
||||
|
||||
func TestConfigStatus_ReportClicked(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, false, config.Data.DataV1.ReportClick)
|
||||
require.NoError(t, config.ReportClicked())
|
||||
require.Equal(t, true, config.Data.DataV1.ReportClick)
|
||||
|
||||
config2, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "1.0.0", config2.Data.Metadata.Version)
|
||||
require.Equal(t, false, config2.Data.DataV1.PendingSince.IsZero())
|
||||
require.Equal(t, true, config2.Data.DataV1.LastProgress.IsZero())
|
||||
require.Equal(t, "", config2.Data.DataV1.Autoconf)
|
||||
require.Equal(t, uint64(0), config2.Data.DataV1.ClickedLink)
|
||||
require.Equal(t, false, config2.Data.DataV1.ReportSent)
|
||||
require.Equal(t, true, config2.Data.DataV1.ReportClick)
|
||||
require.Equal(t, "", config2.Data.DataV1.FailureDetails)
|
||||
}
|
||||
|
||||
func TestConfigStatus_ReportSent(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, false, config.Data.DataV1.ReportSent)
|
||||
require.NoError(t, config.ReportSent())
|
||||
require.Equal(t, true, config.Data.DataV1.ReportSent)
|
||||
|
||||
config2, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "1.0.0", config2.Data.Metadata.Version)
|
||||
require.Equal(t, false, config2.Data.DataV1.PendingSince.IsZero())
|
||||
require.Equal(t, true, config2.Data.DataV1.LastProgress.IsZero())
|
||||
require.Equal(t, "", config2.Data.DataV1.Autoconf)
|
||||
require.Equal(t, uint64(0), config2.Data.DataV1.ClickedLink)
|
||||
require.Equal(t, true, config2.Data.DataV1.ReportSent)
|
||||
require.Equal(t, false, config2.Data.DataV1.ReportClick)
|
||||
require.Equal(t, "", config2.Data.DataV1.FailureDetails)
|
||||
}
|
||||
|
||||
func dumpConfigStatusInFile(data *configstatus.ConfigurationStatusData, file string) error {
|
||||
f, err := os.Create(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
return json.NewEncoder(f).Encode(data)
|
||||
}
|
||||
@ -1,59 +0,0 @@
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package configstatus
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type ConfigAbortValues struct {
|
||||
Duration int `json:"duration"`
|
||||
}
|
||||
|
||||
type ConfigAbortDimensions struct {
|
||||
ReportClick string `json:"report_click"`
|
||||
ReportSent string `json:"report_sent"`
|
||||
ClickedLink string `json:"clicked_link"`
|
||||
}
|
||||
|
||||
type ConfigAbortData struct {
|
||||
MeasurementGroup string
|
||||
Event string
|
||||
Values ConfigSuccessValues
|
||||
Dimensions ConfigSuccessDimensions
|
||||
}
|
||||
|
||||
type ConfigAbortBuilder struct{}
|
||||
|
||||
func (*ConfigAbortBuilder) New(config *ConfigurationStatus) ConfigAbortData {
|
||||
config.DataLock.RLock()
|
||||
defer config.DataLock.RUnlock()
|
||||
|
||||
return ConfigAbortData{
|
||||
MeasurementGroup: "bridge.any.configuration",
|
||||
Event: "bridge_config_abort",
|
||||
Values: ConfigSuccessValues{
|
||||
Duration: config.isPendingSinceMin(),
|
||||
},
|
||||
Dimensions: ConfigSuccessDimensions{
|
||||
ReportClick: strconv.FormatBool(config.Data.DataV1.ReportClick),
|
||||
ReportSent: strconv.FormatBool(config.Data.DataV1.ReportSent),
|
||||
ClickedLink: config.Data.clickedLinkToString(),
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -1,75 +0,0 @@
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package configstatus_test
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/configstatus"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestConfigurationAbort_default(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
var builder = configstatus.ConfigAbortBuilder{}
|
||||
req := builder.New(config)
|
||||
|
||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||
require.Equal(t, "bridge_config_abort", req.Event)
|
||||
require.Equal(t, 0, req.Values.Duration)
|
||||
require.Equal(t, "false", req.Dimensions.ReportClick)
|
||||
require.Equal(t, "false", req.Dimensions.ReportSent)
|
||||
require.Equal(t, "", req.Dimensions.ClickedLink)
|
||||
}
|
||||
|
||||
func TestConfigurationAbort_fed(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
var data = configstatus.ConfigurationStatusData{
|
||||
Metadata: configstatus.Metadata{Version: "1.0.0"},
|
||||
DataV1: configstatus.DataV1{
|
||||
PendingSince: time.Now().Add(-10 * time.Minute),
|
||||
LastProgress: time.Time{},
|
||||
Autoconf: "Mr TBird",
|
||||
ClickedLink: 42,
|
||||
ReportSent: false,
|
||||
ReportClick: true,
|
||||
FailureDetails: "Not an error",
|
||||
},
|
||||
}
|
||||
require.NoError(t, dumpConfigStatusInFile(&data, file))
|
||||
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
var builder = configstatus.ConfigAbortBuilder{}
|
||||
req := builder.New(config)
|
||||
|
||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||
require.Equal(t, "bridge_config_abort", req.Event)
|
||||
require.Equal(t, 10, req.Values.Duration)
|
||||
require.Equal(t, "true", req.Dimensions.ReportClick)
|
||||
require.Equal(t, "false", req.Dimensions.ReportSent)
|
||||
require.Equal(t, "[1,3,5]", req.Dimensions.ClickedLink)
|
||||
}
|
||||
@ -1,60 +0,0 @@
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package configstatus
|
||||
|
||||
import "time"
|
||||
|
||||
type ConfigProgressValues struct {
|
||||
NbDay int `json:"nb_day"`
|
||||
NbDaySinceLast int `json:"nb_day_since_last"`
|
||||
}
|
||||
|
||||
type ConfigProgressData struct {
|
||||
MeasurementGroup string
|
||||
Event string
|
||||
Values ConfigProgressValues
|
||||
Dimensions struct{}
|
||||
}
|
||||
|
||||
type ConfigProgressBuilder struct{}
|
||||
|
||||
func (*ConfigProgressBuilder) New(config *ConfigurationStatus) ConfigProgressData {
|
||||
config.DataLock.RLock()
|
||||
defer config.DataLock.RUnlock()
|
||||
|
||||
return ConfigProgressData{
|
||||
MeasurementGroup: "bridge.any.configuration",
|
||||
Event: "bridge_config_progress",
|
||||
Values: ConfigProgressValues{
|
||||
NbDay: numberOfDay(time.Now(), config.Data.DataV1.PendingSince),
|
||||
NbDaySinceLast: numberOfDay(time.Now(), config.Data.DataV1.LastProgress),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func numberOfDay(now, prev time.Time) int {
|
||||
if now.IsZero() || prev.IsZero() {
|
||||
return 1
|
||||
}
|
||||
if now.Year() > prev.Year() {
|
||||
return (365 * (now.Year() - prev.Year())) + now.YearDay() - prev.YearDay()
|
||||
} else if now.YearDay() > prev.YearDay() {
|
||||
return now.YearDay() - prev.YearDay()
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@ -1,100 +0,0 @@
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package configstatus_test
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/configstatus"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestConfigurationProgress_default(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
var builder = configstatus.ConfigProgressBuilder{}
|
||||
req := builder.New(config)
|
||||
|
||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||
require.Equal(t, "bridge_config_progress", req.Event)
|
||||
require.Equal(t, 0, req.Values.NbDay)
|
||||
require.Equal(t, 1, req.Values.NbDaySinceLast)
|
||||
}
|
||||
|
||||
func TestConfigurationProgress_fed(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
var data = configstatus.ConfigurationStatusData{
|
||||
Metadata: configstatus.Metadata{Version: "1.0.0"},
|
||||
DataV1: configstatus.DataV1{
|
||||
PendingSince: time.Now().AddDate(0, 0, -5),
|
||||
LastProgress: time.Now().AddDate(0, 0, -2),
|
||||
Autoconf: "Mr TBird",
|
||||
ClickedLink: 42,
|
||||
ReportSent: false,
|
||||
ReportClick: true,
|
||||
FailureDetails: "Not an error",
|
||||
},
|
||||
}
|
||||
require.NoError(t, dumpConfigStatusInFile(&data, file))
|
||||
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
var builder = configstatus.ConfigProgressBuilder{}
|
||||
req := builder.New(config)
|
||||
|
||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||
require.Equal(t, "bridge_config_progress", req.Event)
|
||||
require.Equal(t, 5, req.Values.NbDay)
|
||||
require.Equal(t, 2, req.Values.NbDaySinceLast)
|
||||
}
|
||||
|
||||
func TestConfigurationProgress_fed_year_change(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
var data = configstatus.ConfigurationStatusData{
|
||||
Metadata: configstatus.Metadata{Version: "1.0.0"},
|
||||
DataV1: configstatus.DataV1{
|
||||
PendingSince: time.Now().AddDate(-1, 0, -5),
|
||||
LastProgress: time.Now().AddDate(0, 0, -2),
|
||||
Autoconf: "Mr TBird",
|
||||
ClickedLink: 42,
|
||||
ReportSent: false,
|
||||
ReportClick: true,
|
||||
FailureDetails: "Not an error",
|
||||
},
|
||||
}
|
||||
require.NoError(t, dumpConfigStatusInFile(&data, file))
|
||||
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
var builder = configstatus.ConfigProgressBuilder{}
|
||||
req := builder.New(config)
|
||||
|
||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||
require.Equal(t, "bridge_config_progress", req.Event)
|
||||
require.True(t, (req.Values.NbDay == 370) || (req.Values.NbDay == 371)) // leap year is accounted for in the simplest manner.
|
||||
require.Equal(t, 2, req.Values.NbDaySinceLast)
|
||||
}
|
||||
@ -1,63 +0,0 @@
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package configstatus
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type ConfigRecoveryValues struct {
|
||||
Duration int `json:"duration"`
|
||||
}
|
||||
|
||||
type ConfigRecoveryDimensions struct {
|
||||
Autoconf string `json:"autoconf"`
|
||||
ReportClick string `json:"report_click"`
|
||||
ReportSent string `json:"report_sent"`
|
||||
ClickedLink string `json:"clicked_link"`
|
||||
FailureDetails string `json:"failure_details"`
|
||||
}
|
||||
|
||||
type ConfigRecoveryData struct {
|
||||
MeasurementGroup string
|
||||
Event string
|
||||
Values ConfigRecoveryValues
|
||||
Dimensions ConfigRecoveryDimensions
|
||||
}
|
||||
|
||||
type ConfigRecoveryBuilder struct{}
|
||||
|
||||
func (*ConfigRecoveryBuilder) New(config *ConfigurationStatus) ConfigRecoveryData {
|
||||
config.DataLock.RLock()
|
||||
defer config.DataLock.RUnlock()
|
||||
|
||||
return ConfigRecoveryData{
|
||||
MeasurementGroup: "bridge.any.configuration",
|
||||
Event: "bridge_config_recovery",
|
||||
Values: ConfigRecoveryValues{
|
||||
Duration: config.isPendingSinceMin(),
|
||||
},
|
||||
Dimensions: ConfigRecoveryDimensions{
|
||||
Autoconf: config.Data.DataV1.Autoconf,
|
||||
ReportClick: strconv.FormatBool(config.Data.DataV1.ReportClick),
|
||||
ReportSent: strconv.FormatBool(config.Data.DataV1.ReportSent),
|
||||
ClickedLink: config.Data.clickedLinkToString(),
|
||||
FailureDetails: config.Data.DataV1.FailureDetails,
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -1,79 +0,0 @@
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package configstatus_test
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/configstatus"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestConfigurationRecovery_default(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
var builder = configstatus.ConfigRecoveryBuilder{}
|
||||
req := builder.New(config)
|
||||
|
||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||
require.Equal(t, "bridge_config_recovery", req.Event)
|
||||
require.Equal(t, 0, req.Values.Duration)
|
||||
require.Equal(t, "", req.Dimensions.Autoconf)
|
||||
require.Equal(t, "false", req.Dimensions.ReportClick)
|
||||
require.Equal(t, "false", req.Dimensions.ReportSent)
|
||||
require.Equal(t, "", req.Dimensions.ClickedLink)
|
||||
require.Equal(t, "", req.Dimensions.FailureDetails)
|
||||
}
|
||||
|
||||
func TestConfigurationRecovery_fed(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
var data = configstatus.ConfigurationStatusData{
|
||||
Metadata: configstatus.Metadata{Version: "1.0.0"},
|
||||
DataV1: configstatus.DataV1{
|
||||
PendingSince: time.Now().Add(-10 * time.Minute),
|
||||
LastProgress: time.Time{},
|
||||
Autoconf: "Mr TBird",
|
||||
ClickedLink: 42,
|
||||
ReportSent: false,
|
||||
ReportClick: true,
|
||||
FailureDetails: "Not an error",
|
||||
},
|
||||
}
|
||||
require.NoError(t, dumpConfigStatusInFile(&data, file))
|
||||
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
var builder = configstatus.ConfigRecoveryBuilder{}
|
||||
req := builder.New(config)
|
||||
|
||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||
require.Equal(t, "bridge_config_recovery", req.Event)
|
||||
require.Equal(t, 10, req.Values.Duration)
|
||||
require.Equal(t, "Mr TBird", req.Dimensions.Autoconf)
|
||||
require.Equal(t, "true", req.Dimensions.ReportClick)
|
||||
require.Equal(t, "false", req.Dimensions.ReportSent)
|
||||
require.Equal(t, "[1,3,5]", req.Dimensions.ClickedLink)
|
||||
require.Equal(t, "Not an error", req.Dimensions.FailureDetails)
|
||||
}
|
||||
@ -1,61 +0,0 @@
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package configstatus
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type ConfigSuccessValues struct {
|
||||
Duration int `json:"duration"`
|
||||
}
|
||||
|
||||
type ConfigSuccessDimensions struct {
|
||||
Autoconf string `json:"autoconf"`
|
||||
ReportClick string `json:"report_click"`
|
||||
ReportSent string `json:"report_sent"`
|
||||
ClickedLink string `json:"clicked_link"`
|
||||
}
|
||||
|
||||
type ConfigSuccessData struct {
|
||||
MeasurementGroup string
|
||||
Event string
|
||||
Values ConfigSuccessValues
|
||||
Dimensions ConfigSuccessDimensions
|
||||
}
|
||||
|
||||
type ConfigSuccessBuilder struct{}
|
||||
|
||||
func (*ConfigSuccessBuilder) New(config *ConfigurationStatus) ConfigSuccessData {
|
||||
config.DataLock.RLock()
|
||||
defer config.DataLock.RUnlock()
|
||||
|
||||
return ConfigSuccessData{
|
||||
MeasurementGroup: "bridge.any.configuration",
|
||||
Event: "bridge_config_success",
|
||||
Values: ConfigSuccessValues{
|
||||
Duration: config.isPendingSinceMin(),
|
||||
},
|
||||
Dimensions: ConfigSuccessDimensions{
|
||||
Autoconf: config.Data.DataV1.Autoconf,
|
||||
ReportClick: strconv.FormatBool(config.Data.DataV1.ReportClick),
|
||||
ReportSent: strconv.FormatBool(config.Data.DataV1.ReportSent),
|
||||
ClickedLink: config.Data.clickedLinkToString(),
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -1,77 +0,0 @@
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package configstatus_test
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/configstatus"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestConfigurationSuccess_default(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
var builder = configstatus.ConfigSuccessBuilder{}
|
||||
req := builder.New(config)
|
||||
|
||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||
require.Equal(t, "bridge_config_success", req.Event)
|
||||
require.Equal(t, 0, req.Values.Duration)
|
||||
require.Equal(t, "", req.Dimensions.Autoconf)
|
||||
require.Equal(t, "false", req.Dimensions.ReportClick)
|
||||
require.Equal(t, "false", req.Dimensions.ReportSent)
|
||||
require.Equal(t, "", req.Dimensions.ClickedLink)
|
||||
}
|
||||
|
||||
func TestConfigurationSuccess_fed(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "dummy.json")
|
||||
var data = configstatus.ConfigurationStatusData{
|
||||
Metadata: configstatus.Metadata{Version: "1.0.0"},
|
||||
DataV1: configstatus.DataV1{
|
||||
PendingSince: time.Now().Add(-10 * time.Minute),
|
||||
LastProgress: time.Time{},
|
||||
Autoconf: "Mr TBird",
|
||||
ClickedLink: 42,
|
||||
ReportSent: false,
|
||||
ReportClick: true,
|
||||
FailureDetails: "Not an error",
|
||||
},
|
||||
}
|
||||
require.NoError(t, dumpConfigStatusInFile(&data, file))
|
||||
|
||||
config, err := configstatus.LoadConfigurationStatus(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
var builder = configstatus.ConfigSuccessBuilder{}
|
||||
req := builder.New(config)
|
||||
|
||||
require.Equal(t, "bridge.any.configuration", req.MeasurementGroup)
|
||||
require.Equal(t, "bridge_config_success", req.Event)
|
||||
require.Equal(t, 10, req.Values.Duration)
|
||||
require.Equal(t, "Mr TBird", req.Dimensions.Autoconf)
|
||||
require.Equal(t, "true", req.Dimensions.ReportClick)
|
||||
require.Equal(t, "false", req.Dimensions.ReportSent)
|
||||
require.Equal(t, "[1,3,5]", req.Dimensions.ClickedLink)
|
||||
}
|
||||
@ -1,56 +0,0 @@
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package configstatus
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||
)
|
||||
|
||||
const ProgressCheckInterval = time.Hour
|
||||
|
||||
type Metadata struct {
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
type MetadataOnly struct {
|
||||
Metadata Metadata `json:"metadata"`
|
||||
}
|
||||
|
||||
type DataV1 struct {
|
||||
PendingSince time.Time `json:"pending_since"`
|
||||
LastProgress time.Time `json:"last_progress"`
|
||||
Autoconf string `json:"auto_conf"`
|
||||
ClickedLink uint64 `json:"clicked_link"`
|
||||
ReportSent bool `json:"report_sent"`
|
||||
ReportClick bool `json:"report_click"`
|
||||
FailureDetails string `json:"failure_details"`
|
||||
}
|
||||
|
||||
type ConfigurationStatusData struct {
|
||||
Metadata Metadata `json:"metadata"`
|
||||
DataV1 DataV1 `json:"dataV1"`
|
||||
}
|
||||
|
||||
type ConfigurationStatus struct {
|
||||
FilePath string
|
||||
DataLock safe.RWMutex
|
||||
|
||||
Data *ConfigurationStatusData
|
||||
}
|
||||
@ -66,7 +66,7 @@ const (
|
||||
KeyChainName = "bridge-v3"
|
||||
|
||||
// Host is the hostname of the bridge server.
|
||||
Host = "127.0.0.1"
|
||||
Host = "0.0.0.0"
|
||||
)
|
||||
|
||||
// nolint:goconst
|
||||
|
||||
@ -151,7 +151,7 @@ func getClientWithJar(t *testing.T, persister Persister) (*http.Client, *Jar) {
|
||||
func getTestServer(t *testing.T, wantCookies []testCookie) *httptest.Server {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
mux.HandleFunc("/set", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
mux.HandleFunc("/set", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
for _, cookie := range wantCookies {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: cookie.name,
|
||||
|
||||
@ -26,7 +26,7 @@ import (
|
||||
// ShowErrorNotification shows a system notification that the app with the given appName has crashed.
|
||||
// NOTE: Icons shouldn't be hardcoded.
|
||||
func ShowErrorNotification(appName string) RecoveryAction {
|
||||
return func(r interface{}) error {
|
||||
return func(_ interface{}) error {
|
||||
notify := notificator.New(notificator.Options{
|
||||
DefaultIcon: "../frontend/ui/icon/icon.png",
|
||||
AppName: appName,
|
||||
|
||||
@ -31,7 +31,7 @@ import (
|
||||
func TestTLSReporter_DoubleReport(t *testing.T) {
|
||||
reportCounter := 0
|
||||
|
||||
reportServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
reportServer := httptest.NewTLSServer(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {
|
||||
reportCounter++
|
||||
}))
|
||||
|
||||
|
||||
@ -33,7 +33,7 @@ func TestProxyProvider_FindProxy(t *testing.T) {
|
||||
defer closeServer(proxy)
|
||||
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"not used"}, async.NoopPanicHandler{})
|
||||
p.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{proxy.URL}, nil }
|
||||
p.dohLookup = func(_ context.Context, _, _ string) ([]string, error) { return []string{proxy.URL}, nil }
|
||||
|
||||
url, err := p.findReachableServer()
|
||||
r.NoError(t, err)
|
||||
@ -49,7 +49,7 @@ func TestProxyProvider_FindProxy_ChooseReachableProxy(t *testing.T) {
|
||||
closeServer(unreachableProxy)
|
||||
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"not used"}, async.NoopPanicHandler{})
|
||||
p.dohLookup = func(ctx context.Context, q, p string) ([]string, error) {
|
||||
p.dohLookup = func(_ context.Context, _, _ string) ([]string, error) {
|
||||
return []string{reachableProxy.URL, unreachableProxy.URL}, nil
|
||||
}
|
||||
|
||||
@ -70,7 +70,7 @@ func TestProxyProvider_FindProxy_ChooseTrustedProxy(t *testing.T) {
|
||||
dialer := NewPinningTLSDialer(NewBasicTLSDialer(""), reporter, checker)
|
||||
|
||||
p := newProxyProvider(dialer, "", []string{"not used"}, async.NoopPanicHandler{})
|
||||
p.dohLookup = func(ctx context.Context, q, p string) ([]string, error) {
|
||||
p.dohLookup = func(_ context.Context, _, _ string) ([]string, error) {
|
||||
return []string{untrustedProxy.URL, trustedProxy.URL}, nil
|
||||
}
|
||||
|
||||
@ -87,7 +87,7 @@ func TestProxyProvider_FindProxy_FailIfNoneReachable(t *testing.T) {
|
||||
closeServer(unreachableProxy2)
|
||||
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"not used"}, async.NoopPanicHandler{})
|
||||
p.dohLookup = func(ctx context.Context, q, p string) ([]string, error) {
|
||||
p.dohLookup = func(_ context.Context, _, _ string) ([]string, error) {
|
||||
return []string{unreachableProxy1.URL, unreachableProxy2.URL}, nil
|
||||
}
|
||||
|
||||
@ -107,7 +107,7 @@ func TestProxyProvider_FindProxy_FailIfNoneTrusted(t *testing.T) {
|
||||
dialer := NewPinningTLSDialer(NewBasicTLSDialer(""), reporter, checker)
|
||||
|
||||
p := newProxyProvider(dialer, "", []string{"not used"}, async.NoopPanicHandler{})
|
||||
p.dohLookup = func(ctx context.Context, q, p string) ([]string, error) {
|
||||
p.dohLookup = func(_ context.Context, _, _ string) ([]string, error) {
|
||||
return []string{untrustedProxy1.URL, untrustedProxy2.URL}, nil
|
||||
}
|
||||
|
||||
@ -118,7 +118,7 @@ func TestProxyProvider_FindProxy_FailIfNoneTrusted(t *testing.T) {
|
||||
func TestProxyProvider_FindProxy_RefreshCacheTimeout(t *testing.T) {
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"not used"}, async.NoopPanicHandler{})
|
||||
p.cacheRefreshTimeout = 1 * time.Second
|
||||
p.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { time.Sleep(2 * time.Second); return nil, nil }
|
||||
p.dohLookup = func(_ context.Context, _, _ string) ([]string, error) { time.Sleep(2 * time.Second); return nil, nil }
|
||||
|
||||
// We should fail to refresh the proxy cache because the doh provider
|
||||
// takes 2 seconds to respond but we timeout after just 1 second.
|
||||
@ -135,7 +135,7 @@ func TestProxyProvider_FindProxy_CanReachTimeout(t *testing.T) {
|
||||
|
||||
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"not used"}, async.NoopPanicHandler{})
|
||||
p.canReachTimeout = 1 * time.Second
|
||||
p.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{slowProxy.URL}, nil }
|
||||
p.dohLookup = func(_ context.Context, _, _ string) ([]string, error) { return []string{slowProxy.URL}, nil }
|
||||
|
||||
// We should fail to reach the returned proxy because it takes 2 seconds
|
||||
// to reach it and we only allow 1.
|
||||
|
||||
@ -112,7 +112,7 @@ vwRMog6lPhlRhHh/FZ43Cg==
|
||||
|
||||
// getUntrustedServer returns a server but it doesn't add its public key to the list of pinned ones.
|
||||
func getUntrustedServer() *httptest.Server {
|
||||
server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
||||
server := httptest.NewUnstartedServer(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {}))
|
||||
|
||||
cert, err := tls.X509KeyPair([]byte(servercrt), []byte(serverkey))
|
||||
if err != nil {
|
||||
@ -145,7 +145,7 @@ func TestProxyDialer_UseProxy(t *testing.T) {
|
||||
provider := newProxyProvider(NewBasicTLSDialer(""), "", DoHProviders, async.NoopPanicHandler{})
|
||||
d := NewProxyTLSDialer(NewBasicTLSDialer(""), "", async.NoopPanicHandler{})
|
||||
d.proxyProvider = provider
|
||||
provider.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{trustedProxy.URL}, nil }
|
||||
provider.dohLookup = func(_ context.Context, _, _ string) ([]string, error) { return []string{trustedProxy.URL}, nil }
|
||||
|
||||
err := d.switchToReachableServer()
|
||||
require.NoError(t, err)
|
||||
@ -163,7 +163,7 @@ func TestProxyDialer_UseProxy_MultipleTimes(t *testing.T) {
|
||||
provider := newProxyProvider(NewBasicTLSDialer(""), "", DoHProviders, async.NoopPanicHandler{})
|
||||
d := NewProxyTLSDialer(NewBasicTLSDialer(""), "", async.NoopPanicHandler{})
|
||||
d.proxyProvider = provider
|
||||
provider.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{proxy1.URL}, nil }
|
||||
provider.dohLookup = func(_ context.Context, _, _ string) ([]string, error) { return []string{proxy1.URL}, nil }
|
||||
|
||||
err := d.switchToReachableServer()
|
||||
require.NoError(t, err)
|
||||
@ -172,7 +172,7 @@ func TestProxyDialer_UseProxy_MultipleTimes(t *testing.T) {
|
||||
// Have to wait so as to not get rejected.
|
||||
time.Sleep(proxyLookupWait)
|
||||
|
||||
provider.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{proxy2.URL}, nil }
|
||||
provider.dohLookup = func(_ context.Context, _, _ string) ([]string, error) { return []string{proxy2.URL}, nil }
|
||||
err = d.switchToReachableServer()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, formatAsAddress(proxy2.URL), d.proxyAddress)
|
||||
@ -180,7 +180,7 @@ func TestProxyDialer_UseProxy_MultipleTimes(t *testing.T) {
|
||||
// Have to wait so as to not get rejected.
|
||||
time.Sleep(proxyLookupWait)
|
||||
|
||||
provider.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{proxy3.URL}, nil }
|
||||
provider.dohLookup = func(_ context.Context, _, _ string) ([]string, error) { return []string{proxy3.URL}, nil }
|
||||
err = d.switchToReachableServer()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, formatAsAddress(proxy3.URL), d.proxyAddress)
|
||||
@ -195,7 +195,7 @@ func TestProxyDialer_UseProxy_RevertAfterTime(t *testing.T) {
|
||||
d.proxyProvider = provider
|
||||
d.proxyUseDuration = time.Second
|
||||
|
||||
provider.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{trustedProxy.URL}, nil }
|
||||
provider.dohLookup = func(_ context.Context, _, _ string) ([]string, error) { return []string{trustedProxy.URL}, nil }
|
||||
err := d.switchToReachableServer()
|
||||
require.NoError(t, err)
|
||||
|
||||
@ -216,7 +216,7 @@ func TestProxyDialer_UseProxy_RevertIfProxyStopsWorkingAndOriginalAPIIsReachable
|
||||
provider := newProxyProvider(NewBasicTLSDialer(""), "", DoHProviders, async.NoopPanicHandler{})
|
||||
d := NewProxyTLSDialer(NewBasicTLSDialer(""), "", async.NoopPanicHandler{})
|
||||
d.proxyProvider = provider
|
||||
provider.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{trustedProxy.URL}, nil }
|
||||
provider.dohLookup = func(_ context.Context, _, _ string) ([]string, error) { return []string{trustedProxy.URL}, nil }
|
||||
|
||||
err := d.switchToReachableServer()
|
||||
require.NoError(t, err)
|
||||
@ -246,7 +246,7 @@ func TestProxyDialer_UseProxy_FindSecondAlternativeIfFirstFailsAndAPIIsStillBloc
|
||||
provider := newProxyProvider(NewBasicTLSDialer(""), "", DoHProviders, async.NoopPanicHandler{})
|
||||
d := NewProxyTLSDialer(NewBasicTLSDialer(""), "", async.NoopPanicHandler{})
|
||||
d.proxyProvider = provider
|
||||
provider.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{proxy1.URL, proxy2.URL}, nil }
|
||||
provider.dohLookup = func(_ context.Context, _, _ string) ([]string, error) { return []string{proxy1.URL, proxy2.URL}, nil }
|
||||
|
||||
err := d.switchToReachableServer()
|
||||
require.NoError(t, err)
|
||||
|
||||
@ -212,3 +212,16 @@ type UserLoadedCheckResync struct {
|
||||
func (event UserLoadedCheckResync) String() string {
|
||||
return fmt.Sprintf("UserLoadedCheckResync: UserID: %s", event.UserID)
|
||||
}
|
||||
|
||||
type UserNotification struct {
|
||||
eventBase
|
||||
|
||||
UserID string
|
||||
Title string
|
||||
Subtitle string
|
||||
Body string
|
||||
}
|
||||
|
||||
func (event UserNotification) String() string {
|
||||
return fmt.Sprintf("UserNotification: UserID: %s, Title: %s, Subtitle: %s, Body: %s", event.UserID, event.Title, event.Subtitle, event.Body)
|
||||
}
|
||||
|
||||
@ -30,7 +30,7 @@ using namespace bridgepp;
|
||||
namespace {
|
||||
|
||||
QString const defaultKeychain = "defaultKeychain"; ///< The default keychain.
|
||||
QString const HV_ERROR_TEMPLATE = "failed to create new API client: 422 POST https://mail-api.proton.me/auth/v4: CAPTCHA validation failed (Code=12087, Status=422)";
|
||||
QString const HV_ERROR_TEMPLATE = "Human verification failed. Please try again.";
|
||||
|
||||
}
|
||||
|
||||
@ -364,9 +364,9 @@ grpc::Status GRPCService::RequestKnowledgeBaseSuggestions(ServerContext*, String
|
||||
|
||||
QList<bridgepp::KnowledgeBaseSuggestion> suggestions;
|
||||
for (qsizetype i = 1; i <= 3; ++i) {
|
||||
suggestions.push_back( {
|
||||
.title = QString("Suggested link %1").arg(i),
|
||||
suggestions.push_back({
|
||||
.url = QString("https://proton.me/support/bridge#%1").arg(i),
|
||||
.title = QString("Suggested link %1").arg(i),
|
||||
});
|
||||
}
|
||||
qtProxy_.sendDelayedEvent(newKnowledgeBaseSuggestionsEvent(app().mainWindow().knowledgeBaseTab().getSuggestions()));
|
||||
@ -846,32 +846,6 @@ Status GRPCService::InstallTLSCertificate(ServerContext *, Empty const *, Empty
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] request The request.
|
||||
//****************************************************************************************************************************************************
|
||||
Status GRPCService::ExternalLinkClicked(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) {
|
||||
app().log().debug(QString("%1 - URL = %2").arg(__FUNCTION__, QString::fromStdString(request->value())));
|
||||
return Status::OK;
|
||||
}
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
//
|
||||
//****************************************************************************************************************************************************
|
||||
Status GRPCService::ReportBugClicked(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::Empty *) {
|
||||
app().log().debug(__FUNCTION__);
|
||||
return Status::OK;
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] request The request.
|
||||
//****************************************************************************************************************************************************
|
||||
Status GRPCService::AutoconfigClicked(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *response) {
|
||||
app().log().debug(QString("%1 - Client = %2").arg(__FUNCTION__, QString::fromStdString(request->value())));
|
||||
return Status::OK;
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] request The request
|
||||
/// \param[in] writer The writer
|
||||
|
||||
@ -97,9 +97,6 @@ public: // member functions.
|
||||
grpc::Status IsTLSCertificateInstalled(::grpc::ServerContext *, ::google::protobuf::Empty const*, ::google::protobuf::BoolValue *response) override;
|
||||
grpc::Status InstallTLSCertificate(::grpc::ServerContext *, ::google::protobuf::Empty const*, ::google::protobuf::Empty *) override;
|
||||
grpc::Status ExportTLSCertificates(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
|
||||
grpc::Status ReportBugClicked(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::google::protobuf::Empty *) override;
|
||||
grpc::Status AutoconfigClicked(::grpc::ServerContext *context, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
|
||||
grpc::Status ExternalLinkClicked(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
|
||||
grpc::Status RunEventStream(::grpc::ServerContext *ctx, ::grpc::EventStreamRequest const *request, ::grpc::ServerWriter<::grpc::StreamEvent> *writer) override;
|
||||
grpc::Status StopEventStream(::grpc::ServerContext *, ::google::protobuf::Empty const *, ::google::protobuf::Empty *) override;
|
||||
bool sendEvent(bridgepp::SPStreamEvent const &event); ///< Queue an event for sending through the event stream.
|
||||
|
||||
@ -57,6 +57,7 @@ UsersTab::UsersTab(QWidget *parent)
|
||||
connect(ui_.checkUsernamePasswordError, &QCheckBox::toggled, this, &UsersTab::updateGUIState);
|
||||
connect(ui_.checkSync, &QCheckBox::toggled, this, &UsersTab::onCheckSyncToggled);
|
||||
connect(ui_.sliderSync, &QSlider::valueChanged, this, &UsersTab::onSliderSyncValueChanged);
|
||||
connect(ui_.sendNotificationButton, &QPushButton::clicked, this, &UsersTab::onSendUserNotification);
|
||||
|
||||
users_.append(defaultUser());
|
||||
|
||||
@ -216,6 +217,7 @@ void UsersTab::updateGUIState() {
|
||||
ui_.editUsernamePasswordError->setEnabled(ui_.checkUsernamePasswordError->isChecked());
|
||||
ui_.spinUsedBytes->setValue(user ? user->usedBytes() : 0.0);
|
||||
ui_.groupboxSync->setEnabled(user.get());
|
||||
ui_.groupBoxNotification->setEnabled(hasSelectedUser && (UserState::Connected == state));
|
||||
|
||||
if (user)
|
||||
ui_.editIMAPLoginFailedUsername->setText(user->primaryEmailOrUsername());
|
||||
@ -489,3 +491,41 @@ void UsersTab::onSliderSyncValueChanged(int value) {
|
||||
app().grpc().sendEvent(newSyncProgressEvent(user->id(), progress, 1, 1)); // we do not simulate elapsed & remaining.
|
||||
this->updateGUIState();
|
||||
}
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return the title for the notification.
|
||||
//****************************************************************************************************************************************************
|
||||
QString UsersTab::notificationTitle() const {
|
||||
return ui_.notificationTitle->text();
|
||||
}
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return the subtitle for the notification.
|
||||
//****************************************************************************************************************************************************
|
||||
QString UsersTab::notificationSubtitle() const {
|
||||
return ui_.notificationSubtitleText->text();
|
||||
}
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return the body for the notification.
|
||||
//****************************************************************************************************************************************************
|
||||
QString UsersTab::notificationBody() const {
|
||||
return ui_.notticationBodyText->text();
|
||||
}
|
||||
|
||||
|
||||
void UsersTab::onSendUserNotification() {
|
||||
SPUser const user = selectedUser();
|
||||
if (!user) {
|
||||
app().log().error(QString("%1 failed. Unkown user.").arg(__FUNCTION__));
|
||||
return;
|
||||
}
|
||||
|
||||
GRPCService &grpc = app().grpc();
|
||||
|
||||
if (grpc.isStreaming()) {
|
||||
QString const userID = user->id();
|
||||
grpc.sendEvent(newUserNotificationEvent(userID, notificationTitle(), notificationSubtitle(), notificationBody()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -23,7 +23,6 @@
|
||||
#include "Tabs/ui_UsersTab.h"
|
||||
#include "UserTable.h"
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \brief The 'Users' tab of the main window.
|
||||
//****************************************************************************************************************************************************
|
||||
@ -50,12 +49,15 @@ public: // member functions.
|
||||
bool nextUserTwoPasswordsError() const; ///< Check if next user login should trigger 2nd password error.
|
||||
bool nextUserTwoPasswordsAbort() const; ///< Check if next user login should trigger 2nd password abort.
|
||||
QString usernamePasswordErrorMessage() const; ///< Return the username password error message.
|
||||
QString notificationTitle() const; ///< Return the user notification title.
|
||||
QString notificationSubtitle() const; ///< Return the user notification subtitle.
|
||||
QString notificationBody() const; ///< Return the user notification body.
|
||||
|
||||
public slots:
|
||||
void setUserSplitMode(QString const &userID, bool makeItActive); ///< Slot for the split mode.
|
||||
void logoutUser(QString const &userID); ///< slot for the logging out of a user.
|
||||
void removeUser(QString const &userID); ///< Slot for the removal of a user.
|
||||
static void configureUserAppleMail(QString const &userID, QString const &address); ///< Slot for the configuration of Apple mail.
|
||||
static void configureUserAppleMail(QString const &userID, QString const &address); ///< Slot for the configuration of Apple mail.
|
||||
void processBadEventUserFeedback(QString const& userID, bool doResync); ///< Slot for the reception of a bad event user feedback.
|
||||
|
||||
private slots:
|
||||
@ -69,6 +71,7 @@ private slots:
|
||||
void onCheckSyncToggled(bool checked); ///< Slot for the 'Synchronizing' check box.
|
||||
void onSliderSyncValueChanged(int value); ///< Slot for the sync 'Progress' slider.
|
||||
void updateGUIState(); ///< Update the GUI state.
|
||||
void onSendUserNotification(); ///< Send a user notification event to the GUI.
|
||||
|
||||
private: // member functions.
|
||||
qint32 selectedIndex() const; ///< Get the index of the selected row.
|
||||
|
||||
@ -7,13 +7,19 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1221</width>
|
||||
<height>894</height>
|
||||
<height>408</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout" stretch="1,0">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout" stretch="0,0">
|
||||
<item>
|
||||
<widget class="QTableView" name="tableUserList">
|
||||
<property name="selectionMode">
|
||||
@ -30,6 +36,27 @@
|
||||
</attribute>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QScrollArea" name="scrollArea">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="widgetResizable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<widget class="QWidget" name="scrollAreaWidgetContents">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>327</width>
|
||||
<height>905</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_6">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
@ -66,6 +93,68 @@
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBoxNotification">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>300</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>300</width>
|
||||
<height>400</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Notification</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_9">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_6" stretch="0,0,0,0">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="notificationTitle">
|
||||
<property name="placeholderText">
|
||||
<string>Title</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="notificationSubtitleText">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="placeholderText">
|
||||
<string>Subtitle</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="notticationBodyText">
|
||||
<property name="placeholderText">
|
||||
<string>Body</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="sendNotificationButton">
|
||||
<property name="text">
|
||||
<string>Send</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupboxSync">
|
||||
<property name="minimumSize">
|
||||
@ -240,7 +329,7 @@
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>100</height>
|
||||
<height>250</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="title">
|
||||
@ -360,6 +449,10 @@
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<tabstops>
|
||||
<tabstop>buttonNewUser</tabstop>
|
||||
<tabstop>buttonEditUser</tabstop>
|
||||
|
||||
@ -75,7 +75,7 @@ if(NOT UNIX)
|
||||
set(CMAKE_INSTALL_BINDIR ".")
|
||||
endif(NOT UNIX)
|
||||
|
||||
find_package(Qt6 COMPONENTS Core Quick Qml QuickControls2 Widgets Svg REQUIRED)
|
||||
find_package(Qt6 COMPONENTS Core Quick Qml QuickControls2 Widgets Svg Gui REQUIRED)
|
||||
qt_standard_project_setup()
|
||||
set(CMAKE_AUTORCC ON)
|
||||
message(STATUS "Using Qt ${Qt6_VERSION}")
|
||||
@ -120,6 +120,7 @@ add_executable(bridge-gui
|
||||
UserList.cpp UserList.h
|
||||
SentryUtils.cpp SentryUtils.h
|
||||
Settings.cpp Settings.h
|
||||
ClipboardProxy.cpp ClipboardProxy.h
|
||||
${DOCK_ICON_SRC_FILE} MacOS/DockIcon.h
|
||||
)
|
||||
|
||||
@ -148,6 +149,7 @@ target_link_libraries(bridge-gui
|
||||
Qt6::Qml
|
||||
Qt6::QuickControls2
|
||||
Qt6::Svg
|
||||
Qt6::Gui
|
||||
sentry::sentry
|
||||
bridgepp
|
||||
)
|
||||
|
||||
25
internal/frontend/bridge-gui/bridge-gui/ClipboardProxy.cpp
Normal file
25
internal/frontend/bridge-gui/bridge-gui/ClipboardProxy.cpp
Normal file
@ -0,0 +1,25 @@
|
||||
// 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/>.
|
||||
#include "ClipboardProxy.h"
|
||||
|
||||
// The following definitions were taken and adapted from:
|
||||
// https://stackoverflow.com/questions/40092352/passing-qclipboard-to-qml
|
||||
// Author: krzaq
|
||||
|
||||
ClipboardProxy::ClipboardProxy(QClipboard* c) : clipboard(c) {
|
||||
connect(clipboard, &QClipboard::dataChanged, this, &ClipboardProxy::textChanged);
|
||||
}
|
||||
|
||||
QString ClipboardProxy::text() const {
|
||||
return clipboard->text();
|
||||
}
|
||||
38
internal/frontend/bridge-gui/bridge-gui/ClipboardProxy.h
Normal file
38
internal/frontend/bridge-gui/bridge-gui/ClipboardProxy.h
Normal file
@ -0,0 +1,38 @@
|
||||
// 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/>.
|
||||
#ifndef BRIDGE_GUI_CLIPBOARDPROXY_H
|
||||
#define BRIDGE_GUI_CLIPBOARDPROXY_H
|
||||
|
||||
// The following class declarations were taken and adapted from:
|
||||
// https://stackoverflow.com/questions/40092352/passing-qclipboard-to-qml
|
||||
// Author: krzaq
|
||||
|
||||
|
||||
class ClipboardProxy : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QString text READ text NOTIFY textChanged)
|
||||
public:
|
||||
explicit ClipboardProxy(QClipboard*);
|
||||
|
||||
QString text() const;
|
||||
|
||||
signals:
|
||||
void textChanged();
|
||||
|
||||
private:
|
||||
QClipboard* clipboard;
|
||||
};
|
||||
|
||||
|
||||
#endif //BRIDGE_GUI_CLIPBOARDPROXY_H
|
||||
@ -26,6 +26,7 @@
|
||||
#include <QtWidgets>
|
||||
#include <QtQuickControls2>
|
||||
#include <QtSvg>
|
||||
#include <QtGui>
|
||||
#include <AppController.h>
|
||||
|
||||
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
#include <bridgepp/GRPC/GRPCClient.h>
|
||||
#include <bridgepp/Worker/Overseer.h>
|
||||
|
||||
#include "Settings.h"
|
||||
|
||||
#define HANDLE_EXCEPTION(x) try { x } \
|
||||
catch (Exception const &e) { emit fatalError(e); } \
|
||||
@ -59,15 +60,19 @@ QMLBackend::QMLBackend()
|
||||
/// \param[in] serviceConfig
|
||||
//****************************************************************************************************************************************************
|
||||
void QMLBackend::init(GRPCConfig const &serviceConfig) {
|
||||
Log &log = app().log();
|
||||
log.info(QString("Connecting to gRPC service"));
|
||||
|
||||
trayIcon_.reset(new TrayIcon());
|
||||
connect(this, &QMLBackend::trayIconVisibleChanged, trayIcon_.get(), &TrayIcon::setVisible);
|
||||
log.info(QString("Tray icon is visible: %1").arg(trayIcon_->isVisible() ? "true" : "false"));
|
||||
this->setNormalTrayIcon();
|
||||
|
||||
|
||||
connect(this, &QMLBackend::fatalError, &app(), &AppController::onFatalError);
|
||||
|
||||
users_ = new UserList(this);
|
||||
|
||||
Log &log = app().log();
|
||||
log.info(QString("Connecting to gRPC service"));
|
||||
app().grpc().setLog(&log);
|
||||
this->connectGrpcEvents();
|
||||
|
||||
@ -298,7 +303,6 @@ void QMLBackend::openExternalLink(QString const &url) {
|
||||
HANDLE_EXCEPTION(
|
||||
QString const u = url.isEmpty() ? bridgeKBUrl : url;
|
||||
QDesktopServices::openUrl(u);
|
||||
emit notifyExternalLinkClicked(u);
|
||||
)
|
||||
}
|
||||
|
||||
@ -731,6 +735,32 @@ void QMLBackend::setDockIconVisible(bool visible) {
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] visible Should the tray icon be visible.
|
||||
//****************************************************************************************************************************************************
|
||||
void QMLBackend::setTrayIconVisible(bool visible) {
|
||||
HANDLE_EXCEPTION(
|
||||
AppController& app = ::app();
|
||||
if (visible == app.settings().trayIconVisible()) {
|
||||
return;
|
||||
}
|
||||
app.settings().setTrayIconVisible(visible);
|
||||
emit trayIconVisibleChanged(visible);
|
||||
app.log().info(QString("Changing tray icon visibility to %1").arg(visible ? "true" : "false"));
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
//
|
||||
//****************************************************************************************************************************************************
|
||||
bool QMLBackend::trayIconVisible() const {
|
||||
HANDLE_EXCEPTION_RETURN_BOOL(
|
||||
return app().settings().trayIconVisible();
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] active Should we activate autostart.
|
||||
//****************************************************************************************************************************************************
|
||||
@ -1064,33 +1094,6 @@ void QMLBackend::sendBadEventUserFeedback(QString const &userID, bool doResync)
|
||||
)
|
||||
}
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
///
|
||||
//****************************************************************************************************************************************************
|
||||
void QMLBackend::notifyReportBugClicked() const {
|
||||
HANDLE_EXCEPTION(
|
||||
app().grpc().reportBugClicked();
|
||||
)
|
||||
}
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] client The selected Mail client for autoconfig.
|
||||
//****************************************************************************************************************************************************
|
||||
void QMLBackend::notifyAutoconfigClicked(QString const &client) const {
|
||||
HANDLE_EXCEPTION(
|
||||
app().grpc().autoconfigClicked(client);
|
||||
)
|
||||
}
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] article The url of the KB article.
|
||||
//****************************************************************************************************************************************************
|
||||
void QMLBackend::notifyExternalLinkClicked(QString const &article) const {
|
||||
HANDLE_EXCEPTION(
|
||||
app().grpc().externalLinkClicked(article);
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
//
|
||||
//****************************************************************************************************************************************************
|
||||
@ -1330,6 +1333,7 @@ void QMLBackend::connectGrpcEvents() {
|
||||
connect(client, &GRPCClient::knowledgeBasSuggestionsReceived, this, &QMLBackend::receivedKnowledgeBaseSuggestions);
|
||||
connect(client, &GRPCClient::repairStarted, this, &QMLBackend::repairStarted);
|
||||
connect(client, &GRPCClient::allUsersLoaded, this, &QMLBackend::allUsersLoaded);
|
||||
connect(client, &GRPCClient::userNotificationReceived, this, &QMLBackend::processUserNotification);
|
||||
|
||||
// cache events
|
||||
connect(client, &GRPCClient::cantMoveDiskCache, this, &QMLBackend::cantMoveDiskCache);
|
||||
@ -1418,3 +1422,25 @@ void QMLBackend::triggerRepair() const {
|
||||
app().grpc().triggerRepair();
|
||||
)
|
||||
}
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] notification The user notification received from the event loop.
|
||||
//****************************************************************************************************************************************************
|
||||
void QMLBackend::processUserNotification(bridgepp::UserNotification const& notification) {
|
||||
this->userNotificationStack_.push(notification);
|
||||
trayIcon_->showUserNotification(notification.title, notification.subtitle);
|
||||
emit receivedUserNotification(notification);
|
||||
}
|
||||
|
||||
void QMLBackend::userNotificationDismissed() {
|
||||
if (!this->userNotificationStack_.size()) return;
|
||||
|
||||
// Remove the user notification from the top of the queue as it has been dismissed.
|
||||
this->userNotificationStack_.pop();
|
||||
if (!this->userNotificationStack_.size()) return;
|
||||
|
||||
// Display the user notification that is on top of the queue, if there is one.
|
||||
auto notification = this->userNotificationStack_.top();
|
||||
emit receivedUserNotification(notification);
|
||||
}
|
||||
|
||||
|
||||
@ -28,6 +28,7 @@
|
||||
#include <bridgepp/GRPC/GRPCClient.h>
|
||||
#include <bridgepp/GRPC/GRPCUtils.h>
|
||||
#include <bridgepp/Worker/Overseer.h>
|
||||
#include <stack>
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
@ -101,6 +102,7 @@ public: // Qt/QML properties. Note that the NOTIFY-er signal is required even fo
|
||||
Q_PROPERTY(QVariantList bugQuestions READ bugQuestions NOTIFY bugQuestionsChanged)
|
||||
Q_PROPERTY(UserList *users MEMBER users_ NOTIFY usersChanged)
|
||||
Q_PROPERTY(bool dockIconVisible READ dockIconVisible WRITE setDockIconVisible NOTIFY dockIconVisibleChanged)
|
||||
Q_PROPERTY(bool trayIconVisible READ trayIconVisible WRITE setTrayIconVisible NOTIFY trayIconVisibleChanged)
|
||||
|
||||
// Qt Property system setters & getters.
|
||||
bool showOnStartup() const; ///< Getter for the 'showOnStartup' property.
|
||||
@ -140,6 +142,9 @@ public: // Qt/QML properties. Note that the NOTIFY-er signal is required even fo
|
||||
QVariantList bugQuestions() const; ///< Getter for the 'bugQuestions' property.
|
||||
void setDockIconVisible(bool visible); ///< Setter for the 'dockIconVisible' property.
|
||||
bool dockIconVisible() const;; ///< Getter for the 'dockIconVisible' property.
|
||||
void setTrayIconVisible(bool visible); ///< Setter for the 'trayIconVisible' property.
|
||||
bool trayIconVisible() const; ///< Getter for the 'trayIconVisible' property.
|
||||
|
||||
|
||||
signals: // Signal used by the Qt property system. Many of them are unused but required to avoid warning from the QML engine.
|
||||
void showSplashScreenChanged(bool value); ///<Signal for the change of the 'showSplashScreen' property.
|
||||
@ -174,6 +179,9 @@ signals: // Signal used by the Qt property system. Many of them are unused but r
|
||||
void isAutostartOnChanged(bool value); ///<Signal for the change of the 'isAutostartOn' property.
|
||||
void usersChanged(UserList *users); ///<Signal for the change of the 'users' property.
|
||||
void dockIconVisibleChanged(bool value); ///<Signal for the change of the 'dockIconVisible' property.
|
||||
void trayIconVisibleChanged(bool value); ///< Signal for the change of the 'trayIconVisible' property.
|
||||
void receivedUserNotification(bridgepp::UserNotification const& notification); ///< Signal to display the userNotification modal
|
||||
|
||||
|
||||
public slots: // slot for signals received from QML -> To be forwarded to Bridge via RPC Client calls.
|
||||
void toggleAutostart(bool active); ///< Slot for the autostart toggle.
|
||||
@ -205,10 +213,8 @@ public slots: // slot for signals received from QML -> To be forwarded to Bridge
|
||||
void onVersionChanged(); ///< Slot for the version change signal.
|
||||
void setMailServerSettings(int imapPort, int smtpPort, bool useSSLForIMAP, bool useSSLForSMTP) const; ///< Forwards a connection mode change request from QML to gRPC
|
||||
void sendBadEventUserFeedback(QString const &userID, bool doResync); ///< Slot the providing user feedback for a bad event.
|
||||
void notifyReportBugClicked() const; ///< Slot for the ReportBugClicked gRPC event.
|
||||
void notifyAutoconfigClicked(QString const &client) const; ///< Slot for gAutoconfigClicked gRPC event.
|
||||
void notifyExternalLinkClicked(QString const &article) const; ///< Slot for KBArticleClicked gRPC event.
|
||||
void triggerRepair() const; ///< Slot for the triggering of the bridge repair function i.e. 'resync'.
|
||||
void userNotificationDismissed(); ///< Slot to pop the notification from the stack and display the rest.
|
||||
|
||||
public slots: // slots for functions that need to be processed locally.
|
||||
void setNormalTrayIcon(); ///< Set the tray icon to normal.
|
||||
@ -224,6 +230,7 @@ public slots: // slot for signals received from gRPC that need transformation in
|
||||
void onLoginAlreadyLoggedIn(QString const &userID); ///< Slot for the LoginAlreadyLoggedIn gRPC event.
|
||||
void onUserBadEvent(QString const& userID, QString const& errorMessage); ///< Slot for the userBadEvent gRPC event.
|
||||
void onIMAPLoginFailed(QString const& username); ///< Slot the the imapLoginFailed event.
|
||||
void processUserNotification(bridgepp::UserNotification const& notification); ///< Slot for the userNotificationReceived gRCP event.
|
||||
|
||||
signals: // Signals received from the Go backend, to be forwarded to QML
|
||||
void toggleAutostartFinished(); ///< Signal for the 'toggleAutostartFinished' gRPC stream event.
|
||||
@ -310,6 +317,7 @@ private: // data members
|
||||
QList<QString> badEventDisplayQueue_; ///< THe queue for displaying 'bad event feedback request dialog'.
|
||||
std::unique_ptr<TrayIcon> trayIcon_; ///< The tray icon for the application.
|
||||
bridgepp::BugReportFlow reportFlow_; ///< The bug report flow.
|
||||
std::stack<bridgepp::UserNotification> userNotificationStack_; ///< The stack which holds all of the active notifications that the user needs to acknowledge.
|
||||
friend class AppController;
|
||||
};
|
||||
|
||||
|
||||
@ -71,6 +71,7 @@
|
||||
<file>qml/icons/systray-mono-update.png</file>
|
||||
<file>qml/icons/systray-mono-warn.png</file>
|
||||
<file>qml/icons/systray.svg</file>
|
||||
<file>qml/icons/ic-notification-bell.svg</file>
|
||||
<file alias="bridge.svg">../../../../dist/bridge.svg</file>
|
||||
<file alias="bridgeMacOS.svg">../../../../dist/bridgeMacOS.svg</file>
|
||||
<file>qml/KeychainSettings.qml</file>
|
||||
@ -78,6 +79,7 @@
|
||||
<file>qml/MainWindow.qml</file>
|
||||
<file>qml/NoAccountView.qml</file>
|
||||
<file>qml/NotificationDialog.qml</file>
|
||||
<file>qml/UserNotificationDialog.qml</file>
|
||||
<file>qml/NotificationPopups.qml</file>
|
||||
<file>qml/Notifications/Notification.qml</file>
|
||||
<file>qml/Notifications/NotificationFilter.qml</file>
|
||||
@ -132,5 +134,6 @@
|
||||
<file>qml/ConnectionModeSettings.qml</file>
|
||||
<file>qml/SplashScreen.qml</file>
|
||||
<file>qml/Status.qml</file>
|
||||
<file>qml/Proton/ContextMenu.qml</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
@ -28,6 +28,7 @@ namespace {
|
||||
|
||||
QString const settingsFileName = "bridge-gui.ini"; ///< The name of the settings file.
|
||||
QString const keyUseSoftwareRenderer = "UseSoftwareRenderer"; ///< The key for storing the 'Use software rendering' setting.
|
||||
QString const keyTrayIconVisible = "TrayIconVisible"; ///< The key for storing the 'Tray icon visible' setting.
|
||||
|
||||
|
||||
}
|
||||
@ -36,7 +37,7 @@ QString const keyUseSoftwareRenderer = "UseSoftwareRenderer"; ///< The key for s
|
||||
//
|
||||
//****************************************************************************************************************************************************
|
||||
Settings::Settings()
|
||||
: settings_(QDir(userConfigDir()).absoluteFilePath("bridge-gui.ini"), QSettings::Format::IniFormat) {
|
||||
: settings_(QDir(userConfigDir()).absoluteFilePath(settingsFileName), QSettings::Format::IniFormat) {
|
||||
}
|
||||
|
||||
|
||||
@ -54,3 +55,17 @@ bool Settings::useSoftwareRenderer() const {
|
||||
void Settings::setUseSoftwareRenderer(bool value) {
|
||||
settings_.setValue(keyUseSoftwareRenderer, value);
|
||||
}
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] value The value for the 'Tray icon visible' setting.
|
||||
//****************************************************************************************************************************************************
|
||||
void Settings::setTrayIconVisible(bool value) {
|
||||
settings_.setValue(keyTrayIconVisible, value);
|
||||
}
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \return The value for the 'Tray icon visible' setting.
|
||||
//****************************************************************************************************************************************************
|
||||
bool Settings::trayIconVisible() const {
|
||||
return settings_.value(keyTrayIconVisible, true).toBool();
|
||||
}
|
||||
|
||||
@ -33,6 +33,8 @@ public: // member functions.
|
||||
|
||||
bool useSoftwareRenderer() const; ///< Get the 'Use software renderer' settings value.
|
||||
void setUseSoftwareRenderer(bool value); ///< Set the 'Use software renderer' settings value.
|
||||
void setTrayIconVisible(bool value); ///< Get the 'Tray icon visible' setting value.
|
||||
bool trayIconVisible() const; ///< Set the 'Tray icon visible' setting value.
|
||||
|
||||
private: // member functions.
|
||||
Settings(); ///< Default constructor.
|
||||
|
||||
@ -21,6 +21,7 @@
|
||||
#include <bridgepp/Exception/Exception.h>
|
||||
#include <bridgepp/BridgeUtils.h>
|
||||
|
||||
#include "Settings.h"
|
||||
|
||||
using namespace bridgepp;
|
||||
|
||||
@ -195,7 +196,7 @@ TrayIcon::TrayIcon()
|
||||
}
|
||||
|
||||
this->setIcon();
|
||||
this->show();
|
||||
this->setVisible(app().settings().trayIconVisible());
|
||||
|
||||
// TrayIcon does not expose its screen, so we connect relevant screen events to our DPI change handler.
|
||||
for (QScreen *screen: QGuiApplication::screens()) {
|
||||
@ -209,7 +210,6 @@ TrayIcon::TrayIcon()
|
||||
connect(&iconRefreshTimer_, &QTimer::timeout, this, &TrayIcon::onIconRefreshTimer);
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
//
|
||||
//****************************************************************************************************************************************************
|
||||
@ -322,16 +322,42 @@ void TrayIcon::setState(TrayIcon::State state, QString const &stateString, QStri
|
||||
this->generateStatusIcon(statusIconPath, stateColor(state));
|
||||
}
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \brief A helper struct to temporarily force the tray to be visible. Useful for operations that do not work when the tray is not visible,
|
||||
/// such as showMessage().
|
||||
//****************************************************************************************************************************************************
|
||||
struct ScopedTrayVisibility {
|
||||
explicit ScopedTrayVisibility(TrayIcon& trayIcon) : trayIcon_(trayIcon), wasVisible_(app().settings().trayIconVisible()) {
|
||||
trayIcon_.setVisible(true);
|
||||
}
|
||||
~ScopedTrayVisibility() { trayIcon_.setVisible(wasVisible_); }
|
||||
|
||||
private:
|
||||
TrayIcon &trayIcon_;
|
||||
bool wasVisible_;
|
||||
};
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] title The title.
|
||||
/// \param[in] message The message.
|
||||
//****************************************************************************************************************************************************
|
||||
void TrayIcon::showErrorPopupNotification(QString const &title, QString const &message) {
|
||||
ScopedTrayVisibility visible(*this);
|
||||
this->showMessage(title, message, notificationErrorIcon_);
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// Used only by user notifications received from the event loop
|
||||
/// \param[in] title The title.
|
||||
/// \param[in] subtitle The subtitle.
|
||||
//****************************************************************************************************************************************************
|
||||
void TrayIcon::showUserNotification(QString const &title, QString const &subtitle) {
|
||||
ScopedTrayVisibility visible(*this);
|
||||
this->showMessage(title, subtitle, QSystemTrayIcon::NoIcon);
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] svgPath The path of the SVG file for the icon.
|
||||
/// \param[in] color The color to apply to the icon.
|
||||
|
||||
@ -42,7 +42,7 @@ public: // data members
|
||||
TrayIcon& operator=(TrayIcon&&) = delete; ///< Disabled move assignment operator.
|
||||
void setState(State state, QString const& stateString, QString const &statusIconPath); ///< Set the state of the icon
|
||||
void showErrorPopupNotification(QString const& title, QString const &message); ///< Display a pop up notification.
|
||||
|
||||
void showUserNotification(QString const& title, QString const &subtitle); ///< Display an OS pop up notification (without icon).
|
||||
signals:
|
||||
void selectUser(QString const& userID, bool forceShowWindow); ///< Signal for selecting a user with a given userID
|
||||
|
||||
|
||||
@ -28,6 +28,7 @@
|
||||
#include <bridgepp/Log/Log.h>
|
||||
#include <bridgepp/Log/LogUtils.h>
|
||||
#include <bridgepp/ProcessMonitor.h>
|
||||
#include <ClipboardProxy.h>
|
||||
|
||||
#include "bridgepp/CLI/CLIUtils.h"
|
||||
|
||||
@ -53,6 +54,7 @@ QString const bridgeGUILock = "bridge-v3-gui.lock"; ///< The file name used for
|
||||
QString const exeName = "bridge" + exeSuffix; ///< The bridge executable file name.*
|
||||
qint64 constexpr grpcServiceConfigWaitDelayMs = 180000; ///< The wait delay for the gRPC config file in milliseconds.
|
||||
QString const waitFlag = "--wait"; ///< The wait command-line flag.
|
||||
QString const orphanInstanceException = "An orphan instance of bridge is already running. Please terminate it and relaunch the application.";
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
@ -316,7 +318,7 @@ int main(int argc, char *argv[]) {
|
||||
QString bridgeExe;
|
||||
if (!cliOptions.attach) {
|
||||
if (isBridgeRunning()) {
|
||||
throw Exception("An orphan instance of bridge is already running. Please terminate it and relaunch the application.",
|
||||
throw Exception(orphanInstanceException,
|
||||
QString(), __FUNCTION__, tailOfLatestBridgeLog(sessionID));
|
||||
}
|
||||
|
||||
@ -347,6 +349,8 @@ int main(int argc, char *argv[]) {
|
||||
log.info(QString("Qt Quick renderer: %1").arg(QQuickWindow::sceneGraphBackend()));
|
||||
|
||||
QQmlApplicationEngine engine;
|
||||
// Set up clipboard
|
||||
engine.rootContext()->setContextProperty("clipboard", new ClipboardProxy(QGuiApplication::clipboard()));
|
||||
std::unique_ptr<QQmlComponent> rootComponent(createRootQmlComponent(engine));
|
||||
std::unique_ptr<QObject> rootObject(rootComponent->create(engine.rootContext()));
|
||||
if (!rootObject) {
|
||||
@ -410,13 +414,18 @@ int main(int argc, char *argv[]) {
|
||||
lock.unlock();
|
||||
return result;
|
||||
} catch (Exception const &e) {
|
||||
sentry_uuid_s const uuid = reportSentryException("Exception occurred during main", e);
|
||||
QString message = e.qwhat();
|
||||
if (e.showSupportLink()) {
|
||||
message += R"(<br/><br/>If the issue persists, please contact our <a href="https://proton.me/support/contact">customer support</a>.)";
|
||||
}
|
||||
QMessageBox::critical(nullptr, "Error", message);
|
||||
QTextStream(stderr) << "reportID: " << QByteArray(uuid.bytes, 16).toHex() << " Captured exception :" << e.detailedWhat() << "\n";
|
||||
|
||||
if (e.qwhat() != orphanInstanceException) {
|
||||
sentry_uuid_s const uuid = reportSentryException("Exception occurred during main", e);
|
||||
QTextStream(stderr) << "reportID: " << QByteArray(uuid.bytes, 16).toHex() << " Captured exception :"
|
||||
<< e.detailedWhat() << "\n";
|
||||
}
|
||||
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
@ -86,11 +86,13 @@ Item {
|
||||
}
|
||||
}
|
||||
Button {
|
||||
id: signIn
|
||||
Layout.alignment: Qt.AlignTop
|
||||
colorScheme: root.colorScheme
|
||||
secondary: true
|
||||
text: qsTr("Sign in")
|
||||
visible: root.user ? (root.user.state === EUserState.SignedOut) : false
|
||||
Accessible.name: text
|
||||
|
||||
onClicked: {
|
||||
if (user) {
|
||||
@ -99,11 +101,13 @@ Item {
|
||||
}
|
||||
}
|
||||
Button {
|
||||
id: removeAccount
|
||||
Layout.alignment: Qt.AlignTop
|
||||
colorScheme: root.colorScheme
|
||||
icon.source: "/qml/icons/ic-trash.svg"
|
||||
secondary: true
|
||||
visible: root.user ? root.user.state !== EUserState.Locked : false
|
||||
Accessible.name: qsTr("Remove account")
|
||||
|
||||
onClicked: {
|
||||
if (!root.user)
|
||||
@ -118,6 +122,7 @@ Item {
|
||||
height: root._lineThickness
|
||||
}
|
||||
SettingsItem {
|
||||
id: configureEmailClient
|
||||
Layout.fillWidth: true
|
||||
actionText: qsTr("Configure email client")
|
||||
colorScheme: root.colorScheme
|
||||
@ -126,6 +131,7 @@ Item {
|
||||
text: qsTr("Email clients")
|
||||
type: SettingsItem.PrimaryButton
|
||||
visible: _connected && ((!root.user.splitMode) || (root.user.addresses.length === 1))
|
||||
Accessible.name: actionText
|
||||
|
||||
onClicked: {
|
||||
if (!root.user)
|
||||
@ -143,6 +149,7 @@ Item {
|
||||
text: qsTr("Split addresses")
|
||||
type: SettingsItem.Toggle
|
||||
visible: _connected && root.user.addresses.length > 1
|
||||
Accessible.name: text
|
||||
|
||||
onClicked: {
|
||||
if (!splitMode.checked) {
|
||||
|
||||
@ -28,7 +28,7 @@ Popup {
|
||||
implicitWidth: 600 // contentLayout.implicitWidth + contentLayout.anchors.leftMargin + contentLayout.anchors.rightMargin
|
||||
leftMargin: (mainWindow.width - root.implicitWidth) / 2
|
||||
modal: false
|
||||
popupType: ApplicationWindow.PopupType.Banner
|
||||
popupPriority: ApplicationWindow.PopupPriority.Banner
|
||||
shouldShow: notification ? (notification.active && !notification.dismissed) : false
|
||||
topMargin: 37
|
||||
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Controls.impl
|
||||
import Proton
|
||||
|
||||
Item {
|
||||
|
||||
@ -114,6 +114,7 @@ Item {
|
||||
colorScheme: leftBar.colorScheme
|
||||
horizontalPadding: 0
|
||||
icon.source: "/qml/icons/ic-question-circle.svg"
|
||||
Accessible.name: qsTr("Help")
|
||||
|
||||
onClicked: rightContent.showHelpView()
|
||||
}
|
||||
@ -130,6 +131,7 @@ Item {
|
||||
colorScheme: leftBar.colorScheme
|
||||
horizontalPadding: 0
|
||||
icon.source: "/qml/icons/ic-cog-wheel.svg"
|
||||
Accessible.name: qsTr("Settings")
|
||||
|
||||
onClicked: rightContent.showGeneralSettings()
|
||||
}
|
||||
@ -147,6 +149,7 @@ Item {
|
||||
colorScheme: leftBar.colorScheme
|
||||
horizontalPadding: 0
|
||||
icon.source: "/qml/icons/ic-three-dots-vertical.svg"
|
||||
Accessible.name: "..."
|
||||
|
||||
onClicked: {
|
||||
dotMenu.open();
|
||||
@ -319,6 +322,7 @@ Item {
|
||||
horizontalPadding: 0
|
||||
icon.source: "/qml/icons/ic-plus.svg"
|
||||
width: 36
|
||||
Accessible.name: qsTr("Add account")
|
||||
|
||||
onClicked: {
|
||||
root.showLogin("");
|
||||
|
||||
@ -147,6 +147,19 @@ SettingsView {
|
||||
|
||||
onClicked: Backend.changeColorScheme(darkMode.checked ? "light" : "dark")
|
||||
}
|
||||
SettingsItem {
|
||||
id: trayIconVisible
|
||||
Layout.fillWidth: true
|
||||
checked: Backend.trayIconVisible
|
||||
colorScheme: root.colorScheme
|
||||
description: qsTr("Show the Bridge icon in the menu bar. When the Bridge icon is not visible, launch the " +
|
||||
"application again to display the main window.")
|
||||
text: qsTr("Show the Bridge icon in the menu bar")
|
||||
type: SettingsItem.Toggle
|
||||
visible: (Backend.goos === "darwin") && root._isAdvancedShown
|
||||
|
||||
onClicked: Backend.trayIconVisible = !trayIconVisible.checked
|
||||
}
|
||||
SettingsItem {
|
||||
id: allMail
|
||||
Layout.fillWidth: true
|
||||
|
||||
@ -83,7 +83,6 @@ SettingsView {
|
||||
|
||||
onClicked: {
|
||||
Backend.updateCurrentMailClient();
|
||||
Backend.notifyReportBugClicked();
|
||||
root.parent.showBugReport();
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,6 +22,7 @@ Dialog {
|
||||
|
||||
default property alias data: additionalChildrenContainer.children
|
||||
property var notification
|
||||
property bool isUserNotification: false
|
||||
|
||||
modal: true
|
||||
shouldShow: notification && notification.active && !notification.dismissed
|
||||
|
||||
@ -109,4 +109,8 @@ Item {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.repairBridge
|
||||
}
|
||||
UserNotificationDialog {
|
||||
colorScheme: root.colorScheme
|
||||
notification: root.notifications.userNotification
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,7 +19,8 @@ QtObject {
|
||||
Info,
|
||||
Success,
|
||||
Warning,
|
||||
Danger
|
||||
Danger,
|
||||
UserNotification
|
||||
}
|
||||
|
||||
property list<Action> action
|
||||
@ -36,6 +37,9 @@ QtObject {
|
||||
readonly property var occurred: active ? new Date() : undefined
|
||||
property string title // title is used in dialogs only
|
||||
property int type
|
||||
property string subtitle
|
||||
property string username
|
||||
|
||||
|
||||
onActiveChanged: {
|
||||
dismissed = false;
|
||||
|
||||
@ -62,7 +62,7 @@ QtObject {
|
||||
target: Backend
|
||||
}
|
||||
}
|
||||
property var all: [root.noInternet, root.imapPortStartupError, root.smtpPortStartupError, root.imapPortChangeError, root.smtpPortChangeError, root.imapConnectionModeChangeError, root.smtpConnectionModeChangeError, root.updateManualReady, root.updateManualRestartNeeded, root.updateManualError, root.updateForce, root.updateForceError, root.updateSilentRestartNeeded, root.updateSilentError, root.updateIsLatestVersion, root.loginConnectionError, root.onlyPaidUsers, root.alreadyLoggedIn, root.enableBeta, root.bugReportSendSuccess, root.bugReportSendError, root.bugReportSendFallback, root.cacheCantMove, root.cacheLocationChangeSuccess, root.enableSplitMode, root.resetBridge, root.changeAllMailVisibility, root.deleteAccount, root.noKeychain, root.rebuildKeychain, root.addressChanged, root.apiCertIssue, root.userBadEvent, root.imapLoginWhileSignedOut, root.genericError, root.genericQuestion, root.hvErrorEvent, root.repairBridge]
|
||||
property var all: [root.noInternet, root.imapPortStartupError, root.smtpPortStartupError, root.imapPortChangeError, root.smtpPortChangeError, root.imapConnectionModeChangeError, root.smtpConnectionModeChangeError, root.updateManualReady, root.updateManualRestartNeeded, root.updateManualError, root.updateForce, root.updateForceError, root.updateSilentRestartNeeded, root.updateSilentError, root.updateIsLatestVersion, root.loginConnectionError, root.onlyPaidUsers, root.alreadyLoggedIn, root.enableBeta, root.bugReportSendSuccess, root.bugReportSendError, root.bugReportSendFallback, root.cacheCantMove, root.cacheLocationChangeSuccess, root.enableSplitMode, root.resetBridge, root.changeAllMailVisibility, root.deleteAccount, root.noKeychain, root.rebuildKeychain, root.addressChanged, root.apiCertIssue, root.userBadEvent, root.imapLoginWhileSignedOut, root.genericError, root.genericQuestion, root.hvErrorEvent, root.repairBridge, root.userNotification]
|
||||
property Notification alreadyLoggedIn: Notification {
|
||||
brief: qsTr("Already signed in")
|
||||
description: qsTr("This account is already signed in.")
|
||||
@ -1200,6 +1200,35 @@ QtObject {
|
||||
|
||||
}
|
||||
|
||||
property Notification userNotification: Notification {
|
||||
brief: title
|
||||
group: Notifications.Group.Dialogs
|
||||
type: Notification.NotificationType.UserNotification
|
||||
icon: "./icons/ic-exclamation-circle-filled.svg" // If it's not included QML complains
|
||||
|
||||
action: [
|
||||
Action {
|
||||
text: qsTr("Okay")
|
||||
onTriggered: {
|
||||
root.userNotification.active = false;
|
||||
Backend.userNotificationDismissed();
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Connections {
|
||||
function onReceivedUserNotification(notification) {
|
||||
const userPrimaryEmailOrUsername = Backend.users.primaryEmailOrUsername(notification.userID)
|
||||
root.userNotification.title = notification.title
|
||||
root.userNotification.subtitle = notification.subtitle
|
||||
root.userNotification.description = notification.body
|
||||
root.userNotification.username = userPrimaryEmailOrUsername
|
||||
root.userNotification.active = true
|
||||
}
|
||||
target: Backend
|
||||
}
|
||||
}
|
||||
|
||||
signal askChangeAllMailVisibility(var isVisibleNow)
|
||||
signal askDeleteAccount(var user)
|
||||
signal askEnableBeta
|
||||
|
||||
@ -21,7 +21,7 @@ T.ApplicationWindow {
|
||||
id: root
|
||||
|
||||
// popup priority based on types
|
||||
enum PopupType {
|
||||
enum PopupPriority {
|
||||
Banner,
|
||||
Dialog
|
||||
}
|
||||
@ -73,10 +73,15 @@ T.ApplicationWindow {
|
||||
if (obj.shouldShow === false) {
|
||||
continue;
|
||||
}
|
||||
if (topmost && (topmost.popupType > obj.popupType)) {
|
||||
// User notifications should have display priority
|
||||
if (obj.shouldShow && obj.isUserNotification) {
|
||||
topmost = obj;
|
||||
break;
|
||||
}
|
||||
if (topmost && (topmost.popupPriority > obj.popupPriority)) {
|
||||
continue;
|
||||
}
|
||||
if (topmost && (topmost.popupType === obj.popupType) && (topmost.occurred > obj.occurred)) {
|
||||
if (topmost && (topmost.popupPriority === obj.popupPriority) && (topmost.occurred > obj.occurred)) {
|
||||
continue;
|
||||
}
|
||||
topmost = obj;
|
||||
|
||||
@ -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/>.
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
Item {
|
||||
property var parentObject: null
|
||||
property var colorScheme: null
|
||||
property bool readOnly: false
|
||||
property bool isPassword: false
|
||||
|
||||
MouseArea {
|
||||
id: controlMouseArea
|
||||
width: parentObject ? parentObject.width : 0
|
||||
height: parentObject ? parentObject.height : 0
|
||||
acceptedButtons: Qt.RightButton
|
||||
onClicked: controlContextMenu.popup()
|
||||
|
||||
propagateComposedEvents: true
|
||||
}
|
||||
|
||||
Menu {
|
||||
id: controlContextMenu
|
||||
colorScheme: root.colorScheme
|
||||
onVisibleChanged: {
|
||||
if (controlContextMenu.visible) {
|
||||
const hasSelectedText = parentObject.selectedText.length > 0;
|
||||
const hasClipboardText = clipboard.text.length > 0;
|
||||
|
||||
copyMenuItem.visible = hasSelectedText && !isPassword;
|
||||
cutMenuItem.visible = hasSelectedText && !readOnly && !isPassword;
|
||||
pasteMenuItem.visible = hasClipboardText && !readOnly;
|
||||
controlContextMenu.visible = copyMenuItem.visible || cutMenuItem.visible || pasteMenuItem.visible;
|
||||
}
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
id: cutMenuItem
|
||||
colorScheme: root.colorScheme
|
||||
height: visible ? implicitHeight : 0
|
||||
text: qsTr("Cut")
|
||||
|
||||
onClicked: {
|
||||
parentObject.cut()
|
||||
}
|
||||
}
|
||||
MenuItem {
|
||||
id: copyMenuItem
|
||||
colorScheme: root.colorScheme
|
||||
height: visible ? implicitHeight : 0
|
||||
text: qsTr("Copy")
|
||||
|
||||
onTriggered: {
|
||||
parentObject.copy()
|
||||
}
|
||||
}
|
||||
MenuItem {
|
||||
id: pasteMenuItem
|
||||
colorScheme: root.colorScheme
|
||||
height: visible ? implicitHeight : 0
|
||||
|
||||
text: qsTr("Paste")
|
||||
onTriggered: {
|
||||
parentObject.paste()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -21,7 +21,7 @@ T.Dialog {
|
||||
|
||||
property ColorScheme colorScheme
|
||||
readonly property var occurred: shouldShow ? new Date() : undefined
|
||||
readonly property int popupType: ApplicationWindow.PopupType.Dialog
|
||||
readonly property int popupPriority: ApplicationWindow.PopupPriority.Dialog
|
||||
property bool shouldShow: false
|
||||
|
||||
function close() {
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Controls.impl
|
||||
import QtQuick.Layouts
|
||||
|
||||
ColorImage {
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Controls.impl
|
||||
import QtQuick.Layouts
|
||||
|
||||
RowLayout {
|
||||
|
||||
@ -21,7 +21,7 @@ T.Popup {
|
||||
|
||||
property ColorScheme colorScheme
|
||||
readonly property var occurred: shouldShow ? new Date() : undefined
|
||||
property int popupType: ApplicationWindow.PopupType.Banner
|
||||
property int popupPriority: ApplicationWindow.PopupPriority.Banner
|
||||
property bool shouldShow: false
|
||||
|
||||
function close() {
|
||||
|
||||
@ -362,4 +362,9 @@ FocusScope {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Proton.ContextMenu {
|
||||
parentObject: root
|
||||
colorScheme: root.colorScheme
|
||||
}
|
||||
}
|
||||
|
||||
@ -172,6 +172,8 @@ FocusScope {
|
||||
|
||||
implicitHeight: children[0].implicitHeight
|
||||
implicitWidth: children[0].implicitWidth
|
||||
Accessible.role: Accessible.Grouping
|
||||
Accessible.name: label.text
|
||||
|
||||
onEditingFinished: {
|
||||
if (!validateOnEditingFinished) {
|
||||
@ -274,6 +276,7 @@ FocusScope {
|
||||
selectionColor: control.palette.highlight
|
||||
topPadding: 8
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
Accessible.name: label.text + qsTr(" edit")
|
||||
|
||||
background: Item {
|
||||
implicitHeight: 36
|
||||
@ -331,6 +334,15 @@ FocusScope {
|
||||
x: control.leftPadding
|
||||
y: control.topPadding
|
||||
}
|
||||
|
||||
Proton.ContextMenu {
|
||||
parentObject: control
|
||||
colorScheme: root.colorScheme
|
||||
isPassword: control.echoMode === TextInput.Password
|
||||
readOnly: control.readOnly
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Proton.Button {
|
||||
id: eyeButton
|
||||
@ -340,6 +352,7 @@ FocusScope {
|
||||
icon.color: control.color
|
||||
icon.source: checked ? "../icons/ic-eye-slash.svg" : "../icons/ic-eye.svg"
|
||||
visible: root.echoMode === TextInput.Password
|
||||
Accessible.name: label.text + qsTr(" show check")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,3 +39,4 @@ TextArea 4.0 TextArea.qml
|
||||
TextField 4.0 TextField.qml
|
||||
Toggle 4.0 Toggle.qml
|
||||
WebFrame 4.0 WebFrame.qml
|
||||
ContextMenu 4.0 ContextMenu.qml
|
||||
|
||||
@ -41,6 +41,8 @@ Item {
|
||||
|
||||
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
|
||||
implicitWidth: children[0].implicitWidth + children[0].anchors.leftMargin + children[0].anchors.rightMargin
|
||||
Accessible.name: text
|
||||
Accessible.role: Accessible.Grouping
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
@ -77,6 +79,8 @@ Item {
|
||||
colorScheme: root.colorScheme
|
||||
loading: root.loading
|
||||
visible: root.type === SettingsItem.ActionType.Toggle
|
||||
Accessible.role: Accessible.CheckBox
|
||||
Accessible.name: root.Accessible.name + " toggle"
|
||||
|
||||
onClicked: {
|
||||
if (!root.loading)
|
||||
@ -92,6 +96,8 @@ Item {
|
||||
secondary: root.type !== SettingsItem.PrimaryButton
|
||||
text: root.actionText
|
||||
visible: root.type === SettingsItem.Button || root.type === SettingsItem.PrimaryButton
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: root.Accessible.name + " button"
|
||||
|
||||
onClicked: {
|
||||
if (!root.loading)
|
||||
|
||||
@ -80,6 +80,7 @@ Item {
|
||||
horizontalPadding: 8
|
||||
icon.source: "/qml/icons/ic-arrow-left.svg"
|
||||
secondary: true
|
||||
Accessible.name: qsTr("Back")
|
||||
|
||||
onClicked: root.back()
|
||||
|
||||
|
||||
@ -51,7 +51,7 @@ Item {
|
||||
color: colorScheme.text_weak
|
||||
colorScheme: wizard.colorScheme
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: qsTr("A system pop-up will appear. Double click on the entry with your email, and click ’Install’ in the dialog that appears.")
|
||||
text: qsTr("A series of pop-ups will appear. Follow the instructions to install the profile.")
|
||||
type: Label.LabelType.Body
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user