Messagelib

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

KDE's Doxygen guidelines are available online.