diff --git a/internal/frontend/bridge-gui/CMakeLists.txt b/internal/frontend/bridge-gui/CMakeLists.txt
index f873fbfe..cca7559d 100644
--- a/internal/frontend/bridge-gui/CMakeLists.txt
+++ b/internal/frontend/bridge-gui/CMakeLists.txt
@@ -31,4 +31,4 @@ project(frontend)
add_subdirectory(bridgepp)
add_subdirectory(bridge-gui)
-
+add_subdirectory(bridge-gui-tester)
diff --git a/internal/frontend/bridge-gui/bridge-gui-tester/AppController.cpp b/internal/frontend/bridge-gui/bridge-gui-tester/AppController.cpp
new file mode 100644
index 00000000..d06fc0c9
--- /dev/null
+++ b/internal/frontend/bridge-gui/bridge-gui-tester/AppController.cpp
@@ -0,0 +1,106 @@
+// Copyright (c) 2022 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 .
+
+
+#include "AppController.h"
+#include "GRPCService.h"
+#include
+#include
+#include "MainWindow.h"
+#include
+
+
+using namespace bridgepp;
+
+
+//****************************************************************************************************************************************************
+/// \return A reference to the application controller.
+//****************************************************************************************************************************************************
+AppController &app()
+{
+ static AppController app;
+ return app;
+}
+
+
+//****************************************************************************************************************************************************
+//
+//****************************************************************************************************************************************************
+AppController::AppController()
+ : log_(std::make_unique())
+ , bridgeGUILog_(std::make_unique())
+ , grpc_(std::make_unique())
+{
+
+}
+
+
+//****************************************************************************************************************************************************
+//
+//****************************************************************************************************************************************************
+AppController::~AppController() // NOLINT(modernize-use-equals-default): implementation in cpp file is required because of forward declaration of Log in header
+{
+
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] mainWindow The main window.
+//****************************************************************************************************************************************************
+void AppController::setMainWindow(MainWindow *mainWindow)
+{
+ mainWindow_ = mainWindow;
+ grpc_->connectProxySignals();
+}
+
+
+//****************************************************************************************************************************************************
+/// \return The main window.
+//****************************************************************************************************************************************************
+MainWindow &AppController::mainWindow()
+{
+ if (!mainWindow_)
+ throw Exception("mainWindow has not yet been registered.");
+ return *mainWindow_;
+}
+
+
+//****************************************************************************************************************************************************
+/// \return A reference to the log.
+//****************************************************************************************************************************************************
+bridgepp::Log &AppController::log()
+{
+ return *log_;
+}
+
+
+//****************************************************************************************************************************************************
+/// \return A reference to the bridge-gui log.
+//****************************************************************************************************************************************************
+bridgepp::Log &AppController::bridgeGUILog()
+{
+ return *bridgeGUILog_;
+}
+
+
+//****************************************************************************************************************************************************
+/// \return A reference to the gRPC service.
+//****************************************************************************************************************************************************
+GRPCService &AppController::grpc()
+{
+ return *grpc_;
+}
diff --git a/internal/frontend/bridge-gui/bridge-gui-tester/AppController.h b/internal/frontend/bridge-gui/bridge-gui-tester/AppController.h
new file mode 100644
index 00000000..379b3510
--- /dev/null
+++ b/internal/frontend/bridge-gui/bridge-gui-tester/AppController.h
@@ -0,0 +1,63 @@
+// Copyright (c) 2022 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 .
+
+
+#ifndef BRIDGE_GUI_TESTER_APP_CONTROLLER_H
+#define BRIDGE_GUI_TESTER_APP_CONTROLLER_H
+
+
+class MainWindow;
+class GRPCService;
+namespace grpc { class StreamEvent; }
+namespace bridgepp { class Log; }
+
+
+//**********************************************************************************************************************
+/// \brief Application controller class
+//**********************************************************************************************************************
+class AppController : public QObject
+{
+Q_OBJECT
+public: // member functions.
+ friend AppController &app();
+
+ AppController(AppController const &) = delete; ///< Disabled copy-constructor.
+ AppController(AppController &&) = delete; ///< Disabled assignment copy-constructor.
+ ~AppController() override; ///< Destructor.
+ AppController &operator=(AppController const &) = delete; ///< Disabled assignment operator.
+ AppController &operator=(AppController &&) = delete; ///< Disabled move assignment operator.
+ void setMainWindow(MainWindow *mainWindow); ///< Set the main window.
+ MainWindow &mainWindow(); ///< Return the main window.
+ bridgepp::Log &log(); ///< Return a reference to the log.
+ bridgepp::Log &bridgeGUILog(); ///< Return a reference to the bridge-gui log.
+ GRPCService &grpc(); ///< Return a reference to the gRPC service.
+
+private: // member functions.
+ AppController(); ///< Default constructor.
+
+private: // data members.
+ MainWindow *mainWindow_ { nullptr }; ///< The main window.
+ std::unique_ptr log_; ///< The log.
+ std::unique_ptr bridgeGUILog_; ///< The bridge-gui log.
+ std::unique_ptr grpc_; ///< The gRPC service.
+};
+
+
+AppController &app(); ///< Return a reference to the app controller.
+
+
+#endif // BRIDGE_GUI_TESTER_APP_CONTROLLER_H
diff --git a/internal/frontend/bridge-gui/bridge-gui-tester/CMakeLists.txt b/internal/frontend/bridge-gui/bridge-gui-tester/CMakeLists.txt
new file mode 100644
index 00000000..325ada4c
--- /dev/null
+++ b/internal/frontend/bridge-gui/bridge-gui-tester/CMakeLists.txt
@@ -0,0 +1,90 @@
+# Copyright (c) 2022 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 .
+
+
+cmake_minimum_required(VERSION 3.22)
+
+
+set(VCPKG_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../../../../extern/vcpkg")
+include(../BridgeSetup.cmake)
+
+
+#*****************************************************************************************************************************************************
+# Project
+#*****************************************************************************************************************************************************
+
+
+project(bridge-gui-tester LANGUAGES CXX)
+
+if (NOT DEFINED BRIDGE_APP_VERSION)
+ message(FATAL_ERROR "BRIDGE_APP_VERSION is not defined.")
+else()
+ message(STATUS "Bridge version is ${BRIDGE_APP_VERSION}")
+endif()
+
+set(CMAKE_CXX_STANDARD 20)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+
+
+#*****************************************************************************************************************************************************
+# Qt
+#*****************************************************************************************************************************************************
+
+
+if (NOT DEFINED ENV{QT6DIR})
+ message(FATAL_ERROR "QT6DIR needs to be defined and point to the root of your Qt 6 folder (e.g. /Users/MyName/Qt/6.3.1/clang_64).")
+endif()
+
+set(CMAKE_PREFIX_PATH $ENV{QT6DIR} ${CMAKE_PREFIX_PATH})
+find_package(Qt6 COMPONENTS Core Gui Widgets Qml REQUIRED)
+qt_standard_project_setup()
+message(STATUS "Using Qt ${Qt6_VERSION}")
+
+
+#*****************************************************************************************************************************************************
+# Source files and output
+#*****************************************************************************************************************************************************
+
+if (NOT TARGET bridgepp)
+add_subdirectory(../bridgepp bridgepp)
+endif()
+
+add_executable(bridge-gui-tester
+ AppController.cpp AppController.h
+ main.cpp
+ MainWindow.cpp MainWindow.h
+ GRPCQtProxy.cpp GRPCQtProxy.h
+ GRPCService.cpp GRPCService.h
+ GRPCServerWorker.cpp GRPCServerWorker.h
+ Tabs/SettingsTab.cpp Tabs/SettingsTab.h
+ Tabs/UsersTab.cpp Tabs/UsersTab.h
+ UserDialog.cpp UserDialog.h
+ UserTable.cpp UserTable.h
+ )
+
+target_precompile_headers(bridge-gui-tester PRIVATE Pch.h)
+target_include_directories(bridge-gui-tester PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
+target_compile_definitions(bridge-gui-tester PRIVATE BRIDGE_APP_VERSION=\"${BRIDGE_APP_VERSION}\")
+
+
+target_link_libraries(bridge-gui-tester
+ Qt6::Core
+ Qt6::Gui
+ Qt6::Widgets
+ Qt6::Qml
+ bridgepp
+ )
diff --git a/internal/frontend/bridge-gui/bridge-gui-tester/GRPCQtProxy.cpp b/internal/frontend/bridge-gui/bridge-gui-tester/GRPCQtProxy.cpp
new file mode 100644
index 00000000..936d7304
--- /dev/null
+++ b/internal/frontend/bridge-gui/bridge-gui-tester/GRPCQtProxy.cpp
@@ -0,0 +1,210 @@
+// Copyright (c) 2022 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 .
+
+
+#include "GRPCQtProxy.h"
+#include "MainWindow.h"
+
+
+//****************************************************************************************************************************************************
+//
+//****************************************************************************************************************************************************
+GRPCQtProxy::GRPCQtProxy()
+ : QObject(nullptr)
+{
+}
+
+//****************************************************************************************************************************************************
+//
+//****************************************************************************************************************************************************
+void GRPCQtProxy::connectSignals()
+{
+ MainWindow &mainWindow = app().mainWindow();
+ SettingsTab &settingsTab = mainWindow.settingsTab();
+ UsersTab &usersTab = mainWindow.usersTab();
+ connect(this, &GRPCQtProxy::delayedEventRequested, &mainWindow, &MainWindow::sendDelayedEvent);
+ connect(this, &GRPCQtProxy::setIsAutostartOnReceived, &settingsTab, &SettingsTab::setIsAutostartOn);
+ connect(this, &GRPCQtProxy::setIsBetaEnabledReceived, &settingsTab, &SettingsTab::setIsBetaEnabled);
+ connect(this, &GRPCQtProxy::setColorSchemeNameReceived, &settingsTab, &SettingsTab::setColorSchemeName);
+ connect(this, &GRPCQtProxy::reportBugReceived, &settingsTab, &SettingsTab::setBugReport);
+ connect(this, &GRPCQtProxy::setIsStreamingReceived, &settingsTab, &SettingsTab::setIsStreaming);
+ connect(this, &GRPCQtProxy::setClientPlatformReceived, &settingsTab, &SettingsTab::setClientPlatform);
+ connect(this, &GRPCQtProxy::changePortsReceived, &settingsTab, &SettingsTab::changePorts);
+ connect(this, &GRPCQtProxy::setUseSSLForSMTPReceived, &settingsTab, &SettingsTab::setUseSSLForSMTP);
+ connect(this, &GRPCQtProxy::setIsDoHEnabledReceived, &settingsTab, &SettingsTab::setIsDoHEnabled);
+ connect(this, &GRPCQtProxy::changeLocalCacheReceived, &settingsTab, &SettingsTab::changeLocalCache);
+ connect(this, &GRPCQtProxy::setIsAutomaticUpdateOnReceived, &settingsTab, &SettingsTab::setIsAutomaticUpdateOn);
+ connect(this, &GRPCQtProxy::setUserSplitModeReceived, &usersTab, &UsersTab::setUserSplitMode);
+ connect(this, &GRPCQtProxy::removeUserReceived, &usersTab, &UsersTab::removeUser);
+ connect(this, &GRPCQtProxy::logoutUserReceived, &usersTab, &UsersTab::logoutUser);
+ connect(this, &GRPCQtProxy::setUserSplitModeReceived, &usersTab, &UsersTab::setUserSplitMode);
+ connect(this, &GRPCQtProxy::configureUserAppleMailReceived, &usersTab, &UsersTab::configureUserAppleMail);
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] event The event.
+//****************************************************************************************************************************************************
+void GRPCQtProxy::sendDelayedEvent(bridgepp::SPStreamEvent const &event)
+{
+ emit delayedEventRequested(event);
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] on The value.
+//****************************************************************************************************************************************************
+void GRPCQtProxy::setIsAutostartOn(bool on)
+{
+ emit setIsAutostartOnReceived(on);
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] enabled The value.
+//****************************************************************************************************************************************************
+void GRPCQtProxy::setIsBetaEnabled(bool enabled)
+{
+ emit setIsBetaEnabledReceived(enabled);
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] name The color scheme.
+//****************************************************************************************************************************************************
+void GRPCQtProxy::setColorSchemeName(QString const &name)
+{
+ emit setColorSchemeNameReceived(name);
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] osType The OS type.
+/// \param[in] osVersion The OS version.
+/// \param[in] emailClient The email client.
+/// \param[in] address The address.
+/// \param[in] description The description.
+/// \param[in] includeLogs Should the logs be included.
+//****************************************************************************************************************************************************
+void GRPCQtProxy::reportBug(QString const &osType, QString const &osVersion, QString const &emailClient, QString const &address,
+ QString const &description, bool includeLogs)
+{
+ emit reportBugReceived(osType, osVersion, emailClient, address, description, includeLogs);
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] isStreaming Is the gRPC server streaming.
+//****************************************************************************************************************************************************
+void GRPCQtProxy::setIsStreaming(bool isStreaming)
+{
+ emit setIsStreamingReceived(isStreaming);
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] clientPlatform The client platform.
+//****************************************************************************************************************************************************
+void GRPCQtProxy::setClientPlatform(QString const &clientPlatform)
+{
+ emit setClientPlatformReceived(clientPlatform);
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] imapPort The IMAP port
+/// \param[in] smtpPort The SMTP port
+//****************************************************************************************************************************************************
+void GRPCQtProxy::changePorts(qint32 imapPort, qint32 smtpPort)
+{
+ emit changePortsReceived(imapPort, smtpPort);
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] use Should SMTP use SSL?
+//****************************************************************************************************************************************************
+void GRPCQtProxy::setUseSSLForSMTP(bool use)
+{
+ emit setUseSSLForSMTPReceived(use);
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] enabled Is DoH enabled?
+//****************************************************************************************************************************************************
+void GRPCQtProxy::setIsDoHEnabled(bool enabled)
+{
+ emit setIsDoHEnabledReceived(enabled);
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] enabled is cache on disk enabled?
+/// \param[in] path The path for the cache on disk.
+//****************************************************************************************************************************************************
+void GRPCQtProxy::changeLocalCache(bool enabled, QString const &path)
+{
+ emit changeLocalCacheReceived(enabled, path);
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] on Is automatic update on?
+//****************************************************************************************************************************************************
+void GRPCQtProxy::setIsAutomaticUpdateOn(bool on)
+{
+ emit setIsAutomaticUpdateOnReceived(on);
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] userID The userID.
+/// \param[in] makeItActive Should split mode be active.
+//****************************************************************************************************************************************************
+void GRPCQtProxy::setUserSplitMode(QString const &userID, bool makeItActive)
+{
+ emit setUserSplitModeReceived(userID, makeItActive);
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] userID The userID.
+//****************************************************************************************************************************************************
+void GRPCQtProxy::logoutUser(QString const &userID)
+{
+ emit logoutUserReceived(userID);
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] userID The userID.
+//****************************************************************************************************************************************************
+void GRPCQtProxy::removeUser(QString const &userID)
+{
+ emit removeUserReceived(userID);
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] userID The userID.
+/// \param[in] address The address.
+//****************************************************************************************************************************************************
+void GRPCQtProxy::configureUserAppleMail(QString const &userID, QString const &address)
+{
+ emit configureUserAppleMailReceived(userID, address);
+}
diff --git a/internal/frontend/bridge-gui/bridge-gui-tester/GRPCQtProxy.h b/internal/frontend/bridge-gui/bridge-gui-tester/GRPCQtProxy.h
new file mode 100644
index 00000000..7d3a1c45
--- /dev/null
+++ b/internal/frontend/bridge-gui/bridge-gui-tester/GRPCQtProxy.h
@@ -0,0 +1,80 @@
+// Copyright (c) 2022 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 .
+
+
+#ifndef BRIDGE_GUI_TESTER_GRPC_QT_PROXY_H
+#define BRIDGE_GUI_TESTER_GRPC_QT_PROXY_H
+
+
+#include
+
+
+//****************************************************************************************************************************************************
+/// \brief Proxy object used by the gRPC service (which does not inherit QObject) to use the Qt Signal/Slot system.
+//****************************************************************************************************************************************************
+class GRPCQtProxy : public QObject
+{
+Q_OBJECT
+public: // member functions.
+ GRPCQtProxy(); ///< Default constructor.
+ GRPCQtProxy(GRPCQtProxy const &) = delete; ///< Disabled copy-constructor.
+ GRPCQtProxy(GRPCQtProxy &&) = delete; ///< Disabled assignment copy-constructor.
+ ~GRPCQtProxy() override = default; ///< Destructor.
+ GRPCQtProxy &operator=(GRPCQtProxy const &) = delete; ///< Disabled assignment operator.
+ GRPCQtProxy &operator=(GRPCQtProxy &&) = delete; ///< Disabled move assignment operator.
+
+ void connectSignals(); // connect the signals to the main window.
+ void sendDelayedEvent(bridgepp::SPStreamEvent const &event); ///< Sends a delayed stream event.
+ void setIsAutostartOn(bool on); ///< Forwards a SetIsAutostartOn call via a Qt signal.
+ void setIsBetaEnabled(bool enabled); ///< Forwards a SetIsBetaEnabled call via a Qt signal.
+ void setColorSchemeName(QString const &name); ///< Forward a SetColorSchemeName call via a Qt Signal
+ void reportBug(QString const &osType, QString const &osVersion, QString const &emailClient, QString const &address,
+ QString const &description, bool includeLogs); ///< Forwards a ReportBug call via a Qt signal.
+ void setIsStreaming(bool isStreaming); ///< Forward a isStreaming internal messages via a Qt signal.
+ void setClientPlatform(QString const &clientPlatform); ///< Forward s setClientPlatform call via a Qt signal.
+ void changePorts(qint32 imapPort, qint32 smtpPort); ///< Forwards a ChangePorts call via a Qt signal.
+ void setUseSSLForSMTP(bool use); ///< Forwards a SetUseSSLForSMTP call via a Qt signal.
+ void setIsDoHEnabled(bool enabled); ///< Forwards a setIsDoHEnabled call via a Qt signal.
+ void changeLocalCache(bool enabled, QString const &path); ///< Forwards a ChangeLocalPath call via a Qt signal.
+ void setIsAutomaticUpdateOn(bool on); ///< Forwards a SetIsAutomaticUpdateOn call via a Qt signal.
+ void setUserSplitMode(QString const &userID, bool makeItActive); ///< Forwards a setUserSplitMode call via a Qt signal.
+ void logoutUser(QString const &userID); ///< Forwards a logoutUser call via a Qt signal.
+ void removeUser(QString const &userID); ///< Forwards a removeUser call via a Qt signal.
+ void configureUserAppleMail(QString const &userID, QString const &address); ///< Forwards a configureUserAppleMail call via a Qt signal.
+
+signals:
+ void delayedEventRequested(bridgepp::SPStreamEvent const &event); ///< Signal for sending a delayed event. delayed is set in the UI.
+ void setIsAutostartOnReceived(bool on); ///< Forwards a SetIsAutostartOn call via a Qt signal.
+ void setIsBetaEnabledReceived(bool enabled); ///< Forwards a SetIsBetaEnabled call via a Qt signal.
+ void setColorSchemeNameReceived(QString const &name); ///< Forward a SetColorScheme call via a Qt Signal
+ void reportBugReceived(QString const &osType, QString const &osVersion, QString const &emailClient, QString const &address,
+ QString const &description, bool includeLogs); ///< Signal for the ReportBug gRPC call
+ void setIsStreamingReceived(bool isStreaming); ///< Signal for the IsStreaming internal message.
+ void setClientPlatformReceived(QString const &clientPlatform); ///< Signal for the SetClientPlatform gRPC call.
+ void changePortsReceived(qint32 imapPort, qint32 smtpPort); ///< Signal for the ChangePorts gRPC call.
+ void setUseSSLForSMTPReceived(bool use); ///< Signal for the SetUseSSLForSMTP gRPC call.
+ void setIsDoHEnabledReceived(bool enabled); ///< Signal for the SetIsDoHEnabled gRPC call.
+ void changeLocalCacheReceived(bool enabled, QString const &path); ///< Signal for the ChangeLocalPath gRPC call.
+ void setIsAutomaticUpdateOnReceived(bool on); ///< Signal for the SetIsAutomaticUpdateOn gRPC call.
+ void setUserSplitModeReceived(QString const &userID, bool makeItActive); ///< Signal for the SetUserSplitModeReceived gRPC call.
+ void logoutUserReceived(QString const &userID); ///< Signal for the LogoutUserReceived gRPC call.
+ void removeUserReceived(QString const &userID); ///< Signal for the RemoveUserReceived gRPC call.
+ void configureUserAppleMailReceived(QString const &userID, QString const &address); ///< Signal for the ConfigureAppleMail gRPC call.
+};
+
+
+#endif //BRIDGE_GUI_TESTER_GRPC_QT_PROXY_H
diff --git a/internal/frontend/bridge-gui/bridge-gui-tester/GRPCServerWorker.cpp b/internal/frontend/bridge-gui/bridge-gui-tester/GRPCServerWorker.cpp
new file mode 100644
index 00000000..f15ae895
--- /dev/null
+++ b/internal/frontend/bridge-gui/bridge-gui-tester/GRPCServerWorker.cpp
@@ -0,0 +1,109 @@
+// Copyright (c) 2022 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 .
+
+
+#include "GRPCServerWorker.h"
+#include "GRPCService.h"
+#include
+#include
+
+
+using namespace bridgepp;
+using namespace grpc;
+
+
+namespace
+{
+
+
+//****************************************************************************************************************************************************
+/// \return The content of the file.
+//****************************************************************************************************************************************************
+QString loadAsciiTextFile(QString const &path) {
+ QFile file(path);
+ return file.open(QIODevice::ReadOnly | QIODevice::Text) ? QString::fromLocal8Bit(file.readAll()) : QString();
+}
+
+
+}
+
+
+//****************************************************************************************************************************************************
+//
+//****************************************************************************************************************************************************
+GRPCServerWorker::GRPCServerWorker(QObject *parent)
+ : Worker(parent)
+{
+
+}
+
+
+//****************************************************************************************************************************************************
+//
+//****************************************************************************************************************************************************
+void GRPCServerWorker::run()
+{
+ try
+ {
+ emit started();
+
+ QString cert = loadAsciiTextFile(serverCertificatePath());
+ if (cert.isEmpty())
+ throw Exception("Could not locate server certificate. Make sure to launch bridge once before starting this application");
+
+ QString key = loadAsciiTextFile(serverKeyPath());
+ if (key.isEmpty())
+ throw Exception("Could not locate server key. Make sure to launch bridge once before starting this application");
+
+ SslServerCredentialsOptions::PemKeyCertPair pair;
+ pair.private_key = key.toStdString();
+ pair.cert_chain = cert.toStdString();
+ SslServerCredentialsOptions ssl_opts;
+ ssl_opts.pem_root_certs="";
+ ssl_opts.pem_key_cert_pairs.push_back(pair);
+ std::shared_ptr credentials = grpc::SslServerCredentials(ssl_opts);
+
+ ServerBuilder builder;
+ builder.AddListeningPort("127.0.0.1:9292", credentials);
+ builder.RegisterService(&app().grpc());
+ server_ = builder.BuildAndStart();
+ if (!server_)
+ throw Exception("Could not create gRPC server.");
+ app().log().debug("gRPC Server started.");
+
+ server_->Wait();
+ emit finished();
+ app().log().debug("gRPC Server exited.");
+ }
+ catch (Exception const &e)
+ {
+ if (server_)
+ server_->Shutdown();
+
+ emit error(e.qwhat());
+ }
+}
+
+
+//****************************************************************************************************************************************************
+//
+//****************************************************************************************************************************************************
+void GRPCServerWorker::stop()
+{
+ if (server_)
+ server_->Shutdown();
+}
diff --git a/internal/frontend/bridge-gui/bridge-gui-tester/GRPCServerWorker.h b/internal/frontend/bridge-gui/bridge-gui-tester/GRPCServerWorker.h
new file mode 100644
index 00000000..8a881ba8
--- /dev/null
+++ b/internal/frontend/bridge-gui/bridge-gui-tester/GRPCServerWorker.h
@@ -0,0 +1,50 @@
+// Copyright (c) 2022 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 .
+
+
+#ifndef BRIDGE_GUI_TESTER_SERVER_WORKER_H
+#define BRIDGE_GUI_TESTER_SERVER_WORKER_H
+
+
+#include
+#include "GRPCService.h"
+#include
+
+
+//**********************************************************************************************************************
+/// \brief gRPC server worker
+//**********************************************************************************************************************
+class GRPCServerWorker : public bridgepp::Worker
+{
+Q_OBJECT
+public: // member functions.
+ explicit GRPCServerWorker(QObject *parent); ///< Default constructor.
+ GRPCServerWorker(GRPCServerWorker const &) = delete; ///< Disabled copy-constructor.
+ GRPCServerWorker(GRPCServerWorker &&) = delete; ///< Disabled assignment copy-constructor.
+ ~GRPCServerWorker() override = default; ///< Destructor.
+ GRPCServerWorker &operator=(GRPCServerWorker const &) = delete; ///< Disabled assignment operator.
+ GRPCServerWorker &operator=(GRPCServerWorker &&) = delete; ///< Disabled move assignment operator.
+
+ void run() override; ///< Run the worker.
+ void stop(); ///< Stop the gRPC service.
+
+private: // data members
+ std::unique_ptr server_ { nullptr }; ///< The gRPC server.
+};
+
+
+#endif // BRIDGE_GUI_TESTER_SERVER_WORKER_H
diff --git a/internal/frontend/bridge-gui/bridge-gui-tester/GRPCService.cpp b/internal/frontend/bridge-gui/bridge-gui-tester/GRPCService.cpp
new file mode 100644
index 00000000..db6571c0
--- /dev/null
+++ b/internal/frontend/bridge-gui/bridge-gui-tester/GRPCService.cpp
@@ -0,0 +1,878 @@
+// Copyright (c) 2022 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 .
+
+
+#include "GRPCService.h"
+#include "MainWindow.h"
+#include
+#include
+
+
+using namespace grpc;
+using namespace google::protobuf;
+using namespace bridgepp;
+
+namespace
+{
+
+
+QString const defaultKeychain = "defaultKeychain"; ///< The default keychain.
+
+
+}
+
+
+//****************************************************************************************************************************************************
+//
+//****************************************************************************************************************************************************
+void GRPCService::connectProxySignals()
+{
+ qtProxy_.connectSignals();
+}
+
+
+//****************************************************************************************************************************************************
+/// \return true iff the service is streaming events.
+//****************************************************************************************************************************************************
+bool GRPCService::isStreaming() const
+{
+ QMutexLocker locker(&eventStreamMutex_);
+ return isStreaming_;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] request the request.
+/// \return The status for the call.
+//****************************************************************************************************************************************************
+Status GRPCService::AddLogEntry(ServerContext *, AddLogEntryRequest const *request, Empty *)
+{
+ app().bridgeGUILog().addEntry(logLevelFromGRPC(request->level()), QString::fromStdString(request->message()));
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \return The status for the call.
+//****************************************************************************************************************************************************
+Status GRPCService::GuiReady(ServerContext *, Empty const *, Empty *)
+{
+ app().log().debug(__FUNCTION__);
+ app().mainWindow().settingsTab().setGUIReady(true);
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \return The status for the call.
+//****************************************************************************************************************************************************
+Status GRPCService::Quit(ServerContext *, Empty const *, Empty *)
+{
+ // We do not actually quit.
+ app().log().debug(__FUNCTION__);
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \return The status for the call.
+//****************************************************************************************************************************************************
+Status GRPCService::Restart(ServerContext *, Empty const *, Empty *)
+{
+ // we do not actually restart.
+ app().log().debug(__FUNCTION__);
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[out] response The response.
+/// \return The status for the call.
+//****************************************************************************************************************************************************
+Status GRPCService::ShowOnStartup(ServerContext *, Empty const *, BoolValue *response)
+{
+ app().log().debug(__FUNCTION__);
+ response->set_value(app().mainWindow().settingsTab().showOnStartup());
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[out] response The response.
+/// \return The status for the call.
+//****************************************************************************************************************************************************
+Status GRPCService::ShowSplashScreen(ServerContext *, Empty const *, BoolValue *response)
+{
+ app().log().debug(__FUNCTION__);
+ response->set_value(app().mainWindow().settingsTab().showSplashScreen());
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[out] response The response.
+/// \return The status for the call.
+//****************************************************************************************************************************************************
+Status GRPCService::IsFirstGuiStart(ServerContext *, Empty const *, BoolValue *response)
+{
+ app().log().debug(__FUNCTION__);
+ response->set_value(app().mainWindow().settingsTab().isFirstGUIStart());
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] request The request.
+/// \return The status for the call.
+//****************************************************************************************************************************************************
+Status GRPCService::SetIsAutostartOn(ServerContext *, BoolValue const *request, Empty *)
+{
+ app().log().debug(__FUNCTION__);
+ app().mainWindow().settingsTab().setIsAutostartOn(request->value());
+ qtProxy_.sendDelayedEvent(newToggleAutostartFinishedEvent());
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[out] response The response.
+/// \return The status for the call.
+//****************************************************************************************************************************************************
+Status GRPCService::IsAutostartOn(ServerContext *, Empty const *, BoolValue *response)
+{
+ app().log().debug(__FUNCTION__);
+ response->set_value(app().mainWindow().settingsTab().isAutostartOn());
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] request The request.
+/// \return The status for the call.
+//****************************************************************************************************************************************************
+Status GRPCService::SetIsBetaEnabled(ServerContext *, BoolValue const *request, Empty *)
+{
+ app().log().debug(__FUNCTION__);
+ qtProxy_.setIsBetaEnabled(request->value());
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[out] response The response.
+/// \return The status for the call.
+//****************************************************************************************************************************************************
+Status GRPCService::IsBetaEnabled(ServerContext *, Empty const *, BoolValue *response)
+{
+ app().log().debug(__FUNCTION__);
+ response->set_value(app().mainWindow().settingsTab().isBetaEnabled());
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[out] response The response.
+/// \return The status for the call.
+//****************************************************************************************************************************************************
+Status GRPCService::GoOs(ServerContext *, Empty const *, StringValue *response)
+{
+ app().log().debug(__FUNCTION__);
+ response->set_value(app().mainWindow().settingsTab().os().toStdString());
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \return The status for the call.
+//****************************************************************************************************************************************************
+Status GRPCService::TriggerReset(ServerContext *, Empty const *, Empty *)
+{
+ app().log().debug(__FUNCTION__);
+ app().log().info("Bridge GUI requested a reset");
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[out] response The response.
+/// \return The status for the call.
+//****************************************************************************************************************************************************
+grpc::Status GRPCService::Version(ServerContext *, Empty const *, StringValue *response)
+{
+ app().log().debug(__FUNCTION__);
+ response->set_value(app().mainWindow().settingsTab().bridgeVersion().toStdString());
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[out] response The response.
+/// \return The status for the call.
+//****************************************************************************************************************************************************
+Status GRPCService::LogsPath(ServerContext *, Empty const *, StringValue *response)
+{
+ app().log().debug(__FUNCTION__);
+ response->set_value(app().mainWindow().settingsTab().logsPath().toStdString());
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[out] response The response.
+/// \return The status for the call.
+//****************************************************************************************************************************************************
+Status GRPCService::LicensePath(ServerContext *, Empty const *, StringValue *response)
+{
+ app().log().debug(__FUNCTION__);
+ response->set_value(app().mainWindow().settingsTab().licensePath().toStdString());
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[out] response The response.
+/// \return The status for the call.
+//****************************************************************************************************************************************************
+Status GRPCService::ReleaseNotesPageLink(ServerContext *, Empty const *, StringValue *response)
+{
+ app().log().debug(__FUNCTION__);
+ response->set_value(app().mainWindow().settingsTab().releaseNotesPageLink().toStdString());
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[out] response The response.
+/// \return The status for the call.
+//****************************************************************************************************************************************************
+Status GRPCService::DependencyLicensesLink(ServerContext *, Empty const *, StringValue *response)
+{
+ app().log().debug(__FUNCTION__);
+ response->set_value(app().mainWindow().settingsTab().dependencyLicenseLink().toStdString());
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[out] response The response.
+/// \return The status for the call.
+//****************************************************************************************************************************************************
+Status GRPCService::LandingPageLink(ServerContext *, Empty const *, StringValue *response)
+{
+ app().log().debug(__FUNCTION__);
+ response->set_value(app().mainWindow().settingsTab().landingPageLink().toStdString());
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] request The request.
+/// \return The status for the call.
+//****************************************************************************************************************************************************
+Status GRPCService::SetColorSchemeName(ServerContext *, StringValue const *request, Empty *)
+{
+ app().log().debug(__FUNCTION__);
+ qtProxy_.setColorSchemeName(QString::fromStdString(request->value()));
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] response The response.
+/// \return The status for the call.
+//****************************************************************************************************************************************************
+Status GRPCService::ColorSchemeName(ServerContext *, Empty const *, StringValue *response)
+{
+ app().log().debug(__FUNCTION__);
+ response->set_value(app().mainWindow().settingsTab().colorSchemeName().toStdString());
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] response The response.
+/// \return The status for the call.
+//****************************************************************************************************************************************************
+Status GRPCService::CurrentEmailClient(ServerContext *, Empty const *, StringValue *response)
+{
+ app().log().debug(__FUNCTION__);
+ response->set_value(app().mainWindow().settingsTab().currentEmailClient().toStdString());
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] response The request.
+/// \return The status for the call.
+//****************************************************************************************************************************************************
+Status GRPCService::ForceLauncher(ServerContext *, StringValue const *request, Empty *)
+{
+ app().log().debug(__FUNCTION__);
+ app().log().info(QString("ForceLauncher: %1").arg(QString::fromStdString(request->value())));
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] request The request
+//****************************************************************************************************************************************************
+Status GRPCService::ReportBug(ServerContext *, ReportBugRequest const *request, Empty *)
+{
+ app().log().debug(__FUNCTION__);
+ SettingsTab &tab = app().mainWindow().settingsTab();
+ qtProxy_.reportBug(QString::fromStdString(request->ostype()), QString::fromStdString(request->osversion()),
+ QString::fromStdString(request->emailclient()), QString::fromStdString(request->address()), QString::fromStdString(request->description()),
+ request->includelogs());
+ qtProxy_.sendDelayedEvent(tab.nextBugReportWillSucceed() ? newReportBugSuccessEvent() : newReportBugErrorEvent());
+ qtProxy_.sendDelayedEvent(newReportBugFinishedEvent());
+
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] request The request.
+/// \return The status for the call.
+//****************************************************************************************************************************************************
+Status GRPCService::Login(ServerContext *, LoginRequest const *request, Empty *)
+{
+ app().log().debug(__FUNCTION__);
+ UsersTab &usersTab = app().mainWindow().usersTab();
+ loginUsername_ = QString::fromStdString(request->username());
+ if (usersTab.nextUserUsernamePasswordError())
+ {
+ qtProxy_.sendDelayedEvent(newLoginError(LoginErrorType::USERNAME_PASSWORD_ERROR, "Username/password error."));
+ return Status::OK;
+ }
+ if (usersTab.nextUserFreeUserError())
+ {
+ qtProxy_.sendDelayedEvent(newLoginError(LoginErrorType::FREE_USER, "Free user error."));
+ return Status::OK;
+ }
+ if (usersTab.nextUserTFARequired())
+ {
+ qtProxy_.sendDelayedEvent(newLoginTfaRequestedEvent(loginUsername_));
+ return Status::OK;
+ }
+ if (usersTab.nextUserTwoPasswordsRequired())
+ {
+ qtProxy_.sendDelayedEvent(newLoginTwoPasswordsRequestedEvent());
+ return Status::OK;
+ }
+
+ SPUser const user = randomUser();
+ QString const userID = user->id();
+ user->setUsername(QString::fromStdString(request->username()));
+ usersTab.userTable().append(user);
+
+ if (usersTab.nextUserAlreadyLoggedIn())
+ qtProxy_.sendDelayedEvent(newLoginAlreadyLoggedInEvent(userID));
+ qtProxy_.sendDelayedEvent(newLoginFinishedEvent(userID));
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] request The request.
+/// \return The status for the call.
+//****************************************************************************************************************************************************
+Status GRPCService::Login2FA(ServerContext *, LoginRequest const *request, Empty *)
+{
+ app().log().debug(__FUNCTION__);
+ UsersTab &usersTab = app().mainWindow().usersTab();
+ if (usersTab.nextUserTFAError())
+ {
+ qtProxy_.sendDelayedEvent(newLoginError(LoginErrorType::TFA_ERROR, "2FA Error."));
+ return Status::OK;
+ }
+ if (usersTab.nextUserTFAAbort())
+ {
+ qtProxy_.sendDelayedEvent(newLoginError(LoginErrorType::TFA_ABORT, "2FA Abort."));
+ return Status::OK;
+ }
+ if (usersTab.nextUserTwoPasswordsRequired())
+ {
+ qtProxy_.sendDelayedEvent(newLoginTwoPasswordsRequestedEvent());
+ return Status::OK;
+ }
+
+ SPUser const user = randomUser();
+ QString const userID = user->id();
+ user->setUsername(QString::fromStdString(request->username()));
+ usersTab.userTable().append(user);
+
+ if (usersTab.nextUserAlreadyLoggedIn())
+ qtProxy_.sendDelayedEvent(newLoginAlreadyLoggedInEvent(userID));
+ qtProxy_.sendDelayedEvent(newLoginFinishedEvent(userID));
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] request The request.
+/// \return The status for the call.
+//****************************************************************************************************************************************************
+Status GRPCService::Login2Passwords(ServerContext *, LoginRequest const *request, Empty *)
+{
+ app().log().debug(__FUNCTION__);
+ UsersTab &usersTab = app().mainWindow().usersTab();
+
+ if (usersTab.nextUserTwoPasswordsError())
+ {
+ qtProxy_.sendDelayedEvent(newLoginError(LoginErrorType::TWO_PASSWORDS_ERROR, "Two Passwords error."));
+ return Status::OK;
+ }
+
+ if (usersTab.nextUserTwoPasswordsAbort())
+ {
+ qtProxy_.sendDelayedEvent(newLoginError(LoginErrorType::TWO_PASSWORDS_ABORT, "Two Passwords abort."));
+ return Status::OK;
+ }
+
+ SPUser const user = randomUser();
+ QString const userID = user->id();
+ user->setUsername(QString::fromStdString(request->username()));
+ usersTab.userTable().append(user);
+
+ if (usersTab.nextUserAlreadyLoggedIn())
+ qtProxy_.sendDelayedEvent(newLoginAlreadyLoggedInEvent(userID));
+ qtProxy_.sendDelayedEvent(newLoginFinishedEvent(userID));
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] request The request.
+/// \return The status for the call.
+//****************************************************************************************************************************************************
+Status GRPCService::LoginAbort(ServerContext *, LoginAbortRequest const *request, Empty *)
+{
+ app().log().debug(__FUNCTION__);
+ loginUsername_ = QString();
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \return The status for the call.
+//****************************************************************************************************************************************************
+Status GRPCService::CheckUpdate(ServerContext *, Empty const *, Empty *)
+{
+ /// \todo simulate update availability.
+ app().log().debug(__FUNCTION__);
+ app().log().info("Check for updates");
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \return The status for the call.
+//****************************************************************************************************************************************************
+Status GRPCService::InstallUpdate(ServerContext *, Empty const *, Empty *)
+{
+ /// Simulate update availability.
+ app().log().debug(__FUNCTION__);
+ app().log().info("Install update");
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] request The request.
+/// \return The status for the call.
+//****************************************************************************************************************************************************
+Status GRPCService::SetIsAutomaticUpdateOn(ServerContext *, BoolValue const *request, Empty *)
+{
+ app().log().debug(__FUNCTION__);
+ qtProxy_.setIsAutomaticUpdateOn(request->value());
+ return Status::OK;
+}
+
+//****************************************************************************************************************************************************
+/// \param[out] response The response.
+/// \return The status for the call.
+//****************************************************************************************************************************************************
+Status GRPCService::IsAutomaticUpdateOn(ServerContext *, Empty const *, BoolValue *response)
+{
+ app().log().debug(__FUNCTION__);
+ response->set_value(app().mainWindow().settingsTab().isAutomaticUpdateOn());
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] response The response.
+/// \return The status for the call
+//****************************************************************************************************************************************************
+Status GRPCService::IsCacheOnDiskEnabled(ServerContext *, Empty const *, BoolValue *response)
+{
+ app().log().debug(__FUNCTION__);
+ response->set_value(app().mainWindow().settingsTab().isCacheOnDiskEnabled());
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] response The response.
+/// \return The status for the call
+//****************************************************************************************************************************************************
+Status GRPCService::DiskCachePath(ServerContext *, Empty const *, StringValue *response)
+{
+ app().log().debug(__FUNCTION__);
+ response->set_value(app().mainWindow().settingsTab().diskCachePath().toStdString());
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] request The request.
+/// \return The status for the call.
+//****************************************************************************************************************************************************
+Status GRPCService::ChangeLocalCache(ServerContext *, ChangeLocalCacheRequest const *request, Empty *)
+{
+ app().log().debug(__FUNCTION__);
+ SettingsTab &tab = app().mainWindow().settingsTab();
+ QString const path = QString::fromStdString(request->diskcachepath());
+
+ // we mimic the behaviour of Bridge
+ if (!tab.nextCacheChangeWillSucceed())
+ qtProxy_.sendDelayedEvent(newCacheErrorEvent(grpc::CacheErrorType(tab.cacheError())));
+ else
+ qtProxy_.sendDelayedEvent(newCacheLocationChangeSuccessEvent());
+ qtProxy_.sendDelayedEvent(newDiskCachePathChanged(path));
+ qtProxy_.sendDelayedEvent(newIsCacheOnDiskEnabledChanged(request->enablediskcache()));
+ qtProxy_.sendDelayedEvent(newChangeLocalCacheFinishedEvent());
+
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] request The request.
+/// \return The status for the call.
+//****************************************************************************************************************************************************
+Status GRPCService::SetIsDoHEnabled(ServerContext *, BoolValue const *request, Empty *)
+{
+ app().log().debug(__FUNCTION__);
+ qtProxy_.setIsDoHEnabled(request->value());
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[out] response The response
+/// \return The status for the call.
+//****************************************************************************************************************************************************
+Status GRPCService::IsDoHEnabled(ServerContext *, Empty const *, BoolValue *response)
+{
+ app().log().debug(__FUNCTION__);
+ response->set_value(app().mainWindow().settingsTab().isDoHEnabled());
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] request The request.
+/// \return The status for the call.
+//****************************************************************************************************************************************************
+Status GRPCService::SetUseSslForSmtp(ServerContext *, BoolValue const *request, Empty *)
+{
+ app().log().debug(__FUNCTION__);
+ qtProxy_.setUseSSLForSMTP(request->value());
+ qtProxy_.sendDelayedEvent(newUseSslForSmtpFinishedEvent());
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[out] response The response.
+/// \return The status for the call.
+//****************************************************************************************************************************************************
+Status GRPCService::UseSslForSmtp(ServerContext *, Empty const *, BoolValue *response)
+{
+ app().log().debug(__FUNCTION__);
+ response->set_value(app().mainWindow().settingsTab().useSSLForSMTP());
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[out] response The response.
+/// \return The status for the call.
+//****************************************************************************************************************************************************
+Status GRPCService::Hostname(ServerContext *, Empty const *, StringValue *response)
+{
+ app().log().debug(__FUNCTION__);
+ response->set_value(app().mainWindow().settingsTab().hostname().toStdString());
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[out] response The response.
+/// \return The status for the call.
+//****************************************************************************************************************************************************
+Status GRPCService::ImapPort(ServerContext *, Empty const *, Int32Value *response)
+{
+ app().log().debug(__FUNCTION__);
+ response->set_value(app().mainWindow().settingsTab().imapPort());
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[out] response The response.
+/// \return The status for the call.
+//****************************************************************************************************************************************************
+Status GRPCService::SmtpPort(ServerContext *, Empty const *, Int32Value *response)
+{
+ app().log().debug(__FUNCTION__);
+ response->set_value(app().mainWindow().settingsTab().smtpPort());
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] request The request.
+/// \return The status for the call.
+//****************************************************************************************************************************************************
+Status GRPCService::ChangePorts(ServerContext *, ChangePortsRequest const *request, Empty *)
+{
+ app().log().debug(__FUNCTION__);
+ qtProxy_.changePorts(request->imapport(), request->smtpport());
+ qtProxy_.sendDelayedEvent(newChangePortsFinishedEvent());
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[out] response the response.
+/// \return The status for the call.
+//****************************************************************************************************************************************************
+Status GRPCService::IsPortFree(ServerContext *, Int32Value const *request, BoolValue *response)
+{
+ app().log().debug(__FUNCTION__);
+ response->set_value(app().mainWindow().settingsTab().isPortFree());
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[out] response The response.
+/// \return The status for the call
+//****************************************************************************************************************************************************
+Status GRPCService::AvailableKeychains(ServerContext *, Empty const *, AvailableKeychainsResponse *response)
+{
+ /// \todo Implement keychains configuration.
+ app().log().debug(__FUNCTION__);
+ response->clear_keychains();
+ response->add_keychains(defaultKeychain.toStdString());
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] request The request.
+/// \return The status for the call
+//****************************************************************************************************************************************************
+Status GRPCService::SetCurrentKeychain(ServerContext *, StringValue const *request, Empty *)
+{
+ /// \todo Implement keychains configuration.
+ app().log().debug(__FUNCTION__);
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[out] response The response.
+/// \return The status for the call
+//****************************************************************************************************************************************************
+Status GRPCService::CurrentKeychain(ServerContext *, Empty const *, StringValue *response)
+{
+ /// \todo Implement keychains configuration.
+ app().log().debug(__FUNCTION__);
+ response->set_value(defaultKeychain.toStdString());
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[out] response The response.
+/// \return The status for the call
+//****************************************************************************************************************************************************
+Status GRPCService::GetUserList(ServerContext *, Empty const *, UserListResponse *response)
+{
+ app().log().debug(__FUNCTION__);
+ response->clear_users();
+
+ QList userList = app().mainWindow().usersTab().userTable().users();
+ RepeatedPtrField *users = response->mutable_users();
+ for (SPUser const &user: userList)
+ {
+ if (!user)
+ continue;
+ users->Add();
+ grpc::User &grpcUser = (*users)[users->size() - 1];
+ userToGRPC(*user, grpcUser);
+ }
+
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] request The request.
+/// \param[out] response The response.
+/// \return The status for the call.
+//****************************************************************************************************************************************************
+Status GRPCService::GetUser(ServerContext *, StringValue const *request, grpc::User *response)
+{
+ app().log().debug(__FUNCTION__);
+ QString userID = QString::fromStdString(request->value());
+ SPUser user = app().mainWindow().usersTab().userWithID(userID);
+ if (!user)
+ return Status(NOT_FOUND, QString("user not found %1").arg(userID).toStdString());
+
+ userToGRPC(*user, *response);
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] request The request.
+/// \return The status for the call.
+//****************************************************************************************************************************************************
+Status GRPCService::SetUserSplitMode(ServerContext *, UserSplitModeRequest const *request, Empty *)
+{
+ app().log().debug(__FUNCTION__);
+ qtProxy_.setUserSplitMode(QString::fromStdString(request->userid()), request->active());
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] request The request.
+/// \return The status for the call.
+//****************************************************************************************************************************************************
+Status GRPCService::LogoutUser(ServerContext *, StringValue const *request, Empty *)
+{
+ app().log().debug(__FUNCTION__);
+ qtProxy_.logoutUser(QString::fromStdString(request->value()));
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] request The request.
+/// \return The status for the call.
+//****************************************************************************************************************************************************
+Status GRPCService::RemoveUser(ServerContext *, StringValue const *request, Empty *)
+{
+ app().log().debug(__FUNCTION__);
+ qtProxy_.removeUser(QString::fromStdString(request->value()));
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] request The request.
+//****************************************************************************************************************************************************
+Status GRPCService::ConfigureUserAppleMail(ServerContext *, ConfigureAppleMailRequest const *request, Empty *)
+{
+ app().log().debug(__FUNCTION__);
+ qtProxy_.configureUserAppleMail(QString::fromStdString(request->userid()), QString::fromStdString(request->address()));
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] request The request
+/// \param[in] writer The writer
+/// \return The status for the call.
+//****************************************************************************************************************************************************
+Status GRPCService::StartEventStream(ServerContext *, EventStreamRequest const *request, ServerWriter *writer)
+{
+ app().log().debug(__FUNCTION__);
+ {
+ QMutexLocker locker(&eventStreamMutex_);
+ if (isStreaming_)
+ return { grpc::ALREADY_EXISTS, "the service is already streaming" };
+ isStreaming_ = true;
+ qtProxy_.setIsStreaming(true);
+ qtProxy_.setClientPlatform(QString::fromStdString(request->clientplatform()));
+ eventStreamShouldStop_ = false;
+ }
+
+ while (true)
+ {
+ QMutexLocker locker(&eventStreamMutex_);
+ if (eventStreamShouldStop_)
+ {
+ qtProxy_.setIsStreaming(false);
+ qtProxy_.setClientPlatform(QString());
+ isStreaming_ = false;
+ return Status::OK;
+ }
+
+
+ if (eventQueue_.isEmpty())
+ {
+ locker.unlock();
+ QThread::msleep(100);
+ continue;
+ }
+ SPStreamEvent const event = eventQueue_.front();
+ eventQueue_.pop_front();
+ locker.unlock();
+
+ if (writer->Write(*event))
+ app().log().debug(QString("event sent: %1").arg(QString::fromStdString(event->ShortDebugString())));
+ else
+ app().log().error(QString("Could not send event: %1").arg(QString::fromStdString(event->ShortDebugString())));
+ }
+}
+
+
+//****************************************************************************************************************************************************
+/// \return The status for the call.
+//****************************************************************************************************************************************************
+Status GRPCService::StopEventStream(ServerContext *, Empty const *, Empty *)
+{
+ app().log().debug(__FUNCTION__);
+ QMutexLocker mutex(&eventStreamMutex_);
+ if (!isStreaming_)
+ return Status(NOT_FOUND, "The service is not streaming");
+ eventStreamShouldStop_ = true;
+ return Status::OK;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] event The event
+/// \return true if the event was queued, and false if the server in not streaming.
+//****************************************************************************************************************************************************
+bool GRPCService::sendEvent(SPStreamEvent const &event)
+{
+ QMutexLocker mutexLocker(&eventStreamMutex_);
+ if (isStreaming_)
+ eventQueue_.push_back(event);
+ return isStreaming_;
+}
+
+
+
+
diff --git a/internal/frontend/bridge-gui/bridge-gui-tester/GRPCService.h b/internal/frontend/bridge-gui/bridge-gui-tester/GRPCService.h
new file mode 100644
index 00000000..703a74ea
--- /dev/null
+++ b/internal/frontend/bridge-gui/bridge-gui-tester/GRPCService.h
@@ -0,0 +1,112 @@
+// Copyright (c) 2022 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 .
+
+
+#ifndef BRIDGE_GUI_TESTER_GRPC_SERVER_H
+#define BRIDGE_GUI_TESTER_GRPC_SERVER_H
+
+
+#include "GRPCQtProxy.h"
+#include
+#include
+
+
+//**********************************************************************************************************************
+/// \brief gRPC server implementation.
+//**********************************************************************************************************************
+class GRPCService : public grpc::Bridge::Service
+{
+
+public: // member functions.
+ GRPCService() = default; ///< Default constructor.
+ GRPCService(GRPCService const &) = delete; ///< Disabled copy-constructor.
+ GRPCService(GRPCService &&) = delete; ///< Disabled assignment copy-constructor.
+ ~GRPCService() override = default; ///< Destructor.
+ GRPCService &operator=(GRPCService const &) = delete; ///< Disabled assignment operator.
+ GRPCService &operator=(GRPCService &&) = delete; ///< Disabled move assignment operator.
+ void connectProxySignals(); ///< Connect the signals of the Qt Proxy to the GUI components
+ bool isStreaming() const; ///< Check if the service is currently streaming events.
+
+ grpc::Status AddLogEntry(::grpc::ServerContext *context, ::grpc::AddLogEntryRequest const *request, ::google::protobuf::Empty *response) override;
+ grpc::Status GuiReady(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::google::protobuf::Empty *response) override;
+ grpc::Status Quit(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::google::protobuf::Empty *response) override;
+ grpc::Status Restart(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::google::protobuf::Empty *response) override;
+ grpc::Status ShowOnStartup(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::google::protobuf::BoolValue *response) override;
+ grpc::Status ShowSplashScreen(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::google::protobuf::BoolValue *response) override;
+ grpc::Status IsFirstGuiStart(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::google::protobuf::BoolValue *response) override;
+ grpc::Status SetIsAutostartOn(::grpc::ServerContext *context, ::google::protobuf::BoolValue const *request, ::google::protobuf::Empty *response) override;
+ grpc::Status IsAutostartOn(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::google::protobuf::BoolValue *response) override;
+ grpc::Status SetIsBetaEnabled(::grpc::ServerContext *context, ::google::protobuf::BoolValue const *request, ::google::protobuf::Empty *response) override;
+ grpc::Status IsBetaEnabled(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::google::protobuf::BoolValue *response) override;
+ grpc::Status GoOs(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::google::protobuf::StringValue *response) override;
+ grpc::Status TriggerReset(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::google::protobuf::Empty *response) override;
+ grpc::Status Version(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::google::protobuf::StringValue *response) override;
+ grpc::Status LogsPath(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::google::protobuf::StringValue *response) override;
+ grpc::Status LicensePath(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::google::protobuf::StringValue *response) override;
+ grpc::Status ReleaseNotesPageLink(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::google::protobuf::StringValue *response) override;
+ grpc::Status DependencyLicensesLink(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::google::protobuf::StringValue *response) override;
+ grpc::Status LandingPageLink(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::google::protobuf::StringValue *response) override;
+ grpc::Status SetColorSchemeName(::grpc::ServerContext *context, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *response) override;
+ grpc::Status ColorSchemeName(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::google::protobuf::StringValue *response) override;
+ grpc::Status CurrentEmailClient(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::google::protobuf::StringValue *response) override;
+ grpc::Status ReportBug(::grpc::ServerContext *context, ::grpc::ReportBugRequest const *request, ::google::protobuf::Empty *response) override;
+ grpc::Status ForceLauncher(::grpc::ServerContext *context, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *response) override;
+ grpc::Status Login(::grpc::ServerContext *context, ::grpc::LoginRequest const *request, ::google::protobuf::Empty *response) override;
+ grpc::Status Login2FA(::grpc::ServerContext *context, ::grpc::LoginRequest const *request, ::google::protobuf::Empty *response) override;
+ grpc::Status Login2Passwords(::grpc::ServerContext *context, ::grpc::LoginRequest const *request, ::google::protobuf::Empty *response) override;
+ grpc::Status LoginAbort(::grpc::ServerContext *context, ::grpc::LoginAbortRequest const *request, ::google::protobuf::Empty *response) override;
+ grpc::Status CheckUpdate(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::google::protobuf::Empty *response) override;
+ grpc::Status InstallUpdate(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::google::protobuf::Empty *response) override;
+ grpc::Status SetIsAutomaticUpdateOn(::grpc::ServerContext *context, ::google::protobuf::BoolValue const *request, ::google::protobuf::Empty *response) override;
+ grpc::Status IsAutomaticUpdateOn(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::google::protobuf::BoolValue *response) override;
+ grpc::Status IsCacheOnDiskEnabled(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::google::protobuf::BoolValue *response) override;
+ grpc::Status DiskCachePath(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::google::protobuf::StringValue *response) override;
+ grpc::Status ChangeLocalCache(::grpc::ServerContext *context, ::grpc::ChangeLocalCacheRequest const *request, ::google::protobuf::Empty *response) override;
+ grpc::Status SetIsDoHEnabled(::grpc::ServerContext *context, ::google::protobuf::BoolValue const *request, ::google::protobuf::Empty *response) override;
+ grpc::Status IsDoHEnabled(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::google::protobuf::BoolValue *response) override;
+ grpc::Status SetUseSslForSmtp(::grpc::ServerContext *context, ::google::protobuf::BoolValue const *request, ::google::protobuf::Empty *response) override;
+ grpc::Status UseSslForSmtp(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::google::protobuf::BoolValue *response) override;
+ grpc::Status Hostname(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::google::protobuf::StringValue *response) override;
+ grpc::Status ImapPort(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::google::protobuf::Int32Value *response) override;
+ grpc::Status SmtpPort(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::google::protobuf::Int32Value *response) override;
+ grpc::Status ChangePorts(::grpc::ServerContext *context, ::grpc::ChangePortsRequest const *request, ::google::protobuf::Empty *response) override;
+ grpc::Status IsPortFree(::grpc::ServerContext *context, ::google::protobuf::Int32Value const *request, ::google::protobuf::BoolValue *response) override;
+ grpc::Status AvailableKeychains(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::grpc::AvailableKeychainsResponse *response) override;
+ grpc::Status SetCurrentKeychain(::grpc::ServerContext *context, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *response) override;
+ grpc::Status CurrentKeychain(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::google::protobuf::StringValue *response) override;
+ grpc::Status GetUserList(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::grpc::UserListResponse *response) override;
+ grpc::Status GetUser(::grpc::ServerContext *context, ::google::protobuf::StringValue const *request, ::grpc::User *response) override;
+ grpc::Status SetUserSplitMode(::grpc::ServerContext *context, ::grpc::UserSplitModeRequest const *request, ::google::protobuf::Empty *response) override;
+ grpc::Status LogoutUser(::grpc::ServerContext *context, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *response) override;
+ grpc::Status RemoveUser(::grpc::ServerContext *context, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *response) override;
+ grpc::Status ConfigureUserAppleMail(::grpc::ServerContext *context, ::grpc::ConfigureAppleMailRequest const *request, ::google::protobuf::Empty *response) override;
+ grpc::Status StartEventStream(::grpc::ServerContext *context, ::grpc::EventStreamRequest const *request, ::grpc::ServerWriter<::grpc::StreamEvent> *writer) override;
+ grpc::Status StopEventStream(::grpc::ServerContext *context, ::google::protobuf::Empty const *request, ::google::protobuf::Empty *response) override;
+
+ bool sendEvent(bridgepp::SPStreamEvent const &event); ///< Queue an event for sending through the event stream.
+
+private: // data member
+ mutable QMutex eventStreamMutex_; ///< Mutex used to access eventQueue_, isStreaming_ and shouldStopStreaming_;
+ QList eventQueue_; ///< The event queue. Acces protected by eventStreamMutex_;
+ bool isStreaming_; ///< Is the gRPC stream running. Access protected by eventStreamMutex_;
+ bool eventStreamShouldStop_; ///< Should the stream be stopped? Access protected by eventStreamMutex
+ QString loginUsername_; ///< The username used for the current login procedure.
+ GRPCQtProxy qtProxy_; ///< Qt Proxy used to send signals, as this class is not a QObject.
+};
+
+
+#endif // BRIDGE_GUI_TESTER_GRPC_SERVER_H
diff --git a/internal/frontend/bridge-gui/bridge-gui-tester/MainWindow.cpp b/internal/frontend/bridge-gui/bridge-gui-tester/MainWindow.cpp
new file mode 100644
index 00000000..606dd867
--- /dev/null
+++ b/internal/frontend/bridge-gui/bridge-gui-tester/MainWindow.cpp
@@ -0,0 +1,111 @@
+// Copyright (c) 2022 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 .
+
+
+#include "MainWindow.h"
+#include
+
+
+using namespace bridgepp;
+
+
+namespace
+{
+
+
+//****************************************************************************************************************************************************
+/// \param[in] level The log level.
+/// \param[in] message The log message.
+/// \param[in] logEdit The plain text edit widget that displays the log.
+//****************************************************************************************************************************************************
+void addEntryToLogEdit(bridgepp::Log::Level level, const QString &message, QPlainTextEdit &logEdit)
+{
+ /// \todo This may cause performance issue when log grows big. A better alternative should be implemented.
+ QString log = logEdit.toPlainText().trimmed();
+ if (!log.isEmpty())
+ log += "\n";
+ logEdit.setPlainText(log + Log::logEntryToString(level, message));
+}
+
+
+} // Anonymous namespace
+
+
+//****************************************************************************************************************************************************
+/// \param[in] parent The parent widget of the window.
+//****************************************************************************************************************************************************
+MainWindow::MainWindow(QWidget *parent)
+ : QMainWindow(parent)
+{
+ ui_.setupUi(this);
+ ui_.tabTop->setCurrentIndex(0);
+ ui_.tabBottom->setCurrentIndex(0);
+ ui_.splitter->setStretchFactor(0, 0);
+ ui_.splitter->setStretchFactor(1, 1);
+ ui_.splitter->setSizes({100, 10000});
+ connect(&app().log(), &Log::entryAdded, this, &MainWindow::addLogEntry);
+ connect(&app().bridgeGUILog(), &Log::entryAdded, this, &MainWindow::addBridgeGUILogEntry);
+}
+
+
+//****************************************************************************************************************************************************
+/// \return A reference to the 'General' tab.
+//****************************************************************************************************************************************************
+SettingsTab &MainWindow::settingsTab()
+{
+ return *ui_.settingsTab;
+}
+
+
+//****************************************************************************************************************************************************
+/// \return A reference to the users tab.
+//****************************************************************************************************************************************************
+UsersTab &MainWindow::usersTab()
+{
+ return *ui_.usersTab;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] level The log level.
+/// \param[in] message The log message
+//****************************************************************************************************************************************************
+void MainWindow::addLogEntry(bridgepp::Log::Level level, const QString &message)
+{
+ addEntryToLogEdit(level, message, *ui_.editLog);
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] level The log level.
+/// \param[in] message The log message
+//****************************************************************************************************************************************************
+void MainWindow::addBridgeGUILogEntry(bridgepp::Log::Level level, const QString &message)
+{
+ addEntryToLogEdit(level, message, *ui_.editBridgeGUILog);
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] event The event.
+//****************************************************************************************************************************************************
+void MainWindow::sendDelayedEvent(SPStreamEvent const &event)
+{
+ QTimer::singleShot(this->settingsTab().eventDelayMs(), [event] { app().grpc().sendEvent(event); });
+}
+
+
diff --git a/internal/frontend/bridge-gui/bridge-gui-tester/MainWindow.h b/internal/frontend/bridge-gui/bridge-gui-tester/MainWindow.h
new file mode 100644
index 00000000..d0c7c108
--- /dev/null
+++ b/internal/frontend/bridge-gui/bridge-gui-tester/MainWindow.h
@@ -0,0 +1,57 @@
+// Copyright (c) 2022 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 .
+
+
+#ifndef BRIDGE_GUI_TESTER_MAIN_WINDOW_H
+#define BRIDGE_GUI_TESTER_MAIN_WINDOW_H
+
+
+#include "ui_MainWindow.h"
+#include "GRPCService.h"
+#include
+
+
+//**********************************************************************************************************************
+/// \brief Main window class
+//**********************************************************************************************************************
+class MainWindow : public QMainWindow
+{
+Q_OBJECT
+public: // member functions.
+ explicit MainWindow(QWidget *parent); ///< Default constructor.
+ MainWindow(MainWindow const &) = delete; ///< Disabled copy-constructor.
+ MainWindow(MainWindow &&) = delete; ///< Disabled assignment copy-constructor.
+ ~MainWindow() override = default; ///< Destructor.
+ MainWindow &operator=(MainWindow const &) = delete; ///< Disabled assignment operator.
+ MainWindow &operator=(MainWindow &&) = delete; ///< Disabled move assignment operator.
+
+ SettingsTab &settingsTab(); ///< Returns a reference the 'Settings' tab.
+ UsersTab &usersTab(); ///< Returns a reference to the 'Users' tab.
+
+public slots:
+ void sendDelayedEvent(bridgepp::SPStreamEvent const& event); ///< Sends a gRPC event after the delay specified in the UI. The call is non blocking.
+
+private slots:
+ void addLogEntry(bridgepp::Log::Level level, QString const &message); ///< Add an entry to the log.
+ void addBridgeGUILogEntry(bridgepp::Log::Level level, const QString &message); ///< Add an entry to the log.
+
+private:
+ Ui::MainWindow ui_ {}; ///< The GUI for the window.
+};
+
+
+#endif // BRIDGE_GUI_TESTER_MAIN_WINDOW_H
diff --git a/internal/frontend/bridge-gui/bridge-gui-tester/MainWindow.ui b/internal/frontend/bridge-gui/bridge-gui-tester/MainWindow.ui
new file mode 100644
index 00000000..703e875f
--- /dev/null
+++ b/internal/frontend/bridge-gui/bridge-gui-tester/MainWindow.ui
@@ -0,0 +1,102 @@
+
+
+ MainWindow
+
+
+
+ 0
+ 0
+ 1226
+ 1086
+
+
+
+ MainWindow
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 0
+
+
+
+ Settings
+
+
+
+
+ Users
+
+
+
+
+
+ 0
+
+
+
+ Log
+
+
+
-
+
+
+
+ Monaco
+
+
+
+ true
+
+
+
+
+
+
+
+ Bridge-GUI Log
+
+
+ -
+
+
+
+ Monaco
+
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+ SettingsTab
+ QWidget
+
+ 1
+
+
+ UsersTab
+ QWidget
+
+ 1
+
+
+
+
+
diff --git a/internal/frontend/bridge-gui/bridge-gui-tester/Pch.h b/internal/frontend/bridge-gui/bridge-gui-tester/Pch.h
new file mode 100644
index 00000000..cb05660f
--- /dev/null
+++ b/internal/frontend/bridge-gui/bridge-gui-tester/Pch.h
@@ -0,0 +1,29 @@
+// Copyright (c) 2022 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 .
+
+
+#ifndef BRIDGE_GUI_PCH_H
+#define BRIDGE_GUI_PCH_H
+
+
+#include
+#include
+#include
+#include "AppController.h"
+
+
+#endif // BRIDGE_GUI_PCH_H
diff --git a/internal/frontend/bridge-gui/bridge-gui-tester/Tabs/SettingsTab.cpp b/internal/frontend/bridge-gui/bridge-gui-tester/Tabs/SettingsTab.cpp
new file mode 100644
index 00000000..f410c469
--- /dev/null
+++ b/internal/frontend/bridge-gui/bridge-gui-tester/Tabs/SettingsTab.cpp
@@ -0,0 +1,496 @@
+// Copyright (c) 2022 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 .
+
+
+#include "SettingsTab.h"
+#include "GRPCService.h"
+#include
+#include
+
+
+using namespace bridgepp;
+
+
+namespace
+{
+QString const colorSchemeDark = "dark"; ///< The dark color scheme name.
+QString const colorSchemeLight = "light"; ///< THe light color scheme name.
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] parent The parent widget of the tab.
+//****************************************************************************************************************************************************
+SettingsTab::SettingsTab(QWidget *parent)
+ : QWidget(parent)
+{
+ ui_.setupUi(this);
+
+ connect(ui_.buttonInternetOn, &QPushButton::clicked, []() { app().grpc().sendEvent(newInternetStatusEvent(true)); });
+ connect(ui_.buttonInternetOff, &QPushButton::clicked, []() { app().grpc().sendEvent(newInternetStatusEvent(false)); });
+ connect(ui_.buttonShowMainWindow, &QPushButton::clicked, []() { app().grpc().sendEvent(newShowMainWindowEvent()); });
+ connect(ui_.checkNextCacheChangeWillSucceed, &QCheckBox::toggled, this, &SettingsTab::updateGUIState);
+ this->resetUI();
+ this->updateGUIState();
+}
+
+
+
+//****************************************************************************************************************************************************
+//
+//****************************************************************************************************************************************************
+void SettingsTab::updateGUIState()
+{
+ bool connected = app().grpc().isStreaming();
+ for (QWidget *widget: { ui_.groupVersion, ui_.groupGeneral, ui_.groupMail, ui_.groupPaths, ui_.groupCache })
+ widget->setEnabled(!connected);
+ ui_.comboCacheError -> setEnabled(!ui_.checkNextCacheChangeWillSucceed->isChecked());
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] isStreaming Is the event stream on?
+//****************************************************************************************************************************************************
+void SettingsTab::setIsStreaming(bool isStreaming)
+{
+ ui_.labelStreamingValue->setText(isStreaming ? "Yes" : "No");
+ this->updateGUIState();
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] clientPlatform The client platform.
+//****************************************************************************************************************************************************
+void SettingsTab::setClientPlatform(QString const &clientPlatform)
+{
+ ui_.labelClientPlatformValue->setText(clientPlatform);
+}
+
+
+//****************************************************************************************************************************************************
+/// \return The version of Bridge
+//****************************************************************************************************************************************************
+QString SettingsTab::bridgeVersion() const
+{
+ return ui_.editVersion->text();
+}
+
+
+//****************************************************************************************************************************************************
+/// \return The OS as a Go GOOS compatible value ("darwin", "linux" or "windows").
+//****************************************************************************************************************************************************
+QString SettingsTab::os() const
+{
+ return ui_.comboOS->currentText();
+}
+
+
+//****************************************************************************************************************************************************
+/// \return The value for the 'Current Email Client' edit.
+//****************************************************************************************************************************************************
+QString SettingsTab::currentEmailClient() const
+{
+ return ui_.editCurrentEmailClient->text();
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] ready Is the GUI ready?
+//****************************************************************************************************************************************************
+void SettingsTab::setGUIReady(bool ready)
+{
+ this->updateGUIState();
+ ui_.labelGUIReadyValue->setText(ready ? "Yes" : "No");
+}
+
+
+//****************************************************************************************************************************************************
+/// \return true iff the 'Show On Startup' check box is checked.
+//****************************************************************************************************************************************************
+bool SettingsTab::showOnStartup() const
+{
+ return ui_.checkShowOnStartup->isChecked();
+}
+
+
+//****************************************************************************************************************************************************
+/// \return true iff the 'Show Splash Screen' check box is checked.
+//****************************************************************************************************************************************************
+bool SettingsTab::showSplashScreen() const
+{
+ return ui_.checkShowSplashScreen->isChecked();
+}
+
+
+//****************************************************************************************************************************************************
+/// \return true iff the 'Show Splash Screen' check box is checked.
+//****************************************************************************************************************************************************
+bool SettingsTab::isFirstGUIStart() const
+{
+ return ui_.checkIsFirstGUIStart->isChecked();
+}
+
+
+//****************************************************************************************************************************************************
+/// \return true iff the 'Show Splash Screen' check box is checked.
+//****************************************************************************************************************************************************
+bool SettingsTab::isAutostartOn() const
+{
+ return ui_.checkAutostart->isChecked();
+}
+
+
+//****************************************************************************************************************************************************
+/// \return true iff the 'Show Splash Screen' check box is checked.
+//****************************************************************************************************************************************************
+void SettingsTab::setIsAutostartOn(bool on)
+{
+ ui_.checkAutostart->setChecked(on);
+}
+
+
+//****************************************************************************************************************************************************
+/// \return true if the 'Use Dark Theme' check box is checked.
+//****************************************************************************************************************************************************
+QString SettingsTab::colorSchemeName() const
+{
+ return ui_.checkDarkTheme->isChecked() ? colorSchemeDark : colorSchemeLight;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] name True if the 'Use Dark Theme' check box should be checked.
+//****************************************************************************************************************************************************
+void SettingsTab::setColorSchemeName(QString const &name)
+{
+ ui_.checkDarkTheme->setChecked(name == colorSchemeDark);
+}
+
+
+//****************************************************************************************************************************************************
+/// \return true if the 'Beta Enabled' check box is checked.
+//****************************************************************************************************************************************************
+bool SettingsTab::isBetaEnabled() const
+{
+ return ui_.checkBetaEnabled->isChecked();
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] enabled The new state for the 'Beta Enabled' check box.
+//****************************************************************************************************************************************************
+void SettingsTab::setIsBetaEnabled(bool enabled)
+{
+ ui_.checkBetaEnabled->setChecked(enabled);
+}
+
+
+//****************************************************************************************************************************************************
+/// \return The delay to apply before sending automatically generated events.
+//****************************************************************************************************************************************************
+qint32 SettingsTab::eventDelayMs() const
+{
+ return ui_.spinEventDelay->value();
+}
+
+
+//****************************************************************************************************************************************************
+/// \return The path
+//****************************************************************************************************************************************************
+QString SettingsTab::logsPath() const
+{
+ return ui_.editLogsPath->text();
+}
+
+
+//****************************************************************************************************************************************************
+/// \return The path
+//****************************************************************************************************************************************************
+QString SettingsTab::licensePath() const
+{
+ return ui_.editLicensePath->text();
+}
+
+
+//****************************************************************************************************************************************************
+/// \return The link.
+//****************************************************************************************************************************************************
+QString SettingsTab::releaseNotesPageLink() const
+{
+ return ui_.editReleaseNotesLink->text();
+}
+
+
+//****************************************************************************************************************************************************
+/// \return The link.
+//****************************************************************************************************************************************************
+QString SettingsTab::dependencyLicenseLink() const
+{
+ return ui_.editDependencyLicenseLink->text();
+}
+
+
+//****************************************************************************************************************************************************
+/// \return The link.
+//****************************************************************************************************************************************************
+QString SettingsTab::landingPageLink() const
+{
+ return ui_.editLandingPageLink->text();
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] osType The OS type.
+/// \param[in] osVersion The OS version.
+/// \param[in] emailClient The email client.
+/// \param[in] address The email address.
+/// \param[in] description The description.
+/// \param[in] includeLogs Are the log included.
+//****************************************************************************************************************************************************
+void SettingsTab::setBugReport(QString const &osType, QString const &osVersion, QString const &emailClient, QString const &address,
+ QString const &description, bool includeLogs)
+{
+ ui_.editOSType->setText(osType);
+ ui_.editOSVersion->setText(osVersion);
+ ui_.editEmailClient->setText(emailClient);
+ ui_.editAddress->setText(address);
+ ui_.editDescription->setPlainText(description);
+ ui_.labelIncludeLogsValue->setText(includeLogs ? "Yes" : "No");
+}
+
+
+//****************************************************************************************************************************************************
+/// \return The state of the check box.
+//****************************************************************************************************************************************************
+bool SettingsTab::nextBugReportWillSucceed() const
+{
+ return ui_.checkNextBugReportWillSucceed->isChecked();
+}
+
+
+//****************************************************************************************************************************************************
+/// \return The value of the 'Hostname' edit.
+//****************************************************************************************************************************************************
+QString SettingsTab::hostname() const
+{
+ return ui_.editHostname->text();
+}
+
+
+//****************************************************************************************************************************************************
+/// \return The value of the IMAP port spin box.
+//****************************************************************************************************************************************************
+qint32 SettingsTab::imapPort()
+{
+ return ui_.spinPortIMAP->value();
+}
+
+
+//****************************************************************************************************************************************************
+/// \return The value of the SMTP port spin box.
+//****************************************************************************************************************************************************
+qint32 SettingsTab::smtpPort()
+{
+ return ui_.spinPortSMTP->value();
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] imapPort The IMAP port.
+/// \param[in] smtpPort The SMTP port.
+//****************************************************************************************************************************************************
+void SettingsTab::changePorts(qint32 imapPort, qint32 smtpPort)
+{
+ ui_.spinPortIMAP->setValue(imapPort);
+ ui_.spinPortSMTP->setValue(smtpPort);
+}
+
+
+//****************************************************************************************************************************************************
+/// \return The state of the 'Use SSL for SMTP' check box.
+//****************************************************************************************************************************************************
+bool SettingsTab::useSSLForSMTP() const
+{
+ return ui_.checkUseSSLForSMTP->isChecked();
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] use The state of the 'Use SSL for SMTP' check box.
+//****************************************************************************************************************************************************
+void SettingsTab::setUseSSLForSMTP(bool use)
+{
+ ui_.checkUseSSLForSMTP->setChecked(use);
+}
+
+
+//****************************************************************************************************************************************************
+/// \return The state of the the 'DoH enabled' check box.
+//****************************************************************************************************************************************************
+bool SettingsTab::isDoHEnabled() const
+{
+ return ui_.checkDoHEnabled->isChecked();
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] enabled The state of the 'DoH enabled' check box.
+//****************************************************************************************************************************************************
+void SettingsTab::setIsDoHEnabled(bool enabled)
+{
+ ui_.checkDoHEnabled->setChecked(enabled);
+}
+
+
+//****************************************************************************************************************************************************
+/// \return The reply for the next IsPortFree gRPC call.
+//****************************************************************************************************************************************************
+bool SettingsTab::isPortFree() const
+{
+ return ui_.checkIsPortFree->isChecked();
+}
+
+
+//****************************************************************************************************************************************************
+/// \return true iff cache on disk is enabled.
+//****************************************************************************************************************************************************
+bool SettingsTab::isCacheOnDiskEnabled() const
+{
+ return ui_.checkCacheOnDiskEnabled->isChecked();
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] enabled Is the cache on disk enabled?
+//****************************************************************************************************************************************************
+void SettingsTab::changeLocalCache(bool enabled, QString const &path)
+{
+ ui_.checkCacheOnDiskEnabled->setChecked(enabled);
+ ui_.editDiskCachePath->setText(path);
+}
+
+
+//****************************************************************************************************************************************************
+/// \return The disk cache path.
+//****************************************************************************************************************************************************
+QString SettingsTab::diskCachePath() const
+{
+ return ui_.editDiskCachePath->text();
+}
+
+
+//****************************************************************************************************************************************************
+/// \return The value for the 'Next Cache Change Will Succeed' check box.
+//****************************************************************************************************************************************************
+bool SettingsTab::nextCacheChangeWillSucceed() const
+{
+ return ui_.checkNextCacheChangeWillSucceed->isChecked();
+}
+
+
+//****************************************************************************************************************************************************
+/// \return The index of the selected cache error.
+//****************************************************************************************************************************************************
+qint32 SettingsTab::cacheError() const
+{
+ return ui_.comboCacheError->currentIndex();
+}
+
+
+//****************************************************************************************************************************************************
+/// \return the value for the 'Automatic Update' check.
+//****************************************************************************************************************************************************
+bool SettingsTab::isAutomaticUpdateOn() const
+{
+ return ui_.checkAutomaticUpdate->isChecked();
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] on The value for the 'Automatic Update' check.
+//****************************************************************************************************************************************************
+void SettingsTab::setIsAutomaticUpdateOn(bool on)
+{
+ ui_.checkAutomaticUpdate->setChecked(on);
+}
+
+
+//****************************************************************************************************************************************************
+//
+//****************************************************************************************************************************************************
+void SettingsTab::resetUI()
+{
+ this->setGUIReady(false);
+ this->setIsStreaming(false);
+ this->setClientPlatform("Unknown");
+
+ ui_.editVersion->setText(BRIDGE_APP_VERSION);
+ ui_.comboOS->setCurrentText(bridgepp::goos());
+ ui_.editCurrentEmailClient->setText("Thunderbird/102.0.3");
+ ui_.checkShowOnStartup->setChecked(true);
+ ui_.checkShowSplashScreen->setChecked(false);
+ ui_.checkIsFirstGUIStart->setChecked(false);
+ ui_.checkAutostart->setChecked(true);
+ ui_.checkBetaEnabled->setChecked(true);
+ ui_.checkDarkTheme->setChecked(false);
+
+ QString const tmpDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation);
+
+ QString const logsDir = QDir(tmpDir).absoluteFilePath("logs");
+ QDir().mkpath(logsDir);
+ ui_.editLogsPath->setText(QDir::toNativeSeparators(logsDir));
+
+ QString const filePath = QDir(tmpDir).absoluteFilePath("LICENSE.txt");
+ QFile file(filePath);
+ if (!file.exists())
+ {
+ // we don't really care if it fails.
+ file.open(QIODevice::WriteOnly | QIODevice::Text);
+ file.write(QString("This is were the license should be.").toLocal8Bit());
+ file.close();
+ }
+ ui_.editLicensePath->setText(filePath);
+
+ ui_.editReleaseNotesLink->setText("https://en.wikipedia.org/wiki/Release_notes");
+ ui_.editDependencyLicenseLink->setText("https://en.wikipedia.org/wiki/Dependency_relation");
+ ui_.editLandingPageLink->setText("https://proton.me");
+
+ ui_.editOSType->setText(QString());
+ ui_.editOSVersion->setText(QString());
+ ui_.editEmailClient->setText(QString());
+ ui_.editAddress->setText(QString());
+ ui_.editDescription->setPlainText(QString());
+ ui_.labelIncludeLogsValue->setText(QString());
+ ui_.checkNextBugReportWillSucceed->setChecked(true);
+
+ ui_.editHostname->setText("localhost");
+ ui_.spinPortIMAP->setValue(1143);
+ ui_.spinPortSMTP->setValue(1025);
+ ui_.checkUseSSLForSMTP->setChecked(false);
+ ui_.checkDoHEnabled->setChecked(true);
+ ui_.checkIsPortFree->setChecked(true);
+
+ ui_.checkCacheOnDiskEnabled->setChecked(true);
+ QString const cacheDir = QDir(tmpDir).absoluteFilePath("cache");
+ QDir().mkpath(cacheDir);
+ ui_.editDiskCachePath->setText(QDir::toNativeSeparators(cacheDir));
+ ui_.checkNextCacheChangeWillSucceed->setChecked(true);
+ ui_.comboCacheError->setCurrentIndex(0);
+
+ ui_.checkAutomaticUpdate->setChecked(true);
+}
+
diff --git a/internal/frontend/bridge-gui/bridge-gui-tester/Tabs/SettingsTab.h b/internal/frontend/bridge-gui/bridge-gui-tester/Tabs/SettingsTab.h
new file mode 100644
index 00000000..66f453f5
--- /dev/null
+++ b/internal/frontend/bridge-gui/bridge-gui-tester/Tabs/SettingsTab.h
@@ -0,0 +1,92 @@
+// Copyright (c) 2022 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 .
+
+
+#ifndef BRIDGE_GUI_TESTER_GENERAL_TAB_H
+#define BRIDGE_GUI_TESTER_GENERAL_TAB_H
+
+
+#include "Tab/ui_SettingsTab.h"
+
+
+//****************************************************************************************************************************************************
+/// \brief The 'General' tab of the main window.
+//****************************************************************************************************************************************************
+class SettingsTab : public QWidget
+{
+Q_OBJECT
+public: // member functions.
+ explicit SettingsTab(QWidget *parent = nullptr); ///< Default constructor.
+ SettingsTab(SettingsTab const &) = delete; ///< Disabled copy-constructor.
+ SettingsTab(SettingsTab &&) = delete; ///< Disabled assignment copy-constructor.
+ ~SettingsTab() = default; ///< Destructor.
+ SettingsTab &operator=(SettingsTab const &) = delete; ///< Disabled assignment operator.
+ SettingsTab &operator=(SettingsTab &&) = delete; ///< Disabled move assignment operator.
+
+ QString bridgeVersion() const; ///< Get the Bridge version.
+ QString os() const; ///< Return the OS string.
+ QString currentEmailClient() const; ///< Return the content of the current email client
+ void setGUIReady(bool ready); ///< Set the GUI as ready.
+ bool showOnStartup() const; ///< Get the value for the 'Show On Startup' check.
+ bool showSplashScreen() const; ///< Get the value for the 'Show Splash Screen' check.
+ bool isFirstGUIStart() const; ///< Get the value for the 'Is First GUI Start' check.
+ bool isAutostartOn() const; ///< Get the value for the 'Autostart' check.
+ bool isBetaEnabled() const; ///< Get the value for the 'Beta Enabled' check.
+ QString colorSchemeName() const; ///< Get the value of the 'Use Dark Theme' checkbox.
+ qint32 eventDelayMs() const; ///< Get the delay for sending automatically generated events.
+ QString logsPath() const; ///< Get the content of the 'Logs Path' edit.
+ QString licensePath() const; ///< Get the content of the 'License Path' edit.
+ QString releaseNotesPageLink() const; ///< Get the content of the 'Release Notes Page Link' edit.
+ QString dependencyLicenseLink() const; ///< Get the content of the 'Dependency License Link' edit.
+ QString landingPageLink() const; ///< Get the content of the 'Landing Page Link' edit.
+ bool nextBugReportWillSucceed() const; ///< Get the status of the 'Next Bug Report Will Fail' check box.
+ QString hostname() const; ///< Get the value of the 'Hostname' edit.
+ qint32 imapPort(); ///< Get the value of the IMAP port spin.
+ qint32 smtpPort(); ///< Get the value of the SMTP port spin.
+ bool useSSLForSMTP() const; ///< Get the value for the 'Use SSL for SMTP' check box.
+ bool isDoHEnabled() const; ///< Get the value for the 'DoH Enabled' check box.
+ bool isPortFree() const; ///< Get the value for the "Is Port Free" check box.
+ bool isCacheOnDiskEnabled() const; ///< get the value for the 'Cache On Disk Enabled' check box.
+ QString diskCachePath() const; ///< Get the value for the 'Disk Cache Path' edit.
+ bool nextCacheChangeWillSucceed() const; ///< Get the value for the 'Next Cache Change will succeed' edit.
+ qint32 cacheError() const; ///< Return the index of the selected cache error.
+ bool isAutomaticUpdateOn() const; ///
+
+ SettingsTab
+
+
+
+ 0
+ 0
+ 1066
+ 808
+
+
+
+ Form
+
+
+ -
+
+
-
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ Version Info
+
+
+
-
+
+
-
+
+
+ Bridge Version
+
+
+
+ -
+
+
+
+
+ -
+
+
-
+
+
+ OS
+
+
+
+ -
+
+
-
+
+ darwin
+
+
+ -
+
+ linux
+
+
+ -
+
+ windows
+
+
+
+
+ -
+
+
+ Current Email Client
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ General Settings
+
+
+
-
+
+
+ Show On Startup
+
+
+ false
+
+
+
+ -
+
+
+ Show Splash Screen
+
+
+ false
+
+
+
+ -
+
+
+ Is FIrst GUI Start
+
+
+ false
+
+
+
+ -
+
+
+ Autostart
+
+
+ false
+
+
+
+ -
+
+
+ Beta Enabled
+
+
+ false
+
+
+
+ -
+
+
+ Dark Theme
+
+
+ false
+
+
+
+ -
+
+
+ Automatic Update
+
+
+
+
+
+
+ -
+
+
+ Mail
+
+
+
-
+
+
-
+
+
+ Hostname
+
+
+
+ -
+
+
+
+ 200
+ 0
+
+
+
+
+
+
+ -
+
+
-
+
+
+ IMAP Port
+
+
+
+ -
+
+
+ QAbstractSpinBox::NoButtons
+
+
+ 1
+
+
+ 65535
+
+
+
+ -
+
+
+ SMTP Port
+
+
+
+ -
+
+
+ QAbstractSpinBox::NoButtons
+
+
+ 1
+
+
+ 65535
+
+
+
+ -
+
+
+ Use SSL For SMTP
+
+
+
+
+
+ -
+
+
+ DoH Enabled
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Paths && Links
+
+
+
-
+
+
+ Logs Path
+
+
+
+ -
+
+
+ -
+
+
+ License Path
+
+
+
+ -
+
+
+ -
+
+
+ Release Notes Page Link
+
+
+
+ -
+
+
+ -
+
+
+ Dependency License Link
+
+
+
+ -
+
+
+ -
+
+
+ Landing Page Link
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Cache
+
+
+
-
+
+
+ Cache On Disk Enabled
+
+
+
+ -
+
+
-
+
+
+ Disk Cache Path
+
+
+
+ -
+
+
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+ -
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ Status
+
+
+
-
+
+
-
+
+
+ GUI Ready:
+
+
+
+ -
+
+
+
+ 50
+ 0
+
+
+
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 1
+
+
+
+
+
+
+ -
+
+
-
+
+
+ Streaming:
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+ Client Platform:
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Bug Report
+
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 250
+ 0
+
+
+
+ true
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ true
+
+
+
+ -
+
+
+ OS Type
+
+
+
+ -
+
+
+ Include Logs
+
+
+
+ -
+
+
+ Address
+
+
+
+ -
+
+
+ OS Version
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 250
+ 0
+
+
+
+ true
+
+
+
+ -
+
+
+ ?
+
+
+
+ -
+
+
+ Description
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+ true
+
+
+
+ -
+
+
+ Email Cient
+
+
+
+ -
+
+
+ true
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Events && Errors
+
+
+
-
+
+
-
+
+
+ Delay applied before sending automatically generated events
+
+
+ Delay for asynchronous events
+
+
+
+ -
+
+
+ ms
+
+
+ 3600000
+
+
+ 100
+
+
+ 1000
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 1
+
+
+
+
+
+
+ -
+
+
-
+
+
+ Internet Off
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 0
+ 10
+
+
+
+
+ -
+
+
+ Internet On
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 0
+ 10
+
+
+
+
+ -
+
+
+ Show Main Window
+
+
+
+
+
+ -
+
+
+ Reply true to the next 'Is Port Free' request.
+
+
+
+ -
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ Next Cache Change will succeed
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 1
+
+
+
+
+ -
+
+
-
+
+ Cache Unavailable
+
+
+ -
+
+ Can't Move Cache
+
+
+ -
+
+ Disk Full
+
+
+
+
+
+
+ -
+
+
+ Next Bug Report Will Succeed
+
+
+
+
+
+
+
+
+
+
+
+
+
+ checkShowOnStartup
+ checkShowSplashScreen
+ checkIsFirstGUIStart
+ checkAutostart
+ checkBetaEnabled
+ checkDarkTheme
+ editVersion
+ comboOS
+ editCurrentEmailClient
+ editOSType
+ editOSVersion
+ editEmailClient
+ editAddress
+ editDescription
+ editLogsPath
+ editLicensePath
+ editReleaseNotesLink
+ editDependencyLicenseLink
+ editLandingPageLink
+
+
+
+
diff --git a/internal/frontend/bridge-gui/bridge-gui-tester/Tabs/UsersTab.cpp b/internal/frontend/bridge-gui/bridge-gui-tester/Tabs/UsersTab.cpp
new file mode 100644
index 00000000..76ff5650
--- /dev/null
+++ b/internal/frontend/bridge-gui/bridge-gui-tester/Tabs/UsersTab.cpp
@@ -0,0 +1,318 @@
+// Copyright (c) 2022 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 .
+
+
+#include "UsersTab.h"
+#include "MainWindow.h"
+#include "UserDialog.h"
+#include
+#include
+#include
+
+
+using namespace bridgepp;
+
+
+//****************************************************************************************************************************************************
+/// \param[in] parent The parent widget of the tab.
+//****************************************************************************************************************************************************
+UsersTab::UsersTab(QWidget *parent)
+ : QWidget(parent)
+ , users_(nullptr)
+{
+ ui_.setupUi(this);
+
+ ui_.tableUserList->setModel(&users_);
+
+ QItemSelectionModel *model = ui_.tableUserList->selectionModel();
+ if (!model)
+ throw Exception("Could not get user table selection model.");
+ connect(model, &QItemSelectionModel::selectionChanged, this, &UsersTab::onSelectionChanged);
+
+ ui_.tableUserList->setColumnWidth(0, 150);
+ ui_.tableUserList->setColumnWidth(1, 250);
+ ui_.tableUserList->setColumnWidth(2, 350);
+
+ connect(ui_.buttonNewUser, &QPushButton::clicked, this, &UsersTab::onAddUserButton);
+ connect(ui_.buttonEditUser, &QPushButton::clicked, this, &UsersTab::onEditUserButton);
+ connect(ui_.tableUserList, &QTableView::doubleClicked, this, &UsersTab::onEditUserButton);
+ connect(ui_.buttonRemoveUser, &QPushButton::clicked, this, &UsersTab::onRemoveUserButton);
+
+ users_.append(randomUser());
+
+ this->updateGUIState();
+}
+
+
+//****************************************************************************************************************************************************
+//
+//****************************************************************************************************************************************************
+void UsersTab::onAddUserButton()
+{
+ SPUser user = randomUser();
+ UserDialog dialog(user, this);
+ if (QDialog::Accepted != dialog.exec())
+ return;
+ users_.append(user);
+ GRPCService &grpc = app().grpc();
+ if (grpc.isStreaming())
+ grpc.sendEvent(newLoginFinishedEvent(user->id()));
+}
+
+
+//****************************************************************************************************************************************************
+//
+//****************************************************************************************************************************************************
+void UsersTab::onEditUserButton()
+{
+ int index = selectedIndex();
+ if ((index < 0) || (index >= users_.userCount()))
+ return;
+
+ SPUser user = this->selectedUser();
+ UserDialog dialog(user, this);
+ if (QDialog::Accepted != dialog.exec())
+ return;
+
+ users_.touch(index);
+ GRPCService &grpc = app().grpc();
+ if (grpc.isStreaming())
+ grpc.sendEvent(newUserChangedEvent(user->id()));
+}
+
+
+//****************************************************************************************************************************************************
+//
+//****************************************************************************************************************************************************
+void UsersTab::onRemoveUserButton()
+{
+ int index = selectedIndex();
+ if ((index < 0) || (index >= users_.userCount()))
+ return;
+
+ SPUser const user = users_.userAtIndex(index);
+ users_.remove(index);
+ GRPCService &grpc = app().grpc();
+ if (grpc.isStreaming())
+ grpc.sendEvent(newUserChangedEvent(user->id()));
+}
+
+
+//****************************************************************************************************************************************************
+//
+//****************************************************************************************************************************************************
+void UsersTab::onSelectionChanged(QItemSelection, QItemSelection)
+{
+ this->updateGUIState();
+}
+
+
+//****************************************************************************************************************************************************
+//
+//****************************************************************************************************************************************************
+void UsersTab::updateGUIState()
+{
+ bool const hasSelectedUser = ui_.tableUserList->selectionModel()->hasSelection();
+ ui_.buttonEditUser->setEnabled(hasSelectedUser);
+ ui_.buttonRemoveUser->setEnabled(hasSelectedUser);
+}
+
+
+//****************************************************************************************************************************************************
+//
+//****************************************************************************************************************************************************
+qint32 UsersTab::selectedIndex() const
+{
+ return ui_.tableUserList->selectionModel()->hasSelection() ? ui_.tableUserList->currentIndex().row() : -1;
+}
+
+
+//****************************************************************************************************************************************************
+/// \return The selected user.
+/// \return A null pointer if no user is selected.
+//****************************************************************************************************************************************************
+bridgepp::SPUser UsersTab::selectedUser()
+{
+ return users_.userAtIndex(this->selectedIndex());
+}
+
+
+//****************************************************************************************************************************************************
+/// \return The list of users.
+//****************************************************************************************************************************************************
+UserTable &UsersTab::userTable()
+{
+ return users_;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] userID The userID.
+/// \return The user with the given userID.
+/// \return A null pointer if the user is not in the list.
+//****************************************************************************************************************************************************
+bridgepp::SPUser UsersTab::userWithID(QString const &userID)
+{
+ return users_.userWithID(userID);
+}
+
+
+//****************************************************************************************************************************************************
+/// \return true iff the next login attempt should trigger a username/password error.
+//****************************************************************************************************************************************************
+bool UsersTab::nextUserUsernamePasswordError() const
+{
+ return ui_.checkUsernamePasswordError->isChecked();
+}
+
+
+//****************************************************************************************************************************************************
+/// \return true iff the next login attempt should trigger a free user error.
+//****************************************************************************************************************************************************
+bool UsersTab::nextUserFreeUserError() const
+{
+ return ui_.checkFreeUserError->isChecked();
+}
+
+
+//****************************************************************************************************************************************************
+/// \return true iff the next login attempt will require 2FA.
+//****************************************************************************************************************************************************
+bool UsersTab::nextUserTFARequired() const
+{
+ return ui_.checkTFARequired->isChecked();
+}
+
+
+//****************************************************************************************************************************************************
+/// \return true iff the next login attempt should trigger a 2FA error.
+//****************************************************************************************************************************************************
+bool UsersTab::nextUserTFAError() const
+{
+ return ui_.checkTFAError->isChecked();
+}
+
+
+//****************************************************************************************************************************************************
+/// \return true iff the next login attempt should trigger a 2FA error with abort.
+//****************************************************************************************************************************************************
+bool UsersTab::nextUserTFAAbort() const
+{
+ return ui_.checkTFAAbort->isChecked();
+}
+
+
+//****************************************************************************************************************************************************
+/// \return true iff the next login attempt will require a 2nd password.
+//****************************************************************************************************************************************************
+bool UsersTab::nextUserTwoPasswordsRequired() const
+{
+ return ui_.checkTwoPasswordsRequired->isChecked();
+}
+
+
+//****************************************************************************************************************************************************
+/// \return true iff the next login attempt should trigger a 2nd password error.
+//****************************************************************************************************************************************************
+bool UsersTab::nextUserTwoPasswordsError() const
+{
+ return ui_.checkTwoPasswordsError->isChecked();
+}
+
+
+//****************************************************************************************************************************************************
+/// \return true iff the next login attempt should trigger a 2nd password error with abort.
+//****************************************************************************************************************************************************
+bool UsersTab::nextUserTwoPasswordsAbort() const
+{
+ return ui_.checkTwoPasswordsAbort->isChecked();
+}
+
+
+//****************************************************************************************************************************************************
+/// \return true iff the next login attempt should trigger a 2nd password error with abort.
+//****************************************************************************************************************************************************
+bool UsersTab::nextUserAlreadyLoggedIn() const
+{
+ return ui_.checkAlreadyLoggedIn->isChecked();
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] userID The userID.
+/// \param[in] makeItActive Should split mode be activated.
+//****************************************************************************************************************************************************
+void UsersTab::setUserSplitMode(QString const &userID, bool makeItActive)
+{
+ qint32 const index = users_.indexOfUser(userID);
+ SPUser const user = users_.userAtIndex(index);
+ if (!user)
+ {
+ app().log().error(QString("%1 failed. unknown user %1").arg(__FUNCTION__, userID));
+ return;
+ }
+ user->setSplitMode(makeItActive);
+ users_.touch(index);
+ MainWindow &mainWindow = app().mainWindow();
+ mainWindow.sendDelayedEvent(newUserChangedEvent(userID));
+ mainWindow.sendDelayedEvent(newToggleSplitModeFinishedEvent(userID));
+}
+
+//****************************************************************************************************************************************************
+/// \param[in] userID The userID.
+//****************************************************************************************************************************************************
+void UsersTab::logoutUser(QString const &userID)
+{
+ qint32 const index = users_.indexOfUser(userID);
+ SPUser const user = users_.userAtIndex(index);
+ if (!user)
+ {
+ app().log().error(QString("%1 failed. unknown user %1").arg(__FUNCTION__, userID));
+ return;
+ }
+ user->setLoggedIn(false);
+ users_.touch(index);
+ app().mainWindow().sendDelayedEvent(newUserChangedEvent(userID));
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] userID The userID.
+//****************************************************************************************************************************************************
+void UsersTab::removeUser(QString const &userID)
+{
+ qint32 const index = users_.indexOfUser(userID);
+ SPUser const user = users_.userAtIndex(index);
+ if (!user)
+ {
+ app().log().error(QString("%1 failed. unknown user %1").arg(__FUNCTION__, userID));
+ return;
+ }
+ users_.remove(index);
+ app().mainWindow().sendDelayedEvent(newUserChangedEvent(userID));
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] userID The userID.
+/// \param[in] address The address.
+//****************************************************************************************************************************************************
+void UsersTab::configureUserAppleMail(QString const &userID, QString const &address)
+{
+ app().log().info(QString("Apple mail configuration was requested for user %1, address %2").arg(userID, address));
+
+}
diff --git a/internal/frontend/bridge-gui/bridge-gui-tester/Tabs/UsersTab.h b/internal/frontend/bridge-gui/bridge-gui-tester/Tabs/UsersTab.h
new file mode 100644
index 00000000..55a44c49
--- /dev/null
+++ b/internal/frontend/bridge-gui/bridge-gui-tester/Tabs/UsersTab.h
@@ -0,0 +1,75 @@
+// Copyright (c) 2022 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 .
+
+
+#ifndef BRIDGE_GUI_TESTER_USERS_TAB_H
+#define BRIDGE_GUI_TESTER_USERS_TAB_H
+
+
+#include "Tabs/ui_UsersTab.h"
+#include "UserTable.h"
+
+
+//****************************************************************************************************************************************************
+/// \brief The 'Users' tab of the main window.
+//****************************************************************************************************************************************************
+class UsersTab : public QWidget
+{
+Q_OBJECT
+public: // member functions.
+ explicit UsersTab(QWidget *parent = nullptr); ///< Default constructor.
+ UsersTab(UsersTab const &) = delete; ///< Disabled copy-constructor.
+ UsersTab(UsersTab &&) = delete; ///< Disabled assignment copy-constructor.
+ ~UsersTab() override = default; ///< Destructor.
+ UsersTab &operator=(UsersTab const &) = delete; ///< Disabled assignment operator.
+ UsersTab &operator=(UsersTab &&) = delete; ///< Disabled move assignment operator.
+ UserTable &userTable(); ///< Returns a reference to the user table.
+ bridgepp::SPUser userWithID(QString const &userID); ///< Get the user with the given ID.
+ bool nextUserUsernamePasswordError() const; ///< Check if next user login should trigger a username/password error.
+ bool nextUserFreeUserError() const; ///< Check if next user login should trigger a Free user error.
+ bool nextUserTFARequired() const; ///< Check if next user login should requires 2FA.
+ bool nextUserTFAError() const; ///< Check if next user login should trigger 2FA error
+ bool nextUserTFAAbort() const; ///< Check if next user login should trigger 2FA abort.
+ bool nextUserTwoPasswordsRequired() const; ///< Check if next user login requires 2nd password
+ bool nextUserTwoPasswordsError() const; ///< Check if next user login should trigger 2nd password error.
+ bool nextUserTwoPasswordsAbort() const; ///< Check if next user login should trigger 2nd password abort.
+ bool nextUserAlreadyLoggedIn() const; ///< Check if next user login should report user as already logged in.
+
+public slots:
+ void setUserSplitMode(QString const &userID, bool makeItActive); ///< Slot for the split mode.
+ void logoutUser(QString const &userID); ///< slot for the logging out of a user.
+ void removeUser(QString const &userID); ///< Slot for the removal of a user.
+ void configureUserAppleMail(QString const &userID, QString const &address); ///< Slot for the configuration of Apple mail.
+
+private slots:
+ void onAddUserButton(); ///< Add a user to the user list.
+ void onEditUserButton(); ///< Edit the currently selected user.
+ void onRemoveUserButton(); ///< Remove the currently selected user.
+ void onSelectionChanged(QItemSelection, QItemSelection); ///< Slot for the change of the selection.
+
+private: // member functions.
+ void updateGUIState(); ///< Update the GUI state.
+ qint32 selectedIndex() const; ///< Get the index of the selected row.
+ bridgepp::SPUser selectedUser(); ///< Get the selected user.
+
+private: // data members.
+ Ui::UsersTab ui_ {}; ///< The UI for the tab.
+ UserTable users_; ///< The User list.
+};
+
+
+#endif //BRIDGE_GUI_TESTER_USERS_TAB_H
diff --git a/internal/frontend/bridge-gui/bridge-gui-tester/Tabs/UsersTab.ui b/internal/frontend/bridge-gui/bridge-gui-tester/Tabs/UsersTab.ui
new file mode 100644
index 00000000..ce297a51
--- /dev/null
+++ b/internal/frontend/bridge-gui/bridge-gui-tester/Tabs/UsersTab.ui
@@ -0,0 +1,159 @@
+
+
+ UsersTab
+
+
+
+ 0
+ 0
+ 1207
+ 709
+
+
+
+ Form
+
+
+ -
+
+
+ QAbstractItemView::SingleSelection
+
+
+ QAbstractItemView::SelectRows
+
+
+ true
+
+
+ false
+
+
+
+ -
+
+
-
+
+
+ New User
+
+
+
+ -
+
+
+ Edit User
+
+
+
+ -
+
+
+ Remove User
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+ -
+
+
+
+ 0
+ 100
+
+
+
+ Next Login Attempt
+
+
+
-
+
+
+ Username/password error
+
+
+
+ -
+
+
+ Free user error
+
+
+
+ -
+
+
+ 2FA required
+
+
+
+ -
+
+
+ 2FA error
+
+
+
+ -
+
+
+ 2FA abort
+
+
+
+ -
+
+
+ 2nd password required
+
+
+
+ -
+
+
+ 2nd password error
+
+
+
+ -
+
+
+ 2nd password abort
+
+
+
+ -
+
+
+ Already logged in
+
+
+
+
+
+
+
+
+
+
+
+ buttonNewUser
+ buttonEditUser
+ buttonRemoveUser
+ tableUserList
+
+
+
+
diff --git a/internal/frontend/bridge-gui/bridge-gui-tester/UserDialog.cpp b/internal/frontend/bridge-gui/bridge-gui-tester/UserDialog.cpp
new file mode 100644
index 00000000..1003aedf
--- /dev/null
+++ b/internal/frontend/bridge-gui/bridge-gui-tester/UserDialog.cpp
@@ -0,0 +1,65 @@
+// Copyright (c) 2022 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 .
+
+
+#include "UserDialog.h"
+
+
+//****************************************************************************************************************************************************
+/// \param[in] user The user.
+/// \param[in] parent The parent widget of the dialog.
+//****************************************************************************************************************************************************
+UserDialog::UserDialog(bridgepp::SPUser &user, QWidget *parent)
+ : QDialog(parent)
+ , user_(user)
+{
+ ui_.setupUi(this);
+
+ connect(ui_.buttonOK, &QPushButton::clicked, this, &UserDialog::onOK);
+ connect(ui_.buttonCancel, &QPushButton::clicked, this, &UserDialog::reject);
+
+ ui_.editUserID->setText(user_->id());
+ ui_.editUsername->setText(user_->username());
+ ui_.editPassword->setText(user->password());
+ ui_.editAddresses->setPlainText(user->addresses().join("\n"));
+ ui_.editAvatarText->setText(user_->avatarText());
+ ui_.checkLoggedIn->setChecked(user_->loggedIn());
+ ui_.checkSplitMode->setChecked(user_->splitMode());
+ ui_.checkSetupGuideSeen->setChecked(user_->setupGuideSeen());
+ ui_.spinUsedBytes->setValue(user->usedBytes());
+ ui_.spinTotalBytes->setValue(user->totalBytes());
+}
+
+
+//****************************************************************************************************************************************************
+//
+//****************************************************************************************************************************************************
+void UserDialog::onOK()
+{
+ user_->setID(ui_.editUserID->text());
+ user_->setUsername(ui_.editUsername->text());
+ user_->setPassword(ui_.editPassword->text());
+ user_->setAddresses(ui_.editAddresses->toPlainText().split(QRegularExpression(R"(\s+)"), Qt::SkipEmptyParts));
+ user_->setAvatarText(ui_.editAvatarText->text());
+ user_->setLoggedIn(ui_.checkLoggedIn->isChecked());
+ user_->setSplitMode(ui_.checkSplitMode->isChecked());
+ user_->setSetupGuideSeen(ui_.checkSetupGuideSeen->isChecked());
+ user_->setUsedBytes(float(ui_.spinUsedBytes->value()));
+ user_->setTotalBytes(float(ui_.spinTotalBytes->value()));
+
+ this->accept();
+}
diff --git a/internal/frontend/bridge-gui/bridge-gui-tester/UserDialog.h b/internal/frontend/bridge-gui/bridge-gui-tester/UserDialog.h
new file mode 100644
index 00000000..2ec081ca
--- /dev/null
+++ b/internal/frontend/bridge-gui/bridge-gui-tester/UserDialog.h
@@ -0,0 +1,49 @@
+// Copyright (c) 2022 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 .
+
+
+#ifndef BRIDGE_GUI_TESTER_USER_DIALOG_H
+#define BRIDGE_GUI_TESTER_USER_DIALOG_H
+
+
+#include "ui_UserDialog.h"
+#include
+
+//****************************************************************************************************************************************************
+/// \brief User dialog class.
+//****************************************************************************************************************************************************
+class UserDialog : public QDialog
+{
+Q_OBJECT
+public: // member functions.
+ UserDialog(bridgepp::SPUser &user, QWidget *parent); ///< Default constructor.
+ UserDialog(UserDialog const &) = delete; ///< Disabled copy-constructor.
+ UserDialog(UserDialog &&) = delete; ///< Disabled assignment copy-constructor.
+ ~UserDialog() override = default; ///< Destructor.
+ UserDialog &operator=(UserDialog const &) = delete; ///< Disabled assignment operator.
+ UserDialog &operator=(UserDialog &&) = delete; ///< Disabled move assignment operator.
+
+private slots:
+ void onOK(); ///< Slot for the OK button.
+
+private:
+ Ui::UserDialog ui_ {}; ///< The UI for the dialog.
+ bridgepp::SPUser user_; ///< The user
+};
+
+
+#endif //BRIDGE_GUI_TESTER_USER_DIALOG_H
diff --git a/internal/frontend/bridge-gui/bridge-gui-tester/UserDialog.ui b/internal/frontend/bridge-gui/bridge-gui-tester/UserDialog.ui
new file mode 100644
index 00000000..138ab000
--- /dev/null
+++ b/internal/frontend/bridge-gui/bridge-gui-tester/UserDialog.ui
@@ -0,0 +1,220 @@
+
+
+ UserDialog
+
+
+
+ 0
+ 0
+ 521
+ 432
+
+
+
+ User
+
+
+ -
+
+
-
+
+
+ Avatar Text
+
+
+
+ -
+
+
+ Password
+
+
+
+ -
+
+
+ Account Name
+
+
+
+ -
+
+
+ UserID
+
+
+
+ -
+
+
+ Used Bytes
+
+
+
+ -
+
+
+ Setup Guide Seen
+
+
+
+ -
+
+
+ -
+
+
+ Total Bytes
+
+
+
+ -
+
+
+ -
+
+
+ Adresses
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop
+
+
+
+ -
+
+
+
+ 0
+ 100
+
+
+
+
+ 0
+ 150
+
+
+
+ true
+
+
+
+ -
+
+
+ QAbstractSpinBox::NoButtons
+
+
+ 0
+
+
+ 1000000000000000.000000000000000
+
+
+
+ -
+
+
+ true
+
+
+
+ -
+
+
+ Logged in
+
+
+
+ -
+
+
+ -
+
+
+ QAbstractSpinBox::NoButtons
+
+
+ 0
+
+
+ 1000000000000000.000000000000000
+
+
+
+ -
+
+
+ Split Mode
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+ -
+
+
-
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 1
+
+
+
+
+ -
+
+
+ &Cancel
+
+
+
+ -
+
+
+ &OK
+
+
+ true
+
+
+
+
+
+
+
+
+ editUserID
+ editUsername
+ editPassword
+ editAddresses
+ editAvatarText
+ spinUsedBytes
+ spinTotalBytes
+ checkLoggedIn
+ checkSplitMode
+ checkSetupGuideSeen
+ buttonOK
+ buttonCancel
+
+
+
+
diff --git a/internal/frontend/bridge-gui/bridge-gui-tester/UserTable.cpp b/internal/frontend/bridge-gui/bridge-gui-tester/UserTable.cpp
new file mode 100644
index 00000000..1411bc2e
--- /dev/null
+++ b/internal/frontend/bridge-gui/bridge-gui-tester/UserTable.cpp
@@ -0,0 +1,207 @@
+// Copyright (c) 2022 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 .
+
+
+#include "UserTable.h"
+
+
+using namespace bridgepp;
+
+
+//****************************************************************************************************************************************************
+/// \param[in] parent The parent object of the class
+//****************************************************************************************************************************************************
+UserTable::UserTable(QObject *parent)
+ : QAbstractTableModel(parent)
+{
+
+}
+
+
+//****************************************************************************************************************************************************
+/// \return The number of rows in the table.
+//****************************************************************************************************************************************************
+int UserTable::rowCount(QModelIndex const &) const
+{
+ return users_.size();
+}
+
+
+//****************************************************************************************************************************************************
+/// \return The number of columns in the table.
+//****************************************************************************************************************************************************
+int UserTable::columnCount(QModelIndex const &) const
+{
+ return 3;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in]
+//****************************************************************************************************************************************************
+QVariant UserTable::data(QModelIndex const &index, int role) const
+{
+ int const row = index.row();
+ if ((row < 0) || (row >= users_.size()) || (Qt::DisplayRole != role))
+ return QVariant();
+
+ SPUser const user = users_[row];
+ if (!user)
+ return QVariant();
+
+ switch (index.column())
+ {
+ case 0:
+ return user->property("username");
+ case 1:
+ return user->property("addresses").toStringList().join(" ");
+ case 2:
+ return user->property("id");
+ default:
+ return QVariant();
+ }
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] section The section (column).
+/// \param[in] orientation The orientation.
+/// \param[in] role The role to retrieve data
+//****************************************************************************************************************************************************
+QVariant UserTable::headerData(int section, Qt::Orientation orientation, int role) const
+{
+ if (Qt::DisplayRole != role)
+ return QAbstractTableModel::headerData(section, orientation, role);
+
+ if (Qt::Horizontal != orientation)
+ return QString();
+
+ switch (section)
+ {
+ case 0:
+ return "UserName";
+ case 1:
+ return "Addresses";
+ case 2:
+ return "UserID";
+ default:
+ return QString();
+ }
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] user The user to add.
+//****************************************************************************************************************************************************
+void UserTable::append(SPUser const &user)
+{
+ qint32 const count = users_.size();
+ this->beginInsertRows(QModelIndex(), count, count);
+ users_.append(user);
+ this->endInsertRows();
+}
+
+
+//****************************************************************************************************************************************************
+/// \return The number of users in the table.
+//****************************************************************************************************************************************************
+qint32 UserTable::userCount() const
+{
+ return users_.count();
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] index The index of the user in the list.
+/// \return the user at the given index.
+/// \return null if the index is out of bounds.
+//****************************************************************************************************************************************************
+bridgepp::SPUser UserTable::userAtIndex(qint32 index)
+{
+ return isIndexValid(index) ? users_[index] : nullptr;
+}
+
+
+//****************************************************************************************************************************************************
+/// \return The user with the given userID.
+/// \return A null pointer if the user is not in the list.
+//****************************************************************************************************************************************************
+bridgepp::SPUser UserTable::userWithID(QString const &userID)
+{
+ QList::const_iterator it = std::find_if(users_.constBegin(), users_.constEnd(), [&userID](SPUser const& user) -> bool {
+ return user->id() == userID;
+ });
+
+ return it == users_.end() ? nullptr : *it;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] userID The userID.
+/// \return the index of the user.
+/// \return -1 if the user could not be found.
+//****************************************************************************************************************************************************
+qint32 UserTable::indexOfUser(QString const &userID)
+{
+ QList::const_iterator it = std::find_if(users_.constBegin(), users_.constEnd(), [&userID](SPUser const& user) -> bool {
+ return user->id() == userID;
+ });
+
+ return it == users_.end() ? -1 : it - users_.constBegin();
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] index The index of the user in the list.
+//****************************************************************************************************************************************************
+void UserTable::touch(qint32 index)
+{
+ if (isIndexValid(index))
+ emit dataChanged(this->index(index, 0), this->index(index, this->columnCount(QModelIndex()) - 1));
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] index The index of the user in the list.
+//****************************************************************************************************************************************************
+void UserTable::remove(qint32 index)
+{
+ if (!isIndexValid(index))
+ return;
+
+ this->beginRemoveRows(QModelIndex(), index, index);
+ users_.removeAt(index);
+ this->endRemoveRows();
+}
+
+
+//****************************************************************************************************************************************************
+/// \return true iff the index is valid.
+//****************************************************************************************************************************************************
+bool UserTable::isIndexValid(qint32 index) const
+{
+ return (index >= 0) && (index < users_.count());
+}
+
+
+//****************************************************************************************************************************************************
+/// \return The user list.
+//****************************************************************************************************************************************************
+QList UserTable::users() const
+{
+ return users_;
+}
diff --git a/internal/frontend/bridge-gui/bridge-gui-tester/UserTable.h b/internal/frontend/bridge-gui/bridge-gui-tester/UserTable.h
new file mode 100644
index 00000000..4705a789
--- /dev/null
+++ b/internal/frontend/bridge-gui/bridge-gui-tester/UserTable.h
@@ -0,0 +1,61 @@
+// Copyright (c) 2022 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 .
+
+
+
+#ifndef BRIDGE_GUI_TESTER_USER_TABLE_H
+#define BRIDGE_GUI_TESTER_USER_TABLE_H
+
+
+#include
+
+//****************************************************************************************************************************************************
+/// \brief User table model class
+//****************************************************************************************************************************************************
+
+class UserTable : public QAbstractTableModel
+{
+Q_OBJECT
+public: // member functions.
+ explicit UserTable(QObject *parent); ///< Default constructor.
+ UserTable(UserTable const &) = delete; ///< Disabled copy-constructor.
+ UserTable(UserTable &&) = delete; ///< Disabled assignment copy-constructor.
+ ~UserTable() = default; ///< Destructor.
+ UserTable &operator=(UserTable const &) = delete; ///< Disabled assignment operator.
+ UserTable &operator=(UserTable &&) = delete; ///< Disabled move assignment operator.
+ qint32 userCount() const; ///< Return the number of users in the table.
+ void append(bridgepp::SPUser const& user); ///< Append a user.
+ bridgepp::SPUser userAtIndex(qint32 index); ///< Return the user at the given index.
+ bridgepp::SPUser userWithID(QString const &userID); ///< Return the user with a given id.
+ qint32 indexOfUser(QString const& userID); ///< Return the index of a given User.
+ void touch(qint32 index); ///< touch the user at a given index (indicates it has been modified).
+ void remove(qint32 index); ///< Remove the user at a given index.
+ QList users() const; ///< Return a copy of the user list.
+
+private: // data members.
+ int rowCount(QModelIndex const &parent) const override; ///< Get the number of rows in the table.
+ int columnCount(QModelIndex const &parent) const override; ///< Get the number of columns in the table.
+ QVariant data(QModelIndex const &index, int role) const override; ///< Get the data for a role at a given index.
+ QVariant headerData(int section, Qt::Orientation orientation, int role) const override; ///< Get header data.
+ bool isIndexValid(qint32 index) const; ///< return true iff the index is valid.
+
+public:
+ QList users_;
+};
+
+
+#endif //BRIDGE_GUI_TESTER_USER_TABLE_H
diff --git a/internal/frontend/bridge-gui/bridge-gui-tester/main.cpp b/internal/frontend/bridge-gui/bridge-gui-tester/main.cpp
new file mode 100644
index 00000000..0801bfb3
--- /dev/null
+++ b/internal/frontend/bridge-gui/bridge-gui-tester/main.cpp
@@ -0,0 +1,98 @@
+// Copyright (c) 2022 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 .
+
+
+#include "MainWindow.h"
+#include "AppController.h"
+#include "GRPCServerWorker.h"
+#include
+#include
+#include
+
+
+#ifndef BRIDGE_APP_VERSION
+#error "BRIDGE_APP_VERSION is not defined"
+#endif
+
+
+namespace
+{
+
+
+QString const applicationName = "Proton Mail Bridge GUI Tester"; ///< The name of the application.
+
+
+}
+
+
+using namespace bridgepp;
+
+
+//****************************************************************************************************************************************************
+/// \param[in] argc The number of command-line arguments.
+/// \param[in] argv The list of command-line arguments.
+/// \return The exit code for the application.
+//****************************************************************************************************************************************************
+int main(int argc, char **argv)
+{
+
+ try
+ {
+ QApplication a(argc, argv);
+ QApplication::setApplicationName(applicationName);
+ QApplication::setOrganizationName("Proton AG");
+ QApplication::setOrganizationDomain("proton.ch");
+ QApplication::setQuitOnLastWindowClosed(true);
+
+ app().log().setEchoInConsole(true);
+ app().log().info(QString("%1 started.").arg(applicationName));
+
+ MainWindow window(nullptr);
+ app().setMainWindow(&window);
+ window.setWindowTitle(QApplication::applicationName());
+ window.show();
+
+ GRPCServerWorker *serverWorker = new GRPCServerWorker(nullptr);
+ QObject::connect(serverWorker, &Worker::started, []() { app().log().info("Server worker started."); });
+ QObject::connect(serverWorker, &Worker::finished, []() { app().log().info("Server worker finished."); });
+ QObject::connect(serverWorker, &Worker::error, [](QString const &message) {
+ throw Exception(QString("gRPC Server encountered an error: %1").arg(message));
+ });
+ UPOverseer overseer = std::make_unique(serverWorker, nullptr);
+ overseer->startWorker(true);
+
+ qint32 const exitCode = QApplication::exec();
+
+ serverWorker->stop();
+ while (!overseer->isFinished())
+ {
+ QThread::msleep(10);
+ }
+
+ app().log().info(QString("%1 exiting with code %2.").arg(applicationName).arg(exitCode));
+
+ return exitCode;
+
+ }
+ catch (Exception const &e)
+ {
+ QTextStream(stderr) << QString("A fatal error occurred: %1\n").arg(e.qwhat());
+ return EXIT_FAILURE;
+ }
+}
+
+
diff --git a/internal/frontend/bridge-gui/bridge-gui/QMLBackend.cpp b/internal/frontend/bridge-gui/bridge-gui/QMLBackend.cpp
index 34e59a12..dde3a11d 100644
--- a/internal/frontend/bridge-gui/bridge-gui/QMLBackend.cpp
+++ b/internal/frontend/bridge-gui/bridge-gui/QMLBackend.cpp
@@ -108,8 +108,10 @@ void QMLBackend::connectGrpcEvents()
connect(client, &GRPCClient::login2PasswordError, this, &QMLBackend::login2PasswordError);
connect(client, &GRPCClient::login2PasswordErrorAbort, this, &QMLBackend::login2PasswordErrorAbort);
connect(client, &GRPCClient::loginFinished, this, [&](QString const &userID) {
+ this->retrieveUserList();
qint32 const index = users_->rowOfUserID(userID); emit loginFinished(index); });
connect(client, &GRPCClient::loginAlreadyLoggedIn, this, [&](QString const &userID) {
+ this->retrieveUserList();
qint32 const index = users_->rowOfUserID(userID); emit loginAlreadyLoggedIn(index); });
// update events
diff --git a/internal/frontend/bridge-gui/bridgepp/CMakeLists.txt b/internal/frontend/bridge-gui/bridgepp/CMakeLists.txt
index 951f0549..8ab7fb51 100644
--- a/internal/frontend/bridge-gui/bridgepp/CMakeLists.txt
+++ b/internal/frontend/bridge-gui/bridgepp/CMakeLists.txt
@@ -104,6 +104,7 @@ add_library(bridgepp
bridgepp/BridgeUtils.cpp bridgepp/BridgeUtils.h
bridgepp/Exception/Exception.h bridgepp/Exception/Exception.cpp
bridgepp/GRPC/GRPCClient.cpp bridgepp/GRPC/GRPCClient.h
+ bridgepp/GRPC/EventFactory.cpp bridgepp/GRPC/EventFactory.h
bridgepp/GRPC/GRPCUtils.cpp bridgepp/GRPC/GRPCUtils.h
${PROTO_CPP_FILE} ${PROTO_H_FILE} ${GRPC_CPP_FILE} ${GRPC_H_FILE}
bridgepp/Log/Log.h bridgepp/Log/Log.cpp
diff --git a/internal/frontend/bridge-gui/bridgepp/bridgepp/BridgeUtils.cpp b/internal/frontend/bridge-gui/bridgepp/bridgepp/BridgeUtils.cpp
index 1631cbe4..5620f98f 100644
--- a/internal/frontend/bridge-gui/bridgepp/bridgepp/BridgeUtils.cpp
+++ b/internal/frontend/bridge-gui/bridgepp/bridgepp/BridgeUtils.cpp
@@ -18,19 +18,55 @@
#include "BridgeUtils.h"
#include "Exception/Exception.h"
+#include
namespace bridgepp
{
+
namespace {
+
QString const configFolder = "protonmail/bridge";
+QMutex rngMutex; ///< the mutex to use when accessing the rng.
+
+QStringList const firstNames {
+ "James", "John", "Robert", "Michael", "William", "David", "Richard", "Charles", "Joseph", "Thomas", "Christopher", "Daniel", "Paul", "Mark",
+ "Donald", "George", "Kenneth", "Steven", "Edward", "Brian", "Ronald", "Anthony", "Kevin", "Jason", "Matthew", "Gary", "Timothy", "Jose", "Larry",
+ "Jeffrey", "Frank", "Scott", "Eric", "Stephen", "Andrew", "Raymond", "Jack", "Gregory", "Joshua", "Jerry", "Dennis", "Walter", "Patrick", "Peter",
+ "Harold", "Douglas", "Henry", "Carl", "Arthur", "Ryan", "Mary", "Patricia", "Barbara", "Linda", "Elizabeth", "Maria", "Jennifer", "Susan",
+ "Margaret", "Dorothy", "Lisa", "Nancy", "Karen", "Betty", "Helen", "Sandra", "Donna", "Ruth", "Sharon", "Michelle", "Laura", "Sarah", "Kimberly",
+ "Deborah", "Jessica", "Shirley", "Cynthia", "Angela", "Emily", "Brenda", "Amy", "Anna", "Rebecca", "Virginia", "Kathleen", "Pamela", "Martha",
+ "Debra", "Amanda", "Stephanie", "Caroline", "Christine", "Marie", "Janet", "Catherine", "Frances", "Ann", "Joyce", "Diane", "Alice",
+}; ///< List of common US first names. (source: census.gov)
+QStringList const lastNames {
+ "Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Rodriguez", "Martinez", "Hernandez", "Lopez", "Gonzalez",
+ "Wilson", "Anderson", "Thomas", "Taylor", "Moore", "Jackson", "Martin", "Lee", "Perez", "Thompson", "White", "Harris", "Sanchez", "Clark",
+ "Ramirez", "Lewis", "Robinson", "Walker", "Young", "Allen", "King", "Wright", "Scott", "Torres", "Nguyen", "Hill", "Flores", "Green", "Adams",
+ "Nelson", "Baker", "Hall", "Rivera", "Campbell", "Mitchell", "Carter", "Roberts", "Gomez", "Phillips", "Evans", "Turner", "Diaz", "Parker",
+ "Cruz", "Edwards", "Collins", "Reyes", "Stewart", "Morris", "Morales", "Murphy", "Cook", "Rogers", "Gutierrez", "Ortiz", "Morgan", "Cooper",
+ "Peterson", "Bailey", "Reed", "Kelly", "Howard", "Ramos", "Kim", "Cox", "Ward", "Richardson", "Watson", "Brooks", "Chavez", "Wood", "James",
+ "Bennett", "Gray", "Mendoza", "Ruiz", "Hughes", "Price", "Alvarez", "Castillo", "Sanders", "Patel", "Myers", "Long", "Ross", "Foster", "Jimenez"
+}; ///< List of common US last names (source: census.gov)
+
+
+//****************************************************************************************************************************************************
+/// \brief Return the 64 bit Mersenne twister random number generator.
+//****************************************************************************************************************************************************
+std::mt19937_64& rng()
+{
+ // Do not use for crypto. std::random_device is not good enough.
+ static std::mt19937_64 generator = std::mt19937_64(std::random_device()());
+ return generator;
}
+} // anonymous namespace
+
+
//****************************************************************************************************************************************************
/// \return user configuration directory used by bridge (based on Golang OS/File's UserConfigDir).
//****************************************************************************************************************************************************
@@ -93,4 +129,73 @@ QString userCacheDir()
}
+//****************************************************************************************************************************************************
+/// \return The value GOOS would return for the current platform.
+//****************************************************************************************************************************************************
+QString goos()
+{
+#if defined(Q_OS_DARWIN)
+ return "darwin";
+#elif defined(Q_OS_WINDOWS)
+ return "windows";
+#else
+ return "linux";
+#endif
+}
+
+
+//****************************************************************************************************************************************************
+/// Slow, but not biased. Not for use in crypto functions though, as the RNG use std::random_device as a seed.
+///
+/// \return a random number in the range [0, n-1]
+//****************************************************************************************************************************************************
+qint64 randN(qint64 n)
+{
+ QMutexLocker locker(&rngMutex);
+ return (n > 0) ? std::uniform_int_distribution(0, n - 1)(rng()) : 0;
+}
+
+
+//****************************************************************************************************************************************************
+/// \return A random first name.
+//****************************************************************************************************************************************************
+QString randomFirstName()
+{
+ return firstNames[randN(firstNames.size())];
+}
+
+
+//****************************************************************************************************************************************************
+/// \return A random last name.
+//****************************************************************************************************************************************************
+QString randomLastName()
+{
+ return lastNames[randN(lastNames.size())];
+}
+
+
+//****************************************************************************************************************************************************
+//
+//****************************************************************************************************************************************************
+SPUser randomUser()
+{
+ SPUser user = User::newUser(nullptr);
+ user->setID(QUuid::createUuid().toString());
+ QString const firstName = randomFirstName();
+ QString const lastName = randomLastName();
+ QString const username = QString("%1.%2").arg(firstName.toLower(), lastName.toLower());
+ user->setUsername(username);
+ user->setAddresses(QStringList() << (username + "@proton.me") << (username + "@protonmail.com") );
+ user->setPassword(QUuid().createUuid().toString(QUuid::StringFormat::WithoutBraces).left(20));
+ user->setAvatarText(firstName.left(1) + lastName.left(1));
+ user->setLoggedIn(true);
+ user->setSplitMode(false);
+ user->setSetupGuideSeen(true);
+ qint64 const totalBytes = (500 + randN(2501)) * 1000000;
+ user->setUsedBytes(float(bridgepp::randN(totalBytes + 1)) * 1.05f); // we maybe slightly over quota
+ user->setTotalBytes(float(totalBytes));
+ return user;
+}
+
+
} // namespace bridgepp
diff --git a/internal/frontend/bridge-gui/bridgepp/bridgepp/BridgeUtils.h b/internal/frontend/bridge-gui/bridgepp/bridgepp/BridgeUtils.h
index 43f9f9e3..8dc05854 100644
--- a/internal/frontend/bridge-gui/bridgepp/bridgepp/BridgeUtils.h
+++ b/internal/frontend/bridge-gui/bridgepp/bridgepp/BridgeUtils.h
@@ -20,12 +20,19 @@
#define BRIDGE_GUI_TESTER_BRIDGE_UTILS_H
+#include
+
+
namespace bridgepp {
QString userConfigDir(); ///< Get the path of the user configuration folder.
QString userCacheDir(); ///< Get the path of the user cache folder.
-
+QString goos(); ///< return the value of Go's GOOS for the current platform ("darwin", "linux" and "windows" are supported).
+qint64 randN(qint64 n); ///< return a random integer in the half open range [0,n)
+QString randomFirstName(); ///< Get a random first name from a pre-determined list.
+QString randomLastName(); ///< Get a random first name from a pre-determined list.
+SPUser randomUser(); ///< Get a random user.
} // namespace
diff --git a/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/EventFactory.cpp b/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/EventFactory.cpp
new file mode 100644
index 00000000..760294d9
--- /dev/null
+++ b/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/EventFactory.cpp
@@ -0,0 +1,620 @@
+// Copyright (c) 2022 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 .
+
+
+#include "EventFactory.h"
+
+
+namespace bridgepp
+{
+
+
+namespace
+{
+
+
+//****************************************************************************************************************************************************
+/// \return a new SPStreamEvent
+//****************************************************************************************************************************************************
+bridgepp::SPStreamEvent newStreamEvent()
+{
+ return std::make_shared();
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] appEvent The app event.
+/// \return The stream event.
+//****************************************************************************************************************************************************
+bridgepp::SPStreamEvent wrapAppEvent(grpc::AppEvent *appEvent)
+{
+ auto event = newStreamEvent();
+ event->set_allocated_app(appEvent);
+ return event;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] loginEvent The login event.
+/// \return The stream event.
+//****************************************************************************************************************************************************
+bridgepp::SPStreamEvent wrapLoginEvent(grpc::LoginEvent *loginEvent)
+{
+ auto event = newStreamEvent();
+ event->set_allocated_login(loginEvent);
+ return event;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] updateEvent The app event.
+/// \return The stream event.
+//****************************************************************************************************************************************************
+bridgepp::SPStreamEvent wrapUpdateEvent(grpc::UpdateEvent *updateEvent)
+{
+ auto event = newStreamEvent();
+ event->set_allocated_update(updateEvent);
+ return event;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] cacheEvent The cache event.
+/// \return The stream event.
+//****************************************************************************************************************************************************
+bridgepp::SPStreamEvent wrapCacheEvent(grpc::CacheEvent *cacheEvent)
+{
+ auto event = newStreamEvent();
+ event->set_allocated_cache(cacheEvent);
+ return event;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] mailSettingsEvent The mail settings event.
+/// \return The stream event.
+//****************************************************************************************************************************************************
+bridgepp::SPStreamEvent wrapMailSettingsEvent(grpc::MailSettingsEvent *mailSettingsEvent)
+{
+ auto event = newStreamEvent();
+ event->set_allocated_mailsettings(mailSettingsEvent);
+ return event;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] keychainEvent The keychain event.
+/// \return The stream event.
+//****************************************************************************************************************************************************
+bridgepp::SPStreamEvent wrapKeychainEvent(grpc::KeychainEvent *keychainEvent)
+{
+ auto event = newStreamEvent();
+ event->set_allocated_keychain(keychainEvent);
+ return event;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] mailEvent The mail event.
+/// \return The stream event.
+//****************************************************************************************************************************************************
+bridgepp::SPStreamEvent wrapMailEvent(grpc::MailEvent *mailEvent)
+{
+ auto event = newStreamEvent();
+ event->set_allocated_mail(mailEvent);
+ return event;
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] userEvent The user event.
+/// \return The stream event.
+//****************************************************************************************************************************************************
+bridgepp::SPStreamEvent wrapUserEvent(grpc::UserEvent *userEvent)
+{
+ auto event = newStreamEvent();
+ event->set_allocated_user(userEvent);
+ return event;
+}
+
+
+} // namespace
+
+//****************************************************************************************************************************************************
+/// \param[in] connected The internet status.
+/// \return The event.
+//****************************************************************************************************************************************************
+SPStreamEvent newInternetStatusEvent(bool connected)
+{
+ auto *internetStatusEvent = new grpc::InternetStatusEvent();
+ internetStatusEvent->set_connected(connected);
+ auto appEvent = new grpc::AppEvent;
+ appEvent->set_allocated_internetstatus(internetStatusEvent);
+ return wrapAppEvent(appEvent);
+}
+
+
+//****************************************************************************************************************************************************
+/// \return The event.
+//****************************************************************************************************************************************************
+SPStreamEvent newToggleAutostartFinishedEvent()
+{
+ auto *event = new grpc::ToggleAutostartFinishedEvent;
+ auto appEvent = new grpc::AppEvent;
+ appEvent->set_allocated_toggleautostartfinished(event);
+ return wrapAppEvent(appEvent);
+}
+
+
+//****************************************************************************************************************************************************
+/// \return The event.
+//****************************************************************************************************************************************************
+SPStreamEvent newResetFinishedEvent()
+{
+ auto event = new grpc::ResetFinishedEvent;
+ auto appEvent = new grpc::AppEvent;
+ appEvent->set_allocated_resetfinished(event);
+ return wrapAppEvent(appEvent);
+}
+
+
+//****************************************************************************************************************************************************
+/// \return The event.
+//****************************************************************************************************************************************************
+SPStreamEvent newReportBugFinishedEvent()
+{
+ auto event = new grpc::ReportBugFinishedEvent;
+ auto appEvent = new grpc::AppEvent;
+ appEvent->set_allocated_reportbugfinished(event);
+ return wrapAppEvent(appEvent);
+}
+
+
+//****************************************************************************************************************************************************
+/// \return The event.
+//****************************************************************************************************************************************************
+SPStreamEvent newReportBugSuccessEvent()
+{
+ auto event = new grpc::ReportBugSuccessEvent;
+ auto appEvent = new grpc::AppEvent;
+ appEvent->set_allocated_reportbugsuccess(event);
+ return wrapAppEvent(appEvent);
+}
+
+
+//****************************************************************************************************************************************************
+/// \return The event.
+//****************************************************************************************************************************************************
+SPStreamEvent newReportBugErrorEvent()
+{
+ auto event = new grpc::ReportBugErrorEvent;
+ auto appEvent = new grpc::AppEvent;
+ appEvent->set_allocated_reportbugerror(event);
+ return wrapAppEvent(appEvent);
+}
+
+
+//****************************************************************************************************************************************************
+/// \return The event.
+//****************************************************************************************************************************************************
+SPStreamEvent newShowMainWindowEvent()
+{
+ auto event = new grpc::ShowMainWindowEvent;
+ auto appEvent = new grpc::AppEvent;
+ appEvent->set_allocated_showmainwindow(event);
+ return wrapAppEvent(appEvent);
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] error The error.
+/// \param[in] message The message.
+/// \return The event.
+//****************************************************************************************************************************************************
+SPStreamEvent newLoginError(grpc::LoginErrorType error, QString const &message)
+{
+ auto event = new ::grpc::LoginErrorEvent;
+ event->set_type(error);
+ event->set_message(message.toStdString());
+ auto loginEvent = new grpc::LoginEvent;
+ loginEvent->set_allocated_error(event);
+ return wrapLoginEvent(loginEvent);
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] username The username.
+/// \return The event.
+//****************************************************************************************************************************************************
+SPStreamEvent newLoginTfaRequestedEvent(QString const &username)
+{
+ auto event = new ::grpc::LoginTfaRequestedEvent;
+ event->set_username(username.toStdString());
+ auto loginEvent = new grpc::LoginEvent;
+ loginEvent->set_allocated_tfarequested(event);
+ return wrapLoginEvent(loginEvent);
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] username The username.
+/// \return The event.
+//****************************************************************************************************************************************************
+SPStreamEvent newLoginTwoPasswordsRequestedEvent()
+{
+ auto event = new ::grpc::LoginTwoPasswordsRequestedEvent;
+ auto loginEvent = new grpc::LoginEvent;
+ loginEvent->set_allocated_twopasswordrequested(event);
+ return wrapLoginEvent(loginEvent);
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] userID The userID.
+/// \return The event.
+//****************************************************************************************************************************************************
+SPStreamEvent newLoginFinishedEvent(QString const &userID)
+{
+ auto event = new ::grpc::LoginFinishedEvent;
+ event->set_userid(userID.toStdString());
+ auto loginEvent = new grpc::LoginEvent;
+ loginEvent->set_allocated_finished(event);
+ return wrapLoginEvent(loginEvent);
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] userID The userID.
+/// \return The event.
+//****************************************************************************************************************************************************
+SPStreamEvent newLoginAlreadyLoggedInEvent(QString const &userID)
+{
+ auto event = new ::grpc::LoginFinishedEvent;
+ event->set_userid(userID.toStdString());
+ auto loginEvent = new grpc::LoginEvent;
+ loginEvent->set_allocated_alreadyloggedin(event);
+ return wrapLoginEvent(loginEvent);
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] errorType The error type.
+/// \return The event.
+//****************************************************************************************************************************************************
+SPStreamEvent newUpdateErrorEvent(grpc::UpdateErrorType errorType)
+{
+ auto event = new grpc::UpdateErrorEvent;
+ event->set_type(errorType);
+ auto updateEvent = new grpc::UpdateEvent;
+ updateEvent->set_allocated_error(event);
+ return wrapUpdateEvent(updateEvent);
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] version The version.
+/// \return The event.
+//****************************************************************************************************************************************************
+SPStreamEvent newUpdateManualReadyEvent(QString const &version)
+{
+ auto event = new grpc::UpdateManualReadyEvent;
+ event->set_version(version.toStdString());
+ auto updateEvent = new grpc::UpdateEvent;
+ updateEvent->set_allocated_manualready(event);
+ return wrapUpdateEvent(updateEvent);
+}
+
+
+//****************************************************************************************************************************************************
+/// \return the event.
+//****************************************************************************************************************************************************
+SPStreamEvent newUpdateManualRestartNeededEvent()
+{
+ auto event = new grpc::UpdateManualRestartNeededEvent;
+ auto updateEvent = new grpc::UpdateEvent;
+ updateEvent->set_allocated_manualrestartneeded(event);
+ return wrapUpdateEvent(updateEvent);
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] version The version.
+/// \return The event.
+//****************************************************************************************************************************************************
+SPStreamEvent newUpdateForceEvent(QString const &version)
+{
+ auto event = new grpc::UpdateForceEvent;
+ event->set_version(version.toStdString());
+ auto updateEvent = new grpc::UpdateEvent;
+ updateEvent->set_allocated_force(event);
+ return wrapUpdateEvent(updateEvent);
+}
+
+
+//****************************************************************************************************************************************************
+/// \return the event.
+//****************************************************************************************************************************************************
+SPStreamEvent newUpdateSilentRestartNeeded()
+{
+ auto event = new grpc::UpdateSilentRestartNeeded;
+ auto updateEvent = new grpc::UpdateEvent;
+ updateEvent->set_allocated_silentrestartneeded(event);
+ return wrapUpdateEvent(updateEvent);
+}
+
+
+//****************************************************************************************************************************************************
+/// \return The event.
+//****************************************************************************************************************************************************
+SPStreamEvent newUpdateIsLatestVersion()
+{
+ auto event = new grpc::UpdateIsLatestVersion;
+ auto updateEvent = new grpc::UpdateEvent;
+ updateEvent->set_allocated_islatestversion(event);
+ return wrapUpdateEvent(updateEvent);
+}
+
+
+//****************************************************************************************************************************************************
+/// \return The event.
+//****************************************************************************************************************************************************
+SPStreamEvent newUpdateCheckFinished()
+{
+ auto event = new grpc::UpdateCheckFinished;
+ auto updateEvent = new grpc::UpdateEvent;
+ updateEvent->set_allocated_checkfinished(event);
+ return wrapUpdateEvent(updateEvent);
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] errorType The error type.
+/// \return The event.
+//****************************************************************************************************************************************************
+SPStreamEvent newCacheErrorEvent(grpc::CacheErrorType errorType)
+{
+ auto event = new grpc::CacheErrorEvent;
+ event->set_type(errorType);
+ auto cacheEvent = new grpc::CacheEvent;
+ cacheEvent->set_allocated_error(event);
+ return wrapCacheEvent(cacheEvent);
+}
+
+
+//****************************************************************************************************************************************************
+/// \return The event.
+//****************************************************************************************************************************************************
+SPStreamEvent newCacheLocationChangeSuccessEvent()
+{
+ auto event = new grpc::CacheLocationChangeSuccessEvent;
+ auto cacheEvent = new grpc::CacheEvent;
+ cacheEvent->set_allocated_locationchangedsuccess(event);
+ return wrapCacheEvent(cacheEvent);
+}
+
+
+//****************************************************************************************************************************************************
+/// \return The event.
+//****************************************************************************************************************************************************
+SPStreamEvent newChangeLocalCacheFinishedEvent()
+{
+ auto event = new grpc::ChangeLocalCacheFinishedEvent;
+ auto cacheEvent = new grpc::CacheEvent;
+ cacheEvent->set_allocated_changelocalcachefinished(event);
+ return wrapCacheEvent(cacheEvent);
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] enabled The new state of the cache.
+/// \return The event.
+//****************************************************************************************************************************************************
+SPStreamEvent newIsCacheOnDiskEnabledChanged(bool enabled)
+{
+ auto event = new grpc::IsCacheOnDiskEnabledChanged;
+ event->set_enabled(enabled);
+ auto cacheEvent = new grpc::CacheEvent;
+ cacheEvent->set_allocated_iscacheondiskenabledchanged(event);
+ return wrapCacheEvent(cacheEvent);
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] path The path of the cache.
+/// \return The event.
+//****************************************************************************************************************************************************
+SPStreamEvent newDiskCachePathChanged(QString const &path)
+{
+ auto event = new grpc::DiskCachePathChanged;
+ event->set_path(path.toStdString());
+ auto cacheEvent = new grpc::CacheEvent;
+ cacheEvent->set_allocated_diskcachepathchanged(event);
+ return wrapCacheEvent(cacheEvent);
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] errorType The error type.
+/// \return The event.
+//****************************************************************************************************************************************************
+SPStreamEvent newMailSettingsErrorEvent(grpc::MailSettingsErrorType errorType)
+{
+ auto event = new grpc::MailSettingsErrorEvent;
+ event->set_type(errorType);
+ auto mailSettingsEvent = new grpc::MailSettingsEvent;
+ mailSettingsEvent->set_allocated_error(event);
+ return wrapMailSettingsEvent(mailSettingsEvent);
+}
+
+
+//****************************************************************************************************************************************************
+/// \return The event.
+//****************************************************************************************************************************************************
+SPStreamEvent newUseSslForSmtpFinishedEvent()
+{
+ auto event = new grpc::UseSslForSmtpFinishedEvent;
+ auto mailSettingsEvent = new grpc::MailSettingsEvent;
+ mailSettingsEvent->set_allocated_usesslforsmtpfinished(event);
+ return wrapMailSettingsEvent(mailSettingsEvent);
+}
+
+
+//****************************************************************************************************************************************************
+/// \return The event.
+//****************************************************************************************************************************************************
+SPStreamEvent newChangePortsFinishedEvent()
+{
+ auto event = new grpc::ChangePortsFinishedEvent;
+ auto mailSettingsEvent = new grpc::MailSettingsEvent;
+ mailSettingsEvent->set_allocated_changeportsfinished(event);
+ return wrapMailSettingsEvent(mailSettingsEvent);
+}
+
+
+//****************************************************************************************************************************************************
+/// \return The event.
+//****************************************************************************************************************************************************
+SPStreamEvent newChangeKeychainFinishedEvent()
+{
+ auto event = new grpc::ChangeKeychainFinishedEvent;
+ auto keychainEvent = new grpc::KeychainEvent;
+ keychainEvent->set_allocated_changekeychainfinished(event);
+ return wrapKeychainEvent(keychainEvent);
+}
+
+
+//****************************************************************************************************************************************************
+/// \return The event.
+//****************************************************************************************************************************************************
+SPStreamEvent newHasNoKeychainEvent()
+{
+ auto event = new grpc::HasNoKeychainEvent;
+ auto keychainEvent = new grpc::KeychainEvent;
+ keychainEvent->set_allocated_hasnokeychain(event);
+ return wrapKeychainEvent(keychainEvent);
+}
+
+
+//****************************************************************************************************************************************************
+/// \return The event.
+//****************************************************************************************************************************************************
+SPStreamEvent newRebuildKeychainEvent()
+{
+ auto event = new grpc::RebuildKeychainEvent;
+ auto keychainEvent = new grpc::KeychainEvent;
+ keychainEvent->set_allocated_rebuildkeychain(event);
+ return wrapKeychainEvent(keychainEvent);
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] email The email.
+/// \return The event.
+//****************************************************************************************************************************************************
+SPStreamEvent newNoActiveKeyForRecipientEvent(QString const &email)
+{
+ auto event = new grpc::NoActiveKeyForRecipientEvent;
+ event->set_email(email.toStdString());
+ auto mailEvent = new grpc::MailEvent;
+ mailEvent->set_allocated_noactivekeyforrecipientevent(event);
+ return wrapMailEvent(mailEvent);
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] address The address.
+/// /// \return The event.
+//****************************************************************************************************************************************************
+SPStreamEvent newAddressChangedEvent(QString const &address)
+{
+ auto event = new grpc::AddressChangedEvent;
+ event->set_address(address.toStdString());
+ auto mailEvent = new grpc::MailEvent;
+ mailEvent->set_allocated_addresschanged(event);
+ return wrapMailEvent(mailEvent);
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] address The address.
+/// \return The event.
+//****************************************************************************************************************************************************
+SPStreamEvent newAddressChangedLogoutEvent(QString const &address)
+{
+ auto event = new grpc::AddressChangedLogoutEvent;
+ event->set_address(address.toStdString());
+ auto mailEvent = new grpc::MailEvent;
+ mailEvent->set_allocated_addresschangedlogout(event);
+ return wrapMailEvent(mailEvent);
+}
+
+
+//****************************************************************************************************************************************************
+/// \return The event.
+//****************************************************************************************************************************************************
+SPStreamEvent newApiCertIssueEvent()
+{
+ auto event = new grpc::ApiCertIssueEvent;
+ auto mailEvent = new grpc::MailEvent;
+ mailEvent->set_allocated_apicertissue(event);
+ return wrapMailEvent(mailEvent);
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] userID The userID.
+/// \return The event.
+//****************************************************************************************************************************************************
+SPStreamEvent newToggleSplitModeFinishedEvent(QString const &userID)
+{
+ auto event = new grpc::ToggleSplitModeFinishedEvent;
+ event->set_userid(userID.toStdString());
+ auto userEvent = new grpc::UserEvent;
+ userEvent->set_allocated_togglesplitmodefinished(event);
+ return wrapUserEvent(userEvent);
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] username The username.
+/// /// \return The event.
+//****************************************************************************************************************************************************
+SPStreamEvent newUserDisconnectedEvent(QString const &username)
+{
+ auto event = new grpc::UserDisconnectedEvent;
+ event->set_username(username.toStdString());
+ auto userEvent = new grpc::UserEvent;
+ userEvent->set_allocated_userdisconnected(event);
+ return wrapUserEvent(userEvent);
+}
+
+
+//****************************************************************************************************************************************************
+/// \param[in] userID The userID.
+/// \return The event.
+//****************************************************************************************************************************************************
+SPStreamEvent newUserChangedEvent(QString const &userID)
+{
+ auto event = new grpc::UserChangedEvent;
+ event->set_userid(userID.toStdString());
+ auto userEvent = new grpc::UserEvent;
+ userEvent->set_allocated_userchanged(event);
+ return wrapUserEvent(userEvent);
+}
+
+
+} // namespace bridgepp
diff --git a/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/EventFactory.h b/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/EventFactory.h
new file mode 100644
index 00000000..3f28b7ec
--- /dev/null
+++ b/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/EventFactory.h
@@ -0,0 +1,88 @@
+// Copyright (c) 2022 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 .
+
+
+#ifndef BRIDGE_GUI_TESTER_EVENT_FACTORY_H
+#define BRIDGE_GUI_TESTER_EVENT_FACTORY_H
+
+
+#include "bridge.grpc.pb.h"
+#include "GRPCUtils.h"
+
+
+namespace bridgepp
+{
+
+
+// App events
+SPStreamEvent newInternetStatusEvent(bool connected); ///< Create a new InternetStatusEvent event.
+SPStreamEvent newToggleAutostartFinishedEvent(); ///< Create a new ToggleAutostartFinishedEvent event.
+SPStreamEvent newResetFinishedEvent(); ///< Create a new ResetFinishedEvent event.
+SPStreamEvent newReportBugFinishedEvent(); ///< Create a new ReportBugFinishedEvent event.
+SPStreamEvent newReportBugSuccessEvent(); ///< Create a new ReportBugSuccessEvent event.
+SPStreamEvent newReportBugErrorEvent(); ///< Create a new ReportBugErrorEvent event.
+SPStreamEvent newShowMainWindowEvent(); ///< Create a new ShowMainWindowEvent event.
+
+// Login events
+SPStreamEvent newLoginError(grpc::LoginErrorType error, QString const &message); ///< Create a new LoginError event.
+SPStreamEvent newLoginTfaRequestedEvent(QString const &username); ///< Create a new LoginTfaRequestedEvent event.
+SPStreamEvent newLoginTwoPasswordsRequestedEvent(); ///< Create a new LoginTwoPasswordsRequestedEvent event.
+SPStreamEvent newLoginFinishedEvent(QString const &userID); ///< Create a new LoginFinishedEvent event.
+SPStreamEvent newLoginAlreadyLoggedInEvent(QString const &userID); ///< Create a new LoginAlreadyLoggedInEvent event.
+
+// Update related events
+SPStreamEvent newUpdateErrorEvent(grpc::UpdateErrorType errorType); ///< Create a new UpdateErrorEvent event.
+SPStreamEvent newUpdateManualReadyEvent(QString const &version); ///< Create a new UpdateManualReadyEvent event.
+SPStreamEvent newUpdateManualRestartNeededEvent(); ///< Create a new UpdateManualRestartNeededEvent event.
+SPStreamEvent newUpdateForceEvent(QString const &version); ///< Create a new UpdateForceEvent event.
+SPStreamEvent newUpdateSilentRestartNeeded(); ///< Create a new UpdateSilentRestartNeeded event.
+SPStreamEvent newUpdateIsLatestVersion(); ///< Create a new UpdateIsLatestVersion event.
+SPStreamEvent newUpdateCheckFinished(); ///< Create a new UpdateCheckFinished event.
+
+// Cache on disk related events
+SPStreamEvent newCacheErrorEvent(grpc::CacheErrorType errorType); ///< Create a new CacheErrorEvent event.
+SPStreamEvent newCacheLocationChangeSuccessEvent(); ///< Create a new CacheLocationChangeSuccessEvent event.
+SPStreamEvent newChangeLocalCacheFinishedEvent(); ///< Create a new ChangeLocalCacheFinishedEvent event.
+SPStreamEvent newIsCacheOnDiskEnabledChanged(bool enabled); ///< Create a new IsCacheOnDiskEnabledChanged event.
+SPStreamEvent newDiskCachePathChanged(QString const &path); ///< Create a new DiskCachePathChanged event.
+
+// Mail settings related events
+SPStreamEvent newMailSettingsErrorEvent(grpc::MailSettingsErrorType errorType); ///< Create a new MailSettingsErrorEvent event.
+SPStreamEvent newUseSslForSmtpFinishedEvent(); ///< Create a new UseSslForSmtpFinishedEvent event.
+SPStreamEvent newChangePortsFinishedEvent(); ///< Create a new ChangePortsFinishedEvent event.
+
+// keychain related events
+SPStreamEvent newChangeKeychainFinishedEvent(); ///< Create a new ChangeKeychainFinishedEvent event.
+SPStreamEvent newHasNoKeychainEvent(); ///< Create a new HasNoKeychainEvent event.
+SPStreamEvent newRebuildKeychainEvent(); ///< Create a new RebuildKeychainEvent event.
+
+// Mail related events
+SPStreamEvent newNoActiveKeyForRecipientEvent(QString const &email); ///< Create a new NoActiveKeyForRecipientEvent event.
+SPStreamEvent newAddressChangedEvent(QString const &address); ///< Create a new AddressChangedEvent event.
+SPStreamEvent newAddressChangedLogoutEvent(QString const &address); ///< Create a new AddressChangedLogoutEvent event.
+SPStreamEvent newApiCertIssueEvent(); ///< Create a new ApiCertIssueEvent event.
+
+// User list related event
+SPStreamEvent newToggleSplitModeFinishedEvent(QString const &userID); ///< Create a new ToggleSplitModeFinishedEvent event.
+SPStreamEvent newUserDisconnectedEvent(QString const &username); ///< Create a new UserDisconnectedEvent event.
+SPStreamEvent newUserChangedEvent(QString const &userID); ///< Create a new UserChangedEvent event.
+
+
+} // namespace bridgepp
+
+
+#endif //BRIDGE_GUI_TESTER_EVENT_FACTORY_H
diff --git a/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/GRPCUtils.h b/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/GRPCUtils.h
index 9fee503a..5c4ad567 100644
--- a/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/GRPCUtils.h
+++ b/internal/frontend/bridge-gui/bridgepp/bridgepp/GRPC/GRPCUtils.h
@@ -29,6 +29,9 @@ namespace bridgepp
{
+typedef std::shared_ptr SPStreamEvent; ///< Type definition for shared pointer to grpc::StreamEvent.
+
+
QString serverCertificatePath(); ///< Return the path of the server certificate.
QString serverKeyPath(); ///< Return the path of the server key.
grpc::LogLevel logLevelToGRPC(Log::Level level); ///< Convert a Log::Level to gRPC enum value.