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("\"/> ");
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