diff --git a/server/editor/attachment/attachmentcontrollerbase.cpp b/server/editor/attachment/attachmentcontrollerbase.cpp index 31aeaf3..359142d 100644 --- a/server/editor/attachment/attachmentcontrollerbase.cpp +++ b/server/editor/attachment/attachmentcontrollerbase.cpp @@ -1,959 +1,963 @@ /* * This file is part of KMail. * SPDX-FileCopyrightText: 2009 Constantin Berzan * * Parts based on KMail code by: * Various authors. * * SPDX-License-Identifier: GPL-2.0-or-later */ #include "attachmentcontrollerbase.h" #include "../attachment/attachmentjob.h" #include "../attachment/attachmentmodel.h" #include "../attachment/attachmentupdatejob.h" #include "../attachment/attachmentfrompublickeyjob.h" #include "../attachment/attachmentfromurlbasejob.h" #include "../attachment/attachmentcompressjob.h" #include "../attachment/attachmentclipboardjob.h" #include "../attachment/attachmentfromurlutils.h" #include "../attachment/attachmentpropertiesdialog.h" #include "../composer.h" #include "../part/globalpart.h" #include "messagecomposersettings.h" #include "editor_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace MessageComposer; using namespace MessageCore; class MessageComposer::AttachmentControllerBase::AttachmentControllerBasePrivate { public: AttachmentControllerBasePrivate(AttachmentControllerBase *qq); ~AttachmentControllerBasePrivate(); void attachmentRemoved(const AttachmentPart::Ptr &part); // slot void compressJobResult(KJob *job); // slot void loadJobResult(KJob *job); // slot void openSelectedAttachments(); // slot void viewSelectedAttachments(); // slot void editSelectedAttachment(); // slot void editSelectedAttachmentWith(); // slot void removeSelectedAttachments(); // slot void saveSelectedAttachmentAs(); // slot void selectedAttachmentProperties(); // slot void editDone(MessageComposer::EditorWatcher *watcher); // slot void attachPublicKeyJobResult(KJob *job); // slot void slotAttachmentContentCreated(KJob *job); // slot void addAttachmentPart(AttachmentPart::Ptr part); void attachClipBoardElement(KJob *job); void selectedAllAttachment(); void createOpenWithMenu(QMenu *topMenu, const AttachmentPart::Ptr &part); void reloadAttachment(); void updateJobResult(KJob *); AttachmentPart::List selectedParts; AttachmentControllerBase *const q; MessageComposer::AttachmentModel *model = nullptr; QWidget *wParent = nullptr; QHash editorPart; QHash editorTempFile; KActionCollection *mActionCollection = nullptr; QAction *attachPublicKeyAction = nullptr; QAction *attachMyPublicKeyAction = nullptr; QAction *openContextAction = nullptr; QAction *viewContextAction = nullptr; QAction *editContextAction = nullptr; QAction *editWithContextAction = nullptr; QAction *removeAction = nullptr; QAction *removeContextAction = nullptr; QAction *saveAsAction = nullptr; QAction *saveAsContextAction = nullptr; QAction *propertiesAction = nullptr; QAction *propertiesContextAction = nullptr; QAction *addAttachmentFileAction = nullptr; QAction *addAttachmentDirectoryAction = nullptr; QAction *addContextAction = nullptr; QAction *selectAllAction = nullptr; KActionMenu *attachmentMenu = nullptr; QAction *reloadAttachmentAction = nullptr; QAction *attachClipBoardAction = nullptr; // If part p is compressed, uncompressedParts[p] is the uncompressed part. QHash uncompressedParts; bool encryptEnabled = false; bool signEnabled = false; }; AttachmentControllerBase::AttachmentControllerBasePrivate::AttachmentControllerBasePrivate(AttachmentControllerBase *qq) : q(qq) { } AttachmentControllerBase::AttachmentControllerBasePrivate::~AttachmentControllerBasePrivate() = default; void AttachmentControllerBase::setSelectedParts(const AttachmentPart::List &selectedParts) { d->selectedParts = selectedParts; const int selectedCount = selectedParts.count(); const bool enableEditAction = (selectedCount == 1) && (!selectedParts.first()->isMessageOrMessageCollection()); d->openContextAction->setEnabled(selectedCount > 0); d->viewContextAction->setEnabled(selectedCount > 0); d->editContextAction->setEnabled(enableEditAction); d->editWithContextAction->setEnabled(enableEditAction); d->removeAction->setEnabled(selectedCount > 0); d->removeContextAction->setEnabled(selectedCount > 0); d->saveAsAction->setEnabled(selectedCount == 1); d->saveAsContextAction->setEnabled(selectedCount == 1); d->propertiesAction->setEnabled(selectedCount == 1); d->propertiesContextAction->setEnabled(selectedCount == 1); } void AttachmentControllerBase::AttachmentControllerBasePrivate::attachmentRemoved(const AttachmentPart::Ptr &part) { uncompressedParts.remove(part); } void AttachmentControllerBase::AttachmentControllerBasePrivate::compressJobResult(KJob *job) { if (job->error()) { KMessageBox::error(wParent, job->errorString(), i18nc("@title:window", "Failed to compress attachment")); return; } auto ajob = qobject_cast(job); Q_ASSERT(ajob); AttachmentPart::Ptr originalPart = ajob->originalPart(); AttachmentPart::Ptr compressedPart = ajob->compressedPart(); if (ajob->isCompressedPartLarger()) { const int result = KMessageBox::questionTwoActions(wParent, i18n("The compressed attachment is larger than the original. " "Do you want to keep the original one?"), QString(/*caption*/), KGuiItem(i18nc("Do not compress", "Keep")), KGuiItem(i18n("Compress"))); if (result == KMessageBox::ButtonCode::PrimaryAction) { // The user has chosen to keep the uncompressed file. return; } } qCDebug(EDITOR_LOG) << "Replacing uncompressed part in model."; uncompressedParts[compressedPart] = originalPart; bool ok = model->replaceAttachment(originalPart, compressedPart); if (!ok) { // The attachment was removed from the model while we were compressing. qCDebug(EDITOR_LOG) << "Compressed a zombie."; } } void AttachmentControllerBase::AttachmentControllerBasePrivate::loadJobResult(KJob *job) { if (job->error()) { KMessageBox::error(wParent, job->errorString(), i18n("Failed to attach file")); return; } auto ajob = qobject_cast(job); Q_ASSERT(ajob); AttachmentPart::Ptr part = ajob->attachmentPart(); q->addAttachment(part); } void AttachmentControllerBase::AttachmentControllerBasePrivate::openSelectedAttachments() { Q_ASSERT(selectedParts.count() >= 1); for (const AttachmentPart::Ptr &part : std::as_const(selectedParts)) { q->openAttachment(part); } } void AttachmentControllerBase::AttachmentControllerBasePrivate::viewSelectedAttachments() { Q_ASSERT(selectedParts.count() >= 1); for (const AttachmentPart::Ptr &part : std::as_const(selectedParts)) { q->viewAttachment(part); } } void AttachmentControllerBase::AttachmentControllerBasePrivate::editSelectedAttachment() { Q_ASSERT(selectedParts.count() == 1); q->editAttachment(selectedParts.constFirst(), MessageComposer::EditorWatcher::NoOpenWithDialog); } void AttachmentControllerBase::AttachmentControllerBasePrivate::editSelectedAttachmentWith() { Q_ASSERT(selectedParts.count() == 1); q->editAttachment(selectedParts.constFirst(), MessageComposer::EditorWatcher::OpenWithDialog); } void AttachmentControllerBase::AttachmentControllerBasePrivate::removeSelectedAttachments() { Q_ASSERT(selectedParts.count() >= 1); // We must store list, otherwise when we remove it changes selectedParts (as selection changed) => it will crash. const AttachmentPart::List toRemove = selectedParts; for (const AttachmentPart::Ptr &part : toRemove) { - model->removeAttachment(part); + if (!model->removeAttachment(part)) { + qCWarning(EDITOR_LOG) << "Impossible to remove attachment" << part->fileName(); + } } } void AttachmentControllerBase::AttachmentControllerBasePrivate::saveSelectedAttachmentAs() { Q_ASSERT(selectedParts.count() == 1); q->saveAttachmentAs(selectedParts.constFirst()); } void AttachmentControllerBase::AttachmentControllerBasePrivate::selectedAttachmentProperties() { Q_ASSERT(selectedParts.count() == 1); q->attachmentProperties(selectedParts.constFirst()); } void AttachmentControllerBase::AttachmentControllerBasePrivate::reloadAttachment() { Q_ASSERT(selectedParts.count() == 1); auto ajob = new AttachmentUpdateJob(selectedParts.constFirst(), q); connect(ajob, &AttachmentUpdateJob::result, q, [this](KJob *job) { updateJobResult(job); }); ajob->start(); } void AttachmentControllerBase::AttachmentControllerBasePrivate::updateJobResult(KJob *job) { if (job->error()) { KMessageBox::error(wParent, job->errorString(), i18n("Failed to reload attachment")); return; } auto ajob = qobject_cast(job); Q_ASSERT(ajob); AttachmentPart::Ptr originalPart = ajob->originalPart(); AttachmentPart::Ptr updatedPart = ajob->updatedPart(); attachmentRemoved(originalPart); bool ok = model->replaceAttachment(originalPart, updatedPart); if (!ok) { // The attachment was removed from the model while we were compressing. qCDebug(EDITOR_LOG) << "Updated a zombie."; } } void AttachmentControllerBase::AttachmentControllerBasePrivate::editDone(MessageComposer::EditorWatcher *watcher) { AttachmentPart::Ptr part = editorPart.take(watcher); Q_ASSERT(part); QTemporaryFile *tempFile = editorTempFile.take(watcher); Q_ASSERT(tempFile); if (watcher->fileChanged()) { qCDebug(EDITOR_LOG) << "File has changed."; const QString name = watcher->url().path(); QFile file(name); if (file.open(QIODevice::ReadOnly)) { const QByteArray data = file.readAll(); part->setData(data); model->updateAttachment(part); } } delete tempFile; // The watcher deletes itself. } void AttachmentControllerBase::AttachmentControllerBasePrivate::createOpenWithMenu(QMenu *topMenu, const AttachmentPart::Ptr &part) { const QString contentTypeStr = QString::fromLatin1(part->mimeType()); const KService::List offers = KFileItemActions::associatedApplications(QStringList() << contentTypeStr); if (!offers.isEmpty()) { QMenu *menu = topMenu; auto actionGroup = new QActionGroup(menu); connect(actionGroup, &QActionGroup::triggered, q, &AttachmentControllerBase::slotOpenWithAction); if (offers.count() > 1) { // submenu 'open with' menu = new QMenu(i18nc("@title:menu", "&Open With"), topMenu); menu->menuAction()->setObjectName(QLatin1StringView("openWith_submenu")); // for the unittest topMenu->addMenu(menu); } // qCDebug(EDITOR_LOG) << offers.count() << "offers" << topMenu << menu; KService::List::ConstIterator it = offers.constBegin(); KService::List::ConstIterator end = offers.constEnd(); for (; it != end; ++it) { /* TODO Carl QAction *act = MessageViewer::Util::createAppAction(*it, // no submenu -> prefix single offer menu == topMenu, actionGroup, menu); menu->addAction(act); */ } QString openWithActionName; if (menu != topMenu) { // submenu menu->addSeparator(); openWithActionName = i18nc("@action:inmenu Open With", "&Other..."); } else { openWithActionName = i18nc("@title:menu", "&Open With..."); } auto openWithAct = new QAction(menu); openWithAct->setText(openWithActionName); QObject::connect(openWithAct, &QAction::triggered, q, &AttachmentControllerBase::slotOpenWithDialog); menu->addAction(openWithAct); } else { // no app offers -> Open With... auto act = new QAction(topMenu); act->setText(i18nc("@title:menu", "&Open With...")); QObject::connect(act, &QAction::triggered, q, &AttachmentControllerBase::slotOpenWithDialog); topMenu->addAction(act); } } void AttachmentControllerBase::exportPublicKey(const GpgME::Key &key) { if (key.isNull() || !QGpgME::openpgp()) { qCWarning(EDITOR_LOG) << "Tried to export key with empty fingerprint, or no OpenPGP."; return; } auto ajob = new MessageComposer::AttachmentFromPublicKeyJob(key, this); connect(ajob, &AttachmentFromPublicKeyJob::result, this, [this](KJob *job) { d->attachPublicKeyJobResult(job); }); ajob->start(); } void AttachmentControllerBase::AttachmentControllerBasePrivate::attachPublicKeyJobResult(KJob *job) { // The only reason we can't use loadJobResult() and need a separate method // is that we want to show the proper caption ("public key" instead of "file")... if (job->error()) { KMessageBox::error(wParent, job->errorString(), i18n("Failed to attach public key")); return; } Q_ASSERT(dynamic_cast(job)); auto ajob = static_cast(job); AttachmentPart::Ptr part = ajob->attachmentPart(); q->addAttachment(part); } void AttachmentControllerBase::AttachmentControllerBasePrivate::attachClipBoardElement(KJob *job) { if (job->error()) { qCDebug(EDITOR_LOG) << " Error during when get try to attach text from clipboard"; KMessageBox::error(wParent, job->errorString(), i18n("Failed to attach text from clipboard")); return; } auto ajob = static_cast(job); AttachmentPart::Ptr part = ajob->attachmentPart(); q->addAttachment(part); } static QTemporaryFile *dumpAttachmentToTempFile(const AttachmentPart::Ptr &part) // local { auto file = new QTemporaryFile; if (!file->open()) { qCCritical(EDITOR_LOG) << "Could not open tempfile" << file->fileName(); delete file; return nullptr; } if (file->write(part->data()) == -1) { qCCritical(EDITOR_LOG) << "Could not dump attachment to tempfile."; delete file; return nullptr; } file->flush(); return file; } AttachmentControllerBase::AttachmentControllerBase(MessageComposer::AttachmentModel *model, QWidget *wParent, KActionCollection *actionCollection) : QObject(wParent) , d(new AttachmentControllerBasePrivate(this)) { d->model = model; connect(model, &MessageComposer::AttachmentModel::attachUrlsRequested, this, &AttachmentControllerBase::addAttachments); connect(model, &MessageComposer::AttachmentModel::attachmentRemoved, this, [this](const MessageCore::AttachmentPart::Ptr &attr) { d->attachmentRemoved(attr); }); connect(model, &AttachmentModel::attachmentCompressRequested, this, &AttachmentControllerBase::compressAttachment); connect(model, &MessageComposer::AttachmentModel::encryptEnabled, this, &AttachmentControllerBase::setEncryptEnabled); connect(model, &MessageComposer::AttachmentModel::signEnabled, this, &AttachmentControllerBase::setSignEnabled); d->wParent = wParent; d->mActionCollection = actionCollection; } AttachmentControllerBase::~AttachmentControllerBase() = default; void AttachmentControllerBase::createActions() { // Create the actions. d->attachPublicKeyAction = new QAction(i18n("Attach &Public Key..."), this); connect(d->attachPublicKeyAction, &QAction::triggered, this, &AttachmentControllerBase::showAttachPublicKeyDialog); d->attachMyPublicKeyAction = new QAction(i18n("Attach &My Public Key"), this); connect(d->attachMyPublicKeyAction, &QAction::triggered, this, &AttachmentControllerBase::attachMyPublicKey); d->attachmentMenu = new KActionMenu(QIcon::fromTheme(QStringLiteral("mail-attachment")), i18n("Attach"), this); connect(d->attachmentMenu, &QAction::triggered, this, &AttachmentControllerBase::showAddAttachmentFileDialog); d->attachmentMenu->setPopupMode(QToolButton::DelayedPopup); d->addAttachmentFileAction = new QAction(QIcon::fromTheme(QStringLiteral("mail-attachment")), i18n("&Attach File..."), this); d->addAttachmentFileAction->setIconText(i18n("Attach")); d->addContextAction = new QAction(QIcon::fromTheme(QStringLiteral("mail-attachment")), i18n("Add Attachment..."), this); connect(d->addAttachmentFileAction, &QAction::triggered, this, &AttachmentControllerBase::showAddAttachmentFileDialog); connect(d->addContextAction, &QAction::triggered, this, &AttachmentControllerBase::showAddAttachmentFileDialog); d->addAttachmentDirectoryAction = new QAction(QIcon::fromTheme(QStringLiteral("mail-attachment")), i18n("&Attach Directory..."), this); d->addAttachmentDirectoryAction->setIconText(i18n("Attach")); connect(d->addAttachmentDirectoryAction, &QAction::triggered, this, &AttachmentControllerBase::showAddAttachmentCompressedDirectoryDialog); d->attachClipBoardAction = new QAction(QIcon::fromTheme(QStringLiteral("mail-attachment")), i18n("&Attach Text From Clipboard..."), this); d->attachClipBoardAction->setIconText(i18n("Attach Text From Clipboard")); connect(d->attachClipBoardAction, &QAction::triggered, this, &AttachmentControllerBase::showAttachClipBoard); d->attachmentMenu->addAction(d->addAttachmentFileAction); d->attachmentMenu->addAction(d->addAttachmentDirectoryAction); d->attachmentMenu->addSeparator(); d->attachmentMenu->addAction(d->attachClipBoardAction); d->removeAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18n("&Remove Attachment"), this); d->removeContextAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18n("Remove"), this); // FIXME need two texts. is there a better way? connect(d->removeAction, &QAction::triggered, this, [this]() { d->removeSelectedAttachments(); }); connect(d->removeContextAction, &QAction::triggered, this, [this]() { d->removeSelectedAttachments(); }); d->openContextAction = new QAction(i18nc("to open", "Open"), this); connect(d->openContextAction, &QAction::triggered, this, [this]() { d->openSelectedAttachments(); }); d->viewContextAction = new QAction(i18nc("to view", "View"), this); connect(d->viewContextAction, &QAction::triggered, this, [this]() { d->viewSelectedAttachments(); }); d->editContextAction = new QAction(i18nc("to edit", "Edit"), this); connect(d->editContextAction, &QAction::triggered, this, [this]() { d->editSelectedAttachment(); }); d->editWithContextAction = new QAction(i18n("Edit With..."), this); connect(d->editWithContextAction, &QAction::triggered, this, [this]() { d->editSelectedAttachmentWith(); }); d->saveAsAction = new QAction(QIcon::fromTheme(QStringLiteral("document-save-as")), i18n("&Save Attachment As..."), this); d->saveAsContextAction = new QAction(QIcon::fromTheme(QStringLiteral("document-save-as")), i18n("Save As..."), this); connect(d->saveAsAction, &QAction::triggered, this, [this]() { d->saveSelectedAttachmentAs(); }); connect(d->saveAsContextAction, &QAction::triggered, this, [this]() { d->saveSelectedAttachmentAs(); }); d->propertiesAction = new QAction(i18n("Attachment Pr&operties..."), this); d->propertiesContextAction = new QAction(i18n("Properties"), this); connect(d->propertiesAction, &QAction::triggered, this, [this]() { d->selectedAttachmentProperties(); }); connect(d->propertiesContextAction, &QAction::triggered, this, [this]() { d->selectedAttachmentProperties(); }); d->selectAllAction = new QAction(i18n("Select All"), this); connect(d->selectAllAction, &QAction::triggered, this, &AttachmentControllerBase::selectedAllAttachment); d->reloadAttachmentAction = new QAction(QIcon::fromTheme(QStringLiteral("view-refresh")), i18n("Reload"), this); connect(d->reloadAttachmentAction, &QAction::triggered, this, [this]() { d->reloadAttachment(); }); // Insert the actions into the composer window's menu. KActionCollection *collection = d->mActionCollection; collection->addAction(QStringLiteral("attach_public_key"), d->attachPublicKeyAction); collection->addAction(QStringLiteral("attach_my_public_key"), d->attachMyPublicKeyAction); collection->addAction(QStringLiteral("attach"), d->addAttachmentFileAction); collection->setDefaultShortcut(d->addAttachmentFileAction, QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_A)); collection->addAction(QStringLiteral("attach_directory"), d->addAttachmentDirectoryAction); collection->addAction(QStringLiteral("remove"), d->removeAction); collection->addAction(QStringLiteral("attach_save"), d->saveAsAction); collection->addAction(QStringLiteral("attach_properties"), d->propertiesAction); collection->addAction(QStringLiteral("select_all_attachment"), d->selectAllAction); collection->addAction(QStringLiteral("attach_menu"), d->attachmentMenu); setSelectedParts(AttachmentPart::List()); Q_EMIT actionsCreated(); } void AttachmentControllerBase::setEncryptEnabled(bool enabled) { d->encryptEnabled = enabled; } void AttachmentControllerBase::setSignEnabled(bool enabled) { d->signEnabled = enabled; } void AttachmentControllerBase::compressAttachment(const AttachmentPart::Ptr &part, bool compress) { if (compress) { qCDebug(EDITOR_LOG) << "Compressing part."; auto ajob = new AttachmentCompressJob(part, this); connect(ajob, &AttachmentCompressJob::result, this, [this](KJob *job) { d->compressJobResult(job); }); ajob->start(); } else { qCDebug(EDITOR_LOG) << "Uncompressing part."; // Replace the compressed part with the original uncompressed part, and delete // the compressed part. AttachmentPart::Ptr originalPart = d->uncompressedParts.take(part); Q_ASSERT(originalPart); // Found in uncompressedParts. bool ok = d->model->replaceAttachment(part, originalPart); Q_ASSERT(ok); Q_UNUSED(ok) } } void AttachmentControllerBase::showContextMenu() { Q_EMIT refreshSelection(); const int numberOfParts(d->selectedParts.count()); QMenu menu; const bool enableEditAction = (numberOfParts == 1) && (!d->selectedParts.first()->isMessageOrMessageCollection()); if (numberOfParts > 0) { if (numberOfParts == 1) { const QString mimetype = QString::fromLatin1(d->selectedParts.first()->mimeType()); QMimeDatabase mimeDb; auto mime = mimeDb.mimeTypeForName(mimetype); QStringList parentMimeType; if (mime.isValid()) { parentMimeType = mime.allAncestors(); } if ((mimetype == QLatin1String("text/plain")) || (mimetype == QLatin1String("image/png")) || (mimetype == QLatin1String("image/jpeg")) || parentMimeType.contains(QLatin1String("text/plain")) || parentMimeType.contains(QLatin1String("image/png")) || parentMimeType.contains(QLatin1String("image/jpeg"))) { menu.addAction(d->viewContextAction); } d->createOpenWithMenu(&menu, d->selectedParts.constFirst()); } menu.addAction(d->openContextAction); } if (enableEditAction) { menu.addAction(d->editWithContextAction); menu.addAction(d->editContextAction); } menu.addSeparator(); if (numberOfParts == 1) { if (!d->selectedParts.first()->url().isEmpty()) { menu.addAction(d->reloadAttachmentAction); } menu.addAction(d->saveAsContextAction); menu.addSeparator(); menu.addAction(d->propertiesContextAction); menu.addSeparator(); } if (numberOfParts > 0) { menu.addAction(d->removeContextAction); menu.addSeparator(); } const int nbAttachment = d->model->rowCount(); if (nbAttachment != numberOfParts) { menu.addAction(d->selectAllAction); menu.addSeparator(); } if (numberOfParts == 0) { menu.addAction(d->addContextAction); } menu.exec(QCursor::pos()); } void AttachmentControllerBase::slotOpenWithDialog() { openWith(); } void AttachmentControllerBase::slotOpenWithAction(QAction *act) { auto app = act->data().value(); Q_ASSERT(d->selectedParts.count() == 1); openWith(app); } void AttachmentControllerBase::openWith(const KService::Ptr &offer) { QTemporaryFile *tempFile = dumpAttachmentToTempFile(d->selectedParts.constFirst()); if (!tempFile) { KMessageBox::error(d->wParent, i18n("KMail was unable to write the attachment to a temporary file."), i18nc("@title:window", "Unable to open attachment")); return; } QUrl url = QUrl::fromLocalFile(tempFile->fileName()); tempFile->setPermissions(QFile::ReadUser); // If offer is null, this will show the "open with" dialog auto job = new KIO::ApplicationLauncherJob(offer); job->setUrls({url}); job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, d->wParent)); job->start(); connect(job, &KJob::result, this, [tempFile, job]() { if (job->error()) { delete tempFile; } }); // Delete the file only when the composer is closed // (and this object is destroyed). tempFile->setParent(this); // Manages lifetime. } void AttachmentControllerBase::openAttachment(const AttachmentPart::Ptr &part) { QTemporaryFile *tempFile = dumpAttachmentToTempFile(part); if (!tempFile) { KMessageBox::error(d->wParent, i18n("KMail was unable to write the attachment to a temporary file."), i18nc("@title:window", "Unable to open attachment")); return; } tempFile->setPermissions(QFile::ReadUser); auto job = new KIO::OpenUrlJob(QUrl::fromLocalFile(tempFile->fileName()), QString::fromLatin1(part->mimeType())); job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, d->wParent)); job->setDeleteTemporaryFile(true); connect(job, &KIO::OpenUrlJob::result, this, [this, tempFile](KJob *job) { if (job->error() == KIO::ERR_USER_CANCELED) { KMessageBox::error(d->wParent, i18n("KMail was unable to open the attachment."), job->errorString()); delete tempFile; } else { // The file was opened. Delete it only when the composer is closed // (and this object is destroyed). tempFile->setParent(this); // Manages lifetime. } }); job->start(); } void AttachmentControllerBase::viewAttachment(const AttachmentPart::Ptr &part) { auto composer = new MessageComposer::Composer; composer->globalPart()->setFallbackCharsetEnabled(true); auto attachmentJob = new MessageComposer::AttachmentJob(part, composer); connect(attachmentJob, &AttachmentJob::result, this, [this](KJob *job) { d->slotAttachmentContentCreated(job); }); attachmentJob->start(); } void AttachmentControllerBase::AttachmentControllerBasePrivate::slotAttachmentContentCreated(KJob *job) { if (!job->error()) { const MessageComposer::AttachmentJob *const attachmentJob = qobject_cast(job); Q_ASSERT(attachmentJob); if (attachmentJob) { Q_EMIT q->showAttachment(attachmentJob->content(), QByteArray()); } } else { // TODO: show warning to the user qCWarning(EDITOR_LOG) << "Error creating KMime::Content for attachment:" << job->errorText(); } } void AttachmentControllerBase::editAttachment(AttachmentPart::Ptr part, MessageComposer::EditorWatcher::OpenWithOption openWithOption) { QTemporaryFile *tempFile = dumpAttachmentToTempFile(part); if (!tempFile) { KMessageBox::error(d->wParent, i18n("KMail was unable to write the attachment to a temporary file."), i18nc("@title:window", "Unable to edit attachment")); return; } auto watcher = new MessageComposer::EditorWatcher(QUrl::fromLocalFile(tempFile->fileName()), QString::fromLatin1(part->mimeType()), openWithOption, this, d->wParent); connect(watcher, &MessageComposer::EditorWatcher::editDone, this, [this](MessageComposer::EditorWatcher *watcher) { d->editDone(watcher); }); switch (watcher->start()) { case MessageComposer::EditorWatcher::NoError: // The attachment is being edited. // We will clean things up in editDone(). d->editorPart[watcher] = part; d->editorTempFile[watcher] = tempFile; // Delete the temp file if the composer is closed (and this object is destroyed). tempFile->setParent(this); // Manages lifetime. break; case MessageComposer::EditorWatcher::CannotStart: qCWarning(EDITOR_LOG) << "Could not start EditorWatcher."; [[fallthrough]]; case MessageComposer::EditorWatcher::Unknown: case MessageComposer::EditorWatcher::Canceled: case MessageComposer::EditorWatcher::NoServiceFound: delete watcher; delete tempFile; break; } } void AttachmentControllerBase::editAttachmentWith(const AttachmentPart::Ptr &part) { editAttachment(part, MessageComposer::EditorWatcher::OpenWithDialog); } void AttachmentControllerBase::saveAttachmentAs(const AttachmentPart::Ptr &part) { QString pname = part->name(); if (pname.isEmpty()) { pname = i18n("unnamed"); } const QUrl url = QFileDialog::getSaveFileUrl(d->wParent, i18n("Save Attachment As"), QUrl::fromLocalFile(pname)); if (url.isEmpty()) { qCDebug(EDITOR_LOG) << "Save Attachment As dialog canceled."; return; } byteArrayToRemoteFile(part->data(), url); } void AttachmentControllerBase::byteArrayToRemoteFile(const QByteArray &aData, const QUrl &aURL, bool overwrite) { KIO::StoredTransferJob *job = KIO::storedPut(aData, aURL, -1, overwrite ? KIO::Overwrite : KIO::DefaultFlags); connect(job, &KIO::StoredTransferJob::result, this, &AttachmentControllerBase::slotPutResult); } void AttachmentControllerBase::slotPutResult(KJob *job) { auto _job = qobject_cast(job); if (job->error()) { if (job->error() == KIO::ERR_FILE_ALREADY_EXIST) { if (KMessageBox::warningContinueCancel(nullptr, i18n("File %1 exists.\nDo you want to replace it?", _job->url().toLocalFile()), i18nc("@title:window", "Save to File"), KGuiItem(i18n("&Replace"))) == KMessageBox::Continue) { byteArrayToRemoteFile(_job->data(), _job->url(), true); } } else { KJobUiDelegate *ui = static_cast(job)->uiDelegate(); ui->showErrorMessage(); } } } void AttachmentControllerBase::attachmentProperties(const AttachmentPart::Ptr &part) { QPointer dialog = new AttachmentPropertiesDialog(part, false, d->wParent); dialog->setEncryptEnabled(d->encryptEnabled); dialog->setSignEnabled(d->signEnabled); if (dialog->exec() && dialog) { d->model->updateAttachment(part); } delete dialog; } void AttachmentControllerBase::attachDirectory(const QUrl &url) { const int rc = KMessageBox::warningTwoActions(d->wParent, i18n("Do you really want to attach this directory \"%1\"?", url.toLocalFile()), i18nc("@title:window", "Attach directory"), KGuiItem(i18nc("@action:button", "Attach")), KStandardGuiItem::cancel()); if (rc == KMessageBox::ButtonCode::PrimaryAction) { addAttachment(url); } } void AttachmentControllerBase::showAttachClipBoard() { auto job = new MessageComposer::AttachmentClipBoardJob(this); connect(job, &AttachmentClipBoardJob::result, this, [this](KJob *job) { d->attachClipBoardElement(job); }); job->start(); } void AttachmentControllerBase::showAddAttachmentCompressedDirectoryDialog() { const QUrl url = QFileDialog::getExistingDirectoryUrl(d->wParent, i18nc("@title:window", "Attach Directory")); if (url.isValid()) { attachDirectory(url); } } static QString fixEncoding(const QString &encoding) { QString returnEncoding = encoding; // According to https://www.iana.org/assignments/character-sets, uppercase is // preferred in MIME headers const QString returnEncodingToUpper = returnEncoding.toUpper(); if (returnEncodingToUpper.contains(QLatin1String("ISO "))) { returnEncoding = returnEncodingToUpper; returnEncoding.replace(QLatin1String("ISO "), QStringLiteral("ISO-")); } return returnEncoding; } void AttachmentControllerBase::showAddAttachmentFileDialog() { const KEncodingFileDialog::Result result = KEncodingFileDialog::getOpenUrlsAndEncoding(QString(), QUrl(), QString(), d->wParent, i18nc("@title:window", "Attach File")); if (!result.URLs.isEmpty()) { const QString encoding = fixEncoding(result.encoding); const int numberOfFiles(result.URLs.count()); for (int i = 0; i < numberOfFiles; ++i) { const QUrl url = result.URLs.at(i); QUrl urlWithEncoding = url; QUrlQuery query; query.addQueryItem(QStringLiteral("charset"), encoding); urlWithEncoding.setQuery(query); QMimeDatabase mimeDb; const auto mimeType = mimeDb.mimeTypeForUrl(urlWithEncoding); if (mimeType.name() == QLatin1String("inode/directory")) { const int rc = KMessageBox::warningTwoActions(d->wParent, i18n("Do you really want to attach this directory \"%1\"?", url.toLocalFile()), i18nc("@title:window", "Attach directory"), KGuiItem(i18nc("@action:button", "Attach")), KStandardGuiItem::cancel()); if (rc == KMessageBox::ButtonCode::PrimaryAction) { addAttachment(urlWithEncoding); } } else { addAttachment(urlWithEncoding); } } } } void AttachmentControllerBase::addAttachment(const AttachmentPart::Ptr &part) { part->setEncrypted(d->model->isEncryptSelected()); part->setSigned(d->model->isSignSelected()); d->model->addAttachment(part); Q_EMIT fileAttached(); } void AttachmentControllerBase::addAttachmentUrlSync(const QUrl &url) { MessageCore::AttachmentFromUrlBaseJob *ajob = MessageCore::AttachmentFromUrlUtils::createAttachmentJob(url, this); if (ajob->exec()) { AttachmentPart::Ptr part = ajob->attachmentPart(); addAttachment(part); } else { if (ajob->error()) { KMessageBox::error(d->wParent, ajob->errorString(), i18nc("@title:window", "Failed to attach file")); } } } void AttachmentControllerBase::addAttachment(const QUrl &url) { MessageCore::AttachmentFromUrlBaseJob *ajob = MessageCore::AttachmentFromUrlUtils::createAttachmentJob(url, this); connect(ajob, &AttachmentFromUrlBaseJob::result, this, [this](KJob *job) { d->loadJobResult(job); }); ajob->start(); } void AttachmentControllerBase::addAttachments(const QList &urls) { for (const QUrl &url : urls) { addAttachment(url); } } void AttachmentControllerBase::showAttachPublicKeyDialog() { using Kleo::KeySelectionDialog; QPointer dialog = new KeySelectionDialog(i18n("Attach Public OpenPGP Key"), i18n("Select the public key which should be attached."), std::vector(), KeySelectionDialog::PublicKeys | KeySelectionDialog::OpenPGPKeys, false /* no multi selection */, false /* no remember choice box */, d->wParent); if (dialog->exec() == QDialog::Accepted) { exportPublicKey(dialog->selectedKey()); } delete dialog; } void AttachmentControllerBase::attachMyPublicKey() { } void AttachmentControllerBase::enableAttachPublicKey(bool enable) { d->attachPublicKeyAction->setEnabled(enable); } void AttachmentControllerBase::enableAttachMyPublicKey(bool enable) { d->attachMyPublicKeyAction->setEnabled(enable); } void AttachmentControllerBase::clear() { const auto parts = d->model->attachments(); for (const auto &attachmentPart : parts) { - d->model->removeAttachment(attachmentPart); + if (!d->model->removeAttachment(attachmentPart)) { + qCWarning(EDITOR_LOG) << "Impossible to remove attachment" << attachmentPart->fileName(); + } } } #include "moc_attachmentcontrollerbase.cpp" diff --git a/server/editor/job/contentjobbase.h b/server/editor/job/contentjobbase.h index 1ea0f91..2d9038d 100644 --- a/server/editor/job/contentjobbase.h +++ b/server/editor/job/contentjobbase.h @@ -1,91 +1,91 @@ /* SPDX-FileCopyrightText: 2009 Constantin Berzan Based on ideas by Stephen Kelly. SPDX-License-Identifier: LGPL-2.0-or-later */ #pragma once #include "jobbase.h" namespace KMime { class Content; } namespace MessageComposer { class ContentJobBasePrivate; /** * @brief The ContentJobBase class */ class ContentJobBase : public JobBase { Q_OBJECT public: explicit ContentJobBase(QObject *parent = nullptr); ~ContentJobBase() override; /** Starts processing this ContentJobBase asynchronously. This processes all children in order first, then calls process(). Emits finished() after all processing is done, and the content is reachable through content(). */ void start() override; /** Get the resulting KMime::Content that the ContentJobBase has generated. Jobs never delete their content. */ [[nodiscard]] KMime::Content *content() const; /** This is meant to be used instead of KCompositeJob::addSubjob(), making it possible to add subjobs from the outside. Transfers ownership of the @p job to this object. */ - [[nodiscard]] bool appendSubjob(ContentJobBase *job); + bool appendSubjob(ContentJobBase *job); /** Set some extra content to be saved with the job, and available later, for example, in slot handling result of job. Job does not take care of deleting extra content. */ void setExtraContent(KMime::Content *extra); /** Get extra content that was previously added. */ [[nodiscard]] KMime::Content *extraContent() const; protected: ContentJobBase(ContentJobBasePrivate &dd, QObject *parent); /** Use appendSubjob() instead. */ bool addSubjob(KJob *job) override; protected Q_SLOTS: /** Reimplement to do additional stuff before processing children, such as adding more subjobs. Remember to call the base implementation. */ virtual void doStart(); /** This is called after all the children have been processed. (You must use their resulting contents, or delete them.) Reimplement in subclasses to process concrete content. Call emitResult() when finished. */ virtual void process() = 0; void slotResult(KJob *job) override; private: Q_DECLARE_PRIVATE(ContentJobBase) }; } // namespace MessageComposer diff --git a/server/editor/job/encryptjob.cpp b/server/editor/job/encryptjob.cpp index 9e2d84c..8d65d89 100644 --- a/server/editor/job/encryptjob.cpp +++ b/server/editor/job/encryptjob.cpp @@ -1,277 +1,279 @@ /* SPDX-FileCopyrightText: 2009 Klaralvdalens Datakonsult AB, a KDAB Group company, info@kdab.net SPDX-FileCopyrightText: 2009 Leo Franchi SPDX-License-Identifier: LGPL-2.0-or-later */ #include "encryptjob.h" #include "contentjobbase_p.h" #include "protectedheadersjob.h" #include "../util_p.h" #include "../part/infopart.h" #include #include #include "editor_debug.h" #include #include #include #include using namespace MessageComposer; class MessageComposer::EncryptJobPrivate : public ContentJobBasePrivate { public: EncryptJobPrivate(EncryptJob *qq) : ContentJobBasePrivate(qq) { } QString gnupgHome; QStringList recipients; std::vector keys; Kleo::CryptoMessageFormat format; KMime::Content *content = nullptr; KMime::Message *skeletonMessage = nullptr; bool protectedHeaders = true; bool protectedHeadersObvoscate = false; // copied from messagecomposer.cpp bool binaryHint(Kleo::CryptoMessageFormat f) { switch (f) { case Kleo::SMIMEFormat: case Kleo::SMIMEOpaqueFormat: return true; default: case Kleo::OpenPGPMIMEFormat: case Kleo::InlineOpenPGPFormat: return false; } } GpgME::SignatureMode signingMode(Kleo::CryptoMessageFormat f) { switch (f) { case Kleo::SMIMEOpaqueFormat: return GpgME::NormalSignatureMode; case Kleo::InlineOpenPGPFormat: return GpgME::Clearsigned; default: case Kleo::SMIMEFormat: case Kleo::OpenPGPMIMEFormat: return GpgME::Detached; } } Q_DECLARE_PUBLIC(EncryptJob) }; EncryptJob::EncryptJob(QObject *parent) : ContentJobBase(*new EncryptJobPrivate(this), parent) { } EncryptJob::~EncryptJob() = default; void EncryptJob::setContent(KMime::Content *content) { Q_D(EncryptJob); d->content = content; d->content->assemble(); } void EncryptJob::setCryptoMessageFormat(Kleo::CryptoMessageFormat format) { Q_D(EncryptJob); d->format = format; } void EncryptJob::setEncryptionKeys(const std::vector &keys) { Q_D(EncryptJob); d->keys = keys; } void EncryptJob::setRecipients(const QStringList &recipients) { Q_D(EncryptJob); d->recipients = recipients; } void EncryptJob::setSkeletonMessage(KMime::Message *skeletonMessage) { Q_D(EncryptJob); d->skeletonMessage = skeletonMessage; } void EncryptJob::setProtectedHeaders(bool protectedHeaders) { Q_D(EncryptJob); d->protectedHeaders = protectedHeaders; } void EncryptJob::setProtectedHeadersObvoscate(bool protectedHeadersObvoscate) { Q_D(EncryptJob); d->protectedHeadersObvoscate = protectedHeadersObvoscate; } void EncryptJob::setGnupgHome(const QString &path) { Q_D(EncryptJob); d->gnupgHome = path; } QStringList EncryptJob::recipients() const { Q_D(const EncryptJob); return d->recipients; } std::vector EncryptJob::encryptionKeys() const { Q_D(const EncryptJob); return d->keys; } void EncryptJob::doStart() { Q_D(EncryptJob); Q_ASSERT(d->resultContent == nullptr); // Not processed before. if (d->keys.size() == 0) { // should not happen---resolver should have dealt with it earlier qCDebug(EDITOR_LOG) << "HELP! Encrypt job but have no keys to encrypt with."; return; } // if setContent hasn't been called, we assume that a subjob was added // and we want to use that if (!d->content || !d->content->hasContent()) { if (d->subjobContents.size() == 1) { d->content = d->subjobContents.constFirst(); } } if (d->protectedHeaders && d->skeletonMessage && d->format & Kleo::OpenPGPMIMEFormat) { auto pJob = new ProtectedHeadersJob; pJob->setContent(d->content); pJob->setSkeletonMessage(d->skeletonMessage); pJob->setObvoscate(d->protectedHeadersObvoscate); QObject::connect(pJob, &ProtectedHeadersJob::finished, this, [d, pJob](KJob *job) { if (job->error()) { return; } d->content = pJob->content(); }); - appendSubjob(pJob); + if (!appendSubjob(pJob)) { + qCWarning(EDITOR_LOG) << "Impossible to add subjob"; + } } ContentJobBase::doStart(); } void EncryptJob::slotResult(KJob *job) { // Q_D(EncryptJob); if (error() || job->error()) { ContentJobBase::slotResult(job); return; } if (subjobs().size() == 2) { auto pjob = static_cast(subjobs().last()); if (pjob) { auto cjob = qobject_cast(job); Q_ASSERT(cjob); pjob->setContent(cjob->content()); } } ContentJobBase::slotResult(job); } void EncryptJob::process() { Q_D(EncryptJob); // if setContent hasn't been called, we assume that a subjob was added // and we want to use that if (!d->content || !d->content->hasContent()) { Q_ASSERT(d->subjobContents.size() == 1); d->content = d->subjobContents.constFirst(); } const QGpgME::Protocol *proto = nullptr; if (d->format & Kleo::AnyOpenPGP) { proto = QGpgME::openpgp(); } else if (d->format & Kleo::AnySMIME) { proto = QGpgME::smime(); } else { qCDebug(EDITOR_LOG) << "HELP! Encrypt job but have protocol to encrypt with."; return; } Q_ASSERT(proto); // for now just do the main recipients QByteArray content; d->content->assemble(); if (d->format & Kleo::InlineOpenPGPFormat) { content = d->content->body(); } else { content = d->content->encodedContent(); } qCDebug(EDITOR_LOG) << "got backend, starting job"; QGpgME::EncryptJob *eJob = proto->encryptJob(!d->binaryHint(d->format), d->format == Kleo::InlineOpenPGPFormat); if (!(d->format & Kleo::InlineOpenPGPFormat)) { eJob->setInputEncoding(GpgME::Data::MimeEncoding); } if (!d->gnupgHome.isEmpty()) { QGpgME::Job::context(eJob)->setEngineHomeDirectory(d->gnupgHome.toUtf8().constData()); } QObject::connect( eJob, &QGpgME::EncryptJob::result, this, [this, d](const GpgME::EncryptionResult &result, const QByteArray &cipherText, const QString &auditLogAsHtml, const GpgME::Error &auditLogError) { Q_UNUSED(auditLogAsHtml) Q_UNUSED(auditLogError) if (result.error()) { setError(result.error().code()); setErrorText(QString::fromLocal8Bit(result.error().asString())); emitResult(); return; } d->resultContent = MessageComposer::Util::composeHeadersAndBody(d->content, cipherText, d->format, false); emitResult(); }); const auto error = eJob->start(d->keys, content, true); if (error.code()) { eJob->deleteLater(); setError(error.code()); setErrorText(QString::fromLocal8Bit(error.asString())); emitResult(); } } #include "moc_encryptjob.cpp" diff --git a/server/editor/job/multipartjob.cpp b/server/editor/job/multipartjob.cpp index fbebcdf..03a1780 100644 --- a/server/editor/job/multipartjob.cpp +++ b/server/editor/job/multipartjob.cpp @@ -1,69 +1,69 @@ /* SPDX-FileCopyrightText: 2009 Constantin Berzan SPDX-License-Identifier: LGPL-2.0-or-later */ #include "multipartjob.h" #include "contentjobbase_p.h" #include "editor_debug.h" #include using namespace MessageComposer; class MessageComposer::MultipartJobPrivate : public ContentJobBasePrivate { public: MultipartJobPrivate(MultipartJob *qq) : ContentJobBasePrivate(qq) { } QByteArray subtype; }; MultipartJob::MultipartJob(QObject *parent) : ContentJobBase(*new MultipartJobPrivate(this), parent) { } MultipartJob::~MultipartJob() = default; QByteArray MultipartJob::multipartSubtype() const { Q_D(const MultipartJob); return d->subtype; } void MultipartJob::setMultipartSubtype(const QByteArray &subtype) { Q_D(MultipartJob); d->subtype = subtype; } void MultipartJob::process() { Q_D(MultipartJob); Q_ASSERT(d->resultContent == nullptr); // Not processed before. Q_ASSERT(!d->subtype.isEmpty()); d->resultContent = new KMime::Content; auto contentType = d->resultContent->contentType(true); contentType->setMimeType("multipart/" + d->subtype); contentType->setBoundary(KMime::multiPartBoundary()); d->resultContent->contentTransferEncoding()->setEncoding(KMime::Headers::CE7Bit); d->resultContent->setPreamble("This is a multi-part message in MIME format.\n"); for (KMime::Content *c : std::as_const(d->subjobContents)) { - d->resultContent->addContent(c); + d->resultContent->appendContent(c); if (c->contentTransferEncoding()->encoding() == KMime::Headers::CE8Bit) { d->resultContent->contentTransferEncoding()->setEncoding(KMime::Headers::CE8Bit); break; } } qCDebug(EDITOR_LOG) << "Created" << d->resultContent->contentType()->mimeType() << "content with" << d->resultContent->contents().count() << "subjobContents."; emitResult(); } #include "moc_multipartjob.cpp" diff --git a/server/editor/job/protectedheadersjob.cpp b/server/editor/job/protectedheadersjob.cpp index 2a937eb..3e2f7fc 100644 --- a/server/editor/job/protectedheadersjob.cpp +++ b/server/editor/job/protectedheadersjob.cpp @@ -1,156 +1,159 @@ /* SPDX-FileCopyrightText: 2020 Sandro Knauß SPDX-License-Identifier: LGPL-2.0-or-later */ #include "protectedheadersjob.h" #include "contentjobbase_p.h" #include "singlepartjob.h" +#include "editor_debug.h" #include #include using namespace MessageComposer; class MessageComposer::ProtectedHeadersJobPrivate : public ContentJobBasePrivate { public: ProtectedHeadersJobPrivate(ProtectedHeadersJob *qq) : ContentJobBasePrivate(qq) { } KMime::Content *content = nullptr; KMime::Message *skeletonMessage = nullptr; bool obvoscate = false; Q_DECLARE_PUBLIC(ProtectedHeadersJob) }; ProtectedHeadersJob::ProtectedHeadersJob(QObject *parent) : ContentJobBase(*new ProtectedHeadersJobPrivate(this), parent) { } ProtectedHeadersJob::~ProtectedHeadersJob() = default; void ProtectedHeadersJob::setContent(KMime::Content *content) { Q_D(ProtectedHeadersJob); d->content = content; if (content) { d->content->assemble(); } } void ProtectedHeadersJob::setSkeletonMessage(KMime::Message *skeletonMessage) { Q_D(ProtectedHeadersJob); d->skeletonMessage = skeletonMessage; } void ProtectedHeadersJob::setObvoscate(bool obvoscate) { Q_D(ProtectedHeadersJob); d->obvoscate = obvoscate; } void ProtectedHeadersJob::doStart() { Q_D(ProtectedHeadersJob); Q_ASSERT(d->resultContent == nullptr); // Not processed before. Q_ASSERT(d->skeletonMessage); // We need a skeletonMessage to proceed auto subject = d->skeletonMessage->header(); if (d->obvoscate && subject) { // Create protected header lagacy mimepart with replaced headers auto cjob = new SinglepartJob; auto ct = cjob->contentType(); ct->setMimeType("text/plain"); ct->setCharset(subject->rfc2047Charset()); ct->setParameter(QStringLiteral("protected-headers"), QStringLiteral("v1")); cjob->contentDisposition()->setDisposition(KMime::Headers::contentDisposition::CDinline); cjob->setData(subject->type() + QByteArray(": ") + subject->asUnicodeString().toUtf8()); QObject::connect(cjob, &SinglepartJob::finished, this, [d, cjob]() { auto mixedPart = new KMime::Content(); const QByteArray boundary = KMime::multiPartBoundary(); mixedPart->contentType()->setMimeType("multipart/mixed"); mixedPart->contentType(false)->setBoundary(boundary); - mixedPart->addContent(cjob->content()); + mixedPart->appendContent(cjob->content()); // if setContent hasn't been called, we assume that a subjob was added // and we want to use that if (!d->content || !d->content->hasContent()) { Q_ASSERT(d->subjobContents.size() == 1); d->content = d->subjobContents.constFirst(); } - mixedPart->addContent(d->content); + mixedPart->appendContent(d->content); d->content = mixedPart; }); - appendSubjob(cjob); + if (!appendSubjob(cjob)) { + qCWarning(EDITOR_LOG) << "Impossible to add subjob"; + } } ContentJobBase::doStart(); } void ProtectedHeadersJob::process() { Q_D(ProtectedHeadersJob); // if setContent hasn't been called, we assume that a subjob was added // and we want to use that if (!d->content || !d->content->hasContent()) { Q_ASSERT(d->subjobContents.size() == 1); d->content = d->subjobContents.constFirst(); } auto subject = d->skeletonMessage->header(); const auto headers = d->skeletonMessage->headers(); for (const auto &header : headers) { const QByteArray headerType(header->type()); if (headerType.startsWith("X-KMail-")) { continue; } if (headerType == "Bcc") { continue; } if (headerType.startsWith("Content-")) { continue; } // A workaround for #439958 // KMime strips sometimes the newlines from long headers, if those // headers are in the signature block, this breaks the signature. // The simplest workaround is not to sign those headers until this // get fixed in KMime. if (header->as7BitString().length() > 70) { continue; } auto copyHeader = KMime::Headers::createHeader(headerType); if (!copyHeader) { copyHeader = new KMime::Headers::Generic(headerType.constData(), headerType.size()); } copyHeader->from7BitString(header->as7BitString(false)); d->content->appendHeader(copyHeader); } if (d->obvoscate && subject) { subject->clear(); subject->from7BitString("..."); } auto contentType = d->content->header(); contentType->setParameter(QStringLiteral("protected-headers"), QStringLiteral("v1")); d->resultContent = d->content; emitResult(); } #include "moc_protectedheadersjob.cpp" diff --git a/server/editor/job/signencryptjob.cpp b/server/editor/job/signencryptjob.cpp index 04e4c63..5dbf952 100644 --- a/server/editor/job/signencryptjob.cpp +++ b/server/editor/job/signencryptjob.cpp @@ -1,274 +1,276 @@ /* SPDX-FileCopyrightText: 2009 Klaralvdalens Datakonsult AB, a KDAB Group company, info@kdab.net SPDX-FileCopyrightText: 2009 Leo Franchi SPDX-License-Identifier: LGPL-2.0-or-later */ #include "signencryptjob.h" #include "contentjobbase_p.h" #include "protectedheadersjob.h" #include "../util_p.h" #include #include #include "editor_debug.h" #include #include #include #include #include #include #include using namespace MessageComposer; class MessageComposer::SignEncryptJobPrivate : public ContentJobBasePrivate { public: SignEncryptJobPrivate(SignEncryptJob *qq) : ContentJobBasePrivate(qq) { } std::vector signers; std::vector encKeys; QStringList recipients; Kleo::CryptoMessageFormat format; KMime::Content *content = nullptr; KMime::Message *skeletonMessage = nullptr; bool protectedHeaders = true; bool protectedHeadersObvoscate = false; // copied from messagecomposer.cpp bool binaryHint(Kleo::CryptoMessageFormat f) { switch (f) { case Kleo::SMIMEFormat: case Kleo::SMIMEOpaqueFormat: return true; default: case Kleo::OpenPGPMIMEFormat: case Kleo::InlineOpenPGPFormat: return false; } } Q_DECLARE_PUBLIC(SignEncryptJob) }; SignEncryptJob::SignEncryptJob(QObject *parent) : ContentJobBase(*new SignEncryptJobPrivate(this), parent) { } SignEncryptJob::~SignEncryptJob() = default; void SignEncryptJob::setContent(KMime::Content *content) { Q_D(SignEncryptJob); Q_ASSERT(content); d->content = content; } void SignEncryptJob::setCryptoMessageFormat(Kleo::CryptoMessageFormat format) { Q_D(SignEncryptJob); // There *must* be a concrete format set at this point. Q_ASSERT(format == Kleo::OpenPGPMIMEFormat || format == Kleo::InlineOpenPGPFormat || format == Kleo::SMIMEFormat || format == Kleo::SMIMEOpaqueFormat); d->format = format; } void SignEncryptJob::setSigningKeys(const std::vector &signers) { Q_D(SignEncryptJob); d->signers = signers; } KMime::Content *SignEncryptJob::origContent() { Q_D(SignEncryptJob); return d->content; } void SignEncryptJob::setEncryptionKeys(const std::vector &keys) { Q_D(SignEncryptJob); d->encKeys = keys; } void SignEncryptJob::setRecipients(const QStringList &recipients) { Q_D(SignEncryptJob); d->recipients = recipients; } void SignEncryptJob::setSkeletonMessage(KMime::Message *skeletonMessage) { Q_D(SignEncryptJob); d->skeletonMessage = skeletonMessage; } void SignEncryptJob::setProtectedHeaders(bool protectedHeaders) { Q_D(SignEncryptJob); d->protectedHeaders = protectedHeaders; } void SignEncryptJob::setProtectedHeadersObvoscate(bool protectedHeadersObvoscate) { Q_D(SignEncryptJob); d->protectedHeadersObvoscate = protectedHeadersObvoscate; } QStringList SignEncryptJob::recipients() const { Q_D(const SignEncryptJob); return d->recipients; } std::vector SignEncryptJob::encryptionKeys() const { Q_D(const SignEncryptJob); return d->encKeys; } void SignEncryptJob::doStart() { Q_D(SignEncryptJob); Q_ASSERT(d->resultContent == nullptr); // Not processed before. if (d->protectedHeaders && d->skeletonMessage && d->format & Kleo::OpenPGPMIMEFormat) { auto pJob = new ProtectedHeadersJob; pJob->setContent(d->content); pJob->setSkeletonMessage(d->skeletonMessage); pJob->setObvoscate(d->protectedHeadersObvoscate); QObject::connect(pJob, &ProtectedHeadersJob::finished, this, [d, pJob](KJob *job) { if (job->error()) { return; } d->content = pJob->content(); }); - appendSubjob(pJob); + if (!appendSubjob(pJob)) { + qCWarning(EDITOR_LOG) << "Impossible to add subjob"; + } } ContentJobBase::doStart(); } void SignEncryptJob::slotResult(KJob *job) { // Q_D(SignEncryptJob); if (error() || job->error()) { ContentJobBase::slotResult(job); return; } if (subjobs().size() == 2) { auto pjob = static_cast(subjobs().last()); if (pjob) { auto cjob = qobject_cast(job); Q_ASSERT(cjob); pjob->setContent(cjob->content()); } } ContentJobBase::slotResult(job); } void SignEncryptJob::process() { Q_D(SignEncryptJob); Q_ASSERT(d->resultContent == nullptr); // Not processed before. // if setContent hasn't been called, we assume that a subjob was added // and we want to use that if (!d->content || !d->content->hasContent()) { Q_ASSERT(d->subjobContents.size() == 1); d->content = d->subjobContents.constFirst(); } const QGpgME::Protocol *proto = nullptr; if (d->format & Kleo::AnyOpenPGP) { proto = QGpgME::openpgp(); } else if (d->format & Kleo::AnySMIME) { proto = QGpgME::smime(); } else { return; } Q_ASSERT(proto); // d->resultContent = new KMime::Content; qCDebug(EDITOR_LOG) << "creating signencrypt from:" << proto->name() << proto->displayName(); QByteArray encBody; d->content->assemble(); // replace simple LFs by CRLFs for all MIME supporting CryptPlugs // according to RfC 2633, 3.1.1 Canonicalization QByteArray content; if (d->format & Kleo::InlineOpenPGPFormat) { content = d->content->body(); } else if (!(d->format & Kleo::SMIMEOpaqueFormat)) { content = KMime::LFtoCRLF(d->content->encodedContent()); } else { // SMimeOpaque doesn't need LFtoCRLF, else it gets munged content = d->content->encodedContent(); } QGpgME::SignEncryptJob *job(proto->signEncryptJob(!d->binaryHint(d->format), d->format == Kleo::InlineOpenPGPFormat)); QObject::connect(job, &QGpgME::SignEncryptJob::result, this, [this, d](const GpgME::SigningResult &signingResult, const GpgME::EncryptionResult &encryptionResult, const QByteArray &cipherText, const QString &auditLogAsHtml, const GpgME::Error &auditLogError) { Q_UNUSED(auditLogAsHtml) Q_UNUSED(auditLogError) if (signingResult.error()) { qCDebug(EDITOR_LOG) << "signing failed:" << signingResult.error().asString(); setError(signingResult.error().code()); setErrorText(QString::fromLocal8Bit(signingResult.error().asString())); emitResult(); return; } if (encryptionResult.error()) { qCDebug(EDITOR_LOG) << "encrypting failed:" << encryptionResult.error().asString(); setError(encryptionResult.error().code()); setErrorText(QString::fromLocal8Bit(encryptionResult.error().asString())); emitResult(); return; } QByteArray signatureHashAlgo = signingResult.createdSignature(0).hashAlgorithmAsString(); d->resultContent = MessageComposer::Util::composeHeadersAndBody(d->content, cipherText, d->format, false, signatureHashAlgo); emitResult(); }); const auto error = job->start(d->signers, d->encKeys, content, false); if (error.code()) { job->deleteLater(); setError(error.code()); setErrorText(QString::fromLocal8Bit(error.asString())); emitResult(); } } #include "moc_signencryptjob.cpp" diff --git a/server/editor/job/signjob.cpp b/server/editor/job/signjob.cpp index cf27da9..064a3f4 100644 --- a/server/editor/job/signjob.cpp +++ b/server/editor/job/signjob.cpp @@ -1,289 +1,291 @@ /* SPDX-FileCopyrightText: 2009 Klaralvdalens Datakonsult AB, a KDAB Group company, info@kdab.net SPDX-FileCopyrightText: 2009 Leo Franchi SPDX-License-Identifier: LGPL-2.0-or-later */ #include "signjob.h" #include "contentjobbase_p.h" #include "protectedheadersjob.h" #include "../util_p.h" #include #include #include #include "editor_debug.h" #include #include #include #include #include #include #include using namespace MessageComposer; class MessageComposer::SignJobPrivate : public ContentJobBasePrivate { public: SignJobPrivate(SignJob *qq) : ContentJobBasePrivate(qq) { } KMime::Content *content = nullptr; KMime::Message *skeletonMessage = nullptr; std::vector signers; Kleo::CryptoMessageFormat format; bool protectedHeaders = true; // copied from messagecomposer.cpp [[nodiscard]] bool binaryHint(Kleo::CryptoMessageFormat f) { switch (f) { case Kleo::SMIMEFormat: case Kleo::SMIMEOpaqueFormat: return true; default: case Kleo::OpenPGPMIMEFormat: case Kleo::InlineOpenPGPFormat: return false; } } [[nodiscard]] GpgME::SignatureMode signingMode(Kleo::CryptoMessageFormat f) { switch (f) { case Kleo::SMIMEOpaqueFormat: return GpgME::NormalSignatureMode; case Kleo::InlineOpenPGPFormat: return GpgME::Clearsigned; default: case Kleo::SMIMEFormat: case Kleo::OpenPGPMIMEFormat: return GpgME::Detached; } } Q_DECLARE_PUBLIC(SignJob) }; SignJob::SignJob(QObject *parent) : ContentJobBase(*new SignJobPrivate(this), parent) { } SignJob::~SignJob() = default; void SignJob::setContent(KMime::Content *content) { Q_D(SignJob); d->content = content; } void SignJob::setCryptoMessageFormat(Kleo::CryptoMessageFormat format) { Q_D(SignJob); // There *must* be a concrete format set at this point. Q_ASSERT(format == Kleo::OpenPGPMIMEFormat || format == Kleo::InlineOpenPGPFormat || format == Kleo::SMIMEFormat || format == Kleo::SMIMEOpaqueFormat); d->format = format; } void SignJob::setSigningKeys(const std::vector &signers) { Q_D(SignJob); d->signers = signers; } void SignJob::setSkeletonMessage(KMime::Message *skeletonMessage) { Q_D(SignJob); d->skeletonMessage = skeletonMessage; } void SignJob::setProtectedHeaders(bool protectedHeaders) { Q_D(SignJob); d->protectedHeaders = protectedHeaders; } KMime::Content *SignJob::origContent() { Q_D(SignJob); return d->content; } void SignJob::doStart() { Q_D(SignJob); Q_ASSERT(d->resultContent == nullptr); // Not processed before. if (d->protectedHeaders && d->skeletonMessage && d->format & Kleo::OpenPGPMIMEFormat) { auto pJob = new ProtectedHeadersJob; pJob->setContent(d->content); pJob->setSkeletonMessage(d->skeletonMessage); pJob->setObvoscate(false); QObject::connect(pJob, &ProtectedHeadersJob::finished, this, [d, pJob](KJob *job) { if (job->error()) { return; } d->content = pJob->content(); }); - appendSubjob(pJob); + if (!appendSubjob(pJob)) { + qCWarning(EDITOR_LOG) << "Impossible to add subjob."; + }; } ContentJobBase::doStart(); } void SignJob::slotResult(KJob *job) { if (error() || job->error()) { ContentJobBase::slotResult(job); return; } if (subjobs().size() == 2) { auto pjob = static_cast(subjobs().last()); if (pjob) { auto cjob = qobject_cast(job); Q_ASSERT(cjob); pjob->setContent(cjob->content()); } } ContentJobBase::slotResult(job); } void SignJob::process() { Q_D(SignJob); Q_ASSERT(d->resultContent == nullptr); // Not processed before. // if setContent hasn't been called, we assume that a subjob was added // and we want to use that if (!d->content) { Q_ASSERT(d->subjobContents.size() == 1); d->content = d->subjobContents.constFirst(); } // d->resultContent = new KMime::Content; const QGpgME::Protocol *proto = nullptr; if (d->format & Kleo::AnyOpenPGP) { proto = QGpgME::openpgp(); } else if (d->format & Kleo::AnySMIME) { proto = QGpgME::smime(); } Q_ASSERT(proto); qCDebug(EDITOR_LOG) << "creating signJob from:" << proto->name() << proto->displayName(); // for now just do the main recipients QByteArray signature; d->content->assemble(); // replace simple LFs by CRLFs for all MIME supporting CryptPlugs // according to RfC 2633, 3.1.1 Canonicalization QByteArray content; if (d->format & Kleo::InlineOpenPGPFormat) { content = d->content->body(); } else if (!(d->format & Kleo::SMIMEOpaqueFormat)) { // replace "From " and "--" at the beginning of lines // with encoded versions according to RfC 3156, 3 // Note: If any line begins with the string "From ", it is strongly // suggested that either the Quoted-Printable or Base64 MIME encoding // be applied. const auto encoding = d->content->contentTransferEncoding()->encoding(); if ((encoding == KMime::Headers::CEquPr || encoding == KMime::Headers::CE7Bit) && !d->content->contentType(false)) { QByteArray body = d->content->encodedBody(); bool changed = false; QList search; search.reserve(3); QList replacements; replacements.reserve(3); search << "From " << "from " << "-"; replacements << "From=20" << "from=20" << "=2D"; if (d->content->contentTransferEncoding()->encoding() == KMime::Headers::CE7Bit) { for (int i = 0, total = search.size(); i < total; ++i) { const auto pos = body.indexOf(search[i]); if (pos == 0 || (pos > 0 && body.at(pos - 1) == '\n')) { changed = true; break; } } if (changed) { d->content->contentTransferEncoding()->setEncoding(KMime::Headers::CEquPr); d->content->assemble(); body = d->content->encodedBody(); } } for (int i = 0; i < search.size(); ++i) { const auto pos = body.indexOf(search[i]); if (pos == 0 || (pos > 0 && body.at(pos - 1) == '\n')) { changed = true; body.replace(pos, search[i].size(), replacements[i]); } } if (changed) { qCDebug(EDITOR_LOG) << "Content changed"; d->content->setBody(body); d->content->contentTransferEncoding()->setDecoded(false); } } content = KMime::LFtoCRLF(d->content->encodedContent()); } else { // SMimeOpaque doesn't need LFtoCRLF, else it gets munged content = d->content->encodedContent(); } QGpgME::SignJob *job(proto->signJob(!d->binaryHint(d->format), d->format == Kleo::InlineOpenPGPFormat)); QObject::connect( job, &QGpgME::SignJob::result, this, [this, d](const GpgME::SigningResult &result, const QByteArray &signature, const QString &auditLogAsHtml, const GpgME::Error &auditLogError) { Q_UNUSED(auditLogAsHtml) Q_UNUSED(auditLogError) if (result.error().code()) { qCDebug(EDITOR_LOG) << "signing failed:" << result.error().asString(); // job->showErrorDialog( globalPart()->parentWidgetForGui() ); setError(result.error().code()); setErrorText(QString::fromLocal8Bit(result.error().asString())); emitResult(); return; } QByteArray signatureHashAlgo = result.createdSignature(0).hashAlgorithmAsString(); d->resultContent = MessageComposer::Util::composeHeadersAndBody(d->content, signature, d->format, true, signatureHashAlgo); emitResult(); }); const auto error = job->start(d->signers, content, d->signingMode(d->format)); if (error.code()) { job->deleteLater(); setError(error.code()); setErrorText(QString::fromLocal8Bit(error.asString())); emitResult(); } } #include "moc_signjob.cpp" diff --git a/server/editor/mailtemplates.cpp b/server/editor/mailtemplates.cpp index 2c3c3df..1d99c45 100644 --- a/server/editor/mailtemplates.cpp +++ b/server/editor/mailtemplates.cpp @@ -1,972 +1,932 @@ /* Copyright (c) 2009 Constantin Berzan Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Copyright (c) 2010 Leo Franchi Copyright (c) 2017 Christian Mollekopf This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "mailtemplates.h" #include #include #include #include #include #include #include #include #include #include #include #include using namespace Qt::Literals::StringLiterals; QDebug operator<<(QDebug dbg, const KMime::Types::Mailbox &mb) { dbg << mb.addrSpec().asString(); return dbg; } namespace KMime { namespace Types { static bool operator==(const KMime::Types::AddrSpec &left, const KMime::Types::AddrSpec &right) { return (left.asString() == right.asString()); } static bool operator==(const KMime::Types::Mailbox &left, const KMime::Types::Mailbox &right) { return (left.addrSpec().asString() == right.addrSpec().asString()); } } Message* contentToMessage(Content* content) { content->assemble(); const auto encoded = content->encodedContent(); auto message = new Message(); message->setContent(encoded); message->parse(); return message; } } static KMime::Types::Mailbox::List stripMyAddressesFromAddressList(const KMime::Types::Mailbox::List &list, const KMime::Types::AddrSpecList me) { KMime::Types::Mailbox::List addresses(list); for (KMime::Types::Mailbox::List::Iterator it = addresses.begin(); it != addresses.end();) { if (me.contains(it->addrSpec())) { it = addresses.erase(it); } else { ++it; } } return addresses; } static QString toPlainText(const QString &s) { QTextDocument doc; doc.setHtml(s); return doc.toPlainText(); } QString replacePrefixes(const QString &str, const QStringList &prefixRegExps, const QString &newPrefix) { // construct a big regexp that // 1. is anchored to the beginning of str (sans whitespace) // 2. matches at least one of the part regexps in prefixRegExps const QString bigRegExp = QStringLiteral("^(?:\\s+|(?:%1))+\\s*").arg(prefixRegExps.join(QStringLiteral(")|(?:"))); QRegularExpression rx(bigRegExp, QRegularExpression::CaseInsensitiveOption); if (!rx.isValid()) { qWarning() << "bigRegExp = \"" << bigRegExp << "\"\n" << "prefix regexp is invalid!"; qWarning() << "Error: " << rx.errorString() << rx; Q_ASSERT(false); return str; } QString tmp = str; //We expect a match at the beginning of the string QRegularExpressionMatch match; if (tmp.indexOf(rx, 0, &match) == 0) { return tmp.replace(0, match.capturedLength(), newPrefix + QLatin1Char(' ')); } //No match, we just prefix the newPrefix return newPrefix + u' ' + str; } const QStringList getForwardPrefixes() { //See https://en.wikipedia.org/wiki/List_of_email_subject_abbreviations QStringList list; //We want to be able to potentially reply to a variety of languages, so only translating is not enough list << i18nc("FWD abbreviation for forwarded in emails", "fwd"); list << u"fwd"_s; list << u"fw"_s; list << u"wg"_s; list << u"vs"_s; list << u"tr"_s; list << u"rv"_s; list << u"enc"_s; return list; } static QString forwardSubject(const QString &s) { //The standandard prefix const QString localPrefix = QStringLiteral("FW:"); QStringList forwardPrefixes; for (const auto &prefix : getForwardPrefixes()) { forwardPrefixes << prefix + QStringLiteral("\\s*:"); } return replacePrefixes(s, forwardPrefixes, localPrefix); } static QStringList getReplyPrefixes() { //See https://en.wikipedia.org/wiki/List_of_email_subject_abbreviations QStringList list; //We want to be able to potentially reply to a variety of languages, so only translating is not enough list << i18nc("RE abbreviation for reply in emails", "re"); list << u"re"_s; list << u"aw"_s; list << u"sv"_s; list << u"antw"_s; list << u"ref"_s; return list; } static QString replySubject(const QString &s) { //The standandard prefix (latin for "in re", in matter of) const QString localPrefix = QStringLiteral("RE:"); QStringList replyPrefixes; for (const auto &prefix : getReplyPrefixes()) { replyPrefixes << prefix + u"\\s*:"_s; replyPrefixes << prefix + u"\\[.+\\]:"_s; replyPrefixes << prefix + u"\\d+:"_s; } return replacePrefixes(s, replyPrefixes, localPrefix); } static QByteArray getRefStr(const QByteArray &references, const QByteArray &messageId) { QByteArray firstRef, lastRef, refStr{references.trimmed()}, retRefStr; int i, j; if (refStr.isEmpty()) { return messageId; } i = refStr.indexOf('<'); j = refStr.indexOf('>'); firstRef = refStr.mid(i, j - i + 1); if (!firstRef.isEmpty()) { retRefStr = firstRef + ' '; } i = refStr.lastIndexOf('<'); j = refStr.lastIndexOf('>'); lastRef = refStr.mid(i, j - i + 1); if (!lastRef.isEmpty() && lastRef != firstRef) { retRefStr += lastRef + ' '; } retRefStr += messageId; return retRefStr; } KMime::Content *createPlainPartContent(const QString &plainBody, KMime::Content *parent = nullptr) { KMime::Content *textPart = new KMime::Content(parent); textPart->contentType()->setMimeType("text/plain"); //FIXME This is supposed to select a charset out of the available charsets that contains all necessary characters to render the text // QTextCodec *charset = selectCharset(m_charsets, plainBody); // textPart->contentType()->setCharset(charset->name()); textPart->contentType()->setCharset("utf-8"); textPart->contentTransferEncoding()->setEncoding(KMime::Headers::CE8Bit); textPart->fromUnicodeString(plainBody); return textPart; } KMime::Content *createMultipartAlternativeContent(const QString &plainBody, const QString &htmlBody, KMime::Message *parent = nullptr) { KMime::Content *multipartAlternative = new KMime::Content(parent); multipartAlternative->contentType()->setMimeType("multipart/alternative"); multipartAlternative->contentType()->setBoundary(KMime::multiPartBoundary()); KMime::Content *textPart = createPlainPartContent(plainBody, multipartAlternative); - multipartAlternative->addContent(textPart); + multipartAlternative->appendContent(textPart); KMime::Content *htmlPart = new KMime::Content(multipartAlternative); htmlPart->contentType()->setMimeType("text/html"); //FIXME This is supposed to select a charset out of the available charsets that contains all necessary characters to render the text // QTextCodec *charset = selectCharset(m_charsets, htmlBody); // htmlPart->contentType()->setCharset(charset->name()); htmlPart->contentType()->setCharset("utf-8"); htmlPart->contentTransferEncoding()->setEncoding(KMime::Headers::CE8Bit); htmlPart->fromUnicodeString(htmlBody); - multipartAlternative->addContent(htmlPart); + multipartAlternative->appendContent(htmlPart); return multipartAlternative; } KMime::Content *createMultipartMixedContent(QVector contents) { KMime::Content *multiPartMixed = new KMime::Content(); multiPartMixed->contentType()->setMimeType("multipart/mixed"); multiPartMixed->contentType()->setBoundary(KMime::multiPartBoundary()); for (const auto &content : contents) { - multiPartMixed->addContent(content); + multiPartMixed->appendContent(content); } return multiPartMixed; } QString plainToHtml(const QString &body) { QString str = body; str = str.toHtmlEscaped(); str.replace(u'\n', u"
\n"_s); return str; } //TODO implement this function using a DOM tree parser void makeValidHtml(QString &body, const QString &headElement) { QRegularExpression regEx(u""_s, QRegularExpression::InvertedGreedinessOption); if (!body.isEmpty() && !body.contains(regEx)) { regEx.setPattern(u""_s); if (!body.contains(regEx)) { body = u""_s + body + u"
"_s; } regEx.setPattern(u""_s); if (!body.contains(regEx)) { body = u""_s + headElement + u""_s + body; } body = u""_s + body + u""_s; } } //FIXME strip signature works partially for HTML mails static QString stripSignature(const QString &msg) { // Following RFC 3676, only > before -- // I prefer to not delete a SB instead of delete good mail content. // We expect no CRLF from the ObjectTreeParser. The regex won't handle it. if (msg.contains(QStringLiteral("\r\n"))) { qWarning() << "Message contains CRLF, but shouldn't: " << msg; Q_ASSERT(false); } static const QRegularExpression sbDelimiterSearch(u"(^|\n)[> ]*-- \n"_s); // The regular expression to look for prefix change static const QRegularExpression commonReplySearch(u"^[ ]*>"_s); QString res = msg; int posDeletingStart = 1; // to start looking at 0 // While there are SB delimiters (start looking just before the deleted SB) while ((posDeletingStart = res.indexOf(sbDelimiterSearch, posDeletingStart - 1)) >= 0) { QString prefix; // the current prefix QString line; // the line to check if is part of the SB int posNewLine = -1; // Look for the SB beginning int posSignatureBlock = res.indexOf(u'-', posDeletingStart); // The prefix before "-- "$ if (res.at(posDeletingStart) == u'\n') { ++posDeletingStart; } prefix = res.mid(posDeletingStart, posSignatureBlock - posDeletingStart); posNewLine = res.indexOf(QLatin1Char('\n'), posSignatureBlock) + 1; // now go to the end of the SB while (posNewLine < res.size() && posNewLine > 0) { // handle the undefined case for mid ( x , -n ) where n>1 int nextPosNewLine = res.indexOf(QLatin1Char('\n'), posNewLine); if (nextPosNewLine < 0) { nextPosNewLine = posNewLine - 1; } line = res.mid(posNewLine, nextPosNewLine - posNewLine); // check when the SB ends: // * does not starts with prefix or // * starts with prefix+(any substring of prefix) if ((prefix.isEmpty() && line.indexOf(commonReplySearch) < 0) || (!prefix.isEmpty() && line.startsWith(prefix) && line.mid(prefix.size()).indexOf(commonReplySearch) < 0)) { posNewLine = res.indexOf(QLatin1Char('\n'), posNewLine) + 1; } else { break; // end of the SB } } // remove the SB or truncate when is the last SB if (posNewLine > 0) { res.remove(posDeletingStart, posNewLine - posDeletingStart); } else { res.truncate(posDeletingStart); } } return res; } static void plainMessageText(const QString &plainTextContent, const QString &htmlContent, const std::function &callback) { const auto result = plainTextContent.isEmpty() ? toPlainText(htmlContent) : plainTextContent; callback(result); } -static QString extractHeaderBodyScript() -{ - return QStringLiteral("(function() {" - "var res = {" - " body: document.getElementsByTagName('body')[0].innerHTML," - " header: document.getElementsByTagName('head')[0].innerHTML" - "};" - "return res;" - "})()"); -} - void htmlMessageText(const QString &plainTextContent, const QString &htmlContent, const std::function &callback) { QString htmlElement = htmlContent; if (htmlElement.isEmpty()) { //plain mails only QString htmlReplace = plainTextContent.toHtmlEscaped(); htmlReplace = htmlReplace.replace(QStringLiteral("\n"), QStringLiteral("
")); htmlElement = QStringLiteral("%1\n").arg(htmlReplace); } QDomDocument document; document.setContent(htmlElement); QString body; QTextStream bodyStream(&body); QString head; QTextStream headStream(&head); const auto bodies = document.elementsByTagName(u"body"_s); const auto heads = document.elementsByTagName(u"head"_s); if (bodies.isEmpty()) { body = htmlElement; } else { bodies.item(0).save(bodyStream, 2); } if (!heads.isEmpty()) { heads.item(0).save(headStream, 2); } callback(body, head); } QString formatQuotePrefix(const QString &wildString, const QString &fromDisplayString) { QString result; if (wildString.isEmpty()) { return wildString; } unsigned int strLength(wildString.length()); for (uint i = 0; i < strLength;) { QChar ch = wildString[i++]; if (ch == QLatin1Char('%') && i < strLength) { ch = wildString[i++]; switch (ch.toLatin1()) { case 'f': { // sender's initals if (fromDisplayString.isEmpty()) { break; } uint j = 0; const unsigned int strLength(fromDisplayString.length()); for (; j < strLength && fromDisplayString[j] > QLatin1Char(' '); ++j) ; for (; j < strLength && fromDisplayString[j] <= QLatin1Char(' '); ++j) ; result += fromDisplayString[0]; if (j < strLength && fromDisplayString[j] > QLatin1Char(' ')) { result += fromDisplayString[j]; } else if (strLength > 1) { if (fromDisplayString[1] > QLatin1Char(' ')) { result += fromDisplayString[1]; } } } break; case '_': result += QLatin1Char(' '); break; case '%': result += QLatin1Char('%'); break; default: result += QLatin1Char('%'); result += ch; break; } } else { result += ch; } } return result; } QString quotedPlainText(const QString &selection, const QString &fromDisplayString) { QString content = selection; // Remove blank lines at the beginning: const int firstNonWS = content.indexOf(QRegularExpression(u"\\S"_s)); const int lineStart = content.lastIndexOf(QLatin1Char('\n'), firstNonWS); if (lineStart >= 0) { content.remove(0, static_cast(lineStart)); } const auto quoteString = QStringLiteral("> "); const QString indentStr = formatQuotePrefix(quoteString, fromDisplayString); //FIXME // if (TemplateParserSettings::self()->smartQuote() && mWrap) { // content = MessageCore::StringUtil::smartQuote(content, mColWrap - indentStr.length()); // } content.replace(QLatin1Char('\n'), QLatin1Char('\n') + indentStr); content.prepend(indentStr); content += QLatin1Char('\n'); return content; } QString quotedHtmlText(const QString &selection) { QString content = selection; //TODO 1) look for all the variations of
and remove the blank lines //2) implement vertical bar for quoted HTML mail. //3) After vertical bar is implemented, If a user wants to edit quoted message, // then the
tags below should open and close as when required. //Add blockquote tag, so that quoted message can be differentiated from normal message content = QLatin1String("
") + content + QLatin1String("
"); return content; } enum ReplyStrategy { ReplyList, ReplySmart, ReplyAll, ReplyAuthor, ReplyNone }; static QByteArray as7BitString(const KMime::Headers::Base *h) { if (h) { return h->as7BitString(false); } return {}; } static QString asUnicodeString(const KMime::Headers::Base *h) { if (h) { return h->asUnicodeString(); } return {}; } static KMime::Types::Mailbox::List getMailingListAddresses(const KMime::Headers::Base *listPostHeader) { KMime::Types::Mailbox::List mailingListAddresses; const QString listPost = asUnicodeString(listPostHeader); if (listPost.contains(QStringLiteral("mailto:"), Qt::CaseInsensitive)) { static QRegularExpression rx(QStringLiteral("]+)@([^>]+)>"), QRegularExpression::CaseInsensitiveOption); QRegularExpressionMatch match; if (listPost.indexOf(rx, 0, &match) != -1) { // matched KMime::Types::Mailbox mailbox; mailbox.fromUnicodeString(match.captured(1) + u'@' + match.captured(2)); mailingListAddresses << mailbox; } } return mailingListAddresses; } struct RecipientMailboxes { KMime::Types::Mailbox::List to; KMime::Types::Mailbox::List cc; }; static RecipientMailboxes getRecipients(const KMime::Types::Mailbox::List &from, const KMime::Types::Mailbox::List &to, const KMime::Types::Mailbox::List &cc, const KMime::Types::Mailbox::List &replyToList, const KMime::Types::Mailbox::List &mailingListAddresses, const KMime::Types::AddrSpecList &me) { KMime::Types::Mailbox::List toList; KMime::Types::Mailbox::List ccList; auto listContainsMe = [&] (const KMime::Types::Mailbox::List &list) { for (const auto &m : me) { KMime::Types::Mailbox mailbox; mailbox.setAddress(m); if (list.contains(mailbox)) { return true; } } return false; }; if (listContainsMe(from)) { // sender seems to be one of our own identities, so we assume that this // is a reply to a "sent" mail where the users wants to add additional // information for the recipient. return {to, cc}; } KMime::Types::Mailbox::List recipients; KMime::Types::Mailbox::List ccRecipients; // add addresses from the Reply-To header to the list of recipients if (!replyToList.isEmpty()) { recipients = replyToList; // strip all possible mailing list addresses from the list of Reply-To addresses for (const KMime::Types::Mailbox &mailbox : std::as_const(mailingListAddresses)) { for (const KMime::Types::Mailbox &recipient : std::as_const(recipients)) { if (mailbox == recipient) { recipients.removeAll(recipient); } } } } if (!mailingListAddresses.isEmpty()) { // this is a mailing list message if (recipients.isEmpty() && !from.isEmpty()) { // The sender didn't set a Reply-to address, so we add the From // address to the list of CC recipients. ccRecipients += from; qDebug() << "Added" << from << "to the list of CC recipients"; } // if it is a mailing list, add the posting address recipients.prepend(mailingListAddresses[0]); } else { // this is a normal message if (recipients.isEmpty() && !from.isEmpty()) { // in case of replying to a normal message only then add the From // address to the list of recipients if there was no Reply-to address recipients += from; qDebug() << "Added" << from << "to the list of recipients"; } } // strip all my addresses from the list of recipients toList = stripMyAddressesFromAddressList(recipients, me); // merge To header and CC header into a list of CC recipients auto appendToCcRecipients = [&](const KMime::Types::Mailbox::List & list) { for (const KMime::Types::Mailbox &mailbox : list) { if (!recipients.contains(mailbox) && !ccRecipients.contains(mailbox)) { ccRecipients += mailbox; qDebug() << "Added" << mailbox.prettyAddress() << "to the list of CC recipients"; } } }; appendToCcRecipients(to); appendToCcRecipients(cc); if (!ccRecipients.isEmpty()) { // strip all my addresses from the list of CC recipients ccRecipients = stripMyAddressesFromAddressList(ccRecipients, me); // in case of a reply to self, toList might be empty. if that's the case // then propagate a cc recipient to To: (if there is any). if (toList.isEmpty() && !ccRecipients.isEmpty()) { toList << ccRecipients.at(0); ccRecipients.pop_front(); } ccList = ccRecipients; } if (toList.isEmpty() && !recipients.isEmpty()) { // reply to self without other recipients toList << recipients.at(0); } return {toList, ccList}; } void MailTemplates::reply(const KMime::Message::Ptr &origMsg, const std::function &callback, const KMime::Types::AddrSpecList &me) { //FIXME const bool alwaysPlain = true; // Decrypt what we have to MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(origMsg.data()); otp.decryptAndVerify(); auto partList = otp.collectContentParts(); if (partList.isEmpty()) { Q_ASSERT(false); return; } auto part = partList[0]; Q_ASSERT(part); // Prepare the reply message KMime::Message::Ptr msg(new KMime::Message); msg->removeHeader(); msg->removeHeader(); msg->contentType(true)->setMimeType("text/plain"); msg->contentType()->setCharset("utf-8"); auto getMailboxes = [](const KMime::Headers::Base *h) -> KMime::Types::Mailbox::List { if (h) { return static_cast(h)->mailboxes(); } return {}; }; auto fromHeader = static_cast(part->header(KMime::Headers::From::staticType())); const auto recipients = getRecipients( fromHeader ? fromHeader->mailboxes() : KMime::Types::Mailbox::List{}, getMailboxes(part->header(KMime::Headers::To::staticType())), getMailboxes(part->header(KMime::Headers::Cc::staticType())), getMailboxes(part->header(KMime::Headers::ReplyTo::staticType())), getMailingListAddresses(part->header("List-Post")), me ); for (const auto &mailbox : recipients.to) { msg->to()->addAddress(mailbox); } for (const auto &mailbox : recipients.cc) { msg->cc(true)->addAddress(mailbox); } const auto messageId = as7BitString(part->header(KMime::Headers::MessageID::staticType())); const QByteArray refStr = getRefStr(as7BitString(part->header(KMime::Headers::References::staticType())), messageId); if (!refStr.isEmpty()) { msg->references()->fromUnicodeString(QString::fromLocal8Bit(refStr), "utf-8"); } //In-Reply-To = original msg-id msg->inReplyTo()->from7BitString(messageId); const auto subjectHeader = part->header(KMime::Headers::Subject::staticType()); msg->subject()->fromUnicodeString(replySubject(asUnicodeString(subjectHeader)), "utf-8"); auto definedLocale = QLocale::system(); //Add quoted body QString plainBody; QString htmlBody; //On $datetime you wrote: auto dateHeader = static_cast(part->header(KMime::Headers::Date::staticType())); const QDateTime date = dateHeader ? dateHeader->dateTime() : QDateTime{}; const auto dateTimeString = QStringLiteral("%1 %2").arg(definedLocale.toString(date.date(), QLocale::LongFormat)).arg(definedLocale.toString(date.time(), QLocale::LongFormat)); const auto onDateYouWroteLine = i18nc("Reply header", "On %1 you wrote:\n", dateTimeString); plainBody.append(onDateYouWroteLine); htmlBody.append(plainToHtml(onDateYouWroteLine)); const auto plainTextContent = otp.plainTextContent(); const auto htmlContent = otp.htmlContent(); plainMessageText(plainTextContent, htmlContent, [=] (const QString &body) { QString result = stripSignature(body); //Quoted body result = quotedPlainText(result, fromHeader ? fromHeader->displayString() : QString{}); if (result.endsWith(u'\n')) { result.chop(1); } //The plain body is complete auto plainBodyResult = plainBody + result; htmlMessageText(plainTextContent, htmlContent, [=] (const QString &body, const QString &headElement) { QString result = stripSignature(body); //The html body is complete const auto htmlBodyResult = [&]() { if (!alwaysPlain) { QString htmlBodyResult = htmlBody + quotedHtmlText(result); makeValidHtml(htmlBodyResult, headElement); return htmlBodyResult; } return QString{}; }(); //Assemble the message msg->contentType()->clear(); // to get rid of old boundary KMime::Content *const mainTextPart = htmlBodyResult.isEmpty() ? createPlainPartContent(plainBodyResult, msg.data()) : createMultipartAlternativeContent(plainBodyResult, htmlBodyResult, msg.data()); mainTextPart->assemble(); msg->setBody(mainTextPart->encodedBody()); msg->setHeader(mainTextPart->contentType()); msg->setHeader(mainTextPart->contentTransferEncoding()); //FIXME this does more harm than good right now. msg->assemble(); callback(msg); }); }); } void MailTemplates::forward(const KMime::Message::Ptr &origMsg, const std::function &callback) { MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(origMsg.data()); otp.decryptAndVerify(); KMime::Message::Ptr wrapperMsg(new KMime::Message); wrapperMsg->to()->clear(); wrapperMsg->cc()->clear(); // Decrypt the original message, it will be encrypted again in the composer // for the right recipient KMime::Message::Ptr forwardedMessage(new KMime::Message()); if (isEncrypted(origMsg.data())) { qDebug() << "Original message was encrypted, decrypting it"; auto htmlContent = otp.htmlContent(); KMime::Content *recreatedMsg = htmlContent.isEmpty() ? createPlainPartContent(otp.plainTextContent()) : createMultipartAlternativeContent(otp.plainTextContent(), htmlContent); KMime::Message::Ptr tmpForwardedMessage; auto attachments = otp.collectAttachmentParts(); if (!attachments.isEmpty()) { QVector contents = {recreatedMsg}; for (const auto &attachment : attachments) { //Copy the node, to avoid deleting the parts node. auto c = new KMime::Content; c->setContent(attachment->node()->encodedContent()); c->parse(); contents.append(c); } auto msg = createMultipartMixedContent(contents); tmpForwardedMessage.reset(KMime::contentToMessage(msg)); } else { tmpForwardedMessage.reset(KMime::contentToMessage(recreatedMsg)); } origMsg->contentType()->fromUnicodeString(tmpForwardedMessage->contentType()->asUnicodeString(), "utf-8"); origMsg->assemble(); forwardedMessage->setHead(origMsg->head()); forwardedMessage->setBody(tmpForwardedMessage->encodedBody()); forwardedMessage->parse(); } else { qDebug() << "Original message was not encrypted, using it as-is"; forwardedMessage = origMsg; } auto partList = otp.collectContentParts(); if (partList.isEmpty()) { Q_ASSERT(false); callback({}); return; } auto part = partList[0]; Q_ASSERT(part); const auto subjectHeader = part->header(KMime::Headers::Subject::staticType()); const auto subject = asUnicodeString(subjectHeader); const QByteArray refStr = getRefStr( as7BitString(part->header(KMime::Headers::References::staticType())), as7BitString(part->header(KMime::Headers::MessageID::staticType())) ); wrapperMsg->subject()->fromUnicodeString(forwardSubject(subject), "utf-8"); if (!refStr.isEmpty()) { wrapperMsg->references()->fromUnicodeString(QString::fromLocal8Bit(refStr), "utf-8"); } KMime::Content *fwdAttachment = new KMime::Content; fwdAttachment->contentDisposition()->setDisposition(KMime::Headers::CDinline); fwdAttachment->contentType()->setMimeType("message/rfc822"); fwdAttachment->contentDisposition()->setFilename(subject + u".eml"_s); fwdAttachment->setBody(KMime::CRLFtoLF(forwardedMessage->encodedContent(false))); - wrapperMsg->addContent(fwdAttachment); + wrapperMsg->appendContent(fwdAttachment); wrapperMsg->assemble(); callback(wrapperMsg); } QString MailTemplates::plaintextContent(const KMime::Message::Ptr &msg) { MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(msg.data()); otp.decryptAndVerify(); const auto plain = otp.plainTextContent(); if (plain.isEmpty()) { //Maybe not as good as the webengine version, but works at least for simple html content return toPlainText(otp.htmlContent()); } return plain; } QString MailTemplates::body(const KMime::Message::Ptr &msg, bool &isHtml) { MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(msg.data()); otp.decryptAndVerify(); const auto html = otp.htmlContent(); if (html.isEmpty()) { isHtml = false; return otp.plainTextContent(); } isHtml = true; return html; } -static KMime::Content *createAttachmentPart(const QByteArray &content, const QString &filename, bool isInline, const QByteArray &mimeType, const QString &name, bool base64Encode = true) -{ - - KMime::Content *part = new KMime::Content; - part->contentDisposition(true)->setFilename(filename); - if (isInline) { - part->contentDisposition(true)->setDisposition(KMime::Headers::CDinline); - } else { - part->contentDisposition(true)->setDisposition(KMime::Headers::CDattachment); - } - - part->contentType(true)->setMimeType(mimeType); - if (!name.isEmpty()) { - part->contentType(true)->setName(name, "utf-8"); - } - if(base64Encode) { - part->contentTransferEncoding(true)->setEncoding(KMime::Headers::CEbase64); - } - part->setBody(content); - return part; -} - -static KMime::Content *createBodyPart(const QString &body, bool htmlBody) { - if (htmlBody) { - return createMultipartAlternativeContent(toPlainText(body), body); - } - return createPlainPartContent(body); -} - static KMime::Types::Mailbox::List stringListToMailboxes(const QStringList &list) { KMime::Types::Mailbox::List mailboxes; for (const auto &s : list) { KMime::Types::Mailbox mb; mb.fromUnicodeString(s); if (mb.hasAddress()) { mailboxes << mb; } else { qWarning() << "Got an invalid address: " << s << list; Q_ASSERT(false); } } return mailboxes; } static void setRecipients(KMime::Message &message, const Recipients &recipients) { message.to(true)->clear(); for (const auto &mb : stringListToMailboxes(recipients.to)) { message.to()->addAddress(mb); } message.cc(true)->clear(); for (const auto &mb : stringListToMailboxes(recipients.cc)) { message.cc()->addAddress(mb); } message.bcc(true)->clear(); for (const auto &mb : stringListToMailboxes(recipients.bcc)) { message.bcc()->addAddress(mb); } } KMime::Message::Ptr MailTemplates::createIMipMessage( const QString &from, const Recipients &recipients, const QString &subject, const QString &body, const QString &attachment) { KMime::Message::Ptr message = KMime::Message::Ptr( new KMime::Message ); message->contentTransferEncoding()->clear(); // 7Bit, decoded. // Set the headers message->userAgent()->fromUnicodeString(QStringLiteral("%1/%2(%3)").arg(QString::fromLocal8Bit("GPGOL.js")).arg(u"0.1"_s).arg(QSysInfo::prettyProductName()), "utf-8"); message->from()->fromUnicodeString(from, "utf-8"); setRecipients(*message, recipients); message->date()->setDateTime(QDateTime::currentDateTime()); message->subject()->fromUnicodeString(subject, "utf-8"); message->contentType()->setMimeType("multipart/alternative"); message->contentType()->setBoundary(KMime::multiPartBoundary()); // Set the first multipart, the body message. KMime::Content *bodyMessage = new KMime::Content{message.data()}; bodyMessage->contentType()->setMimeType("text/plain"); bodyMessage->contentType()->setCharset("utf-8"); bodyMessage->contentTransferEncoding()->setEncoding(KMime::Headers::CEbase64); bodyMessage->setBody(KMime::CRLFtoLF(body.toUtf8())); message->appendContent(bodyMessage); // Set the second multipart, the attachment. KMime::Content *attachMessage = new KMime::Content{message.data()}; attachMessage->contentDisposition()->setDisposition(KMime::Headers::CDattachment); attachMessage->contentType()->setMimeType("text/calendar"); attachMessage->contentType()->setCharset("utf-8"); attachMessage->contentType()->setName(QLatin1String("event.ics"), "utf-8"); attachMessage->contentType()->setParameter(QLatin1String("method"), QLatin1String("REPLY")); attachMessage->contentTransferEncoding()->setEncoding(KMime::Headers::CEbase64); attachMessage->setBody(KMime::CRLFtoLF(attachment.toUtf8())); message->appendContent(attachMessage); // Job done, attach the both multiparts and assemble the message. message->assemble(); return message; } diff --git a/server/editor/util.cpp b/server/editor/util.cpp index 7a4ed92..79f9551 100644 --- a/server/editor/util.cpp +++ b/server/editor/util.cpp @@ -1,400 +1,400 @@ /* SPDX-FileCopyrightText: 2009 Constantin Berzan SPDX-FileCopyrightText: 2009 Klaralvdalens Datakonsult AB, a KDAB Group company, info@kdab.net SPDX-FileCopyrightText: 2009 Leo Franchi Parts based on KMail code by: SPDX-License-Identifier: LGPL-2.0-or-later */ #include "util.h" #include "util_p.h" #include #include #include #include #include "editor_debug.h" #include #include #include #include #include #include "job/singlepartjob.h" #include "composer.h" static QString stripOffPrefixes(const QString &subject) { const QStringList replyPrefixes = { QStringLiteral("Re\\s*:"), QStringLiteral("Re\\[\\d+\\]:"), QStringLiteral("Re\\d+:"), }; const QStringList forwardPrefixes = { QStringLiteral("Fwd:"), QStringLiteral("FW:<"), }; const QStringList prefixRegExps = replyPrefixes + forwardPrefixes; // construct a big regexp that // 1. is anchored to the beginning of str (sans whitespace) // 2. matches at least one of the part regexps in prefixRegExps const QString bigRegExp = QStringLiteral("^(?:\\s+|(?:%1))+\\s*").arg(prefixRegExps.join(QStringLiteral(")|(?:"))); static QRegularExpression regex; if (regex.pattern() != bigRegExp) { // the prefixes have changed, so update the regexp regex.setPattern(bigRegExp); regex.setPatternOptions(QRegularExpression::CaseInsensitiveOption); } if (regex.isValid()) { QRegularExpressionMatch match = regex.match(subject); if (match.hasMatch()) { return subject.mid(match.capturedEnd(0)); } } else { qCWarning(EDITOR_LOG) << "bigRegExp = \"" << bigRegExp << "\"\n" << "prefix regexp is invalid!"; } return subject; } KMime::Content *setBodyAndCTE(QByteArray &encodedBody, KMime::Headers::ContentType *contentType, KMime::Content *ret) { MessageComposer::Composer composer; MessageComposer::SinglepartJob cteJob(&composer); cteJob.contentType()->setMimeType(contentType->mimeType()); cteJob.contentType()->setCharset(contentType->charset()); cteJob.setData(encodedBody); cteJob.exec(); cteJob.content()->assemble(); ret->contentTransferEncoding()->setEncoding(cteJob.contentTransferEncoding()->encoding()); ret->setBody(cteJob.content()->encodedBody()); return ret; } KMime::Content *MessageComposer::Util::composeHeadersAndBody(KMime::Content *orig, QByteArray encodedBody, Kleo::CryptoMessageFormat format, bool sign, const QByteArray &hashAlgo) { auto result = new KMime::Content; // called should have tested that the signing/encryption failed Q_ASSERT(!encodedBody.isEmpty()); if (!(format & Kleo::InlineOpenPGPFormat)) { // make a MIME message qCDebug(EDITOR_LOG) << "making MIME message, format:" << format; makeToplevelContentType(result, format, sign, hashAlgo); if (makeMultiMime(format, sign)) { // sign/enc PGPMime, sign SMIME const QByteArray boundary = KMime::multiPartBoundary(); result->contentType()->setBoundary(boundary); result->assemble(); // qCDebug(EDITOR_LOG) << "processed header:" << result->head(); // Build the encapsulated MIME parts. // Build a MIME part holding the code information // taking the body contents returned in ciphertext. auto code = new KMime::Content; setNestedContentType(code, format, sign); setNestedContentDisposition(code, format, sign); if (sign) { // sign PGPMime, sign SMIME if (format & Kleo::AnySMIME) { // sign SMIME auto ct = code->contentTransferEncoding(); // create ct->setEncoding(KMime::Headers::CEbase64); ct->needToEncode(); code->setBody(encodedBody); } else { // sign PGPMmime setBodyAndCTE(encodedBody, orig->contentType(), code); } - result->addContent(orig); - result->addContent(code); + result->appendContent(orig); + result->appendContent(code); } else { // enc PGPMime setBodyAndCTE(encodedBody, orig->contentType(), code); // Build a MIME part holding the version information // taking the body contents returned in // structuring.data.bodyTextVersion. auto vers = new KMime::Content; vers->contentType()->setMimeType("application/pgp-encrypted"); vers->contentDisposition()->setDisposition(KMime::Headers::CDattachment); vers->contentTransferEncoding()->setEncoding(KMime::Headers::CE7Bit); vers->setBody("Version: 1"); - result->addContent(vers); - result->addContent(code); + result->appendContent(vers); + result->appendContent(code); } } else { // enc SMIME, sign/enc SMIMEOpaque result->contentTransferEncoding()->setEncoding(KMime::Headers::CEbase64); auto ct = result->contentDisposition(); // Create ct->setDisposition(KMime::Headers::CDattachment); ct->setFilename(QStringLiteral("smime.p7m")); result->assemble(); // qCDebug(EDITOR_LOG) << "processed header:" << result->head(); result->setBody(encodedBody); } } else { // sign/enc PGPInline result->setHead(orig->head()); result->parse(); // fixing ContentTransferEncoding setBodyAndCTE(encodedBody, orig->contentType(), result); } return result; } // set the correct top-level ContentType on the message void MessageComposer::Util::makeToplevelContentType(KMime::Content *content, Kleo::CryptoMessageFormat format, bool sign, const QByteArray &hashAlgo) { switch (format) { default: case Kleo::InlineOpenPGPFormat: case Kleo::OpenPGPMIMEFormat: { auto ct = content->contentType(); // Create if (sign) { ct->setMimeType(QByteArrayLiteral("multipart/signed")); ct->setParameter(QStringLiteral("protocol"), QStringLiteral("application/pgp-signature")); ct->setParameter(QStringLiteral("micalg"), QString::fromLatin1(QByteArray(QByteArrayLiteral("pgp-") + hashAlgo)).toLower()); } else { ct->setMimeType(QByteArrayLiteral("multipart/encrypted")); ct->setParameter(QStringLiteral("protocol"), QStringLiteral("application/pgp-encrypted")); } } return; case Kleo::SMIMEFormat: { if (sign) { auto ct = content->contentType(); // Create qCDebug(EDITOR_LOG) << "setting headers for SMIME"; ct->setMimeType(QByteArrayLiteral("multipart/signed")); ct->setParameter(QStringLiteral("protocol"), QStringLiteral("application/pkcs7-signature")); ct->setParameter(QStringLiteral("micalg"), QString::fromLatin1(hashAlgo).toLower()); return; } // fall through (for encryption, there's no difference between // SMIME and SMIMEOpaque, since there is no mp/encrypted for // S/MIME) } [[fallthrough]]; case Kleo::SMIMEOpaqueFormat: qCDebug(EDITOR_LOG) << "setting headers for SMIME/opaque"; auto ct = content->contentType(); // Create ct->setMimeType(QByteArrayLiteral("application/pkcs7-mime")); if (sign) { ct->setParameter(QStringLiteral("smime-type"), QStringLiteral("signed-data")); } else { ct->setParameter(QStringLiteral("smime-type"), QStringLiteral("enveloped-data")); } ct->setParameter(QStringLiteral("name"), QStringLiteral("smime.p7m")); } } void MessageComposer::Util::setNestedContentType(KMime::Content *content, Kleo::CryptoMessageFormat format, bool sign) { switch (format) { case Kleo::OpenPGPMIMEFormat: { auto ct = content->contentType(); // Create if (sign) { ct->setMimeType(QByteArrayLiteral("application/pgp-signature")); ct->setParameter(QStringLiteral("name"), QStringLiteral("signature.asc")); content->contentDescription()->from7BitString("This is a digitally signed message part."); } else { ct->setMimeType(QByteArrayLiteral("application/octet-stream")); } } return; case Kleo::SMIMEFormat: { if (sign) { auto ct = content->contentType(); // Create ct->setMimeType(QByteArrayLiteral("application/pkcs7-signature")); ct->setParameter(QStringLiteral("name"), QStringLiteral("smime.p7s")); return; } } [[fallthrough]]; // fall through: default: case Kleo::InlineOpenPGPFormat: case Kleo::SMIMEOpaqueFormat:; } } void MessageComposer::Util::setNestedContentDisposition(KMime::Content *content, Kleo::CryptoMessageFormat format, bool sign) { auto ct = content->contentDisposition(); if (!sign && format & Kleo::OpenPGPMIMEFormat) { ct->setDisposition(KMime::Headers::CDinline); ct->setFilename(QStringLiteral("msg.asc")); } else if (sign && format & Kleo::SMIMEFormat) { ct->setDisposition(KMime::Headers::CDattachment); ct->setFilename(QStringLiteral("smime.p7s")); } } bool MessageComposer::Util::makeMultiMime(Kleo::CryptoMessageFormat format, bool sign) { switch (format) { default: case Kleo::InlineOpenPGPFormat: case Kleo::SMIMEOpaqueFormat: return false; case Kleo::OpenPGPMIMEFormat: return true; case Kleo::SMIMEFormat: return sign; // only on sign - there's no mp/encrypted for S/MIME } } QByteArray MessageComposer::Util::selectCharset(const QList &charsets, const QString &text) { for (const QByteArray &name : charsets) { // We use KCharsets::codecForName() instead of QTextCodec::codecForName() here, because // the former knows us-ascii is latin1. QStringEncoder codec(name.constData()); if (!codec.isValid()) { qCWarning(EDITOR_LOG) << "Could not get text codec for charset" << name; continue; } if (codec.encode(text); !codec.hasError()) { // Special check for us-ascii (needed because us-ascii is not exactly latin1). if (name == "us-ascii" && !KMime::isUsAscii(text)) { continue; } qCDebug(EDITOR_LOG) << "Chosen charset" << name; return name; } } qCDebug(EDITOR_LOG) << "No appropriate charset found."; return {}; } QStringList MessageComposer::Util::AttachmentKeywords() { return i18nc( "comma-separated list of keywords that are used to detect whether " "the user forgot to attach his attachment. Do not add space between words.", "attachment,attached") .split(QLatin1Char(',')); } QString MessageComposer::Util::cleanedUpHeaderString(const QString &s) { // remove invalid characters from the header strings QString res(s); res.remove(QChar::fromLatin1('\r')); res.replace(QChar::fromLatin1('\n'), QLatin1Char(' ')); return res.trimmed(); } KMime::Content *MessageComposer::Util::findTypeInMessage(KMime::Content *data, const QByteArray &mimeType, const QByteArray &subType) { if (!data->contentType()->isEmpty()) { if (mimeType.isEmpty() || subType.isEmpty()) { return data; } if ((mimeType == data->contentType()->mediaType()) && (subType == data->contentType(false)->subType())) { return data; } } const auto contents = data->contents(); for (auto child : contents) { if ((!child->contentType()->isEmpty()) && (mimeType == child->contentType()->mimeType()) && (subType == child->contentType()->subType())) { return child; } auto ret = findTypeInMessage(child, mimeType, subType); if (ret) { return ret; } } return nullptr; } bool MessageComposer::Util::hasMissingAttachments(const QStringList &attachmentKeywords, QTextDocument *doc, const QString &subj) { if (!doc) { return false; } QStringList attachWordsList = attachmentKeywords; QRegularExpression rx(QLatin1String("\\b") + attachWordsList.join(QLatin1String("\\b|\\b")) + QLatin1String("\\b"), QRegularExpression::CaseInsensitiveOption); // check whether the subject contains one of the attachment key words // unless the message is a reply or a forwarded message bool gotMatch = (stripOffPrefixes(subj) == subj) && (rx.match(subj).hasMatch()); if (!gotMatch) { // check whether the non-quoted text contains one of the attachment key // words static QRegularExpression quotationRx(QStringLiteral("^([ \\t]*([|>:}#]|[A-Za-z]+>))+")); QTextBlock end(doc->end()); for (QTextBlock it = doc->begin(); it != end; it = it.next()) { const QString line = it.text(); gotMatch = (!quotationRx.match(line).hasMatch()) && (rx.match(line).hasMatch()); if (gotMatch) { break; } } } if (!gotMatch) { return false; } return true; } static QStringList encodeIdn(const QStringList &emails) { QStringList encoded; encoded.reserve(emails.count()); for (const QString &email : emails) { encoded << KEmailAddress::normalizeAddressesAndEncodeIdn(email); } return encoded; } QStringList MessageComposer::Util::cleanEmailList(const QStringList &emails) { QStringList clean; clean.reserve(emails.count()); for (const QString &email : emails) { clean << KEmailAddress::extractEmailAddress(email); } return clean; } QStringList MessageComposer::Util::cleanUpEmailListAndEncoding(const QStringList &emails) { return cleanEmailList(encodeIdn(emails)); } void MessageComposer::Util::addCustomHeaders(const KMime::Message::Ptr &message, const QMap &custom) { QMapIterator customHeader(custom); while (customHeader.hasNext()) { customHeader.next(); auto header = new KMime::Headers::Generic(customHeader.key().constData()); header->fromUnicodeString(customHeader.value(), "utf-8"); message->setHeader(header); } }