mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-10 20:56:51 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 43cbedafb8 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -7,7 +7,6 @@
|
|||||||
*~
|
*~
|
||||||
.idea
|
.idea
|
||||||
.vscode
|
.vscode
|
||||||
.vs
|
|
||||||
|
|
||||||
# Test files
|
# Test files
|
||||||
godog.test
|
godog.test
|
||||||
@ -36,8 +35,6 @@ cmd/Import-Export/deploy
|
|||||||
proton-bridge
|
proton-bridge
|
||||||
cmd/Desktop-Bridge/*.exe
|
cmd/Desktop-Bridge/*.exe
|
||||||
cmd/launcher/*.exe
|
cmd/launcher/*.exe
|
||||||
bin/
|
|
||||||
obj/
|
|
||||||
|
|
||||||
# Jetbrains (CLion, Golang) cmake build dirs
|
# Jetbrains (CLion, Golang) cmake build dirs
|
||||||
cmake-build-*/
|
cmake-build-*/
|
||||||
|
|||||||
13
Changelog.md
13
Changelog.md
@ -3,19 +3,6 @@
|
|||||||
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
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
|
## Colorado Bridge 3.13.0
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
2
Makefile
2
Makefile
@ -12,7 +12,7 @@ ROOT_DIR:=$(realpath .)
|
|||||||
.PHONY: build build-gui build-nogui build-launcher versioner hasher
|
.PHONY: build build-gui build-nogui build-launcher versioner hasher
|
||||||
|
|
||||||
# Keep version hardcoded so app build works also without Git repository.
|
# Keep version hardcoded so app build works also without Git repository.
|
||||||
BRIDGE_APP_VERSION?=3.14.0+git
|
BRIDGE_APP_VERSION?=3.13.0+git
|
||||||
APP_VERSION:=${BRIDGE_APP_VERSION}
|
APP_VERSION:=${BRIDGE_APP_VERSION}
|
||||||
APP_FULL_NAME:=Proton Mail Bridge
|
APP_FULL_NAME:=Proton Mail Bridge
|
||||||
APP_VENDOR:=Proton AG
|
APP_VENDOR:=Proton AG
|
||||||
|
|||||||
6
go.mod
6
go.mod
@ -7,9 +7,9 @@ toolchain go1.21.9
|
|||||||
require (
|
require (
|
||||||
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
|
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
|
||||||
github.com/Masterminds/semver/v3 v3.2.0
|
github.com/Masterminds/semver/v3 v3.2.0
|
||||||
github.com/ProtonMail/gluon v0.17.1-0.20240923151549-d23b4bec3602
|
github.com/ProtonMail/gluon v0.17.1-0.20240514133734-79cdd0fec41c
|
||||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
|
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
|
||||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20240918100656-b4860af56d47
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20240829112804-d663a2ef90c2
|
||||||
github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton
|
github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton
|
||||||
github.com/PuerkitoBio/goquery v1.8.1
|
github.com/PuerkitoBio/goquery v1.8.1
|
||||||
github.com/abiosoft/ishell v2.0.0+incompatible
|
github.com/abiosoft/ishell v2.0.0+incompatible
|
||||||
@ -122,7 +122,7 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
replace (
|
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/emersion/go-smtp => github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865
|
||||||
github.com/go-resty/resty/v2 => github.com/LBeernaertProton/resty/v2 v2.0.0-20231129100320-dddf8030d93a
|
github.com/go-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
|
github.com/keybase/go-keychain => github.com/cuthix/go-keychain v0.0.0-20240103134243-0b6a41580b77
|
||||||
|
|||||||
18
go.sum
18
go.sum
@ -29,12 +29,6 @@ github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs
|
|||||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
|
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
|
||||||
github.com/ProtonMail/gluon v0.17.1-0.20240514133734-79cdd0fec41c h1:P3SvCACt13Zqdj0IRDB4bgwqI68+oMB2j0uVuPQyoTw=
|
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.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/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
|
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-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
|
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
|
||||||
@ -42,14 +36,6 @@ 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-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 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.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 h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
|
||||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
|
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
|
||||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20240612082117-0f92424eed80 h1:cP4+6RFn9vVgYnoDwxBU4EtIAZA+eM4rzOaSZNqZ1xg=
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20240612082117-0f92424eed80 h1:cP4+6RFn9vVgYnoDwxBU4EtIAZA+eM4rzOaSZNqZ1xg=
|
||||||
@ -70,10 +56,6 @@ github.com/ProtonMail/go-proton-api v0.4.1-0.20240827132526-849231fc34a1 h1:gATl
|
|||||||
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.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 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.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-smtp v0.0.0-20231109081432-2b3d50599865 h1:EP1gnxLL5Z7xBSymE9nSTM27nRYINuvssAtDmG0suD8=
|
github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865 h1:EP1gnxLL5Z7xBSymE9nSTM27nRYINuvssAtDmG0suD8=
|
||||||
github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
|
github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
|
||||||
github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
|
github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
|
||||||
|
|||||||
@ -267,8 +267,6 @@ func newBridge(
|
|||||||
|
|
||||||
unleashService := unleash.NewBridgeService(ctx, api, locator, panicHandler)
|
unleashService := unleash.NewBridgeService(ctx, api, locator, panicHandler)
|
||||||
|
|
||||||
observabilityService := observability.NewService(ctx, panicHandler)
|
|
||||||
|
|
||||||
bridge := &Bridge{
|
bridge := &Bridge{
|
||||||
vault: vault,
|
vault: vault,
|
||||||
|
|
||||||
@ -308,11 +306,11 @@ func newBridge(
|
|||||||
lastVersion: lastVersion,
|
lastVersion: lastVersion,
|
||||||
|
|
||||||
tasks: tasks,
|
tasks: tasks,
|
||||||
syncService: syncservice.NewService(panicHandler, observabilityService),
|
syncService: syncservice.NewService(reporter, panicHandler),
|
||||||
|
|
||||||
unleashService: unleashService,
|
unleashService: unleashService,
|
||||||
|
|
||||||
observabilityService: observabilityService,
|
observabilityService: observability.NewService(ctx, panicHandler),
|
||||||
|
|
||||||
notificationStore: notifications.NewStore(locator.ProvideNotificationsCachePath),
|
notificationStore: notifications.NewStore(locator.ProvideNotificationsCachePath),
|
||||||
}
|
}
|
||||||
@ -344,7 +342,7 @@ func newBridge(
|
|||||||
|
|
||||||
bridge.unleashService.Run()
|
bridge.unleashService.Run()
|
||||||
|
|
||||||
bridge.observabilityService.Run(bridge)
|
bridge.observabilityService.Run()
|
||||||
|
|
||||||
return bridge, nil
|
return bridge, nil
|
||||||
}
|
}
|
||||||
@ -712,13 +710,5 @@ func (bridge *Bridge) GetFeatureFlagValue(key string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) PushObservabilityMetric(metric proton.ObservabilityMetric) {
|
func (bridge *Bridge) PushObservabilityMetric(metric proton.ObservabilityMetric) {
|
||||||
bridge.observabilityService.AddMetrics(metric)
|
bridge.observabilityService.AddMetric(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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
|
||||||
}
|
|
||||||
@ -95,70 +95,3 @@ func TestBridge_Observability(t *testing.T) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBridge_Observability_Heartbeat(t *testing.T) {
|
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
|
||||||
throttlePeriod := time.Millisecond * 300
|
|
||||||
observability.ModifyThrottlePeriod(throttlePeriod)
|
|
||||||
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
|
||||||
require.NoError(t, getErr(bridge.LoginFull(ctx, username, password, nil, nil)))
|
|
||||||
bridge.ModifyObservabilityHeartbeatInterval(throttlePeriod)
|
|
||||||
|
|
||||||
require.Equal(t, 0, len(s.GetObservabilityStatistics().Metrics))
|
|
||||||
time.Sleep(time.Millisecond * 150)
|
|
||||||
require.Equal(t, 0, len(s.GetObservabilityStatistics().Metrics))
|
|
||||||
time.Sleep(time.Millisecond * 200)
|
|
||||||
require.Equal(t, 1, len(s.GetObservabilityStatistics().Metrics))
|
|
||||||
time.Sleep(time.Millisecond * 350)
|
|
||||||
require.Equal(t, 2, len(s.GetObservabilityStatistics().Metrics))
|
|
||||||
time.Sleep(time.Millisecond * 350)
|
|
||||||
require.Equal(t, 3, len(s.GetObservabilityStatistics().Metrics))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBridge_Observability_UserMetric(t *testing.T) {
|
|
||||||
testMetric := proton.ObservabilityMetric{
|
|
||||||
Name: "test1",
|
|
||||||
Version: 1,
|
|
||||||
Timestamp: time.Now().Unix(),
|
|
||||||
Data: nil,
|
|
||||||
}
|
|
||||||
|
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
|
||||||
userMetricPeriod := time.Millisecond * 200
|
|
||||||
heartbeatPeriod := time.Second * 10
|
|
||||||
throttlePeriod := time.Millisecond * 100
|
|
||||||
observability.ModifyUserMetricInterval(userMetricPeriod)
|
|
||||||
observability.ModifyThrottlePeriod(throttlePeriod)
|
|
||||||
|
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
|
||||||
require.NoError(t, getErr(bridge.LoginFull(ctx, username, password, nil, nil)))
|
|
||||||
bridge.ModifyObservabilityHeartbeatInterval(heartbeatPeriod)
|
|
||||||
|
|
||||||
time.Sleep(throttlePeriod)
|
|
||||||
require.Equal(t, 0, len(s.GetObservabilityStatistics().Metrics))
|
|
||||||
|
|
||||||
bridge.PushDistinctObservabilityMetrics(observability.SyncError, testMetric)
|
|
||||||
time.Sleep(throttlePeriod)
|
|
||||||
// We're expecting two observability metrics to be sent, the actual metric + the user metric.
|
|
||||||
require.Equal(t, 2, len(s.GetObservabilityStatistics().Metrics))
|
|
||||||
|
|
||||||
bridge.PushDistinctObservabilityMetrics(observability.SyncError, testMetric)
|
|
||||||
time.Sleep(throttlePeriod)
|
|
||||||
// We're expecting only a single metric to be sent, since the user metric update has been sent already within the predefined period.
|
|
||||||
require.Equal(t, 3, len(s.GetObservabilityStatistics().Metrics))
|
|
||||||
|
|
||||||
bridge.PushDistinctObservabilityMetrics(observability.SyncError, testMetric)
|
|
||||||
time.Sleep(throttlePeriod)
|
|
||||||
// Two metric updates should be sent again.
|
|
||||||
require.Equal(t, 5, len(s.GetObservabilityStatistics().Metrics))
|
|
||||||
|
|
||||||
bridge.PushDistinctObservabilityMetrics(observability.SyncError, testMetric)
|
|
||||||
time.Sleep(throttlePeriod)
|
|
||||||
// Only a single one should be sent.
|
|
||||||
require.Equal(t, 6, len(s.GetObservabilityStatistics().Metrics))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@ -29,12 +29,13 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
|
"github.com/golang/mock/gomock"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBridge_Report(t *testing.T) {
|
func TestBridge_Report(t *testing.T) {
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
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{}))
|
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
|
||||||
defer done()
|
defer done()
|
||||||
|
|
||||||
@ -55,6 +56,12 @@ func TestBridge_Report(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer func() { require.NoError(t, conn.Close()) }()
|
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.
|
// Read lines from the IMAP port.
|
||||||
lineCh := liner.New(conn).Lines(func() error { return nil })
|
lineCh := liner.New(conn).Lines(func() error { return nil })
|
||||||
|
|
||||||
|
|||||||
@ -21,7 +21,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"github.com/ProtonMail/gluon/reporter"
|
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
||||||
@ -116,17 +115,6 @@ func (bridge *Bridge) installUpdate(ctx context.Context, job installJob) {
|
|||||||
err := bridge.updater.InstallUpdate(ctx, bridge.api, job.version)
|
err := bridge.updater.InstallUpdate(ctx, bridge.api, job.version)
|
||||||
|
|
||||||
switch {
|
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):
|
case errors.Is(err, updater.ErrUpdateAlreadyInstalled):
|
||||||
log.Info("The update was already installed")
|
log.Info("The update was already installed")
|
||||||
|
|
||||||
|
|||||||
@ -571,6 +571,7 @@ func (bridge *Bridge) addUserWithVault(
|
|||||||
isNew,
|
isNew,
|
||||||
bridge.notificationStore,
|
bridge.notificationStore,
|
||||||
bridge.unleashService.GetFlagValue,
|
bridge.unleashService.GetFlagValue,
|
||||||
|
bridge.observabilityService.AddMetric,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create user: %w", err)
|
return fmt.Errorf("failed to create user: %w", err)
|
||||||
|
|||||||
@ -38,6 +38,9 @@ func (bridge *Bridge) handleUserEvent(ctx context.Context, user *user.User, even
|
|||||||
|
|
||||||
case events.UserLoadedCheckResync:
|
case events.UserLoadedCheckResync:
|
||||||
user.VerifyResyncAndExecute()
|
user.VerifyResyncAndExecute()
|
||||||
|
|
||||||
|
case events.UncategorizedEventError:
|
||||||
|
bridge.handleUncategorizedErrorEvent(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,3 +67,12 @@ func (bridge *Bridge) handleUserBadEvent(ctx context.Context, user *user.User, e
|
|||||||
user.OnBadEvent(ctx)
|
user.OnBadEvent(ctx)
|
||||||
}, bridge.usersLock)
|
}, 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -54,7 +54,6 @@ QString const bridgeGUILock = "bridge-v3-gui.lock"; ///< The file name used for
|
|||||||
QString const exeName = "bridge" + exeSuffix; ///< The bridge executable file name.*
|
QString const exeName = "bridge" + exeSuffix; ///< The bridge executable file name.*
|
||||||
qint64 constexpr grpcServiceConfigWaitDelayMs = 180000; ///< The wait delay for the gRPC config file in milliseconds.
|
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 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
|
} // anonymous namespace
|
||||||
|
|
||||||
@ -318,7 +317,7 @@ int main(int argc, char *argv[]) {
|
|||||||
QString bridgeExe;
|
QString bridgeExe;
|
||||||
if (!cliOptions.attach) {
|
if (!cliOptions.attach) {
|
||||||
if (isBridgeRunning()) {
|
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));
|
QString(), __FUNCTION__, tailOfLatestBridgeLog(sessionID));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -414,18 +413,13 @@ int main(int argc, char *argv[]) {
|
|||||||
lock.unlock();
|
lock.unlock();
|
||||||
return result;
|
return result;
|
||||||
} catch (Exception const &e) {
|
} catch (Exception const &e) {
|
||||||
|
sentry_uuid_s const uuid = reportSentryException("Exception occurred during main", e);
|
||||||
QString message = e.qwhat();
|
QString message = e.qwhat();
|
||||||
if (e.showSupportLink()) {
|
if (e.showSupportLink()) {
|
||||||
message += R"(<br/><br/>If the issue persists, please contact our <a href="https://proton.me/support/contact">customer support</a>.)";
|
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);
|
QMessageBox::critical(nullptr, "Error", message);
|
||||||
|
QTextStream(stderr) << "reportID: " << QByteArray(uuid.bytes, 16).toHex() << " Captured exception :" << e.detailedWhat() << "\n";
|
||||||
if (e.qwhat() != orphanInstanceException) {
|
|
||||||
sentry_uuid_s const uuid = reportSentryException("Exception occurred during main", e);
|
|
||||||
QTextStream(stderr) << "reportID: " << QByteArray(uuid.bytes, 16).toHex() << " Captured exception :"
|
|
||||||
<< e.detailedWhat() << "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,11 +34,6 @@ var (
|
|||||||
// getInitials based on webapp implementation:
|
// getInitials based on webapp implementation:
|
||||||
// https://github.com/ProtonMail/WebClients/blob/55d96a8b4afaaa4372fc5f1ef34953f2070fd7ec/packages/shared/lib/helpers/string.ts#L145
|
// https://github.com/ProtonMail/WebClients/blob/55d96a8b4afaaa4372fc5f1ef34953f2070fd7ec/packages/shared/lib/helpers/string.ts#L145
|
||||||
func getInitials(fullName string) string {
|
func getInitials(fullName string) string {
|
||||||
fullName = strings.TrimSpace(fullName)
|
|
||||||
if fullName == "" {
|
|
||||||
return "?"
|
|
||||||
}
|
|
||||||
|
|
||||||
words := strings.Split(
|
words := strings.Split(
|
||||||
reMultiSpaces.ReplaceAllString(fullName, " "),
|
reMultiSpaces.ReplaceAllString(fullName, " "),
|
||||||
" ",
|
" ",
|
||||||
|
|||||||
@ -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"))
|
|
||||||
}
|
|
||||||
@ -31,6 +31,7 @@ import (
|
|||||||
"github.com/ProtonMail/gluon/connector"
|
"github.com/ProtonMail/gluon/connector"
|
||||||
"github.com/ProtonMail/gluon/imap"
|
"github.com/ProtonMail/gluon/imap"
|
||||||
"github.com/ProtonMail/gluon/reporter"
|
"github.com/ProtonMail/gluon/reporter"
|
||||||
|
"github.com/ProtonMail/gluon/rfc5322"
|
||||||
"github.com/ProtonMail/gluon/rfc822"
|
"github.com/ProtonMail/gluon/rfc822"
|
||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||||
@ -689,6 +690,8 @@ func (s *Connector) importMessage(
|
|||||||
|
|
||||||
isDraft := slices.Contains(labelIDs, proton.DraftsLabel)
|
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 {
|
if err := s.identityState.WithAddrKR(s.addrID, func(_, addrKR *crypto.KeyRing) error {
|
||||||
primaryKey, errKey := addrKR.FirstKey()
|
primaryKey, errKey := addrKR.FirstKey()
|
||||||
if errKey != nil {
|
if errKey != nil {
|
||||||
@ -874,3 +877,80 @@ func stripPlusAlias(a string) string {
|
|||||||
func equalAddresses(a, b string) bool {
|
func equalAddresses(a, b string) bool {
|
||||||
return strings.EqualFold(stripPlusAlias(a), stripPlusAlias(b))
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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")
|
|
||||||
}
|
|
||||||
@ -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")
|
|
||||||
}
|
|
||||||
@ -30,7 +30,6 @@ import (
|
|||||||
"github.com/ProtonMail/gluon/watcher"
|
"github.com/ProtonMail/gluon/watcher"
|
||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
"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/orderedtasks"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/sendrecorder"
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/sendrecorder"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice"
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice"
|
||||||
@ -97,8 +96,6 @@ type Service struct {
|
|||||||
syncConfigPath string
|
syncConfigPath string
|
||||||
lastHandledEventID string
|
lastHandledEventID string
|
||||||
isSyncing atomic.Bool
|
isSyncing atomic.Bool
|
||||||
|
|
||||||
observabilitySender observability.Sender
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(
|
func NewService(
|
||||||
@ -119,7 +116,6 @@ func NewService(
|
|||||||
syncConfigDir string,
|
syncConfigDir string,
|
||||||
maxSyncMemory uint64,
|
maxSyncMemory uint64,
|
||||||
showAllMail bool,
|
showAllMail bool,
|
||||||
observabilitySender observability.Sender,
|
|
||||||
) *Service {
|
) *Service {
|
||||||
subscriberName := fmt.Sprintf("imap-%v", identityState.User.ID)
|
subscriberName := fmt.Sprintf("imap-%v", identityState.User.ID)
|
||||||
|
|
||||||
@ -164,8 +160,6 @@ func NewService(
|
|||||||
syncMessageBuilder: syncMessageBuilder,
|
syncMessageBuilder: syncMessageBuilder,
|
||||||
syncReporter: syncReporter,
|
syncReporter: syncReporter,
|
||||||
syncConfigPath: GetSyncConfigPath(syncConfigDir, identityState.User.ID),
|
syncConfigPath: GetSyncConfigPath(syncConfigDir, identityState.User.ID),
|
||||||
|
|
||||||
observabilitySender: observabilitySender,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -26,11 +26,11 @@ import (
|
|||||||
|
|
||||||
"github.com/ProtonMail/gluon"
|
"github.com/ProtonMail/gluon"
|
||||||
"github.com/ProtonMail/gluon/imap"
|
"github.com/ProtonMail/gluon/imap"
|
||||||
|
"github.com/ProtonMail/gluon/reporter"
|
||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
"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/ProtonMail/proton-bridge/v3/internal/usertypes"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"golang.org/x/exp/maps"
|
"golang.org/x/exp/maps"
|
||||||
@ -46,7 +46,7 @@ func (s *Service) HandleMessageEvents(ctx context.Context, events []proton.Messa
|
|||||||
case proton.EventCreate:
|
case proton.EventCreate:
|
||||||
updates, err := onMessageCreated(logging.WithLogrusField(ctx, "action", "create message"), s, event.Message, false)
|
updates, err := onMessageCreated(logging.WithLogrusField(ctx, "action", "create message"), s, event.Message, false)
|
||||||
if err != nil {
|
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)
|
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,
|
event,
|
||||||
)
|
)
|
||||||
if err != nil {
|
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)
|
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,
|
event.Message,
|
||||||
)
|
)
|
||||||
if err != nil {
|
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)
|
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 {
|
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)
|
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.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
|
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.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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -298,6 +299,24 @@ func onMessageDeleted(ctx context.Context, s *Service, event proton.MessageEvent
|
|||||||
return updates
|
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
|
// 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
|
// 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.
|
// 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)
|
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
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,8 +23,6 @@ import (
|
|||||||
|
|
||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
"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"
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/userevents"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -57,7 +55,7 @@ func (s syncMessageEventHandler) HandleMessageEvents(ctx context.Context, events
|
|||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
if err != nil {
|
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)
|
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 {
|
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)
|
return fmt.Errorf("failed to handle delete message event in gluon: %w", err)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
|||||||
@ -44,16 +44,15 @@ type Service struct {
|
|||||||
|
|
||||||
store *Store
|
store *Store
|
||||||
|
|
||||||
getFlagValueFn unleash.GetFlagValueFn
|
getFlagValueFn unleash.GetFlagValueFn
|
||||||
|
pushObservabilityMetricFn observability.PushObsMetricFn
|
||||||
observabilitySender observability.Sender
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const bitfieldRegexPattern = `^\\\d+`
|
const bitfieldRegexPattern = `^\\\d+`
|
||||||
const disableNotificationsKillSwitch = "InboxBridgeEventLoopNotificationDisabled"
|
const disableNotificationsKillSwitch = "InboxBridgeEventLoopNotificationDisabled"
|
||||||
|
|
||||||
func NewService(userID string, service userevents.Subscribable, eventPublisher events.EventPublisher, store *Store,
|
func NewService(userID string, service userevents.Subscribable, eventPublisher events.EventPublisher, store *Store,
|
||||||
getFlagFn unleash.GetFlagValueFn, observabilitySender observability.Sender) *Service {
|
getFlagFn unleash.GetFlagValueFn, pushMetricFn observability.PushObsMetricFn) *Service {
|
||||||
return &Service{
|
return &Service{
|
||||||
userID: userID,
|
userID: userID,
|
||||||
|
|
||||||
@ -69,8 +68,8 @@ func NewService(userID string, service userevents.Subscribable, eventPublisher e
|
|||||||
|
|
||||||
store: store,
|
store: store,
|
||||||
|
|
||||||
getFlagValueFn: getFlagFn,
|
getFlagValueFn: getFlagFn,
|
||||||
observabilitySender: observabilitySender,
|
pushObservabilityMetricFn: pushMetricFn,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,7 +110,7 @@ func (s *Service) HandleNotificationEvents(ctx context.Context, notificationEven
|
|||||||
s.log.Debug("Handling notification events")
|
s.log.Debug("Handling notification events")
|
||||||
|
|
||||||
// Publish observability metrics that we've received notifications
|
// Publish observability metrics that we've received notifications
|
||||||
s.observabilitySender.AddMetrics(GenerateReceivedMetric(len(notificationEvents)))
|
s.pushObservabilityMetricFn(GenerateReceivedMetric(len(notificationEvents)))
|
||||||
|
|
||||||
for _, event := range notificationEvents {
|
for _, event := range notificationEvents {
|
||||||
ctx = logging.WithLogrusField(ctx, "notificationID", event.ID)
|
ctx = logging.WithLogrusField(ctx, "notificationID", event.ID)
|
||||||
@ -134,7 +133,7 @@ func (s *Service) HandleNotificationEvents(ctx context.Context, notificationEven
|
|||||||
Subtitle: event.Payload.Subtitle, Body: event.Payload.Body})
|
Subtitle: event.Payload.Subtitle, Body: event.Payload.Body})
|
||||||
|
|
||||||
// Publish observability metric that we've successfully processed notifications
|
// Publish observability metric that we've successfully processed notifications
|
||||||
s.observabilitySender.AddMetrics(GenerateProcessedMetric(1))
|
s.pushObservabilityMetricFn(GenerateProcessedMetric(1))
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|||||||
@ -109,7 +109,7 @@ func (s *Store) readCache() {
|
|||||||
|
|
||||||
file, err := os.Open(s.cacheFilepath)
|
file, err := os.Open(s.cacheFilepath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.log.WithError(err).Info("Unable to open cache file")
|
s.log.WithError(err).Error("Unable to open cache file")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,47 +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 observability
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
// DistinctionErrorTypeEnum - maps to the specific error schema for which we
|
|
||||||
// want to send a user update.
|
|
||||||
type DistinctionErrorTypeEnum int
|
|
||||||
|
|
||||||
const (
|
|
||||||
SyncError DistinctionErrorTypeEnum = iota
|
|
||||||
EventLoopError
|
|
||||||
)
|
|
||||||
|
|
||||||
// errorSchemaMap - maps between the DistinctionErrorTypeEnum and the relevant schema name.
|
|
||||||
var errorSchemaMap = map[DistinctionErrorTypeEnum]string{ //nolint:gochecknoglobals
|
|
||||||
SyncError: "bridge_sync_errors_users_total",
|
|
||||||
EventLoopError: "bridge_event_loop_events_errors_users_total",
|
|
||||||
}
|
|
||||||
|
|
||||||
// createLastSentMap - needs to be updated whenever we make changes to the enum.
|
|
||||||
func createLastSentMap() map[DistinctionErrorTypeEnum]time.Time {
|
|
||||||
registerTime := time.Now().Add(-updateInterval)
|
|
||||||
lastSentMap := make(map[DistinctionErrorTypeEnum]time.Time)
|
|
||||||
|
|
||||||
for errType := SyncError; errType <= EventLoopError; errType++ {
|
|
||||||
lastSentMap[errType] = registerTime
|
|
||||||
}
|
|
||||||
|
|
||||||
return lastSentMap
|
|
||||||
}
|
|
||||||
@ -1,170 +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 observability
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ProtonMail/gluon/async"
|
|
||||||
"github.com/ProtonMail/go-proton-api"
|
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
|
||||||
)
|
|
||||||
|
|
||||||
var updateInterval = time.Minute * 5 //nolint:gochecknoglobals
|
|
||||||
|
|
||||||
type observabilitySender interface {
|
|
||||||
addMetricsIfClients(metric ...proton.ObservabilityMetric)
|
|
||||||
}
|
|
||||||
|
|
||||||
// distinctionUtility - used to discern whether X number of events stem from Y number of users.
|
|
||||||
type distinctionUtility struct {
|
|
||||||
ctx context.Context
|
|
||||||
|
|
||||||
panicHandler async.PanicHandler
|
|
||||||
|
|
||||||
lastSentMap map[DistinctionErrorTypeEnum]time.Time // Ensures we don't step over the limit of one user update every 5 mins.
|
|
||||||
|
|
||||||
observabilitySender observabilitySender
|
|
||||||
settingsGetter settingsGetter
|
|
||||||
|
|
||||||
userPlanUnsafe string
|
|
||||||
userPlanLock sync.Mutex
|
|
||||||
|
|
||||||
heartbeatData heartbeatData
|
|
||||||
heartbeatDataLock sync.Mutex
|
|
||||||
heartbeatTicker *time.Ticker
|
|
||||||
}
|
|
||||||
|
|
||||||
func newDistinctionUtility(ctx context.Context, panicHandler async.PanicHandler, observabilitySender observabilitySender) *distinctionUtility {
|
|
||||||
distinctionUtility := &distinctionUtility{
|
|
||||||
ctx: ctx,
|
|
||||||
|
|
||||||
panicHandler: panicHandler,
|
|
||||||
|
|
||||||
lastSentMap: createLastSentMap(),
|
|
||||||
|
|
||||||
observabilitySender: observabilitySender,
|
|
||||||
|
|
||||||
userPlanUnsafe: planUnknown,
|
|
||||||
|
|
||||||
heartbeatData: heartbeatData{},
|
|
||||||
heartbeatTicker: time.NewTicker(updateInterval),
|
|
||||||
}
|
|
||||||
|
|
||||||
return distinctionUtility
|
|
||||||
}
|
|
||||||
|
|
||||||
// sendMetricsWithGuard - schedules the metrics to be sent only if there are authenticated clients.
|
|
||||||
func (d *distinctionUtility) sendMetricsWithGuard(metrics ...proton.ObservabilityMetric) {
|
|
||||||
if d.observabilitySender == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
d.observabilitySender.addMetricsIfClients(metrics...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *distinctionUtility) setSettingsGetter(getter settingsGetter) {
|
|
||||||
d.settingsGetter = getter
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkAndUpdateLastSentMap - checks whether we have sent a relevant user update metric
|
|
||||||
// within the last 5 minutes.
|
|
||||||
func (d *distinctionUtility) checkAndUpdateLastSentMap(key DistinctionErrorTypeEnum) bool {
|
|
||||||
curTime := time.Now()
|
|
||||||
val, ok := d.lastSentMap[key]
|
|
||||||
if !ok {
|
|
||||||
d.lastSentMap[key] = curTime
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if val.Add(updateInterval).Before(curTime) {
|
|
||||||
d.lastSentMap[key] = curTime
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateUserMetric creates the relevant user update metric based on its type
|
|
||||||
// and the relevant settings. In the future this will need to be expanded to support multiple
|
|
||||||
// versions of the metric if we ever decide to change them.
|
|
||||||
func (d *distinctionUtility) generateUserMetric(
|
|
||||||
metricType DistinctionErrorTypeEnum,
|
|
||||||
) proton.ObservabilityMetric {
|
|
||||||
schemaName, ok := errorSchemaMap[metricType]
|
|
||||||
if !ok {
|
|
||||||
return proton.ObservabilityMetric{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return generateUserMetric(schemaName, d.getUserPlanSafe(),
|
|
||||||
d.getEmailClientUserAgent(),
|
|
||||||
getEnabled(d.getProxyAllowed()),
|
|
||||||
getEnabled(d.getBetaAccessEnabled()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateUserMetric(schemaName, plan, mailClient, dohEnabled, betaAccess string) proton.ObservabilityMetric {
|
|
||||||
return proton.ObservabilityMetric{
|
|
||||||
Name: schemaName,
|
|
||||||
Version: 1,
|
|
||||||
Timestamp: time.Now().Unix(),
|
|
||||||
Data: map[string]interface{}{
|
|
||||||
"Value": 1,
|
|
||||||
"Labels": map[string]string{
|
|
||||||
"plan": plan,
|
|
||||||
"mailClient": mailClient,
|
|
||||||
"dohEnabled": dohEnabled,
|
|
||||||
"betaAccessEnabled": betaAccess,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *distinctionUtility) generateDistinctMetrics(errType DistinctionErrorTypeEnum, metrics ...proton.ObservabilityMetric) []proton.ObservabilityMetric {
|
|
||||||
d.updateHeartbeatData(errType)
|
|
||||||
|
|
||||||
if d.checkAndUpdateLastSentMap(errType) {
|
|
||||||
metrics = append(metrics, d.generateUserMetric(errType))
|
|
||||||
}
|
|
||||||
return metrics
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *distinctionUtility) getEmailClientUserAgent() string {
|
|
||||||
ua := ""
|
|
||||||
if d.settingsGetter != nil {
|
|
||||||
ua = d.settingsGetter.GetCurrentUserAgent()
|
|
||||||
}
|
|
||||||
|
|
||||||
return matchUserAgent(ua)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *distinctionUtility) getBetaAccessEnabled() bool {
|
|
||||||
if d.settingsGetter == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return d.settingsGetter.GetUpdateChannel() == updater.EarlyChannel
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *distinctionUtility) getProxyAllowed() bool {
|
|
||||||
if d.settingsGetter == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return d.settingsGetter.GetProxyAllowed()
|
|
||||||
}
|
|
||||||
@ -1,119 +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 observability
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ProtonMail/gluon/async"
|
|
||||||
"github.com/ProtonMail/go-proton-api"
|
|
||||||
)
|
|
||||||
|
|
||||||
const genericHeartbeatSchemaName = "bridge_generic_user_heartbeat_total"
|
|
||||||
|
|
||||||
type heartbeatData struct {
|
|
||||||
receivedSyncError bool
|
|
||||||
receivedEventLoopError bool
|
|
||||||
receivedOtherError bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *distinctionUtility) resetHeartbeatData() {
|
|
||||||
d.heartbeatData.receivedSyncError = false
|
|
||||||
d.heartbeatData.receivedOtherError = false
|
|
||||||
d.heartbeatData.receivedEventLoopError = false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *distinctionUtility) updateHeartbeatData(errType DistinctionErrorTypeEnum) {
|
|
||||||
d.withUpdateHeartbeatDataLock(func() {
|
|
||||||
switch errType {
|
|
||||||
case SyncError:
|
|
||||||
d.heartbeatData.receivedSyncError = true
|
|
||||||
case EventLoopError:
|
|
||||||
d.heartbeatData.receivedEventLoopError = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *distinctionUtility) runHeartbeat() {
|
|
||||||
go func() {
|
|
||||||
defer async.HandlePanic(d.panicHandler)
|
|
||||||
defer d.heartbeatTicker.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-d.ctx.Done():
|
|
||||||
return
|
|
||||||
case <-d.heartbeatTicker.C:
|
|
||||||
d.sendHeartbeat()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *distinctionUtility) withUpdateHeartbeatDataLock(fn func()) {
|
|
||||||
d.heartbeatDataLock.Lock()
|
|
||||||
defer d.heartbeatDataLock.Unlock()
|
|
||||||
fn()
|
|
||||||
}
|
|
||||||
|
|
||||||
// sendHeartbeat - will only send a heartbeat if there is an authenticated client
|
|
||||||
// otherwise we might end up polluting the cache and therefore our metrics.
|
|
||||||
func (d *distinctionUtility) sendHeartbeat() {
|
|
||||||
d.withUpdateHeartbeatDataLock(func() {
|
|
||||||
d.sendMetricsWithGuard(d.generateHeartbeatUserMetric())
|
|
||||||
d.resetHeartbeatData()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatBool(value bool) string {
|
|
||||||
return fmt.Sprintf("%t", value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateHeartbeatUserMetric creates the heartbeat user metric and includes the relevant data.
|
|
||||||
func (d *distinctionUtility) generateHeartbeatUserMetric() proton.ObservabilityMetric {
|
|
||||||
return generateHeartbeatMetric(
|
|
||||||
d.getUserPlanSafe(),
|
|
||||||
d.getEmailClientUserAgent(),
|
|
||||||
getEnabled(d.settingsGetter.GetProxyAllowed()),
|
|
||||||
getEnabled(d.getBetaAccessEnabled()),
|
|
||||||
formatBool(d.heartbeatData.receivedOtherError),
|
|
||||||
formatBool(d.heartbeatData.receivedSyncError),
|
|
||||||
formatBool(d.heartbeatData.receivedEventLoopError),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateHeartbeatMetric(plan, mailClient, dohEnabled, betaAccess, otherError, syncError, eventLoopError string) proton.ObservabilityMetric {
|
|
||||||
return proton.ObservabilityMetric{
|
|
||||||
Name: genericHeartbeatSchemaName,
|
|
||||||
Version: 1,
|
|
||||||
Timestamp: time.Now().Unix(),
|
|
||||||
Data: map[string]interface{}{
|
|
||||||
"Value": 1,
|
|
||||||
"Labels": map[string]string{
|
|
||||||
"plan": plan,
|
|
||||||
"mailClient": mailClient,
|
|
||||||
"dohEnabled": dohEnabled,
|
|
||||||
"betaAccessEnabled": betaAccess,
|
|
||||||
"receivedOtherError": otherError,
|
|
||||||
"receivedSyncError": syncError,
|
|
||||||
"receivedEventLoopError": eventLoopError,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,121 +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 observability
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/ProtonMail/gluon/async"
|
|
||||||
"github.com/ProtonMail/go-proton-api"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
planUnknown = "unknown"
|
|
||||||
planOther = "other"
|
|
||||||
planBusiness = "business"
|
|
||||||
planIndividual = "individual"
|
|
||||||
planGroup = "group"
|
|
||||||
)
|
|
||||||
|
|
||||||
var planHierarchy = map[string]int{ //nolint:gochecknoglobals
|
|
||||||
planBusiness: 4,
|
|
||||||
planGroup: 3,
|
|
||||||
planIndividual: 2,
|
|
||||||
planOther: 1,
|
|
||||||
planUnknown: 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
type planGetter interface {
|
|
||||||
GetOrganizationData(ctx context.Context) (proton.OrganizationResponse, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func isHigherPriority(currentPlan, newPlan string) bool {
|
|
||||||
newRank, ok := planHierarchy[newPlan]
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
currentRank, ok2 := planHierarchy[currentPlan]
|
|
||||||
if !ok2 {
|
|
||||||
return true // we don't have a valid plan, might as well replace it
|
|
||||||
}
|
|
||||||
|
|
||||||
return newRank > currentRank
|
|
||||||
}
|
|
||||||
|
|
||||||
func mapUserPlan(planName string) string {
|
|
||||||
if planName == "" {
|
|
||||||
return planUnknown
|
|
||||||
}
|
|
||||||
switch strings.TrimSpace(strings.ToLower(planName)) {
|
|
||||||
case "mail2022":
|
|
||||||
return planIndividual
|
|
||||||
case "bundle2022":
|
|
||||||
return planIndividual
|
|
||||||
case "family2022":
|
|
||||||
return planGroup
|
|
||||||
case "visionary2022":
|
|
||||||
return planGroup
|
|
||||||
case "mailpro2022":
|
|
||||||
return planBusiness
|
|
||||||
case "planbiz2024":
|
|
||||||
return planBusiness
|
|
||||||
case "bundlepro2022":
|
|
||||||
return planBusiness
|
|
||||||
case "bundlepro2024":
|
|
||||||
return planBusiness
|
|
||||||
case "duo2024":
|
|
||||||
return planGroup
|
|
||||||
|
|
||||||
default:
|
|
||||||
return planOther
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *distinctionUtility) setUserPlan(planName string) {
|
|
||||||
if planName == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
d.userPlanLock.Lock()
|
|
||||||
defer d.userPlanLock.Unlock()
|
|
||||||
|
|
||||||
userPlanMapped := mapUserPlan(planName)
|
|
||||||
if isHigherPriority(d.userPlanUnsafe, userPlanMapped) {
|
|
||||||
d.userPlanUnsafe = userPlanMapped
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *distinctionUtility) registerUserPlan(ctx context.Context, getter planGetter, panicHandler async.PanicHandler) {
|
|
||||||
go func() {
|
|
||||||
defer async.HandlePanic(panicHandler)
|
|
||||||
|
|
||||||
orgRes, err := getter.GetOrganizationData(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
d.setUserPlan(orgRes.Organization.PlanName)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *distinctionUtility) getUserPlanSafe() string {
|
|
||||||
d.userPlanLock.Lock()
|
|
||||||
defer d.userPlanLock.Unlock()
|
|
||||||
return d.userPlanUnsafe
|
|
||||||
}
|
|
||||||
@ -36,18 +36,13 @@ const (
|
|||||||
maxBatchSize = 1000
|
maxBatchSize = 1000
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type PushObsMetricFn func(metric proton.ObservabilityMetric)
|
||||||
|
|
||||||
type client struct {
|
type client struct {
|
||||||
isTelemetryEnabled func(context.Context) bool
|
isTelemetryEnabled func(context.Context) bool
|
||||||
sendMetrics func(context.Context, proton.ObservabilityBatch) error
|
sendMetrics func(context.Context, proton.ObservabilityBatch) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sender - interface maps to the observability service methods,
|
|
||||||
// so we can easily pass them down to relevant components.
|
|
||||||
type Sender interface {
|
|
||||||
AddMetrics(metrics ...proton.ObservabilityMetric)
|
|
||||||
AddDistinctMetrics(errType DistinctionErrorTypeEnum, metrics ...proton.ObservabilityMetric)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
@ -67,8 +62,6 @@ type Service struct {
|
|||||||
|
|
||||||
userClientStore map[string]*client
|
userClientStore map[string]*client
|
||||||
userClientStoreLock sync.Mutex
|
userClientStoreLock sync.Mutex
|
||||||
|
|
||||||
distinctionUtility *distinctionUtility
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(ctx context.Context, panicHandler async.PanicHandler) *Service {
|
func NewService(ctx context.Context, panicHandler async.PanicHandler) *Service {
|
||||||
@ -92,19 +85,11 @@ func NewService(ctx context.Context, panicHandler async.PanicHandler) *Service {
|
|||||||
userClientStore: make(map[string]*client),
|
userClientStore: make(map[string]*client),
|
||||||
}
|
}
|
||||||
|
|
||||||
service.distinctionUtility = newDistinctionUtility(ctx, panicHandler, service)
|
|
||||||
|
|
||||||
return service
|
return service
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run starts the observability service goroutine.
|
func (s *Service) Run() {
|
||||||
// The function also sets some utility functions to a helper struct aimed at differentiating the amount of users sending metric updates.
|
|
||||||
func (s *Service) Run(settingsGetter settingsGetter) {
|
|
||||||
s.log.Info("Starting service")
|
s.log.Info("Starting service")
|
||||||
|
|
||||||
s.distinctionUtility.setSettingsGetter(settingsGetter)
|
|
||||||
s.distinctionUtility.runHeartbeat()
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
s.start()
|
s.start()
|
||||||
}()
|
}()
|
||||||
@ -215,7 +200,7 @@ func (s *Service) scheduleDispatch() {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) addMetrics(metric ...proton.ObservabilityMetric) {
|
func (s *Service) AddMetric(metric proton.ObservabilityMetric) {
|
||||||
s.withMetricStoreLock(func() {
|
s.withMetricStoreLock(func() {
|
||||||
metricStoreLength := len(s.metricStore)
|
metricStoreLength := len(s.metricStore)
|
||||||
if metricStoreLength >= maxStorageSize {
|
if metricStoreLength >= maxStorageSize {
|
||||||
@ -224,32 +209,12 @@ func (s *Service) addMetrics(metric ...proton.ObservabilityMetric) {
|
|||||||
dropCount := metricStoreLength - maxStorageSize + 1
|
dropCount := metricStoreLength - maxStorageSize + 1
|
||||||
s.metricStore = s.metricStore[dropCount:]
|
s.metricStore = s.metricStore[dropCount:]
|
||||||
}
|
}
|
||||||
s.metricStore = append(s.metricStore, metric...)
|
s.metricStore = append(s.metricStore, metric)
|
||||||
})
|
})
|
||||||
|
|
||||||
// If the context has been cancelled i.e. the service has been stopped then we should be free to exit.
|
|
||||||
if s.ctx.Err() != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
s.sendSignal(s.signalDataArrived)
|
s.sendSignal(s.signalDataArrived)
|
||||||
}
|
}
|
||||||
|
|
||||||
// addMetricsIfClients - will append a metric only if there are authenticated clients
|
|
||||||
// via which we can reach the endpoint.
|
|
||||||
func (s *Service) addMetricsIfClients(metric ...proton.ObservabilityMetric) {
|
|
||||||
hasClients := false
|
|
||||||
s.withUserClientStoreLock(func() {
|
|
||||||
hasClients = len(s.userClientStore) > 0
|
|
||||||
})
|
|
||||||
|
|
||||||
if !hasClients {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
s.addMetrics(metric...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) RegisterUserClient(userID string, protonClient *proton.Client, telemetryService *telemetry.Service) {
|
func (s *Service) RegisterUserClient(userID string, protonClient *proton.Client, telemetryService *telemetry.Service) {
|
||||||
s.log.Info("Registering user client, ID:", userID)
|
s.log.Info("Registering user client, ID:", userID)
|
||||||
|
|
||||||
@ -260,8 +225,6 @@ func (s *Service) RegisterUserClient(userID string, protonClient *proton.Client,
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
s.distinctionUtility.registerUserPlan(s.ctx, protonClient, s.panicHandler)
|
|
||||||
|
|
||||||
// There may be a case where we already have metric updates stored, so try to flush;
|
// There may be a case where we already have metric updates stored, so try to flush;
|
||||||
s.sendSignal(s.signalDataArrived)
|
s.sendSignal(s.signalDataArrived)
|
||||||
}
|
}
|
||||||
@ -316,25 +279,3 @@ func (s *Service) sendSignal(channel chan struct{}) {
|
|||||||
func ModifyThrottlePeriod(duration time.Duration) {
|
func ModifyThrottlePeriod(duration time.Duration) {
|
||||||
throttleDuration = duration
|
throttleDuration = duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) AddMetrics(metrics ...proton.ObservabilityMetric) {
|
|
||||||
s.addMetrics(metrics...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddDistinctMetrics - sends an additional metric related to the user, so we can determine
|
|
||||||
// what number of events come from what number of users.
|
|
||||||
// As the binning interval is what allows us to do this we
|
|
||||||
// should not send these if there are no logged-in users at that moment.
|
|
||||||
func (s *Service) AddDistinctMetrics(errType DistinctionErrorTypeEnum, metrics ...proton.ObservabilityMetric) {
|
|
||||||
metrics = s.distinctionUtility.generateDistinctMetrics(errType, metrics...)
|
|
||||||
s.addMetricsIfClients(metrics...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ModifyHeartbeatInterval - should only be used for testing. Resets the heartbeat ticker.
|
|
||||||
func (s *Service) ModifyHeartbeatInterval(duration time.Duration) {
|
|
||||||
s.distinctionUtility.heartbeatTicker.Reset(duration)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ModifyUserMetricInterval(duration time.Duration) {
|
|
||||||
updateInterval = duration
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,106 +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 observability
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/ProtonMail/go-proton-api"
|
|
||||||
)
|
|
||||||
|
|
||||||
func GenerateAllUsedDistinctionMetricPermutations() []proton.ObservabilityMetric {
|
|
||||||
planValues := []string{
|
|
||||||
planUnknown,
|
|
||||||
planOther,
|
|
||||||
planBusiness,
|
|
||||||
planIndividual,
|
|
||||||
planGroup}
|
|
||||||
mailClientValues := []string{
|
|
||||||
emailAgentAppleMail,
|
|
||||||
emailAgentOutlook,
|
|
||||||
emailAgentThunderbird,
|
|
||||||
emailAgentOther,
|
|
||||||
emailAgentUnknown,
|
|
||||||
}
|
|
||||||
enabledValues := []string{
|
|
||||||
getEnabled(true), getEnabled(false),
|
|
||||||
}
|
|
||||||
|
|
||||||
var metrics []proton.ObservabilityMetric
|
|
||||||
|
|
||||||
for _, schemaName := range errorSchemaMap {
|
|
||||||
for _, plan := range planValues {
|
|
||||||
for _, mailClient := range mailClientValues {
|
|
||||||
for _, dohEnabled := range enabledValues {
|
|
||||||
for _, betaAccess := range enabledValues {
|
|
||||||
metrics = append(metrics, generateUserMetric(schemaName, plan, mailClient, dohEnabled, betaAccess))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return metrics
|
|
||||||
}
|
|
||||||
|
|
||||||
func GenerateAllHeartbeatMetricPermutations() []proton.ObservabilityMetric {
|
|
||||||
planValues := []string{
|
|
||||||
planUnknown,
|
|
||||||
planOther,
|
|
||||||
planBusiness,
|
|
||||||
planIndividual,
|
|
||||||
planGroup}
|
|
||||||
mailClientValues := []string{
|
|
||||||
emailAgentAppleMail,
|
|
||||||
emailAgentOutlook,
|
|
||||||
emailAgentThunderbird,
|
|
||||||
emailAgentOther,
|
|
||||||
emailAgentUnknown,
|
|
||||||
}
|
|
||||||
enabledValues := []string{
|
|
||||||
getEnabled(true), getEnabled(false),
|
|
||||||
}
|
|
||||||
|
|
||||||
trueFalseValues := []string{
|
|
||||||
"true", "false",
|
|
||||||
}
|
|
||||||
|
|
||||||
var metrics []proton.ObservabilityMetric
|
|
||||||
for _, plan := range planValues {
|
|
||||||
for _, mailClient := range mailClientValues {
|
|
||||||
for _, dohEnabled := range enabledValues {
|
|
||||||
for _, betaAccess := range enabledValues {
|
|
||||||
for _, receivedOtherError := range trueFalseValues {
|
|
||||||
for _, receivedSyncError := range trueFalseValues {
|
|
||||||
for _, receivedEventLoopError := range trueFalseValues {
|
|
||||||
metrics = append(metrics,
|
|
||||||
generateHeartbeatMetric(plan,
|
|
||||||
mailClient,
|
|
||||||
dohEnabled,
|
|
||||||
betaAccess,
|
|
||||||
receivedOtherError,
|
|
||||||
receivedSyncError,
|
|
||||||
receivedEventLoopError,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return metrics
|
|
||||||
}
|
|
||||||
@ -1,68 +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 observability
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
|
||||||
)
|
|
||||||
|
|
||||||
// settingsGetter - interface that maps to bridge object methods such that we
|
|
||||||
// can pass the whole object instead of individual function callbacks.
|
|
||||||
type settingsGetter interface {
|
|
||||||
GetCurrentUserAgent() string
|
|
||||||
GetProxyAllowed() bool
|
|
||||||
GetUpdateChannel() updater.Channel
|
|
||||||
}
|
|
||||||
|
|
||||||
// User agent mapping.
|
|
||||||
const (
|
|
||||||
emailAgentAppleMail = "apple_mail"
|
|
||||||
emailAgentOutlook = "outlook"
|
|
||||||
emailAgentThunderbird = "thunderbird"
|
|
||||||
emailAgentOther = "other"
|
|
||||||
emailAgentUnknown = "unknown"
|
|
||||||
)
|
|
||||||
|
|
||||||
func matchUserAgent(userAgent string) string {
|
|
||||||
if userAgent == "" {
|
|
||||||
return emailAgentUnknown
|
|
||||||
}
|
|
||||||
|
|
||||||
userAgent = strings.ToLower(userAgent)
|
|
||||||
switch {
|
|
||||||
case strings.Contains(userAgent, "outlook"):
|
|
||||||
return emailAgentOutlook
|
|
||||||
case strings.Contains(userAgent, "thunderbird"):
|
|
||||||
return emailAgentThunderbird
|
|
||||||
case strings.Contains(userAgent, "mac") && strings.Contains(userAgent, "mail"):
|
|
||||||
return emailAgentAppleMail
|
|
||||||
case strings.Contains(userAgent, "mac") && strings.Contains(userAgent, "notes"):
|
|
||||||
return emailAgentUnknown
|
|
||||||
default:
|
|
||||||
return emailAgentOther
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getEnabled(value bool) string {
|
|
||||||
if !value {
|
|
||||||
return "disabled"
|
|
||||||
}
|
|
||||||
return "enabled"
|
|
||||||
}
|
|
||||||
@ -1,111 +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 observability
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMatchUserAgent(t *testing.T) {
|
|
||||||
type agentParseResult struct {
|
|
||||||
agent string
|
|
||||||
result string
|
|
||||||
}
|
|
||||||
|
|
||||||
testCases := []agentParseResult{
|
|
||||||
{
|
|
||||||
agent: "Microsoft Outlook/16.0.17928.20114 (Windows 11 Version 23H2)",
|
|
||||||
result: emailAgentOutlook,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
agent: "Mailbird/3.0.18.0 (Windows 11 Version 23H2)",
|
|
||||||
result: emailAgentOther,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
agent: "Microsoft Outlook/16.0.17830.20166 (Windows 11 Version 23H2)",
|
|
||||||
result: emailAgentOutlook,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
agent: "Mac OS X Mail/16.0-3776.700.51 (macOS 14.6)",
|
|
||||||
result: emailAgentAppleMail,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
agent: "/ (Windows 11 Version 23H2)",
|
|
||||||
result: emailAgentOther,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
agent: "Microsoft Outlook for Mac/16.88.0-BUILDDAY (macOS 14.6)",
|
|
||||||
result: emailAgentOutlook,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
agent: "/ (macOS 14.5)",
|
|
||||||
result: emailAgentOther,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
agent: "/ (Freedesktop SDK 23.08 (Flatpak runtime))",
|
|
||||||
result: emailAgentOther,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
agent: "Mac OS X Mail/16.0-3774.600.62 (macOS 14.5)",
|
|
||||||
result: emailAgentAppleMail,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
agent: "Mac OS X Notes/4.11-2817 (macOS 14.6)",
|
|
||||||
result: emailAgentUnknown,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
agent: "NoClient/0.0.1 (macOS 14.6)",
|
|
||||||
result: emailAgentOther,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
agent: "Thunderbird/115.15.0 (Ubuntu 20.04.6 LTS)",
|
|
||||||
result: emailAgentThunderbird,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
agent: "Thunderbird/115.14.0 (macOS 14.6)",
|
|
||||||
result: emailAgentThunderbird,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
agent: "Thunderbird/115.10.2 (Windows 11 Version 23H2)",
|
|
||||||
result: emailAgentThunderbird,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
agent: "Mac OS X Notes/4.9-1965 (macOS Monterey (12.0))",
|
|
||||||
result: emailAgentUnknown,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
agent: " Thunderbird/115.14.0 (macOS 14.6) ",
|
|
||||||
result: emailAgentThunderbird,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
agent: "",
|
|
||||||
result: emailAgentUnknown,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, testCase := range testCases {
|
|
||||||
require.Equal(t, testCase.result, matchUserAgent(testCase.agent))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFormatBool(t *testing.T) {
|
|
||||||
require.Equal(t, "false", formatBool(false))
|
|
||||||
require.Equal(t, "true", formatBool(true))
|
|
||||||
}
|
|
||||||
@ -30,6 +30,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ProtonMail/gluon/async"
|
"github.com/ProtonMail/gluon/async"
|
||||||
|
"github.com/ProtonMail/gluon/reporter"
|
||||||
"github.com/ProtonMail/gluon/rfc5322"
|
"github.com/ProtonMail/gluon/rfc5322"
|
||||||
"github.com/ProtonMail/gluon/rfc822"
|
"github.com/ProtonMail/gluon/rfc822"
|
||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
@ -196,7 +197,13 @@ func (s *Service) sendWithKey(
|
|||||||
}
|
}
|
||||||
parentID, draftsToDelete, err := getParentID(ctx, s.client, authAddrID, addrMode, references)
|
parentID, draftsToDelete, err := getParentID(ctx, s.client, authAddrID, addrMode, references)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Sentry event has been removed; should be replaced with observability - BRIDGE-206.
|
if err := s.reporter.ReportMessageWithContext("Failed to get parent ID", reporter.Context{
|
||||||
|
"error": err,
|
||||||
|
"references": message.References,
|
||||||
|
}); err != nil {
|
||||||
|
logrus.WithError(err).Error("Failed to report error")
|
||||||
|
}
|
||||||
|
|
||||||
s.log.WithError(err).Warn("Failed to get parent ID")
|
s.log.WithError(err).Warn("Failed to get parent ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -160,6 +160,10 @@ func (s *childJob) onError(err error) {
|
|||||||
s.job.onError(err)
|
s.job.onError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *childJob) userID() string {
|
||||||
|
return s.job.userID
|
||||||
|
}
|
||||||
|
|
||||||
func (s *childJob) chunkDivide(chunks [][]proton.FullMessage) []childJob {
|
func (s *childJob) chunkDivide(chunks [][]proton.FullMessage) []childJob {
|
||||||
numChunks := len(chunks)
|
numChunks := len(chunks)
|
||||||
|
|
||||||
|
|||||||
@ -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 observabilitymetrics
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ProtonMail/go-proton-api"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
errorCaseSchemaName = "bridge_sync_message_build_errors_total"
|
|
||||||
errorCaseSchemaVersion = 1
|
|
||||||
successCaseSchemaName = "bridge_sync_message_build_success_total"
|
|
||||||
successCaseSchemaVersion = 1
|
|
||||||
)
|
|
||||||
|
|
||||||
func generateStageBuildFailureObservabilityMetric(errorType string) proton.ObservabilityMetric {
|
|
||||||
return proton.ObservabilityMetric{
|
|
||||||
Name: errorCaseSchemaName,
|
|
||||||
Version: errorCaseSchemaVersion,
|
|
||||||
Timestamp: time.Now().Unix(),
|
|
||||||
Data: map[string]interface{}{
|
|
||||||
"Value": 1,
|
|
||||||
"Labels": map[string]string{
|
|
||||||
"errorType": errorType,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func GenerateNoUnlockedKeyringMetric() proton.ObservabilityMetric {
|
|
||||||
return generateStageBuildFailureObservabilityMetric("noUnlockedKeyring")
|
|
||||||
}
|
|
||||||
|
|
||||||
func GenerateFailedToBuildMetric() proton.ObservabilityMetric {
|
|
||||||
return generateStageBuildFailureObservabilityMetric("failedToBuild")
|
|
||||||
}
|
|
||||||
|
|
||||||
// GenerateMessageBuiltSuccessMetric - Maybe this is incorrect, I'm not sure how metrics with no labels
|
|
||||||
// should be dealt with. The integration tests will tell us.
|
|
||||||
func GenerateMessageBuiltSuccessMetric() proton.ObservabilityMetric {
|
|
||||||
return proton.ObservabilityMetric{
|
|
||||||
Name: successCaseSchemaName,
|
|
||||||
Version: successCaseSchemaVersion,
|
|
||||||
Timestamp: time.Now().Unix(),
|
|
||||||
Data: map[string]interface{}{
|
|
||||||
"Value": 1,
|
|
||||||
"Labels": map[string]string{},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -21,7 +21,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/ProtonMail/gluon/async"
|
"github.com/ProtonMail/gluon/async"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
|
"github.com/ProtonMail/gluon/reporter"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Service which mediates IMAP syncing in Bridge.
|
// Service which mediates IMAP syncing in Bridge.
|
||||||
@ -36,9 +36,8 @@ type Service struct {
|
|||||||
group *async.Group
|
group *async.Group
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(
|
func NewService(reporter reporter.Reporter,
|
||||||
panicHandler async.PanicHandler,
|
panicHandler async.PanicHandler,
|
||||||
observabilitySender observability.Sender,
|
|
||||||
) *Service {
|
) *Service {
|
||||||
limits := newSyncLimits(2 * Gigabyte)
|
limits := newSyncLimits(2 * Gigabyte)
|
||||||
|
|
||||||
@ -51,7 +50,7 @@ func NewService(
|
|||||||
limits: limits,
|
limits: limits,
|
||||||
metadataStage: NewMetadataStage(metaCh, downloadCh, limits.DownloadRequestMem, panicHandler),
|
metadataStage: NewMetadataStage(metaCh, downloadCh, limits.DownloadRequestMem, panicHandler),
|
||||||
downloadStage: NewDownloadStage(downloadCh, buildCh, limits.MaxParallelDownloads, panicHandler),
|
downloadStage: NewDownloadStage(downloadCh, buildCh, limits.MaxParallelDownloads, panicHandler),
|
||||||
buildStage: NewBuildStage(buildCh, applyCh, limits.MessageBuildMem, panicHandler, observabilitySender),
|
buildStage: NewBuildStage(buildCh, applyCh, limits.MessageBuildMem, panicHandler, reporter),
|
||||||
applyStage: NewApplyStage(applyCh),
|
applyStage: NewApplyStage(applyCh),
|
||||||
metaCh: metaCh,
|
metaCh: metaCh,
|
||||||
group: async.NewGroup(context.Background(), panicHandler),
|
group: async.NewGroup(context.Background(), panicHandler),
|
||||||
|
|||||||
@ -26,10 +26,9 @@ import (
|
|||||||
|
|
||||||
"github.com/ProtonMail/gluon/async"
|
"github.com/ProtonMail/gluon/async"
|
||||||
"github.com/ProtonMail/gluon/logging"
|
"github.com/ProtonMail/gluon/logging"
|
||||||
|
"github.com/ProtonMail/gluon/reporter"
|
||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
|
|
||||||
obsMetrics "github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice/observabilitymetrics"
|
|
||||||
"github.com/bradenaw/juniper/parallel"
|
"github.com/bradenaw/juniper/parallel"
|
||||||
"github.com/bradenaw/juniper/xslices"
|
"github.com/bradenaw/juniper/xslices"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
@ -51,10 +50,8 @@ type BuildStage struct {
|
|||||||
maxBuildMem uint64
|
maxBuildMem uint64
|
||||||
|
|
||||||
panicHandler async.PanicHandler
|
panicHandler async.PanicHandler
|
||||||
|
reporter reporter.Reporter
|
||||||
log *logrus.Entry
|
log *logrus.Entry
|
||||||
|
|
||||||
// Observability
|
|
||||||
observabilitySender observability.Sender
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBuildStage(
|
func NewBuildStage(
|
||||||
@ -62,15 +59,15 @@ func NewBuildStage(
|
|||||||
output BuildStageOutput,
|
output BuildStageOutput,
|
||||||
maxBuildMem uint64,
|
maxBuildMem uint64,
|
||||||
panicHandler async.PanicHandler,
|
panicHandler async.PanicHandler,
|
||||||
observabilitySender observability.Sender,
|
reporter reporter.Reporter,
|
||||||
) *BuildStage {
|
) *BuildStage {
|
||||||
return &BuildStage{
|
return &BuildStage{
|
||||||
input: input,
|
input: input,
|
||||||
output: output,
|
output: output,
|
||||||
maxBuildMem: maxBuildMem,
|
maxBuildMem: maxBuildMem,
|
||||||
log: logrus.WithField("sync-stage", "build"),
|
log: logrus.WithField("sync-stage", "build"),
|
||||||
panicHandler: panicHandler,
|
panicHandler: panicHandler,
|
||||||
observabilitySender: observabilitySender,
|
reporter: reporter,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,24 +147,35 @@ func (b *BuildStage) run(ctx context.Context) {
|
|||||||
req.job.log.WithError(err).Error("Failed to add failed message ID")
|
req.job.log.WithError(err).Error("Failed to add failed message ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
b.observabilitySender.AddDistinctMetrics(observability.SyncError, obsMetrics.GenerateNoUnlockedKeyringMetric())
|
if err := b.reporter.ReportMessageWithContext("Failed to build message - no unlocked keyring (sync)", reporter.Context{
|
||||||
|
"messageID": msg.ID,
|
||||||
|
"userID": req.userID(),
|
||||||
|
}); err != nil {
|
||||||
|
req.job.log.WithError(err).Error("Failed to report message build error")
|
||||||
|
}
|
||||||
return BuildResult{}, nil
|
return BuildResult{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := req.job.messageBuilder.BuildMessage(req.job.labels, msg, kr, new(bytes.Buffer))
|
res, err := req.job.messageBuilder.BuildMessage(req.job.labels, msg, kr, new(bytes.Buffer))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
req.job.log.WithError(err).WithField("msgID", msg.ID).Error("Failed to build message (sync)")
|
req.job.log.WithError(err).WithField("msgID", msg.ID).Error("Failed to build message (syn)")
|
||||||
|
|
||||||
if err := req.job.state.AddFailedMessageID(req.getContext(), msg.ID); err != nil {
|
if err := req.job.state.AddFailedMessageID(req.getContext(), msg.ID); err != nil {
|
||||||
req.job.log.WithError(err).Error("Failed to add failed message ID")
|
req.job.log.WithError(err).Error("Failed to add failed message ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
b.observabilitySender.AddDistinctMetrics(observability.SyncError, obsMetrics.GenerateFailedToBuildMetric())
|
if err := b.reporter.ReportMessageWithContext("Failed to build message (sync)", reporter.Context{
|
||||||
|
"messageID": msg.ID,
|
||||||
|
"error": err,
|
||||||
|
"userID": req.userID(),
|
||||||
|
}); err != nil {
|
||||||
|
req.job.log.WithError(err).Error("Failed to report message build error")
|
||||||
|
}
|
||||||
|
|
||||||
// We could sync a placeholder message here, but for now we skip it entirely.
|
// We could sync a placeholder message here, but for now we skip it entirely.
|
||||||
return BuildResult{}, nil
|
return BuildResult{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
b.observabilitySender.AddMetrics(obsMetrics.GenerateMessageBuiltSuccessMetric())
|
|
||||||
return res, nil
|
return res, nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -24,11 +24,10 @@ import (
|
|||||||
|
|
||||||
"github.com/ProtonMail/gluon/async"
|
"github.com/ProtonMail/gluon/async"
|
||||||
"github.com/ProtonMail/gluon/imap"
|
"github.com/ProtonMail/gluon/imap"
|
||||||
|
"github.com/ProtonMail/gluon/reporter"
|
||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge/mocks"
|
"github.com/ProtonMail/proton-bridge/v3/internal/bridge/mocks"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
|
|
||||||
obsMetrics "github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice/observabilitymetrics"
|
|
||||||
"github.com/bradenaw/juniper/xslices"
|
"github.com/bradenaw/juniper/xslices"
|
||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@ -68,6 +67,7 @@ func TestBuildStage_SuccessRemovesFailedMessage(t *testing.T) {
|
|||||||
|
|
||||||
input := NewChannelConsumerProducer[BuildRequest]()
|
input := NewChannelConsumerProducer[BuildRequest]()
|
||||||
output := NewChannelConsumerProducer[ApplyRequest]()
|
output := NewChannelConsumerProducer[ApplyRequest]()
|
||||||
|
reporter := mocks.NewMockReporter(mockCtrl)
|
||||||
|
|
||||||
labels := getTestLabels()
|
labels := getTestLabels()
|
||||||
|
|
||||||
@ -105,10 +105,7 @@ func TestBuildStage_SuccessRemovesFailedMessage(t *testing.T) {
|
|||||||
tj.messageBuilder.EXPECT().BuildMessage(gomock.Eq(labels), gomock.Eq(msg), gomock.Any(), gomock.Any()).Return(buildResult, nil)
|
tj.messageBuilder.EXPECT().BuildMessage(gomock.Eq(labels), gomock.Eq(msg), gomock.Any(), gomock.Any()).Return(buildResult, nil)
|
||||||
tj.state.EXPECT().RemFailedMessageID(gomock.Any(), gomock.Eq("MSG"))
|
tj.state.EXPECT().RemFailedMessageID(gomock.Any(), gomock.Eq("MSG"))
|
||||||
|
|
||||||
observabilityService := mocks.NewMockObservabilitySender(mockCtrl)
|
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, reporter)
|
||||||
observabilityService.EXPECT().AddMetrics(obsMetrics.GenerateMessageBuiltSuccessMetric())
|
|
||||||
|
|
||||||
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, observabilityService)
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
stage.run(ctx)
|
stage.run(ctx)
|
||||||
@ -128,7 +125,7 @@ func TestBuildStage_BuildFailureIsReportedButDoesNotCancelJob(t *testing.T) {
|
|||||||
|
|
||||||
input := NewChannelConsumerProducer[BuildRequest]()
|
input := NewChannelConsumerProducer[BuildRequest]()
|
||||||
output := NewChannelConsumerProducer[ApplyRequest]()
|
output := NewChannelConsumerProducer[ApplyRequest]()
|
||||||
mockObservabilityService := mocks.NewMockObservabilitySender(mockCtrl)
|
mockReporter := mocks.NewMockReporter(mockCtrl)
|
||||||
|
|
||||||
labels := getTestLabels()
|
labels := getTestLabels()
|
||||||
|
|
||||||
@ -159,12 +156,15 @@ func TestBuildStage_BuildFailureIsReportedButDoesNotCancelJob(t *testing.T) {
|
|||||||
|
|
||||||
tj.messageBuilder.EXPECT().BuildMessage(gomock.Eq(labels), gomock.Eq(msg), gomock.Any(), gomock.Any()).Return(BuildResult{}, buildError)
|
tj.messageBuilder.EXPECT().BuildMessage(gomock.Eq(labels), gomock.Eq(msg), gomock.Any(), gomock.Any()).Return(BuildResult{}, buildError)
|
||||||
tj.state.EXPECT().AddFailedMessageID(gomock.Any(), gomock.Eq([]string{"MSG"}))
|
tj.state.EXPECT().AddFailedMessageID(gomock.Any(), gomock.Eq([]string{"MSG"}))
|
||||||
|
mockReporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Eq(reporter.Context{
|
||||||
|
"userID": "u",
|
||||||
|
"messageID": "MSG",
|
||||||
|
"error": buildError,
|
||||||
|
})).Return(nil)
|
||||||
|
|
||||||
tj.syncReporter.EXPECT().OnProgress(gomock.Any(), gomock.Eq(int64(10)))
|
tj.syncReporter.EXPECT().OnProgress(gomock.Any(), gomock.Eq(int64(10)))
|
||||||
|
|
||||||
mockObservabilityService.EXPECT().AddDistinctMetrics(observability.SyncError, obsMetrics.GenerateNoUnlockedKeyringMetric())
|
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, mockReporter)
|
||||||
|
|
||||||
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, mockObservabilityService)
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
stage.run(ctx)
|
stage.run(ctx)
|
||||||
@ -183,6 +183,7 @@ func TestBuildStage_FailedToLocateKeyRingIsReportedButDoesNotFailBuild(t *testin
|
|||||||
|
|
||||||
input := NewChannelConsumerProducer[BuildRequest]()
|
input := NewChannelConsumerProducer[BuildRequest]()
|
||||||
output := NewChannelConsumerProducer[ApplyRequest]()
|
output := NewChannelConsumerProducer[ApplyRequest]()
|
||||||
|
mockReporter := mocks.NewMockReporter(mockCtrl)
|
||||||
|
|
||||||
labels := getTestLabels()
|
labels := getTestLabels()
|
||||||
|
|
||||||
@ -208,13 +209,14 @@ func TestBuildStage_FailedToLocateKeyRingIsReportedButDoesNotFailBuild(t *testin
|
|||||||
tj.job.end()
|
tj.job.end()
|
||||||
|
|
||||||
tj.state.EXPECT().AddFailedMessageID(gomock.Any(), gomock.Eq([]string{"MSG"}))
|
tj.state.EXPECT().AddFailedMessageID(gomock.Any(), gomock.Eq([]string{"MSG"}))
|
||||||
|
mockReporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Eq(reporter.Context{
|
||||||
|
"userID": "u",
|
||||||
|
"messageID": "MSG",
|
||||||
|
})).Return(nil)
|
||||||
|
|
||||||
tj.syncReporter.EXPECT().OnProgress(gomock.Any(), gomock.Eq(int64(10)))
|
tj.syncReporter.EXPECT().OnProgress(gomock.Any(), gomock.Eq(int64(10)))
|
||||||
|
|
||||||
observabilitySender := mocks.NewMockObservabilitySender(mockCtrl)
|
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, mockReporter)
|
||||||
observabilitySender.EXPECT().AddDistinctMetrics(observability.SyncError)
|
|
||||||
|
|
||||||
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, observabilitySender)
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
stage.run(ctx)
|
stage.run(ctx)
|
||||||
@ -233,6 +235,7 @@ func TestBuildStage_OtherErrorsFailJob(t *testing.T) {
|
|||||||
|
|
||||||
input := NewChannelConsumerProducer[BuildRequest]()
|
input := NewChannelConsumerProducer[BuildRequest]()
|
||||||
output := NewChannelConsumerProducer[ApplyRequest]()
|
output := NewChannelConsumerProducer[ApplyRequest]()
|
||||||
|
mockReporter := mocks.NewMockReporter(mockCtrl)
|
||||||
|
|
||||||
labels := getTestLabels()
|
labels := getTestLabels()
|
||||||
|
|
||||||
@ -258,7 +261,7 @@ func TestBuildStage_OtherErrorsFailJob(t *testing.T) {
|
|||||||
childJob := tj.job.newChildJob("f", 10)
|
childJob := tj.job.newChildJob("f", 10)
|
||||||
tj.job.end()
|
tj.job.end()
|
||||||
|
|
||||||
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, mocks.NewMockObservabilitySender(mockCtrl))
|
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, mockReporter)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
stage.run(ctx)
|
stage.run(ctx)
|
||||||
@ -280,6 +283,7 @@ func TestBuildStage_CancelledJobIsDiscarded(t *testing.T) {
|
|||||||
|
|
||||||
input := NewChannelConsumerProducer[BuildRequest]()
|
input := NewChannelConsumerProducer[BuildRequest]()
|
||||||
output := NewChannelConsumerProducer[ApplyRequest]()
|
output := NewChannelConsumerProducer[ApplyRequest]()
|
||||||
|
mockReporter := mocks.NewMockReporter(mockCtrl)
|
||||||
|
|
||||||
msg := proton.FullMessage{
|
msg := proton.FullMessage{
|
||||||
Message: proton.Message{
|
Message: proton.Message{
|
||||||
@ -290,7 +294,7 @@ func TestBuildStage_CancelledJobIsDiscarded(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, mocks.NewMockObservabilitySender(mockCtrl))
|
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, mockReporter)
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
@ -323,6 +327,7 @@ func TestTask_EmptyInputDoesNotCrash(t *testing.T) {
|
|||||||
|
|
||||||
input := NewChannelConsumerProducer[BuildRequest]()
|
input := NewChannelConsumerProducer[BuildRequest]()
|
||||||
output := NewChannelConsumerProducer[ApplyRequest]()
|
output := NewChannelConsumerProducer[ApplyRequest]()
|
||||||
|
reporter := mocks.NewMockReporter(mockCtrl)
|
||||||
|
|
||||||
labels := getTestLabels()
|
labels := getTestLabels()
|
||||||
|
|
||||||
@ -335,7 +340,7 @@ func TestTask_EmptyInputDoesNotCrash(t *testing.T) {
|
|||||||
childJob := tj.job.newChildJob("f", 10)
|
childJob := tj.job.newChildJob("f", 10)
|
||||||
tj.job.end()
|
tj.job.end()
|
||||||
|
|
||||||
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, mocks.NewMockObservabilitySender(mockCtrl))
|
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, reporter)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
stage.run(ctx)
|
stage.run(ctx)
|
||||||
|
|||||||
@ -111,7 +111,7 @@ func (s *Service) readCacheFile() {
|
|||||||
|
|
||||||
file, err := os.Open(s.cacheFilepath)
|
file, err := os.Open(s.cacheFilepath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.log.WithError(err).Info("Unable to open cache file")
|
s.log.WithError(err).Error("Unable to open cache file")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -101,7 +101,7 @@ func (u *Updater) InstallUpdate(ctx context.Context, downloader Downloader, upda
|
|||||||
update.Package+".sig",
|
update.Package+".sig",
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: %w", ErrDownloadVerify, err)
|
return ErrDownloadVerify
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := u.installer.InstallUpdate(update.Version, bytes.NewReader(b)); err != nil {
|
if err := u.installer.InstallUpdate(update.Version, bytes.NewReader(b)); err != nil {
|
||||||
|
|||||||
@ -115,6 +115,7 @@ func New(
|
|||||||
isNew bool,
|
isNew bool,
|
||||||
notificationStore *notifications.Store,
|
notificationStore *notifications.Store,
|
||||||
getFlagValFn unleash.GetFlagValueFn,
|
getFlagValFn unleash.GetFlagValueFn,
|
||||||
|
pushObservabilityMetric observability.PushObsMetricFn,
|
||||||
) (*User, error) {
|
) (*User, error) {
|
||||||
user, err := newImpl(
|
user, err := newImpl(
|
||||||
ctx,
|
ctx,
|
||||||
@ -136,6 +137,7 @@ func New(
|
|||||||
isNew,
|
isNew,
|
||||||
notificationStore,
|
notificationStore,
|
||||||
getFlagValFn,
|
getFlagValFn,
|
||||||
|
pushObservabilityMetric,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Cleanup any pending resources on error
|
// Cleanup any pending resources on error
|
||||||
@ -170,6 +172,7 @@ func newImpl(
|
|||||||
isNew bool,
|
isNew bool,
|
||||||
notificationStore *notifications.Store,
|
notificationStore *notifications.Store,
|
||||||
getFlagValueFn unleash.GetFlagValueFn,
|
getFlagValueFn unleash.GetFlagValueFn,
|
||||||
|
pushObservabilityMetric observability.PushObsMetricFn,
|
||||||
) (*User, error) {
|
) (*User, error) {
|
||||||
logrus.WithField("userID", apiUser.ID).Info("Creating new user")
|
logrus.WithField("userID", apiUser.ID).Info("Creating new user")
|
||||||
|
|
||||||
@ -285,10 +288,9 @@ func newImpl(
|
|||||||
syncConfigDir,
|
syncConfigDir,
|
||||||
user.maxSyncMemory,
|
user.maxSyncMemory,
|
||||||
showAllMail,
|
showAllMail,
|
||||||
observabilityService,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
user.notificationService = notifications.NewService(user.id, user.eventService, user, notificationStore, getFlagValueFn, observabilityService)
|
user.notificationService = notifications.NewService(user.id, user.eventService, user, notificationStore, getFlagValueFn, pushObservabilityMetric)
|
||||||
|
|
||||||
// Check for status_progress when triggered.
|
// Check for status_progress when triggered.
|
||||||
user.goStatusProgress = user.tasks.PeriodicOrTrigger(configstatus.ProgressCheckInterval, 0, func(ctx context.Context) {
|
user.goStatusProgress = user.tasks.PeriodicOrTrigger(configstatus.ProgressCheckInterval, 0, func(ctx context.Context) {
|
||||||
|
|||||||
@ -175,6 +175,7 @@ func withUser(tb testing.TB, ctx context.Context, _ *server.Server, m *proton.Ma
|
|||||||
func(_ string) bool {
|
func(_ string) bool {
|
||||||
return false
|
return false
|
||||||
},
|
},
|
||||||
|
func(_ proton.ObservabilityMetric) {},
|
||||||
)
|
)
|
||||||
require.NoError(tb, err)
|
require.NoError(tb, err)
|
||||||
defer user.Close()
|
defer user.Close()
|
||||||
|
|||||||
@ -19,7 +19,6 @@ package vault
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/bradenaw/juniper/xslices"
|
"github.com/bradenaw/juniper/xslices"
|
||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
@ -38,10 +37,6 @@ func (user *User) Username() string {
|
|||||||
return user.vault.getUser(user.userID).Username
|
return user.vault.getUser(user.userID).Username
|
||||||
}
|
}
|
||||||
|
|
||||||
func (user *User) usernameUnsafe() string {
|
|
||||||
return user.vault.getUserUnsafe(user.userID).Username
|
|
||||||
}
|
|
||||||
|
|
||||||
// PrimaryEmail returns the user's primary email address.
|
// PrimaryEmail returns the user's primary email address.
|
||||||
func (user *User) PrimaryEmail() string {
|
func (user *User) PrimaryEmail() string {
|
||||||
return user.vault.getUser(user.userID).PrimaryEmail
|
return user.vault.getUser(user.userID).PrimaryEmail
|
||||||
@ -247,15 +242,3 @@ func (user *User) SetShouldSync(shouldResync bool) error {
|
|||||||
func (user *User) GetShouldResync() bool {
|
func (user *User) GetShouldResync() bool {
|
||||||
return user.vault.getUser(user.userID).ShouldResync
|
return user.vault.getUser(user.userID).ShouldResync
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateUsernameUnsafe - updates the username of the relevant user, provided that the new username is not empty
|
|
||||||
// and differs from the previous. Writes are not performed if this case is not met.
|
|
||||||
// Should only be called from contexts where the vault mutex is already locked.
|
|
||||||
func (user *User) updateUsernameUnsafe(username string) error {
|
|
||||||
if strings.TrimSpace(username) == "" || user.usernameUnsafe() == username {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return user.vault.modUserUnsafe(user.userID, func(userData *UserData) {
|
|
||||||
userData.Username = username
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@ -240,10 +240,6 @@ func (vault *Vault) GetOrAddUser(userID, username, primaryEmail, authUID, authRe
|
|||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := user.updateUsernameUnsafe(username); err != nil {
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return user, false, nil
|
return user, false, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -454,22 +450,6 @@ func (vault *Vault) getUser(userID string) UserData {
|
|||||||
return users[idx]
|
return users[idx]
|
||||||
}
|
}
|
||||||
|
|
||||||
// getUserUnsafe - fetches the relevant UserData.
|
|
||||||
// Should only be called from contexts in which the vault mutex has been read locked.
|
|
||||||
func (vault *Vault) getUserUnsafe(userID string) UserData {
|
|
||||||
users := vault.getUnsafe().Users
|
|
||||||
|
|
||||||
idx := xslices.IndexFunc(users, func(user UserData) bool {
|
|
||||||
return user.UserID == userID
|
|
||||||
})
|
|
||||||
|
|
||||||
if idx < 0 {
|
|
||||||
panic("Unknown user")
|
|
||||||
}
|
|
||||||
|
|
||||||
return users[idx]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (vault *Vault) modUser(userID string, fn func(userData *UserData)) error {
|
func (vault *Vault) modUser(userID string, fn func(userData *UserData)) error {
|
||||||
vault.lock.Lock()
|
vault.lock.Lock()
|
||||||
defer vault.lock.Unlock()
|
defer vault.lock.Unlock()
|
||||||
|
|||||||
@ -857,23 +857,6 @@ func getFileReader(filename string) io.Reader {
|
|||||||
return f
|
return f
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseInvalidOriginalBoundary(t *testing.T) {
|
|
||||||
f := getFileReader("incorrect_boundary_w_invalid_character_tuta.eml")
|
|
||||||
|
|
||||||
p, err := parser.New(f)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.Equal(t, true, p.Root().Header.Get("Content-Type") == `multipart/related; boundary="------------1234567890@tutanota"`)
|
|
||||||
|
|
||||||
m, err := ParseWithParser(p, false)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.Equal(t, true, strings.HasPrefix(string(m.MIMEBody), "Content-Type: multipart/related;\r\n boundary="))
|
|
||||||
require.Equal(t, false, strings.HasPrefix(string(m.MIMEBody), `Content-Type: multipart/related;\n boundary="------------1234567890@tutanota"`))
|
|
||||||
require.Equal(t, false, strings.HasPrefix(string(m.MIMEBody), `Content-Type: multipart/related;\n boundary=------------1234567890@tutanota`))
|
|
||||||
require.Equal(t, false, strings.HasPrefix(string(m.MIMEBody), `Content-Type: multipart/related;\n boundary=1234567890@tutanota`))
|
|
||||||
}
|
|
||||||
|
|
||||||
type panicReader struct{}
|
type panicReader struct{}
|
||||||
|
|
||||||
func (panicReader) Read(_ []byte) (int, error) {
|
func (panicReader) Read(_ []byte) (int, error) {
|
||||||
|
|||||||
@ -1,13 +0,0 @@
|
|||||||
Date: Mon, 01 Jan 2000 00:00:00 +0000 (UTC)
|
|
||||||
From: Daniel at Test <daniel@test.com>
|
|
||||||
Mime-Version: 1.0
|
|
||||||
Subject: Test incorrect original boundary w. invalid character
|
|
||||||
To: david@test.com
|
|
||||||
Content-Type: multipart/related; boundary="------------1234567890@tutanota"
|
|
||||||
|
|
||||||
--------------1234567890@tutanota
|
|
||||||
Content-Type: text/html; charset=UTF-8
|
|
||||||
Content-transfer-encoding: base64
|
|
||||||
|
|
||||||
PGh0bWw+PGgxPkhlbGxvIFdvcmxkITwvaDE+PC9odG1sPg==
|
|
||||||
--------------1234567890@tutanota--
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net8.0-windows7.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
|
|
||||||
<IsPackable>false</IsPackable>
|
|
||||||
<IsTestProject>true</IsTestProject>
|
|
||||||
<PlatformTarget>x64</PlatformTarget>
|
|
||||||
<Platforms>AnyCPU;x64</Platforms>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="coverlet.collector" Version="6.0.2">
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="FlaUI.Core" Version="4.0.0" />
|
|
||||||
<PackageReference Include="FlaUI.UIA3" Version="4.0.0" />
|
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.0" />
|
|
||||||
<PackageReference Include="NUnit" Version="4.2.1" />
|
|
||||||
<PackageReference Include="NUnit.Analyzers" Version="4.3.0">
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
|
||||||
<PackageReference Include="System.Drawing.Common" Version="8.0.8" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Using Include="NUnit.Framework" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
using FlaUI.Core.AutomationElements;
|
|
||||||
using FlaUI.Core.Definitions;
|
|
||||||
|
|
||||||
namespace ProtonMailBridge.UI.Tests.Results
|
|
||||||
{
|
|
||||||
public class HomeResult : UIActions
|
|
||||||
{
|
|
||||||
private Button SignOutButton => AccountView.FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("Sign out"))).AsButton();
|
|
||||||
private AutomationElement NotificationWindow => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Window));
|
|
||||||
private TextBox FreeAccountErrorText => NotificationWindow.FindFirstDescendant(cf => cf.ByControlType(ControlType.Text)).AsTextBox();
|
|
||||||
private TextBox SignedOutAccount => AccountView.FindFirstDescendant(cf => cf.ByControlType(ControlType.Text)).AsTextBox();
|
|
||||||
public HomeResult CheckIfLoggedIn()
|
|
||||||
{
|
|
||||||
Assert.That(SignOutButton.IsAvailable, Is.True);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public HomeResult CheckIfFreeAccountErrorIsDisplayed(string ErrorText)
|
|
||||||
{
|
|
||||||
Assert.That(FreeAccountErrorText.Name == ErrorText, Is.True);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
public HomeResult CheckIfAccountIsSignedOut()
|
|
||||||
{
|
|
||||||
Assert.That(SignedOutAccount.IsAvailable, Is.True);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Threading;
|
|
||||||
using FlaUI.Core.AutomationElements;
|
|
||||||
using FlaUI.Core;
|
|
||||||
using FlaUI.UIA3;
|
|
||||||
using ProtonMailBridge.UI.Tests.TestsHelper;
|
|
||||||
using FlaUI.Core.Input;
|
|
||||||
|
|
||||||
namespace ProtonMailBridge.UI.Tests
|
|
||||||
{
|
|
||||||
public class TestSession
|
|
||||||
{
|
|
||||||
|
|
||||||
public static Application App;
|
|
||||||
protected static Application Service;
|
|
||||||
protected static Window Window;
|
|
||||||
|
|
||||||
protected static void ClientCleanup()
|
|
||||||
{
|
|
||||||
App.Kill();
|
|
||||||
App.Dispose();
|
|
||||||
// Give some time to properly exit the app
|
|
||||||
Thread.Sleep(2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void LaunchApp()
|
|
||||||
{
|
|
||||||
string appExecutable = TestData.AppExecutable;
|
|
||||||
Application.Launch(appExecutable);
|
|
||||||
Wait.UntilInputIsProcessed(TestData.FiveSecondsTimeout);
|
|
||||||
App = Application.Attach("bridge-gui.exe");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Window = App.GetMainWindow(new UIA3Automation(), TestData.ThirtySecondsTimeout);
|
|
||||||
}
|
|
||||||
catch (System.TimeoutException)
|
|
||||||
{
|
|
||||||
Assert.Fail("Failed to get window of application!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,51 +0,0 @@
|
|||||||
using NUnit.Framework;
|
|
||||||
using ProtonMailBridge.UI.Tests.TestsHelper;
|
|
||||||
using ProtonMailBridge.UI.Tests.Windows;
|
|
||||||
using ProtonMailBridge.UI.Tests.Results;
|
|
||||||
|
|
||||||
namespace ProtonMailBridge.UI.Tests.Tests
|
|
||||||
{
|
|
||||||
[TestFixture]
|
|
||||||
public class LoginLogoutTests : TestSession
|
|
||||||
{
|
|
||||||
private readonly LoginWindow _loginWindow = new();
|
|
||||||
private readonly HomeWindow _mainWindow = new();
|
|
||||||
private readonly HomeResult _homeResult = new();
|
|
||||||
private readonly string FreeAccountErrorText = "Bridge is exclusive to our mail paid plans. Upgrade your account to use Bridge.";
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void LoginAsPaidUser()
|
|
||||||
{
|
|
||||||
_loginWindow.SignIn(TestUserData.GetPaidUser());
|
|
||||||
_homeResult.CheckIfLoggedIn();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void LoginAsFreeUser()
|
|
||||||
{
|
|
||||||
_loginWindow.SignIn(TestUserData.GetFreeUser());
|
|
||||||
_homeResult.CheckIfFreeAccountErrorIsDisplayed(FreeAccountErrorText);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void SuccessfullLogout()
|
|
||||||
{
|
|
||||||
_loginWindow.SignIn(TestUserData.GetPaidUser());
|
|
||||||
_mainWindow.SignOutAccount();
|
|
||||||
_homeResult.CheckIfAccountIsSignedOut();
|
|
||||||
}
|
|
||||||
|
|
||||||
[SetUp]
|
|
||||||
public void TestInitialize()
|
|
||||||
{
|
|
||||||
LaunchApp();
|
|
||||||
}
|
|
||||||
|
|
||||||
[TearDown]
|
|
||||||
public void TestCleanup()
|
|
||||||
{
|
|
||||||
_mainWindow.RemoveAccount();
|
|
||||||
ClientCleanup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using System.IO;
|
|
||||||
|
|
||||||
namespace ProtonMailBridge.UI.Tests.TestsHelper
|
|
||||||
{
|
|
||||||
public static class TestData
|
|
||||||
{
|
|
||||||
public static TimeSpan FiveSecondsTimeout => TimeSpan.FromSeconds(5);
|
|
||||||
public static TimeSpan TenSecondsTimeout => TimeSpan.FromSeconds(10);
|
|
||||||
public static TimeSpan ThirtySecondsTimeout => TimeSpan.FromSeconds(30);
|
|
||||||
public static TimeSpan OneMinuteTimeout => TimeSpan.FromSeconds(60);
|
|
||||||
public static TimeSpan RetryInterval => TimeSpan.FromMilliseconds(1000);
|
|
||||||
public static string AppExecutable => "C:\\Program Files\\Proton AG\\Proton Mail Bridge\\bridge-gui.exe";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace ProtonMailBridge.UI.Tests.TestsHelper
|
|
||||||
{
|
|
||||||
public class TestUserData
|
|
||||||
{
|
|
||||||
public string Username { get; set; }
|
|
||||||
public string Password { get; set; }
|
|
||||||
|
|
||||||
public TestUserData(string username, string password)
|
|
||||||
{
|
|
||||||
Username = username;
|
|
||||||
Password = password;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static TestUserData GetFreeUser()
|
|
||||||
{
|
|
||||||
(string username, string password) = GetusernameAndPassword("BRIDGE_FLAUI_FREE_USER");
|
|
||||||
return new TestUserData(username, password);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static TestUserData GetPaidUser()
|
|
||||||
{
|
|
||||||
(string username, string password) = GetusernameAndPassword("BRIDGE_FLAUI_PAID_USER");
|
|
||||||
return new TestUserData(username, password);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static TestUserData GetIncorrectCredentialsUser()
|
|
||||||
{
|
|
||||||
return new TestUserData("IncorrectUsername", "IncorrectPass");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static (string, string) GetusernameAndPassword(string userType)
|
|
||||||
{
|
|
||||||
// Get the environment variable for the user and check if missing
|
|
||||||
// When changing or adding an environment variable, you must restart Visual Studio
|
|
||||||
// if you have it open while doing this
|
|
||||||
string? str = Environment.GetEnvironmentVariable(userType);
|
|
||||||
if (string.IsNullOrEmpty(str))
|
|
||||||
{
|
|
||||||
throw new Exception($"Missing environment variable: {userType}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the environment variable contains only one ':'
|
|
||||||
// The ':' character must be between the username/email and password
|
|
||||||
string ch = ":";
|
|
||||||
if ((str.IndexOf(ch) != str.LastIndexOf(ch)) | (str.IndexOf(ch) == -1))
|
|
||||||
{
|
|
||||||
throw new Exception(
|
|
||||||
$"Environment variable {str} must contain one ':' and it must be between username and password!"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
string[] split = str.Split(':');
|
|
||||||
return (split[0], split[1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
using System;
|
|
||||||
using FlaUI.Core.AutomationElements;
|
|
||||||
using FlaUI.Core.Definitions;
|
|
||||||
using FlaUI.Core.Input;
|
|
||||||
using FlaUI.Core.Tools;
|
|
||||||
using NUnit.Framework;
|
|
||||||
|
|
||||||
namespace ProtonMailBridge.UI.Tests
|
|
||||||
{
|
|
||||||
public class UIActions : TestSession
|
|
||||||
{
|
|
||||||
public AutomationElement AccountView => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Pane));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
using FlaUI.Core.AutomationElements;
|
|
||||||
using FlaUI.Core.Definitions;
|
|
||||||
using System;
|
|
||||||
|
|
||||||
|
|
||||||
namespace ProtonMailBridge.UI.Tests.Windows
|
|
||||||
{
|
|
||||||
public class HomeWindow : UIActions
|
|
||||||
{
|
|
||||||
private AutomationElement[] AccountViewButtons => AccountView.FindAllChildren(cf => cf.ByControlType(ControlType.Button));
|
|
||||||
private Button RemoveAccountButton => AccountViewButtons[1].AsButton();
|
|
||||||
private AutomationElement RemoveAccountConfirmModal => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Window));
|
|
||||||
private Button ConfirmRemoveAccountButton => RemoveAccountConfirmModal.FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("Remove this account"))).AsButton();
|
|
||||||
private Button SignOutButton => AccountView.FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("Sign out"))).AsButton();
|
|
||||||
public HomeWindow RemoveAccount()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
RemoveAccountButton.Click();
|
|
||||||
ConfirmRemoveAccountButton.Click();
|
|
||||||
}
|
|
||||||
catch (System.NullReferenceException)
|
|
||||||
{
|
|
||||||
ClientCleanup();
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
public HomeWindow SignOutAccount()
|
|
||||||
{
|
|
||||||
SignOutButton.Click();
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
using FlaUI.Core.AutomationElements;
|
|
||||||
using FlaUI.Core.Input;
|
|
||||||
using FlaUI.Core.Definitions;
|
|
||||||
using ProtonMailBridge.UI.Tests.TestsHelper;
|
|
||||||
|
|
||||||
namespace ProtonMailBridge.UI.Tests.Windows
|
|
||||||
{
|
|
||||||
public class LoginWindow : UIActions
|
|
||||||
{
|
|
||||||
private AutomationElement[] InputFields => Window.FindAllDescendants(cf => cf.ByControlType(ControlType.Edit));
|
|
||||||
private TextBox UsernameInput => InputFields[0].AsTextBox();
|
|
||||||
private TextBox PasswordInput => InputFields[1].AsTextBox();
|
|
||||||
private Button SignInButton => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("Sign in"))).AsButton();
|
|
||||||
private Button StartSetupButton => Window.FindFirstDescendant(cf => cf.ByName("Start setup")).AsButton();
|
|
||||||
private Button SetUpLater => Window.FindFirstDescendant(cf => cf.ByName("Setup later")).AsButton();
|
|
||||||
|
|
||||||
public LoginWindow SignIn(TestUserData user)
|
|
||||||
{
|
|
||||||
ClickStartSetupButton();
|
|
||||||
EnterCredentials(user);
|
|
||||||
Wait.UntilInputIsProcessed(TestData.TenSecondsTimeout);
|
|
||||||
SetUpLater?.Click();
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LoginWindow SignIn(string username, string password)
|
|
||||||
{
|
|
||||||
TestUserData user = new TestUserData(username, password);
|
|
||||||
SignIn(user);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LoginWindow ClickStartSetupButton()
|
|
||||||
{
|
|
||||||
StartSetupButton?.Click();
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LoginWindow EnterCredentials(TestUserData user)
|
|
||||||
{
|
|
||||||
UsernameInput.Text = user.Username;
|
|
||||||
PasswordInput.Text = user.Password;
|
|
||||||
SignInButton.Click();
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<configuration>
|
|
||||||
<runtime>
|
|
||||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
|
||||||
<dependentAssembly>
|
|
||||||
<assemblyIdentity name="System.Drawing.Common" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
|
|
||||||
<bindingRedirect oldVersion="0.0.0.0-8.0.0.0" newVersion="8.0.0.0"/>
|
|
||||||
</dependentAssembly>
|
|
||||||
<dependentAssembly>
|
|
||||||
<assemblyIdentity name="System.Diagnostics.PerformanceCounter" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
|
|
||||||
<bindingRedirect oldVersion="0.0.0.0-8.0.0.0" newVersion="8.0.0.0"/>
|
|
||||||
</dependentAssembly>
|
|
||||||
<dependentAssembly>
|
|
||||||
<assemblyIdentity name="System.Runtime.CompilerServices.Unsafe" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
|
||||||
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
|
||||||
</dependentAssembly>
|
|
||||||
<dependentAssembly>
|
|
||||||
<assemblyIdentity name="System.Reflection.Metadata" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
|
||||||
<bindingRedirect oldVersion="0.0.0.0-8.0.0.0" newVersion="8.0.0.0"/>
|
|
||||||
</dependentAssembly>
|
|
||||||
<dependentAssembly>
|
|
||||||
<assemblyIdentity name="System.Diagnostics.DiagnosticSource" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
|
|
||||||
<bindingRedirect oldVersion="0.0.0.0-8.0.0.1" newVersion="8.0.0.1"/>
|
|
||||||
</dependentAssembly>
|
|
||||||
<dependentAssembly>
|
|
||||||
<assemblyIdentity name="System.Memory" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
|
|
||||||
<bindingRedirect oldVersion="0.0.0.0-4.0.1.2" newVersion="4.0.1.2"/>
|
|
||||||
</dependentAssembly>
|
|
||||||
<dependentAssembly>
|
|
||||||
<assemblyIdentity name="System.Collections.Immutable" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
|
||||||
<bindingRedirect oldVersion="0.0.0.0-8.0.0.0" newVersion="8.0.0.0"/>
|
|
||||||
</dependentAssembly>
|
|
||||||
</assemblyBinding>
|
|
||||||
</runtime>
|
|
||||||
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8"/></startup></configuration>
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
|
||||||
# Visual Studio Version 17
|
|
||||||
VisualStudioVersion = 17.11.35208.52
|
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProtonMailBridge.UI.Tests", "ProtonMailBridge.UI.Tests.csproj", "{027E5266-E353-4095-AF24-B3ED240EACAA}"
|
|
||||||
EndProject
|
|
||||||
Global
|
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
|
||||||
Debug|Any CPU = Debug|Any CPU
|
|
||||||
Debug|x64 = Debug|x64
|
|
||||||
Release|Any CPU = Release|Any CPU
|
|
||||||
Release|x64 = Release|x64
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
|
||||||
{027E5266-E353-4095-AF24-B3ED240EACAA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{027E5266-E353-4095-AF24-B3ED240EACAA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{027E5266-E353-4095-AF24-B3ED240EACAA}.Debug|x64.ActiveCfg = Debug|x64
|
|
||||||
{027E5266-E353-4095-AF24-B3ED240EACAA}.Debug|x64.Build.0 = Debug|x64
|
|
||||||
{027E5266-E353-4095-AF24-B3ED240EACAA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{027E5266-E353-4095-AF24-B3ED240EACAA}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{027E5266-E353-4095-AF24-B3ED240EACAA}.Release|x64.ActiveCfg = Release|x64
|
|
||||||
{027E5266-E353-4095-AF24-B3ED240EACAA}.Release|x64.Build.0 = Release|x64
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
|
||||||
HideSolutionNode = FALSE
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
|
||||||
SolutionGuid = {817DD45A-EA2C-4F16-A680-5810DADCE4E7}
|
|
||||||
EndGlobalSection
|
|
||||||
EndGlobal
|
|
||||||
@ -57,6 +57,7 @@ Feature: IMAP create messages
|
|||||||
And IMAP client "1" eventually sees the following messages in "All Mail":
|
And IMAP client "1" eventually sees the following messages in "All Mail":
|
||||||
| from | to | subject | body |
|
| from | to | subject | body |
|
||||||
| [alias:alias]@[domain] | john.doe@email.com | foo | bar |
|
| [alias:alias]@[domain] | john.doe@email.com | foo | bar |
|
||||||
|
And bridge reports a message with "GODT-3185: import with non-default address in combined mode: using sender address"
|
||||||
|
|
||||||
Scenario: Imports an unrelated message to inbox
|
Scenario: Imports an unrelated message to inbox
|
||||||
When IMAP client "1" appends the following messages to "INBOX":
|
When IMAP client "1" appends the following messages to "INBOX":
|
||||||
|
|||||||
@ -164,6 +164,7 @@ Feature: IMAP Draft messages
|
|||||||
And IMAP client "1" eventually sees the following messages in "Drafts":
|
And IMAP client "1" eventually sees the following messages in "Drafts":
|
||||||
| to | subject | body |
|
| to | subject | body |
|
||||||
| someone@example.com | Draft without From | This is a Draft without From in header |
|
| someone@example.com | Draft without From | This is a Draft without From in header |
|
||||||
|
And bridge reports a message with "GODT-3185: draft with non-default invalid address in combined mode: error import/draft"
|
||||||
|
|
||||||
@regression
|
@regression
|
||||||
Scenario: Only one draft in Drafts and All Mail after editing it locally multiple times
|
Scenario: Only one draft in Drafts and All Mail after editing it locally multiple times
|
||||||
|
|||||||
@ -651,83 +651,3 @@ Feature: IMAP import messages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
Scenario: Message import multipart/related with invalid boundary character
|
|
||||||
When IMAP client "1" appends the following message to "INBOX":
|
|
||||||
"""
|
|
||||||
From: Bridge Test <bridgetest@pm.test>
|
|
||||||
Date: 01 Jan 1980 00:00:00 +0000
|
|
||||||
To: Internal Bridge <bridgetest@example.com>
|
|
||||||
Received: by 2002:0:0:0:0:0:0:0 with SMTP id 0123456789abcdef; Wed, 30 Dec 2020 01:23:45 0000
|
|
||||||
Subject: Message with invalid boundary
|
|
||||||
Content-Type: multipart/related; boundary="------------123456789@tutanota"
|
|
||||||
|
|
||||||
--------------123456789@tutanota
|
|
||||||
Content-Type: text/html; charset=UTF-8
|
|
||||||
Content-transfer-encoding: base64
|
|
||||||
|
|
||||||
PGRpdiBjbGFzcz0iIj4KPHAgY2xhc3M9IiI+PGEgbmFtZT0iX0hsazE5MDA1NjM2IiByZWw9Im5vb3
|
|
||||||
BlbmVyIG5vcmVmZXJyZXIiIHRhcmdldD0iX2JsYW5rIj48c3BhbiBzdHlsZT0ibXNvLWZhcmVhc3Qt
|
|
||||||
|
|
||||||
--------------123456789@tutanota
|
|
||||||
Content-Type: image/png;
|
|
||||||
name==?UTF-8?B?MC5wbmc=?=
|
|
||||||
Content-Transfer-Encoding: base64
|
|
||||||
Content-Disposition: attachment;
|
|
||||||
filename=image1.png
|
|
||||||
|
|
||||||
iVBORw0KGgoAAAANSUhEUgAAACsAAAArCAYAAADhXXHAAAAPq3pUWHRSYXcgcHJvZmlsZSB0eXBlIG
|
|
||||||
V4aWYAAHjarZlrliOpkoT/s4pZAuCAw3J4njM7mOXP54SUlZmV1bd7plNVEVIoAhx/mJsht//nv4/7
|
|
||||||
|
|
||||||
--------------123456789@tutanota
|
|
||||||
Content-Type: image/png;
|
|
||||||
name==?UTF-8?B?Mi5wbmc=?=
|
|
||||||
Content-Transfer-Encoding: base64
|
|
||||||
Content-Disposition: attachment;
|
|
||||||
filename=img2.png
|
|
||||||
|
|
||||||
iVBORw0KGgoAAAANSUhEUgAAACsAAAArCAYAAADhXXHAAAAR+HpUWHRSYXcgcHJvZmlsZSB0eXBlIG
|
|
||||||
V4aWYAAHjarZprdhs5DoX/cxWzBD4Bcjl8njM7mOXPB5bsOI49SU+3nViKLFWxgIv7YMXt//z7uH/x
|
|
||||||
|
|
||||||
--------------123456789@tutanota--
|
|
||||||
|
|
||||||
"""
|
|
||||||
Then it succeeds
|
|
||||||
And IMAP client "1" eventually sees the following message in "INBOX" with this structure:
|
|
||||||
"""
|
|
||||||
{
|
|
||||||
"from": "Bridge Test <bridgetest@pm.test>",
|
|
||||||
"date": "01 Jan 80 00:00 +0000",
|
|
||||||
"to": "Internal Bridge <bridgetest@example.com>",
|
|
||||||
"subject": "Message with invalid boundary",
|
|
||||||
"content": {
|
|
||||||
"content-type": "multipart/mixed",
|
|
||||||
"sections":[
|
|
||||||
{
|
|
||||||
"content-type": "multipart/related",
|
|
||||||
"sections": [
|
|
||||||
{
|
|
||||||
"content-type": "text/html",
|
|
||||||
"transfer-encoding": "base64",
|
|
||||||
"body-is": "PGRpdiBjbGFzcz0iIj4KPHAgY2xhc3M9IiI+PGEgbmFtZT0iX0hsazE5MDA1NjM2IiByZWw9Im5v\r\nb3BlbmVyIG5vcmVmZXJyZXIiIHRhcmdldD0iX2JsYW5rIj48c3BhbiBzdHlsZT0ibXNvLWZhcmVh\r\nc3Qt"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"content-type": "image/png",
|
|
||||||
"transfer-encoding": "base64",
|
|
||||||
"content-disposition": "attachment",
|
|
||||||
"content-disposition-filename": "image1.png",
|
|
||||||
"body-is": "iVBORw0KGgoAAAANSUhEUgAAACsAAAArCAYAAADhXXHAAAAPq3pUWHRSYXcgcHJvZmlsZSB0eXBl\r\nIGV4aWYAAHjarZlrliOpkoT/s4pZAuCAw3J4njM7mOXP54SUlZmV1bd7plNVEVIoAhx/mJsht//n\r\nv4/7"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"content-type": "image/png",
|
|
||||||
"transfer-encoding": "base64",
|
|
||||||
"content-disposition": "attachment",
|
|
||||||
"content-disposition-filename": "img2.png",
|
|
||||||
"body-is": "iVBORw0KGgoAAAANSUhEUgAAACsAAAArCAYAAADhXXHAAAAR+HpUWHRSYXcgcHJvZmlsZSB0eXBl\r\nIGV4aWYAAHjarZprdhs5DoX/cxWzBD4Bcjl8njM7mOXPB5bsOI49SU+3nViKLFWxgIv7YMXt//z7\r\nuH/x"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|||||||
@ -1,38 +0,0 @@
|
|||||||
Feature: Bridge send remote notification observability metrics
|
|
||||||
Background:
|
|
||||||
Given there exists an account with username "[user:user1]" and password "password"
|
|
||||||
Then it succeeds
|
|
||||||
When bridge starts
|
|
||||||
Then it succeeds
|
|
||||||
|
|
||||||
Scenario: Test all possible heartbeat metrics
|
|
||||||
When the user logs in with username "[user:user1]" and password "password"
|
|
||||||
And the user with username "[user:user1]" sends all possible observability heartbeat metrics
|
|
||||||
Then it succeeds
|
|
||||||
|
|
||||||
Scenario: Test all possible user discrimination metrics
|
|
||||||
When the user logs in with username "[user:user1]" and password "password"
|
|
||||||
And the user with username "[user:user1]" sends all possible user distinction metrics
|
|
||||||
Then it succeeds
|
|
||||||
|
|
||||||
Scenario: Test all possible sync message event failure observability metrics
|
|
||||||
When the user logs in with username "[user:user1]" and password "password"
|
|
||||||
And the user with username "[user:user1]" sends all possible sync message event failure observability metrics
|
|
||||||
Then it succeeds
|
|
||||||
|
|
||||||
Scenario: Test all possible event loop message events observability metrics
|
|
||||||
When the user logs in with username "[user:user1]" and password "password"
|
|
||||||
And the user with username "[user:user1]" sends all possible event loop message events observability metrics
|
|
||||||
Then it succeeds
|
|
||||||
|
|
||||||
Scenario: Test all possible sync message building failure observability metrics
|
|
||||||
When the user logs in with username "[user:user1]" and password "password"
|
|
||||||
And the user with username "[user:user1]" sends all possible sync message building failure observability metrics
|
|
||||||
Then it succeeds
|
|
||||||
|
|
||||||
Scenario: Test all possible sync message building success observability metrics
|
|
||||||
When the user logs in with username "[user:user1]" and password "password"
|
|
||||||
And the user with username "[user:user1]" sends all possible sync message building success observability metrics
|
|
||||||
Then it succeeds
|
|
||||||
|
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,111 +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 tests
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/ProtonMail/go-proton-api"
|
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice/observabilitymetrics/evtloopmsgevents"
|
|
||||||
"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/syncservice/observabilitymetrics"
|
|
||||||
)
|
|
||||||
|
|
||||||
// userHeartbeatPermutationsObservability - corresponds to bridge_generic_user_heartbeat_total_v1.schema.json.
|
|
||||||
func (s *scenario) userHeartbeatPermutationsObservability(username string) error {
|
|
||||||
metrics := observability.GenerateAllHeartbeatMetricPermutations()
|
|
||||||
return s.t.withClientPass(context.Background(), username, s.t.getUserByName(username).userPass, func(ctx context.Context, c *proton.Client) error {
|
|
||||||
batch := proton.ObservabilityBatch{Metrics: metrics}
|
|
||||||
return c.SendObservabilityBatch(ctx, batch)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// userDistinctionMetricsPermutationsObservability - corresponds to:
|
|
||||||
// bridge_sync_errors_users_total_v1.schema.json
|
|
||||||
// bridge_event_loop_events_errors_users_total_v1.schema.json.
|
|
||||||
func (s *scenario) userDistinctionMetricsPermutationsObservability(username string) error {
|
|
||||||
batch := proton.ObservabilityBatch{
|
|
||||||
Metrics: observability.GenerateAllUsedDistinctionMetricPermutations()}
|
|
||||||
return s.t.withClientPass(context.Background(), username, s.t.getUserByName(username).userPass, func(ctx context.Context, c *proton.Client) error {
|
|
||||||
err := c.SendObservabilityBatch(ctx, batch)
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// syncFailureMessageEventsObservability - corresponds to bridge_sync_message_event_failures_total_v1.schema.json.
|
|
||||||
func (s *scenario) syncFailureMessageEventsObservability(username string) error {
|
|
||||||
batch := proton.ObservabilityBatch{
|
|
||||||
Metrics: []proton.ObservabilityMetric{
|
|
||||||
syncmsgevents.GenerateSyncFailureCreateMessageEventMetric(),
|
|
||||||
syncmsgevents.GenerateSyncFailureDeleteMessageEventMetric(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return s.t.withClientPass(context.Background(), username, s.t.getUserByName(username).userPass, func(ctx context.Context, c *proton.Client) error {
|
|
||||||
err := c.SendObservabilityBatch(ctx, batch)
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// eventLoopFailureMessageEventsObservability - corresponds to bridge_event_loop_message_event_failures_total_v1.schema.json.
|
|
||||||
func (s *scenario) eventLoopFailureMessageEventsObservability(username string) error {
|
|
||||||
batch := proton.ObservabilityBatch{
|
|
||||||
Metrics: []proton.ObservabilityMetric{
|
|
||||||
evtloopmsgevents.GenerateMessageEventFailedToBuildDraft(),
|
|
||||||
evtloopmsgevents.GenerateMessageEventFailedToBuildMessage(),
|
|
||||||
evtloopmsgevents.GenerateMessageEventFailureCreateMessageMetric(),
|
|
||||||
evtloopmsgevents.GenerateMessageEventFailureDeleteMessageMetric(),
|
|
||||||
evtloopmsgevents.GenerateMessageEventFailureUpdateMetric(),
|
|
||||||
evtloopmsgevents.GenerateMessageEventUpdateChannelDoesNotExist(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.t.withClientPass(context.Background(), username, s.t.getUserByName(username).userPass, func(ctx context.Context, c *proton.Client) error {
|
|
||||||
err := c.SendObservabilityBatch(ctx, batch)
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// syncFailureMessageBuiltObservability - corresponds to bridge_sync_message_event_failures_total_v1.schema.json.
|
|
||||||
func (s *scenario) syncFailureMessageBuiltObservability(username string) error {
|
|
||||||
batch := proton.ObservabilityBatch{
|
|
||||||
Metrics: []proton.ObservabilityMetric{
|
|
||||||
observabilitymetrics.GenerateNoUnlockedKeyringMetric(),
|
|
||||||
observabilitymetrics.GenerateFailedToBuildMetric(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.t.withClientPass(context.Background(), username, s.t.getUserByName(username).userPass, func(ctx context.Context, c *proton.Client) error {
|
|
||||||
err := c.SendObservabilityBatch(ctx, batch)
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// syncSuccessMessageBuiltObservability - corresponds to bridge_sync_message_build_success_total_v1.schema.json.
|
|
||||||
func (s *scenario) syncSuccessMessageBuiltObservability(username string) error {
|
|
||||||
batch := proton.ObservabilityBatch{
|
|
||||||
Metrics: []proton.ObservabilityMetric{
|
|
||||||
observabilitymetrics.GenerateMessageBuiltSuccessMetric(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.t.withClientPass(context.Background(), username, s.t.getUserByName(username).userPass, func(ctx context.Context, c *proton.Client) error {
|
|
||||||
err := c.SendObservabilityBatch(ctx, batch)
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@ -218,12 +218,6 @@ func (s *scenario) steps(ctx *godog.ScenarioContext) {
|
|||||||
ctx.Step(`^the contact "([^"]*)" of user "([^"]*)" has public key from file "([^"]*)"$`, s.contactOfUserHasPubKeyFromFile)
|
ctx.Step(`^the contact "([^"]*)" of user "([^"]*)" has public key from file "([^"]*)"$`, s.contactOfUserHasPubKeyFromFile)
|
||||||
|
|
||||||
// ==== OBSERVABILITY METRICS ====
|
// ==== OBSERVABILITY METRICS ====
|
||||||
ctx.Step(`^the user with username "([^"]*)" sends the following remote notification observability metric "([^"]*)"$`,
|
ctx.Step(`^the user with username "([^"]*)" sends the following remote notification observability metric "([^"]*)"`,
|
||||||
s.userRemoteNotificationMetricTest)
|
s.userRemoteNotificationMetricTest)
|
||||||
ctx.Step(`^the user with username "([^"]*)" sends all possible observability heartbeat metrics$`, s.userHeartbeatPermutationsObservability)
|
|
||||||
ctx.Step(`^the user with username "([^"]*)" sends all possible user distinction metrics$`, s.userDistinctionMetricsPermutationsObservability)
|
|
||||||
ctx.Step(`^the user with username "([^"]*)" sends all possible sync message event failure observability metrics$`, s.syncFailureMessageEventsObservability)
|
|
||||||
ctx.Step(`^the user with username "([^"]*)" sends all possible event loop message events observability metrics$`, s.eventLoopFailureMessageEventsObservability)
|
|
||||||
ctx.Step(`^the user with username "([^"]*)" sends all possible sync message building failure observability metrics$`, s.syncFailureMessageBuiltObservability)
|
|
||||||
ctx.Step(`^the user with username "([^"]*)" sends all possible sync message building success observability metrics$`, s.syncSuccessMessageBuiltObservability)
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user