feat(BRIDGE-75): Bridge repair button/feature implemented

This commit is contained in:
Atanas Janeshliev
2024-06-03 12:37:23 +00:00
parent e2b361b9a6
commit ff0615167b
23 changed files with 1502 additions and 1040 deletions

View File

@ -539,6 +539,49 @@ func (bridge *Bridge) onStatusDown(ctx context.Context) {
} }
} }
func (bridge *Bridge) Repair() {
var wg sync.WaitGroup
userIDS := bridge.GetUserIDs()
for _, userID := range userIDS {
logPkg.Info("Initiating repair for userID:", userID)
userInfo, err := bridge.GetUserInfo(userID)
if err != nil {
logPkg.WithError(err).Error("Failed getting user info for repair; ID:", userID)
continue
}
if userInfo.State != Connected {
logPkg.Info("User is not connected. Repair will be executed on following successful log in.", userID)
if err := bridge.vault.GetUser(userID, func(user *vault.User) {
if err := user.SetShouldSync(true); err != nil {
logPkg.WithError(err).Error("Failed setting vault should sync for user:", userID)
}
}); err != nil {
logPkg.WithError(err).Error("Unable to get user vault when scheduling repair:", userID)
}
continue
}
bridgeUser, ok := bridge.users[userID]
if !ok {
logPkg.Info("UserID does not exist in bridge user map", userID)
continue
}
wg.Add(1)
go func(userID string) {
defer wg.Done()
if err = bridgeUser.ResyncIMAP(); err != nil {
logPkg.WithError(err).Error("Failed re-syncing IMAP for userID", userID)
}
}(userID)
}
wg.Wait()
}
func loadTLSConfig(vault *vault.Vault) (*tls.Config, error) { func loadTLSConfig(vault *vault.Vault) (*tls.Config, error) {
cert, err := tls.X509KeyPair(vault.GetBridgeTLSCert()) cert, err := tls.X509KeyPair(vault.GetBridgeTLSCert())
if err != nil { if err != nil {

View File

@ -606,6 +606,8 @@ func (bridge *Bridge) addUserWithVault(
// As we need at least one user to send heartbeat, try to send it. // As we need at least one user to send heartbeat, try to send it.
bridge.heartbeat.start() bridge.heartbeat.start()
user.PublishEvent(ctx, events.UserLoadedCheckResync{UserID: user.ID()})
return nil return nil
} }

View File

@ -36,6 +36,9 @@ func (bridge *Bridge) handleUserEvent(ctx context.Context, user *user.User, even
case events.UserBadEvent: case events.UserBadEvent:
bridge.handleUserBadEvent(ctx, user, event) bridge.handleUserBadEvent(ctx, user, event)
case events.UserLoadedCheckResync:
user.VerifyResyncAndExecute()
case events.UncategorizedEventError: case events.UncategorizedEventError:
bridge.handleUncategorizedErrorEvent(event) bridge.handleUncategorizedErrorEvent(event)
} }

View File

@ -202,3 +202,13 @@ type UncategorizedEventError struct {
func (event UncategorizedEventError) String() string { func (event UncategorizedEventError) String() string {
return fmt.Sprintf("UncategorizedEventError: UserID: %s, Source:%T, Error: %s", event.UserID, event.Error, event.Error) return fmt.Sprintf("UncategorizedEventError: UserID: %s, Source:%T, Error: %s", event.UserID, event.Error, event.Error)
} }
type UserLoadedCheckResync struct {
eventBase
UserID string
}
func (event UserLoadedCheckResync) String() string {
return fmt.Sprintf("UserLoadedCheckResync: UserID: %s", event.UserID)
}

View File

@ -1328,6 +1328,8 @@ void QMLBackend::connectGrpcEvents() {
connect(client, &GRPCClient::certificateInstallFailed, this, &QMLBackend::certificateInstallFailed); connect(client, &GRPCClient::certificateInstallFailed, this, &QMLBackend::certificateInstallFailed);
connect(client, &GRPCClient::showMainWindow, [&]() { this->showMainWindow("gRPC showMainWindow event"); }); connect(client, &GRPCClient::showMainWindow, [&]() { this->showMainWindow("gRPC showMainWindow event"); });
connect(client, &GRPCClient::knowledgeBasSuggestionsReceived, this, &QMLBackend::receivedKnowledgeBaseSuggestions); connect(client, &GRPCClient::knowledgeBasSuggestionsReceived, this, &QMLBackend::receivedKnowledgeBaseSuggestions);
connect(client, &GRPCClient::repairStarted, this, &QMLBackend::repairStarted);
connect(client, &GRPCClient::allUsersLoaded, this, &QMLBackend::allUsersLoaded);
// cache events // cache events
connect(client, &GRPCClient::cantMoveDiskCache, this, &QMLBackend::cantMoveDiskCache); connect(client, &GRPCClient::cantMoveDiskCache, this, &QMLBackend::cantMoveDiskCache);
@ -1410,3 +1412,9 @@ void QMLBackend::displayBadEventDialog(QString const &userID) {
emit showMainWindow(); emit showMainWindow();
) )
} }
void QMLBackend::triggerRepair() const {
HANDLE_EXCEPTION(
app().grpc().triggerRepair();
)
}

View File

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

View File

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

View File

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

View File

@ -13,6 +13,8 @@
import QtQml import QtQml
import Qt.labs.platform import Qt.labs.platform
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts
import QtQuick
import "../" import "../"
QtObject { QtObject {
@ -60,7 +62,7 @@ QtObject {
target: Backend target: Backend
} }
} }
property var all: [root.noInternet, root.imapPortStartupError, root.smtpPortStartupError, root.imapPortChangeError, root.smtpPortChangeError, root.imapConnectionModeChangeError, root.smtpConnectionModeChangeError, root.updateManualReady, root.updateManualRestartNeeded, root.updateManualError, root.updateForce, root.updateForceError, root.updateSilentRestartNeeded, root.updateSilentError, root.updateIsLatestVersion, root.loginConnectionError, root.onlyPaidUsers, root.alreadyLoggedIn, root.enableBeta, root.bugReportSendSuccess, root.bugReportSendError, root.bugReportSendFallback, root.cacheCantMove, root.cacheLocationChangeSuccess, root.enableSplitMode, root.resetBridge, root.changeAllMailVisibility, root.deleteAccount, root.noKeychain, root.rebuildKeychain, root.addressChanged, root.apiCertIssue, root.userBadEvent, root.imapLoginWhileSignedOut, root.genericError, root.genericQuestion, root.hvErrorEvent] property var all: [root.noInternet, root.imapPortStartupError, root.smtpPortStartupError, root.imapPortChangeError, root.smtpPortChangeError, root.imapConnectionModeChangeError, root.smtpConnectionModeChangeError, root.updateManualReady, root.updateManualRestartNeeded, root.updateManualError, root.updateForce, root.updateForceError, root.updateSilentRestartNeeded, root.updateSilentError, root.updateIsLatestVersion, root.loginConnectionError, root.onlyPaidUsers, root.alreadyLoggedIn, root.enableBeta, root.bugReportSendSuccess, root.bugReportSendError, root.bugReportSendFallback, root.cacheCantMove, root.cacheLocationChangeSuccess, root.enableSplitMode, root.resetBridge, root.changeAllMailVisibility, root.deleteAccount, root.noKeychain, root.rebuildKeychain, root.addressChanged, root.apiCertIssue, root.userBadEvent, root.imapLoginWhileSignedOut, root.genericError, root.genericQuestion, root.hvErrorEvent, root.repairBridge]
property Notification alreadyLoggedIn: Notification { property Notification alreadyLoggedIn: Notification {
brief: qsTr("Already signed in") brief: qsTr("Already signed in")
description: qsTr("This account is already signed in.") description: qsTr("This account is already signed in.")
@ -1150,6 +1152,52 @@ QtObject {
target: Backend target: Backend
} }
}
property Notification repairBridge: Notification {
brief: title
description: qsTr("This action will reload all accounts, cached data, and re-download emails. Messages may temporarily disappear but will reappear progressively. Email clients stay connected to Bridge.")
group: Notifications.Group.Configuration | Notifications.Group.Dialogs
icon: "./icons/ic-exclamation-circle-filled.svg"
title: qsTr("Repair Bridge?")
type: Notification.NotificationType.Danger
action: [
Action {
id: repairBridge_cancel
text: qsTr("Cancel")
onTriggered: {
root.repairBridge.active = false;
}
},
Action {
id: repairBridge_repair
text: qsTr("Repair")
onTriggered: {
repairBridge_repair.loading = true;
repairBridge_repair.enabled = false;
repairBridge_cancel.enabled = false;
Backend.triggerRepair();
}
}
]
Connections {
function onAskRepairBridge() {
root.repairBridge.active = true;
}
target: root
}
Connections {
function onRepairStarted() {
root.repairBridge.active = false;
repairBridge_repair.loading = false;
repairBridge_repair.enabled = true;
repairBridge_cancel.enabled = true;
}
target: Backend
}
} }
signal askChangeAllMailVisibility(var isVisibleNow) signal askChangeAllMailVisibility(var isVisibleNow)
@ -1158,4 +1206,5 @@ QtObject {
signal askEnableSplitMode(var user) signal askEnableSplitMode(var user)
signal askQuestion(var title, var description, var option1, var option2, var action1, var action2) signal askQuestion(var title, var description, var option1, var option2, var action1, var action2)
signal askResetBridge signal askResetBridge
signal askRepairBridge
} }

View File

@ -1198,6 +1198,14 @@ void GRPCClient::processAppEvent(AppEvent const &event) {
emit knowledgeBasSuggestionsReceived(suggestions); emit knowledgeBasSuggestionsReceived(suggestions);
break; break;
} }
case AppEvent::kRepairStarted:
this->logTrace("App event received: RepairStarted.");
emit repairStarted();
break;
case AppEvent::kAllUsersLoaded:
this->logTrace("App event received: AllUsersLoaded");
emit allUsersLoaded();
break;
default: default:
this->logError("Unknown App event received."); this->logError("Unknown App event received.");
} }
@ -1580,5 +1588,12 @@ grpc::Status GRPCClient::externalLinkClicked(QString const &link) {
return this->logGRPCCallStatus(stub_->ExternalLinkClicked(this->clientContext().get(), s, &empty), __FUNCTION__); return this->logGRPCCallStatus(stub_->ExternalLinkClicked(this->clientContext().get(), s, &empty), __FUNCTION__);
} }
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
grpc::Status GRPCClient::triggerRepair() {
return this->logGRPCCallStatus(stub_->TriggerRepair(this->clientContext().get(), empty, &empty), __FUNCTION__ );
}
} // namespace bridgepp } // namespace bridgepp

View File

@ -108,6 +108,7 @@ public: // member functions.
grpc::Status landingPageLink(QUrl &outUrl); ///< Performs the 'landingPageLink' call. grpc::Status landingPageLink(QUrl &outUrl); ///< Performs the 'landingPageLink' call.
grpc::Status hostname(QString &outHostname); ///< Performs the 'Hostname' call. grpc::Status hostname(QString &outHostname); ///< Performs the 'Hostname' call.
grpc::Status requestKnowledgeBaseSuggestions(QString const &input); ///< Performs the 'RequestKnowledgeBaseSuggestions' call. grpc::Status requestKnowledgeBaseSuggestions(QString const &input); ///< Performs the 'RequestKnowledgeBaseSuggestions' call.
grpc::Status triggerRepair(); ///< Performs the triggerRepair gRPC call.
signals: // app related signals signals: // app related signals
void internetStatus(bool isOn); void internetStatus(bool isOn);
@ -122,6 +123,8 @@ signals: // app related signals
void certificateInstallFailed(); void certificateInstallFailed();
void showMainWindow(); void showMainWindow();
void knowledgeBasSuggestionsReceived(QList<KnowledgeBaseSuggestion> const& suggestions); void knowledgeBasSuggestionsReceived(QList<KnowledgeBaseSuggestion> const& suggestions);
void repairStarted();
void allUsersLoaded();
public: // cache related calls public: // cache related calls

View File

@ -306,6 +306,12 @@ func New(
Aliases: []string{"del", "rm", "remove"}, Aliases: []string{"del", "rm", "remove"},
Completer: fe.completeUsernames, Completer: fe.completeUsernames,
}) })
fe.AddCmd(&ishell.Cmd{
Name: "repair",
Help: "reload all accounts and cached data, re-download emails. Email clients remain connected. Logged out users will be repaired on next login. (aliases: rep)",
Func: fe.repair,
Aliases: []string{"rep"},
})
badEventCmd := &ishell.Cmd{ badEventCmd := &ishell.Cmd{
Name: "bad-event", Name: "bad-event",

View File

@ -359,3 +359,9 @@ func (f *frontendCLI) isFile(location string) bool {
return !stat.IsDir() return !stat.IsDir()
} }
func (f *frontendCLI) repair(_ *ishell.Context) {
if f.yesNoQuestion("Are you sure you want to initialize a repair, this may take a while") {
f.bridge.Repair()
}
}

File diff suppressed because it is too large Load Diff

View File

@ -111,6 +111,9 @@ service Bridge {
// Server -> Client event stream // Server -> Client event stream
rpc RunEventStream(EventStreamRequest) returns (stream StreamEvent); // Keep streaming until StopEventStream is called. rpc RunEventStream(EventStreamRequest) returns (stream StreamEvent); // Keep streaming until StopEventStream is called.
rpc StopEventStream(google.protobuf.Empty) returns (google.protobuf.Empty); rpc StopEventStream(google.protobuf.Empty) returns (google.protobuf.Empty);
// Repair
rpc TriggerRepair(google.protobuf.Empty) returns (google.protobuf.Empty);
} }
//********************************************************************************************************************** //**********************************************************************************************************************
@ -272,6 +275,8 @@ message AppEvent {
CertificateInstallCanceledEvent certificateInstallCanceled = 10; CertificateInstallCanceledEvent certificateInstallCanceled = 10;
CertificateInstallFailedEvent certificateInstallFailed = 11; CertificateInstallFailedEvent certificateInstallFailed = 11;
KnowledgeBaseSuggestionsEvent knowledgeBaseSuggestions = 12; KnowledgeBaseSuggestionsEvent knowledgeBaseSuggestions = 12;
RepairStartedEvent repairStarted = 13;
AllUsersLoadedEvent allUsersLoaded = 14;
} }
} }
@ -289,6 +294,8 @@ message ReportBugFallbackEvent {}
message CertificateInstallSuccessEvent {} message CertificateInstallSuccessEvent {}
message CertificateInstallCanceledEvent {} message CertificateInstallCanceledEvent {}
message CertificateInstallFailedEvent {} message CertificateInstallFailedEvent {}
message RepairStartedEvent {}
message AllUsersLoadedEvent {}
message KnowledgeBaseSuggestion { message KnowledgeBaseSuggestion {
string url = 1; string url = 1;

View File

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

View File

@ -241,6 +241,14 @@ func NewGenericErrorEvent(errorCode ErrorCode) *StreamEvent {
return genericErrorEvent(&GenericErrorEvent{Code: errorCode}) return genericErrorEvent(&GenericErrorEvent{Code: errorCode})
} }
func NewRepairStartedEvent() *StreamEvent {
return appEvent(&AppEvent{Event: &AppEvent_RepairStarted{RepairStarted: &RepairStartedEvent{}}})
}
func NewAllUsersLoadedEvent() *StreamEvent {
return appEvent(&AppEvent{Event: &AppEvent_AllUsersLoaded{AllUsersLoaded: &AllUsersLoadedEvent{}}})
}
// Event category factory functions. // Event category factory functions.
func appEvent(appEvent *AppEvent) *StreamEvent { func appEvent(appEvent *AppEvent) *StreamEvent {

View File

@ -401,6 +401,9 @@ func (s *Service) watchEvents() {
case events.TLSIssue: case events.TLSIssue:
_ = s.SendEvent(NewMailApiCertIssue()) _ = s.SendEvent(NewMailApiCertIssue())
case events.AllUsersLoaded:
_ = s.SendEvent(NewAllUsersLoadedEvent())
} }
} }
} }

View File

@ -838,6 +838,20 @@ func (s *Service) CurrentKeychain(_ context.Context, _ *emptypb.Empty) (*wrapper
return wrapperspb.String(helper), nil return wrapperspb.String(helper), nil
} }
func (s *Service) TriggerRepair(_ context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) {
s.log.Debug("TriggerRepair")
go func() {
defer func() {
async.HandlePanic(s.panicHandler)
_ = s.SendEvent(NewRepairStartedEvent())
}()
s.bridge.Repair()
}()
return &emptypb.Empty{}, nil
}
func base64Decode(in []byte) ([]byte, error) { func base64Decode(in []byte) ([]byte, error) {
out := make([]byte, base64.StdEncoding.DecodedLen(len(in))) out := make([]byte, base64.StdEncoding.DecodedLen(len(in)))

View File

@ -717,3 +717,22 @@ func (user *User) protonAddresses() []proton.Address {
return addresses return addresses
} }
func (user *User) VerifyResyncAndExecute() {
user.log.Info("Checking whether logged in user should re-sync. UserID:", user.ID())
if user.vault.GetShouldResync() {
user.log.Info("User should re-sync, starting re-sync process. UserID:", user.ID())
if err := user.vault.SetShouldSync(false); err != nil {
user.log.WithError(err).Error("Failed to disable re-sync flag in user vault. UserID:", user.ID())
}
if err := user.ResyncIMAP(); err != nil {
user.log.WithError(err).Error("Failed re-syncing IMAP for userID", user.ID())
}
}
}
func (user *User) ResyncIMAP() error {
return user.imapService.Resync(context.Background())
}

View File

@ -40,6 +40,8 @@ type UserData struct {
// **WARNING**: This value can't be removed until we have vault migration support. // **WARNING**: This value can't be removed until we have vault migration support.
UIDValidity map[string]imap.UID UIDValidity map[string]imap.UID
ShouldResync bool // Whether user should re-sync on log-in (this is triggered by the `repair` button)
} }
type AddressMode int type AddressMode int
@ -88,5 +90,7 @@ func newDefaultUser(userID, username, primaryEmail, authUID, authRef string, key
AuthUID: authUID, AuthUID: authUID,
AuthRef: authRef, AuthRef: authRef,
KeyPass: keyPass, KeyPass: keyPass,
ShouldResync: false,
} }
} }

View File

@ -232,3 +232,13 @@ func (user *User) Clear() error {
func (user *User) Close() error { func (user *User) Close() error {
return user.vault.detachUser(user.userID) return user.vault.detachUser(user.userID)
} }
func (user *User) SetShouldSync(shouldResync bool) error {
return user.vault.modUser(user.userID, func(data *UserData) {
data.ShouldResync = shouldResync
})
}
func (user *User) GetShouldResync() bool {
return user.vault.getUser(user.userID).ShouldResync
}

View File

@ -61,6 +61,7 @@ func TestUser_New(t *testing.T) {
// Check the user's initial sync status. // Check the user's initial sync status.
require.False(t, user.SyncStatus().HasLabels) require.False(t, user.SyncStatus().HasLabels)
require.False(t, user.SyncStatus().HasMessages) require.False(t, user.SyncStatus().HasMessages)
require.False(t, user.GetShouldResync())
} }
func TestUser_Clear(t *testing.T) { func TestUser_Clear(t *testing.T) {
@ -239,3 +240,34 @@ func TestUser_ForEach(t *testing.T) {
// The store should have no users again. // The store should have no users again.
require.Empty(t, s.GetUserIDs()) require.Empty(t, s.GetUserIDs())
} }
func TestUser_ShouldResync(t *testing.T) {
// Replace the token generator with a dummy one.
vault.RandomToken = func(size int) ([]byte, error) {
return []byte("token"), nil
}
// Create a new test vault.
s := newVault(t)
// There should be no users in the store.
require.Empty(t, s.GetUserIDs())
// Create a new user.
user, err := s.AddUser("userID", "username", "username@pm.me", "authUID", "authRef", []byte("keyPass"))
require.NoError(t, err)
// The user should be listed in the store.
require.ElementsMatch(t, []string{"userID"}, s.GetUserIDs())
// The shouldResync field is supposed to be false for new users.
require.False(t, user.GetShouldResync())
// Set it to true
if err := user.SetShouldSync(true); err != nil {
t.Fatalf("Failed to set should-sync: %v", err)
}
// Check whether it matches the correct value
require.True(t, user.GetShouldResync())
}