Page MenuHome GnuPG

No OneTemporary

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

Mime Type
text/x-diff
Expires
Mon, Dec 23, 1:24 PM (5 h, 37 m)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
e9/96/ef3f33e5c6d69d0513e2a6894cdf

Event Timeline