Compare commits

...

25 Commits

Author SHA1 Message Date
5c69af4418 chore: Xikou Bridge 3.8.1 changelog. 2023-12-11 11:49:01 +01:00
416f696863 feat(GODT-3121): added options to kb-tester CLI tool. 2023-12-08 11:04:48 +01:00
789c1cc816 feat(GODT-3121): kb suggestion first version of complete list. 2023-12-08 10:01:38 +01:00
58736dd254 chore: keep nighlty-job log as artifact. 2023-12-08 09:31:57 +01:00
a057138880 feat(GODT-3121): KB suggestion test tool now support multi-line input. 2023-12-07 10:48:33 +01:00
76087f1749 feat(GODT-3121): minimalist CLI tool to test KB suggestions. 2023-12-07 09:36:47 +01:00
83935f3a03 feat(GODT-3121): refactored retrieval kb article index lookup. 2023-12-07 09:35:05 +01:00
b93c10ad47 feat(GODT-3121): adds KB suggestion scoring. 2023-12-07 09:35:05 +01:00
3309137b80 feat(GODT-3121): forward user input to bridge. 2023-12-07 09:35:05 +01:00
88c4737ba4 feat(GODT-3121): reuse InfoTooltip. 2023-12-07 09:35:05 +01:00
e5db9b1ccc feat(GODT-3121): added display of bug report user input in bridge-gui-tester. 2023-12-07 09:35:05 +01:00
6e2e622a2f feat(GODT-3121): added tooltip for KB suggestions. 2023-12-07 09:35:05 +01:00
3a66063938 feat(GODT-3121): change log level of click on external link. 2023-12-07 09:35:05 +01:00
120ddbbcbb feat(GODT-3121): finalize UI for KB suggestions. 2023-12-07 09:35:05 +01:00
39b31abef8 feat(GODT-3121): fix issues reported by the resharper C++ engine. 2023-12-07 09:35:05 +01:00
ebeca394c7 feat(GODT-3121): implement suggestion list in bridge-gui. 2023-12-07 09:35:05 +01:00
2206cb3f12 feat(GODT-3121): suggestions links are in the final bug report page. 2023-12-07 09:35:05 +01:00
cfd07cf893 feat(GODT-3121): suggestions are transferred to QML. 2023-12-07 09:35:05 +01:00
2e2648fcd5 feat(GODT-3121): QML request suggestions. 2023-12-07 09:35:05 +01:00
3070912416 feat(GODT-3121): added gRPC call and event for KB suggestions. 2023-12-07 09:35:05 +01:00
51722eb1a4 feat(GODT-3121): introduced knowledgebase package. 2023-12-07 09:35:05 +01:00
5950eff083 chore(GODT-3160): silence vuln 2023-12-07 08:15:10 +01:00
5c67cc2e76 fix(GODT-3153): Do not take into account full address when hasing messages. 2023-12-06 16:14:38 +00:00
01db488caa feat(GODT-2001): add govulncheck to scan for vulnerabilities. 2023-12-06 15:29:21 +01:00
6cbef1d786 test: Improve TestMetadata_JobCorrectlyFinishesAfterCancel 2023-12-04 13:48:44 +00:00
78 changed files with 3717 additions and 2050 deletions

View File

@ -3,6 +3,20 @@
Changelog [format](http://keepachangelog.com/en/1.0.0/)
## Xikou Bridge 3.8.1
### Added
* GODT-3121: Suggest relevant KB articles in the in-app bug report form.
* GODT-2001: Add govulncheck to scan for vulnerabilities.
### Changed
* Keep nighlty-job log as artifact.
* Test: Improve TestMetadata_JobCorrectlyFinishesAfterCancel.
### Fixed
* GODT-3153: Do not take into account full address when hasing messages.
## Xikou Bridge 3.8.0
### Added

View File

@ -11,7 +11,7 @@ ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
.PHONY: build build-gui build-nogui build-launcher versioner hasher
# Keep version hardcoded so app build works also without Git repository.
BRIDGE_APP_VERSION?=3.8.0+git
BRIDGE_APP_VERSION?=3.8.1+git
APP_VERSION:=${BRIDGE_APP_VERSION}
APP_FULL_NAME:=Proton Mail Bridge
APP_VENDOR:=Proton AG
@ -328,13 +328,6 @@ lint-bug-report:
lint-bug-report-preview:
python3 utils/validate_bug_report_file.py --file "internal/frontend/bridge-gui/bridge-gui/qml/Resources/bug_report_flow.json" --preview
gobinsec: gobinsec-cache.yml build
gobinsec -wait -cache -config utils/gobinsec_conf.yml ${EXE_TARGET} ${DEPLOY_DIR}/${TARGET_OS}/${LAUNCHER_EXE}
gobinsec-cache.yml:
./utils/gobinsec_update.sh
cp ./utils/gobinsec_update/gobinsec-cache-valid.yml ./gobinsec-cache.yml
updates: install-go-mod-outdated
# Uncomment the "-ci" to fail the job if something can be updated.
go list -u -m -json all | go-mod-outdated -update -direct #-ci

View File

@ -71,7 +71,11 @@ test-integration-nightly:
needs:
- test-integration
script:
- make test-integration-nightly
- make test-integration-nightly | tee -a nightly-job.log
artifacts:
when: always
paths:
- nightly-job.log
test-windows:
extends:
@ -107,3 +111,18 @@ test-coverage:
coverage_report:
coverage_format: cobertura
path: coverage.xml
go-vuln-check:
extends:
- .rules-branch-manual-MR-and-devel-always
stage: test
tags:
- shared-medium
script:
- apt-get -y install jq
- ./utils/govulncheck.sh
artifacts:
when: always
paths:
- vulns*

2
go.mod
View File

@ -5,7 +5,7 @@ go 1.20
require (
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
github.com/Masterminds/semver/v3 v3.2.0
github.com/ProtonMail/gluon v0.17.1-0.20231114153341-2ecbdd2739f7
github.com/ProtonMail/gluon v0.17.1-0.20231206152152-caaf10897f9e
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
github.com/ProtonMail/go-proton-api v0.4.1-0.20231130083229-e8aa47d7a366
github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton

4
go.sum
View File

@ -28,8 +28,8 @@ github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
github.com/ProtonMail/gluon v0.17.1-0.20231114153341-2ecbdd2739f7 h1:w+VoSAq9FQvKMm3DlH1MIEZ1KGe7LJ+81EJFVwSV4VU=
github.com/ProtonMail/gluon v0.17.1-0.20231114153341-2ecbdd2739f7/go.mod h1:Og5/Dz1MiGpCJn51XujZwxiLG7WzvvjE5PRpZBQmAHo=
github.com/ProtonMail/gluon v0.17.1-0.20231206152152-caaf10897f9e h1:kHmSOTxynSip1WJvwZTFOGJPVfI42e/I8bDzDjLK7aM=
github.com/ProtonMail/gluon v0.17.1-0.20231206152152-caaf10897f9e/go.mod h1:Og5/Dz1MiGpCJn51XujZwxiLG7WzvvjE5PRpZBQmAHo=
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=

View File

@ -22,6 +22,7 @@ import (
"fmt"
"github.com/Masterminds/semver/v3"
"github.com/ProtonMail/proton-bridge/v3/internal/kb"
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/services/userevents"
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
@ -310,6 +311,10 @@ func (bridge *Bridge) SetColorScheme(colorScheme string) error {
return bridge.vault.SetColorScheme(colorScheme)
}
func (bridge *Bridge) GetKnowledgeBaseSuggestions(userInput string) (kb.ArticleList, error) {
return kb.GetSuggestions(userInput)
}
// FactoryReset deletes all users, wipes the vault, and deletes all files.
// Note: it does not clear the keychain. The only entry in the keychain is the vault password,
// which we need at next startup to decrypt the vault.

View File

@ -135,7 +135,7 @@ func (status *ConfigurationStatus) ApplyProgress() error {
return status.Save()
}
func (status *ConfigurationStatus) RecordLinkClicked(link uint) error {
func (status *ConfigurationStatus) RecordLinkClicked(link uint64) error {
status.DataLock.Lock()
defer status.DataLock.Unlock()
@ -198,11 +198,11 @@ func (data *ConfigurationStatusData) init() {
data.DataV1.FailureDetails = ""
}
func (data *ConfigurationStatusData) setClickedLink(pos uint) {
func (data *ConfigurationStatusData) setClickedLink(pos uint64) {
data.DataV1.ClickedLink |= 1 << pos
}
func (data *ConfigurationStatusData) hasLinkClicked(pos uint) bool {
func (data *ConfigurationStatusData) hasLinkClicked(pos uint64) bool {
val := data.DataV1.ClickedLink & (1 << pos)
return val > 0
}
@ -211,7 +211,7 @@ func (data *ConfigurationStatusData) clickedLinkToString() string {
var str = ""
var first = true
for i := 0; i < 64; i++ {
if data.hasLinkClicked(uint(i)) {
if data.hasLinkClicked(uint64(i)) {
if !first {
str += ","
} else {

View File

@ -18,7 +18,6 @@
#include "AppController.h"
#include "GRPCService.h"
#include <bridgepp/GRPC/GRPCUtils.h>
#include <bridgepp/Exception/Exception.h>
#include "MainWindow.h"
#include <bridgepp/Log/Log.h>
@ -68,7 +67,7 @@ void AppController::setMainWindow(MainWindow *mainWindow) {
//****************************************************************************************************************************************************
/// \return The main window.
//****************************************************************************************************************************************************
MainWindow &AppController::mainWindow() {
MainWindow &AppController::mainWindow() const {
if (!mainWindow_) {
throw Exception("mainWindow has not yet been registered.");
}
@ -79,7 +78,7 @@ MainWindow &AppController::mainWindow() {
//****************************************************************************************************************************************************
/// \return A reference to the log.
//****************************************************************************************************************************************************
bridgepp::Log &AppController::log() {
bridgepp::Log &AppController::log() const {
return *log_;
}
@ -87,7 +86,7 @@ bridgepp::Log &AppController::log() {
//****************************************************************************************************************************************************
/// \return A reference to the bridge-gui log.
//****************************************************************************************************************************************************
bridgepp::Log &AppController::bridgeGUILog() {
bridgepp::Log &AppController::bridgeGUILog() const {
return *bridgeGUILog_;
}
@ -95,6 +94,6 @@ bridgepp::Log &AppController::bridgeGUILog() {
//****************************************************************************************************************************************************
/// \return A reference to the gRPC service.
//****************************************************************************************************************************************************
GRPCService &AppController::grpc() {
GRPCService &AppController::grpc() const {
return *grpc_;
}

View File

@ -42,10 +42,10 @@ public: // member functions.
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.
MainWindow &mainWindow() const; ///< Return the main window.
bridgepp::Log &log() const; ///< Return a reference to the log.
bridgepp::Log &bridgeGUILog() const; ///< Return a reference to the bridge-gui log.
GRPCService &grpc() const; ///< Return a reference to the gRPC service.
private: // member functions.
AppController(); ///< Default constructor.

View File

@ -66,11 +66,13 @@ add_executable(bridge-gui-tester
GRPCQtProxy.cpp GRPCQtProxy.h
GRPCService.cpp GRPCService.h
GRPCServerWorker.cpp GRPCServerWorker.h
Tabs/EventsTab.cpp Tabs/EventsTab.h
Tabs/KnowledgeBaseTab.cpp Tabs/KnowledgeBaseTab.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})

View File

@ -26,7 +26,7 @@ using namespace grpc;
//****************************************************************************************************************************************************
/// \param[in] The server token expected from gRPC calls
/// \param[in] serverToken The server token expected from gRPC calls
//****************************************************************************************************************************************************
GRPCMetadataProcessor::GRPCMetadataProcessor(QString const &serverToken)
: serverToken_(serverToken.toStdString()) {
@ -43,16 +43,16 @@ bool GRPCMetadataProcessor::IsBlocking() const {
//****************************************************************************************************************************************************
/// \param[in] inputMeta
/// \param authMetadata The authentication metadata.
/// \return the result of the metadata processing.
//****************************************************************************************************************************************************
Status GRPCMetadataProcessor::Process(AuthMetadataProcessor::InputMetadata const &auth_metadata, AuthContext *,
AuthMetadataProcessor::OutputMetadata *, AuthMetadataProcessor::OutputMetadata *) {
Status GRPCMetadataProcessor::Process(InputMetadata const &authMetadata, AuthContext *,
OutputMetadata *, OutputMetadata *) {
try {
AuthMetadataProcessor::InputMetadata::const_iterator pathIt = auth_metadata.find(":path");
QString const callName = (pathIt == auth_metadata.end()) ? ("unkown gRPC call") : QString::fromLocal8Bit(pathIt->second);
const InputMetadata::const_iterator pathIt = authMetadata.find(":path");
QString const callName = (pathIt == authMetadata.end()) ? ("unkown gRPC call") : QString::fromLocal8Bit(pathIt->second);
AuthMetadataProcessor::InputMetadata::size_type const count = auth_metadata.count(grpcMetadataServerTokenKey);
AuthMetadataProcessor::InputMetadata::size_type const count = authMetadata.count(grpcMetadataServerTokenKey);
if (count == 0) {
throw Exception(QString("Missing server token in gRPC client call '%1'.").arg(callName));
}
@ -61,7 +61,7 @@ Status GRPCMetadataProcessor::Process(AuthMetadataProcessor::InputMetadata const
throw Exception(QString("Several server tokens were provided in gRPC client call '%1'.").arg(callName));
}
if (auth_metadata.find(grpcMetadataServerTokenKey)->second != serverToken_) {
if (authMetadata.find(grpcMetadataServerTokenKey)->second != serverToken_) {
throw Exception(QString("Invalid server token provided by gRPC client call '%1'.").arg(callName));
}
@ -70,7 +70,7 @@ Status GRPCMetadataProcessor::Process(AuthMetadataProcessor::InputMetadata const
}
catch (Exception const &e) {
app().log().error(e.qwhat());
return Status(StatusCode::UNAUTHENTICATED, e.qwhat().toStdString());
return Status(UNAUTHENTICATED, e.qwhat().toStdString());
}
}

View File

@ -35,7 +35,7 @@ public: // member functions.
GRPCMetadataProcessor &operator=(GRPCMetadataProcessor const &) = delete; ///< Disabled assignment operator.
GRPCMetadataProcessor &operator=(GRPCMetadataProcessor &&) = delete; ///< Disabled move assignment operator.
bool IsBlocking() const override; ///< Is the processor blocking?
grpc::Status Process(InputMetadata const &auth_metadata, grpc::AuthContext *context, OutputMetadata *consumed_auth_metadata,
grpc::Status Process(InputMetadata const &authMetadata, grpc::AuthContext *context, OutputMetadata *consumed_auth_metadata,
OutputMetadata *response_metadata) override; ///< Process the metadata
private:

View File

@ -31,10 +31,11 @@ GRPCQtProxy::GRPCQtProxy()
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void GRPCQtProxy::connectSignals() {
void GRPCQtProxy::connectSignals() const {
MainWindow &mainWindow = app().mainWindow();
SettingsTab &settingsTab = mainWindow.settingsTab();
UsersTab &usersTab = mainWindow.usersTab();
SettingsTab const &settingsTab = mainWindow.settingsTab();
UsersTab const &usersTab = mainWindow.usersTab();
KnowledgeBaseTab const &kbTab = mainWindow.knowledgeBaseTab();
connect(this, &GRPCQtProxy::delayedEventRequested, &mainWindow, &MainWindow::sendDelayedEvent);
connect(this, &GRPCQtProxy::setIsAutostartOnReceived, &settingsTab, &SettingsTab::setIsAutostartOn);
connect(this, &GRPCQtProxy::setIsBetaEnabledReceived, &settingsTab, &SettingsTab::setIsBetaEnabled);
@ -56,6 +57,7 @@ void GRPCQtProxy::connectSignals() {
connect(this, &GRPCQtProxy::setUserSplitModeReceived, &usersTab, &UsersTab::setUserSplitMode);
connect(this, &GRPCQtProxy::configureUserAppleMailReceived, &usersTab, &UsersTab::configureUserAppleMail);
connect(this, &GRPCQtProxy::sendBadEventUserFeedbackReceived, &usersTab, &UsersTab::processBadEventUserFeedback);
connect(this, &GRPCQtProxy::requestKnowledgeBaseSuggestionsReceived, &kbTab, &KnowledgeBaseTab::requestKnowledgeBaseSuggestions);
}
@ -119,6 +121,13 @@ void GRPCQtProxy::reportBug(QString const &osType, QString const &osVersion, QSt
emit reportBugReceived(osType, osVersion, emailClient, address, description, includeLogs);
}
//****************************************************************************************************************************************************
/// \param[in] userInput The user input.
//****************************************************************************************************************************************************
void GRPCQtProxy::requestKnowledgeBaseSuggestions(QString const& userInput) {
emit requestKnowledgeBaseSuggestionsReceived(userInput);
}
//****************************************************************************************************************************************************
//
@ -157,8 +166,8 @@ void GRPCQtProxy::setClientPlatform(QString const &clientPlatform) {
/// \param[in] useSSLForIMAP The IMAP connexion mode.
/// \param[in] useSSLForSMTP The IMAP connexion mode.
//****************************************************************************************************************************************************
void GRPCQtProxy::setMailServerSettings(qint32 imapPort, qint32 smtpPort, bool useSSLForIMAP, bool userSSLForSMTP) {
emit setMailServerSettingsReceived(imapPort, smtpPort, useSSLForIMAP, userSSLForSMTP);
void GRPCQtProxy::setMailServerSettings(qint32 imapPort, qint32 smtpPort, bool useSSLForIMAP, bool useSSLForSMTP) {
emit setMailServerSettingsReceived(imapPort, smtpPort, useSSLForIMAP, useSSLForSMTP);
}

View File

@ -36,7 +36,7 @@ public: // member functions.
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 connectSignals() const; // 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.
@ -45,11 +45,12 @@ public: // member functions.
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 requestKnowledgeBaseSuggestions(QString const &userInput); ///< Forwards a RequestKnowledgeBaseSuggestions call via a Qt signal.
void installTLSCertificate(); ///< Forwards a InstallTLScertificate call via a Qt signal.
void exportTLSCertificates(QString const &folderPath); //< Forward an 'ExportTLSCertificates' call via a Qt signal.
void setIsStreaming(bool isStreaming); ///< Forward a isStreaming internal messages via a Qt signal.
void setClientPlatform(QString const &clientPlatform); ///< Forward a setClientPlatform call via a Qt signal.
void setMailServerSettings(qint32 imapPort, qint32 smtpPort, bool useSSLForIMAP, bool userSSLForSMTP); ///< Forwards a setMailServerSettings' call via a Qt signal.
void setMailServerSettings(qint32 imapPort, qint32 smtpPort, bool useSSLForIMAP, bool useSSLForSMTP); ///< Forwards a setMailServerSettings' call via a Qt signal.
void setIsDoHEnabled(bool enabled); ///< Forwards a setIsDoHEnabled call via a Qt signal.
void setDiskCachePath(QString const &path); ///< Forwards a setDiskCachePath call via a Qt signal.
void setIsAutomaticUpdateOn(bool on); ///< Forwards a SetIsAutomaticUpdateOn call via a Qt signal.
@ -68,6 +69,7 @@ signals:
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 requestKnowledgeBaseSuggestionsReceived(QString const &userInput); ///< Signal for the RequestKnowledgeBaseSuggestions gRPC call.
void installTLSCertificateReceived(); ///< Signal for the InstallTLSCertificate gRPC call.
void exportTLSCertificatesReceived(QString const &folderPath); ///< Signal for the ExportTLSCertificates gRPC call.
void setIsStreamingReceived(bool isStreaming); ///< Signal for the IsStreaming internal message.

View File

@ -50,7 +50,7 @@ void GRPCServerWorker::run() {
SslServerCredentialsOptions ssl_opts;
ssl_opts.pem_root_certs = "";
ssl_opts.pem_key_cert_pairs.push_back(pair);
std::shared_ptr<ServerCredentials> credentials = grpc::SslServerCredentials(ssl_opts);
std::shared_ptr<ServerCredentials> const credentials = SslServerCredentials(ssl_opts);
GRPCConfig config;
config.cert = testTLSCert;
@ -59,8 +59,7 @@ void GRPCServerWorker::run() {
credentials->SetAuthMetadataProcessor(processor_); // gRPC interceptors are still experimental in C++, so we use AuthMetadataProcessor
ServerBuilder builder;
int port = 0; // Port will not be known until ServerBuilder::BuildAndStart() is called
bool const useFileSocket = useFileSocketForGRPC();
if (useFileSocket) {
if (useFileSocketForGRPC()) {
QString const fileSocketPath = getAvailableFileSocketPath();
if (fileSocketPath.isEmpty()) {
throw Exception("Could not get an available file socket.");
@ -102,7 +101,7 @@ void GRPCServerWorker::run() {
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void GRPCServerWorker::stop() {
void GRPCServerWorker::stop() const {
if (server_) {
server_->Shutdown();
}

View File

@ -40,7 +40,7 @@ public: // member functions.
GRPCServerWorker &operator=(GRPCServerWorker &&) = delete; ///< Disabled move assignment operator.
void run() override; ///< Run the worker.
void stop(); ///< Stop the gRPC service.
void stop() const; ///< Stop the gRPC service.
private: // data members
std::unique_ptr<grpc::Server> server_ { nullptr }; ///< The gRPC server.

View File

@ -39,7 +39,7 @@ QString const defaultKeychain = "defaultKeychain"; ///< The default keychain.
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void GRPCService::connectProxySignals() {
void GRPCService::connectProxySignals() const {
qtProxy_.connectSignals();
}
@ -185,7 +185,7 @@ Status GRPCService::SetIsAllMailVisible(ServerContext *, BoolValue const *reques
/// \param[out] response The response.
/// \return The status for the call.
//****************************************************************************************************************************************************
Status GRPCService::IsAllMailVisible(ServerContext *, Empty const *request, BoolValue *response) {
Status GRPCService::IsAllMailVisible(ServerContext *, Empty const *, BoolValue *response) {
app().log().debug(__FUNCTION__);
response->set_value(app().mainWindow().settingsTab().isAllMailVisible());
return Status::OK;
@ -354,25 +354,45 @@ Status GRPCService::SetMainExecutable(ServerContext *, StringValue const *reques
return Status::OK;
}
//****************************************************************************************************************************************************
/// \param[in] request The request.
/// \return The status for the call.
//****************************************************************************************************************************************************
grpc::Status GRPCService::RequestKnowledgeBaseSuggestions(ServerContext*, StringValue const* request, Empty*) {
QString const userInput = QString::fromUtf8(request->value());
app().log().info(QString("RequestKnowledgeBaseSuggestions: %1").arg(userInput.left(10) + "..."));
qtProxy_.requestKnowledgeBaseSuggestionsReceived(userInput);
QList<bridgepp::KnowledgeBaseSuggestion> suggestions;
for (qsizetype i = 1; i <= 3; ++i) {
suggestions.push_back( {
.title = QString("Suggested link %1").arg(i),
.url = QString("https://proton.me/support/bridge#%1").arg(i),
});
}
qtProxy_.sendDelayedEvent(newKnowledgeBaseSuggestionsEvent(app().mainWindow().knowledgeBaseTab().getSuggestions()));
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();
EventsTab const&eventsTab = app().mainWindow().eventsTab();
qtProxy_.reportBug(QString::fromStdString(request->ostype()), QString::fromStdString(request->osversion()),
QString::fromStdString(request->emailclient()), QString::fromStdString(request->address()), QString::fromStdString(request->description()),
request->includelogs());
SPStreamEvent event;
switch (tab.nextBugReportResult()) {
case SettingsTab::BugReportResult::Success:
switch (eventsTab.nextBugReportResult()) {
case EventsTab::BugReportResult::Success:
event = newReportBugSuccessEvent();
break;
case SettingsTab::BugReportResult::Error:
case EventsTab::BugReportResult::Error:
event = newReportBugErrorEvent();
break;
case SettingsTab::BugReportResult::DataSharingError:
case EventsTab::BugReportResult::DataSharingError:
event = newReportBugFallbackEvent();
break;
}
@ -392,7 +412,7 @@ Status GRPCService::Login(ServerContext *, LoginRequest const *request, Empty *)
UsersTab &usersTab = app().mainWindow().usersTab();
loginUsername_ = QString::fromStdString(request->username());
SPUser const& user = usersTab.userTable().userWithUsernameOrEmail(QString::fromStdString(request->username()));
SPUser const &user = usersTab.userTable().userWithUsernameOrEmail(QString::fromStdString(request->username()));
if (user) {
qtProxy_.sendDelayedEvent(newLoginAlreadyLoggedInEvent(user->id()));
return Status::OK;
@ -427,7 +447,7 @@ Status GRPCService::Login(ServerContext *, LoginRequest const *request, Empty *)
//****************************************************************************************************************************************************
Status GRPCService::Login2FA(ServerContext *, LoginRequest const *request, Empty *) {
app().log().debug(__FUNCTION__);
UsersTab &usersTab = app().mainWindow().usersTab();
UsersTab const &usersTab = app().mainWindow().usersTab();
if (usersTab.nextUserTFAError()) {
qtProxy_.sendDelayedEvent(newLoginError(LoginErrorType::TFA_ERROR, "2FA Error."));
return Status::OK;
@ -452,7 +472,7 @@ Status GRPCService::Login2FA(ServerContext *, LoginRequest const *request, Empty
//****************************************************************************************************************************************************
Status GRPCService::Login2Passwords(ServerContext *, LoginRequest const *request, Empty *) {
app().log().debug(__FUNCTION__);
UsersTab &usersTab = app().mainWindow().usersTab();
UsersTab const &usersTab = app().mainWindow().usersTab();
if (usersTab.nextUserTwoPasswordsError()) {
qtProxy_.sendDelayedEvent(newLoginError(LoginErrorType::TWO_PASSWORDS_ERROR, "Two Passwords error."));
@ -542,12 +562,12 @@ Status GRPCService::DiskCachePath(ServerContext *, Empty const *, StringValue *r
Status GRPCService::SetDiskCachePath(ServerContext *, StringValue const *path, Empty *) {
app().log().debug(__FUNCTION__);
SettingsTab &tab = app().mainWindow().settingsTab();
EventsTab const &eventsTab = app().mainWindow().eventsTab();
QString const qPath = QString::fromStdString(path->value());
// we mimic the behaviour of Bridge
if (!tab.nextCacheChangeWillSucceed()) {
qtProxy_.sendDelayedEvent(newDiskCacheErrorEvent(grpc::DiskCacheErrorType(CANT_MOVE_DISK_CACHE_ERROR)));
if (!eventsTab.nextCacheChangeWillSucceed()) {
qtProxy_.sendDelayedEvent(newDiskCacheErrorEvent(static_cast<DiskCacheErrorType>(CANT_MOVE_DISK_CACHE_ERROR)));
} else {
qtProxy_.setDiskCachePath(qPath);
qtProxy_.sendDelayedEvent(newDiskCachePathChangedEvent(qPath));
@ -584,7 +604,7 @@ Status GRPCService::IsDoHEnabled(ServerContext *, Empty const *, BoolValue *resp
/// \param[in] settings The IMAP/SMTP settings.
/// \return The status for the call.
//****************************************************************************************************************************************************
Status GRPCService::SetMailServerSettings(::grpc::ServerContext *context, ImapSmtpSettings const *settings, Empty *) {
Status GRPCService::SetMailServerSettings(ServerContext *, ImapSmtpSettings const *settings, Empty *) {
app().log().debug(__FUNCTION__);
qtProxy_.setMailServerSettings(settings->imapport(), settings->smtpport(), settings->usesslforimap(), settings->usesslforsmtp());
qtProxy_.sendDelayedEvent(newMailServerSettingsChanged(*settings));
@ -597,9 +617,9 @@ Status GRPCService::SetMailServerSettings(::grpc::ServerContext *context, ImapSm
/// \param[out] outSettings The settings
/// \return The status for the call.
//****************************************************************************************************************************************************
Status GRPCService::MailServerSettings(::grpc::ServerContext *, Empty const *, ImapSmtpSettings *outSettings) {
Status GRPCService::MailServerSettings(ServerContext *, Empty const *, ImapSmtpSettings *outSettings) {
app().log().debug(__FUNCTION__);
SettingsTab &tab = app().mainWindow().settingsTab();
SettingsTab const &tab = app().mainWindow().settingsTab();
outSettings->set_imapport(tab.imapPort());
outSettings->set_smtpport(tab.smtpPort());
outSettings->set_usesslforimap(tab.useSSLForIMAP());
@ -626,7 +646,7 @@ Status GRPCService::Hostname(ServerContext *, Empty const *, StringValue *respon
//****************************************************************************************************************************************************
Status GRPCService::IsPortFree(ServerContext *, Int32Value const *request, BoolValue *response) {
app().log().debug(__FUNCTION__);
response->set_value(app().mainWindow().settingsTab().isPortFree());
response->set_value(app().mainWindow().eventsTab().isPortFree());
return Status::OK;
}
@ -697,8 +717,8 @@ Status GRPCService::GetUserList(ServerContext *, Empty const *, UserListResponse
//****************************************************************************************************************************************************
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);
QString const userID = QString::fromStdString(request->value());
SPUser const user = app().mainWindow().usersTab().userWithID(userID);
if (!user) {
return Status(NOT_FOUND, QString("user not found %1").arg(userID).toStdString());
}
@ -766,14 +786,14 @@ Status GRPCService::ConfigureUserAppleMail(ServerContext *, ConfigureAppleMailRe
/// \param[in] request The request
/// \return The status for the call.
//****************************************************************************************************************************************************
Status GRPCService::ExportTLSCertificates(ServerContext *, StringValue const *request, Empty *response) {
Status GRPCService::ExportTLSCertificates(ServerContext *, StringValue const *request, Empty *) {
app().log().debug(__FUNCTION__);
SettingsTab &tab = app().mainWindow().settingsTab();
SettingsTab const &tab = app().mainWindow().settingsTab();
if (!tab.nextTLSCertExportWillSucceed()) {
qtProxy_.sendDelayedEvent(newGenericErrorEvent(grpc::TLS_CERT_EXPORT_ERROR));
qtProxy_.sendDelayedEvent(newGenericErrorEvent(TLS_CERT_EXPORT_ERROR));
}
if (!tab.nextTLSKeyExportWillSucceed()) {
qtProxy_.sendDelayedEvent(newGenericErrorEvent(grpc::TLS_KEY_EXPORT_ERROR));
qtProxy_.sendDelayedEvent(newGenericErrorEvent(TLS_KEY_EXPORT_ERROR));
}
qtProxy_.exportTLSCertificates(QString::fromStdString(request->value()));
return Status::OK;

View File

@ -37,7 +37,7 @@ public: // member functions.
~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
void connectProxySignals() const; ///< Connect the signals of the Qt Proxy to the GUI components
bool isStreaming() const; ///< Check if the service is currently streaming events.
grpc::Status CheckTokens(::grpc::ServerContext *context, ::google::protobuf::StringValue const *request, ::google::protobuf::StringValue *response) override;
grpc::Status AddLogEntry(::grpc::ServerContext *, ::grpc::AddLogEntryRequest const *request, ::google::protobuf::Empty *) override;
@ -67,6 +67,7 @@ public: // member functions.
grpc::Status ReportBug(::grpc::ServerContext *, ::grpc::ReportBugRequest const *request, ::google::protobuf::Empty *) override;
grpc::Status ForceLauncher(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
grpc::Status SetMainExecutable(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
grpc::Status RequestKnowledgeBaseSuggestions(::grpc::ServerContext *, ::google::protobuf::StringValue const *request, ::google::protobuf::Empty *) override;
grpc::Status Login(::grpc::ServerContext *, ::grpc::LoginRequest const *request, ::google::protobuf::Empty *) override;
grpc::Status Login2FA(::grpc::ServerContext *, ::grpc::LoginRequest const *request, ::google::protobuf::Empty *) override;
grpc::Status Login2Passwords(::grpc::ServerContext *, ::grpc::LoginRequest const *request, ::google::protobuf::Empty *) override;

View File

@ -63,7 +63,7 @@ MainWindow::MainWindow(QWidget *parent)
//****************************************************************************************************************************************************
/// \return A reference to the 'General' tab.
//****************************************************************************************************************************************************
SettingsTab &MainWindow::settingsTab() {
SettingsTab &MainWindow::settingsTab() const {
return *ui_.settingsTab;
}
@ -71,16 +71,32 @@ SettingsTab &MainWindow::settingsTab() {
//****************************************************************************************************************************************************
/// \return A reference to the users tab.
//****************************************************************************************************************************************************
UsersTab &MainWindow::usersTab() {
UsersTab &MainWindow::usersTab() const {
return *ui_.usersTab;
}
//****************************************************************************************************************************************************
/// \return A reference to the events tab.
//****************************************************************************************************************************************************
EventsTab& MainWindow::eventsTab() const {
return *ui_.eventsTab;
}
//****************************************************************************************************************************************************
/// \return A reference to the knowledge base tab.
//****************************************************************************************************************************************************
KnowledgeBaseTab& MainWindow::knowledgeBaseTab() const {
return *ui_.knowledgeBaseTab;
}
//****************************************************************************************************************************************************
/// \param[in] level The log level.
/// \param[in] message The log message
//****************************************************************************************************************************************************
void MainWindow::addLogEntry(bridgepp::Log::Level level, const QString &message) {
void MainWindow::addLogEntry(bridgepp::Log::Level level, const QString &message) const {
addEntryToLogEdit(level, message, *ui_.editLog);
}
@ -89,7 +105,7 @@ void MainWindow::addLogEntry(bridgepp::Log::Level level, const QString &message)
/// \param[in] level The log level.
/// \param[in] message The log message
//****************************************************************************************************************************************************
void MainWindow::addBridgeGUILogEntry(bridgepp::Log::Level level, const QString &message) {
void MainWindow::addBridgeGUILogEntry(bridgepp::Log::Level level, const QString &message) const {
addEntryToLogEdit(level, message, *ui_.editBridgeGUILog);
}
@ -97,8 +113,8 @@ void MainWindow::addBridgeGUILogEntry(bridgepp::Log::Level level, const QString
//****************************************************************************************************************************************************
/// \param[in] event The event.
//****************************************************************************************************************************************************
void MainWindow::sendDelayedEvent(SPStreamEvent const &event) {
QTimer::singleShot(this->settingsTab().eventDelayMs(), [event] { app().grpc().sendEvent(event); });
void MainWindow::sendDelayedEvent(SPStreamEvent const &event) const {
QTimer::singleShot(this->eventsTab().eventDelayMs(), [event] { app().grpc().sendEvent(event); });
}

View File

@ -38,15 +38,17 @@ public: // member functions.
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.
SettingsTab &settingsTab() const; ///< Returns a reference the 'Settings' tab.
UsersTab &usersTab() const; ///< Returns a reference to the 'Users' tab.
EventsTab &eventsTab() const; ///< Returns a reference to the 'Events' tab.
KnowledgeBaseTab &knowledgeBaseTab() const; ///< Returns a reference to the 'Knowledge Base' 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.
void sendDelayedEvent(bridgepp::SPStreamEvent const &event) const; ///< 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.
void addLogEntry(bridgepp::Log::Level level, QString const &message) const; ///< Add an entry to the log.
void addBridgeGUILogEntry(bridgepp::Log::Level level, const QString &message) const; ///< Add an entry to the log.
private:
Ui::MainWindow ui_ {}; ///< The GUI for the window.

View File

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>1226</width>
<height>1086</height>
<width>1096</width>
<height>876</height>
</rect>
</property>
<property name="windowTitle">
@ -22,7 +22,7 @@
</property>
<widget class="QTabWidget" name="tabTop">
<property name="currentIndex">
<number>0</number>
<number>3</number>
</property>
<widget class="SettingsTab" name="settingsTab">
<attribute name="title">
@ -34,6 +34,16 @@
<string>Users</string>
</attribute>
</widget>
<widget class="EventsTab" name="eventsTab">
<attribute name="title">
<string>Events &amp;&amp; Errors</string>
</attribute>
</widget>
<widget class="KnowledgeBaseTab" name="knowledgeBaseTab">
<attribute name="title">
<string>Knowledge Base</string>
</attribute>
</widget>
</widget>
<widget class="QTabWidget" name="tabBottom">
<property name="currentIndex">
@ -96,6 +106,18 @@
<header>Tabs/UsersTab.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>EventsTab</class>
<extends>QWidget</extends>
<header>Tabs/EventsTab.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>KnowledgeBaseTab</class>
<extends>QWidget</extends>
<header>Tabs/KnowledgeBaseTab.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>

View File

@ -0,0 +1,109 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
#include "EventsTab.h"
#include "GRPCService.h"
#include <bridgepp/GRPC/EventFactory.h>
using namespace bridgepp;
//****************************************************************************************************************************************************
/// \brief Connect an address error button to the generation of an address error event.
///
/// \param[in] button The error button.
/// \param[in] edit The edit containing the address.
/// \param[in] eventGenerator The factory function creating the event.
//****************************************************************************************************************************************************
void connectAddressError(QPushButton const* button, QLineEdit* edit, SPStreamEvent (*eventGenerator)(QString const&)) {
QObject::connect(button, &QPushButton::clicked, [edit, eventGenerator]() { app().grpc().sendEvent(eventGenerator(edit->text())); });
}
//****************************************************************************************************************************************************
/// \param[in] parent The parent widget.
//****************************************************************************************************************************************************
EventsTab::EventsTab(QWidget* parent)
: QWidget(parent) {
ui_.setupUi(this);
this->resetUI();
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_.buttonNoKeychain, &QPushButton::clicked, []() { app().grpc().sendEvent(newHasNoKeychainEvent()); });
connect(ui_.buttonAPICertIssue, &QPushButton::clicked, []() { app().grpc().sendEvent(newApiCertIssueEvent()); });
connectAddressError(ui_.buttonAddressChanged, ui_.editAddressErrors, newAddressChangedEvent);
connectAddressError(ui_.buttonAddressChangedLogout, ui_.editAddressErrors, newAddressChangedLogoutEvent);
//connect(ui_.checkNextCacheChangeWillSucceed, &QCheckBox::toggled, this, &SettingsTab::updateGUIState);
connect(ui_.buttonUpdateError, &QPushButton::clicked, [&]() {
app().grpc().sendEvent(newUpdateErrorEvent(static_cast<grpc::UpdateErrorType>(ui_.comboUpdateError->currentIndex())));
});
connect(ui_.buttonUpdateManualReady, &QPushButton::clicked, [&] {
app().grpc().sendEvent(newUpdateManualReadyEvent(ui_.editUpdateVersion->text()));
});
connect(ui_.buttonUpdateForce, &QPushButton::clicked, [&] {
app().grpc().sendEvent(newUpdateForceEvent(ui_.editUpdateVersion->text()));
});
connect(ui_.buttonUpdateManualRestart, &QPushButton::clicked, []() { app().grpc().sendEvent(newUpdateManualRestartNeededEvent()); });
connect(ui_.buttonUpdateSilentRestart, &QPushButton::clicked, []() { app().grpc().sendEvent(newUpdateSilentRestartNeededEvent()); });
connect(ui_.buttonUpdateIsLatest, &QPushButton::clicked, []() { app().grpc().sendEvent(newUpdateIsLatestVersionEvent()); });
connect(ui_.buttonUpdateCheckFinished, &QPushButton::clicked, []() { app().grpc().sendEvent(newUpdateCheckFinishedEvent()); });
connect(ui_.buttonUpdateVersionChanged, &QPushButton::clicked, []() { app().grpc().sendEvent(newUpdateVersionChangedEvent()); });
}
//****************************************************************************************************************************************************
/// \return The delay to apply before sending automatically generated events.
//****************************************************************************************************************************************************
qint32 EventsTab::eventDelayMs() const {
return ui_.spinEventDelay->value();
}
//****************************************************************************************************************************************************
/// \return The bug report results
//****************************************************************************************************************************************************
EventsTab::BugReportResult EventsTab::nextBugReportResult() const {
return static_cast<BugReportResult>(ui_.comboBugReportResult->currentIndex());
}
//****************************************************************************************************************************************************
/// \return The reply for the next IsPortFree gRPC call.
//****************************************************************************************************************************************************
bool EventsTab::isPortFree() const {
return ui_.checkIsPortFree->isChecked();
}
//****************************************************************************************************************************************************
/// \return The value for the 'Next Cache Change Will Succeed' check box.
//****************************************************************************************************************************************************
bool EventsTab::nextCacheChangeWillSucceed() const {
return ui_.checkNextCacheChangeWillSucceed->isChecked();
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void EventsTab::resetUI() const {
ui_.comboBugReportResult->setCurrentIndex(0);
ui_.checkIsPortFree->setChecked(true);
ui_.checkNextCacheChangeWillSucceed->setChecked(true);
}

View File

@ -0,0 +1,59 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
#ifndef BRIDGE_GUI_TESTER_EVENTS_TAB_H
#define BRIDGE_GUI_TESTER_EVENTS_TAB_H
#include "ui_EventsTab.h"
//****************************************************************************************************************************************************
/// \brief Events tabs
//****************************************************************************************************************************************************
class EventsTab: public QWidget {
Q_OBJECT
public: // data types
enum class BugReportResult {
Success = 0,
Error = 1,
DataSharingError = 2,
}; ///< Enumeration for the result of bug report sending
public: // member functions.
explicit EventsTab(QWidget *parent = nullptr); ///< Default constructor.
EventsTab(EventsTab const&) = delete; ///< Disabled copy-constructor.
EventsTab(EventsTab&&) = delete; ///< Disabled assignment copy-constructor.
~EventsTab() override = default; ///< Destructor.
EventsTab& operator=(EventsTab const&) = delete; ///< Disabled assignment operator.
EventsTab& operator=(EventsTab&&) = delete; ///< Disabled move assignment operator.
qint32 eventDelayMs() const; ///< Get the delay for sending automatically generated events.
BugReportResult nextBugReportResult() const; ///< Get the value of the 'Next bug report result' combo box.
bool isPortFree() const; ///< Get the value for the "Is Port Free" check box.
bool nextCacheChangeWillSucceed() const; ///< Get the value for the 'Next Cache Change will succeed' edit.
void resetUI() const; ///< Resets the UI.
private: // data members
Ui::EventsTab ui_ {}; ///< The UI for the widget.
};
#endif

View File

@ -0,0 +1,359 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>EventsTab</class>
<widget class="QWidget" name="EventsTab">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>563</width>
<height>571</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_11">
<item>
<widget class="QLabel" name="labelEventDelay">
<property name="toolTip">
<string>Delay applied before sending automatically generated events</string>
</property>
<property name="text">
<string>Delay for asynchronous events</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinEventDelay">
<property name="suffix">
<string> ms</string>
</property>
<property name="minimum">
<number>-1</number>
</property>
<property name="maximum">
<number>3600000</number>
</property>
<property name="singleStep">
<number>100</number>
</property>
<property name="value">
<number>0</number>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_5">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>1</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="title">
<string>Address related errors</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_9">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_12">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Address</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="editAddressErrors">
<property name="text">
<string>dummy.user@proton.me</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_15">
<item>
<widget class="QPushButton" name="buttonAddressChanged">
<property name="text">
<string>Address Changed</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="buttonAddressChangedLogout">
<property name="text">
<string>Address Changed Logout</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="2">
<widget class="QCheckBox" name="checkNextCacheChangeWillSucceed">
<property name="text">
<string>Next Cache Change will succeed</string>
</property>
</widget>
</item>
<item row="0" column="3">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>1</height>
</size>
</property>
</spacer>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="checkIsPortFree">
<property name="text">
<string>Reply true to the next 'Is Port Free' request.</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_13">
<item>
<widget class="QLabel" name="labelNextBugReportResult">
<property name="text">
<string>Next bug report result</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBugReportResult">
<item>
<property name="text">
<string>Success</string>
</property>
</item>
<item>
<property name="text">
<string>Error</string>
</property>
</item>
<item>
<property name="text">
<string>Data sharing error</string>
</property>
</item>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>1</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QGridLayout" name="gridLayout_6">
<property name="verticalSpacing">
<number>8</number>
</property>
<item row="0" column="0">
<widget class="QPushButton" name="buttonUpdateManualReady">
<property name="text">
<string>Update Manual Ready</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QLineEdit" name="editUpdateVersion">
<property name="text">
<string>4.0</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QPushButton" name="buttonUpdateVersionChanged">
<property name="text">
<string>Update version changed</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QPushButton" name="buttonUpdateForce">
<property name="text">
<string>Update Force</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QPushButton" name="buttonUpdateManualRestart">
<property name="text">
<string>Update manual restart</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QPushButton" name="buttonUpdateCheckFinished">
<property name="text">
<string>Update check finished</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="buttonUpdateSilentRestart">
<property name="text">
<string>Update silent restart</string>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QComboBox" name="comboUpdateError">
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToContents</enum>
</property>
<item>
<property name="text">
<string>Update manual error</string>
</property>
</item>
<item>
<property name="text">
<string>Update force error</string>
</property>
</item>
<item>
<property name="text">
<string>Update silent error</string>
</property>
</item>
</widget>
</item>
<item row="2" column="1">
<widget class="QPushButton" name="buttonUpdateError">
<property name="text">
<string>Update error</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QPushButton" name="buttonUpdateIsLatest">
<property name="text">
<string>Update is latest</string>
</property>
</widget>
</item>
<item row="4" column="1" colspan="2">
<spacer name="horizontalSpacer_6">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>1</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QGridLayout" name="gridLayout_5">
<property name="horizontalSpacing">
<number>-1</number>
</property>
<property name="verticalSpacing">
<number>8</number>
</property>
<item row="0" column="1">
<widget class="QPushButton" name="buttonInternetOn">
<property name="text">
<string>Internet On</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QPushButton" name="buttonShowMainWindow">
<property name="text">
<string>Show Main Window</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QPushButton" name="buttonInternetOff">
<property name="text">
<string>Internet Off</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="buttonAPICertIssue">
<property name="text">
<string>API Certficate Issue</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QPushButton" name="buttonNoKeychain">
<property name="text">
<string>No Keychain</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>200</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,96 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
#include "KnowledgeBaseTab.h"
#include "GRPCService.h"
#include "bridgepp/GRPC/EventFactory.h"
using namespace bridgepp;
//****************************************************************************************************************************************************
/// \param[in] parent The parent widget of the tab.
//****************************************************************************************************************************************************
KnowledgeBaseTab::KnowledgeBaseTab(QWidget* parent)
: QWidget(parent) {
ui_.setupUi(this);
connect(ui_.checkSuggestion1, &QCheckBox::stateChanged, this, &KnowledgeBaseTab::updateGuiState);
connect(ui_.checkSuggestion2, &QCheckBox::stateChanged, this, &KnowledgeBaseTab::updateGuiState);
connect(ui_.checkSuggestion3, &QCheckBox::stateChanged, this, &KnowledgeBaseTab::updateGuiState);
connect(ui_.buttonSend, &QCheckBox::clicked, this, &KnowledgeBaseTab::sendKnowledgeBaseSuggestions);
}
//****************************************************************************************************************************************************
/// \param[in] checkbox The check box.
/// \param[in] widgets The widgets to conditionally enable.
//****************************************************************************************************************************************************
void enableWidgetsIfChecked(QCheckBox const* checkbox, QWidgetList const& widgets) {
bool const checked = checkbox->isChecked();
for (QWidget *const widget: widgets) {
widget->setEnabled(checked);
}
}
//****************************************************************************************************************************************************
/// \return The suggestions.
//****************************************************************************************************************************************************
QList<KnowledgeBaseSuggestion> KnowledgeBaseTab::getSuggestions() const {
QList<KnowledgeBaseSuggestion> result;
if (ui_.checkSuggestion1->isChecked()) {
result.push_back({ .url = ui_.editUrl1->text(), .title = ui_.editTitle1->text() });
}
if (ui_.checkSuggestion2->isChecked()) {
result.push_back({ .url = ui_.editUrl2->text(), .title = ui_.editTitle2->text() });
}
if (ui_.checkSuggestion3->isChecked()) {
result.push_back({ .url = ui_.editUrl3->text(), .title = ui_.editTitle3->text() });
}
return result;
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void KnowledgeBaseTab::sendKnowledgeBaseSuggestions() const {
app().grpc().sendEvent(newKnowledgeBaseSuggestionsEvent(this->getSuggestions()));
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void KnowledgeBaseTab::updateGuiState() {
enableWidgetsIfChecked(ui_.checkSuggestion1, { ui_.labelTitle1, ui_.editTitle1, ui_.labelUrl1, ui_.editUrl1});
enableWidgetsIfChecked(ui_.checkSuggestion2, { ui_.labelTitle2, ui_.editTitle2, ui_.labelUrl2, ui_.editUrl2});
enableWidgetsIfChecked(ui_.checkSuggestion3, { ui_.labelTitle3, ui_.editTitle3, ui_.labelUrl3, ui_.editUrl3});
}
//****************************************************************************************************************************************************
/// \param[in] userInput The user input.
//****************************************************************************************************************************************************
void KnowledgeBaseTab::requestKnowledgeBaseSuggestions(QString const& userInput) const {
ui_.editUserInput->setPlainText(userInput);
ui_.labelLastReceived->setText(tr("Last received: %1").arg(QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm:ss.zzz")));
}

View File

@ -0,0 +1,53 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
#ifndef BRIDGE_GUI_TESTER_KNOWLEDGE_BASE_TAB_H
#define BRIDGE_GUI_TESTER_KNOWLEDGE_BASE_TAB_H
#include "ui_KnowledgeBaseTab.h"
#include <bridgepp/GRPC/GRPCClient.h>
//****************************************************************************************************************************************************
/// \brief Knowledge base table.
//****************************************************************************************************************************************************
class KnowledgeBaseTab: public QWidget {
public: // member functions.
explicit KnowledgeBaseTab(QWidget *parent = nullptr); ///< Default constructor.
KnowledgeBaseTab(KnowledgeBaseTab const&) = delete; ///< Disabled copy-constructor.
KnowledgeBaseTab(KnowledgeBaseTab&&) = delete; ///< Disabled assignment copy-constructor.
~KnowledgeBaseTab() override = default; ///< Destructor.
KnowledgeBaseTab& operator=(KnowledgeBaseTab const&) = delete; ///< Disabled assignment operator.
KnowledgeBaseTab& operator=(KnowledgeBaseTab&&) = delete; ///< Disabled move assignment operator.
QList<bridgepp::KnowledgeBaseSuggestion> getSuggestions() const; ///< Returns the suggestions.
public slots:
void requestKnowledgeBaseSuggestions(QString const &userInput) const; ///< Slot for the 'RequestKnowledgeBaseSuggestions' gRPC call.
private slots:
void sendKnowledgeBaseSuggestions() const; ///< Send a KnowledgeBaseSuggestions event.
void updateGuiState(); ///< Update the GUI state.
private: // data members
Ui::KnowledgeBaseTab ui_ {}; ///< The UI for the widget.
};
#endif //BRIDGE_GUI_TESTER_KNOWLEDGE_BASE_TAB_H

View File

@ -0,0 +1,239 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>KnowledgeBaseTab</class>
<widget class="QWidget" name="KnowledgeBaseTab">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>857</width>
<height>905</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QGroupBox" name="groupUserInput">
<property name="title">
<string>User Input</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QPlainTextEdit" name="editUserInput">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="labelLastReceived">
<property name="text">
<string>Last received: never</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="groupBoxSuggestion1">
<property name="title">
<string>Suggestion 1</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0" colspan="2">
<widget class="QCheckBox" name="checkSuggestion1">
<property name="text">
<string>Enabled</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="labelTitle1">
<property name="text">
<string>Title</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="editTitle1">
<property name="text">
<string>Automatically start Bridge</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="labelUrl1">
<property name="text">
<string>URL</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="editUrl1">
<property name="text">
<string>https://proton.me/support/automatically-start-bridge</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBoxSuggestion2">
<property name="title">
<string>Suggestion 2</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0" colspan="2">
<widget class="QCheckBox" name="checkSuggestion2">
<property name="text">
<string>Enabled</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="labelTitle2">
<property name="text">
<string>Title</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="editTitle2">
<property name="text">
<string>Proton Mail Bridge connection issues with Thunderbird, Outlook, and Apple Mail</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="labelUrl2">
<property name="text">
<string>URL</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="editUrl2">
<property name="text">
<string>https://proton.me/support/bridge-ssl-connection-issue</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBoxSuggestion3">
<property name="title">
<string>Suggestion 3</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0" colspan="2">
<widget class="QCheckBox" name="checkSuggestion3">
<property name="text">
<string>Enabled</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="labelTitle3">
<property name="text">
<string>Title</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="editTitle3">
<property name="text">
<string>Difference between combined addresses mode and split addresses mode</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="labelUrl3">
<property name="text">
<string>URL</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="editUrl3">
<property name="text">
<string>https://proton.me/support/difference-combined-addresses-mode-split-addresses-mode</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="buttonSend">
<property name="text">
<string>Send Knowledge Base Suggestions</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>208</width>
<height>289</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -18,7 +18,6 @@
#include "SettingsTab.h"
#include "GRPCService.h"
#include <bridgepp/GRPC/EventFactory.h>
#include <bridgepp/BridgeUtils.h>
@ -31,18 +30,6 @@ QString const colorSchemeLight = "light"; ///< THe light color scheme name.
}
//****************************************************************************************************************************************************
/// \brief Connect an address error button to the generation of an address error event.
///
/// \param[in] button The error button.
/// \param[in] edit The edit containing the address.
/// \param[in] eventGenerator The factory function creating the event.
//****************************************************************************************************************************************************
void connectAddressError(QPushButton *button, QLineEdit* edit, bridgepp::SPStreamEvent (*eventGenerator)(QString const &)) {
QObject::connect(button, &QPushButton::clicked, [edit, eventGenerator]() { app().grpc().sendEvent(eventGenerator(edit->text())); });
}
//****************************************************************************************************************************************************
/// \param[in] parent The parent widget of the tab.
//****************************************************************************************************************************************************
@ -50,29 +37,6 @@ 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_.buttonNoKeychain, &QPushButton::clicked, []() { app().grpc().sendEvent(newHasNoKeychainEvent()); });
connect(ui_.buttonAPICertIssue, &QPushButton::clicked, []() { app().grpc().sendEvent(newApiCertIssueEvent()); });
connectAddressError(ui_.buttonAddressChanged, ui_.editAddressErrors, newAddressChangedEvent);
connectAddressError(ui_.buttonAddressChangedLogout, ui_.editAddressErrors, newAddressChangedLogoutEvent);
connect(ui_.checkNextCacheChangeWillSucceed, &QCheckBox::toggled, this, &SettingsTab::updateGUIState);
connect(ui_.buttonUpdateError, &QPushButton::clicked, [&]() {
app().grpc().sendEvent(newUpdateErrorEvent(static_cast<grpc::UpdateErrorType>(ui_.comboUpdateError->currentIndex())));
});
connect(ui_.buttonUpdateManualReady, &QPushButton::clicked, [&] {
app().grpc().sendEvent(newUpdateManualReadyEvent(ui_.editUpdateVersion->text()));
});
connect(ui_.buttonUpdateForce, &QPushButton::clicked, [&] {
app().grpc().sendEvent(newUpdateForceEvent(ui_.editUpdateVersion->text()));
});
connect(ui_.buttonUpdateManualRestart, &QPushButton::clicked, []() { app().grpc().sendEvent(newUpdateManualRestartNeededEvent()); });
connect(ui_.buttonUpdateSilentRestart, &QPushButton::clicked, []() { app().grpc().sendEvent(newUpdateSilentRestartNeededEvent()); });
connect(ui_.buttonUpdateIsLatest, &QPushButton::clicked, []() { app().grpc().sendEvent(newUpdateIsLatestVersionEvent()); });
connect(ui_.buttonUpdateCheckFinished, &QPushButton::clicked, []() { app().grpc().sendEvent(newUpdateCheckFinishedEvent()); });
connect(ui_.buttonUpdateVersionChanged, &QPushButton::clicked, []() { app().grpc().sendEvent(newUpdateVersionChangedEvent()); });
this->resetUI();
this->updateGUIState();
}
@ -82,7 +46,7 @@ SettingsTab::SettingsTab(QWidget *parent)
//
//****************************************************************************************************************************************************
void SettingsTab::updateGUIState() {
bool connected = app().grpc().isStreaming();
bool const connected = app().grpc().isStreaming();
for (QWidget *widget: { ui_.groupVersion, ui_.groupGeneral, ui_.groupMail, ui_.groupPaths, ui_.groupCache }) {
widget->setEnabled(!connected);
}
@ -101,7 +65,7 @@ void SettingsTab::setIsStreaming(bool isStreaming) {
//****************************************************************************************************************************************************
/// \param[in] clientPlatform The client platform.
//****************************************************************************************************************************************************
void SettingsTab::setClientPlatform(QString const &clientPlatform) {
void SettingsTab::setClientPlatform(QString const &clientPlatform) const {
ui_.labelClientPlatformValue->setText(clientPlatform);
}
@ -166,7 +130,7 @@ bool SettingsTab::isAutostartOn() const {
//****************************************************************************************************************************************************
/// \param[in] on Should autostart be turned on?
//****************************************************************************************************************************************************
void SettingsTab::setIsAutostartOn(bool on) {
void SettingsTab::setIsAutostartOn(bool on) const {
ui_.checkAutostart->setChecked(on);
}
@ -182,7 +146,7 @@ QString SettingsTab::colorSchemeName() const {
//****************************************************************************************************************************************************
/// \param[in] name True if the 'Use Dark Theme' check box should be checked.
//****************************************************************************************************************************************************
void SettingsTab::setColorSchemeName(QString const &name) {
void SettingsTab::setColorSchemeName(QString const &name) const {
ui_.checkDarkTheme->setChecked(name == colorSchemeDark);
}
@ -198,7 +162,7 @@ bool SettingsTab::isBetaEnabled() const {
//****************************************************************************************************************************************************
/// \param[in] enabled The new state for the 'Beta Enabled' check box.
//****************************************************************************************************************************************************
void SettingsTab::setIsBetaEnabled(bool enabled) {
void SettingsTab::setIsBetaEnabled(bool enabled) const {
ui_.checkBetaEnabled->setChecked(enabled);
}
@ -214,7 +178,7 @@ bool SettingsTab::isAllMailVisible() const {
//****************************************************************************************************************************************************
/// \param[in] visible The new value for the 'All Mail Visible' check box.
//****************************************************************************************************************************************************
void SettingsTab::setIsAllMailVisible(bool visible) {
void SettingsTab::setIsAllMailVisible(bool visible) const {
ui_.checkAllMailVisible->setChecked(visible);
}
@ -230,19 +194,11 @@ bool SettingsTab::isTelemetryDisabled() const {
//****************************************************************************************************************************************************
/// \param[in] isDisabled The new value for the 'Disable Telemetry' check box.
//****************************************************************************************************************************************************
void SettingsTab::setIsTelemetryDisabled(bool isDisabled) {
void SettingsTab::setIsTelemetryDisabled(bool isDisabled) const {
ui_.checkIsTelemetryDisabled->setChecked(isDisabled);
}
//****************************************************************************************************************************************************
/// \return The delay to apply before sending automatically generated events.
//****************************************************************************************************************************************************
qint32 SettingsTab::eventDelayMs() const {
return ui_.spinEventDelay->value();
}
//****************************************************************************************************************************************************
/// \return The path
//****************************************************************************************************************************************************
@ -292,7 +248,7 @@ QString SettingsTab::landingPageLink() const {
/// \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) {
QString const &description, bool includeLogs) const {
ui_.editOSType->setText(osType);
ui_.editOSVersion->setText(osVersion);
ui_.editEmailClient->setText(emailClient);
@ -305,7 +261,7 @@ void SettingsTab::setBugReport(QString const &osType, QString const &osVersion,
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void SettingsTab::installTLSCertificate() {
void SettingsTab::installTLSCertificate() const {
ui_.labelLastTLSCertInstall->setText(QString("Last install: %1").arg(QDateTime::currentDateTime().toString(Qt::ISODateWithMs)));
ui_.checkTLSCertIsInstalled->setChecked(this->nextTLSCertInstallResult() == TLSCertInstallResult::Success);
}
@ -314,19 +270,11 @@ void SettingsTab::installTLSCertificate() {
//****************************************************************************************************************************************************
/// \param[in] folderPath The folder path.
//****************************************************************************************************************************************************
void SettingsTab::exportTLSCertificates(QString const &folderPath) {
void SettingsTab::exportTLSCertificates(QString const &folderPath) const {
ui_.labeLastTLSCertExport->setText(QString("%1 Export to %2").arg(QDateTime::currentDateTime().toString(Qt::ISODateWithMs),folderPath));
}
//****************************************************************************************************************************************************
/// \return The state of the check box.
//****************************************************************************************************************************************************
SettingsTab::BugReportResult SettingsTab::nextBugReportResult() const {
return BugReportResult(ui_.comboBugReportResult->currentIndex());
}
//****************************************************************************************************************************************************
/// \return the state of the 'TLS Certificate is installed' check box.
//****************************************************************************************************************************************************
@ -339,7 +287,7 @@ bool SettingsTab::isTLSCertificateInstalled() const {
/// \return The value for the 'Next TLS cert install result'.
//****************************************************************************************************************************************************
SettingsTab::TLSCertInstallResult SettingsTab::nextTLSCertInstallResult() const {
return TLSCertInstallResult(ui_.comboNextTLSCertInstallResult->currentIndex());
return static_cast<TLSCertInstallResult>(ui_.comboNextTLSCertInstallResult->currentIndex());
}
@ -370,7 +318,7 @@ QString SettingsTab::hostname() const {
//****************************************************************************************************************************************************
/// \return The value of the IMAP port spin box.
//****************************************************************************************************************************************************
qint32 SettingsTab::imapPort() {
qint32 SettingsTab::imapPort() const {
return ui_.spinPortIMAP->value();
}
@ -378,7 +326,7 @@ qint32 SettingsTab::imapPort() {
//****************************************************************************************************************************************************
/// \return The value of the SMTP port spin box.
//****************************************************************************************************************************************************
qint32 SettingsTab::smtpPort() {
qint32 SettingsTab::smtpPort() const {
return ui_.spinPortSMTP->value();
}
@ -389,7 +337,7 @@ qint32 SettingsTab::smtpPort() {
/// \param[in] useSSLForIMAP The IMAP connexion mode.
/// \param[in] useSSLForSMTP The IMAP connexion mode.
//****************************************************************************************************************************************************
void SettingsTab::setMailServerSettings(qint32 imapPort, qint32 smtpPort, bool useSSLForIMAP, bool useSSLForSMTP) {
void SettingsTab::setMailServerSettings(qint32 imapPort, qint32 smtpPort, bool useSSLForIMAP, bool useSSLForSMTP) const {
ui_.spinPortIMAP->setValue(imapPort);
ui_.spinPortSMTP->setValue(smtpPort);
ui_.checkUseSSLForIMAP->setChecked(useSSLForIMAP);
@ -424,23 +372,15 @@ bool SettingsTab::isDoHEnabled() const {
//****************************************************************************************************************************************************
/// \param[in] enabled The state of the 'DoH enabled' check box.
//****************************************************************************************************************************************************
void SettingsTab::setIsDoHEnabled(bool enabled) {
void SettingsTab::setIsDoHEnabled(bool enabled) const {
ui_.checkDoHEnabled->setChecked(enabled);
}
//****************************************************************************************************************************************************
/// \return The reply for the next IsPortFree gRPC call.
//****************************************************************************************************************************************************
bool SettingsTab::isPortFree() const {
return ui_.checkIsPortFree->isChecked();
}
//****************************************************************************************************************************************************
/// \param[in] path The path of the local cache.
//****************************************************************************************************************************************************
void SettingsTab::setDiskCachePath(const QString &path) {
void SettingsTab::setDiskCachePath(const QString &path) const {
ui_.editDiskCachePath->setText(path);
}
@ -453,14 +393,6 @@ QString SettingsTab::diskCachePath() const {
}
//****************************************************************************************************************************************************
/// \return The value for the 'Next Cache Change Will Succeed' check box.
//****************************************************************************************************************************************************
bool SettingsTab::nextCacheChangeWillSucceed() const {
return ui_.checkNextCacheChangeWillSucceed->isChecked();
}
//****************************************************************************************************************************************************
/// \return the value for the 'Automatic Update' check.
//****************************************************************************************************************************************************
@ -472,7 +404,7 @@ bool SettingsTab::isAutomaticUpdateOn() const {
//****************************************************************************************************************************************************
/// \param[in] on The value for the 'Automatic Update' check.
//****************************************************************************************************************************************************
void SettingsTab::setIsAutomaticUpdateOn(bool on) {
void SettingsTab::setIsAutomaticUpdateOn(bool on) const {
ui_.checkAutomaticUpdate->setChecked(on);
}
@ -521,19 +453,16 @@ void SettingsTab::resetUI() {
ui_.editAddress->setText(QString());
ui_.editDescription->setPlainText(QString());
ui_.labelIncludeLogsValue->setText(QString());
ui_.comboBugReportResult->setCurrentIndex(0);
ui_.editHostname->setText("localhost");
ui_.spinPortIMAP->setValue(1143);
ui_.spinPortSMTP->setValue(1025);
ui_.checkUseSSLForSMTP->setChecked(false);
ui_.checkDoHEnabled->setChecked(true);
ui_.checkIsPortFree->setChecked(true);
QString const cacheDir = QDir(tmpDir).absoluteFilePath("cache");
QDir().mkpath(cacheDir);
ui_.editDiskCachePath->setText(QDir::toNativeSeparators(cacheDir));
ui_.checkNextCacheChangeWillSucceed->setChecked(true);
ui_.checkAutomaticUpdate->setChecked(true);

View File

@ -35,12 +35,6 @@ public: // data types.
Failure = 2
}; ///< Enumeration for the result of a TLS certificate installation.
enum class BugReportResult {
Success = 0,
Error = 1,
DataSharingError = 2,
}; ///< Enumeration for the result of bug report sending
public: // member functions.
explicit SettingsTab(QWidget *parent = nullptr); ///< Default constructor.
SettingsTab(SettingsTab const &) = delete; ///< Disabled copy-constructor.
@ -60,45 +54,41 @@ public: // member functions.
bool isAllMailVisible() const; ///< Get the value for the 'All Mail Visible' check.
bool isTelemetryDisabled() const; ///< Get the value for the 'Disable Telemetry' check box.
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.
BugReportResult nextBugReportResult() const; ///< Get the value of the 'Next bug report result' combo box.
bool isTLSCertificateInstalled() const; ///< Get the status of the 'TLS Certificate is installed' check box.
TLSCertInstallResult nextTLSCertInstallResult() const; ///< Get the value of the 'Next TLS Certificate install result' combo box.
bool nextTLSCertExportWillSucceed() const; ///< Get the status of the 'Next TLS Cert export will succeed' check box.
bool nextTLSKeyExportWillSucceed() const; ///< Get the status of the 'Next TLS Key export will succeed' 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.
qint32 imapPort() const; ///< Get the value of the IMAP port spin.
qint32 smtpPort() const; ///< Get the value of the SMTP port spin.
bool useSSLForSMTP() const; ///< Get the value for the 'Use SSL for SMTP' check box.
bool useSSLForIMAP() const; ///< Get the value for the 'Use SSL for IMAP' 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.
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.
bool isAutomaticUpdateOn() const; ///<Get the value for the 'Automatic Update' check box.
public slots:
void updateGUIState(); ///< Update the GUI state.
void setIsStreaming(bool isStreaming); ///< Set the isStreamingEvents value.
void setClientPlatform(QString const &clientPlatform); ///< Set the client platform.
void setIsAutostartOn(bool on); ///< Set the value for the 'Autostart' check box.
void setIsBetaEnabled(bool enabled); ///< Set the value for the 'Beta Enabled' check box.
void setIsAllMailVisible(bool visible); ///< Set the value for the 'All Mail Visible' check box.
void setIsTelemetryDisabled(bool isDisabled); ///< Set the value for the 'Disable Telemetry' check box.
void setColorSchemeName(QString const &name); ///< Set the value for the 'Use Dark Theme' check box.
void setClientPlatform(QString const &clientPlatform) const; ///< Set the client platform.
void setIsAutostartOn(bool on) const; ///< Set the value for the 'Autostart' check box.
void setIsBetaEnabled(bool enabled) const; ///< Set the value for the 'Beta Enabled' check box.
void setIsAllMailVisible(bool visible) const; ///< Set the value for the 'All Mail Visible' check box.
void setIsTelemetryDisabled(bool isDisabled) const; ///< Set the value for the 'Disable Telemetry' check box.
void setColorSchemeName(QString const &name) const; ///< Set the value for the 'Use Dark Theme' check box.
void setBugReport(QString const &osType, QString const &osVersion, QString const &emailClient, QString const &address, QString const &description,
bool includeLogs); ///< Set the content of the bug report box.
void installTLSCertificate(); ///< Install the TLS certificate.
void exportTLSCertificates(QString const &folderPath); ///< Export the TLS certificates.
void setMailServerSettings(qint32 imapPort, qint32 smtpPort, bool useSSLForIMAP, bool useSSLForSMTP); ///< Change the mail server settings.
void setIsDoHEnabled(bool enabled); ///< Set the value for the 'DoH Enabled' check box.
void setDiskCachePath(QString const &path); ///< Set the value for the 'Cache On Disk Enabled' check box.
void setIsAutomaticUpdateOn(bool on); ///< Set the value for the 'Automatic Update' check box.
bool includeLogs) const; ///< Set the content of the bug report box.
void installTLSCertificate() const; ///< Install the TLS certificate.
void exportTLSCertificates(QString const &folderPath) const; ///< Export the TLS certificates.
void setMailServerSettings(qint32 imapPort, qint32 smtpPort, bool useSSLForIMAP, bool useSSLForSMTP) const; ///< Change the mail server settings.
void setIsDoHEnabled(bool enabled) const; ///< Set the value for the 'DoH Enabled' check box.
void setDiskCachePath(QString const &path) const; ///< Set the value for the 'Cache On Disk Enabled' check box.
void setIsAutomaticUpdateOn(bool on) const; ///< Set the value for the 'Automatic Update' check box.
private: // member functions.
void resetUI(); ///< Reset the widget.

View File

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>1160</width>
<height>777</height>
<width>1146</width>
<height>716</height>
</rect>
</property>
<property name="windowTitle">
@ -208,7 +208,7 @@
<widget class="QLineEdit" name="editHostname">
<property name="minimumSize">
<size>
<width>200</width>
<width>0</width>
<height>0</height>
</size>
</property>
@ -749,328 +749,6 @@
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupApp">
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="title">
<string>Events &amp;&amp; Errors</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_8">
<property name="spacing">
<number>4</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_11">
<item>
<widget class="QLabel" name="labelEventDelay">
<property name="toolTip">
<string>Delay applied before sending automatically generated events</string>
</property>
<property name="text">
<string>Delay for asynchronous events</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinEventDelay">
<property name="suffix">
<string> ms</string>
</property>
<property name="minimum">
<number>-1</number>
</property>
<property name="maximum">
<number>3600000</number>
</property>
<property name="singleStep">
<number>100</number>
</property>
<property name="value">
<number>0</number>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_5">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>1</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QGridLayout" name="gridLayout_5">
<property name="horizontalSpacing">
<number>-1</number>
</property>
<property name="verticalSpacing">
<number>8</number>
</property>
<item row="0" column="1">
<widget class="QPushButton" name="buttonInternetOn">
<property name="text">
<string>Internet On</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QPushButton" name="buttonShowMainWindow">
<property name="text">
<string>Show Main Window</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QPushButton" name="buttonInternetOff">
<property name="text">
<string>Internet Off</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="buttonAPICertIssue">
<property name="text">
<string>API Certficate Issue</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QPushButton" name="buttonNoKeychain">
<property name="text">
<string>No Keychain</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="title">
<string>Address related errors</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_9">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_12">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Address</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="editAddressErrors">
<property name="text">
<string>dummy.user@proton.me</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_15">
<item>
<widget class="QPushButton" name="buttonAddressChanged">
<property name="text">
<string>Address Changed</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="buttonAddressChangedLogout">
<property name="text">
<string>Address Changed Logout</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkIsPortFree">
<property name="text">
<string>Reply true to the next 'Is Port Free' request.</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkNextCacheChangeWillSucceed">
<property name="text">
<string>Next Cache Change will succeed</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_13">
<item>
<widget class="QLabel" name="labelNextBugReportResult">
<property name="text">
<string>Next bug report result</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBugReportResult">
<item>
<property name="text">
<string>Success</string>
</property>
</item>
<item>
<property name="text">
<string>Error</string>
</property>
</item>
<item>
<property name="text">
<string>Data sharing error</string>
</property>
</item>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>1</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QGridLayout" name="gridLayout_6">
<property name="verticalSpacing">
<number>8</number>
</property>
<item row="0" column="0">
<widget class="QPushButton" name="buttonUpdateManualReady">
<property name="text">
<string>Update Manual Ready</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QLineEdit" name="editUpdateVersion">
<property name="text">
<string>4.0</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QPushButton" name="buttonUpdateVersionChanged">
<property name="text">
<string>Update version changed</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QPushButton" name="buttonUpdateForce">
<property name="text">
<string>Update Force</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QPushButton" name="buttonUpdateManualRestart">
<property name="text">
<string>Update manual restart</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QPushButton" name="buttonUpdateCheckFinished">
<property name="text">
<string>Update check finished</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="buttonUpdateSilentRestart">
<property name="text">
<string>Update silent restart</string>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QComboBox" name="comboUpdateError">
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToContents</enum>
</property>
<item>
<property name="text">
<string>Update manual error</string>
</property>
</item>
<item>
<property name="text">
<string>Update force error</string>
</property>
</item>
<item>
<property name="text">
<string>Update silent error</string>
</property>
</item>
</widget>
</item>
<item row="2" column="1">
<widget class="QPushButton" name="buttonUpdateError">
<property name="text">
<string>Update error</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QPushButton" name="buttonUpdateIsLatest">
<property name="text">
<string>Update is latest</string>
</property>
</widget>
</item>
<item row="4" column="1" colspan="2">
<spacer name="horizontalSpacer_6">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>1</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</item>
</layout>
@ -1096,8 +774,6 @@
<tabstop>editDiskCachePath</tabstop>
<tabstop>editOSVersion</tabstop>
<tabstop>editEmailClient</tabstop>
<tabstop>spinEventDelay</tabstop>
<tabstop>checkIsPortFree</tabstop>
</tabstops>
<resources/>
<connections/>

View File

@ -85,7 +85,7 @@ void UsersTab::onAddUserButton() {
//
//****************************************************************************************************************************************************
void UsersTab::onEditUserButton() {
int index = selectedIndex();
const int index = selectedIndex();
if ((index < 0) || (index >= users_.userCount())) {
return;
}
@ -110,7 +110,7 @@ void UsersTab::onEditUserButton() {
//
//****************************************************************************************************************************************************
void UsersTab::onRemoveUserButton() {
int index = selectedIndex();
const int index = selectedIndex();
if ((index < 0) || (index >= users_.userCount())) {
return;
}
@ -127,7 +127,7 @@ void UsersTab::onRemoveUserButton() {
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
void UsersTab::onSelectionChanged(QItemSelection, QItemSelection) {
void UsersTab::onSelectionChanged(QItemSelection const&, QItemSelection const&) {
this->updateGUIState();
}
@ -137,7 +137,6 @@ void UsersTab::onSelectionChanged(QItemSelection, QItemSelection) {
//****************************************************************************************************************************************************
void UsersTab::onSendUserBadEvent() {
SPUser const user = selectedUser();
int const index = this->selectedIndex();
if (!user) {
app().log().error(QString("%1 failed. Unkown user.").arg(__FUNCTION__));
@ -175,8 +174,8 @@ void UsersTab::onSendUsedBytesChangedEvent() {
app().log().error(QString("%1 failed. User is not connected").arg(__FUNCTION__));
}
qint64 const usedBytes = qint64(ui_.spinUsedBytes->value());
user->setUsedBytes(usedBytes);
auto const usedBytes = static_cast<qint64>(ui_.spinUsedBytes->value());
user->setUsedBytes(static_cast<float>(usedBytes));
users_.touch(index);
GRPCService &grpc = app().grpc();
@ -224,9 +223,10 @@ void UsersTab::updateGUIState() {
QSignalBlocker b(ui_.checkSync);
bool const syncing = user && user->isSyncing();
ui_.checkSync->setChecked(syncing);
// ReSharper disable once CppDFAUnusedValue
b = QSignalBlocker(ui_.sliderSync);
ui_.sliderSync->setEnabled(syncing);
qint32 const progressPercent = syncing ? qint32(user->syncProgress() * 100.0f) : 0;
qint32 const progressPercent = syncing ? static_cast<qint32>(user->syncProgress() * 100.0f) : 0;
ui_.sliderSync->setValue(progressPercent);
ui_.labelSync->setText(syncing ? QString("%1%").arg(progressPercent) : "" );
}
@ -418,7 +418,7 @@ void UsersTab::processBadEventUserFeedback(QString const &userID, bool doResync)
return; // we do not do any form of emulation for resync.
}
SPUser user = users_.userWithID(userID);
SPUser const user = users_.userWithID(userID);
if (!user) {
app().log().error(QString("%1(): could not find user with id %1.").arg(__func__, userID));
}
@ -464,12 +464,12 @@ void UsersTab::onCheckSyncToggled(bool checked) {
//****************************************************************************************************************************************************
void UsersTab::onSliderSyncValueChanged(int value) {
SPUser const user = this->selectedUser();
if ((!user) || (!user->isSyncing()) || user->syncProgress() == value) {
if ((!user) || (!user->isSyncing()) || user->syncProgress() == static_cast<float>(value)) {
return;
}
double const progress = value / 100.0;
user->setSyncProgress(progress);
user->setSyncProgress(static_cast<float>(progress));
app().grpc().sendEvent(newSyncProgressEvent(user->id(), progress, 1, 1)); // we do not simulate elapsed & remaining.
this->updateGUIState();
}

View File

@ -53,14 +53,14 @@ 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.
static void configureUserAppleMail(QString const &userID, QString const &address); ///< Slot for the configuration of Apple mail.
void processBadEventUserFeedback(QString const& userID, bool doResync); ///< Slot for the reception of a bad event user feedback.
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.
void onSelectionChanged(QItemSelection const&, QItemSelection const&); ///< Slot for the change of the selection.
void onSendUserBadEvent(); ///< Slot for the 'Send Bad Event Error' button.
void onSendUsedBytesChangedEvent(); ///< Slot for the 'Send Used Bytes Changed Event' button.
void onSendIMAPLoginFailedEvent(); ///< Slot for the 'Send IMAP Login failure Event' button.

View File

@ -26,7 +26,7 @@ using namespace bridgepp;
/// \param[in] user The user.
/// \param[in] parent The parent widget of the dialog.
//****************************************************************************************************************************************************
UserDialog::UserDialog(bridgepp::SPUser &user, QWidget *parent)
UserDialog::UserDialog(const bridgepp::SPUser &user, QWidget *parent)
: QDialog(parent)
, user_(user) {
ui_.setupUi(this);
@ -57,8 +57,8 @@ void UserDialog::onOK() {
user_->setAvatarText(ui_.editAvatarText->text());
user_->setState(this->state());
user_->setSplitMode(ui_.checkSplitMode->isChecked());
user_->setUsedBytes(float(ui_.spinUsedBytes->value()));
user_->setTotalBytes(float(ui_.spinTotalBytes->value()));
user_->setUsedBytes(static_cast<float>(ui_.spinUsedBytes->value()));
user_->setTotalBytes(static_cast<float>(ui_.spinTotalBytes->value()));
this->accept();
}
@ -67,14 +67,14 @@ void UserDialog::onOK() {
//****************************************************************************************************************************************************
/// \return The user state that is currently selected in the dialog.
//****************************************************************************************************************************************************
UserState UserDialog::state() {
return UserState(ui_.comboState->currentIndex());
UserState UserDialog::state() const {
return static_cast<UserState>(ui_.comboState->currentIndex());
}
//****************************************************************************************************************************************************
/// \param[in] state The user state to select in the dialog.
//****************************************************************************************************************************************************
void UserDialog::setState(UserState state) {
ui_.comboState->setCurrentIndex(qint32(state));
void UserDialog::setState(UserState state) const {
ui_.comboState->setCurrentIndex(static_cast<qint32>(state));
}

View File

@ -30,7 +30,7 @@
class UserDialog : public QDialog {
Q_OBJECT
public: // member functions.
UserDialog(bridgepp::SPUser &user, QWidget *parent); ///< Default constructor.
UserDialog(const 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.
@ -38,8 +38,8 @@ public: // member functions.
UserDialog &operator=(UserDialog &&) = delete; ///< Disabled move assignment operator.
private: // member functions
bridgepp::UserState state(); ///< Get the user state selected in the dialog.
void setState(bridgepp::UserState state); ///< Set the user state selected in the dialog
bridgepp::UserState state() const; ///< Get the user state selected in the dialog.
void setState(bridgepp::UserState state) const; ///< Set the user state selected in the dialog
private slots:
void onOK(); ///< Slot for the OK button.

View File

@ -35,7 +35,7 @@ UserTable::UserTable(QObject *parent)
/// \return The number of rows in the table.
//****************************************************************************************************************************************************
int UserTable::rowCount(QModelIndex const &) const {
return users_.size();
return static_cast<int>(users_.size());
}
@ -111,7 +111,7 @@ QVariant UserTable::headerData(int section, Qt::Orientation orientation, int rol
/// \param[in] user The user to add.
//****************************************************************************************************************************************************
void UserTable::append(SPUser const &user) {
qint32 const count = users_.size();
qint32 const count = static_cast<int>(users_.size());
this->beginInsertRows(QModelIndex(), count, count);
users_.append(user);
this->endInsertRows();
@ -122,7 +122,7 @@ void UserTable::append(SPUser const &user) {
/// \return The number of users in the table.
//****************************************************************************************************************************************************
qint32 UserTable::userCount() const {
return users_.count();
return static_cast<qint32>(users_.count());
}
@ -141,7 +141,7 @@ bridgepp::SPUser UserTable::userAtIndex(qint32 index) {
/// \return A null pointer if the user is not in the list.
//****************************************************************************************************************************************************
bridgepp::SPUser UserTable::userWithID(QString const &userID) {
QList<SPUser>::const_iterator it = std::find_if(users_.constBegin(), users_.constEnd(), [&userID](SPUser const &user) -> bool {
QList<SPUser>::const_iterator const it = std::find_if(users_.constBegin(), users_.constEnd(), [&userID](SPUser const &user) -> bool {
return user->id() == userID;
});
@ -155,7 +155,7 @@ bridgepp::SPUser UserTable::userWithID(QString const &userID) {
/// \return A null pointer if the user is not in the list.
//****************************************************************************************************************************************************
bridgepp::SPUser UserTable::userWithUsernameOrEmail(QString const &username) {
QList<SPUser>::const_iterator it = std::find_if(users_.constBegin(), users_.constEnd(), [&username](SPUser const &user) -> bool {
QList<SPUser>::const_iterator const it = std::find_if(users_.constBegin(), users_.constEnd(), [&username](SPUser const &user) -> bool {
if (user->username().compare(username, Qt::CaseInsensitive) == 0) {
return true;
}
@ -172,7 +172,7 @@ bridgepp::SPUser UserTable::userWithUsernameOrEmail(QString const &username) {
/// \return -1 if the user could not be found.
//****************************************************************************************************************************************************
qint32 UserTable::indexOfUser(QString const &userID) {
QList<SPUser>::const_iterator it = std::find_if(users_.constBegin(), users_.constEnd(), [&userID](SPUser const &user) -> bool {
QList<SPUser>::const_iterator const it = std::find_if(users_.constBegin(), users_.constEnd(), [&userID](SPUser const &user) -> bool {
return user->id() == userID;
});

View File

@ -33,7 +33,7 @@ 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() override = 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.

View File

@ -71,7 +71,7 @@ int main(int argc, char **argv) {
app().log().error(message);
qApp->exit(EXIT_FAILURE);
});
UPOverseer overseer = std::make_unique<Overseer>(serverWorker, nullptr);
UPOverseer const overseer = std::make_unique<Overseer>(serverWorker, nullptr);
overseer->startWorker(true);
qint32 const exitCode = QApplication::exec();

View File

@ -303,6 +303,16 @@ void QMLBackend::openExternalLink(QString const &url) {
}
//****************************************************************************************************************************************************
/// \param[in] categoryID The ID of the bug report category.
//****************************************************************************************************************************************************
void QMLBackend::requestKnowledgeBaseSuggestions(qint8 categoryID) const {
HANDLE_EXCEPTION(
app().grpc().requestKnowledgeBaseSuggestions(reportFlow_.collectUserInput(categoryID));
)
}
//****************************************************************************************************************************************************
/// \return The value for the 'showOnStartup' property.
//****************************************************************************************************************************************************
@ -1305,6 +1315,7 @@ void QMLBackend::connectGrpcEvents() {
connect(client, &GRPCClient::certificateInstallCanceled, this, &QMLBackend::certificateInstallCanceled);
connect(client, &GRPCClient::certificateInstallFailed, this, &QMLBackend::certificateInstallFailed);
connect(client, &GRPCClient::showMainWindow, [&]() { this->showMainWindow("gRPC showMainWindow event"); });
connect(client, &GRPCClient::knowledgeBasSuggestionsReceived, this, &QMLBackend::receivedKnowledgeBaseSuggestions);
// cache events
connect(client, &GRPCClient::cantMoveDiskCache, this, &QMLBackend::cantMoveDiskCache);

View File

@ -66,6 +66,7 @@ public: // member functions.
Q_INVOKABLE void clearAnswers(); ///< Clear all collected answers.
Q_INVOKABLE bool isTLSCertificateInstalled(); ///< Check if the bridge certificate is installed in the OS keychain.
Q_INVOKABLE void openExternalLink(QString const & url = QString()); ///< Open a knowledge base article.
Q_INVOKABLE void requestKnowledgeBaseSuggestions(qint8 categoryID) const; ///< Request knowledgebase article suggestions.
public: // Qt/QML properties. Note that the NOTIFY-er signal is required even for read-only properties (QML warning otherwise)
Q_PROPERTY(bool showOnStartup READ showOnStartup NOTIFY showOnStartupChanged)
@ -278,6 +279,7 @@ signals: // Signals received from the Go backend, to be forwarded to QML
void selectUser(QString const& userID, bool forceShowWindow); ///< Signal emitted in order to selected a user with a given ID in the list.
void genericError(QString const &title, QString const &description); ///< Signal for the 'genericError' gRPC stream event.
void imapLoginWhileSignedOut(QString const& username); ///< Signal for the notification of IMAP login attempt on a signed out account.
void receivedKnowledgeBaseSuggestions(QList<bridgepp::KnowledgeBaseSuggestion> const& suggestions); ///< Signal for the reception of knowledgebase article suggestions.
// This signal is emitted when an exception is intercepted is calls triggered by QML. QML engine would intercept the exception otherwise.
void fatalError(bridgepp::Exception const& e) const; ///< Signal emitted when an fatal error occurs.

View File

@ -5,11 +5,6 @@
<file>qml/AccountView.qml</file>
<file>qml/Banner.qml</file>
<file>qml/Bridge.qml</file>
<file>qml/BugCategoryView.qml</file>
<file>qml/BugQuestionView.qml</file>
<file>qml/BugReportFlow.qml</file>
<file>qml/BugReportView.qml</file>
<file>qml/CategoryItem.qml</file>
<file>qml/Configuration.qml</file>
<file>qml/ConfigurationItem.qml</file>
<file>qml/ContentWrapper.qml</file>
@ -89,6 +84,12 @@
<file>qml/Notifications/Notifications.qml</file>
<file>qml/Notifications/qmldir</file>
<file>qml/PortSettings.qml</file>
<file>qml/BugReport/BugCategoryView.qml</file>
<file>qml/BugReport/BugQuestionView.qml</file>
<file>qml/BugReport/BugReportFlow.qml</file>
<file>qml/BugReport/BugReportView.qml</file>
<file>qml/BugReport/CategoryItem.qml</file>
<file>qml/BugReport/QuestionItem.qml</file>
<file>qml/Proton/Action.qml</file>
<file>qml/Proton/ApplicationWindow.qml</file>
<file>qml/Proton/Button.qml</file>
@ -96,6 +97,7 @@
<file>qml/Proton/ColorScheme.qml</file>
<file>qml/Proton/ComboBox.qml</file>
<file>qml/Proton/Dialog.qml</file>
<file>qml/Proton/InfoTooltip.qml</file>
<file>qml/Proton/Label.qml</file>
<file>qml/Proton/LinkLabel.qml</file>
<file>qml/Proton/Menu.qml</file>
@ -108,7 +110,6 @@
<file>qml/Proton/TextArea.qml</file>
<file>qml/Proton/TextField.qml</file>
<file>qml/Proton/Toggle.qml</file>
<file>qml/QuestionItem.qml</file>
<file>qml/Resources/bug_report_flow.json</file>
<file>qml/Resources/Help/Template.html</file>
<file>qml/Resources/Help/WhyBridge.html</file>

View File

@ -14,6 +14,7 @@ import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Proton
import ".."
SettingsView {
id: root

View File

@ -14,6 +14,7 @@ import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Proton
import ".."
SettingsView {
id: root

View File

@ -15,6 +15,7 @@ import QtQuick.Layouts
import QtQuick.Controls
import Proton
import Notifications
import ".."
Item {
id: root
@ -35,7 +36,7 @@ Item {
}
function showBugQuestion() {
bugQuestion.setCategoryId(root.categoryId);
bugQuestion.positionViewAtBegining();
bugQuestion.positionViewAtBeginning();
bugReportFlow.currentIndex = 1;
}
function showBugReport() {
@ -77,6 +78,7 @@ Item {
root.showBugCategory();
}
onQuestionAnswered: {
Backend.requestKnowledgeBaseSuggestions(categoryId);
root.showBugReport();
}
}

View File

@ -0,0 +1,201 @@
// Copyright (c) 2023 Proton AG
// This file is part of Proton Mail Bridge.
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Proton
import ".."
SettingsView {
id: root
property var selectedAddress
property var categoryId: -1
property string category: Backend.getBugCategory(root.categoryId)
property var suggestions: null
signal bugReportWasSent
function isValidEmail(text) {
const reEmail = /^[^@]+@[^@]+\.[A-Za-z]+\s*$/;
return reEmail.test(text);
}
function setCategoryId(catId) {
root.categoryId = catId;
}
function setDefaultValue() {
description.text = Backend.collectAnswers(root.categoryId);
address.text = root.selectedAddress;
emailClient.text = Backend.currentEmailClient;
includeLogs.checked = true;
}
function submit() {
sendButton.loading = true;
Backend.reportBug(root.category, description.text, address.text, emailClient.text, includeLogs.checked);
}
fillHeight: true
onVisibleChanged: {
root.setDefaultValue();
}
ColumnLayout {
spacing: 32
Label {
colorScheme: root.colorScheme
text: qsTr("Send report")
type: Label.Heading
}
TextArea {
id: description
KeyNavigation.priority: KeyNavigation.BeforeItem
KeyNavigation.tab: address
Layout.fillHeight: true
Layout.fillWidth: true
Layout.minimumHeight: heightForLinesVisible(4)
colorScheme: root.colorScheme
textFormat: Text.MarkdownText
// set implicitHeight to explicit height because se don't
// want TextArea implicitHeight (which is height of all text)
// to be considered in SettingsView internal scroll view
implicitHeight: height
label: "Your answers to: " + qsTr(root.category);
readOnly: true
}
ColumnLayout {
id: suggestionBox
visible: suggestions && suggestions.length > 0
spacing: 8
RowLayout {
Label {
colorScheme: root.colorScheme
text: qsTr("We believe these links may help you solve your problem")
type: Label.Body_semibold
}
InfoTooltip {
colorScheme: root.colorScheme
text: qsTr("The links will open in an external browser. If you cancel the report, your input will be preserved until you restart Bridge.")
Layout.bottomMargin: -4
}
}
Repeater {
model: suggestions
LinkLabel {
required property var modelData
colorScheme: root.colorScheme
text: modelData.title
link: modelData.url
external: true
}
}
}
RowLayout {
spacing: 12
TextField {
id: address
Layout.preferredWidth: 1
Layout.fillWidth: true
colorScheme: root.colorScheme
label: qsTr("Your contact email")
placeholderText: qsTr("e.g. jane.doe@protonmail.com")
validator: function (str) {
if (!isValidEmail(str)) {
return qsTr("Enter valid email address");
}
}
}
TextField {
id: emailClient
Layout.preferredWidth: 1
Layout.fillWidth: true
colorScheme: root.colorScheme
label: qsTr("Your email client (including version)")
placeholderText: qsTr("e.g. Apple Mail 14.0")
validator: function (str) {
if (str.length === 0) {
return qsTr("Enter an email client name and version");
}
}
}
}
RowLayout {
spacing: 12
CheckBox {
id: includeLogs
checked: true
colorScheme: root.colorScheme
text: qsTr("Include my recent logs")
}
Button {
colorScheme: root.colorScheme
secondary: true
text: qsTr("View logs")
onClicked: Backend.openExternalLink(Backend.logsPath)
}
Label {
Layout.fillWidth: true
verticalAlignment: Qt.AlignVCenter
colorScheme: root.colorScheme
type: Label.Caption
color: root.colorScheme.text_weak
text: qsTr("Reports are not end-to-end encrypted, please do not send any sensitive information.")
wrapMode: Text.WordWrap
}
}
Button {
id: sendButton
colorScheme: root.colorScheme
enabled: !loading
text: qsTr("Send")
onClicked: {
description.validate();
address.validate();
emailClient.validate();
if (description.error || address.error || emailClient.error) {
return;
}
submit();
}
Connections {
function onBugReportSendSuccess() {
root.bugReportWasSent();
}
function onReportBugFinished() {
sendButton.loading = false;
}
function onReceivedKnowledgeBaseSuggestions(suggestions) {
root.suggestions = suggestions
}
target: Backend
}
}
}
}

View File

@ -14,6 +14,7 @@ import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Proton
import ".."
Item {
id: root
@ -32,7 +33,7 @@ Item {
RowLayout {
anchors.fill: parent
spacing: 16
spacing: 12
Label {
id: mainLabel
@ -44,42 +45,13 @@ Item {
wrapMode: Text.WordWrap
}
ColorImage {
id: infoImage
InfoTooltip {
Layout.alignment: Qt.AlignVCenter
Layout.topMargin: 4
Layout.bottomMargin: root._bottomMargin
color: root.colorScheme.interaction_norm
height: 21
width: 21
source: "/qml/icons/ic-info-circle.svg"
sourceSize.height: 21
sourceSize.width: 21
visible: root.hint !== ""
MouseArea {
id: imageArea
anchors.fill: infoImage
hoverEnabled: true
}
ToolTip {
id: toolTipinfo
text: root.hint
visible: imageArea.containsMouse
implicitWidth: Math.min(400, tooltipText.implicitWidth)
background: Rectangle {
radius: 4
border.color: root.colorScheme.border_weak
color: root.colorScheme.background_weak
}
contentItem: Text {
id: tooltipText
color: root.colorScheme.text_hint
text: toolTipinfo.text
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
colorScheme: root.colorScheme
text: root.hint
size: 16
}
// fill height so the footer label will always be attached to the bottom

View File

@ -1,161 +0,0 @@
// Copyright (c) 2023 Proton AG
// This file is part of Proton Mail Bridge.
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Proton
SettingsView {
id: root
property var selectedAddress
property var categoryId:-1
property string category: Backend.getBugCategory(root.categoryId)
signal bugReportWasSent
function isValidEmail(text) {
const reEmail = /^[^@]+@[^@]+\.[A-Za-z]+\s*$/;
return reEmail.test(text);
}
function setCategoryId(catId) {
root.categoryId = catId;
}
function setDefaultValue() {
description.text = Backend.collectAnswers(root.categoryId);
address.text = root.selectedAddress;
emailClient.text = Backend.currentEmailClient;
includeLogs.checked = true;
}
function submit() {
sendButton.loading = true;
Backend.reportBug(root.category, description.text, address.text, emailClient.text, includeLogs.checked);
}
fillHeight: true
onVisibleChanged: {
root.setDefaultValue();
}
Label {
colorScheme: root.colorScheme
text: qsTr("Send report")
type: Label.Heading
}
TextArea {
id: description
KeyNavigation.priority: KeyNavigation.BeforeItem
KeyNavigation.tab: address
Layout.fillHeight: true
Layout.fillWidth: true
Layout.minimumHeight: heightForLinesVisible(4)
colorScheme: root.colorScheme
textFormat: Text.MarkdownText
// set implicitHeight to explicit height because se don't
// want TextArea implicitHeight (which is height of all text)
// to be considered in SettingsView internal scroll view
implicitHeight: height
label: "Your answers to: " + qsTr(root.category);
readOnly : true
}
TextField {
id: address
Layout.fillWidth: true
colorScheme: root.colorScheme
label: qsTr("Your contact email")
placeholderText: qsTr("e.g. jane.doe@protonmail.com")
validator: function (str) {
if (!isValidEmail(str)) {
return qsTr("Enter valid email address");
}
return;
}
}
TextField {
id: emailClient
Layout.fillWidth: true
colorScheme: root.colorScheme
label: qsTr("Your email client (including version)")
placeholderText: qsTr("e.g. Apple Mail 14.0")
validator: function (str) {
if (str.length === 0) {
return qsTr("Enter an email client name and version");
}
return;
}
}
RowLayout {
CheckBox {
id: includeLogs
checked: true
colorScheme: root.colorScheme
text: qsTr("Include my recent logs")
}
Button {
Layout.leftMargin: 12
colorScheme: root.colorScheme
secondary: true
text: qsTr("View logs")
onClicked: Backend.openExternalLink(Backend.logsPath)
}
}
TextEdit {
Layout.fillWidth: true
color: root.colorScheme.text_weak
font.family: ProtonStyle.font_family
font.letterSpacing: ProtonStyle.caption_letter_spacing
font.pixelSize: ProtonStyle.caption_font_size
font.weight: ProtonStyle.fontWeight_400
readOnly: true
selectByMouse: true
selectedTextColor: root.colorScheme.text_invert
// No way to set lineHeight: ProtonStyle.caption_line_height
selectionColor: root.colorScheme.interaction_norm
text: qsTr("Reports are not end-to-end encrypted, please do not send any sensitive information.")
wrapMode: Text.WordWrap
}
Button {
id: sendButton
colorScheme: root.colorScheme
enabled: !loading
text: qsTr("Send")
onClicked: {
description.validate();
address.validate();
emailClient.validate();
if (description.error || address.error || emailClient.error) {
return;
}
submit();
}
Connections {
function onBugReportSendSuccess() {
root.bugReportWasSent();
}
function onReportBugFinished() {
sendButton.loading = false;
}
target: Backend
}
}
}

View File

@ -15,6 +15,7 @@ import QtQuick.Layouts
import QtQuick.Controls
import Proton
import Notifications
import "BugReport"
Item {
id: root

View File

@ -18,6 +18,7 @@ import QtQuick.Controls
import Proton
import Notifications
import "SetupWizard"
import "BugReport"
ApplicationWindow {
id: root

View File

@ -0,0 +1,60 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
ColorImage {
id: root
property var colorScheme
property string text
property int size: 16
color: root.colorScheme.interaction_norm
height: size
width: size
source: "/qml/icons/ic-info-circle.svg"
sourceSize.height: size
sourceSize.width: size
visible: root.hint !== ""
MouseArea {
id: imageArea
anchors.fill: parent
hoverEnabled: true
}
ToolTip {
id: toolTipinfo
text: root.text
visible: imageArea.containsMouse
implicitWidth: Math.min(400, tooltipText.implicitWidth)
background: Rectangle {
radius: 4
border.color: root.colorScheme.border_weak
color: root.colorScheme.background_weak
}
contentItem: Text {
id: tooltipText
color: root.colorScheme.text_norm
text: root.text
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
}

View File

@ -64,11 +64,12 @@ RowLayout {
}
ColorImage {
Layout.alignment: Qt.AlignVCenter
Layout.bottomMargin: -6
color: label.linkColor
height: sourceSize.height
source: "/qml/icons/ic-external-link.svg"
sourceSize.height: 16
sourceSize.width: 16
sourceSize.height: 14
sourceSize.width: 14
visible: external
width: sourceSize.width

View File

@ -27,6 +27,7 @@ Button 4.0 Button.qml
CheckBox 4.0 CheckBox.qml
ComboBox 4.0 ComboBox.qml
Dialog 4.0 Dialog.qml
InfoTooltip 4.0 InfoTooltip.qml
Label 4.0 Label.qml
LinkLabel 4.0 LinkLabel.qml
Menu 4.0 Menu.qml

View File

@ -30,7 +30,7 @@ Item {
property bool fillHeight: false
default property alias items: content.children
function positionViewAtBegining() {
function positionViewAtBeginning() {
scrollView.ScrollBar.vertical.position = 0
}
signal back

View File

@ -30,13 +30,6 @@ namespace {
namespace bridgepp {
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************
BugReportFlow::BugReportFlow() {
}
//****************************************************************************************************************************************************
/// \param[in] filepath The path of the file to parse.
/// \return True iff the file can be properly parsed.
@ -92,7 +85,7 @@ bool BugReportFlow::setAnswer(quint8 questionId, QString const &answer) {
//****************************************************************************************************************************************************
/// \param[in] questionId The id of the question.
/// \param[in] categoryId The id of the question.
/// \return answer the given question.
//****************************************************************************************************************************************************
QString BugReportFlow::getCategory(quint8 categoryId) const {
@ -128,7 +121,7 @@ QString BugReportFlow::collectAnswers(quint8 categoryId) const {
QVariantList sets = this->questionSet(categoryId);
for (QVariant const &var: sets) {
const QString& answer = getAnswer(var.toInt());
const QString answer = getAnswer(var.toInt());
if (answer.isEmpty())
continue;
answers += "#### " + questions_[var.toInt()].toMap()["text"].toString() + "\n\r";
@ -139,6 +132,24 @@ QString BugReportFlow::collectAnswers(quint8 categoryId) const {
}
//****************************************************************************************************************************************************
/// \param[in] categoryId The id of the question set.
//****************************************************************************************************************************************************
QString BugReportFlow::collectUserInput(quint8 categoryId) const {
if (categoryId > categories_.count() - 1)
return {};
QString input = this->getCategory(categoryId);
for (QVariant const &var: this->questionSet(categoryId)) {
QString const answer = getAnswer(var.toInt());
if (!answer.isEmpty())
input += " " + answer;
}
return input;
}
//****************************************************************************************************************************************************
//
//****************************************************************************************************************************************************

View File

@ -28,7 +28,7 @@ namespace bridgepp {
class BugReportFlow {
public: // member functions.
BugReportFlow(); ///< Default constructor.
BugReportFlow() = default; ///< Default constructor.
BugReportFlow(BugReportFlow const &) = delete; ///< Disabled copy-constructor.
BugReportFlow(BugReportFlow &&) = delete; ///< Disabled assignment copy-constructor.
~BugReportFlow() = default; ///< Destructor.
@ -42,6 +42,7 @@ public: // member functions.
[[nodiscard]] QString getCategory(quint8 categoryId) const; ///< Get category name.
[[nodiscard]] QString getAnswer(quint8 questionId) const; ///< Get answer for a given question.
[[nodiscard]] QString collectAnswers(quint8 categoryId) const; ///< Collect answer for a given set of questions.
[[nodiscard]] QString collectUserInput(quint8 categoryId) const; ///< Collect the user input (user answers without quesitons) for a given set of questions.
void clearAnswers(); ///< Clear all collected answers.

View File

@ -247,6 +247,22 @@ SPStreamEvent newCertificateInstallFailedEvent() {
}
//****************************************************************************************************************************************************
/// \param[in] suggestions the suggestions
/// \return The event.
//****************************************************************************************************************************************************
SPStreamEvent newKnowledgeBaseSuggestionsEvent(QList<KnowledgeBaseSuggestion> const& suggestions) {
auto event = new grpc::KnowledgeBaseSuggestionsEvent;
for (KnowledgeBaseSuggestion const &suggestion: suggestions) {
grpc::KnowledgeBaseSuggestion *s = event->add_suggestions();
s->set_url(suggestion.url.toStdString());
s->set_title(suggestion.title.toStdString());
}
auto appEvent = new grpc::AppEvent;
appEvent->set_allocated_knowledgebasesuggestions(event);
return wrapAppEvent(appEvent);
}
//****************************************************************************************************************************************************
/// \return The event.
//****************************************************************************************************************************************************

View File

@ -22,6 +22,7 @@
#include "bridge.grpc.pb.h"
#include "GRPCUtils.h"
#include <bridgepp/GRPC/GRPCClient.h>
namespace bridgepp {
@ -39,6 +40,7 @@ SPStreamEvent newCertificateInstallSuccessEvent(); ///< Create a new Certificate
SPStreamEvent newCertificateInstallCanceledEvent(); ///< Create a new CertificateInstallCanceledEvent event.
SPStreamEvent newCertificateInstallFailedEvent(); ///< Create anew CertificateInstallFailedEvent event.
SPStreamEvent newShowMainWindowEvent(); ///< Create a new ShowMainWindowEvent event.
SPStreamEvent newKnowledgeBaseSuggestionsEvent(QList<KnowledgeBaseSuggestion> const& suggestions); ///< Create a new KnowledgeBaseSuggestions event.
// Login events
SPStreamEvent newLoginError(grpc::LoginErrorType error, QString const &message); ///< Create a new LoginError event.

View File

@ -568,6 +568,14 @@ grpc::Status GRPCClient::hostname(QString &outHostname) {
}
//****************************************************************************************************************************************************
/// \param[in] input The user input to analyze.
//****************************************************************************************************************************************************
grpc::Status GRPCClient::requestKnowledgeBaseSuggestions(QString const &input) {
return this->logGRPCCallStatus(this->setString(&Bridge::Stub::RequestKnowledgeBaseSuggestions, input), __FUNCTION__);
}
//****************************************************************************************************************************************************
/// \param[out] outPath The value for the property.
/// \return The status for the gRPC call.
@ -1164,6 +1172,19 @@ void GRPCClient::processAppEvent(AppEvent const &event) {
this->logTrace("App event received: CertificateInstallFailed.");
emit certificateInstallFailed();
break;
case AppEvent::kKnowledgeBaseSuggestions:
{
this->logTrace("App event received: KnowledgeBaseSuggestions.");
QList<KnowledgeBaseSuggestion> suggestions;
for (grpc::KnowledgeBaseSuggestion const &suggestion: event.knowledgebasesuggestions().suggestions()) {
suggestions.push_back(KnowledgeBaseSuggestion{
.url = QString::fromUtf8(suggestion.url()),
.title = QString::fromUtf8(suggestion.title())
});
}
emit knowledgeBasSuggestionsReceived(suggestions);
break;
}
default:
this->logError("Unknown App event received.");
}

View File

@ -42,6 +42,20 @@ typedef grpc::Status (grpc::Bridge::Stub::*StringParamMethod)(grpc::ClientContex
typedef std::unique_ptr<grpc::ClientContext> UPClientContext;
//****************************************************************************************************************************************************
/// \brief A struct for knowledge base suggestion.
//****************************************************************************************************************************************************
struct KnowledgeBaseSuggestion {
// The following lines make the type transmissible to QML (but not instanciable there)
Q_GADGET
Q_PROPERTY(QString url MEMBER url)
Q_PROPERTY(QString title MEMBER title)
public:
QString url; ///< The URL of the knowledge base article
QString title; ///< The title of the knowledge base article.
};
//****************************************************************************************************************************************************
/// \brief gRPC client class. This class encapsulate the gRPC service, abstracting all data type conversions.
//****************************************************************************************************************************************************
@ -93,6 +107,7 @@ public: // member functions.
grpc::Status releaseNotesPageLink(QUrl &outUrl); ///< Performs the 'releaseNotesPageLink' call.
grpc::Status landingPageLink(QUrl &outUrl); ///< Performs the 'landingPageLink' call.
grpc::Status hostname(QString &outHostname); ///< Performs the 'Hostname' call.
grpc::Status requestKnowledgeBaseSuggestions(QString const &input); ///< Performs the 'RequestKnowledgeBaseSuggestions' call.
signals: // app related signals
void internetStatus(bool isOn);
@ -106,9 +121,10 @@ signals: // app related signals
void certificateInstallCanceled();
void certificateInstallFailed();
void showMainWindow();
void knowledgeBasSuggestionsReceived(QList<KnowledgeBaseSuggestion> const& suggestions);
// cache related calls
public:
public: // cache related calls
grpc::Status diskCachePath(QUrl &outPath); ///< Performs the 'diskCachePath' call.
grpc::Status setDiskCachePath(QUrl const &path); ///< Performs the 'setDiskCachePath' call
@ -117,8 +133,8 @@ signals:
void diskCachePathChanged(QUrl const &path);
void diskCachePathChangeFinished();
// mail settings related calls
public:
public: // mail settings related calls
grpc::Status mailServerSettings(qint32 &outIMAPPort, qint32 &outSMTPPort, bool &outUseSSLForIMAP, bool &outUseSSLForSMTP); ///< Performs the 'MailServerSettings' gRPC call.
grpc::Status setMailServerSettings(qint32 imapPort, qint32 smtpPort, bool useSSLForIMAP, bool useSSLForSMTP); ///< Performs the 'SetMailServerSettings' gRPC call.
grpc::Status isDoHEnabled(bool &outEnabled); ///< Performs the 'isDoHEnabled' gRPC call.

File diff suppressed because it is too large Load Diff

View File

@ -58,6 +58,7 @@ service Bridge {
rpc ReportBug(ReportBugRequest) returns (google.protobuf.Empty);
rpc ForceLauncher(google.protobuf.StringValue) returns (google.protobuf.Empty);
rpc SetMainExecutable(google.protobuf.StringValue) returns (google.protobuf.Empty);
rpc RequestKnowledgeBaseSuggestions(google.protobuf.StringValue) returns (google.protobuf.Empty);
// login
rpc Login(LoginRequest) returns (google.protobuf.Empty);
@ -269,6 +270,7 @@ message AppEvent {
CertificateInstallSuccessEvent certificateInstallSuccess = 9;
CertificateInstallCanceledEvent certificateInstallCanceled = 10;
CertificateInstallFailedEvent certificateInstallFailed = 11;
KnowledgeBaseSuggestionsEvent knowledgeBaseSuggestions = 12;
}
}
@ -287,6 +289,15 @@ message CertificateInstallSuccessEvent {}
message CertificateInstallCanceledEvent {}
message CertificateInstallFailedEvent {}
message KnowledgeBaseSuggestion {
string url = 1;
string title = 2;
}
message KnowledgeBaseSuggestionsEvent {
repeated KnowledgeBaseSuggestion suggestions = 1;
}
//**********************************************************
// Login related events
//**********************************************************
@ -478,6 +489,7 @@ message UserEvent {
}
}
message ToggleSplitModeFinishedEvent {
string userID = 1;
}

View File

@ -38,68 +38,69 @@ import (
const _ = grpc.SupportPackageIsVersion7
const (
Bridge_CheckTokens_FullMethodName = "/grpc.Bridge/CheckTokens"
Bridge_AddLogEntry_FullMethodName = "/grpc.Bridge/AddLogEntry"
Bridge_GuiReady_FullMethodName = "/grpc.Bridge/GuiReady"
Bridge_Quit_FullMethodName = "/grpc.Bridge/Quit"
Bridge_Restart_FullMethodName = "/grpc.Bridge/Restart"
Bridge_ShowOnStartup_FullMethodName = "/grpc.Bridge/ShowOnStartup"
Bridge_SetIsAutostartOn_FullMethodName = "/grpc.Bridge/SetIsAutostartOn"
Bridge_IsAutostartOn_FullMethodName = "/grpc.Bridge/IsAutostartOn"
Bridge_SetIsBetaEnabled_FullMethodName = "/grpc.Bridge/SetIsBetaEnabled"
Bridge_IsBetaEnabled_FullMethodName = "/grpc.Bridge/IsBetaEnabled"
Bridge_SetIsAllMailVisible_FullMethodName = "/grpc.Bridge/SetIsAllMailVisible"
Bridge_IsAllMailVisible_FullMethodName = "/grpc.Bridge/IsAllMailVisible"
Bridge_SetIsTelemetryDisabled_FullMethodName = "/grpc.Bridge/SetIsTelemetryDisabled"
Bridge_IsTelemetryDisabled_FullMethodName = "/grpc.Bridge/IsTelemetryDisabled"
Bridge_GoOs_FullMethodName = "/grpc.Bridge/GoOs"
Bridge_TriggerReset_FullMethodName = "/grpc.Bridge/TriggerReset"
Bridge_Version_FullMethodName = "/grpc.Bridge/Version"
Bridge_LogsPath_FullMethodName = "/grpc.Bridge/LogsPath"
Bridge_LicensePath_FullMethodName = "/grpc.Bridge/LicensePath"
Bridge_ReleaseNotesPageLink_FullMethodName = "/grpc.Bridge/ReleaseNotesPageLink"
Bridge_DependencyLicensesLink_FullMethodName = "/grpc.Bridge/DependencyLicensesLink"
Bridge_LandingPageLink_FullMethodName = "/grpc.Bridge/LandingPageLink"
Bridge_SetColorSchemeName_FullMethodName = "/grpc.Bridge/SetColorSchemeName"
Bridge_ColorSchemeName_FullMethodName = "/grpc.Bridge/ColorSchemeName"
Bridge_CurrentEmailClient_FullMethodName = "/grpc.Bridge/CurrentEmailClient"
Bridge_ReportBug_FullMethodName = "/grpc.Bridge/ReportBug"
Bridge_ForceLauncher_FullMethodName = "/grpc.Bridge/ForceLauncher"
Bridge_SetMainExecutable_FullMethodName = "/grpc.Bridge/SetMainExecutable"
Bridge_Login_FullMethodName = "/grpc.Bridge/Login"
Bridge_Login2FA_FullMethodName = "/grpc.Bridge/Login2FA"
Bridge_Login2Passwords_FullMethodName = "/grpc.Bridge/Login2Passwords"
Bridge_LoginAbort_FullMethodName = "/grpc.Bridge/LoginAbort"
Bridge_CheckUpdate_FullMethodName = "/grpc.Bridge/CheckUpdate"
Bridge_InstallUpdate_FullMethodName = "/grpc.Bridge/InstallUpdate"
Bridge_SetIsAutomaticUpdateOn_FullMethodName = "/grpc.Bridge/SetIsAutomaticUpdateOn"
Bridge_IsAutomaticUpdateOn_FullMethodName = "/grpc.Bridge/IsAutomaticUpdateOn"
Bridge_DiskCachePath_FullMethodName = "/grpc.Bridge/DiskCachePath"
Bridge_SetDiskCachePath_FullMethodName = "/grpc.Bridge/SetDiskCachePath"
Bridge_SetIsDoHEnabled_FullMethodName = "/grpc.Bridge/SetIsDoHEnabled"
Bridge_IsDoHEnabled_FullMethodName = "/grpc.Bridge/IsDoHEnabled"
Bridge_MailServerSettings_FullMethodName = "/grpc.Bridge/MailServerSettings"
Bridge_SetMailServerSettings_FullMethodName = "/grpc.Bridge/SetMailServerSettings"
Bridge_Hostname_FullMethodName = "/grpc.Bridge/Hostname"
Bridge_IsPortFree_FullMethodName = "/grpc.Bridge/IsPortFree"
Bridge_AvailableKeychains_FullMethodName = "/grpc.Bridge/AvailableKeychains"
Bridge_SetCurrentKeychain_FullMethodName = "/grpc.Bridge/SetCurrentKeychain"
Bridge_CurrentKeychain_FullMethodName = "/grpc.Bridge/CurrentKeychain"
Bridge_GetUserList_FullMethodName = "/grpc.Bridge/GetUserList"
Bridge_GetUser_FullMethodName = "/grpc.Bridge/GetUser"
Bridge_SetUserSplitMode_FullMethodName = "/grpc.Bridge/SetUserSplitMode"
Bridge_SendBadEventUserFeedback_FullMethodName = "/grpc.Bridge/SendBadEventUserFeedback"
Bridge_LogoutUser_FullMethodName = "/grpc.Bridge/LogoutUser"
Bridge_RemoveUser_FullMethodName = "/grpc.Bridge/RemoveUser"
Bridge_ConfigureUserAppleMail_FullMethodName = "/grpc.Bridge/ConfigureUserAppleMail"
Bridge_ReportBugClicked_FullMethodName = "/grpc.Bridge/ReportBugClicked"
Bridge_AutoconfigClicked_FullMethodName = "/grpc.Bridge/AutoconfigClicked"
Bridge_ExternalLinkClicked_FullMethodName = "/grpc.Bridge/ExternalLinkClicked"
Bridge_IsTLSCertificateInstalled_FullMethodName = "/grpc.Bridge/IsTLSCertificateInstalled"
Bridge_InstallTLSCertificate_FullMethodName = "/grpc.Bridge/InstallTLSCertificate"
Bridge_ExportTLSCertificates_FullMethodName = "/grpc.Bridge/ExportTLSCertificates"
Bridge_RunEventStream_FullMethodName = "/grpc.Bridge/RunEventStream"
Bridge_StopEventStream_FullMethodName = "/grpc.Bridge/StopEventStream"
Bridge_CheckTokens_FullMethodName = "/grpc.Bridge/CheckTokens"
Bridge_AddLogEntry_FullMethodName = "/grpc.Bridge/AddLogEntry"
Bridge_GuiReady_FullMethodName = "/grpc.Bridge/GuiReady"
Bridge_Quit_FullMethodName = "/grpc.Bridge/Quit"
Bridge_Restart_FullMethodName = "/grpc.Bridge/Restart"
Bridge_ShowOnStartup_FullMethodName = "/grpc.Bridge/ShowOnStartup"
Bridge_SetIsAutostartOn_FullMethodName = "/grpc.Bridge/SetIsAutostartOn"
Bridge_IsAutostartOn_FullMethodName = "/grpc.Bridge/IsAutostartOn"
Bridge_SetIsBetaEnabled_FullMethodName = "/grpc.Bridge/SetIsBetaEnabled"
Bridge_IsBetaEnabled_FullMethodName = "/grpc.Bridge/IsBetaEnabled"
Bridge_SetIsAllMailVisible_FullMethodName = "/grpc.Bridge/SetIsAllMailVisible"
Bridge_IsAllMailVisible_FullMethodName = "/grpc.Bridge/IsAllMailVisible"
Bridge_SetIsTelemetryDisabled_FullMethodName = "/grpc.Bridge/SetIsTelemetryDisabled"
Bridge_IsTelemetryDisabled_FullMethodName = "/grpc.Bridge/IsTelemetryDisabled"
Bridge_GoOs_FullMethodName = "/grpc.Bridge/GoOs"
Bridge_TriggerReset_FullMethodName = "/grpc.Bridge/TriggerReset"
Bridge_Version_FullMethodName = "/grpc.Bridge/Version"
Bridge_LogsPath_FullMethodName = "/grpc.Bridge/LogsPath"
Bridge_LicensePath_FullMethodName = "/grpc.Bridge/LicensePath"
Bridge_ReleaseNotesPageLink_FullMethodName = "/grpc.Bridge/ReleaseNotesPageLink"
Bridge_DependencyLicensesLink_FullMethodName = "/grpc.Bridge/DependencyLicensesLink"
Bridge_LandingPageLink_FullMethodName = "/grpc.Bridge/LandingPageLink"
Bridge_SetColorSchemeName_FullMethodName = "/grpc.Bridge/SetColorSchemeName"
Bridge_ColorSchemeName_FullMethodName = "/grpc.Bridge/ColorSchemeName"
Bridge_CurrentEmailClient_FullMethodName = "/grpc.Bridge/CurrentEmailClient"
Bridge_ReportBug_FullMethodName = "/grpc.Bridge/ReportBug"
Bridge_ForceLauncher_FullMethodName = "/grpc.Bridge/ForceLauncher"
Bridge_SetMainExecutable_FullMethodName = "/grpc.Bridge/SetMainExecutable"
Bridge_RequestKnowledgeBaseSuggestions_FullMethodName = "/grpc.Bridge/RequestKnowledgeBaseSuggestions"
Bridge_Login_FullMethodName = "/grpc.Bridge/Login"
Bridge_Login2FA_FullMethodName = "/grpc.Bridge/Login2FA"
Bridge_Login2Passwords_FullMethodName = "/grpc.Bridge/Login2Passwords"
Bridge_LoginAbort_FullMethodName = "/grpc.Bridge/LoginAbort"
Bridge_CheckUpdate_FullMethodName = "/grpc.Bridge/CheckUpdate"
Bridge_InstallUpdate_FullMethodName = "/grpc.Bridge/InstallUpdate"
Bridge_SetIsAutomaticUpdateOn_FullMethodName = "/grpc.Bridge/SetIsAutomaticUpdateOn"
Bridge_IsAutomaticUpdateOn_FullMethodName = "/grpc.Bridge/IsAutomaticUpdateOn"
Bridge_DiskCachePath_FullMethodName = "/grpc.Bridge/DiskCachePath"
Bridge_SetDiskCachePath_FullMethodName = "/grpc.Bridge/SetDiskCachePath"
Bridge_SetIsDoHEnabled_FullMethodName = "/grpc.Bridge/SetIsDoHEnabled"
Bridge_IsDoHEnabled_FullMethodName = "/grpc.Bridge/IsDoHEnabled"
Bridge_MailServerSettings_FullMethodName = "/grpc.Bridge/MailServerSettings"
Bridge_SetMailServerSettings_FullMethodName = "/grpc.Bridge/SetMailServerSettings"
Bridge_Hostname_FullMethodName = "/grpc.Bridge/Hostname"
Bridge_IsPortFree_FullMethodName = "/grpc.Bridge/IsPortFree"
Bridge_AvailableKeychains_FullMethodName = "/grpc.Bridge/AvailableKeychains"
Bridge_SetCurrentKeychain_FullMethodName = "/grpc.Bridge/SetCurrentKeychain"
Bridge_CurrentKeychain_FullMethodName = "/grpc.Bridge/CurrentKeychain"
Bridge_GetUserList_FullMethodName = "/grpc.Bridge/GetUserList"
Bridge_GetUser_FullMethodName = "/grpc.Bridge/GetUser"
Bridge_SetUserSplitMode_FullMethodName = "/grpc.Bridge/SetUserSplitMode"
Bridge_SendBadEventUserFeedback_FullMethodName = "/grpc.Bridge/SendBadEventUserFeedback"
Bridge_LogoutUser_FullMethodName = "/grpc.Bridge/LogoutUser"
Bridge_RemoveUser_FullMethodName = "/grpc.Bridge/RemoveUser"
Bridge_ConfigureUserAppleMail_FullMethodName = "/grpc.Bridge/ConfigureUserAppleMail"
Bridge_ReportBugClicked_FullMethodName = "/grpc.Bridge/ReportBugClicked"
Bridge_AutoconfigClicked_FullMethodName = "/grpc.Bridge/AutoconfigClicked"
Bridge_ExternalLinkClicked_FullMethodName = "/grpc.Bridge/ExternalLinkClicked"
Bridge_IsTLSCertificateInstalled_FullMethodName = "/grpc.Bridge/IsTLSCertificateInstalled"
Bridge_InstallTLSCertificate_FullMethodName = "/grpc.Bridge/InstallTLSCertificate"
Bridge_ExportTLSCertificates_FullMethodName = "/grpc.Bridge/ExportTLSCertificates"
Bridge_RunEventStream_FullMethodName = "/grpc.Bridge/RunEventStream"
Bridge_StopEventStream_FullMethodName = "/grpc.Bridge/StopEventStream"
)
// BridgeClient is the client API for Bridge service.
@ -135,6 +136,7 @@ type BridgeClient interface {
ReportBug(ctx context.Context, in *ReportBugRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
ForceLauncher(ctx context.Context, in *wrapperspb.StringValue, opts ...grpc.CallOption) (*emptypb.Empty, error)
SetMainExecutable(ctx context.Context, in *wrapperspb.StringValue, opts ...grpc.CallOption) (*emptypb.Empty, error)
RequestKnowledgeBaseSuggestions(ctx context.Context, in *wrapperspb.StringValue, opts ...grpc.CallOption) (*emptypb.Empty, error)
// login
Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
Login2FA(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
@ -440,6 +442,15 @@ func (c *bridgeClient) SetMainExecutable(ctx context.Context, in *wrapperspb.Str
return out, nil
}
func (c *bridgeClient) RequestKnowledgeBaseSuggestions(ctx context.Context, in *wrapperspb.StringValue, opts ...grpc.CallOption) (*emptypb.Empty, error) {
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, Bridge_RequestKnowledgeBaseSuggestions_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *bridgeClient) Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, Bridge_Login_FullMethodName, in, out, opts...)
@ -802,6 +813,7 @@ type BridgeServer interface {
ReportBug(context.Context, *ReportBugRequest) (*emptypb.Empty, error)
ForceLauncher(context.Context, *wrapperspb.StringValue) (*emptypb.Empty, error)
SetMainExecutable(context.Context, *wrapperspb.StringValue) (*emptypb.Empty, error)
RequestKnowledgeBaseSuggestions(context.Context, *wrapperspb.StringValue) (*emptypb.Empty, error)
// login
Login(context.Context, *LoginRequest) (*emptypb.Empty, error)
Login2FA(context.Context, *LoginRequest) (*emptypb.Empty, error)
@ -936,6 +948,9 @@ func (UnimplementedBridgeServer) ForceLauncher(context.Context, *wrapperspb.Stri
func (UnimplementedBridgeServer) SetMainExecutable(context.Context, *wrapperspb.StringValue) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method SetMainExecutable not implemented")
}
func (UnimplementedBridgeServer) RequestKnowledgeBaseSuggestions(context.Context, *wrapperspb.StringValue) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method RequestKnowledgeBaseSuggestions not implemented")
}
func (UnimplementedBridgeServer) Login(context.Context, *LoginRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method Login not implemented")
}
@ -1555,6 +1570,24 @@ func _Bridge_SetMainExecutable_Handler(srv interface{}, ctx context.Context, dec
return interceptor(ctx, in, info, handler)
}
func _Bridge_RequestKnowledgeBaseSuggestions_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(wrapperspb.StringValue)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(BridgeServer).RequestKnowledgeBaseSuggestions(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Bridge_RequestKnowledgeBaseSuggestions_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(BridgeServer).RequestKnowledgeBaseSuggestions(ctx, req.(*wrapperspb.StringValue))
}
return interceptor(ctx, in, info, handler)
}
func _Bridge_Login_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(LoginRequest)
if err := dec(in); err != nil {
@ -2289,6 +2322,10 @@ var Bridge_ServiceDesc = grpc.ServiceDesc{
MethodName: "SetMainExecutable",
Handler: _Bridge_SetMainExecutable_Handler,
},
{
MethodName: "RequestKnowledgeBaseSuggestions",
Handler: _Bridge_RequestKnowledgeBaseSuggestions_Handler,
},
{
MethodName: "Login",
Handler: _Bridge_Login_Handler,

View File

@ -17,6 +17,11 @@
package grpc
import (
"github.com/ProtonMail/proton-bridge/v3/internal/kb"
"github.com/bradenaw/juniper/xslices"
)
func NewInternetStatusEvent(connected bool) *StreamEvent {
return appEvent(&AppEvent{Event: &AppEvent_InternetStatus{InternetStatus: &InternetStatusEvent{Connected: connected}}})
}
@ -61,6 +66,20 @@ func NewShowMainWindowEvent() *StreamEvent {
return appEvent(&AppEvent{Event: &AppEvent_ShowMainWindow{ShowMainWindow: &ShowMainWindowEvent{}}})
}
func NewRequestKnowledgeBaseSuggestionsEvent(suggestions kb.ArticleList) *StreamEvent {
s := xslices.Map(
suggestions,
func(article *kb.Article) *KnowledgeBaseSuggestion {
return &KnowledgeBaseSuggestion{Url: article.URL, Title: article.Title}
},
)
return appEvent(&AppEvent{Event: &AppEvent_KnowledgeBaseSuggestions{
KnowledgeBaseSuggestions: &KnowledgeBaseSuggestionsEvent{
Suggestions: s,
},
}})
}
func NewLoginError(err LoginErrorType, message string) *StreamEvent {
return loginEvent(&LoginEvent{Event: &LoginEvent_Error{Error: &LoginErrorEvent{Type: err, Message: message}}})
}

View File

@ -30,6 +30,7 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
"github.com/ProtonMail/proton-bridge/v3/internal/frontend/theme"
"github.com/ProtonMail/proton-bridge/v3/internal/kb"
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/service"
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
@ -375,6 +376,23 @@ func (s *Service) SetMainExecutable(_ context.Context, exe *wrapperspb.StringVal
return &emptypb.Empty{}, nil
}
func (s *Service) RequestKnowledgeBaseSuggestions(_ context.Context, userInput *wrapperspb.StringValue) (*emptypb.Empty, error) {
s.log.Debug("RequestKnowledgeBaseSuggestions")
go func() {
defer async.HandlePanic(s.panicHandler)
articles, err := s.bridge.GetKnowledgeBaseSuggestions(userInput.Value)
if err != nil {
s.log.WithError(err).Error("Could not retrieve KB article suggestions")
articles = kb.ArticleList{}
}
_ = s.SendEvent(NewRequestKnowledgeBaseSuggestionsEvent(articles))
}()
return &emptypb.Empty{}, nil
}
func (s *Service) Login(_ context.Context, login *LoginRequest) (*emptypb.Empty, error) {
s.log.WithField("username", login.Username).Debug("Login")

View File

@ -21,6 +21,7 @@ import (
"context"
"github.com/ProtonMail/gluon/async"
"github.com/ProtonMail/proton-bridge/v3/internal/kb"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
@ -122,6 +123,7 @@ func (s *Service) StartEventTest() error {
NewReportBugSuccessEvent(),
NewReportBugErrorEvent(),
NewShowMainWindowEvent(),
NewRequestKnowledgeBaseSuggestionsEvent(kb.ArticleList{}),
// login
NewLoginError(LoginErrorType_FREE_USER, "error"),

View File

@ -0,0 +1,425 @@
[
{
"index": 0,
"url": "https://proton.me/support/automatically-start-bridge",
"title": "Automatically start Bridge",
"keywords": [
"automatic",
"login",
"start",
"boot"
]
},
{
"index": 1,
"url": "https://proton.me/support/bridge-automatic-update",
"title": "Automatic Update and Bridge",
"keywords": [
"update",
"upgrade",
"restart",
"automatic",
"manual"
]
},
{
"index": 2,
"url": "https://proton.me/support/messages-encrypted-via-bridge",
"title": "Are my messages encrypted via Proton Mail Bridge?",
"keywords": [
"encrypted",
"privacy",
"message",
"security",
"gpg",
"pgp",
"crypto"
]
},
{
"index": 3,
"url": "https://proton.me/support/labels-in-bridge",
"title": "Labels in Bridge",
"keywords": [
"labels",
"folders",
"directories"
]
},
{
"index": 4,
"url": "https://proton.me/support/bridge-ssl-connection-issue",
"title": "Proton Mail Bridge connection issues with Thunderbird, Outlook, and Apple Mail",
"keywords": [
"connect",
"SSL",
"STARTTLS",
"client",
"program",
"Outlook",
"Apple Mail",
"Thunderbird"
]
},
{
"index": 5,
"url": "https://proton.me/support/sending-pgp-emails-bridge",
"title": "Sending PGP emails in Proton Mail Bridge",
"keywords": [
"pgp",
"gpg",
"encrypt",
"crypto"
]
},
{
"index": 6,
"url": "https://proton.me/support/difference-combined-addresses-mode-split-addresses-mode",
"title": "Difference between combined addresses mode and split addresses mode",
"keywords": [
"combined",
"split",
"address",
"mode"
]
},
{
"index": 7,
"url": "https://proton.me/support/thunderbird-connection-server-timed-error",
"title": "Thunderbird: 'Connection to server timed out' error",
"keywords": [
"Thunderbird",
"Connection to server timed out",
"Connection",
"Timeout"
]
},
{
"index": 8,
"url": "https://proton.me/support/update-required",
"title": "Update required",
"keywords": [
"update required",
"update",
"upgrade",
"restart",
"reboot"
]
},
{
"index": 9,
"url": "https://proton.me/support/port-already-occupied-error",
"title": "Port already occupied error",
"keywords": [
"Port",
"occupied",
"1143",
"1025",
"SMTP",
"IMAP",
"error"
]
},
{
"index": 10,
"url": "https://proton.me/support/clients-supported-bridge",
"title": "Email clients supported by Proton Mail Bridge",
"keywords": [
"client",
"Outlook",
"Thunderbird",
"Apple Mail",
"EM Client",
"The Bat",
"Eudora",
"Postbox",
"Canary",
"Spark"
]
},
{
"index": 11,
"url": "https://proton.me/support/imap-smtp-and-pop3-setup",
"title": "IMAP, SMTP, and POP3 setup",
"keywords": [
"IMAP",
"SMTP",
"setup",
"set up",
"configure",
"configuration",
"parameters"
]
},
{
"index": 12,
"url": "https://proton.me/support/protonmail-bridge-install",
"title": "How to install Proton Mail Bridge",
"keywords": [
"install",
"installer",
"setup",
"download",
"windows",
"mac",
"macos",
"linux"
]
},
{
"index": 13,
"url": "https://proton.me/support/bridge-for-linux",
"title": "Proton Mail Bridge for Linux",
"keywords": [
"Linux",
"Ubuntu",
"Fedora",
"Debian",
"Unix",
"deb",
"rpm",
"CentOS",
"Arch",
"Mint"
]
},
{
"index": 14,
"url": "https://proton.me/support/operating-systems-supported-bridge",
"title": "System requirements for Proton Mail Bridge",
"keywords": [
"requirement",
"cpu",
"memory",
"Windows",
"macOS",
"linux"
]
},
{
"index": 15,
"url": "https://proton.me/support/protonmail-bridge-configure-client",
"title": "How to configure your email client for Proton Mail Bridge",
"keywords": [
"Client",
"Outlook",
"configure",
"setup",
"application",
"setup",
"IMAP",
"SMTP"
]
},
{
"index": 16,
"url": "https://proton.me/support/invalid-password-error-setting-email-client",
"title": "Invalid password error while setting up email client",
"keywords": [
"password",
"invalid",
"error"
]
},
{
"index": 17,
"url": "https://proton.me/support/protonmail-bridge-clients-windows-outlook-2019",
"title": "Proton Mail Bridge Microsoft Outlook for Windows 2019 setup guide",
"keywords": [
"Outlook",
"2019",
"setup",
"configuration"
]
},
{
"index": 18,
"url": "https://proton.me/support/protonmail-bridge-clients-windows-outlook-2016",
"title": "Proton Mail Bridge Microsoft Outlook 2016 for Windows setup guide",
"keywords": [
"Outlook",
"2019",
"setup",
"configuration"
]
},
{
"index": 19,
"url": "https://proton.me/support/protonmail-bridge-clients-apple-mail",
"title": "Proton Mail Bridge Apple Mail setup guide",
"keywords": [
"Apple Mail",
"setup",
"configuration"
]
},
{
"index": 20,
"url": "https://proton.me/support/protonmail-bridge-clients-macos-new-outlook",
"title": "Proton Mail Bridge new Outlook for macOS setup guide",
"keywords": [
"Outlook",
"setup",
"configuration"
]
},
{
"index": 21,
"url": "https://proton.me/support/protonmail-bridge-clients-windows-thunderbird",
"title": "Proton Mail Bridge Thunderbird setup guide for Windows, macOS, and Linux",
"keywords": [
"Thunderbird",
"setup",
"configuration"
]
},
{
"index": 22,
"url": "https://proton.me/support/protonmail-bridge-clients-macos-outlook-2016",
"title": "Proton Mail Bridge Microsoft Outlook 2016 for macOS setup guide",
"keywords": [
"Outlook 2016",
"macOS"
]
},
{
"index": 23,
"url": "https://proton.me/support/protonmail-bridge-clients-macos-outlook-2019",
"title": "Proton Mail Bridge Microsoft Outlook 2019 for macOS setup guide",
"keywords": [
"Outlook 2019",
"macOS"
]
},
{
"index": 24,
"url": "https://proton.me/support/protonmail-bridge-clients-windows-outlook-2013",
"title": "Proton Mail Bridge Microsoft Outlook 2013 for Windows setup guide",
"keywords": [
"Outlook 2013",
"macOS"
]
},
{
"index": 25,
"url": "https://proton.me/support/protonmail-bridge-clients-macos-outlook-2011",
"title": "Proton Mail Bridge Microsoft Outlook 2011 for macOS setup guide",
"keywords": [
"Outlook 2011",
"macOS"
]
},
{
"index": 26,
"url": "https://proton.me/support/install-bridge-linux-pkgbuild-file",
"title": "Installing Proton Mail Bridge for Linux using a PKGBUILD file",
"keywords": [
"Linux",
"pkgbuild"
]
},
{
"index": 27,
"url": "https://proton.me/support/installing-bridge-linux-deb-file",
"title": "Installing Proton Mail Bridge for Linux using a DEB file",
"keywords": [
"Linux",
"deb"
]
},
{
"index": 28,
"url": "https://proton.me/support/verifying-bridge-package",
"title": "Verifying the Proton Mail Bridge package for Linux",
"keywords": [
"Linux",
"Package",
"Verify"
]
},
{
"index": 29,
"url": "https://proton.me/support/bridge-cli-guide",
"title": "Bridge CLI (command line interface) guide",
"keywords": [
"CLI",
"Terminal",
"Command-line",
"Powershell"
]
},
{
"index": 30,
"url": "https://proton.me/support/install-bridge-linux-rpm-file",
"title": "Installing Proton Mail Bridge for Linux using an RPM file",
"keywords": [
"Linux",
"Install",
"RPM"
]
},
{
"index": 31,
"url": "https://proton.me/support/bridge-linux-login-error",
"title": "How to fix Proton Bridge login errors",
"keywords": [
"login",
"error",
"connection"
]
},
{
"index": 32,
"url": "https://proton.me/support/bridge-linux-tray-icon",
"title": "How to fix a missing system tray icon in Linux",
"keywords": [
"linux",
"tray",
"notification"
]
},
{
"index": 33,
"url": "https://proton.me/support/why-you-need-bridge",
"title": "Why you need Proton Mail Bridge",
"keywords": [
"Bridge",
"email",
"client"
]
},
{
"index": 34,
"url": "https://proton.me/support/protonmail-bridge-manual-update",
"title": "How to manually update Proton Mail Bridge",
"keywords": [
"update",
"upgrade",
"manual",
"download"
]
},
{
"index": 35,
"url": "https://proton.me/support/macos-certificate-warning",
"title": "Warning when installing Proton Mail Bridge on macOS",
"keywords": [
"install",
"macOS",
"warning",
"certificate"
]
},
{
"index": 36,
"url": "https://proton.me/support/apple-mail-certificate",
"title": "Why you need to install a certificate for Apple Mail with Proton Mail Bridge",
"keywords": [
"certificate",
"Apple Mail",
"macOS"
]
}
]

107
internal/kb/suggester.go Normal file
View File

@ -0,0 +1,107 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package kb
import (
_ "embed"
"encoding/json"
"errors"
"regexp"
"strings"
"github.com/bradenaw/juniper/xslices"
"golang.org/x/exp/slices"
)
var ErrArticleNotFound = errors.New("KB article not found")
//go:embed kbArticleList.json
var articleListString []byte
// Article is a struct that holds information about a knowledge-base article.
type Article struct {
Index uint64 `json:"index"`
URL string `json:"url"`
Title string `json:"title"`
Keywords []string `json:"keywords"`
Score int
}
type ArticleList []*Article
// GetArticleList returns the list of KB articles.
func GetArticleList() (ArticleList, error) {
var articles ArticleList
err := json.Unmarshal(articleListString, &articles)
return articles, err
}
// GetSuggestions returns a list of up to 3 suggestions for the built-in list of KB articles matching the given user input.
func GetSuggestions(userInput string) (ArticleList, error) {
articles, err := GetArticleList()
if err != nil {
return ArticleList{}, err
}
return GetSuggestionsFromArticleList(userInput, articles)
}
// GetSuggestionsFromArticleList returns a list of up to 3 suggestions for the given list of KB articles matching the given user input.
func GetSuggestionsFromArticleList(userInput string, articles ArticleList) (ArticleList, error) {
userInput = strings.ToUpper(userInput)
for _, article := range articles {
for _, keyword := range article.Keywords {
if strings.Contains(userInput, strings.ToUpper(keyword)) {
article.Score++
}
}
}
articles = xslices.Filter(articles, func(article *Article) bool { return article.Score > 0 })
slices.SortFunc(articles, func(lhs, rhs *Article) bool { return lhs.Score > rhs.Score })
if len(articles) > 3 {
return articles[:3], nil
}
return articles, nil
}
// GetArticleIndex retrieves the index of an article from its url. if the article is not found, ErrArticleNotFound is returned.
func GetArticleIndex(url string) (uint64, error) {
articles, err := GetArticleList()
if err != nil {
return 0, err
}
index := xslices.IndexFunc(articles, func(article *Article) bool { return strings.EqualFold(article.URL, url) })
if index == -1 {
return 0, ErrArticleNotFound
}
return uint64(index), nil
}
func simplifyUserInput(input string) string {
// replace any sequence not matching of the following with a single space:
// - letters in any language (accentuated or not)
// - numbers
// - the apostrophe character '
return strings.TrimSpace(regexp.MustCompile(`[^\p{L}\p{N}']+`).ReplaceAllString(input, " "))
}

View File

@ -0,0 +1,91 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package kb
import (
"testing"
"github.com/stretchr/testify/require"
)
func Test_ArticleList(t *testing.T) {
articles, err := GetArticleList()
require.NoError(t, err)
require.NotEmpty(t, articles)
var bits uint64
for _, article := range articles {
require.Truef(t, article.Index < 64, "Invalid KB article index %d, (must be < 64)", article.Index)
require.Zerof(t, bits&(1<<article.Index), "Duplicate index %d in knowledge base", article.Index)
bits |= bits | (1 << article.Index)
require.NotEmpty(t, article.URL, "KB article with index %d has no URL", article.Index)
require.NotEmpty(t, article.Title, "KB article with index %d has no title", article.Index)
require.NotEmpty(t, article.Keywords, "KB article with index %d has no keyword", article.Index)
}
}
func Test_GetSuggestions(t *testing.T) {
suggestions, err := GetSuggestions("Thunderbird is not working, error during password")
require.NoError(t, err)
count := len(suggestions)
require.True(t, (count > 0) && (count <= 3))
suggestions, err = GetSuggestions("Supercalifragilisticexpialidocious Sesquipedalian Worcestershire")
require.NoError(t, err)
require.Empty(t, suggestions)
}
func Test_GetSuggestionsFromArticleList(t *testing.T) {
articleList := ArticleList{}
suggestions, err := GetSuggestionsFromArticleList("Thunderbird", articleList)
require.NoError(t, err)
require.Empty(t, suggestions)
articleList = ArticleList{
&Article{
Index: 0,
URL: "https://proton.me",
Title: "Proton home page",
Keywords: []string{"proton"},
},
&Article{
Index: 1,
URL: "https://mozilla.org",
Title: "Mozilla home page",
Keywords: []string{"mozilla"},
},
}
suggestions, err = GetSuggestionsFromArticleList("PRoToN", articleList)
require.NoError(t, err)
require.Len(t, suggestions, 1)
require.Equal(t, suggestions[0].URL, "https://proton.me")
}
func Test_GetArticleIndex(t *testing.T) {
index1, err := GetArticleIndex("https://proton.me/support/bridge-for-linux")
require.NoError(t, err)
index2, err := GetArticleIndex("HTTPS://PROTON.ME/support/bridge-for-linux")
require.NoError(t, err)
require.Equal(t, index1, index2)
_, err = GetArticleIndex("https://proton.me")
require.ErrorIs(t, err, ErrArticleNotFound)
}
func Test_simplifyUserInput(t *testing.T) {
require.Equal(t, "word1 ñóÄ don't déjà 33 pizza", simplifyUserInput(" \nword1 \n\tñóÄ don't\n\n\ndéjà, 33 pizza=🍕\n,\n"))
}

View File

@ -102,9 +102,6 @@ func TestMetadataStage_JobCorrectlyFinishesAfterCancel(t *testing.T) {
// read one output then cancel
request, err := output.Consume(ctx)
require.NoError(t, err)
request.onFinished(ctx)
// cancel job context
jobCancel()
wg := sync.WaitGroup{}
wg.Add(1)
@ -117,10 +114,13 @@ func TestMetadataStage_JobCorrectlyFinishesAfterCancel(t *testing.T) {
return
}
// cancel job context
jobCancel()
req.checkCancelled()
}
}()
wg.Wait()
request.onFinished(ctx)
err = tj.job.waitAndClose(ctx)
require.Error(t, err)
require.ErrorIs(t, err, context.Canceled)

View File

@ -20,9 +20,11 @@ package user
import (
"context"
"encoding/json"
"errors"
"github.com/ProtonMail/gluon/reporter"
"github.com/ProtonMail/proton-bridge/v3/internal/configstatus"
"github.com/ProtonMail/proton-bridge/v3/internal/kb"
)
func (user *User) SendConfigStatusSuccess(ctx context.Context) {
@ -193,39 +195,25 @@ func (user *User) AutoconfigUsed(client string) {
}
}
func (user *User) ExternalLinkClicked(article string) {
func (user *User) ExternalLinkClicked(url string) {
if !user.configStatus.IsPending() {
return
}
var trackedLinks = [...]string{
"https://proton.me/support/bridge",
"https://proton.me/support/protonmail-bridge-clients-apple-mail",
"https://proton.me/support/protonmail-bridge-clients-macos-outlook-2019",
"https://proton.me/support/protonmail-bridge-clients-windows-outlook-2019",
"https://proton.me/support/protonmail-bridge-clients-windows-thunderbird",
"https://proton.me/support/protonmail-bridge-configure-client",
"https://proton.me/support/bridge-address-list-has-changed",
"https://proton.me/blog/tls-ssl-certificate#Extra-security-precautions-taken-by-ProtonMail",
"https://proton.me/support/bridge-cant-move-cache",
"https://proton.me/support/difference-combined-addresses-mode-split-addresses-mode",
"https://proton.me/support/bridge-imap-login-failed",
"https://proton.me/support/port-already-occupied-error",
"https://proton.me/support/bridge-cannot-access-keychain",
"https://proton.me/support/protonmail-bridge-manual-update",
"https://proton.me/support/bridge-internal-error",
"https://proton.me/support/apple-mail-certificate",
"https://proton.me/support/macos-certificate-warning",
"https://proton.me/support/why-you-need-bridge",
const externalLinkWasClicked = "External link was clicked."
index, err := kb.GetArticleIndex(url)
if err != nil {
if errors.Is(err, kb.ErrArticleNotFound) {
user.log.WithField("report", false).WithField("url", url).Debug(externalLinkWasClicked)
} else {
user.log.WithError(err).Error("Failed to retrieve list of KB articles.")
}
return
}
for id, url := range trackedLinks {
if url == article {
if err := user.configStatus.RecordLinkClicked(uint(id)); err != nil {
user.log.WithError(err).Error("Failed to log LinkClicked in config_status.")
}
return
}
if err := user.configStatus.RecordLinkClicked(index); err != nil {
user.log.WithError(err).Error("Failed to log LinkClicked in config_status.")
} else {
user.log.WithField("report", true).WithField("url", url).Debug(externalLinkWasClicked)
}
user.log.WithField("article", article).Error("Failed to find KB article id.")
}

View File

@ -1,21 +0,0 @@
---
wait: true
strict: true
file:
name: "./gobinsec-cache.yml"
expiration: "24h"
ignore:
# golang.org/x/net wrong match, we are using v0.1.0, fixed by 37e1c6af in v0.0.xxx
- "CVE-2021-33194"
# golang.org/x/crypto wrong match, we are using v0.1.0 all of this have been fixed in vO.O.xx
- "CVE-2019-11840"
- "CVE-2020-29652"
- "CVE-2021-43565"
- "CVE-2022-27191"
- "CVE-2020-9283"
- "CVE-2017-3204"
# golang.org/x/text wrong match, we are using v0.4.0, fixed in a previous version
- "CVE-2020-14040"

View File

@ -1,32 +0,0 @@
#!/bin/bash
# Copyright (c) 2023 Proton AG
#
# This file is part of Proton Mail Bridge.
#
# Proton Mail Bridge is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Proton Mail Bridge is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
cd "$(\
cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd \
)"/gobinsec_update || exit 1
for i in $(seq 10); do
echo "Trying $i"
if make run; then
echo "Try $i succeeded"
break
fi
echo "Waiting to try again..."
sleep 30
done

View File

@ -1,6 +0,0 @@
run:
FILECACHE_FILE=gobinsec-cache-valid.yml \
FILECACHE_EXPIRATION=1h \
go run main.go

View File

@ -1,15 +0,0 @@
module gobinsec_update
go 1.18
require github.com/intercloud/gobinsec v0.10.2
require (
github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/memcachier/gomemcache v0.0.0-20170425125614-d027381f7653 // indirect
golang.org/x/sys v0.1.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@ -1,33 +0,0 @@
github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d h1:pVrfxiGfwelyab6n21ZBkbkmbevaf+WvMIiR7sr97hw=
github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/intercloud/gobinsec v0.10.2 h1:4L2d4SaIqlHnUQ6Hlg1E51dqUg4jK+TpSILVTHaEvx4=
github.com/intercloud/gobinsec v0.10.2/go.mod h1:Y/AMKT0aQM40WDkTqlEe18W/IL6ZUuuJjdOXdayi+CI=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/memcachier/gomemcache v0.0.0-20170425125614-d027381f7653 h1:222emoxOt/bCmNHp8Xt0Pr5Am3gIbqRKFpb4CQ9O2SI=
github.com/memcachier/gomemcache v0.0.0-20170425125614-d027381f7653/go.mod h1:KoYVbOQexD45AOLfn+gsFB6c3o4ANzP1QKzjE6tZbK0=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 h1:xHms4gcpe1YE7A3yIllJXP16CMAGuqwO2lX1mTyyRRc=
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -1,76 +0,0 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>
package main
import (
"fmt"
"io/ioutil"
"regexp"
"strings"
"github.com/intercloud/gobinsec/gobinsec"
)
type Depend struct {
Name string
Version string
}
func loadDependencies(file string) []Depend {
var dependencies []Depend
txt, err := ioutil.ReadFile(file)
if err != nil {
return dependencies
}
re := regexp.MustCompile(`\t[a-zA-Z0-9-\/\.]* v.*`)
matches := re.FindAllString(string(txt), -1)
for _, str := range matches {
withoutTab := strings.Split(str, "\t")
split := strings.Split(withoutTab[1], " ")
dependencies = append(dependencies, Depend{split[0], split[1]})
}
return dependencies
}
func main() {
dependencies := loadDependencies("../../go.mod")
if err := gobinsec.LoadConfig("", true, true, true, true); err != nil {
panic(err)
}
if err := gobinsec.BuildCache(); err != nil {
panic(err)
}
for _, dep := range dependencies {
fmt.Println("... Checking " + dep.Name + " " + dep.Version)
dep, err := gobinsec.NewDependency(dep.Name, dep.Version)
if err != nil {
panic(err)
}
if err := dep.LoadVulnerabilities(); err != nil {
panic(err)
}
if err := gobinsec.CacheInstance.Close(); err != nil {
panic(err)
}
}
}

71
utils/govulncheck.sh Executable file
View File

@ -0,0 +1,71 @@
#!/usr/bin/env bash
# Copyright (c) 2023 Proton AG
#
# This file is part of Proton Mail Bridge.
#
# Proton Mail Bridge is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Proton Mail Bridge is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
set -eo pipefail
main(){
go install golang.org/x/vuln/cmd/govulncheck@latest
make gofiles
govulncheck -json ./... > vulns.json
jq -r '.osv.id | select( . != null )' < vulns.json > vulns_osv_ids.txt
ignore GO-2023-2102 "GODT-3160 update go to 1.21.4"
ignore GO-2023-2043 "GODT-3160 update go to 1.21.4"
ignore GO-2023-2041 "GODT-3160 update go to 1.21.4"
ignore GO-2023-1878 "GODT-3160 update go to 1.21.4"
ignore GO-2023-1987 "GODT-3160 update go to 1.21.4"
ignore GO-2023-1840 "GODT-3160 update go to 1.21.4"
ignore GO-2023-2185 "GODT-3160 update go to 1.21.4"
ignore GO-2023-2186 "GODT-3160 update go to 1.21.4"
ignore GO-2023-2382 "GODT-3160 update go to 1.21.4"
ignore GO-2023-2328 "GODT-3124 RESTY race condition"
has_vulns
echo
echo "No new vulnerabilities found."
}
ignore(){
echo "ignoring $1 fix: $2"
cp vulns_osv_ids.txt tmp
grep -v "$1" < tmp > vulns_osv_ids.txt || true
rm tmp
}
has_vulns(){
has=false
while read -r osv; do
jq \
--arg osvid "$osv" \
'.osv | select ( .id == $osvid) | {"id":.id, "ranges": .affected[0].ranges, "import": .affected[0].ecosystem_specific.imports[0].path}' \
< vulns.json
has=true
done < vulns_osv_ids.txt
if [ "$has" == true ]; then
echo
echo "Vulnerability found"
return 1
fi
}
main

View File

@ -0,0 +1,137 @@
// Copyright (c) 2023 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package main
import (
"encoding/json"
"errors"
"fmt"
"io"
"log"
"os"
"path/filepath"
"github.com/ProtonMail/proton-bridge/v3/internal/kb"
"github.com/urfave/cli/v2"
)
const flagArticles = "articles"
const flagInput = "input"
func main() {
app := &cli.App{
Name: "kb-suggester",
Usage: "test bridge KB article suggester",
HideHelpCommand: true,
ArgsUsage: "",
Flags: []cli.Flag{
&cli.StringFlag{
Name: flagArticles,
Aliases: []string{"a"},
Usage: "use `articles.json` as the JSON article list",
TakesFile: true,
},
&cli.StringFlag{
Name: flagInput,
Aliases: []string{"i"},
Usage: "read user input from the `userInput` file",
TakesFile: true,
},
},
Action: run,
}
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}
}
func getUserInput(ctx *cli.Context) (string, error) {
inputFile := ctx.String(flagInput)
var bytes []byte
var err error
if len(inputFile) == 0 {
var fi os.FileInfo
if fi, err = os.Stdin.Stat(); err != nil {
return "", err
}
if (fi.Mode() & os.ModeNamedPipe) == 0 {
fmt.Println("Type your input, Ctrl+D to finish: ")
}
bytes, err = io.ReadAll(os.Stdin)
} else {
bytes, err = os.ReadFile(filepath.Clean(inputFile))
}
if err != nil {
return "", err
}
return string(bytes), nil
}
func getArticleList(ctx *cli.Context) (kb.ArticleList, error) {
articleFile := ctx.String(flagArticles)
if len(articleFile) == 0 {
return kb.GetArticleList()
}
bytes, err := os.ReadFile(filepath.Clean(articleFile))
if err != nil {
return nil, err
}
var result kb.ArticleList
err = json.Unmarshal(bytes, &result)
return result, err
}
func run(ctx *cli.Context) error {
if ctx.Args().Len() > 0 {
_ = cli.ShowAppHelp(ctx)
return errors.New("command accept no argument")
}
articles, err := getArticleList(ctx)
if err != nil {
return err
}
userInput, err := getUserInput(ctx)
if err != nil {
return err
}
suggestions, err := kb.GetSuggestionsFromArticleList(userInput, articles)
if err != nil {
return err
}
if len(suggestions) == 0 {
fmt.Println("No suggestions found")
return nil
}
for _, suggestion := range suggestions {
fmt.Printf("Score %v: %v (%v)\n", suggestion.Score, suggestion.Title, suggestion.URL)
}
return nil
}