Compare commits

...

21 Commits

Author SHA1 Message Date
c6f1f159f3 chore: Bridge Perth Narrows 3.0.18 2023-02-28 06:53:16 +01:00
82af4e01bc feat(GODT-2364): wait and retry once if the gRPC service config file exists but cannot be opened. 2023-02-28 06:21:36 +01:00
9ad5f74409 feat(GODT-2364): added optional details to C++ exceptions. 2023-02-28 06:21:25 +01:00
10cf153678 fix(GODT-2413): use qEnvironmentVariable() instead of qgetenv(). 2023-02-27 15:41:26 +01:00
5ba07db7e3 chore: Bump Gluon for GODT-2399, GODT-2400 and GODT-2414
fix(GODT-2399): Defer updated message deletion
fix(GODT-2400): Allow state updates to be applied if command fails
fix(GODT-2414): Multiple deletion bug in WriteControlledStore
2023-02-27 14:53:37 +01:00
ad0d4ebd36 fix(GODT-2412): Don't treat context cancellation as BadEvent 2023-02-27 14:34:35 +01:00
9f3c14ab1e fix(GODT-2404): Handle unexpected EOF
When fetching too many attachment bodies at once, the read can fail with
io.ErrUnexpectedEOF. In that case, we returun an error so the fetch is retried.
2023-02-27 14:33:44 +01:00
74cf5d422b fix(GODT-2390): Missing changes from pervious commit
Always reports error type to sentry.

Add error checks for get event as well.
2023-02-27 14:33:38 +01:00
dcf694588c fix(GODT-2390): Add reports for uncaught json and net.opErr
Report to sentry if we see some uncaught network err, but don't force
the user logout.

If we catch an uncaught json parser error we report the error to sentry
and let the user be logged out later.

Finally this patch also prints the error type in UserBadEvent sentry
report to further help diagnose issues.
2023-02-27 14:33:21 +01:00
82c388a0dd chore: Bridge Perth Narrows 3.0.18 2023-02-23 06:58:54 +01:00
94ed09b437 feat(GODT-2366): Handle failed message updates as creates
This handles the following case:
- event says message was created
- we try to fetch the message but API says the doesn’t exist yet — we skip applying the “message created” update
- event then says message was updated at some point in the future
- we try to handle it but fail because we don’t have the message — we should treat it as a creation
2023-02-21 16:07:27 +01:00
57962e5757 chore: Bump gluon to create missing messages during MessageUpdated 2023-02-21 16:07:27 +01:00
8a5c8eaf6e chore: Use gluon temp/hotfix-perth-narrows branch 2023-02-21 16:07:27 +01:00
30029f489e doc: changelog typo 2023-02-17 13:34:03 +01:00
2faeebe9e7 chore: Bridge Perth Narrows 3.0.16/17 2023-02-16 17:46:31 +01:00
f6727a56d2 fix(GODT-2371): Continue, not return, when handling draft 2023-02-16 17:46:24 +01:00
d7fd39503f chore: Bridge Perth Narrows 3.0.15/16 2023-02-13 15:06:36 +01:00
b4b66f94ec feat(GODT-2355): improve wording and actions on bad event 2023-02-13 14:27:34 +01:00
cbd36184bd feat(GODT-2354): report failed to load users. 2023-02-10 11:53:05 +00:00
465f754803 feat(GODT-2353): show popup only after 3.0.16 2023-02-09 17:00:58 +01:00
2fa7c97f39 fix(GODT-2351): Bump GPA to better handle net.OpError 2023-02-09 12:04:39 +01:00
30 changed files with 357 additions and 119 deletions

View File

@ -2,6 +2,43 @@
Changelog [format](http://keepachangelog.com/en/1.0.0/)
## [Bridge 3.0.18] Perth Narrows
### Fixed
* GODT-2364: Wait and retry once if the gRPC service config file exists but cannot be opened.
* GODT-2364: Added optional details to C++ exceptions.
* GODT-2413: Use qEnvironmentVariable() instead of qgetenv().
* GODT-2412: Don't treat context cancellation as BadEvent.
* GODT-2404: Handle unexpected EOF.
* GODT-2400: Allow state updates to be applied if command fails.
* GODT-2399: Fix immediate message deletion during updates.
* GODT-2390: Missing changes from pervious commit.
* GODT-2390: Add reports for uncaught json and net.opErr.
* GODT-2414: Multiple deletion bug in WriteControlledStore.
## [Bridge 3.0.18] Perth Narrows
### Fixed
* GODT-2392: Create message if gluon updateMessage returns `no such message`.
* GODT-2391: Create draft if missing during message update on gluon side.
## [Bridge 3.0.16/17] Perth Narrows
### Fixed
* GODT-2371: Continue, not return, when handling draft.
## [Bridge 3.0.15] Perth Narrows
### Changed
* GODT-2355: Improve wording and actions on bad event.
### Fixed
* GODT-2354: Report failed load users.
* GODT-2353: Show popup only after 3.0.16.
* GODT-2351: Bump GPA to better handle net.OpError.
## [Bridge 3.0.14] Perth Narrows
### Fixed

View File

@ -11,7 +11,7 @@ ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
.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.0.14+git
BRIDGE_APP_VERSION?=3.0.19+git
APP_VERSION:=${BRIDGE_APP_VERSION}
APP_FULL_NAME:=Proton Mail Bridge
APP_VENDOR:=Proton AG
@ -294,7 +294,7 @@ gofiles: ./internal/bridge/credits.go
cd ./utils/ && ./credits.sh bridge
## Run and debug
.PHONY: run run-qt run-qt-cli run-nogui run-cli run-noninteractive run-debug run-qml-preview clean-vendor clean-frontend-qt clean-frontend-qt-common clean
.PHONY: run run-qt run-qt-cli run-nogui run-cli run-noninteractive run-debug run-gui-tester clean-vendor clean-frontend-qt clean-frontend-qt-common clean
LOG?=debug
LOG_IMAP?=client # client/server/all, or empty to turn it off
@ -321,6 +321,20 @@ run-nogui: build-nogui clean-vendor gofiles
run-debug:
dlv debug ./cmd/Desktop-Bridge/main.go -- -l=debug
ifeq "${TARGET_OS}" "windows"
EXE_SUFFIX=.exe
endif
bridge-gui-tester: build-gui
cp ./cmd/Desktop-Bridge/deploy/${TARGET_OS}/bridge-gui${EXE_SUFFIX} .
cd ./internal/frontend/bridge-gui/bridge-gui-tester && cmake . && make
run-gui-tester: bridge-gui-tester
# copying tester as bridge so bridge-gui will start it and connect to it automatically
cp ./internal/frontend/bridge-gui/bridge-gui-tester/bridge-gui-tester${EXE_SUFFIX} bridge${EXE_SUFFIX}
./bridge-gui${EXE_SUFFIX}
clean-vendor:
rm -rf ./vendor

4
go.mod
View File

@ -5,9 +5,9 @@ go 1.18
require (
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
github.com/Masterminds/semver/v3 v3.1.1
github.com/ProtonMail/gluon v0.14.2-0.20230207072331-53797c5aa3f6
github.com/ProtonMail/gluon v0.14.2-0.20230227135029-cef8f5824680
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
github.com/ProtonMail/go-proton-api v0.3.1-0.20230207122130-dd2095ddc7fe
github.com/ProtonMail/go-proton-api v0.3.1-0.20230209110241-fe7894c4931a
github.com/ProtonMail/go-rfc5322 v0.11.0
github.com/ProtonMail/gopenpgp/v2 v2.4.10
github.com/PuerkitoBio/goquery v1.8.0

8
go.sum
View File

@ -28,8 +28,8 @@ 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/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
github.com/ProtonMail/gluon v0.14.2-0.20230207072331-53797c5aa3f6 h1:HR944ZH7lN6sCA9OJMTdyoH1IRU0dBjxQHc7W0vFVrg=
github.com/ProtonMail/gluon v0.14.2-0.20230207072331-53797c5aa3f6/go.mod h1:z2AxLIiBCT1K+0OBHyaDI7AEaO5qI6/BEC2TE42vs4Q=
github.com/ProtonMail/gluon v0.14.2-0.20230227135029-cef8f5824680 h1:NGp7LfbsKePRHBgMcgquycHx3CSuS7255i0wanAiCuY=
github.com/ProtonMail/gluon v0.14.2-0.20230227135029-cef8f5824680/go.mod h1:z2AxLIiBCT1K+0OBHyaDI7AEaO5qI6/BEC2TE42vs4Q=
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-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
@ -41,8 +41,8 @@ github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753/go.mod h1:NB
github.com/ProtonMail/go-mime v0.0.0-20220302105931-303f85f7fe0f/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4=
github.com/ProtonMail/go-mime v0.0.0-20220429130430-2192574d760f h1:4IWzKjHzZxdrW9k4zl/qCwenOVHDbVDADPPHFLjs0Oc=
github.com/ProtonMail/go-mime v0.0.0-20220429130430-2192574d760f/go.mod h1:qRZgbeASl2a9OwmsV85aWwRqic0NHPh+9ewGAzb4cgM=
github.com/ProtonMail/go-proton-api v0.3.1-0.20230207122130-dd2095ddc7fe h1:um5Kp4WLzq28G7JMafv9lpmXFxasyg4RI2MhEFRjoJY=
github.com/ProtonMail/go-proton-api v0.3.1-0.20230207122130-dd2095ddc7fe/go.mod h1:JUo5IQG0hNuPRuDpOUsCOvtee6UjTEHHF1QN2i8RSos=
github.com/ProtonMail/go-proton-api v0.3.1-0.20230209110241-fe7894c4931a h1:h9KLPt0HTCJjILYHREWCYnZv+1xaYmOVx/rxiT/1dIg=
github.com/ProtonMail/go-proton-api v0.3.1-0.20230209110241-fe7894c4931a/go.mod h1:JUo5IQG0hNuPRuDpOUsCOvtee6UjTEHHF1QN2i8RSos=
github.com/ProtonMail/go-rfc5322 v0.11.0 h1:o5Obrm4DpmQEffvgsVqG6S4BKwC1Wat+hYwjIp2YcCY=
github.com/ProtonMail/go-rfc5322 v0.11.0/go.mod h1:6oOKr0jXvpoE6pwTx/HukigQpX2J9WUf6h0auplrFTw=
github.com/ProtonMail/go-srp v0.0.5 h1:xhUioxZgDbCnpo9JehyFhwwsn9JLWkUGfB0oiKXgiGg=

View File

@ -21,6 +21,7 @@ package bridge
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net"
"net/http"
@ -38,6 +39,7 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/focus"
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/sentry"
"github.com/ProtonMail/proton-bridge/v3/internal/user"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/bradenaw/juniper/xslices"
@ -378,6 +380,9 @@ func (bridge *Bridge) init(tlsReporter TLSReporter) error {
bridge.goLoad = bridge.tasks.Trigger(func(ctx context.Context) {
if err := bridge.loadUsers(ctx); err != nil {
logrus.WithError(err).Error("Failed to load users")
if netErr := new(proton.NetError); !errors.As(err, &netErr) {
sentry.ReportError(bridge.reporter, "Failed to load users", err)
}
} else {
bridge.publish(events.AllUsersLoaded{})
}

View File

@ -22,6 +22,7 @@ import (
"fmt"
"github.com/ProtonMail/gluon/reporter"
"github.com/ProtonMail/proton-bridge/v3/internal"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/user"
@ -56,6 +57,9 @@ func (bridge *Bridge) handleUserEvent(ctx context.Context, user *user.User, even
case events.UserBadEvent:
bridge.handleUserBadEvent(ctx, user, event.Error)
case events.UncategorizedEventError:
bridge.handleUncategorizedErrorEvent(event)
}
return nil
@ -139,7 +143,8 @@ func (bridge *Bridge) handleUserDeauth(ctx context.Context, user *user.User) {
func (bridge *Bridge) handleUserBadEvent(ctx context.Context, user *user.User, err error) {
safe.Lock(func() {
if rerr := bridge.reporter.ReportMessageWithContext("Failed to handle event", reporter.Context{
"error": err,
"error_type": fmt.Sprintf("%T", internal.ErrCause(err)),
"error": err,
}); rerr != nil {
logrus.WithError(rerr).Error("Failed to report failed event handling")
}
@ -147,3 +152,12 @@ func (bridge *Bridge) handleUserBadEvent(ctx context.Context, user *user.User, e
bridge.logoutUser(ctx, user, true, false)
}, 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": fmt.Sprintf("%T", internal.ErrCause(event.Error)),
"error": event.Error,
}); rerr != nil {
logrus.WithError(rerr).Error("Failed to report failed event handling")
}
}

31
internal/errors.go Normal file
View File

@ -0,0 +1,31 @@
// Copyright (c) 2023 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 internal
import "errors"
// ErrCause returns the cause of the error, the inner-most error in the wrapped chain.
func ErrCause(err error) error {
cause := err
for errors.Unwrap(cause) != nil {
cause = errors.Unwrap(cause)
}
return cause
}

View File

@ -156,3 +156,14 @@ type AddressModeChanged struct {
func (event AddressModeChanged) String() string {
return fmt.Sprintf("AddressModeChanged: UserID: %s, AddressMode: %s", event.UserID, event.AddressMode)
}
type UncategorizedEventError struct {
eventBase
UserID string
Error error
}
func (event UncategorizedEventError) String() string {
return fmt.Sprintf("UncategorizedEventError: UserID: %s, Source:%T, Error: %s", event.UserID, event.Error, event.Error)
}

View File

@ -73,13 +73,17 @@ ProcessMonitor *AppController::bridgeMonitor() const {
//****************************************************************************************************************************************************
/// \param[in] function The function that caught the exception.
/// \param[in] message The error message.
/// \param[in] details The details for the error.
//****************************************************************************************************************************************************
void AppController::onFatalError(QString const &function, QString const &message) {
QString const fullMessage = QString("%1(): %2").arg(function, message);
auto uuid = reportSentryException(SENTRY_LEVEL_ERROR, "AppController got notified of a fatal error", "Exception", fullMessage.toLocal8Bit());
void AppController::onFatalError(QString const &function, QString const &message, QString const& details) {
QString fullMessage = QString("%1(): %2").arg(function, message);
if (!details.isEmpty())
fullMessage += "\n\nDetails:\n" + details;
sentry_uuid_s const uuid = reportSentryException(SENTRY_LEVEL_ERROR, "AppController got notified of a fatal error", "Exception",
fullMessage.toLocal8Bit());
QMessageBox::critical(nullptr, tr("Error"), message);
restart(true);
log().fatal(QString("reportID: %1 Captured exception: %2").arg(QByteArray(uuid.bytes, 16).toHex()).arg(fullMessage));
log().fatal(QString("reportID: %1 Captured exception: %2").arg(QByteArray(uuid.bytes, 16).toHex(), fullMessage));
qApp->exit(EXIT_FAILURE);
}

View File

@ -58,7 +58,7 @@ public: // member functions.
void setLauncherArgs(const QString& launcher, const QStringList& args);
public slots:
void onFatalError(QString const &function, QString const &message); ///< Handle fatal errors.
void onFatalError(QString const &function, QString const &message, QString const& details); ///< Handle fatal errors.
private: // member functions
AppController(); ///< Default constructor.

View File

@ -25,8 +25,8 @@
#define HANDLE_EXCEPTION(x) try { x } \
catch (Exception const &e) { emit fatalError(__func__, e.qwhat()); } \
catch (...) { emit fatalError(__func__, QString("An unknown exception occurred")); }
catch (Exception const &e) { emit fatalError(__func__, e.qwhat(), e.details()); } \
catch (...) { emit fatalError(__func__, QString("An unknown exception occurred"), QString()); }
#define HANDLE_EXCEPTION_RETURN_BOOL(x) HANDLE_EXCEPTION(x) return false;
#define HANDLE_EXCEPTION_RETURN_QSTRING(x) HANDLE_EXCEPTION(x) return QString();
#define HANDLE_EXCEPTION_RETURN_ZERO(x) HANDLE_EXCEPTION(x) return 0;
@ -56,12 +56,8 @@ void QMLBackend::init(GRPCConfig const &serviceConfig) {
app().grpc().setLog(&log);
this->connectGrpcEvents();
QString error;
if (app().grpc().connectToServer(serviceConfig, app().bridgeMonitor(), error)) {
app().log().info("Connected to backend via gRPC service.");
} else {
throw Exception(QString("Cannot connectToServer to go backend via gRPC: %1").arg(error));
}
app().grpc().connectToServer(serviceConfig, app().bridgeMonitor());
app().log().info("Connected to backend via gRPC service.");
QString bridgeVer;
app().grpc().version(bridgeVer);
@ -597,7 +593,8 @@ void QMLBackend::setDiskCachePath(QUrl const &path) const {
void QMLBackend::login(QString const &username, QString const &password) const {
HANDLE_EXCEPTION(
if (username.compare("coco@bandicoot", Qt::CaseInsensitive) == 0) {
throw Exception("User requested bridge-gui to crash by trying to log as coco@bandicoot");
throw Exception("User requested bridge-gui to crash by trying to log as coco@bandicoot",
"This error exists for test purposes and should be ignored.");
}
app().grpc().login(username, password);
)
@ -874,12 +871,14 @@ void QMLBackend::onLoginAlreadyLoggedIn(QString const &userID) {
//****************************************************************************************************************************************************
void QMLBackend::onUserBadEvent(QString const &userID, QString const &errorMessage) {
HANDLE_EXCEPTION(
Q_UNUSED(errorMessage);
SPUser const user = users_->getUserWithID(userID);
if (!user)
app().log().error(QString("Received bad event for unknown user %1").arg(user->id()));
user->setState(UserState::SignedOut);
emit userBadEvent(tr("%1 was logged out because of an internal error.").arg(user->primaryEmailOrUsername()));
emit userBadEvent(
tr("Internal error: %1 was automatically logged out. Please log in again or report this problem if the issue persists.").arg(user->primaryEmailOrUsername()),
errorMessage
);
emit selectUser(userID);
emit showMainWindow();
)

View File

@ -222,7 +222,7 @@ signals: // Signals received from the Go backend, to be forwarded to QML
void addressChangedLogout(QString const &address); ///< Signal for the 'addressChangedLogout' gRPC stream event.
void apiCertIssue(); ///< Signal for the 'apiCertIssue' gRPC stream event.
void userDisconnected(QString const &username); ///< Signal for the 'userDisconnected' gRPC stream event.
void userBadEvent(QString const &message); ///< Signal for the 'userBadEvent' gRPC stream event.
void userBadEvent(QString const &description, QString const &errorMessage); ///< Signal for the 'userBadEvent' gRPC stream event.
void internetOff(); ///< Signal for the 'internetOff' gRPC stream event.
void internetOn(); ///< Signal for the 'internetOn' gRPC stream event.
void resetFinished(); ///< Signal for the 'resetFinished' gRPC stream event.
@ -235,7 +235,7 @@ signals: // Signals received from the Go backend, to be forwarded to QML
void selectUser(QString const); ///< Signal that request the given user account to be displayed.
// This signal is emitted when an exception is intercepted is calls triggered by QML. QML engine would intercept the exception otherwise.
void fatalError(QString const &function, QString const &message) const; ///< Signal emitted when an fatal error occurs.
void fatalError(QString const &function, QString const &message, QString const &details) const; ///< Signal emitted when an fatal error occurs.
private: // member functions
void retrieveUserList(); ///< Retrieve the list of users via gRPC.

View File

@ -428,9 +428,16 @@ int main(int argc, char *argv[]) {
return result;
}
catch (Exception const &e) {
auto uuid = reportSentryException(SENTRY_LEVEL_ERROR, "Exception occurred during main", "Exception", e.what());
QString fullMessage = e.qwhat();
bool const hasDetails = !e.details().isEmpty();
if (hasDetails)
fullMessage += "\n\nDetails:\n" + e.details();
sentry_uuid_s const uuid = reportSentryException(SENTRY_LEVEL_ERROR, "Exception occurred during main", "Exception", fullMessage.toLocal8Bit());
QMessageBox::critical(nullptr, "Error", e.qwhat());
QTextStream(stderr) << "reportID: " << QByteArray(uuid.bytes, 16).toHex() << "Captured exception :" << e.qwhat() << "\n";
QTextStream errStream(stderr);
errStream << "reportID: " << QByteArray(uuid.bytes, 16).toHex() << " Captured exception :" << e.qwhat() << "\n";
if (hasDetails)
errStream << "\nDetails:\n" << e.details() << "\n";
return EXIT_FAILURE;
}
}

View File

@ -170,6 +170,10 @@ SettingsView {
}
}
function setDescription(message) {
description.text = message
}
function setDefaultValue() {
description.text = ""
address.text = root.selectedAddress

View File

@ -348,6 +348,7 @@ Item {
}
BugReportView { // 8
id: bugReport
colorScheme: root.colorScheme
selectedAddress: {
if (accounts.currentIndex < 0) return ""
@ -409,10 +410,14 @@ Item {
}
accounts.currentIndex = i;
if (user.state === EUserState.SignedOut)
showSignIn(user.primaryEmailOrUsername())
showSignIn(user.primaryEmailOrUsername())
return;
}
console.error("User with ID ", userID, " was not found in the account list")
}
function showBugReportAndPrefill(description) {
rightContent.showBugReport()
bugReport.setDescription(description)
}
}

View File

@ -169,7 +169,6 @@ ApplicationWindow {
root.showSetup(null,"")
}
}
}
NotificationPopups {
@ -188,6 +187,10 @@ ApplicationWindow {
function showHelp() { contentWrapper.showHelp() }
function selectUser(userID) { contentWrapper.selectUser(userID) }
function showBugReportAndPrefill(message) {
contentWrapper.showBugReportAndPrefill(message)
}
function showSignIn(username) {
if (contentLayout.currentIndex == 1) return
contentWrapper.showSignIn(username)

View File

@ -129,6 +129,11 @@ Item {
notification: root.notifications.noActiveKeyForRecipient
}
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.userBadEvent
}
NotificationDialog {
colorScheme: root.colorScheme
notification: root.notifications.genericError

View File

@ -1046,8 +1046,8 @@ QtObject {
property Notification apiCertIssue: Notification {
title: qsTr("Unable to establish a \nsecure connection to \nProton servers")
description: qsTr("Bridge cannot verify the authenticity of Proton servers on your current network due to a TLS certificate error. " +
"Start Bridge again after ensuring your connection is secure and/or connecting to a VPN. Learn more about TLS pinning " +
"<a href=\"https://proton.me/blog/tls-ssl-certificate#Extra-security-precautions-taken-by-ProtonMail\">here</a>.")
"Start Bridge again after ensuring your connection is secure and/or connecting to a VPN. Learn more about TLS pinning " +
"<a href=\"https://proton.me/blog/tls-ssl-certificate#Extra-security-precautions-taken-by-ProtonMail\">here</a>.")
brief: title
icon: "./icons/ic-exclamation-circle-filled.svg"
@ -1086,7 +1086,7 @@ QtObject {
function onNoActiveKeyForRecipient(email) {
root.noActiveKeyForRecipient.description = qsTr("There are no active keys to encrypt your message to %1. "+
"Please update the setting for this contact.").arg(email)
"Please update the setting for this contact.").arg(email)
root.noActiveKeyForRecipient.active = true
}
}
@ -1103,17 +1103,21 @@ QtObject {
}
property Notification userBadEvent: Notification {
title: qsTr("User was logged out")
title: qsTr("Your account was logged out")
brief: title
description: "#PlaceHolderText"
icon: "./icons/ic-exclamation-circle-filled.svg"
type: Notification.NotificationType.Danger
group: Notifications.Group.Connection
group: Notifications.Group.Connection | Notifications.Group.Dialogs
property var bugReportMsg: "Reporting an issue:\n\n\"%1\"\n\nError: %2\n\nThe issue persists even after loggin back in."
property var errorMessage: ""
Connections {
target: Backend
function onUserBadEvent(message) {
root.userBadEvent.description = message
function onUserBadEvent(description, errorMessage) {
root.userBadEvent.description = description
root.userBadEvent.errorMessage = errorMessage
root.userBadEvent.active = true
}
}
@ -1125,8 +1129,22 @@ QtObject {
onTriggered: {
root.userBadEvent.active = false
}
},
Action {
text: qsTr("Report")
onTriggered: {
root.frontendMain.showBugReportAndPrefill(
root.userBadEvent.bugReportMsg.
arg( root.userBadEvent.description).
arg(root.userBadEvent.errorMessage)
)
root.userBadEvent.active = false
}
}
]
}
property Notification genericError: Notification {
@ -1135,14 +1153,14 @@ QtObject {
icon: "./icons/ic-exclamation-circle-filled.svg"
type: Notification.NotificationType.Danger
group: Notifications.Group.Dialogs
Connections {
target: Backend
function onGenericError(title, description) {
root.genericError.title = title
root.genericError.description = description
root.genericError.active = true;
}
Connections {
target: Backend
function onGenericError(title, description) {
root.genericError.title = title
root.genericError.description = description
root.genericError.active = true;
}
}
action: [
Action {

View File

@ -71,20 +71,20 @@ std::mt19937_64 &rng() {
QString userConfigDir() {
QString dir;
#ifdef Q_OS_WIN
dir = qgetenv ("AppData");
dir = qEnvironmentVariable("AppData");
if (dir.isEmpty())
throw Exception("%AppData% is not defined.");
#elif defined(Q_OS_IOS) || defined(Q_OS_DARWIN)
dir = qgetenv("HOME");
dir = qEnvironmentVariable("HOME");
if (dir.isEmpty()) {
throw Exception("$HOME is not defined.");
}
dir += "/Library/Application Support";
#else
dir = qgetenv ("XDG_CONFIG_HOME");
dir = qEnvironmentVariable("XDG_CONFIG_HOME");
if (dir.isEmpty())
{
dir = qgetenv ("HOME");
dir = qEnvironmentVariable("HOME");
if (dir.isEmpty())
throw Exception("neither $XDG_CONFIG_HOME nor $HOME are defined");
dir += "/.config";
@ -104,20 +104,20 @@ QString userCacheDir() {
QString dir;
#ifdef Q_OS_WIN
dir = qgetenv ("LocalAppData");
dir = qEnvironmentVariable("LocalAppData");
if (dir.isEmpty())
throw Exception("%LocalAppData% is not defined.");
#elif defined(Q_OS_IOS) || defined(Q_OS_DARWIN)
dir = qgetenv("HOME");
dir = qEnvironmentVariable("HOME");
if (dir.isEmpty()) {
throw Exception("$HOME is not defined.");
}
dir += "/Library/Caches";
#else
dir = qgetenv ("XDG_CACHE_HOME");
dir = qEnvironmentVariable("XDG_CACHE_HOME");
if (dir.isEmpty())
{
dir = qgetenv ("HOME");
dir = qEnvironmentVariable("HOME");
if (dir.isEmpty())
throw Exception("neither $XDG_CACHE_HOME nor $HOME are defined");
dir += "/.cache";
@ -138,10 +138,10 @@ QString userDataDir() {
QString folder;
#ifdef Q_OS_LINUX
QString dir = qgetenv ("XDG_DATA_HOME");
QString dir = qEnvironmentVariable("XDG_DATA_HOME");
if (dir.isEmpty())
{
dir = qgetenv ("HOME");
dir = qEnvironmentVariable("HOME");
if (dir.isEmpty())
throw Exception("neither $XDG_DATA_HOME nor $HOME are defined");
dir += "/.local/share";

View File

@ -23,11 +23,13 @@ namespace bridgepp {
//****************************************************************************************************************************************************
/// \param[in] what A description of the exception
/// \param[in] what A description of the exception.
/// \param[in] details The optional details for the exception.
//****************************************************************************************************************************************************
Exception::Exception(QString what) noexcept
Exception::Exception(QString what, QString details) noexcept
: std::exception()
, what_(std::move(what)) {
, what_(std::move(what))
, details_(std::move(details)) {
}
@ -36,7 +38,8 @@ Exception::Exception(QString what) noexcept
//****************************************************************************************************************************************************
Exception::Exception(Exception const &ref) noexcept
: std::exception(ref)
, what_(ref.what_) {
, what_(ref.what_)
, details_(ref.details_) {
}
@ -45,14 +48,15 @@ Exception::Exception(Exception const &ref) noexcept
//****************************************************************************************************************************************************
Exception::Exception(Exception &&ref) noexcept
: std::exception(ref)
, what_(ref.what_) {
, what_(ref.what_)
, details_(ref.details_) {
}
//****************************************************************************************************************************************************
/// \return a string describing the exception
//****************************************************************************************************************************************************
QString const &Exception::qwhat() const noexcept {
QString Exception::qwhat() const noexcept {
return what_;
}
@ -65,4 +69,12 @@ const char *Exception::what() const noexcept {
}
//****************************************************************************************************************************************************
/// \return The details for the exception.
//****************************************************************************************************************************************************
QString Exception::details() const noexcept {
return details_;
}
} // namespace bridgepp

View File

@ -31,17 +31,19 @@ namespace bridgepp {
//****************************************************************************************************************************************************
class Exception : public std::exception {
public: // member functions
explicit Exception(QString what = QString()) noexcept; ///< Constructor
explicit Exception(QString what = QString(), QString details = QString()) noexcept; ///< Constructor
Exception(Exception const &ref) noexcept; ///< copy constructor
Exception(Exception &&ref) noexcept; ///< copy constructor
Exception &operator=(Exception const &) = delete; ///< Disabled assignment operator
Exception &operator=(Exception &&) = delete; ///< Disabled assignment operator
~Exception() noexcept override = default; ///< Destructor
QString const &qwhat() const noexcept; ///< Return the description of the exception as a QString
QString qwhat() const noexcept; ///< Return the description of the exception as a QString
const char *what() const noexcept override; ///< Return the description of the exception as C style string
QString details() const noexcept; ///< Return the details for the exception
private: // data members
QString const what_; ///< The description of the exception
QString const what_; ///< The description of the exception.
QString const details_; ///< The optional details for the exception.
};

View File

@ -88,8 +88,9 @@ GRPCConfig GRPCClient::waitAndRetrieveServiceConfig(qint64 timeoutMs, ProcessMon
}
GRPCConfig sc;
if (!sc.load(path)) {
throw Exception("The gRPC service configuration file is invalid.");
QString err;
if (!sc.load(path, &err)) {
throw Exception("The gRPC service configuration file is invalid.", err);
}
return sc;
@ -105,11 +106,10 @@ void GRPCClient::setLog(Log *log) {
//****************************************************************************************************************************************************
/// \param[out] outError If the function returns false, this variable contains a description of the error.
/// \param[in] serverProcess An optional server process to monitor. If the process it, no need and retry, as connexion cannot be established. Ignored if null.
/// \return true iff the connection was successful.
//****************************************************************************************************************************************************
bool GRPCClient::connectToServer(GRPCConfig const &config, ProcessMonitor *serverProcess, QString &outError) {
void GRPCClient::connectToServer(GRPCConfig const &config, ProcessMonitor *serverProcess) {
try {
serverToken_ = config.token.toStdString();
QString address;
@ -158,9 +158,10 @@ bool GRPCClient::connectToServer(GRPCConfig const &config, ProcessMonitor *serve
this->logInfo("Successfully connected to gRPC server.");
QString const clientToken = QUuid::createUuid().toString();
QString clientConfigPath = createClientConfigFile(clientToken);
QString error;
QString clientConfigPath = createClientConfigFile(clientToken, &error);
if (clientConfigPath.isEmpty()) {
throw Exception("gRPC client config could not be saved.");
throw Exception("gRPC client config could not be saved.", error);
}
this->logInfo(QString("Client config file was saved to '%1'").arg(QDir::toNativeSeparators(clientConfigPath)));
@ -176,12 +177,9 @@ bool GRPCClient::connectToServer(GRPCConfig const &config, ProcessMonitor *serve
}
log_->info("gRPC token was validated");
return true;
}
catch (Exception const &e) {
outError = e.qwhat();
return false;
throw Exception("Cannot connect to Go backend via gRPC: " + e.qwhat(), e.details());
}
}

View File

@ -59,7 +59,7 @@ public: // member functions.
GRPCClient &operator=(GRPCClient const &) = delete; ///< Disabled assignment operator.
GRPCClient &operator=(GRPCClient &&) = delete; ///< Disabled move assignment operator.
void setLog(Log *log); ///< Set the log for the client.
bool connectToServer(GRPCConfig const &config, class ProcessMonitor *serverProcess, QString &outError); ///< Establish connection to the gRPC server.
void connectToServer(GRPCConfig const &config, class ProcessMonitor *serverProcess); ///< Establish connection to the gRPC server.
grpc::Status checkTokens(QString const &clientConfigPath, QString &outReturnedClientToken); ///< Performs a token check.
grpc::Status addLogEntry(Log::Level level, QString const &package, QString const &message); ///< Performs the "AddLogEntry" gRPC call.

View File

@ -25,8 +25,7 @@ using namespace bridgepp;
namespace {
Exception const invalidFileException("The service configuration file is invalid"); // Exception for invalid config.
Exception const couldNotSaveException("The service configuration file could not be saved"); ///< Exception for write errors.
Exception const invalidFileException("The content of the service configuration file is invalid"); // Exception for invalid config.
QString const keyPort = "port"; ///< The JSON key for the port.
QString const keyCert = "cert"; ///< The JSON key for the TLS certificate.
QString const keyToken = "token"; ///< The JSON key for the identification token.
@ -78,8 +77,14 @@ qint32 jsonIntValue(QJsonObject const &object, QString const &key) {
bool GRPCConfig::load(QString const &path, QString *outError) {
try {
QFile file(path);
if (!file.exists())
throw Exception("The gRPC service configuration file does not exist.");
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
throw Exception("Could not open gRPC service config file.");
QThread::msleep(500); // we wait a bit and retry once, just in case server is not done writing/moving the config file.
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
throw Exception("The gRPC service configuration file exists but cannot be opened.");
}
}
QJsonDocument const doc = QJsonDocument::fromJson(file.readAll());
@ -93,7 +98,7 @@ bool GRPCConfig::load(QString const &path, QString *outError) {
}
catch (Exception const &e) {
if (outError) {
*outError = e.qwhat();
*outError = QString("Error loading gRPC service configuration file '%1'.\n%2").arg(QFileInfo(path).absoluteFilePath(), e.qwhat());
}
return false;
}
@ -115,19 +120,19 @@ bool GRPCConfig::save(QString const &path, QString *outError) {
QFile file(path);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
throw couldNotSaveException;
throw Exception("The file could not be opened for writing.");
}
QByteArray const array = QJsonDocument(object).toJson();
if (array.size() != file.write(array)) {
throw couldNotSaveException;
throw Exception("An error occurred while writing to the file.");
}
return true;
}
catch (Exception const &e) {
if (outError) {
*outError = e.qwhat();
*outError = QString("Error saving gRPC service configuration file '%1'.\n%2").arg(QFileInfo(path).absoluteFilePath(), e.qwhat());
}
return false;
}

View File

@ -76,10 +76,12 @@ QString grpcClientConfigBasePath() {
//****************************************************************************************************************************************************
/// \param[in] token The token to put in the file.
/// \param[out] outError if the function returns an empty string and this pointer is not null, the pointer variable holds a description of the error
/// on exit.
/// \return The path of the created file.
/// \return A null string if the file could not be saved..
/// \return A null string if the file could not be saved.
//****************************************************************************************************************************************************
QString createClientConfigFile(QString const &token) {
QString createClientConfigFile(QString const &token, QString *outError) {
QString const basePath = grpcClientConfigBasePath();
QString path, error;
for (qint32 i = 0; i < 1000; ++i) // we try a decent amount of times
@ -88,13 +90,16 @@ QString createClientConfigFile(QString const &token) {
if (!QFileInfo(path).exists()) {
GRPCConfig config;
config.token = token;
if (!config.save(path)) {
if (!config.save(path, outError)) {
return QString();
}
return path;
}
}
if (outError)
*outError = "no usable client configuration file name could be found.";
return QString();
}

View File

@ -36,7 +36,7 @@ typedef std::shared_ptr<grpc::StreamEvent> SPStreamEvent; ///< Type definition f
QString grpcServerConfigPath(); ///< Return the path of the gRPC server config file.
QString grpcClientConfigBasePath(); ///< Return the path of the gRPC client config file.
QString createClientConfigFile(QString const &token); ///< Create the client config file the server will retrieve and return its path.
QString createClientConfigFile(QString const &token, QString *outError); ///< Create the client config file the server will retrieve and return its path.
grpc::LogLevel logLevelToGRPC(Log::Level level); ///< Convert a Log::Level to gRPC enum value.
Log::Level logLevelFromGRPC(grpc::LogLevel level); ///< Convert a grpc::LogLevel to a Log::Level.
grpc::UserState userStateToGRPC(UserState state); ///< Convert a bridgepp::UserState to a grpc::UserState.

View File

@ -96,9 +96,11 @@ func (s *Service) GuiReady(ctx context.Context, _ *emptypb.Empty) (*GuiReadyResp
s.initializationDone.Do(s.initializing.Done)
// Splash screen should be displayed only to users who start v3 for the first time after upgrading from v2.
// Splash screen should be displayed only to users who start v3.0.17 or later for the first time after upgrading from v2.
return &GuiReadyResponse{
ShowSplashScreen: (!s.bridge.GetFirstStart()) && s.bridge.GetLastVersion().LessThan(semver.MustParse("3.0.0")),
ShowSplashScreen: (!s.bridge.GetFirstStart()) &&
s.bridge.GetLastVersion().LessThan(semver.MustParse("3.0.0")) &&
s.bridge.GetCurrentVersion().GreaterThan(semver.MustParse("3.0.16")),
}, nil
}

View File

@ -26,6 +26,7 @@ import (
"runtime"
"time"
"github.com/ProtonMail/gluon/reporter"
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/pkg/restarter"
"github.com/getsentry/sentry-go"
@ -163,6 +164,14 @@ func (r *Reporter) scopedReport(context map[string]interface{}, doReport func())
return nil
}
func ReportError(r reporter.Reporter, msg string, err error) {
if rerr := r.ReportMessageWithContext(msg, reporter.Context{
"error": err.Error(),
}); rerr != nil {
logrus.WithError(rerr).WithField("msg", msg).Error("Failed to send report")
}
}
// SkipDuringUnwind removes caller from the traceback.
func SkipDuringUnwind() {
pcs := make([]uintptr, 2)

View File

@ -23,6 +23,7 @@ import (
"fmt"
"net/http"
"github.com/ProtonMail/gluon"
"github.com/ProtonMail/gluon/imap"
"github.com/ProtonMail/gluon/queue"
"github.com/ProtonMail/gluon/reporter"
@ -415,9 +416,7 @@ func (user *User) handleMessageEvents(ctx context.Context, messageEvents []proto
switch event.Action {
case proton.EventCreate:
updates, err := user.handleCreateMessageEvent(
logging.WithLogrusField(ctx, "action", "create message"),
event)
updates, err := user.handleCreateMessageEvent(logging.WithLogrusField(ctx, "action", "create message"), event.Message)
if err != nil {
if rerr := user.reporter.ReportMessageWithContext("Failed to apply create message event", reporter.Context{
"error": err,
@ -446,6 +445,7 @@ func (user *User) handleMessageEvents(ctx context.Context, messageEvents []proto
}); rerr != nil {
user.log.WithError(err).Error("Failed to report update draft message event error")
}
return fmt.Errorf("failed to handle update draft event: %w", err)
}
@ -453,7 +453,7 @@ func (user *User) handleMessageEvents(ctx context.Context, messageEvents []proto
return err
}
return nil
continue
}
// GODT-2028 - Use better events here. It should be possible to have 3 separate events that refrain to
@ -462,7 +462,7 @@ func (user *User) handleMessageEvents(ctx context.Context, messageEvents []proto
// Issue regular update to handle mailboxes and flag changes.
updates, err := user.handleUpdateMessageEvent(
logging.WithLogrusField(ctx, "action", "update message"),
event,
event.Message,
)
if err != nil {
if rerr := user.reporter.ReportMessageWithContext("Failed to apply update message event", reporter.Context{
@ -470,10 +470,23 @@ func (user *User) handleMessageEvents(ctx context.Context, messageEvents []proto
}); rerr != nil {
user.log.WithError(err).Error("Failed to report update message event error")
}
return fmt.Errorf("failed to handle update message event: %w", err)
}
if err := waitOnIMAPUpdates(ctx, updates); err != nil {
// If the update fails on the gluon side because it doesn't exist, we try to create the message instead.
if err := waitOnIMAPUpdates(ctx, updates); gluon.IsNoSuchMessage(err) {
user.log.WithError(err).Error("Failed to handle update message event in gluon, will try creating it")
updates, err := user.handleCreateMessageEvent(ctx, event.Message)
if err != nil {
return fmt.Errorf("failed to handle update message event as create: %w", err)
}
if err := waitOnIMAPUpdates(ctx, updates); err != nil {
return err
}
} else if err != nil {
return err
}
@ -488,6 +501,7 @@ func (user *User) handleMessageEvents(ctx context.Context, messageEvents []proto
}); rerr != nil {
user.log.WithError(err).Error("Failed to report delete message event error")
}
return fmt.Errorf("failed to handle delete message event: %w", err)
}
@ -500,12 +514,17 @@ func (user *User) handleMessageEvents(ctx context.Context, messageEvents []proto
return nil
}
func (user *User) handleCreateMessageEvent(ctx context.Context, event proton.MessageEvent) ([]imap.Update, error) {
full, err := user.client.GetFullMessage(ctx, event.Message.ID)
func (user *User) handleCreateMessageEvent(ctx context.Context, message proton.MessageMetadata) ([]imap.Update, error) {
user.log.WithFields(logrus.Fields{
"messageID": message.ID,
"subject": logging.Sensitive(message.Subject),
}).Info("Handling message created event")
full, err := user.client.GetFullMessage(ctx, message.ID)
if err != nil {
// If the message is not found, it means that it has been deleted before we could fetch it.
if apiErr := new(proton.APIError); errors.As(err, &apiErr) && apiErr.Status == http.StatusUnprocessableEntity {
user.log.WithField("messageID", event.Message.ID).Warn("Cannot add new message: full message is missing on API")
user.log.WithField("messageID", message.ID).Warn("Cannot create new message: full message is missing on API")
return nil, nil
}
@ -513,19 +532,15 @@ func (user *User) handleCreateMessageEvent(ctx context.Context, event proton.Mes
}
return safe.RLockRetErr(func() ([]imap.Update, error) {
user.log.WithFields(logrus.Fields{
"messageID": event.ID,
"subject": logging.Sensitive(event.Message.Subject),
}).Info("Handling message created event")
var update imap.Update
if err := withAddrKR(user.apiUser, user.apiAddrs[event.Message.AddressID], user.vault.KeyPass(), func(_, addrKR *crypto.KeyRing) error {
if err := withAddrKR(user.apiUser, user.apiAddrs[message.AddressID], user.vault.KeyPass(), func(_, addrKR *crypto.KeyRing) error {
res := buildRFC822(user.apiLabels, full, addrKR)
if res.err != nil {
user.log.WithError(err).Error("Failed to build RFC822 message")
if err := user.vault.AddFailedMessageID(event.ID); err != nil {
if err := user.vault.AddFailedMessageID(message.ID); err != nil {
user.log.WithError(err).Error("Failed to add failed message ID to vault")
}
@ -539,7 +554,7 @@ func (user *User) handleCreateMessageEvent(ctx context.Context, event proton.Mes
return nil
}
if err := user.vault.RemFailedMessageID(event.ID); err != nil {
if err := user.vault.RemFailedMessageID(message.ID); err != nil {
user.log.WithError(err).Error("Failed to remove failed message ID from vault")
}
@ -555,21 +570,21 @@ func (user *User) handleCreateMessageEvent(ctx context.Context, event proton.Mes
}, user.apiUserLock, user.apiAddrsLock, user.apiLabelsLock, user.updateChLock)
}
func (user *User) handleUpdateMessageEvent(ctx context.Context, event proton.MessageEvent) ([]imap.Update, error) { //nolint:unparam
func (user *User) handleUpdateMessageEvent(ctx context.Context, message proton.MessageMetadata) ([]imap.Update, error) { //nolint:unparam
return safe.RLockRetErr(func() ([]imap.Update, error) {
user.log.WithFields(logrus.Fields{
"messageID": event.ID,
"subject": logging.Sensitive(event.Message.Subject),
"messageID": message.ID,
"subject": logging.Sensitive(message.Subject),
}).Info("Handling message updated event")
update := imap.NewMessageMailboxesUpdated(
imap.MessageID(event.ID),
mapTo[string, imap.MailboxID](wantLabels(user.apiLabels, event.Message.LabelIDs)),
event.Message.Seen(),
event.Message.Starred(),
imap.MessageID(message.ID),
mapTo[string, imap.MailboxID](wantLabels(user.apiLabels, message.LabelIDs)),
message.Seen(),
message.Starred(),
)
user.updateCh[event.Message.AddressID].Enqueue(update)
user.updateCh[message.AddressID].Enqueue(update)
return []imap.Update{update}, nil
}, user.apiLabelsLock, user.updateChLock)
@ -602,7 +617,7 @@ func (user *User) handleUpdateDraftEvent(ctx context.Context, event proton.Messa
if err != nil {
// If the message is not found, it means that it has been deleted before we could fetch it.
if apiErr := new(proton.APIError); errors.As(err, &apiErr) && apiErr.Status == http.StatusUnprocessableEntity {
user.log.WithField("messageID", event.Message.ID).Warn("Cannot add new draft: full message is missing on API")
user.log.WithField("messageID", event.Message.ID).Warn("Cannot update draft: full message is missing on API")
return nil, nil
}
@ -640,6 +655,7 @@ func (user *User) handleUpdateDraftEvent(ctx context.Context, event proton.Messa
res.update.Literal,
res.update.MailboxIDs,
res.update.ParsedMessage,
true, // Is the message doesn't exist, silently create it.
)
user.updateCh[full.AddressID].Enqueue(update)

View File

@ -20,10 +20,11 @@ package user
import (
"context"
"crypto/subtle"
"encoding/json"
"errors"
"fmt"
"io"
"net/url"
"net"
"strings"
"sync/atomic"
"time"
@ -33,6 +34,7 @@ import (
"github.com/ProtonMail/gluon/queue"
"github.com/ProtonMail/gluon/reporter"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/proton-bridge/v3/internal"
"github.com/ProtonMail/proton-bridge/v3/internal/async"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
@ -626,7 +628,7 @@ func (user *User) doEventPoll(ctx context.Context) error {
event, err := user.client.GetEvent(ctx, user.vault.EventID())
if err != nil {
return fmt.Errorf("failed to get event: %w", err)
return fmt.Errorf("failed to get event (caused by %T): %w", internal.ErrCause(err), err)
}
// If the event ID hasn't changed, there are no new events.
@ -642,14 +644,44 @@ func (user *User) doEventPoll(ctx context.Context) error {
// Handle the event.
if err := user.handleAPIEvent(ctx, event); err != nil {
// If the error is a context cancellation, return error to retry later.
if errors.Is(err, context.Canceled) {
return fmt.Errorf("failed to handle event due to context cancellation: %w", err)
}
// If the error is a network error, return error to retry later.
if netErr := new(proton.NetError); errors.As(err, &netErr) {
return fmt.Errorf("failed to handle event due to network issue: %w", err)
}
// If the error is a url.Error, return error to retry later.
if urlErr := new(url.Error); errors.As(err, &urlErr) {
return fmt.Errorf("failed to handle event due to URL issue: %w", err)
// Catch all for uncategorized net errors that may slip through.
if netErr := new(net.OpError); errors.As(err, &netErr) {
user.eventCh.Enqueue(events.UncategorizedEventError{
UserID: user.ID(),
Error: err,
})
return fmt.Errorf("failed to handle event due to network issues (uncategorized): %w", err)
}
// In case a json decode error slips through.
if jsonErr := new(json.UnmarshalTypeError); errors.As(err, &jsonErr) {
user.eventCh.Enqueue(events.UncategorizedEventError{
UserID: user.ID(),
Error: err,
})
return fmt.Errorf("failed to handle event due to JSON issue: %w", err)
}
// If the error is an unexpected EOF, return error to retry later.
if errors.Is(err, io.ErrUnexpectedEOF) {
user.eventCh.Enqueue(events.UncategorizedEventError{
UserID: user.ID(),
Error: err,
})
return fmt.Errorf("failed to handle event due to EOF: %w", err)
}
// If the error is a server-side issue, return error to retry later.