File indexing completed on 2024-06-23 05:18:24

0001 /*
0002  * This file is part of KMail.
0003  * SPDX-FileCopyrightText: 2009 Constantin Berzan <exit3219@gmail.com>
0004  *
0005  * Parts based on KMail code by:
0006  * Various authors.
0007  *
0008  * SPDX-License-Identifier: GPL-2.0-or-later
0009  */
0010 
0011 #include "attachmentcontrollerbase.h"
0012 
0013 #include "MessageComposer/AttachmentClipBoardJob"
0014 #include "MessageComposer/AttachmentFromPublicKeyJob"
0015 #include "MessageComposer/AttachmentJob"
0016 #include "MessageComposer/AttachmentVcardFromAddressBookJob"
0017 #include "MessageComposer/Composer"
0018 #include "MessageComposer/GlobalPart"
0019 #include <MessageComposer/AttachmentModel>
0020 
0021 #include <MessageViewer/MessageViewerUtil>
0022 
0023 #include <MimeTreeParser/NodeHelper>
0024 
0025 #include <MessageCore/StringUtil>
0026 
0027 #include <Akonadi/ItemFetchJob>
0028 #include <KIO/JobUiDelegateFactory>
0029 #include <QIcon>
0030 
0031 #include <QMenu>
0032 #include <QPointer>
0033 #include <QTreeView>
0034 
0035 #include "messagecomposer_debug.h"
0036 #include <KActionCollection>
0037 #include <KActionMenu>
0038 #include <KEncodingFileDialog>
0039 #include <KFileItemActions>
0040 #include <KIO/ApplicationLauncherJob>
0041 #include <KLocalizedString>
0042 #include <KMessageBox>
0043 #include <QAction>
0044 #include <QMimeDatabase>
0045 #include <QPushButton>
0046 #include <QTemporaryFile>
0047 
0048 #include <Libkleo/KeySelectionDialog>
0049 #include <QGpgME/Protocol>
0050 
0051 #include "attachment/attachmentfromurljob.h"
0052 #include "messagecore/attachmentupdatejob.h"
0053 #include "settings/messagecomposersettings.h"
0054 #include <Akonadi/EmailAddressSelectionDialog>
0055 #include <Akonadi/EmailAddressSelectionWidget>
0056 #include <KIO/JobUiDelegate>
0057 #include <KIO/OpenUrlJob>
0058 #include <KIO/StoredTransferJob>
0059 #include <MessageCore/AttachmentCompressJob>
0060 #include <MessageCore/AttachmentFromUrlUtils>
0061 #include <MessageCore/AttachmentPropertiesDialog>
0062 
0063 #include <KJob>
0064 #include <KMime/Content>
0065 
0066 #include <QActionGroup>
0067 #include <QFileDialog>
0068 
0069 using namespace MessageComposer;
0070 using namespace MessageCore;
0071 
0072 class MessageComposer::AttachmentControllerBase::AttachmentControllerBasePrivate
0073 {
0074 public:
0075     explicit AttachmentControllerBasePrivate(AttachmentControllerBase *qq);
0076     ~AttachmentControllerBasePrivate();
0077 
0078     void attachmentRemoved(const AttachmentPart::Ptr &part); // slot
0079     void compressJobResult(KJob *job); // slot
0080     void loadJobResult(KJob *job); // slot
0081     void openSelectedAttachments(); // slot
0082     void viewSelectedAttachments(); // slot
0083     void editSelectedAttachment(); // slot
0084     void editSelectedAttachmentWith(); // slot
0085     void removeSelectedAttachments(); // slot
0086     void saveSelectedAttachmentAs(); // slot
0087     void selectedAttachmentProperties(); // slot
0088     void editDone(MessageComposer::EditorWatcher *watcher); // slot
0089     void attachPublicKeyJobResult(KJob *job); // slot
0090     void slotAttachmentContentCreated(KJob *job); // slot
0091     void addAttachmentPart(AttachmentPart::Ptr part);
0092     void attachVcardFromAddressBook(KJob *job); // slot
0093     void attachClipBoardElement(KJob *job);
0094     void selectedAllAttachment();
0095     void createOpenWithMenu(QMenu *topMenu, const AttachmentPart::Ptr &part);
0096     void reloadAttachment();
0097     void updateJobResult(KJob *);
0098 
0099     AttachmentPart::List selectedParts;
0100     AttachmentControllerBase *const q;
0101     MessageComposer::AttachmentModel *model = nullptr;
0102     QWidget *wParent = nullptr;
0103     QHash<MessageComposer::EditorWatcher *, AttachmentPart::Ptr> editorPart;
0104     QHash<MessageComposer::EditorWatcher *, QTemporaryFile *> editorTempFile;
0105 
0106     KActionCollection *mActionCollection = nullptr;
0107     QAction *attachPublicKeyAction = nullptr;
0108     QAction *attachMyPublicKeyAction = nullptr;
0109     QAction *openContextAction = nullptr;
0110     QAction *viewContextAction = nullptr;
0111     QAction *editContextAction = nullptr;
0112     QAction *editWithContextAction = nullptr;
0113     QAction *removeAction = nullptr;
0114     QAction *removeContextAction = nullptr;
0115     QAction *saveAsAction = nullptr;
0116     QAction *saveAsContextAction = nullptr;
0117     QAction *propertiesAction = nullptr;
0118     QAction *propertiesContextAction = nullptr;
0119     QAction *addAttachmentFileAction = nullptr;
0120     QAction *addAttachmentDirectoryAction = nullptr;
0121     QAction *addContextAction = nullptr;
0122     QAction *selectAllAction = nullptr;
0123     KActionMenu *attachmentMenu = nullptr;
0124     QAction *addOwnVcardAction = nullptr;
0125     QAction *reloadAttachmentAction = nullptr;
0126     QAction *attachVCardsAction = nullptr;
0127     QAction *attachClipBoardAction = nullptr;
0128     // If part p is compressed, uncompressedParts[p] is the uncompressed part.
0129     QHash<AttachmentPart::Ptr, AttachmentPart::Ptr> uncompressedParts;
0130     bool encryptEnabled = false;
0131     bool signEnabled = false;
0132 };
0133 
0134 AttachmentControllerBase::AttachmentControllerBasePrivate::AttachmentControllerBasePrivate(AttachmentControllerBase *qq)
0135     : q(qq)
0136 {
0137 }
0138 
0139 AttachmentControllerBase::AttachmentControllerBasePrivate::~AttachmentControllerBasePrivate() = default;
0140 
0141 void AttachmentControllerBase::setSelectedParts(const AttachmentPart::List &selectedParts)
0142 {
0143     d->selectedParts = selectedParts;
0144     const int selectedCount = selectedParts.count();
0145     const bool enableEditAction = (selectedCount == 1) && (!selectedParts.first()->isMessageOrMessageCollection());
0146 
0147     d->openContextAction->setEnabled(selectedCount > 0);
0148     d->viewContextAction->setEnabled(selectedCount > 0);
0149     d->editContextAction->setEnabled(enableEditAction);
0150     d->editWithContextAction->setEnabled(enableEditAction);
0151     d->removeAction->setEnabled(selectedCount > 0);
0152     d->removeContextAction->setEnabled(selectedCount > 0);
0153     d->saveAsAction->setEnabled(selectedCount == 1);
0154     d->saveAsContextAction->setEnabled(selectedCount == 1);
0155     d->propertiesAction->setEnabled(selectedCount == 1);
0156     d->propertiesContextAction->setEnabled(selectedCount == 1);
0157 }
0158 
0159 void AttachmentControllerBase::AttachmentControllerBasePrivate::attachmentRemoved(const AttachmentPart::Ptr &part)
0160 {
0161     uncompressedParts.remove(part);
0162 }
0163 
0164 void AttachmentControllerBase::AttachmentControllerBasePrivate::compressJobResult(KJob *job)
0165 {
0166     if (job->error()) {
0167         KMessageBox::error(wParent, job->errorString(), i18nc("@title:window", "Failed to compress attachment"));
0168         return;
0169     }
0170 
0171     auto ajob = qobject_cast<AttachmentCompressJob *>(job);
0172     Q_ASSERT(ajob);
0173     AttachmentPart::Ptr originalPart = ajob->originalPart();
0174     AttachmentPart::Ptr compressedPart = ajob->compressedPart();
0175 
0176     if (ajob->isCompressedPartLarger()) {
0177         const int result = KMessageBox::questionTwoActions(wParent,
0178                                                            i18n("The compressed attachment is larger than the original. "
0179                                                                 "Do you want to keep the original one?"),
0180                                                            QString(/*caption*/),
0181                                                            KGuiItem(i18nc("Do not compress", "Keep")),
0182                                                            KGuiItem(i18n("Compress")));
0183         if (result == KMessageBox::ButtonCode::PrimaryAction) {
0184             // The user has chosen to keep the uncompressed file.
0185             return;
0186         }
0187     }
0188 
0189     qCDebug(MESSAGECOMPOSER_LOG) << "Replacing uncompressed part in model.";
0190     uncompressedParts[compressedPart] = originalPart;
0191     bool ok = model->replaceAttachment(originalPart, compressedPart);
0192     if (!ok) {
0193         // The attachment was removed from the model while we were compressing.
0194         qCDebug(MESSAGECOMPOSER_LOG) << "Compressed a zombie.";
0195     }
0196 }
0197 
0198 void AttachmentControllerBase::AttachmentControllerBasePrivate::loadJobResult(KJob *job)
0199 {
0200     if (job->error()) {
0201         KMessageBox::error(wParent, job->errorString(), i18n("Failed to attach file"));
0202         return;
0203     }
0204 
0205     auto ajob = qobject_cast<AttachmentLoadJob *>(job);
0206     Q_ASSERT(ajob);
0207     AttachmentPart::Ptr part = ajob->attachmentPart();
0208     q->addAttachment(part);
0209 }
0210 
0211 void AttachmentControllerBase::AttachmentControllerBasePrivate::openSelectedAttachments()
0212 {
0213     Q_ASSERT(selectedParts.count() >= 1);
0214     for (const AttachmentPart::Ptr &part : std::as_const(selectedParts)) {
0215         q->openAttachment(part);
0216     }
0217 }
0218 
0219 void AttachmentControllerBase::AttachmentControllerBasePrivate::viewSelectedAttachments()
0220 {
0221     Q_ASSERT(selectedParts.count() >= 1);
0222     for (const AttachmentPart::Ptr &part : std::as_const(selectedParts)) {
0223         q->viewAttachment(part);
0224     }
0225 }
0226 
0227 void AttachmentControllerBase::AttachmentControllerBasePrivate::editSelectedAttachment()
0228 {
0229     Q_ASSERT(selectedParts.count() == 1);
0230     q->editAttachment(selectedParts.constFirst(), MessageComposer::EditorWatcher::NoOpenWithDialog);
0231 }
0232 
0233 void AttachmentControllerBase::AttachmentControllerBasePrivate::editSelectedAttachmentWith()
0234 {
0235     Q_ASSERT(selectedParts.count() == 1);
0236     q->editAttachment(selectedParts.constFirst(), MessageComposer::EditorWatcher::OpenWithDialog);
0237 }
0238 
0239 void AttachmentControllerBase::AttachmentControllerBasePrivate::removeSelectedAttachments()
0240 {
0241     Q_ASSERT(selectedParts.count() >= 1);
0242     // We must store list, otherwise when we remove it changes selectedParts (as selection changed) => it will crash.
0243     const AttachmentPart::List toRemove = selectedParts;
0244     for (const AttachmentPart::Ptr &part : toRemove) {
0245         model->removeAttachment(part);
0246     }
0247 }
0248 
0249 void AttachmentControllerBase::AttachmentControllerBasePrivate::saveSelectedAttachmentAs()
0250 {
0251     Q_ASSERT(selectedParts.count() == 1);
0252     q->saveAttachmentAs(selectedParts.constFirst());
0253 }
0254 
0255 void AttachmentControllerBase::AttachmentControllerBasePrivate::selectedAttachmentProperties()
0256 {
0257     Q_ASSERT(selectedParts.count() == 1);
0258     q->attachmentProperties(selectedParts.constFirst());
0259 }
0260 
0261 void AttachmentControllerBase::AttachmentControllerBasePrivate::reloadAttachment()
0262 {
0263     Q_ASSERT(selectedParts.count() == 1);
0264     auto ajob = new AttachmentUpdateJob(selectedParts.constFirst(), q);
0265     connect(ajob, &AttachmentUpdateJob::result, q, [this](KJob *job) {
0266         updateJobResult(job);
0267     });
0268     ajob->start();
0269 }
0270 
0271 void AttachmentControllerBase::AttachmentControllerBasePrivate::updateJobResult(KJob *job)
0272 {
0273     if (job->error()) {
0274         KMessageBox::error(wParent, job->errorString(), i18n("Failed to reload attachment"));
0275         return;
0276     }
0277     auto ajob = qobject_cast<AttachmentUpdateJob *>(job);
0278     Q_ASSERT(ajob);
0279     AttachmentPart::Ptr originalPart = ajob->originalPart();
0280     AttachmentPart::Ptr updatedPart = ajob->updatedPart();
0281 
0282     attachmentRemoved(originalPart);
0283     bool ok = model->replaceAttachment(originalPart, updatedPart);
0284     if (!ok) {
0285         // The attachment was removed from the model while we were compressing.
0286         qCDebug(MESSAGECOMPOSER_LOG) << "Updated a zombie.";
0287     }
0288 }
0289 
0290 void AttachmentControllerBase::AttachmentControllerBasePrivate::editDone(MessageComposer::EditorWatcher *watcher)
0291 {
0292     AttachmentPart::Ptr part = editorPart.take(watcher);
0293     Q_ASSERT(part);
0294     QTemporaryFile *tempFile = editorTempFile.take(watcher);
0295     Q_ASSERT(tempFile);
0296     if (watcher->fileChanged()) {
0297         qCDebug(MESSAGECOMPOSER_LOG) << "File has changed.";
0298         const QString name = watcher->url().path();
0299         QFile file(name);
0300         if (file.open(QIODevice::ReadOnly)) {
0301             const QByteArray data = file.readAll();
0302             part->setData(data);
0303             model->updateAttachment(part);
0304         }
0305     }
0306 
0307     delete tempFile;
0308     // The watcher deletes itself.
0309 }
0310 
0311 void AttachmentControllerBase::AttachmentControllerBasePrivate::createOpenWithMenu(QMenu *topMenu, const AttachmentPart::Ptr &part)
0312 {
0313     const QString contentTypeStr = QString::fromLatin1(part->mimeType());
0314     const KService::List offers = KFileItemActions::associatedApplications(QStringList() << contentTypeStr);
0315     if (!offers.isEmpty()) {
0316         QMenu *menu = topMenu;
0317         auto actionGroup = new QActionGroup(menu);
0318         connect(actionGroup, &QActionGroup::triggered, q, &AttachmentControllerBase::slotOpenWithAction);
0319 
0320         if (offers.count() > 1) { // submenu 'open with'
0321             menu = new QMenu(i18nc("@title:menu", "&Open With"), topMenu);
0322             menu->menuAction()->setObjectName(QLatin1StringView("openWith_submenu")); // for the unittest
0323             topMenu->addMenu(menu);
0324         }
0325         // qCDebug(MESSAGECOMPOSER_LOG) << offers.count() << "offers" << topMenu << menu;
0326 
0327         KService::List::ConstIterator it = offers.constBegin();
0328         KService::List::ConstIterator end = offers.constEnd();
0329         for (; it != end; ++it) {
0330             QAction *act = MessageViewer::Util::createAppAction(*it,
0331                                                                 // no submenu -> prefix single offer
0332                                                                 menu == topMenu,
0333                                                                 actionGroup,
0334                                                                 menu);
0335             menu->addAction(act);
0336         }
0337 
0338         QString openWithActionName;
0339         if (menu != topMenu) { // submenu
0340             menu->addSeparator();
0341             openWithActionName = i18nc("@action:inmenu Open With", "&Other...");
0342         } else {
0343             openWithActionName = i18nc("@title:menu", "&Open With...");
0344         }
0345         auto openWithAct = new QAction(menu);
0346         openWithAct->setText(openWithActionName);
0347         QObject::connect(openWithAct, &QAction::triggered, q, &AttachmentControllerBase::slotOpenWithDialog);
0348         menu->addAction(openWithAct);
0349     } else { // no app offers -> Open With...
0350         auto act = new QAction(topMenu);
0351         act->setText(i18nc("@title:menu", "&Open With..."));
0352         QObject::connect(act, &QAction::triggered, q, &AttachmentControllerBase::slotOpenWithDialog);
0353         topMenu->addAction(act);
0354     }
0355 }
0356 
0357 void AttachmentControllerBase::exportPublicKey(const QString &fingerprint)
0358 {
0359     if (fingerprint.isEmpty() || !QGpgME::openpgp()) {
0360         qCWarning(MESSAGECOMPOSER_LOG) << "Tried to export key with empty fingerprint, or no OpenPGP.";
0361         return;
0362     }
0363 
0364     auto ajob = new MessageComposer::AttachmentFromPublicKeyJob(fingerprint, this);
0365     connect(ajob, &AttachmentFromPublicKeyJob::result, this, [this](KJob *job) {
0366         d->attachPublicKeyJobResult(job);
0367     });
0368     ajob->start();
0369 }
0370 
0371 void AttachmentControllerBase::AttachmentControllerBasePrivate::attachPublicKeyJobResult(KJob *job)
0372 {
0373     // The only reason we can't use loadJobResult() and need a separate method
0374     // is that we want to show the proper caption ("public key" instead of "file")...
0375 
0376     if (job->error()) {
0377         KMessageBox::error(wParent, job->errorString(), i18n("Failed to attach public key"));
0378         return;
0379     }
0380 
0381     Q_ASSERT(dynamic_cast<MessageComposer::AttachmentFromPublicKeyJob *>(job));
0382     auto ajob = static_cast<MessageComposer::AttachmentFromPublicKeyJob *>(job);
0383     AttachmentPart::Ptr part = ajob->attachmentPart();
0384     q->addAttachment(part);
0385 }
0386 
0387 void AttachmentControllerBase::AttachmentControllerBasePrivate::attachVcardFromAddressBook(KJob *job)
0388 {
0389     if (job->error()) {
0390         qCDebug(MESSAGECOMPOSER_LOG) << " Error during when get vCard";
0391         KMessageBox::error(wParent, job->errorString(), i18n("Failed to attach vCard"));
0392         return;
0393     }
0394 
0395     auto ajob = static_cast<MessageComposer::AttachmentVcardFromAddressBookJob *>(job);
0396     AttachmentPart::Ptr part = ajob->attachmentPart();
0397     q->addAttachment(part);
0398 }
0399 
0400 void AttachmentControllerBase::AttachmentControllerBasePrivate::attachClipBoardElement(KJob *job)
0401 {
0402     if (job->error()) {
0403         qCDebug(MESSAGECOMPOSER_LOG) << " Error during when get try to attach text from clipboard";
0404         KMessageBox::error(wParent, job->errorString(), i18n("Failed to attach text from clipboard"));
0405         return;
0406     }
0407 
0408     auto ajob = static_cast<MessageComposer::AttachmentClipBoardJob *>(job);
0409     AttachmentPart::Ptr part = ajob->attachmentPart();
0410     q->addAttachment(part);
0411 }
0412 
0413 static QTemporaryFile *dumpAttachmentToTempFile(const AttachmentPart::Ptr &part) // local
0414 {
0415     auto file = new QTemporaryFile;
0416     if (!file->open()) {
0417         qCCritical(MESSAGECOMPOSER_LOG) << "Could not open tempfile" << file->fileName();
0418         delete file;
0419         return nullptr;
0420     }
0421     if (file->write(part->data()) == -1) {
0422         qCCritical(MESSAGECOMPOSER_LOG) << "Could not dump attachment to tempfile.";
0423         delete file;
0424         return nullptr;
0425     }
0426     file->flush();
0427     return file;
0428 }
0429 
0430 AttachmentControllerBase::AttachmentControllerBase(MessageComposer::AttachmentModel *model, QWidget *wParent, KActionCollection *actionCollection)
0431     : QObject(wParent)
0432     , d(new AttachmentControllerBasePrivate(this))
0433 {
0434     d->model = model;
0435     connect(model, &MessageComposer::AttachmentModel::attachUrlsRequested, this, &AttachmentControllerBase::addAttachments);
0436     connect(model, &MessageComposer::AttachmentModel::attachmentRemoved, this, [this](const MessageCore::AttachmentPart::Ptr &attr) {
0437         d->attachmentRemoved(attr);
0438     });
0439     connect(model, &AttachmentModel::attachmentCompressRequested, this, &AttachmentControllerBase::compressAttachment);
0440     connect(model, &MessageComposer::AttachmentModel::encryptEnabled, this, &AttachmentControllerBase::setEncryptEnabled);
0441     connect(model, &MessageComposer::AttachmentModel::signEnabled, this, &AttachmentControllerBase::setSignEnabled);
0442 
0443     d->wParent = wParent;
0444     d->mActionCollection = actionCollection;
0445 }
0446 
0447 AttachmentControllerBase::~AttachmentControllerBase() = default;
0448 
0449 void AttachmentControllerBase::createActions()
0450 {
0451     // Create the actions.
0452     d->attachPublicKeyAction = new QAction(i18n("Attach &Public Key..."), this);
0453     connect(d->attachPublicKeyAction, &QAction::triggered, this, &AttachmentControllerBase::showAttachPublicKeyDialog);
0454 
0455     d->attachMyPublicKeyAction = new QAction(i18n("Attach &My Public Key"), this);
0456     connect(d->attachMyPublicKeyAction, &QAction::triggered, this, &AttachmentControllerBase::attachMyPublicKey);
0457 
0458     d->attachmentMenu = new KActionMenu(QIcon::fromTheme(QStringLiteral("mail-attachment")), i18n("Attach"), this);
0459     connect(d->attachmentMenu, &QAction::triggered, this, &AttachmentControllerBase::showAddAttachmentFileDialog);
0460     d->attachmentMenu->setPopupMode(QToolButton::DelayedPopup);
0461 
0462     d->addAttachmentFileAction = new QAction(QIcon::fromTheme(QStringLiteral("mail-attachment")), i18n("&Attach File..."), this);
0463     d->addAttachmentFileAction->setIconText(i18n("Attach"));
0464     d->addContextAction = new QAction(QIcon::fromTheme(QStringLiteral("mail-attachment")), i18n("Add Attachment..."), this);
0465     connect(d->addAttachmentFileAction, &QAction::triggered, this, &AttachmentControllerBase::showAddAttachmentFileDialog);
0466     connect(d->addContextAction, &QAction::triggered, this, &AttachmentControllerBase::showAddAttachmentFileDialog);
0467 
0468     d->addAttachmentDirectoryAction = new QAction(QIcon::fromTheme(QStringLiteral("mail-attachment")), i18n("&Attach Directory..."), this);
0469     d->addAttachmentDirectoryAction->setIconText(i18n("Attach"));
0470     connect(d->addAttachmentDirectoryAction, &QAction::triggered, this, &AttachmentControllerBase::showAddAttachmentCompressedDirectoryDialog);
0471 
0472     d->addOwnVcardAction = new QAction(i18n("Attach Own vCard"), this);
0473     d->addOwnVcardAction->setIconText(i18n("Own vCard"));
0474     d->addOwnVcardAction->setCheckable(true);
0475     connect(d->addOwnVcardAction, &QAction::triggered, this, &AttachmentControllerBase::addOwnVcard);
0476 
0477     d->attachVCardsAction = new QAction(QIcon::fromTheme(QStringLiteral("mail-attachment")), i18n("&Attach vCards..."), this);
0478     d->attachVCardsAction->setIconText(i18n("Attach"));
0479     connect(d->attachVCardsAction, &QAction::triggered, this, &AttachmentControllerBase::showAttachVcard);
0480 
0481     d->attachClipBoardAction = new QAction(QIcon::fromTheme(QStringLiteral("mail-attachment")), i18n("&Attach Text From Clipboard..."), this);
0482     d->attachClipBoardAction->setIconText(i18n("Attach Text From Clipboard"));
0483     connect(d->attachClipBoardAction, &QAction::triggered, this, &AttachmentControllerBase::showAttachClipBoard);
0484 
0485     d->attachmentMenu->addAction(d->addAttachmentFileAction);
0486     d->attachmentMenu->addAction(d->addAttachmentDirectoryAction);
0487     d->attachmentMenu->addSeparator();
0488     d->attachmentMenu->addAction(d->addOwnVcardAction);
0489     d->attachmentMenu->addSeparator();
0490     d->attachmentMenu->addAction(d->attachVCardsAction);
0491     d->attachmentMenu->addSeparator();
0492     d->attachmentMenu->addAction(d->attachClipBoardAction);
0493 
0494     d->removeAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18n("&Remove Attachment"), this);
0495     d->removeContextAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18n("Remove"), this); // FIXME need two texts. is there a better way?
0496     connect(d->removeAction, &QAction::triggered, this, [this]() {
0497         d->removeSelectedAttachments();
0498     });
0499     connect(d->removeContextAction, &QAction::triggered, this, [this]() {
0500         d->removeSelectedAttachments();
0501     });
0502 
0503     d->openContextAction = new QAction(i18nc("to open", "Open"), this);
0504     connect(d->openContextAction, &QAction::triggered, this, [this]() {
0505         d->openSelectedAttachments();
0506     });
0507 
0508     d->viewContextAction = new QAction(i18nc("to view", "View"), this);
0509     connect(d->viewContextAction, &QAction::triggered, this, [this]() {
0510         d->viewSelectedAttachments();
0511     });
0512 
0513     d->editContextAction = new QAction(i18nc("to edit", "Edit"), this);
0514     connect(d->editContextAction, &QAction::triggered, this, [this]() {
0515         d->editSelectedAttachment();
0516     });
0517 
0518     d->editWithContextAction = new QAction(i18n("Edit With..."), this);
0519     connect(d->editWithContextAction, &QAction::triggered, this, [this]() {
0520         d->editSelectedAttachmentWith();
0521     });
0522 
0523     d->saveAsAction = new QAction(QIcon::fromTheme(QStringLiteral("document-save-as")), i18n("&Save Attachment As..."), this);
0524     d->saveAsContextAction = new QAction(QIcon::fromTheme(QStringLiteral("document-save-as")), i18n("Save As..."), this);
0525     connect(d->saveAsAction, &QAction::triggered, this, [this]() {
0526         d->saveSelectedAttachmentAs();
0527     });
0528     connect(d->saveAsContextAction, &QAction::triggered, this, [this]() {
0529         d->saveSelectedAttachmentAs();
0530     });
0531 
0532     d->propertiesAction = new QAction(i18n("Attachment Pr&operties..."), this);
0533     d->propertiesContextAction = new QAction(i18n("Properties"), this);
0534     connect(d->propertiesAction, &QAction::triggered, this, [this]() {
0535         d->selectedAttachmentProperties();
0536     });
0537     connect(d->propertiesContextAction, &QAction::triggered, this, [this]() {
0538         d->selectedAttachmentProperties();
0539     });
0540 
0541     d->selectAllAction = new QAction(i18n("Select All"), this);
0542     connect(d->selectAllAction, &QAction::triggered, this, &AttachmentControllerBase::selectedAllAttachment);
0543 
0544     d->reloadAttachmentAction = new QAction(QIcon::fromTheme(QStringLiteral("view-refresh")), i18n("Reload"), this);
0545     connect(d->reloadAttachmentAction, &QAction::triggered, this, [this]() {
0546         d->reloadAttachment();
0547     });
0548 
0549     // Insert the actions into the composer window's menu.
0550     KActionCollection *collection = d->mActionCollection;
0551     collection->addAction(QStringLiteral("attach_public_key"), d->attachPublicKeyAction);
0552     collection->addAction(QStringLiteral("attach_my_public_key"), d->attachMyPublicKeyAction);
0553     collection->addAction(QStringLiteral("attach"), d->addAttachmentFileAction);
0554     collection->setDefaultShortcut(d->addAttachmentFileAction, QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_A));
0555     collection->addAction(QStringLiteral("attach_directory"), d->addAttachmentDirectoryAction);
0556 
0557     collection->addAction(QStringLiteral("remove"), d->removeAction);
0558     collection->addAction(QStringLiteral("attach_save"), d->saveAsAction);
0559     collection->addAction(QStringLiteral("attach_properties"), d->propertiesAction);
0560     collection->addAction(QStringLiteral("select_all_attachment"), d->selectAllAction);
0561     collection->addAction(QStringLiteral("attach_menu"), d->attachmentMenu);
0562     collection->addAction(QStringLiteral("attach_own_vcard"), d->addOwnVcardAction);
0563     collection->addAction(QStringLiteral("attach_vcards"), d->attachVCardsAction);
0564 
0565     setSelectedParts(AttachmentPart::List());
0566     Q_EMIT actionsCreated();
0567 }
0568 
0569 void AttachmentControllerBase::setEncryptEnabled(bool enabled)
0570 {
0571     d->encryptEnabled = enabled;
0572 }
0573 
0574 void AttachmentControllerBase::setSignEnabled(bool enabled)
0575 {
0576     d->signEnabled = enabled;
0577 }
0578 
0579 void AttachmentControllerBase::compressAttachment(const AttachmentPart::Ptr &part, bool compress)
0580 {
0581     if (compress) {
0582         qCDebug(MESSAGECOMPOSER_LOG) << "Compressing part.";
0583 
0584         auto ajob = new AttachmentCompressJob(part, this);
0585         connect(ajob, &AttachmentCompressJob::result, this, [this](KJob *job) {
0586             d->compressJobResult(job);
0587         });
0588         ajob->start();
0589     } else {
0590         qCDebug(MESSAGECOMPOSER_LOG) << "Uncompressing part.";
0591 
0592         // Replace the compressed part with the original uncompressed part, and delete
0593         // the compressed part.
0594         AttachmentPart::Ptr originalPart = d->uncompressedParts.take(part);
0595         Q_ASSERT(originalPart); // Found in uncompressedParts.
0596         bool ok = d->model->replaceAttachment(part, originalPart);
0597         Q_ASSERT(ok);
0598         Q_UNUSED(ok)
0599     }
0600 }
0601 
0602 void AttachmentControllerBase::showContextMenu()
0603 {
0604     Q_EMIT refreshSelection();
0605 
0606     const int numberOfParts(d->selectedParts.count());
0607     QMenu menu;
0608 
0609     const bool enableEditAction = (numberOfParts == 1) && (!d->selectedParts.first()->isMessageOrMessageCollection());
0610 
0611     if (numberOfParts > 0) {
0612         if (numberOfParts == 1) {
0613             const QString mimetype = QString::fromLatin1(d->selectedParts.first()->mimeType());
0614             QMimeDatabase mimeDb;
0615             auto mime = mimeDb.mimeTypeForName(mimetype);
0616             QStringList parentMimeType;
0617             if (mime.isValid()) {
0618                 parentMimeType = mime.allAncestors();
0619             }
0620             if ((mimetype == QLatin1StringView("text/plain")) || (mimetype == QLatin1StringView("image/png")) || (mimetype == QLatin1StringView("image/jpeg"))
0621                 || parentMimeType.contains(QLatin1StringView("text/plain")) || parentMimeType.contains(QLatin1StringView("image/png"))
0622                 || parentMimeType.contains(QLatin1StringView("image/jpeg"))) {
0623                 menu.addAction(d->viewContextAction);
0624             }
0625             d->createOpenWithMenu(&menu, d->selectedParts.constFirst());
0626         }
0627         menu.addAction(d->openContextAction);
0628     }
0629     if (enableEditAction) {
0630         menu.addAction(d->editWithContextAction);
0631         menu.addAction(d->editContextAction);
0632     }
0633     menu.addSeparator();
0634     if (numberOfParts == 1) {
0635         if (!d->selectedParts.first()->url().isEmpty()) {
0636             menu.addAction(d->reloadAttachmentAction);
0637         }
0638         menu.addAction(d->saveAsContextAction);
0639         menu.addSeparator();
0640         menu.addAction(d->propertiesContextAction);
0641         menu.addSeparator();
0642     }
0643 
0644     if (numberOfParts > 0) {
0645         menu.addAction(d->removeContextAction);
0646         menu.addSeparator();
0647     }
0648     const int nbAttachment = d->model->rowCount();
0649     if (nbAttachment != numberOfParts) {
0650         menu.addAction(d->selectAllAction);
0651         menu.addSeparator();
0652     }
0653     if (numberOfParts == 0) {
0654         menu.addAction(d->addContextAction);
0655     }
0656 
0657     menu.exec(QCursor::pos());
0658 }
0659 
0660 void AttachmentControllerBase::slotOpenWithDialog()
0661 {
0662     openWith();
0663 }
0664 
0665 void AttachmentControllerBase::slotOpenWithAction(QAction *act)
0666 {
0667     auto app = act->data().value<KService::Ptr>();
0668     Q_ASSERT(d->selectedParts.count() == 1);
0669 
0670     openWith(app);
0671 }
0672 
0673 void AttachmentControllerBase::openWith(const KService::Ptr &offer)
0674 {
0675     QTemporaryFile *tempFile = dumpAttachmentToTempFile(d->selectedParts.constFirst());
0676     if (!tempFile) {
0677         KMessageBox::error(d->wParent,
0678                            i18n("KMail was unable to write the attachment to a temporary file."),
0679                            i18nc("@title:window", "Unable to open attachment"));
0680         return;
0681     }
0682     QUrl url = QUrl::fromLocalFile(tempFile->fileName());
0683     tempFile->setPermissions(QFile::ReadUser);
0684     // If offer is null, this will show the "open with" dialog
0685     auto job = new KIO::ApplicationLauncherJob(offer);
0686     job->setUrls({url});
0687     job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, d->wParent));
0688     job->start();
0689     connect(job, &KJob::result, this, [tempFile, job]() {
0690         if (job->error()) {
0691             delete tempFile;
0692         }
0693     });
0694     // Delete the file only when the composer is closed
0695     // (and this object is destroyed).
0696     tempFile->setParent(this); // Manages lifetime.
0697 }
0698 
0699 void AttachmentControllerBase::openAttachment(const AttachmentPart::Ptr &part)
0700 {
0701     QTemporaryFile *tempFile = dumpAttachmentToTempFile(part);
0702     if (!tempFile) {
0703         KMessageBox::error(d->wParent,
0704                            i18n("KMail was unable to write the attachment to a temporary file."),
0705                            i18nc("@title:window", "Unable to open attachment"));
0706         return;
0707     }
0708     tempFile->setPermissions(QFile::ReadUser);
0709     auto job = new KIO::OpenUrlJob(QUrl::fromLocalFile(tempFile->fileName()), QString::fromLatin1(part->mimeType()));
0710     job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, d->wParent));
0711     job->setDeleteTemporaryFile(true);
0712     connect(job, &KIO::OpenUrlJob::result, this, [this, tempFile](KJob *job) {
0713         if (job->error() == KIO::ERR_USER_CANCELED) {
0714             KMessageBox::error(d->wParent, i18n("KMail was unable to open the attachment."), job->errorString());
0715             delete tempFile;
0716         } else {
0717             // The file was opened.  Delete it only when the composer is closed
0718             // (and this object is destroyed).
0719             tempFile->setParent(this); // Manages lifetime.
0720         }
0721     });
0722     job->start();
0723 }
0724 
0725 void AttachmentControllerBase::viewAttachment(const AttachmentPart::Ptr &part)
0726 {
0727     auto composer = new MessageComposer::Composer;
0728     composer->globalPart()->setFallbackCharsetEnabled(true);
0729     auto attachmentJob = new MessageComposer::AttachmentJob(part, composer);
0730     connect(attachmentJob, &AttachmentJob::result, this, [this](KJob *job) {
0731         d->slotAttachmentContentCreated(job);
0732     });
0733     attachmentJob->start();
0734 }
0735 
0736 void AttachmentControllerBase::AttachmentControllerBasePrivate::slotAttachmentContentCreated(KJob *job)
0737 {
0738     if (!job->error()) {
0739         const MessageComposer::AttachmentJob *const attachmentJob = qobject_cast<MessageComposer::AttachmentJob *>(job);
0740         Q_ASSERT(attachmentJob);
0741         if (attachmentJob) {
0742             Q_EMIT q->showAttachment(attachmentJob->content(), QByteArray());
0743         }
0744     } else {
0745         // TODO: show warning to the user
0746         qCWarning(MESSAGECOMPOSER_LOG) << "Error creating KMime::Content for attachment:" << job->errorText();
0747     }
0748 }
0749 
0750 void AttachmentControllerBase::editAttachment(AttachmentPart::Ptr part, MessageComposer::EditorWatcher::OpenWithOption openWithOption)
0751 {
0752     QTemporaryFile *tempFile = dumpAttachmentToTempFile(part);
0753     if (!tempFile) {
0754         KMessageBox::error(d->wParent,
0755                            i18n("KMail was unable to write the attachment to a temporary file."),
0756                            i18nc("@title:window", "Unable to edit attachment"));
0757         return;
0758     }
0759 
0760     auto watcher =
0761         new MessageComposer::EditorWatcher(QUrl::fromLocalFile(tempFile->fileName()), QString::fromLatin1(part->mimeType()), openWithOption, this, d->wParent);
0762     connect(watcher, &MessageComposer::EditorWatcher::editDone, this, [this](MessageComposer::EditorWatcher *watcher) {
0763         d->editDone(watcher);
0764     });
0765 
0766     switch (watcher->start()) {
0767     case MessageComposer::EditorWatcher::NoError:
0768         // The attachment is being edited.
0769         // We will clean things up in editDone().
0770         d->editorPart[watcher] = part;
0771         d->editorTempFile[watcher] = tempFile;
0772 
0773         // Delete the temp file if the composer is closed (and this object is destroyed).
0774         tempFile->setParent(this); // Manages lifetime.
0775         break;
0776     case MessageComposer::EditorWatcher::CannotStart:
0777         qCWarning(MESSAGECOMPOSER_LOG) << "Could not start EditorWatcher.";
0778         [[fallthrough]];
0779     case MessageComposer::EditorWatcher::Unknown:
0780     case MessageComposer::EditorWatcher::Canceled:
0781     case MessageComposer::EditorWatcher::NoServiceFound:
0782         delete watcher;
0783         delete tempFile;
0784         break;
0785     }
0786 }
0787 
0788 void AttachmentControllerBase::editAttachmentWith(const AttachmentPart::Ptr &part)
0789 {
0790     editAttachment(part, MessageComposer::EditorWatcher::OpenWithDialog);
0791 }
0792 
0793 void AttachmentControllerBase::saveAttachmentAs(const AttachmentPart::Ptr &part)
0794 {
0795     QString pname = part->name();
0796     if (pname.isEmpty()) {
0797         pname = i18n("unnamed");
0798     }
0799 
0800     const QUrl url = QFileDialog::getSaveFileUrl(d->wParent, i18n("Save Attachment As"), QUrl::fromLocalFile(pname));
0801 
0802     if (url.isEmpty()) {
0803         qCDebug(MESSAGECOMPOSER_LOG) << "Save Attachment As dialog canceled.";
0804         return;
0805     }
0806 
0807     byteArrayToRemoteFile(part->data(), url);
0808 }
0809 
0810 void AttachmentControllerBase::byteArrayToRemoteFile(const QByteArray &aData, const QUrl &aURL, bool overwrite)
0811 {
0812     KIO::StoredTransferJob *job = KIO::storedPut(aData, aURL, -1, overwrite ? KIO::Overwrite : KIO::DefaultFlags);
0813     connect(job, &KIO::StoredTransferJob::result, this, &AttachmentControllerBase::slotPutResult);
0814 }
0815 
0816 void AttachmentControllerBase::slotPutResult(KJob *job)
0817 {
0818     auto _job = qobject_cast<KIO::StoredTransferJob *>(job);
0819 
0820     if (job->error()) {
0821         if (job->error() == KIO::ERR_FILE_ALREADY_EXIST) {
0822             if (KMessageBox::warningContinueCancel(nullptr,
0823                                                    i18n("File %1 exists.\nDo you want to replace it?", _job->url().toLocalFile()),
0824                                                    i18nc("@title:window", "Save to File"),
0825                                                    KGuiItem(i18n("&Replace")))
0826                 == KMessageBox::Continue) {
0827                 byteArrayToRemoteFile(_job->data(), _job->url(), true);
0828             }
0829         } else {
0830             KJobUiDelegate *ui = static_cast<KIO::Job *>(job)->uiDelegate();
0831             ui->showErrorMessage();
0832         }
0833     }
0834 }
0835 
0836 void AttachmentControllerBase::attachmentProperties(const AttachmentPart::Ptr &part)
0837 {
0838     QPointer<AttachmentPropertiesDialog> dialog = new AttachmentPropertiesDialog(part, false, d->wParent);
0839 
0840     dialog->setEncryptEnabled(d->encryptEnabled);
0841     dialog->setSignEnabled(d->signEnabled);
0842 
0843     if (dialog->exec() && dialog) {
0844         d->model->updateAttachment(part);
0845     }
0846     delete dialog;
0847 }
0848 
0849 void AttachmentControllerBase::attachDirectory(const QUrl &url)
0850 {
0851     const int rc = KMessageBox::warningTwoActions(d->wParent,
0852                                                   i18n("Do you really want to attach this directory \"%1\"?", url.toLocalFile()),
0853                                                   i18nc("@title:window", "Attach directory"),
0854                                                   KGuiItem(i18nc("@action:button", "Attach")),
0855                                                   KStandardGuiItem::cancel());
0856     if (rc == KMessageBox::ButtonCode::PrimaryAction) {
0857         addAttachment(url);
0858     }
0859 }
0860 
0861 void AttachmentControllerBase::showAttachVcard()
0862 {
0863     QPointer<Akonadi::EmailAddressSelectionDialog> dlg = new Akonadi::EmailAddressSelectionDialog(d->wParent);
0864     dlg->view()->view()->setSelectionMode(QAbstractItemView::MultiSelection);
0865     if (dlg->exec()) {
0866         const Akonadi::EmailAddressSelection::List selectedEmail = dlg->selectedAddresses();
0867         for (const Akonadi::EmailAddressSelection &selected : selectedEmail) {
0868             auto ajob = new MessageComposer::AttachmentVcardFromAddressBookJob(selected.item(), this);
0869             connect(ajob, &AttachmentVcardFromAddressBookJob::result, this, [this](KJob *job) {
0870                 d->attachVcardFromAddressBook(job);
0871             });
0872             ajob->start();
0873         }
0874     }
0875     delete dlg;
0876 }
0877 
0878 void AttachmentControllerBase::showAttachClipBoard()
0879 {
0880     auto job = new MessageComposer::AttachmentClipBoardJob(this);
0881     connect(job, &AttachmentClipBoardJob::result, this, [this](KJob *job) {
0882         d->attachClipBoardElement(job);
0883     });
0884     job->start();
0885 }
0886 
0887 void AttachmentControllerBase::showAddAttachmentCompressedDirectoryDialog()
0888 {
0889     const QUrl url = QFileDialog::getExistingDirectoryUrl(d->wParent, i18nc("@title:window", "Attach Directory"));
0890     if (url.isValid()) {
0891         attachDirectory(url);
0892     }
0893 }
0894 
0895 void AttachmentControllerBase::showAddAttachmentFileDialog()
0896 {
0897     const KEncodingFileDialog::Result result =
0898         KEncodingFileDialog::getOpenUrlsAndEncoding(QString(), QUrl(), QString(), d->wParent, i18nc("@title:window", "Attach File"));
0899     if (!result.URLs.isEmpty()) {
0900         const QString encoding = MimeTreeParser::NodeHelper::fixEncoding(result.encoding);
0901         const int numberOfFiles(result.URLs.count());
0902         for (int i = 0; i < numberOfFiles; ++i) {
0903             const QUrl url = result.URLs.at(i);
0904             QUrl urlWithEncoding = url;
0905             MessageCore::StringUtil::setEncodingFile(urlWithEncoding, encoding);
0906             QMimeDatabase mimeDb;
0907             const auto mimeType = mimeDb.mimeTypeForUrl(urlWithEncoding);
0908             if (mimeType.name() == QLatin1StringView("inode/directory")) {
0909                 const int rc = KMessageBox::warningTwoActions(d->wParent,
0910                                                               i18n("Do you really want to attach this directory \"%1\"?", url.toLocalFile()),
0911                                                               i18nc("@title:window", "Attach directory"),
0912                                                               KGuiItem(i18nc("@action:button", "Attach")),
0913                                                               KStandardGuiItem::cancel());
0914                 if (rc == KMessageBox::ButtonCode::PrimaryAction) {
0915                     addAttachment(urlWithEncoding);
0916                 }
0917             } else {
0918                 addAttachment(urlWithEncoding);
0919             }
0920         }
0921     }
0922 }
0923 
0924 void AttachmentControllerBase::addAttachment(const AttachmentPart::Ptr &part)
0925 {
0926     part->setEncrypted(d->model->isEncryptSelected());
0927     part->setSigned(d->model->isSignSelected());
0928     d->model->addAttachment(part);
0929 
0930     Q_EMIT fileAttached();
0931 }
0932 
0933 void AttachmentControllerBase::addAttachmentUrlSync(const QUrl &url)
0934 {
0935     MessageCore::AttachmentFromUrlBaseJob *ajob = MessageCore::AttachmentFromUrlUtils::createAttachmentJob(url, this);
0936     if (ajob->exec()) {
0937         AttachmentPart::Ptr part = ajob->attachmentPart();
0938         addAttachment(part);
0939     } else {
0940         if (ajob->error()) {
0941             KMessageBox::error(d->wParent, ajob->errorString(), i18nc("@title:window", "Failed to attach file"));
0942         }
0943     }
0944 }
0945 
0946 void AttachmentControllerBase::addAttachment(const QUrl &url)
0947 {
0948     MessageCore::AttachmentFromUrlBaseJob *ajob = MessageCore::AttachmentFromUrlUtils::createAttachmentJob(url, this);
0949     connect(ajob, &AttachmentFromUrlBaseJob::result, this, [this](KJob *job) {
0950         d->loadJobResult(job);
0951     });
0952     ajob->start();
0953 }
0954 
0955 void AttachmentControllerBase::addAttachments(const QList<QUrl> &urls)
0956 {
0957     for (const QUrl &url : urls) {
0958         addAttachment(url);
0959     }
0960 }
0961 
0962 void AttachmentControllerBase::showAttachPublicKeyDialog()
0963 {
0964     using Kleo::KeySelectionDialog;
0965     QPointer<KeySelectionDialog> dialog = new KeySelectionDialog(i18n("Attach Public OpenPGP Key"),
0966                                                                  i18n("Select the public key which should be attached."),
0967                                                                  std::vector<GpgME::Key>(),
0968                                                                  KeySelectionDialog::PublicKeys | KeySelectionDialog::OpenPGPKeys,
0969                                                                  false /* no multi selection */,
0970                                                                  false /* no remember choice box */,
0971                                                                  d->wParent);
0972 
0973     if (dialog->exec() == QDialog::Accepted) {
0974         exportPublicKey(dialog->fingerprint());
0975     }
0976     delete dialog;
0977 }
0978 
0979 void AttachmentControllerBase::attachMyPublicKey()
0980 {
0981 }
0982 
0983 void AttachmentControllerBase::enableAttachPublicKey(bool enable)
0984 {
0985     d->attachPublicKeyAction->setEnabled(enable);
0986 }
0987 
0988 void AttachmentControllerBase::enableAttachMyPublicKey(bool enable)
0989 {
0990     d->attachMyPublicKeyAction->setEnabled(enable);
0991 }
0992 
0993 void AttachmentControllerBase::setAttachOwnVcard(bool attachVcard)
0994 {
0995     d->addOwnVcardAction->setChecked(attachVcard);
0996 }
0997 
0998 bool AttachmentControllerBase::attachOwnVcard() const
0999 {
1000     return d->addOwnVcardAction->isChecked();
1001 }
1002 
1003 void AttachmentControllerBase::setIdentityHasOwnVcard(bool state)
1004 {
1005     d->addOwnVcardAction->setEnabled(state);
1006 }
1007 
1008 #include "moc_attachmentcontrollerbase.cpp"