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#include <QStringConverter>
31
32#include <QMenu>
33#include <QPointer>
34#include <QTreeView>
35
36#include "messagecomposer_debug.h"
37#include <KActionCollection>
38#include <KActionMenu>
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(i18nc("@action:button", "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
204 auto ajob = qobject_cast<AttachmentLoadJob *>(job);
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);
294 Q_ASSERT(tempFile);
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());
313 const KService::List offers = KFileItemActions::associatedApplications(QStringList() << contentTypeStr);
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
337 QString openWithActionName;
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);
345 openWithAct->setText(openWithActionName);
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);
364 connect(ajob, &AttachmentFromPublicKeyJob::result, this, [this](KJob *job) {
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
380 Q_ASSERT(dynamic_cast<MessageComposer::AttachmentFromPublicKeyJob *>(job));
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
394 auto ajob = static_cast<MessageComposer::AttachmentVcardFromAddressBookJob *>(job);
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(i18nc("@action", "Attach &Public Key..."), this);
452 connect(d->attachPublicKeyAction, &QAction::triggered, this, &AttachmentControllerBase::showAttachPublicKeyDialog);
453
454 d->attachMyPublicKeyAction = new QAction(i18nc("@action", "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(i18nc("@action", "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(i18nc("@action", "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(i18nc("@action", "Attachment Pr&operties..."), this);
532 d->propertiesContextAction = new QAction(i18nc("@action", "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(i18nc("@action", "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());
613 QMimeDatabase mimeDb;
614 auto mime = mimeDb.mimeTypeForName(mimetype);
615 QStringList parentMimeType;
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();
648 if (nbAttachment != numberOfParts) {
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());
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 }
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{
726 auto composer = new MessageComposer::Composer;
727 auto attachmentJob = new MessageComposer::AttachmentJob(part, composer);
728 connect(attachmentJob, &AttachmentJob::result, this, [this](KJob *job) {
729 d->slotAttachmentContentCreated(job);
730 });
731 attachmentJob->start();
732}
733
734void AttachmentControllerBase::AttachmentControllerBasePrivate::slotAttachmentContentCreated(KJob *job)
735{
736 if (!job->error()) {
738 Q_ASSERT(attachmentJob);
739 if (attachmentJob) {
740 Q_EMIT q->showAttachment(attachmentJob->content(), QByteArray());
741 }
742 } else {
743 // TODO: show warning to the user
744 qCWarning(MESSAGECOMPOSER_LOG) << "Error creating KMime::Content for attachment:" << job->errorText();
745 }
746}
747
748void AttachmentControllerBase::editAttachment(AttachmentPart::Ptr part, MessageComposer::EditorWatcher::OpenWithOption openWithOption)
749{
750 QTemporaryFile *tempFile = dumpAttachmentToTempFile(part);
751 if (!tempFile) {
752 KMessageBox::error(d->wParent,
753 i18n("KMail was unable to write the attachment to a temporary file."),
754 i18nc("@title:window", "Unable to edit attachment"));
755 return;
756 }
757
758 auto watcher =
759 new MessageComposer::EditorWatcher(QUrl::fromLocalFile(tempFile->fileName()), QString::fromLatin1(part->mimeType()), openWithOption, this, d->wParent);
760 connect(watcher, &MessageComposer::EditorWatcher::editDone, this, [this](MessageComposer::EditorWatcher *watcher) {
761 d->editDone(watcher);
762 });
763
764 switch (watcher->start()) {
765 case MessageComposer::EditorWatcher::NoError:
766 // The attachment is being edited.
767 // We will clean things up in editDone().
768 d->editorPart[watcher] = part;
769 d->editorTempFile[watcher] = tempFile;
770
771 // Delete the temp file if the composer is closed (and this object is destroyed).
772 tempFile->setParent(this); // Manages lifetime.
773 break;
774 case MessageComposer::EditorWatcher::CannotStart:
775 qCWarning(MESSAGECOMPOSER_LOG) << "Could not start EditorWatcher.";
776 [[fallthrough]];
777 case MessageComposer::EditorWatcher::Unknown:
778 case MessageComposer::EditorWatcher::Canceled:
779 case MessageComposer::EditorWatcher::NoServiceFound:
780 delete watcher;
781 delete tempFile;
782 break;
783 }
784}
785
786void AttachmentControllerBase::editAttachmentWith(const AttachmentPart::Ptr &part)
787{
788 editAttachment(part, MessageComposer::EditorWatcher::OpenWithDialog);
789}
790
791void AttachmentControllerBase::saveAttachmentAs(const AttachmentPart::Ptr &part)
792{
793 QString pname = part->name();
794 if (pname.isEmpty()) {
795 pname = i18n("unnamed");
796 }
797
798 const QUrl url = QFileDialog::getSaveFileUrl(d->wParent, i18n("Save Attachment As"), QUrl::fromLocalFile(pname));
799
800 if (url.isEmpty()) {
801 qCDebug(MESSAGECOMPOSER_LOG) << "Save Attachment As dialog canceled.";
802 return;
803 }
804
805 byteArrayToRemoteFile(part->data(), url);
806}
807
808void AttachmentControllerBase::byteArrayToRemoteFile(const QByteArray &aData, const QUrl &aURL, bool overwrite)
809{
810 KIO::StoredTransferJob *job = KIO::storedPut(aData, aURL, -1, overwrite ? KIO::Overwrite : KIO::DefaultFlags);
811 connect(job, &KIO::StoredTransferJob::result, this, &AttachmentControllerBase::slotPutResult);
812}
813
814void AttachmentControllerBase::slotPutResult(KJob *job)
815{
817
818 if (job->error()) {
819 if (job->error() == KIO::ERR_FILE_ALREADY_EXIST) {
821 i18n("File %1 exists.\nDo you want to replace it?", _job->url().toLocalFile()),
822 i18nc("@title:window", "Save to File"),
823 KGuiItem(i18nc("@action:button", "&Replace")))
825 byteArrayToRemoteFile(_job->data(), _job->url(), true);
826 }
827 } else {
828 KJobUiDelegate *ui = static_cast<KIO::Job *>(job)->uiDelegate();
829 ui->showErrorMessage();
830 }
831 }
832}
833
834void AttachmentControllerBase::attachmentProperties(const AttachmentPart::Ptr &part)
835{
836 QPointer<AttachmentPropertiesDialog> dialog = new AttachmentPropertiesDialog(part, false, d->wParent);
837
838 dialog->setEncryptEnabled(d->encryptEnabled);
839 dialog->setSignEnabled(d->signEnabled);
840
841 if (dialog->exec() && dialog) {
842 d->model->updateAttachment(part);
843 }
844 delete dialog;
845}
846
847void AttachmentControllerBase::attachDirectory(const QUrl &url)
848{
849 const int rc = KMessageBox::warningTwoActions(d->wParent,
850 i18n("Do you really want to attach this directory \"%1\"?", url.toLocalFile()),
851 i18nc("@title:window", "Attach directory"),
852 KGuiItem(i18nc("@action:button", "Attach")),
854 if (rc == KMessageBox::ButtonCode::PrimaryAction) {
855 addAttachment(url);
856 }
857}
858
859void AttachmentControllerBase::showAttachVcard()
860{
862 dlg->view()->view()->setSelectionMode(QAbstractItemView::MultiSelection);
863 if (dlg->exec()) {
864 const Akonadi::EmailAddressSelection::List selectedEmail = dlg->selectedAddresses();
865 for (const Akonadi::EmailAddressSelection &selected : selectedEmail) {
866 auto ajob = new MessageComposer::AttachmentVcardFromAddressBookJob(selected.item(), this);
867 connect(ajob, &AttachmentVcardFromAddressBookJob::result, this, [this](KJob *job) {
868 d->attachVcardFromAddressBook(job);
869 });
870 ajob->start();
871 }
872 }
873 delete dlg;
874}
875
876void AttachmentControllerBase::showAttachClipBoard()
877{
878 auto job = new MessageComposer::AttachmentClipBoardJob(this);
879 connect(job, &AttachmentClipBoardJob::result, this, [this](KJob *job) {
880 d->attachClipBoardElement(job);
881 });
882 job->start();
883}
884
885void AttachmentControllerBase::showAddAttachmentCompressedDirectoryDialog()
886{
887 const QUrl url = QFileDialog::getExistingDirectoryUrl(d->wParent, i18nc("@title:window", "Attach Directory"));
888 if (url.isValid()) {
889 attachDirectory(url);
890 }
891}
892
893void AttachmentControllerBase::showAddAttachmentFileDialog()
894{
895 const auto urls = QFileDialog::getOpenFileUrls(d->wParent, i18nc("@title:window", "Attach File"));
896 if (!urls.isEmpty()) {
897 const int numberOfFiles(urls.count());
898 for (int i = 0; i < numberOfFiles; ++i) {
899 const QUrl url = urls.at(i);
900 std::optional<QStringConverter::Encoding> encoding;
901
902 QFile file(url.toLocalFile());
903 if (file.open(QIODeviceBase::ReadOnly)) {
904 auto content = file.read(1024 * 1024); // only read the first 1MB
905 if (content.isEmpty()) {
906 encoding = QStringConverter::System;
907 } else if (url.toLocalFile().endsWith(QStringLiteral("html"))) {
908 encoding = QStringConverter::encodingForHtml(content);
909 } else {
910 encoding = QStringConverter::encodingForData(content);
911 }
912 }
913
914 auto encodingName = QStringConverter::nameForEncoding(encoding.value_or(QStringConverter::System));
915 if (!encodingName) {
916 encodingName = "UTF-8";
917 }
918
919 if (strcmp(encodingName, "Locale") == 0) {
920 encodingName = "UTF-8";
921 }
922
923 QUrl urlWithEncoding = url;
924 MessageCore::StringUtil::setEncodingFile(urlWithEncoding, QLatin1StringView(encodingName));
925 QMimeDatabase mimeDb;
926 const auto mimeType = mimeDb.mimeTypeForUrl(urlWithEncoding);
927 if (mimeType.name() == QLatin1StringView("inode/directory")) {
928 const int rc = KMessageBox::warningTwoActions(d->wParent,
929 i18n("Do you really want to attach this directory \"%1\"?", url.toLocalFile()),
930 i18nc("@title:window", "Attach directory"),
931 KGuiItem(i18nc("@action:button", "Attach")),
933 if (rc == KMessageBox::ButtonCode::PrimaryAction) {
934 addAttachment(urlWithEncoding);
935 }
936 } else {
937 addAttachment(urlWithEncoding);
938 }
939 }
940 }
941}
942
944{
945 part->setEncrypted(d->model->isEncryptSelected());
946 part->setSigned(d->model->isSignSelected());
947 d->model->addAttachment(part);
948
949 Q_EMIT fileAttached();
950}
951
952void AttachmentControllerBase::addAttachmentUrlSync(const QUrl &url)
953{
954 MessageCore::AttachmentFromUrlBaseJob *ajob = MessageCore::AttachmentFromUrlUtils::createAttachmentJob(url, this);
955 if (ajob->exec()) {
956 AttachmentPart::Ptr part = ajob->attachmentPart();
957 addAttachment(part);
958 } else {
959 if (ajob->error()) {
960 KMessageBox::error(d->wParent, ajob->errorString(), i18nc("@title:window", "Failed to attach file"));
961 }
962 }
963}
964
966{
967 MessageCore::AttachmentFromUrlBaseJob *ajob = MessageCore::AttachmentFromUrlUtils::createAttachmentJob(url, this);
968 connect(ajob, &AttachmentFromUrlBaseJob::result, this, [this](KJob *job) {
969 d->loadJobResult(job);
970 });
971 ajob->start();
972}
973
974void AttachmentControllerBase::addAttachments(const QList<QUrl> &urls)
975{
976 for (const QUrl &url : urls) {
977 addAttachment(url);
978 }
979}
980
981void AttachmentControllerBase::showAttachPublicKeyDialog()
982{
983 using Kleo::KeySelectionDialog;
984 QPointer<KeySelectionDialog> dialog = new KeySelectionDialog(i18n("Attach Public OpenPGP Key"),
985 i18n("Select the public key which should be attached."),
986 std::vector<GpgME::Key>(),
987 KeySelectionDialog::PublicKeys | KeySelectionDialog::OpenPGPKeys,
988 false /* no multi selection */,
989 false /* no remember choice box */,
990 d->wParent);
991
992 if (dialog->exec() == QDialog::Accepted) {
993 exportPublicKey(dialog->fingerprint());
994 }
995 delete dialog;
996}
997
998void AttachmentControllerBase::attachMyPublicKey()
999{
1000}
1001
1002void AttachmentControllerBase::enableAttachPublicKey(bool enable)
1003{
1004 d->attachPublicKeyAction->setEnabled(enable);
1005}
1006
1007void AttachmentControllerBase::enableAttachMyPublicKey(bool enable)
1008{
1009 d->attachMyPublicKeyAction->setEnabled(enable);
1010}
1011
1012void AttachmentControllerBase::setAttachOwnVcard(bool attachVcard)
1013{
1014 d->addOwnVcardAction->setChecked(attachVcard);
1015}
1016
1017bool AttachmentControllerBase::attachOwnVcard() const
1018{
1019 return d->addOwnVcardAction->isChecked();
1020}
1021
1022void AttachmentControllerBase::setIdentityHasOwnVcard(bool state)
1023{
1024 d->addOwnVcardAction->setEnabled(state);
1025}
1026
1027#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 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
KMime::Content * content() const
Get the resulting KMime::Content that the ContentJobBase has generated.
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.
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
KCALUTILS_EXPORT QString mimeType()
QString name(GameStandardAction id)
QAction * end(const QObject *recvr, const char *slot, QObject *parent)
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()
Simple interface that both EncryptJob and SignEncryptJob implement so the composer can extract some e...
QVariant data() const const
void setText(const QString &text)
void triggered(bool checked)
QPoint pos()
virtual bool setPermissions(Permissions permissions) override
QUrl getExistingDirectoryUrl(QWidget *parent, const QString &caption, const QUrl &dir, Options options, const QStringList &supportedSchemes)
QList< QUrl > getOpenFileUrls(QWidget *parent, const QString &caption, const QUrl &dir, const QString &filter, QString *selectedFilter, Options options, const QStringList &supportedSchemes)
QUrl getSaveFileUrl(QWidget *parent, const QString &caption, const QUrl &dir, const QString &filter, QString *selectedFilter, Options options, const QStringList &supportedSchemes)
bool remove(const Key &key)
QIcon fromTheme(const QString &name)
typedef ConstIterator
const_iterator constBegin() const const
const_iterator constEnd() const const
const T & constFirst() const const
qsizetype count() const const
T & first()
bool isEmpty() const const
QAction * addAction(const QIcon &icon, const QString &text, Functor functor, const QKeySequence &shortcut)
QAction * addMenu(QMenu *menu)
QAction * addSeparator()
QAction * exec()
QAction * menuAction() const const
QMimeType mimeTypeForName(const QString &nameOrAlias) const const
QMimeType mimeTypeForUrl(const QUrl &url) const const
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
T qobject_cast(QObject *object)
void setObjectName(QAnyStringView name)
void setParent(QObject *parent)
bool endsWith(QChar c, Qt::CaseSensitivity cs) const const
QString fromLatin1(QByteArrayView str)
bool isEmpty() const const
std::optional< Encoding > encodingForData(QByteArrayView data, char16_t expectedFirstCharacter)
std::optional< Encoding > encodingForHtml(QByteArrayView data)
const char * nameForEncoding(Encoding e)
bool contains(QLatin1StringView str, Qt::CaseSensitivity cs) const const
virtual QString fileName() const const override
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
QUrl fromLocalFile(const QString &localFile)
bool isEmpty() const const
bool isValid() const const
QString path(ComponentFormattingOptions options) const const
QString toLocalFile() const const
T value() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Jul 26 2024 11:54:19 by doxygen 1.11.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.