1 /*
2 SPDX-FileCopyrightText: 2009 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net
3 SPDX-FileCopyrightText: 2009 Andras Mantia <andras@kdab.net>
4
5 SPDX-License-Identifier: GPL-2.0-or-later
6 */
7
8 #include "nodehelper.h"
9 #include "interfaces/bodypart.h"
10 #include "messagepart.h"
11 #include "mimetreeparser_debug.h"
12 #include "partmetadata.h"
13 #include "temporaryfile/attachmenttemporaryfilesdirs.h"
14
15 #include <KMime/Content>
16
17 #include <KCharsets>
18 #include <KLocalizedString>
19 #include <QTemporaryFile>
20
21 #include <QDir>
22 #include <QRegularExpressionMatch>
23 #include <QTextCodec>
24 #include <QUrl>
25
26 #include <QFileDevice>
27 #include <QMimeDatabase>
28 #include <QMimeType>
29 #include <algorithm>
30 #include <sstream>
31 #include <string>
32
33 namespace MimeTreeParser
34 {
NodeHelper()35 NodeHelper::NodeHelper()
36 : mAttachmentFilesDir(new AttachmentTemporaryFilesDirs())
37 {
38 mListAttachmentTemporaryDirs.append(mAttachmentFilesDir);
39 // TODO(Andras) add methods to modify these prefixes
40
41 mLocalCodec = QTextCodec::codecForLocale();
42
43 // In the case of Japan. Japanese locale name is "eucjp" but
44 // The Japanese mail systems normally used "iso-2022-jp" of locale name.
45 // We want to change locale name from eucjp to iso-2022-jp at KMail only.
46
47 // (Introduction to i18n, 6.6 Limit of Locale technology):
48 // EUC-JP is the de-facto standard for UNIX systems, ISO 2022-JP
49 // is the standard for Internet, and Shift-JIS is the encoding
50 // for Windows and Macintosh.
51 if (mLocalCodec) {
52 const QByteArray codecNameLower = mLocalCodec->name().toLower();
53 if (codecNameLower == "eucjp"
54 #if defined Q_OS_WIN || defined Q_OS_MACX
55 || codecNameLower == "shift-jis" // OK?
56 #endif
57 ) {
58 mLocalCodec = QTextCodec::codecForName("jis7");
59 // QTextCodec *cdc = QTextCodec::codecForName("jis7");
60 // QTextCodec::setCodecForLocale(cdc);
61 // KLocale::global()->setEncoding(cdc->mibEnum());
62 }
63 }
64 }
65
~NodeHelper()66 NodeHelper::~NodeHelper()
67 {
68 for (auto att : mListAttachmentTemporaryDirs) {
69 if (att) {
70 att->forceCleanTempFiles();
71 delete att;
72 }
73 }
74 clear();
75 }
76
setNodeProcessed(KMime::Content * node,bool recurse)77 void NodeHelper::setNodeProcessed(KMime::Content *node, bool recurse)
78 {
79 if (!node) {
80 return;
81 }
82 mProcessedNodes.append(node);
83 qCDebug(MIMETREEPARSER_LOG) << "Node processed: " << node->index().toString() << node->contentType()->as7BitString();
84 //<< " decodedContent" << node->decodedContent();
85 if (recurse) {
86 const auto contents = node->contents();
87 for (KMime::Content *c : contents) {
88 setNodeProcessed(c, true);
89 }
90 }
91 }
92
setNodeUnprocessed(KMime::Content * node,bool recurse)93 void NodeHelper::setNodeUnprocessed(KMime::Content *node, bool recurse)
94 {
95 if (!node) {
96 return;
97 }
98 mProcessedNodes.removeAll(node);
99
100 // avoid double addition of extra nodes, eg. encrypted attachments
101 const QMap<KMime::Content *, QVector<KMime::Content *>>::iterator it = mExtraContents.find(node);
102 if (it != mExtraContents.end()) {
103 const auto contents = it.value();
104 for (KMime::Content *c : contents) {
105 KMime::Content *p = c->parent();
106 if (p) {
107 p->removeContent(c);
108 }
109 }
110 qDeleteAll(it.value());
111 qCDebug(MIMETREEPARSER_LOG) << "mExtraContents deleted for" << it.key();
112 mExtraContents.erase(it);
113 }
114
115 qCDebug(MIMETREEPARSER_LOG) << "Node UNprocessed: " << node;
116 if (recurse) {
117 const auto contents = node->contents();
118 for (KMime::Content *c : contents) {
119 setNodeUnprocessed(c, true);
120 }
121 }
122 }
123
nodeProcessed(KMime::Content * node) const124 bool NodeHelper::nodeProcessed(KMime::Content *node) const
125 {
126 if (!node) {
127 return true;
128 }
129 return mProcessedNodes.contains(node);
130 }
131
clearBodyPartMemento(QMap<QByteArray,Interface::BodyPartMemento * > & bodyPartMementoMap)132 static void clearBodyPartMemento(QMap<QByteArray, Interface::BodyPartMemento *> &bodyPartMementoMap)
133 {
134 for (QMap<QByteArray, Interface::BodyPartMemento *>::iterator it = bodyPartMementoMap.begin(), end = bodyPartMementoMap.end(); it != end; ++it) {
135 Interface::BodyPartMemento *memento = it.value();
136 memento->detach();
137 delete memento;
138 }
139 bodyPartMementoMap.clear();
140 }
141
clear()142 void NodeHelper::clear()
143 {
144 mProcessedNodes.clear();
145 mEncryptionState.clear();
146 mSignatureState.clear();
147 mOverrideCodecs.clear();
148 std::for_each(mBodyPartMementoMap.begin(), mBodyPartMementoMap.end(), &clearBodyPartMemento);
149 mBodyPartMementoMap.clear();
150 QMap<KMime::Content *, QVector<KMime::Content *>>::ConstIterator end(mExtraContents.constEnd());
151
152 for (QMap<KMime::Content *, QVector<KMime::Content *>>::ConstIterator it = mExtraContents.constBegin(); it != end; ++it) {
153 const auto contents = it.value();
154 for (KMime::Content *c : contents) {
155 KMime::Content *p = c->parent();
156 if (p) {
157 p->removeContent(c);
158 }
159 }
160 qDeleteAll(it.value());
161 qCDebug(MIMETREEPARSER_LOG) << "mExtraContents deleted for" << it.key();
162 }
163 mExtraContents.clear();
164 mDisplayEmbeddedNodes.clear();
165 mDisplayHiddenNodes.clear();
166 }
167
setEncryptionState(const KMime::Content * node,const KMMsgEncryptionState state)168 void NodeHelper::setEncryptionState(const KMime::Content *node, const KMMsgEncryptionState state)
169 {
170 mEncryptionState[node] = state;
171 }
172
encryptionState(const KMime::Content * node) const173 KMMsgEncryptionState NodeHelper::encryptionState(const KMime::Content *node) const
174 {
175 return mEncryptionState.value(node, KMMsgNotEncrypted);
176 }
177
setSignatureState(const KMime::Content * node,const KMMsgSignatureState state)178 void NodeHelper::setSignatureState(const KMime::Content *node, const KMMsgSignatureState state)
179 {
180 mSignatureState[node] = state;
181 }
182
signatureState(const KMime::Content * node) const183 KMMsgSignatureState NodeHelper::signatureState(const KMime::Content *node) const
184 {
185 return mSignatureState.value(node, KMMsgNotSigned);
186 }
187
partMetaData(KMime::Content * node)188 PartMetaData NodeHelper::partMetaData(KMime::Content *node)
189 {
190 return mPartMetaDatas.value(node, PartMetaData());
191 }
192
setPartMetaData(KMime::Content * node,const PartMetaData & metaData)193 void NodeHelper::setPartMetaData(KMime::Content *node, const PartMetaData &metaData)
194 {
195 mPartMetaDatas.insert(node, metaData);
196 }
197
writeFileToTempFile(KMime::Content * node,const QString & filename)198 QString NodeHelper::writeFileToTempFile(KMime::Content *node, const QString &filename)
199 {
200 QString fname = createTempDir(persistentIndex(node));
201 if (fname.isEmpty()) {
202 return QString();
203 }
204 fname += QLatin1Char('/') + filename;
205 QFile f(fname);
206 if (!f.open(QIODevice::ReadWrite)) {
207 qCWarning(MIMETREEPARSER_LOG) << "Failed to write note to file:" << f.errorString();
208 mAttachmentFilesDir->addTempFile(fname);
209 return QString();
210 }
211 f.write(QByteArray());
212 mAttachmentFilesDir->addTempFile(fname);
213 // make file read-only so that nobody gets the impression that he might
214 // edit attached files (cf. bug #52813)
215 f.setPermissions(QFileDevice::ReadUser);
216 f.close();
217
218 return fname;
219 }
220
writeNodeToTempFile(KMime::Content * node)221 QString NodeHelper::writeNodeToTempFile(KMime::Content *node)
222 {
223 // If the message part is already written to a file, no point in doing it again.
224 // This function is called twice actually, once from the rendering of the attachment
225 // in the body and once for the header.
226 const QUrl existingFileName = tempFileUrlFromNode(node);
227 if (!existingFileName.isEmpty()) {
228 return existingFileName.toLocalFile();
229 }
230
231 QString fname = createTempDir(persistentIndex(node));
232 if (fname.isEmpty()) {
233 return QString();
234 }
235
236 QString fileName = NodeHelper::fileName(node);
237 // strip off a leading path
238 int slashPos = fileName.lastIndexOf(QLatin1Char('/'));
239 if (-1 != slashPos) {
240 fileName = fileName.mid(slashPos + 1);
241 }
242 if (fileName.isEmpty()) {
243 fileName = QStringLiteral("unnamed");
244 }
245 fname += QLatin1Char('/') + fileName;
246
247 qCDebug(MIMETREEPARSER_LOG) << "Create temp file: " << fname;
248 QByteArray data = node->decodedContent();
249 if (node->contentType()->isText() && !data.isEmpty()) {
250 // convert CRLF to LF before writing text attachments to disk
251 data = KMime::CRLFtoLF(data);
252 }
253 QFile f(fname);
254 if (!f.open(QIODevice::ReadWrite)) {
255 qCWarning(MIMETREEPARSER_LOG) << "Failed to write note to file:" << f.errorString();
256 mAttachmentFilesDir->addTempFile(fname);
257 return QString();
258 }
259 f.write(data);
260 mAttachmentFilesDir->addTempFile(fname);
261 // make file read-only so that nobody gets the impression that he might
262 // edit attached files (cf. bug #52813)
263 f.setPermissions(QFileDevice::ReadUser);
264 f.close();
265
266 return fname;
267 }
268
tempFileUrlFromNode(const KMime::Content * node)269 QUrl NodeHelper::tempFileUrlFromNode(const KMime::Content *node)
270 {
271 if (!node) {
272 return QUrl();
273 }
274
275 const QString index = persistentIndex(node);
276
277 const QStringList temporaryFiles = mAttachmentFilesDir->temporaryFiles();
278 for (const QString &path : temporaryFiles) {
279 const int right = path.lastIndexOf(QLatin1Char('/'));
280 int left = path.lastIndexOf(QLatin1String(".index."), right);
281 if (left != -1) {
282 left += 7;
283 }
284
285 const QStringRef storedIndex(&path, left, right - left);
286 if (left != -1 && storedIndex == index) {
287 return QUrl::fromLocalFile(path);
288 }
289 }
290 return QUrl();
291 }
292
createTempDir(const QString & param)293 QString NodeHelper::createTempDir(const QString ¶m)
294 {
295 auto tempFile = new QTemporaryFile(QDir::tempPath() + QLatin1String("/messageviewer_XXXXXX") + QLatin1String(".index.") + param);
296 tempFile->open();
297 const QString fname = tempFile->fileName();
298 delete tempFile;
299
300 QFile fFile(fname);
301 if (!(fFile.permissions() & QFileDevice::WriteUser)) {
302 // Not there or not writable
303 if (!QDir().mkpath(fname) || !fFile.setPermissions(QFileDevice::WriteUser | QFileDevice::ReadUser | QFileDevice::ExeUser)) {
304 mAttachmentFilesDir->addTempDir(fname);
305 return QString(); // failed create
306 }
307 }
308
309 Q_ASSERT(!fname.isNull());
310
311 mAttachmentFilesDir->addTempDir(fname);
312 return fname;
313 }
314
forceCleanTempFiles()315 void NodeHelper::forceCleanTempFiles()
316 {
317 mAttachmentFilesDir->forceCleanTempFiles();
318 delete mAttachmentFilesDir;
319 mAttachmentFilesDir = nullptr;
320 }
321
removeTempFiles()322 void NodeHelper::removeTempFiles()
323 {
324 // Don't delete as it will be deleted in class
325 if (mAttachmentFilesDir) {
326 mAttachmentFilesDir->removeTempFiles();
327 }
328 mAttachmentFilesDir = new AttachmentTemporaryFilesDirs();
329 mListAttachmentTemporaryDirs.append(mAttachmentFilesDir);
330 }
331
addTempFile(const QString & file)332 void NodeHelper::addTempFile(const QString &file)
333 {
334 mAttachmentFilesDir->addTempFile(file);
335 }
336
isInEncapsulatedMessage(KMime::Content * node)337 bool NodeHelper::isInEncapsulatedMessage(KMime::Content *node)
338 {
339 const KMime::Content *const topLevel = node->topLevel();
340 const KMime::Content *cur = node;
341 while (cur && cur != topLevel) {
342 const bool parentIsMessage =
343 cur->parent() && cur->parent()->contentType(false) && cur->parent()->contentType(false)->mimeType().toLower() == "message/rfc822";
344 if (parentIsMessage && cur->parent() != topLevel) {
345 return true;
346 }
347 cur = cur->parent();
348 }
349 return false;
350 }
351
charset(KMime::Content * node)352 QByteArray NodeHelper::charset(KMime::Content *node)
353 {
354 if (node->contentType(false)) {
355 return node->contentType(false)->charset();
356 } else {
357 return node->defaultCharset();
358 }
359 }
360
overallEncryptionState(KMime::Content * node) const361 KMMsgEncryptionState NodeHelper::overallEncryptionState(KMime::Content *node) const
362 {
363 KMMsgEncryptionState myState = KMMsgEncryptionStateUnknown;
364 if (!node) {
365 return myState;
366 }
367
368 KMime::Content *parent = node->parent();
369 auto contents = parent ? parent->contents() : KMime::Content::List();
370 if (contents.isEmpty()) {
371 contents.append(node);
372 }
373 int i = contents.indexOf(const_cast<KMime::Content *>(node));
374 if (i < 0) {
375 return myState;
376 }
377 for (; i < contents.size(); ++i) {
378 auto next = contents.at(i);
379 KMMsgEncryptionState otherState = encryptionState(next);
380
381 // NOTE: children are tested ONLY when parent is not encrypted
382 if (otherState == KMMsgNotEncrypted && !next->contents().isEmpty()) {
383 otherState = overallEncryptionState(next->contents().at(0));
384 }
385
386 if (otherState == KMMsgNotEncrypted && !extraContents(next).isEmpty()) {
387 otherState = overallEncryptionState(extraContents(next).at(0));
388 }
389
390 if (next == node) {
391 myState = otherState;
392 }
393
394 switch (otherState) {
395 case KMMsgEncryptionStateUnknown:
396 break;
397 case KMMsgNotEncrypted:
398 if (myState == KMMsgFullyEncrypted) {
399 myState = KMMsgPartiallyEncrypted;
400 } else if (myState != KMMsgPartiallyEncrypted) {
401 myState = KMMsgNotEncrypted;
402 }
403 break;
404 case KMMsgPartiallyEncrypted:
405 myState = KMMsgPartiallyEncrypted;
406 break;
407 case KMMsgFullyEncrypted:
408 if (myState != KMMsgFullyEncrypted) {
409 myState = KMMsgPartiallyEncrypted;
410 }
411 break;
412 case KMMsgEncryptionProblematic:
413 break;
414 }
415 }
416
417 qCDebug(MIMETREEPARSER_LOG) << "\n\n KMMsgEncryptionState:" << myState;
418
419 return myState;
420 }
421
overallSignatureState(KMime::Content * node) const422 KMMsgSignatureState NodeHelper::overallSignatureState(KMime::Content *node) const
423 {
424 KMMsgSignatureState myState = KMMsgSignatureStateUnknown;
425 if (!node) {
426 return myState;
427 }
428
429 KMime::Content *parent = node->parent();
430 auto contents = parent ? parent->contents() : KMime::Content::List();
431 if (contents.isEmpty()) {
432 contents.append(node);
433 }
434 int i = contents.indexOf(const_cast<KMime::Content *>(node));
435 if (i < 0) { // Be safe
436 return myState;
437 }
438 for (; i < contents.size(); ++i) {
439 auto next = contents.at(i);
440 KMMsgSignatureState otherState = signatureState(next);
441
442 // NOTE: children are tested ONLY when parent is not encrypted
443 if (otherState == KMMsgNotSigned && !next->contents().isEmpty()) {
444 otherState = overallSignatureState(next->contents().at(0));
445 }
446
447 if (otherState == KMMsgNotSigned && !extraContents(next).isEmpty()) {
448 otherState = overallSignatureState(extraContents(next).at(0));
449 }
450
451 if (next == node) {
452 myState = otherState;
453 }
454
455 switch (otherState) {
456 case KMMsgSignatureStateUnknown:
457 break;
458 case KMMsgNotSigned:
459 if (myState == KMMsgFullySigned) {
460 myState = KMMsgPartiallySigned;
461 } else if (myState != KMMsgPartiallySigned) {
462 myState = KMMsgNotSigned;
463 }
464 break;
465 case KMMsgPartiallySigned:
466 myState = KMMsgPartiallySigned;
467 break;
468 case KMMsgFullySigned:
469 if (myState != KMMsgFullySigned) {
470 myState = KMMsgPartiallySigned;
471 }
472 break;
473 case KMMsgSignatureProblematic:
474 break;
475 }
476 }
477
478 qCDebug(MIMETREEPARSER_LOG) << "\n\n KMMsgSignatureState:" << myState;
479
480 return myState;
481 }
482
magicSetType(KMime::Content * node,bool aAutoDecode)483 void NodeHelper::magicSetType(KMime::Content *node, bool aAutoDecode)
484 {
485 const QByteArray body = aAutoDecode ? node->decodedContent() : node->body();
486 QMimeDatabase db;
487 const QMimeType mime = db.mimeTypeForData(body);
488
489 const QString mimetype = mime.name();
490 node->contentType()->setMimeType(mimetype.toLatin1());
491 }
492
hasMailHeader(const char * header,const KMime::Content * message) const493 bool NodeHelper::hasMailHeader(const char *header, const KMime::Content *message) const
494 {
495 if (mHeaderOverwrite.contains(message)) {
496 const auto parts = mHeaderOverwrite.value(message);
497 for (const auto &messagePart : parts) {
498 if (messagePart->hasHeader(header)) {
499 return true;
500 }
501 }
502 }
503 return message->hasHeader(header);
504 }
505
messagePartsOfMailHeader(const char * header,const KMime::Content * message) const506 QVector<MessagePart::Ptr> NodeHelper::messagePartsOfMailHeader(const char *header, const KMime::Content *message) const
507 {
508 QVector<MessagePart::Ptr> ret;
509 if (mHeaderOverwrite.contains(message)) {
510 const auto parts = mHeaderOverwrite.value(message);
511 for (const auto &messagePart : parts) {
512 if (messagePart->hasHeader(header)) {
513 ret << messagePart;
514 }
515 }
516 }
517 return ret;
518 }
519
headers(const char * header,const KMime::Content * message)520 QVector<KMime::Headers::Base *> NodeHelper::headers(const char *header, const KMime::Content *message)
521 {
522 const auto mp = messagePartsOfMailHeader(header, message);
523 if (mp.size() > 0) {
524 return mp.value(0)->headers(header);
525 }
526
527 return message->headersByType(header);
528 }
529
mailHeaderAsBase(const char * header,const KMime::Content * message) const530 KMime::Headers::Base const *NodeHelper::mailHeaderAsBase(const char *header, const KMime::Content *message) const
531 {
532 if (mHeaderOverwrite.contains(message)) {
533 const auto parts = mHeaderOverwrite.value(message);
534 for (const auto &messagePart : parts) {
535 if (messagePart->hasHeader(header)) {
536 return messagePart->header(header); // Found.
537 }
538 }
539 }
540 return message->headerByType(header);
541 }
542
mailHeaderAsAddressList(const char * header,const KMime::Content * message) const543 QSharedPointer<KMime::Headers::Generics::AddressList> NodeHelper::mailHeaderAsAddressList(const char *header, const KMime::Content *message) const
544 {
545 const auto hrd = mailHeaderAsBase(header, message);
546 if (!hrd) {
547 return nullptr;
548 }
549 QSharedPointer<KMime::Headers::Generics::AddressList> addressList(new KMime::Headers::Generics::AddressList());
550 addressList->from7BitString(hrd->as7BitString(false));
551 return addressList;
552 }
553
clearOverrideHeaders()554 void NodeHelper::clearOverrideHeaders()
555 {
556 mHeaderOverwrite.clear();
557 }
558
registerOverrideHeader(KMime::Content * message,MessagePart::Ptr part)559 void NodeHelper::registerOverrideHeader(KMime::Content *message, MessagePart::Ptr part)
560 {
561 if (!mHeaderOverwrite.contains(message)) {
562 mHeaderOverwrite[message] = QVector<MessagePart::Ptr>();
563 }
564 mHeaderOverwrite[message].append(part);
565 }
566
dateHeader(KMime::Content * message) const567 QDateTime NodeHelper::dateHeader(KMime::Content *message) const
568 {
569 const auto dateHeader = mailHeaderAsBase("date", message);
570 if (dateHeader != nullptr) {
571 return static_cast<const KMime::Headers::Date *>(dateHeader)->dateTime();
572 }
573 return QDateTime();
574 }
575
setOverrideCodec(KMime::Content * node,const QTextCodec * codec)576 void NodeHelper::setOverrideCodec(KMime::Content *node, const QTextCodec *codec)
577 {
578 if (!node) {
579 return;
580 }
581
582 mOverrideCodecs[node] = codec;
583 }
584
codec(KMime::Content * node)585 const QTextCodec *NodeHelper::codec(KMime::Content *node)
586 {
587 if (!node) {
588 return mLocalCodec;
589 }
590
591 const QTextCodec *c = mOverrideCodecs.value(node, nullptr);
592 if (!c) {
593 // no override-codec set for this message, try the CT charset parameter:
594 QByteArray charset = node->contentType()->charset();
595
596 // utf-8 is a superset of us-ascii, so we don't loose anything, if we it insead
597 // utf-8 is nowadays that widely, that it is a good guess to use it to fix issus with broken clients.
598 if (charset.toLower() == "us-ascii") {
599 charset = "utf-8";
600 }
601 c = codecForName(charset);
602 }
603 if (!c) {
604 // no charset means us-ascii (RFC 2045), so using local encoding should
605 // be okay
606 c = mLocalCodec;
607 }
608 return c;
609 }
610
codecForName(const QByteArray & _str)611 const QTextCodec *NodeHelper::codecForName(const QByteArray &_str)
612 {
613 if (_str.isEmpty()) {
614 return nullptr;
615 }
616 QByteArray codec = _str.toLower();
617 return KCharsets::charsets()->codecForName(QLatin1String(codec));
618 }
619
fileName(const KMime::Content * node)620 QString NodeHelper::fileName(const KMime::Content *node)
621 {
622 QString name = const_cast<KMime::Content *>(node)->contentDisposition()->filename();
623 if (name.isEmpty()) {
624 name = const_cast<KMime::Content *>(node)->contentType()->name();
625 }
626
627 name = name.trimmed();
628 return name;
629 }
630
631 // FIXME(Andras) review it (by Marc?) to see if I got it right. This is supposed to be the partNode::internalBodyPartMemento replacement
bodyPartMemento(KMime::Content * node,const QByteArray & which) const632 Interface::BodyPartMemento *NodeHelper::bodyPartMemento(KMime::Content *node, const QByteArray &which) const
633 {
634 const QMap<QString, QMap<QByteArray, Interface::BodyPartMemento *>>::const_iterator nit = mBodyPartMementoMap.find(persistentIndex(node));
635 if (nit == mBodyPartMementoMap.end()) {
636 return nullptr;
637 }
638 const QMap<QByteArray, Interface::BodyPartMemento *>::const_iterator it = nit->find(which.toLower());
639 return it != nit->end() ? it.value() : nullptr;
640 }
641
642 // FIXME(Andras) review it (by Marc?) to see if I got it right. This is supposed to be the partNode::internalSetBodyPartMemento replacement
setBodyPartMemento(KMime::Content * node,const QByteArray & which,Interface::BodyPartMemento * memento)643 void NodeHelper::setBodyPartMemento(KMime::Content *node, const QByteArray &which, Interface::BodyPartMemento *memento)
644 {
645 QMap<QByteArray, Interface::BodyPartMemento *> &mementos = mBodyPartMementoMap[persistentIndex(node)];
646
647 const QByteArray whichLower = which.toLower();
648 const QMap<QByteArray, Interface::BodyPartMemento *>::iterator it = mementos.lowerBound(whichLower);
649
650 if (it != mementos.end() && it.key() == whichLower) {
651 delete it.value();
652 if (memento) {
653 it.value() = memento;
654 } else {
655 mementos.erase(it);
656 }
657 } else {
658 mementos.insert(whichLower, memento);
659 }
660 }
661
isNodeDisplayedEmbedded(KMime::Content * node) const662 bool NodeHelper::isNodeDisplayedEmbedded(KMime::Content *node) const
663 {
664 qCDebug(MIMETREEPARSER_LOG) << "IS NODE: " << mDisplayEmbeddedNodes.contains(node);
665 return mDisplayEmbeddedNodes.contains(node);
666 }
667
setNodeDisplayedEmbedded(KMime::Content * node,bool displayedEmbedded)668 void NodeHelper::setNodeDisplayedEmbedded(KMime::Content *node, bool displayedEmbedded)
669 {
670 qCDebug(MIMETREEPARSER_LOG) << "SET NODE: " << node << displayedEmbedded;
671 if (displayedEmbedded) {
672 mDisplayEmbeddedNodes.insert(node);
673 } else {
674 mDisplayEmbeddedNodes.remove(node);
675 }
676 }
677
isNodeDisplayedHidden(KMime::Content * node) const678 bool NodeHelper::isNodeDisplayedHidden(KMime::Content *node) const
679 {
680 return mDisplayHiddenNodes.contains(node);
681 }
682
setNodeDisplayedHidden(KMime::Content * node,bool displayedHidden)683 void NodeHelper::setNodeDisplayedHidden(KMime::Content *node, bool displayedHidden)
684 {
685 if (displayedHidden) {
686 mDisplayHiddenNodes.insert(node);
687 } else {
688 mDisplayEmbeddedNodes.remove(node);
689 }
690 }
691
692 /*!
693 Creates a persistent index string that bridges the gap between the
694 permanent nodes and the temporary ones.
695
696 Used internally for robust indexing.
697 */
persistentIndex(const KMime::Content * node) const698 QString NodeHelper::persistentIndex(const KMime::Content *node) const
699 {
700 if (!node) {
701 return QString();
702 }
703
704 QString indexStr = node->index().toString();
705 if (indexStr.isEmpty()) {
706 QMapIterator<KMime::Message::Content *, QVector<KMime::Content *>> it(mExtraContents);
707 while (it.hasNext()) {
708 it.next();
709 const auto &extraNodes = it.value();
710 for (int i = 0; i < extraNodes.size(); ++i) {
711 if (extraNodes[i] == node) {
712 indexStr = QStringLiteral("e%1").arg(i);
713 const QString parentIndex = persistentIndex(it.key());
714 if (!parentIndex.isEmpty()) {
715 indexStr = QStringLiteral("%1:%2").arg(parentIndex, indexStr);
716 }
717 return indexStr;
718 }
719 }
720 }
721 } else {
722 const KMime::Content *const topLevel = node->topLevel();
723 // if the node is an extra node, prepend the index of the extra node to the url
724 QMapIterator<KMime::Message::Content *, QVector<KMime::Content *>> it(mExtraContents);
725 while (it.hasNext()) {
726 it.next();
727 const QVector<KMime::Content *> &extraNodes = extraContents(it.key());
728 for (int i = 0; i < extraNodes.size(); ++i) {
729 KMime::Content *const extraNode = extraNodes[i];
730 if (topLevel == extraNode) {
731 indexStr.prepend(QStringLiteral("e%1:").arg(i));
732 const QString parentIndex = persistentIndex(it.key());
733 if (!parentIndex.isEmpty()) {
734 indexStr = QStringLiteral("%1:%2").arg(parentIndex, indexStr);
735 }
736 return indexStr;
737 }
738 }
739 }
740 }
741
742 return indexStr;
743 }
744
contentFromIndex(KMime::Content * node,const QString & persistentIndex) const745 KMime::Content *NodeHelper::contentFromIndex(KMime::Content *node, const QString &persistentIndex) const
746 {
747 if (!node) {
748 return nullptr;
749 }
750 KMime::Content *c = node->topLevel();
751 if (c) {
752 const QStringList pathParts = persistentIndex.split(QLatin1Char(':'), Qt::SkipEmptyParts);
753 const int pathPartsSize(pathParts.size());
754 for (int i = 0; i < pathPartsSize; ++i) {
755 const QString &path = pathParts[i];
756 if (path.startsWith(QLatin1Char('e'))) {
757 const QVector<KMime::Content *> &extraParts = mExtraContents.value(c);
758 const int idx = QStringView(path).mid(1).toInt();
759 c = (idx < extraParts.size()) ? extraParts[idx] : nullptr;
760 } else {
761 c = c->content(KMime::ContentIndex(path));
762 }
763 if (!c) {
764 break;
765 }
766 }
767 }
768 return c;
769 }
770
asHREF(const KMime::Content * node,const QString & place) const771 QString NodeHelper::asHREF(const KMime::Content *node, const QString &place) const
772 {
773 return QStringLiteral("attachment:%1?place=%2").arg(persistentIndex(node), place);
774 }
775
fromHREF(const KMime::Message::Ptr & mMessage,const QUrl & url) const776 KMime::Content *NodeHelper::fromHREF(const KMime::Message::Ptr &mMessage, const QUrl &url) const
777 {
778 if (url.isEmpty()) {
779 return mMessage.data();
780 }
781
782 if (!url.isLocalFile()) {
783 return contentFromIndex(mMessage.data(), url.adjusted(QUrl::StripTrailingSlash).path());
784 } else {
785 const QString path = url.toLocalFile();
786 const QString extractedPath = extractAttachmentIndex(path);
787 if (!extractedPath.isEmpty()) {
788 return contentFromIndex(mMessage.data(), extractedPath);
789 }
790 return mMessage.data();
791 }
792 }
793
extractAttachmentIndex(const QString & path) const794 QString NodeHelper::extractAttachmentIndex(const QString &path) const
795 {
796 // extract from /<path>/qttestn28554.index.2.3:0:2/unnamed -> "2.3:0:2"
797 // start of the index is something that is not a number followed by a dot: \D.
798 // index is only made of numbers,"." and ":": ([0-9.:]+)
799 // index is the last part of the folder name: /
800 static const QRegularExpression re(QStringLiteral("\\D\\.([e0-9.:]+)/"));
801 QRegularExpressionMatch rmatch;
802 path.lastIndexOf(re, -1, &rmatch);
803 if (rmatch.hasMatch()) {
804 return rmatch.captured(1);
805 }
806 return QString();
807 }
808
fixEncoding(const QString & encoding)809 QString NodeHelper::fixEncoding(const QString &encoding)
810 {
811 QString returnEncoding = encoding;
812 // According to https://www.iana.org/assignments/character-sets, uppercase is
813 // preferred in MIME headers
814 const QString returnEncodingToUpper = returnEncoding.toUpper();
815 if (returnEncodingToUpper.contains(QLatin1String("ISO "))) {
816 returnEncoding = returnEncodingToUpper;
817 returnEncoding.replace(QLatin1String("ISO "), QStringLiteral("ISO-"));
818 }
819 return returnEncoding;
820 }
821
822 //-----------------------------------------------------------------------------
encodingForName(const QString & descriptiveName)823 QString NodeHelper::encodingForName(const QString &descriptiveName)
824 {
825 const QString encoding = KCharsets::charsets()->encodingForName(descriptiveName);
826 return NodeHelper::fixEncoding(encoding);
827 }
828
supportedEncodings(bool usAscii)829 QStringList NodeHelper::supportedEncodings(bool usAscii)
830 {
831 QStringList encodingNames = KCharsets::charsets()->availableEncodingNames();
832 QStringList encodings;
833 QMap<QString, bool> mimeNames;
834 QStringList::ConstIterator constEnd(encodingNames.constEnd());
835 for (QStringList::ConstIterator it = encodingNames.constBegin(); it != constEnd; ++it) {
836 QTextCodec *codec = KCharsets::charsets()->codecForName(*it);
837 const QString mimeName = (codec) ? QString::fromLatin1(codec->name()).toLower() : (*it);
838 if (!mimeNames.contains(mimeName)) {
839 encodings.append(KCharsets::charsets()->descriptionForEncoding(*it));
840 mimeNames.insert(mimeName, true);
841 }
842 }
843 encodings.sort();
844 if (usAscii) {
845 encodings.prepend(KCharsets::charsets()->descriptionForEncoding(QStringLiteral("us-ascii")));
846 }
847 return encodings;
848 }
849
fromAsString(KMime::Content * node) const850 QString NodeHelper::fromAsString(KMime::Content *node) const
851 {
852 if (auto topLevel = dynamic_cast<KMime::Message *>(node->topLevel())) {
853 return topLevel->from()->asUnicodeString();
854 } else {
855 auto realNode = std::find_if(mExtraContents.cbegin(), mExtraContents.cend(), [node](const QVector<KMime::Content *> &nodes) {
856 return nodes.contains(node);
857 });
858 if (realNode != mExtraContents.cend()) {
859 return fromAsString(realNode.key());
860 }
861 }
862
863 return QString();
864 }
865
attachExtraContent(KMime::Content * topLevelNode,KMime::Content * content)866 void NodeHelper::attachExtraContent(KMime::Content *topLevelNode, KMime::Content *content)
867 {
868 qCDebug(MIMETREEPARSER_LOG) << "mExtraContents added for" << topLevelNode << " extra content: " << content;
869 mExtraContents[topLevelNode].append(content);
870 }
871
cleanExtraContent(KMime::Content * topLevelNode)872 void NodeHelper::cleanExtraContent(KMime::Content *topLevelNode)
873 {
874 qCDebug(MIMETREEPARSER_LOG) << "remove all extraContents for" << topLevelNode;
875 mExtraContents[topLevelNode].clear();
876 }
877
extraContents(KMime::Content * topLevelnode) const878 QVector<KMime::Content *> NodeHelper::extraContents(KMime::Content *topLevelnode) const
879 {
880 return mExtraContents.value(topLevelnode);
881 }
882
mergeExtraNodes(KMime::Content * node)883 void NodeHelper::mergeExtraNodes(KMime::Content *node)
884 {
885 if (!node) {
886 return;
887 }
888
889 const QVector<KMime::Content *> extraNodes = extraContents(node);
890 for (KMime::Content *extra : extraNodes) {
891 if (node->bodyIsMessage()) {
892 qCWarning(MIMETREEPARSER_LOG) << "Asked to attach extra content to a kmime::message, this does not make sense. Attaching to:" << node
893 << node->encodedContent() << "\n====== with =======\n"
894 << extra << extra->encodedContent();
895 continue;
896 }
897 auto c = new KMime::Content(node);
898 c->setContent(extra->encodedContent());
899 c->parse();
900 node->addContent(c);
901 }
902
903 const auto contents{node->contents()};
904 for (KMime::Content *child : contents) {
905 mergeExtraNodes(child);
906 }
907 }
908
cleanFromExtraNodes(KMime::Content * node)909 void NodeHelper::cleanFromExtraNodes(KMime::Content *node)
910 {
911 if (!node) {
912 return;
913 }
914 const QVector<KMime::Content *> extraNodes = extraContents(node);
915 for (KMime::Content *extra : extraNodes) {
916 QByteArray s = extra->encodedContent();
917 const auto children = node->contents();
918 for (KMime::Content *c : children) {
919 if (c->encodedContent() == s) {
920 node->removeContent(c);
921 }
922 }
923 }
924 const auto contents{node->contents()};
925 for (KMime::Content *child : contents) {
926 cleanFromExtraNodes(child);
927 }
928 }
929
messageWithExtraContent(KMime::Content * topLevelNode)930 KMime::Message *NodeHelper::messageWithExtraContent(KMime::Content *topLevelNode)
931 {
932 /*The merge is done in several steps:
933 1) merge the extra nodes into topLevelNode
934 2) copy the modified (merged) node tree into a new node tree
935 3) restore the original node tree in topLevelNode by removing the extra nodes from it
936
937 The reason is that extra nodes are assigned by pointer value to the nodes in the original tree.
938 */
939 if (!topLevelNode) {
940 return nullptr;
941 }
942
943 mergeExtraNodes(topLevelNode);
944
945 auto m = new KMime::Message;
946 m->setContent(topLevelNode->encodedContent());
947 m->parse();
948
949 cleanFromExtraNodes(topLevelNode);
950 // qCDebug(MIMETREEPARSER_LOG) << "MESSAGE WITH EXTRA: " << m->encodedContent();
951 // qCDebug(MIMETREEPARSER_LOG) << "MESSAGE WITHOUT EXTRA: " << topLevelNode->encodedContent();
952
953 return m;
954 }
955
decryptedNodeForContent(KMime::Content * content) const956 KMime::Content *NodeHelper::decryptedNodeForContent(KMime::Content *content) const
957 {
958 const QVector<KMime::Content *> xc = extraContents(content);
959 if (!xc.empty()) {
960 if (xc.size() == 1) {
961 return xc.front();
962 } else {
963 qCWarning(MIMETREEPARSER_LOG) << "WTF, encrypted node has multiple extra contents?";
964 }
965 }
966 return nullptr;
967 }
968
unencryptedMessage_helper(KMime::Content * node,QByteArray & resultingData,bool addHeaders,int recursionLevel)969 bool NodeHelper::unencryptedMessage_helper(KMime::Content *node, QByteArray &resultingData, bool addHeaders, int recursionLevel)
970 {
971 bool returnValue = false;
972 if (node) {
973 KMime::Content *curNode = node;
974 KMime::Content *decryptedNode = nullptr;
975 const QByteArray type = node->contentType(false) ? QByteArray(node->contentType(false)->mediaType()).toLower() : "text";
976 const QByteArray subType = node->contentType(false) ? node->contentType(false)->subType().toLower() : "plain";
977 const bool isMultipart = node->contentType(false) && node->contentType(false)->isMultipart();
978 bool isSignature = false;
979
980 qCDebug(MIMETREEPARSER_LOG) << "(" << recursionLevel << ") Looking at" << type << "/" << subType;
981
982 if (isMultipart) {
983 if (subType == "signed") {
984 isSignature = true;
985 } else if (subType == "encrypted") {
986 decryptedNode = decryptedNodeForContent(curNode);
987 }
988 } else if (type == "application") {
989 if (subType == "octet-stream") {
990 decryptedNode = decryptedNodeForContent(curNode);
991 } else if (subType == "pkcs7-signature") {
992 isSignature = true;
993 } else if (subType == "pkcs7-mime") {
994 // note: subtype pkcs7-mime can also be signed
995 // and we do NOT want to remove the signature!
996 if (encryptionState(curNode) != KMMsgNotEncrypted) {
997 decryptedNode = decryptedNodeForContent(curNode);
998 }
999 }
1000 }
1001
1002 if (decryptedNode) {
1003 qCDebug(MIMETREEPARSER_LOG) << "Current node has an associated decrypted node, adding a modified header "
1004 "and then processing the children.";
1005
1006 Q_ASSERT(addHeaders);
1007 KMime::Content headers;
1008 headers.setHead(curNode->head());
1009 headers.parse();
1010 if (auto ct = decryptedNode->contentType(false)) {
1011 headers.contentType()->from7BitString(ct->as7BitString(false));
1012 } else {
1013 headers.removeHeader<KMime::Headers::ContentType>();
1014 }
1015 if (auto ct = decryptedNode->contentTransferEncoding(false)) {
1016 headers.contentTransferEncoding()->from7BitString(ct->as7BitString(false));
1017 } else {
1018 headers.removeHeader<KMime::Headers::ContentTransferEncoding>();
1019 }
1020 if (auto cd = decryptedNode->contentDisposition(false)) {
1021 headers.contentDisposition()->from7BitString(cd->as7BitString(false));
1022 } else {
1023 headers.removeHeader<KMime::Headers::ContentDisposition>();
1024 }
1025 if (auto cd = decryptedNode->contentDescription(false)) {
1026 headers.contentDescription()->from7BitString(cd->as7BitString(false));
1027 } else {
1028 headers.removeHeader<KMime::Headers::ContentDescription>();
1029 }
1030 headers.assemble();
1031
1032 resultingData += headers.head() + '\n';
1033 unencryptedMessage_helper(decryptedNode, resultingData, false, recursionLevel + 1);
1034
1035 returnValue = true;
1036 } else if (isSignature) {
1037 qCDebug(MIMETREEPARSER_LOG) << "Current node is a signature, adding it as-is.";
1038 // We can't change the nodes under the signature, as that would invalidate it. Add the signature
1039 // and its child as-is
1040 if (addHeaders) {
1041 resultingData += curNode->head() + '\n';
1042 }
1043 resultingData += curNode->encodedBody();
1044 returnValue = false;
1045 } else if (isMultipart) {
1046 qCDebug(MIMETREEPARSER_LOG) << "Current node is a multipart node, adding its header and then processing all children.";
1047 // Normal multipart node, add the header and all of its children
1048 bool somethingChanged = false;
1049 if (addHeaders) {
1050 resultingData += curNode->head() + '\n';
1051 }
1052 const QByteArray boundary = curNode->contentType()->boundary();
1053 const auto contents = curNode->contents();
1054 for (KMime::Content *child : contents) {
1055 resultingData += "\n--" + boundary + '\n';
1056 const bool changed = unencryptedMessage_helper(child, resultingData, true, recursionLevel + 1);
1057 if (changed) {
1058 somethingChanged = true;
1059 }
1060 }
1061 resultingData += "\n--" + boundary + "--\n\n";
1062 returnValue = somethingChanged;
1063 } else if (curNode->bodyIsMessage()) {
1064 qCDebug(MIMETREEPARSER_LOG) << "Current node is a message, adding the header and then processing the child.";
1065 if (addHeaders) {
1066 resultingData += curNode->head() + '\n';
1067 }
1068
1069 returnValue = unencryptedMessage_helper(curNode->bodyAsMessage().data(), resultingData, true, recursionLevel + 1);
1070 } else {
1071 qCDebug(MIMETREEPARSER_LOG) << "Current node is an ordinary leaf node, adding it as-is.";
1072 if (addHeaders) {
1073 resultingData += curNode->head() + '\n';
1074 }
1075 resultingData += curNode->body();
1076 returnValue = false;
1077 }
1078 }
1079
1080 qCDebug(MIMETREEPARSER_LOG) << "(" << recursionLevel << ") done.";
1081 return returnValue;
1082 }
1083
unencryptedMessage(const KMime::Message::Ptr & originalMessage)1084 KMime::Message::Ptr NodeHelper::unencryptedMessage(const KMime::Message::Ptr &originalMessage)
1085 {
1086 QByteArray resultingData;
1087 const bool messageChanged = unencryptedMessage_helper(originalMessage.data(), resultingData, true);
1088 if (messageChanged) {
1089 #if 0
1090 qCDebug(MIMETREEPARSER_LOG) << "Resulting data is:" << resultingData;
1091 QFile bla("stripped.mbox");
1092 bla.open(QIODevice::WriteOnly);
1093 bla.write(resultingData);
1094 bla.close();
1095 #endif
1096 KMime::Message::Ptr newMessage(new KMime::Message);
1097 newMessage->setContent(resultingData);
1098 newMessage->parse();
1099 return newMessage;
1100 } else {
1101 return KMime::Message::Ptr();
1102 }
1103 }
1104
attachmentsOfExtraContents() const1105 QVector<KMime::Content *> NodeHelper::attachmentsOfExtraContents() const
1106 {
1107 QVector<KMime::Content *> result;
1108 for (auto it = mExtraContents.begin(), end = mExtraContents.end(); it != end; ++it) {
1109 const auto contents = it.value();
1110 for (auto content : contents) {
1111 if (KMime::isAttachment(content)) {
1112 result.push_back(content);
1113 } else {
1114 result += content->attachments();
1115 }
1116 }
1117 }
1118 return result;
1119 }
1120 }
1121