Page Menu
Home
GnuPG
Search
Configure Global Search
Log In
Files
F18824972
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Size
82 KB
Subscribers
None
View Options
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
Details
Attached
Mime Type
text/x-diff
Expires
Mon, Dec 23, 1:41 PM (1 d, 1 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
c1/0e/28e164279d62a8cdfc7c5bda69f2
Attached To
rOJ GpgOL.js
Event Timeline
Log In to Comment