1 /* Copyright (C) 2006 - 2016 Jan Kundrát <jkt@kde.org>
2    Copyright (C) 2014        Luke Dashjr <luke+trojita@dashjr.org>
3    Copyright (C) 2014 - 2015 Stephan Platz <trojita@paalsteek.de>
4 
5    This file is part of the Trojita Qt IMAP e-mail client,
6    http://trojita.flaska.net/
7 
8    This program is free software; you can redistribute it and/or
9    modify it under the terms of the GNU General Public License as
10    published by the Free Software Foundation; either version 2 of
11    the License or (at your option) version 3 or any later version
12    accepted by the membership of KDE e.V. (or its successor approved
13    by the membership of KDE e.V.), which shall act as a proxy
14    defined in Section 14 of version 3 of the license.
15 
16    This program is distributed in the hope that it will be useful,
17    but WITHOUT ANY WARRANTY; without even the implied warranty of
18    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19    GNU General Public License for more details.
20 
21    You should have received a copy of the GNU General Public License
22    along with this program.  If not, see <http://www.gnu.org/licenses/>.
23 */
24 
25 #include <QKeyEvent>
26 #include <QMenu>
27 #include <QSettings>
28 #include <QStackedLayout>
29 
30 #include "Common/InvokeMethod.h"
31 #include "Common/SettingsNames.h"
32 #include "Composer/QuoteText.h"
33 #include "Composer/SubjectMangling.h"
34 #include "Cryptography/MessageModel.h"
35 #include "Gui/MessageView.h"
36 #include "Gui/ComposeWidget.h"
37 #include "Gui/EnvelopeView.h"
38 #include "Gui/ExternalElementsWidget.h"
39 #include "Gui/OverlayWidget.h"
40 #include "Gui/PartWidgetFactoryVisitor.h"
41 #include "Gui/ShortcutHandler/ShortcutHandler.h"
42 #include "Gui/SimplePartWidget.h"
43 #include "Gui/Spinner.h"
44 #include "Gui/TagListWidget.h"
45 #include "Gui/UserAgentWebPage.h"
46 #include "Gui/Window.h"
47 #include "Imap/Model/MailboxTree.h"
48 #include "Imap/Model/MsgListModel.h"
49 #include "Imap/Model/NetworkWatcher.h"
50 #include "Imap/Model/Utils.h"
51 #include "Imap/Network/MsgPartNetAccessManager.h"
52 #include "Plugins/PluginManager.h"
53 #include "UiUtils/IconLoader.h"
54 
55 namespace Gui
56 {
57 
MessageView(QWidget * parent,QSettings * settings,Plugins::PluginManager * pluginManager)58 MessageView::MessageView(QWidget *parent, QSettings *settings, Plugins::PluginManager *pluginManager)
59     : QWidget(parent)
60     , m_stack(new QStackedLayout(this))
61     , messageModel(0)
62     , netAccess(new Imap::Network::MsgPartNetAccessManager(this))
63     , factory(new PartWidgetFactory(netAccess, this,
64                                     std::unique_ptr<PartWidgetFactoryVisitor>(new PartWidgetFactoryVisitor())))
65     , m_settings(settings)
66     , m_pluginManager(pluginManager)
67 {
68     connect(netAccess, &Imap::Network::MsgPartNetAccessManager::requestingExternal, this, &MessageView::externalsRequested);
69 
70 
71     setBackgroundRole(QPalette::Base);
72     setForegroundRole(QPalette::Text);
73     setAutoFillBackground(true);
74     setFocusPolicy(Qt::StrongFocus); // not by the wheel
75 
76 
77     m_zoomIn = ShortcutHandler::instance()->createAction(QStringLiteral("action_zoom_in"), this);
78     addAction(m_zoomIn);
79     connect(m_zoomIn, &QAction::triggered, this, &MessageView::zoomIn);
80 
81     m_zoomOut = ShortcutHandler::instance()->createAction(QStringLiteral("action_zoom_out"), this);
82     addAction(m_zoomOut);
83     connect(m_zoomOut, &QAction::triggered, this, &MessageView::zoomOut);
84 
85     m_zoomOriginal = ShortcutHandler::instance()->createAction(QStringLiteral("action_zoom_original"), this);
86     addAction(m_zoomOriginal);
87     connect(m_zoomOriginal, &QAction::triggered, this, &MessageView::zoomOriginal);
88 
89 
90     // The homepage widget -- our poor man's splashscreen
91     m_homePage = new EmbeddedWebView(this, new QNetworkAccessManager(this));
92     m_homePage->setFixedSize(450,300);
93     CALL_LATER_NOARG(m_homePage, handlePageLoadFinished);
94     m_homePage->setPage(new UserAgentWebPage(m_homePage));
95     m_homePage->installEventFilter(this);
96     m_homePage->setAutoFillBackground(false);
97     m_stack->addWidget(m_homePage);
98 
99 
100     // The actual widget for the actual message
101     m_messageWidget = new QWidget(this);
102     auto fullMsgLayout = new QVBoxLayout(m_messageWidget);
103     m_stack->addWidget(m_messageWidget);
104 
105     m_envelope = new EnvelopeView(m_messageWidget, this);
106     fullMsgLayout->addWidget(m_envelope, 1);
107 
108     tags = new TagListWidget(m_messageWidget);
109     connect(tags, &TagListWidget::tagAdded, this, &MessageView::newLabelAction);
110     connect(tags, &TagListWidget::tagRemoved, this, &MessageView::deleteLabelAction);
111     fullMsgLayout->addWidget(tags, 3);
112 
113     externalElements = new ExternalElementsWidget(this);
114     externalElements->hide();
115     connect(externalElements, &ExternalElementsWidget::loadingEnabled, this, &MessageView::enableExternalData);
116     fullMsgLayout->addWidget(externalElements, 1);
117 
118     // put the actual messages into an extra horizontal view
119     // this allows us easy usage of the trailing stretch and also to indent the message a bit
120     m_msgLayout = new QHBoxLayout;
121     m_msgLayout->setContentsMargins(6,6,6,0);
122     fullMsgLayout->addLayout(m_msgLayout, 1);
123     // add a strong stretch to squeeze header and message to the top
124     // possibly passing a large stretch factor to the message could be enough...
125     fullMsgLayout->addStretch(1000);
126 
127 
128     markAsReadTimer = new QTimer(this);
129     markAsReadTimer->setSingleShot(true);
130     connect(markAsReadTimer, &QTimer::timeout, this, &MessageView::markAsRead);
131 
132     m_loadingSpinner = new Spinner(this);
133     m_loadingSpinner->setText(tr("Fetching\nMessage"));
134     m_loadingSpinner->setType(Spinner::Sun);
135 }
136 
~MessageView()137 MessageView::~MessageView()
138 {
139     // Redmine #496 -- the default order of destruction starts with our QNAM subclass which in turn takes care of all pending
140     // QNetworkReply instances created by that manager. When the destruction goes to the WebKit objects, they try to disconnect
141     // from the network replies which are however gone already. We can mitigate that by simply making sure that the destruction
142     // starts with the QWebView subclasses and only after that proceeds to the QNAM. Qt's default order leads to segfaults here.
143     unsetPreviousMessage();
144 }
145 
unsetPreviousMessage()146 void MessageView::unsetPreviousMessage()
147 {
148     clearWaitingConns();
149     m_loadingItems.clear();
150     message = QModelIndex();
151     markAsReadTimer->stop();
152     if (auto w = bodyWidget()) {
153         m_stack->removeWidget(dynamic_cast<QWidget *>(w));
154         delete w;
155     }
156     m_envelope->setMessage(QModelIndex());
157     delete messageModel;
158     messageModel = nullptr;
159 }
160 
setEmpty()161 void MessageView::setEmpty()
162 {
163     unsetPreviousMessage();
164     m_loadingSpinner->stop();
165     m_stack->setCurrentWidget(m_homePage);
166     emit messageChanged();
167 }
168 
bodyWidget() const169 AbstractPartWidget *MessageView::bodyWidget() const
170 {
171     if (m_msgLayout->itemAt(0) && m_msgLayout->itemAt(0)->widget()) {
172         return dynamic_cast<AbstractPartWidget *>(m_msgLayout->itemAt(0)->widget());
173     } else {
174         return nullptr;
175     }
176 }
177 
setMessage(const QModelIndex & index)178 void MessageView::setMessage(const QModelIndex &index)
179 {
180     Q_ASSERT(index.isValid());
181     QModelIndex messageIndex = Imap::deproxifiedIndex(index);
182     Q_ASSERT(messageIndex.isValid());
183 
184     if (message == messageIndex) {
185         // This is a duplicate call, let's do nothing.
186         // It might not be our fat-fingered user, but also just a side-effect of our duplicate invocation through
187         // QAbstractItemView::clicked() and activated().
188         return;
189     }
190 
191     unsetPreviousMessage();
192 
193     message = messageIndex;
194     messageModel = new Cryptography::MessageModel(this, message);
195     messageModel->setObjectName(QStringLiteral("cryptoMessageModel-%1-%2")
196                                 .arg(message.data(Imap::Mailbox::RoleMailboxName).toString(),
197                                      message.data(Imap::Mailbox::RoleMessageUid).toString()));
198     for (const auto &module: m_pluginManager->mimePartReplacers()) {
199         messageModel->registerPartHandler(module);
200     }
201     emit messageModelChanged(messageModel);
202 
203     // The data might be available from the local cache, so let's try to save a possible roundtrip here
204     // by explicitly requesting the data
205     message.data(Imap::Mailbox::RolePartData);
206 
207     if (!message.data(Imap::Mailbox::RoleIsFetched).toBool()) {
208         // This happens when the message placeholder is already available in the GUI, but the actual message data haven't been
209         // loaded yet. This is especially common with the threading model, but also with bigger unsynced mailboxes.
210         // Note that the data might be already available in the cache, it's just that it isn't in the mailbox tree yet.
211         m_waitingMessageConns.emplace_back(
212                     connect(messageModel, &QAbstractItemModel::dataChanged, this, [this](const QModelIndex &topLeft){
213             if (topLeft.data(Imap::Mailbox::RoleIsFetched).toBool()) {
214                 // OK, message is fully fetched now
215                 showMessageNow();
216             }
217         }));
218         m_loadingSpinner->setText(tr("Waiting\nfor\nMessage..."));
219         m_loadingSpinner->start();
220     } else {
221         showMessageNow();
222     }
223 }
224 
225 /** @short Implementation of the "hey, let's really display the message, its BODYSTRUCTURE is available now" */
showMessageNow()226 void MessageView::showMessageNow()
227 {
228     Q_ASSERT(message.data(Imap::Mailbox::RoleIsFetched).toBool());
229 
230     clearWaitingConns();
231 
232     QModelIndex rootPartIndex = messageModel->index(0,0);
233     Q_ASSERT(rootPartIndex.child(0,0).isValid());
234 
235     netAccess->setExternalsEnabled(false);
236     externalElements->hide();
237 
238     netAccess->setModelMessage(rootPartIndex);
239 
240     m_loadingItems.clear();
241     m_loadingSpinner->stop();
242 
243     m_envelope->setMessage(message);
244 
245     auto updateTagList = [this]() {
246         tags->setTagList(message.data(Imap::Mailbox::RoleMessageFlags).toStringList());
247     };
248     connect(messageModel, &QAbstractItemModel::dataChanged, this, updateTagList);
249     updateTagList();
250 
251     UiUtils::PartLoadingOptions loadingMode;
252     if (m_settings->value(Common::SettingsNames::guiPreferPlaintextRendering, QVariant(true)).toBool())
253         loadingMode |= UiUtils::PART_PREFER_PLAINTEXT_OVER_HTML;
254     auto viewer = factory->walk(rootPartIndex.child(0,0), 0, loadingMode);
255     viewer->setParent(this);
256     m_msgLayout->addWidget(viewer);
257     m_msgLayout->setAlignment(viewer, Qt::AlignTop|Qt::AlignLeft);
258     viewer->show();
259     // We want to propagate the QWheelEvent to upper layers
260     viewer->installEventFilter(this);
261     m_stack->setCurrentWidget(m_messageWidget);
262 
263     if (m_netWatcher && m_netWatcher->effectiveNetworkPolicy() != Imap::Mailbox::NETWORK_OFFLINE
264             && m_settings->value(Common::SettingsNames::autoMarkReadEnabled, QVariant(true)).toBool()) {
265         // No additional delay is needed here because the MsgListView won't open a message while the user keeps scrolling,
266         // which was AFAIK the original intention
267         markAsReadTimer->start(m_settings->value(Common::SettingsNames::autoMarkReadSeconds, QVariant(0)).toUInt() * 1000);
268     }
269 
270     emit messageChanged();
271 }
272 
273 /** @short There's no point in waiting for the message to appear */
clearWaitingConns()274 void MessageView::clearWaitingConns()
275 {
276     for (auto &conn: m_waitingMessageConns) {
277         disconnect(conn);
278     }
279     m_waitingMessageConns.clear();
280 }
281 
markAsRead()282 void MessageView::markAsRead()
283 {
284     if (!message.isValid())
285         return;
286     Imap::Mailbox::Model *model = const_cast<Imap::Mailbox::Model *>(dynamic_cast<const Imap::Mailbox::Model *>(message.model()));
287     Q_ASSERT(model);
288     if (!model->isNetworkAvailable())
289         return;
290     if (!message.data(Imap::Mailbox::RoleMessageIsMarkedRead).toBool())
291         model->markMessagesRead(QModelIndexList() << message, Imap::Mailbox::FLAG_ADD);
292 }
293 
294 /** @short Inhibit the automatic marking of the current message as already read
295 
296 The user might have e.g. explicitly marked a previously read message as unread again immediately after navigating back to it
297 in the message listing. In that situation, the message viewer shall respect this decision and inhibit the helper which would
298 otherwise mark the current message as read after a short timeout.
299 */
stopAutoMarkAsRead()300 void MessageView::stopAutoMarkAsRead()
301 {
302     markAsReadTimer->stop();
303 }
304 
eventFilter(QObject * object,QEvent * event)305 bool MessageView::eventFilter(QObject *object, QEvent *event)
306 {
307     if (event->type() == QEvent::Wheel) {
308         if (static_cast<QWheelEvent *>(event)->modifiers() == Qt::ControlModifier) {
309             if (static_cast<QWheelEvent *>(event)->delta() > 0) {
310                 zoomIn();
311             } else {
312                 zoomOut();
313             }
314         } else {
315             // while the containing scrollview has Qt::StrongFocus, the event forwarding breaks that
316             // -> completely disable focus for the following wheel event ...
317             parentWidget()->setFocusPolicy(Qt::NoFocus);
318             MessageView::event(event);
319             // ... set reset it
320             parentWidget()->setFocusPolicy(Qt::StrongFocus);
321         }
322         return true;
323     } else if (event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease) {
324         QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
325         switch (keyEvent->key()) {
326         case Qt::Key_Left:
327         case Qt::Key_Right:
328         case Qt::Key_Up:
329         case Qt::Key_Down:
330         case Qt::Key_PageUp:
331         case Qt::Key_PageDown:
332             MessageView::event(event);
333             return true;
334         case Qt::Key_Home:
335         case Qt::Key_End:
336             return false;
337         default:
338             return QObject::eventFilter(object, event);
339         }
340     } else {
341         return QObject::eventFilter(object, event);
342     }
343 }
344 
quoteText() const345 QString MessageView::quoteText() const
346 {
347     if (auto w = bodyWidget()) {
348         QStringList quote = Composer::quoteText(w->quoteMe().split(QLatin1Char('\n')));
349         const Imap::Message::Envelope &e = message.data(Imap::Mailbox::RoleMessageEnvelope).value<Imap::Message::Envelope>();
350         QString sender;
351         if (!e.from.isEmpty())
352             sender = e.from[0].prettyName(Imap::Message::MailAddress::FORMAT_JUST_NAME);
353         if (e.from.isEmpty())
354             sender = tr("you");
355 
356         // One extra newline at the end of the quoted text to separate the response
357         quote << QString();
358 
359         return tr("On %1, %2 wrote:\n").arg(e.date.toLocalTime().toString(Qt::SystemLocaleLongDate), sender) + quote.join(QStringLiteral("\n"));
360     }
361     return QString();
362 }
363 
364 #define FORWARD_METHOD(METHOD) \
365 void MessageView::METHOD() \
366 { \
367     if (auto w = bodyWidget()) { \
368         w->METHOD(); \
369     } \
370 }
371 FORWARD_METHOD(zoomIn)
FORWARD_METHOD(zoomOut)372 FORWARD_METHOD(zoomOut)
373 FORWARD_METHOD(zoomOriginal)
374 
375 void MessageView::setNetworkWatcher(Imap::Mailbox::NetworkWatcher *netWatcher)
376 {
377     m_netWatcher = netWatcher;
378     factory->setNetworkWatcher(netWatcher);
379 }
380 
reply(MainWindow * mainWindow,Composer::ReplyMode mode)381 void MessageView::reply(MainWindow *mainWindow, Composer::ReplyMode mode)
382 {
383     if (!message.isValid())
384         return;
385 
386     // The Message-Id of the original message might have been empty; be sure we can handle that
387     QByteArray messageId = message.data(Imap::Mailbox::RoleMessageMessageId).toByteArray();
388     QList<QByteArray> messageIdList;
389     if (!messageId.isEmpty()) {
390         messageIdList.append(messageId);
391     }
392 
393     ComposeWidget::warnIfMsaNotConfigured(
394                 ComposeWidget::createReply(mainWindow, mode, message, QList<QPair<Composer::RecipientKind,QString> >(),
395                                            Composer::Util::replySubject(message.data(Imap::Mailbox::RoleMessageSubject).toString()),
396                                            quoteText(), messageIdList,
397                                            message.data(Imap::Mailbox::RoleMessageHeaderReferences).value<QList<QByteArray> >() + messageIdList),
398                 mainWindow);
399 }
400 
forward(MainWindow * mainWindow,const Composer::ForwardMode mode)401 void MessageView::forward(MainWindow *mainWindow, const Composer::ForwardMode mode)
402 {
403     if (!message.isValid())
404         return;
405 
406     // The Message-Id of the original message might have been empty; be sure we can handle that
407     QByteArray messageId = message.data(Imap::Mailbox::RoleMessageMessageId).toByteArray();
408     QList<QByteArray> messageIdList;
409     if (!messageId.isEmpty()) {
410         messageIdList.append(messageId);
411     }
412 
413     ComposeWidget::warnIfMsaNotConfigured(
414                 ComposeWidget::createForward(mainWindow, mode, message, Composer::Util::forwardSubject(message.data(Imap::Mailbox::RoleMessageSubject).toString()),
415                                              messageIdList, message.data(Imap::Mailbox::RoleMessageHeaderReferences).value<QList<QByteArray>>() + messageIdList),
416                 mainWindow);
417 }
418 
externalsRequested(const QUrl & url)419 void MessageView::externalsRequested(const QUrl &url)
420 {
421     Q_UNUSED(url);
422     externalElements->show();
423 }
424 
enableExternalData()425 void MessageView::enableExternalData()
426 {
427     netAccess->setExternalsEnabled(true);
428     externalElements->hide();
429     if (auto w = bodyWidget()) {
430         w->reloadContents();
431     }
432 }
433 
newLabelAction(const QString & tag)434 void MessageView::newLabelAction(const QString &tag)
435 {
436     if (!message.isValid())
437         return;
438 
439     Imap::Mailbox::Model *model = dynamic_cast<Imap::Mailbox::Model *>(const_cast<QAbstractItemModel *>(message.model()));
440     model->setMessageFlags(QModelIndexList() << message, tag, Imap::Mailbox::FLAG_ADD);
441 }
442 
deleteLabelAction(const QString & tag)443 void MessageView::deleteLabelAction(const QString &tag)
444 {
445     if (!message.isValid())
446         return;
447 
448     Imap::Mailbox::Model *model = dynamic_cast<Imap::Mailbox::Model *>(const_cast<QAbstractItemModel *>(message.model()));
449     model->setMessageFlags(QModelIndexList() << message, tag, Imap::Mailbox::FLAG_REMOVE);
450 }
451 
setHomepageUrl(const QUrl & homepage)452 void MessageView::setHomepageUrl(const QUrl &homepage)
453 {
454     m_homePage->load(homepage);
455 }
456 
showEvent(QShowEvent * se)457 void MessageView::showEvent(QShowEvent *se)
458 {
459     QWidget::showEvent(se);
460     // The Oxygen style reset the attribute - since we're gonna cause an update() here anyway, it's
461     // a good moment to stress that "we know better, Hugo ;-)" -- Thomas
462     setAutoFillBackground(true);
463 }
464 
partContextMenuRequested(const QPoint & point)465 void MessageView::partContextMenuRequested(const QPoint &point)
466 {
467     if (SimplePartWidget *w = qobject_cast<SimplePartWidget *>(sender())) {
468         QMenu menu(w);
469         w->buildContextMenu(point, menu);
470         menu.exec(w->mapToGlobal(point));
471     }
472 }
473 
partLinkHovered(const QString & link,const QString & title,const QString & textContent)474 void MessageView::partLinkHovered(const QString &link, const QString &title, const QString &textContent)
475 {
476     Q_UNUSED(title);
477     Q_UNUSED(textContent);
478     emit linkHovered(link);
479 }
480 
triggerSearchDialog()481 void MessageView::triggerSearchDialog()
482 {
483     emit searchRequestedBy(qobject_cast<EmbeddedWebView*>(sender()));
484 }
485 
currentMessage() const486 QModelIndex MessageView::currentMessage() const
487 {
488     return message;
489 }
490 
onWebViewLoadStarted()491 void MessageView::onWebViewLoadStarted()
492 {
493     QWebView *wv = qobject_cast<QWebView*>(sender());
494     Q_ASSERT(wv);
495 
496     if (m_netWatcher && m_netWatcher->effectiveNetworkPolicy() != Imap::Mailbox::NETWORK_OFFLINE) {
497         m_loadingItems << wv;
498         m_loadingSpinner->start(250);
499     }
500 }
501 
onWebViewLoadFinished()502 void MessageView::onWebViewLoadFinished()
503 {
504     QWebView *wv = qobject_cast<QWebView*>(sender());
505     Q_ASSERT(wv);
506     m_loadingItems.remove(wv);
507     if (m_loadingItems.isEmpty())
508         m_loadingSpinner->stop();
509 }
510 
pluginManager() const511 Plugins::PluginManager *MessageView::pluginManager() const
512 {
513     return m_pluginManager;
514 }
515 
516 }
517