Page MenuHome GnuPG

No OneTemporary

diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt
index 1764c58..8b3ebe2 100644
--- a/client/CMakeLists.txt
+++ b/client/CMakeLists.txt
@@ -1,326 +1,320 @@
# SPDX-FileCopyrightText: 2023 g10 code GmbH
# SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
# SPDX-License-Identifier: BSD-2-Clause
add_definitions(-DDATAROUTDIR="${KDE_INSTALL_FULL_DATAROOTDIR}")
add_library(gpgol-client-static STATIC)
target_sources(gpgol-client-static PRIVATE
emailviewer.cpp
emailviewer.h
websocketclient.cpp
websocketclient.h
- webserver.h
- webserver.cpp
messagedispatcher.cpp
messagedispatcher.h
firsttimedialog.cpp
firsttimedialog.h
# Identity
identity/addressvalidationjob.cpp
identity/addressvalidationjob.h
identity/identitymanager.cpp
identity/identitymanager.h
identity/identitydialog.cpp
identity/identitydialog.h
identity/identity.cpp
identity/identity.h
identity/signature.h
identity/signature.cpp
identity/signatureconfigurator.cpp
identity/signatureconfigurator.h
identity/signaturerichtexteditor.cpp
identity/signaturerichtexteditor_p.h
- # HTTP Controller
- controllers/emailcontroller.cpp
- controllers/emailcontroller.h
-
# Draft
draft/draft.cpp
draft/draft.h
draft/draftmanager.cpp
draft/draftmanager.h
# EWS integration
ews/ewsattachment.cpp
ews/ewsattachment.h
ews/ewsattendee.cpp
ews/ewsattendee.h
ews/ewsclient_debug.cpp
ews/ewsclient_debug.h
ews/ewsid.cpp
ews/ewsid.h
ews/ewsitem.cpp
ews/ewsitem.h
ews/ewsitembase.cpp
ews/ewsitembase.h
ews/ewsitembase_p.h
ews/ewsmailbox.cpp
ews/ewsmailbox.h
ews/ewsmailfactory.cpp
ews/ewsmailfactory.h
ews/ewsmessagedispatcher.cpp
ews/ewsmessagedispatcher.h
ews/ewsoccurrence.cpp
ews/ewsoccurrence.h
ews/ewspropertyfield.cpp
ews/ewspropertyfield.h
ews/ewsrecurrence.cpp
ews/ewsrecurrence.h
ews/ewsserverversion.cpp
ews/ewsserverversion.h
ews/ewstypes.cpp
ews/ewstypes.h
ews/ewsxml.cpp
ews/ewsxml.h
# Editor
editor/addresseelineedit.cpp
editor/addresseelineedit.h
editor/addresseelineeditmanager.cpp
editor/addresseelineeditmanager.h
editor/composerviewbase.cpp
editor/composerviewbase.h
editor/composerwindow.cpp
editor/composerwindow.h
editor/composerwindowfactory.cpp
editor/composerwindowfactory.h
editor/cryptostateindicatorwidget.cpp
editor/cryptostateindicatorwidget.h
editor/kmcomposerglobalaction.cpp
editor/kmcomposerglobalaction.h
editor/nearexpirywarning.cpp
editor/nearexpirywarning.h
editor/mailtemplates.cpp
editor/mailtemplates.h
editor/recipient.cpp
editor/recipient.h
editor/recipientline.cpp
editor/recipientline.h
editor/recipientseditor.cpp
editor/recipientseditor.h
editor/util.h
editor/util.cpp
editor/kmailcompletion.cpp
editor/kmailcompletion.h
editor/richtextcomposerng.cpp
editor/richtextcomposerng.h
editor/richtextcomposersignatures.cpp
editor/richtextcomposersignatures.h
editor/nodehelper.cpp
editor/nodehelper.h
editor/signaturecontroller.cpp
editor/signaturecontroller.h
editor/spellcheckerconfigdialog.cpp
editor/spellcheckerconfigdialog.h
# Editor job
editor/job/abstractencryptjob.h
editor/job/autocryptheadersjob.h
editor/job/contentjobbase.h
editor/job/contentjobbase_p.h
editor/job/composerjob.cpp
editor/job/composerjob.h
editor/job/encryptjob.h
editor/job/inserttextfilejob.h
editor/job/itipjob.h
editor/job/jobbase.h
editor/job/jobbase_p.h
editor/job/maintextjob.h
editor/job/multipartjob.h
editor/job/protectedheadersjob.h
editor/job/signencryptjob.h
editor/job/signjob.h
editor/job/singlepartjob.h
editor/job/skeletonmessagejob.h
editor/job/transparentjob.h
editor/job/autocryptheadersjob.cpp
editor/job/contentjobbase.cpp
editor/job/encryptjob.cpp
editor/job/inserttextfilejob.cpp
editor/job/itipjob.cpp
editor/job/jobbase.cpp
editor/job/maintextjob.cpp
editor/job/multipartjob.cpp
editor/job/protectedheadersjob.cpp
editor/job/saveasfilejob.cpp
editor/job/saveasfilejob.h
editor/job/signencryptjob.cpp
editor/job/signjob.cpp
editor/job/singlepartjob.cpp
editor/job/skeletonmessagejob.cpp
editor/job/transparentjob.cpp
## Editor Part
editor/part/globalpart.h
editor/part/infopart.h
editor/part/itippart.h
editor/part/messagepart.h
editor/part/textpart.h
editor/part/globalpart.cpp
editor/part/infopart.cpp
editor/part/itippart.cpp
editor/part/messagepart.cpp
editor/part/textpart.cpp
## Attachment
editor/attachment/attachmentjob.cpp
editor/attachment/attachmentjob.h
editor/attachment/attachmentclipboardjob.cpp
editor/attachment/attachmentclipboardjob.h
editor/attachment/attachmentcompressjob.cpp
editor/attachment/attachmentcompressjob.h
editor/attachment/attachmentcontroller.cpp
editor/attachment/attachmentcontroller.h
editor/attachment/attachmentcontrollerbase.cpp
editor/attachment/attachmentcontrollerbase.h
editor/attachment/attachmentfromfolderjob.cpp
editor/attachment/attachmentfromfolderjob.h
editor/attachment/attachmentfrommimecontentjob.cpp
editor/attachment/attachmentfrommimecontentjob.h
editor/attachment/attachmentfromurlbasejob.cpp
editor/attachment/attachmentfromurlbasejob.h
editor/attachment/attachmentfromurljob.cpp
editor/attachment/attachmentfromurljob.h
editor/attachment/attachmentfromurlutils.cpp
editor/attachment/attachmentfromurlutils.h
editor/attachment/attachmentfrompublickeyjob.cpp
editor/attachment/attachmentfrompublickeyjob.h
editor/attachment/attachmentloadjob.cpp
editor/attachment/attachmentloadjob.h
editor/attachment/attachmentmodel.cpp
editor/attachment/attachmentmodel.h
editor/attachment/attachmentpart.cpp
editor/attachment/attachmentpart.h
editor/attachment/attachmentpropertiesdialog.cpp
editor/attachment/attachmentpropertiesdialog.h
editor/attachment/attachmentupdatejob.cpp
editor/attachment/attachmentupdatejob.h
editor/attachment/attachmentview.cpp
editor/attachment/attachmentview.h
utils/kuniqueservice.h
utils/kuniqueservice.cpp
)
if (WIN32)
add_definitions(-DHAVE_QDBUS=false)
target_sources(gpgol-client-static PRIVATE utils/kuniqueservice_win.cpp)
else()
add_definitions(-DHAVE_QDBUS=true)
target_sources(gpgol-client-static PRIVATE utils/kuniqueservice_dbus.cpp)
endif()
ki18n_wrap_ui(gpgol-client-static
editor/attachment/ui/attachmentpropertiesdialog.ui
editor/attachment/ui/attachmentpropertiesdialog_readonly.ui
)
ecm_qt_declare_logging_category(gpgol-client-static_SRCS
HEADER gpgol_client_debug.h
IDENTIFIER GPGOL_CLIENT_LOG
CATEGORY_NAME org.gpgol.client
DESCRIPTION "General client log"
EXPORT GPGOL
)
ecm_qt_declare_logging_category(gpgol-client-static_SRCS
HEADER websocket_debug.h
IDENTIFIER WEBSOCKET_LOG
CATEGORY_NAME org.gpgol.client.websocket
DESCRIPTION "Websocket connection in the client"
EXPORT GPGOL
)
ecm_qt_declare_logging_category(gpgol-client-static_SRCS
HEADER ewsresource_debug.h
IDENTIFIER EWSRES_LOG
CATEGORY_NAME org.gpgol.ews
DESCRIPTION "Ews mail client"
EXPORT GPGOL
)
ecm_qt_declare_logging_category(gpgol-client-static_SRCS
HEADER ewscli_debug.h
IDENTIFIER EWSCLI_LOG
CATEGORY_NAME org.gpgol.ews.client
DESCRIPTION "ews client (gpgol-client)"
EXPORT GPGOL
)
ecm_qt_declare_logging_category(gpgol-client-static_SRCS
HEADER editor_debug.h
IDENTIFIER EDITOR_LOG
CATEGORY_NAME org.gpgol.editor
DESCRIPTION "mail composer"
EXPORT GPGOL
)
set(WARN_TOOMANY_RECIPIENTS_DEFAULT true)
set(ALLOW_SEMICOLON_AS_ADDRESS_SEPARATOR_DEFAULT true)
configure_file(editor/settings/messagecomposer.kcfg.in ${CMAKE_CURRENT_BINARY_DIR}/messagecomposer.kcfg)
kconfig_add_kcfg_files(gpgol-client-static editor/settings/messagecomposersettings.kcfgc config.kcfgc)
install(FILES composerui.rc DESTINATION ${KDE_INSTALL_KXMLGUIDIR}/gpgol-client)
target_sources(gpgol-client-static PUBLIC ${gpgol-client-static_SRCS})
ki18n_wrap_ui(gpgol-client-static firsttimedialog.ui)
target_link_libraries(gpgol-client-static PUBLIC
common
LibGpgError::LibGpgError
Qt6::HttpServer
Qt6::Widgets
Qt6::PrintSupport
Qt6::WebSockets
KF6::I18n
KF6::JobWidgets
KF6::CalendarCore
KF6::ConfigCore
KF6::ConfigGui
KF6::Contacts
KF6::Completion
KF6::CoreAddons
KF6::ColorScheme
KF6::Codecs
KF6::GuiAddons
KF6::SonnetUi
KF6::WidgetsAddons
KF6::XmlGui
KF6::Archive
KF6::TextAutoCorrectionCore
KPim6::MimeTreeParserCore
KPim6::MimeTreeParserWidgets
KPim6::Libkleo
KPim6::Libkdepim
KPim6::PimTextEdit
${_gpgol_dbusaddons_libs}
)
add_executable(gpgol-client main.cpp)
target_link_libraries(gpgol-client PRIVATE gpgol-client-static)
qt_add_resources(gpgol-client
PREFIX
"/"
FILES
assets/64-gpgpass.png
)
if (BUILD_TESTING)
add_subdirectory(autotests)
endif()
install(TARGETS gpgol-client ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
install(FILES com.gnupg.gpgoljs.desktop DESTINATION ${KDE_INSTALL_APPDIR})
install(FILES com.gnupg.gpgoljs.svg DESTINATION ${KDE_INSTALL_FULL_ICONDIR}/hicolor/scalable/apps)
diff --git a/client/autotests/CMakeLists.txt b/client/autotests/CMakeLists.txt
index 132bb80..5a47a36 100644
--- a/client/autotests/CMakeLists.txt
+++ b/client/autotests/CMakeLists.txt
@@ -1,55 +1,50 @@
# SPDX-FileCopyrightText: 2023 g10 code GmbH
# SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
# SPDX-License-Identifier: BSD-2-Clause
add_subdirectory(gnupg_home)
add_definitions(-DDATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/testdata" )
-ecm_add_test(emailcontrollertest.cpp
- LINK_LIBRARIES gpgol-client-static Qt::Test
- NAME_PREFIX "client-http-"
-)
-
ecm_add_test(globalparttest.cpp
LINK_LIBRARIES gpgol-client-static Qt::Test
NAME_PREFIX "client-part-"
)
ecm_add_test(infoparttest.cpp
LINK_LIBRARIES gpgol-client-static Qt::Test
NAME_PREFIX "client-part-"
)
ecm_add_test(itipjobtest.cpp
LINK_LIBRARIES gpgol-client-static Qt::Test
NAME_PREFIX "client-job-"
)
ecm_add_test(multipartjobtest.cpp
LINK_LIBRARIES gpgol-client-static Qt::Test
NAME_PREFIX "client-job-"
)
ecm_add_test(attachmentjobtest.cpp
LINK_LIBRARIES gpgol-client-static Qt::Test
NAME_PREFIX "client-job-"
)
include(add_gpg_crypto_test)
function(add_gpgoljs_crypto_unittest _name)
add_executable(${_name} ${_name}.cpp setupenv.cpp)
target_link_libraries(${_name} PRIVATE Gpgmepp)
target_include_directories(${_name} PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/..
${GPGME_INCLUDE_DIRS}
)
target_link_libraries(${_name} PRIVATE
Qt::Test
gpgol-client-static
)
add_gpg_crypto_test(${_name} client-${_name})
endfunction()
add_gpgoljs_crypto_unittest(composerviewbasetest)
diff --git a/client/autotests/emailcontrollertest.cpp b/client/autotests/emailcontrollertest.cpp
deleted file mode 100644
index 15ff624..0000000
--- a/client/autotests/emailcontrollertest.cpp
+++ /dev/null
@@ -1,151 +0,0 @@
-// SPDX-FileCopyrightText: 2023 g10 code GmbH
-// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#include <MimeTreeParserWidgets/MessageViewerDialog>
-#include <QCoreApplication>
-#include <QFile>
-#include <QJsonArray>
-#include <QJsonDocument>
-#include <QJsonObject>
-#include <QNetworkAccessManager>
-#include <QNetworkReply>
-#include <QObject>
-#include <QSignalSpy>
-#include <QStandardPaths>
-#include <QTest>
-#include <QToolBar>
-
-#include "../draft/draftmanager.h"
-#include "../editor/composerwindow.h"
-#include "../editor/recipientseditor.h"
-#include "../webserver.h"
-#include "../websocketclient.h"
-
-#include <KLocalizedString>
-#include <QWebSocket>
-#include <QWebSocketServer>
-
-using namespace Qt::Literals::StringLiterals;
-
-class EmailControllerTest : public QObject
-{
- Q_OBJECT
-
-private Q_SLOTS:
- void initTestCase()
- {
- DraftManager::self(true);
- QCoreApplication::setApplicationName(u"gpgol-server"_s);
-
- KLocalizedString::setApplicationDomain(QByteArrayLiteral("gpgol"));
-
- m_webServer = new WebServer;
- m_webServer->run();
-
- auto webSocketServer = new QWebSocketServer(QStringLiteral("SSL Server"), QWebSocketServer::NonSecureMode);
-
- if (webSocketServer->listen(QHostAddress::Any, 5657)) { }
- }
-
- void testInfoEmailAction()
- {
- QFile file(QStringLiteral(DATA_DIR) + u"/encrypted.mbox"_s);
- QVERIFY(file.open(QIODeviceBase::ReadOnly));
-
- QNetworkRequest request(QUrl(u"http://127.0.0.1:%1/info"_s.arg(m_webServer->port())));
- auto reply = m_qnam.post(request, file.readAll());
- QSignalSpy spy(reply, &QNetworkReply::finished);
- spy.wait();
-
- qWarning() << reply->errorString();
- QCOMPARE(reply->error(), QNetworkReply::NoError);
-
- const auto doc = QJsonDocument::fromJson(reply->readAll());
- QVERIFY(!doc.isNull() && doc.isObject());
-
- const auto object = doc.object();
- QVERIFY(object["drafts"_L1].toArray().isEmpty());
- QVERIFY(object["encrypted"_L1].toBool());
- QVERIFY(!object["signed"_L1].toBool());
- }
-
- void testViewEmailAction()
- {
- QFile file(QStringLiteral(DATA_DIR) + u"/plaintext.mbox"_s);
- QVERIFY(file.open(QIODeviceBase::ReadOnly));
-
- QNetworkRequest request(QUrl(u"http://127.0.0.1:%1/view"_s.arg(m_webServer->port())));
- auto reply = m_qnam.post(request, file.readAll());
- QSignalSpy spy(reply, &QNetworkReply::finished);
- spy.wait();
-
- QCOMPARE(reply->error(), QNetworkReply::NoError);
-
- const auto widgets = qApp->topLevelWidgets();
- QVERIFY(!widgets.isEmpty());
-
- MimeTreeParser::Widgets::MessageViewerDialog *dialog = nullptr;
- for (auto widget : widgets) {
- if (!widget->isHidden()) {
- if (const auto messageViewer = qobject_cast<MimeTreeParser::Widgets::MessageViewerDialog *>(widget)) {
- dialog = messageViewer;
- break;
- }
- }
- }
-
- QVERIFY(dialog);
-
- WebsocketClient::self(QUrl(u"ws://127.0.0.1"_s), 5656);
-
- const auto toolBar = dialog->toolBar();
- QVERIFY(toolBar->isVisible());
-
- const auto actions = toolBar->actions();
- QCOMPARE(actions.count(), 3);
- qWarning() << actions;
-
- QCOMPARE(actions[1]->icon().name(), u"mail-reply-sender-symbolic"_s);
- actions[1]->trigger();
-
- const auto widgets2 = qApp->topLevelWidgets();
- QVERIFY(!widgets2.isEmpty());
-
- ComposerWindow *composer = nullptr;
- for (auto widget : widgets2) {
- if (!widget->isHidden()) {
- if (const auto composerWindow = qobject_cast<ComposerWindow *>(widget)) {
- composer = composerWindow;
- break;
- }
- }
- }
- QVERIFY(composer);
-
- QSignalSpy spyInit(composer, &ComposerWindow::initialized);
- spyInit.wait();
-
- QCOMPARE(composer->subject(), u"RE: A random subject with alternative contenttype"_s);
-
- const auto recipients = composer->recipientsEditor()->recipients();
- QCOMPARE(recipients.count(), 2);
- QCOMPARE(recipients[0]->email(), u"konqi@example.org"_s);
- QCOMPARE(recipients[0]->name(), u"Konqi"_s);
-
- QCOMPARE(recipients[1]->email(), u"konqi@kde.org"_s);
- QVERIFY(recipients[1]->name().isEmpty());
- }
-
- void cleanupTestCase()
- {
- m_webServer->deleteLater();
- }
-
-private:
- WebServer *m_webServer = nullptr;
- QNetworkAccessManager m_qnam;
-};
-
-QTEST_MAIN(EmailControllerTest)
-#include "emailcontrollertest.moc"
diff --git a/client/controllers/emailcontroller.cpp b/client/controllers/emailcontroller.cpp
deleted file mode 100644
index 3421522..0000000
--- a/client/controllers/emailcontroller.cpp
+++ /dev/null
@@ -1,31 +0,0 @@
-// SPDX-FileCopyrightText: 2023 g10 code GmbH
-// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#include "emailcontroller.h"
-
-#include <KMime/Message>
-#include <MimeTreeParserCore/ObjectTreeParser>
-
-#include "draft/draftmanager.h"
-#include <utils.h>
-
-using namespace Qt::Literals::StringLiterals;
-
-QHttpServerResponse EmailController::infoEmailAction(const QHttpServerRequest &request)
-{
- const auto content = request.body();
- KMime::Message::Ptr message(new KMime::Message());
- message->setContent(KMime::CRLFtoLF(content));
- message->parse();
-
- MimeTreeParser::ObjectTreeParser treeParser;
- treeParser.parseObjectTree(message.get());
-
- return QHttpServerResponse(QJsonObject{
- {"status"_L1, "200"_L1},
- {"encrypted"_L1, treeParser.hasEncryptedParts()},
- {"signed"_L1, treeParser.hasSignedParts()},
- {"drafts"_L1, DraftManager::self().toJson()},
- });
-}
diff --git a/client/controllers/emailcontroller.h b/client/controllers/emailcontroller.h
deleted file mode 100644
index 12de99d..0000000
--- a/client/controllers/emailcontroller.h
+++ /dev/null
@@ -1,14 +0,0 @@
-// SPDX-FileCopyrightText: 2023 g10 code GmbH
-// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#pragma once
-
-#include <QHttpServerRequest>
-#include <QHttpServerResponse>
-
-class EmailController
-{
-public:
- static QHttpServerResponse infoEmailAction(const QHttpServerRequest &request);
-};
\ No newline at end of file
diff --git a/client/ews/ewsmessagedispatcher.cpp b/client/ews/ewsmessagedispatcher.cpp
index 8e94612..7a0bd66 100644
--- a/client/ews/ewsmessagedispatcher.cpp
+++ b/client/ews/ewsmessagedispatcher.cpp
@@ -1,68 +1,42 @@
// SPDX-FileCopyrightText: 2024 g10 code GmbH
// SPDX-FileContributor: Carl Schwan <carl.schwan@gnupg.com>
// SPDX-License-Identifier: LGPL-2.0-or-later
#include "ewsmessagedispatcher.h"
#include "editor_debug.h"
#include "ewsmailfactory.h"
-#include "qnam.h"
+#include "websocketclient.h"
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkReply>
#include <KLocalizedString>
using namespace Qt::StringLiterals;
EWSMessageDispatcher::EWSMessageDispatcher(QObject *parent)
: MessageDispatcher(parent)
{
}
-void EWSMessageDispatcher::dispatch(const KMime::Message::Ptr &message, const QString &from, const QString &mailId)
+void EWSMessageDispatcher::dispatch(const KMime::Message::Ptr &message, const QString &fromEmail, const QString &mailId)
{
- auto soapRequestBody = EwsMailFactory::create(message, EwsMailFactory::SaveCopy);
-
- QNetworkRequest sendMailRequest(QUrl(u"https://127.0.0.1:5656/socket-web"_s));
- sendMailRequest.setHeader(QNetworkRequest::ContentTypeHeader, u"application/xml"_s);
- sendMailRequest.setRawHeader("X-TOKEN", m_bearerToken);
- sendMailRequest.setRawHeader("X-EMAIL", from.toUtf8());
-
- const QJsonDocument payload(QJsonObject{
- {"command"_L1, "ews"_L1},
- {"arguments"_L1, soapRequestBody},
- {"id"_L1, mailId},
- });
-
- auto sendMailResponse = qnam->post(sendMailRequest, payload.toJson());
-
- // TODO remove me
- QObject::connect(qnam, &QNetworkAccessManager::sslErrors, qnam, [](QNetworkReply *reply, const QList<QSslError> &errors) {
- Q_UNUSED(errors);
- reply->ignoreSslErrors();
- });
-
- connect(sendMailResponse, &QNetworkReply::finished, this, [this, sendMailResponse]() {
- if (sendMailResponse->error() != QNetworkReply::NoError) {
- Q_EMIT errorOccurred(i18nc("Error message", "There were a problem sending the message: %1", sendMailResponse->errorString()));
- return;
- }
-
- qCDebug(EDITOR_LOG) << "Message sent successfully";
+ const auto soapRequestBody = EwsMailFactory::create(message, EwsMailFactory::SaveCopy);
+ auto &websocketClient = WebsocketClient::self();
+ const auto ok = websocketClient.sendEWSRequest(fromEmail, mailId, soapRequestBody);
+ if (ok) {
Q_EMIT sentSuccessfully();
- });
-
- qCDebug(EDITOR_LOG) << "Request body" << soapRequestBody;
+ }
}
QByteArray EWSMessageDispatcher::bearerToken() const
{
return m_bearerToken;
}
void EWSMessageDispatcher::setBearerToken(const QByteArray &bearerToken)
{
m_bearerToken = bearerToken;
}
diff --git a/client/main.cpp b/client/main.cpp
index 8fcdc1f..aa630e6 100644
--- a/client/main.cpp
+++ b/client/main.cpp
@@ -1,119 +1,106 @@
// SPDX-FileCopyrightText: 2023 g10 code Gmbh
// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
// SPDX-License-Identifier: GPL-2.0-or-later
-#include "controllers/emailcontroller.h"
-
#include <QApplication>
#include <QCommandLineParser>
#include <QHttpServer>
#include <QHttpServerResponse>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkReply>
#include <QPointer>
#include <QTimer>
#include <QUuid>
#include <Libkleo/KeyCache>
#include <KAboutData>
#include <KLocalizedString>
#include "config.h"
#include "firsttimedialog.h"
#include "gpgol_client_debug.h"
#include "gpgoljs_version.h"
#include "qnam.h"
#include "utils/kuniqueservice.h"
-#include "webserver.h"
#include "websocketclient.h"
using namespace Qt::Literals::StringLiterals;
using namespace std::chrono;
#ifdef Q_OS_WINDOWS
#include <windows.h>
#endif
#define STARTUP_TIMING qCDebug(GPGOL_CLIENT_LOG) << "Startup timing:" << startupTimer.elapsed() << "ms:"
#define STARTUP_TRACE qCDebug(GPGOL_CLIENT_LOG) << "Startup timing:" << startupTimer.elapsed() << "ms:" << SRCNAME << __func__ << __LINE__;
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
app.setQuitOnLastWindowClosed(false);
KLocalizedString::setApplicationDomain(QByteArrayLiteral("gpgol"));
KAboutData about(QStringLiteral("gpgol-client"),
i18nc("@title:window", "GnuPG Outlook Add-in"),
QStringLiteral(GPGOLJS_VERSION_STRING),
i18nc("@info", "GPG Outlook add-in"),
KAboutLicense::GPL,
i18nc("@info:credit", "© 2023-2024 g10 Code GmbH"));
about.setDesktopFileName(u"com.gnupg.gpgoljs"_s);
about.setProgramLogo(QIcon::fromTheme(u"com.gnupg.gpgoljs"_s));
about.addAuthor(i18nc("@info:credit", "Carl Schwan"), i18nc("@info:credit", "Maintainer"), u"carl.schwan@gnupg.com"_s, u"https://carlschwan.eu"_s);
about.setTranslator(i18nc("NAME OF TRANSLATORS", "Your names"), i18nc("EMAIL OF TRANSLATORS", "Your emails"));
about.setOrganizationDomain("gnupg.com");
about.setBugAddress("https://dev.gnupg.org/maniphest/task/edit/form/1/");
QCommandLineParser parser;
KAboutData::setApplicationData(about);
about.setupCommandLine(&parser);
parser.process(app);
about.processCommandLine(&parser);
QElapsedTimer startupTimer;
startupTimer.start();
STARTUP_TIMING << "Application created";
/* Create the unique service ASAP to prevent double starts if
* the application is started twice very quickly. */
KUniqueService service;
STARTUP_TIMING << "Service created";
QObject::connect(qnam, &QNetworkAccessManager::sslErrors, qnam, [](QNetworkReply *reply, const QList<QSslError> &) {
reply->ignoreSslErrors();
});
#ifdef Q_OS_WINDOWS
if (AttachConsole(ATTACH_PARENT_PROCESS)) {
freopen("CONOUT$", "w", stdout);
freopen("CONOUT$", "w", stderr);
}
#endif
const auto clientId = QUuid::createUuid().toString(QUuid::WithoutBraces);
- STARTUP_TIMING << "Server start";
- WebServer server;
- server.run();
- if (!server.running()) {
- qWarning() << "Server failed to listen on a port.";
- return 1;
- }
- const auto port = server.port();
- STARTUP_TIMING << "Server running";
-
STARTUP_TIMING << "KeyCache creation";
- WebsocketClient::self(QUrl(u"wss://localhost:5657/"_s), port, clientId);
+ WebsocketClient::self(QUrl(u"wss://localhost:5657/"_s), clientId);
auto keyCache = Kleo::KeyCache::mutableInstance();
keyCache->startKeyListing();
if (Config::self()->showLauncher()) {
QPointer<FirstTimeDialog> launcher = new FirstTimeDialog;
QObject::connect(&WebsocketClient::self(), &WebsocketClient::stateChanged, launcher.get(), &FirstTimeDialog::slotStateChanged);
launcher->show();
}
return app.exec();
}
diff --git a/client/webserver.cpp b/client/webserver.cpp
deleted file mode 100644
index 7c76729..0000000
--- a/client/webserver.cpp
+++ /dev/null
@@ -1,60 +0,0 @@
-// SPDX-FileCopyrightText: 2024 g10 code Gmbh
-// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#include "webserver.h"
-#include "controllers/emailcontroller.h"
-#include <QJsonDocument>
-#include <QJsonObject>
-
-using namespace Qt::Literals::StringLiterals;
-
-WebServer::WebServer(QObject *parent)
- : QObject(parent)
-{
- m_server.route(u"/info"_s, &EmailController::infoEmailAction);
-
-#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
- m_server.addAfterRequestHandler(this, [](const QHttpServerRequest &, QHttpServerResponse &resp) {
- auto h = resp.headers();
- h.append("Access-Control-Allow-Origin", "*");
- resp.setHeaders(std::move(h));
- });
-#else
- m_server.afterRequest([](QHttpServerResponse &&resp) {
- resp.setHeader("Access-Control-Allow-Origin", "*");
- return std::move(resp);
- });
-#endif
-}
-
-void WebServer::run()
-{
-#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
- if (!m_tcpserver.listen(QHostAddress::Any) || !m_server.bind(&m_tcpserver)) {
- qWarning() << "Server failed to listen on a port.";
- m_running = false;
- return;
- }
- m_port = m_tcpserver.serverPort();
-#else
- m_port = m_server.listen();
-#endif
- if (!m_port) {
- qWarning() << "Server failed to listen on a port.";
- m_running = false;
- return;
- }
- qWarning() << u"Running on http://127.0.0.1:%1/ (Press CTRL+C to quit)"_s.arg(m_port);
- m_running = true;
-}
-
-int WebServer::port() const
-{
- return m_port;
-}
-
-bool WebServer::running() const
-{
- return m_running;
-}
diff --git a/client/webserver.h b/client/webserver.h
deleted file mode 100644
index 64e5317..0000000
--- a/client/webserver.h
+++ /dev/null
@@ -1,35 +0,0 @@
-// SPDX-FileCopyrightText: 2024 g10 code Gmbh
-// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#include <QHttpServer>
-#include <QObject>
-#include <QTcpServer>
-
-/// The webserver of the native client
-///
-/// This webserver will receive webrequests from the outlook web client via
-/// the broker.
-class WebServer : public QObject
-{
- Q_OBJECT
-public:
- /// Default contructor.
- WebServer(QObject *parent = nullptr);
-
- /// Start webserver.
- void run();
-
- /// \return the port of the webserver. Qt will randomly assign a free port to this
- /// process.
- int port() const;
-
- /// \return whether the webserver is running.
- bool running() const;
-
-private:
- int m_port;
- bool m_running = false;
- QHttpServer m_server;
- QTcpServer m_tcpserver;
-};
diff --git a/client/websocketclient.cpp b/client/websocketclient.cpp
index d22a45f..b1a9302 100644
--- a/client/websocketclient.cpp
+++ b/client/websocketclient.cpp
@@ -1,293 +1,307 @@
// SPDX-FileCopyrightText: 2023 g10 code Gmbh
// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "websocketclient.h"
// Qt headers
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QStandardPaths>
#include <QTimer>
// KDE headers
#include <KLocalizedString>
#include <Libkleo/KeyCache>
+#include <MimeTreeParserCore/ObjectTreeParser>
// gpgme headers
#include <gpgme++/key.h>
#include "draft/draftmanager.h"
#include "editor/composerwindow.h"
#include "editor/composerwindowfactory.h"
#include "emailviewer.h"
+#include "gpgol_client_debug.h"
#include "protocol.h"
#include "qnam.h"
#include "websocket_debug.h"
using namespace Qt::Literals::StringLiterals;
namespace
{
QStringList trustedEmails(const std::shared_ptr<const Kleo::KeyCache> &keyCache)
{
QStringList emails;
const auto keys = keyCache->keys();
for (const auto &key : keys) {
for (const auto &userId : key.userIDs()) {
if (key.ownerTrust() == GpgME::Key::Ultimate) {
emails << QString::fromLatin1(userId.email()).toLower();
break;
}
}
}
return emails;
}
}
-void WebsocketClient::registerServer()
-{
- QNetworkRequest registerRequest(QUrl(u"https://127.0.0.1:5656/register"_s));
- registerRequest.setHeader(QNetworkRequest::ContentTypeHeader, u"application/json"_s);
- auto registerReply = qnam->post(registerRequest,
- QJsonDocument(QJsonObject{
- {"port"_L1, m_port},
- {"emails"_L1, QJsonArray::fromStringList(m_emails)},
- {"clientId"_L1, m_clientId},
- })
- .toJson());
-
- connect(registerReply, &QNetworkReply::finished, qnam, [this, registerReply]() {
- if (registerReply->error() != QNetworkReply::NoError) {
- QTimer::singleShot(m_delay, this, [this]() {
- m_delay += m_delay / 2;
- registerServer();
- });
- qWarning() << "Failed to register" << registerReply->errorString() << "retrying in" << m_delay;
- } else {
- qWarning() << "Register";
- m_delay = 2000ms;
- }
-
- registerReply->deleteLater();
- });
-}
-
-WebsocketClient &WebsocketClient::self(const QUrl &url, int port, const QString &clientId)
+WebsocketClient &WebsocketClient::self(const QUrl &url, const QString &clientId)
{
static WebsocketClient *client = nullptr;
if (!client && url.isEmpty()) {
qFatal() << "Unable to create a client without an url";
} else if (!client) {
- client = new WebsocketClient(url, port, clientId);
+ client = new WebsocketClient(url, clientId);
}
return *client;
};
-WebsocketClient::WebsocketClient(const QUrl &url, int port, const QString &clientId)
+WebsocketClient::WebsocketClient(const QUrl &url, const QString &clientId)
: QObject(nullptr)
, m_webSocket(QWebSocket(QStringLiteral("Client")))
, m_url(url)
- , m_port(port)
, m_clientId(clientId)
, m_state(NotConnected)
, m_stateDisplay(i18nc("@info", "Loading..."))
{
auto globalKeyCache = Kleo::KeyCache::instance();
m_emails = trustedEmails(globalKeyCache);
if (m_emails.isEmpty()) {
qWarning() << "No ultimately keys found in keychain";
return;
}
connect(&m_webSocket, &QWebSocket::connected, this, &WebsocketClient::slotConnected);
connect(&m_webSocket, &QWebSocket::disconnected, this, [this] {
m_state = NotConnected;
m_stateDisplay = i18nc("@info", "Connection to outlook lost due to a disconnection with the broker.");
Q_EMIT stateChanged();
});
connect(&m_webSocket, &QWebSocket::errorOccurred, this, &WebsocketClient::slotErrorOccurred);
connect(&m_webSocket, &QWebSocket::textMessageReceived, this, &WebsocketClient::slotTextMessageReceived);
connect(&m_webSocket, QOverload<const QList<QSslError> &>::of(&QWebSocket::sslErrors), this, [this](const QList<QSslError> &errors) {
// TODO remove
m_webSocket.ignoreSslErrors(errors);
});
QSslConfiguration sslConfiguration;
auto certPath = QStandardPaths::locate(QStandardPaths::AppLocalDataLocation, QStringLiteral("certificate.pem"));
Q_ASSERT(!certPath.isEmpty());
QFile certFile(certPath);
if (!certFile.open(QIODevice::ReadOnly)) {
qFatal() << "Couldn't read certificate" << certPath;
}
QSslCertificate certificate(&certFile, QSsl::Pem);
certFile.close();
sslConfiguration.addCaCertificate(certificate);
m_webSocket.setSslConfiguration(sslConfiguration);
m_webSocket.open(url);
// TODO remove me
QObject::connect(qnam, &QNetworkAccessManager::sslErrors, qnam, [](QNetworkReply *reply, const QList<QSslError> &errors) {
reply->ignoreSslErrors();
});
}
void WebsocketClient::slotConnected()
{
qCInfo(WEBSOCKET_LOG) << "websocket connected";
- QJsonDocument doc(QJsonObject{{"command"_L1, Protocol::commandToString(Protocol::Register)},
- {"arguments"_L1, QJsonObject{{"emails"_L1, QJsonArray::fromStringList(m_emails)}, {"type"_L1, "nativeclient"_L1}}}});
+ QJsonDocument doc(QJsonObject{
+ {"command"_L1, Protocol::commandToString(Protocol::Register)},
+ {
+ "arguments"_L1,
+ QJsonObject{{"emails"_L1, QJsonArray::fromStringList(m_emails)}, {"type"_L1, "nativeclient"_L1}},
+ },
+ });
m_webSocket.sendTextMessage(QString::fromUtf8(doc.toJson()));
- registerServer();
-
m_state = NotConnected; /// We still need to connect to the web client
m_stateDisplay = i18nc("@info", "Waiting for web client.");
Q_EMIT stateChanged();
}
void WebsocketClient::slotErrorOccurred(QAbstractSocket::SocketError error)
{
qCWarning(WEBSOCKET_LOG) << error << m_webSocket.errorString();
m_state = NotConnected;
m_stateDisplay = i18nc("@info", "Could not reach the Outlook extension.");
Q_EMIT stateChanged();
reconnect();
}
+bool WebsocketClient::sendEWSRequest(const QString &fromEmail, const QString &mailId, const QString &requestBody)
+{
+ const QJsonObject json{
+ {"command"_L1, Protocol::commandToString(Protocol::EWS)},
+ {"arguments"_L1,
+ QJsonObject{
+ {"body"_L1, requestBody},
+ {"email"_L1, fromEmail},
+ {"mailId"_L1, mailId},
+ }},
+ };
+
+ m_webSocket.sendTextMessage(QString::fromUtf8(QJsonDocument(json).toJson()));
+ return true;
+}
+
void WebsocketClient::slotTextMessageReceived(QString message)
{
const auto doc = QJsonDocument::fromJson(message.toUtf8());
if (!doc.isObject()) {
qCWarning(WEBSOCKET_LOG) << "invalid text message received" << message;
return;
}
const auto object = doc.object();
const auto command = Protocol::commandFromString(object["command"_L1].toString());
const auto args = object["arguments"_L1].toObject();
switch (command) {
case Protocol::Disconnection:
// disconnection of the web client
m_state = NotConnected;
m_stateDisplay = i18nc("@info", "Connection to the Outlook extension lost. Make sure the extension pane is open.");
Q_EMIT stateChanged();
return;
case Protocol::Connection:
// reconnection of the web client
m_state = Connected;
m_stateDisplay = i18nc("@info", "Connected.");
Q_EMIT stateChanged();
return;
case Protocol::EmailSent: {
// confirmation that the email was sent
const auto args = object["arguments"_L1].toObject();
Q_EMIT emailSentSuccessfully(args["id"_L1].toString());
return;
}
case Protocol::View: {
const auto email = args["email"_L1].toString();
const auto displayName = args["displayName"_L1].toString();
const auto content = args["body"_L1].toString();
const auto token = args["token"_L1].toString();
auto emailViewer = new EmailViewer(content, email, displayName, token);
emailViewer->setAttribute(Qt::WA_DeleteOnClose);
emailViewer->show();
emailViewer->activateWindow();
emailViewer->raise();
return;
}
case Protocol::RestoreAutosave: {
const auto email = args["email"_L1].toString();
const auto displayName = args["displayName"_L1].toString();
const auto bearerToken = args["token"_L1].toString().toUtf8();
ComposerWindowFactory::self().restoreAutosave(email, displayName, bearerToken);
return;
}
case Protocol::Composer:
case Protocol::Reply:
case Protocol::Forward:
case Protocol::OpenDraft: {
const auto email = args["email"_L1].toString();
const auto displayName = args["displayName"_L1].toString();
const auto bearerToken = args["token"_L1].toString().toUtf8();
auto dialog = ComposerWindowFactory::self().create(email, displayName, bearerToken);
if (command == Protocol::Reply || command == Protocol::Forward) {
const auto content = args["body"_L1].toString();
KMime::Message::Ptr message(new KMime::Message());
message->setContent(KMime::CRLFtoLF(content.toUtf8()));
message->parse();
if (command == Protocol::Reply) {
dialog->reply(message);
} else {
dialog->forward(message);
}
} else if (command == Protocol::OpenDraft) {
const auto draftId = args["id"_L1].toString();
if (draftId.isEmpty()) {
return;
}
const auto draft = DraftManager::self().draftById(draftId.toUtf8());
dialog->setMessage(draft.mime());
}
dialog->show();
dialog->activateWindow();
dialog->raise();
return;
}
case Protocol::DeleteDraft: {
const auto email = args["email"_L1].toString();
const auto displayName = args["displayName"_L1].toString();
const auto bearerToken = args["token"_L1].toString().toUtf8();
const auto draftId = args["id"_L1].toString();
if (draftId.isEmpty()) {
qWarning() << "Draft not valid";
return;
}
const auto draft = DraftManager::self().draftById(draftId.toUtf8());
if (!draft.isValid()) {
qWarning() << "Draft not valid";
return;
}
if (!DraftManager::self().remove(draft)) {
- qWarning() << "Could not delete draft";
+ qCWarning(GPGOL_CLIENT_LOG) << "Could not delete draft";
return;
}
}
+ case Protocol::Info: {
+ const auto content = args["body"_L1].toString();
+ KMime::Message::Ptr message(new KMime::Message());
+ message->setContent(KMime::CRLFtoLF(content.toUtf8()));
+ message->parse();
+
+ MimeTreeParser::ObjectTreeParser treeParser;
+ treeParser.parseObjectTree(message.get());
+
+ const QJsonObject json{
+ {"command"_L1, Protocol::commandToString(Protocol::InfoFetched)},
+ {"arguments"_L1,
+ QJsonObject{{"itemId"_L1, args["itemId"_L1]},
+ {"email"_L1, args["email"_L1]},
+ {"encrypted"_L1, treeParser.hasEncryptedParts()},
+ {"signed"_L1, treeParser.hasSignedParts()},
+ {"drafts"_L1, DraftManager::self().toJson()}}},
+ };
+
+ m_webSocket.sendTextMessage(QString::fromUtf8(QJsonDocument(json).toJson()));
+ return;
+ }
default:
- qWarning() << "Unabled command" << command;
+ qCWarning(WEBSOCKET_LOG) << "Unhandled command" << command;
}
}
void WebsocketClient::reconnect()
{
QTimer::singleShot(1000ms, this, [this]() {
m_webSocket.open(m_url);
});
}
WebsocketClient::State WebsocketClient::state() const
{
return m_state;
}
QString WebsocketClient::stateDisplay() const
{
return m_stateDisplay;
}
diff --git a/client/websocketclient.h b/client/websocketclient.h
index 099ef0b..afbd223 100644
--- a/client/websocketclient.h
+++ b/client/websocketclient.h
@@ -1,55 +1,59 @@
// SPDX-FileCopyrightText: 2023 g10 code Gmbh
// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QWebSocket>
#include <chrono>
using namespace std::chrono;
class WebsocketClient : public QObject
{
Q_OBJECT
Q_PROPERTY(State state READ state NOTIFY stateChanged)
Q_PROPERTY(QString stateDisplay READ stateDisplay NOTIFY stateChanged)
public:
enum State {
Connected,
NotConnected,
};
- static WebsocketClient &self(const QUrl &url = {}, int port = -1, const QString &clientId = {});
+ static WebsocketClient &self(const QUrl &url = {}, const QString &clientId = {});
State state() const;
QString stateDisplay() const;
+ /// \params fromEmail The email address who should send this EWS request.
+ /// \params mailId The internal mailId of the email, this will be sent back once the email
+ /// is sent without errors.
+ /// \params requestBody The SOAP request body.
+ bool sendEWSRequest(const QString &fromEmail, const QString &mailId, const QString &requestBody);
+
Q_SIGNALS:
void stateChanged();
/// Signal emited when an email was sent succesffully,
void emailSentSuccessfully(const QString &id);
private Q_SLOTS:
void slotConnected();
void slotErrorOccurred(QAbstractSocket::SocketError error);
void slotTextMessageReceived(QString message);
private:
- explicit WebsocketClient(const QUrl &url, int port, const QString &clientId);
+ explicit WebsocketClient(const QUrl &url, const QString &clientId);
void reconnect();
- void registerServer();
bool m_wasConnected = false;
QWebSocket m_webSocket;
QUrl m_url;
- const int m_port;
QStringList m_emails;
QString m_clientId;
std::chrono::milliseconds m_delay = 2000ms;
State m_state = NotConnected;
QString m_stateDisplay;
};
diff --git a/common/protocol.cpp b/common/protocol.cpp
index c43267f..d2e14a3 100644
--- a/common/protocol.cpp
+++ b/common/protocol.cpp
@@ -1,52 +1,58 @@
// SPDX-FileCopyrightText: 2024 g10 code Gmbh
// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "protocol.h"
#include <QHash>
using namespace Qt::StringLiterals;
using namespace Protocol;
Command Protocol::commandFromString(const QString &type)
{
static const QHash<QString, Command> typeMap{
{u"disconnection"_s, Command::Disconnection},
{u"connection"_s, Command::Connection},
{u"email-sent"_s, Command::EmailSent},
{u"view"_s, Command::View},
{u"register"_s, Command::Register},
{u"email-sent"_s, Command::EmailSent},
{u"log"_s, Command::Log},
{u"reply"_s, Command::Reply},
{u"forward"_s, Command::Forward},
{u"composer"_s, Command::Composer},
{u"open-draft"_s, Command::OpenDraft},
{u"delete-draft"_s, Command::DeleteDraft},
{u"restore-autosave"_s, Command::RestoreAutosave},
+ {u"info"_s, Command::Info},
+ {u"info-fetched"_s, Command::InfoFetched},
+ {u"ews"_s, Command::EWS},
};
return typeMap[type];
}
QString Protocol::commandToString(Command type)
{
static const QHash<Command, QString> typeMap{
{Command::Disconnection, u"disconnection"_s},
{Command::Connection, u"connection"_s},
{Command::EmailSent, u"email-sent"_s},
{Command::View, u"view"_s},
{Command::Register, u"register"_s},
{Command::EmailSent, u"email-sent"_s},
{Command::Log, u"log"_s},
{Command::Reply, u"reply"_s},
{Command::Forward, u"forward"_s},
{Command::Composer, u"composer"_s},
{Command::OpenDraft, u"open-draft"_s},
{Command::DeleteDraft, u"delete-draft"_s},
{Command::RestoreAutosave, u"restore-autosave"_s},
+ {Command::Info, u"info"_s},
+ {Command::InfoFetched, u"info-fetched"_s},
+ {Command::EWS, u"ews"_s},
};
return typeMap[type];
}
diff --git a/common/protocol.h b/common/protocol.h
index c446ddb..259ea2f 100644
--- a/common/protocol.h
+++ b/common/protocol.h
@@ -1,31 +1,37 @@
// SPDX-FileCopyrightText: 2024 g10 code Gmbh
// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
+#include <QObject>
#include <QString>
namespace Protocol
{
+Q_NAMESPACE
enum Command {
- Unknown, //< Unknow action type
- Disconnection, //< Disconnection of the web client
- Connection, //< Connection or reconnection of the web client
- EmailSent, //< Signal from the web client that the email was correctly sent
+ Unknown, ///< Unknow action type
+ Disconnection, ///< Disconnection of the web client
+ Connection, ///< Connection or reconnection of the web client
+ EmailSent, ///< Signal from the web client that the email was correctly sent
Register, ///< Registration of a client (native or web).
Log, ///< Web client logs
View, ///< View email in mailviewer
Reply,
Forward,
Composer,
OpenDraft,
DeleteDraft,
RestoreAutosave,
+ Info,
+ InfoFetched,
+ EWS, ///< Send EWS request
};
+Q_ENUM_NS(Command);
Command commandFromString(const QString &type);
QString commandToString(Command type);
}
diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt
index 90217c1..8b2a96e 100644
--- a/server/CMakeLists.txt
+++ b/server/CMakeLists.txt
@@ -1,67 +1,61 @@
# SPDX-FileCopyrightText: 2023 g10 code GmbH
# SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
# SPDX-License-Identifier: BSD-2-Clause
add_executable(gpgol-server)
ecm_qt_declare_logging_category(gpgol-server-static_SRCS
HEADER websocket_debug.h
IDENTIFIER WEBSOCKET_LOG
CATEGORY_NAME org.gpgol.server.websocket
DESCRIPTION "Websocket connection"
EXPORT GPGOL
)
ecm_qt_declare_logging_category(gpgol-server-static_SRCS
HEADER http_debug.h
IDENTIFIER HTTP_LOG
CATEGORY_NAME org.gpgol.server.http
DESCRIPTION "HTTP connection"
EXPORT GPGOL
)
target_sources(gpgol-server PRIVATE
${gpgol-server-static_SRCS}
# Controllers
- controllers/abstractcontroller.cpp
- controllers/abstractcontroller.h
- controllers/registrationcontroller.cpp
- controllers/registrationcontroller.h
controllers/staticcontroller.h
controllers/staticcontroller.cpp
- controllers/emailcontroller.cpp
- controllers/emailcontroller.h
# State
model/serverstate.cpp
model/serverstate.h
# web sever
webserver.cpp
webserver.h
main.cpp
)
qt_add_resources(gpgol-server
PREFIX
"/"
FILES
web/assets/logo.png
web/assets/document-decrypt-16.png
web/assets/document-decrypt-32.png
web/assets/document-decrypt-64.png
web/assets/document-decrypt-80.png
web/assets/script.js
web/assets/translation.js
web/assets/vue.global.v3.4.21.js
web/assets/main.css
web/index.html
)
target_link_libraries(gpgol-server PRIVATE Qt6::HttpServer Qt6::Core common Qt6::WebSockets KF6::I18n)
install(TARGETS gpgol-server ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
install(FILES manifest.xml DESTINATION ${KDE_INSTALL_DATAROOTDIR}/gpgol)
diff --git a/server/controllers/abstractcontroller.cpp b/server/controllers/abstractcontroller.cpp
deleted file mode 100644
index b0f0ed5..0000000
--- a/server/controllers/abstractcontroller.cpp
+++ /dev/null
@@ -1,41 +0,0 @@
-// SPDX-FileCopyrightText: 2023 g10 code GmbH
-// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#include "abstractcontroller.h"
-
-#include "../model/serverstate.h"
-#include "http_debug.h"
-
-#include <QHttpServerRequest>
-#include <QJsonObject>
-#include <utils.h>
-
-using namespace Qt::Literals::StringLiterals;
-
-QHttpServerResponse AbstractController::badRequest(const QString &reason)
-{
- if (reason.isEmpty()) {
- return QHttpServerResponse(QJsonObject{{"errorMessage"_L1, "Invalid request"_L1}}, QHttpServerResponse::StatusCode::BadRequest);
- } else {
- return QHttpServerResponse(QJsonObject{{"errorMessage"_L1, QJsonValue("Invalid request: "_L1 + reason)}}, QHttpServerResponse::StatusCode::BadRequest);
- }
-}
-
-QHttpServerResponse AbstractController::forbidden()
-{
- return QHttpServerResponse(QJsonObject{{"errorMessage"_L1, "Unable to authentificate"_L1}}, QHttpServerResponse::StatusCode::Forbidden);
-}
-
-std::optional<Client> AbstractController::checkAuthentification(const QHttpServerRequest &request)
-{
- const auto &state = ServerState::instance();
- const auto email = QString::fromUtf8(Utils::findHeader(request.headers(), "X-EMAIL"));
-
- if (email.isEmpty() || !state.clients.contains(email)) {
- qCWarning(HTTP_LOG) << "no email found" << email;
- return std::nullopt;
- }
-
- return state.clients[email];
-}
diff --git a/server/controllers/abstractcontroller.h b/server/controllers/abstractcontroller.h
deleted file mode 100644
index 3169e41..0000000
--- a/server/controllers/abstractcontroller.h
+++ /dev/null
@@ -1,20 +0,0 @@
-// SPDX-FileCopyrightText: 2023 g10 code GmbH
-// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#pragma once
-
-#include <QHttpServerResponse>
-#include <QString>
-
-#include "../model/serverstate.h"
-
-class AbstractController
-{
-protected:
- Q_REQUIRED_RESULT
- static QHttpServerResponse badRequest(const QString &reason = {});
- static QHttpServerResponse forbidden();
-
- static std::optional<Client> checkAuthentification(const QHttpServerRequest &request);
-};
diff --git a/server/controllers/emailcontroller.cpp b/server/controllers/emailcontroller.cpp
deleted file mode 100644
index b3a4604..0000000
--- a/server/controllers/emailcontroller.cpp
+++ /dev/null
@@ -1,75 +0,0 @@
-// SPDX-FileCopyrightText: 2023 g10 code GmbH
-// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#include "emailcontroller.h"
-
-#include <QEventLoop>
-#include <QHttpServerRequest>
-#include <QJsonDocument>
-#include <QJsonObject>
-#include <QNetworkReply>
-#include <QNetworkRequest>
-#include <QPromise>
-#include <QUuid>
-#include <utils.h>
-
-#include "http_debug.h"
-#include "webserver.h"
-
-using namespace Qt::Literals::StringLiterals;
-
-QHttpServerResponse checkStatus(int port, const QByteArray &body)
-{
- QNetworkRequest infoEmailRequest(QUrl(u"http://127.0.0.1:"_s + QString::number(port) + u"/info"_s));
- infoEmailRequest.setHeader(QNetworkRequest::ContentTypeHeader, u"application/json"_s);
-
- auto &state = ServerState::instance();
- QEventLoop eventLoop;
- auto reply = state.qnam.post(infoEmailRequest, body);
- QObject::connect(reply, &QNetworkReply::finished, &eventLoop, &QEventLoop::quit);
- eventLoop.exec();
-
- QJsonParseError error;
- const auto resultBody = QJsonDocument::fromJson(reply->readAll(), &error);
- if (resultBody.isNull()) {
- return QHttpServerResponse(QHttpServerResponse::StatusCode::BadRequest);
- }
-
- if (!resultBody.isObject()) {
- return QHttpServerResponse(QHttpServerResponse::StatusCode::BadRequest);
- }
-
- return QHttpServerResponse{resultBody.object()};
-}
-
-QHttpServerResponse EmailController::infoEmailAction(const QHttpServerRequest &request)
-{
- if (request.method() != QHttpServerRequest::Method::Post) {
- return badRequest(u"Endpoint only supports POST request"_s);
- }
-
- const auto server = checkAuthentification(request);
- if (!server) {
- return forbidden();
- }
-
- return checkStatus(server->port, request.body());
-}
-
-QHttpServerResponse EmailController::socketWebAction(const QHttpServerRequest &request, WebServer *webServer)
-{
- const auto email = QString::fromUtf8(Utils::findHeader(request.headers(), "X-EMAIL"));
- const auto token = Utils::findHeader(request.headers(), "X-TOKEN");
- const auto &serverState = ServerState::instance();
-
- if (serverState.composerRequest[token] != email) {
- return forbidden();
- }
-
- webServer->sendMessageToWebClient(email, request.body());
-
- return QHttpServerResponse(QJsonObject{
- {"status"_L1, "OK"_L1},
- });
-}
diff --git a/server/controllers/emailcontroller.h b/server/controllers/emailcontroller.h
deleted file mode 100644
index 59ffdc3..0000000
--- a/server/controllers/emailcontroller.h
+++ /dev/null
@@ -1,24 +0,0 @@
-// SPDX-FileCopyrightText: 2023 g10 code GmbH
-// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#pragma once
-
-#include "abstractcontroller.h"
-
-#include <QHttpServerRequest>
-
-class WebServer;
-
-class EmailController : public AbstractController
-{
-public:
- // Request from the web client
- static QHttpServerResponse infoEmailAction(const QHttpServerRequest &request);
- /// Forward request from the native client to the web client
- static QHttpServerResponse socketWebAction(const QHttpServerRequest &request, WebServer *webServer);
-
-private:
- static QHttpServerResponse
- abstractEmailAction(const QHttpServerRequest &request, const QString &action, QHttpServerRequest::Method method = QHttpServerRequest::Method::Post);
-};
diff --git a/server/controllers/registrationcontroller.cpp b/server/controllers/registrationcontroller.cpp
deleted file mode 100644
index 9459f34..0000000
--- a/server/controllers/registrationcontroller.cpp
+++ /dev/null
@@ -1,45 +0,0 @@
-// SPDX-FileCopyrightText: 2023 g10 code GmbH
-// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#include "registrationcontroller.h"
-
-#include <QCryptographicHash>
-#include <QJsonArray>
-#include <QJsonDocument>
-#include <QJsonObject>
-#include <QNetworkReply>
-#include <QString>
-
-#include "../model/serverstate.h"
-#include "http_debug.h"
-
-using namespace Qt::Literals::StringLiterals;
-
-QHttpServerResponse RegistrationController::registerAction(const QHttpServerRequest &request)
-{
- const auto json = QJsonDocument::fromJson(request.body());
- if (json.isEmpty() || !json.isObject()) {
- return badRequest();
- }
-
- const auto object = json.object();
-
- if (object.isEmpty() || !object.contains("port"_L1) || !object.contains("emails"_L1)) {
- return badRequest();
- }
-
- const auto emails = object["emails"_L1].toArray();
- const Client::Id clientId = object["clientId"_L1].toString();
- const auto port = object["port"_L1].toInt();
- for (const auto email : emails) {
- ServerState::instance().clients[email.toString().toLower()] = Client{
- clientId,
- port,
- };
-
- qCInfo(HTTP_LOG) << "Registration of email" << email.toString() << "on port" << object["port"_L1].toInt() << "with clientId" << clientId;
- }
-
- return QHttpServerResponse("text/plain", "Server added\n");
-}
diff --git a/server/controllers/registrationcontroller.h b/server/controllers/registrationcontroller.h
deleted file mode 100644
index a17b1e1..0000000
--- a/server/controllers/registrationcontroller.h
+++ /dev/null
@@ -1,16 +0,0 @@
-// SPDX-FileCopyrightText: 2023 g10 code GmbH
-// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#pragma once
-
-#include "abstractcontroller.h"
-
-#include <QHttpServerRequest>
-#include <QHttpServerResponse>
-
-class RegistrationController : public AbstractController
-{
-public:
- static QHttpServerResponse registerAction(const QHttpServerRequest &request);
-};
diff --git a/server/controllers/staticcontroller.h b/server/controllers/staticcontroller.h
index 6665b5e..32a81bc 100644
--- a/server/controllers/staticcontroller.h
+++ b/server/controllers/staticcontroller.h
@@ -1,14 +1,15 @@
// SPDX-FileCopyrightText: 2023 g10 code GmbH
// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
-#include "abstractcontroller.h"
+#include <QHttpServerRequest>
+#include <QHttpServerResponse>
-class StaticController : public AbstractController
+class StaticController
{
public:
static QHttpServerResponse homeAction(const QHttpServerRequest &request);
static QHttpServerResponse assetsAction(QString fileName, const QHttpServerRequest &request);
};
diff --git a/server/model/serverstate.h b/server/model/serverstate.h
index 985c8f9..13d0cba 100644
--- a/server/model/serverstate.h
+++ b/server/model/serverstate.h
@@ -1,35 +1,25 @@
// SPDX-FileCopyrightText: 2023 g10 code Gmbh
// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QHash>
#include <QNetworkAccessManager>
#include <QString>
using Token = QByteArray;
-using ClientId = QString;
using Email = QString;
-using Port = int;
-
-struct Client {
- using Id = QString;
- Id id;
- Port port;
-};
class ServerState
{
public:
static ServerState &instance();
- QHash<Email, Client> clients;
-
QNetworkAccessManager qnam;
QHash<Token, Email> composerRequest;
private:
ServerState();
};
diff --git a/server/web/assets/script.js b/server/web/assets/script.js
index 6e39964..4c877d6 100644
--- a/server/web/assets/script.js
+++ b/server/web/assets/script.js
@@ -1,291 +1,303 @@
// SPDX-FileCopyrightText: 2023 g10 code GmbH
// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
// SPDX-License-Identifier: GPL-2.0-or-later
/**
* Download the mail content from the EWS API
* @returns {Promise<string>}
*/
function downloadViaRest(item) {
return new Promise((resolve, reject) => {
const request =
'<?xml version="1.0" encoding="utf-8"?>' +
'<soap:Envelope xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"' +
' xmlns:xsd="https://www.w3.org/2001/XMLSchema"' +
' xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"' +
' xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types">' +
' <soap:Header>' +
' <RequestServerVersion Version="Exchange2013" xmlns="http://schemas.microsoft.com/exchange/services/2006/types" soap:mustUnderstand="0" />' +
' </soap:Header>' +
' <soap:Body>' +
' <GetItem xmlns="http://schemas.microsoft.com/exchange/services/2006/messages">' +
' <ItemShape>' +
' <t:BaseShape>IdOnly</t:BaseShape>' +
' <t:IncludeMimeContent>true</t:IncludeMimeContent>' +
' </ItemShape>' +
' <ItemIds><t:ItemId Id="' + item.itemId + '"/></ItemIds>' +
' </GetItem>' +
' </soap:Body>' +
'</soap:Envelope>';
Office.context.mailbox.makeEwsRequestAsync(request, (asyncResult) => {
const parser = new DOMParser();
xmlDoc = parser.parseFromString(asyncResult.value, "text/xml");
const mimeContent = xmlDoc.getElementsByTagName('t:MimeContent')[0].innerHTML;
resolve(atob(mimeContent));
});
})
}
const {createApp} = Vue
function i18n(text) {
const lang = Office.context.displayLanguage
if (lang in messages && text in messages[lang]) {
return messages[lang][text];
}
return text;
}
function i18nc(context, text) {
const lang = Office.context.displayLanguage
if (lang in messages && text in messages[lang]) {
return messages[lang][text];
}
return text;
}
const vueApp = {
data() {
return {
error: '',
content: '',
hasSelection: true,
status: {
encrypted: false,
signed: false,
drafts: [],
+ fetched: false,
},
socket: null,
}
},
computed: {
loaded() {
- return this.content.length > 0;
+ return this.content.length > 0 && this.status.fetched;
},
statusText() {
if (!this.loaded) {
return i18nc("Loading placeholder", "Loading...");
}
if (this.status.encrypted) {
return this.status.signed ? i18n("This mail is encrypted and signed.") : i18n("This mail is encrypted.");
}
if (this.status.signed) {
return i18n("This mail is signed")
}
return i18n("This mail is not encrypted nor signed.");
},
decryptButtonText() {
if (!this.loaded) {
return '';
}
if (this.status.encrypted) {
return this.i18nc("@action:button", "Decrypt")
}
return this.i18nc("@action:button", "View email")
},
},
methods: {
i18n,
i18nc,
gpgolLog(message, args) {
console.log(message, args);
if (this.socket) {
this.socket.send(JSON.stringify({
- type: "log",
+ command: "log",
arguments: {
message,
args: JSON.stringify(args),
},
}));
}
},
genericMailAction(command) {
console.log(this, this.socket)
this.socket.send(JSON.stringify({
command,
arguments: {
body: this.content,
email: Office.context.mailbox.userProfile.emailAddress,
name: Office.context.mailbox.userProfile.displayName,
}
}));
},
view() {
this.genericMailAction('view');
},
reply() {
this.genericMailAction('reply');
},
forward() {
this.genericMailAction('forward');
},
newEmail() {
this.socket.send(JSON.stringify({
command: 'composer',
arguments: {
email: Office.context.mailbox.userProfile.emailAddress,
name: Office.context.mailbox.userProfile.displayName,
}
}));
},
openDraft(id) {
this.socket.send(JSON.stringify({
command: 'open-draft',
arguments: {
id: id,
email: Office.context.mailbox.userProfile.emailAddress,
name: Office.context.mailbox.userProfile.displayName,
}
}));
},
deleteDraft(id) {
this.socket.send(JSON.stringify({
command: 'delete-draft',
arguments: {
id: id,
email: Office.context.mailbox.userProfile.emailAddress,
name: Office.context.mailbox.userProfile.displayName,
}
}));
// TODO this.status.drafts.splice(this.status.drafts.findIndex((draft) => draft.id === id), 1);
},
- async info() {
- const response = await fetch('/info', {
- method: 'POST',
- body: this.content,
- headers: {
- 'X-EMAIL': Office.context.mailbox.userProfile.emailAddress,
- 'X-NAME': Office.context.mailbox.userProfile.displayName,
- },
- });
- const status = await response.json();
- this.status.encrypted = status.encrypted;
- this.status.signed = status.signed;
- this.status.drafts = status.drafts;
+ info() {
+ this.socket.send(JSON.stringify({
+ command: 'info',
+ arguments: {
+ itemId: Office.context.mailbox.item.itemId,
+ email: Office.context.mailbox.userProfile.emailAddress,
+ body: this.content,
+ }
+ }));
},
displayDate(timestamp) {
const date = new Date(timestamp * 1000);
let todayDate = new Date();
let lastModification = '';
if ((new Date(date)).setHours(0, 0, 0, 0) === todayDate.setHours(0, 0, 0, 0)) {
return date.toLocaleTimeString([], {
hour: 'numeric',
minute: 'numeric',
});
} else {
return date.toLocaleDateString();
}
},
async downloadContent() {
this.content = await downloadViaRest(Office.context.mailbox.item);
this.hasSelection = true;
- await this.info()
+ if (this.socket && this.socket.readyState === WebSocket.OPEN) {
+ this.info();
+ }
},
webSocketConnect(vueInstance = null) {
if (!vueInstance) {
vueInstance = this;
}
console.log("Set socket")
vueInstance.socket = new WebSocket("wss://localhost:5657");
// Connection opened
vueInstance.socket.addEventListener("open", (event) => {
vueInstance.error = '';
vueInstance.socket.send(JSON.stringify({
command: "register",
arguments: {
emails: [Office.context.mailbox.userProfile.emailAddress],
type: 'webclient',
},
}));
- this.socket.send(JSON.stringify({
+ vueInstance.socket.send(JSON.stringify({
command: 'restore-autosave',
arguments: {
email: Office.context.mailbox.userProfile.emailAddress,
name: Office.context.mailbox.userProfile.displayName,
}
}));
+
+ if (vueInstance.content.length > 0) {
+ vueInstance.info()
+ }
});
vueInstance.socket.addEventListener("close", (event) => {
- vueInstance.error = i18n("Native client was disconnected");
+ vueInstance.error = i18n("Native client was disconnected, reconnecting in 1 second.");
+ console.log(event.reason)
setTimeout(function () {
- vueInstance.webSocketConnect(vueInstance);
+ //vueInstance.webSocketConnect(vueInstance);
}, 1000);
});
vueInstance.socket.addEventListener("error", (event) => {
vueInstance.error = i18n("Native client received an error");
- setTimeout(function () {
- vueInstance.webSocketConnect(vueInstance);
- }, 1000);
+ vueInstance.socket.close();
});
// Listen for messages
vueInstance.socket.addEventListener("message", function(result) {
const { data } = result;
const message = JSON.parse(data);
- vueInstance.gpgolLog("Received message from server", {});
+ vueInstance.gpgolLog("Received message from server", { command: message.command});
switch (message.command) {
case 'ews':
- Office.context.mailbox.makeEwsRequestAsync(message.arguments, (asyncResult) => {
+ Office.context.mailbox.makeEwsRequestAsync(message.arguments.body, (asyncResult) => {
if (asyncResult.error) {
vueInstance.gpgolLog("Error while trying to send email via EWS", {error: asyncResult.error, value: asyncResult.value});
return;
}
vueInstance.gpgolLog("Email sent", {value: asyncResult.value});
// let the client known that the email was sent
vueInstance.socket.send(JSON.stringify({
command: 'email-sent',
arguments: {
- id: message.id,
+ id: message.arguments.mailId,
email: Office.context.mailbox.userProfile.emailAddress,
}
}));
});
break;
case 'disconnection':
vueInstance.error = i18n("Native client was disconnected")
break;
case 'connection':
vueInstance.error = '';
break;
+ case 'info-fetched':
+ const { itemId, encrypted, signed, drafts } = message.arguments;
+ vueInstance.status.drafts = drafts;
+ if (itemId === Office.context.mailbox.item.itemId) {
+ vueInstance.status.fetched = true;
+ vueInstance.status.encrypted = encrypted;
+ vueInstance.status.signed = signed;
+ } else {
+ vueInstance.status.fetched = false;
+ }
}
});
}
},
async mounted() {
await this.downloadContent();
Office.context.mailbox.addHandlerAsync(Office.EventType.ItemChanged, (eventArgs) => {
if (Office.context.mailbox.item) {
this.downloadContent();
} else {
this.content = '';
this.hasSelection = false;
}
});
this.webSocketConnect();
},
}
Office.onReady().then(() => {
createApp(vueApp).mount('#app')
document.getElementById('app').classList.remove('d-none');
});
diff --git a/server/webserver.cpp b/server/webserver.cpp
index 1939040..105e2ff 100644
--- a/server/webserver.cpp
+++ b/server/webserver.cpp
@@ -1,337 +1,352 @@
// SPDX-FileCopyrightText: 2023 g10 code GmbH
// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "webserver.h"
#include <QDebug>
#include <QFile>
#include <QHttpServer>
#include <QHttpServerResponse>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QSslCertificate>
#include <QSslKey>
#include <QSslServer>
#include <QStandardPaths>
#include <QWebSocket>
#include <QWebSocketServer>
#include <protocol.h>
-#include "controllers/emailcontroller.h"
-#include "controllers/registrationcontroller.h"
#include "controllers/staticcontroller.h"
#include "http_debug.h"
+#include "model/serverstate.h"
#include "websocket_debug.h"
#include <KLocalizedString>
using namespace Qt::Literals::StringLiterals;
using namespace Protocol;
WebServer::WebServer(QObject *parent)
: QObject(parent)
, m_httpServer(new QHttpServer(this))
, m_webSocketServer(new QWebSocketServer(u"GPGOL"_s, QWebSocketServer::SslMode::SecureMode, this))
{
}
WebServer::~WebServer() = default;
bool WebServer::run()
{
auto keyPath = QStandardPaths::locate(QStandardPaths::AppLocalDataLocation, QStringLiteral("certificate-key.pem"));
auto certPath = QStandardPaths::locate(QStandardPaths::AppLocalDataLocation, QStringLiteral("certificate.pem"));
Q_ASSERT(!keyPath.isEmpty());
Q_ASSERT(!certPath.isEmpty());
QFile privateKeyFile(keyPath);
if (!privateKeyFile.open(QIODevice::ReadOnly)) {
qCFatal(HTTP_LOG) << "Couldn't open file" << keyPath << "for reading:" << privateKeyFile.errorString();
return false;
}
const QSslKey sslKey(&privateKeyFile, QSsl::Rsa);
privateKeyFile.close();
const auto sslCertificateChain = QSslCertificate::fromPath(certPath);
if (sslCertificateChain.isEmpty()) {
qCFatal(HTTP_LOG) << u"Couldn't retrieve SSL certificate from file:"_s << certPath;
return false;
}
// Static assets controller
m_httpServer->route(u"/home"_s, &StaticController::homeAction);
m_httpServer->route(u"/assets/"_s, &StaticController::assetsAction);
- // Registration controller
- m_httpServer->route(u"/register"_s, &RegistrationController::registerAction);
-
- // Email controller
- m_httpServer->route(u"/info"_s, &EmailController::infoEmailAction);
- m_httpServer->route(u"/socket-web"_s, [this](const QHttpServerRequest &request) {
- return EmailController::socketWebAction(request, this);
- });
-
QSslConfiguration sslConfiguration;
sslConfiguration.setPeerVerifyMode(QSslSocket::VerifyNone);
sslConfiguration.setLocalCertificate(sslCertificateChain.front());
sslConfiguration.setPrivateKey(sslKey);
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
m_tcpserver = std::make_unique<QSslServer>();
m_tcpserver->setSslConfiguration(sslConfiguration);
- if (!m_tcpserver->listen(QHostAddress(u"127.0.0.1"_s), WebServer::Port) || !m_httpServer->bind(m_tcpserver.get())) {
+ if (!m_tcpserver->listen(QHostAddress::LocalHost, WebServer::Port) || !m_httpServer->bind(m_tcpserver.get())) {
qCFatal(HTTP_LOG) << "Server failed to listen on a port.";
return false;
}
quint16 port = m_tcpserver->serverPort();
#else
m_httpServer->sslSetup(sslCertificateChain.front(), sslKey);
quint16 port = m_httpServer->listen(QHostAddress::Any, WebServer::Port);
#endif
if (!port) {
qCFatal(HTTP_LOG) << "Server failed to listen on a port.";
return false;
}
qInfo(HTTP_LOG) << u"Running http server on https://127.0.0.1:%1/ (Press CTRL+C to quit)"_s.arg(port);
m_webSocketServer->setSslConfiguration(sslConfiguration);
- if (m_webSocketServer->listen(QHostAddress::Any, WebServer::WebSocketPort)) {
+ if (m_webSocketServer->listen(QHostAddress::LocalHost, WebServer::WebSocketPort)) {
qCInfo(WEBSOCKET_LOG) << u"Running websocket server on wss://127.0.0.1:%1/ (Press CTRL+C to quit)"_s.arg(WebServer::Port + 1);
connect(m_webSocketServer, &QWebSocketServer::newConnection, this, &WebServer::onNewConnection);
}
return true;
}
void WebServer::onNewConnection()
{
auto pSocket = m_webSocketServer->nextPendingConnection();
if (!pSocket) {
return;
}
qCInfo(WEBSOCKET_LOG) << "Client connected:" << pSocket->peerName() << pSocket->origin() << pSocket->localAddress() << pSocket->localPort();
connect(pSocket, &QWebSocket::textMessageReceived, this, &WebServer::processTextMessage);
connect(pSocket, &QWebSocket::binaryMessageReceived, this, &WebServer::processBinaryMessage);
connect(pSocket, &QWebSocket::disconnected, this, &WebServer::socketDisconnected);
m_clients << pSocket;
}
void WebServer::processTextMessage(QString message)
{
auto webClient = qobject_cast<QWebSocket *>(sender());
if (webClient) {
QJsonParseError error;
const auto doc = QJsonDocument::fromJson(message.toUtf8(), &error);
if (error.error != QJsonParseError::NoError) {
qCWarning(WEBSOCKET_LOG) << "Error parsing json" << error.errorString();
return;
}
if (!doc.isObject()) {
qCWarning(WEBSOCKET_LOG) << "Invalid json received";
return;
}
const auto object = doc.object();
processCommand(object, webClient);
}
}
void WebServer::processCommand(const QJsonObject &object, QWebSocket *socket)
{
if (!object.contains("command"_L1) || !object["command"_L1].isString() || !object.contains("arguments"_L1) || !object["arguments"_L1].isObject()) {
- qCWarning(WEBSOCKET_LOG) << "Invalid json received: no type or arguments set";
+ qCWarning(WEBSOCKET_LOG) << "Invalid json received: no type or arguments set" << object;
return;
}
const auto arguments = object["arguments"_L1].toObject();
const auto command = commandFromString(object["command"_L1].toString());
switch (command) {
case Command::Register: {
const auto type = arguments["type"_L1].toString();
qCWarning(WEBSOCKET_LOG) << "Register" << arguments;
if (type.isEmpty()) {
qCWarning(WEBSOCKET_LOG) << "empty client type given when registering";
return;
}
const auto emails = arguments["emails"_L1].toArray();
if (type == "webclient"_L1) {
if (emails.isEmpty()) {
qCWarning(WEBSOCKET_LOG) << "empty email given";
}
for (const auto &email : emails) {
m_webClientsMappingToEmail[email.toString()] = socket;
qCWarning(WEBSOCKET_LOG) << "email" << email.toString() << "mapped to a web client";
const auto nativeClient = m_nativeClientsMappingToEmail[email.toString()];
if (nativeClient) {
QJsonDocument doc(QJsonObject{{"command"_L1, Protocol::commandToString(Protocol::Connection)},
{"payload"_L1, QJsonObject{{"client_type"_L1, "web_client"_L1}}}});
nativeClient->sendTextMessage(QString::fromUtf8(doc.toJson()));
}
}
} else {
if (emails.isEmpty()) {
qCWarning(WEBSOCKET_LOG) << "empty email given";
}
for (const auto &email : emails) {
m_nativeClientsMappingToEmail[email.toString()] = socket;
qCWarning(WEBSOCKET_LOG) << "email" << email.toString() << "mapped to a native client";
const auto webClient = m_webClientsMappingToEmail[email.toString()];
bool hasWebClient = false;
if (webClient) {
hasWebClient = true;
const QJsonDocument doc(QJsonObject{{"command"_L1, Protocol::commandToString(Protocol::Connection)},
{
"payload"_L1,
QJsonObject{{"client_type"_L1, "native_client"_L1}},
}});
webClient->sendTextMessage(QString::fromUtf8(doc.toJson()));
}
if (hasWebClient) {
const QJsonDocument doc(QJsonObject{
{"command"_L1, Protocol::commandToString(Protocol::Connection)},
{"payload"_L1, QJsonObject{{"client_type"_L1, "web_client"_L1}}},
});
socket->sendTextMessage(QString::fromUtf8(doc.toJson()));
}
}
}
return;
}
case Command::EmailSent: {
const auto email = arguments["email"_L1].toString();
QJsonDocument doc(QJsonObject{
{"command"_L1, Protocol::commandToString(Protocol::EmailSent)},
{"arguments"_L1, arguments},
});
sendMessageToNativeClient(email, doc.toJson());
return;
}
case Command::View:
case Command::Reply:
case Command::Forward:
case Command::Composer:
case Command::OpenDraft:
case Command::RestoreAutosave:
+ case Command::Info:
case Command::DeleteDraft: {
const auto email = arguments["email"_L1].toString();
const auto token = QUuid::createUuid().toString(QUuid::WithoutBraces);
object["token"_L1] = token;
auto &serverState = ServerState::instance();
serverState.composerRequest[token.toUtf8()] = email;
const QJsonDocument doc(object); // simple forwarding to the native client
const auto ok = sendMessageToNativeClient(email, doc.toJson());
if (!ok) {
socket->sendTextMessage(i18n("Unable to found corresponding native client."));
}
return;
}
+ case Command::InfoFetched: {
+ const auto email = arguments["email"_L1].toString();
+ QJsonDocument doc(QJsonObject{
+ {"command"_L1, Protocol::commandToString(Protocol::InfoFetched)},
+ {"arguments"_L1, arguments},
+ });
+ const auto ok = sendMessageToWebClient(email, doc.toJson());
+ if (!ok) {
+ qCWarning(WEBSOCKET_LOG) << "Unable to send info fetched to web client";
+ }
+ return;
+ }
+ case Command::EWS: {
+ const auto email = arguments["email"_L1].toString();
+ QJsonDocument doc(QJsonObject{
+ {"command"_L1, Protocol::commandToString(Protocol::EWS)},
+ {"arguments"_L1, arguments},
+ });
+ const auto ok = sendMessageToWebClient(email, doc.toJson());
+ if (!ok) {
+ qCWarning(WEBSOCKET_LOG) << "Unable to send ews request to web client";
+ }
+ return;
+ }
case Command::Log:
qWarning(WEBSOCKET_LOG) << arguments["message"_L1].toString() << arguments["args"_L1].toString();
return;
default:
qCWarning(WEBSOCKET_LOG) << "Invalid json received: invalid command";
return;
}
}
bool WebServer::sendMessageToWebClient(const QString &email, const QByteArray &payload)
{
auto socket = m_webClientsMappingToEmail[email];
if (!socket) {
return false;
}
socket->sendTextMessage(QString::fromUtf8(payload));
return true;
}
bool WebServer::sendMessageToNativeClient(const QString &email, const QByteArray &payload)
{
auto socket = m_nativeClientsMappingToEmail[email];
if (!socket) {
return false;
}
socket->sendTextMessage(QString::fromUtf8(payload));
return true;
}
void WebServer::processBinaryMessage(QByteArray message)
{
qCWarning(WEBSOCKET_LOG) << "got binary message" << message;
QWebSocket *pClient = qobject_cast<QWebSocket *>(sender());
if (pClient) {
pClient->sendBinaryMessage(message);
}
}
void WebServer::socketDisconnected()
{
auto pClient = qobject_cast<QWebSocket *>(sender());
if (!pClient) {
return;
}
qCWarning(WEBSOCKET_LOG) << "Client disconnected" << pClient;
// Web client was disconnected
{
const auto it = std::find_if(m_webClientsMappingToEmail.cbegin(), m_webClientsMappingToEmail.cend(), [pClient](QWebSocket *webSocket) {
return pClient == webSocket;
});
if (it != m_webClientsMappingToEmail.cend()) {
const auto email = it.key();
const auto nativeClient = m_nativeClientsMappingToEmail[email];
qCInfo(WEBSOCKET_LOG) << "webclient with email disconnected" << email << nativeClient;
if (nativeClient) {
QJsonDocument doc(QJsonObject{
{"command"_L1, Protocol::commandToString(Protocol::Disconnection)},
});
nativeClient->sendTextMessage(QString::fromUtf8(doc.toJson()));
}
m_webClientsMappingToEmail.removeIf([pClient](auto socket) {
return pClient == socket.value();
});
}
}
// Native client was disconnected
const auto emails = m_nativeClientsMappingToEmail.keys();
for (const auto &email : emails) {
const auto webSocket = m_nativeClientsMappingToEmail[email];
if (webSocket != pClient) {
qCWarning(WEBSOCKET_LOG) << "webSocket not equal" << email << webSocket << pClient;
continue;
}
qCInfo(WEBSOCKET_LOG) << "native client for" << email << "was disconnected.";
QJsonDocument doc(QJsonObject{
{"command"_L1, Protocol::commandToString(Protocol::Disconnection)},
});
sendMessageToWebClient(email, doc.toJson());
}
m_nativeClientsMappingToEmail.removeIf([pClient](auto socket) {
return pClient == socket.value();
});
m_clients.removeAll(pClient);
}

File Metadata

Mime Type
text/x-diff
Expires
Mon, Dec 23, 1:41 PM (1 d, 7 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
c1/0e/28e164279d62a8cdfc7c5bda69f2

Event Timeline