1 /*
2   SPDX-FileCopyrightText: 1997 Markus Wuebben <markus.wuebben@kde.org>
3   SPDX-FileCopyrightText: 2009 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net
4   SPDX-FileCopyrightText: 2009 Andras Mantia <andras@kdab.net>
5   SPDX-FileCopyrightText: 2010 Torgny Nyblom <nyblom@kde.org>
6   SPDX-FileCopyrightText: 2011-2021 Laurent Montel <montel@kde.org>
7 
8   SPDX-License-Identifier: GPL-2.0-or-later
9 */
10 #include "viewer_p.h"
11 #include "viewerpurposemenuwidget.h"
12 
13 #include "messagedisplayformatattribute.h"
14 #include "messageviewer_debug.h"
15 #include "scamdetection/scamattribute.h"
16 #include "scamdetection/scamdetectionwarningwidget.h"
17 #include "utils/iconnamecache.h"
18 #include "utils/mimetype.h"
19 #include "viewer/mimeparttree/mimeparttreeview.h"
20 #include "viewer/objecttreeemptysource.h"
21 #include "viewer/objecttreeviewersource.h"
22 #include "viewer/renderer/messageviewerrenderer.h"
23 
24 #include "messageviewer/headerstrategy.h"
25 #include "messageviewer/headerstyle.h"
26 #include <KPIMTextEdit/SlideContainer>
27 
28 #include "job/modifymessagedisplayformatjob.h"
29 
30 #include "htmlwriter/webengineembedpart.h"
31 #include "viewerplugins/viewerplugintoolmanager.h"
32 #include <KContacts/VCardConverter>
33 // KDE includes
34 #include <KActionCollection>
35 #include <KActionMenu>
36 #include <KCharsets>
37 #include <QAction>
38 #include <QHBoxLayout>
39 #include <QPrintPreviewDialog>
40 #include <QVBoxLayout>
41 
42 #include <Akonadi/ItemCreateJob>
43 #include <Akonadi/ItemModifyJob>
44 #include <Akonadi/KMime/SpecialMailCollections>
45 #include <KApplicationTrader>
46 #include <KEmailAddress>
47 #include <KFileItemActions>
48 #include <KIO/ApplicationLauncherJob>
49 #include <KIO/JobUiDelegate>
50 #include <KIO/OpenUrlJob>
51 #include <KLocalizedString>
52 #include <KMessageBox>
53 #include <KMimeTypeChooser>
54 #include <KSelectAction>
55 #include <KSharedConfig>
56 #include <KStandardGuiItem>
57 #include <KToggleAction>
58 #include <MailTransportAkonadi/ErrorAttribute>
59 #include <MessageCore/Util>
60 #include <QIcon>
61 #include <QMenu>
62 #include <QMimeData>
63 #include <QTemporaryDir>
64 #include <messageflags.h>
65 
66 // Qt includes
67 #include <QClipboard>
68 #include <QItemSelectionModel>
69 #include <QMimeDatabase>
70 #include <QPrintDialog>
71 #include <QPrinter>
72 #include <QSplitter>
73 #include <QTreeView>
74 #include <QWheelEvent>
75 #include <WebEngineViewer/WebEngineExportHtmlPageJob>
76 // libkdepim
77 #include <MessageCore/AttachmentPropertiesDialog>
78 #include <PimCommon/BroadcastStatus>
79 
80 #include <Akonadi/AttributeFactory>
81 #include <Akonadi/Collection>
82 #include <Akonadi/ItemFetchJob>
83 #include <Akonadi/ItemFetchScope>
84 #include <Akonadi/KMime/MessageParts>
85 #include <Akonadi/KMime/MessageStatus>
86 
87 #include <MessageCore/AutocryptUtils>
88 #include <KIdentityManagement/Identity>
89 #include <KIdentityManagement/IdentityManager>
90 
91 // own includes
92 #include "csshelper.h"
93 #include "messageviewer/messageviewerutil.h"
94 #include "settings/messageviewersettings.h"
95 #include "utils/messageviewerutil_p.h"
96 #include "viewer/attachmentstrategy.h"
97 #include "viewer/mimeparttree/mimetreemodel.h"
98 #include "viewer/urlhandlermanager.h"
99 #include "widgets/attachmentdialog.h"
100 #include "widgets/htmlstatusbar.h"
101 #include "widgets/shownextmessagewidget.h"
102 #include "widgets/vcardviewer.h"
103 
104 #include "header/headerstylemenumanager.h"
105 #include "htmlwriter/webengineparthtmlwriter.h"
106 #include "viewer/webengine/mailwebengineview.h"
107 #include <WebEngineViewer/FindBarWebEngineView>
108 #include <WebEngineViewer/LocalDataBaseManager>
109 #include <WebEngineViewer/SubmittedFormWarningWidget>
110 #include <WebEngineViewer/WebEngineExportPdfPageJob>
111 #include <WebEngineViewer/WebHitTestResult>
112 #include <widgets/mailsourcewebengineviewer.h>
113 
114 #include "interfaces/htmlwriter.h"
115 #include <MimeTreeParser/BodyPart>
116 #include <MimeTreeParser/NodeHelper>
117 #include <MimeTreeParser/ObjectTreeParser>
118 
119 #include <MessageCore/StringUtil>
120 
121 #include <MessageCore/MessageCoreSettings>
122 #include <MessageCore/NodeHelper>
123 
124 #include <Akonadi/AgentInstance>
125 #include <Akonadi/AgentManager>
126 #include <Akonadi/CollectionFetchJob>
127 #include <Akonadi/CollectionFetchScope>
128 
129 #include <KJobWidgets/KJobWidgets>
130 #include <KPIMTextEdit/TextToSpeechWidget>
131 #include <QApplication>
132 #include <QStandardPaths>
133 #include <QWebEngineSettings>
134 #include <WebEngineViewer/DeveloperToolDialog>
135 #include <WebEngineViewer/TrackingWarningWidget>
136 #include <WebEngineViewer/ZoomActionMenu>
137 #include <boost/bind.hpp>
138 #include <header/headerstyleplugin.h>
139 #include <viewerplugins/viewerplugininterface.h>
140 
141 #include <GrantleeTheme/GrantleeTheme>
142 #include <GrantleeTheme/GrantleeThemeManager>
143 
144 #include "dkim-verify/dkimmanager.h"
145 #include "dkim-verify/dkimmanagerulesdialog.h"
146 #include "dkim-verify/dkimresultattribute.h"
147 #include "dkim-verify/dkimviewermenu.h"
148 #include "dkim-verify/dkimwidgetinfo.h"
149 
150 #include "remote-content/remotecontentmenu.h"
151 #include <chrono>
152 using namespace std::chrono_literals;
153 using namespace boost;
154 using namespace MailTransport;
155 using namespace MessageViewer;
156 using namespace MessageCore;
157 
158 static QAtomicInt _k_attributeInitialized;
159 
160 template<typename Arg, typename R, typename C> struct InvokeWrapper {
161     R *receiver;
162     void (C::*memberFun)(Arg);
operator ()InvokeWrapper163     void operator()(Arg result)
164     {
165         (receiver->*memberFun)(result);
166     }
167 };
168 
invoke(R * receiver,void (C::* memberFun)(Arg))169 template<typename Arg, typename R, typename C> InvokeWrapper<Arg, R, C> invoke(R *receiver, void (C::*memberFun)(Arg))
170 {
171     InvokeWrapper<Arg, R, C> wrapper = {receiver, memberFun};
172     return wrapper;
173 }
174 
ViewerPrivate(Viewer * aParent,QWidget * mainWindow,KActionCollection * actionCollection)175 ViewerPrivate::ViewerPrivate(Viewer *aParent, QWidget *mainWindow, KActionCollection *actionCollection)
176     : QObject(aParent)
177     , mNodeHelper(new MimeTreeParser::NodeHelper)
178     , mOldGlobalOverrideEncoding(QStringLiteral("---"))
179     , mMainWindow(mainWindow)
180     , mActionCollection(actionCollection)
181     , q(aParent)
182     , mSession(new Akonadi::Session("MessageViewer-" + QByteArray::number(reinterpret_cast<quintptr>(this)), this))
183 {
184     if (!mainWindow) {
185         mMainWindow = aParent;
186     }
187     mMessageViewerRenderer = new MessageViewerRenderer;
188 
189     mRemoteContentMenu = new MessageViewer::RemoteContentMenu(mMainWindow);
190     connect(mRemoteContentMenu, &MessageViewer::RemoteContentMenu::updateEmail, this, &ViewerPrivate::updateReaderWin);
191 
192     mDkimWidgetInfo = new MessageViewer::DKIMWidgetInfo(mMainWindow);
193     if (_k_attributeInitialized.testAndSetAcquire(0, 1)) {
194         Akonadi::AttributeFactory::registerAttribute<MessageViewer::MessageDisplayFormatAttribute>();
195         Akonadi::AttributeFactory::registerAttribute<MessageViewer::ScamAttribute>();
196     }
197     mPhishingDatabase = new WebEngineViewer::LocalDataBaseManager(this);
198     mPhishingDatabase->initialize();
199     connect(mPhishingDatabase, &WebEngineViewer::LocalDataBaseManager::checkUrlFinished, this, &ViewerPrivate::slotCheckedUrlFinished);
200 
201     mShareServiceManager = new PimCommon::ShareServiceUrlManager(this);
202 
203     mDisplayFormatMessageOverwrite = MessageViewer::Viewer::UseGlobalSetting;
204 
205     mUpdateReaderWinTimer.setObjectName(QStringLiteral("mUpdateReaderWinTimer"));
206     mResizeTimer.setObjectName(QStringLiteral("mResizeTimer"));
207 
208     createWidgets();
209     createActions();
210     initHtmlWidget();
211     readConfig();
212 
213     mLevelQuote = MessageViewer::MessageViewerSettings::self()->collapseQuoteLevelSpin() - 1;
214 
215     mResizeTimer.setSingleShot(true);
216     connect(&mResizeTimer, &QTimer::timeout, this, &ViewerPrivate::slotDelayedResize);
217 
218     mUpdateReaderWinTimer.setSingleShot(true);
219     connect(&mUpdateReaderWinTimer, &QTimer::timeout, this, &ViewerPrivate::updateReaderWin);
220 
221     connect(mNodeHelper, &MimeTreeParser::NodeHelper::update, this, &ViewerPrivate::update);
222 
223     connect(mColorBar, &HtmlStatusBar::clicked, this, &ViewerPrivate::slotToggleHtmlMode);
224 
225     // FIXME: Don't use the full payload here when attachment loading on demand is used, just
226     //        like in KMMainWidget::slotMessageActivated().
227     mMonitor.setObjectName(QStringLiteral("MessageViewerMonitor"));
228     mMonitor.setSession(mSession);
229     Akonadi::ItemFetchScope fs;
230     fs.fetchFullPayload();
231     fs.fetchAttribute<MailTransport::ErrorAttribute>();
232     fs.fetchAttribute<MessageViewer::MessageDisplayFormatAttribute>();
233     fs.fetchAttribute<MessageViewer::ScamAttribute>();
234     fs.fetchAttribute<MessageViewer::DKIMResultAttribute>();
235     mMonitor.setItemFetchScope(fs);
236     connect(&mMonitor, &Akonadi::Monitor::itemChanged, this, &ViewerPrivate::slotItemChanged);
237     connect(&mMonitor, &Akonadi::Monitor::itemRemoved, this, &ViewerPrivate::slotClear);
238     connect(&mMonitor, &Akonadi::Monitor::itemMoved, this, &ViewerPrivate::slotItemMoved);
239 }
240 
~ViewerPrivate()241 ViewerPrivate::~ViewerPrivate()
242 {
243     delete mDeveloperToolDialog;
244     delete mMessageViewerRenderer;
245     MessageViewer::MessageViewerSettings::self()->save();
246     delete mHtmlWriter;
247     mHtmlWriter = nullptr;
248     delete mViewer;
249     mViewer = nullptr;
250     mNodeHelper->forceCleanTempFiles();
251     qDeleteAll(mListMailSourceViewer);
252     mMessage.clear();
253     delete mNodeHelper;
254 }
255 
256 //-----------------------------------------------------------------------------
nodeFromUrl(const QUrl & url) const257 KMime::Content *ViewerPrivate::nodeFromUrl(const QUrl &url) const
258 {
259     return mNodeHelper->fromHREF(mMessage, url);
260 }
261 
openAttachment(KMime::Content * node,const QUrl & url)262 void ViewerPrivate::openAttachment(KMime::Content *node, const QUrl &url)
263 {
264     if (!node) {
265         return;
266     }
267 
268     if (auto ct = node->contentType(false)) {
269         if (ct->mimeType() == "text/x-moz-deleted") {
270             return;
271         }
272         if (ct->mimeType() == "message/external-body") {
273             if (ct->hasParameter(QStringLiteral("url"))) {
274                 auto job = new KIO::OpenUrlJob(url, QStringLiteral("text/html"));
275                 job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, q));
276                 job->start();
277                 return;
278             }
279         }
280     }
281 
282     const bool isEncapsulatedMessage = node->parent() && node->parent()->bodyIsMessage();
283     if (isEncapsulatedMessage) {
284         // the viewer/urlhandlermanager expects that the message (mMessage) it is passed is the root when doing index calculation
285         // in urls. Simply passing the result of bodyAsMessage() does not cut it as the resulting pointer is a child in its tree.
286         KMime::Message::Ptr m(new KMime::Message);
287         m->setContent(node->parent()->bodyAsMessage()->encodedContent());
288         m->parse();
289         attachmentViewMessage(m);
290         return;
291     }
292     // determine the MIME type of the attachment
293     // prefer the value of the Content-Type header
294     QMimeDatabase mimeDb;
295     auto mimetype = mimeDb.mimeTypeForName(QString::fromLatin1(node->contentType()->mimeType().toLower()));
296     if (mimetype.isValid() && mimetype.inherits(KContacts::Addressee::mimeType())) {
297         showVCard(node);
298         return;
299     }
300 
301     // special case treatment on mac and windows
302     QUrl atmUrl = url;
303     if (url.isEmpty()) {
304         atmUrl = mNodeHelper->tempFileUrlFromNode(node);
305     }
306     if (Util::handleUrlWithQDesktopServices(atmUrl)) {
307         return;
308     }
309 
310     if (!mimetype.isValid() || mimetype.name() == QLatin1String("application/octet-stream")) {
311         mimetype = MimeTreeParser::Util::mimetype(url.isLocalFile() ? url.toLocalFile() : url.fileName());
312     }
313     KService::Ptr offer = KApplicationTrader::preferredService(mimetype.name());
314 
315     const QString filenameText = MimeTreeParser::NodeHelper::fileName(node);
316 
317     QPointer<AttachmentDialog> dialog = new AttachmentDialog(mMainWindow, filenameText, offer, QLatin1String("askSave_") + mimetype.name());
318     const int choice = dialog->exec();
319     delete dialog;
320     if (choice == AttachmentDialog::Save) {
321         QList<QUrl> urlList;
322         if (Util::saveContents(mMainWindow, KMime::Content::List() << node, urlList)) {
323             showSavedFileFolderWidget(urlList, MessageViewer::OpenSavedFileFolderWidget::FileType::Attachment);
324         }
325     } else if (choice == AttachmentDialog::Open) { // Open
326         if (offer) {
327             attachmentOpenWith(node, offer);
328         } else {
329             attachmentOpen(node);
330         }
331     } else if (choice == AttachmentDialog::OpenWith) {
332         attachmentOpenWith(node);
333     } else { // Cancel
334         qCDebug(MESSAGEVIEWER_LOG) << "Canceled opening attachment";
335     }
336 }
337 
deleteAttachment(KMime::Content * node,bool showWarning)338 bool ViewerPrivate::deleteAttachment(KMime::Content *node, bool showWarning)
339 {
340     if (!node) {
341         return true;
342     }
343     KMime::Content *parent = node->parent();
344     if (!parent) {
345         return true;
346     }
347 
348     const QVector<KMime::Content *> extraNodes = mNodeHelper->extraContents(mMessage.data());
349     if (extraNodes.contains(node->topLevel())) {
350         KMessageBox::error(mMainWindow,
351                            i18n("Deleting an attachment from an encrypted or old-style mailman message is not supported."),
352                            i18n("Delete Attachment"));
353         return true; // cancelled
354     }
355 
356     if (showWarning
357         && KMessageBox::warningContinueCancel(mMainWindow,
358                                               i18n("Deleting an attachment might invalidate any digital signature on this message."),
359                                               i18n("Delete Attachment"),
360                                               KStandardGuiItem::del(),
361                                               KStandardGuiItem::cancel(),
362                                               QStringLiteral("DeleteAttachmentSignatureWarning"))
363             != KMessageBox::Continue) {
364         return false; // cancelled
365     }
366     // don't confuse the model
367     mMimePartTree->clearModel();
368     QString filename;
369     QString name;
370     QByteArray mimetype;
371     if (auto cd = node->contentDisposition(false)) {
372         filename = cd->filename();
373     }
374 
375     if (auto ct = node->contentType(false)) {
376         name = ct->name();
377         mimetype = ct->mimeType();
378     }
379 
380     // text/plain part:
381     auto deletePart = new KMime::Content(parent);
382     auto deleteCt = deletePart->contentType(true);
383     deleteCt->setMimeType("text/x-moz-deleted");
384     deleteCt->setName(QStringLiteral("Deleted: %1").arg(name), "utf8");
385     deletePart->contentDisposition(true)->setDisposition(KMime::Headers::CDattachment);
386     deletePart->contentDisposition(false)->setFilename(QStringLiteral("Deleted: %1").arg(name));
387 
388     deleteCt->setCharset("utf-8");
389     deletePart->contentTransferEncoding()->setEncoding(KMime::Headers::CE7Bit);
390     QByteArray bodyMessage = QByteArrayLiteral("\nYou deleted an attachment from this message. The original MIME headers for the attachment were:");
391     bodyMessage += ("\nContent-Type: ") + mimetype;
392     bodyMessage += ("\nname=\"") + name.toUtf8() + "\"";
393     bodyMessage += ("\nfilename=\"") + filename.toUtf8() + "\"";
394     deletePart->setBody(bodyMessage);
395     parent->replaceContent(node, deletePart);
396 
397     parent->assemble();
398 
399     KMime::Message *modifiedMessage = mNodeHelper->messageWithExtraContent(mMessage.data());
400     mMimePartTree->mimePartModel()->setRoot(modifiedMessage);
401     mMessageItem.setPayloadFromData(mMessage->encodedContent());
402     // Modifying the payload might change the remote id (e.g. for IMAP) of the item, so don't try to force on it
403     // a potentially old remote id. Without clearing the remote id, deleting multiple attachments from a message
404     // stored on an IMAP server will likely fail with "Invalid attempt to modify the remoteID for item [...]".
405     mMessageItem.setRemoteId({});
406     auto job = new Akonadi::ItemModifyJob(mMessageItem, mSession);
407     job->disableRevisionCheck();
408     connect(job, &KJob::result, this, &ViewerPrivate::itemModifiedResult);
409     return true;
410 }
411 
itemModifiedResult(KJob * job)412 void ViewerPrivate::itemModifiedResult(KJob *job)
413 {
414     if (job->error()) {
415         qCDebug(MESSAGEVIEWER_LOG) << "Item update failed:" << job->errorString();
416     } else {
417         setMessageItem(mMessageItem, MimeTreeParser::Force);
418     }
419 }
420 
scrollToAnchor(const QString & anchor)421 void ViewerPrivate::scrollToAnchor(const QString &anchor)
422 {
423     mViewer->scrollToAnchor(anchor);
424 }
425 
createOpenWithMenu(QMenu * topMenu,const QString & contentTypeStr,bool fromCurrentContent)426 void ViewerPrivate::createOpenWithMenu(QMenu *topMenu, const QString &contentTypeStr, bool fromCurrentContent)
427 {
428     const KService::List offers = KFileItemActions::associatedApplications(QStringList() << contentTypeStr);
429     if (!offers.isEmpty()) {
430         QMenu *menu = topMenu;
431         auto actionGroup = new QActionGroup(menu);
432 
433         if (fromCurrentContent) {
434             connect(actionGroup, &QActionGroup::triggered, this, &ViewerPrivate::slotOpenWithActionCurrentContent);
435         } else {
436             connect(actionGroup, &QActionGroup::triggered, this, &ViewerPrivate::slotOpenWithAction);
437         }
438 
439         if (offers.count() > 1) { // submenu 'open with'
440             menu = new QMenu(i18nc("@title:menu", "&Open With"), topMenu);
441             menu->menuAction()->setObjectName(QStringLiteral("openWith_submenu")); // for the unittest
442             topMenu->addMenu(menu);
443         }
444         // qCDebug(MESSAGEVIEWER_LOG) << offers.count() << "offers" << topMenu << menu;
445 
446         for (const KService::Ptr &ser : offers) {
447             QAction *act = MessageViewer::Util::createAppAction(ser,
448                                                                 // no submenu -> prefix single offer
449                                                                 menu == topMenu,
450                                                                 actionGroup,
451                                                                 menu);
452             menu->addAction(act);
453         }
454 
455         QString openWithActionName;
456         if (menu != topMenu) { // submenu
457             menu->addSeparator();
458             openWithActionName = i18nc("@action:inmenu Open With", "&Other...");
459         } else {
460             openWithActionName = i18nc("@title:menu", "&Open With...");
461         }
462         auto openWithAct = new QAction(menu);
463         openWithAct->setText(openWithActionName);
464         if (fromCurrentContent) {
465             connect(openWithAct, &QAction::triggered, this, &ViewerPrivate::slotOpenWithDialogCurrentContent);
466         } else {
467             connect(openWithAct, &QAction::triggered, this, &ViewerPrivate::slotOpenWithDialog);
468         }
469 
470         menu->addAction(openWithAct);
471     } else { // no app offers -> Open With...
472         auto act = new QAction(topMenu);
473         act->setText(i18nc("@title:menu", "&Open With..."));
474         if (fromCurrentContent) {
475             connect(act, &QAction::triggered, this, &ViewerPrivate::slotOpenWithDialogCurrentContent);
476         } else {
477             connect(act, &QAction::triggered, this, &ViewerPrivate::slotOpenWithDialog);
478         }
479         topMenu->addAction(act);
480     }
481 }
482 
slotOpenWithDialogCurrentContent()483 void ViewerPrivate::slotOpenWithDialogCurrentContent()
484 {
485     if (!mCurrentContent) {
486         return;
487     }
488     attachmentOpenWith(mCurrentContent);
489 }
490 
slotOpenWithDialog()491 void ViewerPrivate::slotOpenWithDialog()
492 {
493     const auto contents = selectedContents();
494     if (contents.count() == 1) {
495         attachmentOpenWith(contents.first());
496     }
497 }
498 
slotOpenWithActionCurrentContent(QAction * act)499 void ViewerPrivate::slotOpenWithActionCurrentContent(QAction *act)
500 {
501     if (!mCurrentContent) {
502         return;
503     }
504     const auto app = act->data().value<KService::Ptr>();
505     attachmentOpenWith(mCurrentContent, app);
506 }
507 
slotOpenWithAction(QAction * act)508 void ViewerPrivate::slotOpenWithAction(QAction *act)
509 {
510     const auto app = act->data().value<KService::Ptr>();
511     const auto contents = selectedContents();
512     if (contents.count() == 1) {
513         attachmentOpenWith(contents.first(), app);
514     }
515 }
516 
showAttachmentPopup(KMime::Content * node,const QString & name,const QPoint & globalPos)517 void ViewerPrivate::showAttachmentPopup(KMime::Content *node, const QString &name, const QPoint &globalPos)
518 {
519     Q_UNUSED(name)
520     prepareHandleAttachment(node);
521     bool deletedAttachment = false;
522     QString contentTypeStr;
523     if (auto contentType = node->contentType(false)) {
524         contentTypeStr = QLatin1String(contentType->mimeType());
525     }
526     if (contentTypeStr == QStringLiteral("message/global")) { // Not registered in mimetype => it's a message/rfc822
527         contentTypeStr = QStringLiteral("message/rfc822");
528     }
529     deletedAttachment = (contentTypeStr == QStringLiteral("text/x-moz-deleted"));
530     // Not necessary to show popup menu when attachment was removed
531     if (deletedAttachment) {
532         return;
533     }
534 
535     QMenu menu;
536 
537     QAction *action = menu.addAction(QIcon::fromTheme(QStringLiteral("document-open")), i18nc("to open", "Open"));
538     action->setEnabled(!deletedAttachment);
539     connect(action, &QAction::triggered, this, [this]() {
540         slotHandleAttachment(Viewer::Open);
541     });
542     createOpenWithMenu(&menu, contentTypeStr, true);
543 
544     QMimeDatabase mimeDb;
545     auto mimetype = mimeDb.mimeTypeForName(contentTypeStr);
546     if (mimetype.isValid()) {
547         const QStringList parentMimeType = mimetype.parentMimeTypes();
548         if ((contentTypeStr == QLatin1String("text/plain")) || (contentTypeStr == QLatin1String("image/png")) || (contentTypeStr == QLatin1String("image/jpeg"))
549             || parentMimeType.contains(QLatin1String("text/plain")) || parentMimeType.contains(QLatin1String("image/png"))
550             || parentMimeType.contains(QLatin1String("image/jpeg"))) {
551             action = menu.addAction(i18nc("to view something", "View"));
552             action->setEnabled(!deletedAttachment);
553             connect(action, &QAction::triggered, this, [this]() {
554                 slotHandleAttachment(Viewer::View);
555             });
556         }
557     }
558 
559     action = menu.addAction(i18n("Scroll To"));
560     connect(action, &QAction::triggered, this, [this]() {
561         slotHandleAttachment(Viewer::ScrollTo);
562     });
563 
564     action = menu.addAction(QIcon::fromTheme(QStringLiteral("document-save-as")), i18n("Save As..."));
565     action->setEnabled(!deletedAttachment);
566     connect(action, &QAction::triggered, this, [this]() {
567         slotHandleAttachment(Viewer::Save);
568     });
569 
570     action = menu.addAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy"));
571     action->setEnabled(!deletedAttachment);
572     connect(action, &QAction::triggered, this, [this]() {
573         slotHandleAttachment(Viewer::Copy);
574     });
575 
576     const bool isEncapsulatedMessage = node->parent() && node->parent()->bodyIsMessage();
577     const bool canChange = mMessageItem.isValid() && mMessageItem.parentCollection().isValid()
578         && (mMessageItem.parentCollection().rights() != Akonadi::Collection::ReadOnly) && !isEncapsulatedMessage;
579 
580     menu.addSeparator();
581     action = menu.addAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18n("Delete Attachment"));
582     connect(action, &QAction::triggered, this, [this]() {
583         slotHandleAttachment(Viewer::Delete);
584     });
585 
586     action->setEnabled(canChange && !deletedAttachment);
587 #if 0
588     menu->addSeparator();
589 
590     action
591         = menu->addAction(QIcon::fromTheme(QStringLiteral("mail-reply-sender")),
592                           i18n("Reply To Author"));
593     connect(action, &QAction::triggered, this, [this]() {
594         slotHandleAttachment(Viewer::ReplyMessageToAuthor);
595     });
596 
597     menu->addSeparator();
598 
599     action = menu->addAction(QIcon::fromTheme(QStringLiteral("mail-reply-all")), i18n(
600                                  "Reply To All"));
601     connect(action, &QAction::triggered, this, [this]() {
602         slotHandleAttachment(Viewer::ReplyMessageToAll);
603     });
604 #endif
605     menu.addSeparator();
606     action = menu.addAction(i18n("Properties"));
607     connect(action, &QAction::triggered, this, [this]() {
608         slotHandleAttachment(Viewer::Properties);
609     });
610     menu.exec(globalPos);
611 }
612 
prepareHandleAttachment(KMime::Content * node)613 void ViewerPrivate::prepareHandleAttachment(KMime::Content *node)
614 {
615     mCurrentContent = node;
616 }
617 
getServiceOffer(KMime::Content * content)618 KService::Ptr ViewerPrivate::getServiceOffer(KMime::Content *content)
619 {
620     const QString fileName = mNodeHelper->writeNodeToTempFile(content);
621 
622     const QString contentTypeStr = QLatin1String(content->contentType()->mimeType());
623 
624     // determine the MIME type of the attachment
625     // prefer the value of the Content-Type header
626     QMimeDatabase mimeDb;
627     auto mimetype = mimeDb.mimeTypeForName(contentTypeStr);
628 
629     if (mimetype.isValid() && mimetype.inherits(KContacts::Addressee::mimeType())) {
630         attachmentView(content);
631         return KService::Ptr(nullptr);
632     }
633 
634     if (!mimetype.isValid() || mimetype.name() == QLatin1String("application/octet-stream")) {
635         /*TODO(Andris) port when on-demand loading is done   && msgPart.isComplete() */
636         mimetype = MimeTreeParser::Util::mimetype(fileName);
637     }
638     return KApplicationTrader::preferredService(mimetype.name());
639 }
640 
selectedContents() const641 KMime::Content::List ViewerPrivate::selectedContents() const
642 {
643     return mMimePartTree->selectedContents();
644 }
645 
attachmentOpenWith(KMime::Content * node,const KService::Ptr & offer)646 void ViewerPrivate::attachmentOpenWith(KMime::Content *node, const KService::Ptr &offer)
647 {
648     QString name = mNodeHelper->writeNodeToTempFile(node);
649 
650     // Make sure that it will not deleted when we switch from message.
651     auto tmpDir = new QTemporaryDir(QDir::tempPath() + QLatin1String("/messageviewer_attachment_XXXXXX"));
652     if (tmpDir->isValid()) {
653         tmpDir->setAutoRemove(false);
654         const QString path = tmpDir->path();
655         delete tmpDir;
656         QFile f(name);
657         const QUrl tmpFileName = QUrl::fromLocalFile(name);
658         const QString newPath = path + QLatin1Char('/') + tmpFileName.fileName();
659 
660         if (!f.copy(newPath)) {
661             qCDebug(MESSAGEVIEWER_LOG) << " File was not able to copy: filename: " << name << " to " << path;
662         } else {
663             name = newPath;
664         }
665         f.close();
666     } else {
667         delete tmpDir;
668     }
669 
670     const QFileDevice::Permissions perms = QFile::permissions(name);
671     QFile::setPermissions(name, perms | QFileDevice::ReadUser | QFileDevice::WriteUser);
672     const QUrl url = QUrl::fromLocalFile(name);
673 
674     auto job = new KIO::ApplicationLauncherJob(offer);
675     job->setUrls({url});
676     job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, mMainWindow));
677     job->start();
678     connect(job, &KJob::result, this, [url, job]() {
679         if (job->error()) {
680             QFile::remove(url.toLocalFile());
681         }
682     });
683 }
684 
attachmentOpen(KMime::Content * node)685 void ViewerPrivate::attachmentOpen(KMime::Content *node)
686 {
687     const KService::Ptr offer = getServiceOffer(node);
688     if (!offer) {
689         qCDebug(MESSAGEVIEWER_LOG) << "got no offer";
690         return;
691     }
692     attachmentOpenWith(node, offer);
693 }
694 
showEmoticons() const695 bool ViewerPrivate::showEmoticons() const
696 {
697     return mForceEmoticons;
698 }
699 
htmlWriter() const700 HtmlWriter *ViewerPrivate::htmlWriter() const
701 {
702     return mHtmlWriter;
703 }
704 
cssHelper() const705 CSSHelper *ViewerPrivate::cssHelper() const
706 {
707     return mMessageViewerRenderer->cssHelper();
708 }
709 
nodeHelper() const710 MimeTreeParser::NodeHelper *ViewerPrivate::nodeHelper() const
711 {
712     return mNodeHelper;
713 }
714 
viewer() const715 Viewer *ViewerPrivate::viewer() const
716 {
717     return q;
718 }
719 
messageItem() const720 Akonadi::Item ViewerPrivate::messageItem() const
721 {
722     return mMessageItem;
723 }
724 
message() const725 KMime::Message::Ptr ViewerPrivate::message() const
726 {
727     return mMessage;
728 }
729 
decryptMessage() const730 bool ViewerPrivate::decryptMessage() const
731 {
732     if (MessageViewer::MessageViewerSettings::self()->alwaysDecrypt()) {
733         return true;
734     } else {
735         return mDecrytMessageOverwrite;
736     }
737 }
738 
displaySplashPage(const QString & message)739 void ViewerPrivate::displaySplashPage(const QString &message)
740 {
741     displaySplashPage(QStringLiteral("status.html"),
742                       {{QStringLiteral("icon"), QStringLiteral("kmail")},
743                        {QStringLiteral("name"), i18n("KMail")},
744                        {QStringLiteral("subtitle"), i18n("The KDE Mail Client")},
745                        {QStringLiteral("message"), message}});
746 }
747 
displaySplashPage(const QString & templateName,const QVariantHash & data,const QByteArray & domain)748 void ViewerPrivate::displaySplashPage(const QString &templateName, const QVariantHash &data, const QByteArray &domain)
749 {
750     if (mViewer) {
751         mMsgDisplay = false;
752         adjustLayout();
753 
754         GrantleeTheme::ThemeManager manager(QStringLiteral("splashPage"), QStringLiteral("splash.theme"), nullptr, QStringLiteral("messageviewer/about/"));
755         GrantleeTheme::Theme theme = manager.theme(QStringLiteral("default"));
756         if (theme.isValid()) {
757             mViewer->setHtml(theme.render(templateName, data, domain), QUrl::fromLocalFile(theme.absolutePath() + QLatin1Char('/')));
758         } else {
759             qCDebug(MESSAGEVIEWER_LOG) << "Theme error: failed to find splash theme";
760         }
761         mViewer->show();
762     }
763 }
764 
enableMessageDisplay()765 void ViewerPrivate::enableMessageDisplay()
766 {
767     if (mMsgDisplay) {
768         return;
769     }
770     mMsgDisplay = true;
771     adjustLayout();
772 }
773 
displayMessage()774 void ViewerPrivate::displayMessage()
775 {
776     showHideMimeTree();
777 
778     mNodeHelper->setOverrideCodec(mMessage.data(), overrideCodec());
779 
780     if (mMessageItem.hasAttribute<MessageViewer::MessageDisplayFormatAttribute>()) {
781         const MessageViewer::MessageDisplayFormatAttribute *const attr = mMessageItem.attribute<MessageViewer::MessageDisplayFormatAttribute>();
782         setHtmlLoadExtOverride(attr->remoteContent());
783         setDisplayFormatMessageOverwrite(attr->messageFormat());
784     }
785 
786     htmlWriter()->begin();
787     htmlWriter()->write(cssHelper()->htmlHead(mUseFixedFont));
788 
789     if (!mMainWindow) {
790         q->setWindowTitle(mMessage->subject()->asUnicodeString());
791     }
792 
793     // Don't update here, parseMsg() can overwrite the HTML mode, which would lead to flicker.
794     // It is updated right after parseMsg() instead.
795     mColorBar->setMode(MimeTreeParser::Util::Normal, HtmlStatusBar::NoUpdate);
796 
797     if (mMessageItem.hasAttribute<ErrorAttribute>()) {
798         // TODO: Insert link to clear error so that message might be resent
799         const ErrorAttribute *const attr = mMessageItem.attribute<ErrorAttribute>();
800         Q_ASSERT(attr);
801         initializeColorFromScheme();
802 
803         htmlWriter()->write(QStringLiteral("<div style=\"background:%1;color:%2;border:1px solid %2\">%3</div>")
804                                 .arg(mBackgroundError.name(), mForegroundError.name(), attr->message().toHtmlEscaped()));
805         htmlWriter()->write(QStringLiteral("<p></p>"));
806     }
807 
808     parseContent(mMessage.data());
809     mMimePartTree->setRoot(mNodeHelper->messageWithExtraContent(mMessage.data()));
810     mColorBar->update();
811 
812     htmlWriter()->write(cssHelper()->endBodyHtml());
813     connect(mViewer, &MailWebEngineView::loadFinished, this, &ViewerPrivate::executeCustomScriptsAfterLoading, Qt::UniqueConnection);
814     connect(mPartHtmlWriter.data(), &WebEnginePartHtmlWriter::finished, this, &ViewerPrivate::slotMessageRendered, Qt::UniqueConnection);
815 
816     htmlWriter()->end();
817 }
818 
parseContent(KMime::Content * content)819 void ViewerPrivate::parseContent(KMime::Content *content)
820 {
821     Q_ASSERT(content != nullptr);
822     mNodeHelper->removeTempFiles();
823 
824     // Check if any part of this message is a v-card
825     // v-cards can be either text/x-vcard or text/directory, so we need to check
826     // both.
827     KMime::Content *vCardContent = findContentByType(content, "text/x-vcard");
828     if (!vCardContent) {
829         vCardContent = findContentByType(content, "text/directory");
830     }
831     bool hasVCard = false;
832     if (vCardContent) {
833         // ### FIXME: We should only do this if the vCard belongs to the sender,
834         // ### i.e. if the sender's email address is contained in the vCard.
835         const QByteArray vCard = vCardContent->decodedContent();
836         KContacts::VCardConverter t;
837         if (!t.parseVCards(vCard).isEmpty()) {
838             hasVCard = true;
839             mNodeHelper->writeNodeToTempFile(vCardContent);
840         }
841     }
842 
843     auto message = dynamic_cast<KMime::Message *>(content);
844     bool onlySingleNode = mMessage.data() != content;
845 
846     // Pass control to the OTP now, which does the real work
847     mNodeHelper->setNodeUnprocessed(mMessage.data(), true);
848     MailViewerSource otpSource(this);
849     MimeTreeParser::ObjectTreeParser otp(&otpSource, mNodeHelper);
850 
851     otp.setAllowAsync(!mPrinting);
852     otp.parseObjectTree(content, onlySingleNode);
853     htmlWriter()->setCodec(otp.plainTextContentCharset());
854     if (message) {
855         htmlWriter()->write(writeMessageHeader(message, hasVCard ? vCardContent : nullptr, true));
856     }
857 
858     otpSource.render(otp.parsedPart(), onlySingleNode);
859 
860     // TODO: Setting the signature state to nodehelper is not enough, it should actually
861     // be added to the store, so that the message list correctly displays the signature state
862     // of messages that were parsed at least once
863     // store encrypted/signed status information in the KMMessage
864     //  - this can only be done *after* calling parseObjectTree()
865     MimeTreeParser::KMMsgEncryptionState encryptionState = mNodeHelper->overallEncryptionState(content);
866     MimeTreeParser::KMMsgSignatureState signatureState = mNodeHelper->overallSignatureState(content);
867     mNodeHelper->setEncryptionState(content, encryptionState);
868     // Don't reset the signature state to "not signed" (e.g. if one canceled the
869     // decryption of a signed messages which has already been decrypted before).
870     if (signatureState != MimeTreeParser::KMMsgNotSigned || mNodeHelper->signatureState(content) == MimeTreeParser::KMMsgSignatureStateUnknown) {
871         mNodeHelper->setSignatureState(content, signatureState);
872     }
873 
874     if (!onlySingleNode && isAutocryptEnabled(message)) {
875         auto mixup = HeaderMixupNodeHelper(mNodeHelper, message);
876         processAutocryptfromMail(mixup);
877     }
878 
879     showHideMimeTree();
880 }
881 
writeMessageHeader(KMime::Message * aMsg,KMime::Content * vCardNode,bool topLevel)882 QString ViewerPrivate::writeMessageHeader(KMime::Message *aMsg, KMime::Content *vCardNode, bool topLevel)
883 {
884     if (!headerStylePlugin()) {
885         qCCritical(MESSAGEVIEWER_LOG) << "trying to writeMessageHeader() without a header style set!";
886         return {};
887     }
888     HeaderStyle *style = headerStylePlugin()->headerStyle();
889     if (vCardNode) {
890         style->setVCardName(mNodeHelper->asHREF(vCardNode, QStringLiteral("body")));
891     } else {
892         style->setVCardName(QString());
893     }
894     style->setHeaderStrategy(headerStylePlugin()->headerStrategy());
895     style->setPrinting(mPrinting);
896     style->setTopLevel(topLevel);
897     style->setAllowAsync(true);
898     style->setSourceObject(this);
899     style->setNodeHelper(mNodeHelper);
900     style->setAttachmentHtml(attachmentHtml());
901     if (mMessageItem.isValid()) {
902         Akonadi::MessageStatus status;
903         status.setStatusFromFlags(mMessageItem.flags());
904 
905         style->setMessageStatus(status);
906     } else {
907         style->setReadOnlyMessage(true);
908     }
909 
910     return style->format(aMsg);
911 }
912 
showVCard(KMime::Content * msgPart)913 void ViewerPrivate::showVCard(KMime::Content *msgPart)
914 {
915     const QByteArray vCard = msgPart->decodedContent();
916 
917     auto vcv = new VCardViewer(mMainWindow, vCard);
918     vcv->setAttribute(Qt::WA_DeleteOnClose);
919     vcv->show();
920 }
921 
initHtmlWidget()922 void ViewerPrivate::initHtmlWidget()
923 {
924     if (!htmlWriter()) {
925         mPartHtmlWriter = new WebEnginePartHtmlWriter(mViewer, nullptr);
926         mHtmlWriter = mPartHtmlWriter;
927     }
928     connect(mViewer->page(), &QWebEnginePage::linkHovered, this, &ViewerPrivate::slotUrlOn);
929     connect(mViewer, &MailWebEngineView::openUrl, this, &ViewerPrivate::slotUrlOpen, Qt::QueuedConnection);
930     connect(mViewer, &MailWebEngineView::popupMenu, this, &ViewerPrivate::slotUrlPopup);
931     connect(mViewer, &MailWebEngineView::wheelZoomChanged, this, &ViewerPrivate::slotWheelZoomChanged);
932     connect(mViewer, &MailWebEngineView::messageMayBeAScam, this, &ViewerPrivate::slotMessageMayBeAScam);
933     connect(mViewer, &MailWebEngineView::formSubmittedForbidden, mSubmittedFormWarning, &WebEngineViewer::SubmittedFormWarningWidget::showWarning);
934     connect(mViewer, &MailWebEngineView::mailTrackingFound, mMailTrackingWarning, &WebEngineViewer::TrackingWarningWidget::addTracker);
935     connect(mScamDetectionWarning, &ScamDetectionWarningWidget::showDetails, mViewer, &MailWebEngineView::slotShowDetails);
936     connect(mScamDetectionWarning, &ScamDetectionWarningWidget::moveMessageToTrash, this, &ViewerPrivate::moveMessageToTrash);
937     connect(mScamDetectionWarning, &ScamDetectionWarningWidget::messageIsNotAScam, this, &ViewerPrivate::slotMessageIsNotAScam);
938     connect(mScamDetectionWarning, &ScamDetectionWarningWidget::addToWhiteList, this, &ViewerPrivate::slotAddToWhiteList);
939     connect(mViewer, &MailWebEngineView::pageIsScrolledToBottom, this, &ViewerPrivate::pageIsScrolledToBottom);
940     connect(mViewer, &MailWebEngineView::urlBlocked, this, &ViewerPrivate::slotUrlBlocked);
941 }
942 
slotUrlBlocked(const QUrl & url)943 void ViewerPrivate::slotUrlBlocked(const QUrl &url)
944 {
945     mRemoteContentMenu->appendUrl(url.adjusted(QUrl::RemovePath | QUrl::RemovePort | QUrl::RemoveQuery).toString());
946 }
947 
remoteContentMenu() const948 RemoteContentMenu *ViewerPrivate::remoteContentMenu() const
949 {
950     return mRemoteContentMenu;
951 }
952 
applyZoomValue(qreal factor,bool saveConfig)953 void ViewerPrivate::applyZoomValue(qreal factor, bool saveConfig)
954 {
955     if (mZoomActionMenu) {
956         if (factor >= 10 && factor <= 300) {
957             if (!qFuzzyCompare(mZoomActionMenu->zoomFactor(), factor)) {
958                 mZoomActionMenu->setZoomFactor(factor);
959                 mZoomActionMenu->setWebViewerZoomFactor(factor / 100.0);
960                 if (saveConfig) {
961                     MessageViewer::MessageViewerSettings::self()->setZoomFactor(factor);
962                 }
963             }
964         }
965     }
966 }
967 
setWebViewZoomFactor(qreal factor)968 void ViewerPrivate::setWebViewZoomFactor(qreal factor)
969 {
970     applyZoomValue(factor, false);
971 }
972 
webViewZoomFactor() const973 qreal ViewerPrivate::webViewZoomFactor() const
974 {
975     qreal zoomFactor = -1;
976     if (mZoomActionMenu) {
977         zoomFactor = mZoomActionMenu->zoomFactor();
978     }
979     return zoomFactor;
980 }
981 
slotWheelZoomChanged(int numSteps)982 void ViewerPrivate::slotWheelZoomChanged(int numSteps)
983 {
984     const qreal factor = mZoomActionMenu->zoomFactor() + numSteps * 10;
985     applyZoomValue(factor);
986 }
987 
readConfig()988 void ViewerPrivate::readConfig()
989 {
990     mMessageViewerRenderer->setCurrentWidget(mViewer);
991     recreateCssHelper();
992 
993     mForceEmoticons = MessageViewer::MessageViewerSettings::self()->showEmoticons();
994     if (mDisableEmoticonAction) {
995         mDisableEmoticonAction->setChecked(!mForceEmoticons);
996     }
997     if (headerStylePlugin()) {
998         headerStylePlugin()->headerStyle()->setShowEmoticons(mForceEmoticons);
999     }
1000 
1001     mUseFixedFont = MessageViewer::MessageViewerSettings::self()->useFixedFont();
1002     if (mToggleFixFontAction) {
1003         mToggleFixFontAction->setChecked(mUseFixedFont);
1004     }
1005 
1006     mHtmlMailGlobalSetting = MessageViewer::MessageViewerSettings::self()->htmlMail();
1007 
1008     MessageViewer::Util::readGravatarConfig();
1009     if (mHeaderStyleMenuManager) {
1010         mHeaderStyleMenuManager->readConfig();
1011     }
1012 
1013     setAttachmentStrategy(AttachmentStrategy::create(MessageViewer::MessageViewerSettings::self()->attachmentStrategy()));
1014     KToggleAction *raction = actionForAttachmentStrategy(attachmentStrategy());
1015     if (raction) {
1016         raction->setChecked(true);
1017     }
1018 
1019     adjustLayout();
1020 
1021     readGlobalOverrideCodec();
1022     mViewer->readConfig();
1023     mViewer->settings()->setFontSize(QWebEngineSettings::MinimumFontSize, MessageViewer::MessageViewerSettings::self()->minimumFontSize());
1024     mViewer->settings()->setFontSize(QWebEngineSettings::MinimumLogicalFontSize, MessageViewer::MessageViewerSettings::self()->minimumFontSize());
1025     if (mMessage) {
1026         update();
1027     }
1028     mColorBar->update();
1029     applyZoomValue(MessageViewer::MessageViewerSettings::self()->zoomFactor(), false);
1030 }
1031 
recreateCssHelper()1032 void ViewerPrivate::recreateCssHelper()
1033 {
1034     mMessageViewerRenderer->recreateCssHelper();
1035 }
1036 
hasMultiMessages(bool messages)1037 void ViewerPrivate::hasMultiMessages(bool messages)
1038 {
1039     mShowNextMessageWidget->setVisible(messages);
1040 }
1041 
slotGeneralFontChanged()1042 void ViewerPrivate::slotGeneralFontChanged()
1043 {
1044     recreateCssHelper();
1045     if (mMessage) {
1046         update();
1047     }
1048 }
1049 
writeConfig(bool sync)1050 void ViewerPrivate::writeConfig(bool sync)
1051 {
1052     MessageViewer::MessageViewerSettings::self()->setShowEmoticons(mForceEmoticons);
1053     MessageViewer::MessageViewerSettings::self()->setUseFixedFont(mUseFixedFont);
1054     if (attachmentStrategy()) {
1055         MessageViewer::MessageViewerSettings::self()->setAttachmentStrategy(QLatin1String(attachmentStrategy()->name()));
1056     }
1057     saveSplitterSizes();
1058     if (sync) {
1059         Q_EMIT requestConfigSync();
1060     }
1061 }
1062 
attachmentStrategy() const1063 const AttachmentStrategy *ViewerPrivate::attachmentStrategy() const
1064 {
1065     return mAttachmentStrategy;
1066 }
1067 
setAttachmentStrategy(const AttachmentStrategy * strategy)1068 void ViewerPrivate::setAttachmentStrategy(const AttachmentStrategy *strategy)
1069 {
1070     if (mAttachmentStrategy == strategy) {
1071         return;
1072     }
1073     mAttachmentStrategy = strategy ? strategy : AttachmentStrategy::smart();
1074     update(MimeTreeParser::Force);
1075 }
1076 
overrideEncoding() const1077 QString ViewerPrivate::overrideEncoding() const
1078 {
1079     return mOverrideEncoding;
1080 }
1081 
setOverrideEncoding(const QString & encoding)1082 void ViewerPrivate::setOverrideEncoding(const QString &encoding)
1083 {
1084     if (encoding == mOverrideEncoding) {
1085         return;
1086     }
1087 
1088     mOverrideEncoding = encoding;
1089     if (mSelectEncodingAction) {
1090         if (encoding.isEmpty()) {
1091             mSelectEncodingAction->setCurrentItem(0);
1092         } else {
1093             const QStringList encodings = mSelectEncodingAction->items();
1094             int i = 0;
1095             for (QStringList::const_iterator it = encodings.constBegin(), end = encodings.constEnd(); it != end; ++it, ++i) {
1096                 if (MimeTreeParser::NodeHelper::encodingForName(*it) == encoding) {
1097                     mSelectEncodingAction->setCurrentItem(i);
1098                     break;
1099                 }
1100             }
1101             if (i == encodings.size()) {
1102                 // the value of encoding is unknown => use Auto
1103                 qCWarning(MESSAGEVIEWER_LOG) << "Unknown override character encoding" << encoding << ". Using Auto instead.";
1104                 mSelectEncodingAction->setCurrentItem(0);
1105                 mOverrideEncoding.clear();
1106             }
1107         }
1108     }
1109     update(MimeTreeParser::Force);
1110 }
1111 
setPrinting(bool enable)1112 void ViewerPrivate::setPrinting(bool enable)
1113 {
1114     mPrinting = enable;
1115 }
1116 
printingMode() const1117 bool ViewerPrivate::printingMode() const
1118 {
1119     return mPrinting;
1120 }
1121 
printMessage(const Akonadi::Item & message)1122 void ViewerPrivate::printMessage(const Akonadi::Item &message)
1123 {
1124     disconnect(mPartHtmlWriter.data(), &WebEnginePartHtmlWriter::finished, this, &ViewerPrivate::slotPrintMessage);
1125     connect(mPartHtmlWriter.data(), &WebEnginePartHtmlWriter::finished, this, &ViewerPrivate::slotPrintMessage);
1126     // need to set htmlLoadExtOverride() when we set Item otherwise this settings is reset
1127     setMessageItem(message, MimeTreeParser::Force, htmlLoadExtOverride());
1128 }
1129 
printPreviewMessage(const Akonadi::Item & message)1130 void ViewerPrivate::printPreviewMessage(const Akonadi::Item &message)
1131 {
1132     disconnect(mPartHtmlWriter.data(), &WebEnginePartHtmlWriter::finished, this, &ViewerPrivate::slotPrintPreview);
1133     connect(mPartHtmlWriter.data(), &WebEnginePartHtmlWriter::finished, this, &ViewerPrivate::slotPrintPreview);
1134     setMessageItem(message, MimeTreeParser::Force, htmlLoadExtOverride());
1135 }
1136 
resetStateForNewMessage()1137 void ViewerPrivate::resetStateForNewMessage()
1138 {
1139     mDkimWidgetInfo->clear();
1140     mHtmlLoadExtOverride = false;
1141     mClickedUrl.clear();
1142     mImageUrl.clear();
1143     enableMessageDisplay(); // just to make sure it's on
1144     mMessage.reset();
1145     mNodeHelper->clear();
1146     mMessagePartNode = nullptr;
1147     mMimePartTree->clearModel();
1148     if (mViewer) {
1149         mViewer->clearRelativePosition();
1150         mViewer->hideAccessKeys();
1151     }
1152     if (!mPrinting) {
1153         setShowSignatureDetails(false);
1154     }
1155     mViewerPluginToolManager->closeAllTools();
1156     mScamDetectionWarning->setVisible(false);
1157     mOpenSavedFileFolderWidget->setVisible(false);
1158     mSubmittedFormWarning->setVisible(false);
1159     mMailTrackingWarning->hideAndClear();
1160     mRemoteContentMenu->clearUrls();
1161 
1162     if (mPrinting) {
1163         if (MessageViewer::MessageViewerSettings::self()->respectExpandCollapseSettings()) {
1164             if (MessageViewer::MessageViewerSettings::self()->showExpandQuotesMark()) {
1165                 mLevelQuote = MessageViewer::MessageViewerSettings::self()->collapseQuoteLevelSpin() - 1;
1166             } else {
1167                 mLevelQuote = -1;
1168             }
1169         } else {
1170             mLevelQuote = -1;
1171         }
1172     } else {
1173         //        mDisplayFormatMessageOverwrite
1174         //            = (mDisplayFormatMessageOverwrite
1175         //               == MessageViewer::Viewer::UseGlobalSetting) ? MessageViewer::Viewer::UseGlobalSetting
1176         //              :
1177         //              MessageViewer::Viewer::Unknown;
1178     }
1179 }
1180 
setMessageInternal(const KMime::Message::Ptr & message,MimeTreeParser::UpdateMode updateMode)1181 void ViewerPrivate::setMessageInternal(const KMime::Message::Ptr &message, MimeTreeParser::UpdateMode updateMode)
1182 {
1183     mViewerPluginToolManager->updateActions(mMessageItem);
1184     mMessage = message;
1185     if (message) {
1186         mNodeHelper->setOverrideCodec(mMessage.data(), overrideCodec());
1187     }
1188 
1189     mMimePartTree->setRoot(mNodeHelper->messageWithExtraContent(message.data()));
1190     update(updateMode);
1191 }
1192 
assignMessageItem(const Akonadi::Item & item)1193 void ViewerPrivate::assignMessageItem(const Akonadi::Item &item)
1194 {
1195     mMessageItem = item;
1196 }
1197 
setMessageItem(const Akonadi::Item & item,MimeTreeParser::UpdateMode updateMode,bool forceHtmlLoadExtOverride)1198 void ViewerPrivate::setMessageItem(const Akonadi::Item &item, MimeTreeParser::UpdateMode updateMode, bool forceHtmlLoadExtOverride)
1199 {
1200     resetStateForNewMessage();
1201     if (forceHtmlLoadExtOverride) {
1202         setHtmlLoadExtOverride(true);
1203     }
1204 
1205     const auto itemsMonitoredEx = mMonitor.itemsMonitoredEx();
1206 
1207     for (const Akonadi::Item::Id monitoredId : itemsMonitoredEx) {
1208         mMonitor.setItemMonitored(Akonadi::Item(monitoredId), false);
1209     }
1210     Q_ASSERT(mMonitor.itemsMonitoredEx().isEmpty());
1211 
1212     assignMessageItem(item);
1213     if (mMessageItem.isValid()) {
1214         mMonitor.setItemMonitored(mMessageItem, true);
1215     }
1216 
1217     if (!mMessageItem.hasPayload<KMime::Message::Ptr>()) {
1218         if (mMessageItem.isValid()) {
1219             qCWarning(MESSAGEVIEWER_LOG) << "Payload is not a MessagePtr!";
1220         }
1221         return;
1222     }
1223     if (!mPrinting) {
1224         if (MessageViewer::MessageViewerSettings::self()->enabledDkim()) {
1225             if (messageIsInSpecialFolder()) {
1226                 mDkimWidgetInfo->clear();
1227             } else {
1228                 mDkimWidgetInfo->setCurrentItemId(mMessageItem.id());
1229                 MessageViewer::DKIMManager::self()->checkDKim(mMessageItem);
1230             }
1231         }
1232     }
1233 
1234     setMessageInternal(mMessageItem.payload<KMime::Message::Ptr>(), updateMode);
1235 }
1236 
messageIsInSpecialFolder() const1237 bool ViewerPrivate::messageIsInSpecialFolder() const
1238 {
1239     const Akonadi::Collection parentCollection = mMessageItem.parentCollection();
1240     if ((Akonadi::SpecialMailCollections::self()->defaultCollection(Akonadi::SpecialMailCollections::SentMail) != parentCollection)
1241         && (Akonadi::SpecialMailCollections::self()->defaultCollection(Akonadi::SpecialMailCollections::Outbox) != parentCollection)
1242         && (Akonadi::SpecialMailCollections::self()->defaultCollection(Akonadi::SpecialMailCollections::Templates) != parentCollection)
1243         && (Akonadi::SpecialMailCollections::self()->defaultCollection(Akonadi::SpecialMailCollections::Drafts) != parentCollection)) {
1244         return false;
1245     } else {
1246         return true;
1247     }
1248 }
1249 
setMessage(const KMime::Message::Ptr & aMsg,MimeTreeParser::UpdateMode updateMode)1250 void ViewerPrivate::setMessage(const KMime::Message::Ptr &aMsg, MimeTreeParser::UpdateMode updateMode)
1251 {
1252     resetStateForNewMessage();
1253 
1254     Akonadi::Item item;
1255     item.setMimeType(KMime::Message::mimeType());
1256     item.setPayload(aMsg);
1257     assignMessageItem(item);
1258 
1259     setMessageInternal(aMsg, updateMode);
1260 }
1261 
setMessagePart(KMime::Content * node)1262 void ViewerPrivate::setMessagePart(KMime::Content *node)
1263 {
1264     // Cancel scheduled updates of the reader window, as that would stop the
1265     // timer of the HTML writer, which would make viewing attachment not work
1266     // anymore as not all HTML is written to the HTML part.
1267     // We're updating the reader window here ourselves anyway.
1268     mUpdateReaderWinTimer.stop();
1269 
1270     if (node) {
1271         mMessagePartNode = node;
1272         if (node->bodyIsMessage()) {
1273             mMainWindow->setWindowTitle(node->bodyAsMessage()->subject()->asUnicodeString());
1274         } else {
1275             QString windowTitle = MimeTreeParser::NodeHelper::fileName(node);
1276             if (windowTitle.isEmpty()) {
1277                 windowTitle = node->contentDescription()->asUnicodeString();
1278             }
1279             if (!windowTitle.isEmpty()) {
1280                 mMainWindow->setWindowTitle(i18nc("@title:window", "View Attachment: %1", windowTitle));
1281             }
1282         }
1283 
1284         htmlWriter()->begin();
1285         htmlWriter()->write(cssHelper()->htmlHead(mUseFixedFont));
1286 
1287         parseContent(node);
1288 
1289         htmlWriter()->write(cssHelper()->endBodyHtml());
1290         htmlWriter()->end();
1291     }
1292 }
1293 
showHideMimeTree()1294 void ViewerPrivate::showHideMimeTree()
1295 {
1296     if (mimePartTreeIsEmpty()) {
1297         mMimePartTree->hide();
1298         return;
1299     }
1300     bool showMimeTree = false;
1301     if (MessageViewer::MessageViewerSettings::self()->mimeTreeMode2() == MessageViewer::MessageViewerSettings::EnumMimeTreeMode2::Always) {
1302         mMimePartTree->show();
1303         showMimeTree = true;
1304     } else {
1305         // don't rely on QSplitter maintaining sizes for hidden widgets:
1306         saveSplitterSizes();
1307         mMimePartTree->hide();
1308         showMimeTree = false;
1309     }
1310     if (mToggleMimePartTreeAction && (mToggleMimePartTreeAction->isChecked() != showMimeTree)) {
1311         mToggleMimePartTreeAction->setChecked(showMimeTree);
1312     }
1313 }
1314 
attachmentViewMessage(const KMime::Message::Ptr & message)1315 void ViewerPrivate::attachmentViewMessage(const KMime::Message::Ptr &message)
1316 {
1317     Q_ASSERT(message);
1318     Q_EMIT showMessage(message, overrideEncoding());
1319 }
1320 
adjustLayout()1321 void ViewerPrivate::adjustLayout()
1322 {
1323     const int mimeH = MessageViewer::MessageViewerSettings::self()->mimePaneHeight();
1324     const int messageH = MessageViewer::MessageViewerSettings::self()->messagePaneHeight();
1325     const QList<int> splitterSizes{messageH, mimeH};
1326 
1327     mSplitter->addWidget(mMimePartTree);
1328     mSplitter->setSizes(splitterSizes);
1329 
1330     if (MessageViewer::MessageViewerSettings::self()->mimeTreeMode2() == MessageViewer::MessageViewerSettings::EnumMimeTreeMode2::Always && mMsgDisplay) {
1331         mMimePartTree->show();
1332     } else {
1333         mMimePartTree->hide();
1334     }
1335 
1336     if (mMsgDisplay) {
1337         mColorBar->show();
1338     } else {
1339         mColorBar->hide();
1340     }
1341 }
1342 
saveSplitterSizes() const1343 void ViewerPrivate::saveSplitterSizes() const
1344 {
1345     if (!mSplitter || !mMimePartTree) {
1346         return;
1347     }
1348     if (mMimePartTree->isHidden()) {
1349         return; // don't rely on QSplitter maintaining sizes for hidden widgets.
1350     }
1351     MessageViewer::MessageViewerSettings::self()->setMimePaneHeight(mSplitter->sizes().at(1));
1352     MessageViewer::MessageViewerSettings::self()->setMessagePaneHeight(mSplitter->sizes().at(0));
1353 }
1354 
createWidgets()1355 void ViewerPrivate::createWidgets()
1356 {
1357     // TODO: Make a MDN bar similar to Mozillas password bar and show MDNs here as soon as a
1358     //      MDN enabled message is shown.
1359     auto vlay = new QVBoxLayout(q);
1360     vlay->setContentsMargins({});
1361     mSplitter = new QSplitter(Qt::Vertical, q);
1362     connect(mSplitter, &QSplitter::splitterMoved, this, &ViewerPrivate::saveSplitterSizes);
1363     mSplitter->setObjectName(QStringLiteral("mSplitter"));
1364     mSplitter->setChildrenCollapsible(false);
1365     vlay->addWidget(mSplitter);
1366     mMimePartTree = new MimePartTreeView(mSplitter);
1367     mMimePartTree->setMinimumHeight(10);
1368     connect(mMimePartTree, &QAbstractItemView::activated, this, &ViewerPrivate::slotMimePartSelected);
1369     connect(mMimePartTree, &QWidget::customContextMenuRequested, this, &ViewerPrivate::slotMimeTreeContextMenuRequested);
1370 
1371     mBox = new QWidget(mSplitter);
1372     auto mBoxHBoxLayout = new QHBoxLayout(mBox);
1373     mBoxHBoxLayout->setContentsMargins({});
1374 
1375     mColorBar = new HtmlStatusBar(mBox);
1376     mBoxHBoxLayout->addWidget(mColorBar);
1377     auto readerBox = new QWidget(mBox);
1378     auto readerBoxVBoxLayout = new QVBoxLayout(readerBox);
1379     readerBoxVBoxLayout->setContentsMargins({});
1380     mBoxHBoxLayout->addWidget(readerBox);
1381 
1382     mColorBar->setObjectName(QStringLiteral("mColorBar"));
1383     mColorBar->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Ignored);
1384 
1385     mShowNextMessageWidget = new MessageViewer::ShowNextMessageWidget(readerBox);
1386     mShowNextMessageWidget->setObjectName(QStringLiteral("shownextmessagewidget"));
1387     readerBoxVBoxLayout->addWidget(mShowNextMessageWidget);
1388     mShowNextMessageWidget->hide();
1389     connect(mShowNextMessageWidget, &ShowNextMessageWidget::showPreviousMessage, this, &ViewerPrivate::showPreviousMessage);
1390     connect(mShowNextMessageWidget, &ShowNextMessageWidget::showNextMessage, this, &ViewerPrivate::showNextMessage);
1391 
1392     mSubmittedFormWarning = new WebEngineViewer::SubmittedFormWarningWidget(readerBox);
1393     mSubmittedFormWarning->setObjectName(QStringLiteral("submittedformwarning"));
1394     readerBoxVBoxLayout->addWidget(mSubmittedFormWarning);
1395 
1396     mMailTrackingWarning = new WebEngineViewer::TrackingWarningWidget(readerBox);
1397     mMailTrackingWarning->setObjectName(QStringLiteral("mailtrackingwarning"));
1398     readerBoxVBoxLayout->addWidget(mMailTrackingWarning);
1399 
1400     mScamDetectionWarning = new ScamDetectionWarningWidget(readerBox);
1401     mScamDetectionWarning->setObjectName(QStringLiteral("scandetectionwarning"));
1402     readerBoxVBoxLayout->addWidget(mScamDetectionWarning);
1403 
1404     mOpenSavedFileFolderWidget = new OpenSavedFileFolderWidget(readerBox);
1405     mOpenSavedFileFolderWidget->setObjectName(QStringLiteral("opensavefilefolderwidget"));
1406     readerBoxVBoxLayout->addWidget(mOpenSavedFileFolderWidget);
1407 
1408     mTextToSpeechWidget = new KPIMTextEdit::TextToSpeechWidget(readerBox);
1409     mTextToSpeechWidget->setObjectName(QStringLiteral("texttospeechwidget"));
1410     readerBoxVBoxLayout->addWidget(mTextToSpeechWidget);
1411 
1412     mViewer = new MailWebEngineView(mActionCollection, readerBox);
1413     mViewer->setViewer(this);
1414     readerBoxVBoxLayout->addWidget(mViewer);
1415     mViewer->setObjectName(QStringLiteral("mViewer"));
1416 
1417     mViewerPluginToolManager = new MessageViewer::ViewerPluginToolManager(readerBox, this);
1418     mViewerPluginToolManager->setActionCollection(mActionCollection);
1419     mViewerPluginToolManager->setPluginName(QStringLiteral("messageviewer"));
1420     mViewerPluginToolManager->setPluginDirectory(QStringLiteral("messageviewer/viewerplugin"));
1421     if (!mViewerPluginToolManager->initializePluginList()) {
1422         qCWarning(MESSAGEVIEWER_LOG) << " Impossible to initialize plugins";
1423     }
1424     mViewerPluginToolManager->createView();
1425     connect(mViewerPluginToolManager, &MessageViewer::ViewerPluginToolManager::activatePlugin, this, &ViewerPrivate::slotActivatePlugin);
1426 
1427     mSliderContainer = new KPIMTextEdit::SlideContainer(readerBox);
1428     mSliderContainer->setObjectName(QStringLiteral("slidercontainer"));
1429     readerBoxVBoxLayout->addWidget(mSliderContainer);
1430     mFindBar = new WebEngineViewer::FindBarWebEngineView(mViewer, q);
1431     connect(mFindBar, &WebEngineViewer::FindBarWebEngineView::hideFindBar, mSliderContainer, &KPIMTextEdit::SlideContainer::slideOut);
1432     mSliderContainer->setContent(mFindBar);
1433 
1434     mSplitter->setStretchFactor(mSplitter->indexOf(mMimePartTree), 0);
1435 }
1436 
slotStyleChanged(MessageViewer::HeaderStylePlugin * plugin)1437 void ViewerPrivate::slotStyleChanged(MessageViewer::HeaderStylePlugin *plugin)
1438 {
1439     cssHelper()->setHeaderPlugin(plugin);
1440     mHeaderStylePlugin = plugin;
1441     update(MimeTreeParser::Force);
1442 }
1443 
slotStyleUpdated()1444 void ViewerPrivate::slotStyleUpdated()
1445 {
1446     update(MimeTreeParser::Force);
1447 }
1448 
createActions()1449 void ViewerPrivate::createActions()
1450 {
1451     KActionCollection *ac = mActionCollection;
1452     mHeaderStyleMenuManager = new MessageViewer::HeaderStyleMenuManager(ac, this);
1453     connect(mHeaderStyleMenuManager, &MessageViewer::HeaderStyleMenuManager::styleChanged, this, &ViewerPrivate::slotStyleChanged);
1454     connect(mHeaderStyleMenuManager, &MessageViewer::HeaderStyleMenuManager::styleUpdated, this, &ViewerPrivate::slotStyleUpdated);
1455     if (!ac) {
1456         return;
1457     }
1458     mZoomActionMenu = new WebEngineViewer::ZoomActionMenu(this);
1459     connect(mZoomActionMenu, &WebEngineViewer::ZoomActionMenu::zoomChanged, this, &ViewerPrivate::slotZoomChanged);
1460     mZoomActionMenu->setActionCollection(ac);
1461     mZoomActionMenu->createZoomActions();
1462 
1463     // attachment style
1464     auto attachmentMenu = new KActionMenu(i18nc("View->", "&Attachments"), this);
1465     ac->addAction(QStringLiteral("view_attachments"), attachmentMenu);
1466     MessageViewer::Util::addHelpTextAction(attachmentMenu, i18n("Choose display style of attachments"));
1467 
1468     auto group = new QActionGroup(this);
1469     auto raction = new KToggleAction(i18nc("View->attachments->", "&As Icons"), this);
1470     ac->addAction(QStringLiteral("view_attachments_as_icons"), raction);
1471     connect(raction, &QAction::triggered, this, &ViewerPrivate::slotIconicAttachments);
1472     MessageViewer::Util::addHelpTextAction(raction, i18n("Show all attachments as icons. Click to see them."));
1473     group->addAction(raction);
1474     attachmentMenu->addAction(raction);
1475 
1476     raction = new KToggleAction(i18nc("View->attachments->", "&Smart"), this);
1477     ac->addAction(QStringLiteral("view_attachments_smart"), raction);
1478     connect(raction, &QAction::triggered, this, &ViewerPrivate::slotSmartAttachments);
1479     MessageViewer::Util::addHelpTextAction(raction, i18n("Show attachments as suggested by sender."));
1480     group->addAction(raction);
1481     attachmentMenu->addAction(raction);
1482 
1483     raction = new KToggleAction(i18nc("View->attachments->", "&Inline"), this);
1484     ac->addAction(QStringLiteral("view_attachments_inline"), raction);
1485     connect(raction, &QAction::triggered, this, &ViewerPrivate::slotInlineAttachments);
1486     MessageViewer::Util::addHelpTextAction(raction, i18n("Show all attachments inline (if possible)"));
1487     group->addAction(raction);
1488     attachmentMenu->addAction(raction);
1489 
1490     raction = new KToggleAction(i18nc("View->attachments->", "&Hide"), this);
1491     ac->addAction(QStringLiteral("view_attachments_hide"), raction);
1492     connect(raction, &QAction::triggered, this, &ViewerPrivate::slotHideAttachments);
1493     MessageViewer::Util::addHelpTextAction(raction, i18n("Do not show attachments in the message viewer"));
1494     group->addAction(raction);
1495     attachmentMenu->addAction(raction);
1496 
1497     mHeaderOnlyAttachmentsAction = new KToggleAction(i18nc("View->attachments->", "In Header Only"), this);
1498     ac->addAction(QStringLiteral("view_attachments_headeronly"), mHeaderOnlyAttachmentsAction);
1499     connect(mHeaderOnlyAttachmentsAction, &QAction::triggered, this, &ViewerPrivate::slotHeaderOnlyAttachments);
1500     MessageViewer::Util::addHelpTextAction(mHeaderOnlyAttachmentsAction, i18n("Show Attachments only in the header of the mail"));
1501     group->addAction(mHeaderOnlyAttachmentsAction);
1502     attachmentMenu->addAction(mHeaderOnlyAttachmentsAction);
1503 
1504     // Set Encoding submenu
1505     mSelectEncodingAction = new KSelectAction(QIcon::fromTheme(QStringLiteral("character-set")), i18n("&Set Encoding"), this);
1506     mSelectEncodingAction->setToolBarMode(KSelectAction::MenuMode);
1507     ac->addAction(QStringLiteral("encoding"), mSelectEncodingAction);
1508     connect(mSelectEncodingAction, &KSelectAction::indexTriggered, this, &ViewerPrivate::slotSetEncoding);
1509     QStringList encodings = MimeTreeParser::NodeHelper::supportedEncodings(false);
1510     encodings.prepend(i18n("Auto"));
1511     mSelectEncodingAction->setItems(encodings);
1512     mSelectEncodingAction->setCurrentItem(0);
1513 
1514     //
1515     // Message Menu
1516     //
1517 
1518     // copy selected text to clipboard
1519     mCopyAction = ac->addAction(KStandardAction::Copy, QStringLiteral("kmail_copy"));
1520     mCopyAction->setText(i18n("Copy Text"));
1521     connect(mCopyAction, &QAction::triggered, this, &ViewerPrivate::slotCopySelectedText);
1522 
1523     connect(mViewer, &MailWebEngineView::selectionChanged, this, &ViewerPrivate::viewerSelectionChanged);
1524     viewerSelectionChanged();
1525 
1526     // copy all text to clipboard
1527     mSelectAllAction = new QAction(i18n("Select All Text"), this);
1528     ac->addAction(QStringLiteral("mark_all_text"), mSelectAllAction);
1529     connect(mSelectAllAction, &QAction::triggered, this, &ViewerPrivate::selectAll);
1530     ac->setDefaultShortcut(mSelectAllAction, QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_A));
1531 
1532     // copy Email address to clipboard
1533     mCopyURLAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy Link Address"), this);
1534     ac->addAction(QStringLiteral("copy_url"), mCopyURLAction);
1535     connect(mCopyURLAction, &QAction::triggered, this, &ViewerPrivate::slotUrlCopy);
1536 
1537     // open URL
1538     mUrlOpenAction = new QAction(QIcon::fromTheme(QStringLiteral("document-open")), i18n("Open URL"), this);
1539     ac->addAction(QStringLiteral("open_url"), mUrlOpenAction);
1540     connect(mUrlOpenAction, &QAction::triggered, this, &ViewerPrivate::slotOpenUrl);
1541 
1542     // use fixed font
1543     mToggleFixFontAction = new KToggleAction(i18n("Use Fi&xed Font"), this);
1544     ac->addAction(QStringLiteral("toggle_fixedfont"), mToggleFixFontAction);
1545     connect(mToggleFixFontAction, &QAction::triggered, this, &ViewerPrivate::slotToggleFixedFont);
1546     ac->setDefaultShortcut(mToggleFixFontAction, QKeySequence(Qt::Key_X));
1547 
1548     // Show message structure viewer
1549     mToggleMimePartTreeAction = new KToggleAction(i18n("Show Message Structure"), this);
1550     ac->addAction(QStringLiteral("toggle_mimeparttree"), mToggleMimePartTreeAction);
1551     connect(mToggleMimePartTreeAction, &QAction::toggled, this, &ViewerPrivate::slotToggleMimePartTree);
1552     ac->setDefaultShortcut(mToggleMimePartTreeAction, QKeySequence(Qt::Key_D | Qt::CTRL | Qt::ALT));
1553 
1554     mViewSourceAction = new QAction(i18n("&View Source"), this);
1555     ac->addAction(QStringLiteral("view_source"), mViewSourceAction);
1556     connect(mViewSourceAction, &QAction::triggered, this, &ViewerPrivate::slotShowMessageSource);
1557     ac->setDefaultShortcut(mViewSourceAction, QKeySequence(Qt::Key_V));
1558 
1559     mSaveMessageAction = new QAction(QIcon::fromTheme(QStringLiteral("document-save-as")), i18n("&Save Message..."), this);
1560     ac->addAction(QStringLiteral("save_message"), mSaveMessageAction);
1561     connect(mSaveMessageAction, &QAction::triggered, this, &ViewerPrivate::slotSaveMessage);
1562     // Laurent: conflict with kmail shortcut
1563     // mSaveMessageAction->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_S));
1564 
1565     mSaveMessageDisplayFormat = new QAction(i18n("&Save Display Format"), this);
1566     ac->addAction(QStringLiteral("save_message_display_format"), mSaveMessageDisplayFormat);
1567     connect(mSaveMessageDisplayFormat, &QAction::triggered, this, &ViewerPrivate::slotSaveMessageDisplayFormat);
1568 
1569     mResetMessageDisplayFormat = new QAction(i18n("&Reset Display Format"), this);
1570     ac->addAction(QStringLiteral("reset_message_display_format"), mResetMessageDisplayFormat);
1571     connect(mResetMessageDisplayFormat, &QAction::triggered, this, &ViewerPrivate::slotResetMessageDisplayFormat);
1572 
1573     //
1574     // Scroll actions
1575     //
1576     mScrollUpAction = new QAction(i18n("Scroll Message Up"), this);
1577     ac->setDefaultShortcut(mScrollUpAction, QKeySequence(Qt::Key_Up));
1578     ac->addAction(QStringLiteral("scroll_up"), mScrollUpAction);
1579     connect(mScrollUpAction, &QAction::triggered, q, &Viewer::slotScrollUp);
1580 
1581     mScrollDownAction = new QAction(i18n("Scroll Message Down"), this);
1582     ac->setDefaultShortcut(mScrollDownAction, QKeySequence(Qt::Key_Down));
1583     ac->addAction(QStringLiteral("scroll_down"), mScrollDownAction);
1584     connect(mScrollDownAction, &QAction::triggered, q, &Viewer::slotScrollDown);
1585 
1586     mScrollUpMoreAction = new QAction(i18n("Scroll Message Up (More)"), this);
1587     ac->setDefaultShortcut(mScrollUpMoreAction, QKeySequence(Qt::Key_PageUp));
1588     ac->addAction(QStringLiteral("scroll_up_more"), mScrollUpMoreAction);
1589     connect(mScrollUpMoreAction, &QAction::triggered, q, &Viewer::slotScrollPrior);
1590 
1591     mScrollDownMoreAction = new QAction(i18n("Scroll Message Down (More)"), this);
1592     ac->setDefaultShortcut(mScrollDownMoreAction, QKeySequence(Qt::Key_PageDown));
1593     ac->addAction(QStringLiteral("scroll_down_more"), mScrollDownMoreAction);
1594     connect(mScrollDownMoreAction, &QAction::triggered, q, &Viewer::slotScrollNext);
1595 
1596     //
1597     // Actions not in menu
1598     //
1599 
1600     // Toggle HTML display mode.
1601     mToggleDisplayModeAction = new KToggleAction(i18n("Toggle HTML Display Mode"), this);
1602     ac->addAction(QStringLiteral("toggle_html_display_mode"), mToggleDisplayModeAction);
1603     ac->setDefaultShortcut(mToggleDisplayModeAction, QKeySequence(Qt::SHIFT | Qt::Key_H));
1604     connect(mToggleDisplayModeAction, &QAction::triggered, this, &ViewerPrivate::slotToggleHtmlMode);
1605     MessageViewer::Util::addHelpTextAction(mToggleDisplayModeAction, i18n("Toggle display mode between HTML and plain text"));
1606 
1607     // Load external reference
1608     auto loadExternalReferenceAction = new QAction(i18n("Load external references"), this);
1609     ac->addAction(QStringLiteral("load_external_reference"), loadExternalReferenceAction);
1610     ac->setDefaultShortcut(loadExternalReferenceAction, QKeySequence(Qt::SHIFT | Qt::CTRL | Qt::Key_R));
1611     connect(loadExternalReferenceAction, &QAction::triggered, this, &ViewerPrivate::slotLoadExternalReference);
1612     MessageViewer::Util::addHelpTextAction(loadExternalReferenceAction, i18n("Load external references from the Internet for this message."));
1613 
1614     mSpeakTextAction = new QAction(i18n("Speak Text"), this);
1615     mSpeakTextAction->setIcon(QIcon::fromTheme(QStringLiteral("preferences-desktop-text-to-speech")));
1616     ac->addAction(QStringLiteral("speak_text"), mSpeakTextAction);
1617     connect(mSpeakTextAction, &QAction::triggered, this, &ViewerPrivate::slotSpeakText);
1618 
1619     auto purposeMenuWidget = new MailfilterPurposeMenuWidget(mViewer, this);
1620     mShareTextAction = new QAction(i18n("Share Text..."), this);
1621     mShareTextAction->setMenu(purposeMenuWidget->menu());
1622     mShareTextAction->setIcon(QIcon::fromTheme(QStringLiteral("document-share")));
1623     ac->addAction(QStringLiteral("purpose_share_text_menu"), mShareTextAction);
1624     purposeMenuWidget->setViewer(mViewer);
1625 
1626     mCopyImageLocation = new QAction(i18n("Copy Image Location"), this);
1627     mCopyImageLocation->setIcon(QIcon::fromTheme(QStringLiteral("view-media-visualization")));
1628     ac->addAction(QStringLiteral("copy_image_location"), mCopyImageLocation);
1629     ac->setShortcutsConfigurable(mCopyImageLocation, false);
1630     connect(mCopyImageLocation, &QAction::triggered, this, &ViewerPrivate::slotCopyImageLocation);
1631 
1632     mFindInMessageAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-find")), i18n("&Find in Message..."), this);
1633     ac->addAction(QStringLiteral("find_in_messages"), mFindInMessageAction);
1634     connect(mFindInMessageAction, &QAction::triggered, this, &ViewerPrivate::slotFind);
1635     ac->setDefaultShortcut(mFindInMessageAction, KStandardShortcut::find().first());
1636 
1637     mShareServiceUrlMenu = mShareServiceManager->menu();
1638     ac->addAction(QStringLiteral("shareservice_menu"), mShareServiceUrlMenu);
1639     connect(mShareServiceManager, &PimCommon::ShareServiceUrlManager::serviceUrlSelected, this, &ViewerPrivate::slotServiceUrlSelected);
1640 
1641     mDisableEmoticonAction = new KToggleAction(i18n("Disable Emoticon"), this);
1642     ac->addAction(QStringLiteral("disable_emoticon"), mDisableEmoticonAction);
1643     connect(mDisableEmoticonAction, &QAction::triggered, this, &ViewerPrivate::slotToggleEmoticons);
1644 
1645     // Don't translate it.
1646     mDevelopmentToolsAction = new QAction(QStringLiteral("Development Tools"), this);
1647     ac->addAction(QStringLiteral("development_tools"), mDevelopmentToolsAction);
1648     ac->setDefaultShortcut(mDevelopmentToolsAction, QKeySequence(Qt::SHIFT | Qt::CTRL | Qt::Key_I));
1649 
1650     connect(mDevelopmentToolsAction, &QAction::triggered, this, &ViewerPrivate::slotShowDevelopmentTools);
1651 }
1652 
slotShowDevelopmentTools()1653 void ViewerPrivate::slotShowDevelopmentTools()
1654 {
1655     if (!mDeveloperToolDialog) {
1656         mDeveloperToolDialog = new WebEngineViewer::DeveloperToolDialog(nullptr);
1657         mViewer->page()->setDevToolsPage(mDeveloperToolDialog->enginePage());
1658         mViewer->page()->triggerAction(QWebEnginePage::InspectElement);
1659         connect(mDeveloperToolDialog,
1660                 &WebEngineViewer::DeveloperToolDialog::rejected,
1661                 mDeveloperToolDialog,
1662                 &WebEngineViewer::DeveloperToolDialog::deleteLater);
1663     }
1664     if (mDeveloperToolDialog->isHidden()) {
1665         mDeveloperToolDialog->show();
1666     }
1667 
1668     mDeveloperToolDialog->raise();
1669     mDeveloperToolDialog->activateWindow();
1670 }
1671 
showContextMenu(KMime::Content * content,const QPoint & pos)1672 void ViewerPrivate::showContextMenu(KMime::Content *content, const QPoint &pos)
1673 {
1674     if (!content) {
1675         return;
1676     }
1677 
1678     if (auto ct = content->contentType(false)) {
1679         if (ct->mimeType() == "text/x-moz-deleted") {
1680             return;
1681         }
1682     }
1683     const bool isAttachment = !content->contentType()->isMultipart() && !content->isTopLevel();
1684     const bool isExtraContent = !mMessage->content(content->index());
1685     const auto hasAttachments = KMime::hasAttachment(mMessage.data());
1686 
1687     QMenu popup;
1688 
1689     if (!content->isTopLevel()) {
1690         popup.addAction(QIcon::fromTheme(QStringLiteral("document-save-as")), i18n("Save &As..."), this, &ViewerPrivate::slotAttachmentSaveAs);
1691 
1692         if (isAttachment) {
1693             popup.addAction(QIcon::fromTheme(QStringLiteral("document-open")), i18nc("to open", "Open"), this, &ViewerPrivate::slotAttachmentOpen);
1694 
1695             if (selectedContents().count() == 1) {
1696                 createOpenWithMenu(&popup, QLatin1String(content->contentType()->mimeType()), false);
1697             } else {
1698                 popup.addAction(i18n("Open With..."), this, &ViewerPrivate::slotAttachmentOpenWith);
1699             }
1700             popup.addAction(i18nc("to view something", "View"), this, &ViewerPrivate::slotAttachmentView);
1701         }
1702     }
1703 
1704     if (hasAttachments) {
1705         popup.addAction(i18n("Save All Attachments..."), this, &ViewerPrivate::slotAttachmentSaveAll);
1706     }
1707 
1708     if (!content->isTopLevel()) {
1709         if (isAttachment) {
1710             popup.addAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy"), this, &ViewerPrivate::slotAttachmentCopy);
1711         }
1712 
1713         popup.addSeparator();
1714         auto deleteAction =
1715             popup.addAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18n("Delete Attachment"), this, &ViewerPrivate::slotAttachmentDelete);
1716         // body parts can only be deleted one at a time, and extra content cannot be delete
1717         deleteAction->setEnabled(selectedContents().size() == 1 && !isExtraContent);
1718 
1719         popup.addSeparator();
1720         popup.addAction(i18n("Properties"), this, &ViewerPrivate::slotAttachmentProperties);
1721     }
1722     popup.exec(mMimePartTree->viewport()->mapToGlobal(pos));
1723 }
1724 
actionForAttachmentStrategy(const AttachmentStrategy * as)1725 KToggleAction *ViewerPrivate::actionForAttachmentStrategy(const AttachmentStrategy *as)
1726 {
1727     if (!mActionCollection) {
1728         return nullptr;
1729     }
1730     QString actionName;
1731     if (as == AttachmentStrategy::iconic()) {
1732         actionName = QStringLiteral("view_attachments_as_icons");
1733     } else if (as == AttachmentStrategy::smart()) {
1734         actionName = QStringLiteral("view_attachments_smart");
1735     } else if (as == AttachmentStrategy::inlined()) {
1736         actionName = QStringLiteral("view_attachments_inline");
1737     } else if (as == AttachmentStrategy::hidden()) {
1738         actionName = QStringLiteral("view_attachments_hide");
1739     } else if (as == AttachmentStrategy::headerOnly()) {
1740         actionName = QStringLiteral("view_attachments_headeronly");
1741     } else {
1742         qCWarning(MESSAGEVIEWER_LOG) << "actionForAttachmentStrategy invalid attachment type";
1743     }
1744 
1745     if (actionName.isEmpty()) {
1746         return nullptr;
1747     } else {
1748         return static_cast<KToggleAction *>(mActionCollection->action(actionName));
1749     }
1750 }
1751 
readGlobalOverrideCodec()1752 void ViewerPrivate::readGlobalOverrideCodec()
1753 {
1754     // if the global character encoding wasn't changed then there's nothing to do
1755     if (MessageCore::MessageCoreSettings::self()->overrideCharacterEncoding() == mOldGlobalOverrideEncoding) {
1756         return;
1757     }
1758 
1759     setOverrideEncoding(MessageCore::MessageCoreSettings::self()->overrideCharacterEncoding());
1760     mOldGlobalOverrideEncoding = MessageCore::MessageCoreSettings::self()->overrideCharacterEncoding();
1761 }
1762 
overrideCodec() const1763 const QTextCodec *ViewerPrivate::overrideCodec() const
1764 {
1765     if (mOverrideEncoding.isEmpty() || mOverrideEncoding == QLatin1String("Auto")) { // Auto
1766         return nullptr;
1767     } else {
1768         return MessageViewer::Util::codecForName(mOverrideEncoding.toLatin1());
1769     }
1770 }
1771 
nextColor(const QColor & c)1772 static QColor nextColor(const QColor &c)
1773 {
1774     int h;
1775     int s;
1776     int v;
1777     c.getHsv(&h, &s, &v);
1778     return QColor::fromHsv((h + 50) % 360, qMax(s, 64), v);
1779 }
1780 
renderAttachments(KMime::Content * node,const QColor & bgColor) const1781 QString ViewerPrivate::renderAttachments(KMime::Content *node, const QColor &bgColor) const
1782 {
1783     if (!node) {
1784         return QString();
1785     }
1786 
1787     QString html;
1788     KMime::Content *child = MessageCore::NodeHelper::firstChild(node);
1789 
1790     if (child) {
1791         const QString subHtml = renderAttachments(child, nextColor(bgColor));
1792         if (!subHtml.isEmpty()) {
1793             QString margin;
1794             if (node != mMessage.data() || headerStylePlugin()->hasMargin()) {
1795                 margin = QStringLiteral("padding:2px; margin:2px; ");
1796             }
1797             const QString align = headerStylePlugin()->alignment();
1798             const QByteArray mediaTypeLower = node->contentType()->mediaType().toLower();
1799             const bool result = (mediaTypeLower == "message" || mediaTypeLower == "multipart" || node == mMessage.data());
1800             if (result) {
1801                 html += QStringLiteral(
1802                             "<div style=\"background:%1; %2"
1803                             "vertical-align:middle; float:%3;\">")
1804                             .arg(bgColor.name())
1805                             .arg(margin, align);
1806             }
1807             html += subHtml;
1808             if (result) {
1809                 html += QLatin1String("</div>");
1810             }
1811         }
1812     } else {
1813         Util::AttachmentDisplayInfo info = Util::attachmentDisplayInfo(node);
1814         if (info.displayInHeader) {
1815             html += QLatin1String("<div style=\"float:left;\">");
1816             html += QStringLiteral(
1817                         "<span style=\"white-space:nowrap; border-width: 0px; border-left-width: 5px; border-color: %1; 2px; border-left-style: solid;\">")
1818                         .arg(bgColor.name());
1819             mNodeHelper->writeNodeToTempFile(node);
1820             const QString href = mNodeHelper->asHREF(node, QStringLiteral("header"));
1821             html += QLatin1String("<a href=\"") + href + QLatin1String("\">");
1822             const QString imageMaxSize = QStringLiteral("width=\"16\" height=\"16\"");
1823 #if 0
1824             if (!info.icon.isEmpty()) {
1825                 QImage tmpImg(info.icon);
1826                 if (tmpImg.width() > 48 || tmpImg.height() > 48) {
1827                     imageMaxSize = QStringLiteral("width=\"48\" height=\"48\"");
1828                 }
1829             }
1830 #endif
1831             html += QStringLiteral("<img %1 style=\"vertical-align:middle;\" src=\"").arg(imageMaxSize) + info.icon + QLatin1String("\"/>&nbsp;");
1832             const int elidedTextSize = headerStylePlugin()->elidedTextSize();
1833             if (elidedTextSize == -1) {
1834                 html += info.label;
1835             } else {
1836                 QFont bodyFont = cssHelper()->bodyFont(mUseFixedFont);
1837                 QFontMetrics fm(bodyFont);
1838                 html += fm.elidedText(info.label, Qt::ElideRight, elidedTextSize);
1839             }
1840             html += QLatin1String("</a></span></div> ");
1841         }
1842     }
1843 
1844     for (KMime::Content *extraNode : mNodeHelper->extraContents(node)) {
1845         html += renderAttachments(extraNode, bgColor);
1846     }
1847 
1848     KMime::Content *next = MessageCore::NodeHelper::nextSibling(node);
1849     if (next) {
1850         html += renderAttachments(next, nextColor(bgColor));
1851     }
1852 
1853     return html;
1854 }
1855 
findContentByType(KMime::Content * content,const QByteArray & type)1856 KMime::Content *ViewerPrivate::findContentByType(KMime::Content *content, const QByteArray &type)
1857 {
1858     const auto list = content->contents();
1859     for (KMime::Content *c : list) {
1860         if (c->contentType()->mimeType() == type) {
1861             return c;
1862         }
1863     }
1864     return nullptr;
1865 }
1866 
1867 //-----------------------------------------------------------------------------
update(MimeTreeParser::UpdateMode updateMode)1868 void ViewerPrivate::update(MimeTreeParser::UpdateMode updateMode)
1869 {
1870     // Avoid flicker, somewhat of a cludge
1871     if (updateMode == MimeTreeParser::Force) {
1872         // stop the timer to avoid calling updateReaderWin twice
1873         mUpdateReaderWinTimer.stop();
1874         saveRelativePosition();
1875         updateReaderWin();
1876     } else if (mUpdateReaderWinTimer.isActive()) {
1877         mUpdateReaderWinTimer.setInterval(150ms);
1878     } else {
1879         mUpdateReaderWinTimer.start(0);
1880     }
1881 }
1882 
slotOpenUrl()1883 void ViewerPrivate::slotOpenUrl()
1884 {
1885     slotUrlOpen();
1886 }
1887 
slotUrlOpen(const QUrl & url)1888 void ViewerPrivate::slotUrlOpen(const QUrl &url)
1889 {
1890     if (!url.isEmpty()) {
1891         mClickedUrl = url;
1892     }
1893 
1894     // First, let's see if the URL handler manager can handle the URL. If not, try KRun for some
1895     // known URLs, otherwise fallback to emitting a signal.
1896     // That signal is caught by KMail, and in case of mailto URLs, a composer is shown.
1897 
1898     if (URLHandlerManager::instance()->handleClick(mClickedUrl, this)) {
1899         return;
1900     }
1901     Q_EMIT urlClicked(mMessageItem, mClickedUrl);
1902 }
1903 
checkPhishingUrl()1904 void ViewerPrivate::checkPhishingUrl()
1905 {
1906     if (MessageViewer::MessageViewerSettings::self()->checkPhishingUrl() && (mClickedUrl.scheme() != QLatin1String("mailto"))) {
1907         mPhishingDatabase->checkUrl(mClickedUrl);
1908     } else {
1909         executeRunner(mClickedUrl);
1910     }
1911 }
1912 
executeRunner(const QUrl & url)1913 void ViewerPrivate::executeRunner(const QUrl &url)
1914 {
1915     if (!MessageViewer::Util::handleUrlWithQDesktopServices(url)) {
1916         auto job = new KIO::OpenUrlJob(url);
1917         job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, viewer()));
1918         job->setRunExecutables(false);
1919         job->start();
1920     }
1921 }
1922 
slotCheckedUrlFinished(const QUrl & url,WebEngineViewer::CheckPhishingUrlUtil::UrlStatus status)1923 void ViewerPrivate::slotCheckedUrlFinished(const QUrl &url, WebEngineViewer::CheckPhishingUrlUtil::UrlStatus status)
1924 {
1925     switch (status) {
1926     case WebEngineViewer::CheckPhishingUrlUtil::BrokenNetwork:
1927         KMessageBox::error(mMainWindow, i18n("The network is broken."), i18n("Check Phishing URL"));
1928         break;
1929     case WebEngineViewer::CheckPhishingUrlUtil::InvalidUrl:
1930         KMessageBox::error(mMainWindow, i18n("The URL %1 is not valid.", url.toString()), i18n("Check Phishing URL"));
1931         break;
1932     case WebEngineViewer::CheckPhishingUrlUtil::Ok:
1933         break;
1934     case WebEngineViewer::CheckPhishingUrlUtil::MalWare:
1935         if (!urlIsAMalwareButContinue()) {
1936             return;
1937         }
1938         break;
1939     case WebEngineViewer::CheckPhishingUrlUtil::Unknown:
1940         qCWarning(MESSAGEVIEWER_LOG) << "WebEngineViewer::slotCheckedUrlFinished unknown error ";
1941         break;
1942     }
1943     executeRunner(url);
1944 }
1945 
urlIsAMalwareButContinue()1946 bool ViewerPrivate::urlIsAMalwareButContinue()
1947 {
1948     if (KMessageBox::No
1949         == KMessageBox::warningYesNo(mMainWindow,
1950                                      i18n("This web site is a malware, do you want to continue to show it?"),
1951                                      i18n("Malware"),
1952                                      KStandardGuiItem::cont(),
1953                                      KStandardGuiItem::cancel())) {
1954         return false;
1955     }
1956     return true;
1957 }
1958 
slotUrlOn(const QString & link)1959 void ViewerPrivate::slotUrlOn(const QString &link)
1960 {
1961     // The "link" we get here is not URL-encoded, and therefore there is no way QUrl could
1962     // parse it correctly. To workaround that, we use QWebFrame::hitTestContent() on the mouse position
1963     // to get the URL before WebKit managed to mangle it.
1964     QUrl url(link);
1965     const QString protocol = url.scheme();
1966     if (protocol == QLatin1String("kmail") || protocol == QLatin1String("x-kmail") || protocol == QLatin1String("attachment")
1967         || (protocol.isEmpty() && url.path().isEmpty())) {
1968         mViewer->setAcceptDrops(false);
1969     } else {
1970         mViewer->setAcceptDrops(true);
1971     }
1972 
1973     mViewer->setLinkHovered(url);
1974     if (link.trimmed().isEmpty()) {
1975         PimCommon::BroadcastStatus::instance()->reset();
1976         Q_EMIT showStatusBarMessage(QString());
1977         return;
1978     }
1979 
1980     QString msg = URLHandlerManager::instance()->statusBarMessage(url, this);
1981     if (msg.isEmpty()) {
1982         msg = link;
1983     }
1984 
1985     Q_EMIT showStatusBarMessage(msg);
1986 }
1987 
slotUrlPopup(const WebEngineViewer::WebHitTestResult & result)1988 void ViewerPrivate::slotUrlPopup(const WebEngineViewer::WebHitTestResult &result)
1989 {
1990     if (!mMsgDisplay) {
1991         return;
1992     }
1993     mClickedUrl = result.linkUrl();
1994     mImageUrl = result.imageUrl();
1995     const QPoint aPos = mViewer->mapToGlobal(result.pos());
1996     if (URLHandlerManager::instance()->handleContextMenuRequest(mClickedUrl, aPos, this)) {
1997         return;
1998     }
1999 
2000     if (!mActionCollection) {
2001         return;
2002     }
2003 
2004     if (mClickedUrl.scheme() == QLatin1String("mailto")) {
2005         mCopyURLAction->setText(i18n("Copy Email Address"));
2006     } else {
2007         mCopyURLAction->setText(i18n("Copy Link Address"));
2008     }
2009     Q_EMIT displayPopupMenu(mMessageItem, result, aPos);
2010     Q_EMIT popupMenu(mMessageItem, mClickedUrl, mImageUrl, aPos);
2011 }
2012 
slotLoadExternalReference()2013 void ViewerPrivate::slotLoadExternalReference()
2014 {
2015     if (mColorBar->isNormal() || htmlLoadExtOverride()) {
2016         return;
2017     }
2018     setHtmlLoadExtOverride(true);
2019     update(MimeTreeParser::Force);
2020 }
2021 
translateToDisplayFormat(MimeTreeParser::Util::HtmlMode mode)2022 Viewer::DisplayFormatMessage translateToDisplayFormat(MimeTreeParser::Util::HtmlMode mode)
2023 {
2024     switch (mode) {
2025     case MimeTreeParser::Util::Normal:
2026         return Viewer::Unknown;
2027     case MimeTreeParser::Util::Html:
2028         return Viewer::Html;
2029     case MimeTreeParser::Util::MultipartPlain:
2030         return Viewer::Text;
2031     case MimeTreeParser::Util::MultipartHtml:
2032         return Viewer::Html;
2033     case MimeTreeParser::Util::MultipartIcal:
2034         return Viewer::ICal;
2035     }
2036     return Viewer::Unknown;
2037 }
2038 
slotToggleHtmlMode()2039 void ViewerPrivate::slotToggleHtmlMode()
2040 {
2041     const auto availableModes = mColorBar->availableModes();
2042     const int availableModeSize(availableModes.size());
2043     if (mColorBar->isNormal() || availableModeSize < 2) {
2044         return;
2045     }
2046     mScamDetectionWarning->setVisible(false);
2047     const MimeTreeParser::Util::HtmlMode mode = mColorBar->mode();
2048     const int pos = (availableModes.indexOf(mode) + 1) % availableModeSize;
2049     setDisplayFormatMessageOverwrite(translateToDisplayFormat(availableModes[pos]));
2050     update(MimeTreeParser::Force);
2051     mColorBar->setAvailableModes(availableModes);
2052 }
2053 
slotFind()2054 void ViewerPrivate::slotFind()
2055 {
2056     if (mViewer->hasSelection()) {
2057         mFindBar->setText(mViewer->selectedText());
2058     }
2059     mSliderContainer->slideIn();
2060     mFindBar->focusAndSetCursor();
2061 }
2062 
slotToggleFixedFont()2063 void ViewerPrivate::slotToggleFixedFont()
2064 {
2065     mUseFixedFont = !mUseFixedFont;
2066     update(MimeTreeParser::Force);
2067 }
2068 
slotToggleMimePartTree()2069 void ViewerPrivate::slotToggleMimePartTree()
2070 {
2071     if (mToggleMimePartTreeAction->isChecked()) {
2072         MessageViewer::MessageViewerSettings::self()->setMimeTreeMode2(MessageViewer::MessageViewerSettings::EnumMimeTreeMode2::Always);
2073     } else {
2074         MessageViewer::MessageViewerSettings::self()->setMimeTreeMode2(MessageViewer::MessageViewerSettings::EnumMimeTreeMode2::Never);
2075     }
2076     showHideMimeTree();
2077 }
2078 
slotShowMessageSource()2079 void ViewerPrivate::slotShowMessageSource()
2080 {
2081     if (!mMessage) {
2082         return;
2083     }
2084     QPointer<MailSourceWebEngineViewer> viewer = new MailSourceWebEngineViewer; // deletes itself upon close
2085     mListMailSourceViewer.append(viewer);
2086     viewer->setWindowTitle(i18nc("@title:window", "Message as Plain Text"));
2087     const QString rawMessage = QString::fromLatin1(mMessage->encodedContent());
2088     viewer->setRawSource(rawMessage);
2089     viewer->setDisplayedSource(mViewer->page());
2090     if (mUseFixedFont) {
2091         viewer->setFixedFont();
2092     }
2093     viewer->show();
2094 }
2095 
updateReaderWin()2096 void ViewerPrivate::updateReaderWin()
2097 {
2098     if (!mMsgDisplay) {
2099         return;
2100     }
2101 
2102     if (mRecursionCountForDisplayMessage + 1 > 1) {
2103         // This recursion here can happen because the ObjectTreeParser in parseMsg() can exec() an
2104         // eventloop.
2105         // This happens in two cases:
2106         //   1) The ContactSearchJob started by FancyHeaderStyle::format
2107         //   2) Various modal passphrase dialogs for decryption of a message (bug 96498)
2108         //
2109         // While the exec() eventloop is running, it is possible that a timer calls updateReaderWin(),
2110         // and not aborting here would confuse the state terribly.
2111         qCWarning(MESSAGEVIEWER_LOG) << "Danger, recursion while displaying a message!";
2112         return;
2113     }
2114     mRecursionCountForDisplayMessage++;
2115 
2116     if (mViewer) {
2117         mViewer->setAllowExternalContent(htmlLoadExternal());
2118         htmlWriter()->reset();
2119         // TODO: if the item doesn't have the payload fetched, try to fetch it? Maybe not here, but in setMessageItem.
2120         if (mMessage) {
2121             mColorBar->show();
2122             displayMessage();
2123         } else if (mMessagePartNode) {
2124             setMessagePart(mMessagePartNode);
2125         } else {
2126             mColorBar->hide();
2127             mMimePartTree->hide();
2128             htmlWriter()->begin();
2129             htmlWriter()->write(cssHelper()->htmlHead(mUseFixedFont) + cssHelper()->endBodyHtml());
2130             htmlWriter()->end();
2131         }
2132     }
2133     mRecursionCountForDisplayMessage--;
2134 }
2135 
slotMimePartSelected(const QModelIndex & index)2136 void ViewerPrivate::slotMimePartSelected(const QModelIndex &index)
2137 {
2138     auto content = static_cast<KMime::Content *>(index.internalPointer());
2139     if (!mMimePartTree->mimePartModel()->parent(index).isValid() && index.row() == 0) {
2140         update(MimeTreeParser::Force);
2141     } else {
2142         setMessagePart(content);
2143     }
2144 }
2145 
slotIconicAttachments()2146 void ViewerPrivate::slotIconicAttachments()
2147 {
2148     setAttachmentStrategy(AttachmentStrategy::iconic());
2149 }
2150 
slotSmartAttachments()2151 void ViewerPrivate::slotSmartAttachments()
2152 {
2153     setAttachmentStrategy(AttachmentStrategy::smart());
2154 }
2155 
slotInlineAttachments()2156 void ViewerPrivate::slotInlineAttachments()
2157 {
2158     setAttachmentStrategy(AttachmentStrategy::inlined());
2159 }
2160 
slotHideAttachments()2161 void ViewerPrivate::slotHideAttachments()
2162 {
2163     setAttachmentStrategy(AttachmentStrategy::hidden());
2164 }
2165 
slotHeaderOnlyAttachments()2166 void ViewerPrivate::slotHeaderOnlyAttachments()
2167 {
2168     setAttachmentStrategy(AttachmentStrategy::headerOnly());
2169 }
2170 
attachmentView(KMime::Content * atmNode)2171 void ViewerPrivate::attachmentView(KMime::Content *atmNode)
2172 {
2173     if (atmNode) {
2174         const bool isEncapsulatedMessage = atmNode->parent() && atmNode->parent()->bodyIsMessage();
2175         if (isEncapsulatedMessage) {
2176             attachmentViewMessage(atmNode->parent()->bodyAsMessage());
2177         } else if ((qstricmp(atmNode->contentType()->mediaType().constData(), "text") == 0)
2178                    && ((qstricmp(atmNode->contentType()->subType().constData(), "x-vcard") == 0)
2179                        || (qstricmp(atmNode->contentType()->subType().constData(), "directory") == 0))) {
2180             setMessagePart(atmNode);
2181         } else {
2182             Q_EMIT showReader(atmNode, htmlMail(), overrideEncoding());
2183         }
2184     }
2185 }
2186 
slotDelayedResize()2187 void ViewerPrivate::slotDelayedResize()
2188 {
2189     mSplitter->setGeometry(0, 0, q->width(), q->height());
2190 }
2191 
slotPrintPreview()2192 void ViewerPrivate::slotPrintPreview()
2193 {
2194     disconnect(mPartHtmlWriter.data(), &WebEnginePartHtmlWriter::finished, this, &ViewerPrivate::slotPrintPreview);
2195     if (!mMessage) {
2196         return;
2197     }
2198     // Need to delay
2199     QTimer::singleShot(1s, this, &ViewerPrivate::slotDelayPrintPreview); // 1 second
2200 }
2201 
slotDelayPrintPreview()2202 void ViewerPrivate::slotDelayPrintPreview()
2203 {
2204     auto dialog = new QPrintPreviewDialog(q);
2205     dialog->setAttribute(Qt::WA_DeleteOnClose);
2206     dialog->resize(800, 750);
2207 
2208     connect(dialog, &QPrintPreviewDialog::paintRequested, this, [=](QPrinter *printing) {
2209         QApplication::setOverrideCursor(Qt::WaitCursor);
2210 
2211         if (!mViewer->execPrintPreviewPage(printing, 10000)) { // 10 seconds
2212             qCWarning(MESSAGEVIEWER_LOG) << " Impossible to generate preview";
2213         }
2214         QApplication::restoreOverrideCursor();
2215     });
2216 
2217     dialog->open(this, SIGNAL(printingFinished()));
2218 }
2219 
exportToPdf(const QString & fileName)2220 void ViewerPrivate::exportToPdf(const QString &fileName)
2221 {
2222     auto job = new WebEngineViewer::WebEngineExportPdfPageJob(this);
2223     connect(job, &WebEngineViewer::WebEngineExportPdfPageJob::exportToPdfSuccess, this, [this, fileName]() {
2224         showSavedFileFolderWidget({QUrl::fromLocalFile(fileName)}, MessageViewer::OpenSavedFileFolderWidget::FileType::Pdf);
2225     });
2226     job->setEngineView(mViewer);
2227     job->setPdfPath(fileName);
2228     job->start();
2229 }
2230 
slotOpenInBrowser()2231 void ViewerPrivate::slotOpenInBrowser()
2232 {
2233     auto job = new WebEngineViewer::WebEngineExportHtmlPageJob(this);
2234     job->setEngineView(mViewer);
2235     connect(job, &WebEngineViewer::WebEngineExportHtmlPageJob::failed, this, &ViewerPrivate::slotExportHtmlPageFailed);
2236     connect(job, &WebEngineViewer::WebEngineExportHtmlPageJob::success, this, &ViewerPrivate::slotExportHtmlPageSuccess);
2237     job->start();
2238 }
2239 
slotExportHtmlPageSuccess(const QString & filename)2240 void ViewerPrivate::slotExportHtmlPageSuccess(const QString &filename)
2241 {
2242     const QUrl url(QUrl::fromLocalFile(filename));
2243     auto job = new KIO::OpenUrlJob(url, QStringLiteral("text/html"), q);
2244     job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, q));
2245     job->setDeleteTemporaryFile(true);
2246     job->start();
2247 
2248     Q_EMIT printingFinished();
2249 }
2250 
slotExportHtmlPageFailed()2251 void ViewerPrivate::slotExportHtmlPageFailed()
2252 {
2253     qCDebug(MESSAGEVIEWER_LOG) << " Export HTML failed";
2254     Q_EMIT printingFinished();
2255 }
2256 
slotPrintMessage()2257 void ViewerPrivate::slotPrintMessage()
2258 {
2259     disconnect(mPartHtmlWriter.data(), &WebEnginePartHtmlWriter::finished, this, &ViewerPrivate::slotPrintMessage);
2260 
2261     if (!mMessage) {
2262         return;
2263     }
2264     if (mCurrentPrinter) {
2265         return;
2266     }
2267     mCurrentPrinter = new QPrinter();
2268     QPointer<QPrintDialog> dialog = new QPrintDialog(mCurrentPrinter, mMainWindow);
2269     dialog->setWindowTitle(i18nc("@title:window", "Print Document"));
2270     if (dialog->exec() != QDialog::Accepted) {
2271         slotHandlePagePrinted(false);
2272         delete dialog;
2273         return;
2274     }
2275     if (dialog->printer()->outputFormat() == QPrinter::PdfFormat) {
2276         connect(mViewer->page(), &QWebEnginePage::pdfPrintingFinished, this, &ViewerPrivate::slotPdfPrintingFinished);
2277         mViewer->page()->printToPdf(dialog->printer()->outputFileName(), dialog->printer()->pageLayout());
2278     } else {
2279         mViewer->page()->print(mCurrentPrinter, invoke(this, &ViewerPrivate::slotHandlePagePrinted));
2280     }
2281     delete dialog;
2282 }
2283 
slotPdfPrintingFinished(const QString & filePath,bool success)2284 void ViewerPrivate::slotPdfPrintingFinished(const QString &filePath, bool success)
2285 {
2286     Q_UNUSED(filePath)
2287     if (!success) {
2288         qCWarning(MESSAGEVIEWER_LOG) << "Print to pdf failed";
2289     }
2290     delete mCurrentPrinter;
2291     mCurrentPrinter = nullptr;
2292     Q_EMIT printingFinished();
2293 }
2294 
slotHandlePagePrinted(bool result)2295 void ViewerPrivate::slotHandlePagePrinted(bool result)
2296 {
2297     Q_UNUSED(result)
2298     delete mCurrentPrinter;
2299     mCurrentPrinter = nullptr;
2300     Q_EMIT printingFinished();
2301 }
2302 
slotSetEncoding()2303 void ViewerPrivate::slotSetEncoding()
2304 {
2305     if (mSelectEncodingAction) {
2306         if (mSelectEncodingAction->currentItem() == 0) { // Auto
2307             mOverrideEncoding.clear();
2308         } else {
2309             mOverrideEncoding = MimeTreeParser::NodeHelper::encodingForName(mSelectEncodingAction->currentText());
2310         }
2311         update(MimeTreeParser::Force);
2312     }
2313 }
2314 
headerStylePlugin() const2315 HeaderStylePlugin *ViewerPrivate::headerStylePlugin() const
2316 {
2317     return mHeaderStylePlugin;
2318 }
2319 
initializeColorFromScheme()2320 void ViewerPrivate::initializeColorFromScheme()
2321 {
2322     if (!mForegroundError.isValid()) {
2323         const KColorScheme scheme = KColorScheme(QPalette::Active, KColorScheme::View);
2324         mForegroundError = scheme.foreground(KColorScheme::NegativeText).color();
2325         mBackgroundError = scheme.background(KColorScheme::NegativeBackground).color();
2326         mBackgroundAttachment = scheme.background().color();
2327     }
2328 }
2329 
attachmentHtml()2330 QString ViewerPrivate::attachmentHtml()
2331 {
2332     initializeColorFromScheme();
2333     QString html = renderAttachments(mMessage.data(), mBackgroundAttachment);
2334     if (!html.isEmpty()) {
2335         html.prepend(headerStylePlugin()->attachmentHtml());
2336     }
2337     return html;
2338 }
2339 
executeCustomScriptsAfterLoading()2340 void ViewerPrivate::executeCustomScriptsAfterLoading()
2341 {
2342     disconnect(mViewer, &MailWebEngineView::loadFinished, this, &ViewerPrivate::executeCustomScriptsAfterLoading);
2343     // inject attachments in header view
2344     // we have to do that after the otp has run so we also see encrypted parts
2345 
2346     mViewer->scrollToRelativePosition(mViewer->relativePosition());
2347     mViewer->clearRelativePosition();
2348 }
2349 
slotSettingsChanged()2350 void ViewerPrivate::slotSettingsChanged()
2351 {
2352     update(MimeTreeParser::Force);
2353 }
2354 
slotMimeTreeContextMenuRequested(const QPoint & pos)2355 void ViewerPrivate::slotMimeTreeContextMenuRequested(const QPoint &pos)
2356 {
2357     QModelIndex index = mMimePartTree->indexAt(pos);
2358     if (index.isValid()) {
2359         auto content = static_cast<KMime::Content *>(index.internalPointer());
2360         showContextMenu(content, pos);
2361     }
2362 }
2363 
slotAttachmentOpenWith()2364 void ViewerPrivate::slotAttachmentOpenWith()
2365 {
2366     QItemSelectionModel *selectionModel = mMimePartTree->selectionModel();
2367     const QModelIndexList selectedRows = selectionModel->selectedRows();
2368 
2369     for (const QModelIndex &index : selectedRows) {
2370         auto content = static_cast<KMime::Content *>(index.internalPointer());
2371         attachmentOpenWith(content);
2372     }
2373 }
2374 
slotAttachmentOpen()2375 void ViewerPrivate::slotAttachmentOpen()
2376 {
2377     QItemSelectionModel *selectionModel = mMimePartTree->selectionModel();
2378     const QModelIndexList selectedRows = selectionModel->selectedRows();
2379 
2380     for (const QModelIndex &index : selectedRows) {
2381         auto content = static_cast<KMime::Content *>(index.internalPointer());
2382         attachmentOpen(content);
2383     }
2384 }
2385 
showSavedFileFolderWidget(const QList<QUrl> & urls,OpenSavedFileFolderWidget::FileType fileType)2386 void ViewerPrivate::showSavedFileFolderWidget(const QList<QUrl> &urls, OpenSavedFileFolderWidget::FileType fileType)
2387 {
2388     mOpenSavedFileFolderWidget->setUrls(urls, fileType);
2389     mOpenSavedFileFolderWidget->slotShowWarning();
2390 }
2391 
mimePartTreeIsEmpty() const2392 bool ViewerPrivate::mimePartTreeIsEmpty() const
2393 {
2394     return mMimePartTree->model()->rowCount() == 0;
2395 }
2396 
setPluginName(const QString & pluginName)2397 void ViewerPrivate::setPluginName(const QString &pluginName)
2398 {
2399     mHeaderStyleMenuManager->setPluginName(pluginName);
2400 }
2401 
viewerPluginActionList(ViewerPluginInterface::SpecificFeatureTypes features)2402 QList<QAction *> ViewerPrivate::viewerPluginActionList(ViewerPluginInterface::SpecificFeatureTypes features)
2403 {
2404     if (mViewerPluginToolManager) {
2405         return mViewerPluginToolManager->viewerPluginActionList(features);
2406     }
2407     return QList<QAction *>();
2408 }
2409 
slotActivatePlugin(ViewerPluginInterface * interface)2410 void ViewerPrivate::slotActivatePlugin(ViewerPluginInterface *interface)
2411 {
2412     interface->setMessage(mMessage);
2413     interface->setMessageItem(mMessageItem);
2414     interface->setUrl(mClickedUrl);
2415     interface->setCurrentCollection(mMessageItem.parentCollection());
2416     const QString text = mViewer->selectedText();
2417     if (!text.isEmpty()) {
2418         interface->setText(text);
2419     }
2420     interface->execute();
2421 }
2422 
slotAttachmentSaveAs()2423 void ViewerPrivate::slotAttachmentSaveAs()
2424 {
2425     const auto contents = selectedContents();
2426     QList<QUrl> urlList;
2427     if (Util::saveAttachments(contents, mMainWindow, urlList)) {
2428         showSavedFileFolderWidget(urlList, MessageViewer::OpenSavedFileFolderWidget::FileType::Attachment);
2429     }
2430 }
2431 
slotAttachmentSaveAll()2432 void ViewerPrivate::slotAttachmentSaveAll()
2433 {
2434     const auto contents = mMessage->attachments();
2435     QList<QUrl> urlList;
2436     if (Util::saveAttachments(contents, mMainWindow, urlList)) {
2437         showSavedFileFolderWidget(urlList, MessageViewer::OpenSavedFileFolderWidget::FileType::Attachment);
2438     }
2439 }
2440 
slotAttachmentView()2441 void ViewerPrivate::slotAttachmentView()
2442 {
2443     const auto contents = selectedContents();
2444 
2445     for (KMime::Content *content : contents) {
2446         attachmentView(content);
2447     }
2448 }
2449 
slotAttachmentProperties()2450 void ViewerPrivate::slotAttachmentProperties()
2451 {
2452     const auto contents = selectedContents();
2453 
2454     for (KMime::Content *content : contents) {
2455         attachmentProperties(content);
2456     }
2457 }
2458 
attachmentProperties(KMime::Content * content)2459 void ViewerPrivate::attachmentProperties(KMime::Content *content)
2460 {
2461     auto dialog = new MessageCore::AttachmentPropertiesDialog(content, mMainWindow);
2462     dialog->setAttribute(Qt::WA_DeleteOnClose);
2463     dialog->show();
2464 }
2465 
slotAttachmentCopy()2466 void ViewerPrivate::slotAttachmentCopy()
2467 {
2468 #ifndef QT_NO_CLIPBOARD
2469     attachmentCopy(selectedContents());
2470 #endif
2471 }
2472 
attachmentCopy(const KMime::Content::List & contents)2473 void ViewerPrivate::attachmentCopy(const KMime::Content::List &contents)
2474 {
2475 #ifndef QT_NO_CLIPBOARD
2476     if (contents.isEmpty()) {
2477         return;
2478     }
2479 
2480     QList<QUrl> urls;
2481     for (KMime::Content *content : contents) {
2482         auto url = QUrl::fromLocalFile(mNodeHelper->writeNodeToTempFile(content));
2483         if (!url.isValid()) {
2484             continue;
2485         }
2486         urls.append(url);
2487     }
2488 
2489     if (urls.isEmpty()) {
2490         return;
2491     }
2492 
2493     auto mimeData = new QMimeData;
2494     mimeData->setUrls(urls);
2495     QApplication::clipboard()->setMimeData(mimeData, QClipboard::Clipboard);
2496 #endif
2497 }
2498 
slotAttachmentDelete()2499 void ViewerPrivate::slotAttachmentDelete()
2500 {
2501     const auto contents = selectedContents();
2502     if (contents.size() != 1) {
2503         return;
2504     }
2505     // look up the selected content node of the mime part tree in the node tree of the original message;
2506     // since deleting extra content (e.g. attachments inside encrypted message parts) is not supported,
2507     // we do not need to consider the extra content in the lookup
2508     const auto contentIndex = contents[0]->index();
2509     const auto contentInOriginalMessage = mMessage->content(contentIndex);
2510     if (contentInOriginalMessage) {
2511         (void)deleteAttachment(contentInOriginalMessage);
2512     }
2513 }
2514 
slotLevelQuote(int l)2515 void ViewerPrivate::slotLevelQuote(int l)
2516 {
2517     if (mLevelQuote != l) {
2518         mLevelQuote = l;
2519         update(MimeTreeParser::Force);
2520     }
2521 }
2522 
slotHandleAttachment(int choice)2523 void ViewerPrivate::slotHandleAttachment(int choice)
2524 {
2525     if (!mCurrentContent) {
2526         return;
2527     }
2528     switch (choice) {
2529     case Viewer::Delete:
2530         if (!deleteAttachment(mCurrentContent)) {
2531             qCWarning(MESSAGEVIEWER_LOG) << "Impossible to delete attachment";
2532         }
2533         break;
2534     case Viewer::Properties:
2535         attachmentProperties(mCurrentContent);
2536         break;
2537     case Viewer::Save: {
2538         const bool isEncapsulatedMessage = mCurrentContent->parent() && mCurrentContent->parent()->bodyIsMessage();
2539         if (isEncapsulatedMessage) {
2540             KMime::Message::Ptr message(new KMime::Message);
2541             message->setContent(mCurrentContent->parent()->bodyAsMessage()->encodedContent());
2542             message->parse();
2543             Akonadi::Item item;
2544             item.setPayload<KMime::Message::Ptr>(message);
2545             Akonadi::MessageFlags::copyMessageFlags(*message, item);
2546             item.setMimeType(KMime::Message::mimeType());
2547             QUrl url;
2548             if (MessageViewer::Util::saveMessageInMboxAndGetUrl(url, Akonadi::Item::List() << item, mMainWindow)) {
2549                 showSavedFileFolderWidget({url}, MessageViewer::OpenSavedFileFolderWidget::FileType::Attachment);
2550             }
2551         } else {
2552             QList<QUrl> urlList;
2553             if (Util::saveContents(mMainWindow, KMime::Content::List() << mCurrentContent, urlList)) {
2554                 showSavedFileFolderWidget(urlList, MessageViewer::OpenSavedFileFolderWidget::FileType::Attachment);
2555             }
2556         }
2557         break;
2558     }
2559     case Viewer::OpenWith:
2560         attachmentOpenWith(mCurrentContent);
2561         break;
2562     case Viewer::Open:
2563         attachmentOpen(mCurrentContent);
2564         break;
2565     case Viewer::View:
2566         attachmentView(mCurrentContent);
2567         break;
2568     case Viewer::Copy:
2569         attachmentCopy(KMime::Content::List() << mCurrentContent);
2570         break;
2571     case Viewer::ScrollTo:
2572         scrollToAttachment(mCurrentContent);
2573         break;
2574     case Viewer::ReplyMessageToAuthor:
2575         replyMessageToAuthor(mCurrentContent);
2576         break;
2577     case Viewer::ReplyMessageToAll:
2578         replyMessageToAll(mCurrentContent);
2579         break;
2580     }
2581 }
2582 
replyMessageToAuthor(KMime::Content * atmNode)2583 void ViewerPrivate::replyMessageToAuthor(KMime::Content *atmNode)
2584 {
2585     replyMessage(atmNode, false);
2586 }
2587 
replyMessageToAll(KMime::Content * atmNode)2588 void ViewerPrivate::replyMessageToAll(KMime::Content *atmNode)
2589 {
2590     replyMessage(atmNode, true);
2591 }
2592 
replyMessage(KMime::Content * atmNode,bool replyToAll)2593 void ViewerPrivate::replyMessage(KMime::Content *atmNode, bool replyToAll)
2594 {
2595     if (atmNode) {
2596         const bool isEncapsulatedMessage = atmNode->parent() && atmNode->parent()->bodyIsMessage();
2597         if (isEncapsulatedMessage) {
2598             Q_EMIT replyMessageTo(atmNode->parent()->bodyAsMessage(), replyToAll);
2599         }
2600     }
2601 }
2602 
slotSpeakText()2603 void ViewerPrivate::slotSpeakText()
2604 {
2605     const QString text = mViewer->selectedText();
2606     if (!text.isEmpty()) {
2607         mTextToSpeechWidget->say(text);
2608     }
2609 }
2610 
imageUrl() const2611 QUrl ViewerPrivate::imageUrl() const
2612 {
2613     QUrl url;
2614     if (mImageUrl.scheme() == QLatin1String("cid")) {
2615         url = QUrl(MessageViewer::WebEngineEmbedPart::self()->contentUrl(mImageUrl.path()));
2616     } else {
2617         url = mImageUrl;
2618     }
2619     return url;
2620 }
2621 
slotCopyImageLocation()2622 void ViewerPrivate::slotCopyImageLocation()
2623 {
2624 #ifndef QT_NO_CLIPBOARD
2625     QApplication::clipboard()->setText(imageUrl().url());
2626 #endif
2627 }
2628 
slotCopySelectedText()2629 void ViewerPrivate::slotCopySelectedText()
2630 {
2631     mViewer->triggerPageAction(QWebEnginePage::Copy);
2632 }
2633 
viewerSelectionChanged()2634 void ViewerPrivate::viewerSelectionChanged()
2635 {
2636     mActionCollection->action(QStringLiteral("kmail_copy"))->setEnabled(!mViewer->selectedText().isEmpty());
2637 }
2638 
selectAll()2639 void ViewerPrivate::selectAll()
2640 {
2641     mViewer->selectAll();
2642 }
2643 
slotUrlCopy()2644 void ViewerPrivate::slotUrlCopy()
2645 {
2646 #ifndef QT_NO_CLIPBOARD
2647     QClipboard *clip = QApplication::clipboard();
2648     if (mClickedUrl.scheme() == QLatin1String("mailto")) {
2649         // put the url into the mouse selection and the clipboard
2650         const QString address = KEmailAddress::decodeMailtoUrl(mClickedUrl);
2651         clip->setText(address, QClipboard::Clipboard);
2652         clip->setText(address, QClipboard::Selection);
2653         PimCommon::BroadcastStatus::instance()->setStatusMsg(i18n("Address copied to clipboard."));
2654     } else {
2655         // put the url into the mouse selection and the clipboard
2656         const QString clickedUrl = mClickedUrl.url();
2657         clip->setText(clickedUrl, QClipboard::Clipboard);
2658         clip->setText(clickedUrl, QClipboard::Selection);
2659         PimCommon::BroadcastStatus::instance()->setStatusMsg(i18n("URL copied to clipboard."));
2660     }
2661 #endif
2662 }
2663 
slotSaveMessage()2664 void ViewerPrivate::slotSaveMessage()
2665 {
2666     if (!mMessageItem.hasPayload<KMime::Message::Ptr>()) {
2667         if (mMessageItem.isValid()) {
2668             qCWarning(MESSAGEVIEWER_LOG) << "Payload is not a MessagePtr!";
2669         }
2670         return;
2671     }
2672 
2673     if (!Util::saveMessageInMbox(Akonadi::Item::List() << mMessageItem, mMainWindow)) {
2674         qCWarning(MESSAGEVIEWER_LOG) << "Impossible to save as mbox";
2675     }
2676 }
2677 
saveRelativePosition()2678 void ViewerPrivate::saveRelativePosition()
2679 {
2680     if (mViewer) {
2681         mViewer->saveRelativePosition();
2682     }
2683 }
2684 
htmlMail() const2685 bool ViewerPrivate::htmlMail() const
2686 {
2687     if (mDisplayFormatMessageOverwrite == Viewer::UseGlobalSetting) {
2688         return mHtmlMailGlobalSetting;
2689     } else {
2690         return mDisplayFormatMessageOverwrite == Viewer::Html;
2691     }
2692 }
2693 
htmlLoadExternal() const2694 bool ViewerPrivate::htmlLoadExternal() const
2695 {
2696     if (!mNodeHelper || !mMessage) {
2697         return mHtmlLoadExtOverride;
2698     }
2699 
2700     // when displaying an encrypted message, only load external resources on explicit request
2701     if (mNodeHelper->overallEncryptionState(mMessage.data()) != MimeTreeParser::KMMsgNotEncrypted) {
2702         return mHtmlLoadExtOverride;
2703     }
2704 
2705     const bool loadExternal = (mHtmlLoadExternalDefaultSetting && !mHtmlLoadExtOverride) || (!mHtmlLoadExternalDefaultSetting && mHtmlLoadExtOverride);
2706 
2707     return loadExternal;
2708 }
2709 
setDisplayFormatMessageOverwrite(Viewer::DisplayFormatMessage format)2710 void ViewerPrivate::setDisplayFormatMessageOverwrite(Viewer::DisplayFormatMessage format)
2711 {
2712     if (mDisplayFormatMessageOverwrite != format) {
2713         mDisplayFormatMessageOverwrite = format;
2714         // keep toggle display mode action state in sync.
2715         if (mToggleDisplayModeAction) {
2716             mToggleDisplayModeAction->setChecked(htmlMail());
2717         }
2718     }
2719 }
2720 
htmlMailGlobalSetting() const2721 bool ViewerPrivate::htmlMailGlobalSetting() const
2722 {
2723     return mHtmlMailGlobalSetting;
2724 }
2725 
displayFormatMessageOverwrite() const2726 Viewer::DisplayFormatMessage ViewerPrivate::displayFormatMessageOverwrite() const
2727 {
2728     return mDisplayFormatMessageOverwrite;
2729 }
2730 
setHtmlLoadExtDefault(bool loadExtDefault)2731 void ViewerPrivate::setHtmlLoadExtDefault(bool loadExtDefault)
2732 {
2733     mHtmlLoadExternalDefaultSetting = loadExtDefault;
2734 }
2735 
setHtmlLoadExtOverride(bool loadExtOverride)2736 void ViewerPrivate::setHtmlLoadExtOverride(bool loadExtOverride)
2737 {
2738     mHtmlLoadExtOverride = loadExtOverride;
2739 }
2740 
htmlLoadExtOverride() const2741 bool ViewerPrivate::htmlLoadExtOverride() const
2742 {
2743     return mHtmlLoadExtOverride;
2744 }
2745 
setDecryptMessageOverwrite(bool overwrite)2746 void ViewerPrivate::setDecryptMessageOverwrite(bool overwrite)
2747 {
2748     mDecrytMessageOverwrite = overwrite;
2749 }
2750 
showSignatureDetails() const2751 bool ViewerPrivate::showSignatureDetails() const
2752 {
2753     return mShowSignatureDetails;
2754 }
2755 
setShowSignatureDetails(bool showDetails)2756 void ViewerPrivate::setShowSignatureDetails(bool showDetails)
2757 {
2758     mShowSignatureDetails = showDetails;
2759 }
2760 
setShowEncryptionDetails(bool showEncDetails)2761 void ViewerPrivate::setShowEncryptionDetails(bool showEncDetails)
2762 {
2763     mShowEncryptionDetails = showEncDetails;
2764 }
2765 
showEncryptionDetails() const2766 bool ViewerPrivate::showEncryptionDetails() const
2767 {
2768     return mShowEncryptionDetails;
2769 }
2770 
scrollToAttachment(KMime::Content * node)2771 void ViewerPrivate::scrollToAttachment(KMime::Content *node)
2772 {
2773     const QString indexStr = node->index().toString();
2774     // The anchors for this are created in ObjectTreeParser::parseObjectTree()
2775     mViewer->scrollToAnchor(QLatin1String("attachmentDiv") + indexStr);
2776 
2777     // Remove any old color markings which might be there
2778     const KMime::Content *root = node->topLevel();
2779     const int totalChildCount = Util::allContents(root).size();
2780     for (int i = 0; i < totalChildCount + 1; ++i) {
2781         // Not optimal I need to optimize it. But for the moment it removes yellow mark
2782         mViewer->removeAttachmentMarking(QStringLiteral("attachmentDiv%1").arg(i + 1));
2783         mViewer->removeAttachmentMarking(QStringLiteral("attachmentDiv1.%1").arg(i + 1));
2784         mViewer->removeAttachmentMarking(QStringLiteral("attachmentDiv2.%1").arg(i + 1));
2785     }
2786 
2787     // Don't mark hidden nodes, that would just produce a strange yellow line
2788     if (mNodeHelper->isNodeDisplayedHidden(node)) {
2789         return;
2790     }
2791 
2792     // Now, color the div of the attachment in yellow, so that the user sees what happened.
2793     // We created a special marked div for this in writeAttachmentMarkHeader() in ObjectTreeParser,
2794     // find and modify that now.
2795     mViewer->markAttachment(QLatin1String("attachmentDiv") + indexStr, QStringLiteral("border:2px solid %1").arg(cssHelper()->pgpWarnColor().name()));
2796 }
2797 
setUseFixedFont(bool useFixedFont)2798 void ViewerPrivate::setUseFixedFont(bool useFixedFont)
2799 {
2800     if (mUseFixedFont != useFixedFont) {
2801         mUseFixedFont = useFixedFont;
2802         if (mToggleFixFontAction) {
2803             mToggleFixFontAction->setChecked(mUseFixedFont);
2804         }
2805     }
2806 }
2807 
itemFetchResult(KJob * job)2808 void ViewerPrivate::itemFetchResult(KJob *job)
2809 {
2810     if (job->error()) {
2811         displaySplashPage(i18n("Message loading failed: %1.", job->errorText()));
2812     } else {
2813         auto fetch = qobject_cast<Akonadi::ItemFetchJob *>(job);
2814         Q_ASSERT(fetch);
2815         if (fetch->items().isEmpty()) {
2816             displaySplashPage(i18n("Message not found."));
2817         } else {
2818             setMessageItem(fetch->items().constFirst());
2819         }
2820     }
2821 }
2822 
slotItemChanged(const Akonadi::Item & item,const QSet<QByteArray> & parts)2823 void ViewerPrivate::slotItemChanged(const Akonadi::Item &item, const QSet<QByteArray> &parts)
2824 {
2825     if (item.id() != messageItem().id()) {
2826         qCDebug(MESSAGEVIEWER_LOG) << "Update for an already forgotten item. Weird.";
2827         return;
2828     }
2829     if (parts.contains("PLD:RFC822")) {
2830         setMessageItem(item, MimeTreeParser::Force);
2831     }
2832 }
2833 
slotItemMoved(const Akonadi::Item & item,const Akonadi::Collection &,const Akonadi::Collection &)2834 void ViewerPrivate::slotItemMoved(const Akonadi::Item &item, const Akonadi::Collection &, const Akonadi::Collection &)
2835 {
2836     // clear the view after the current item has been moved somewhere else (e.g. to trash)
2837     if (item.id() == messageItem().id()) {
2838         slotClear();
2839     }
2840 }
2841 
slotClear()2842 void ViewerPrivate::slotClear()
2843 {
2844     q->clear(MimeTreeParser::Force);
2845     Q_EMIT itemRemoved();
2846 }
2847 
slotMessageRendered()2848 void ViewerPrivate::slotMessageRendered()
2849 {
2850     if (!mMessageItem.isValid()) {
2851         return;
2852     }
2853 
2854     /**
2855      * This slot might be called multiple times for the same message if
2856      * some asynchronous mementos are involved in rendering. Therefore we
2857      * have to make sure we execute the MessageLoadedHandlers only once.
2858      */
2859     if (mMessageItem.id() == mPreviouslyViewedItemId) {
2860         return;
2861     }
2862 
2863     mPreviouslyViewedItemId = mMessageItem.id();
2864 
2865     for (AbstractMessageLoadedHandler *handler : std::as_const(mMessageLoadedHandlers)) {
2866         handler->setItem(mMessageItem);
2867     }
2868 }
2869 
setZoomFactor(qreal zoomFactor)2870 void ViewerPrivate::setZoomFactor(qreal zoomFactor)
2871 {
2872     mZoomActionMenu->setWebViewerZoomFactor(zoomFactor);
2873 }
2874 
goOnline()2875 void ViewerPrivate::goOnline()
2876 {
2877     Q_EMIT makeResourceOnline(Viewer::AllResources);
2878 }
2879 
goResourceOnline()2880 void ViewerPrivate::goResourceOnline()
2881 {
2882     Q_EMIT makeResourceOnline(Viewer::SelectedResource);
2883 }
2884 
slotSaveMessageDisplayFormat()2885 void ViewerPrivate::slotSaveMessageDisplayFormat()
2886 {
2887     if (mMessageItem.isValid()) {
2888         auto job = new MessageViewer::ModifyMessageDisplayFormatJob(mSession, this);
2889         job->setMessageFormat(displayFormatMessageOverwrite());
2890         job->setMessageItem(mMessageItem);
2891         job->setRemoteContent(htmlLoadExtOverride());
2892         job->start();
2893     }
2894 }
2895 
slotResetMessageDisplayFormat()2896 void ViewerPrivate::slotResetMessageDisplayFormat()
2897 {
2898     if (mMessageItem.isValid()) {
2899         if (mMessageItem.hasAttribute<MessageViewer::MessageDisplayFormatAttribute>()) {
2900             auto job = new MessageViewer::ModifyMessageDisplayFormatJob(mSession, this);
2901             job->setMessageItem(mMessageItem);
2902             job->setResetFormat(true);
2903             job->start();
2904         }
2905     }
2906 }
2907 
slotMessageMayBeAScam()2908 void ViewerPrivate::slotMessageMayBeAScam()
2909 {
2910     if (mMessageItem.isValid()) {
2911         if (mMessageItem.hasAttribute<MessageViewer::ScamAttribute>()) {
2912             const MessageViewer::ScamAttribute *const attr = mMessageItem.attribute<MessageViewer::ScamAttribute>();
2913             if (attr && !attr->isAScam()) {
2914                 return;
2915             }
2916         }
2917         if (mMessageItem.hasPayload<KMime::Message::Ptr>()) {
2918             auto message = mMessageItem.payload<KMime::Message::Ptr>();
2919             const QString email = QLatin1String(KEmailAddress::firstEmailAddress(message->from()->as7BitString(false)));
2920             const QStringList lst = MessageViewer::MessageViewerSettings::self()->scamDetectionWhiteList();
2921             if (lst.contains(email)) {
2922                 return;
2923             }
2924         }
2925     }
2926     mScamDetectionWarning->slotShowWarning();
2927 }
2928 
slotMessageIsNotAScam()2929 void ViewerPrivate::slotMessageIsNotAScam()
2930 {
2931     if (mMessageItem.isValid()) {
2932         auto attr = mMessageItem.attribute<MessageViewer::ScamAttribute>(Akonadi::Item::AddIfMissing);
2933         attr->setIsAScam(false);
2934         auto modify = new Akonadi::ItemModifyJob(mMessageItem, mSession);
2935         modify->setIgnorePayload(true);
2936         modify->disableRevisionCheck();
2937         connect(modify, &KJob::result, this, &ViewerPrivate::slotModifyItemDone);
2938     }
2939 }
2940 
slotModifyItemDone(KJob * job)2941 void ViewerPrivate::slotModifyItemDone(KJob *job)
2942 {
2943     if (job && job->error()) {
2944         qCWarning(MESSAGEVIEWER_LOG) << " Error trying to change attribute:" << job->errorText();
2945     }
2946 }
2947 
saveMainFrameScreenshotInFile(const QString & filename)2948 void ViewerPrivate::saveMainFrameScreenshotInFile(const QString &filename)
2949 {
2950     mViewer->saveMainFrameScreenshotInFile(filename);
2951 }
2952 
slotAddToWhiteList()2953 void ViewerPrivate::slotAddToWhiteList()
2954 {
2955     if (mMessageItem.isValid()) {
2956         if (mMessageItem.hasPayload<KMime::Message::Ptr>()) {
2957             auto message = mMessageItem.payload<KMime::Message::Ptr>();
2958             const QString email = QLatin1String(KEmailAddress::firstEmailAddress(message->from()->as7BitString(false)));
2959             QStringList lst = MessageViewer::MessageViewerSettings::self()->scamDetectionWhiteList();
2960             if (lst.contains(email)) {
2961                 return;
2962             }
2963             lst << email;
2964             MessageViewer::MessageViewerSettings::self()->setScamDetectionWhiteList(lst);
2965             MessageViewer::MessageViewerSettings::self()->save();
2966         }
2967     }
2968 }
2969 
slotRefreshMessage(const Akonadi::Item & item)2970 void ViewerPrivate::slotRefreshMessage(const Akonadi::Item &item)
2971 {
2972     if (item.id() == mMessageItem.id()) {
2973         setMessageItem(item, MimeTreeParser::Force);
2974     }
2975 }
2976 
slotServiceUrlSelected(PimCommon::ShareServiceUrlManager::ServiceType serviceType)2977 void ViewerPrivate::slotServiceUrlSelected(PimCommon::ShareServiceUrlManager::ServiceType serviceType)
2978 {
2979     const QUrl url = mShareServiceManager->generateServiceUrl(mClickedUrl.toString(), QString(), serviceType);
2980     mShareServiceManager->openUrl(url);
2981 }
2982 
interceptorUrlActions(const WebEngineViewer::WebHitTestResult & result) const2983 QList<QAction *> ViewerPrivate::interceptorUrlActions(const WebEngineViewer::WebHitTestResult &result) const
2984 {
2985     return mViewer->interceptorUrlActions(result);
2986 }
2987 
setPrintElementBackground(bool printElementBackground)2988 void ViewerPrivate::setPrintElementBackground(bool printElementBackground)
2989 {
2990     mViewer->setPrintElementBackground(printElementBackground);
2991 }
2992 
slotToggleEmoticons()2993 void ViewerPrivate::slotToggleEmoticons()
2994 {
2995     mForceEmoticons = !mForceEmoticons;
2996     // Save value
2997     MessageViewer::MessageViewerSettings::self()->setShowEmoticons(mForceEmoticons);
2998     headerStylePlugin()->headerStyle()->setShowEmoticons(mForceEmoticons);
2999     update(MimeTreeParser::Force);
3000 }
3001 
slotZoomChanged(qreal zoom)3002 void ViewerPrivate::slotZoomChanged(qreal zoom)
3003 {
3004     mViewer->slotZoomChanged(zoom);
3005     const qreal zoomFactor = zoom * 100;
3006     MessageViewer::MessageViewerSettings::self()->setZoomFactor(zoomFactor);
3007     Q_EMIT zoomChanged(zoomFactor);
3008 }
3009 
updateShowMultiMessagesButton(bool enablePreviousButton,bool enableNextButton)3010 void ViewerPrivate::updateShowMultiMessagesButton(bool enablePreviousButton, bool enableNextButton)
3011 {
3012     mShowNextMessageWidget->updateButton(enablePreviousButton, enableNextButton);
3013 }
3014 
dkimViewerMenu()3015 DKIMViewerMenu *ViewerPrivate::dkimViewerMenu()
3016 {
3017     if (MessageViewer::MessageViewerSettings::self()->enabledDkim()) {
3018         if (!messageIsInSpecialFolder()) {
3019             if (!mDkimViewerMenu) {
3020                 mDkimViewerMenu = new MessageViewer::DKIMViewerMenu(this);
3021                 connect(mDkimViewerMenu, &DKIMViewerMenu::recheckSignature, this, [this]() {
3022                     MessageViewer::DKIMManager::self()->checkDKim(mMessageItem);
3023                 });
3024                 connect(mDkimViewerMenu, &DKIMViewerMenu::updateDkimKey, this, []() {
3025                     qWarning() << " Unimplemented yet updateDkimKey";
3026                 });
3027                 connect(mDkimViewerMenu, &DKIMViewerMenu::showDkimRules, this, [this]() {
3028                     QPointer<DKIMManageRulesDialog> dlg = new DKIMManageRulesDialog(viewer());
3029                     dlg->exec();
3030                     delete dlg;
3031                 });
3032             }
3033             mDkimViewerMenu->setEnableUpdateDkimKeyMenu(MessageViewer::MessageViewerSettings::saveKey()
3034                                                         == MessageViewer::MessageViewerSettings::EnumSaveKey::Save);
3035             return mDkimViewerMenu;
3036         }
3037     }
3038     return nullptr;
3039 }
3040 
isAutocryptEnabled(KMime::Message * message)3041 bool ViewerPrivate::isAutocryptEnabled(KMime::Message *message)
3042 {
3043     if (!mIdentityManager) {
3044         return false;
3045     }
3046 
3047     const auto id = MessageCore::Util::identityForMessage(message, mIdentityManager, mFolderIdentity);
3048     return id.autocryptEnabled();
3049 }
3050 
setIdentityManager(KIdentityManagement::IdentityManager * ident)3051 void ViewerPrivate::setIdentityManager(KIdentityManagement::IdentityManager *ident)
3052 {
3053     mIdentityManager = ident;
3054 }
3055 
setFolderIdentity(uint folderIdentity)3056 void MessageViewer::ViewerPrivate::setFolderIdentity(uint folderIdentity)
3057 {
3058     mFolderIdentity = folderIdentity;
3059 }
3060