forked from Silverfish/proton-bridge
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 43cbedafb8 | |||
| ac9ab8ab32 | |||
| f04350c046 | |||
| ed1b65731a | |||
| d12928b31c | |||
| 1ea06a95b7 | |||
| e290cd308b | |||
| 3d53bf7477 | |||
| 84c0b907d7 | |||
| b30455b641 | |||
| db9902e70b | |||
| f1f63c1d03 | |||
| 81a3c2aba8 | |||
| bbfc9beb04 | |||
| c4dba09ee6 | |||
| a5435eb1da | |||
| 54c56efdfa | |||
| fc64dbec59 | |||
| 5d3f084a2b | |||
| 606d6c0e3e | |||
| 9fbb6b4ca5 |
@ -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
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
17
Changelog.md
17
Changelog.md
@ -3,6 +3,23 @@
|
||||
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
|
||||
|
||||
## 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.13.0+git
|
||||
APP_VERSION:=${BRIDGE_APP_VERSION}
|
||||
APP_FULL_NAME:=Proton Mail Bridge
|
||||
APP_VENDOR:=Proton AG
|
||||
@ -189,7 +189,7 @@ ${RESOURCE_FILE}: ./dist/info.rc ./dist/${SRC_ICO} .FORCE
|
||||
|
||||
## Dev dependencies
|
||||
.PHONY: install-devel-tools install-linter install-go-mod-outdated install-git-hooks
|
||||
LINTVER:="v1.55.2"
|
||||
LINTVER:="v1.59.1"
|
||||
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 \
|
||||
|
||||
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_
|
||||
2
go.mod
2
go.mod
@ -9,7 +9,7 @@ require (
|
||||
github.com/Masterminds/semver/v3 v3.2.0
|
||||
github.com/ProtonMail/gluon v0.17.1-0.20240514133734-79cdd0fec41c
|
||||
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.20240829112804-d663a2ef90c2
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton
|
||||
github.com/PuerkitoBio/goquery v1.8.1
|
||||
github.com/abiosoft/ishell v2.0.0+incompatible
|
||||
|
||||
24
go.sum
24
go.sum
@ -27,8 +27,6 @@ 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/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
|
||||
@ -40,10 +38,24 @@ github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7 h1:+j+Kd/
|
||||
github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7/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.20240612082117-0f92424eed80 h1:cP4+6RFn9vVgYnoDwxBU4EtIAZA+eM4rzOaSZNqZ1xg=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20240612082117-0f92424eed80/go.mod h1:3A0cpdo0BIenIPjTG6u8EbzJ8uuJy7rVvM/NaynjCKA=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20240808145610-88df257767f6 h1:nERxOYS4ndSgWEr834YYkb1j0bZK/dJAmhoyYB1MtNY=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20240808145610-88df257767f6/go.mod h1:3A0cpdo0BIenIPjTG6u8EbzJ8uuJy7rVvM/NaynjCKA=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20240819131705-149e50199c5b h1:zifGh4LS5HwQIaVCccSe5/oJGTOjFeVObMRl3QJoJ3k=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20240819131705-149e50199c5b/go.mod h1:3A0cpdo0BIenIPjTG6u8EbzJ8uuJy7rVvM/NaynjCKA=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20240821081056-dd607af0f917 h1:Ma6PfXFDuw7rYYq28FXNW6ubhYquRUmBuLyZrjJWHUE=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20240821081056-dd607af0f917/go.mod h1:3A0cpdo0BIenIPjTG6u8EbzJ8uuJy7rVvM/NaynjCKA=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20240822150235-7a6190889179 h1:6Xo0iRYa4GBgZ2HA+IR3KdqiML8Z10h2F9TYe+9n1+M=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20240822150235-7a6190889179/go.mod h1:3A0cpdo0BIenIPjTG6u8EbzJ8uuJy7rVvM/NaynjCKA=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20240827084449-71096377c391 h1:PW6bE+mhsfAx4+wDCCNjhFrCNiiuMjY6j7RwqRUdPKI=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20240827084449-71096377c391/go.mod h1:3A0cpdo0BIenIPjTG6u8EbzJ8uuJy7rVvM/NaynjCKA=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20240827122236-ca6bb6449bba h1:QtDxgIbgPqRQg7VT+nIUJlaOyNFAoGyg59oW3Hji/0A=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20240827122236-ca6bb6449bba/go.mod h1:3A0cpdo0BIenIPjTG6u8EbzJ8uuJy7rVvM/NaynjCKA=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20240827132526-849231fc34a1 h1:gATlMoj4raG32WyGGh8SpipoQeR2AlU7g+8NAMicTcw=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20240827132526-849231fc34a1/go.mod h1:3A0cpdo0BIenIPjTG6u8EbzJ8uuJy7rVvM/NaynjCKA=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20240829112804-d663a2ef90c2 h1:yx0iejqB5c21HIN5jn9IsbyzUns0dPUUaGfyUHF3TmQ=
|
||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20240829112804-d663a2ef90c2/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=
|
||||
|
||||
@ -79,11 +79,13 @@ const (
|
||||
|
||||
// Hidden flags.
|
||||
const (
|
||||
flagLauncher = "launcher"
|
||||
flagNoWindow = "no-window"
|
||||
flagParentPID = "parent-pid"
|
||||
flagSoftwareRenderer = "software-renderer"
|
||||
FlagSessionID = "session-id"
|
||||
flagLauncher = "launcher"
|
||||
flagNoWindow = "no-window"
|
||||
flagParentPID = "parent-pid"
|
||||
flagSoftwareRenderer = "software-renderer"
|
||||
flagEnableKeychainTest = "enable-keychain-test"
|
||||
flagDisableKeychainTest = "disable-keychain-test"
|
||||
FlagSessionID = "session-id"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -91,6 +93,20 @@ const (
|
||||
appShortName = "bridge"
|
||||
)
|
||||
|
||||
var cliFlagEnableKeychainTest = &cli.BoolFlag{ //nolint:gochecknoglobals
|
||||
Name: flagEnableKeychainTest,
|
||||
Usage: "Enable the keychain test",
|
||||
Hidden: true,
|
||||
Value: false,
|
||||
} //nolint:gochecknoglobals
|
||||
|
||||
var cliFlagDisableKeychainTest = &cli.BoolFlag{ //nolint:gochecknoglobals
|
||||
Name: flagDisableKeychainTest,
|
||||
Usage: "Disable the keychain test",
|
||||
Hidden: true,
|
||||
Value: false,
|
||||
}
|
||||
|
||||
func New() *cli.App {
|
||||
app := cli.NewApp()
|
||||
|
||||
@ -168,6 +184,9 @@ func New() *cli.App {
|
||||
Name: FlagSessionID,
|
||||
Hidden: true,
|
||||
},
|
||||
// the two flags below were introduced by BRIDGE-116
|
||||
cliFlagEnableKeychainTest,
|
||||
cliFlagDisableKeychainTest,
|
||||
}
|
||||
|
||||
app.Action = run
|
||||
@ -238,7 +257,8 @@ func run(c *cli.Context) error {
|
||||
|
||||
return withSingleInstance(settings, locations.GetLockFile(), version, func() error {
|
||||
// Look for available keychains
|
||||
return WithKeychainList(crashHandler, func(keychains *keychain.List) error {
|
||||
skipKeychainTest := checkSkipKeychainTest(c, settings)
|
||||
return WithKeychainList(crashHandler, skipKeychainTest, func(keychains *keychain.List) error {
|
||||
// Unlock the encrypted vault.
|
||||
return WithVault(locations, keychains, crashHandler, func(v *vault.Vault, insecure, corrupt bool) error {
|
||||
if !v.Migrated() {
|
||||
@ -502,11 +522,11 @@ func withCookieJar(vault *vault.Vault, fn func(http.CookieJar) error) error {
|
||||
}
|
||||
|
||||
// WithKeychainList init the list of usable keychains.
|
||||
func WithKeychainList(panicHandler async.PanicHandler, fn func(*keychain.List) error) error {
|
||||
func WithKeychainList(panicHandler async.PanicHandler, skipKeychainTest bool, fn func(*keychain.List) error) error {
|
||||
logrus.Debug("Creating keychain list")
|
||||
defer logrus.Debug("Keychain list stop")
|
||||
defer async.HandlePanic(panicHandler)
|
||||
return fn(keychain.NewList())
|
||||
return fn(keychain.NewList(skipKeychainTest))
|
||||
}
|
||||
|
||||
func setDeviceCookies(jar *cookies.Jar) error {
|
||||
@ -526,3 +546,35 @@ func setDeviceCookies(jar *cookies.Jar) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkSkipKeychainTest(c *cli.Context, settingsDir string) bool {
|
||||
if runtime.GOOS != "darwin" {
|
||||
return false
|
||||
}
|
||||
|
||||
enable := c.Bool(flagEnableKeychainTest)
|
||||
disable := c.Bool(flagDisableKeychainTest)
|
||||
|
||||
skip, err := vault.GetShouldSkipKeychainTest(settingsDir)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Could not load keychain settings.")
|
||||
}
|
||||
|
||||
if (!enable) && (!disable) {
|
||||
return skip
|
||||
}
|
||||
|
||||
// if both switches are passed, 'enable' has priority
|
||||
if disable {
|
||||
skip = true
|
||||
}
|
||||
if enable {
|
||||
skip = false
|
||||
}
|
||||
|
||||
if err := vault.SetShouldSkipKeychainTest(settingsDir, skip); err != nil {
|
||||
logrus.WithError(err).Error("Could not save keychain settings.")
|
||||
}
|
||||
|
||||
return skip
|
||||
}
|
||||
|
||||
65
internal/app/app_test.go
Normal file
65
internal/app/app_test.go
Normal file
@ -0,0 +1,65 @@
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func TestCheckSkipKeychainTest(t *testing.T) {
|
||||
var expectedResult bool
|
||||
dir := t.TempDir()
|
||||
app := cli.App{
|
||||
Flags: []cli.Flag{
|
||||
cliFlagEnableKeychainTest,
|
||||
cliFlagDisableKeychainTest,
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
require.Equal(t, expectedResult, checkSkipKeychainTest(c, dir))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
noArgs := []string{"appName"}
|
||||
enableArgs := []string{"appName", "-" + flagEnableKeychainTest}
|
||||
disableArgs := []string{"appName", "-" + flagDisableKeychainTest}
|
||||
bothArgs := []string{"appName", "-" + flagDisableKeychainTest, "-" + flagEnableKeychainTest}
|
||||
|
||||
const trueOnlyOnMac = runtime.GOOS == "darwin"
|
||||
|
||||
expectedResult = false
|
||||
require.NoError(t, app.Run(noArgs))
|
||||
|
||||
expectedResult = trueOnlyOnMac
|
||||
require.NoError(t, app.Run(disableArgs))
|
||||
require.NoError(t, app.Run(noArgs))
|
||||
|
||||
expectedResult = false
|
||||
require.NoError(t, app.Run(enableArgs))
|
||||
require.NoError(t, app.Run(noArgs))
|
||||
|
||||
expectedResult = trueOnlyOnMac
|
||||
require.NoError(t, app.Run(disableArgs))
|
||||
|
||||
expectedResult = false
|
||||
require.NoError(t, app.Run(bothArgs))
|
||||
}
|
||||
@ -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,8 @@ func newBridge(
|
||||
return nil, fmt.Errorf("failed to create focus service: %w", err)
|
||||
}
|
||||
|
||||
unleashService := unleash.NewBridgeService(ctx, api, locator, panicHandler)
|
||||
|
||||
bridge := &Bridge{
|
||||
vault: vault,
|
||||
|
||||
@ -287,6 +307,12 @@ func newBridge(
|
||||
|
||||
tasks: tasks,
|
||||
syncService: syncservice.NewService(reporter, panicHandler),
|
||||
|
||||
unleashService: unleashService,
|
||||
|
||||
observabilityService: observability.NewService(ctx, panicHandler),
|
||||
|
||||
notificationStore: notifications.NewStore(locator.ProvideNotificationsCachePath),
|
||||
}
|
||||
|
||||
bridge.serverManager = imapsmtpserver.NewService(context.Background(),
|
||||
@ -299,6 +325,9 @@ func newBridge(
|
||||
&bridgeIMAPSMTPTelemetry{b: bridge},
|
||||
)
|
||||
|
||||
// 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 +340,10 @@ func newBridge(
|
||||
|
||||
bridge.syncService.Run()
|
||||
|
||||
bridge.unleashService.Run()
|
||||
|
||||
bridge.observabilityService.Run()
|
||||
|
||||
return bridge, nil
|
||||
}
|
||||
|
||||
@ -438,6 +471,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 +497,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 +580,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)
|
||||
@ -605,3 +644,71 @@ func min(a, b time.Duration) time.Duration {
|
||||
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.AddMetric(metric)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
97
internal/bridge/observability_test.go
Normal file
97
internal/bridge/observability_test.go
Normal file
@ -0,0 +1,97 @@
|
||||
// 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)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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)
|
||||
})
|
||||
|
||||
@ -33,6 +33,8 @@ type Locator interface {
|
||||
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)
|
||||
})
|
||||
})
|
||||
}
|
||||
@ -566,8 +566,12 @@ func (bridge *Bridge) addUserWithVault(
|
||||
bridge.serverManager,
|
||||
&bridgeEventSubscription{b: bridge},
|
||||
bridge.syncService,
|
||||
bridge.observabilityService,
|
||||
syncSettingsPath,
|
||||
isNew,
|
||||
bridge.notificationStore,
|
||||
bridge.unleashService.GetFlagValue,
|
||||
bridge.observabilityService.AddMetric,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create user: %w", err)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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">
|
||||
@ -31,332 +37,419 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonNewUser">
|
||||
<property name="text">
|
||||
<string>New User</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonEditUser">
|
||||
<property name="text">
|
||||
<string>Edit User</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonRemoveUser">
|
||||
<property name="text">
|
||||
<string>Remove User</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupboxSync">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Sync</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_5">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_4" stretch="1,0">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkSync">
|
||||
<property name="text">
|
||||
<string>Synchronizing</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="labelSync">
|
||||
<property name="text">
|
||||
<string>0%</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSlider" name="sliderSync">
|
||||
<property name="maximum">
|
||||
<number>100</number>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="tickInterval">
|
||||
<number>10</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBoxBadEvent">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Bad Event</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="editUserBadEvent">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="placeholderText">
|
||||
<string>error message</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonUserBadEvent">
|
||||
<property name="text">
|
||||
<string>Send</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBoxUsedSpace">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Used Bytes Changed</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="hBoxUsedBytes" stretch="1,0">
|
||||
<item>
|
||||
<widget class="QDoubleSpinBox" name="spinUsedBytes">
|
||||
<property name="buttonSymbols">
|
||||
<enum>QAbstractSpinBox::NoButtons</enum>
|
||||
</property>
|
||||
<property name="decimals">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>1000000000000000.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonUsedBytesChanged">
|
||||
<property name="text">
|
||||
<string>Send</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBoxIMAPLoginFailed">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>IMAP Login Failure</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_8">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_7">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="editIMAPLoginFailedUsername">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="placeholderText">
|
||||
<string>username or primary email</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonImapLoginFailed">
|
||||
<property name="text">
|
||||
<string>Send</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBoxNextLogin">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>100</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Next Login Attempt</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkUsernamePasswordError">
|
||||
<property name="text">
|
||||
<string>Username/password error:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>10</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="editUsernamePasswordError">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Username/password error.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkHV3Required">
|
||||
<property name="text">
|
||||
<string>HV3 required</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkHV3Error">
|
||||
<property name="text">
|
||||
<string>HV3 error</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkFreeUserError">
|
||||
<property name="text">
|
||||
<string>Free user error</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkTFARequired">
|
||||
<property name="text">
|
||||
<string>2FA required</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkTFAError">
|
||||
<property name="text">
|
||||
<string>2FA error</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkTFAAbort">
|
||||
<property name="text">
|
||||
<string>2FA abort</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkTwoPasswordsRequired">
|
||||
<property name="text">
|
||||
<string>2nd password required</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkTwoPasswordsError">
|
||||
<property name="text">
|
||||
<string>2nd password error</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkTwoPasswordsAbort">
|
||||
<property name="text">
|
||||
<string>2nd password abort</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
<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>
|
||||
<widget class="QPushButton" name="buttonNewUser">
|
||||
<property name="text">
|
||||
<string>New User</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonEditUser">
|
||||
<property name="text">
|
||||
<string>Edit User</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonRemoveUser">
|
||||
<property name="text">
|
||||
<string>Remove User</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</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">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Sync</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_5">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_4" stretch="1,0">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkSync">
|
||||
<property name="text">
|
||||
<string>Synchronizing</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="labelSync">
|
||||
<property name="text">
|
||||
<string>0%</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSlider" name="sliderSync">
|
||||
<property name="maximum">
|
||||
<number>100</number>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="tickInterval">
|
||||
<number>10</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBoxBadEvent">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Bad Event</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="editUserBadEvent">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="placeholderText">
|
||||
<string>error message</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonUserBadEvent">
|
||||
<property name="text">
|
||||
<string>Send</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBoxUsedSpace">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Used Bytes Changed</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="hBoxUsedBytes" stretch="1,0">
|
||||
<item>
|
||||
<widget class="QDoubleSpinBox" name="spinUsedBytes">
|
||||
<property name="buttonSymbols">
|
||||
<enum>QAbstractSpinBox::NoButtons</enum>
|
||||
</property>
|
||||
<property name="decimals">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>1000000000000000.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonUsedBytesChanged">
|
||||
<property name="text">
|
||||
<string>Send</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBoxIMAPLoginFailed">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>IMAP Login Failure</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_8">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_7">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="editIMAPLoginFailedUsername">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="placeholderText">
|
||||
<string>username or primary email</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonImapLoginFailed">
|
||||
<property name="text">
|
||||
<string>Send</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBoxNextLogin">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>250</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Next Login Attempt</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkUsernamePasswordError">
|
||||
<property name="text">
|
||||
<string>Username/password error:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>10</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="editUsernamePasswordError">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Username/password error.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkHV3Required">
|
||||
<property name="text">
|
||||
<string>HV3 required</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkHV3Error">
|
||||
<property name="text">
|
||||
<string>HV3 error</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkFreeUserError">
|
||||
<property name="text">
|
||||
<string>Free user error</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkTFARequired">
|
||||
<property name="text">
|
||||
<string>2FA required</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkTFAError">
|
||||
<property name="text">
|
||||
<string>2FA error</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkTFAAbort">
|
||||
<property name="text">
|
||||
<string>2FA abort</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkTwoPasswordsRequired">
|
||||
<property name="text">
|
||||
<string>2nd password required</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkTwoPasswordsError">
|
||||
<property name="text">
|
||||
<string>2nd password error</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkTwoPasswordsAbort">
|
||||
<property name="text">
|
||||
<string>2nd password abort</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
|
||||
@ -1330,6 +1330,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 +1419,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>
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
@ -174,6 +175,8 @@ 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 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.
|
||||
@ -209,6 +212,7 @@ public slots: // slot for signals received from QML -> To be forwarded to Bridge
|
||||
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 +228,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 +315,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>
|
||||
|
||||
@ -331,6 +331,15 @@ void TrayIcon::showErrorPopupNotification(QString const &title, QString const &m
|
||||
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) {
|
||||
this->showMessage(title, subtitle, QSystemTrayIcon::NoIcon);
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] svgPath The path of the SVG file for the icon.
|
||||
|
||||
@ -42,6 +42,8 @@ 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"
|
||||
|
||||
@ -347,6 +348,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) {
|
||||
|
||||
@ -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
|
||||
@ -39,13 +40,13 @@ Dialog {
|
||||
return "";
|
||||
}
|
||||
switch (root.notification.type) {
|
||||
case Notification.NotificationType.Info:
|
||||
return "/qml/icons/ic-info.svg";
|
||||
case Notification.NotificationType.Success:
|
||||
return "/qml/icons/ic-success.svg";
|
||||
case Notification.NotificationType.Warning:
|
||||
case Notification.NotificationType.Danger:
|
||||
return "/qml/icons/ic-alert.svg";
|
||||
case Notification.NotificationType.Info:
|
||||
return "/qml/icons/ic-info.svg";
|
||||
case Notification.NotificationType.Success:
|
||||
return "/qml/icons/ic-success.svg";
|
||||
case Notification.NotificationType.Warning:
|
||||
case Notification.NotificationType.Danger:
|
||||
return "/qml/icons/ic-alert.svg";
|
||||
}
|
||||
}
|
||||
sourceSize.height: 64
|
||||
|
||||
@ -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.")
|
||||
@ -1187,7 +1187,7 @@ QtObject {
|
||||
}
|
||||
target: root
|
||||
}
|
||||
|
||||
|
||||
Connections {
|
||||
function onRepairStarted() {
|
||||
root.repairBridge.active = false;
|
||||
@ -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
|
||||
|
||||
@ -73,6 +73,11 @@ T.ApplicationWindow {
|
||||
if (obj.shouldShow === false) {
|
||||
continue;
|
||||
}
|
||||
// User notifications should have display priority
|
||||
if (obj.shouldShow && obj.isUserNotification) {
|
||||
topmost = obj;
|
||||
break;
|
||||
}
|
||||
if (topmost && (topmost.popupType > obj.popupType)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -362,4 +362,9 @@ FocusScope {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Proton.ContextMenu {
|
||||
parentObject: root
|
||||
colorScheme: root.colorScheme
|
||||
}
|
||||
}
|
||||
|
||||
@ -331,6 +331,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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -0,0 +1,120 @@
|
||||
// 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 QtQml
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import Proton
|
||||
import Notifications
|
||||
|
||||
Dialog {
|
||||
id: root
|
||||
|
||||
property var notification
|
||||
property bool isUserNotification: true
|
||||
padding: 40
|
||||
|
||||
modal: true
|
||||
shouldShow: notification && notification.active && !notification.dismissed
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 0
|
||||
|
||||
Image {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.bottomMargin: 16
|
||||
Layout.preferredHeight: 64
|
||||
Layout.preferredWidth: 64
|
||||
source: {
|
||||
if (!root.notification) {
|
||||
return "";
|
||||
}
|
||||
switch (root.notification.type) {
|
||||
case Notification.NotificationType.UserNotification:
|
||||
return "/qml/icons/ic-notification-bell.svg"
|
||||
}
|
||||
}
|
||||
sourceSize.height: 64
|
||||
sourceSize.width: 64
|
||||
visible: source != ""
|
||||
}
|
||||
// Title Label
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.bottomMargin: 4
|
||||
Layout.preferredWidth: 320
|
||||
colorScheme: root.colorScheme
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: root.notification.title
|
||||
wrapMode: Text.WordWrap
|
||||
type: Label.LabelType.Title
|
||||
}
|
||||
// Username or primary email
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.bottomMargin: 24
|
||||
Layout.preferredWidth: 320
|
||||
colorScheme: root.colorScheme
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: root.notification.username
|
||||
wrapMode: Text.WordWrap
|
||||
visible: root.notification.username.length > 0
|
||||
type: Label.LabelType.Caption
|
||||
}
|
||||
// Subtitle
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.bottomMargin: 24
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: 320
|
||||
colorScheme: root.colorScheme
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: root.notification.subtitle
|
||||
wrapMode: Text.WordWrap
|
||||
visible: root.notification.subtitle.length > 0
|
||||
type: Label.LabelType.Lead
|
||||
color: root.colorScheme.text_weak
|
||||
}
|
||||
Label {
|
||||
Layout.bottomMargin: 24
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: 320
|
||||
colorScheme: root.colorScheme
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: root.notification.description
|
||||
type: Label.LabelType.Body
|
||||
wrapMode: Text.WordWrap
|
||||
|
||||
onLinkActivated: function (link) {
|
||||
Backend.openExternalLink(link);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 40
|
||||
|
||||
Repeater {
|
||||
model: root.notification.action
|
||||
|
||||
delegate: Button {
|
||||
Layout.fillWidth: true
|
||||
action: modelData
|
||||
colorScheme: root.colorScheme
|
||||
loading: modelData.loading
|
||||
secondary: index > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" fill="none" viewBox="0 0 64 64">
|
||||
<g clip-path="url(#a)">
|
||||
<circle cx="13.031" cy="5.288" r="3.166" fill="#2C83DC" transform="rotate(-30 13.031 5.288)"/>
|
||||
<path fill="url(#b)" d="M3.599 27.28A19.757 19.757 0 0 1 36.793 8.115L53.92 25.808a12.42 12.42 0 0 0 4.581 2.998c3.454 1.288 5.556 4.214 3.201 7.05-2.76 3.325-8.795 8.475-21.796 15.981S19.428 61.994 15.169 62.723c-3.634.621-5.117-2.662-4.506-6.298a12.422 12.422 0 0 0-.306-5.466L3.6 27.28Z"/>
|
||||
<path fill="url(#c)" d="M3.599 27.28A19.757 19.757 0 0 1 36.793 8.115L53.92 25.808a12.42 12.42 0 0 0 4.581 2.998c3.454 1.288 5.556 4.214 3.201 7.05-2.76 3.325-8.795 8.475-21.796 15.981S19.428 61.994 15.169 62.723c-3.634.621-5.117-2.662-4.506-6.298a12.422 12.422 0 0 0-.306-5.466L3.6 27.28Z"/>
|
||||
<ellipse cx="37.094" cy="46.965" fill="url(#d)" rx="26.875" ry="3.75" transform="rotate(-30 37.094 46.965)"/>
|
||||
<ellipse cx="37.094" cy="46.965" fill="url(#e)" rx="26.875" ry="3.75" transform="rotate(-30 37.094 46.965)"/>
|
||||
<path fill="#fff" fill-opacity=".2" d="m48.156 19.855-2.032-2.1c-6.302 1.79-12.908 4.57-19.41 8.324-7.591 4.383-14.103 9.553-19.19 14.959l.914 3.201c4.88-5.52 11.835-11.152 20.16-15.958 6.703-3.87 13.425-6.704 19.558-8.426Z"/>
|
||||
<circle cx="36.469" cy="45.883" r="2.5" fill="url(#f)" transform="rotate(-30 36.469 45.883)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="d" x1="49.16" x2="37.094" y1="30.878" y2="49.439" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#256097"/>
|
||||
<stop offset="1" stop-color="#2C83DC"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="e" x1="49.68" x2="49.037" y1="46.478" y2="51.592" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#27ABF4" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="#27ABF4" stop-opacity=".5"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="f" x1="35.719" x2="36.469" y1="43.133" y2="48.383" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".345" stop-color="#B2EAFE" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="#B2EAFE"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="b" cx="0" cy="0" r="1" gradientTransform="rotate(131.347 12.11 6.294) scale(52.6374 54.7116)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#B2EAFE"/>
|
||||
<stop offset="1" stop-color="#27ABF4"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="c" cx="0" cy="0" r="1" gradientTransform="rotate(60 -23.22 57.501) scale(21.25 76.0937)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#fff" stop-opacity="0"/>
|
||||
<stop offset=".46" stop-color="#fff" stop-opacity=".4"/>
|
||||
<stop offset=".58" stop-color="#B2EAFE" stop-opacity=".5"/>
|
||||
</radialGradient>
|
||||
<clipPath id="a">
|
||||
<path fill="#fff" d="M0 0h64v64H0z"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
@ -729,4 +729,23 @@ SPStreamEvent newGenericErrorEvent(grpc::ErrorCode errorCode) {
|
||||
}
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \param[in] userID The user ID that received the notification.
|
||||
/// \param[in] title The title of the notification.
|
||||
/// \param[in] subtitle The subtitle of the notification.
|
||||
/// \param[in] body The body of the notification.
|
||||
/// \return The event.
|
||||
//****************************************************************************************************************************************************
|
||||
SPStreamEvent newUserNotificationEvent(QString const &userID, QString const title, QString const subtitle, QString const body) {
|
||||
auto event = new grpc::UserNotificationEvent;
|
||||
event->set_userid(userID.toStdString());
|
||||
event->set_body(body.toStdString());
|
||||
event->set_subtitle(subtitle.toStdString());
|
||||
event->set_title(title.toStdString());
|
||||
auto appEvent = new grpc::AppEvent;
|
||||
appEvent->set_allocated_usernotification(event);
|
||||
return wrapAppEvent(appEvent);
|
||||
}
|
||||
|
||||
|
||||
} // namespace bridgepp
|
||||
|
||||
@ -94,6 +94,9 @@ SPStreamEvent newSyncProgressEvent(QString const &userID, double progress, qint6
|
||||
// Generic error event
|
||||
SPStreamEvent newGenericErrorEvent(grpc::ErrorCode errorCode); ///< Create a new GenericErrrorEvent event.
|
||||
|
||||
// User notification event
|
||||
SPStreamEvent newUserNotificationEvent(QString const &userID, QString const title, QString const subtitle, QString const body);
|
||||
|
||||
} // namespace bridgepp
|
||||
|
||||
|
||||
|
||||
@ -1206,6 +1206,17 @@ void GRPCClient::processAppEvent(AppEvent const &event) {
|
||||
this->logTrace("App event received: AllUsersLoaded");
|
||||
emit allUsersLoaded();
|
||||
break;
|
||||
case AppEvent::kUserNotification: {
|
||||
this->logTrace("App event received: UserNotification");
|
||||
UserNotification notification{
|
||||
.title = QString::fromStdString(event.usernotification().title()),
|
||||
.subtitle = QString::fromStdString(event.usernotification().subtitle()),
|
||||
.body = QString::fromStdString(event.usernotification().body()),
|
||||
.userID = QString::fromStdString(event.usernotification().userid()),
|
||||
};
|
||||
emit userNotificationReceived(notification);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
this->logError("Unknown App event received.");
|
||||
}
|
||||
|
||||
@ -56,6 +56,24 @@ public:
|
||||
};
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \brief A struct for user notitifications.
|
||||
//****************************************************************************************************************************************************
|
||||
struct UserNotification {
|
||||
// The following lines make the type transmissible to QML (but not instanciable there)
|
||||
Q_GADGET
|
||||
Q_PROPERTY(QString title MEMBER title)
|
||||
Q_PROPERTY(QString subtitle MEMBER subtitle)
|
||||
Q_PROPERTY(QString body MEMBER body)
|
||||
Q_PROPERTY(QString userID MEMBER userID)
|
||||
public:
|
||||
QString title; ///< The title of the notification.
|
||||
QString subtitle; ///< The subtitle of the notification.
|
||||
QString body; ///< The body of the notification.
|
||||
QString userID; ///< The userID that received the notification.
|
||||
};
|
||||
|
||||
|
||||
//****************************************************************************************************************************************************
|
||||
/// \brief gRPC client class. This class encapsulate the gRPC service, abstracting all data type conversions.
|
||||
//****************************************************************************************************************************************************
|
||||
@ -125,6 +143,7 @@ signals: // app related signals
|
||||
void knowledgeBasSuggestionsReceived(QList<KnowledgeBaseSuggestion> const& suggestions);
|
||||
void repairStarted();
|
||||
void allUsersLoaded();
|
||||
void userNotificationReceived(UserNotification const& notification);
|
||||
|
||||
|
||||
public: // cache related calls
|
||||
|
||||
@ -20,6 +20,7 @@ package cli
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
@ -500,6 +501,18 @@ func (f *frontendCLI) watchEvents(eventCh <-chan events.Event) { // nolint:gocyc
|
||||
|
||||
case events.Raise:
|
||||
f.Printf("Hello!")
|
||||
|
||||
case events.UserNotification:
|
||||
user, err := f.bridge.GetUserInfo(event.UserID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("\n--- NOTIFICATION ---\n\n")
|
||||
fmt.Printf("Sent to: %s\n", user.Username)
|
||||
fmt.Printf("Title: %s\n", event.Title)
|
||||
fmt.Printf("Subtitle: %s\n", event.Subtitle)
|
||||
fmt.Printf("Message: %s\n\n", event.Body)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -277,6 +277,7 @@ message AppEvent {
|
||||
KnowledgeBaseSuggestionsEvent knowledgeBaseSuggestions = 12;
|
||||
RepairStartedEvent repairStarted = 13;
|
||||
AllUsersLoadedEvent allUsersLoaded = 14;
|
||||
UserNotificationEvent userNotification = 15;
|
||||
}
|
||||
}
|
||||
|
||||
@ -545,6 +546,14 @@ message SyncProgressEvent {
|
||||
int64 remainingMs = 4;
|
||||
}
|
||||
|
||||
message UserNotificationEvent {
|
||||
string title = 1;
|
||||
string subtitle = 2;
|
||||
string body = 3;
|
||||
string userID = 4;
|
||||
}
|
||||
|
||||
|
||||
//**********************************************************
|
||||
// Generic errors
|
||||
//**********************************************************
|
||||
|
||||
@ -18,6 +18,7 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/kb"
|
||||
"github.com/bradenaw/juniper/xslices"
|
||||
)
|
||||
@ -249,6 +250,16 @@ func NewAllUsersLoadedEvent() *StreamEvent {
|
||||
return appEvent(&AppEvent{Event: &AppEvent_AllUsersLoaded{AllUsersLoaded: &AllUsersLoadedEvent{}}})
|
||||
}
|
||||
|
||||
func NewUserNotificationEvent(event events.UserNotification) *StreamEvent {
|
||||
return appEvent(&AppEvent{Event: &AppEvent_UserNotification{
|
||||
UserNotification: &UserNotificationEvent{
|
||||
UserID: event.UserID,
|
||||
Title: event.Title,
|
||||
Subtitle: event.Subtitle,
|
||||
Body: event.Body,
|
||||
}}})
|
||||
}
|
||||
|
||||
// Event category factory functions.
|
||||
|
||||
func appEvent(appEvent *AppEvent) *StreamEvent {
|
||||
|
||||
@ -404,6 +404,9 @@ func (s *Service) watchEvents() {
|
||||
|
||||
case events.AllUsersLoaded:
|
||||
_ = s.SendEvent(NewAllUsersLoadedEvent())
|
||||
|
||||
case events.UserNotification:
|
||||
_ = s.SendEvent(NewUserNotificationEvent(event))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -581,7 +584,7 @@ func validateServerToken(ctx context.Context, wantToken string) error {
|
||||
|
||||
// newUnaryTokenValidator checks the server token for every unary gRPC call.
|
||||
func newUnaryTokenValidator(wantToken string) grpc.UnaryServerInterceptor {
|
||||
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
|
||||
return func(ctx context.Context, req interface{}, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
|
||||
if err := validateServerToken(ctx, wantToken); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -592,7 +595,7 @@ func newUnaryTokenValidator(wantToken string) grpc.UnaryServerInterceptor {
|
||||
|
||||
// newStreamTokenValidator checks the server token for every gRPC stream request.
|
||||
func newStreamTokenValidator(wantToken string) grpc.StreamServerInterceptor {
|
||||
return func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
|
||||
return func(srv interface{}, stream grpc.ServerStream, _ *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
|
||||
if err := validateServerToken(stream.Context(), wantToken); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -626,9 +629,7 @@ func (s *Service) monitorParentPID() {
|
||||
go func() {
|
||||
defer async.HandlePanic(s.panicHandler)
|
||||
|
||||
if err := s.quit(); err != nil {
|
||||
logrus.WithError(err).Error("Error on quit")
|
||||
}
|
||||
s.quit()
|
||||
}()
|
||||
}
|
||||
|
||||
|
||||
@ -110,10 +110,11 @@ func (s *Service) GuiReady(_ context.Context, _ *emptypb.Empty) (*GuiReadyRespon
|
||||
func (s *Service) Quit(_ context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) {
|
||||
defer async.HandlePanic(s.panicHandler)
|
||||
s.log.Debug("Quit")
|
||||
return &emptypb.Empty{}, s.quit()
|
||||
s.quit()
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (s *Service) quit() error {
|
||||
func (s *Service) quit() {
|
||||
// Windows is notably slow at Quitting. We do it in a goroutine to speed things up a bit.
|
||||
go func() {
|
||||
defer async.HandlePanic(s.panicHandler)
|
||||
@ -132,8 +133,6 @@ func (s *Service) quit() error {
|
||||
// The following call is launched as a goroutine, as it will wait for current calls to end, including this one.
|
||||
s.grpcServer.GracefulStop() // gRPC does clean up and remove the file socket if used.
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Restart implement the Restart gRPC service call.
|
||||
|
||||
@ -76,7 +76,8 @@ func (s *Service) RunEventStream(request *EventStreamRequest, server Bridge_RunE
|
||||
}
|
||||
case <-server.Context().Done():
|
||||
s.log.Debug("Client closed the stream, exiting")
|
||||
return s.quit()
|
||||
s.quit()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,46 +4,45 @@
|
||||
"url": "https://proton.me/support/automatically-start-bridge",
|
||||
"title": "Automatically start Bridge",
|
||||
"keywords": [
|
||||
"automatic",
|
||||
"autostart",
|
||||
"login",
|
||||
"start",
|
||||
"startup",
|
||||
"boot"
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"url": "https://proton.me/support/bridge-automatic-update",
|
||||
"title": "Automatic Update and Bridge",
|
||||
"url": "https://proton.me/support/proton-mail-bridge-new-outlook-for-windows-set-up-guide",
|
||||
"title": "Proton Mail Bridge New Outlook for Windows set up guide",
|
||||
"keywords": [
|
||||
"update",
|
||||
"upgrade",
|
||||
"restart",
|
||||
"automatic",
|
||||
"manual"
|
||||
"app password",
|
||||
"INVALIDCREDENTIALS",
|
||||
"TEMPORARILYUNAVAILABLE",
|
||||
"New Outlook",
|
||||
"Outlook",
|
||||
"Sign in failed"
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"url": "https://proton.me/support/messages-encrypted-via-bridge",
|
||||
"title": "Are my messages encrypted via Proton Mail Bridge?",
|
||||
"url": "https://proton.me/support/what-is-the-recovered-messages-folder-in-bridge",
|
||||
"title": "What is the Recovered Messages folder in Bridge (and your email client)?",
|
||||
"keywords": [
|
||||
"encrypted",
|
||||
"privacy",
|
||||
"message",
|
||||
"security",
|
||||
"gpg",
|
||||
"pgp",
|
||||
"crypto"
|
||||
"recovered messages",
|
||||
"recovered messages folder"
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"url": "https://proton.me/support/labels-in-bridge",
|
||||
"title": "Labels in Bridge",
|
||||
"url": "https://proton.me/support/protonmail-bridge-clients-macos-new-outlook#may-17",
|
||||
"title": "Important notice regarding the New Outlook for Mac and issues you might face",
|
||||
"keywords": [
|
||||
"labels",
|
||||
"folders",
|
||||
"directories"
|
||||
"Receiving",
|
||||
"Sending",
|
||||
"Outlook",
|
||||
"Configuration",
|
||||
"Sync",
|
||||
"New Outlook"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -51,14 +50,15 @@
|
||||
"url": "https://proton.me/support/bridge-ssl-connection-issue",
|
||||
"title": "Proton Mail Bridge connection issues with Thunderbird, Outlook, and Apple Mail",
|
||||
"keywords": [
|
||||
"setup",
|
||||
"disconnected",
|
||||
"connect",
|
||||
"SSL",
|
||||
"STARTTLS",
|
||||
"client",
|
||||
"program",
|
||||
"Outlook",
|
||||
"Apple Mail",
|
||||
"Thunderbird"
|
||||
"Thunderbird",
|
||||
"Mac Mail",
|
||||
"Could not connect to server",
|
||||
"The attempt to read data from the server “127.0.0.1"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -81,7 +81,6 @@
|
||||
"split",
|
||||
"address",
|
||||
"mode"
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -91,7 +90,7 @@
|
||||
"keywords": [
|
||||
"Thunderbird",
|
||||
"Connection to server timed out",
|
||||
"Connection",
|
||||
"Connection timed out",
|
||||
"Timeout"
|
||||
]
|
||||
},
|
||||
@ -110,7 +109,7 @@
|
||||
{
|
||||
"index": 9,
|
||||
"url": "https://proton.me/support/port-already-occupied-error",
|
||||
"title": "Port already occupied error",
|
||||
"title": "How to fix “IMAP or SMTP port error” on Proton Mail Bridge",
|
||||
"keywords": [
|
||||
"Port",
|
||||
"occupied",
|
||||
@ -123,256 +122,28 @@
|
||||
},
|
||||
{
|
||||
"index": 10,
|
||||
"url": "https://proton.me/support/clients-supported-bridge",
|
||||
"title": "Email clients supported by Proton Mail Bridge",
|
||||
"url": "https://proton.me/support/apple-mail-certificate",
|
||||
"title": "Why you need to install a certificate for Apple Mail with Proton Mail Bridge",
|
||||
"keywords": [
|
||||
"client",
|
||||
"Outlook",
|
||||
"Thunderbird",
|
||||
"certificate",
|
||||
"Apple Mail",
|
||||
"EM Client",
|
||||
"The Bat",
|
||||
"Eudora",
|
||||
"Postbox",
|
||||
"Canary",
|
||||
"Spark"
|
||||
"mac",
|
||||
"unsigned"
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 11,
|
||||
"url": "https://proton.me/support/imap-smtp-and-pop3-setup",
|
||||
"title": "IMAP, SMTP, and POP3 setup",
|
||||
"url": "https://proton.me/support/macos-certificate-warning",
|
||||
"title": "Warning when installing Proton Mail Bridge on macOS",
|
||||
"keywords": [
|
||||
"IMAP",
|
||||
"SMTP",
|
||||
"setup",
|
||||
"set up",
|
||||
"configure",
|
||||
"configuration",
|
||||
"parameters"
|
||||
"install",
|
||||
"mac",
|
||||
"warning",
|
||||
"certificate"
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 12,
|
||||
"url": "https://proton.me/support/protonmail-bridge-install",
|
||||
"title": "How to install Proton Mail Bridge",
|
||||
"keywords": [
|
||||
"install",
|
||||
"installer",
|
||||
"setup",
|
||||
"download",
|
||||
"windows",
|
||||
"mac",
|
||||
"macos",
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 13,
|
||||
"url": "https://proton.me/support/bridge-for-linux",
|
||||
"title": "Proton Mail Bridge for Linux",
|
||||
"keywords": [
|
||||
"Linux",
|
||||
"Ubuntu",
|
||||
"Fedora",
|
||||
"Debian",
|
||||
"Unix",
|
||||
"deb",
|
||||
"rpm",
|
||||
"CentOS",
|
||||
"Arch",
|
||||
"Mint"
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 14,
|
||||
"url": "https://proton.me/support/operating-systems-supported-bridge",
|
||||
"title": "System requirements for Proton Mail Bridge",
|
||||
"keywords": [
|
||||
"requirement",
|
||||
"cpu",
|
||||
"memory",
|
||||
"Windows",
|
||||
"macOS",
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 15,
|
||||
"url": "https://proton.me/support/protonmail-bridge-configure-client",
|
||||
"title": "How to configure your email client for Proton Mail Bridge",
|
||||
"keywords": [
|
||||
"Client",
|
||||
"Outlook",
|
||||
"configure",
|
||||
"setup",
|
||||
"application",
|
||||
"setup",
|
||||
"IMAP",
|
||||
"SMTP"
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 16,
|
||||
"url": "https://proton.me/support/invalid-password-error-setting-email-client",
|
||||
"title": "Invalid password error while setting up email client",
|
||||
"keywords": [
|
||||
"password",
|
||||
"invalid",
|
||||
"error"
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 17,
|
||||
"url": "https://proton.me/support/protonmail-bridge-clients-windows-outlook-2019",
|
||||
"title": "Proton Mail Bridge Microsoft Outlook for Windows 2019 setup guide",
|
||||
"keywords": [
|
||||
"Outlook",
|
||||
"2019",
|
||||
"setup",
|
||||
"configuration"
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 18,
|
||||
"url": "https://proton.me/support/protonmail-bridge-clients-windows-outlook-2016",
|
||||
"title": "Proton Mail Bridge Microsoft Outlook 2016 for Windows setup guide",
|
||||
"keywords": [
|
||||
"Outlook",
|
||||
"2019",
|
||||
"setup",
|
||||
"configuration"
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 19,
|
||||
"url": "https://proton.me/support/protonmail-bridge-clients-apple-mail",
|
||||
"title": "Proton Mail Bridge Apple Mail setup guide",
|
||||
"keywords": [
|
||||
"Apple Mail",
|
||||
"setup",
|
||||
"configuration"
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 20,
|
||||
"url": "https://proton.me/support/protonmail-bridge-clients-macos-new-outlook",
|
||||
"title": "Proton Mail Bridge new Outlook for macOS setup guide",
|
||||
"keywords": [
|
||||
"Outlook",
|
||||
"setup",
|
||||
"configuration",
|
||||
"We encountered an error while adding account"
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 21,
|
||||
"url": "https://proton.me/support/protonmail-bridge-clients-windows-thunderbird",
|
||||
"title": "Proton Mail Bridge Thunderbird setup guide for Windows, macOS, and Linux",
|
||||
"keywords": [
|
||||
"Thunderbird",
|
||||
"setup",
|
||||
"configuration"
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 22,
|
||||
"url": "https://proton.me/support/protonmail-bridge-clients-macos-outlook-2016",
|
||||
"title": "Proton Mail Bridge Microsoft Outlook 2016 for macOS setup guide",
|
||||
"keywords": [
|
||||
"Outlook 2016",
|
||||
"macOS"
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 23,
|
||||
"url": "https://proton.me/support/protonmail-bridge-clients-macos-outlook-2019",
|
||||
"title": "Proton Mail Bridge Microsoft Outlook 2019 for macOS setup guide",
|
||||
"keywords": [
|
||||
"Outlook 2019",
|
||||
"macOS"
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 24,
|
||||
"url": "https://proton.me/support/protonmail-bridge-clients-windows-outlook-2013",
|
||||
"title": "Proton Mail Bridge Microsoft Outlook 2013 for Windows setup guide",
|
||||
"keywords": [
|
||||
"Outlook 2013",
|
||||
"macOS"
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 25,
|
||||
"url": "https://proton.me/support/protonmail-bridge-clients-macos-outlook-2011",
|
||||
"title": "Proton Mail Bridge Microsoft Outlook 2011 for macOS setup guide",
|
||||
"keywords": [
|
||||
"Outlook 2011",
|
||||
"macOS"
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 26,
|
||||
"url": "https://proton.me/support/install-bridge-linux-pkgbuild-file",
|
||||
"title": "Installing Proton Mail Bridge for Linux using a PKGBUILD file",
|
||||
"keywords": [
|
||||
"Linux",
|
||||
"pkgbuild"
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 27,
|
||||
"url": "https://proton.me/support/installing-bridge-linux-deb-file",
|
||||
"title": "Installing Proton Mail Bridge for Linux using a DEB file",
|
||||
"keywords": [
|
||||
"Linux",
|
||||
"deb"
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 28,
|
||||
"url": "https://proton.me/support/verifying-bridge-package",
|
||||
"title": "Verifying the Proton Mail Bridge package for Linux",
|
||||
"keywords": [
|
||||
"Linux",
|
||||
"Package",
|
||||
"Verify"
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 29,
|
||||
"url": "https://proton.me/support/bridge-cli-guide",
|
||||
"title": "Bridge CLI (command line interface) guide",
|
||||
"keywords": [
|
||||
"CLI",
|
||||
"Terminal",
|
||||
"Command-line",
|
||||
"Powershell"
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 30,
|
||||
"url": "https://proton.me/support/install-bridge-linux-rpm-file",
|
||||
"title": "Installing Proton Mail Bridge for Linux using an RPM file",
|
||||
"keywords": [
|
||||
"Linux",
|
||||
"Install",
|
||||
"RPM"
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 31,
|
||||
"url": "https://proton.me/support/bridge-linux-login-error",
|
||||
"title": "How to fix Proton Bridge login errors",
|
||||
"keywords": [
|
||||
"login",
|
||||
"error",
|
||||
"connection"
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 32,
|
||||
"url": "https://proton.me/support/bridge-linux-tray-icon",
|
||||
"title": "How to fix a missing system tray icon in Linux",
|
||||
"keywords": [
|
||||
@ -382,78 +153,228 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 33,
|
||||
"url": "https://proton.me/support/why-you-need-bridge",
|
||||
"title": "Why you need Proton Mail Bridge",
|
||||
"index": 13,
|
||||
"url": "https://proton.me/support/bridge-linux-login-error",
|
||||
"title": "How to fix Proton Bridge login errors",
|
||||
"keywords": [
|
||||
"Bridge",
|
||||
"email",
|
||||
"client"
|
||||
"login",
|
||||
"error",
|
||||
"sign in"
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 34,
|
||||
"url": "https://proton.me/support/protonmail-bridge-manual-update",
|
||||
"title": "How to manually update Proton Mail Bridge",
|
||||
"index": 14,
|
||||
"url": "https://proton.me/support/protonmail-bridge-clients-macos-outlook-2019",
|
||||
"title": "Proton Mail Bridge Microsoft Outlook 2019 for macOS setup guide",
|
||||
"keywords": [
|
||||
"update",
|
||||
"upgrade",
|
||||
"manual",
|
||||
"download"
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 35,
|
||||
"url": "https://proton.me/support/macos-certificate-warning",
|
||||
"title": "Warning when installing Proton Mail Bridge on macOS",
|
||||
"keywords": [
|
||||
"install",
|
||||
"macOS",
|
||||
"warning",
|
||||
"certificate"
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 36,
|
||||
"url": "https://proton.me/support/apple-mail-certificate",
|
||||
"title": "Why you need to install a certificate for Apple Mail with Proton Mail Bridge",
|
||||
"keywords": [
|
||||
"certificate",
|
||||
"Apple Mail",
|
||||
"macOS"
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 37,
|
||||
"url": "https://proton.me/support/protonmail-bridge-clients-macos-new-outlook#may-17",
|
||||
"title": "Important notice regarding the New Outlook for Mac and issues you might face",
|
||||
"keywords": [
|
||||
"Receiving",
|
||||
"Sending",
|
||||
"Outlook",
|
||||
"Configuration",
|
||||
"Sync",
|
||||
"New Outlook"
|
||||
"mac"
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 38,
|
||||
"url": "https://proton.me/support/proton-mail-bridge-new-outlook-for-windows-set-up-guide",
|
||||
"title": "What is the Recovered Messages folder in Bridge (and your email client)?",
|
||||
"index": 15,
|
||||
"url": "https://proton.me/support/protonmail-bridge-configure-client",
|
||||
"title": "How to configure your email client for Proton Mail Bridge",
|
||||
"keywords": [
|
||||
"recovered messages",
|
||||
"recovered messages folder"
|
||||
"Client",
|
||||
"configure",
|
||||
"configuration",
|
||||
"setup",
|
||||
"application",
|
||||
"setup"
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 39,
|
||||
"url": "https://proton.me/support/proton-mail-bridge-new-outlook-for-windows-set-up-guide",
|
||||
"title": "Proton Mail Bridge New Outlook for Windows set up guide",
|
||||
"index": 16,
|
||||
"url": "https://proton.me/support/invalid-password-error-setting-email-client",
|
||||
"title": "Invalid password error while setting up email client",
|
||||
"keywords": [
|
||||
"app password",
|
||||
"INVALIDCREDENTIALS",
|
||||
"TEMPORARILYUNAVAILABLE",
|
||||
"New Outlook"
|
||||
"password",
|
||||
"invalid",
|
||||
"error",
|
||||
"incorrect",
|
||||
"verify account name",
|
||||
"login"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"index": 17,
|
||||
"url": "https://proton.me/support/protonmail-bridge-clients-windows-outlook-2019",
|
||||
"title": "Proton Mail Bridge Microsoft Outlook for Windows 2019 setup guide",
|
||||
"keywords": [
|
||||
"Outlook",
|
||||
"setup",
|
||||
"configure",
|
||||
"connect",
|
||||
"Outlook 365"
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 18,
|
||||
"url": "https://proton.me/support/protonmail-bridge-clients-windows-thunderbird",
|
||||
"title": "Proton Mail Bridge Thunderbird setup guide for Windows, macOS, and Linux",
|
||||
"keywords": [
|
||||
"Thunderbird",
|
||||
"setup",
|
||||
"configuration",
|
||||
"connect"
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 19,
|
||||
"url": "https://proton.me/support/protonmail-bridge-clients-apple-mail",
|
||||
"title": "Proton Mail Bridge Apple Mail setup guide",
|
||||
"keywords": [
|
||||
"Apple Mail",
|
||||
"setup",
|
||||
"configure",
|
||||
"configuration",
|
||||
"connect"
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 20,
|
||||
"url": "https://proton.me/support/protonmail-bridge-clients-macos-new-outlook",
|
||||
"title": "Proton Mail Bridge new Outlook for macOS setup guide",
|
||||
"keywords": [
|
||||
"Outlook",
|
||||
"New Outlook",
|
||||
"setup",
|
||||
"configuration",
|
||||
"We encountered an error while adding the account",
|
||||
"error",
|
||||
"mac"
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 21,
|
||||
"url": "https://proton.me/support/how-to-resolve-connection-issues-in-bridge",
|
||||
"title": "How to resolve connection issues in Bridge",
|
||||
"keywords": [
|
||||
"Bridge is not able to contact the server",
|
||||
"connection",
|
||||
"error",
|
||||
"red banner",
|
||||
"No Connection",
|
||||
"issue",
|
||||
"lost connection"
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 22,
|
||||
"url": "https://proton.me/support/bridge-internal-error",
|
||||
"title": "How to fix an “internal error” warning on Proton Mail Bridge",
|
||||
"keywords": [
|
||||
"internal error"
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 23,
|
||||
"url": "https://proton.me/support/bridge-cant-move-cache",
|
||||
"title": "How to fix a “can’t move cache” error on Proton Mail Bridge",
|
||||
"keywords": [
|
||||
"Can’t move cache",
|
||||
"local cache"
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 24,
|
||||
"url": "https://proton.me/support/how-to-troubleshoot-messages-received-with-a-delay-in-your-email-client",
|
||||
"title": "How to troubleshoot messages received with a delay in your email client",
|
||||
"keywords": [
|
||||
"messages arriving slowly",
|
||||
"New emails are not arriving",
|
||||
"Emails arrive with a delay",
|
||||
"messages not appearing",
|
||||
"emails not appearing",
|
||||
"I cannot find emails in my email client"
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 25,
|
||||
"url": "https://proton.me/support/not-receiving-messages-email-client",
|
||||
"title": "How to troubleshoot not receiving new messages in your email client",
|
||||
"keywords": [
|
||||
"emails are not arriving",
|
||||
"emails missing",
|
||||
"missing emails",
|
||||
"emails not appearing",
|
||||
"I cannot find emails in my email client"
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 26,
|
||||
"url": "https://proton.me/support/resolve-invalid-return-path-error",
|
||||
"title": "How to resolve the “Invalid Return Path” error when sending messages from your email client",
|
||||
"keywords": [
|
||||
"Invalid return path",
|
||||
"I am not able to send emails",
|
||||
"sending error",
|
||||
"unable to send",
|
||||
"send"
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 27,
|
||||
"url": "https://proton.me/support/this-computer-only-folder-outlook",
|
||||
"title": "How to troubleshoot “This computer only” folders in Outlook for Windows",
|
||||
"keywords": [
|
||||
"This computer only"
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 28,
|
||||
"url": "https://proton.me/support/missing-messages-in-email-client-using-proton-bridge",
|
||||
"title": "How to troubleshoot missing messages in your email client",
|
||||
"keywords": [
|
||||
"emails are missing",
|
||||
"missing emails"
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 29,
|
||||
"url": "https://proton.me/support/bridge-cannot-access-keychain",
|
||||
"title": "How to fix “cannot access keychain” on Proton Mail Bridge",
|
||||
"keywords": [
|
||||
"cannot access keychain",
|
||||
"No keychain available"
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 30,
|
||||
"url": "https://proton.me/support/bridge-address-list-has-changed",
|
||||
"title": "How to fix “the address list has changed” warning on Proton Mail Bridge",
|
||||
"keywords": [
|
||||
"address list has changed",
|
||||
"address list changed"
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 31,
|
||||
"url": "https://proton.me/support/failed-to-parse-message-error",
|
||||
"title": "Troubleshooting “failed to parse message” errors",
|
||||
"keywords": [
|
||||
"failed to parse message"
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 32,
|
||||
"url": "https://proton.me/support/bridge-imap-login-failed",
|
||||
"title": "How to fix an “IMAP Login failed” warning on Proton Mail Bridge",
|
||||
"keywords": [
|
||||
"imap login failed",
|
||||
"login failed"
|
||||
]
|
||||
},
|
||||
{
|
||||
"index": 33,
|
||||
"url": "https://proton.me/support/resolve-checking-server-notice",
|
||||
"title": "How to resolve issues with “Checking mail server capabilities” notice in Thunderbird",
|
||||
"keywords": [
|
||||
"Checking mail server capabilities",
|
||||
"checking capabilities",
|
||||
"checking",
|
||||
"capabilities"
|
||||
]
|
||||
}
|
||||
]
|
||||
@ -69,7 +69,11 @@ func GetSuggestionsFromArticleList(userInput string, articles ArticleList) (Arti
|
||||
for _, article := range articles {
|
||||
for _, keyword := range article.Keywords {
|
||||
if strings.Contains(userInput, strings.ToUpper(keyword)) {
|
||||
article.Score++
|
||||
if len(keyword) > 12 {
|
||||
article.Score += 2
|
||||
} else {
|
||||
article.Score++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -42,7 +42,7 @@ func Test_GetSuggestions(t *testing.T) {
|
||||
suggestions, err := GetSuggestions("Thunderbird is not working, error during password")
|
||||
require.NoError(t, err)
|
||||
count := len(suggestions)
|
||||
require.True(t, (count > 0) && (count <= 3))
|
||||
require.True(t, (count > 0) && (count <= 5))
|
||||
suggestions, err = GetSuggestions("Supercalifragilisticexpialidocious Sesquipedalian Worcestershire")
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, suggestions)
|
||||
@ -75,10 +75,10 @@ func Test_GetSuggestionsFromArticleList(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_GetArticleIndex(t *testing.T) {
|
||||
index1, err := GetArticleIndex("https://proton.me/support/bridge-for-linux")
|
||||
index1, err := GetArticleIndex("https://proton.me/support/apple-mail-certificate")
|
||||
require.NoError(t, err)
|
||||
|
||||
index2, err := GetArticleIndex("HTTPS://PROTON.ME/support/bridge-for-linux")
|
||||
index2, err := GetArticleIndex("HTTPS://PROTON.ME/support/apple-mail-certificate")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, index1, index2)
|
||||
|
||||
|
||||
@ -78,7 +78,7 @@ func FuzzUnmarshal(f *testing.F) {
|
||||
str := strings.Join(items, sep)
|
||||
f.Add([]byte(str))
|
||||
|
||||
f.Fuzz(func(t *testing.T, secret []byte) {
|
||||
f.Fuzz(func(_ *testing.T, secret []byte) {
|
||||
encodedSecret := base64.StdEncoding.EncodeToString(secret)
|
||||
|
||||
creds := &Credentials{}
|
||||
|
||||
@ -206,6 +206,16 @@ func (l *Locations) ProvideIMAPSyncConfigPath() (string, error) {
|
||||
return l.getIMAPSyncConfigPath(), nil
|
||||
}
|
||||
|
||||
// ProvideUnleashCachePath returns a location for the unleash cache data (e.g. ~/.cache/protonmail/bridge-v3).
|
||||
// It creates it if it doesn't already exist.
|
||||
func (l *Locations) ProvideUnleashCachePath() (string, error) {
|
||||
if err := os.MkdirAll(l.getUnleashCachePath(), 0o700); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return l.getUnleashCachePath(), nil
|
||||
}
|
||||
|
||||
func (l *Locations) getGluonCachePath() string {
|
||||
return filepath.Join(l.userData, "gluon")
|
||||
}
|
||||
@ -238,10 +248,16 @@ func (l *Locations) getUpdatesPath() string {
|
||||
return filepath.Join(l.userData, "updates")
|
||||
}
|
||||
|
||||
func (l *Locations) getNotificationsCachePath() string {
|
||||
return filepath.Join(l.userCache, "notifications")
|
||||
}
|
||||
|
||||
func (l *Locations) getStatsPath() string {
|
||||
return filepath.Join(l.userData, "stats")
|
||||
}
|
||||
|
||||
func (l *Locations) getUnleashCachePath() string { return filepath.Join(l.userCache, "unleash_cache") }
|
||||
|
||||
// Clear removes everything except the lock and update files.
|
||||
func (l *Locations) Clear(except ...string) error {
|
||||
return files.Remove(
|
||||
@ -264,3 +280,13 @@ func (l *Locations) ClearUpdates() error {
|
||||
func (l *Locations) CleanGoIMAPCache() error {
|
||||
return files.Remove(l.getGoIMAPCachePath()).Do()
|
||||
}
|
||||
|
||||
// ProvideNotificationsCachePath returns a location for notification deduplication data.
|
||||
// It creates it if it doesn't already exist.
|
||||
func (l *Locations) ProvideNotificationsCachePath() (string, error) {
|
||||
if err := os.MkdirAll(l.getNotificationsCachePath(), 0o700); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return l.getNotificationsCachePath(), nil
|
||||
}
|
||||
|
||||
@ -172,7 +172,7 @@ func benchRotate(b *testing.B, logSize int64, getFile func(index int) (io.WriteC
|
||||
}
|
||||
|
||||
func getTestFile(b *testing.B, dir string, length int) func(int) (io.WriteCloser, error) {
|
||||
return func(index int) (io.WriteCloser, error) {
|
||||
return func(_ int) (io.WriteCloser, error) {
|
||||
b.StopTimer()
|
||||
defer b.StartTimer()
|
||||
|
||||
|
||||
@ -19,4 +19,5 @@ package service
|
||||
|
||||
type Locator interface {
|
||||
ProvideSettingsPath() (string, error)
|
||||
ProvideUnleashCachePath() (string, error)
|
||||
}
|
||||
|
||||
@ -23,7 +23,6 @@ import (
|
||||
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/bradenaw/juniper/stream"
|
||||
)
|
||||
|
||||
type APIClient interface {
|
||||
@ -41,7 +40,7 @@ type APIClient interface {
|
||||
GetAllMessageIDs(ctx context.Context, afterID string) ([]string, error)
|
||||
CreateDraft(ctx context.Context, addrKR *crypto.KeyRing, req proton.CreateDraftReq) (proton.Message, error)
|
||||
UploadAttachment(ctx context.Context, addrKR *crypto.KeyRing, req proton.CreateAttachmentReq) (proton.Attachment, error)
|
||||
ImportMessages(ctx context.Context, addrKR *crypto.KeyRing, workers, buffer int, req ...proton.ImportReq) (stream.Stream[proton.ImportRes], error)
|
||||
ImportMessages(ctx context.Context, addrKR *crypto.KeyRing, workers, buffer int, req ...proton.ImportReq) (proton.ImportResStream, error)
|
||||
GetFullMessage(ctx context.Context, messageID string, scheduler proton.Scheduler, storageProvider proton.AttachmentAllocator) (proton.FullMessage, error)
|
||||
GetAttachmentInto(ctx context.Context, attachmentID string, reader io.ReaderFrom) error
|
||||
GetAttachment(ctx context.Context, attachmentID string) ([]byte, error)
|
||||
|
||||
51
internal/services/notifications/metrics.go
Normal file
51
internal/services/notifications/metrics.go
Normal file
@ -0,0 +1,51 @@
|
||||
// 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 notifications
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
)
|
||||
|
||||
func generatedNotificationDisplayedMetric(status string, value int) proton.ObservabilityMetric {
|
||||
// Value cannot be zero or negative
|
||||
if value < 1 {
|
||||
value = 1
|
||||
}
|
||||
|
||||
return proton.ObservabilityMetric{
|
||||
Name: "bridge_remoteNotification_displayed_total",
|
||||
Version: 1,
|
||||
Timestamp: time.Now().Unix(),
|
||||
Data: map[string]interface{}{
|
||||
"Value": value,
|
||||
"Labels": map[string]string{
|
||||
"status": status,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func GenerateReceivedMetric(count int) proton.ObservabilityMetric {
|
||||
return generatedNotificationDisplayedMetric("received", count)
|
||||
}
|
||||
|
||||
func GenerateProcessedMetric(count int) proton.ObservabilityMetric {
|
||||
return generatedNotificationDisplayedMetric("processed", count)
|
||||
}
|
||||
112
internal/services/notifications/notification_test.go
Normal file
112
internal/services/notifications/notification_test.go
Normal file
@ -0,0 +1,112 @@
|
||||
// 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 notifications
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestIsBodyBitfieldValid(t *testing.T) {
|
||||
tests := []struct {
|
||||
notification proton.NotificationEvent
|
||||
isValid bool
|
||||
}{
|
||||
{notification: proton.NotificationEvent{Payload: proton.NotificationPayload{Body: ""}}, isValid: true},
|
||||
{notification: proton.NotificationEvent{Payload: proton.NotificationPayload{Body: "HELLO"}}, isValid: true},
|
||||
{notification: proton.NotificationEvent{Payload: proton.NotificationPayload{Body: "What is up?"}}, isValid: true},
|
||||
{notification: proton.NotificationEvent{Payload: proton.NotificationPayload{Body: "123 Hello"}}, isValid: true},
|
||||
{notification: proton.NotificationEvent{Payload: proton.NotificationPayload{Body: "\\123Hello"}}, isValid: false},
|
||||
{notification: proton.NotificationEvent{Payload: proton.NotificationPayload{Body: "\\123 Hello"}}, isValid: false},
|
||||
{notification: proton.NotificationEvent{Payload: proton.NotificationPayload{Body: "\\1test"}}, isValid: false},
|
||||
{notification: proton.NotificationEvent{Payload: proton.NotificationPayload{Body: "\\1 test"}}, isValid: false},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
isValid := isBodyBitfieldValid(test.notification)
|
||||
require.Equal(t, isValid, !test.isValid)
|
||||
}
|
||||
}
|
||||
|
||||
// The notification TTL is defined as timestamp by server + predefined duration.
|
||||
func TestShouldSendAndStore(t *testing.T) {
|
||||
getDirFn := func(dir string) func() (string, error) {
|
||||
return func() (string, error) {
|
||||
return dir, nil
|
||||
}
|
||||
}
|
||||
|
||||
dir1 := t.TempDir()
|
||||
dir2 := t.TempDir()
|
||||
|
||||
store := NewStore(getDirFn(dir1))
|
||||
|
||||
notification1 := proton.NotificationEvent{ID: "1", Payload: proton.NotificationPayload{Title: "test1", Subtitle: "test1", Body: "test1"}, Time: time.Now().Unix()}
|
||||
notification2 := proton.NotificationEvent{ID: "2", Payload: proton.NotificationPayload{Title: "test2", Subtitle: "test2", Body: "test2"}, Time: time.Now().Unix()}
|
||||
notification3 := proton.NotificationEvent{ID: "3", Payload: proton.NotificationPayload{Title: "test3", Subtitle: "test3", Body: "test3"}, Time: time.Now().Unix()}
|
||||
notificationAlt1 := proton.NotificationEvent{ID: "1", Payload: proton.NotificationPayload{Title: "testAlt1", Subtitle: "test1", Body: "test1"}, Time: time.Now().Unix()}
|
||||
notificationAlt2 := proton.NotificationEvent{ID: "1", Payload: proton.NotificationPayload{Title: "test2", Subtitle: "testAlt2", Body: "test2"}, Time: time.Now().Unix()}
|
||||
|
||||
require.Equal(t, true, store.shouldSendAndStore(notification1))
|
||||
require.Equal(t, true, store.shouldSendAndStore(notification2))
|
||||
require.Equal(t, true, store.shouldSendAndStore(notification3))
|
||||
require.Equal(t, true, store.shouldSendAndStore(notificationAlt1))
|
||||
require.Equal(t, true, store.shouldSendAndStore(notificationAlt2))
|
||||
|
||||
require.Equal(t, false, store.shouldSendAndStore(notification1))
|
||||
require.Equal(t, false, store.shouldSendAndStore(notification2))
|
||||
require.Equal(t, false, store.shouldSendAndStore(notification3))
|
||||
|
||||
store = NewStore(getDirFn(dir1))
|
||||
|
||||
// These should be cached in the file
|
||||
require.Equal(t, false, store.shouldSendAndStore(notification1))
|
||||
require.Equal(t, false, store.shouldSendAndStore(notification2))
|
||||
require.Equal(t, false, store.shouldSendAndStore(notification3))
|
||||
|
||||
store = NewStore(getDirFn(dir2))
|
||||
timeOffset = 1 * time.Second
|
||||
|
||||
// We're basing the time based on when the notification is sent
|
||||
// Let's reset it.
|
||||
notification1 = proton.NotificationEvent{ID: "1", Payload: proton.NotificationPayload{Title: "test1", Subtitle: "test1", Body: "test1"}, Time: time.Now().Unix()}
|
||||
notification2 = proton.NotificationEvent{ID: "2", Payload: proton.NotificationPayload{Title: "test2", Subtitle: "test2", Body: "test2"}, Time: time.Now().Unix()}
|
||||
notification3 = proton.NotificationEvent{ID: "3", Payload: proton.NotificationPayload{Title: "test3", Subtitle: "test3", Body: "test3"}, Time: time.Now().Unix()}
|
||||
|
||||
require.Equal(t, true, store.shouldSendAndStore(notification1))
|
||||
require.Equal(t, true, store.shouldSendAndStore(notification2))
|
||||
require.Equal(t, true, store.shouldSendAndStore(notification3))
|
||||
|
||||
require.Equal(t, false, store.shouldSendAndStore(notification1))
|
||||
require.Equal(t, false, store.shouldSendAndStore(notification2))
|
||||
require.Equal(t, false, store.shouldSendAndStore(notification3))
|
||||
|
||||
time.Sleep(1200 * time.Millisecond)
|
||||
|
||||
require.Equal(t, true, store.shouldSendAndStore(notification1))
|
||||
require.Equal(t, true, store.shouldSendAndStore(notification2))
|
||||
require.Equal(t, true, store.shouldSendAndStore(notification3))
|
||||
|
||||
store = NewStore(getDirFn(dir2))
|
||||
require.Equal(t, true, store.shouldSendAndStore(notification1))
|
||||
require.Equal(t, true, store.shouldSendAndStore(notification2))
|
||||
require.Equal(t, true, store.shouldSendAndStore(notification3))
|
||||
}
|
||||
156
internal/services/notifications/service.go
Normal file
156
internal/services/notifications/service.go
Normal file
@ -0,0 +1,156 @@
|
||||
// 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 notifications
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/orderedtasks"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/userevents"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/unleash"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
userID string
|
||||
|
||||
log *logrus.Entry
|
||||
|
||||
eventService userevents.Subscribable
|
||||
subscription *userevents.EventChanneledSubscriber
|
||||
eventPublisher events.EventPublisher
|
||||
|
||||
store *Store
|
||||
|
||||
getFlagValueFn unleash.GetFlagValueFn
|
||||
pushObservabilityMetricFn observability.PushObsMetricFn
|
||||
}
|
||||
|
||||
const bitfieldRegexPattern = `^\\\d+`
|
||||
const disableNotificationsKillSwitch = "InboxBridgeEventLoopNotificationDisabled"
|
||||
|
||||
func NewService(userID string, service userevents.Subscribable, eventPublisher events.EventPublisher, store *Store,
|
||||
getFlagFn unleash.GetFlagValueFn, pushMetricFn observability.PushObsMetricFn) *Service {
|
||||
return &Service{
|
||||
userID: userID,
|
||||
|
||||
log: logrus.WithFields(logrus.Fields{
|
||||
"user": userID,
|
||||
"service": "notification",
|
||||
}),
|
||||
|
||||
eventService: service,
|
||||
subscription: userevents.NewEventSubscriber(
|
||||
fmt.Sprintf("notifications-%v", userID)),
|
||||
eventPublisher: eventPublisher,
|
||||
|
||||
store: store,
|
||||
|
||||
getFlagValueFn: getFlagFn,
|
||||
pushObservabilityMetricFn: pushMetricFn,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) Start(ctx context.Context, group *orderedtasks.OrderedCancelGroup) {
|
||||
group.Go(ctx, s.userID, "notification-service", s.run)
|
||||
}
|
||||
|
||||
func (s *Service) run(ctx context.Context) {
|
||||
s.log.Info("Starting service main loop")
|
||||
defer s.log.Info("Exiting service main loop")
|
||||
|
||||
eventHandler := userevents.EventHandler{
|
||||
NotificationHandler: s,
|
||||
}
|
||||
|
||||
s.eventService.Subscribe(s.subscription)
|
||||
defer s.eventService.Unsubscribe(s.subscription)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case e, ok := <-s.subscription.OnEventCh():
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
e.Consume(func(event proton.Event) error { return eventHandler.OnEvent(ctx, event) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) HandleNotificationEvents(ctx context.Context, notificationEvents []proton.NotificationEvent) error {
|
||||
if s.getFlagValueFn(disableNotificationsKillSwitch) {
|
||||
s.log.Info("Received notification events. Skipping as kill switch is enabled.")
|
||||
return nil
|
||||
}
|
||||
|
||||
s.log.Debug("Handling notification events")
|
||||
|
||||
// Publish observability metrics that we've received notifications
|
||||
s.pushObservabilityMetricFn(GenerateReceivedMetric(len(notificationEvents)))
|
||||
|
||||
for _, event := range notificationEvents {
|
||||
ctx = logging.WithLogrusField(ctx, "notificationID", event.ID)
|
||||
switch strings.ToLower(event.Type) {
|
||||
case "bridge_modal":
|
||||
{
|
||||
// We currently don't support any notification types with bitfields in the body.
|
||||
if isBodyBitfieldValid(event) {
|
||||
continue
|
||||
}
|
||||
|
||||
shouldSend := s.store.shouldSendAndStore(event)
|
||||
if !shouldSend {
|
||||
s.log.Info("Skipping notification event. Notification was displayed previously")
|
||||
continue
|
||||
}
|
||||
|
||||
s.log.Info("Publishing notification event. notificationID:", event.ID) // \todo BRIDGE-141 - change this to UID once it is available
|
||||
s.eventPublisher.PublishEvent(ctx, events.UserNotification{UserID: s.userID, Title: event.Payload.Title,
|
||||
Subtitle: event.Payload.Subtitle, Body: event.Payload.Body})
|
||||
|
||||
// Publish observability metric that we've successfully processed notifications
|
||||
s.pushObservabilityMetricFn(GenerateProcessedMetric(1))
|
||||
}
|
||||
|
||||
default:
|
||||
s.log.Debug("Skipping notification event. Notification type is not related to bridge:", event.Type)
|
||||
continue
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// We will (potentially) encode different notification functionalities based on a starting bitfield "\NUMBER" in the
|
||||
// payload Body. Currently, we don't support this, but we might in the future. This is so versions of Bridge that don't
|
||||
// support this functionality won't be display such a message.
|
||||
func isBodyBitfieldValid(notification proton.NotificationEvent) bool {
|
||||
match, err := regexp.MatchString(bitfieldRegexPattern, notification.Payload.Body)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return match
|
||||
}
|
||||
161
internal/services/notifications/store.go
Normal file
161
internal/services/notifications/store.go
Normal file
@ -0,0 +1,161 @@
|
||||
// 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 notifications
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/exp/maps"
|
||||
)
|
||||
|
||||
// not-const so we can unit test the functionality.
|
||||
var timeOffset = 24 * time.Hour //nolint:gochecknoglobals
|
||||
const filename = "notification_cache"
|
||||
|
||||
type Store struct {
|
||||
displayedMessages map[string]time.Time
|
||||
displayedMessagesLock sync.Mutex
|
||||
|
||||
useCache bool
|
||||
cacheFilepath string
|
||||
cacheLock sync.Mutex
|
||||
|
||||
log *logrus.Entry
|
||||
}
|
||||
|
||||
func NewStore(getCachePath func() (string, error)) *Store {
|
||||
log := logrus.WithField("pkg", "notification-store")
|
||||
|
||||
useCacheFile := true
|
||||
cachePath, err := getCachePath()
|
||||
if err != nil {
|
||||
useCacheFile = false
|
||||
log.WithError(err).Error("Could not obtain cache directory")
|
||||
}
|
||||
|
||||
store := &Store{
|
||||
displayedMessages: make(map[string]time.Time),
|
||||
|
||||
useCache: useCacheFile,
|
||||
cacheFilepath: filepath.Clean(filepath.Join(cachePath, filename)),
|
||||
|
||||
log: log,
|
||||
}
|
||||
|
||||
store.readCache()
|
||||
|
||||
return store
|
||||
}
|
||||
|
||||
func generateHash(payload proton.NotificationPayload) string {
|
||||
hash := crypto.SHA256.New()
|
||||
hash.Write([]byte(payload.Body + payload.Subtitle + payload.Title))
|
||||
return hex.EncodeToString(hash.Sum(nil))
|
||||
}
|
||||
|
||||
func (s *Store) shouldSendAndStore(notification proton.NotificationEvent) bool {
|
||||
s.displayedMessagesLock.Lock()
|
||||
defer s.displayedMessagesLock.Unlock()
|
||||
|
||||
// \todo BRIDGE-141 - Add an additional check for the API returned UID
|
||||
uid := generateHash(notification.Payload)
|
||||
|
||||
value, ok := s.displayedMessages[uid]
|
||||
if !ok {
|
||||
s.displayedMessages[uid] = time.Unix(notification.Time, 0).Add(timeOffset)
|
||||
s.writeCache()
|
||||
return true
|
||||
}
|
||||
|
||||
if !time.Now().After(value) {
|
||||
return false
|
||||
}
|
||||
|
||||
s.displayedMessages[uid] = time.Unix(notification.Time, 0).Add(timeOffset)
|
||||
s.writeCache()
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *Store) readCache() {
|
||||
if !s.useCache {
|
||||
return
|
||||
}
|
||||
|
||||
s.cacheLock.Lock()
|
||||
defer s.cacheLock.Unlock()
|
||||
|
||||
file, err := os.Open(s.cacheFilepath)
|
||||
if err != nil {
|
||||
s.log.WithError(err).Error("Unable to open cache file")
|
||||
return
|
||||
}
|
||||
|
||||
defer func(file *os.File) {
|
||||
err := file.Close()
|
||||
if err != nil {
|
||||
s.log.WithError(err).Error("Unable to close cache file after read")
|
||||
}
|
||||
}(file)
|
||||
|
||||
s.displayedMessagesLock.Lock()
|
||||
defer s.displayedMessagesLock.Unlock()
|
||||
if err = json.NewDecoder(file).Decode(&s.displayedMessages); err != nil {
|
||||
s.log.WithError(err).Error("Unable to decode cache file")
|
||||
}
|
||||
|
||||
// Remove redundant data
|
||||
curTime := time.Now()
|
||||
maps.DeleteFunc(s.displayedMessages, func(_ string, value time.Time) bool {
|
||||
return curTime.After(value)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Store) writeCache() {
|
||||
if !s.useCache {
|
||||
return
|
||||
}
|
||||
|
||||
s.cacheLock.Lock()
|
||||
defer s.cacheLock.Unlock()
|
||||
|
||||
file, err := os.Create(s.cacheFilepath)
|
||||
if err != nil {
|
||||
s.log.WithError(err).Info("Unable to create cache file.")
|
||||
return
|
||||
}
|
||||
|
||||
defer func(file *os.File) {
|
||||
err := file.Close()
|
||||
if err != nil {
|
||||
s.log.WithError(err).Error("Unable to close cache file after write")
|
||||
}
|
||||
}(file)
|
||||
|
||||
// We don't lock the mutex here as the parent does that already
|
||||
if err = json.NewEncoder(file).Encode(s.displayedMessages); err != nil {
|
||||
s.log.WithError(err).Error("Unable to encode data to cache file")
|
||||
}
|
||||
}
|
||||
281
internal/services/observability/service.go
Normal file
281
internal/services/observability/service.go
Normal file
@ -0,0 +1,281 @@
|
||||
// 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 observability
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/telemetry"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Non-const for testing.
|
||||
var throttleDuration = 5 * time.Second //nolint:gochecknoglobals
|
||||
|
||||
const (
|
||||
maxStorageSize = 5000
|
||||
maxBatchSize = 1000
|
||||
)
|
||||
|
||||
type PushObsMetricFn func(metric proton.ObservabilityMetric)
|
||||
|
||||
type client struct {
|
||||
isTelemetryEnabled func(context.Context) bool
|
||||
sendMetrics func(context.Context, proton.ObservabilityBatch) error
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
|
||||
panicHandler async.PanicHandler
|
||||
|
||||
lastDispatch time.Time
|
||||
isDispatchScheduled bool
|
||||
|
||||
signalDataArrived chan struct{}
|
||||
signalDispatch chan struct{}
|
||||
|
||||
log *logrus.Entry
|
||||
|
||||
metricStore []proton.ObservabilityMetric
|
||||
metricStoreLock sync.Mutex
|
||||
|
||||
userClientStore map[string]*client
|
||||
userClientStoreLock sync.Mutex
|
||||
}
|
||||
|
||||
func NewService(ctx context.Context, panicHandler async.PanicHandler) *Service {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
|
||||
service := &Service{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
|
||||
panicHandler: panicHandler,
|
||||
|
||||
lastDispatch: time.Now().Add(-throttleDuration),
|
||||
|
||||
signalDataArrived: make(chan struct{}, 1),
|
||||
signalDispatch: make(chan struct{}, 1),
|
||||
|
||||
log: logrus.WithFields(logrus.Fields{"pkg": "observability"}),
|
||||
|
||||
metricStore: make([]proton.ObservabilityMetric, 0),
|
||||
|
||||
userClientStore: make(map[string]*client),
|
||||
}
|
||||
|
||||
return service
|
||||
}
|
||||
|
||||
func (s *Service) Run() {
|
||||
s.log.Info("Starting service")
|
||||
go func() {
|
||||
s.start()
|
||||
}()
|
||||
}
|
||||
|
||||
// When new data is received, we determine if we can immediately send the request.
|
||||
// First, we check if a dispatch operation is already scheduled. If it is, we do nothing.
|
||||
// If no dispatch is scheduled, we verify if the required time interval has passed since the last send.
|
||||
// If the interval hasn't passed, we schedule the dispatch to occur when the threshold is met.
|
||||
// If the interval has passed, we initiate an immediate dispatch.
|
||||
func (s *Service) start() {
|
||||
defer async.HandlePanic(s.panicHandler)
|
||||
for {
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return
|
||||
|
||||
case <-s.signalDispatch:
|
||||
s.dispatchData()
|
||||
|
||||
case <-s.signalDataArrived:
|
||||
if s.isDispatchScheduled {
|
||||
continue
|
||||
}
|
||||
|
||||
if time.Since(s.lastDispatch) <= throttleDuration {
|
||||
s.scheduleDispatch()
|
||||
continue
|
||||
}
|
||||
|
||||
s.sendSignal(s.signalDispatch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) dispatchData() {
|
||||
s.isDispatchScheduled = false // Only accessed via a single goroutine, so no mutexes.
|
||||
if !s.haveMetricsAndClients() {
|
||||
return
|
||||
}
|
||||
|
||||
// Get a copy of the metrics we want to send and batch them accordingly
|
||||
var numberOfRemainingMetrics int
|
||||
var metricsToSend []proton.ObservabilityMetric
|
||||
|
||||
s.withMetricStoreLock(func() {
|
||||
numberOfMetricsToSend := min(len(s.metricStore), maxBatchSize)
|
||||
metricsToSend = make([]proton.ObservabilityMetric, numberOfMetricsToSend)
|
||||
copy(metricsToSend, s.metricStore[:numberOfMetricsToSend])
|
||||
s.metricStore = s.metricStore[numberOfMetricsToSend:]
|
||||
numberOfRemainingMetrics = len(s.metricStore)
|
||||
})
|
||||
|
||||
// Send them out to the endpoint
|
||||
telemetryEnabled := s.dispatchViaClient(&metricsToSend)
|
||||
|
||||
// If there are more metric updates than the max batch limit and telemetry is enabled for one of the clients
|
||||
// then we immediately schedule another dispatch.
|
||||
if numberOfRemainingMetrics > 0 && telemetryEnabled {
|
||||
s.scheduleDispatch()
|
||||
}
|
||||
}
|
||||
|
||||
// dispatchViaClient - return value tells us whether telemetry is enabled
|
||||
// such that we know whether to schedule another dispatch if more data is present.
|
||||
func (s *Service) dispatchViaClient(metricsToSend *[]proton.ObservabilityMetric) bool {
|
||||
s.log.Info("Sending observability data.")
|
||||
s.userClientStoreLock.Lock()
|
||||
defer s.userClientStoreLock.Unlock()
|
||||
|
||||
for _, value := range s.userClientStore {
|
||||
if !value.isTelemetryEnabled(s.ctx) {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := value.sendMetrics(s.ctx, proton.ObservabilityBatch{Metrics: *metricsToSend}); err != nil {
|
||||
s.log.WithError(err).Error("Issue occurred when sending observability data.")
|
||||
} else {
|
||||
s.log.Info("Successfully sent observability data.")
|
||||
}
|
||||
|
||||
s.lastDispatch = time.Now()
|
||||
return true
|
||||
}
|
||||
|
||||
s.log.Info("Could not send observability data. Telemetry is not enabled.")
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Service) scheduleDispatch() {
|
||||
waitTime := throttleDuration - time.Since(s.lastDispatch)
|
||||
if waitTime <= 0 {
|
||||
s.sendSignal(s.signalDispatch)
|
||||
return
|
||||
}
|
||||
|
||||
s.log.Info("Scheduling observability data sending")
|
||||
|
||||
s.isDispatchScheduled = true
|
||||
go func() {
|
||||
defer async.HandlePanic(s.panicHandler)
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return
|
||||
case <-time.After(waitTime):
|
||||
s.sendSignal(s.signalDispatch)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *Service) AddMetric(metric proton.ObservabilityMetric) {
|
||||
s.withMetricStoreLock(func() {
|
||||
metricStoreLength := len(s.metricStore)
|
||||
if metricStoreLength >= maxStorageSize {
|
||||
s.log.Info("Max metric storage size has been exceeded. Dropping oldest metrics.")
|
||||
|
||||
dropCount := metricStoreLength - maxStorageSize + 1
|
||||
s.metricStore = s.metricStore[dropCount:]
|
||||
}
|
||||
s.metricStore = append(s.metricStore, metric)
|
||||
})
|
||||
|
||||
s.sendSignal(s.signalDataArrived)
|
||||
}
|
||||
|
||||
func (s *Service) RegisterUserClient(userID string, protonClient *proton.Client, telemetryService *telemetry.Service) {
|
||||
s.log.Info("Registering user client, ID:", userID)
|
||||
|
||||
s.withUserClientStoreLock(func() {
|
||||
s.userClientStore[userID] = &client{
|
||||
isTelemetryEnabled: telemetryService.IsTelemetryEnabled,
|
||||
sendMetrics: protonClient.SendObservabilityBatch,
|
||||
}
|
||||
})
|
||||
|
||||
// There may be a case where we already have metric updates stored, so try to flush;
|
||||
s.sendSignal(s.signalDataArrived)
|
||||
}
|
||||
|
||||
func (s *Service) DeregisterUserClient(userID string) {
|
||||
s.log.Info("De-registering user client, ID:", userID)
|
||||
|
||||
s.withUserClientStoreLock(func() {
|
||||
delete(s.userClientStore, userID)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) Stop() {
|
||||
s.log.Info("Stopping service")
|
||||
|
||||
s.cancel()
|
||||
close(s.signalDataArrived)
|
||||
close(s.signalDispatch)
|
||||
}
|
||||
|
||||
// Utility functions below.
|
||||
func (s *Service) haveMetricsAndClients() bool {
|
||||
s.metricStoreLock.Lock()
|
||||
s.userClientStoreLock.Lock()
|
||||
defer s.metricStoreLock.Unlock()
|
||||
defer s.userClientStoreLock.Unlock()
|
||||
|
||||
return len(s.metricStore) > 0 && len(s.userClientStore) > 0
|
||||
}
|
||||
|
||||
func (s *Service) withUserClientStoreLock(fn func()) {
|
||||
s.userClientStoreLock.Lock()
|
||||
defer s.userClientStoreLock.Unlock()
|
||||
fn()
|
||||
}
|
||||
|
||||
func (s *Service) withMetricStoreLock(fn func()) {
|
||||
s.metricStoreLock.Lock()
|
||||
defer s.metricStoreLock.Unlock()
|
||||
fn()
|
||||
}
|
||||
|
||||
// We use buffered channels; we shouldn't block them.
|
||||
func (s *Service) sendSignal(channel chan struct{}) {
|
||||
select {
|
||||
case channel <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// ModifyThrottlePeriod - used for testing.
|
||||
func ModifyThrottlePeriod(duration time.Duration) {
|
||||
throttleDuration = duration
|
||||
}
|
||||
@ -136,7 +136,7 @@ func (b *BuildStage) run(ctx context.Context) {
|
||||
return nil
|
||||
}
|
||||
|
||||
result, err := parallel.MapContext(ctx, maxMessagesInParallel, chunk, func(ctx context.Context, msg proton.FullMessage) (BuildResult, error) {
|
||||
result, err := parallel.MapContext(ctx, maxMessagesInParallel, chunk, func(_ context.Context, msg proton.FullMessage) (BuildResult, error) {
|
||||
defer async.HandlePanic(b.panicHandler)
|
||||
|
||||
kr, ok := addrKRs[msg.AddressID]
|
||||
|
||||
@ -253,7 +253,7 @@ func TestBuildStage_OtherErrorsFailJob(t *testing.T) {
|
||||
|
||||
expectedErr := errors.New("something went wrong")
|
||||
|
||||
tj.messageBuilder.EXPECT().WithKeys(gomock.Any()).DoAndReturn(func(f func(*crypto.KeyRing, map[string]*crypto.KeyRing) error) error {
|
||||
tj.messageBuilder.EXPECT().WithKeys(gomock.Any()).DoAndReturn(func(_ func(*crypto.KeyRing, map[string]*crypto.KeyRing) error) error {
|
||||
return expectedErr
|
||||
})
|
||||
|
||||
|
||||
@ -119,12 +119,12 @@ func TestAutoDownloadScale_429or500x(t *testing.T) {
|
||||
for _, d := range data {
|
||||
switch d {
|
||||
case "m7":
|
||||
call429 := client.EXPECT().GetMessage(gomock.Any(), gomock.Eq("m7")).DoAndReturn(func(_ context.Context, id string) (proton.Message, error) {
|
||||
call429 := client.EXPECT().GetMessage(gomock.Any(), gomock.Eq("m7")).DoAndReturn(func(_ context.Context, _ string) (proton.Message, error) {
|
||||
return proton.Message{}, &proton.APIError{Status: 429}
|
||||
})
|
||||
client.EXPECT().GetMessage(gomock.Any(), gomock.Eq("m7")).After(call429).DoAndReturn(autoDownloadScaleClientDoAndReturn)
|
||||
case "m23":
|
||||
call503 := client.EXPECT().GetMessage(gomock.Any(), gomock.Eq("m23")).DoAndReturn(func(_ context.Context, id string) (proton.Message, error) {
|
||||
call503 := client.EXPECT().GetMessage(gomock.Any(), gomock.Eq("m23")).DoAndReturn(func(_ context.Context, _ string) (proton.Message, error) {
|
||||
return proton.Message{}, &proton.APIError{Status: 503}
|
||||
})
|
||||
client.EXPECT().GetMessage(gomock.Any(), gomock.Eq("m23")).After(call503).DoAndReturn(autoDownloadScaleClientDoAndReturn)
|
||||
|
||||
@ -191,7 +191,7 @@ func TestService_OnBadEventServiceIsPaused(t *testing.T) {
|
||||
NewEventID: secondEventID,
|
||||
EventInfo: secondEvent[0].String(),
|
||||
Error: fmt.Errorf("failed to apply message events: %w", badEventErr),
|
||||
}).Do(func(_ context.Context, event events.Event) {
|
||||
}).Do(func(_ context.Context, _ events.Event) {
|
||||
group.Go(context.Background(), "", "", func(_ context.Context) {
|
||||
// Use background context to avoid having the request cancelled
|
||||
require.True(t, service.IsPaused())
|
||||
|
||||
@ -38,6 +38,7 @@ type EventHandler struct {
|
||||
MessageHandler MessageEventHandler
|
||||
UsedSpaceHandler UserUsedSpaceEventHandler
|
||||
UserSettingsHandler UserSettingsHandler
|
||||
NotificationHandler NotificationEventHandler
|
||||
}
|
||||
|
||||
func (e EventHandler) OnEvent(ctx context.Context, event proton.Event) error {
|
||||
@ -87,6 +88,12 @@ func (e EventHandler) OnEvent(ctx context.Context, event proton.Event) error {
|
||||
}
|
||||
}
|
||||
|
||||
if len(event.Notifications) != 0 && e.NotificationHandler != nil {
|
||||
if err := e.NotificationHandler.HandleNotificationEvents(ctx, event.Notifications); err != nil {
|
||||
return fmt.Errorf("failed to apply notification events: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -116,3 +123,7 @@ type LabelEventHandler interface {
|
||||
type MessageEventHandler interface {
|
||||
HandleMessageEvents(ctx context.Context, events []proton.MessageEvent) error
|
||||
}
|
||||
|
||||
type NotificationEventHandler interface {
|
||||
HandleNotificationEvents(ctx context.Context, events []proton.NotificationEvent) error
|
||||
}
|
||||
|
||||
235
internal/unleash/service.go
Normal file
235
internal/unleash/service.go
Normal file
@ -0,0 +1,235 @@
|
||||
// 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 unleash
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/service"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var pollPeriod = 10 * time.Minute //nolint:gochecknoglobals
|
||||
var pollJitter = 2 * time.Minute //nolint:gochecknoglobals
|
||||
|
||||
const filename = "unleash_flags"
|
||||
|
||||
type requestFeaturesFn func(ctx context.Context) (proton.FeatureFlagResult, error)
|
||||
type GetFlagValueFn func(key string) bool
|
||||
|
||||
type Service struct {
|
||||
panicHandler async.PanicHandler
|
||||
timer *proton.Ticker
|
||||
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
|
||||
log *logrus.Entry
|
||||
|
||||
ffStore map[string]bool
|
||||
ffStoreLock sync.Mutex
|
||||
|
||||
cacheFilepath string
|
||||
cacheFileLock sync.Mutex
|
||||
|
||||
channel chan map[string]bool
|
||||
|
||||
getFeaturesFn func(ctx context.Context) (proton.FeatureFlagResult, error)
|
||||
}
|
||||
|
||||
func NewBridgeService(ctx context.Context, api *proton.Manager, locator service.Locator, panicHandler async.PanicHandler) *Service {
|
||||
log := logrus.WithField("service", "unleash")
|
||||
cacheDir, err := locator.ProvideUnleashCachePath()
|
||||
if err != nil {
|
||||
log.Warn("Could not find or create unleash cache directory")
|
||||
}
|
||||
cachePath := filepath.Clean(filepath.Join(cacheDir, filename))
|
||||
|
||||
return newService(ctx, func(ctx context.Context) (proton.FeatureFlagResult, error) {
|
||||
return api.GetFeatures(ctx)
|
||||
}, log, cachePath, panicHandler)
|
||||
}
|
||||
|
||||
func newService(ctx context.Context, fn requestFeaturesFn, log *logrus.Entry, cachePath string, panicHandler async.PanicHandler) *Service {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
|
||||
unleashService := &Service{
|
||||
panicHandler: panicHandler,
|
||||
timer: proton.NewTicker(pollPeriod, pollJitter, panicHandler),
|
||||
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
|
||||
log: log,
|
||||
|
||||
ffStore: make(map[string]bool),
|
||||
cacheFilepath: cachePath,
|
||||
|
||||
channel: make(chan map[string]bool),
|
||||
|
||||
getFeaturesFn: fn,
|
||||
}
|
||||
|
||||
unleashService.readCacheFile()
|
||||
return unleashService
|
||||
}
|
||||
|
||||
func readResponseData(data proton.FeatureFlagResult) map[string]bool {
|
||||
featureData := make(map[string]bool)
|
||||
for _, el := range data.Toggles {
|
||||
featureData[el.Name] = el.Enabled
|
||||
}
|
||||
|
||||
return featureData
|
||||
}
|
||||
|
||||
func (s *Service) readCacheFile() {
|
||||
defer s.cacheFileLock.Unlock()
|
||||
s.cacheFileLock.Lock()
|
||||
|
||||
file, err := os.Open(s.cacheFilepath)
|
||||
if err != nil {
|
||||
s.log.WithError(err).Error("Unable to open cache file")
|
||||
return
|
||||
}
|
||||
|
||||
defer func(file *os.File) {
|
||||
err := file.Close()
|
||||
if err != nil {
|
||||
s.log.WithError(err).Error("Unable to close cache file after read")
|
||||
}
|
||||
}(file)
|
||||
|
||||
s.ffStoreLock.Lock()
|
||||
defer s.ffStoreLock.Unlock()
|
||||
if err = json.NewDecoder(file).Decode(&s.ffStore); err != nil {
|
||||
s.log.WithError(err).Error("Unable to decode cache file")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) writeCacheFile() {
|
||||
defer s.cacheFileLock.Unlock()
|
||||
s.cacheFileLock.Lock()
|
||||
|
||||
file, err := os.Create(s.cacheFilepath)
|
||||
if err != nil {
|
||||
s.log.WithError(err).Error("Unable to create cache file")
|
||||
return
|
||||
}
|
||||
|
||||
defer func(file *os.File) {
|
||||
err := file.Close()
|
||||
if err != nil {
|
||||
s.log.WithError(err).Error("Unable to close cache file after write")
|
||||
}
|
||||
}(file)
|
||||
|
||||
s.ffStoreLock.Lock()
|
||||
defer s.ffStoreLock.Unlock()
|
||||
if err = json.NewEncoder(file).Encode(s.ffStore); err != nil {
|
||||
s.log.WithError(err).Error("Unable to encode data to cache file")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) Run() {
|
||||
s.log.Info("Starting service")
|
||||
|
||||
go func() {
|
||||
s.runFlagPoll()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
s.runReceiver()
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *Service) runFlagPoll() {
|
||||
defer async.HandlePanic(s.panicHandler)
|
||||
defer s.timer.Stop()
|
||||
s.log.Info("Starting poll service")
|
||||
|
||||
data, err := s.getFeaturesFn(s.ctx)
|
||||
if err != nil {
|
||||
s.log.WithError(err).Error("Failed to get flags from server")
|
||||
} else {
|
||||
s.channel <- readResponseData(data)
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return
|
||||
case <-s.timer.C:
|
||||
s.log.Info("Polling flag service")
|
||||
data, err := s.getFeaturesFn(s.ctx)
|
||||
if err != nil {
|
||||
s.log.WithError(err).Error("Failed to get feature flags from server")
|
||||
continue
|
||||
}
|
||||
s.channel <- readResponseData(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) runReceiver() {
|
||||
defer async.HandlePanic(s.panicHandler)
|
||||
s.log.Info("Starting receiver service")
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return
|
||||
case res := <-s.channel:
|
||||
s.ffStoreLock.Lock()
|
||||
s.ffStore = res
|
||||
s.ffStoreLock.Unlock()
|
||||
s.writeCacheFile()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) GetFlagValue(key string) bool {
|
||||
defer s.ffStoreLock.Unlock()
|
||||
s.ffStoreLock.Lock()
|
||||
|
||||
val, ok := s.ffStore[key]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
func (s *Service) Close() {
|
||||
s.log.Info("Closing service")
|
||||
s.cancel()
|
||||
close(s.channel)
|
||||
}
|
||||
|
||||
// ModifyPollPeriodAndJitter is only used for testing.
|
||||
func ModifyPollPeriodAndJitter(pollInterval, jitterInterval time.Duration) {
|
||||
pollPeriod = pollInterval
|
||||
pollJitter = jitterInterval
|
||||
}
|
||||
@ -32,7 +32,7 @@ func BenchmarkAddrKeyRing(b *testing.B) {
|
||||
b.StopTimer()
|
||||
|
||||
withAPI(b, context.Background(), func(ctx context.Context, s *server.Server, m *proton.Manager) {
|
||||
withAccount(b, s, "username", "password", []string{"email@pm.me"}, func(userID string, addrIDs []string) {
|
||||
withAccount(b, s, "username", "password", []string{"email@pm.me"}, func(_ string, _ []string) {
|
||||
withUser(b, ctx, s, m, "username", "password", func(user *User) {
|
||||
b.StartTimer()
|
||||
|
||||
@ -43,7 +43,7 @@ func BenchmarkAddrKeyRing(b *testing.B) {
|
||||
require.NoError(b, err)
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
require.NoError(b, usertypes.WithAddrKRs(apiUser, apiAddrs, user.vault.KeyPass(), func(_ *crypto.KeyRing, addrKRs map[string]*crypto.KeyRing) error {
|
||||
require.NoError(b, usertypes.WithAddrKRs(apiUser, apiAddrs, user.vault.KeyPass(), func(_ *crypto.KeyRing, _ map[string]*crypto.KeyRing) error {
|
||||
return nil
|
||||
}))
|
||||
}
|
||||
|
||||
@ -31,6 +31,8 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/notifications"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/orderedtasks"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/sendrecorder"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/smtp"
|
||||
@ -39,6 +41,7 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/userevents"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/useridentity"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/telemetry"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/unleash"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/usertypes"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
"github.com/ProtonMail/proton-bridge/v3/pkg/algo"
|
||||
@ -80,11 +83,14 @@ type User struct {
|
||||
// goStatusProgress triggers a check/sending if progress is needed.
|
||||
goStatusProgress func()
|
||||
|
||||
eventService *userevents.Service
|
||||
identityService *useridentity.Service
|
||||
smtpService *smtp.Service
|
||||
imapService *imapservice.Service
|
||||
telemetryService *telemetryservice.Service
|
||||
eventService *userevents.Service
|
||||
identityService *useridentity.Service
|
||||
smtpService *smtp.Service
|
||||
imapService *imapservice.Service
|
||||
telemetryService *telemetryservice.Service
|
||||
notificationService *notifications.Service
|
||||
|
||||
observabilityService *observability.Service
|
||||
|
||||
serviceGroup *orderedtasks.OrderedCancelGroup
|
||||
}
|
||||
@ -104,8 +110,12 @@ func New(
|
||||
smtpServerManager smtp.ServerManager,
|
||||
eventSubscription events.Subscription,
|
||||
syncService syncservice.Regulator,
|
||||
observabilityService *observability.Service,
|
||||
syncConfigDir string,
|
||||
isNew bool,
|
||||
notificationStore *notifications.Store,
|
||||
getFlagValFn unleash.GetFlagValueFn,
|
||||
pushObservabilityMetric observability.PushObsMetricFn,
|
||||
) (*User, error) {
|
||||
user, err := newImpl(
|
||||
ctx,
|
||||
@ -122,8 +132,12 @@ func New(
|
||||
smtpServerManager,
|
||||
eventSubscription,
|
||||
syncService,
|
||||
observabilityService,
|
||||
syncConfigDir,
|
||||
isNew,
|
||||
notificationStore,
|
||||
getFlagValFn,
|
||||
pushObservabilityMetric,
|
||||
)
|
||||
if err != nil {
|
||||
// Cleanup any pending resources on error
|
||||
@ -153,8 +167,12 @@ func newImpl(
|
||||
smtpServerManager smtp.ServerManager,
|
||||
eventSubscription events.Subscription,
|
||||
syncService syncservice.Regulator,
|
||||
observabilityService *observability.Service,
|
||||
syncConfigDir string,
|
||||
isNew bool,
|
||||
notificationStore *notifications.Store,
|
||||
getFlagValueFn unleash.GetFlagValueFn,
|
||||
pushObservabilityMetric observability.PushObsMetricFn,
|
||||
) (*User, error) {
|
||||
logrus.WithField("userID", apiUser.ID).Info("Creating new user")
|
||||
|
||||
@ -215,6 +233,8 @@ func newImpl(
|
||||
|
||||
serviceGroup: orderedtasks.NewOrderedCancelGroup(crashHandler),
|
||||
smtpService: nil,
|
||||
|
||||
observabilityService: observabilityService,
|
||||
}
|
||||
|
||||
user.eventService = userevents.NewService(
|
||||
@ -270,6 +290,8 @@ func newImpl(
|
||||
showAllMail,
|
||||
)
|
||||
|
||||
user.notificationService = notifications.NewService(user.id, user.eventService, user, notificationStore, getFlagValueFn, pushObservabilityMetric)
|
||||
|
||||
// Check for status_progress when triggered.
|
||||
user.goStatusProgress = user.tasks.PeriodicOrTrigger(configstatus.ProgressCheckInterval, 0, func(ctx context.Context) {
|
||||
user.SendConfigStatusProgress(ctx)
|
||||
@ -318,6 +340,12 @@ func newImpl(
|
||||
// Start Identity Service
|
||||
user.identityService.Start(ctx, user.serviceGroup)
|
||||
|
||||
// Add user client to observability service
|
||||
observabilityService.RegisterUserClient(user.id, client, user.telemetryService)
|
||||
|
||||
// Start Notification service
|
||||
user.notificationService.Start(ctx, user.serviceGroup)
|
||||
|
||||
// Start SMTP Service
|
||||
if err := user.smtpService.Start(ctx, user.serviceGroup); err != nil {
|
||||
return user, fmt.Errorf("failed to start smtp service: %w", err)
|
||||
@ -586,6 +614,9 @@ func (user *User) Logout(ctx context.Context, withAPI bool) error {
|
||||
|
||||
user.tasks.CancelAndWait()
|
||||
|
||||
// Close user observability service.
|
||||
user.observabilityService.DeregisterUserClient(user.id)
|
||||
|
||||
// Stop Services
|
||||
user.serviceGroup.CancelAndWait()
|
||||
|
||||
@ -619,6 +650,9 @@ func (user *User) Close() {
|
||||
// Stop any ongoing background tasks.
|
||||
user.tasks.CancelAndWait()
|
||||
|
||||
// Close user observability service.
|
||||
user.observabilityService.DeregisterUserClient(user.id)
|
||||
|
||||
// Stop Services
|
||||
user.serviceGroup.CancelAndWait()
|
||||
|
||||
|
||||
@ -29,6 +29,8 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/certs"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice"
|
||||
"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/smtp"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/telemetry/mocks"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
@ -164,8 +166,16 @@ func withUser(tb testing.TB, ctx context.Context, _ *server.Server, m *proton.Ma
|
||||
nullSMTPServerManager,
|
||||
nullEventSubscription,
|
||||
nil,
|
||||
observability.NewService(context.Background(), nil),
|
||||
"",
|
||||
true,
|
||||
notifications.NewStore(func() (string, error) {
|
||||
return "", nil
|
||||
}),
|
||||
func(_ string) bool {
|
||||
return false
|
||||
},
|
||||
func(_ proton.ObservabilityMetric) {},
|
||||
)
|
||||
require.NoError(tb, err)
|
||||
defer user.Close()
|
||||
|
||||
@ -19,53 +19,57 @@ package vault
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const vaultSecretName = "bridge-vault-key"
|
||||
|
||||
type Keychain struct {
|
||||
Helper string
|
||||
}
|
||||
|
||||
func getKeychainPrefPath(vaultDir string) string {
|
||||
return filepath.Clean(filepath.Join(vaultDir, "keychain.json"))
|
||||
}
|
||||
|
||||
func GetHelper(vaultDir string) (string, error) {
|
||||
if _, err := os.Stat(getKeychainPrefPath(vaultDir)); errors.Is(err, fs.ErrNotExist) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
b, err := os.ReadFile(getKeychainPrefPath(vaultDir))
|
||||
func GetShouldSkipKeychainTest(vaultDir string) (bool, error) {
|
||||
settings, err := LoadKeychainSettings(vaultDir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return false, err
|
||||
}
|
||||
|
||||
var keychain Keychain
|
||||
|
||||
if err := json.Unmarshal(b, &keychain); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return keychain.Helper, nil
|
||||
return settings.DisableTest, nil
|
||||
}
|
||||
|
||||
func SetHelper(vaultDir, helper string) error {
|
||||
b, err := json.MarshalIndent(Keychain{Helper: helper}, "", " ")
|
||||
func SetShouldSkipKeychainTest(vaultDir string, skip bool) error {
|
||||
settings, err := LoadKeychainSettings(vaultDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(getKeychainPrefPath(vaultDir), b, 0o600)
|
||||
log := logrus.WithFields(logrus.Fields{"pkg": "vault", "skipKeychainTest": skip})
|
||||
if skip == settings.DisableTest {
|
||||
log.Info("Skipping change of keychain test setting as value is not modified")
|
||||
return nil
|
||||
}
|
||||
|
||||
logrus.WithFields(logrus.Fields{"pkg": "vault", "skipKeychainTest": skip}).Info("Setting keychain test skip option")
|
||||
settings.DisableTest = skip
|
||||
return settings.Save(vaultDir)
|
||||
}
|
||||
|
||||
func GetHelper(vaultDir string) (string, error) {
|
||||
settings, err := LoadKeychainSettings(vaultDir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return settings.Helper, nil
|
||||
}
|
||||
|
||||
func SetHelper(vaultDir, helper string) error {
|
||||
settings, err := LoadKeychainSettings(vaultDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
settings.Helper = helper
|
||||
return settings.Save(vaultDir)
|
||||
}
|
||||
|
||||
func GetVaultKey(kc *keychain.Keychain) ([]byte, error) {
|
||||
|
||||
46
internal/vault/helper_test.go
Normal file
46
internal/vault/helper_test.go
Normal file
@ -0,0 +1,46 @@
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package vault
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestShouldSkipKeychainTestAccessors(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
skip, err := GetShouldSkipKeychainTest(dir)
|
||||
require.NoError(t, err)
|
||||
require.False(t, skip)
|
||||
require.NoError(t, SetShouldSkipKeychainTest(dir, true))
|
||||
skip, err = GetShouldSkipKeychainTest(dir)
|
||||
require.NoError(t, err)
|
||||
require.True(t, skip)
|
||||
}
|
||||
|
||||
func TestHelperAccessors(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
helper, err := GetHelper(dir)
|
||||
require.NoError(t, err)
|
||||
require.Zero(t, len(helper))
|
||||
require.NoError(t, SetHelper(dir, "keychain"))
|
||||
helper, err = GetHelper(dir)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "keychain", helper)
|
||||
}
|
||||
74
internal/vault/keychain_settings.go
Normal file
74
internal/vault/keychain_settings.go
Normal file
@ -0,0 +1,74 @@
|
||||
// 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 vault
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const keychainSettingsFileName = "keychain.json"
|
||||
|
||||
// KeychainSettings holds settings related to the keychain. It is serialized in the vault directory.
|
||||
type KeychainSettings struct {
|
||||
Helper string // The helper used for keychain.
|
||||
DisableTest bool // Is the keychain test on startup disabled?
|
||||
}
|
||||
|
||||
// LoadKeychainSettings load keychain settings from the vaultDir folder, or returns a default one if the file
|
||||
// does not exists or is invalid.
|
||||
func LoadKeychainSettings(vaultDir string) (KeychainSettings, error) {
|
||||
path := filepath.Join(vaultDir, keychainSettingsFileName)
|
||||
bytes, err := os.ReadFile(path) //nolint:gosec
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
logrus.
|
||||
WithFields(logrus.Fields{"pkg": "vault", "path": path}).
|
||||
Trace("Keychain settings file does not exists, default values will be used")
|
||||
return KeychainSettings{}, nil
|
||||
}
|
||||
return KeychainSettings{}, err
|
||||
}
|
||||
|
||||
var result KeychainSettings
|
||||
if err := json.Unmarshal(bytes, &result); err != nil {
|
||||
return KeychainSettings{}, fmt.Errorf("keychain settings file is invalid settings: %w", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Save saves the keychain settings in a file in the vaultDir folder.
|
||||
func (k KeychainSettings) Save(vaultDir string) error {
|
||||
bytes, err := json.MarshalIndent(k, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = os.MkdirAll(vaultDir, 0o700); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
path := filepath.Join(vaultDir, keychainSettingsFileName)
|
||||
return os.WriteFile(path, bytes, 0o600)
|
||||
}
|
||||
58
internal/vault/keychain_settings_test.go
Normal file
58
internal/vault/keychain_settings_test.go
Normal file
@ -0,0 +1,58 @@
|
||||
// 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 vault
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestKeychainSettingsIO(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
// test loading non existing file. no error but loads defaults.
|
||||
settings, err := LoadKeychainSettings(dir)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, settings, KeychainSettings{})
|
||||
|
||||
// test file creation
|
||||
settings.Helper = "dummy1"
|
||||
settings.DisableTest = true
|
||||
require.NoError(t, settings.Save(dir))
|
||||
|
||||
// test reading existing file
|
||||
readBack, err := LoadKeychainSettings(dir)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, settings, readBack)
|
||||
|
||||
// test file overwrite and read back
|
||||
settings.Helper = "dummy2"
|
||||
require.NoError(t, settings.Save(dir))
|
||||
readBack, err = LoadKeychainSettings(dir)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, settings, readBack)
|
||||
|
||||
// test error on invalid content
|
||||
settingsFilePath := filepath.Join(dir, keychainSettingsFileName)
|
||||
require.NoError(t, os.WriteFile(settingsFilePath, []byte("][INVALID"), 0o600))
|
||||
_, err = LoadKeychainSettings(dir)
|
||||
require.Error(t, err)
|
||||
}
|
||||
@ -27,7 +27,7 @@ import (
|
||||
|
||||
func TestUser_New(t *testing.T) {
|
||||
// Replace the token generator with a dummy one.
|
||||
vault.RandomToken = func(size int) ([]byte, error) {
|
||||
vault.RandomToken = func(_ int) ([]byte, error) {
|
||||
return []byte("token"), nil
|
||||
}
|
||||
|
||||
@ -243,7 +243,7 @@ func TestUser_ForEach(t *testing.T) {
|
||||
|
||||
func TestUser_ShouldResync(t *testing.T) {
|
||||
// Replace the token generator with a dummy one.
|
||||
vault.RandomToken = func(size int) ([]byte, error) {
|
||||
vault.RandomToken = func(_ int) ([]byte, error) {
|
||||
return []byte("token"), nil
|
||||
}
|
||||
|
||||
|
||||
@ -56,7 +56,7 @@ func (op *OpRemove) Do() error {
|
||||
func remove(dir string, except ...string) error {
|
||||
var toRemove []string
|
||||
|
||||
if err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
||||
if err := filepath.Walk(dir, func(path string, _ os.FileInfo, _ error) error {
|
||||
for _, exception := range except {
|
||||
if path == exception || strings.HasPrefix(exception, path) || strings.HasPrefix(path, exception) {
|
||||
return nil
|
||||
|
||||
@ -31,15 +31,20 @@ const (
|
||||
MacOSKeychain = "macos-keychain"
|
||||
)
|
||||
|
||||
func listHelpers() (Helpers, string) {
|
||||
func listHelpers(skipKeychainTest bool) (Helpers, string) {
|
||||
helpers := make(Helpers)
|
||||
|
||||
// MacOS always provides a keychain.
|
||||
if isUsable(newMacOSHelper("")) {
|
||||
if skipKeychainTest {
|
||||
logrus.WithField("pkg", "keychain").Info("Skipping macOS keychain test")
|
||||
helpers[MacOSKeychain] = newMacOSHelper
|
||||
logrus.WithField("keychain", "MacOSKeychain").Info("Keychain is usable.")
|
||||
} else {
|
||||
logrus.WithField("keychain", "MacOSKeychain").Debug("Keychain is not available.")
|
||||
if isUsable(newMacOSHelper("")) {
|
||||
helpers[MacOSKeychain] = newMacOSHelper
|
||||
logrus.WithField("keychain", "MacOSKeychain").Info("Keychain is usable.")
|
||||
} else {
|
||||
logrus.WithField("keychain", "MacOSKeychain").Debug("Keychain is not available.")
|
||||
}
|
||||
}
|
||||
|
||||
// Use MacOSKeychain by default.
|
||||
|
||||
@ -31,7 +31,7 @@ const (
|
||||
SecretServiceDBus = "secret-service-dbus"
|
||||
)
|
||||
|
||||
func listHelpers() (Helpers, string) {
|
||||
func listHelpers(_ bool) (Helpers, string) {
|
||||
helpers := make(Helpers)
|
||||
|
||||
if isUsable(newDBusHelper("")) {
|
||||
|
||||
@ -25,7 +25,7 @@ import (
|
||||
|
||||
const WindowsCredentials = "windows-credentials"
|
||||
|
||||
func listHelpers() (Helpers, string) {
|
||||
func listHelpers(_ bool) (Helpers, string) {
|
||||
helpers := make(Helpers)
|
||||
// Windows always provides a keychain.
|
||||
if isUsable(newWinCredHelper("")) {
|
||||
|
||||
@ -62,9 +62,9 @@ type List struct {
|
||||
// NewList checks availability of every keychains detected on the User Operating System
|
||||
// This will ask the user to unlock keychain(s) to check their usability.
|
||||
// This should only be called once.
|
||||
func NewList() *List {
|
||||
func NewList(skipKeychainTest bool) *List {
|
||||
var list = List{locker: &sync.Mutex{}}
|
||||
list.helpers, list.defaultHelper = listHelpers()
|
||||
list.helpers, list.defaultHelper = listHelpers(skipKeychainTest)
|
||||
return &list
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user