Messagelib

attachmentcontrollerbase.cpp
1/*
2 * This file is part of KMail.
3 * SPDX-FileCopyrightText: 2009 Constantin Berzan <exit3219@gmail.com>
4 *
5 * Parts based on KMail code by:
6 * Various authors.
7 *
8 * SPDX-License-Identifier: GPL-2.0-or-later
9 */
10
11#include "attachmentcontrollerbase.h"
12
13#include "MessageComposer/AttachmentClipBoardJob"
14#include "MessageComposer/AttachmentFromPublicKeyJob"
15#include "MessageComposer/AttachmentJob"
16#include "MessageComposer/AttachmentVcardFromAddressBookJob"
17#include "MessageComposer/Composer"
18#include "MessageComposer/GlobalPart"
19#include <MessageComposer/AttachmentModel>
20
21#include <MessageViewer/MessageViewerUtil>
22
23#include <MimeTreeParser/NodeHelper>
24
25#include <MessageCore/StringUtil>
26
27#include <Akonadi/ItemFetchJob>
28#include <KIO/JobUiDelegateFactory>
29#include <QIcon>
30
31#include <QMenu>
32#include <QPointer>
33#include <QTreeView>
34
35#include "messagecomposer_debug.h"
36#include <KActionCollection>
37#include <KActionMenu>
38#include <KEncodingFileDialog>
39#include <KFileItemActions>
40#include <KIO/ApplicationLauncherJob>
41#include <KLocalizedString>
42#include <KMessageBox>
43#include <QAction>
44#include <QMimeDatabase>
45#include <QPushButton>
46#include <QTemporaryFile>
47
48#include <Libkleo/KeySelectionDialog>
49#include <QGpgME/Protocol>
50
51#include "messagecore/attachmentfromurlbasejob.h"
52#include "messagecore/attachmentupdatejob.h"
53#include <Akonadi/EmailAddressSelectionDialog>
54#include <Akonadi/EmailAddressSelectionWidget>
55#include <KIO/JobUiDelegate>
56#include <KIO/OpenUrlJob>
57#include <KIO/StoredTransferJob>
58#include <MessageCore/AttachmentCompressJob>
59#include <MessageCore/AttachmentFromUrlUtils>
60#include <MessageCore/AttachmentPropertiesDialog>
61
62#include <KJob>
63#include <KMime/Content>
64
65#include <QActionGroup>
66#include <QFileDialog>
67
68using namespace MessageComposer;
69using namespace MessageCore;
70
71class MessageComposer::AttachmentControllerBase::AttachmentControllerBasePrivate
72{
73public:
74 explicit AttachmentControllerBasePrivate(AttachmentControllerBase *qq);
75 ~AttachmentControllerBasePrivate();
76
77 void attachmentRemoved(const AttachmentPart::Ptr &part);
78 void compressJobResult(KJob *job);
79 void loadJobResult(KJob *job);
80 void openSelectedAttachments();
81 void viewSelectedAttachments();
82 void editSelectedAttachment();
83 void editSelectedAttachmentWith();
84 void removeSelectedAttachments();
85 void saveSelectedAttachmentAs();
86 void selectedAttachmentProperties();
87 void editDone(MessageComposer::EditorWatcher *watcher);
88 void attachPublicKeyJobResult(KJob *job);
89 void slotAttachmentContentCreated(KJob *job);
90 void addAttachmentPart(AttachmentPart::Ptr part);
91 void attachVcardFromAddressBook(KJob *job);
92 void attachClipBoardElement(KJob *job);
93 void selectedAllAttachment();
94 void createOpenWithMenu(QMenu *topMenu, const AttachmentPart::Ptr &part);
95 void reloadAttachment();
96 void updateJobResult(KJob *);
97
98 AttachmentPart::List selectedParts;
100 MessageComposer::AttachmentModel *model = nullptr;
101 QWidget *wParent = nullptr;
104
105 KActionCollection *mActionCollection = nullptr;
106 QAction *attachPublicKeyAction = nullptr;
107 QAction *attachMyPublicKeyAction = nullptr;
108 QAction *openContextAction = nullptr;
109 QAction *viewContextAction = nullptr;
110 QAction *editContextAction = nullptr;
111 QAction *editWithContextAction = nullptr;
112 QAction *removeAction = nullptr;
113 QAction *removeContextAction = nullptr;
114 QAction *saveAsAction = nullptr;
115 QAction *saveAsContextAction = nullptr;
116 QAction *propertiesAction = nullptr;
117 QAction *propertiesContextAction = nullptr;
118 QAction *addAttachmentFileAction = nullptr;
119 QAction *addAttachmentDirectoryAction = nullptr;
120 QAction *addContextAction = nullptr;
121 QAction *selectAllAction = nullptr;
122 KActionMenu *attachmentMenu = nullptr;
123 QAction *addOwnVcardAction = nullptr;
124 QAction *reloadAttachmentAction = nullptr;
125 QAction *attachVCardsAction = nullptr;
126 QAction *attachClipBoardAction = nullptr;
127 // If part p is compressed, uncompressedParts[p] is the uncompressed part.
129 bool encryptEnabled = false;
130 bool signEnabled = false;
131};
132
133AttachmentControllerBase::AttachmentControllerBasePrivate::AttachmentControllerBasePrivate(AttachmentControllerBase *qq)
134 : q(qq)
135{
136}
137
138AttachmentControllerBase::AttachmentControllerBasePrivate::~AttachmentControllerBasePrivate() = default;
139
140void AttachmentControllerBase::setSelectedParts(const AttachmentPart::List &selectedParts)
141{
142 d->selectedParts = selectedParts;
143 const int selectedCount = selectedParts.count();
144 const bool enableEditAction = (selectedCount == 1) && (!selectedParts.first()->isMessageOrMessageCollection());
145
146 d->openContextAction->setEnabled(selectedCount > 0);
147 d->viewContextAction->setEnabled(selectedCount > 0);
148 d->editContextAction->setEnabled(enableEditAction);
149 d->editWithContextAction->setEnabled(enableEditAction);
150 d->removeAction->setEnabled(selectedCount > 0);
151 d->removeContextAction->setEnabled(selectedCount > 0);
152 d->saveAsAction->setEnabled(selectedCount == 1);
153 d->saveAsContextAction->setEnabled(selectedCount == 1);
154 d->propertiesAction->setEnabled(selectedCount == 1);
155 d->propertiesContextAction->setEnabled(selectedCount == 1);
156}
157
158void AttachmentControllerBase::AttachmentControllerBasePrivate::attachmentRemoved(const AttachmentPart::Ptr &part)
159{
160 uncompressedParts.remove(part);
161}
162
163void AttachmentControllerBase::AttachmentControllerBasePrivate::compressJobResult(KJob *job)
164{
165 if (job->error()) {
166 KMessageBox::error(wParent, job->errorString(), i18nc("@title:window", "Failed to compress attachment"));
167 return;
168 }
169
171 Q_ASSERT(ajob);
172 AttachmentPart::Ptr originalPart = ajob->originalPart();
173 AttachmentPart::Ptr compressedPart = ajob->compressedPart();
174
175 if (ajob->isCompressedPartLarger()) {
176 const int result = KMessageBox::questionTwoActions(wParent,
177 i18n("The compressed attachment is larger than the original. "
178 "Do you want to keep the original one?"),
179 QString(/*caption*/),
180 KGuiItem(i18nc("Do not compress", "Keep")),
181 KGuiItem(i18n("Compress")));
182 if (result == KMessageBox::ButtonCode::PrimaryAction) {
183 // The user has chosen to keep the uncompressed file.
184 return;
185 }
186 }
187
188 qCDebug(MESSAGECOMPOSER_LOG) << "Replacing uncompressed part in model.";
189 uncompressedParts[compressedPart] = originalPart;
190 bool ok = model->replaceAttachment(originalPart, compressedPart);
191 if (!ok) {
192 // The attachment was removed from the model while we were compressing.
193 qCDebug(MESSAGECOMPOSER_LOG) << "Compressed a zombie.";
194 }
195}
196
197void AttachmentControllerBase::AttachmentControllerBasePrivate::loadJobResult(KJob *job)
198{
199 if (job->error()) {
200 KMessageBox::error(wParent, job->errorString(), i18n("Failed to attach file"));
201 return;
202 }
203
205 Q_ASSERT(ajob);
206 AttachmentPart::Ptr part = ajob->attachmentPart();
207 q->addAttachment(part);
208}
209
210void AttachmentControllerBase::AttachmentControllerBasePrivate::openSelectedAttachments()
211{
212 Q_ASSERT(selectedParts.count() >= 1);
213 for (const AttachmentPart::Ptr &part : std::as_const(selectedParts)) {
214 q->openAttachment(part);
215 }
216}
217
218void AttachmentControllerBase::AttachmentControllerBasePrivate::viewSelectedAttachments()
219{
220 Q_ASSERT(selectedParts.count() >= 1);
221 for (const AttachmentPart::Ptr &part : std::as_const(selectedParts)) {
222 q->viewAttachment(part);
223 }
224}
225
226void AttachmentControllerBase::AttachmentControllerBasePrivate::editSelectedAttachment()
227{
228 Q_ASSERT(selectedParts.count() == 1);
229 q->editAttachment(selectedParts.constFirst(), MessageComposer::EditorWatcher::NoOpenWithDialog);
230}
231
232void AttachmentControllerBase::AttachmentControllerBasePrivate::editSelectedAttachmentWith()
233{
234 Q_ASSERT(selectedParts.count() == 1);
235 q->editAttachment(selectedParts.constFirst(), MessageComposer::EditorWatcher::OpenWithDialog);
236}
237
238void AttachmentControllerBase::AttachmentControllerBasePrivate::removeSelectedAttachments()
239{
240 Q_ASSERT(selectedParts.count() >= 1);
241 // We must store list, otherwise when we remove it changes selectedParts (as selection changed) => it will crash.
242 const AttachmentPart::List toRemove = selectedParts;
243 for (const AttachmentPart::Ptr &part : toRemove) {
244 model->removeAttachment(part);
245 }
246}
247
248void AttachmentControllerBase::AttachmentControllerBasePrivate::saveSelectedAttachmentAs()
249{
250 Q_ASSERT(selectedParts.count() == 1);
251 q->saveAttachmentAs(selectedParts.constFirst());
252}
253
254void AttachmentControllerBase::AttachmentControllerBasePrivate::selectedAttachmentProperties()
255{
256 Q_ASSERT(selectedParts.count() == 1);
257 q->attachmentProperties(selectedParts.constFirst());
258}
259
260void AttachmentControllerBase::AttachmentControllerBasePrivate::reloadAttachment()
261{
262 Q_ASSERT(selectedParts.count() == 1);
263 auto ajob = new AttachmentUpdateJob(selectedParts.constFirst(), q);
264 connect(ajob, &AttachmentUpdateJob::result, q, [this](KJob *job) {
265 updateJobResult(job);
266 });
267 ajob->start();
268}
269
270void AttachmentControllerBase::AttachmentControllerBasePrivate::updateJobResult(KJob *job)
271{
272 if (job->error()) {
273 KMessageBox::error(wParent, job->errorString(), i18n("Failed to reload attachment"));
274 return;
275 }
277 Q_ASSERT(ajob);
278 AttachmentPart::Ptr originalPart = ajob->originalPart();
279 AttachmentPart::Ptr updatedPart = ajob->updatedPart();
280
281 attachmentRemoved(originalPart);
282 bool ok = model->replaceAttachment(originalPart, updatedPart);
283 if (!ok) {
284 // The attachment was removed from the model while we were compressing.
285 qCDebug(MESSAGECOMPOSER_LOG) << "Updated a zombie.";
286 }
287}
288
289void AttachmentControllerBase::AttachmentControllerBasePrivate::editDone(MessageComposer::EditorWatcher *watcher)
290{
291 AttachmentPart::Ptr part = editorPart.take(watcher);
292 Q_ASSERT(part);
293 QTemporaryFile *tempFile = editorTempFile.take(watcher);
295 if (watcher->fileChanged()) {
296 qCDebug(MESSAGECOMPOSER_LOG) << "File has changed.";
297 const QString name = watcher->url().path();
298 QFile file(name);
299 if (file.open(QIODevice::ReadOnly)) {
300 const QByteArray data = file.readAll();
301 part->setData(data);
302 model->updateAttachment(part);
303 }
304 }
305
306 delete tempFile;
307 // The watcher deletes itself.
308}
309
310void AttachmentControllerBase::AttachmentControllerBasePrivate::createOpenWithMenu(QMenu *topMenu, const AttachmentPart::Ptr &part)
311{
312 const QString contentTypeStr = QString::fromLatin1(part->mimeType());
314 if (!offers.isEmpty()) {
315 QMenu *menu = topMenu;
316 auto actionGroup = new QActionGroup(menu);
317 connect(actionGroup, &QActionGroup::triggered, q, &AttachmentControllerBase::slotOpenWithAction);
318
319 if (offers.count() > 1) { // submenu 'open with'
320 menu = new QMenu(i18nc("@title:menu", "&Open With"), topMenu);
321 menu->menuAction()->setObjectName(QLatin1StringView("openWith_submenu")); // for the unittest
322 topMenu->addMenu(menu);
323 }
324 // qCDebug(MESSAGECOMPOSER_LOG) << offers.count() << "offers" << topMenu << menu;
325
328 for (; it != end; ++it) {
329 QAction *act = MessageViewer::Util::createAppAction(*it,
330 // no submenu -> prefix single offer
331 menu == topMenu,
332 actionGroup,
333 menu);
334 menu->addAction(act);
335 }
336
338 if (menu != topMenu) { // submenu
339 menu->addSeparator();
340 openWithActionName = i18nc("@action:inmenu Open With", "&Other...");
341 } else {
342 openWithActionName = i18nc("@title:menu", "&Open With...");
343 }
344 auto openWithAct = new QAction(menu);
346 QObject::connect(openWithAct, &QAction::triggered, q, &AttachmentControllerBase::slotOpenWithDialog);
347 menu->addAction(openWithAct);
348 } else { // no app offers -> Open With...
349 auto act = new QAction(topMenu);
350 act->setText(i18nc("@title:menu", "&Open With..."));
351 QObject::connect(act, &QAction::triggered, q, &AttachmentControllerBase::slotOpenWithDialog);
352 topMenu->addAction(act);
353 }
354}
355
356void AttachmentControllerBase::exportPublicKey(const QString &fingerprint)
357{
358 if (fingerprint.isEmpty() || !QGpgME::openpgp()) {
359 qCWarning(MESSAGECOMPOSER_LOG) << "Tried to export key with empty fingerprint, or no OpenPGP.";
360 return;
361 }
362
363 auto ajob = new MessageComposer::AttachmentFromPublicKeyJob(fingerprint, this);
365 d->attachPublicKeyJobResult(job);
366 });
367 ajob->start();
368}
369
370void AttachmentControllerBase::AttachmentControllerBasePrivate::attachPublicKeyJobResult(KJob *job)
371{
372 // The only reason we can't use loadJobResult() and need a separate method
373 // is that we want to show the proper caption ("public key" instead of "file")...
374
375 if (job->error()) {
376 KMessageBox::error(wParent, job->errorString(), i18n("Failed to attach public key"));
377 return;
378 }
379
381 auto ajob = static_cast<MessageComposer::AttachmentFromPublicKeyJob *>(job);
382 AttachmentPart::Ptr part = ajob->attachmentPart();
383 q->addAttachment(part);
384}
385
386void AttachmentControllerBase::AttachmentControllerBasePrivate::attachVcardFromAddressBook(KJob *job)
387{
388 if (job->error()) {
389 qCDebug(MESSAGECOMPOSER_LOG) << " Error during when get vCard";
390 KMessageBox::error(wParent, job->errorString(), i18n("Failed to attach vCard"));
391 return;
392 }
393
395 AttachmentPart::Ptr part = ajob->attachmentPart();
396 q->addAttachment(part);
397}
398
399void AttachmentControllerBase::AttachmentControllerBasePrivate::attachClipBoardElement(KJob *job)
400{
401 if (job->error()) {
402 qCDebug(MESSAGECOMPOSER_LOG) << " Error during when get try to attach text from clipboard";
403 KMessageBox::error(wParent, job->errorString(), i18n("Failed to attach text from clipboard"));
404 return;
405 }
406
407 auto ajob = static_cast<MessageComposer::AttachmentClipBoardJob *>(job);
408 AttachmentPart::Ptr part = ajob->attachmentPart();
409 q->addAttachment(part);
410}
411
412static QTemporaryFile *dumpAttachmentToTempFile(const AttachmentPart::Ptr &part) // local
413{
414 auto file = new QTemporaryFile;
415 if (!file->open()) {
416 qCCritical(MESSAGECOMPOSER_LOG) << "Could not open tempfile" << file->fileName();
417 delete file;
418 return nullptr;
419 }
420 if (file->write(part->data()) == -1) {
421 qCCritical(MESSAGECOMPOSER_LOG) << "Could not dump attachment to tempfile.";
422 delete file;
423 return nullptr;
424 }
425 file->flush();
426 return file;
427}
428
429AttachmentControllerBase::AttachmentControllerBase(MessageComposer::AttachmentModel *model, QWidget *wParent, KActionCollection *actionCollection)
430 : QObject(wParent)
431 , d(new AttachmentControllerBasePrivate(this))
432{
433 d->model = model;
434 connect(model, &MessageComposer::AttachmentModel::attachUrlsRequested, this, &AttachmentControllerBase::addAttachments);
435 connect(model, &MessageComposer::AttachmentModel::attachmentRemoved, this, [this](const MessageCore::AttachmentPart::Ptr &attr) {
436 d->attachmentRemoved(attr);
437 });
438 connect(model, &AttachmentModel::attachmentCompressRequested, this, &AttachmentControllerBase::compressAttachment);
439 connect(model, &MessageComposer::AttachmentModel::encryptEnabled, this, &AttachmentControllerBase::setEncryptEnabled);
440 connect(model, &MessageComposer::AttachmentModel::signEnabled, this, &AttachmentControllerBase::setSignEnabled);
441
442 d->wParent = wParent;
443 d->mActionCollection = actionCollection;
444}
445
446AttachmentControllerBase::~AttachmentControllerBase() = default;
447
448void AttachmentControllerBase::createActions()
449{
450 // Create the actions.
451 d->attachPublicKeyAction = new QAction(i18n("Attach &Public Key..."), this);
452 connect(d->attachPublicKeyAction, &QAction::triggered, this, &AttachmentControllerBase::showAttachPublicKeyDialog);
453
454 d->attachMyPublicKeyAction = new QAction(i18n("Attach &My Public Key"), this);
455 connect(d->attachMyPublicKeyAction, &QAction::triggered, this, &AttachmentControllerBase::attachMyPublicKey);
456
457 d->attachmentMenu = new KActionMenu(QIcon::fromTheme(QStringLiteral("mail-attachment")), i18n("Attach"), this);
458 connect(d->attachmentMenu, &QAction::triggered, this, &AttachmentControllerBase::showAddAttachmentFileDialog);
459 d->attachmentMenu->setPopupMode(QToolButton::DelayedPopup);
460
461 d->addAttachmentFileAction = new QAction(QIcon::fromTheme(QStringLiteral("mail-attachment")), i18n("&Attach File..."), this);
462 d->addAttachmentFileAction->setIconText(i18n("Attach"));
463 d->addContextAction = new QAction(QIcon::fromTheme(QStringLiteral("mail-attachment")), i18n("Add Attachment..."), this);
464 connect(d->addAttachmentFileAction, &QAction::triggered, this, &AttachmentControllerBase::showAddAttachmentFileDialog);
465 connect(d->addContextAction, &QAction::triggered, this, &AttachmentControllerBase::showAddAttachmentFileDialog);
466
467 d->addAttachmentDirectoryAction = new QAction(QIcon::fromTheme(QStringLiteral("mail-attachment")), i18n("&Attach Directory..."), this);
468 d->addAttachmentDirectoryAction->setIconText(i18n("Attach"));
469 connect(d->addAttachmentDirectoryAction, &QAction::triggered, this, &AttachmentControllerBase::showAddAttachmentCompressedDirectoryDialog);
470
471 d->addOwnVcardAction = new QAction(i18n("Attach Own vCard"), this);
472 d->addOwnVcardAction->setIconText(i18n("Own vCard"));
473 d->addOwnVcardAction->setCheckable(true);
474 connect(d->addOwnVcardAction, &QAction::triggered, this, &AttachmentControllerBase::addOwnVcard);
475
476 d->attachVCardsAction = new QAction(QIcon::fromTheme(QStringLiteral("mail-attachment")), i18n("&Attach vCards..."), this);
477 d->attachVCardsAction->setIconText(i18n("Attach"));
478 connect(d->attachVCardsAction, &QAction::triggered, this, &AttachmentControllerBase::showAttachVcard);
479
480 d->attachClipBoardAction = new QAction(QIcon::fromTheme(QStringLiteral("mail-attachment")), i18n("&Attach Text From Clipboard..."), this);
481 d->attachClipBoardAction->setIconText(i18n("Attach Text From Clipboard"));
482 connect(d->attachClipBoardAction, &QAction::triggered, this, &AttachmentControllerBase::showAttachClipBoard);
483
484 d->attachmentMenu->addAction(d->addAttachmentFileAction);
485 d->attachmentMenu->addAction(d->addAttachmentDirectoryAction);
486 d->attachmentMenu->addSeparator();
487 d->attachmentMenu->addAction(d->addOwnVcardAction);
488 d->attachmentMenu->addSeparator();
489 d->attachmentMenu->addAction(d->attachVCardsAction);
490 d->attachmentMenu->addSeparator();
491 d->attachmentMenu->addAction(d->attachClipBoardAction);
492
493 d->removeAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18n("&Remove Attachment"), this);
494 d->removeContextAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18n("Remove"), this); // FIXME need two texts. is there a better way?
495 connect(d->removeAction, &QAction::triggered, this, [this]() {
496 d->removeSelectedAttachments();
497 });
498 connect(d->removeContextAction, &QAction::triggered, this, [this]() {
499 d->removeSelectedAttachments();
500 });
501
502 d->openContextAction = new QAction(i18nc("to open", "Open"), this);
503 connect(d->openContextAction, &QAction::triggered, this, [this]() {
504 d->openSelectedAttachments();
505 });
506
507 d->viewContextAction = new QAction(i18nc("to view", "View"), this);
508 connect(d->viewContextAction, &QAction::triggered, this, [this]() {
509 d->viewSelectedAttachments();
510 });
511
512 d->editContextAction = new QAction(i18nc("to edit", "Edit"), this);
513 connect(d->editContextAction, &QAction::triggered, this, [this]() {
514 d->editSelectedAttachment();
515 });
516
517 d->editWithContextAction = new QAction(i18n("Edit With..."), this);
518 connect(d->editWithContextAction, &QAction::triggered, this, [this]() {
519 d->editSelectedAttachmentWith();
520 });
521
522 d->saveAsAction = new QAction(QIcon::fromTheme(QStringLiteral("document-save-as")), i18n("&Save Attachment As..."), this);
523 d->saveAsContextAction = new QAction(QIcon::fromTheme(QStringLiteral("document-save-as")), i18n("Save As..."), this);
524 connect(d->saveAsAction, &QAction::triggered, this, [this]() {
525 d->saveSelectedAttachmentAs();
526 });
527 connect(d->saveAsContextAction, &QAction::triggered, this, [this]() {
528 d->saveSelectedAttachmentAs();
529 });
530
531 d->propertiesAction = new QAction(i18n("Attachment Pr&operties..."), this);
532 d->propertiesContextAction = new QAction(i18n("Properties"), this);
533 connect(d->propertiesAction, &QAction::triggered, this, [this]() {
534 d->selectedAttachmentProperties();
535 });
536 connect(d->propertiesContextAction, &QAction::triggered, this, [this]() {
537 d->selectedAttachmentProperties();
538 });
539
540 d->selectAllAction = new QAction(i18n("Select All"), this);
541 connect(d->selectAllAction, &QAction::triggered, this, &AttachmentControllerBase::selectedAllAttachment);
542
543 d->reloadAttachmentAction = new QAction(QIcon::fromTheme(QStringLiteral("view-refresh")), i18n("Reload"), this);
544 connect(d->reloadAttachmentAction, &QAction::triggered, this, [this]() {
545 d->reloadAttachment();
546 });
547
548 // Insert the actions into the composer window's menu.
549 KActionCollection *collection = d->mActionCollection;
550 collection->addAction(QStringLiteral("attach_public_key"), d->attachPublicKeyAction);
551 collection->addAction(QStringLiteral("attach_my_public_key"), d->attachMyPublicKeyAction);
552 collection->addAction(QStringLiteral("attach"), d->addAttachmentFileAction);
553 collection->setDefaultShortcut(d->addAttachmentFileAction, QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_A));
554 collection->addAction(QStringLiteral("attach_directory"), d->addAttachmentDirectoryAction);
555
556 collection->addAction(QStringLiteral("remove"), d->removeAction);
557 collection->addAction(QStringLiteral("attach_save"), d->saveAsAction);
558 collection->addAction(QStringLiteral("attach_properties"), d->propertiesAction);
559 collection->addAction(QStringLiteral("select_all_attachment"), d->selectAllAction);
560 collection->addAction(QStringLiteral("attach_menu"), d->attachmentMenu);
561 collection->addAction(QStringLiteral("attach_own_vcard"), d->addOwnVcardAction);
562 collection->addAction(QStringLiteral("attach_vcards"), d->attachVCardsAction);
563
564 setSelectedParts(AttachmentPart::List());
565 Q_EMIT actionsCreated();
566}
567
569{
570 d->encryptEnabled = enabled;
571}
572
573void AttachmentControllerBase::setSignEnabled(bool enabled)
574{
575 d->signEnabled = enabled;
576}
577
579{
580 if (compress) {
581 qCDebug(MESSAGECOMPOSER_LOG) << "Compressing part.";
582
583 auto ajob = new AttachmentCompressJob(part, this);
584 connect(ajob, &AttachmentCompressJob::result, this, [this](KJob *job) {
585 d->compressJobResult(job);
586 });
587 ajob->start();
588 } else {
589 qCDebug(MESSAGECOMPOSER_LOG) << "Uncompressing part.";
590
591 // Replace the compressed part with the original uncompressed part, and delete
592 // the compressed part.
593 AttachmentPart::Ptr originalPart = d->uncompressedParts.take(part);
594 Q_ASSERT(originalPart); // Found in uncompressedParts.
595 bool ok = d->model->replaceAttachment(part, originalPart);
596 Q_ASSERT(ok);
597 Q_UNUSED(ok)
598 }
599}
600
601void AttachmentControllerBase::showContextMenu()
602{
603 Q_EMIT refreshSelection();
604
605 const int numberOfParts(d->selectedParts.count());
606 QMenu menu;
607
608 const bool enableEditAction = (numberOfParts == 1) && (!d->selectedParts.first()->isMessageOrMessageCollection());
609
610 if (numberOfParts > 0) {
611 if (numberOfParts == 1) {
612 const QString mimetype = QString::fromLatin1(d->selectedParts.first()->mimeType());
614 auto mime = mimeDb.mimeTypeForName(mimetype);
616 if (mime.isValid()) {
617 parentMimeType = mime.allAncestors();
618 }
619 if ((mimetype == QLatin1StringView("text/plain")) || (mimetype == QLatin1StringView("image/png")) || (mimetype == QLatin1StringView("image/jpeg"))
620 || parentMimeType.contains(QLatin1StringView("text/plain")) || parentMimeType.contains(QLatin1StringView("image/png"))
621 || parentMimeType.contains(QLatin1StringView("image/jpeg"))) {
622 menu.addAction(d->viewContextAction);
623 }
624 d->createOpenWithMenu(&menu, d->selectedParts.constFirst());
625 }
626 menu.addAction(d->openContextAction);
627 }
628 if (enableEditAction) {
629 menu.addAction(d->editWithContextAction);
630 menu.addAction(d->editContextAction);
631 }
632 menu.addSeparator();
633 if (numberOfParts == 1) {
634 if (!d->selectedParts.first()->url().isEmpty()) {
635 menu.addAction(d->reloadAttachmentAction);
636 }
637 menu.addAction(d->saveAsContextAction);
638 menu.addSeparator();
639 menu.addAction(d->propertiesContextAction);
640 menu.addSeparator();
641 }
642
643 if (numberOfParts > 0) {
644 menu.addAction(d->removeContextAction);
645 menu.addSeparator();
646 }
647 const int nbAttachment = d->model->rowCount();
649 menu.addAction(d->selectAllAction);
650 menu.addSeparator();
651 }
652 if (numberOfParts == 0) {
653 menu.addAction(d->addContextAction);
654 }
655
656 menu.exec(QCursor::pos());
657}
658
659void AttachmentControllerBase::slotOpenWithDialog()
660{
661 openWith();
662}
663
664void AttachmentControllerBase::slotOpenWithAction(QAction *act)
665{
666 auto app = act->data().value<KService::Ptr>();
667 Q_ASSERT(d->selectedParts.count() == 1);
668
669 openWith(app);
670}
671
672void AttachmentControllerBase::openWith(const KService::Ptr &offer)
673{
674 QTemporaryFile *tempFile = dumpAttachmentToTempFile(d->selectedParts.constFirst());
675 if (!tempFile) {
676 KMessageBox::error(d->wParent,
677 i18n("KMail was unable to write the attachment to a temporary file."),
678 i18nc("@title:window", "Unable to open attachment"));
679 return;
680 }
681 QUrl url = QUrl::fromLocalFile(tempFile->fileName());
682 tempFile->setPermissions(QFile::ReadUser);
683 // If offer is null, this will show the "open with" dialog
684 auto job = new KIO::ApplicationLauncherJob(offer);
685 job->setUrls({url});
687 job->start();
688 connect(job, &KJob::result, this, [tempFile, job]() {
689 if (job->error()) {
690 delete tempFile;
691 }
692 });
693 // Delete the file only when the composer is closed
694 // (and this object is destroyed).
695 tempFile->setParent(this); // Manages lifetime.
696}
697
698void AttachmentControllerBase::openAttachment(const AttachmentPart::Ptr &part)
699{
700 QTemporaryFile *tempFile = dumpAttachmentToTempFile(part);
701 if (!tempFile) {
702 KMessageBox::error(d->wParent,
703 i18n("KMail was unable to write the attachment to a temporary file."),
704 i18nc("@title:window", "Unable to open attachment"));
705 return;
706 }
707 tempFile->setPermissions(QFile::ReadUser);
708 auto job = new KIO::OpenUrlJob(QUrl::fromLocalFile(tempFile->fileName()), QString::fromLatin1(part->mimeType()));
710 job->setDeleteTemporaryFile(true);
711 connect(job, &KIO::OpenUrlJob::result, this, [this, tempFile](KJob *job) {
712 if (job->error() == KIO::ERR_USER_CANCELED) {
713 KMessageBox::error(d->wParent, i18n("KMail was unable to open the attachment."), job->errorString());
714 delete tempFile;
715 } else {
716 // The file was opened. Delete it only when the composer is closed
717 // (and this object is destroyed).
718 tempFile->setParent(this); // Manages lifetime.
719 }
720 });
721 job->start();
722}
723
724void AttachmentControllerBase::viewAttachment(const AttachmentPart::Ptr &part)
725{
727 composer->globalPart()->setFallbackCharsetEnabled(true);
729 connect(attachmentJob, &AttachmentJob::result, this, [this](KJob *job) {
730 d->slotAttachmentContentCreated(job);
731 });
732 attachmentJob->start();
733}
734
735void AttachmentControllerBase::AttachmentControllerBasePrivate::slotAttachmentContentCreated(KJob *job)
736{
737 if (!job->error()) {
740 if (attachmentJob) {
741 Q_EMIT q->showAttachment(attachmentJob->content(), QByteArray());
742 }
743 } else {
744 // TODO: show warning to the user
745 qCWarning(MESSAGECOMPOSER_LOG) << "Error creating KMime::Content for attachment:" << job->errorText();
746 }
747}
748
749void AttachmentControllerBase::editAttachment(AttachmentPart::Ptr part, MessageComposer::EditorWatcher::OpenWithOption openWithOption)
750{
751 QTemporaryFile *tempFile = dumpAttachmentToTempFile(part);
752 if (!tempFile) {
753 KMessageBox::error(d->wParent,
754 i18n("KMail was unable to write the attachment to a temporary file."),
755 i18nc("@title:window", "Unable to edit attachment"));
756 return;
757 }
758
759 auto watcher =
760 new MessageComposer::EditorWatcher(QUrl::fromLocalFile(tempFile->fileName()), QString::fromLatin1(part->mimeType()), openWithOption, this, d->wParent);
761 connect(watcher, &MessageComposer::EditorWatcher::editDone, this, [this](MessageComposer::EditorWatcher *watcher) {
762 d->editDone(watcher);
763 });
764
765 switch (watcher->start()) {
766 case MessageComposer::EditorWatcher::NoError:
767 // The attachment is being edited.
768 // We will clean things up in editDone().
769 d->editorPart[watcher] = part;
770 d->editorTempFile[watcher] = tempFile;
771
772 // Delete the temp file if the composer is closed (and this object is destroyed).
773 tempFile->setParent(this); // Manages lifetime.
774 break;
775 case MessageComposer::EditorWatcher::CannotStart:
776 qCWarning(MESSAGECOMPOSER_LOG) << "Could not start EditorWatcher.";
777 [[fallthrough]];
778 case MessageComposer::EditorWatcher::Unknown:
779 case MessageComposer::EditorWatcher::Canceled:
780 case MessageComposer::EditorWatcher::NoServiceFound:
781 delete watcher;
782 delete tempFile;
783 break;
784 }
785}
786
787void AttachmentControllerBase::editAttachmentWith(const AttachmentPart::Ptr &part)
788{
789 editAttachment(part, MessageComposer::EditorWatcher::OpenWithDialog);
790}
791
792void AttachmentControllerBase::saveAttachmentAs(const AttachmentPart::Ptr &part)
793{
794 QString pname = part->name();
795 if (pname.isEmpty()) {
796 pname = i18n("unnamed");
797 }
798
799 const QUrl url = QFileDialog::getSaveFileUrl(d->wParent, i18n("Save Attachment As"), QUrl::fromLocalFile(pname));
800
801 if (url.isEmpty()) {
802 qCDebug(MESSAGECOMPOSER_LOG) << "Save Attachment As dialog canceled.";
803 return;
804 }
805
806 byteArrayToRemoteFile(part->data(), url);
807}
808
809void AttachmentControllerBase::byteArrayToRemoteFile(const QByteArray &aData, const QUrl &aURL, bool overwrite)
810{
812 connect(job, &KIO::StoredTransferJob::result, this, &AttachmentControllerBase::slotPutResult);
813}
814
815void AttachmentControllerBase::slotPutResult(KJob *job)
816{
818
819 if (job->error()) {
820 if (job->error() == KIO::ERR_FILE_ALREADY_EXIST) {
822 i18n("File %1 exists.\nDo you want to replace it?", _job->url().toLocalFile()),
823 i18nc("@title:window", "Save to File"),
824 KGuiItem(i18n("&Replace")))
826 byteArrayToRemoteFile(_job->data(), _job->url(), true);
827 }
828 } else {
829 KJobUiDelegate *ui = static_cast<KIO::Job *>(job)->uiDelegate();
830 ui->showErrorMessage();
831 }
832 }
833}
834
835void AttachmentControllerBase::attachmentProperties(const AttachmentPart::Ptr &part)
836{
837 QPointer<AttachmentPropertiesDialog> dialog = new AttachmentPropertiesDialog(part, false, d->wParent);
838
839 dialog->setEncryptEnabled(d->encryptEnabled);
840 dialog->setSignEnabled(d->signEnabled);
841
842 if (dialog->exec() && dialog) {
843 d->model->updateAttachment(part);
844 }
845 delete dialog;
846}
847
848void AttachmentControllerBase::attachDirectory(const QUrl &url)
849{
850 const int rc = KMessageBox::warningTwoActions(d->wParent,
851 i18n("Do you really want to attach this directory \"%1\"?", url.toLocalFile()),
852 i18nc("@title:window", "Attach directory"),
853 KGuiItem(i18nc("@action:button", "Attach")),
855 if (rc == KMessageBox::ButtonCode::PrimaryAction) {
856 addAttachment(url);
857 }
858}
859
860void AttachmentControllerBase::showAttachVcard()
861{
863 dlg->view()->view()->setSelectionMode(QAbstractItemView::MultiSelection);
864 if (dlg->exec()) {
865 const Akonadi::EmailAddressSelection::List selectedEmail = dlg->selectedAddresses();
866 for (const Akonadi::EmailAddressSelection &selected : selectedEmail) {
867 auto ajob = new MessageComposer::AttachmentVcardFromAddressBookJob(selected.item(), this);
869 d->attachVcardFromAddressBook(job);
870 });
871 ajob->start();
872 }
873 }
874 delete dlg;
875}
876
877void AttachmentControllerBase::showAttachClipBoard()
878{
879 auto job = new MessageComposer::AttachmentClipBoardJob(this);
880 connect(job, &AttachmentClipBoardJob::result, this, [this](KJob *job) {
881 d->attachClipBoardElement(job);
882 });
883 job->start();
884}
885
886void AttachmentControllerBase::showAddAttachmentCompressedDirectoryDialog()
887{
888 const QUrl url = QFileDialog::getExistingDirectoryUrl(d->wParent, i18nc("@title:window", "Attach Directory"));
889 if (url.isValid()) {
890 attachDirectory(url);
891 }
892}
893
894void AttachmentControllerBase::showAddAttachmentFileDialog()
895{
896 const KEncodingFileDialog::Result result =
897 KEncodingFileDialog::getOpenUrlsAndEncoding(QString(), QUrl(), QString(), d->wParent, i18nc("@title:window", "Attach File"));
898 if (!result.URLs.isEmpty()) {
899 const QString encoding = MimeTreeParser::NodeHelper::fixEncoding(result.encoding);
900 const int numberOfFiles(result.URLs.count());
901 for (int i = 0; i < numberOfFiles; ++i) {
902 const QUrl url = result.URLs.at(i);
903 QUrl urlWithEncoding = url;
904 MessageCore::StringUtil::setEncodingFile(urlWithEncoding, encoding);
906 const auto mimeType = mimeDb.mimeTypeForUrl(urlWithEncoding);
907 if (mimeType.name() == QLatin1StringView("inode/directory")) {
908 const int rc = KMessageBox::warningTwoActions(d->wParent,
909 i18n("Do you really want to attach this directory \"%1\"?", url.toLocalFile()),
910 i18nc("@title:window", "Attach directory"),
911 KGuiItem(i18nc("@action:button", "Attach")),
913 if (rc == KMessageBox::ButtonCode::PrimaryAction) {
915 }
916 } else {
918 }
919 }
920 }
921}
922
924{
925 part->setEncrypted(d->model->isEncryptSelected());
926 part->setSigned(d->model->isSignSelected());
927 d->model->addAttachment(part);
928
929 Q_EMIT fileAttached();
930}
931
932void AttachmentControllerBase::addAttachmentUrlSync(const QUrl &url)
933{
934 MessageCore::AttachmentFromUrlBaseJob *ajob = MessageCore::AttachmentFromUrlUtils::createAttachmentJob(url, this);
935 if (ajob->exec()) {
936 AttachmentPart::Ptr part = ajob->attachmentPart();
937 addAttachment(part);
938 } else {
939 if (ajob->error()) {
940 KMessageBox::error(d->wParent, ajob->errorString(), i18nc("@title:window", "Failed to attach file"));
941 }
942 }
943}
944
946{
947 MessageCore::AttachmentFromUrlBaseJob *ajob = MessageCore::AttachmentFromUrlUtils::createAttachmentJob(url, this);
948 connect(ajob, &AttachmentFromUrlBaseJob::result, this, [this](KJob *job) {
949 d->loadJobResult(job);
950 });
951 ajob->start();
952}
953
954void AttachmentControllerBase::addAttachments(const QList<QUrl> &urls)
955{
956 for (const QUrl &url : urls) {
957 addAttachment(url);
958 }
959}
960
961void AttachmentControllerBase::showAttachPublicKeyDialog()
962{
963 using Kleo::KeySelectionDialog;
964 QPointer<KeySelectionDialog> dialog = new KeySelectionDialog(i18n("Attach Public OpenPGP Key"),
965 i18n("Select the public key which should be attached."),
966 std::vector<GpgME::Key>(),
967 KeySelectionDialog::PublicKeys | KeySelectionDialog::OpenPGPKeys,
968 false /* no multi selection */,
969 false /* no remember choice box */,
970 d->wParent);
971
972 if (dialog->exec() == QDialog::Accepted) {
973 exportPublicKey(dialog->fingerprint());
974 }
975 delete dialog;
976}
977
978void AttachmentControllerBase::attachMyPublicKey()
979{
980}
981
982void AttachmentControllerBase::enableAttachPublicKey(bool enable)
983{
984 d->attachPublicKeyAction->setEnabled(enable);
985}
986
987void AttachmentControllerBase::enableAttachMyPublicKey(bool enable)
988{
989 d->attachMyPublicKeyAction->setEnabled(enable);
990}
991
992void AttachmentControllerBase::setAttachOwnVcard(bool attachVcard)
993{
994 d->addOwnVcardAction->setChecked(attachVcard);
995}
996
997bool AttachmentControllerBase::attachOwnVcard() const
998{
999 return d->addOwnVcardAction->isChecked();
1000}
1001
1002void AttachmentControllerBase::setIdentityHasOwnVcard(bool state)
1003{
1004 d->addOwnVcardAction->setEnabled(state);
1005}
1006
1007#include "moc_attachmentcontrollerbase.cpp"
QAction * addAction(const QString &name, const QObject *receiver=nullptr, const char *member=nullptr)
static void setDefaultShortcut(QAction *action, const QKeySequence &shortcut)
static KIOFILEWIDGETS_EXPORT Result getOpenUrlsAndEncoding(const QString &encoding=QString(), const QUrl &startDir=QUrl(), const QString &filter=QString(), QWidget *parent=nullptr, const QString &title=QString())
static KService::List associatedApplications(const QStringList &mimeTypeList)
virtual void showErrorMessage()
virtual QString errorString() const
int error() const
void result(KJob *job)
virtual Q_SCRIPTABLE void start()=0
QString errorText() const
void setUiDelegate(KJobUiDelegate *delegate)
The AttachmentClipBoardJob class.
void addAttachment(const MessageCore::AttachmentPart::Ptr &part)
sets sign, encrypt, shows properties dialog if so configured
void setEncryptEnabled(bool enabled)
model sets these
void compressAttachment(const MessageCore::AttachmentPart::Ptr &part, bool compress)
compression is async...
The AttachmentJob class.
The AttachmentModel class.
The Composer class.
Definition composer.h:35
Starts an editor for the given URL and emits an signal when editing has been finished.
A job to compress the attachment of an email.
A dialog for editing attachment properties.
static QString fixEncoding(const QString &encoding)
Fixes an encoding received by a KDE function and returns the proper, MIME-compliant encoding name ins...
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
KCALUTILS_EXPORT QString mimeType()
KIOCORE_EXPORT KJobUiDelegate * createDefaultJobUiDelegate()
KIOCORE_EXPORT StoredTransferJob * storedPut(const QByteArray &arr, const QUrl &url, int permissions, JobFlags flags=DefaultFlags)
DefaultFlags
ButtonCode warningContinueCancel(QWidget *parent, const QString &text, const QString &title=QString(), const KGuiItem &buttonContinue=KStandardGuiItem::cont(), const KGuiItem &buttonCancel=KStandardGuiItem::cancel(), const QString &dontAskAgainName=QString(), Options options=Notify)
ButtonCode warningTwoActions(QWidget *parent, const QString &text, const QString &title, const KGuiItem &primaryAction, const KGuiItem &secondaryAction, const QString &dontAskAgainName=QString(), Options options=Options(Notify|Dangerous))
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
ButtonCode questionTwoActions(QWidget *parent, const QString &text, const QString &title, const KGuiItem &primaryAction, const KGuiItem &secondaryAction, const QString &dontAskAgainName=QString(), Options options=Notify)
KGuiItem cancel()
const QList< QKeySequence > & end()
QString name(StandardShortcut id)
Simple interface that both EncryptJob and SignEncryptJob implement so the composer can extract some e...
void triggered(bool checked)
void triggered(QAction *action)
QPoint pos()
QUrl getExistingDirectoryUrl(QWidget *parent, const QString &caption, const QUrl &dir, QFileDialog::Options options, const QStringList &supportedSchemes)
QUrl getSaveFileUrl(QWidget *parent, const QString &caption, const QUrl &dir, const QString &filter, QString *selectedFilter, QFileDialog::Options options, const QStringList &supportedSchemes)
int remove(const Key &key)
QIcon fromTheme(const QString &name)
typedef ConstIterator
const T & constFirst() const const
int count(const T &value) const const
T & first()
QAction * addAction(const QString &text)
QAction * addSeparator()
QAction * exec()
QAction * menuAction() const const
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
void setObjectName(const QString &name)
T qobject_cast(QObject *object)
QString fromLatin1(const char *str, int size)
bool isEmpty() const const
virtual QString fileName() const const override
QUrl fromLocalFile(const QString &localFile)
bool isEmpty() const const
bool isValid() const const
QString toLocalFile() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Sun Feb 25 2024 18:37:30 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.