forked from Silverfish/proton-bridge
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c6f1f159f3 | |||
| 82af4e01bc | |||
| 9ad5f74409 | |||
| 10cf153678 | |||
| 5ba07db7e3 | |||
| ad0d4ebd36 | |||
| 9f3c14ab1e | |||
| 74cf5d422b | |||
| dcf694588c | |||
| 82c388a0dd | |||
| 94ed09b437 | |||
| 57962e5757 | |||
| 8a5c8eaf6e | |||
| 30029f489e | |||
| 2faeebe9e7 | |||
| f6727a56d2 | |||
| d7fd39503f | |||
| b4b66f94ec | |||
| cbd36184bd | |||
| 465f754803 | |||
| 2fa7c97f39 |
37
Changelog.md
37
Changelog.md
@ -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
|
||||
|
||||
18
Makefile
18
Makefile
@ -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
4
go.mod
@ -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
8
go.sum
@ -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=
|
||||
|
||||
@ -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{})
|
||||
}
|
||||
|
||||
@ -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,6 +143,7 @@ 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_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
31
internal/errors.go
Normal 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
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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().grpc().connectToServer(serviceConfig, app().bridgeMonitor());
|
||||
app().log().info("Connected to backend via gRPC service.");
|
||||
} else {
|
||||
throw Exception(QString("Cannot connectToServer to go backend via gRPC: %1").arg(error));
|
||||
}
|
||||
|
||||
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();
|
||||
)
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -170,6 +170,10 @@ SettingsView {
|
||||
}
|
||||
}
|
||||
|
||||
function setDescription(message) {
|
||||
description.text = message
|
||||
}
|
||||
|
||||
function setDefaultValue() {
|
||||
description.text = ""
|
||||
address.text = root.selectedAddress
|
||||
|
||||
@ -348,6 +348,7 @@ Item {
|
||||
}
|
||||
|
||||
BugReportView { // 8
|
||||
id: bugReport
|
||||
colorScheme: root.colorScheme
|
||||
selectedAddress: {
|
||||
if (accounts.currentIndex < 0) return ""
|
||||
@ -415,4 +416,8 @@ Item {
|
||||
console.error("User with ID ", userID, " was not found in the account list")
|
||||
}
|
||||
|
||||
function showBugReportAndPrefill(description) {
|
||||
rightContent.showBugReport()
|
||||
bugReport.setDescription(description)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
};
|
||||
|
||||
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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,12 +470,25 @@ 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 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
|
||||
}
|
||||
|
||||
case proton.EventDelete:
|
||||
updates, err := user.handleDeleteMessageEvent(
|
||||
@ -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)
|
||||
|
||||
@ -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.
|
||||
|
||||
Reference in New Issue
Block a user