Messagelib

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

KDE's Doxygen guidelines are available online.