Page MenuHome GnuPG

No OneTemporary

diff --git a/client/draft/draftmanager.cpp b/client/draft/draftmanager.cpp
index 0ad31f8..0ccb4a2 100644
--- a/client/draft/draftmanager.cpp
+++ b/client/draft/draftmanager.cpp
@@ -1,113 +1,114 @@
// 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"
+#include "../../common/paths.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;
}
QList<Draft> DraftManager::autosaves() const
{
QList<Draft> autosaves;
const QDir directory(autosaveDirectory(false));
const auto entries = directory.entryList(QDir::Files);
for (const QString &entry : entries) {
Draft draft(autosaveDirectory() + entry);
if (draft.isValid()) {
autosaves << draft;
} else {
qFatal(EDITOR_LOG) << "File does not exist or is not readable" << entry;
}
}
return autosaves;
}
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();
if (ok) {
m_drafts.erase(it);
}
return ok;
}
void DraftManager::add(const QString &localFileName)
{
m_drafts.append(Draft(localFileName));
}
Draft DraftManager::draftById(const QByteArray &draftId)
{
return Draft(draftDirectory() + QString::fromUtf8(draftId));
}
diff --git a/client/editor/composerviewbase.cpp b/client/editor/composerviewbase.cpp
index cb7859d..f422bdd 100644
--- a/client/editor/composerviewbase.cpp
+++ b/client/editor/composerviewbase.cpp
@@ -1,1343 +1,1342 @@
/*
SPDX-FileCopyrightText: 2010 Klaralvdalens Datakonsult AB, a KDAB Group company, info@kdab.com
SPDX-FileCopyrightText: 2010 Leo Franchi <lfranchi@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "composerviewbase.h"
#include "attachment/attachmentcontrollerbase.h"
#include "attachment/attachmentmodel.h"
#include "bodytexteditor.h"
#include "composersignatures.h"
#include "job/composerjob.h"
#include "mailtemplates.h"
#include "messagedispatcher.h"
#include "nodehelper.h"
#include "part/globalpart.h"
#include "part/infopart.h"
#include "signaturecontroller.h"
#include "util.h"
#include "util_p.h"
#include "messagecomposersettings.h"
#include "recipientseditor.h"
#include "identity/identity.h"
#include <KCursorSaver>
#include <KEmailAddress>
#include <MimeTreeParserCore/ObjectTreeParser>
#include <Sonnet/DictionaryComboBox>
#include "editor_debug.h"
#include <Libkleo/ExpiryChecker>
#include <Libkleo/ExpiryCheckerSettings>
#include <Libkleo/KeyCache>
#include <Libkleo/KeyResolverCore>
#include <libkleo/enum.h>
#include <QGpgME/ExportJob>
#include <QGpgME/ImportJob>
#include <QGpgME/Protocol>
#include <gpgme++/global.h>
#include <gpgme++/context.h>
#include <gpgme++/importresult.h>
#include <KLocalizedString>
#include <KMessageBox>
#include <QDir>
#include <QSaveFile>
-#include <QStandardPaths>
#include <QTemporaryDir>
#include <QTimer>
#include <QUuid>
#include "draft/draftmanager.h"
using namespace MessageComposer;
using namespace Qt::Literals::StringLiterals;
ComposerViewBase::ComposerViewBase(QObject *parent, QWidget *parentGui)
: QObject(parent)
, m_msg(KMime::Message::Ptr(new KMime::Message))
, m_parentWidget(parentGui)
, m_cryptoMessageFormat(Kleo::AutoFormat)
, m_autoSaveInterval(60000) // default of 1 min
{
initAutoSave();
connect(this, &ComposerViewBase::composerCreated, this, &ComposerViewBase::slotComposerCreated);
}
ComposerViewBase::~ComposerViewBase() = default;
bool ComposerViewBase::isComposing() const
{
return !m_composers.isEmpty();
}
void ComposerViewBase::setMessage(const KMime::Message::Ptr &msg)
{
if (m_attachmentModel) {
const auto attachments{m_attachmentModel->attachments()};
for (const MessageCore::AttachmentPart::Ptr &attachment : attachments) {
if (!m_attachmentModel->removeAttachment(attachment)) {
qCWarning(EDITOR_LOG) << "Attachment not found.";
}
}
}
m_msg = msg;
if (m_recipientsEditor) {
m_recipientsEditor->clear();
bool resultTooManyRecipients = m_recipientsEditor->setRecipientString(m_msg->to()->mailboxes(), Recipient::To);
if (!resultTooManyRecipients) {
resultTooManyRecipients = m_recipientsEditor->setRecipientString(m_msg->cc()->mailboxes(), Recipient::Cc);
}
if (!resultTooManyRecipients) {
resultTooManyRecipients = m_recipientsEditor->setRecipientString(m_msg->bcc()->mailboxes(), Recipient::Bcc);
}
if (!resultTooManyRecipients) {
resultTooManyRecipients = m_recipientsEditor->setRecipientString(m_msg->replyTo()->mailboxes(), Recipient::ReplyTo);
}
m_recipientsEditor->setFocusBottom();
Q_EMIT tooManyRecipient(resultTooManyRecipients);
}
m_subject = m_msg->subject()->asUnicodeString();
// First, we copy the message and then parse it to the object tree parser.
// The otp gets the message text out of it, in textualContent(), and also decrypts
// the message if necessary.
KMime::Content msgContent;
msgContent.setContent(m_msg->encodedContent());
msgContent.parse();
// Load the attachments
const auto attachments{m_msg->attachments()};
for (const auto &att : attachments) {
addAttachmentPart(att);
}
// Set the HTML text and collect HTML images
bool isHtml = false;
const auto body = MailTemplates::body(msg, isHtml);
editor()->setText(body);
}
void ComposerViewBase::saveMailSettings()
{
auto header = new KMime::Headers::Generic("X-KMail-Identity");
header->fromUnicodeString(QString::number(m_identity.uoid()));
m_msg->setHeader(header);
header = new KMime::Headers::Generic("X-KMail-Identity-Name");
header->fromUnicodeString(m_identity.identityName());
m_msg->setHeader(header);
header = new KMime::Headers::Generic("X-KMail-Dictionary");
header->fromUnicodeString(m_dictionary->currentDictionary());
m_msg->setHeader(header);
}
void ComposerViewBase::clearFollowUp()
{
mFollowUpDate = QDate();
}
void ComposerViewBase::send()
{
KCursorSaver saver(Qt::WaitCursor);
saveMailSettings();
readyForSending();
}
void ComposerViewBase::setCustomHeader(const QMap<QByteArray, QString> &customHeader)
{
m_customHeader = customHeader;
}
void ComposerViewBase::readyForSending()
{
qCDebug(EDITOR_LOG) << "Entering readyForSending";
if (!m_msg) {
qCDebug(EDITOR_LOG) << "m_msg == 0!";
return;
}
if (!m_composers.isEmpty()) {
// This may happen if e.g. the autosave timer calls applyChanges.
qCDebug(EDITOR_LOG) << "ready for sending: Called while composer active; ignoring. Number of composer " << m_composers.count();
return;
}
mExpandedFrom = from();
mExpandedTo = m_recipientsEditor->recipientStringList(Recipient::To);
mExpandedCc = m_recipientsEditor->recipientStringList(Recipient::Cc);
mExpandedBcc = m_recipientsEditor->recipientStringList(Recipient::Bcc);
mExpandedReplyTo = m_recipientsEditor->recipientStringList(Recipient::ReplyTo);
Q_ASSERT(m_composers.isEmpty()); // composers should be empty. The caller of this function
// checks for emptiness before calling it
// so just ensure it actually is empty
// and document it
// we first figure out if we need to create multiple messages with different crypto formats
// if so, we create a composer per format
// if we aren't signing or encrypting, this just returns a single empty message
if (m_neverEncrypt) {
auto composer = new MessageComposer::ComposerJob;
composer->setNoCrypto(true);
m_composers.append(composer);
slotComposerCreated();
} else {
generateCryptoMessages();
}
}
void ComposerViewBase::slotComposerCreated()
{
if (m_composers.isEmpty()) {
Q_EMIT failed(i18n("It was not possible to create a message composer."));
return;
}
// Compose each message and prepare it for queueing, sending, or storing
// working copy in case composers instantly emit result
const auto composers = m_composers;
for (MessageComposer::ComposerJob *composer : composers) {
fillComposer(composer, UseExpandedRecipients, false);
connect(composer, &MessageComposer::ComposerJob::result, this, &ComposerViewBase::slotSendComposeResult);
composer->start();
qCDebug(EDITOR_LOG) << "Started a composer for sending!";
}
}
namespace
{
// helper methods for reading encryption settings
inline Kleo::chrono::days encryptOwnKeyNearExpiryWarningThresholdInDays()
{
if (!MessageComposer::MessageComposerSettings::self()->cryptoWarnWhenNearExpire()) {
return Kleo::chrono::days{-1};
}
const int num = MessageComposer::MessageComposerSettings::self()->cryptoWarnOwnEncrKeyNearExpiryThresholdDays();
return Kleo::chrono::days{qMax(1, num)};
}
inline Kleo::chrono::days encryptKeyNearExpiryWarningThresholdInDays()
{
if (!MessageComposer::MessageComposerSettings::self()->cryptoWarnWhenNearExpire()) {
return Kleo::chrono::days{-1};
}
const int num = MessageComposer::MessageComposerSettings::self()->cryptoWarnEncrKeyNearExpiryThresholdDays();
return Kleo::chrono::days{qMax(1, num)};
}
inline Kleo::chrono::days encryptRootCertNearExpiryWarningThresholdInDays()
{
if (!MessageComposer::MessageComposerSettings::self()->cryptoWarnWhenNearExpire()) {
return Kleo::chrono::days{-1};
}
const int num = MessageComposer::MessageComposerSettings::self()->cryptoWarnEncrRootNearExpiryThresholdDays();
return Kleo::chrono::days{qMax(1, num)};
}
inline Kleo::chrono::days encryptChainCertNearExpiryWarningThresholdInDays()
{
if (!MessageComposer::MessageComposerSettings::self()->cryptoWarnWhenNearExpire()) {
return Kleo::chrono::days{-1};
}
const int num = MessageComposer::MessageComposerSettings::self()->cryptoWarnEncrChaincertNearExpiryThresholdDays();
return Kleo::chrono::days{qMax(1, num)};
}
} // nameless namespace
Kleo::KeyResolver *ComposerViewBase::fillKeyResolver(bool encryptSomething)
{
auto keyResolverCore = new Kleo::KeyResolver(m_encrypt, m_sign, GpgME::UnknownProtocol, false);
keyResolverCore->setMinimumValidity(GpgME::UserID::Unknown);
QStringList signingKeys, encryptionKeys;
if (m_cryptoMessageFormat & Kleo::AnyOpenPGP) {
if (!m_identity.pgpSigningKey().isEmpty()) {
signingKeys.push_back(QLatin1String(m_identity.pgpSigningKey()));
}
if (!m_identity.pgpEncryptionKey().isEmpty()) {
encryptionKeys.push_back(QLatin1String(m_identity.pgpEncryptionKey()));
}
}
if (m_cryptoMessageFormat & Kleo::AnySMIME) {
if (!m_identity.smimeSigningKey().isEmpty()) {
signingKeys.push_back(QLatin1String(m_identity.smimeSigningKey()));
}
if (!m_identity.smimeEncryptionKey().isEmpty()) {
encryptionKeys.push_back(QLatin1String(m_identity.smimeEncryptionKey()));
}
}
keyResolverCore->setSender(m_identity.fullEmailAddr());
const auto normalized = GpgME::UserID::addrSpecFromString(m_identity.fullEmailAddr().toUtf8().constData());
const auto normalizedSender = QString::fromUtf8(normalized.c_str());
keyResolverCore->setSigningKeys(signingKeys);
keyResolverCore->setOverrideKeys({{GpgME::UnknownProtocol, {{normalizedSender, encryptionKeys}}}});
if (encryptSomething) {
QStringList recipients;
const auto lst = m_recipientsEditor->lines();
for (auto line : lst) {
auto recipient = line->data().dynamicCast<Recipient>();
recipients.push_back(recipient->email());
}
keyResolverCore->setRecipients(recipients);
}
return keyResolverCore;
}
void ComposerViewBase::generateCryptoMessages()
{
bool canceled = false;
qCDebug(EDITOR_LOG) << "filling crypto info";
connect(expiryChecker().get(),
&Kleo::ExpiryChecker::expiryMessage,
this,
[&canceled](const GpgME::Key &key, QString msg, Kleo::ExpiryChecker::ExpiryInformation info, bool isNewMessage) {
if (!isNewMessage) {
return;
}
if (canceled) {
return;
}
QString title;
QString dontAskAgainName;
if (info == Kleo::ExpiryChecker::OwnKeyExpired || info == Kleo::ExpiryChecker::OwnKeyNearExpiry) {
dontAskAgainName = QStringLiteral("own key expires soon warning");
} else {
dontAskAgainName = QStringLiteral("other encryption key near expiry warning");
}
if (info == Kleo::ExpiryChecker::OwnKeyExpired || info == Kleo::ExpiryChecker::OtherKeyExpired) {
title = key.protocol() == GpgME::OpenPGP ? i18n("OpenPGP Key Expired") : i18n("S/MIME Certificate Expired");
} else {
title = key.protocol() == GpgME::OpenPGP ? i18n("OpenPGP Key Expires Soon") : i18n("S/MIME Certificate Expires Soon");
}
if (KMessageBox::warningContinueCancel(nullptr, msg, title, KStandardGuiItem::cont(), KStandardGuiItem::cancel(), dontAskAgainName)
== KMessageBox::Cancel) {
canceled = true;
}
});
bool signSomething = m_sign;
bool doSignCompletely = m_sign;
bool encryptSomething = m_encrypt;
bool doEncryptCompletely = m_encrypt;
if (m_attachmentModel) {
const auto attachments = m_attachmentModel->attachments();
for (const MessageCore::AttachmentPart::Ptr &attachment : attachments) {
if (attachment->isSigned()) {
signSomething = true;
} else {
doEncryptCompletely = false;
}
if (attachment->isEncrypted()) {
encryptSomething = true;
} else {
doSignCompletely = false;
}
}
}
qCInfo(EDITOR_LOG) << "Encrypting completely:" << doEncryptCompletely;
qCInfo(EDITOR_LOG) << "Signing completely:" << doSignCompletely;
// No encryption or signing is needed
if (!signSomething && !encryptSomething) {
m_composers = {new MessageComposer::ComposerJob};
Q_EMIT composerCreated();
return;
}
auto keyResolver = fillKeyResolver(encryptSomething);
connect(keyResolver, &Kleo::KeyResolver::keysResolved, this, [this, encryptSomething, signSomething, keyResolver](bool success, bool sendUnencrypted) {
if (!success) {
qCDebug(EDITOR_LOG) << "resolveAllKeys: failed to resolve keys! oh noes";
Q_EMIT failed(i18n("Failed to resolve keys. Please report a bug."));
return;
}
qCDebug(EDITOR_LOG) << "done resolving keys.";
if (sendUnencrypted) {
m_composers = {new MessageComposer::ComposerJob};
Q_EMIT composerCreated();
return;
}
const auto result = keyResolver->result();
QList<MessageComposer::ComposerJob *> composers;
auto signingKeyFinder = [&result](const GpgME::Protocol protocol) -> std::optional<GpgME::Key> { // clazy:excludeall=lambda-in-connect
for (const auto &key : result.signingKeys) {
if (key.protocol() == protocol) {
return key;
}
}
return std::nullopt;
};
if (encryptSomething || signSomething) {
if (encryptSomething) {
std::vector<GpgME::Key> pgpKeys;
QStringList pgpRecipients;
std::vector<GpgME::Key> smimeKeys;
QStringList smimeRecipients;
for (const auto &signingKey : result.signingKeys) {
expiryChecker()->checkKey(signingKey, Kleo::ExpiryChecker::EncryptionKey);
}
for (const auto &[recipient, keys] : result.encryptionKeys.asKeyValueRange()) {
const auto recipientKeys = result.encryptionKeys[recipient];
if (recipientKeys.size() > 1) {
// TODO Carl group handling
} else {
const auto &key = recipientKeys[0];
expiryChecker()->checkKey(recipientKeys[0], Kleo::ExpiryChecker::EncryptionKey);
if (key.protocol() == GpgME::CMS) {
smimeRecipients.append(recipient);
smimeKeys.push_back(recipientKeys[0]);
} else {
pgpRecipients.append(recipient);
pgpKeys.push_back(recipientKeys[0]);
}
}
}
Q_ASSERT(smimeRecipients.count() == (int)smimeKeys.size());
Q_ASSERT(pgpRecipients.count() == (int)pgpKeys.size());
if (pgpRecipients.count() > 0) {
auto composer = new MessageComposer::ComposerJob;
composer->setEncryptionKeys({QPair<QStringList, std::vector<GpgME::Key>>(pgpRecipients, pgpKeys)});
auto pgpSigningKey = signingKeyFinder(GpgME::OpenPGP);
if (signSomething && pgpSigningKey) {
composer->setSigningKeys({*pgpSigningKey});
}
composer->setCryptoMessageFormat(Kleo::OpenPGPMIMEFormat);
composer->setSignAndEncrypt(signSomething, encryptSomething);
composers << composer;
}
if (smimeRecipients.count() > 0) {
auto composer = new MessageComposer::ComposerJob;
composer->setEncryptionKeys({QPair<QStringList, std::vector<GpgME::Key>>(smimeRecipients, smimeKeys)});
auto smimeSigningKey = signingKeyFinder(GpgME::CMS);
if (signSomething && smimeSigningKey) {
composer->setSigningKeys({*smimeSigningKey});
}
composer->setCryptoMessageFormat(Kleo::SMIMEFormat);
composer->setSignAndEncrypt(signSomething, encryptSomething);
composers << composer;
}
} else {
// signing only
Q_ASSERT(signSomething);
Q_ASSERT(!encryptSomething);
auto composer = new MessageComposer::ComposerJob;
composer->setSignAndEncrypt(signSomething, encryptSomething);
Q_ASSERT(result.protocol != GpgME::UnknownProtocol); // No mixed protocol allowed here
const auto signingKey = signingKeyFinder(result.protocol);
Q_ASSERT(signingKey);
composer->setSigningKeys({*signingKey});
composer->setCryptoMessageFormat(result.protocol == GpgME::OpenPGP ? Kleo::OpenPGPMIMEFormat : Kleo::SMIMEFormat);
composers << composer;
}
} else {
auto composer = new MessageComposer::ComposerJob;
composers.append(composer);
// If we canceled sign or encrypt be sure to change status in attachment.
markAllAttachmentsForSigning(false);
markAllAttachmentsForEncryption(false);
}
if (composers.isEmpty() && (signSomething || encryptSomething)) {
Q_ASSERT_X(false, "ComposerViewBase::generateCryptoMessages", "No concrete sign or encrypt method selected");
}
m_composers = composers;
Q_EMIT composerCreated();
qWarning() << "composer created" << composers;
});
keyResolver->start(false, m_parentWidget);
}
void ComposerViewBase::fillGlobalPart(MessageComposer::GlobalPart *globalPart)
{
globalPart->setParentWidgetForGui(m_parentWidget);
globalPart->setMDNRequested(m_mdnRequested);
globalPart->setRequestDeleveryConfirmation(m_requestDeleveryConfirmation);
}
void ComposerViewBase::fillInfoPart(MessageComposer::InfoPart *infoPart, ComposerViewBase::RecipientExpansion expansion)
{
// TODO splitAddressList and expandAliases ugliness should be handled by a
// special AddressListEdit widget... (later: see RecipientsEditor)
if (expansion == UseExpandedRecipients) {
infoPart->setFrom(mExpandedFrom);
infoPart->setTo(mExpandedTo);
infoPart->setCc(mExpandedCc);
infoPart->setBcc(mExpandedBcc);
infoPart->setReplyTo(mExpandedReplyTo);
} else {
infoPart->setFrom(from());
infoPart->setTo(m_recipientsEditor->recipientStringList(Recipient::To));
infoPart->setCc(m_recipientsEditor->recipientStringList(Recipient::Cc));
infoPart->setBcc(m_recipientsEditor->recipientStringList(Recipient::Bcc));
infoPart->setReplyTo(m_recipientsEditor->recipientStringList(Recipient::ReplyTo));
}
infoPart->setSubject(subject());
infoPart->setUserAgent(QStringLiteral("GnuPG Outlook Add-In"));
infoPart->setUrgent(m_urgent);
if (auto inReplyTo = m_msg->inReplyTo(false)) {
infoPart->setInReplyTo(inReplyTo->asUnicodeString());
}
if (auto references = m_msg->references(false)) {
infoPart->setReferences(references->asUnicodeString());
}
KMime::Headers::Base::List extras;
if (auto hdr = m_msg->headerByType("X-KMail-SignatureActionEnabled")) {
extras << hdr;
}
if (auto hdr = m_msg->headerByType("X-KMail-EncryptActionEnabled")) {
extras << hdr;
}
if (auto hdr = m_msg->headerByType("X-KMail-CryptoMessageFormat")) {
extras << hdr;
}
if (auto hdr = m_msg->headerByType("X-KMail-UnExpanded-To")) {
extras << hdr;
}
if (auto hdr = m_msg->headerByType("X-KMail-UnExpanded-CC")) {
extras << hdr;
}
if (auto hdr = m_msg->headerByType("X-KMail-UnExpanded-BCC")) {
extras << hdr;
}
if (auto hdr = m_msg->headerByType("X-KMail-UnExpanded-Reply-To")) {
extras << hdr;
}
if (auto hdr = m_msg->organization(false)) {
extras << hdr;
}
if (auto hdr = m_msg->headerByType("X-KMail-Fcc")) {
extras << hdr;
}
if (auto hdr = m_msg->headerByType("X-KMail-Drafts")) {
extras << hdr;
}
if (auto hdr = m_msg->headerByType("X-KMail-Templates")) {
extras << hdr;
}
if (auto hdr = m_msg->headerByType("X-KMail-Link-Message")) {
extras << hdr;
}
if (auto hdr = m_msg->headerByType("X-KMail-Link-Type")) {
extras << hdr;
}
if (auto hdr = m_msg->headerByType("X-Face")) {
extras << hdr;
}
if (auto hdr = m_msg->headerByType("Face")) {
extras << hdr;
}
if (auto hdr = m_msg->headerByType("X-KMail-FccDisabled")) {
extras << hdr;
}
infoPart->setExtraHeaders(extras);
}
void ComposerViewBase::slotSendComposeResult(KJob *job)
{
Q_ASSERT(dynamic_cast<MessageComposer::ComposerJob *>(job));
auto composer = static_cast<MessageComposer::ComposerJob *>(job);
if (composer->error() != MessageComposer::ComposerJob::NoError) {
qCDebug(EDITOR_LOG) << "compose job might have error: " << job->error() << " errorString: " << job->errorString();
}
if (composer->error() == MessageComposer::ComposerJob::NoError) {
Q_ASSERT(m_composers.contains(composer));
// The messages were composed successfully.
qCDebug(EDITOR_LOG) << "NoError.";
const int numberOfMessage(composer->resultMessages().size());
for (int i = 0; i < numberOfMessage; ++i) {
queueMessage(composer->resultMessages().at(i));
}
} else if (composer->error() == MessageComposer::ComposerJob::UserCancelledError) {
// The job warned the user about something, and the user chose to return
// to the message. Nothing to do.
qCDebug(EDITOR_LOG) << "UserCancelledError.";
Q_EMIT failed(i18n("Job cancelled by the user"));
} else {
qCDebug(EDITOR_LOG) << "other Error." << composer->error();
QString msg;
if (composer->error() == MessageComposer::ComposerJob::BugError) {
msg = i18n("Could not compose message: %1 \n Please report this bug.", job->errorString());
} else {
msg = i18n("Could not compose message: %1", job->errorString());
}
Q_EMIT failed(msg);
}
if (!composer->gnupgHome().isEmpty()) {
QDir dir(composer->gnupgHome());
dir.removeRecursively();
}
m_composers.removeAll(composer);
}
void ComposerViewBase::initAutoSave()
{
qCDebug(EDITOR_LOG) << "initialising autosave";
// Ensure that the autosave directory exists.
QDir dataDirectory(DraftManager::autosaveDirectory());
if (!dataDirectory.exists(QStringLiteral("autosave"))) {
qCDebug(EDITOR_LOG) << "Creating autosave directory.";
dataDirectory.mkdir(QStringLiteral("autosave"));
}
updateAutoSave();
}
QDate ComposerViewBase::followUpDate() const
{
return mFollowUpDate;
}
void ComposerViewBase::setFollowUpDate(const QDate &followUpDate)
{
mFollowUpDate = followUpDate;
}
Sonnet::DictionaryComboBox *ComposerViewBase::dictionary() const
{
return m_dictionary;
}
void ComposerViewBase::setDictionary(Sonnet::DictionaryComboBox *dictionary)
{
m_dictionary = dictionary;
}
void ComposerViewBase::updateAutoSave()
{
if (m_autoSaveInterval == 0) {
delete m_autoSaveTimer;
m_autoSaveTimer = nullptr;
} else {
if (!m_autoSaveTimer) {
m_autoSaveTimer = new QTimer(this);
connect(m_autoSaveTimer, &QTimer::timeout, this, &ComposerViewBase::autoSaveMessage);
}
m_autoSaveTimer->start(m_autoSaveInterval);
}
}
void ComposerViewBase::cleanupAutoSave()
{
delete m_autoSaveTimer;
m_autoSaveTimer = nullptr;
if (!mailIdIsEmpty()) {
qCDebug(EDITOR_LOG) << "deleting autosave files" << mailId();
// Delete the autosave files
QDir autoSaveDir(DraftManager::autosaveDirectory());
// Filter out only this composer window's autosave files
const QStringList autoSaveFilter{mailId() + QLatin1String("*")};
autoSaveDir.setNameFilters(autoSaveFilter);
// Return the files to be removed
const QStringList autoSaveFiles = autoSaveDir.entryList();
qCDebug(EDITOR_LOG) << "There are" << autoSaveFiles.count() << "to be deleted.";
// Delete each file
for (const QString &file : autoSaveFiles) {
autoSaveDir.remove(file);
}
m_autoSaveUUID.clear();
}
}
void ComposerViewBase::generateMessage(std::function<void(QList<KMime::Message::Ptr>)> callback)
{
auto composer = new ComposerJob;
fillComposer(composer);
composer->setAutoSave(true);
m_composers.append(composer);
connect(composer, &MessageComposer::ComposerJob::result, this, [composer, callback]() {
callback(composer->resultMessages());
});
composer->start();
}
void ComposerViewBase::autoSaveMessage()
{
qCDebug(EDITOR_LOG) << "Autosaving message";
if (m_autoSaveTimer) {
m_autoSaveTimer->stop();
}
if (!m_composers.isEmpty()) {
// This may happen if e.g. the autosave timer calls applyChanges.
qCDebug(EDITOR_LOG) << "Autosave: Called while composer active; ignoring. Number of composer " << m_composers.count();
return;
}
auto composer = new ComposerJob;
fillComposer(composer);
composer->setAutoSave(true);
composer->setSignAndEncrypt(false, true);
const auto normalized = GpgME::UserID::addrSpecFromString(m_identity.fullEmailAddr().toUtf8().constData());
const auto normalizedSender = QString::fromUtf8(normalized.c_str());
auto encryptionKeys = Kleo::KeyCache::instance()->findByEMailAddress(normalizedSender.toStdString());
std::vector<GpgME::Key> filteredEncryptionKeys;
Kleo::CryptoMessageFormat format = Kleo::SMIMEFormat;
// ensure all keys are valid
for (const auto &key : encryptionKeys) {
if (strlen(key.userID(0).email()) > 0) {
filteredEncryptionKeys.push_back(key);
if (key.protocol() == GpgME::OpenPGP && key.canEncrypt()) {
format = Kleo::OpenPGPMIMEFormat;
}
qWarning() << key.protocol() << key.canEncrypt() << key.userID(0).email();
}
}
Q_ASSERT(!filteredEncryptionKeys.empty());
composer->setEncryptionKeys({QPair<QStringList, std::vector<GpgME::Key>>{{normalizedSender}, filteredEncryptionKeys}});
composer->setCryptoMessageFormat(format);
m_composers.append(composer);
connect(composer, &MessageComposer::ComposerJob::result, this, &ComposerViewBase::slotAutoSaveComposeResult);
composer->start();
}
void ComposerViewBase::slotSaveDraft()
{
qCDebug(EDITOR_LOG) << "Saving draft";
m_composers.clear();
auto composer = new ComposerJob;
fillComposer(composer);
composer->setDraft(true);
composer->setSignAndEncrypt(false, true);
const auto normalized = GpgME::UserID::addrSpecFromString(m_identity.fullEmailAddr().toUtf8().constData());
const auto normalizedSender = QString::fromUtf8(normalized.c_str());
auto encryptionKeys = Kleo::KeyCache::instance()->findByEMailAddress(normalizedSender.toStdString());
std::vector<GpgME::Key> filteredEncryptionKeys;
// ensure all keys are valid
for (const auto &key : encryptionKeys) {
if (strlen(key.userID(0).email()) > 0) {
filteredEncryptionKeys.push_back(key);
}
}
Q_ASSERT(!filteredEncryptionKeys.empty());
composer->setEncryptionKeys({QPair<QStringList, std::vector<GpgME::Key>>{{normalizedSender}, filteredEncryptionKeys}});
composer->setCryptoMessageFormat(m_cryptoMessageFormat);
m_composers.append(composer);
connect(composer, &MessageComposer::ComposerJob::result, this, &ComposerViewBase::slotSaveDraftComposeResult);
composer->start();
}
void ComposerViewBase::setAutoSaveFileName(const QString &fileName)
{
m_autoSaveUUID = fileName;
Q_EMIT modified(true);
}
void ComposerViewBase::slotAutoSaveComposeResult(KJob *job)
{
using MessageComposer::ComposerJob;
Q_ASSERT(dynamic_cast<ComposerJob *>(job));
auto composer = static_cast<ComposerJob *>(job);
if (composer->error() == ComposerJob::NoError) {
Q_ASSERT(m_composers.contains(composer));
// The messages were composed successfully. Only save the first message, there should
// only be one anyway, since crypto is disabled.
qCDebug(EDITOR_LOG) << "NoError.";
writeAutoSaveToDisk(composer->resultMessages().constFirst());
Q_ASSERT(composer->resultMessages().size() == 1);
if (m_autoSaveInterval > 0) {
updateAutoSave();
}
} else if (composer->error() == MessageComposer::ComposerJob::UserCancelledError) {
// The job warned the user about something, and the user chose to return
// to the message. Nothing to do.
qCDebug(EDITOR_LOG) << "UserCancelledError.";
Q_EMIT failed(i18n("Job cancelled by the user"), AutoSave);
} else {
qCDebug(EDITOR_LOG) << "other Error.";
Q_EMIT failed(i18n("Could not autosave message: %1", job->errorString()), AutoSave);
}
m_composers.removeAll(composer);
}
void ComposerViewBase::slotSaveDraftComposeResult(KJob *job)
{
using MessageComposer::ComposerJob;
Q_ASSERT(dynamic_cast<ComposerJob *>(job));
auto composer = static_cast<ComposerJob *>(job);
if (composer->error() == ComposerJob::NoError) {
Q_ASSERT(m_composers.contains(composer));
// The messages were composed successfully. Only save the first message, there should
// only be one anyway, since crypto is disabled.
qCDebug(EDITOR_LOG) << "NoError.";
writeDraftToDisk(composer->resultMessages().constFirst());
Q_ASSERT(composer->resultMessages().size() == 1);
Q_EMIT closeWindow();
} else if (composer->error() == MessageComposer::ComposerJob::UserCancelledError) {
// The job warned the user about something, and the user chose to return
// to the message. Nothing to do.
qCDebug(EDITOR_LOG) << "UserCancelledError.";
Q_EMIT failed(i18n("Job cancelled by the user"), Draft);
} else {
qCDebug(EDITOR_LOG) << "other Error." << composer->error();
Q_EMIT failed(i18n("Could not save message as draft: %1", job->errorString()), Draft);
}
m_composers.removeAll(composer);
}
void ComposerViewBase::writeDraftToDisk(const KMime::Message::Ptr &message)
{
QDir().mkpath(DraftManager::draftDirectory());
const QString filename = DraftManager::draftDirectory() + mailId();
QSaveFile file(filename);
QString errorMessage;
qCDebug(EDITOR_LOG) << "Writing message to disk as" << filename;
if (file.open(QIODevice::WriteOnly)) {
file.setPermissions(QFile::ReadUser | QFile::WriteUser);
if (file.write(message->encodedContent()) != static_cast<qint64>(message->encodedContent().size())) {
errorMessage = i18n("Could not write all data to file.");
} else if (!file.commit()) {
errorMessage = i18n("Could not finalize the file.");
}
} else {
errorMessage = i18n("Could not open file.");
}
if (!errorMessage.isEmpty()) {
qCWarning(EDITOR_LOG) << "Saving draft failed:" << errorMessage << file.errorString() << "mailId" << mailId();
if (!m_autoSaveErrorShown) {
KMessageBox::error(m_parentWidget,
i18n("Saving the draft message as %1 failed.\n"
"%2\n"
"Reason: %3",
filename,
errorMessage,
file.errorString()),
i18nc("@title:window", "Autosaving Message Failed"));
// Error dialog shown, hide the errors the next time
m_autoSaveErrorShown = true;
}
} else {
// No error occurred, the next error should be shown again
m_autoSaveErrorShown = false;
DraftManager::self().add(filename);
}
file.commit();
message->clear();
}
void ComposerViewBase::writeAutoSaveToDisk(const KMime::Message::Ptr &message)
{
QDir().mkpath(DraftManager::autosaveDirectory());
const QString filename = DraftManager::autosaveDirectory() + mailId();
QSaveFile file(filename);
QString errorMessage;
qCDebug(EDITOR_LOG) << "Writing message to disk as" << filename;
if (file.open(QIODevice::WriteOnly)) {
file.setPermissions(QFile::ReadUser | QFile::WriteUser);
if (file.write(message->encodedContent()) != static_cast<qint64>(message->encodedContent().size())) {
errorMessage = i18n("Could not write all data to file.");
} else {
if (!file.commit()) {
errorMessage = i18n("Could not finalize the file.");
}
}
} else {
errorMessage = i18n("Could not open file.");
}
if (!errorMessage.isEmpty()) {
qCWarning(EDITOR_LOG) << "Auto saving failed:" << errorMessage << file.errorString() << "mailId=" << mailId();
if (!m_autoSaveErrorShown) {
KMessageBox::error(m_parentWidget,
i18n("Autosaving the message as %1 failed.\n"
"%2\n"
"Reason: %3",
filename,
errorMessage,
file.errorString()),
i18nc("@title:window", "Autosaving Message Failed"));
// Error dialog shown, hide the errors the next time
m_autoSaveErrorShown = true;
}
} else {
// No error occurred, the next error should be shown again
m_autoSaveErrorShown = false;
}
file.commit();
message->clear();
}
void ComposerViewBase::addAttachment(const QUrl &url, const QString &comment, bool sync)
{
Q_UNUSED(comment)
qCDebug(EDITOR_LOG) << "adding attachment with url:" << url;
if (sync) {
m_attachmentController->addAttachmentUrlSync(url);
} else {
m_attachmentController->addAttachment(url);
}
}
void ComposerViewBase::addAttachment(const QString &name, const QString &filename, const QByteArray &data, const QByteArray &mimeType)
{
MessageCore::AttachmentPart::Ptr attachment = MessageCore::AttachmentPart::Ptr(new MessageCore::AttachmentPart());
if (!data.isEmpty()) {
attachment->setName(name);
attachment->setFileName(filename);
attachment->setData(data);
attachment->setMimeType(mimeType);
// TODO what about the other fields?
m_attachmentController->addAttachment(attachment);
}
}
void ComposerViewBase::addAttachmentPart(KMime::Content *partToAttach)
{
MessageCore::AttachmentPart::Ptr part(new MessageCore::AttachmentPart);
if (partToAttach->contentType()->mimeType() == "multipart/digest" || partToAttach->contentType(false)->mimeType() == "message/rfc822") {
// if it is a digest or a full message, use the encodedContent() of the attachment,
// which already has the proper headers
part->setData(partToAttach->encodedContent());
} else {
part->setData(partToAttach->decodedContent());
}
part->setMimeType(partToAttach->contentType(false)->mimeType());
if (auto cd = partToAttach->contentDescription(false)) {
part->setDescription(cd->asUnicodeString());
}
if (auto ct = partToAttach->contentType(false)) {
if (ct->hasParameter(QByteArrayView("name"))) {
part->setName(ct->parameter(QByteArrayView("name")));
}
}
if (auto cd = partToAttach->contentDisposition(false)) {
part->setFileName(cd->filename());
part->setInline(cd->disposition() == KMime::Headers::CDinline);
}
if (part->name().isEmpty() && !part->fileName().isEmpty()) {
part->setName(part->fileName());
}
if (part->fileName().isEmpty() && !part->name().isEmpty()) {
part->setFileName(part->name());
}
m_attachmentController->addAttachment(part);
}
void ComposerViewBase::queueMessage(const KMime::Message::Ptr &message)
{
Q_ASSERT(m_messageDispatcher);
m_messageDispatcher->dispatch(message, from(), mailId());
}
void ComposerViewBase::fillComposer(MessageComposer::ComposerJob *composer)
{
fillComposer(composer, UseUnExpandedRecipients, false);
}
void ComposerViewBase::fillComposer(MessageComposer::ComposerJob *composer, ComposerViewBase::RecipientExpansion expansion, bool autoresize)
{
fillInfoPart(composer->infoPart(), expansion);
fillGlobalPart(composer->globalPart());
m_editor->fillComposerTextPart(composer->textPart());
fillInfoPart(composer->infoPart(), expansion);
if (m_attachmentModel) {
composer->addAttachmentParts(m_attachmentModel->attachments(), autoresize);
} else {
qCDebug(EDITOR_LOG) << "fillComposer has no attachment model";
}
}
//-----------------------------------------------------------------------------
QString ComposerViewBase::to() const
{
if (m_recipientsEditor) {
return MessageComposer::Util::cleanedUpHeaderString(m_recipientsEditor->recipientString(Recipient::To));
}
return {};
}
//-----------------------------------------------------------------------------
QString ComposerViewBase::cc() const
{
if (m_recipientsEditor) {
return MessageComposer::Util::cleanedUpHeaderString(m_recipientsEditor->recipientString(Recipient::Cc));
}
return {};
}
//-----------------------------------------------------------------------------
QString ComposerViewBase::bcc() const
{
if (m_recipientsEditor) {
return MessageComposer::Util::cleanedUpHeaderString(m_recipientsEditor->recipientString(Recipient::Bcc));
}
return {};
}
QString ComposerViewBase::from() const
{
return MessageComposer::Util::cleanedUpHeaderString(m_from);
}
QString ComposerViewBase::replyTo() const
{
if (m_recipientsEditor) {
return MessageComposer::Util::cleanedUpHeaderString(m_recipientsEditor->recipientString(Recipient::ReplyTo));
}
return {};
}
QString ComposerViewBase::subject() const
{
return MessageComposer::Util::cleanedUpHeaderString(m_subject);
}
void ComposerViewBase::setIdentity(const KIdentityManagementCore::Identity &identity)
{
m_identity = identity;
}
KIdentityManagementCore::Identity ComposerViewBase::identity() const
{
return m_identity;
}
void ComposerViewBase::setParentWidgetForGui(QWidget *w)
{
m_parentWidget = w;
}
void ComposerViewBase::setAttachmentController(MessageComposer::AttachmentControllerBase *controller)
{
m_attachmentController = controller;
}
MessageComposer::AttachmentControllerBase *ComposerViewBase::attachmentController()
{
return m_attachmentController;
}
void ComposerViewBase::setAttachmentModel(MessageComposer::AttachmentModel *model)
{
m_attachmentModel = model;
}
MessageComposer::AttachmentModel *ComposerViewBase::attachmentModel()
{
return m_attachmentModel;
}
void ComposerViewBase::setRecipientsEditor(RecipientsEditor *recEditor)
{
m_recipientsEditor = recEditor;
}
RecipientsEditor *ComposerViewBase::recipientsEditor()
{
return m_recipientsEditor;
}
void ComposerViewBase::setSignatureController(MessageComposer::SignatureController *sigController)
{
m_signatureController = sigController;
}
MessageComposer::SignatureController *ComposerViewBase::signatureController()
{
return m_signatureController;
}
void ComposerViewBase::updateRecipients(const KIdentityManagementCore::Identity &ident, const KIdentityManagementCore::Identity &oldIdent, Recipient::Type type)
{
QString oldIdentList;
QString newIdentList;
if (type == Recipient::Bcc) {
oldIdentList = oldIdent.bcc();
newIdentList = ident.bcc();
} else if (type == Recipient::Cc) {
oldIdentList = oldIdent.cc();
newIdentList = ident.cc();
} else if (type == Recipient::ReplyTo) {
oldIdentList = oldIdent.replyToAddr();
newIdentList = ident.replyToAddr();
} else {
return;
}
if (oldIdentList != newIdentList) {
const auto oldRecipients = KMime::Types::Mailbox::listFromUnicodeString(oldIdentList);
for (const KMime::Types::Mailbox &recipient : oldRecipients) {
m_recipientsEditor->removeRecipient(recipient.prettyAddress(), type);
}
const auto newRecipients = KMime::Types::Mailbox::listFromUnicodeString(newIdentList);
for (const KMime::Types::Mailbox &recipient : newRecipients) {
m_recipientsEditor->addRecipient(recipient.prettyAddress(), type);
}
m_recipientsEditor->setFocusBottom();
}
}
void ComposerViewBase::identityChanged(const KIdentityManagementCore::Identity &ident, const KIdentityManagementCore::Identity &oldIdent, bool msgCleared)
{
updateRecipients(ident, oldIdent, Recipient::Bcc);
updateRecipients(ident, oldIdent, Recipient::Cc);
updateRecipients(ident, oldIdent, Recipient::ReplyTo);
KIdentityManagementCore::Signature oldSig = const_cast<KIdentityManagementCore::Identity &>(oldIdent).signature();
KIdentityManagementCore::Signature newSig = const_cast<KIdentityManagementCore::Identity &>(ident).signature();
// replace existing signatures
const bool replaced = editor()->composerSignatures()->replaceSignature(oldSig, newSig);
// Just append the signature if there was no old signature
if (!replaced && (msgCleared || oldSig.rawText().isEmpty())) {
signatureController()->applySignature(newSig);
}
}
void ComposerViewBase::setEditor(BodyTextEditor *editor)
{
m_editor = editor;
m_editor->document()->setModified(false);
}
BodyTextEditor *ComposerViewBase::editor() const
{
return m_editor;
}
void ComposerViewBase::setFrom(const QString &from)
{
m_from = from;
}
void ComposerViewBase::setSubject(const QString &subject)
{
m_subject = subject;
}
void ComposerViewBase::setAutoSaveInterval(int interval)
{
m_autoSaveInterval = interval;
}
void ComposerViewBase::setCryptoOptions(bool sign, bool encrypt, Kleo::CryptoMessageFormat format, bool neverEncryptDrafts)
{
m_sign = sign;
m_encrypt = encrypt;
m_cryptoMessageFormat = format;
m_neverEncrypt = neverEncryptDrafts;
if (m_attachmentModel) {
m_attachmentModel->setEncryptSelected(encrypt);
m_attachmentModel->setSignSelected(sign);
}
}
void ComposerViewBase::setMDNRequested(bool mdnRequested)
{
m_mdnRequested = mdnRequested;
}
void ComposerViewBase::setUrgent(bool urgent)
{
m_urgent = urgent;
}
int ComposerViewBase::autoSaveInterval() const
{
return m_autoSaveInterval;
}
bool ComposerViewBase::inlineSigningEncryptionSelected() const
{
if (!m_sign && !m_encrypt) {
return false;
}
return m_cryptoMessageFormat == Kleo::InlineOpenPGPFormat;
}
bool ComposerViewBase::hasMissingAttachments(const QStringList &attachmentKeywords)
{
if (attachmentKeywords.isEmpty()) {
return false;
}
if (m_attachmentModel && m_attachmentModel->rowCount() > 0) {
return false;
}
return MessageComposer::Util::hasMissingAttachments(attachmentKeywords, m_editor->document(), subject());
}
ComposerViewBase::MissingAttachment ComposerViewBase::checkForMissingAttachments(const QStringList &attachmentKeywords)
{
if (!hasMissingAttachments(attachmentKeywords)) {
return NoMissingAttachmentFound;
}
const int rc = KMessageBox::warningTwoActionsCancel(m_editor,
i18n("The message you have composed seems to refer to an "
"attached file but you have not attached anything.\n"
"Do you want to attach a file to your message?"),
i18nc("@title:window", "File Attachment Reminder"),
KGuiItem(i18n("&Attach File..."), QLatin1String("mail-attachment")),
KGuiItem(i18n("&Send as Is"), QLatin1String("mail-send")));
if (rc == KMessageBox::Cancel) {
return FoundMissingAttachmentAndCancel;
}
if (rc == KMessageBox::ButtonCode::PrimaryAction) {
m_attachmentController->showAddAttachmentFileDialog();
return FoundMissingAttachmentAndAddedAttachment;
}
return FoundMissingAttachmentAndSending;
}
void ComposerViewBase::markAllAttachmentsForSigning(bool sign)
{
if (m_attachmentModel) {
const auto attachments = m_attachmentModel->attachments();
for (MessageCore::AttachmentPart::Ptr attachment : attachments) {
attachment->setSigned(sign);
}
}
}
void ComposerViewBase::markAllAttachmentsForEncryption(bool encrypt)
{
if (m_attachmentModel) {
const auto attachments = m_attachmentModel->attachments();
for (MessageCore::AttachmentPart::Ptr attachment : attachments) {
attachment->setEncrypted(encrypt);
}
}
}
bool ComposerViewBase::requestDeleveryConfirmation() const
{
return m_requestDeleveryConfirmation;
}
void ComposerViewBase::setRequestDeleveryConfirmation(bool requestDeleveryConfirmation)
{
m_requestDeleveryConfirmation = requestDeleveryConfirmation;
}
KMime::Message::Ptr ComposerViewBase::msg() const
{
return m_msg;
}
std::shared_ptr<Kleo::ExpiryChecker> ComposerViewBase::expiryChecker()
{
if (!mExpiryChecker) {
mExpiryChecker.reset(new Kleo::ExpiryChecker{Kleo::ExpiryCheckerSettings{encryptOwnKeyNearExpiryWarningThresholdInDays(),
encryptKeyNearExpiryWarningThresholdInDays(),
encryptRootCertNearExpiryWarningThresholdInDays(),
encryptChainCertNearExpiryWarningThresholdInDays()}});
}
return mExpiryChecker;
}
QString ComposerViewBase::mailId() const
{
if (m_autoSaveUUID.isEmpty()) {
m_autoSaveUUID = QUuid::createUuid().toString(QUuid::WithoutBraces);
}
return m_autoSaveUUID;
}
void ComposerViewBase::setMailId(const QString &mailId)
{
m_autoSaveUUID = mailId;
}
bool ComposerViewBase::mailIdIsEmpty() const
{
return m_autoSaveUUID.isEmpty();
}
MessageDispatcher *ComposerViewBase::messageDispatcher() const
{
return m_messageDispatcher;
}
void ComposerViewBase::setMessageDispatcher(MessageDispatcher *messageDispatcher)
{
m_messageDispatcher = messageDispatcher;
}
#include "moc_composerviewbase.cpp"
diff --git a/client/firsttimedialog.cpp b/client/firsttimedialog.cpp
index 1828d30..ffff197 100644
--- a/client/firsttimedialog.cpp
+++ b/client/firsttimedialog.cpp
@@ -1,419 +1,418 @@
// SPDX-FileCopyrightText: 2024 g10 code Gmbh
// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "firsttimedialog.h"
+#include "../common/paths.h"
#include "config.h"
#include "gpgolweb_version.h"
#include "rootcagenerator/controller.h"
#include "ui_confpageinstalladdin.h"
#include "ui_confpageproxyoptions.h"
#include "ui_confpagetlscertificate.h"
#include "ui_confpagewelcome.h"
#include "ui_firsttimedialog.h"
#include "websocketclient.h"
#include <QCheckBox>
#include <QClipboard>
#include <QCloseEvent>
#include <QDesktopServices>
#include <QFile>
#include <QMargins>
#include <QDialog>
#include <QDialogButtonBox>
#include <QSaveFile>
-#include <QSettings>
-#include <QStandardPaths>
#include <QStatusBar>
#include <QStyle>
#include <QTemporaryDir>
#include <QToolBar>
#include <Libkleo/Compliance>
#include <KColorScheme>
#include <KIO/OpenFileManagerWindowJob>
#include <KTitleWidget>
using namespace Qt::StringLiterals;
class PairingDialog : public QDialog {
public:
PairingDialog(QWidget *parent) : QDialog(parent) {
setWindowTitle(i18n("Pairing mode active"));
auto l = new QVBoxLayout(this);
auto lab = new QLabel(i18n("<p>Copy and paste the code shown below into the input field shown at the top of the Add-In:</p>"));
lab->setWordWrap(true);
l->addWidget(lab);
m_pairingTokenLabel = new QLabel(i18nc("status message", "<p align='center'><b>Obtaining token...</b></p>"));
l->addWidget(m_pairingTokenLabel);
auto bb = new QDialogButtonBox;
auto endButton = bb->addButton(i18nc("@button", "End pairing mode"), QDialogButtonBox::RejectRole);
connect(endButton, &QAbstractButton::clicked, this, &QDialog::reject);
m_copyButton = bb->addButton(i18nc("@button", "Copy code to clipboard"), QDialogButtonBox::ActionRole);
m_copyButton->setEnabled(false);
l->addWidget(bb);
setFixedSize(sizeHint());
connect(&WebsocketClient::self(), &WebsocketClient::pairingStatusChanged, this, &PairingDialog::pairingStatusChanged);
WebsocketClient::self().enterPairingMode();
}
void pairingStatusChanged(const QString& token, bool pairingActive) {
if (!pairingActive) {
reject();
return;
}
m_pairingTokenLabel->setText(QString(u"<p align='center'><b>%1</b></p>").arg(token));
m_copyButton->setEnabled(true);
connect(m_copyButton, &QAbstractButton::clicked, [this, token]() {
qApp->clipboard()->setText(token);
});
}
private:
QLabel* m_pairingTokenLabel;
QAbstractButton* m_copyButton;
};
FirstTimeDialog::FirstTimeDialog(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::FirstTimeDialog)
, confPageWelcome(new Ui::ConfPageWelcome)
, confPageInstallAddin(new Ui::ConfPageInstallAddin)
, confPageProxyOptions(new Ui::ConfPageProxyOptions)
, confPageTLSCertificate(new Ui::ConfPageTLSCertificate)
, m_systemTrayIcon(QIcon::fromTheme(u"com.gnupg.gpgolweb"_s))
{
ui->setupUi(this);
confPageWelcome->setupUi(ui->confPageWelcome);
ui->confPageWelcome->setProperty("title", i18nc("@title", "Welcome to GpgOL/Web"));
confPageProxyOptions->setupUi(ui->confPageProxyOptions);
ui->confPageProxyOptions->setProperty("title", i18nc("@title", "Configure Proxy and Optional Features"));
confPageInstallAddin->setupUi(ui->confPageInstallAddin);
ui->confPageInstallAddin->setProperty("title", i18nc("@title", "Install Outlook Add-In"));
confPageTLSCertificate->setupUi(ui->confPageTLSCertificate);
ui->confPageTLSCertificate->setProperty("title", i18nc("@title", "Setting Up TLS Certificate for Local Proxy"));
if (ui->stack->indexOf(ui->confPageWelcome) != ConfPageWelcome) {
qFatal("Welcome page misplaced");
}
if (ui->stack->indexOf(ui->confPageTLSCertificate) != ConfPageTLSCertificate) {
qFatal("Tls certification page misplaced");
}
if (ui->stack->indexOf(ui->confPageProxyOptions) != ConfPageProxyOptions) {
qFatal("Proxy options page misplaced");
}
if (ui->stack->indexOf(ui->confPageInstallAddin) != ManifestPage) {
qFatal("Manifest install page misplaced");
}
confPageProxyOptions->reencryptOption->setChecked(Config::self()->reencrypt());
connect(confPageProxyOptions->reencryptOption, &QCheckBox::stateChanged, this, [](int state) {
Config::self()->setReencrypt(state == Qt::Checked);
Config::self()->save();
});
auto margins = confPageProxyOptions->remoteServerLayout->contentsMargins();
margins.setLeft(margins.left() + style()->pixelMetric(QStyle::PM_RadioButtonLabelSpacing) + style()->pixelMetric(QStyle::PM_ExclusiveIndicatorWidth));
confPageProxyOptions->remoteServerLayout->setContentsMargins(margins);
m_systemTrayIcon.setMainWindow(this);
m_systemTrayIcon.show();
m_backAction = new QAction(this);
connect(m_backAction, &QAction::triggered, this, [this]() {
if (Controller::certificateAlreadyGenerated() || !Config::self()->isLocalServer()) {
ui->stack->setCurrentIndex(ui->stack->currentIndex() > 1 ? ConfPageProxyOptions : ConfPageWelcome);
} else {
ui->stack->setCurrentIndex(ui->stack->currentIndex() == ConfPageProxyOptions ? ConfPageWelcome : ConfPageProxyOptions);
}
});
connect(confPageTLSCertificate->backButton, &QAbstractButton::clicked, m_backAction, &QAction::trigger);
connect(confPageInstallAddin->backButton, &QAbstractButton::clicked, m_backAction, &QAction::trigger);
connect(confPageProxyOptions->backButton, &QAbstractButton::clicked, m_backAction, &QAction::trigger);
auto toolbar = new QToolBar(this);
toolbar->setMovable(false);
auto titleWidget = new KTitleWidget(this);
toolbar->addWidget(titleWidget);
addToolBar(Qt::TopToolBarArea, toolbar);
titleWidget->setText(ui->stack->currentWidget()->property("title").toString());
connect(ui->stack, &QStackedWidget::currentChanged, this, [titleWidget, this]() {
titleWidget->setText(ui->stack->currentWidget()->property("title").toString());
});
QPixmap logo = QIcon::fromTheme(u"com.gnupg.gpgolweb"_s).pixmap(64, 64);
confPageWelcome->logo->setPixmap(logo);
confPageWelcome->titleWelcome->setText(i18nc("@info", "GpgOL/Web %1", QString::fromLocal8Bit(GPGOLWEB_VERSION_STRING)));
auto statusBar = new QStatusBar(this);
confPageInstallAddin->showOnStartup->setChecked(Config::self()->showLauncher());
connect(confPageInstallAddin->showOnStartup, &QCheckBox::toggled, this, [](bool checked) {
Config::self()->setShowLauncher(checked);
Config::self()->save();
});
m_status = new QLabel;
statusBar->addPermanentWidget(m_status);
auto version = new QLabel(i18nc("@info", "Version: %1", QString::fromLocal8Bit(GPGOLWEB_VERSION_STRING)));
statusBar->addPermanentWidget(version);
if (Kleo::DeVSCompliance::isActive()) {
auto statusLbl = std::make_unique<QLabel>(Kleo::DeVSCompliance::name());
{
auto statusPalette = qApp->palette();
KColorScheme::adjustForeground(statusPalette,
Kleo::DeVSCompliance::isCompliant() ? KColorScheme::NormalText : KColorScheme::NegativeText,
statusLbl->foregroundRole(),
KColorScheme::View);
statusLbl->setAutoFillBackground(true);
KColorScheme::adjustBackground(statusPalette,
Kleo::DeVSCompliance::isCompliant() ? KColorScheme::PositiveBackground : KColorScheme::NegativeBackground,
QPalette::Window,
KColorScheme::View);
statusLbl->setPalette(statusPalette);
}
statusBar->addPermanentWidget(statusLbl.release());
}
setStatusBar(statusBar);
connect(confPageProxyOptions->continueButton, &QPushButton::clicked, this, &FirstTimeDialog::slotSetup);
connect(confPageWelcome->configureButton, &QPushButton::clicked, this, [this]() {
ui->stack->setCurrentIndex(ConfPageProxyOptions);
});
connect(confPageTLSCertificate->continueButton, &QPushButton::clicked, this, &FirstTimeDialog::slotSetup);
confPageTLSCertificate->continueButton->setEnabled(false);
confPageTLSCertificate->installButton->setVisible(false);
confPageTLSCertificate->label->setVisible(false);
connect(confPageTLSCertificate->installButton, &QPushButton::clicked, this, [this]() {
if (m_controller) {
m_controller->install();
}
});
confPageInstallAddin->manifestPath->setText(QLatin1StringView(DATAROUTDIR) + u"/gpgol/manifest.xml"_s);
connect(confPageInstallAddin->testPageButton, &QPushButton::clicked, this, [this]() {
QDesktopServices::openUrl(QUrl(u"https://"_s + serverDomain() + u"/test"_s));
});
connect(confPageInstallAddin->pairWebClientButton, &QPushButton::clicked, this, [this]() {
PairingDialog d(this);
d.exec();
WebsocketClient::self().quitPairingMode();
});
connect(confPageInstallAddin->minimizeButton, &QPushButton::clicked, this, &QWidget::hide);
connect(confPageInstallAddin->manifestPathCopy, &QPushButton::clicked, this, [this]() {
QGuiApplication::clipboard()->setText(confPageInstallAddin->manifestPath->text());
});
connect(confPageInstallAddin->manifestPathOpenFolder, &QPushButton::clicked, this, [this]() {
auto job = new KIO::OpenFileManagerWindowJob();
job->setHighlightUrls({QUrl::fromUserInput(confPageInstallAddin->manifestPath->text())});
if (!qEnvironmentVariableIsEmpty("XDG_ACTIVATION_TOKEN")) {
job->setStartupId(qgetenv("XDG_ACTIVATION_TOKEN"));
}
job->start();
});
confPageProxyOptions->remoteLabel->setEnabled(!Config::self()->isLocalServer());
confPageProxyOptions->remoteServer->setEnabled(!Config::self()->isLocalServer());
confPageProxyOptions->remoteServer->setText(Config::self()->remoteAddress().toString());
confPageProxyOptions->remoteOption->setChecked(!Config::self()->isLocalServer());
connect(confPageProxyOptions->remoteOption, &QRadioButton::toggled, this, [this](bool checked) {
Config::self()->setIsLocalServer(!checked);
Config::self()->save();
confPageProxyOptions->remoteLabel->setEnabled(!Config::self()->isLocalServer());
confPageProxyOptions->remoteServer->setEnabled(!Config::self()->isLocalServer());
});
connect(confPageProxyOptions->remoteServer, &QLineEdit::textChanged, this, [this]() {
Config::self()->setRemoteAddress(QUrl::fromUserInput(confPageProxyOptions->remoteServer->text()));
Config::self()->save();
});
if (Controller::certificateAlreadyGenerated() || !Config::self()->isLocalServer()) {
ui->stack->setCurrentIndex(ConfPageWelcome);
if (Controller::certificateAlreadyGenerated() && Config::self()->isLocalServer()) {
startLocalServer();
}
startWebsocketClient();
} else {
ui->stack->setCurrentIndex(ConfPageProxyOptions);
}
connect(&m_serverProcess, &QProcess::readyReadStandardError, this, [this]() {
qWarning().noquote() << m_serverProcess.readAllStandardError();
});
connect(&m_serverProcess, &QProcess::readyReadStandardOutput, this, [this]() {
qWarning().noquote() << m_serverProcess.readAllStandardOutput();
});
connect(&m_serverProcess, &QProcess::errorOccurred, this, [this](QProcess::ProcessError err) {
qWarning() << "Process error" << err;
qWarning().noquote() << m_serverProcess.readAllStandardError();
});
connect(&m_serverProcess, &QProcess::finished, this, [this](int exitCode, QProcess::ExitStatus status) {
qWarning() << "Process finished" << exitCode << status;
if (status == QProcess::NormalExit) {
qWarning() << "Status code" << m_serverProcess.exitCode();
}
qWarning().noquote() << m_serverProcess.readAllStandardError();
});
#ifdef Q_OS_WIN
// We intentionally don't use our own config for this: If users disable autostart via
// the Windows settings menu, we want to respect that, too.
{
QSettings winreg(u"HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run"_s, QSettings::NativeFormat);
confPageWelcome->autostartBox->setChecked(!winreg.value(QCoreApplication::applicationName()).toString().isEmpty());
}
connect(confPageWelcome->autostartBox, &QCheckBox::checkStateChanged, this, [this](Qt::CheckState state) {
QSettings winreg(u"HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run"_s, QSettings::NativeFormat);
if (state) {
winreg.setValue(QCoreApplication::applicationName(),
QDir::toNativeSeparators(QCoreApplication::applicationFilePath()));
} else {
winreg.remove(QCoreApplication::applicationName());
}
});
#else
confPageWelcome->autostartBox->setVisible(false);
#endif
}
FirstTimeDialog::~FirstTimeDialog() = default;
void FirstTimeDialog::slotStateChanged(const QString &stateDisplay)
{
m_status->setText(stateDisplay);
m_systemTrayIcon.stateChanged(stateDisplay, WebsocketClient::self().state());
}
void FirstTimeDialog::closeEvent(QCloseEvent *e)
{
e->ignore();
hide();
}
void FirstTimeDialog::slotSetup()
{
if (confPageProxyOptions->localOption->isChecked()) {
if (!Controller::certificateAlreadyGenerated()) {
delete m_controller;
m_controller = new Controller(this);
confPageTLSCertificate->plainTextEdit->clear();
connect(m_controller, &Controller::generationDone, this, [this]() {
confPageTLSCertificate->installButton->setVisible(true);
confPageTLSCertificate->installButton->setEnabled(true);
confPageTLSCertificate->label->setText(
i18nc("@info", "About to install certificate with fingerprint: %1 ", m_controller->rootFingerprint()));
confPageTLSCertificate->label->setVisible(true);
confPageTLSCertificate->continueButton->setVisible(false);
});
ui->stack->setCurrentIndex(ConfPageTLSCertificate);
connect(m_controller, &Controller::result, this, [this](KJob *) {
if (m_controller->error()) {
confPageTLSCertificate->plainTextEdit->appendPlainText(m_controller->errorText());
return;
}
confPageTLSCertificate->installButton->setVisible(false);
confPageTLSCertificate->continueButton->setVisible(true);
confPageTLSCertificate->label->setText(i18nc("@info", "Installed certificate with fingerprint: %1", m_controller->rootFingerprint()));
confPageTLSCertificate->continueButton->setEnabled(true);
});
connect(m_controller, &Controller::debutOutput, this, &FirstTimeDialog::slotTlsDebutOutput);
m_controller->start();
} else {
startLocalServer();
startWebsocketClient();
generateManifest();
}
} else {
generateManifest();
}
}
void FirstTimeDialog::startLocalServer()
{
if (m_serverProcess.state() != QProcess::NotRunning) {
return;
}
m_serverProcess.start(u"gpgol-server"_s);
}
void FirstTimeDialog::startWebsocketClient()
{
const auto clientId = QUuid::createUuid().toString(QUuid::WithoutBraces);
auto &websocketClient = WebsocketClient::self(QUrl(u"wss://"_s + serverDomain() + u"/websocket"_s), clientId);
connect(&websocketClient, &WebsocketClient::stateChanged, this, &FirstTimeDialog::slotStateChanged);
slotStateChanged(websocketClient.stateDisplay());
}
void FirstTimeDialog::slotTlsDebutOutput(const QString &output)
{
confPageTLSCertificate->plainTextEdit->appendPlainText(output);
}
void FirstTimeDialog::generateManifest()
{
QFile file(u":/gpgol-client/manifest.xml.in"_s);
if (!file.open(QIODeviceBase::ReadOnly)) {
Q_ASSERT(false);
return;
}
ui->stack->setCurrentIndex(ManifestPage);
QByteArray manifest = file.readAll();
manifest.replace("%HOST%", serverDomain().toUtf8());
manifest.replace("%VERSION%", GPGOLWEB_VERSION_STRING);
- const auto saveFilePath = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + u"/gpgol-web-manifest.xml"_s;
+ const auto saveFilePath = CommonPaths::writeablePath() + u"/gpgol-web-manifest.xml"_s;
QSaveFile saveFile(saveFilePath);
if (!saveFile.open(QIODeviceBase::WriteOnly)) {
Q_ASSERT(false);
return;
}
saveFile.write(manifest);
saveFile.commit();
confPageInstallAddin->manifestPath->setText(QDir::toNativeSeparators(saveFilePath));
}
QString FirstTimeDialog::serverDomain() const
{
return confPageProxyOptions->localOption->isChecked() ? u"localhost:5656"_s : confPageProxyOptions->remoteServer->text();
}
#ifdef Q_OS_WIN
#include <windows.h>
#endif
void FirstTimeDialog::strongActivateWindow(QWidget* window)
{
#ifdef Q_OS_WIN
// HACK: Simulate Alt-keyPress while bringing the window to the front.
// This helps when our app does not currently have focus - and
// frequently it does not, because we have just clicked in browser/outlook.
// https://stackoverflow.com/questions/72620538/whats-the-correct-way-to-bring-a-window-to-the-front
keybd_event(VK_RMENU, 0, 0, 0);
#endif
window->show();
window->activateWindow();
window->raise();
#ifdef Q_OS_WIN
keybd_event(VK_RMENU, 0, KEYEVENTF_KEYUP, 0);
#endif
}
diff --git a/client/identity/identitydialog.cpp b/client/identity/identitydialog.cpp
index 481208c..d26ea0a 100644
--- a/client/identity/identitydialog.cpp
+++ b/client/identity/identitydialog.cpp
@@ -1,710 +1,709 @@
/*
identitydialog.cpp
This file is part of KMail, the KDE mail client.
SPDX-FileCopyrightText: 2002 Marc Mutz <mutz@kde.org>
SPDX-FileCopyrightText: 2014-2023 Laurent Montel <montel@kde.org>
SPDX-License-Identifier: GPL-2.0-only
*/
#include "identitydialog.h"
#include "addressvalidationjob.h"
#include "identitymanager.h"
#include "kleo_util.h"
#include <QGpgME/Job>
#include <QGpgME/Protocol>
// other KMail headers:
#include <KEditListWidget>
#include <Sonnet/DictionaryComboBox>
// other kdepim headers:
#include "identity/identity.h"
#include "identity/signatureconfigurator.h"
#include <KLineEditEventHandler>
// libkleopatra:
#include <Libkleo/DefaultKeyFilter>
#include <Libkleo/Formatting>
#include <Libkleo/KeySelectionCombo>
#include <libkleo_version.h>
#if LIBKLEO_VERSION >= QT_VERSION_CHECK(6, 2, 40)
#include <Libkleo/KeyParameters>
#include <Libkleo/OpenPGPCertificateCreationDialog>
#include <Libkleo/ProgressDialog>
#endif
// gpgme++
#include <QGpgME/KeyGenerationJob>
#include <gpgme++/context.h>
#include <gpgme++/interfaces/passphraseprovider.h>
#include <gpgme++/keygenerationresult.h>
// other KDE headers:
#include <KEmailAddress>
#include <KEmailValidator>
#include <KLineEdit>
#include <KLocalizedString>
#include <KMessageBox>
// Qt headers:
#include <QCheckBox>
#include <QComboBox>
#include <QDialogButtonBox>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QFormLayout>
#include <QGroupBox>
#include <QHostInfo>
#include <QIcon>
#include <QLabel>
#include <QPushButton>
-#include <QStandardPaths>
#include <QTabWidget>
#include <QToolButton>
#include <QVBoxLayout>
// other headers:
#include <algorithm>
#include <gpgme++/key.h>
#include <iterator>
using namespace Qt::StringLiterals;
#if LIBKLEO_VERSION >= QT_VERSION_CHECK(6, 2, 40)
class EmptyPassphraseProvider : public GpgME::PassphraseProvider
{
public:
char *getPassphrase(const char *useridHint, const char *description, bool previousWasBad, bool &canceled) override
{
Q_UNUSED(useridHint);
Q_UNUSED(description);
Q_UNUSED(previousWasBad);
Q_UNUSED(canceled);
return gpgrt_strdup("");
}
};
#endif
namespace KMail
{
class KeySelectionCombo : public Kleo::KeySelectionCombo
{
Q_OBJECT
public:
enum KeyType {
SigningKey,
EncryptionKey,
};
explicit KeySelectionCombo(KeyType keyType, GpgME::Protocol protocol, QWidget *parent);
~KeySelectionCombo() override;
void setIdentity(const QString &name, const QString &email);
void init() override;
private:
void onCustomItemSelected(const QVariant &type);
QString mEmail;
QString mName;
const KeyType mKeyType;
const GpgME::Protocol mProtocol;
};
#if LIBKLEO_VERSION >= QT_VERSION_CHECK(6, 2, 40)
class KMailKeyGenerationJob : public KJob
{
Q_OBJECT
public:
enum {
GpgError = UserDefinedError,
};
explicit KMailKeyGenerationJob(const QString &name, const QString &email, KeySelectionCombo *parent);
~KMailKeyGenerationJob() override;
bool doKill() override;
void start() override;
private:
void createCertificate(const Kleo::KeyParameters &keyParameters, bool protectKeyWithPassword);
void keyGenerated(const GpgME::KeyGenerationResult &result);
const QString mName;
const QString mEmail;
QGpgME::Job *mJob = nullptr;
EmptyPassphraseProvider emptyPassphraseProvider;
};
KMailKeyGenerationJob::KMailKeyGenerationJob(const QString &name, const QString &email, KeySelectionCombo *parent)
: KJob(parent)
, mName(name)
, mEmail(email)
{
}
KMailKeyGenerationJob::~KMailKeyGenerationJob() = default;
bool KMailKeyGenerationJob::doKill()
{
if (mJob) {
mJob->slotCancel();
}
return true;
}
void KMailKeyGenerationJob::start()
{
auto dialog = new Kleo::OpenPGPCertificateCreationDialog(qobject_cast<KeySelectionCombo *>(parent()));
dialog->setName(mName);
dialog->setEmail(mEmail);
dialog->setAttribute(Qt::WA_DeleteOnClose);
connect(dialog, &QDialog::accepted, this, [this, dialog]() {
const auto keyParameters = dialog->keyParameters();
const auto protectKeyWithPassword = dialog->protectKeyWithPassword();
QMetaObject::invokeMethod(
this,
[this, keyParameters, protectKeyWithPassword] {
createCertificate(keyParameters, protectKeyWithPassword);
},
Qt::QueuedConnection);
});
connect(dialog, &QDialog::rejected, this, [this]() {
emitResult();
});
dialog->show();
}
void KMailKeyGenerationJob::createCertificate(const Kleo::KeyParameters &keyParameters, bool protectKeyWithPassword)
{
Q_ASSERT(keyParameters.protocol() == Kleo::KeyParameters::OpenPGP);
auto keyGenJob = QGpgME::openpgp()->keyGenerationJob();
if (!keyGenJob) {
setError(GpgError);
setErrorText(i18nc("@info:status", "Could not start OpenPGP certificate generation."));
emitResult();
return;
}
if (!protectKeyWithPassword) {
auto ctx = QGpgME::Job::context(keyGenJob);
ctx->setPassphraseProvider(&emptyPassphraseProvider);
ctx->setPinentryMode(GpgME::Context::PinentryLoopback);
}
connect(keyGenJob, &QGpgME::KeyGenerationJob::result, this, &KMailKeyGenerationJob::keyGenerated);
if (const GpgME::Error err = keyGenJob->start(keyParameters.toString())) {
setError(GpgError);
setErrorText(i18n("Could not start OpenPGP certificate generation: %1", Kleo::Formatting::errorAsString(err)));
emitResult();
return;
} else {
mJob = keyGenJob;
}
auto progressDialog = new QProgressDialog;
progressDialog->setAttribute(Qt::WA_DeleteOnClose);
progressDialog->setModal(true);
progressDialog->setWindowTitle(i18nc("@title", "Generating an OpenPGP Certificate…"));
progressDialog->setLabelText(
i18n("The process of generating an OpenPGP certificate requires large amounts of random numbers. This may require several minutes…"));
progressDialog->setRange(0, 0);
connect(progressDialog, &QProgressDialog::canceled, this, [this]() {
kill();
});
connect(mJob, &QGpgME::Job::done, this, [progressDialog]() {
if (progressDialog) {
progressDialog->accept();
}
});
progressDialog->show();
}
void KMailKeyGenerationJob::keyGenerated(const GpgME::KeyGenerationResult &result)
{
mJob = nullptr;
if (result.error()) {
setError(GpgError);
setErrorText(i18n("Could not generate an OpenPGP certificate: %1", Kleo::Formatting::errorAsString(result.error())));
emitResult();
return;
} else if (result.error().isCanceled()) {
setError(GpgError);
setErrorText(i18nc("@info:status", "Key generation was cancelled."));
emitResult();
return;
}
auto combo = qobject_cast<KeySelectionCombo *>(parent());
combo->setDefaultKey(QLatin1StringView(result.fingerprint()));
connect(combo, &KeySelectionCombo::keyListingFinished, this, &KMailKeyGenerationJob::emitResult);
combo->refreshKeys();
}
#endif
KeySelectionCombo::KeySelectionCombo(KeyType keyType, GpgME::Protocol protocol, QWidget *parent)
: Kleo::KeySelectionCombo(parent)
, mKeyType(keyType)
, mProtocol(protocol)
{
}
KeySelectionCombo::~KeySelectionCombo() = default;
void KeySelectionCombo::setIdentity(const QString &name, const QString &email)
{
mName = name;
mEmail = email;
setIdFilter(email);
}
void KeySelectionCombo::init()
{
Kleo::KeySelectionCombo::init();
auto keyFilter = std::make_shared<Kleo::DefaultKeyFilter>();
keyFilter->setIsOpenPGP(mProtocol == GpgME::OpenPGP ? Kleo::DefaultKeyFilter::Set : Kleo::DefaultKeyFilter::NotSet);
if (mKeyType == SigningKey) {
keyFilter->setCanSign(Kleo::DefaultKeyFilter::Set);
keyFilter->setHasSecret(Kleo::DefaultKeyFilter::Set);
} else {
keyFilter->setCanEncrypt(Kleo::DefaultKeyFilter::Set);
}
setKeyFilter(keyFilter);
prependCustomItem(QIcon(), i18n("No key"), QStringLiteral("no-key"));
#if LIBKLEO_VERSION >= QT_VERSION_CHECK(6, 2, 40)
if (mProtocol == GpgME::OpenPGP) {
appendCustomItem(QIcon::fromTheme(QStringLiteral("password-generate")), i18n("Generate a new OpenPGP certificate"), QStringLiteral("generate-new-key"));
}
#endif
connect(this, &KeySelectionCombo::customItemSelected, this, &KeySelectionCombo::onCustomItemSelected);
}
void KeySelectionCombo::onCustomItemSelected(const QVariant &type)
{
if (type == "no-key"_L1) {
return;
} else if (type == "generate-new-key"_L1) {
#if LIBKLEO_VERSION >= QT_VERSION_CHECK(6, 2, 40)
auto job = new KMailKeyGenerationJob(mName, mEmail, this);
setEnabled(false);
connect(job, &KMailKeyGenerationJob::finished, this, [this, job]() {
if (job->error() != KJob::NoError) {
KMessageBox::error(qobject_cast<QWidget *>(parent()), job->errorText(), i18n("Key Generation Error"));
}
setEnabled(true);
});
job->start();
#endif
}
}
IdentityDialog::IdentityDialog(QWidget *parent)
: QDialog(parent)
{
setWindowTitle(i18nc("@title:window", "Edit Identity"));
auto mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins({});
auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok);
okButton->setDefault(true);
okButton->setShortcut(Qt::CTRL | Qt::Key_Return);
connect(buttonBox, &QDialogButtonBox::accepted, this, &IdentityDialog::slotAccepted);
connect(buttonBox, &QDialogButtonBox::rejected, this, &IdentityDialog::reject);
//
// Tab Widget: General
//
auto page = new QWidget(this);
mainLayout->addWidget(page);
auto buttonBoxLayout = new QVBoxLayout;
buttonBoxLayout->setContentsMargins(style()->pixelMetric(QStyle::PM_LayoutLeftMargin),
style()->pixelMetric(QStyle::PM_LayoutTopMargin),
style()->pixelMetric(QStyle::PM_LayoutRightMargin),
style()->pixelMetric(QStyle::PM_LayoutBottomMargin));
buttonBoxLayout->addWidget(buttonBox);
mainLayout->addLayout(buttonBoxLayout);
auto vlay = new QVBoxLayout(page);
vlay->setContentsMargins({});
mTabWidget = new QTabWidget(page);
mTabWidget->tabBar()->setExpanding(true);
mTabWidget->setDocumentMode(true);
mTabWidget->setObjectName(QStringLiteral("config-identity-tab"));
vlay->addWidget(mTabWidget);
auto tab = new QWidget(mTabWidget);
mTabWidget->addTab(tab, i18nc("@title:tab General identity settings.", "General"));
auto formLayout = new QFormLayout(tab);
// "Name" line edit and label:
mNameEdit = new QLineEdit(tab);
KLineEditEventHandler::catchReturnKey(mNameEdit);
auto label = new QLabel(i18n("&Your name:"), tab);
formLayout->addRow(label, mNameEdit);
label->setBuddy(mNameEdit);
QString msg = i18n(
"<qt><h3>Your name</h3>"
"<p>This field should contain your name as you would like "
"it to appear in the email header that is sent out;</p>"
"<p>if you leave this blank your real name will not "
"appear, only the email address.</p></qt>");
label->setWhatsThis(msg);
mNameEdit->setWhatsThis(msg);
// "Organization" line edit and label:
mOrganizationEdit = new QLineEdit(tab);
KLineEditEventHandler::catchReturnKey(mOrganizationEdit);
label = new QLabel(i18n("Organi&zation:"), tab);
formLayout->addRow(label, mOrganizationEdit);
label->setBuddy(mOrganizationEdit);
msg = i18n(
"<qt><h3>Organization</h3>"
"<p>This field should have the name of your organization "
"if you would like it to be shown in the email header that "
"is sent out.</p>"
"<p>It is safe (and normal) to leave this blank.</p></qt>");
label->setWhatsThis(msg);
mOrganizationEdit->setWhatsThis(msg);
// "Dictionary" combo box and label:
mDictionaryCombo = new Sonnet::DictionaryComboBox(tab);
label = new QLabel(i18n("D&ictionary:"), tab);
label->setBuddy(mDictionaryCombo);
formLayout->addRow(label, mDictionaryCombo);
// "Email Address" line edit and label:
// (row 3: spacer)
mEmailEdit = new QLineEdit(tab);
mEmailEdit->setEnabled(false);
KLineEditEventHandler::catchReturnKey(mEmailEdit);
label = new QLabel(i18n("&Email address:"), tab);
formLayout->addRow(label, mEmailEdit);
label->setBuddy(mEmailEdit);
msg = i18n(
"<qt><h3>Email address</h3>"
"<p>This field should have your full email address.</p>"
"<p>This address is the primary one, used for all outgoing mail. "
"If you have more than one address, either create a new identity, "
"or add additional alias addresses in the field below.</p>"
"<p>If you leave this blank, or get it wrong, people "
"will have trouble replying to you.</p></qt>");
label->setWhatsThis(msg);
mEmailEdit->setWhatsThis(msg);
auto emailValidator = new KEmailValidator(this);
mEmailEdit->setValidator(emailValidator);
//
// Tab Widget: Security
//
mCryptographyTab = new QWidget(mTabWidget);
mTabWidget->addTab(mCryptographyTab, i18nc("@title:tab", "Security"));
formLayout = new QFormLayout(mCryptographyTab);
// "OpenPGP Signature Key" requester and label:
mPGPSigningKeyRequester = new KeySelectionCombo(KeySelectionCombo::SigningKey, GpgME::OpenPGP, mCryptographyTab);
mPGPSigningKeyRequester->setObjectName("PGP Signing Key Requester");
msg = i18n(
"<qt><p>The OpenPGP key you choose here will be used "
"to digitally sign messages. You can also use GnuPG keys.</p>"
"<p>You can leave this blank, but KMail will not be able "
"to digitally sign emails using OpenPGP; "
"normal mail functions will not be affected.</p>"
"<p>You can find out more about keys at <a>https://www.gnupg.org</a></p></qt>");
label = new QLabel(i18n("OpenPGP signing key:"), mCryptographyTab);
label->setBuddy(mPGPSigningKeyRequester);
mPGPSigningKeyRequester->setWhatsThis(msg);
label->setWhatsThis(msg);
auto vbox = new QVBoxLayout;
mPGPSameKey = new QCheckBox(i18n("Use same key for encryption and signing"));
vbox->addWidget(mPGPSigningKeyRequester);
vbox->addWidget(mPGPSameKey);
formLayout->addRow(label, vbox);
connect(mPGPSameKey, &QCheckBox::toggled, this, [this, formLayout, vbox](bool checked) {
mPGPEncryptionKeyRequester->setVisible(!checked);
formLayout->labelForField(mPGPEncryptionKeyRequester)->setVisible(!checked);
const auto label = qobject_cast<QLabel *>(formLayout->labelForField(vbox));
if (checked) {
label->setText(i18n("OpenPGP key:"));
const auto key = mPGPSigningKeyRequester->currentKey();
if (!key.isBad()) {
mPGPEncryptionKeyRequester->setCurrentKey(key);
} else if (mPGPSigningKeyRequester->currentData() == QLatin1String("no-key")) {
mPGPEncryptionKeyRequester->setCurrentIndex(mPGPSigningKeyRequester->currentIndex());
}
} else {
label->setText(i18n("OpenPGP signing key:"));
}
});
connect(mPGPSigningKeyRequester, &KeySelectionCombo::currentKeyChanged, this, [&](const GpgME::Key &key) {
if (mPGPSameKey->isChecked()) {
mPGPEncryptionKeyRequester->setCurrentKey(key);
}
});
connect(mPGPSigningKeyRequester, &KeySelectionCombo::customItemSelected, this, [&](const QVariant &type) {
if (mPGPSameKey->isChecked() && type == QLatin1String("no-key")) {
mPGPEncryptionKeyRequester->setCurrentIndex(mPGPSigningKeyRequester->currentIndex());
}
});
connect(mPGPSigningKeyRequester, &KeySelectionCombo::keyListingFinished, this, [this] {
slotKeyListingFinished(mPGPSigningKeyRequester);
});
// "OpenPGP Encryption Key" requester and label:
mPGPEncryptionKeyRequester = new KeySelectionCombo(KeySelectionCombo::EncryptionKey, GpgME::OpenPGP, mCryptographyTab);
msg = i18n(
"<qt><p>The OpenPGP key you choose here will be used "
"to encrypt messages to yourself and for the \"Attach My Public Key\" "
"feature in the composer. You can also use GnuPG keys.</p>"
"<p>You can leave this blank, but KMail will not be able "
"to encrypt copies of outgoing messages to you using OpenPGP; "
"normal mail functions will not be affected.</p>"
"<p>You can find out more about keys at <a>https://www.gnupg.org</a></p></qt>");
label = new QLabel(i18n("OpenPGP encryption key:"), mCryptographyTab);
label->setBuddy(mPGPEncryptionKeyRequester);
label->setWhatsThis(msg);
mPGPEncryptionKeyRequester->setWhatsThis(msg);
formLayout->addRow(label, mPGPEncryptionKeyRequester);
// "S/MIME Signature Key" requester and label:
mSMIMESigningKeyRequester = new KeySelectionCombo(KeySelectionCombo::SigningKey, GpgME::CMS, mCryptographyTab);
mSMIMESigningKeyRequester->setObjectName("SMIME Signing Key Requester");
msg = i18n(
"<qt><p>The S/MIME (X.509) certificate you choose here will be used "
"to digitally sign messages.</p>"
"<p>You can leave this blank, but KMail will not be able "
"to digitally sign emails using S/MIME; "
"normal mail functions will not be affected.</p></qt>");
label = new QLabel(i18n("S/MIME signing certificate:"), mCryptographyTab);
label->setBuddy(mSMIMESigningKeyRequester);
mSMIMESigningKeyRequester->setWhatsThis(msg);
label->setWhatsThis(msg);
formLayout->addRow(label, mSMIMESigningKeyRequester);
connect(mSMIMESigningKeyRequester, &KeySelectionCombo::keyListingFinished, this, [this] {
slotKeyListingFinished(mSMIMESigningKeyRequester);
});
const QGpgME::Protocol *smimeProtocol = QGpgME::smime();
label->setEnabled(smimeProtocol);
mSMIMESigningKeyRequester->setEnabled(smimeProtocol);
// "S/MIME Encryption Key" requester and label:
mSMIMEEncryptionKeyRequester = new KeySelectionCombo(KeySelectionCombo::EncryptionKey, GpgME::CMS, mCryptographyTab);
mSMIMEEncryptionKeyRequester->setObjectName("SMIME Encryption Key Requester");
msg = i18n(
"<qt><p>The S/MIME certificate you choose here will be used "
"to encrypt messages to yourself and for the \"Attach My Certificate\" "
"feature in the composer.</p>"
"<p>You can leave this blank, but KMail will not be able "
"to encrypt copies of outgoing messages to you using S/MIME; "
"normal mail functions will not be affected.</p></qt>");
label = new QLabel(i18n("S/MIME encryption certificate:"), mCryptographyTab);
label->setBuddy(mSMIMEEncryptionKeyRequester);
mSMIMEEncryptionKeyRequester->setWhatsThis(msg);
connect(mSMIMEEncryptionKeyRequester, &KeySelectionCombo::keyListingFinished, this, [this] {
slotKeyListingFinished(mSMIMEEncryptionKeyRequester);
});
label->setWhatsThis(msg);
formLayout->addRow(label, mSMIMEEncryptionKeyRequester);
label->setEnabled(smimeProtocol);
mSMIMEEncryptionKeyRequester->setEnabled(smimeProtocol);
mWarnNotEncrypt = new QCheckBox(i18n("Warn when trying to send unencrypted messages"));
formLayout->addRow(QString(), mWarnNotEncrypt);
//
// Tab Widget: Signature
//
mSignatureConfigurator = new KIdentityManagementWidgets::SignatureConfigurator(mTabWidget);
mTabWidget->addTab(mSignatureConfigurator, i18n("Signature"));
}
IdentityDialog::~IdentityDialog() = default;
void IdentityDialog::slotAccepted()
{
// Validate email addresses
const QString email = mEmailEdit->text().trimmed();
if (email.isEmpty()) {
KMessageBox::error(this, i18n("You must provide an email for this identity."), i18nc("@title:window", "Empty Email Address"));
return;
}
if (!KEmailAddress::isValidSimpleAddress(email)) {
const QString errorMsg(KEmailAddress::simpleEmailAddressErrorMsg());
KMessageBox::error(this, errorMsg, i18n("Invalid Email Address"));
return;
}
const GpgME::Key &pgpSigningKey = mPGPSigningKeyRequester->currentKey();
const GpgME::Key &pgpEncryptionKey = mPGPEncryptionKeyRequester->currentKey();
const GpgME::Key &smimeSigningKey = mSMIMESigningKeyRequester->currentKey();
const GpgME::Key &smimeEncryptionKey = mSMIMEEncryptionKeyRequester->currentKey();
QString msg;
bool err = false;
if (!keyMatchesEmailAddress(pgpSigningKey, email)) {
msg = i18n(
"One of the configured OpenPGP signing keys does not contain "
"any user ID with the configured email address for this "
"identity (%1).\n"
"This might result in warning messages on the receiving side "
"when trying to verify signatures made with this configuration.",
email);
err = true;
} else if (!keyMatchesEmailAddress(pgpEncryptionKey, email)) {
msg = i18n(
"One of the configured OpenPGP encryption keys does not contain "
"any user ID with the configured email address for this "
"identity (%1).",
email);
err = true;
} else if (!keyMatchesEmailAddress(smimeSigningKey, email)) {
msg = i18n(
"One of the configured S/MIME signing certificates does not contain "
"the configured email address for this "
"identity (%1).\n"
"This might result in warning messages on the receiving side "
"when trying to verify signatures made with this configuration.",
email);
err = true;
} else if (!keyMatchesEmailAddress(smimeEncryptionKey, email)) {
msg = i18n(
"One of the configured S/MIME encryption certificates does not contain "
"the configured email address for this "
"identity (%1).",
email);
err = true;
}
if (err
&& KMessageBox::warningContinueCancel(this,
msg,
i18nc("@title:window", "Email Address Not Found in Key/Certificates"),
KStandardGuiItem::cont(),
KStandardGuiItem::cancel(),
QStringLiteral("warn_email_not_in_certificate"))
!= KMessageBox::Continue) {
return;
}
accept();
}
bool IdentityDialog::keyMatchesEmailAddress(const GpgME::Key &key, const QString &email_)
{
if (key.isNull()) {
return true;
}
const QString email = email_.trimmed().toLower();
const auto uids = key.userIDs();
for (const auto &uid : uids) {
QString em = QString::fromUtf8(uid.email() ? uid.email() : uid.id());
if (em.isEmpty()) {
continue;
}
if (em[0] == QLatin1Char('<')) {
em = em.mid(1, em.length() - 2);
}
if (em.toLower() == email) {
return true;
}
}
return false;
}
void IdentityDialog::setIdentity(KIdentityManagementCore::Identity &ident)
{
setWindowTitle(i18nc("@title:window", "Edit Identity \"%1\"", ident.identityName()));
// "General" tab:
mNameEdit->setText(ident.fullName());
mOrganizationEdit->setText(ident.organization());
mEmailEdit->setText(ident.primaryEmailAddress());
mDictionaryCombo->setCurrentByDictionaryName(ident.dictionary());
// "Cryptography" tab:
mPGPSigningKeyRequester->setDefaultKey(QLatin1String(ident.pgpSigningKey()));
mPGPEncryptionKeyRequester->setDefaultKey(QLatin1String(ident.pgpEncryptionKey()));
mPGPSameKey->setChecked(ident.pgpSigningKey() == ident.pgpEncryptionKey());
mSMIMESigningKeyRequester->setDefaultKey(QLatin1String(ident.smimeSigningKey()));
mSMIMEEncryptionKeyRequester->setDefaultKey(QLatin1String(ident.smimeEncryptionKey()));
mWarnNotEncrypt->setChecked(ident.warnNotEncrypt());
// "Signature" tab:
mSignatureConfigurator->setSignature(ident.signature());
// set the configured email address as initial query of the key
// requesters:
const QString name = mNameEdit->text().trimmed();
const QString email = mEmailEdit->text().trimmed();
mPGPEncryptionKeyRequester->setIdentity(name, email);
mPGPSigningKeyRequester->setIdentity(name, email);
mSMIMEEncryptionKeyRequester->setIdentity(name, email);
mSMIMESigningKeyRequester->setIdentity(name, email);
}
void IdentityDialog::updateIdentity(KIdentityManagementCore::Identity &ident)
{
// "General" tab:
ident.setFullName(mNameEdit->text());
ident.setOrganization(mOrganizationEdit->text());
QString email = mEmailEdit->text().trimmed();
ident.setPrimaryEmailAddress(email);
// "Cryptography" tab:
ident.setPGPSigningKey(mPGPSigningKeyRequester->currentKey().primaryFingerprint());
ident.setPGPEncryptionKey(mPGPEncryptionKeyRequester->currentKey().primaryFingerprint());
ident.setSMIMESigningKey(mSMIMESigningKeyRequester->currentKey().primaryFingerprint());
ident.setSMIMEEncryptionKey(mSMIMEEncryptionKeyRequester->currentKey().primaryFingerprint());
ident.setEncryptionOverride(true);
ident.setWarnNotEncrypt(mWarnNotEncrypt->isChecked());
ident.setWarnNotEncrypt(mWarnNotEncrypt->isChecked());
// "Advanced" tab:
ident.setDictionary(mDictionaryCombo->currentDictionaryName());
// "Signature" tab:
ident.setSignature(mSignatureConfigurator->signature());
}
void IdentityDialog::slotKeyListingFinished(KeySelectionCombo *combo)
{
mInitialLoadingFinished << combo;
if (mInitialLoadingFinished.count() == 2) {
Q_EMIT keyListingFinished();
}
}
}
#include "identitydialog.moc"
#include "moc_identitydialog.cpp"
diff --git a/client/identity/signatureconfigurator.cpp b/client/identity/signatureconfigurator.cpp
index 32114d3..78296e3 100644
--- a/client/identity/signatureconfigurator.cpp
+++ b/client/identity/signatureconfigurator.cpp
@@ -1,162 +1,160 @@
/* -*- c++ -*-
SPDX-FileCopyrightText: 2008 Thomas McGuire <Thomas.McGuire@gmx.net>
SPDX-FileCopyrightText: 2008 Edwin Schepers <yez@familieschepers.nl>
SPDX-FileCopyrightText: 2004 Marc Mutz <mutz@kde.org>
SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "signatureconfigurator.h"
#include "editor/bodytexteditor.h"
#include "signaturerichtexteditor_p.h"
#include <KActionCollection>
#include <KLineEdit>
#include <KLocalizedString>
#include <KMessageBox>
#include <KToolBar>
#include <QUrl>
#include <QCheckBox>
#include <QComboBox>
#include <QDir>
#include <QFileInfo>
#include <QLabel>
#include <QStackedWidget>
#include <QHBoxLayout>
#include <QVBoxLayout>
-#include <QStandardPaths>
-
using namespace KIdentityManagementWidgets;
namespace KIdentityManagementWidgets
{
/**
Private class that helps to provide binary compatibility between releases.
@internal
*/
//@cond PRIVATE
class Q_DECL_HIDDEN SignatureConfiguratorPrivate
{
public:
explicit SignatureConfiguratorPrivate(SignatureConfigurator *parent);
void init();
// Returns the current text of the textedit as HTML code, but strips
// unnecessary tags Qt inserts
[[nodiscard]] QString asCleanedHTML() const;
QString imageLocation;
SignatureConfigurator *const q;
QCheckBox *mEnableCheck = nullptr;
MessageComposer::BodyTextEditor *mTextEdit = nullptr;
};
//@endcond
SignatureConfiguratorPrivate::SignatureConfiguratorPrivate(SignatureConfigurator *parent)
: q(parent)
{
}
void SignatureConfiguratorPrivate::init()
{
auto vlay = new QVBoxLayout(q);
vlay->setObjectName(QLatin1StringView("main layout"));
// "enable signature" checkbox:
mEnableCheck = new QCheckBox(i18n("&Enable signature"), q);
mEnableCheck->setWhatsThis(
i18n("Check this box if you want KMail to append a signature to mails "
"written with this identity."));
vlay->addWidget(mEnableCheck);
// "obtain signature text from" combo and label:
auto hlay = new QHBoxLayout(); // inherits spacing
vlay->addLayout(hlay);
// widget stack that is controlled by the source combo:
auto widgetStack = new QStackedWidget(q);
widgetStack->setEnabled(false); // since !mEnableCheck->isChecked()
vlay->addWidget(widgetStack, 1);
// connects for the enabling of the widgets depending on
// signatureEnabled:
q->connect(mEnableCheck, &QCheckBox::toggled, widgetStack, &QStackedWidget::setEnabled);
// The focus might be still in the widget that is disabled
q->connect(mEnableCheck, &QCheckBox::clicked, mEnableCheck, qOverload<>(&QCheckBox::setFocus));
int pageno = 0;
// page 0: input field for direct entering:
auto page = new QWidget(widgetStack);
widgetStack->insertWidget(pageno, page);
auto page_vlay = new QVBoxLayout(page);
page_vlay->setContentsMargins(0, 0, 0, 0);
mTextEdit = new MessageComposer::BodyTextEditor(q);
page_vlay->addWidget(mTextEdit, 2);
mTextEdit->setWhatsThis(i18n("Use this field to enter an arbitrary static signature."));
hlay = new QHBoxLayout(); // inherits spacing
page_vlay->addLayout(hlay);
widgetStack->setCurrentIndex(0); // since mSourceCombo->currentItem() == 0
}
SignatureConfigurator::SignatureConfigurator(QWidget *parent)
: QWidget(parent)
, d(new SignatureConfiguratorPrivate(this))
{
d->init();
}
SignatureConfigurator::~SignatureConfigurator() = default;
bool SignatureConfigurator::isSignatureEnabled() const
{
return d->mEnableCheck->isChecked();
}
void SignatureConfigurator::setSignatureEnabled(bool enable)
{
d->mEnableCheck->setChecked(enable);
}
Signature::Type SignatureConfigurator::signatureType() const
{
return Signature::Inlined;
}
void SignatureConfigurator::setInlineText(const QString &text)
{
d->mTextEdit->setText(text);
}
Signature SignatureConfigurator::signature() const
{
Signature sig;
const Signature::Type sigType = signatureType();
switch (sigType) {
case Signature::Inlined:
sig.setText(d->mTextEdit->toPlainText());
break;
case Signature::Disabled:
/* do nothing */
break;
}
sig.setEnabledSignature(isSignatureEnabled());
sig.setType(sigType);
return sig;
}
void SignatureConfigurator::setSignature(const Signature &sig)
{
setSignatureEnabled(sig.isEnabledSignature());
// Let insertIntoTextEdit() handle setting the text, as that function also adds the images.
d->mTextEdit->clear();
SignatureRichTextEditor::insertIntoTextEdit(sig, Signature::Start, Signature::AddNothing, d->mTextEdit, true);
}
}
#include "moc_signatureconfigurator.cpp"
diff --git a/client/rootcagenerator/CMakeLists.txt b/client/rootcagenerator/CMakeLists.txt
index 7953b04..4c20650 100644
--- a/client/rootcagenerator/CMakeLists.txt
+++ b/client/rootcagenerator/CMakeLists.txt
@@ -1,35 +1,32 @@
# SPDX-FileCopyrightText: 2024 g10 code GmbH
# SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
# SPDX-License-Identifier: BSD-2-Clause
add_library(rootcagenerator STATIC)
target_sources(rootcagenerator PRIVATE
controller.cpp
controller.h
truststore.cpp
truststore.h
)
if (WIN32)
install(FILES install.ps1 DESTINATION ${KDE_INSTALL_BINDIR})
else()
target_link_libraries(rootcagenerator PRIVATE KF6::AuthCore)
add_executable(truststore_linux_helper
truststore_linux_helper.cpp
truststore_linux_helper.h
)
target_link_libraries(truststore_linux_helper KF6::I18n KF6::AuthCore Qt::Core)
install(TARGETS truststore_linux_helper DESTINATION ${KAUTH_HELPER_INSTALL_DIR})
kauth_install_actions(com.gnupg.gpgolweb.truststore com.gnupg.gpgolweb.truststore.actions)
kauth_install_helper_files(truststore_linux_helper com.gnupg.gpgolweb.truststore root)
endif()
target_link_libraries(rootcagenerator PUBLIC KPim6::Libkleo KF6::I18n KF6::CoreAddons)
-add_executable(gpgol-cert-generator main.cpp)
-target_link_libraries(gpgol-cert-generator PUBLIC rootcagenerator)
-install(TARGETS gpgol-cert-generator ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
diff --git a/client/rootcagenerator/controller.cpp b/client/rootcagenerator/controller.cpp
index 7553282..f2b4b93 100644
--- a/client/rootcagenerator/controller.cpp
+++ b/client/rootcagenerator/controller.cpp
@@ -1,415 +1,399 @@
// SPDX-FileCopyrightText: 2023 g10 code GmbH
// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "controller.h"
#include "truststore.h"
+#include "../gpgol_client_debug.h"
+#include "../../common/paths.h"
#include "../utils/formatter.h"
#include <QDate>
#include <QDir>
#include <QProcess>
#include <QSaveFile>
-#include <QStandardPaths>
#include <QTemporaryDir>
#include <QGpgME/ExportJob>
#include <QGpgME/ImportJob>
#include <QGpgME/KeyGenerationJob>
#include <QGpgME/KeyListJob>
#include <QGpgME/Protocol>
#include <gpgme++/context.h>
#include <Libkleo/Formatting>
#include <Libkleo/KeyParameters>
#include <Libkleo/KeyUsage>
#include <KLocalizedString>
using namespace Qt::StringLiterals;
using namespace Kleo;
using namespace GpgME;
static KeyParameters createRootCaParms()
{
KeyParameters keyParameters(KeyParameters::CMS);
keyParameters.setKeyType(GpgME::Subkey::PubkeyAlgo::AlgoRSA);
keyParameters.setKeyUsage(KeyUsage{KeyUsage::Sign | KeyUsage::Certify});
keyParameters.setDN(u"CN=GnuPG Outlook Add-in Local Root CA"_s);
keyParameters.setEmail(u"localroot@gpgoljs.local"_s);
keyParameters.setKeyLength(3072);
keyParameters.setExpirationDate(QDate(2060, 10, 10));
keyParameters.setUseRandomSerial();
keyParameters.setControlStatements({u"%no-protection"_s});
return keyParameters;
}
static KeyParameters createTlsCertParms(QLatin1StringView keyGrip)
{
KeyParameters keyParameters(KeyParameters::CMS);
keyParameters.setKeyType(GpgME::Subkey::PubkeyAlgo::AlgoRSA);
keyParameters.setKeyUsage(KeyUsage{KeyUsage::Sign | KeyUsage::Encrypt});
keyParameters.setDN(u"CN=GnuPG Outlook Add-in Local Server Certificate"_s);
keyParameters.setEmail(u"local@gpgoljs.local"_s);
keyParameters.setKeyLength(3072);
keyParameters.setExpirationDate(QDate(2060, 10, 10));
keyParameters.setIssuerDN(u"CN=GnuPG Outlook Add-in Local Root CA"_s);
keyParameters.setSigningKey(keyGrip);
keyParameters.setUseRandomSerial();
keyParameters.addDomainName(u"localhost"_s);
keyParameters.setControlStatements({u"%no-protection"_s});
return keyParameters;
}
Controller::Controller(QObject *parent)
: KJob(parent)
{
}
Controller::~Controller()
{
if (m_tmpdir) {
QProcess p;
p.start(u"gpgconf"_s, {u"--homedir"_s, m_tmpdir->path(), u"--kill"_s, u"all"_s});
p.waitForFinished();
}
}
QString Controller::caUniqueName() const
{
return u"GPGOL2 CA "_s + QString::fromLatin1(m_ca.issuerSerial());
}
QByteArray Controller::caCert() const
{
return m_publicCA;
}
bool Controller::certificateAlreadyGenerated()
{
- auto certPath = QStandardPaths::locate(QStandardPaths::AppLocalDataLocation, QStringLiteral("certificate.pem"));
+ if (!(CommonPaths::locateFile(u"certificate.pem"_s).isEmpty())) {
+ return true;
+ }
+
+ // Copy from legacy path, if installed, there.
+ // TODO: remove this after grace period
+ // NOTE: copy certiface.pem last: Only if all three files were copied successfully, we shall return true below
+ const QStringList files{ u"certificate-key.pem"_s, u"root-ca.pem"_s, u"certificate.pem"_s };
+ const auto modern_path = CommonPaths::writeablePath();
+ QDir().mkpath(modern_path);
+ for (const auto & file : files) {
+ const auto legacy_path = CommonPaths::locateFileInLegacyPath(file);
+ if (!legacy_path.isEmpty() && QFile::exists(legacy_path)) {
+ qCWarning(GPGOL_CLIENT_LOG) << "Copying from legacy path:" << legacy_path;
+ if (!QFile::copy(legacy_path, QString(modern_path + u"/"_s + file))) {
+ qCWarning(GPGOL_CLIENT_LOG) << "Copying from legacy path failed";
+ return false;
+ }
+ }
+ }
- return !certPath.isEmpty();
+ return !(CommonPaths::locateFile(u"certificate.pem"_s).isEmpty());
}
void Controller::setHomeDirForJob(QGpgME::Job *job)
{
auto context = QGpgME::Job::context(job);
context->setEngineHomeDirectory(m_tmpdir->path().toUtf8().constData());
}
void Controller::start()
{
if (certificateAlreadyGenerated()) {
emitResult();
return;
}
m_tmpdir = std::make_unique<QTemporaryDir>();
auto keyGenerationJob = QGpgME::smime()->keyGenerationJob();
setHomeDirForJob(keyGenerationJob);
connect(keyGenerationJob, &QGpgME::KeyGenerationJob::result, this, &Controller::slotRootCaCreatedSlot);
keyGenerationJob->start(createRootCaParms().toString());
}
void Controller::slotRootCaCreatedSlot(const GpgME::KeyGenerationResult &result, const QByteArray &pubKeyData, const QString &auditLog)
{
Q_UNUSED(auditLog)
if (result.error().code()) {
setErrorText(result.error().isCanceled() ? i18n("Operation canceled.")
: i18n("Could not create key pair: %1", Formatting::errorAsString(result.error())));
setError(UserDefinedError);
emitResult();
return;
}
auto importJob = QGpgME::smime()->importJob();
setHomeDirForJob(importJob);
connect(importJob, &QGpgME::ImportJob::result, this, &Controller::slotRootCaImportedSlot);
importJob->start(pubKeyData);
}
void Controller::slotRootCaImportedSlot(const GpgME::ImportResult &result, const QString &auditLogAsHtml, const GpgME::Error &auditLogError)
{
Q_UNUSED(auditLogAsHtml)
Q_UNUSED(auditLogError)
Q_EMIT debutOutput(i18nc("Debug message", "Imported root CA"));
if (result.error().code()) {
setErrorText(result.error().isCanceled() ? i18n("Operation canceled.")
: i18n("Could not create key pair: %1", Formatting::errorAsString(result.error())));
setError(UserDefinedError);
emitResult();
return;
}
// Get the keygrip
auto keyListJob = QGpgME::smime()->keyListJob();
setHomeDirForJob(keyListJob);
connect(keyListJob, &QGpgME::KeyListJob::result, this, &Controller::slotKeyGripOptained);
keyListJob->start({u"GnuPG Outlook Add-in Local Root CA"_s}, true);
// Export public key
auto exportJob = QGpgME::smime()->publicKeyExportJob(true);
setHomeDirForJob(exportJob);
const auto imports = result.imports();
const auto fingerprint = imports[0].fingerprint();
m_fingerPrint = Formatter::formatX509Fingerprint(QByteArray(fingerprint));
Q_EMIT debutOutput(i18nc("Debug message, %1 is fingerprint", "Root CA created: %1", m_fingerPrint));
exportJob->start({QString::fromLatin1(fingerprint)});
connect(exportJob, &QGpgME::ExportJob::result, this, [this](const GpgME::Error &error, const QByteArray &keyData) {
if (error.code()) {
setErrorText(error.isCanceled() ? i18n("Operation canceled.") : i18n("Could not export public key: %1", Formatting::errorAsString(error)));
setError(UserDefinedError);
emitResult();
return;
}
m_publicCA = keyData;
checkFinished();
});
// Export private key
auto exportSecretJob = QGpgME::smime()->secretKeyExportJob(true);
setHomeDirForJob(exportSecretJob);
exportSecretJob->start({QString::fromLatin1(fingerprint)});
connect(exportSecretJob, &QGpgME::ExportJob::result, this, [this](const GpgME::Error &error, const QByteArray &keyData) {
if (error.code()) {
setErrorText(error.isCanceled() ? i18n("Operation canceled.") : i18n("Could not export secret key: %1", Formatting::errorAsString(error)));
setError(UserDefinedError);
emitResult();
return;
}
m_secretCA = keyData;
checkFinished();
});
}
void Controller::slotKeyGripOptained(const GpgME::KeyListResult &result,
const std::vector<GpgME::Key> &keys,
const QString &auditLogAsHtml,
const GpgME::Error &auditLogError)
{
Q_EMIT debutOutput(i18nc("Debug message", "Got the key grip of Root CA"));
Q_UNUSED(auditLogAsHtml)
Q_UNUSED(auditLogError)
if (result.error().code()) {
setErrorText(result.error().isCanceled() ? i18n("Operation canceled.") : i18n("Could not get keygrip : %1", Formatting::errorAsString(result.error())));
setError(UserDefinedError);
emitResult();
return;
}
if (keys.size() != 1) {
setErrorText(i18n("More than one root certificate found"));
setError(UserDefinedError);
emitResult();
return;
}
m_ca = keys[0];
auto keyGenerationJob = QGpgME::smime()->keyGenerationJob();
setHomeDirForJob(keyGenerationJob);
connect(keyGenerationJob, &QGpgME::KeyGenerationJob::result, this, &Controller::slotCertCreatedSlot);
keyGenerationJob->start(createTlsCertParms(QLatin1StringView(keys[0].subkey(0).keyGrip())).toString());
}
void Controller::slotCertCreatedSlot(const GpgME::KeyGenerationResult &result, const QByteArray &pubKeyData, const QString &auditLog)
{
Q_EMIT debutOutput(i18nc("Debug message", "TLS certificate created"));
Q_UNUSED(auditLog)
if (result.error().code()) {
setErrorText(result.error().isCanceled() ? i18n("Operation canceled.")
: i18n("Could not create key pair for cert: %1", Formatting::errorAsString(result.error())));
setError(UserDefinedError);
emitResult();
return;
}
auto importJob = QGpgME::smime()->importJob();
setHomeDirForJob(importJob);
connect(importJob, &QGpgME::ImportJob::result, this, &Controller::slotCertImportedSlot);
importJob->start(pubKeyData);
}
void Controller::slotCertImportedSlot(const GpgME::ImportResult &result, const QString &auditLogAsHtml, const GpgME::Error &auditLogError)
{
Q_UNUSED(auditLogAsHtml)
Q_UNUSED(auditLogError)
if (result.error().code()) {
setErrorText(result.error().isCanceled() ? i18n("Operation canceled.") : i18n("Could not import cert: %1", Formatting::errorAsString(result.error())));
setError(UserDefinedError);
emitResult();
return;
}
auto keyListJob = QGpgME::smime()->keyListJob();
setHomeDirForJob(keyListJob);
connect(keyListJob,
&QGpgME::KeyListJob::result,
this,
[this](const GpgME::KeyListResult &result, const std::vector<GpgME::Key> &keys, const QString &auditLogAsHtml, const GpgME::Error &auditLogError) {
Q_UNUSED(result);
Q_UNUSED(auditLogAsHtml);
Q_UNUSED(auditLogError);
m_tls = keys[0];
checkFinished();
});
keyListJob->start({u"GnuPG Outlook Add-in Local Root CA"_s}, true);
// Export public key
auto exportJob = QGpgME::smime()->publicKeyExportJob(true);
setHomeDirForJob(exportJob);
const auto imports = result.imports();
const auto fingerprint = imports[0].fingerprint();
exportJob->start({QString::fromLatin1(fingerprint)});
connect(exportJob, &QGpgME::ExportJob::result, this, [this](const GpgME::Error &error, const QByteArray &keyData) {
if (error.code()) {
setErrorText(error.isCanceled() ? i18n("Operation canceled.") : i18n("Could not export public key: %1", Formatting::errorAsString(error)));
setError(UserDefinedError);
emitResult();
return;
}
m_publicTLS = keyData;
checkFinished();
});
// Export private key
auto exportSecretJob = QGpgME::smime()->secretKeyExportJob(true);
setHomeDirForJob(exportSecretJob);
exportSecretJob->start({QString::fromLatin1(fingerprint)});
connect(exportSecretJob, &QGpgME::ExportJob::result, this, [this](const GpgME::Error &error, const QByteArray &keyData) {
if (error.code()) {
setErrorText(error.isCanceled() ? i18n("Operation canceled.") : i18n("Could not export secret key: %1", Formatting::errorAsString(error)));
setError(UserDefinedError);
emitResult();
return;
}
m_secretTLS = keyData;
checkFinished();
});
}
void Controller::checkFinished()
{
if (!m_secretCA.isEmpty() && !m_publicCA.isEmpty() && !m_publicTLS.isEmpty() && !m_secretTLS.isEmpty() && !m_ca.isNull() && !m_tls.isNull()) {
Q_EMIT generationDone();
}
}
void Controller::install()
{
- auto keyPath = QStandardPaths::locate(QStandardPaths::AppLocalDataLocation, QStringLiteral("certificate-key.pem"));
-
// Install for gpgol-client
{
- auto certPath = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation);
+ auto certPath = CommonPaths::writeablePath();
Q_EMIT debutOutput(i18nc("Debug message, %1 is a path", "Installing certificate for gpgol-client in %1", certPath));
QDir dir;
if (!dir.mkpath(certPath)) {
Q_EMIT debutOutput(i18nc("Debug message, %1 is a path", "Unable to create the following path: ", certPath));
setError(UserDefinedError);
emitResult();
return;
}
QSaveFile localhostPub(certPath + u"/certificate.pem"_s);
if (localhostPub.open(QIODeviceBase::WriteOnly)) {
localhostPub.write(m_publicTLS);
localhostPub.commit();
} else {
Q_EMIT debutOutput(
i18nc("Debug message. %1 is a filename. %2 is a path", "No permission to write: %1 in %2", localhostPub.fileName(), dir.absolutePath()));
setError(UserDefinedError);
emitResult();
return;
}
QSaveFile rootCaPub(certPath + u"/root-ca.pem"_s);
if (rootCaPub.open(QIODeviceBase::WriteOnly)) {
rootCaPub.write(m_publicCA);
rootCaPub.commit();
} else {
Q_EMIT debutOutput(
i18nc("Debug message. %1 is a filename. %2 is a path", "No permission to write: %1 in %2", rootCaPub.fileName(), dir.absolutePath()));
setError(UserDefinedError);
emitResult();
return;
}
- }
-
- // Install for gpgol-server
- {
- auto certPath = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation).chopped(QStringLiteral(u"gpgol-client").length()).append(u"gpgol-server");
- Q_EMIT debutOutput(i18nc("Debug message. %1 is a path", "Installing certificate for gpgol-server in %1", certPath));
-
- QDir dir;
- if (!dir.mkpath(certPath)) {
- Q_EMIT debutOutput(i18nc("Debug message. %1 is a path", "Unable to create the following path: %1", certPath));
- setError(UserDefinedError);
- emitResult();
- return;
- }
-
- QSaveFile localhostPub(certPath + u"/certificate.pem"_s);
- if (localhostPub.open(QIODeviceBase::WriteOnly)) {
- localhostPub.write(m_publicTLS);
- localhostPub.commit();
- } else {
- Q_EMIT debutOutput(
- i18nc("Debug message, %1 is a filename. %2 is a path", "No permission to write: %1 in %2", localhostPub.fileName(), dir.absolutePath()));
- setError(UserDefinedError);
- emitResult();
- return;
- }
- QSaveFile localhostKey(certPath + u"/certificate-key.pem"_s);
- if (localhostKey.open(QIODeviceBase::WriteOnly)) {
- localhostKey.write(m_secretTLS);
- localhostKey.commit();
- } else {
- Q_EMIT debutOutput(
- i18nc("Debug message. %1 is a filename. %2 is a path.", "No permission to write: %1 in %2", localhostKey.fileName(), dir.absolutePath()));
- setError(UserDefinedError);
- emitResult();
- return;
- }
- QSaveFile rootCaPub(certPath + u"/root-ca.pem"_s);
- if (rootCaPub.open(QIODeviceBase::WriteOnly)) {
- rootCaPub.write(m_publicCA);
- rootCaPub.commit();
- } else {
- Q_EMIT debutOutput(
- i18nc("Debug message. %1 is a filename. %2 is a path", "No permission to write: %1 in %2", rootCaPub.fileName(), dir.absolutePath()));
- setError(UserDefinedError);
- emitResult();
- return;
+ // This one needed for the server, only
+ {
+ QSaveFile localhostKey(certPath + u"/certificate-key.pem"_s);
+ if (localhostKey.open(QIODeviceBase::WriteOnly)) {
+ localhostKey.write(m_secretTLS);
+ localhostKey.commit();
+ } else {
+ Q_EMIT debutOutput(
+ i18nc("Debug message. %1 is a filename. %2 is a path.", "No permission to write: %1 in %2", localhostKey.fileName(), dir.absolutePath()));
+ setError(UserDefinedError);
+ emitResult();
+ return;
+ }
}
}
auto trustStore = TrustStore();
if (!trustStore.install(*this)) {
Q_EMIT debutOutput(i18nc("Debug message", "Installing certificate to browser failed"));
}
emitResult();
}
QString Controller::rootFingerprint() const
{
return m_fingerPrint;
}
diff --git a/client/rootcagenerator/main.cpp b/client/rootcagenerator/main.cpp
deleted file mode 100644
index 6d6c9e8..0000000
--- a/client/rootcagenerator/main.cpp
+++ /dev/null
@@ -1,63 +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 <QCommandLineParser>
-#include <QCoreApplication>
-#include <QTemporaryDir>
-
-#include <KLocalizedString>
-
-#include "controller.h"
-#include <iostream>
-
-#ifdef Q_OS_WINDOWS
-#include <windows.h>
-#endif
-
-using namespace Qt::StringLiterals;
-
-int main(int argc, char *argv[])
-{
- QCoreApplication app(argc, argv);
- app.setApplicationName(QStringLiteral("GpgOL/Web cert generator"));
- app.setApplicationVersion(QStringLiteral("0.0.1"));
- app.setOrganizationName(QStringLiteral("g10code"));
- app.setOrganizationDomain(QStringLiteral("gnupg.com"));
- KLocalizedString::setApplicationDomain(QByteArrayLiteral("gpgol"));
- QCommandLineParser parser;
-
- auto helpOpt = parser.addHelpOption();
- parser.setApplicationDescription(i18n("Helper application to generate certificates for GgpOL/Web\nWithout any arguments, a certificate set is generated"));
-
- parser.process(app);
- if (parser.isSet(helpOpt)) {
- std::cout << qPrintable(parser.helpText());
- return 0;
- }
- QTemporaryDir tempDir;
- tempDir.setAutoRemove(false);
-
-#ifdef Q_OS_WINDOWS
- if (AttachConsole(ATTACH_PARENT_PROCESS)) {
- freopen("CONOUT$", "w", stdout);
- freopen("CONOUT$", "w", stderr);
- }
-#endif
-
- qWarning() << tempDir.path();
-
- qputenv("GNUPGHOME", tempDir.path().toUtf8());
-
- auto controller = new Controller;
- QObject::connect(controller, &KJob::finished, &app, [&app](KJob *job) {
- if (job->error()) {
- app.exit(1);
- } else {
- app.exit(0);
- }
- });
- controller->start();
-
- return app.exec();
-}
\ No newline at end of file
diff --git a/client/websocketclient.cpp b/client/websocketclient.cpp
index 42e3e21..8d2e85a 100644
--- a/client/websocketclient.cpp
+++ b/client/websocketclient.cpp
@@ -1,458 +1,458 @@
// 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 <QHostInfo>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
-#include <QStandardPaths>
#include <QTimer>
#include <QUuid>
// KDE headers
#include <KConfigGroup>
#include <KLocalizedString>
#include <KMime/Message>
#include <KSharedConfig>
#include <Libkleo/Formatting>
#include <Libkleo/KeyCache>
#include <MimeTreeParserCore/ObjectTreeParser>
// gpgme headers
#include <QGpgME/KeyListJob>
#include <QGpgME/Protocol>
#include <gpgme++/global.h>
#include <gpgme++/key.h>
#include <gpgme++/keylistresult.h>
+#include "../common/paths.h"
#include "config.h"
#include "draft/draftmanager.h"
#include "editor/composerwindow.h"
#include "editor/composerwindowfactory.h"
#include "emailviewer.h"
#include "firsttimedialog.h"
#include "gpgol_client_debug.h"
#include "gpgolweb_version.h"
#include "mailapi.h"
#include "protocol.h"
#include "reencrypt/reencryptjob.h"
#include "websocket_debug.h"
using namespace Qt::Literals::StringLiterals;
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, clientId);
}
return *client;
};
WebsocketClient::WebsocketClient(const QUrl &url, const QString &clientId)
: QObject(nullptr)
, m_webSocket(QWebSocket(QStringLiteral("Client")))
, m_url(url)
, m_clientId(clientId)
, m_state(NotConnected)
, m_stateDisplay(i18nc("@info", "Loading..."))
{
auto job = QGpgME::openpgp()->keyListJob();
connect(job, &QGpgME::KeyListJob::result, this, &WebsocketClient::slotKeyListingDone);
job->start({}, true);
qCWarning(GPGOL_CLIENT_LOG) << "Found the following trusted emails" << m_emails;
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(m_stateDisplay);
});
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"));
+ auto certPath = CommonPaths::locateFile(u"certificate.pem"_s);
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);
}
void WebsocketClient::slotKeyListingDone(const GpgME::KeyListResult &result, const std::vector<GpgME::Key> &keys, const QString &, const GpgME::Error &error)
{
Q_UNUSED(result);
Q_UNUSED(error);
if (error) {
m_stateDisplay = i18nc("@info", "Connection to the Outlook extension lost. Make sure the extension pane is open.");
Q_EMIT stateChanged(m_stateDisplay);
return;
}
QStringList oldEmails = m_emails;
for (const auto &key : keys) {
for (const auto &userId : key.userIDs()) {
const auto email = QString::fromLatin1(userId.email()).toLower();
if (!m_emails.contains(email)) {
m_emails << email;
}
}
}
if (m_emails == oldEmails) {
return;
}
qCWarning(GPGOL_CLIENT_LOG) << "Found the following trusted emails" << m_emails;
if (m_webSocket.state() == QAbstractSocket::ConnectedState) {
slotConnected();
}
}
void WebsocketClient::slotConnected()
{
qCInfo(WEBSOCKET_LOG) << "websocket connected";
sendCommand(Protocol::Register, QJsonObject{
{"emails"_L1, QJsonArray::fromStringList(m_emails)}, // TODO: keep this?
{"type"_L1, "native"_L1},
{"name"_L1, QString(QHostInfo::localHostName() + u" - GpgOL/Web ("_s + QStringLiteral(GPGOLWEB_VERSION_STRING) + u')') }, // TODO: unused
});
sendStatusUpdate(); // in case web client was started before native client
m_state = ConnectedToProxy; /// We still need to connect to the web client
m_stateDisplay = i18nc("@info", "Waiting for web client.");
Q_EMIT stateChanged(m_stateDisplay);
}
void WebsocketClient::slotErrorOccurred(QAbstractSocket::SocketError error)
{
qCWarning(WEBSOCKET_LOG) << error << m_webSocket.errorString();
m_state = (m_webSocket.state() == QAbstractSocket::ConnectedState) ? ConnectedToProxy : NotConnected;
m_stateDisplay = i18nc("@info", "Could not reach the Outlook extension.");
Q_EMIT stateChanged(m_stateDisplay);
reconnect();
}
void WebsocketClient::enterPairingMode()
{
sendCommand(Protocol::PairingRequest, QJsonObject{
{"type"_L1, "native-start-pairing"_L1},
});
}
void WebsocketClient::quitPairingMode()
{
sendCommand(Protocol::PairingRequest, QJsonObject{
{"type"_L1, "native-end-pairing"_L1},
});
}
bool WebsocketClient::sendEWSRequest(const QString &fromEmail, const QString &requestId, const QString &requestBody)
{
KMime::Types::Mailbox mailbox;
mailbox.fromUnicodeString(fromEmail);
sendCommand(Protocol::Ews, QJsonObject{
{"body"_L1, requestBody},
{"email"_L1, QString::fromUtf8(mailbox.address())},
{"requestId"_L1, requestId}
});
return true;
}
// TODO: We should really centralize all calls to this as a single call in the connection stage. Afterwards
// the webclient will not send a new token, anyway.
// However, fixing this is currently on hold pending changes in the pairing process,
// so for now, calls to this are littered all around various requests, but at least we can indentify them
// easily as calls to this function.
void WebsocketClient::initMailApiFromArgs(const QJsonObject &args)
{
MailApiController::init(
(args[u"api"_s].toString() == u"ews"_s) ? MailApiController::EWSApi : MailApiController::GraphApi,
args[u"apiendpoint"_s].toString(),
args[u"ewsAccessToken"_s].toString()
);
}
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 = ConnectedToProxy;
m_stateDisplay = i18nc("@info", "Connection to the Outlook extension lost. Make sure the extension pane is open.");
Q_EMIT stateChanged(m_stateDisplay);
// TODO: handle multiple clients
return;
case Protocol::PairingRequest: {
const auto token = args["token"_L1].toString();
if (token.isEmpty()) {
Q_EMIT pairingStatusChanged(QString(), false);
qCWarning(GPGOL_CLIENT_LOG) << "Pairing complete";
} else {
Q_EMIT pairingStatusChanged(token, true);
}
return;
}
case Protocol::Connection:
// reconnection of the web client
m_state = ConnectedToWebclient;
m_stateDisplay = i18nc("@info", "Connected.");
Q_EMIT stateChanged(m_stateDisplay);
sendStatusUpdate();
return;
case Protocol::View: {
const auto email = args["email"_L1].toString();
const auto displayName = args["displayName"_L1].toString();
const auto id = args["itemId"_L1].toString();
const auto content = m_cachedMime[id];
if (content.isEmpty()) {
return;
}
initMailApiFromArgs(args);
if (!m_emailViewer) {
m_emailViewer = new EmailViewer(QString::fromUtf8(content), email, displayName);
m_emailViewer->setAttribute(Qt::WA_DeleteOnClose);
} else {
m_emailViewer->view(QString::fromUtf8(content), email, displayName);
}
FirstTimeDialog::strongActivateWindow(m_emailViewer);
return;
}
case Protocol::RestoreAutosave: {
const auto email = args["email"_L1].toString();
const auto displayName = args["displayName"_L1].toString();
initMailApiFromArgs(args);
ComposerWindowFactory::self().restoreAutosave(email, displayName);
return;
}
case Protocol::EwsResponse: {
// confirmation that the email was sent
const auto args = object["arguments"_L1].toObject();
Q_EMIT ewsResponseReceived(args["requestId"_L1].toString(), args["body"_L1].toString());
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();
initMailApiFromArgs(args);
auto dialog = ComposerWindowFactory::self().create(email, displayName);
if (command == Protocol::Reply || command == Protocol::Forward) {
const auto id = args["itemId"_L1].toString();
const auto content = m_cachedMime[id];
if (content.isEmpty()) {
return;
}
KMime::Message::Ptr message(new KMime::Message());
message->setContent(KMime::CRLFtoLF(content));
message->parse();
if (command == Protocol::Reply) {
dialog->reply(message);
} else {
dialog->forward(message);
}
} else if (command == Protocol::OpenDraft) {
const auto draftId = args["draftId"_L1].toString();
if (draftId.isEmpty()) {
return;
}
const auto draft = DraftManager::self().draftById(draftId.toUtf8());
dialog->setMessage(draft.mime());
}
FirstTimeDialog::strongActivateWindow(dialog);
return;
}
case Protocol::DeleteDraft: {
const auto draftId = args["draftId"_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)) {
qCWarning(GPGOL_CLIENT_LOG) << "Could not delete draft";
return;
}
sendStatusUpdate();
return;
}
case Protocol::Reencrypt: {
initMailApiFromArgs(args);
reencrypt(args);
return;
}
case Protocol::Info: {
initMailApiFromArgs(args);
info(args);
return;
}
default:
qCWarning(WEBSOCKET_LOG) << "Unhandled command" << command;
}
}
void WebsocketClient::reencrypt(const QJsonObject &args)
{
if (m_reencryptJob) {
if (m_reencryptJob->hasStarted()) {
m_reencryptJob->tryRaiseDialog();
return;
}
m_reencryptJob->deleteLater();
}
// TODO: Looking up the folderId by itemId would allow some simplification in the web.js
m_reencryptJob = new ReencryptJob(args["folderId"_L1].toString());
m_reencryptJob->start();
}
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;
}
void WebsocketClient::sendCommand(Protocol::Command command, const QJsonObject &arguments)
{
const auto json = Protocol::makeCommand(command, arguments, getId());
m_webSocket.sendTextMessage(QString::fromUtf8(QJsonDocument(json).toJson()));
}
void WebsocketClient::sendStatusUpdate(bool viewerJustClosed)
{
QJsonArray features;
if (Config::self()->reencrypt()) {
features << u"reencrypt"_s;
}
sendCommand(Protocol::StatusUpdate, QJsonObject{
{"drafts"_L1, DraftManager::self().toJson()},
{"viewerOpen"_L1, !viewerJustClosed && !m_emailViewer.isNull()},
{"features"_L1, features}
});
}
void WebsocketClient::info(const QJsonObject &args)
{
const auto email = args["email"_L1].toString();
sendStatusUpdate(false); // web client expects to know that info before info-fetched
const QString id(args["itemId"_L1].toString());
qCWarning(GPGOL_CLIENT_LOG) << "Info for" << id << "requested";
if (m_cachedInfo.contains(id)) {
sendCommand(Protocol::InfoFetched, m_cachedInfo[id]);
return;
}
MailApiController::self().setAccessToken(args["ewsAccessToken"_L1].toString());
auto request = MailApiController::self().getMails({id}, GetMailsJob::GetMimeContent | GetMailsJob::GetParentFolderId);
connect(request, &GetMailsJob::finished, this, [this, id, args, request]() {
if (request->error() != KJob::NoError) {
sendCommand(Protocol::Error, QJsonObject{{"error"_L1, request->errorString()}});
qCWarning(GPGOL_CLIENT_LOG) << "Failure to get mail:" << request->errorText();
return;
}
qCWarning(GPGOL_CLIENT_LOG) << "Info for" << id << "fetched";
const auto responses = request->takeResponses();
if (responses.isEmpty()) {
return;
}
const auto item = responses.first();
const auto mimeContent = GetMailsJob::mimeContent(item);
KMime::Message::Ptr message(new KMime::Message());
message->setContent(KMime::CRLFtoLF(mimeContent));
message->parse();
MimeTreeParser::ObjectTreeParser treeParser;
treeParser.parseObjectTree(message.get());
const auto data = QJsonObject{
{"itemId"_L1, args["itemId"_L1]},
{"folderId"_L1, GetMailsJob::parentFolderId(item)},
{"email"_L1, args["email"_L1]},
{"encrypted"_L1, treeParser.hasEncryptedParts()},
{"signed"_L1, treeParser.hasSignedParts()},
{"version"_L1, QStringLiteral(GPGOLWEB_VERSION_STRING)},
};
m_cachedInfo[id] = data;
m_cachedMime[id] = mimeContent;
sendCommand(Protocol::InfoFetched, data);
});
request->start();
}
QString WebsocketClient::getId() const
{
auto config = KSharedConfig::openStateConfig();
auto machineGroup = config->group(u"Machine"_s);
if (machineGroup.exists() && machineGroup.hasKey(u"Id"_s)) {
return machineGroup.readEntry(u"Id"_s);
}
const auto id = QUuid::createUuid().toString(QUuid::WithoutBraces);
machineGroup.writeEntry("Id", id);
config->sync();
return id;
}
diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt
index 864f728..0a6f1ce 100644
--- a/common/CMakeLists.txt
+++ b/common/CMakeLists.txt
@@ -1,14 +1,16 @@
# SPDX-FileCopyrightText: 2023 g10 code Gmbh
# SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
# SPDX-License-Identifier: BSD-2-Clause
add_library(common STATIC
log.cpp
log.h
+ paths.cpp
+ paths.h
protocol.cpp
protocol.h
utils.cpp
utils.h
)
target_link_libraries(common PRIVATE Qt6::Core Qt6::Network KF6::CoreAddons KF6::I18n)
diff --git a/common/paths.cpp b/common/paths.cpp
new file mode 100644
index 0000000..4017709
--- /dev/null
+++ b/common/paths.cpp
@@ -0,0 +1,21 @@
+// SPDX-FileCopyrightText: 2025 g10 code GmbH
+// SPDX-Contributor: Thomas Friedrichsmeier <thomas.friedrichsmeier@gnupg.com>
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <QStandardPaths>
+
+#include "paths.h"
+// replacement for QStandardsPaths::locate(), look up file in the data path shared between client and server
+QString CommonPaths::locateFile(const QString &file) {
+ return QStandardPaths::locate(QStandardPaths::GenericDataLocation, u"gpgol-web/" + file);
+}
+
+QString CommonPaths::writeablePath() {
+ return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + u"/gpgol-web";
+}
+
+QString CommonPaths::locateFileInLegacyPath(const QString &file) {
+ const auto ret = QStandardPaths::locate(QStandardPaths::GenericDataLocation, u"gpgol-client/" + file);
+ if (!ret.isEmpty()) return ret;
+ return QStandardPaths::locate(QStandardPaths::GenericDataLocation, u"gpgol-server/" + file);
+}
diff --git a/common/paths.h b/common/paths.h
new file mode 100644
index 0000000..8076edf
--- /dev/null
+++ b/common/paths.h
@@ -0,0 +1,12 @@
+// SPDX-FileCopyrightText: 2025 g10 code GmbH
+// SPDX-Contributor: Thomas Friedrichsmeier <thomas.friedrichsmeier@gnupg.com>
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <QString>
+
+namespace CommonPaths
+{
+QString locateFile(const QString &file);
+QString locateFileInLegacyPath(const QString &file);
+QString writeablePath();
+}
diff --git a/server/controllers/staticcontroller.cpp b/server/controllers/staticcontroller.cpp
index 49e07ba..aee9125 100644
--- a/server/controllers/staticcontroller.cpp
+++ b/server/controllers/staticcontroller.cpp
@@ -1,67 +1,66 @@
// SPDX-FileCopyrightText: 2023 g10 code GmbH
// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "staticcontroller.h"
#include "http_debug.h"
#include <QDebug>
#include <QFile>
#include <QStandardPaths>
using namespace Qt::Literals::StringLiterals;
QHttpServerResponse StaticController::homeAction(const QHttpServerRequest &)
{
- const auto basePath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, u"gpgol-web/"_s, QStandardPaths::LocateDirectory);
- QFile file(basePath + u"index.html"_s);
+ QFile file(QStandardPaths::locate(QStandardPaths::GenericDataLocation, u"gpgol-web/index.html"_s));
if (!file.open(QIODeviceBase::ReadOnly)) {
qCWarning(HTTP_LOG) << file.errorString() << file.fileName();
return QHttpServerResponse(QHttpServerResponder::StatusCode::NotFound);
}
return QHttpServerResponse("text/html", file.readAll());
}
QHttpServerResponse StaticController::testAction(const QHttpServerRequest &request)
{
#define ICON_OK "<span style=\"color:green\">&#10004;</span>"
#define ICON_WARN "<span style=\"color:orange\">&#9888;</span>"
#define ICON_ERROR "<span style=\"color:red\">&#10060;</span>"
if (request.url().scheme() == QStringLiteral("https")) {
return QHttpServerResponse("text/html", "<html><head></head><body>"
"<p style=\"margin-top: 3em; text-align:center\">" ICON_OK " HTTPS connectiont to GpgOL background service successful.<p>"
"<p style=\"margin-top: 3em; text-align:center\"id=\"socketstatus\">" ICON_WARN " Waiting for websocket connection to background service...<p>"
"<script>\n"
" socket = new WebSocket('wss://' + window.location.host + '/websocket');\n"
" socket.addEventListener('open', (event) => {\n"
" document.getElementById('socketstatus').innerHTML='" ICON_OK " Websocket connection to GpgOL background service successful.'\n"
" socket.value.send(JSON.stringify({ command: 'log', arguments: { message: 'Websocket connection from test page' } }));\n"
" });\n"
" socket.addEventListener('error', (event) => {\n"
" document.getElementById('socketstatus').innerHTML='" ICON_ERROR " Websocket connection to GpgOL background service failed.'\n"
" });\n"
"</script>\n"
"</body></html>");
}
return QHttpServerResponse("text/html", "<html><head></head><body><p style=\"margin-top: 3em; text-align:center\">" ICON_ERROR " Message recieved without https</body></html>");
}
QHttpServerResponse StaticController::assetsAction(QString fileName, const QHttpServerRequest &)
{
const auto basePath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, u"gpgol-web/assets/"_s, QStandardPaths::LocateDirectory);
QFile file(basePath + fileName);
if (!file.open(QIODeviceBase::ReadOnly)) {
qCWarning(HTTP_LOG) << file.errorString() << file.fileName();
return QHttpServerResponse(QHttpServerResponder::StatusCode::NotFound);
}
if (fileName.endsWith(u".png"_s)) {
return QHttpServerResponse("image/png", file.readAll());
} else if (fileName.endsWith(u".js"_s)) {
return QHttpServerResponse("text/javascript", file.readAll());
} else if (fileName.endsWith(u".css"_s)) {
return QHttpServerResponse("text/css", file.readAll());
}
return QHttpServerResponse("text/plain", file.readAll());
}
diff --git a/server/webserver.cpp b/server/webserver.cpp
index 4f019b5..12413d2 100644
--- a/server/webserver.cpp
+++ b/server/webserver.cpp
@@ -1,417 +1,417 @@
// 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 <QWebSocketCorsAuthenticator>
#include <QWebSocketServer>
#include <protocol.h>
#include <gpgme++/context.h>
#include <gpgme++/randomresults.h>
#include "controllers/staticcontroller.h"
+#include "../common/paths.h"
#include "http_debug.h"
#include "websocket_debug.h"
#include <KLocalizedString>
#define PAIRING_TOKEN_LENGTH 30
using namespace Qt::Literals::StringLiterals;
using namespace Protocol;
WebServer::WebServer(QObject *parent)
: QObject(parent)
, m_httpServer(new QHttpServer(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"));
- auto rootCertPath = QStandardPaths::locate(QStandardPaths::AppLocalDataLocation, QStringLiteral("root-ca.pem"));
+ auto keyPath = CommonPaths::locateFile(u"certificate-key.pem"_s);
+ auto certPath = CommonPaths::locateFile(u"certificate.pem"_s);
+ auto rootCertPath = CommonPaths::locateFile(u"root-ca.pem"_s);
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();
auto sslCertificateChain = QSslCertificate::fromPath(certPath);
if (sslCertificateChain.isEmpty()) {
qCFatal(HTTP_LOG) << u"Couldn't retrieve SSL certificate from file:"_s << certPath;
return false;
}
#if !defined(Q_OS_WINDOWS)
// bug in Qt 6.10.1, Windows only: SSL handshake fails, if we offer the full certificate chain, here.
// TODO: track this down in Qt. A mixup, which cert is which?
// For now just omit the root cert. It is installed in the system trust store, anyway.
sslCertificateChain.append(QSslCertificate::fromPath(rootCertPath));
#endif
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
m_httpServer->addWebSocketUpgradeVerifier(this, [](const QHttpServerRequest &request) {
Q_UNUSED(request);
if (request.url().path() == "/websocket"_L1) {
return QHttpServerWebSocketUpgradeResponse::accept();
} else {
return QHttpServerWebSocketUpgradeResponse::passToNext();
}
});
#else
// HAKC: ensure we handle the request so that in QHttpServerStream::handleReadyRead
// the request is updated to a websocket while still sending nothing so that we don't
// break the websocket clients
m_httpServer->route(u"/websocket"_s, [](const QHttpServerRequest &request, QHttpServerResponder &&responder) {
Q_UNUSED(request);
Q_UNUSED(responder);
});
#endif
// Static assets controller
m_httpServer->route(u"/home"_s, &StaticController::homeAction);
m_httpServer->route(u"/assets/"_s, &StaticController::assetsAction);
m_httpServer->route(u"/test"_s, &StaticController::testAction);
QSslConfiguration sslConfiguration;
sslConfiguration.setPeerVerifyMode(QSslSocket::VerifyNone);
sslConfiguration.setLocalCertificateChain(sslCertificateChain);
sslConfiguration.setPrivateKey(sslKey);
m_tcpserver = std::make_unique<QSslServer>();
m_tcpserver->setSslConfiguration(sslConfiguration);
if (!m_tcpserver->listen(QHostAddress::LocalHost, WebServer::Port)) {
qCFatal(HTTP_LOG) << "Server failed to listen on a port.";
return false;
}
// Note: Later versions of QHttpServer::bind returns a bool
// to check succes state. Though the only ways to return false
// is if tcpserver is nullpointer or if the tcpserver isn't listening
m_httpServer->bind(m_tcpserver.get());
quint16 port = m_tcpserver->serverPort();
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);
connect(m_httpServer, &QHttpServer::newWebSocketConnection, this, &WebServer::onNewConnection);
#if defined(Q_OS_WINDOWS) || QT_VERSION >= QT_VERSION_CHECK(6, 11, 0)
connect(m_httpServer, &QHttpServer::webSocketOriginAuthenticationRequired, this, [](QWebSocketCorsAuthenticator *authenticator) {
const auto origin = authenticator->origin();
// Only allow the gpgol-client and localhost:5656 to connect to this host
// Otherwise any tab from the browser is able to access the websockets
authenticator->setAllowed(origin == u"Client"_s || origin == u"https://localhost:5656"_s);
});
#endif
return true;
}
void WebServer::onNewConnection()
{
auto pSocket = m_httpServer->nextPendingWebSocketConnection();
if (!pSocket) {
return;
}
qCInfo(WEBSOCKET_LOG) << "Client connected:" << pSocket->peerName() << pSocket->origin() << pSocket->localAddress() << pSocket->localPort();
connect(pSocket.get(), &QWebSocket::textMessageReceived, this, &WebServer::processTextMessage);
connect(pSocket.get(), &QWebSocket::binaryMessageReceived, this, &WebServer::processBinaryMessage);
connect(pSocket.get(), &QWebSocket::disconnected, this, &WebServer::socketDisconnected);
// such that the socket will be deleted in the d'tor (unless socketDisconnected is called, earlier)
m_clients.add(pSocket.release());
}
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);
}
}
static QString nativeIdToPublicId(const QString &id)
{
//qCWarning(WEBSOCKET_LOG) << id << QCryptographicHash::hash(id.toUtf8().data(), QCryptographicHash::Sha256);
return QString::fromLatin1(QCryptographicHash::hash(id.toUtf8().data(), QCryptographicHash::Sha256).toHex());
}
static QJsonObject errorCommand(const QString &message)
{
return Protocol::makeCommand(Protocol::Error, QJsonObject{{"error"_L1, message}}, QString());
}
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" << object;
return;
}
const auto arguments = object["arguments"_L1].toObject();
const auto command = commandFromString(object["command"_L1].toString());
switch (command) {
// requests originating in proxy:
// case Command::Connection:
// requests originating in both native client and web client:
case Command::Register: {
const auto type = arguments["type"_L1].toString();
qInfo(WEBSOCKET_LOG) << "Register" << arguments;
if (type.isEmpty()) {
qCWarning(WEBSOCKET_LOG) << "Empty client type given when registering";
return;
}
if (type == "webclient"_L1) {
const auto email = arguments["email"_L1].toString();
if (email.isEmpty()) {
qCWarning(WEBSOCKET_LOG) << "Empty email given";
}
const auto id = object["id"_L1].toString();
if (id.isEmpty()) {
qWarning(WEBSOCKET_LOG) << "Unpaired web client registered. Waiting for pairing.";
return;
}
qCWarning(WEBSOCKET_LOG) << "Web client registered for connection id" << id << "and email" << email;
m_webClientsMappingToId[id] = socket; // TODO: Allow multiple webclients per native client
const auto command = Protocol::makeCommand(Protocol::Connection, QJsonObject{{"email"_L1, email}}, id);
sendMessageToNativeClient(id, command);
// NOTE: the native client will respond with a status update to the web client (if already connected)
} else {
const auto rawid = object["id"_L1].toString();
if (rawid.isEmpty()) {
qCWarning(WEBSOCKET_LOG) << "Attempt to register native client with empty id";
return;
}
const auto id = nativeIdToPublicId(rawid);
NativeClient nativeClient{
socket,
arguments["name"_L1].toString(),
id,
QString()
};
m_nativeClientsMappingToId[id] = nativeClient;
qCWarning(WEBSOCKET_LOG) << "Native client registered for connection id" << id;
const auto command = Protocol::makeCommand(Protocol::Connection, QJsonObject{std::pair{"emails"_L1, arguments["emails"_L1]}}, id);
sendMessageToWebClient(id, command);
// NOTE: native client will follow up with a status update to any connected web clients
}
return;
}
case Command::PairingRequest: {
const auto type = arguments["type"_L1].toString();
if (type == "web"_L1) { // originates from web client
const auto token = arguments["token"_L1].toString();
if (token.length() != PAIRING_TOKEN_LENGTH) {
// If users type the token, instead of pasting, we be sent partial tokens. Ignore those.
return;
}
const auto it = std::find_if(m_nativeClientsMappingToId.cbegin(), m_nativeClientsMappingToId.cend(), [token](NativeClient client) {
return client.isPairing() && (token == client.pairingToken);
});
if (it == m_nativeClientsMappingToId.cend()) {
qCWarning(WEBSOCKET_LOG) << "Invalid pairing code supplied" << token;
const auto json = QJsonDocument(errorCommand(i18n("Invalid pairing code.")));
socket->sendTextMessage(QString::fromUtf8(json.toJson()));
} else {
const auto id = it.key();
m_webClientsMappingToId[id] = socket;
const auto command = Protocol::makeCommand(
Protocol::Connection,
QJsonObject(),
id
);
sendMessageToWebClient(id, command);
// also pass on request to native client, in order to signal successful pairing
sendMessageToNativeClient(id, Protocol::makeCommand(Protocol::PairingRequest, QJsonObject(), id));
qCInfo(WEBSOCKET_LOG) << "Pairing sucess on connection" << id;
}
} else {
const auto id = nativeIdToPublicId(object["id"_L1].toString());
if (m_nativeClientsMappingToId.contains(id)) {
auto client = m_nativeClientsMappingToId[id];
if (client.socket != socket) {
qCWarning(WEBSOCKET_LOG) << "Rejecting pairing request for foreign id";
return;
}
if (type == "native-start-pairing"_L1) {
bool unique = false;
QString token;
qCInfo(WEBSOCKET_LOG) << "Client" << client.id << "entered pairing mode";
auto ctx = GpgME::Context::createForProtocol(GpgME::Protocol::OpenPGP);
do {
const auto rnd = ctx->generateRandomBytes(PAIRING_TOKEN_LENGTH+1, GpgME::Context::RandomMode::ZBase32);
token = QString::fromLatin1(QByteArrayView(rnd.value()).chopped(1)).toUpper();
unique = std::find_if(m_nativeClientsMappingToId.cbegin(), m_nativeClientsMappingToId.cend(), [token](auto client) {
return token == client.pairingToken;
}) == m_nativeClientsMappingToId.cend();
if (!unique) {
// log potential DOS attacks
qCWarning(WEBSOCKET_LOG) << "Collision while generating token for" << client.id << "address" << socket->peerAddress();
}
} while(!unique);
delete(ctx);
client.pairingToken = token;
m_nativeClientsMappingToId[id] = client;
sendMessageToNativeClient(id, Protocol::makeCommand(Protocol::PairingRequest, QJsonObject{{ "token"_L1, token }}, id));
} else { // "native-end-pairing"
client.pairingToken.clear();
m_nativeClientsMappingToId[id] = client;
sendMessageToNativeClient(id, Protocol::makeCommand(Protocol::PairingRequest, QJsonObject(), id));
}
}
}
return;
}
// requests originating in web client -> forward to native client
case Command::EwsResponse:
case Command::View:
case Command::Reply:
case Command::Forward:
case Command::Composer:
case Command::OpenDraft:
case Command::RestoreAutosave:
case Command::Info:
case Command::Reencrypt:
case Command::DeleteDraft: {
sendMessageToNativeClient(object["id"_L1].toString(), object);
return;
}
// requests originating in native client -> forward to web client
case Command::InfoFetched:
case Command::Error:
case Command::Ews:
case Command::StatusUpdate: {
const auto id = nativeIdToPublicId(object["id"_L1].toString());
sendMessageToWebClient(id, Protocol::makeCommand(command, arguments, id));
return;
}
// debug
case Command::Log:
qCWarning(WEBSOCKET_LOG) << arguments["message"_L1].toString() << arguments["args"_L1].toString();
return;
default:
qCWarning(WEBSOCKET_LOG) << "Invalid json received: invalid command" << command;
return;
}
}
bool WebServer::sendMessageToWebClient(const QString &id, const QJsonObject &obj)
{
auto socket = m_webClientsMappingToId.value(id);
if (!socket) {
return false;
}
const QJsonDocument doc(obj);
socket->sendTextMessage(QString::fromUtf8(doc.toJson()));
return true;
// TODO: centrally send error to native client, if web client is not available?
}
bool WebServer::sendMessageToNativeClient(const QString &id, const QJsonObject &obj)
{
auto it = m_nativeClientsMappingToId.constFind(id);
if (it == m_nativeClientsMappingToId.cend()) {
sendMessageToWebClient(id, errorCommand(i18n("Unable to find GpgOL/Web native client. Ensure GpgOL/Web is started.")));
return false;
}
auto device = it.value();
const QJsonDocument doc(obj);
device.socket->sendTextMessage(QString::fromUtf8(doc.toJson()));
return true;
}
void WebServer::processBinaryMessage(QByteArray message)
{
// TODO: what's this?!
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_webClientsMappingToId.cbegin(), m_webClientsMappingToId.cend(), [pClient](QWebSocket *webSocket) {
return pClient == webSocket;
});
if (it != m_webClientsMappingToId.cend()) {
const auto id = it.key();
qCInfo(WEBSOCKET_LOG) << "Web client for" << id << "was disconnected.";
sendMessageToNativeClient(id, Protocol::makeCommand(Protocol::Disconnection, QJsonObject(), QString()));
m_webClientsMappingToId.removeIf([pClient](auto device) {
return pClient == device.value();
});
}
}
// Native client was disconnected
const auto it = std::find_if(m_nativeClientsMappingToId.cbegin(), m_nativeClientsMappingToId.cend(), [pClient](NativeClient client) {
return pClient == client.socket;
});
if (it != m_nativeClientsMappingToId.cend()) {
const auto id = it.key();
qCInfo(WEBSOCKET_LOG) << "Native client for" << id << "was disconnected.";
sendMessageToWebClient(id, Protocol::makeCommand(Protocol::Disconnection, QJsonObject(), QString()));
}
m_nativeClientsMappingToId.removeIf([pClient](auto device) {
return pClient == device.value().socket;
});
pClient->deleteLater();
}

File Metadata

Mime Type
text/x-diff
Expires
Thu, Feb 26, 7:20 PM (12 h, 39 m)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
dd/55/23b5e00c1567501419e1afaaea43

Event Timeline