1 /* Copyright (C) 2006 - 2014 Jan Kundrát <jkt@flaska.net>
3 This file is part of the Trojita Qt IMAP e-mail client,
4 http://trojita.flaska.net/
6 This program is free software; you can redistribute it and/or
7 modify it under the terms of the GNU General Public License as
8 published by the Free Software Foundation; either version 2 of
9 the License or (at your option) version 3 or any later version
10 accepted by the membership of KDE e.V. (or its successor approved
11 by the membership of KDE e.V.), which shall act as a proxy
12 defined in Section 14 of version 3 of the license.
14 This program is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 GNU General Public License for more details.
19 You should have received a copy of the GNU General Public License
20 along with this program. If not, see <http://www.gnu.org/licenses/>.
23 #include "UiUtils/PartWalker.h"
24 #include "Imap/Model/ItemRoles.h"
25 #if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
26 # include <QMimeDatabase>
28 # include "mimetypes-qt4/include/QMimeDatabase"
33 template<typename Result
, typename Context
>
34 PartWalker
<Result
, Context
>::PartWalker(Imap::Network::MsgPartNetAccessManager
*manager
,
35 Context context
, std::unique_ptr
<PartVisitor
<Result
, Context
> > visitor
)
36 : m_manager(manager
), m_netWatcher(0), m_context(context
)
38 m_visitor
= std::move(visitor
);
41 template<typename Result
, typename Context
>
42 Result PartWalker
<Result
, Context
>::walk(const QModelIndex
&partIndex
,int recursionDepth
, const UiUtils::PartLoadingOptions loadingMode
)
44 using namespace Imap::Mailbox
;
45 Q_ASSERT(partIndex
.isValid());
47 if (recursionDepth
> 1000) {
48 return m_visitor
->visitError(PartWalker::tr("This message contains too deep nesting of MIME message parts.\n"
49 "To prevent stack exhaustion and your head from exploding, only\n"
50 "the top-most thousand items or so are shown."), 0);
53 QString mimeType
= partIndex
.data(Imap::Mailbox::RolePartMimeType
).toString().toLower();
54 bool isMessageRfc822
= mimeType
== QLatin1String("message/rfc822");
55 bool isCompoundMimeType
= mimeType
.startsWith(QLatin1String("multipart/")) || isMessageRfc822
;
57 if (loadingMode
& PART_IS_HIDDEN
) {
58 return m_visitor
->visitLoadablePart(0, m_manager
, partIndex
, this, recursionDepth
+ 1,
59 loadingMode
| PART_IGNORE_CLICKTHROUGH
);
62 // Check whether we can render this MIME type at all
63 QStringList allowedMimeTypes
;
64 allowedMimeTypes
<< QLatin1String("text/html") << QLatin1String("text/plain") << QLatin1String("image/jpeg") <<
65 QLatin1String("image/jpg") << QLatin1String("image/pjpeg") << QLatin1String("image/png") << QLatin1String("image/gif");
66 bool recognizedMimeType
= isCompoundMimeType
|| allowedMimeTypes
.contains(mimeType
);
67 bool isDerivedMimeType
= false;
68 if (!recognizedMimeType
) {
69 // QMimeType's docs say that one shall use inherit() to check for "is this a recognized MIME type".
70 // E.g. text/x-csrc inherits text/plain.
71 QMimeType partType
= QMimeDatabase().mimeTypeForName(mimeType
);
72 Q_FOREACH(const QString
&candidate
, allowedMimeTypes
) {
73 if (partType
.isValid() && !partType
.isDefault() && partType
.inherits(candidate
)) {
74 // Looks like we shall be able to show this
75 recognizedMimeType
= true;
76 // If it's a derived MIME type, it makes sense to not block inline display, yet still make it possible to hide it
77 // using the regular attachment controls
78 isDerivedMimeType
= true;
79 m_manager
->registerMimeTypeTranslation(mimeType
, candidate
);
85 // Check whether it shall be wrapped inside an AttachmentView
86 // From section 2.8 of RFC 2183: "Unrecognized disposition types should be treated as `attachment'."
87 const QByteArray contentDisposition
= partIndex
.data(Imap::Mailbox::RolePartBodyDisposition
).toByteArray().toLower();
88 const bool isInline
= contentDisposition
.isEmpty() || contentDisposition
== "inline";
89 const bool looksLikeAttachment
= !partIndex
.data(Imap::Mailbox::RolePartFileName
).toString().isEmpty();
90 const bool wrapInAttachmentView
= !(loadingMode
& PART_IGNORE_DISPOSITION_ATTACHMENT
)
91 && (looksLikeAttachment
|| !isInline
|| !recognizedMimeType
|| isDerivedMimeType
|| isMessageRfc822
);
92 if (wrapInAttachmentView
) {
93 // The problem is that some nasty MUAs (hint hint Thunderbird) would
94 // happily attach a .tar.gz and call it "inline"
95 Result contentView
= 0;
96 if (recognizedMimeType
) {
97 PartLoadingOptions options
= loadingMode
| PART_IGNORE_DISPOSITION_ATTACHMENT
;
99 // The widget will be hidden by default, i.e. the "inline preview" will be deactivated.
100 // If the user clicks that action in the AttachmentView, it makes sense to load the plugin without any further ado,
101 // without requiring an extra clickthrough
102 options
|= PART_IS_HIDDEN
;
103 } else if (!isCompoundMimeType
) {
104 // This is to prevent a clickthrough when the data can be already shown
105 partIndex
.data(Imap::Mailbox::RolePartForceFetchFromCache
);
107 // This makes sure that clickthrough only affects big parts during "expensive network" mode
108 if ( (m_netWatcher
&& m_netWatcher
->desiredNetworkPolicy() != Imap::Mailbox::NETWORK_EXPENSIVE
)
109 || partIndex
.data(Imap::Mailbox::RolePartOctets
).toInt() <= ExpensiveFetchThreshold
) {
110 options
|= PART_IGNORE_CLICKTHROUGH
;
113 // A compound type -> make sure we disable clickthrough
114 options
|= PART_IGNORE_CLICKTHROUGH
;
117 if (m_netWatcher
&& m_netWatcher
->effectiveNetworkPolicy() == Imap::Mailbox::NETWORK_OFFLINE
) {
118 // This is to prevent a clickthrough when offline
119 options
|= PART_IGNORE_CLICKTHROUGH
;
121 contentView
= m_visitor
->visitLoadablePart(0, m_manager
, partIndex
, this, recursionDepth
+ 1, options
);
123 m_visitor
->applySetHidden(contentView
);
126 // Previously, we would also hide an attachment if the current policy was "expensive network", the part was too big
127 // and not fetched yet. Arguably, that's a bug -- an item which is marked online shall not be hidden.
128 // Wrapping via a clickthrough is fine, though; the user is clearly informed that this item *should* be visible,
129 // yet the bandwidth is not excessively trashed.
130 return m_visitor
->visitAttachmentPart(0, m_manager
, partIndex
, m_context
, contentView
);
133 // Now we know for sure that it's not supposed to be wrapped in an AttachmentView, cool.
134 if (mimeType
.startsWith(QLatin1String("multipart/"))) {
135 // it's a compound part
136 if (mimeType
== QLatin1String("multipart/alternative")) {
137 return m_visitor
->visitMultipartAlternative(0, this, partIndex
, recursionDepth
, loadingMode
);
138 } else if (mimeType
== QLatin1String("multipart/signed")) {
139 return m_visitor
->visitMultipartSignedView(0, this, partIndex
, recursionDepth
, loadingMode
);
140 } else if (mimeType
== QLatin1String("multipart/related")) {
141 // The purpose of this section is to find a text/html e-mail, along with its associated body parts, and hide
142 // everything else than the HTML widget.
144 // At this point, it might be interesting to somehow respect the user's preference about using text/plain
145 // instead of text/html. However, things are a bit complicated; the widget used at this point really wants
146 // to either show just a single part or alternatively all of them in a sequence.
147 // Furthermore, if someone sends a text/plain and a text/html together inside a multipart/related, they're
150 // Let's see if we know what the root part is
151 QModelIndex mainPartIndex
;
152 QVariant mainPartCID
= partIndex
.data(RolePartMultipartRelatedMainCid
);
153 if (mainPartCID
.isValid()) {
154 mainPartIndex
= Imap::Network::MsgPartNetAccessManager::cidToPart(partIndex
, mainPartCID
.toByteArray());
157 if (!mainPartIndex
.isValid()) {
158 // The Content-Type-based start parameter was not terribly useful. Let's find the HTML part manually.
159 QModelIndex candidate
= partIndex
.child(0, 0);
160 while (candidate
.isValid()) {
161 if (candidate
.data(RolePartMimeType
).toString() == QLatin1String("text/html")) {
162 mainPartIndex
= candidate
;
165 candidate
= candidate
.sibling(candidate
.row() + 1, 0);
169 if (mainPartIndex
.isValid()) {
170 if (mainPartIndex
.data(RolePartMimeType
).toString() == QLatin1String("text/html")) {
171 return walk(mainPartIndex
, recursionDepth
+1, loadingMode
);
173 // Sorry, but anything else than text/html is by definition suspicious here. Better than picking some random
174 // choice, let's just show everything.
175 return m_visitor
->visitGenericMultipartView(0, this, partIndex
, recursionDepth
, loadingMode
);
178 // The RFC2387's wording is clear that in absence of an explicit START argument, the first part is the starting one.
179 // On the other hand, I've seen real-world messages whose first part is some utter garbage (an image sent as
180 // application/octet-stream, for example) and some *other* part is an HTML text. In that case (and if we somehow
181 // failed to pick the HTML part by a heuristic), it's better to show everything.
182 return m_visitor
->visitGenericMultipartView(0, this, partIndex
, recursionDepth
, loadingMode
);
185 return m_visitor
->visitGenericMultipartView(0, this, partIndex
, recursionDepth
, loadingMode
);
187 } else if (mimeType
== QLatin1String("message/rfc822")) {
188 return m_visitor
->visitMessage822View(0, this, partIndex
, recursionDepth
, loadingMode
);
190 partIndex
.data(Imap::Mailbox::RolePartForceFetchFromCache
);
192 if ((loadingMode
& PART_IGNORE_CLICKTHROUGH
) || (loadingMode
& PART_IGNORE_LOAD_ON_SHOW
) ||
193 partIndex
.data(Imap::Mailbox::RoleIsFetched
).toBool() ||
194 (m_netWatcher
&& m_netWatcher
->desiredNetworkPolicy() != Imap::Mailbox::NETWORK_EXPENSIVE
) ||
195 partIndex
.data(Imap::Mailbox::RolePartOctets
).toInt() < ExpensiveFetchThreshold
) {
196 // Show it directly without any fancy wrapping
197 return m_visitor
->visitSimplePartView(0, m_manager
, partIndex
, m_context
);
199 return m_visitor
->visitLoadablePart(0, m_manager
, partIndex
, this, recursionDepth
+ 1,
200 (m_netWatcher
&& m_netWatcher
->effectiveNetworkPolicy() != Imap::Mailbox::NETWORK_OFFLINE
) ?
201 loadingMode
: loadingMode
| PART_IGNORE_CLICKTHROUGH
);
206 template<typename Result
, typename Context
>
207 Context PartWalker
<Result
, Context
>::context() const
212 template<typename Result
, typename Context
>
213 void PartWalker
<Result
, Context
>::setNetworkWatcher(Imap::Mailbox::NetworkWatcher
*netWatcher
)
215 m_netWatcher
= netWatcher
;