Compare commits

..

6 Commits

174 changed files with 2861 additions and 7503 deletions

3
.gitignore vendored
View File

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

View File

@ -25,16 +25,11 @@ variables:
GOMAXPROCS: $(( ${CI_TAG_CPU} / 2 ))
before_script:
- |
if [ "$CI_JOB_NAME" != "grype-scan-code-dependencies" ]; then
apt update && apt-get -y install libsecret-1-dev
git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
fi
- apt update && apt-get -y install libsecret-1-dev
- git config --global url.https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}.insteadOf https://${CI_SERVER_HOST}
stages:
- analyse
- test
- report
- build
include:
@ -42,9 +37,5 @@ 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/kits/devsecops/go@~latest
inputs:
stage: analyse

View File

@ -2,12 +2,11 @@
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.

View File

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

View File

@ -3,7 +3,7 @@
## Prerequisites
* 64-bit OS:
- the go-rfc5322 module cannot currently be compiled for 32-bit OSes
* Go 1.21.9
* Go 1.21.6
* Bash with basic build utils: make, gcc, sed, find, grep, ...
- For Windows, it is recommended to use MinGW 64bit shell from [MSYS2](https://www.msys2.org/)
* GCC (Linux), msvc (Windows) or Xcode (macOS)

View File

@ -3,57 +3,6 @@
Changelog [format](http://keepachangelog.com/en/1.0.0/)
## Dragon Bridge 3.14.0
### Changed
* BRIDGE-207: Failure to download or verify an update now fails silently.
* BRIDGE-204: Removed redundant Sentry events.
* BRIDGE-150: Observability service modification.
* BRIDGE-210: Reduced log level of cache events so they won't be printed to stdout.
### Fixed
* BRIDGE-106: Fixed import of multipart-related messages.
* BRIDGE-108: Fixed GetInitials when empty username is passed.
## Colorado Bridge 3.13.0
### Added
* BRIDGE-37: added message broadcasting functionality.
* BRIDGE-122: added observability service.
* BRIDGE-119: added support for Feature Flags.
* BRIDGE-116: added command-line switches to enable/disable keychain check on macOS.
* BRIDGE-88: added context menu for quick actions on input labels: cut, copy, paste.
### Changed
* BRIDGE-81: KB article suggestion updates + more weight for long keywords.
### Fixed
* BRIDGE-67: Added detection for username changes on macOS & automatic reconfiguration.
* BRIDGE-138: Remove deprecated doc.
## Bastei Bridge 3.12.0
### Added
* BRIDGE-75: Bridge repair button.
* BRIDGE-79: Add New Outlook for Mac KB disclaimer.
### Changed
* BRIDGE-16: Bump version Go 1.21.9 Qt 6.4.3.
* BRIDGE-23: Update gluon to go 1.21.
* BRIDGE-22: Update gpa to go 1.21.
### Fixed
* BRIDGE-90: Disable repair button when bridge cannot connect to proton servers; bump GPA.
* BRIDGE-69: Explicitly handle semver panic for last bridge version from vault.
* BRIDGE-29: Bump gluon version.
* BRIDGE-49: Configure gitleaks baseline and grype config.
* BRIDGE-21: Missing panic handling.
* BRIDGE-17: Broken telemetry heartbeat test.
* BRIDGE-10: Bumped gluon version.
## Alcantara Bridge 3.11.1
### Fixed

View File

@ -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.14.0+git
BRIDGE_APP_VERSION?=3.11.1+git
APP_VERSION:=${BRIDGE_APP_VERSION}
APP_FULL_NAME:=Proton Mail Bridge
APP_VENDOR:=Proton AG
@ -189,7 +189,7 @@ ${RESOURCE_FILE}: ./dist/info.rc ./dist/${SRC_ICO} .FORCE
## Dev dependencies
.PHONY: install-devel-tools install-linter install-go-mod-outdated install-git-hooks
LINTVER:="v1.59.1"
LINTVER:="v1.55.2"
LINTSRC:="https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh"
install-dev-dependencies: install-devel-tools install-linter install-go-mod-outdated
@ -264,8 +264,7 @@ test-integration-race: gofiles
test-integration-nightly: gofiles
mkdir -p coverage/integration
gotestsum \
--junitfile tests/result/feature-tests.xml -- \
go test \
-v -timeout=90m -p=1 -count=1 -tags=test_integration \
${GOCOVERAGE} \
github.com/ProtonMail/proton-bridge/v3/tests \

View File

@ -1,25 +0,0 @@
---
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"

View File

@ -108,7 +108,6 @@ test-integration-nightly:
artifacts:
when: always
paths:
- tests/result/feature-tests.xml
- nightly-job.log
test-coverage:

135
doc/bridge.md Normal file
View File

@ -0,0 +1,135 @@
# 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).

114
doc/communication.md Normal file
View File

@ -0,0 +1,114 @@
# 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
```

27
doc/database.md Normal file
View File

@ -0,0 +1,27 @@
# 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`

12
doc/encryption.md Normal file
View File

@ -0,0 +1,12 @@
# 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.

9
doc/index.md Normal file
View File

@ -0,0 +1,9 @@
# 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 Normal file
View File

@ -0,0 +1,103 @@
# 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_

20
go.mod
View File

@ -2,14 +2,12 @@ module github.com/ProtonMail/proton-bridge/v3
go 1.21
toolchain go1.21.9
require (
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
github.com/Masterminds/semver/v3 v3.2.0
github.com/ProtonMail/gluon v0.17.1-0.20240923151549-d23b4bec3602
github.com/ProtonMail/gluon v0.17.1-0.20240227105633-3734c7694bcd
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
github.com/ProtonMail/go-proton-api v0.4.1-0.20240918100656-b4860af56d47
github.com/ProtonMail/go-proton-api v0.4.1-0.20240405124415-8f966ca60436
github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton
github.com/PuerkitoBio/goquery v1.8.1
github.com/abiosoft/ishell v2.0.0+incompatible
@ -46,11 +44,11 @@ require (
github.com/vmihailenco/msgpack/v5 v5.3.5
go.uber.org/goleak v1.2.1
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
golang.org/x/net v0.24.0
golang.org/x/sys v0.19.0
golang.org/x/net v0.17.0
golang.org/x/sys v0.16.0
golang.org/x/text v0.14.0
google.golang.org/grpc v1.56.3
google.golang.org/protobuf v1.33.0
google.golang.org/protobuf v1.31.0
howett.net/plist v1.0.0
)
@ -64,7 +62,7 @@ require (
github.com/bytedance/sonic v1.9.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/chzyer/test v1.0.0 // indirect
github.com/cloudflare/circl v1.3.7 // indirect
github.com/cloudflare/circl v1.3.3 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/cronokirby/saferith v0.33.0 // indirect
github.com/cucumber/gherkin-go/v19 v19.0.3 // indirect
@ -95,7 +93,7 @@ require (
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/mattn/go-sqlite3 v1.14.17 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
@ -113,7 +111,7 @@ require (
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.22.0 // indirect
golang.org/x/crypto v0.18.0 // indirect
golang.org/x/mod v0.8.0 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/tools v0.6.0 // indirect
@ -122,7 +120,7 @@ require (
)
replace (
github.com/emersion/go-message => github.com/ProtonMail/go-message v0.13.1-0.20240919135104-3bc88e6a9423
github.com/emersion/go-message => github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7
github.com/emersion/go-smtp => github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865
github.com/go-resty/resty/v2 => github.com/LBeernaertProton/resty/v2 v2.0.0-20231129100320-dddf8030d93a
github.com/keybase/go-keychain => github.com/cuthix/go-keychain v0.0.0-20240103134243-0b6a41580b77

66
go.sum
View File

@ -27,14 +27,8 @@ 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.20240514133734-79cdd0fec41c h1:P3SvCACt13Zqdj0IRDB4bgwqI68+oMB2j0uVuPQyoTw=
github.com/ProtonMail/gluon v0.17.1-0.20240514133734-79cdd0fec41c/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
github.com/ProtonMail/gluon v0.17.1-0.20240918150504-3b2e7f40d961 h1:kCaz78X7OKETvK6AGHeyggHKxDBcqX7EWHf7spJ+D3g=
github.com/ProtonMail/gluon v0.17.1-0.20240918150504-3b2e7f40d961/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
github.com/ProtonMail/gluon v0.17.1-0.20240923094038-e319bf6047c5 h1:LzaUpUj6M2PEBArFCkaimViNpGXDgwHVrdhvYwHLoJQ=
github.com/ProtonMail/gluon v0.17.1-0.20240923094038-e319bf6047c5/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
github.com/ProtonMail/gluon v0.17.1-0.20240923151549-d23b4bec3602 h1:EoMjWlC32tg46L/07hWoiZfLkqJyxVMcsq4Cyn+Ofqc=
github.com/ProtonMail/gluon v0.17.1-0.20240923151549-d23b4bec3602/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
github.com/ProtonMail/gluon v0.17.1-0.20240227105633-3734c7694bcd h1:AjJsf5xQGmZPg6GLn+wB+eBoGRopJlG70lQBfSyfX+M=
github.com/ProtonMail/gluon v0.17.1-0.20240227105633-3734c7694bcd/go.mod h1:Og5/Dz1MiGpCJn51XujZwxiLG7WzvvjE5PRpZBQmAHo=
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
@ -42,38 +36,12 @@ github.com/ProtonMail/go-crypto v0.0.0-20230717121622-edf196117233 h1:bdoKdh0f66
github.com/ProtonMail/go-crypto v0.0.0-20230717121622-edf196117233/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7 h1:+j+Kd/DyZ/qGfMT9htAT7HxqIEbZHsatsx+m8AoV6fc=
github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
github.com/ProtonMail/go-message v0.13.1-0.20240906141354-38c596f2f5a8 h1:+eE7FGX+4Hu8RZaRmSebrDVXyLuowKSaO7ZhQ6ca4+E=
github.com/ProtonMail/go-message v0.13.1-0.20240906141354-38c596f2f5a8/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
github.com/ProtonMail/go-message v0.13.1-0.20240906144417-4083506a9542 h1:5DqSycYnKfUdHiu0yOdiYW5R2hVxoE0Mk4PLSYwqGyg=
github.com/ProtonMail/go-message v0.13.1-0.20240906144417-4083506a9542/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
github.com/ProtonMail/go-message v0.13.1-0.20240910093530-2ada52e7dffb h1:uOKp93u6JFYlBoJJvOhzmHZURcvWmXiqhihGWtT3HtY=
github.com/ProtonMail/go-message v0.13.1-0.20240910093530-2ada52e7dffb/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
github.com/ProtonMail/go-message v0.13.1-0.20240919135104-3bc88e6a9423 h1:p8nBDxvRnvDOyrcePKkPpErWGhDoTqpX8a1c54CcSu0=
github.com/ProtonMail/go-message v0.13.1-0.20240919135104-3bc88e6a9423/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
github.com/ProtonMail/go-proton-api v0.4.1-0.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-proton-api v0.4.1-0.20240916123336-3ac75d8041dc h1:SWVPwO1M2jCI1bJHBji/JVU01FpWP/6nzh8NBIjo+Fg=
github.com/ProtonMail/go-proton-api v0.4.1-0.20240916123336-3ac75d8041dc/go.mod h1:3A0cpdo0BIenIPjTG6u8EbzJ8uuJy7rVvM/NaynjCKA=
github.com/ProtonMail/go-proton-api v0.4.1-0.20240918100656-b4860af56d47 h1:a+3dOyIxJEslN5HxyICM8flY9lnCyJupXNcv6fUaivA=
github.com/ProtonMail/go-proton-api v0.4.1-0.20240918100656-b4860af56d47/go.mod h1:3A0cpdo0BIenIPjTG6u8EbzJ8uuJy7rVvM/NaynjCKA=
github.com/ProtonMail/go-proton-api v0.4.1-0.20240226161523-ec58ed7ea4b9 h1:tcQpGQljNsZmfuA6L4hAzio8/AIx5OXcU2JUdyX/qxw=
github.com/ProtonMail/go-proton-api v0.4.1-0.20240226161523-ec58ed7ea4b9/go.mod h1:t+hb0BfkmZ9fpvzVRpHC7limoowym6ln/j0XL9a8DDw=
github.com/ProtonMail/go-proton-api v0.4.1-0.20240405124415-8f966ca60436 h1:ej+W9+UQlb2owkT5arCegmUFkicwesMyFHgBp/wwNg8=
github.com/ProtonMail/go-proton-api v0.4.1-0.20240405124415-8f966ca60436/go.mod h1:t+hb0BfkmZ9fpvzVRpHC7limoowym6ln/j0XL9a8DDw=
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=
@ -121,9 +89,8 @@ github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs=
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
@ -347,8 +314,8 @@ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
@ -500,8 +467,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -555,9 +522,8 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -611,8 +577,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
@ -696,8 +662,8 @@ google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc=
google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@ -83,8 +83,6 @@ const (
flagNoWindow = "no-window"
flagParentPID = "parent-pid"
flagSoftwareRenderer = "software-renderer"
flagEnableKeychainTest = "enable-keychain-test"
flagDisableKeychainTest = "disable-keychain-test"
FlagSessionID = "session-id"
)
@ -93,20 +91,6 @@ 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()
@ -184,9 +168,6 @@ func New() *cli.App {
Name: FlagSessionID,
Hidden: true,
},
// the two flags below were introduced by BRIDGE-116
cliFlagEnableKeychainTest,
cliFlagDisableKeychainTest,
}
app.Action = run
@ -257,8 +238,7 @@ func run(c *cli.Context) error {
return withSingleInstance(settings, locations.GetLockFile(), version, func() error {
// Look for available keychains
skipKeychainTest := checkSkipKeychainTest(c, settings)
return WithKeychainList(crashHandler, skipKeychainTest, func(keychains *keychain.List) error {
return WithKeychainList(crashHandler, func(keychains *keychain.List) error {
// Unlock the encrypted vault.
return WithVault(locations, keychains, crashHandler, func(v *vault.Vault, insecure, corrupt bool) error {
if !v.Migrated() {
@ -522,11 +502,11 @@ func withCookieJar(vault *vault.Vault, fn func(http.CookieJar) error) error {
}
// WithKeychainList init the list of usable keychains.
func WithKeychainList(panicHandler async.PanicHandler, skipKeychainTest bool, fn func(*keychain.List) error) error {
func WithKeychainList(panicHandler async.PanicHandler, fn func(*keychain.List) error) error {
logrus.Debug("Creating keychain list")
defer logrus.Debug("Keychain list stop")
defer async.HandlePanic(panicHandler)
return fn(keychain.NewList(skipKeychainTest))
return fn(keychain.NewList())
}
func setDeviceCookies(jar *cookies.Jar) error {
@ -546,35 +526,3 @@ 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
}

View File

@ -1,65 +0,0 @@
// Copyright (c) 2024 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package app
import (
"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))
}

View File

@ -24,10 +24,6 @@ import (
"errors"
"fmt"
"net/http"
"os"
"regexp"
"runtime"
"strings"
"sync"
"time"
@ -45,11 +41,8 @@ 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"
@ -58,8 +51,6 @@ 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
@ -139,15 +130,6 @@ 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
@ -265,10 +247,6 @@ func newBridge(
return nil, fmt.Errorf("failed to create focus service: %w", err)
}
unleashService := unleash.NewBridgeService(ctx, api, locator, panicHandler)
observabilityService := observability.NewService(ctx, panicHandler)
bridge := &Bridge{
vault: vault,
@ -308,13 +286,7 @@ func newBridge(
lastVersion: lastVersion,
tasks: tasks,
syncService: syncservice.NewService(panicHandler, observabilityService),
unleashService: unleashService,
observabilityService: observabilityService,
notificationStore: notifications.NewStore(locator.ProvideNotificationsCachePath),
syncService: syncservice.NewService(reporter, panicHandler),
}
bridge.serverManager = imapsmtpserver.NewService(context.Background(),
@ -327,9 +299,6 @@ 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
}
@ -342,10 +311,6 @@ func newBridge(
bridge.syncService.Run()
bridge.unleashService.Run()
bridge.observabilityService.Run(bridge)
return bridge, nil
}
@ -473,9 +438,6 @@ 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()
@ -499,9 +461,6 @@ 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()
@ -580,49 +539,6 @@ func (bridge *Bridge) onStatusDown(ctx context.Context) {
}
}
func (bridge *Bridge) Repair() {
var wg sync.WaitGroup
userIDs := bridge.GetUserIDs()
for _, userID := range userIDs {
logPkg.Info("Initiating repair for userID:", userID)
userInfo, err := bridge.GetUserInfo(userID)
if err != nil {
logPkg.WithError(err).Error("Failed getting user info for repair; ID:", userID)
continue
}
if userInfo.State != Connected {
logPkg.Info("User is not connected. Repair will be executed on following successful log in.", userID)
if err := bridge.vault.GetUser(userID, func(user *vault.User) {
if err := user.SetShouldSync(true); err != nil {
logPkg.WithError(err).Error("Failed setting vault should sync for user:", userID)
}
}); err != nil {
logPkg.WithError(err).Error("Unable to get user vault when scheduling repair:", userID)
}
continue
}
bridgeUser, ok := bridge.users[userID]
if !ok {
logPkg.Info("UserID does not exist in bridge user map", userID)
continue
}
wg.Add(1)
go func(userID string) {
defer wg.Done()
if err = bridgeUser.TriggerRepair(); err != nil {
logPkg.WithError(err).Error("Failed re-syncing IMAP for userID", userID)
}
}(userID)
}
wg.Wait()
}
func loadTLSConfig(vault *vault.Vault) (*tls.Config, error) {
cert, err := tls.X509KeyPair(vault.GetBridgeTLSCert())
if err != nil {
@ -642,83 +558,3 @@ func min(a, b time.Duration) time.Duration {
return b
}
func (bridge *Bridge) HasAPIConnection() bool {
return bridge.api.GetStatus() == proton.StatusUp
}
// verifyUsernameChange - works only on macOS
// it attempts to check whether a username change has taken place by comparing the gluon DB path (which is static and provided by bridge)
// to the gluon Cache path - which can be modified by the user and is stored in the vault;
// if a username discrepancy is detected, and the cache folder does not exist with the "old" username
// then we verify whether the gluon cache exists using the "new" username (provided by the DB path in this case)
// if so we modify the cache directory in the user vault.
func (bridge *Bridge) verifyUsernameChange() {
if runtime.GOOS != "darwin" {
return
}
gluonDBPath, err := bridge.GetGluonDataDir()
if err != nil {
logPkg.WithError(err).Error("Failed to get gluon db path")
return
}
gluonCachePath := bridge.GetGluonCacheDir()
// If the cache folder exists even on another user account or is in `/Users/Shared` we would still be able to access it
// though it depends on the permissions; this is an edge-case.
if _, err := os.Stat(gluonCachePath); err == nil {
return
}
newCacheDir := GetUpdatedCachePath(gluonDBPath, gluonCachePath)
if newCacheDir == "" {
return
}
if _, err := os.Stat(newCacheDir); err == nil {
logPkg.Info("Username change detected. Trying to restore gluon cache directory")
if err = bridge.vault.SetGluonDir(newCacheDir); err != nil {
logPkg.WithError(err).Error("Failed to restore gluon cache directory")
return
}
logPkg.Info("Successfully restored gluon cache directory")
}
}
func GetUpdatedCachePath(gluonDBPath, gluonCachePath string) string {
// If gluon cache is moved to an external drive; regex find will fail; as is expected
cachePathMatches := usernameChangeRegex.FindStringSubmatch(gluonCachePath)
if cachePathMatches == nil || len(cachePathMatches) < 2 {
return ""
}
cacheUsername := cachePathMatches[1]
dbPathMatches := usernameChangeRegex.FindStringSubmatch(gluonDBPath)
if dbPathMatches == nil || len(dbPathMatches) < 2 {
return ""
}
dbUsername := dbPathMatches[1]
if cacheUsername == dbUsername {
return ""
}
return strings.Replace(gluonCachePath, "/Users/"+cacheUsername+"/", "/Users/"+dbUsername+"/", 1)
}
func (bridge *Bridge) GetFeatureFlagValue(key string) bool {
return bridge.unleashService.GetFlagValue(key)
}
func (bridge *Bridge) PushObservabilityMetric(metric proton.ObservabilityMetric) {
bridge.observabilityService.AddMetrics(metric)
}
func (bridge *Bridge) PushDistinctObservabilityMetrics(errType observability.DistinctionErrorTypeEnum, metrics ...proton.ObservabilityMetric) {
bridge.observabilityService.AddDistinctMetrics(errType, metrics...)
}
func (bridge *Bridge) ModifyObservabilityHeartbeatInterval(duration time.Duration) {
bridge.observabilityService.ModifyHeartbeatInterval(duration)
}

View File

@ -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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *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) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, []byte("bad"), func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, nil, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *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) {
withBridgeWaitForServers(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *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) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
// We should still see 10 messages in the inbox.
info, err := b.GetUserInfo(userID)
require.NoError(t, err)
@ -1077,57 +1077,3 @@ 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)
}
}

View File

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

View File

@ -1,164 +0,0 @@
// Copyright (c) 2024 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge_test
import (
"testing"
"time"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/go-proton-api/server"
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
"github.com/stretchr/testify/require"
"golang.org/x/net/context"
)
func TestBridge_Observability(t *testing.T) {
testMetric := proton.ObservabilityMetric{
Name: "test1",
Version: 1,
Timestamp: time.Now().Unix(),
Data: nil,
}
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
throttlePeriod := time.Millisecond * 500
observability.ModifyThrottlePeriod(throttlePeriod)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
require.NoError(t, getErr(bridge.LoginFull(ctx, username, password, nil, nil)))
bridge.PushObservabilityMetric(testMetric)
time.Sleep(time.Millisecond * 50) // Wait for the metric to be sent
require.Equal(t, 1, len(s.GetObservabilityStatistics().Metrics))
for i := 0; i < 10; i++ {
time.Sleep(time.Millisecond * 5) // Minor delay between each so our tests aren't flaky
bridge.PushObservabilityMetric(testMetric)
}
// We should still have only 1 metric sent as the throttleDuration has not passed
require.Equal(t, 1, len(s.GetObservabilityStatistics().Metrics))
// Wait for throttle duration to pass; we should have our remaining metrics posted
time.Sleep(throttlePeriod)
require.Equal(t, 11, len(s.GetObservabilityStatistics().Metrics))
// Wait for the throttle duration to reset; i.e. so we have enough time to send a request immediately
time.Sleep(throttlePeriod)
for i := 0; i < 10; i++ {
time.Sleep(time.Millisecond * 5)
bridge.PushObservabilityMetric(testMetric)
}
// We should only have one additional metric sent immediately
require.Equal(t, 12, len(s.GetObservabilityStatistics().Metrics))
// Wait for the others to be sent
time.Sleep(throttlePeriod)
require.Equal(t, 21, len(s.GetObservabilityStatistics().Metrics))
// Spam the endpoint a bit
for i := 0; i < 300; i++ {
if i < 200 {
time.Sleep(time.Millisecond * 10)
}
bridge.PushObservabilityMetric(testMetric)
}
// Ensure we've sent all metrics
time.Sleep(throttlePeriod)
observabilityStats := s.GetObservabilityStatistics()
require.Equal(t, 321, len(observabilityStats.Metrics))
// Verify that each request had a throttleDuration time difference between each request
for i := 0; i < len(observabilityStats.RequestTime)-1; i++ {
tOne := observabilityStats.RequestTime[i]
tTwo := observabilityStats.RequestTime[i+1]
require.True(t, tTwo.Sub(tOne).Abs() > throttlePeriod)
}
})
})
}
func TestBridge_Observability_Heartbeat(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
throttlePeriod := time.Millisecond * 300
observability.ModifyThrottlePeriod(throttlePeriod)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
require.NoError(t, getErr(bridge.LoginFull(ctx, username, password, nil, nil)))
bridge.ModifyObservabilityHeartbeatInterval(throttlePeriod)
require.Equal(t, 0, len(s.GetObservabilityStatistics().Metrics))
time.Sleep(time.Millisecond * 150)
require.Equal(t, 0, len(s.GetObservabilityStatistics().Metrics))
time.Sleep(time.Millisecond * 200)
require.Equal(t, 1, len(s.GetObservabilityStatistics().Metrics))
time.Sleep(time.Millisecond * 350)
require.Equal(t, 2, len(s.GetObservabilityStatistics().Metrics))
time.Sleep(time.Millisecond * 350)
require.Equal(t, 3, len(s.GetObservabilityStatistics().Metrics))
})
})
}
func TestBridge_Observability_UserMetric(t *testing.T) {
testMetric := proton.ObservabilityMetric{
Name: "test1",
Version: 1,
Timestamp: time.Now().Unix(),
Data: nil,
}
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
userMetricPeriod := time.Millisecond * 200
heartbeatPeriod := time.Second * 10
throttlePeriod := time.Millisecond * 100
observability.ModifyUserMetricInterval(userMetricPeriod)
observability.ModifyThrottlePeriod(throttlePeriod)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
require.NoError(t, getErr(bridge.LoginFull(ctx, username, password, nil, nil)))
bridge.ModifyObservabilityHeartbeatInterval(heartbeatPeriod)
time.Sleep(throttlePeriod)
require.Equal(t, 0, len(s.GetObservabilityStatistics().Metrics))
bridge.PushDistinctObservabilityMetrics(observability.SyncError, testMetric)
time.Sleep(throttlePeriod)
// We're expecting two observability metrics to be sent, the actual metric + the user metric.
require.Equal(t, 2, len(s.GetObservabilityStatistics().Metrics))
bridge.PushDistinctObservabilityMetrics(observability.SyncError, testMetric)
time.Sleep(throttlePeriod)
// We're expecting only a single metric to be sent, since the user metric update has been sent already within the predefined period.
require.Equal(t, 3, len(s.GetObservabilityStatistics().Metrics))
bridge.PushDistinctObservabilityMetrics(observability.SyncError, testMetric)
time.Sleep(throttlePeriod)
// Two metric updates should be sent again.
require.Equal(t, 5, len(s.GetObservabilityStatistics().Metrics))
bridge.PushDistinctObservabilityMetrics(observability.SyncError, testMetric)
time.Sleep(throttlePeriod)
// Only a single one should be sent.
require.Equal(t, 6, len(s.GetObservabilityStatistics().Metrics))
})
})
}

View File

@ -29,12 +29,13 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
)
func TestBridge_Report(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
defer done()
@ -55,6 +56,12 @@ func TestBridge_Report(t *testing.T) {
require.NoError(t, err)
defer func() { require.NoError(t, conn.Close()) }()
// Sending garbage to the IMAP port should cause the bridge to report it.
mocks.Reporter.EXPECT().ReportMessageWithContext(
gomock.Eq("Failed to parse IMAP command"),
gomock.Any(),
).Return(nil)
// Read lines from the IMAP port.
lineCh := liner.New(conn).Lines(func() error { return nil })

View File

@ -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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
imapClient, err := eventuallyDial(fmt.Sprintf("%v:%v", constants.Host, bridge.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, imapClient.Logout())
@ -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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
_, err := bridge.LoginFull(ctx, username, password, nil, nil)
require.NoError(t, err)
@ -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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
imapWaiter := waitForIMAPServerReady(bridge)
defer imapWaiter.Done()

View File

@ -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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridgeWaitForServers(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
// By default, first start is true.
require.True(t, bridge.GetFirstStart())

View File

@ -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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
_, err := bridge.LoginFull(context.Background(), "imap", password, nil, nil)
require.NoError(t, err)
})

View File

@ -33,8 +33,6 @@ type Locator interface {
GetDependencyLicensesLink() string
Clear(...string) error
ProvideIMAPSyncConfigPath() (string, error)
ProvideUnleashCachePath() (string, error)
ProvideNotificationsCachePath() (string, error)
}
type ProxyController interface {

View File

@ -1,90 +0,0 @@
// Copyright (c) 2024 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge_test
import (
"context"
"testing"
"time"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/go-proton-api/server"
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
"github.com/ProtonMail/proton-bridge/v3/internal/unleash"
"github.com/stretchr/testify/require"
)
func Test_UnleashService(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
unleash.ModifyPollPeriodAndJitter(500*time.Millisecond, 0)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
// Initial startup assumes there is no cached feature flags.
require.Equal(t, b.GetFeatureFlagValue("test-1"), false)
require.Equal(t, b.GetFeatureFlagValue("test-2"), false)
require.Equal(t, b.GetFeatureFlagValue("test-3"), false)
s.PushFeatureFlag("test-1")
s.PushFeatureFlag("test-2")
// Wait for poll.
time.Sleep(time.Millisecond * 700)
require.Equal(t, b.GetFeatureFlagValue("test-1"), true)
require.Equal(t, b.GetFeatureFlagValue("test-2"), true)
require.Equal(t, b.GetFeatureFlagValue("test-3"), false)
s.PushFeatureFlag("test-3")
time.Sleep(time.Millisecond * 700) // Wait for poll again
require.Equal(t, b.GetFeatureFlagValue("test-1"), true)
require.Equal(t, b.GetFeatureFlagValue("test-2"), true)
require.Equal(t, b.GetFeatureFlagValue("test-3"), true)
})
// Wait for Bridge to close.
time.Sleep(time.Millisecond * 500)
// Second instance should have a feature flag cache file available. Therefore, all of the flags should evaluate to true on startup.
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
require.Equal(t, b.GetFeatureFlagValue("test-1"), true)
require.Equal(t, b.GetFeatureFlagValue("test-2"), true)
require.Equal(t, b.GetFeatureFlagValue("test-3"), true)
s.DeleteFeatureFlags()
require.Equal(t, b.GetFeatureFlagValue("test-1"), true)
require.Equal(t, b.GetFeatureFlagValue("test-2"), true)
require.Equal(t, b.GetFeatureFlagValue("test-3"), true)
time.Sleep(time.Millisecond * 700)
require.Equal(t, b.GetFeatureFlagValue("test-1"), false)
require.Equal(t, b.GetFeatureFlagValue("test-2"), false)
require.Equal(t, b.GetFeatureFlagValue("test-3"), false)
s.PushFeatureFlag("test-3")
require.Equal(t, b.GetFeatureFlagValue("test-1"), false)
require.Equal(t, b.GetFeatureFlagValue("test-2"), false)
require.Equal(t, b.GetFeatureFlagValue("test-3"), false)
time.Sleep(time.Millisecond * 700)
require.Equal(t, b.GetFeatureFlagValue("test-1"), false)
require.Equal(t, b.GetFeatureFlagValue("test-2"), false)
require.Equal(t, b.GetFeatureFlagValue("test-3"), true)
})
})
}

View File

@ -21,7 +21,6 @@ import (
"context"
"errors"
"github.com/ProtonMail/gluon/reporter"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
@ -116,17 +115,6 @@ func (bridge *Bridge) installUpdate(ctx context.Context, job installJob) {
err := bridge.updater.InstallUpdate(ctx, bridge.api, job.version)
switch {
case errors.Is(err, updater.ErrDownloadVerify):
// BRIDGE-207: if download or verification fails, we do not want to trigger a manual update. We report in the log and to Sentry
// and we fail silently.
log.WithError(err).Error("The update could not be installed, but we will fail silently")
if reporterErr := bridge.reporter.ReportMessageWithContext(
"Cannot download or verify update",
reporter.Context{"error": err},
); reporterErr != nil {
log.WithError(reporterErr).Error("Failed to report update error")
}
case errors.Is(err, updater.ErrUpdateAlreadyInstalled):
log.Info("The update was already installed")

View File

@ -566,11 +566,8 @@ func (bridge *Bridge) addUserWithVault(
bridge.serverManager,
&bridgeEventSubscription{b: bridge},
bridge.syncService,
bridge.observabilityService,
syncSettingsPath,
isNew,
bridge.notificationStore,
bridge.unleashService.GetFlagValue,
)
if err != nil {
return fmt.Errorf("failed to create user: %w", err)
@ -609,8 +606,6 @@ func (bridge *Bridge) addUserWithVault(
// As we need at least one user to send heartbeat, try to send it.
bridge.heartbeat.start()
user.PublishEvent(ctx, events.UserLoadedCheckResync{UserID: user.ID()})
return nil
}

View File

@ -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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
require.NoError(t, getErr(bridge.LoginFull(ctx, username, password, nil, nil)))
info, err := bridge.QueryUserInfo(username)

View File

@ -36,8 +36,8 @@ func (bridge *Bridge) handleUserEvent(ctx context.Context, user *user.User, even
case events.UserBadEvent:
bridge.handleUserBadEvent(ctx, user, event)
case events.UserLoadedCheckResync:
user.VerifyResyncAndExecute()
case events.UncategorizedEventError:
bridge.handleUncategorizedErrorEvent(event)
}
}
@ -64,3 +64,12 @@ func (bridge *Bridge) handleUserBadEvent(ctx context.Context, user *user.User, e
user.OnBadEvent(ctx)
}, bridge.usersLock)
}
func (bridge *Bridge) handleUncategorizedErrorEvent(event events.UncategorizedEventError) {
if rerr := bridge.reporter.ReportMessageWithContext("Failed to handle due to uncategorized error", reporter.Context{
"error_type": internal.ErrCauseType(event.Error),
"error": event.Error,
}); rerr != nil {
logrus.WithField("pkg", "bridge/event").WithError(rerr).Error("Failed to report failed event handling")
}
}

View File

@ -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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
userID = must(bridge.LoginFull(ctx, username, password, nil, nil))
})
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *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, _ *bridge.Mocks) {
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, mocks *bridge.Mocks) {
// Get a channel of sync started events.
syncStartCh, done := chToType[events.Event, events.SyncStarted](bridge.GetEvents(events.SyncStarted{}))
defer done()

View File

@ -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, _ *http.Request) {
mux.HandleFunc("/set", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
for _, cookie := range wantCookies {
http.SetCookie(w, &http.Cookie{
Name: cookie.name,

View File

@ -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(_ interface{}) error {
return func(r interface{}) error {
notify := notificator.New(notificator.Options{
DefaultIcon: "../frontend/ui/icon/icon.png",
AppName: appName,

View File

@ -31,7 +31,7 @@ import (
func TestTLSReporter_DoubleReport(t *testing.T) {
reportCounter := 0
reportServer := httptest.NewTLSServer(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {
reportServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
reportCounter++
}))

View File

@ -33,7 +33,7 @@ func TestProxyProvider_FindProxy(t *testing.T) {
defer closeServer(proxy)
p := newProxyProvider(NewBasicTLSDialer(""), "", []string{"not used"}, async.NoopPanicHandler{})
p.dohLookup = func(_ context.Context, _, _ string) ([]string, error) { return []string{proxy.URL}, nil }
p.dohLookup = func(ctx context.Context, q, p 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(_ context.Context, _, _ string) ([]string, error) {
p.dohLookup = func(ctx context.Context, q, p 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(_ context.Context, _, _ string) ([]string, error) {
p.dohLookup = func(ctx context.Context, q, p 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(_ context.Context, _, _ string) ([]string, error) {
p.dohLookup = func(ctx context.Context, q, p 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(_ context.Context, _, _ string) ([]string, error) {
p.dohLookup = func(ctx context.Context, q, p 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(_ context.Context, _, _ string) ([]string, error) { time.Sleep(2 * time.Second); return nil, nil }
p.dohLookup = func(ctx context.Context, q, p 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(_ context.Context, _, _ string) ([]string, error) { return []string{slowProxy.URL}, nil }
p.dohLookup = func(ctx context.Context, q, p 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.

View File

@ -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(_ http.ResponseWriter, _ *http.Request) {}))
server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *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(_ context.Context, _, _ string) ([]string, error) { return []string{trustedProxy.URL}, nil }
provider.dohLookup = func(ctx context.Context, q, p 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(_ context.Context, _, _ string) ([]string, error) { return []string{proxy1.URL}, nil }
provider.dohLookup = func(ctx context.Context, q, p 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(_ context.Context, _, _ string) ([]string, error) { return []string{proxy2.URL}, nil }
provider.dohLookup = func(ctx context.Context, q, p 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(_ context.Context, _, _ string) ([]string, error) { return []string{proxy3.URL}, nil }
provider.dohLookup = func(ctx context.Context, q, p 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(_ context.Context, _, _ string) ([]string, error) { return []string{trustedProxy.URL}, nil }
provider.dohLookup = func(ctx context.Context, q, p 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(_ context.Context, _, _ string) ([]string, error) { return []string{trustedProxy.URL}, nil }
provider.dohLookup = func(ctx context.Context, q, p 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(_ context.Context, _, _ string) ([]string, error) { return []string{proxy1.URL, proxy2.URL}, nil }
provider.dohLookup = func(ctx context.Context, q, p string) ([]string, error) { return []string{proxy1.URL, proxy2.URL}, nil }
err := d.switchToReachableServer()
require.NoError(t, err)

View File

@ -202,26 +202,3 @@ type UncategorizedEventError struct {
func (event UncategorizedEventError) String() string {
return fmt.Sprintf("UncategorizedEventError: UserID: %s, Source:%T, Error: %s", event.UserID, event.Error, event.Error)
}
type UserLoadedCheckResync struct {
eventBase
UserID string
}
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)
}

View File

@ -57,7 +57,6 @@ 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());
@ -217,7 +216,6 @@ 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());
@ -491,41 +489,3 @@ 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()));
}
}

View File

@ -23,6 +23,7 @@
#include "Tabs/ui_UsersTab.h"
#include "UserTable.h"
//****************************************************************************************************************************************************
/// \brief The 'Users' tab of the main window.
//****************************************************************************************************************************************************
@ -49,15 +50,12 @@ 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:
@ -71,7 +69,6 @@ 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.

View File

@ -7,19 +7,13 @@
<x>0</x>
<y>0</y>
<width>1221</width>
<height>408</height>
<height>894</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="0,0">
<layout class="QHBoxLayout" name="horizontalLayout" stretch="1,0">
<item>
<widget class="QTableView" name="tableUserList">
<property name="selectionMode">
@ -36,27 +30,6 @@
</attribute>
</widget>
</item>
<item>
<widget class="QScrollArea" name="scrollArea">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="scrollAreaWidgetContents">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>327</width>
<height>905</height>
</rect>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_6">
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
@ -93,68 +66,6 @@
</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">
@ -329,7 +240,7 @@
<property name="minimumSize">
<size>
<width>0</width>
<height>250</height>
<height>100</height>
</size>
</property>
<property name="title">
@ -449,10 +360,6 @@
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>buttonNewUser</tabstop>
<tabstop>buttonEditUser</tabstop>

View File

@ -75,7 +75,7 @@ if(NOT UNIX)
set(CMAKE_INSTALL_BINDIR ".")
endif(NOT UNIX)
find_package(Qt6 COMPONENTS Core Quick Qml QuickControls2 Widgets Svg Gui REQUIRED)
find_package(Qt6 COMPONENTS Core Quick Qml QuickControls2 Widgets Svg REQUIRED)
qt_standard_project_setup()
set(CMAKE_AUTORCC ON)
message(STATUS "Using Qt ${Qt6_VERSION}")
@ -120,7 +120,6 @@ 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
)
@ -149,7 +148,6 @@ target_link_libraries(bridge-gui
Qt6::Qml
Qt6::QuickControls2
Qt6::Svg
Qt6::Gui
sentry::sentry
bridgepp
)

View File

@ -1,25 +0,0 @@
// Copyright (c) 2024 Proton AG
// This file is part of Proton Mail Bridge.
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
#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();
}

View File

@ -1,38 +0,0 @@
// Copyright (c) 2024 Proton AG
// This file is part of Proton Mail Bridge.
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
#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

View File

@ -26,7 +26,6 @@
#include <QtWidgets>
#include <QtQuickControls2>
#include <QtSvg>
#include <QtGui>
#include <AppController.h>

View File

@ -1328,9 +1328,6 @@ void QMLBackend::connectGrpcEvents() {
connect(client, &GRPCClient::certificateInstallFailed, this, &QMLBackend::certificateInstallFailed);
connect(client, &GRPCClient::showMainWindow, [&]() { this->showMainWindow("gRPC showMainWindow event"); });
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);
@ -1413,31 +1410,3 @@ void QMLBackend::displayBadEventDialog(QString const &userID) {
emit showMainWindow();
)
}
void QMLBackend::triggerRepair() const {
HANDLE_EXCEPTION(
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);
}

View File

@ -28,7 +28,6 @@
#include <bridgepp/GRPC/GRPCClient.h>
#include <bridgepp/GRPC/GRPCUtils.h>
#include <bridgepp/Worker/Overseer.h>
#include <stack>
//****************************************************************************************************************************************************
@ -175,8 +174,6 @@ 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.
@ -211,8 +208,6 @@ public slots: // slot for signals received from QML -> To be forwarded to Bridge
void notifyReportBugClicked() const; ///< Slot for the ReportBugClicked gRPC event.
void notifyAutoconfigClicked(QString const &client) const; ///< Slot for gAutoconfigClicked gRPC event.
void notifyExternalLinkClicked(QString const &article) const; ///< Slot for KBArticleClicked gRPC event.
void triggerRepair() const; ///< Slot for the triggering of the bridge repair function i.e. 'resync'.
void userNotificationDismissed(); ///< Slot to pop the notification from the stack and display the rest.
public slots: // slots for functions that need to be processed locally.
void setNormalTrayIcon(); ///< Set the tray icon to normal.
@ -228,7 +223,6 @@ 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.
@ -288,9 +282,7 @@ signals: // Signals received from the Go backend, to be forwarded to QML
void selectUser(QString const& userID, bool forceShowWindow); ///< Signal emitted in order to selected a user with a given ID in the list.
void genericError(QString const &title, QString const &description); ///< Signal for the 'genericError' gRPC stream event.
void imapLoginWhileSignedOut(QString const& username); ///< Signal for the notification of IMAP login attempt on a signed out account.
void receivedKnowledgeBaseSuggestions(QList<bridgepp::KnowledgeBaseSuggestion> const& suggestions); ///< Signal for the reception of knowledge base article suggestions.
void repairStarted(); ///< Signal for the 'repairStarted' gRPC stream event.
void allUsersLoaded(); ///< Signal for the 'allUsersLoaded' gRPC stream event
void receivedKnowledgeBaseSuggestions(QList<bridgepp::KnowledgeBaseSuggestion> const& suggestions); ///< Signal for the reception of knowledgebase article suggestions.
// This signal is emitted when an exception is intercepted is calls triggered by QML. QML engine would intercept the exception otherwise.
void fatalError(bridgepp::Exception const& e) const; ///< Signal emitted when an fatal error occurs.
@ -315,7 +307,6 @@ 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;
};

View File

@ -71,7 +71,6 @@
<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>
@ -79,7 +78,6 @@
<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>
@ -134,6 +132,5 @@
<file>qml/ConnectionModeSettings.qml</file>
<file>qml/SplashScreen.qml</file>
<file>qml/Status.qml</file>
<file>qml/Proton/ContextMenu.qml</file>
</qresource>
</RCC>

View File

@ -331,15 +331,6 @@ 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.

View File

@ -42,8 +42,6 @@ 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

View File

@ -28,7 +28,6 @@
#include <bridgepp/Log/Log.h>
#include <bridgepp/Log/LogUtils.h>
#include <bridgepp/ProcessMonitor.h>
#include <ClipboardProxy.h>
#include "bridgepp/CLI/CLIUtils.h"
@ -54,7 +53,6 @@ QString const bridgeGUILock = "bridge-v3-gui.lock"; ///< The file name used for
QString const exeName = "bridge" + exeSuffix; ///< The bridge executable file name.*
qint64 constexpr grpcServiceConfigWaitDelayMs = 180000; ///< The wait delay for the gRPC config file in milliseconds.
QString const waitFlag = "--wait"; ///< The wait command-line flag.
QString const orphanInstanceException = "An orphan instance of bridge is already running. Please terminate it and relaunch the application.";
} // anonymous namespace
@ -318,7 +316,7 @@ int main(int argc, char *argv[]) {
QString bridgeExe;
if (!cliOptions.attach) {
if (isBridgeRunning()) {
throw Exception(orphanInstanceException,
throw Exception("An orphan instance of bridge is already running. Please terminate it and relaunch the application.",
QString(), __FUNCTION__, tailOfLatestBridgeLog(sessionID));
}
@ -349,8 +347,6 @@ 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) {
@ -414,18 +410,13 @@ int main(int argc, char *argv[]) {
lock.unlock();
return result;
} catch (Exception const &e) {
sentry_uuid_s const uuid = reportSentryException("Exception occurred during main", e);
QString message = e.qwhat();
if (e.showSupportLink()) {
message += R"(<br/><br/>If the issue persists, please contact our <a href="https://proton.me/support/contact">customer support</a>.)";
}
QMessageBox::critical(nullptr, "Error", message);
if (e.qwhat() != orphanInstanceException) {
sentry_uuid_s const uuid = reportSentryException("Exception occurred during main", e);
QTextStream(stderr) << "reportID: " << QByteArray(uuid.bytes, 16).toHex() << " Captured exception :"
<< e.detailedWhat() << "\n";
}
QTextStream(stderr) << "reportID: " << QByteArray(uuid.bytes, 16).toHex() << " Captured exception :" << e.detailedWhat() << "\n";
return EXIT_FAILURE;
}
}

View File

@ -21,8 +21,6 @@ SettingsView {
property bool _isAdvancedShown: false
property var notifications
property var allUsersLoaded: false
property var hasInternetConnection: true
fillHeight: false
@ -221,37 +219,6 @@ SettingsView {
Backend.exportTLSCertificates();
}
}
SettingsItem {
id: repair
Layout.fillWidth: true
actionText: qsTr("Repair")
colorScheme: root.colorScheme
description: qsTr("Reload all accounts, cached data, and download all emails again. Email clients stay connected to Bridge.")
text: qsTr("Repair Bridge")
type: SettingsItem.Button
visible: root._isAdvancedShown
enabled: root.allUsersLoaded && Backend.users.count && root.hasInternetConnection
onClicked: {
root.notifications.askRepairBridge();
}
Connections {
function onInternetOff() {
root.hasInternetConnection = false;
repair.description = qsTr("This feature requires internet access to the Proton servers.")
}
function onInternetOn() {
root.hasInternetConnection = true;
repair.description = qsTr("Reload all accounts, cached data, and download all emails again. Email clients stay connected to Bridge.")
}
function onAllUsersLoaded() {
root.allUsersLoaded = true;
}
target: Backend
}
}
SettingsItem {
id: reset
Layout.fillWidth: true

View File

@ -22,7 +22,6 @@ Dialog {
default property alias data: additionalChildrenContainer.children
property var notification
property bool isUserNotification: false
modal: true
shouldShow: notification && notification.active && !notification.dismissed

View File

@ -105,12 +105,4 @@ Item {
colorScheme: root.colorScheme
notification: root.notifications.genericQuestion
}
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.repairBridge
}
UserNotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.userNotification
}
}

View File

@ -19,8 +19,7 @@ QtObject {
Info,
Success,
Warning,
Danger,
UserNotification
Danger
}
property list<Action> action
@ -37,9 +36,6 @@ 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;

View File

@ -13,8 +13,6 @@
import QtQml
import Qt.labs.platform
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick
import "../"
QtObject {
@ -62,7 +60,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, root.userNotification]
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]
property Notification alreadyLoggedIn: Notification {
brief: qsTr("Already signed in")
description: qsTr("This account is already signed in.")
@ -1153,81 +1151,6 @@ QtObject {
}
}
property Notification repairBridge: Notification {
brief: title
description: qsTr("This action will reload all accounts, cached data, and re-download emails. Messages may temporarily disappear but will reappear progressively. Email clients stay connected to Bridge.")
group: Notifications.Group.Configuration | Notifications.Group.Dialogs
icon: "./icons/ic-exclamation-circle-filled.svg"
title: qsTr("Repair Bridge?")
type: Notification.NotificationType.Danger
action: [
Action {
id: repairBridge_cancel
text: qsTr("Cancel")
onTriggered: {
root.repairBridge.active = false;
}
},
Action {
id: repairBridge_repair
text: qsTr("Repair")
onTriggered: {
repairBridge_repair.loading = true;
repairBridge_repair.enabled = false;
repairBridge_cancel.enabled = false;
Backend.triggerRepair();
}
}
]
Connections {
function onAskRepairBridge() {
root.repairBridge.active = true;
}
target: root
}
Connections {
function onRepairStarted() {
root.repairBridge.active = false;
repairBridge_repair.loading = false;
repairBridge_repair.enabled = true;
repairBridge_cancel.enabled = true;
}
target: Backend
}
}
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)
@ -1235,5 +1158,4 @@ QtObject {
signal askEnableSplitMode(var user)
signal askQuestion(var title, var description, var option1, var option2, var action1, var action2)
signal askResetBridge
signal askRepairBridge
}

View File

@ -73,11 +73,6 @@ 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;
}

View File

@ -1,79 +0,0 @@
// Copyright (c) 2024 Proton AG
// This file is part of Proton Mail Bridge.
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
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()
}
}
}
}

View File

@ -362,9 +362,4 @@ FocusScope {
}
}
}
Proton.ContextMenu {
parentObject: root
colorScheme: root.colorScheme
}
}

View File

@ -331,15 +331,6 @@ 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

View File

@ -39,4 +39,3 @@ 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

View File

@ -1,120 +0,0 @@
// Copyright (c) 2024 Proton AG
// This file is part of Proton Mail Bridge.
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
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
}
}
}
}
}

View File

@ -1,37 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -729,23 +729,4 @@ 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

View File

@ -94,9 +94,6 @@ 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

View File

@ -1198,25 +1198,6 @@ void GRPCClient::processAppEvent(AppEvent const &event) {
emit knowledgeBasSuggestionsReceived(suggestions);
break;
}
case AppEvent::kRepairStarted:
this->logTrace("App event received: RepairStarted.");
emit repairStarted();
break;
case AppEvent::kAllUsersLoaded:
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.");
}
@ -1599,12 +1580,5 @@ grpc::Status GRPCClient::externalLinkClicked(QString const &link) {
return this->logGRPCCallStatus(stub_->ExternalLinkClicked(this->clientContext().get(), s, &empty), __FUNCTION__);
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
grpc::Status GRPCClient::triggerRepair() {
return this->logGRPCCallStatus(stub_->TriggerRepair(this->clientContext().get(), empty, &empty), __FUNCTION__ );
}
} // namespace bridgepp

View File

@ -56,24 +56,6 @@ 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.
//****************************************************************************************************************************************************
@ -126,7 +108,6 @@ public: // member functions.
grpc::Status landingPageLink(QUrl &outUrl); ///< Performs the 'landingPageLink' call.
grpc::Status hostname(QString &outHostname); ///< Performs the 'Hostname' call.
grpc::Status requestKnowledgeBaseSuggestions(QString const &input); ///< Performs the 'RequestKnowledgeBaseSuggestions' call.
grpc::Status triggerRepair(); ///< Performs the triggerRepair gRPC call.
signals: // app related signals
void internetStatus(bool isOn);
@ -141,9 +122,6 @@ signals: // app related signals
void certificateInstallFailed();
void showMainWindow();
void knowledgeBasSuggestionsReceived(QList<KnowledgeBaseSuggestion> const& suggestions);
void repairStarted();
void allUsersLoaded();
void userNotificationReceived(UserNotification const& notification);
public: // cache related calls

View File

@ -20,7 +20,6 @@ package cli
import (
"errors"
"fmt"
"os"
"runtime"
@ -307,12 +306,6 @@ func New(
Aliases: []string{"del", "rm", "remove"},
Completer: fe.completeUsernames,
})
fe.AddCmd(&ishell.Cmd{
Name: "repair",
Help: "reload all accounts and cached data, re-download emails. Email clients remain connected. Logged out users will be repaired on next login. (aliases: rep)",
Func: fe.repair,
Aliases: []string{"rep"},
})
badEventCmd := &ishell.Cmd{
Name: "bad-event",
@ -501,18 +494,6 @@ 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)
}
}

View File

@ -359,13 +359,3 @@ func (f *frontendCLI) isFile(location string) bool {
return !stat.IsDir()
}
func (f *frontendCLI) repair(_ *ishell.Context) {
if f.bridge.HasAPIConnection() {
if f.yesNoQuestion("Are you sure you want to initialize a repair, this may take a while") {
f.bridge.Repair()
}
} else {
f.Println("Bridge cannot connect to the Proton servers. A connection is required to utilize this feature.")
}
}

File diff suppressed because it is too large Load Diff

View File

@ -111,9 +111,6 @@ service Bridge {
// Server -> Client event stream
rpc RunEventStream(EventStreamRequest) returns (stream StreamEvent); // Keep streaming until StopEventStream is called.
rpc StopEventStream(google.protobuf.Empty) returns (google.protobuf.Empty);
// Repair
rpc TriggerRepair(google.protobuf.Empty) returns (google.protobuf.Empty);
}
//**********************************************************************************************************************
@ -275,9 +272,6 @@ message AppEvent {
CertificateInstallCanceledEvent certificateInstallCanceled = 10;
CertificateInstallFailedEvent certificateInstallFailed = 11;
KnowledgeBaseSuggestionsEvent knowledgeBaseSuggestions = 12;
RepairStartedEvent repairStarted = 13;
AllUsersLoadedEvent allUsersLoaded = 14;
UserNotificationEvent userNotification = 15;
}
}
@ -295,8 +289,6 @@ message ReportBugFallbackEvent {}
message CertificateInstallSuccessEvent {}
message CertificateInstallCanceledEvent {}
message CertificateInstallFailedEvent {}
message RepairStartedEvent {}
message AllUsersLoadedEvent {}
message KnowledgeBaseSuggestion {
string url = 1;
@ -546,14 +538,6 @@ message SyncProgressEvent {
int64 remainingMs = 4;
}
message UserNotificationEvent {
string title = 1;
string subtitle = 2;
string body = 3;
string userID = 4;
}
//**********************************************************
// Generic errors
//**********************************************************

View File

@ -101,7 +101,6 @@ const (
Bridge_ExportTLSCertificates_FullMethodName = "/grpc.Bridge/ExportTLSCertificates"
Bridge_RunEventStream_FullMethodName = "/grpc.Bridge/RunEventStream"
Bridge_StopEventStream_FullMethodName = "/grpc.Bridge/StopEventStream"
Bridge_TriggerRepair_FullMethodName = "/grpc.Bridge/TriggerRepair"
)
// BridgeClient is the client API for Bridge service.
@ -181,8 +180,6 @@ type BridgeClient interface {
// Server -> Client event stream
RunEventStream(ctx context.Context, in *EventStreamRequest, opts ...grpc.CallOption) (Bridge_RunEventStreamClient, error)
StopEventStream(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
// Repair
TriggerRepair(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
}
type bridgeClient struct {
@ -783,15 +780,6 @@ func (c *bridgeClient) StopEventStream(ctx context.Context, in *emptypb.Empty, o
return out, nil
}
func (c *bridgeClient) TriggerRepair(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) {
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, Bridge_TriggerRepair_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// BridgeServer is the server API for Bridge service.
// All implementations must embed UnimplementedBridgeServer
// for forward compatibility
@ -869,8 +857,6 @@ type BridgeServer interface {
// Server -> Client event stream
RunEventStream(*EventStreamRequest, Bridge_RunEventStreamServer) error
StopEventStream(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
// Repair
TriggerRepair(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
mustEmbedUnimplementedBridgeServer()
}
@ -1067,9 +1053,6 @@ func (UnimplementedBridgeServer) RunEventStream(*EventStreamRequest, Bridge_RunE
func (UnimplementedBridgeServer) StopEventStream(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method StopEventStream not implemented")
}
func (UnimplementedBridgeServer) TriggerRepair(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method TriggerRepair not implemented")
}
func (UnimplementedBridgeServer) mustEmbedUnimplementedBridgeServer() {}
// UnsafeBridgeServer may be embedded to opt out of forward compatibility for this service.
@ -2220,24 +2203,6 @@ func _Bridge_StopEventStream_Handler(srv interface{}, ctx context.Context, dec f
return interceptor(ctx, in, info, handler)
}
func _Bridge_TriggerRepair_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(emptypb.Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(BridgeServer).TriggerRepair(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Bridge_TriggerRepair_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(BridgeServer).TriggerRepair(ctx, req.(*emptypb.Empty))
}
return interceptor(ctx, in, info, handler)
}
// Bridge_ServiceDesc is the grpc.ServiceDesc for Bridge service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@ -2493,10 +2458,6 @@ var Bridge_ServiceDesc = grpc.ServiceDesc{
MethodName: "StopEventStream",
Handler: _Bridge_StopEventStream_Handler,
},
{
MethodName: "TriggerRepair",
Handler: _Bridge_TriggerRepair_Handler,
},
},
Streams: []grpc.StreamDesc{
{

View File

@ -18,7 +18,6 @@
package grpc
import (
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/kb"
"github.com/bradenaw/juniper/xslices"
)
@ -242,24 +241,6 @@ func NewGenericErrorEvent(errorCode ErrorCode) *StreamEvent {
return genericErrorEvent(&GenericErrorEvent{Code: errorCode})
}
func NewRepairStartedEvent() *StreamEvent {
return appEvent(&AppEvent{Event: &AppEvent_RepairStarted{RepairStarted: &RepairStartedEvent{}}})
}
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 {

View File

@ -401,12 +401,6 @@ func (s *Service) watchEvents() {
case events.TLSIssue:
_ = s.SendEvent(NewMailApiCertIssue())
case events.AllUsersLoaded:
_ = s.SendEvent(NewAllUsersLoadedEvent())
case events.UserNotification:
_ = s.SendEvent(NewUserNotificationEvent(event))
}
}
}
@ -584,7 +578,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{}, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
if err := validateServerToken(ctx, wantToken); err != nil {
return nil, err
}
@ -595,7 +589,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, _ *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
return func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
if err := validateServerToken(stream.Context(), wantToken); err != nil {
return err
}
@ -629,7 +623,9 @@ func (s *Service) monitorParentPID() {
go func() {
defer async.HandlePanic(s.panicHandler)
s.quit()
if err := s.quit(); err != nil {
logrus.WithError(err).Error("Error on quit")
}
}()
}

View File

@ -30,8 +30,6 @@ import (
)
func (s *Service) IsTLSCertificateInstalled(context.Context, *emptypb.Empty) (*wrapperspb.BoolValue, error) {
defer async.HandlePanic(s.panicHandler)
s.log.Info("IsTLSCertificateInstalled")
cert, _ := s.bridge.GetBridgeTLSCert()

View File

@ -45,7 +45,6 @@ import (
// CheckTokens implements the CheckToken gRPC service call.
func (s *Service) CheckTokens(_ context.Context, clientConfigPath *wrapperspb.StringValue) (*wrapperspb.StringValue, error) {
defer async.HandlePanic(s.panicHandler)
s.log.Debug("CheckTokens")
path := clientConfigPath.Value
@ -64,7 +63,6 @@ func (s *Service) CheckTokens(_ context.Context, clientConfigPath *wrapperspb.St
}
func (s *Service) AddLogEntry(_ context.Context, request *AddLogEntryRequest) (*emptypb.Empty, error) {
defer async.HandlePanic(s.panicHandler)
entry := s.log
if len(request.Package) > 0 {
@ -93,7 +91,6 @@ func (s *Service) AddLogEntry(_ context.Context, request *AddLogEntryRequest) (*
// GuiReady implement the GuiReady gRPC service call.
func (s *Service) GuiReady(_ context.Context, _ *emptypb.Empty) (*GuiReadyResponse, error) {
defer async.HandlePanic(s.panicHandler)
s.log.Debug("GuiReady")
s.initializationDone.Do(s.initializing.Done)
@ -108,13 +105,11 @@ func (s *Service) GuiReady(_ context.Context, _ *emptypb.Empty) (*GuiReadyRespon
// Quit implement the Quit gRPC service call.
func (s *Service) Quit(_ context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) {
defer async.HandlePanic(s.panicHandler)
s.log.Debug("Quit")
s.quit()
return &emptypb.Empty{}, nil
return &emptypb.Empty{}, s.quit()
}
func (s *Service) quit() {
func (s *Service) quit() error {
// 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)
@ -133,11 +128,12 @@ func (s *Service) quit() {
// 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.
func (s *Service) Restart(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) {
defer async.HandlePanic(s.panicHandler)
s.log.Debug("Restart")
s.restarter.Set(true, false)
@ -145,14 +141,12 @@ func (s *Service) Restart(ctx context.Context, empty *emptypb.Empty) (*emptypb.E
}
func (s *Service) ShowOnStartup(_ context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) {
defer async.HandlePanic(s.panicHandler)
s.log.Debug("ShowOnStartup")
return wrapperspb.Bool(s.showOnStartup), nil
}
func (s *Service) SetIsAutostartOn(_ context.Context, isOn *wrapperspb.BoolValue) (*emptypb.Empty, error) {
defer async.HandlePanic(s.panicHandler)
s.log.WithField("show", isOn.Value).Debug("SetIsAutostartOn")
defer func() { _ = s.SendEvent(NewToggleAutostartFinishedEvent()) }()
@ -173,14 +167,12 @@ func (s *Service) SetIsAutostartOn(_ context.Context, isOn *wrapperspb.BoolValue
}
func (s *Service) IsAutostartOn(_ context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) {
defer async.HandlePanic(s.panicHandler)
s.log.Debug("IsAutostartOn")
return wrapperspb.Bool(s.bridge.GetAutostart()), nil
}
func (s *Service) SetIsBetaEnabled(_ context.Context, isEnabled *wrapperspb.BoolValue) (*emptypb.Empty, error) {
defer async.HandlePanic(s.panicHandler)
s.log.WithField("isEnabled", isEnabled.Value).Debug("SetIsBetaEnabled")
channel := updater.StableChannel
@ -197,14 +189,12 @@ func (s *Service) SetIsBetaEnabled(_ context.Context, isEnabled *wrapperspb.Bool
}
func (s *Service) IsBetaEnabled(_ context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) {
defer async.HandlePanic(s.panicHandler)
s.log.Debug("IsBetaEnabled")
return wrapperspb.Bool(s.bridge.GetUpdateChannel() == updater.EarlyChannel), nil
}
func (s *Service) SetIsAllMailVisible(_ context.Context, isVisible *wrapperspb.BoolValue) (*emptypb.Empty, error) {
defer async.HandlePanic(s.panicHandler)
s.log.WithField("isVisible", isVisible.Value).Debug("SetIsAllMailVisible")
if err := s.bridge.SetShowAllMail(isVisible.Value); err != nil {
@ -216,14 +206,12 @@ func (s *Service) SetIsAllMailVisible(_ context.Context, isVisible *wrapperspb.B
}
func (s *Service) IsAllMailVisible(_ context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) {
defer async.HandlePanic(s.panicHandler)
s.log.Debug("IsAllMailVisible")
return wrapperspb.Bool(s.bridge.GetShowAllMail()), nil
}
func (s *Service) SetIsTelemetryDisabled(_ context.Context, isDisabled *wrapperspb.BoolValue) (*emptypb.Empty, error) {
defer async.HandlePanic(s.panicHandler)
s.log.WithField("isEnabled", isDisabled.Value).Debug("SetIsTelemetryDisabled")
if err := s.bridge.SetTelemetryDisabled(isDisabled.Value); err != nil {
@ -235,14 +223,12 @@ func (s *Service) SetIsTelemetryDisabled(_ context.Context, isDisabled *wrappers
}
func (s *Service) IsTelemetryDisabled(_ context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) {
defer async.HandlePanic(s.panicHandler)
s.log.Debug("IsTelemetryDisabled")
return wrapperspb.Bool(s.bridge.GetTelemetryDisabled()), nil
}
func (s *Service) GoOs(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
defer async.HandlePanic(s.panicHandler)
s.log.Debug("GoOs") // TO-DO We can probably get rid of this and use QSysInfo::product name
return wrapperspb.String(runtime.GOOS), nil
@ -260,14 +246,12 @@ func (s *Service) TriggerReset(_ context.Context, _ *emptypb.Empty) (*emptypb.Em
}
func (s *Service) Version(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
defer async.HandlePanic(s.panicHandler)
s.log.Debug("Version")
return wrapperspb.String(s.bridge.GetCurrentVersion().Original()), nil
}
func (s *Service) LogsPath(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
defer async.HandlePanic(s.panicHandler)
s.log.Debug("LogsPath")
path, err := s.bridge.GetLogsPath()
@ -279,40 +263,30 @@ func (s *Service) LogsPath(_ context.Context, _ *emptypb.Empty) (*wrapperspb.Str
}
func (s *Service) LicensePath(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
defer async.HandlePanic(s.panicHandler)
s.log.Debug("LicensePath")
return wrapperspb.String(s.bridge.GetLicenseFilePath()), nil
}
func (s *Service) DependencyLicensesLink(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
defer async.HandlePanic(s.panicHandler)
return wrapperspb.String(s.bridge.GetDependencyLicensesLink()), nil
}
func (s *Service) ReleaseNotesPageLink(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
s.latestLock.RLock()
defer func() {
async.HandlePanic(s.panicHandler)
s.latestLock.RUnlock()
}()
defer s.latestLock.RUnlock()
return wrapperspb.String(s.latest.ReleaseNotesPage), nil
}
func (s *Service) LandingPageLink(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
s.latestLock.RLock()
defer func() {
async.HandlePanic(s.panicHandler)
s.latestLock.RUnlock()
}()
defer s.latestLock.RUnlock()
return wrapperspb.String(s.latest.LandingPage), nil
}
func (s *Service) SetColorSchemeName(_ context.Context, name *wrapperspb.StringValue) (*emptypb.Empty, error) {
defer async.HandlePanic(s.panicHandler)
s.log.WithField("ColorSchemeName", name.Value).Debug("SetColorSchemeName")
if !theme.IsAvailable(theme.Theme(name.Value)) {
@ -329,8 +303,6 @@ func (s *Service) SetColorSchemeName(_ context.Context, name *wrapperspb.StringV
}
func (s *Service) ColorSchemeName(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
defer async.HandlePanic(s.panicHandler)
s.log.Debug("ColorSchemeName")
current := s.bridge.GetColorScheme()
@ -346,8 +318,6 @@ func (s *Service) ColorSchemeName(_ context.Context, _ *emptypb.Empty) (*wrapper
}
func (s *Service) CurrentEmailClient(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
defer async.HandlePanic(s.panicHandler)
s.log.Debug("CurrentEmailClient")
return wrapperspb.String(s.bridge.GetCurrentUserAgent()), nil
@ -391,8 +361,6 @@ func (s *Service) ReportBug(_ context.Context, report *ReportBugRequest) (*empty
}
func (s *Service) ForceLauncher(_ context.Context, launcher *wrapperspb.StringValue) (*emptypb.Empty, error) {
defer async.HandlePanic(s.panicHandler)
s.log.WithField("launcher", launcher.Value).Debug("ForceLauncher")
s.restarter.Override(launcher.Value)
@ -401,8 +369,6 @@ func (s *Service) ForceLauncher(_ context.Context, launcher *wrapperspb.StringVa
}
func (s *Service) SetMainExecutable(_ context.Context, exe *wrapperspb.StringValue) (*emptypb.Empty, error) {
defer async.HandlePanic(s.panicHandler)
s.log.WithField("executable", exe.Value).Debug("SetMainExecutable")
s.restarter.AddFlags("--wait", exe.Value)
@ -624,8 +590,6 @@ func (s *Service) InstallUpdate(_ context.Context, _ *emptypb.Empty) (*emptypb.E
}
func (s *Service) SetIsAutomaticUpdateOn(_ context.Context, isOn *wrapperspb.BoolValue) (*emptypb.Empty, error) {
defer async.HandlePanic(s.panicHandler)
s.log.WithField("isOn", isOn.Value).Debug("SetIsAutomaticUpdateOn")
if currentlyOn := s.bridge.GetAutoUpdate(); currentlyOn == isOn.Value {
@ -641,16 +605,12 @@ func (s *Service) SetIsAutomaticUpdateOn(_ context.Context, isOn *wrapperspb.Boo
}
func (s *Service) IsAutomaticUpdateOn(_ context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) {
defer async.HandlePanic(s.panicHandler)
s.log.Debug("IsAutomaticUpdateOn")
return wrapperspb.Bool(s.bridge.GetAutoUpdate()), nil
}
func (s *Service) DiskCachePath(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
defer async.HandlePanic(s.panicHandler)
s.log.Debug("DiskCachePath")
return wrapperspb.String(s.bridge.GetGluonCacheDir()), nil
@ -688,8 +648,6 @@ func (s *Service) SetDiskCachePath(_ context.Context, newPath *wrapperspb.String
}
func (s *Service) SetIsDoHEnabled(_ context.Context, isEnabled *wrapperspb.BoolValue) (*emptypb.Empty, error) {
defer async.HandlePanic(s.panicHandler)
s.log.WithField("isEnabled", isEnabled.Value).Debug("SetIsDohEnabled")
if err := s.bridge.SetProxyAllowed(isEnabled.Value); err != nil {
@ -701,16 +659,12 @@ func (s *Service) SetIsDoHEnabled(_ context.Context, isEnabled *wrapperspb.BoolV
}
func (s *Service) IsDoHEnabled(_ context.Context, _ *emptypb.Empty) (*wrapperspb.BoolValue, error) {
defer async.HandlePanic(s.panicHandler)
s.log.Debug("IsDohEnabled")
return wrapperspb.Bool(s.bridge.GetProxyAllowed()), nil
}
func (s *Service) MailServerSettings(_ context.Context, _ *emptypb.Empty) (*ImapSmtpSettings, error) {
defer async.HandlePanic(s.panicHandler)
s.log.Debug("ConnectionMode")
return &ImapSmtpSettings{
@ -774,32 +728,24 @@ func (s *Service) SetMailServerSettings(_ context.Context, settings *ImapSmtpSet
}
func (s *Service) Hostname(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
defer async.HandlePanic(s.panicHandler)
s.log.Debug("Hostname")
return wrapperspb.String(constants.Host), nil
}
func (s *Service) IsPortFree(_ context.Context, port *wrapperspb.Int32Value) (*wrapperspb.BoolValue, error) {
defer async.HandlePanic(s.panicHandler)
s.log.Debug("IsPortFree")
return wrapperspb.Bool(ports.IsPortFree(int(port.Value))), nil
}
func (s *Service) AvailableKeychains(_ context.Context, _ *emptypb.Empty) (*AvailableKeychainsResponse, error) {
defer async.HandlePanic(s.panicHandler)
s.log.Debug("AvailableKeychains")
return &AvailableKeychainsResponse{Keychains: s.bridge.GetHelpersNames()}, nil
}
func (s *Service) SetCurrentKeychain(ctx context.Context, keychain *wrapperspb.StringValue) (*emptypb.Empty, error) {
defer async.HandlePanic(s.panicHandler)
s.log.WithField("keychain", keychain.Value).Debug("SetCurrentKeyChain") // we do not check validity.
defer func() { _, _ = s.Restart(ctx, &emptypb.Empty{}) }()
@ -824,8 +770,6 @@ func (s *Service) SetCurrentKeychain(ctx context.Context, keychain *wrapperspb.S
}
func (s *Service) CurrentKeychain(_ context.Context, _ *emptypb.Empty) (*wrapperspb.StringValue, error) {
defer async.HandlePanic(s.panicHandler)
s.log.Debug("CurrentKeychain")
helper, err := s.bridge.GetKeychainApp()
@ -837,20 +781,6 @@ func (s *Service) CurrentKeychain(_ context.Context, _ *emptypb.Empty) (*wrapper
return wrapperspb.String(helper), nil
}
func (s *Service) TriggerRepair(_ context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) {
s.log.Debug("TriggerRepair")
go func() {
defer func() {
async.HandlePanic(s.panicHandler)
_ = s.SendEvent(NewRepairStartedEvent())
}()
s.bridge.Repair()
}()
return &emptypb.Empty{}, nil
}
func base64Decode(in []byte) ([]byte, error) {
out := make([]byte, base64.StdEncoding.DecodedLen(len(in)))

View File

@ -76,8 +76,7 @@ func (s *Service) RunEventStream(request *EventStreamRequest, server Bridge_RunE
}
case <-server.Context().Done():
s.log.Debug("Client closed the stream, exiting")
s.quit()
return nil
return s.quit()
}
}
}

View File

@ -20,25 +20,21 @@ package grpc
import (
"context"
"github.com/ProtonMail/gluon/async"
"google.golang.org/protobuf/types/known/emptypb"
"google.golang.org/protobuf/types/known/wrapperspb"
)
func (s *Service) ReportBugClicked(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
defer async.HandlePanic(s.panicHandler)
s.bridge.ReportBugClicked()
return &emptypb.Empty{}, nil
}
func (s *Service) AutoconfigClicked(_ context.Context, client *wrapperspb.StringValue) (*emptypb.Empty, error) {
defer async.HandlePanic(s.panicHandler)
s.bridge.AutoconfigUsed(client.Value)
return &emptypb.Empty{}, nil
}
func (s *Service) ExternalLinkClicked(_ context.Context, article *wrapperspb.StringValue) (*emptypb.Empty, error) {
defer async.HandlePanic(s.panicHandler)
s.bridge.ExternalLinkClicked(article.Value)
return &emptypb.Empty{}, nil
}

View File

@ -29,7 +29,6 @@ import (
)
func (s *Service) GetUserList(_ context.Context, _ *emptypb.Empty) (*UserListResponse, error) {
defer async.HandlePanic(s.panicHandler)
s.log.Debug("GetUserList")
userIDs := s.bridge.GetUserIDs()
@ -53,7 +52,6 @@ func (s *Service) GetUserList(_ context.Context, _ *emptypb.Empty) (*UserListRes
}
func (s *Service) GetUser(_ context.Context, userID *wrapperspb.StringValue) (*User, error) {
defer async.HandlePanic(s.panicHandler)
s.log.WithField("userID", userID).Debug("GetUser")
user, err := s.bridge.GetUserInfo(userID.Value)
@ -65,7 +63,6 @@ func (s *Service) GetUser(_ context.Context, userID *wrapperspb.StringValue) (*U
}
func (s *Service) SetUserSplitMode(_ context.Context, splitMode *UserSplitModeRequest) (*emptypb.Empty, error) {
defer async.HandlePanic(s.panicHandler)
s.log.WithField("UserID", splitMode.UserID).WithField("Active", splitMode.Active).Debug("SetUserSplitMode")
user, err := s.bridge.GetUserInfo(splitMode.UserID)
@ -100,7 +97,6 @@ func (s *Service) SetUserSplitMode(_ context.Context, splitMode *UserSplitModeRe
}
func (s *Service) SendBadEventUserFeedback(_ context.Context, feedback *UserBadEventFeedbackRequest) (*emptypb.Empty, error) {
defer async.HandlePanic(s.panicHandler)
l := s.log.WithField("UserID", feedback.UserID).WithField("doResync", feedback.DoResync)
l.Debug("SendBadEventUserFeedback")
@ -119,7 +115,6 @@ func (s *Service) SendBadEventUserFeedback(_ context.Context, feedback *UserBadE
}
func (s *Service) LogoutUser(_ context.Context, userID *wrapperspb.StringValue) (*emptypb.Empty, error) {
defer async.HandlePanic(s.panicHandler)
s.log.WithField("UserID", userID.Value).Debug("LogoutUser")
if _, err := s.bridge.GetUserInfo(userID.Value); err != nil {
@ -138,7 +133,6 @@ func (s *Service) LogoutUser(_ context.Context, userID *wrapperspb.StringValue)
}
func (s *Service) RemoveUser(_ context.Context, userID *wrapperspb.StringValue) (*emptypb.Empty, error) {
defer async.HandlePanic(s.panicHandler)
s.log.WithField("UserID", userID.Value).Debug("RemoveUser")
go func() {
@ -154,7 +148,6 @@ func (s *Service) RemoveUser(_ context.Context, userID *wrapperspb.StringValue)
}
func (s *Service) ConfigureUserAppleMail(ctx context.Context, request *ConfigureAppleMailRequest) (*emptypb.Empty, error) {
defer async.HandlePanic(s.panicHandler)
s.log.WithField("UserID", request.UserID).WithField("Address", request.Address).Debug("ConfigureUserAppleMail")
sslWasEnabled := s.bridge.GetSMTPSSL()

View File

@ -34,11 +34,6 @@ var (
// getInitials based on webapp implementation:
// https://github.com/ProtonMail/WebClients/blob/55d96a8b4afaaa4372fc5f1ef34953f2070fd7ec/packages/shared/lib/helpers/string.ts#L145
func getInitials(fullName string) string {
fullName = strings.TrimSpace(fullName)
if fullName == "" {
return "?"
}
words := strings.Split(
reMultiSpaces.ReplaceAllString(fullName, " "),
" ",

View File

@ -1,45 +0,0 @@
// Copyright (c) 2024 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package grpc
import (
"testing"
"github.com/stretchr/testify/require"
)
func Test_GetInitials(t *testing.T) {
require.Equal(t, "?", getInitials(""))
require.Equal(t, "T", getInitials(" test"))
require.Equal(t, "T", getInitials("test "))
require.Equal(t, "T", getInitials(" test "))
require.Equal(t, "JD", getInitials(" John Doe "))
require.Equal(t, "J", getInitials(" JohnDoe@proton.me "))
require.Equal(t, "JD", getInitials("\t\r\n John Doe \t\r\n "))
require.Equal(t, "T", getInitials("TestTestman"))
require.Equal(t, "TT", getInitials("Test Testman"))
require.Equal(t, "J", getInitials("JamesJoyce"))
require.Equal(t, "J", getInitials("JamesJoyceJeremy"))
require.Equal(t, "J", getInitials("james.joyce"))
require.Equal(t, "JJ", getInitials("James Joyce"))
require.Equal(t, "JM", getInitials("James Joyce Mahabharata"))
require.Equal(t, "JL", getInitials("James Joyce Jeremy Lin"))
require.Equal(t, "JM", getInitials("Jean Michel"))
require.Equal(t, "GC", getInitials("George Michael Carrie"))
}

View File

@ -4,45 +4,46 @@
"url": "https://proton.me/support/automatically-start-bridge",
"title": "Automatically start Bridge",
"keywords": [
"autostart",
"automatic",
"login",
"startup",
"start",
"boot"
]
},
{
"index": 1,
"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",
"url": "https://proton.me/support/bridge-automatic-update",
"title": "Automatic Update and Bridge",
"keywords": [
"app password",
"INVALIDCREDENTIALS",
"TEMPORARILYUNAVAILABLE",
"New Outlook",
"Outlook",
"Sign in failed"
"update",
"upgrade",
"restart",
"automatic",
"manual"
]
},
{
"index": 2,
"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)?",
"url": "https://proton.me/support/messages-encrypted-via-bridge",
"title": "Are my messages encrypted via Proton Mail Bridge?",
"keywords": [
"recovered messages",
"recovered messages folder"
"encrypted",
"privacy",
"message",
"security",
"gpg",
"pgp",
"crypto"
]
},
{
"index": 3,
"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",
"url": "https://proton.me/support/labels-in-bridge",
"title": "Labels in Bridge",
"keywords": [
"Receiving",
"Sending",
"Outlook",
"Configuration",
"Sync",
"New Outlook"
"labels",
"folders",
"directories"
]
},
{
@ -50,15 +51,14 @@
"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",
"Mac Mail",
"Could not connect to server",
"The attempt to read data from the server “127.0.0.1"
"Thunderbird"
]
},
{
@ -81,6 +81,7 @@
"split",
"address",
"mode"
]
},
{
@ -90,7 +91,7 @@
"keywords": [
"Thunderbird",
"Connection to server timed out",
"Connection timed out",
"Connection",
"Timeout"
]
},
@ -109,7 +110,7 @@
{
"index": 9,
"url": "https://proton.me/support/port-already-occupied-error",
"title": "How to fix “IMAP or SMTP port error” on Proton Mail Bridge",
"title": "Port already occupied error",
"keywords": [
"Port",
"occupied",
@ -122,53 +123,78 @@
},
{
"index": 10,
"url": "https://proton.me/support/apple-mail-certificate",
"title": "Why you need to install a certificate for Apple Mail with Proton Mail Bridge",
"url": "https://proton.me/support/clients-supported-bridge",
"title": "Email clients supported by Proton Mail Bridge",
"keywords": [
"certificate",
"client",
"Outlook",
"Thunderbird",
"Apple Mail",
"mac",
"unsigned"
"EM Client",
"The Bat",
"Eudora",
"Postbox",
"Canary",
"Spark"
]
},
{
"index": 11,
"url": "https://proton.me/support/macos-certificate-warning",
"title": "Warning when installing Proton Mail Bridge on macOS",
"url": "https://proton.me/support/imap-smtp-and-pop3-setup",
"title": "IMAP, SMTP, and POP3 setup",
"keywords": [
"install",
"mac",
"warning",
"certificate"
"IMAP",
"SMTP",
"setup",
"set up",
"configure",
"configuration",
"parameters"
]
},
{
"index": 12,
"url": "https://proton.me/support/bridge-linux-tray-icon",
"title": "How to fix a missing system tray icon in Linux",
"url": "https://proton.me/support/protonmail-bridge-install",
"title": "How to install Proton Mail Bridge",
"keywords": [
"linux",
"tray",
"notification"
"install",
"installer",
"setup",
"download",
"windows",
"mac",
"macos",
"linux"
]
},
{
"index": 13,
"url": "https://proton.me/support/bridge-linux-login-error",
"title": "How to fix Proton Bridge login errors",
"url": "https://proton.me/support/bridge-for-linux",
"title": "Proton Mail Bridge for Linux",
"keywords": [
"login",
"error",
"sign in"
"Linux",
"Ubuntu",
"Fedora",
"Debian",
"Unix",
"deb",
"rpm",
"CentOS",
"Arch",
"Mint"
]
},
{
"index": 14,
"url": "https://proton.me/support/protonmail-bridge-clients-macos-outlook-2019",
"title": "Proton Mail Bridge Microsoft Outlook 2019 for macOS setup guide",
"url": "https://proton.me/support/operating-systems-supported-bridge",
"title": "System requirements for Proton Mail Bridge",
"keywords": [
"Outlook",
"mac"
"requirement",
"cpu",
"memory",
"Windows",
"macOS",
"linux"
]
},
{
@ -177,11 +203,14 @@
"title": "How to configure your email client for Proton Mail Bridge",
"keywords": [
"Client",
"Outlook",
"configure",
"configuration",
"setup",
"application",
"setup"
"setup",
"IMAP",
"SMTP"
]
},
{
@ -191,10 +220,7 @@
"keywords": [
"password",
"invalid",
"error",
"incorrect",
"verify account name",
"login"
"error"
]
},
{
@ -203,21 +229,20 @@
"title": "Proton Mail Bridge Microsoft Outlook for Windows 2019 setup guide",
"keywords": [
"Outlook",
"2019",
"setup",
"configure",
"connect",
"Outlook 365"
"configuration"
]
},
{
"index": 18,
"url": "https://proton.me/support/protonmail-bridge-clients-windows-thunderbird",
"title": "Proton Mail Bridge Thunderbird setup guide for Windows, macOS, and Linux",
"url": "https://proton.me/support/protonmail-bridge-clients-windows-outlook-2016",
"title": "Proton Mail Bridge Microsoft Outlook 2016 for Windows setup guide",
"keywords": [
"Thunderbird",
"Outlook",
"2019",
"setup",
"configuration",
"connect"
"configuration"
]
},
{
@ -227,9 +252,7 @@
"keywords": [
"Apple Mail",
"setup",
"configure",
"configuration",
"connect"
"configuration"
]
},
{
@ -238,143 +261,165 @@
"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"
"configuration"
]
},
{
"index": 21,
"url": "https://proton.me/support/how-to-resolve-connection-issues-in-bridge",
"title": "How to resolve connection issues in Bridge",
"url": "https://proton.me/support/protonmail-bridge-clients-windows-thunderbird",
"title": "Proton Mail Bridge Thunderbird setup guide for Windows, macOS, and Linux",
"keywords": [
"Bridge is not able to contact the server",
"connection",
"error",
"red banner",
"No Connection",
"issue",
"lost connection"
"Thunderbird",
"setup",
"configuration"
]
},
{
"index": 22,
"url": "https://proton.me/support/bridge-internal-error",
"title": "How to fix an “internal error” warning on Proton Mail Bridge",
"url": "https://proton.me/support/protonmail-bridge-clients-macos-outlook-2016",
"title": "Proton Mail Bridge Microsoft Outlook 2016 for macOS setup guide",
"keywords": [
"internal error"
"Outlook 2016",
"macOS"
]
},
{
"index": 23,
"url": "https://proton.me/support/bridge-cant-move-cache",
"title": "How to fix a “cant move cache” error on Proton Mail Bridge",
"url": "https://proton.me/support/protonmail-bridge-clients-macos-outlook-2019",
"title": "Proton Mail Bridge Microsoft Outlook 2019 for macOS setup guide",
"keywords": [
"Cant move cache",
"local cache"
"Outlook 2019",
"macOS"
]
},
{
"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",
"url": "https://proton.me/support/protonmail-bridge-clients-windows-outlook-2013",
"title": "Proton Mail Bridge Microsoft Outlook 2013 for Windows setup guide",
"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"
"Outlook 2013",
"macOS"
]
},
{
"index": 25,
"url": "https://proton.me/support/not-receiving-messages-email-client",
"title": "How to troubleshoot not receiving new messages in your email client",
"url": "https://proton.me/support/protonmail-bridge-clients-macos-outlook-2011",
"title": "Proton Mail Bridge Microsoft Outlook 2011 for macOS setup guide",
"keywords": [
"emails are not arriving",
"emails missing",
"missing emails",
"emails not appearing",
"I cannot find emails in my email client"
"Outlook 2011",
"macOS"
]
},
{
"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",
"url": "https://proton.me/support/install-bridge-linux-pkgbuild-file",
"title": "Installing Proton Mail Bridge for Linux using a PKGBUILD file",
"keywords": [
"Invalid return path",
"I am not able to send emails",
"sending error",
"unable to send",
"send"
"Linux",
"pkgbuild"
]
},
{
"index": 27,
"url": "https://proton.me/support/this-computer-only-folder-outlook",
"title": "How to troubleshoot “This computer only” folders in Outlook for Windows",
"url": "https://proton.me/support/installing-bridge-linux-deb-file",
"title": "Installing Proton Mail Bridge for Linux using a DEB file",
"keywords": [
"This computer only"
"Linux",
"deb"
]
},
{
"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",
"url": "https://proton.me/support/verifying-bridge-package",
"title": "Verifying the Proton Mail Bridge package for Linux",
"keywords": [
"emails are missing",
"missing emails"
"Linux",
"Package",
"Verify"
]
},
{
"index": 29,
"url": "https://proton.me/support/bridge-cannot-access-keychain",
"title": "How to fix “cannot access keychain” on Proton Mail Bridge",
"url": "https://proton.me/support/bridge-cli-guide",
"title": "Bridge CLI (command line interface) guide",
"keywords": [
"cannot access keychain",
"No keychain available"
"CLI",
"Terminal",
"Command-line",
"Powershell"
]
},
{
"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",
"url": "https://proton.me/support/install-bridge-linux-rpm-file",
"title": "Installing Proton Mail Bridge for Linux using an RPM file",
"keywords": [
"address list has changed",
"address list changed"
"Linux",
"Install",
"RPM"
]
},
{
"index": 31,
"url": "https://proton.me/support/failed-to-parse-message-error",
"title": "Troubleshooting “failed to parse message” errors",
"url": "https://proton.me/support/bridge-linux-login-error",
"title": "How to fix Proton Bridge login errors",
"keywords": [
"failed to parse message"
"login",
"error",
"connection"
]
},
{
"index": 32,
"url": "https://proton.me/support/bridge-imap-login-failed",
"title": "How to fix an “IMAP Login failed” warning on Proton Mail Bridge",
"url": "https://proton.me/support/bridge-linux-tray-icon",
"title": "How to fix a missing system tray icon in Linux",
"keywords": [
"imap login failed",
"login failed"
"linux",
"tray",
"notification"
]
},
{
"index": 33,
"url": "https://proton.me/support/resolve-checking-server-notice",
"title": "How to resolve issues with “Checking mail server capabilities” notice in Thunderbird",
"url": "https://proton.me/support/why-you-need-bridge",
"title": "Why you need Proton Mail Bridge",
"keywords": [
"Checking mail server capabilities",
"checking capabilities",
"checking",
"capabilities"
"Bridge",
"email",
"client"
]
},
{
"index": 34,
"url": "https://proton.me/support/protonmail-bridge-manual-update",
"title": "How to manually update Proton Mail Bridge",
"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"
]
}
]

View File

@ -69,14 +69,10 @@ func GetSuggestionsFromArticleList(userInput string, articles ArticleList) (Arti
for _, article := range articles {
for _, keyword := range article.Keywords {
if strings.Contains(userInput, strings.ToUpper(keyword)) {
if len(keyword) > 12 {
article.Score += 2
} else {
article.Score++
}
}
}
}
articles = xslices.Filter(articles, func(article *Article) bool { return article.Score > 0 })
slices.SortFunc(articles, func(lhs, rhs *Article) bool { return lhs.Score > rhs.Score })

View File

@ -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 <= 5))
require.True(t, (count > 0) && (count <= 3))
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/apple-mail-certificate")
index1, err := GetArticleIndex("https://proton.me/support/bridge-for-linux")
require.NoError(t, err)
index2, err := GetArticleIndex("HTTPS://PROTON.ME/support/apple-mail-certificate")
index2, err := GetArticleIndex("HTTPS://PROTON.ME/support/bridge-for-linux")
require.NoError(t, err)
require.Equal(t, index1, index2)

View File

@ -78,7 +78,7 @@ func FuzzUnmarshal(f *testing.F) {
str := strings.Join(items, sep)
f.Add([]byte(str))
f.Fuzz(func(_ *testing.T, secret []byte) {
f.Fuzz(func(t *testing.T, secret []byte) {
encodedSecret := base64.StdEncoding.EncodeToString(secret)
creds := &Credentials{}

View File

@ -206,16 +206,6 @@ 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")
}
@ -248,16 +238,10 @@ 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(
@ -280,13 +264,3 @@ 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
}

View File

@ -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(_ int) (io.WriteCloser, error) {
return func(index int) (io.WriteCloser, error) {
b.StopTimer()
defer b.StartTimer()

View File

@ -19,5 +19,4 @@ package service
type Locator interface {
ProvideSettingsPath() (string, error)
ProvideUnleashCachePath() (string, error)
}

View File

@ -23,6 +23,7 @@ import (
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/bradenaw/juniper/stream"
)
type APIClient interface {
@ -40,7 +41,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) (proton.ImportResStream, error)
ImportMessages(ctx context.Context, addrKR *crypto.KeyRing, workers, buffer int, req ...proton.ImportReq) (stream.Stream[proton.ImportRes], 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)

View File

@ -31,6 +31,7 @@ import (
"github.com/ProtonMail/gluon/connector"
"github.com/ProtonMail/gluon/imap"
"github.com/ProtonMail/gluon/reporter"
"github.com/ProtonMail/gluon/rfc5322"
"github.com/ProtonMail/gluon/rfc822"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/gopenpgp/v2/crypto"
@ -689,6 +690,8 @@ func (s *Connector) importMessage(
isDraft := slices.Contains(labelIDs, proton.DraftsLabel)
s.reportGODT3185(isDraft, addr.Email, p, s.addressMode == usertypes.AddressModeCombined)
if err := s.identityState.WithAddrKR(s.addrID, func(_, addrKR *crypto.KeyRing) error {
primaryKey, errKey := addrKR.FirstKey()
if errKey != nil {
@ -874,3 +877,80 @@ func stripPlusAlias(a string) string {
func equalAddresses(a, b string) bool {
return strings.EqualFold(stripPlusAlias(a), stripPlusAlias(b))
}
func (s *Connector) reportGODT3185(isDraft bool, defaultAddr string, p *parser.Parser, isCombinedMode bool) {
reportAction := "draft"
if !isDraft {
reportAction = "import"
}
reportMode := "combined"
if !isCombinedMode {
reportMode = "split"
}
senderAddr := ""
if p != nil && p.Root() != nil && p.Root().Header.Len() != 0 {
addrField := p.Root().Header.Get("From")
if addrField == "" {
addrField = p.Root().Header.Get("Sender")
}
if addrField != "" {
sender, err := rfc5322.ParseAddressList(addrField)
if err == nil && len(sender) > 0 {
senderAddr = sender[0].Address
} else {
s.log.WithError(err).Warn("Invalid sender address in reporter")
}
}
}
if equalAddresses(defaultAddr, senderAddr) {
return
}
isDisabled := false
isUserAddress := false
for _, a := range s.identityState.GetAddresses() {
if !equalAddresses(a.Email, senderAddr) {
continue
}
isUserAddress = true
isDisabled = !bool(a.Send) || (a.Status != proton.AddressStatusEnabled)
break
}
if !isUserAddress && senderAddr != "" {
return
}
reportResult := "using sender address"
if !isCombinedMode {
reportResult = "error address not match"
}
reportAddress := ""
if senderAddr == "" {
reportAddress = " invalid"
reportResult = "error import/draft"
}
if isDisabled {
reportAddress = " disabled"
if isDraft {
reportResult = "error draft"
}
}
report := fmt.Sprintf(
"GODT-3185: %s with non-default%s address in %s mode: %s",
reportAction, reportAddress, reportMode, reportResult,
)
s.log.Warn(report)
if s.reporter != nil {
_ = s.reporter.ReportMessage(report)
}
}

View File

@ -1,67 +0,0 @@
// Copyright (c) 2024 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package evtloopmsgevents
import (
"time"
"github.com/ProtonMail/go-proton-api"
)
const (
messageEventErrorCaseSchemaName = "bridge_event_loop_message_event_failures_total"
messageEventErrorCaseSchemaVersion = 1
)
func generateMessageEventFailureObservabilityMetric(eventType string) proton.ObservabilityMetric {
return proton.ObservabilityMetric{
Name: messageEventErrorCaseSchemaName,
Version: messageEventErrorCaseSchemaVersion,
Timestamp: time.Now().Unix(),
Data: map[string]interface{}{
"Value": 1,
"Labels": map[string]string{
"eventType": eventType,
},
},
}
}
func GenerateMessageEventFailureCreateMessageMetric() proton.ObservabilityMetric {
return generateMessageEventFailureObservabilityMetric("createMessageEvent")
}
func GenerateMessageEventFailureDeleteMessageMetric() proton.ObservabilityMetric {
return generateMessageEventFailureObservabilityMetric("deleteMessageEvent")
}
func GenerateMessageEventFailureUpdateMetric() proton.ObservabilityMetric {
return generateMessageEventFailureObservabilityMetric("updateEvent")
}
func GenerateMessageEventFailedToBuildMessage() proton.ObservabilityMetric {
return generateMessageEventFailureObservabilityMetric("failedToBuildMessage")
}
func GenerateMessageEventFailedToBuildDraft() proton.ObservabilityMetric {
return generateMessageEventFailureObservabilityMetric("failedToBuildDraft")
}
func GenerateMessageEventUpdateChannelDoesNotExist() proton.ObservabilityMetric {
return generateMessageEventFailureObservabilityMetric("messageUpdateChannelDoesNotExist")
}

View File

@ -1,51 +0,0 @@
// Copyright (c) 2024 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package syncmsgevents
import (
"time"
"github.com/ProtonMail/go-proton-api"
)
const (
syncEventErrorCaseSchemaName = "bridge_sync_message_event_failures_total"
syncEventErrorCaseSchemaVersion = 1
)
func generateSyncEventFailureObservabilityMetric(eventType string) proton.ObservabilityMetric {
return proton.ObservabilityMetric{
Name: syncEventErrorCaseSchemaName,
Version: syncEventErrorCaseSchemaVersion,
Timestamp: time.Now().Unix(),
Data: map[string]interface{}{
"Value": 1,
"Labels": map[string]string{
"eventType": eventType,
},
},
}
}
func GenerateSyncFailureCreateMessageEventMetric() proton.ObservabilityMetric {
return generateSyncEventFailureObservabilityMetric("createMessageEvent")
}
func GenerateSyncFailureDeleteMessageEventMetric() proton.ObservabilityMetric {
return generateSyncEventFailureObservabilityMetric("deleteMessageEvent")
}

View File

@ -30,7 +30,6 @@ import (
"github.com/ProtonMail/gluon/watcher"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"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/syncservice"
@ -97,8 +96,6 @@ type Service struct {
syncConfigPath string
lastHandledEventID string
isSyncing atomic.Bool
observabilitySender observability.Sender
}
func NewService(
@ -119,7 +116,6 @@ func NewService(
syncConfigDir string,
maxSyncMemory uint64,
showAllMail bool,
observabilitySender observability.Sender,
) *Service {
subscriberName := fmt.Sprintf("imap-%v", identityState.User.ID)
@ -164,8 +160,6 @@ func NewService(
syncMessageBuilder: syncMessageBuilder,
syncReporter: syncReporter,
syncConfigPath: GetSyncConfigPath(syncConfigDir, identityState.User.ID),
observabilitySender: observabilitySender,
}
}

View File

@ -26,11 +26,11 @@ import (
"github.com/ProtonMail/gluon"
"github.com/ProtonMail/gluon/imap"
"github.com/ProtonMail/gluon/reporter"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/v3/internal"
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
obsMetrics "github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice/observabilitymetrics/evtloopmsgevents"
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
"github.com/ProtonMail/proton-bridge/v3/internal/usertypes"
"github.com/sirupsen/logrus"
"golang.org/x/exp/maps"
@ -46,7 +46,7 @@ func (s *Service) HandleMessageEvents(ctx context.Context, events []proton.Messa
case proton.EventCreate:
updates, err := onMessageCreated(logging.WithLogrusField(ctx, "action", "create message"), s, event.Message, false)
if err != nil {
s.observabilitySender.AddDistinctMetrics(observability.EventLoopError, obsMetrics.GenerateMessageEventFailureCreateMessageMetric())
reportError(s.reporter, s.log, "Failed to apply create message event", err)
return fmt.Errorf("failed to handle create message event: %w", err)
}
@ -64,7 +64,7 @@ func (s *Service) HandleMessageEvents(ctx context.Context, events []proton.Messa
event,
)
if err != nil {
s.observabilitySender.AddDistinctMetrics(observability.EventLoopError, obsMetrics.GenerateMessageEventFailureUpdateMetric())
reportError(s.reporter, s.log, "Failed to apply update draft message event", err)
return fmt.Errorf("failed to handle update draft event: %w", err)
}
@ -85,7 +85,7 @@ func (s *Service) HandleMessageEvents(ctx context.Context, events []proton.Messa
event.Message,
)
if err != nil {
s.observabilitySender.AddDistinctMetrics(observability.EventLoopError, obsMetrics.GenerateMessageEventFailureUpdateMetric())
reportError(s.reporter, s.log, "Failed to apply update message event", err)
return fmt.Errorf("failed to handle update message event: %w", err)
}
@ -113,7 +113,6 @@ func (s *Service) HandleMessageEvents(ctx context.Context, events []proton.Messa
)
if err := waitOnIMAPUpdates(ctx, updates); err != nil {
s.observabilitySender.AddDistinctMetrics(observability.EventLoopError, obsMetrics.GenerateMessageEventFailureDeleteMessageMetric())
return fmt.Errorf("failed to handle delete message event in gluon: %w", err)
}
}
@ -159,7 +158,8 @@ func onMessageCreated(
s.log.WithError(err).Error("Failed to add failed message ID to vault")
}
s.observabilitySender.AddDistinctMetrics(observability.EventLoopError, obsMetrics.GenerateMessageEventFailedToBuildMessage())
reportErrorAndMessageID(s.reporter, s.log, "Failed to build message (event create)", res.err, res.messageID)
return nil
}
@ -221,7 +221,8 @@ func onMessageUpdateDraftOrSent(ctx context.Context, s *Service, event proton.Me
s.log.WithError(err).Error("Failed to add failed message ID to vault")
}
s.observabilitySender.AddDistinctMetrics(observability.EventLoopError, obsMetrics.GenerateMessageEventFailedToBuildDraft())
reportErrorAndMessageID(s.reporter, s.log, "Failed to build draft message (event update)", res.err, res.messageID)
return nil
}
@ -298,6 +299,24 @@ func onMessageDeleted(ctx context.Context, s *Service, event proton.MessageEvent
return updates
}
func reportError(r reporter.Reporter, entry *logrus.Entry, title string, err error) {
reportErrorNoContextCancel(r, entry, title, err, reporter.Context{})
}
func reportErrorAndMessageID(r reporter.Reporter, entry *logrus.Entry, title string, err error, messgeID string) {
reportErrorNoContextCancel(r, entry, title, err, reporter.Context{"messageID": messgeID})
}
func reportErrorNoContextCancel(r reporter.Reporter, entry *logrus.Entry, title string, err error, reportContext reporter.Context) {
if !errors.Is(err, context.Canceled) {
reportContext["error"] = err
reportContext["error_type"] = internal.ErrCauseType(err)
if rerr := r.ReportMessageWithContext(title, reportContext); rerr != nil {
entry.WithError(err).WithField("title", title).Error("Failed to report message")
}
}
}
// safePublishMessageUpdate handles the rare case where the address' update channel may have been deleted in the same
// event. This rare case can take place if in the same event fetch request there is an update for delete address and
// create/update message.
@ -322,7 +341,7 @@ func safePublishMessageUpdate(ctx context.Context, s *Service, addressID string,
}
logrus.Warnf("Update channel not found for address %v, it may have been already deleted", addressID)
s.observabilitySender.AddDistinctMetrics(observability.EventLoopError, obsMetrics.GenerateMessageEventUpdateChannelDoesNotExist())
_ = s.reporter.ReportMessage("Message Update channel does not exist")
return false, nil
}

View File

@ -23,8 +23,6 @@ import (
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
obsMetrics "github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice/observabilitymetrics/syncmsgevents"
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
"github.com/ProtonMail/proton-bridge/v3/internal/services/userevents"
)
@ -57,7 +55,7 @@ func (s syncMessageEventHandler) HandleMessageEvents(ctx context.Context, events
true,
)
if err != nil {
s.service.observabilitySender.AddDistinctMetrics(observability.SyncError, obsMetrics.GenerateSyncFailureCreateMessageEventMetric())
reportError(s.service.reporter, s.service.log, "Failed to apply create message event", err)
return fmt.Errorf("failed to handle create message event: %w", err)
}
@ -73,7 +71,6 @@ func (s syncMessageEventHandler) HandleMessageEvents(ctx context.Context, events
)
if err := waitOnIMAPUpdates(ctx, updates); err != nil {
s.service.observabilitySender.AddDistinctMetrics(observability.SyncError, obsMetrics.GenerateSyncFailureDeleteMessageEventMetric())
return fmt.Errorf("failed to handle delete message event in gluon: %w", err)
}
default:

View File

@ -1,51 +0,0 @@
// Copyright (c) 2024 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package 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)
}

View File

@ -1,112 +0,0 @@
// Copyright (c) 2024 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package 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))
}

View File

@ -1,157 +0,0 @@
// Copyright (c) 2024 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package 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
observabilitySender observability.Sender
}
const bitfieldRegexPattern = `^\\\d+`
const disableNotificationsKillSwitch = "InboxBridgeEventLoopNotificationDisabled"
func NewService(userID string, service userevents.Subscribable, eventPublisher events.EventPublisher, store *Store,
getFlagFn unleash.GetFlagValueFn, observabilitySender observability.Sender) *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,
observabilitySender: observabilitySender,
}
}
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.observabilitySender.AddMetrics(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.observabilitySender.AddMetrics(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
}

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