Page Menu
Home
GnuPG
Search
Configure Global Search
Log In
Files
F18824791
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Size
19 KB
Subscribers
None
View Options
diff --git a/client/draft/draft.cpp b/client/draft/draft.cpp
index 8d56422..4cddd1e 100644
--- a/client/draft/draft.cpp
+++ b/client/draft/draft.cpp
@@ -1,68 +1,68 @@
// SPDX-FileCopyrightText: 2023 g10 code GmbH
// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "draft.h"
#include <QFile>
#include "editor_debug.h"
using namespace Qt::Literals::StringLiterals;
Draft::Draft(const QString &localUrl)
: m_localUrl(localUrl)
, m_fileInfo(m_localUrl)
{
}
bool Draft::isValid() const
{
return m_fileInfo.exists() && m_fileInfo.isReadable();
}
QJsonObject Draft::toJson() const
{
return {
{"id"_L1, m_fileInfo.fileName()},
{"url"_L1, m_localUrl},
{"last_modification"_L1, lastModified().toSecsSinceEpoch()},
};
}
QString Draft::localUrl() const
{
return m_localUrl;
}
QDateTime Draft::lastModified() const
{
return m_fileInfo.lastModified();
}
bool Draft::remove()
{
- QFile file(m_fileInfo.filePath());
- if (!file.exists()) {
+ if (!m_fileInfo.exists()) {
qCWarning(EDITOR_LOG) << "File doesn't exist anymore.";
return false;
}
+ QFile file(m_fileInfo.filePath());
return file.remove();
}
KMime::Message::Ptr Draft::mime() const
{
Q_ASSERT(isValid()); // should be checked by the caller
QFile file(m_fileInfo.filePath());
if (!file.open(QIODeviceBase::ReadOnly)) {
qFatal() << "Can open file" << m_fileInfo.filePath();
}
KMime::Message::Ptr message(new KMime::Message());
message->setContent(KMime::CRLFtoLF(file.readAll()));
message->assemble();
return message;
}
diff --git a/client/draft/draftmanager.cpp b/client/draft/draftmanager.cpp
index 383d923..5511074 100644
--- a/client/draft/draftmanager.cpp
+++ b/client/draft/draftmanager.cpp
@@ -1,89 +1,91 @@
// SPDX-FileCopyrightText: 2023 g10 code GmbH
// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "draftmanager.h"
#include <QDir>
#include <QStandardPaths>
#include "editor_debug.h"
DraftManager::DraftManager(bool testMode)
: m_testMode(testMode)
{
const QDir directory(draftDirectory(testMode));
const auto entries = directory.entryList(QDir::Files);
for (const QString &entry : entries) {
Draft draft(draftDirectory() + entry);
if (draft.isValid()) {
m_drafts << draft;
} else {
qFatal(EDITOR_LOG) << "File does not exist or is not readable" << entry;
}
}
}
QString DraftManager::draftDirectory(bool testMode)
{
if (testMode) {
static const QString path = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + QLatin1String("/gpgol-server/draft/");
return path;
} else {
static const QString path = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/gpgol-server/draft/");
return path;
}
}
QString DraftManager::autosaveDirectory(bool testMode)
{
if (testMode) {
static const QString path = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + QLatin1String("/gpgol-server/autosave/");
return path;
} else {
static const QString path = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/gpgol-server/autosave/");
return path;
}
}
DraftManager &DraftManager::self(bool testMode)
{
static DraftManager s_draftManager(testMode);
return s_draftManager;
}
QList<Draft> DraftManager::drafts() const
{
return m_drafts;
}
QJsonArray DraftManager::toJson() const
{
if (m_drafts.isEmpty()) {
return {};
}
QJsonArray array;
std::transform(m_drafts.cbegin(), m_drafts.cend(), std::back_inserter(array), [](const auto draft) {
return draft.toJson();
});
return array;
}
bool DraftManager::remove(const Draft &draft)
{
auto it = std::find(m_drafts.begin(), m_drafts.end(), draft);
if (it == m_drafts.end()) {
return false;
}
bool ok = it->remove();
- m_drafts.erase(it);
+ if (ok) {
+ m_drafts.erase(it);
+ }
return ok;
}
Draft DraftManager::draftById(const QByteArray &draftId)
{
return Draft(draftDirectory() + QString::fromUtf8(draftId));
}
diff --git a/server/controllers/emailcontroller.cpp b/server/controllers/emailcontroller.cpp
index 553aea5..857d970 100644
--- a/server/controllers/emailcontroller.cpp
+++ b/server/controllers/emailcontroller.cpp
@@ -1,159 +1,159 @@
// 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 EmailController::abstractEmailAction(const QHttpServerRequest &request, const QString &action, QHttpServerRequest::Method method)
{
const auto client = checkAuthentification(request);
if (!client) {
return forbidden();
}
QNetworkRequest viewEmailRequest(QUrl(u"http://127.0.0.1:"_s + QString::number(client->port) + u'/' + action));
viewEmailRequest.setHeader(QNetworkRequest::ContentTypeHeader, u"application/json"_s);
auto email = Utils::findHeader(request.headers(), "X-EMAIL");
auto displayName = Utils::findHeader(request.headers(), "X-NAME");
auto token = QUuid::createUuid().toString(QUuid::WithoutBraces).toUtf8();
viewEmailRequest.setRawHeader("X-EMAIL", email);
viewEmailRequest.setRawHeader("X-TOKEN", token);
viewEmailRequest.setRawHeader("X-NAME", displayName);
auto &serverState = ServerState::instance();
serverState.composerRequest[token] = QString::fromUtf8(email);
auto &state = ServerState::instance();
QNetworkReply *reply;
if (method == QHttpServerRequest::Method::Post) {
const auto body = request.body();
reply = state.qnam.post(viewEmailRequest, body);
} else {
reply = state.qnam.deleteResource(viewEmailRequest);
}
QObject::connect(reply, &QNetworkReply::finished, reply, [reply]() {
if (reply->error() != QNetworkReply::NoError) {
qCWarning(HTTP_LOG) << reply->error() << reply->errorString();
}
});
return QHttpServerResponse(QJsonObject{
{"status"_L1, "ok"_L1},
});
}
QHttpServerResponse EmailController::viewEmailAction(const QHttpServerRequest &request)
{
if (request.method() != QHttpServerRequest::Method::Post) {
return badRequest(u"Endpoint only supports POST request"_s);
}
return abstractEmailAction(request, u"view"_s);
}
QHttpServerResponse EmailController::newEmailAction(const QHttpServerRequest &request)
{
if (request.method() != QHttpServerRequest::Method::Post) {
return badRequest(u"Endpoint only supports POST request"_s);
}
return abstractEmailAction(request, u"new"_s);
}
QHttpServerResponse EmailController::draftAction(QString, const QHttpServerRequest &request)
{
if (request.method() != QHttpServerRequest::Method::Post && request.method() != QHttpServerRequest::Method::Delete) {
- return badRequest(u"Endpoint only supports POST request"_s);
+ return badRequest(u"Endpoint only supports POST AND DELETE request"_s);
}
- return abstractEmailAction(request, request.url().path(), request.method());
+ return abstractEmailAction(request, request.url().path().mid(1), request.method());
}
QHttpServerResponse EmailController::replyEmailAction(const QHttpServerRequest &request)
{
if (request.method() != QHttpServerRequest::Method::Post) {
return badRequest(u"Endpoint only supports POST request"_s);
}
return abstractEmailAction(request, u"reply"_s);
}
QHttpServerResponse EmailController::forwardEmailAction(const QHttpServerRequest &request)
{
if (request.method() != QHttpServerRequest::Method::Post) {
return badRequest(u"Endpoint only supports POST request"_s);
}
return abstractEmailAction(request, u"forward"_s);
}
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/web/assets/script.js b/server/web/assets/script.js
index 21a8c66..fd0deda 100644
--- a/server/web/assets/script.js
+++ b/server/web/assets/script.js
@@ -1,294 +1,302 @@
// 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;
}
function gpgolLog(message, args) {
console.log(message, args);
if (this.socket) {
this.socket.send(JSON.stringify({
command: "log",
arguments: {
message,
args: JSON.stringify(args),
},
}));
}
}
const vueApp = {
data() {
return {
error: '',
content: '',
hasSelection: true,
status: {
encrypted: false,
signed: false,
drafts: [],
},
socket: null,
}
},
computed: {
loaded() {
return this.content.length > 0;
},
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,
async view() {
const response = await fetch('/view', {
method: 'POST',
body: this.content,
headers: {
'X-EMAIL': Office.context.mailbox.userProfile.emailAddress,
'X-NAME': Office.context.mailbox.userProfile.displayName,
},
});
},
async reply() {
await fetch('/reply', {
method: 'POST',
body: this.content,
headers: {
'X-EMAIL': Office.context.mailbox.userProfile.emailAddress,
'X-NAME': Office.context.mailbox.userProfile.displayName,
},
});
},
async forward() {
await fetch('/forward', {
method: 'POST',
body: this.content,
headers: {
'X-EMAIL': Office.context.mailbox.userProfile.emailAddress,
'X-NAME': Office.context.mailbox.userProfile.displayName,
},
});
},
async newEmail() {
const response = await fetch('/new', {
method: 'POST',
headers: {
'X-EMAIL': Office.context.mailbox.userProfile.emailAddress,
'X-NAME': Office.context.mailbox.userProfile.displayName,
},
});
},
async openDraft(id) {
const response = await fetch(`/draft/${id}`, {
method: 'POST',
headers: {
'X-EMAIL': Office.context.mailbox.userProfile.emailAddress,
'X-NAME': Office.context.mailbox.userProfile.displayName,
},
});
},
async deleteDraft(id) {
- const response = await fetch(`/draft/${id}`, {
- method: 'DELETE',
- headers: {
- 'X-EMAIL': Office.context.mailbox.userProfile.emailAddress,
- 'X-NAME': Office.context.mailbox.userProfile.displayName,
- },
- });
+ try {
+ const response = await fetch(`/draft/${id}`, {
+ method: 'DELETE',
+ headers: {
+ 'X-EMAIL': Office.context.mailbox.userProfile.emailAddress,
+ 'X-NAME': Office.context.mailbox.userProfile.displayName,
+ },
+ });
+ if (!response.ok) {
+ throw new Error(response.statusText);
+ }
+ this.status.drafts.splice(this.status.drafts.findIndex((draft) => draft.id === id), 1);
+ } catch (err) {
+ gpgolLog(err);
+ }
},
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;
},
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()
},
},
async mounted() {
await this.downloadContent();
this.gpgolLog("test");
Office.context.mailbox.addHandlerAsync(Office.EventType.ItemChanged, (eventArgs) => {
if (Office.context.mailbox.item) {
this.downloadContent();
} else {
this.content = '';
this.hasSelection = false;
}
});
function webSocketConnect() {
this.socket = new WebSocket("wss://localhost:5657");
// Connection opened
this.socket.addEventListener("open", (event) => {
this.error = '';
this.socket.send(JSON.stringify({
command: "register",
arguments: {
emails: [Office.context.mailbox.userProfile.emailAddress],
type: 'webclient',
},
}));
});
this.socket.addEventListener("close", (event) => {
this.error = i18n("Native client was disconnected");
setTimeout(function () {
webSocketConnect();
}, 1000);
});
this.socket.addEventListener("error", (event) => {
this.error = i18n("Native client received an error");
setTimeout(function () {
webSocketConnect();
}, 1000);
});
const vueInstance = this;
// Listen for messages
this.socket.addEventListener("message", function(result) {
const { data } = result;
const message = JSON.parse(data);
vueInstance.gpgolLog("Received message from server", {});
switch (message.type) {
case 'ews':
Office.context.mailbox.makeEwsRequestAsync(message.payload, (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,
email: Office.context.mailbox.userProfile.emailAddress,
}
}));
});
break;
case 'disconnection':
this.error = i18n("Native client was disconnected")
break;
case 'connection':
this.error = '';
break;
}
});
}
webSocketConnect();
},
}
Office.onReady().then(() => {
createApp(vueApp).mount('#app')
document.getElementById('app').classList.remove('d-none');
});
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Mon, Dec 23, 1:24 PM (15 m, 38 s)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
e9/96/ef3f33e5c6d69d0513e2a6894cdf
Attached To
rOJ GpgOL.js
Event Timeline
Log In to Comment