Compare commits

..

1 Commits

Author SHA1 Message Date
43cbedafb8 chore: Colorado Bridge 3.13.0 changelog. 2024-08-30 15:35:30 +02:00
63 changed files with 662 additions and 2431 deletions

3
.gitignore vendored
View File

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

View File

@ -3,19 +3,6 @@
Changelog [format](http://keepachangelog.com/en/1.0.0/)
## Dragon Bridge 3.14.0
### Changed
* BRIDGE-207: Failure to download or verify an update now fails silently.
* BRIDGE-204: Removed redundant Sentry events.
* BRIDGE-150: Observability service modification.
* BRIDGE-210: Reduced log level of cache events so they won't be printed to stdout.
### Fixed
* BRIDGE-106: Fixed import of multipart-related messages.
* BRIDGE-108: Fixed GetInitials when empty username is passed.
## Colorado Bridge 3.13.0
### Added

View File

@ -12,7 +12,7 @@ ROOT_DIR:=$(realpath .)
.PHONY: build build-gui build-nogui build-launcher versioner hasher
# Keep version hardcoded so app build works also without Git repository.
BRIDGE_APP_VERSION?=3.14.0+git
BRIDGE_APP_VERSION?=3.13.0+git
APP_VERSION:=${BRIDGE_APP_VERSION}
APP_FULL_NAME:=Proton Mail Bridge
APP_VENDOR:=Proton AG

6
go.mod
View File

@ -7,9 +7,9 @@ toolchain go1.21.9
require (
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
github.com/Masterminds/semver/v3 v3.2.0
github.com/ProtonMail/gluon v0.17.1-0.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-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/PuerkitoBio/goquery v1.8.1
github.com/abiosoft/ishell v2.0.0+incompatible
@ -122,7 +122,7 @@ require (
)
replace (
github.com/emersion/go-message => github.com/ProtonMail/go-message v0.13.1-0.20240919135104-3bc88e6a9423
github.com/emersion/go-message => github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7
github.com/emersion/go-smtp => github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865
github.com/go-resty/resty/v2 => github.com/LBeernaertProton/resty/v2 v2.0.0-20231129100320-dddf8030d93a
github.com/keybase/go-keychain => github.com/cuthix/go-keychain v0.0.0-20240103134243-0b6a41580b77

18
go.sum
View File

@ -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/gluon v0.17.1-0.20240514133734-79cdd0fec41c h1:P3SvCACt13Zqdj0IRDB4bgwqI68+oMB2j0uVuPQyoTw=
github.com/ProtonMail/gluon v0.17.1-0.20240514133734-79cdd0fec41c/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
github.com/ProtonMail/gluon v0.17.1-0.20240918150504-3b2e7f40d961 h1:kCaz78X7OKETvK6AGHeyggHKxDBcqX7EWHf7spJ+D3g=
github.com/ProtonMail/gluon v0.17.1-0.20240918150504-3b2e7f40d961/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
github.com/ProtonMail/gluon v0.17.1-0.20240923094038-e319bf6047c5 h1:LzaUpUj6M2PEBArFCkaimViNpGXDgwHVrdhvYwHLoJQ=
github.com/ProtonMail/gluon v0.17.1-0.20240923094038-e319bf6047c5/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
github.com/ProtonMail/gluon v0.17.1-0.20240923151549-d23b4bec3602 h1:EoMjWlC32tg46L/07hWoiZfLkqJyxVMcsq4Cyn+Ofqc=
github.com/ProtonMail/gluon v0.17.1-0.20240923151549-d23b4bec3602/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
@ -42,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-message v0.13.1-0.20230526094639-b62c999c85b7 h1:+j+Kd/DyZ/qGfMT9htAT7HxqIEbZHsatsx+m8AoV6fc=
github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
github.com/ProtonMail/go-message v0.13.1-0.20240906141354-38c596f2f5a8 h1:+eE7FGX+4Hu8RZaRmSebrDVXyLuowKSaO7ZhQ6ca4+E=
github.com/ProtonMail/go-message v0.13.1-0.20240906141354-38c596f2f5a8/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
github.com/ProtonMail/go-message v0.13.1-0.20240906144417-4083506a9542 h1:5DqSycYnKfUdHiu0yOdiYW5R2hVxoE0Mk4PLSYwqGyg=
github.com/ProtonMail/go-message v0.13.1-0.20240906144417-4083506a9542/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
github.com/ProtonMail/go-message v0.13.1-0.20240910093530-2ada52e7dffb h1:uOKp93u6JFYlBoJJvOhzmHZURcvWmXiqhihGWtT3HtY=
github.com/ProtonMail/go-message v0.13.1-0.20240910093530-2ada52e7dffb/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
github.com/ProtonMail/go-message v0.13.1-0.20240919135104-3bc88e6a9423 h1:p8nBDxvRnvDOyrcePKkPpErWGhDoTqpX8a1c54CcSu0=
github.com/ProtonMail/go-message v0.13.1-0.20240919135104-3bc88e6a9423/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
github.com/ProtonMail/go-proton-api v0.4.1-0.20240612082117-0f92424eed80 h1:cP4+6RFn9vVgYnoDwxBU4EtIAZA+eM4rzOaSZNqZ1xg=
@ -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.20240829112804-d663a2ef90c2 h1:yx0iejqB5c21HIN5jn9IsbyzUns0dPUUaGfyUHF3TmQ=
github.com/ProtonMail/go-proton-api v0.4.1-0.20240829112804-d663a2ef90c2/go.mod h1:3A0cpdo0BIenIPjTG6u8EbzJ8uuJy7rVvM/NaynjCKA=
github.com/ProtonMail/go-proton-api v0.4.1-0.20240916123336-3ac75d8041dc h1:SWVPwO1M2jCI1bJHBji/JVU01FpWP/6nzh8NBIjo+Fg=
github.com/ProtonMail/go-proton-api v0.4.1-0.20240916123336-3ac75d8041dc/go.mod h1:3A0cpdo0BIenIPjTG6u8EbzJ8uuJy7rVvM/NaynjCKA=
github.com/ProtonMail/go-proton-api v0.4.1-0.20240918100656-b4860af56d47 h1:a+3dOyIxJEslN5HxyICM8flY9lnCyJupXNcv6fUaivA=
github.com/ProtonMail/go-proton-api v0.4.1-0.20240918100656-b4860af56d47/go.mod h1:3A0cpdo0BIenIPjTG6u8EbzJ8uuJy7rVvM/NaynjCKA=
github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865 h1:EP1gnxLL5Z7xBSymE9nSTM27nRYINuvssAtDmG0suD8=
github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=

View File

@ -267,8 +267,6 @@ func newBridge(
unleashService := unleash.NewBridgeService(ctx, api, locator, panicHandler)
observabilityService := observability.NewService(ctx, panicHandler)
bridge := &Bridge{
vault: vault,
@ -308,11 +306,11 @@ func newBridge(
lastVersion: lastVersion,
tasks: tasks,
syncService: syncservice.NewService(panicHandler, observabilityService),
syncService: syncservice.NewService(reporter, panicHandler),
unleashService: unleashService,
observabilityService: observabilityService,
observabilityService: observability.NewService(ctx, panicHandler),
notificationStore: notifications.NewStore(locator.ProvideNotificationsCachePath),
}
@ -344,7 +342,7 @@ func newBridge(
bridge.unleashService.Run()
bridge.observabilityService.Run(bridge)
bridge.observabilityService.Run()
return bridge, nil
}
@ -712,13 +710,5 @@ func (bridge *Bridge) GetFeatureFlagValue(key string) bool {
}
func (bridge *Bridge) PushObservabilityMetric(metric proton.ObservabilityMetric) {
bridge.observabilityService.AddMetrics(metric)
}
func (bridge *Bridge) PushDistinctObservabilityMetrics(errType observability.DistinctionErrorTypeEnum, metrics ...proton.ObservabilityMetric) {
bridge.observabilityService.AddDistinctMetrics(errType, metrics...)
}
func (bridge *Bridge) ModifyObservabilityHeartbeatInterval(duration time.Duration) {
bridge.observabilityService.ModifyHeartbeatInterval(duration)
bridge.observabilityService.AddMetric(metric)
}

View File

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

View File

@ -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))
})
})
}

View File

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

View File

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

View File

@ -571,6 +571,7 @@ func (bridge *Bridge) addUserWithVault(
isNew,
bridge.notificationStore,
bridge.unleashService.GetFlagValue,
bridge.observabilityService.AddMetric,
)
if err != nil {
return fmt.Errorf("failed to create user: %w", err)

View File

@ -38,6 +38,9 @@ func (bridge *Bridge) handleUserEvent(ctx context.Context, user *user.User, even
case events.UserLoadedCheckResync:
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)
}, bridge.usersLock)
}
func (bridge *Bridge) handleUncategorizedErrorEvent(event events.UncategorizedEventError) {
if rerr := bridge.reporter.ReportMessageWithContext("Failed to handle due to uncategorized error", reporter.Context{
"error_type": internal.ErrCauseType(event.Error),
"error": event.Error,
}); rerr != nil {
logrus.WithField("pkg", "bridge/event").WithError(rerr).Error("Failed to report failed event handling")
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -45,15 +45,14 @@ type Service struct {
store *Store
getFlagValueFn unleash.GetFlagValueFn
observabilitySender observability.Sender
pushObservabilityMetricFn observability.PushObsMetricFn
}
const bitfieldRegexPattern = `^\\\d+`
const disableNotificationsKillSwitch = "InboxBridgeEventLoopNotificationDisabled"
func NewService(userID string, service userevents.Subscribable, eventPublisher events.EventPublisher, store *Store,
getFlagFn unleash.GetFlagValueFn, observabilitySender observability.Sender) *Service {
getFlagFn unleash.GetFlagValueFn, pushMetricFn observability.PushObsMetricFn) *Service {
return &Service{
userID: userID,
@ -70,7 +69,7 @@ func NewService(userID string, service userevents.Subscribable, eventPublisher e
store: store,
getFlagValueFn: getFlagFn,
observabilitySender: observabilitySender,
pushObservabilityMetricFn: pushMetricFn,
}
}
@ -111,7 +110,7 @@ func (s *Service) HandleNotificationEvents(ctx context.Context, notificationEven
s.log.Debug("Handling notification events")
// Publish observability metrics that we've received notifications
s.observabilitySender.AddMetrics(GenerateReceivedMetric(len(notificationEvents)))
s.pushObservabilityMetricFn(GenerateReceivedMetric(len(notificationEvents)))
for _, event := range notificationEvents {
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})
// Publish observability metric that we've successfully processed notifications
s.observabilitySender.AddMetrics(GenerateProcessedMetric(1))
s.pushObservabilityMetricFn(GenerateProcessedMetric(1))
}
default:

View File

@ -109,7 +109,7 @@ func (s *Store) readCache() {
file, err := os.Open(s.cacheFilepath)
if err != nil {
s.log.WithError(err).Info("Unable to open cache file")
s.log.WithError(err).Error("Unable to open cache file")
return
}

View File

@ -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
}

View File

@ -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()
}

View File

@ -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,
},
},
}
}

View File

@ -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
}

View File

@ -36,18 +36,13 @@ const (
maxBatchSize = 1000
)
type PushObsMetricFn func(metric proton.ObservabilityMetric)
type client struct {
isTelemetryEnabled func(context.Context) bool
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 {
ctx context.Context
cancel context.CancelFunc
@ -67,8 +62,6 @@ type Service struct {
userClientStore map[string]*client
userClientStoreLock sync.Mutex
distinctionUtility *distinctionUtility
}
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),
}
service.distinctionUtility = newDistinctionUtility(ctx, panicHandler, service)
return service
}
// Run starts the observability service goroutine.
// 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) {
func (s *Service) Run() {
s.log.Info("Starting service")
s.distinctionUtility.setSettingsGetter(settingsGetter)
s.distinctionUtility.runHeartbeat()
go func() {
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() {
metricStoreLength := len(s.metricStore)
if metricStoreLength >= maxStorageSize {
@ -224,32 +209,12 @@ func (s *Service) addMetrics(metric ...proton.ObservabilityMetric) {
dropCount := metricStoreLength - maxStorageSize + 1
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)
}
// 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) {
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;
s.sendSignal(s.signalDataArrived)
}
@ -316,25 +279,3 @@ func (s *Service) sendSignal(channel chan struct{}) {
func ModifyThrottlePeriod(duration time.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
}

View File

@ -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
}

View File

@ -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"
}

View File

@ -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))
}

View File

@ -30,6 +30,7 @@ import (
"time"
"github.com/ProtonMail/gluon/async"
"github.com/ProtonMail/gluon/reporter"
"github.com/ProtonMail/gluon/rfc5322"
"github.com/ProtonMail/gluon/rfc822"
"github.com/ProtonMail/go-proton-api"
@ -196,7 +197,13 @@ func (s *Service) sendWithKey(
}
parentID, draftsToDelete, err := getParentID(ctx, s.client, authAddrID, addrMode, references)
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")
}

View File

@ -160,6 +160,10 @@ func (s *childJob) onError(err error) {
s.job.onError(err)
}
func (s *childJob) userID() string {
return s.job.userID
}
func (s *childJob) chunkDivide(chunks [][]proton.FullMessage) []childJob {
numChunks := len(chunks)

View File

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

View File

@ -21,7 +21,7 @@ import (
"context"
"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.
@ -36,9 +36,8 @@ type Service struct {
group *async.Group
}
func NewService(
func NewService(reporter reporter.Reporter,
panicHandler async.PanicHandler,
observabilitySender observability.Sender,
) *Service {
limits := newSyncLimits(2 * Gigabyte)
@ -51,7 +50,7 @@ func NewService(
limits: limits,
metadataStage: NewMetadataStage(metaCh, downloadCh, limits.DownloadRequestMem, 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),
metaCh: metaCh,
group: async.NewGroup(context.Background(), panicHandler),

View File

@ -26,10 +26,9 @@ import (
"github.com/ProtonMail/gluon/async"
"github.com/ProtonMail/gluon/logging"
"github.com/ProtonMail/gluon/reporter"
"github.com/ProtonMail/go-proton-api"
"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/xslices"
"github.com/sirupsen/logrus"
@ -51,10 +50,8 @@ type BuildStage struct {
maxBuildMem uint64
panicHandler async.PanicHandler
reporter reporter.Reporter
log *logrus.Entry
// Observability
observabilitySender observability.Sender
}
func NewBuildStage(
@ -62,7 +59,7 @@ func NewBuildStage(
output BuildStageOutput,
maxBuildMem uint64,
panicHandler async.PanicHandler,
observabilitySender observability.Sender,
reporter reporter.Reporter,
) *BuildStage {
return &BuildStage{
input: input,
@ -70,7 +67,7 @@ func NewBuildStage(
maxBuildMem: maxBuildMem,
log: logrus.WithField("sync-stage", "build"),
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")
}
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
}
res, err := req.job.messageBuilder.BuildMessage(req.job.labels, msg, kr, new(bytes.Buffer))
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 {
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.
return BuildResult{}, nil
}
b.observabilitySender.AddMetrics(obsMetrics.GenerateMessageBuiltSuccessMetric())
return res, nil
})
if err != nil {

View File

@ -24,11 +24,10 @@ import (
"github.com/ProtonMail/gluon/async"
"github.com/ProtonMail/gluon/imap"
"github.com/ProtonMail/gluon/reporter"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/v3/internal/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/golang/mock/gomock"
"github.com/stretchr/testify/require"
@ -68,6 +67,7 @@ func TestBuildStage_SuccessRemovesFailedMessage(t *testing.T) {
input := NewChannelConsumerProducer[BuildRequest]()
output := NewChannelConsumerProducer[ApplyRequest]()
reporter := mocks.NewMockReporter(mockCtrl)
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.state.EXPECT().RemFailedMessageID(gomock.Any(), gomock.Eq("MSG"))
observabilityService := mocks.NewMockObservabilitySender(mockCtrl)
observabilityService.EXPECT().AddMetrics(obsMetrics.GenerateMessageBuiltSuccessMetric())
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, observabilityService)
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, reporter)
go func() {
stage.run(ctx)
@ -128,7 +125,7 @@ func TestBuildStage_BuildFailureIsReportedButDoesNotCancelJob(t *testing.T) {
input := NewChannelConsumerProducer[BuildRequest]()
output := NewChannelConsumerProducer[ApplyRequest]()
mockObservabilityService := mocks.NewMockObservabilitySender(mockCtrl)
mockReporter := mocks.NewMockReporter(mockCtrl)
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.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)))
mockObservabilityService.EXPECT().AddDistinctMetrics(observability.SyncError, obsMetrics.GenerateNoUnlockedKeyringMetric())
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, mockObservabilityService)
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, mockReporter)
go func() {
stage.run(ctx)
@ -183,6 +183,7 @@ func TestBuildStage_FailedToLocateKeyRingIsReportedButDoesNotFailBuild(t *testin
input := NewChannelConsumerProducer[BuildRequest]()
output := NewChannelConsumerProducer[ApplyRequest]()
mockReporter := mocks.NewMockReporter(mockCtrl)
labels := getTestLabels()
@ -208,13 +209,14 @@ func TestBuildStage_FailedToLocateKeyRingIsReportedButDoesNotFailBuild(t *testin
tj.job.end()
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)))
observabilitySender := mocks.NewMockObservabilitySender(mockCtrl)
observabilitySender.EXPECT().AddDistinctMetrics(observability.SyncError)
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, observabilitySender)
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, mockReporter)
go func() {
stage.run(ctx)
@ -233,6 +235,7 @@ func TestBuildStage_OtherErrorsFailJob(t *testing.T) {
input := NewChannelConsumerProducer[BuildRequest]()
output := NewChannelConsumerProducer[ApplyRequest]()
mockReporter := mocks.NewMockReporter(mockCtrl)
labels := getTestLabels()
@ -258,7 +261,7 @@ func TestBuildStage_OtherErrorsFailJob(t *testing.T) {
childJob := tj.job.newChildJob("f", 10)
tj.job.end()
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, mocks.NewMockObservabilitySender(mockCtrl))
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, mockReporter)
go func() {
stage.run(ctx)
@ -280,6 +283,7 @@ func TestBuildStage_CancelledJobIsDiscarded(t *testing.T) {
input := NewChannelConsumerProducer[BuildRequest]()
output := NewChannelConsumerProducer[ApplyRequest]()
mockReporter := mocks.NewMockReporter(mockCtrl)
msg := proton.FullMessage{
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())
@ -323,6 +327,7 @@ func TestTask_EmptyInputDoesNotCrash(t *testing.T) {
input := NewChannelConsumerProducer[BuildRequest]()
output := NewChannelConsumerProducer[ApplyRequest]()
reporter := mocks.NewMockReporter(mockCtrl)
labels := getTestLabels()
@ -335,7 +340,7 @@ func TestTask_EmptyInputDoesNotCrash(t *testing.T) {
childJob := tj.job.newChildJob("f", 10)
tj.job.end()
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, mocks.NewMockObservabilitySender(mockCtrl))
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, reporter)
go func() {
stage.run(ctx)

View File

@ -111,7 +111,7 @@ func (s *Service) readCacheFile() {
file, err := os.Open(s.cacheFilepath)
if err != nil {
s.log.WithError(err).Info("Unable to open cache file")
s.log.WithError(err).Error("Unable to open cache file")
return
}

View File

@ -101,7 +101,7 @@ func (u *Updater) InstallUpdate(ctx context.Context, downloader Downloader, upda
update.Package+".sig",
)
if err != nil {
return fmt.Errorf("%w: %w", ErrDownloadVerify, err)
return ErrDownloadVerify
}
if err := u.installer.InstallUpdate(update.Version, bytes.NewReader(b)); err != nil {

View File

@ -115,6 +115,7 @@ func New(
isNew bool,
notificationStore *notifications.Store,
getFlagValFn unleash.GetFlagValueFn,
pushObservabilityMetric observability.PushObsMetricFn,
) (*User, error) {
user, err := newImpl(
ctx,
@ -136,6 +137,7 @@ func New(
isNew,
notificationStore,
getFlagValFn,
pushObservabilityMetric,
)
if err != nil {
// Cleanup any pending resources on error
@ -170,6 +172,7 @@ func newImpl(
isNew bool,
notificationStore *notifications.Store,
getFlagValueFn unleash.GetFlagValueFn,
pushObservabilityMetric observability.PushObsMetricFn,
) (*User, error) {
logrus.WithField("userID", apiUser.ID).Info("Creating new user")
@ -285,10 +288,9 @@ func newImpl(
syncConfigDir,
user.maxSyncMemory,
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.
user.goStatusProgress = user.tasks.PeriodicOrTrigger(configstatus.ProgressCheckInterval, 0, func(ctx context.Context) {

View File

@ -175,6 +175,7 @@ func withUser(tb testing.TB, ctx context.Context, _ *server.Server, m *proton.Ma
func(_ string) bool {
return false
},
func(_ proton.ObservabilityMetric) {},
)
require.NoError(tb, err)
defer user.Close()

View File

@ -19,7 +19,6 @@ package vault
import (
"fmt"
"strings"
"github.com/bradenaw/juniper/xslices"
"golang.org/x/exp/slices"
@ -38,10 +37,6 @@ func (user *User) Username() string {
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.
func (user *User) PrimaryEmail() string {
return user.vault.getUser(user.userID).PrimaryEmail
@ -247,15 +242,3 @@ func (user *User) SetShouldSync(shouldResync bool) error {
func (user *User) GetShouldResync() bool {
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
})
}

View File

@ -240,10 +240,6 @@ func (vault *Vault) GetOrAddUser(userID, username, primaryEmail, authUID, authRe
return nil, false, err
}
if err := user.updateUsernameUnsafe(username); err != nil {
return nil, false, err
}
return user, false, nil
}
}
@ -454,22 +450,6 @@ func (vault *Vault) getUser(userID string) UserData {
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 {
vault.lock.Lock()
defer vault.lock.Unlock()

View File

@ -857,23 +857,6 @@ func getFileReader(filename string) io.Reader {
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{}
func (panicReader) Read(_ []byte) (int, error) {

View File

@ -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--

View File

@ -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>

View File

@ -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;
}
}
}

View File

@ -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!");
}
}
}
}

View File

@ -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();
}
}
}

View File

@ -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";
}
}

View File

@ -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]);
}
}
}

View File

@ -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));
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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>

View File

@ -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

View File

@ -57,6 +57,7 @@ Feature: IMAP create messages
And IMAP client "1" eventually sees the following messages in "All Mail":
| from | to | subject | body |
| [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
When IMAP client "1" appends the following messages to "INBOX":

View File

@ -164,6 +164,7 @@ Feature: IMAP Draft messages
And IMAP client "1" eventually sees the following messages in "Drafts":
| to | subject | body |
| 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
Scenario: Only one draft in Drafts and All Mail after editing it locally multiple times

View File

@ -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"
}
]
}
]
}
}
"""

View File

@ -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

View File

@ -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
})
}

View File

@ -218,12 +218,6 @@ func (s *scenario) steps(ctx *godog.ScenarioContext) {
ctx.Step(`^the contact "([^"]*)" of user "([^"]*)" has public key from file "([^"]*)"$`, s.contactOfUserHasPubKeyFromFile)
// ==== 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)
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)
}