1 /*
2   SPDX-FileCopyrightText: 2010 Bertjan Broeksema <broeksema@kde.org>
3   SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
4 
5   SPDX-License-Identifier: LGPL-2.0-or-later
6 */
7 
8 #include "incidenceattachment.h"
9 #include "attachmenteditdialog.h"
10 #include "attachmenticonview.h"
11 #include "ui_dialogdesktop.h"
12 
13 #include <CalendarSupport/UriHandler>
14 
15 #include <KContacts/VCardDrag>
16 
17 #include <KMime/Message>
18 
19 #include <KActionCollection>
20 #include <KIO/Job>
21 #include <KIO/JobUiDelegate>
22 #include <KIO/OpenUrlJob>
23 #include <KIO/StoredTransferJob>
24 #include <KJobWidgets>
25 #include <KLocalizedString>
26 #include <KMessageBox>
27 #include <KProtocolManager>
28 #include <QAction>
29 #include <QFileDialog>
30 #include <QIcon>
31 #include <QMenu>
32 #include <QUrl>
33 
34 #include <QClipboard>
35 #include <QMimeData>
36 #include <QMimeDatabase>
37 #include <QMimeType>
38 
39 using namespace IncidenceEditorNG;
40 
IncidenceAttachment(Ui::EventOrTodoDesktop * ui)41 IncidenceAttachment::IncidenceAttachment(Ui::EventOrTodoDesktop *ui)
42     : IncidenceEditor(nullptr)
43     , mUi(ui)
44     , mPopupMenu(new QMenu)
45 {
46     setupActions();
47     setupAttachmentIconView();
48     setObjectName(QStringLiteral("IncidenceAttachment"));
49 
50     connect(mUi->mAddButton, &QPushButton::clicked, this, &IncidenceAttachment::addAttachment);
51     connect(mUi->mRemoveButton, &QPushButton::clicked, this, &IncidenceAttachment::removeSelectedAttachments);
52 }
53 
~IncidenceAttachment()54 IncidenceAttachment::~IncidenceAttachment()
55 {
56     delete mPopupMenu;
57 }
58 
load(const KCalendarCore::Incidence::Ptr & incidence)59 void IncidenceAttachment::load(const KCalendarCore::Incidence::Ptr &incidence)
60 {
61     mLoadedIncidence = incidence;
62     mAttachmentView->clear();
63 
64     KCalendarCore::Attachment::List attachments = incidence->attachments();
65     for (KCalendarCore::Attachment::List::ConstIterator it = attachments.constBegin(), end = attachments.constEnd(); it != end; ++it) {
66         new AttachmentIconItem((*it), mAttachmentView);
67     }
68 
69     mWasDirty = false;
70 }
71 
save(const KCalendarCore::Incidence::Ptr & incidence)72 void IncidenceAttachment::save(const KCalendarCore::Incidence::Ptr &incidence)
73 {
74     incidence->clearAttachments();
75 
76     for (int itemIndex = 0; itemIndex < mAttachmentView->count(); ++itemIndex) {
77         QListWidgetItem *item = mAttachmentView->item(itemIndex);
78         auto attitem = dynamic_cast<AttachmentIconItem *>(item);
79         Q_ASSERT(item);
80         incidence->addAttachment(attitem->attachment());
81     }
82 }
83 
isDirty() const84 bool IncidenceAttachment::isDirty() const
85 {
86     if (mLoadedIncidence) {
87         if (mAttachmentView->count() != mLoadedIncidence->attachments().count()) {
88             return true;
89         }
90 
91         KCalendarCore::Attachment::List origAttachments = mLoadedIncidence->attachments();
92         for (int itemIndex = 0; itemIndex < mAttachmentView->count(); ++itemIndex) {
93             QListWidgetItem *item = mAttachmentView->item(itemIndex);
94             Q_ASSERT(dynamic_cast<AttachmentIconItem *>(item));
95 
96             const KCalendarCore::Attachment listAttachment = static_cast<AttachmentIconItem *>(item)->attachment();
97 
98             for (int i = 0; i < origAttachments.count(); ++i) {
99                 const KCalendarCore::Attachment attachment = origAttachments.at(i);
100 
101                 if (attachment == listAttachment) {
102                     origAttachments.remove(i);
103                     break;
104                 }
105             }
106         }
107         // All attachments are removed from the list, meaning, the items in mAttachmentView
108         // are equal to the attachments set on mLoadedIncidence.
109         return !origAttachments.isEmpty();
110     } else {
111         // No incidence loaded, so if the user added attachments we're dirty.
112         return mAttachmentView->count() != 0;
113     }
114 }
115 
attachmentCount() const116 int IncidenceAttachment::attachmentCount() const
117 {
118     return mAttachmentView->count();
119 }
120 
121 /// Private slots
122 
addAttachment()123 void IncidenceAttachment::addAttachment()
124 {
125     QPointer<QObject> that(this);
126     auto item = new AttachmentIconItem(KCalendarCore::Attachment(), mAttachmentView);
127 
128     QPointer<AttachmentEditDialog> dialog(new AttachmentEditDialog(item, mAttachmentView));
129     dialog->setWindowTitle(i18nc("@title", "Add Attachment"));
130     auto dialogResult = dialog->exec();
131     if (!that) {
132         return;
133     }
134 
135     if (dialogResult == QDialog::Rejected) {
136         delete item;
137     } else {
138         Q_EMIT attachmentCountChanged(mAttachmentView->count());
139     }
140     delete dialog;
141 
142     checkDirtyStatus();
143 }
144 
copyToClipboard()145 void IncidenceAttachment::copyToClipboard()
146 {
147 #ifndef QT_NO_CLIPBOARD
148     QApplication::clipboard()->setMimeData(mAttachmentView->mimeData(), QClipboard::Clipboard);
149 #endif
150 }
151 
openURL(const QUrl & url)152 void IncidenceAttachment::openURL(const QUrl &url)
153 {
154     QString uri = url.url();
155     CalendarSupport::UriHandler::process(uri);
156 }
157 
pasteFromClipboard()158 void IncidenceAttachment::pasteFromClipboard()
159 {
160 #ifndef QT_NO_CLIPBOARD
161     handlePasteOrDrop(QApplication::clipboard()->mimeData());
162 #endif
163 }
164 
removeSelectedAttachments()165 void IncidenceAttachment::removeSelectedAttachments()
166 {
167     QList<QListWidgetItem *> selected;
168     QStringList labels;
169     selected.reserve(mAttachmentView->count());
170     labels.reserve(mAttachmentView->count());
171 
172     for (int itemIndex = 0; itemIndex < mAttachmentView->count(); ++itemIndex) {
173         QListWidgetItem *it = mAttachmentView->item(itemIndex);
174         if (it->isSelected()) {
175             auto attitem = static_cast<AttachmentIconItem *>(it);
176             if (attitem) {
177                 const KCalendarCore::Attachment att = attitem->attachment();
178                 labels << att.label();
179                 selected << it;
180             }
181         }
182     }
183 
184     if (selected.isEmpty()) {
185         return;
186     }
187 
188     QString labelsStr = labels.join(QLatin1String("<nl/>"));
189 
190     if (KMessageBox::questionYesNo(nullptr,
191                                    xi18nc("@info", "Do you really want to remove these attachments?<nl/>%1", labelsStr),
192                                    i18nc("@title:window", "Remove Attachments?"),
193                                    KStandardGuiItem::remove(),
194                                    KStandardGuiItem::cancel(),
195                                    QStringLiteral("calendarRemoveAttachments"))
196         != KMessageBox::Yes) {
197         return;
198     }
199 
200     for (QList<QListWidgetItem *>::iterator it(selected.begin()), end(selected.end()); it != end; ++it) {
201         int row = mAttachmentView->row(*it);
202         QListWidgetItem *next = mAttachmentView->item(++row);
203         QListWidgetItem *prev = mAttachmentView->item(--row);
204         if (next) {
205             next->setSelected(true);
206         } else if (prev) {
207             prev->setSelected(true);
208         }
209         delete *it;
210     }
211 
212     mAttachmentView->update();
213     Q_EMIT attachmentCountChanged(mAttachmentView->count());
214     checkDirtyStatus();
215 }
216 
saveAttachment(QListWidgetItem * item)217 void IncidenceAttachment::saveAttachment(QListWidgetItem *item)
218 {
219     Q_ASSERT(item);
220     Q_ASSERT(dynamic_cast<AttachmentIconItem *>(item));
221 
222     auto attitem = static_cast<AttachmentIconItem *>(item);
223     if (attitem->attachment().isEmpty()) {
224         return;
225     }
226 
227     KCalendarCore::Attachment att = attitem->attachment();
228 
229     // get the saveas file name
230     const QString saveAsFile = QFileDialog::getSaveFileName(nullptr, i18nc("@title", "Save Attachment"), att.label());
231 
232     if (saveAsFile.isEmpty()) {
233         return;
234     }
235 
236     QUrl sourceUrl;
237     if (att.isUri()) {
238         sourceUrl = QUrl(att.uri());
239     } else {
240         sourceUrl = attitem->tempFileForAttachment();
241     }
242     // save the attachment url
243     auto job = KIO::file_copy(sourceUrl, QUrl::fromLocalFile(saveAsFile));
244     if (!job->exec() && job->error()) {
245         KMessageBox::error(nullptr, job->errorString());
246     }
247 }
248 
saveSelectedAttachments()249 void IncidenceAttachment::saveSelectedAttachments()
250 {
251     for (int itemIndex = 0; itemIndex < mAttachmentView->count(); ++itemIndex) {
252         QListWidgetItem *item = mAttachmentView->item(itemIndex);
253         if (item->isSelected()) {
254             saveAttachment(item);
255         }
256     }
257 }
258 
showAttachment(QListWidgetItem * item)259 void IncidenceAttachment::showAttachment(QListWidgetItem *item)
260 {
261     Q_ASSERT(item);
262     Q_ASSERT(dynamic_cast<AttachmentIconItem *>(item));
263     auto attitem = static_cast<AttachmentIconItem *>(item);
264     if (attitem->attachment().isEmpty()) {
265         return;
266     }
267 
268     const KCalendarCore::Attachment att = attitem->attachment();
269     if (att.isUri()) {
270         openURL(QUrl(att.uri()));
271     } else {
272         auto job = new KIO::OpenUrlJob(attitem->tempFileForAttachment(), att.mimeType());
273         job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, mAttachmentView));
274         job->setDeleteTemporaryFile(true);
275         job->start();
276     }
277 }
278 
showContextMenu(const QPoint & pos)279 void IncidenceAttachment::showContextMenu(const QPoint &pos)    // clazy:exclude=function-args-by-value
280 {
281     const bool enable = mAttachmentView->itemAt(pos) != nullptr;
282 
283     int numSelected = 0;
284     for (int itemIndex = 0; itemIndex < mAttachmentView->count(); ++itemIndex) {
285         QListWidgetItem *item = mAttachmentView->item(itemIndex);
286         if (item->isSelected()) {
287             numSelected++;
288         }
289     }
290 
291     mOpenAction->setEnabled(enable);
292     // TODO: support saving multiple attachments into a directory
293     mSaveAsAction->setEnabled(enable && numSelected == 1);
294 #ifndef QT_NO_CLIPBOARD
295     mCopyAction->setEnabled(enable && numSelected == 1);
296     mCutAction->setEnabled(enable && numSelected == 1);
297 #endif
298     mDeleteAction->setEnabled(enable);
299     mEditAction->setEnabled(enable);
300     mPopupMenu->exec(mAttachmentView->mapToGlobal(pos));
301 }
302 
showSelectedAttachments()303 void IncidenceAttachment::showSelectedAttachments()
304 {
305     for (int itemIndex = 0; itemIndex < mAttachmentView->count(); ++itemIndex) {
306         QListWidgetItem *item = mAttachmentView->item(itemIndex);
307         if (item->isSelected()) {
308             showAttachment(item);
309         }
310     }
311 }
312 
cutToClipboard()313 void IncidenceAttachment::cutToClipboard()
314 {
315 #ifndef QT_NO_CLIPBOARD
316     copyToClipboard();
317     removeSelectedAttachments();
318 #endif
319 }
320 
editSelectedAttachments()321 void IncidenceAttachment::editSelectedAttachments()
322 {
323     for (int itemIndex = 0; itemIndex < mAttachmentView->count(); ++itemIndex) {
324         QListWidgetItem *item = mAttachmentView->item(itemIndex);
325         if (item->isSelected()) {
326             Q_ASSERT(dynamic_cast<AttachmentIconItem *>(item));
327 
328             auto attitem = static_cast<AttachmentIconItem *>(item);
329             if (attitem->attachment().isEmpty()) {
330                 return;
331             }
332 
333             QPointer<AttachmentEditDialog> dialog(new AttachmentEditDialog(attitem, mAttachmentView, false));
334             dialog->setModal(false);
335             dialog->setAttribute(Qt::WA_DeleteOnClose, true);
336             dialog->show();
337         }
338     }
339 }
340 
slotItemRenamed(QListWidgetItem * item)341 void IncidenceAttachment::slotItemRenamed(QListWidgetItem *item)
342 {
343     Q_ASSERT(item);
344     Q_ASSERT(dynamic_cast<AttachmentIconItem *>(item));
345     static_cast<AttachmentIconItem *>(item)->setLabel(item->text());
346     checkDirtyStatus();
347 }
348 
slotSelectionChanged()349 void IncidenceAttachment::slotSelectionChanged()
350 {
351     bool selected = false;
352     for (int itemIndex = 0; itemIndex < mAttachmentView->count(); ++itemIndex) {
353         QListWidgetItem *item = mAttachmentView->item(itemIndex);
354         if (item->isSelected()) {
355             selected = true;
356             break;
357         }
358     }
359     mUi->mRemoveButton->setEnabled(selected);
360 }
361 
362 /// Private functions
363 
handlePasteOrDrop(const QMimeData * mimeData)364 void IncidenceAttachment::handlePasteOrDrop(const QMimeData *mimeData)
365 {
366     if (!mimeData) {
367         return;
368     }
369     QList<QUrl> urls;
370     bool probablyWeHaveUris = false;
371     QStringList labels;
372 
373     if (KContacts::VCardDrag::canDecode(mimeData)) {
374         KContacts::Addressee::List addressees;
375         KContacts::VCardDrag::fromMimeData(mimeData, addressees);
376         urls.reserve(addressees.count());
377         labels.reserve(addressees.count());
378         const KContacts::Addressee::List::ConstIterator end(addressees.constEnd());
379         for (KContacts::Addressee::List::ConstIterator it = addressees.constBegin(); it != end; ++it) {
380             urls.append(QUrl(QStringLiteral("uid:") + (*it).uid()));
381             // there is some weirdness about realName(), hence fromUtf8
382             labels.append(QString::fromUtf8((*it).realName().toLatin1()));
383         }
384         probablyWeHaveUris = true;
385     } else if (mimeData->hasUrls()) {
386         QMap<QString, QString> metadata;
387 
388         // QT5
389         // urls = QList<QUrl>::fromMimeData( mimeData, &metadata );
390         probablyWeHaveUris = true;
391         labels = metadata[QStringLiteral("labels")].split(QLatin1Char(':'), Qt::SkipEmptyParts);
392         const QStringList::Iterator end(labels.end());
393         for (QStringList::Iterator it = labels.begin(); it != end; ++it) {
394             *it = QUrl::fromPercentEncoding((*it).toLatin1());
395         }
396     } else if (mimeData->hasText()) {
397         const QString text = mimeData->text();
398         QStringList lst = text.split(QLatin1Char('\n'), Qt::SkipEmptyParts);
399         urls.reserve(lst.count());
400         QStringList::ConstIterator end(lst.constEnd());
401         for (QStringList::ConstIterator it = lst.constBegin(); it != end; ++it) {
402             urls.append(QUrl(*it));
403         }
404         probablyWeHaveUris = true;
405     }
406     QMenu menu;
407     QAction *linkAction = nullptr;
408     QAction *cancelAction = nullptr;
409     if (probablyWeHaveUris) {
410         linkAction = menu.addAction(QIcon::fromTheme(QStringLiteral("insert-link")), i18nc("@action:inmenu", "&Link here"));
411         // we need to check if we can reasonably expect to copy the objects
412         bool weCanCopy = true;
413         QList<QUrl>::ConstIterator end(urls.constEnd());
414         for (QList<QUrl>::ConstIterator it = urls.constBegin(); it != end; ++it) {
415             if (!(weCanCopy = KProtocolManager::supportsReading(*it))) {
416                 break; // either we can copy them all, or no copying at all
417             }
418         }
419         if (weCanCopy) {
420             menu.addAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18nc("@action:inmenu", "&Copy here"));
421         }
422     } else {
423         menu.addAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18nc("@action:inmenu", "&Copy here"));
424     }
425 
426     menu.addSeparator();
427     cancelAction = menu.addAction(QIcon::fromTheme(QStringLiteral("process-stop")), i18nc("@action:inmenu", "C&ancel"));
428 
429     QByteArray data;
430     QString mimeType;
431     QString label;
432 
433     if (!mimeData->formats().isEmpty() && !probablyWeHaveUris) {
434         mimeType = mimeData->formats().first();
435         data = mimeData->data(mimeType);
436         QMimeDatabase db;
437         QMimeType mime = db.mimeTypeForName(mimeType);
438         if (mime.isValid()) {
439             label = mime.comment();
440         }
441     }
442 
443     QAction *ret = menu.exec(QCursor::pos());
444     if (linkAction == ret) {
445         QStringList::ConstIterator jt = labels.constBegin();
446         const QList<QUrl>::ConstIterator jtEnd = urls.constEnd();
447         for (QList<QUrl>::ConstIterator it = urls.constBegin(); it != jtEnd; ++it) {
448             addUriAttachment((*it).url(), QString(), (jt == labels.constEnd() ? QString() : *(jt++)), true);
449         }
450     } else if (cancelAction != ret) {
451         if (probablyWeHaveUris) {
452             QList<QUrl>::ConstIterator end = urls.constEnd();
453             for (QList<QUrl>::ConstIterator it = urls.constBegin(); it != end; ++it) {
454                 KIO::Job *job = KIO::storedGet(*it);
455                 // TODO verify if slot exist !
456                 connect(job, &KIO::Job::result, this, &IncidenceAttachment::downloadComplete);
457             }
458         } else { // we take anything
459             addDataAttachment(data, mimeType, label);
460         }
461     }
462 }
463 
downloadComplete(KJob *)464 void IncidenceAttachment::downloadComplete(KJob *)
465 {
466     // TODO
467 }
468 
setupActions()469 void IncidenceAttachment::setupActions()
470 {
471     auto ac = new KActionCollection(this);
472     //  ac->addAssociatedWidget( this );
473 
474     mOpenAction = new QAction(QIcon::fromTheme(QStringLiteral("document-open")), i18nc("@action:inmenu open the attachment in a viewer", "&Open"), this);
475     connect(mOpenAction, &QAction::triggered, this, &IncidenceAttachment::showSelectedAttachments);
476     ac->addAction(QStringLiteral("view"), mOpenAction);
477     mPopupMenu->addAction(mOpenAction);
478 
479     mSaveAsAction =
480         new QAction(QIcon::fromTheme(QStringLiteral("document-save-as")), i18nc("@action:inmenu save the attachment to a file", "Save As..."), this);
481     connect(mSaveAsAction, &QAction::triggered, this, &IncidenceAttachment::saveSelectedAttachments);
482     mPopupMenu->addAction(mSaveAsAction);
483     mPopupMenu->addSeparator();
484 
485 #ifndef QT_NO_CLIPBOARD
486     mCopyAction = KStandardAction::copy(this, &IncidenceAttachment::copyToClipboard, ac);
487     mPopupMenu->addAction(mCopyAction);
488 
489     mCutAction = KStandardAction::cut(this, &IncidenceAttachment::cutToClipboard, ac);
490     mPopupMenu->addAction(mCutAction);
491 
492     QAction *action = KStandardAction::paste(this, &IncidenceAttachment::pasteFromClipboard, ac);
493     mPopupMenu->addAction(action);
494     mPopupMenu->addSeparator();
495 #endif
496 
497     mDeleteAction = new QAction(QIcon::fromTheme(QStringLiteral("list-remove")), i18nc("@action:inmenu remove the attachment", "&Remove"), this);
498     connect(mDeleteAction, &QAction::triggered, this, &IncidenceAttachment::removeSelectedAttachments);
499     ac->addAction(QStringLiteral("remove"), mDeleteAction);
500     mDeleteAction->setShortcut(Qt::Key_Delete);
501     mPopupMenu->addAction(mDeleteAction);
502     mPopupMenu->addSeparator();
503 
504     mEditAction = new QAction(QIcon::fromTheme(QStringLiteral("document-properties")),
505                               i18nc("@action:inmenu show a dialog used to edit the attachment", "&Properties..."),
506                               this);
507     connect(mEditAction, &QAction::triggered, this, &IncidenceAttachment::editSelectedAttachments);
508     ac->addAction(QStringLiteral("edit"), mEditAction);
509     mPopupMenu->addAction(mEditAction);
510 }
511 
setupAttachmentIconView()512 void IncidenceAttachment::setupAttachmentIconView()
513 {
514     mAttachmentView = new AttachmentIconView;
515     mAttachmentView->setWhatsThis(i18nc("@info:whatsthis",
516                                         "Displays items (files, mail, etc.) that "
517                                         "have been associated with this event or to-do."));
518 
519     connect(mAttachmentView, &AttachmentIconView::itemDoubleClicked, this, &IncidenceAttachment::showAttachment);
520     connect(mAttachmentView, &AttachmentIconView::itemChanged, this, &IncidenceAttachment::slotItemRenamed);
521     connect(mAttachmentView, &AttachmentIconView::itemSelectionChanged, this, &IncidenceAttachment::slotSelectionChanged);
522     connect(mAttachmentView, &AttachmentIconView::customContextMenuRequested, this, &IncidenceAttachment::showContextMenu);
523 
524     auto layout = new QGridLayout(mUi->mAttachmentViewPlaceHolder);
525     layout->setContentsMargins(0, 0, 0, 0);
526     layout->addWidget(mAttachmentView);
527 }
528 
529 // void IncidenceAttachmentEditor::addAttachment( KCalendarCore::Attachment *attachment )
530 // {
531 //   new AttachmentIconItem( attachment, mAttachmentView );
532 // }
533 
addDataAttachment(const QByteArray & data,const QString & mimeType,const QString & label)534 void IncidenceAttachment::addDataAttachment(const QByteArray &data, const QString &mimeType, const QString &label)
535 {
536     auto item = new AttachmentIconItem(KCalendarCore::Attachment(), mAttachmentView);
537 
538     QString nlabel = label;
539     if (mimeType == QLatin1String("message/rfc822")) {
540         // mail message. try to set the label from the mail Subject:
541         KMime::Message msg;
542         msg.setContent(data);
543         msg.parse();
544         nlabel = msg.subject()->asUnicodeString();
545     }
546 
547     item->setData(data);
548     item->setLabel(nlabel);
549     if (mimeType.isEmpty()) {
550         QMimeDatabase db;
551         item->setMimeType(db.mimeTypeForData(data).name());
552     } else {
553         item->setMimeType(mimeType);
554     }
555 
556     checkDirtyStatus();
557 }
558 
addUriAttachment(const QString & uri,const QString & mimeType,const QString & label,bool inLine)559 void IncidenceAttachment::addUriAttachment(const QString &uri, const QString &mimeType, const QString &label, bool inLine)
560 {
561     if (!inLine) {
562         auto item = new AttachmentIconItem(KCalendarCore::Attachment(), mAttachmentView);
563         item->setUri(uri);
564         item->setLabel(label);
565         if (mimeType.isEmpty()) {
566             if (uri.startsWith(QLatin1String("uid:"))) {
567                 item->setMimeType(QStringLiteral("text/directory"));
568             } else if (uri.startsWith(QLatin1String("kmail:"))) {
569                 item->setMimeType(QStringLiteral("message/rfc822"));
570             } else if (uri.startsWith(QLatin1String("urn:x-ical"))) {
571                 item->setMimeType(QStringLiteral("text/calendar"));
572             } else if (uri.startsWith(QLatin1String("news:"))) {
573                 item->setMimeType(QStringLiteral("message/news"));
574             } else {
575                 QMimeDatabase db;
576                 item->setMimeType(db.mimeTypeForUrl(QUrl(uri)).name());
577             }
578         }
579     } else {
580         auto job = KIO::storedGet(QUrl(uri));
581         KJobWidgets::setWindow(job, nullptr);
582         if (job->exec()) {
583             const QByteArray data = job->data();
584             addDataAttachment(data, mimeType, label);
585         }
586     }
587 }
588