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 &param)
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