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