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"