Messagelib

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 = cur->parent() && cur->parent()->contentType() && cur->parent()->contentType()->mimeType().toLower() == "message/rfc822";
319 if (parentIsMessage && cur->parent() != topLevel) {
320 return true;
321 }
322 cur = cur->parent();
323 }
324 return false;
325}
326
327QByteArray NodeHelper::charset(const KMime::Content *node)
328{
329 if (const auto ct = node->contentType(); ct) {
330 return ct->charset();
331 }
332 return QByteArrayLiteral("utf-8");
333}
334
335KMMsgEncryptionState NodeHelper::overallEncryptionState(KMime::Content *node) const
336{
337 KMMsgEncryptionState myState = KMMsgEncryptionStateUnknown;
338 if (!node) {
339 return myState;
340 }
341
342 KMime::Content *parent = node->parent();
343 auto contents = parent ? parent->contents() : KMime::Content::List();
344 if (contents.isEmpty()) {
345 contents.append(node);
346 }
347 int i = contents.indexOf(const_cast<KMime::Content *>(node));
348 if (i < 0) {
349 return myState;
350 }
351 for (; i < contents.size(); ++i) {
352 auto next = contents.at(i);
353 KMMsgEncryptionState otherState = encryptionState(next);
354
355 // NOTE: children are tested ONLY when parent is not encrypted
356 if (otherState == KMMsgNotEncrypted && !next->contents().isEmpty()) {
357 otherState = overallEncryptionState(next->contents().at(0));
358 }
359
360 if (otherState == KMMsgNotEncrypted && !extraContents(next).isEmpty()) {
361 otherState = overallEncryptionState(extraContents(next).at(0));
362 }
363
364 if (next == node) {
365 myState = otherState;
366 }
367
368 switch (otherState) {
369 case KMMsgEncryptionStateUnknown:
370 break;
371 case KMMsgNotEncrypted:
372 if (myState == KMMsgFullyEncrypted) {
373 myState = KMMsgPartiallyEncrypted;
374 } else if (myState != KMMsgPartiallyEncrypted) {
375 myState = KMMsgNotEncrypted;
376 }
377 break;
378 case KMMsgPartiallyEncrypted:
379 myState = KMMsgPartiallyEncrypted;
380 break;
381 case KMMsgFullyEncrypted:
382 if (myState != KMMsgFullyEncrypted) {
383 myState = KMMsgPartiallyEncrypted;
384 }
385 break;
386 case KMMsgEncryptionProblematic:
387 break;
388 }
389 }
390
391 qCDebug(MIMETREEPARSER_LOG) << "\n\n KMMsgEncryptionState:" << myState;
392
393 return myState;
394}
395
396KMMsgSignatureState NodeHelper::overallSignatureState(KMime::Content *node) const
397{
398 KMMsgSignatureState myState = KMMsgSignatureStateUnknown;
399 if (!node) {
400 return myState;
401 }
402
403 KMime::Content *parent = node->parent();
404 auto contents = parent ? parent->contents() : KMime::Content::List();
405 if (contents.isEmpty()) {
406 contents.append(node);
407 }
408 int i = contents.indexOf(const_cast<KMime::Content *>(node));
409 if (i < 0) { // Be safe
410 return myState;
411 }
412 for (; i < contents.size(); ++i) {
413 auto next = contents.at(i);
414 KMMsgSignatureState otherState = signatureState(next);
415
416 // NOTE: children are tested ONLY when parent is not encrypted
417 if (otherState == KMMsgNotSigned && !next->contents().isEmpty()) {
418 otherState = overallSignatureState(next->contents().at(0));
419 }
420
421 if (otherState == KMMsgNotSigned && !extraContents(next).isEmpty()) {
422 otherState = overallSignatureState(extraContents(next).at(0));
423 }
424
425 if (next == node) {
426 myState = otherState;
427 }
428
429 switch (otherState) {
430 case KMMsgSignatureStateUnknown:
431 break;
432 case KMMsgNotSigned:
433 if (myState == KMMsgFullySigned) {
434 myState = KMMsgPartiallySigned;
435 } else if (myState != KMMsgPartiallySigned) {
436 myState = KMMsgNotSigned;
437 }
438 break;
439 case KMMsgPartiallySigned:
440 myState = KMMsgPartiallySigned;
441 break;
442 case KMMsgFullySigned:
443 if (myState != KMMsgFullySigned) {
444 myState = KMMsgPartiallySigned;
445 }
446 break;
447 case KMMsgSignatureProblematic:
448 break;
449 }
450 }
451
452 qCDebug(MIMETREEPARSER_LOG) << "\n\n KMMsgSignatureState:" << myState;
453
454 return myState;
455}
456
457void NodeHelper::magicSetType(KMime::Content *node, bool aAutoDecode)
458{
459 const QByteArray body = aAutoDecode ? node->decodedContent() : node->body();
460 QMimeDatabase db;
461 const QMimeType mime = db.mimeTypeForData(body);
462
463 const QString mimetype = mime.name();
464 node->contentType()->setMimeType(mimetype.toLatin1());
465}
466
467bool NodeHelper::hasMailHeader(const char *header, const KMime::Content *message) const
468{
469 if (mHeaderOverwrite.contains(message)) {
470 const auto parts = mHeaderOverwrite.value(message);
471 for (const auto &messagePart : parts) {
472 if (messagePart->hasHeader(header)) {
473 return true;
474 }
475 }
476 }
477 return message->hasHeader(header);
478}
479
480QList<MessagePart::Ptr> NodeHelper::messagePartsOfMailHeader(const char *header, const KMime::Content *message) const
481{
483 if (mHeaderOverwrite.contains(message)) {
484 const auto parts = mHeaderOverwrite.value(message);
485 for (const auto &messagePart : parts) {
486 if (messagePart->hasHeader(header)) {
487 ret << messagePart;
488 }
489 }
490 }
491 return ret;
492}
493
494QList<KMime::Headers::Base *> NodeHelper::headers(const char *header, const KMime::Content *message)
495{
496 const auto mp = messagePartsOfMailHeader(header, message);
497 if (mp.size() > 0) {
498 return mp.value(0)->headers(header);
499 }
500
501 return message->headersByType(header);
502}
503
504KMime::Headers::Base const *NodeHelper::mailHeaderAsBase(const char *header, const KMime::Content *message) const
505{
506 if (mHeaderOverwrite.contains(message)) {
507 const auto parts = mHeaderOverwrite.value(message);
508 for (const auto &messagePart : parts) {
509 if (messagePart->hasHeader(header)) {
510 return messagePart->header(header); // Found.
511 }
512 }
513 }
514 return message->headerByType(header);
515}
516
517QSharedPointer<KMime::Headers::Generics::AddressList> NodeHelper::mailHeaderAsAddressList(const char *header, const KMime::Content *message) const
518{
519 const auto hrd = mailHeaderAsBase(header, message);
520 if (!hrd) {
521 return nullptr;
522 }
524 addressList->from7BitString(hrd->as7BitString(false));
525 return addressList;
526}
527
528void NodeHelper::clearOverrideHeaders()
529{
530 mHeaderOverwrite.clear();
531}
532
533void NodeHelper::registerOverrideHeader(KMime::Content *message, MessagePart::Ptr part)
534{
535 if (!mHeaderOverwrite.contains(message)) {
536 mHeaderOverwrite[message] = QList<MessagePart::Ptr>();
537 }
538 mHeaderOverwrite[message].append(part);
539}
540
541QDateTime NodeHelper::dateHeader(KMime::Content *message) const
542{
543 const auto dateHeader = mailHeaderAsBase("date", message);
544 if (dateHeader != nullptr) {
545 return static_cast<const KMime::Headers::Date *>(dateHeader)->dateTime();
546 }
547 return {};
548}
549
550void NodeHelper::setOverrideCodec(KMime::Content *node, const QByteArray &codec)
551{
552 if (!node) {
553 return;
554 }
555
556 mOverrideCodecs[node] = codec;
557}
558
559QByteArray NodeHelper::codecName(KMime::Content *node) const
560{
561 if (!node || !node->contentType()) {
562 return "utf-8";
563 }
564
565 auto c = mOverrideCodecs.value(node);
566 if (c.isEmpty()) {
567 // no override-codec set for this message, try the CT charset parameter:
568 c = node->contentType()->charset();
569
570 // utf-8 is a superset of us-ascii, so we don't loose anything, if we it insead
571 // utf-8 is nowadays that widely, that it is a good guess to use it to fix issus with broken clients.
572 if (c.toLower() == "us-ascii") {
573 c = "utf-8";
574 }
575 if (QStringDecoder codec(c.constData()); !codec.isValid()) {
576 c = "utf-8";
577 }
578 }
579 if (c.isEmpty()) {
580 // no charset means us-ascii (RFC 2045), so using local encoding should
581 // be okay
582 c = "UTF-8";
583 }
584 return c;
585}
586
587QString NodeHelper::fileName(const KMime::Content *node)
588{
589 QString name = const_cast<KMime::Content *>(node)->contentDisposition()->filename();
590 if (name.isEmpty()) {
591 name = const_cast<KMime::Content *>(node)->contentType()->name();
592 }
593
594 name = name.trimmed();
595 return name;
596}
597
598// FIXME(Andras) review it (by Marc?) to see if I got it right. This is supposed to be the partNode::internalBodyPartMemento replacement
599Interface::BodyPartMemento *NodeHelper::bodyPartMemento(KMime::Content *node, const QByteArray &which) const
600{
601 const QMap<QString, QMap<QByteArray, Interface::BodyPartMemento *>>::const_iterator nit = mBodyPartMementoMap.find(persistentIndex(node));
602 if (nit == mBodyPartMementoMap.end()) {
603 return nullptr;
604 }
606 return it != nit->end() ? it.value() : nullptr;
607}
608
609// FIXME(Andras) review it (by Marc?) to see if I got it right. This is supposed to be the partNode::internalSetBodyPartMemento replacement
610void NodeHelper::setBodyPartMemento(KMime::Content *node, const QByteArray &which, Interface::BodyPartMemento *memento)
611{
612 QMap<QByteArray, Interface::BodyPartMemento *> &mementos = mBodyPartMementoMap[persistentIndex(node)];
613
614 const QByteArray whichLower = which.toLower();
616
617 if (it != mementos.end() && it.key() == whichLower) {
618 delete it.value();
619 if (memento) {
620 it.value() = memento;
621 } else {
622 mementos.erase(it);
623 }
624 } else {
625 mementos.insert(whichLower, memento);
626 }
627}
628
629bool NodeHelper::isNodeDisplayedEmbedded(KMime::Content *node) const
630{
631 qCDebug(MIMETREEPARSER_LOG) << "IS NODE: " << mDisplayEmbeddedNodes.contains(node);
632 return mDisplayEmbeddedNodes.contains(node);
633}
634
635void NodeHelper::setNodeDisplayedEmbedded(KMime::Content *node, bool displayedEmbedded)
636{
637 qCDebug(MIMETREEPARSER_LOG) << "SET NODE: " << node << displayedEmbedded;
638 if (displayedEmbedded) {
639 mDisplayEmbeddedNodes.insert(node);
640 } else {
641 mDisplayEmbeddedNodes.remove(node);
642 }
643}
644
645bool NodeHelper::isNodeDisplayedHidden(KMime::Content *node) const
646{
647 return mDisplayHiddenNodes.contains(node);
648}
649
650void NodeHelper::setNodeDisplayedHidden(KMime::Content *node, bool displayedHidden)
651{
652 if (displayedHidden) {
653 mDisplayHiddenNodes.insert(node);
654 } else {
655 mDisplayEmbeddedNodes.remove(node);
656 }
657}
658
659/*!
660 Creates a persistent index string that bridges the gap between the
661 permanent nodes and the temporary ones.
662
663 Used internally for robust indexing.
664*/
665QString NodeHelper::persistentIndex(const KMime::Content *node) const
666{
667 if (!node) {
668 return {};
669 }
670
671 QString indexStr = node->index().toString();
672 if (indexStr.isEmpty()) {
674 while (it.hasNext()) {
675 it.next();
676 const auto &extraNodes = it.value();
677 for (int i = 0; i < extraNodes.size(); ++i) {
678 if (extraNodes[i] == node) {
679 indexStr = QStringLiteral("e%1").arg(i);
680 const QString parentIndex = persistentIndex(it.key());
681 if (!parentIndex.isEmpty()) {
682 indexStr = QStringLiteral("%1:%2").arg(parentIndex, indexStr);
683 }
684 return indexStr;
685 }
686 }
687 }
688 } else {
689 const KMime::Content *const topLevel = node->topLevel();
690 // if the node is an extra node, prepend the index of the extra node to the url
692 while (it.hasNext()) {
693 it.next();
694 const QList<KMime::Content *> &extraNodes = extraContents(it.key());
695 for (int i = 0; i < extraNodes.size(); ++i) {
696 KMime::Content *const extraNode = extraNodes[i];
697 if (topLevel == extraNode) {
698 indexStr.prepend(QStringLiteral("e%1:").arg(i));
699 const QString parentIndex = persistentIndex(it.key());
700 if (!parentIndex.isEmpty()) {
701 indexStr = QStringLiteral("%1:%2").arg(parentIndex, indexStr);
702 }
703 return indexStr;
704 }
705 }
706 }
707 }
708
709 return indexStr;
710}
711
712KMime::Content *NodeHelper::contentFromIndex(KMime::Content *node, const QString &persistentIndex) const
713{
714 if (!node) {
715 return nullptr;
716 }
717 KMime::Content *c = node->topLevel();
718 if (c) {
719 const QStringList pathParts = persistentIndex.split(QLatin1Char(':'), Qt::SkipEmptyParts);
720 const int pathPartsSize(pathParts.size());
721 for (int i = 0; i < pathPartsSize; ++i) {
722 const QString &path = pathParts[i];
723 if (path.startsWith(QLatin1Char('e'))) {
724 const QList<KMime::Content *> &extraParts = mExtraContents.value(c);
725 const int idx = QStringView(path).mid(1).toInt();
726 c = (idx < extraParts.size()) ? extraParts[idx] : nullptr;
727 } else {
728 c = c->content(KMime::ContentIndex(path));
729 }
730 if (!c) {
731 break;
732 }
733 }
734 }
735 return c;
736}
737
738QString NodeHelper::asHREF(const KMime::Content *node, const QString &place) const
739{
740 return QStringLiteral("attachment:%1?place=%2").arg(persistentIndex(node), place);
741}
742
743QString NodeHelper::asHREF(const KMime::Content *node) const
744{
745 return QStringLiteral("attachment:%1").arg(persistentIndex(node));
746}
747
748KMime::Content *NodeHelper::fromHREF(const KMime::Message::Ptr &mMessage, const QUrl &url) const
749{
750 if (url.isEmpty()) {
751 return mMessage.data();
752 }
753
754 if (!url.isLocalFile()) {
755 return contentFromIndex(mMessage.data(), url.adjusted(QUrl::StripTrailingSlash).path());
756 } else {
757 const QString path = url.toLocalFile();
758 const QString extractedPath = extractAttachmentIndex(path);
759 if (!extractedPath.isEmpty()) {
760 return contentFromIndex(mMessage.data(), extractedPath);
761 }
762 return mMessage.data();
763 }
764}
765
766QString NodeHelper::extractAttachmentIndex(const QString &path) const
767{
768 // extract from /<path>/qttestn28554.index.2.3:0:2/unnamed -> "2.3:0:2"
769 // start of the index is something that is not a number followed by a dot: \D.
770 // index is only made of numbers,"." and ":": ([0-9.:]+)
771 // index is the last part of the folder name: /
772 static const QRegularExpression re(QStringLiteral("\\D\\.([e0-9.:]+)/"));
774 path.lastIndexOf(re, -1, &rmatch);
775 if (rmatch.hasMatch()) {
776 return rmatch.captured(1);
777 }
778 return {};
779}
780
781QString NodeHelper::fixEncoding(const QString &encoding)
782{
783 QString returnEncoding = encoding;
784 // According to https://www.iana.org/assignments/character-sets, uppercase is
785 // preferred in MIME headers
786 const QString returnEncodingToUpper = returnEncoding.toUpper();
787 if (returnEncodingToUpper.contains(QLatin1StringView("ISO "))) {
788 returnEncoding = returnEncodingToUpper;
789 returnEncoding.replace(QLatin1StringView("ISO "), QStringLiteral("ISO-"));
790 }
791 return returnEncoding;
792}
793
794//-----------------------------------------------------------------------------
795QString NodeHelper::encodingForName(const QString &descriptiveName)
796{
797 const QString encoding = KCharsets::charsets()->encodingForName(descriptiveName);
798 return NodeHelper::fixEncoding(encoding);
799}
800
801QStringList NodeHelper::supportedEncodings()
802{
804 QStringList encodings;
805 QMap<QString, bool> mimeNames;
806 QStringList::ConstIterator constEnd(encodingNames.constEnd());
807 for (QStringList::ConstIterator it = encodingNames.constBegin(); it != constEnd; ++it) {
808 QStringDecoder codec((*it).toLatin1().constData());
809 const QString mimeName = (codec.isValid()) ? QString::fromLatin1(codec.name()).toLower() : (*it);
810 if (!mimeNames.contains(mimeName)) {
811 encodings.append(KCharsets::charsets()->descriptionForEncoding(*it));
812 mimeNames.insert(mimeName, true);
813 }
814 }
815 encodings.sort();
816 return encodings;
817}
818
819QString NodeHelper::fromAsString(KMime::Content *node) const
820{
821 if (auto topLevel = dynamic_cast<KMime::Message *>(node->topLevel())) {
822 return topLevel->from()->asUnicodeString();
823 } else {
824 auto realNode = std::find_if(mExtraContents.cbegin(), mExtraContents.cend(), [node](const QList<KMime::Content *> &nodes) {
825 return nodes.contains(node);
826 });
827 if (realNode != mExtraContents.cend()) {
828 return fromAsString(realNode.key());
829 }
830 }
831
832 return {};
833}
834
835void NodeHelper::attachExtraContent(KMime::Content *topLevelNode, KMime::Content *content)
836{
837 qCDebug(MIMETREEPARSER_LOG) << "mExtraContents added for" << topLevelNode << " extra content: " << content;
838 mExtraContents[topLevelNode].append(content);
839}
840
841void NodeHelper::cleanExtraContent(KMime::Content *topLevelNode)
842{
843 qCDebug(MIMETREEPARSER_LOG) << "remove all extraContents for" << topLevelNode;
844 mExtraContents[topLevelNode].clear();
845}
846
847QList<KMime::Content *> NodeHelper::extraContents(KMime::Content *topLevelnode) const
848{
849 return mExtraContents.value(topLevelnode);
850}
851
852void NodeHelper::mergeExtraNodes(KMime::Content *node)
853{
854 if (!node) {
855 return;
856 }
857
858 const QList<KMime::Content *> extraNodes = extraContents(node);
859 for (KMime::Content *extra : extraNodes) {
860 if (node->bodyIsMessage()) {
861 qCWarning(MIMETREEPARSER_LOG) << "Asked to attach extra content to a kmime::message, this does not make sense. Attaching to:" << node
862 << node->encodedContent() << "\n====== with =======\n"
863 << extra << extra->encodedContent();
864 continue;
865 }
866 auto c = new KMime::Content(node);
867 c->setContent(extra->encodedContent());
868 c->parse();
869 node->appendContent(c);
870 }
871
872 const auto contents{node->contents()};
873 for (KMime::Content *child : contents) {
874 mergeExtraNodes(child);
875 }
876}
877
878void NodeHelper::cleanFromExtraNodes(KMime::Content *node)
879{
880 if (!node) {
881 return;
882 }
883 const QList<KMime::Content *> extraNodes = extraContents(node);
884 for (KMime::Content *extra : extraNodes) {
885 QByteArray s = extra->encodedContent();
886 const auto children = node->contents();
887 for (KMime::Content *c : children) {
888 if (c->encodedContent() == s) {
889 node->takeContent(c);
890 }
891 }
892 }
893 const auto contents{node->contents()};
894 for (KMime::Content *child : contents) {
895 cleanFromExtraNodes(child);
896 }
897}
898
899KMime::Message *NodeHelper::messageWithExtraContent(KMime::Content *topLevelNode)
900{
901 /*The merge is done in several steps:
902 1) merge the extra nodes into topLevelNode
903 2) copy the modified (merged) node tree into a new node tree
904 3) restore the original node tree in topLevelNode by removing the extra nodes from it
905
906 The reason is that extra nodes are assigned by pointer value to the nodes in the original tree.
907 */
908 if (!topLevelNode) {
909 return nullptr;
910 }
911
912 mergeExtraNodes(topLevelNode);
913
914 auto m = new KMime::Message;
915 m->setContent(topLevelNode->encodedContent());
916 m->parse();
917
918 cleanFromExtraNodes(topLevelNode);
919 // qCDebug(MIMETREEPARSER_LOG) << "MESSAGE WITH EXTRA: " << m->encodedContent();
920 // qCDebug(MIMETREEPARSER_LOG) << "MESSAGE WITHOUT EXTRA: " << topLevelNode->encodedContent();
921
922 return m;
923}
924
925KMime::Content *NodeHelper::decryptedNodeForContent(KMime::Content *content) const
926{
927 const QList<KMime::Content *> xc = extraContents(content);
928 if (!xc.empty()) {
929 if (xc.size() == 1) {
930 return xc.front();
931 } else {
932 qCWarning(MIMETREEPARSER_LOG) << "WTF, encrypted node has multiple extra contents?";
933 }
934 }
935 return nullptr;
936}
937
938bool NodeHelper::unencryptedMessage_helper(KMime::Content *node, QByteArray &resultingData, bool addHeaders, int recursionLevel)
939{
940 bool returnValue = false;
941 if (node) {
942 KMime::Content *curNode = node;
943 KMime::Content *decryptedNode = nullptr;
944 const QByteArray type = node->contentType(false) ? QByteArray(node->contentType(false)->mediaType()).toLower() : "text";
945 const QByteArray subType = node->contentType(false) ? node->contentType(false)->subType().toLower() : "plain";
946 const bool isMultipart = node->contentType(false) && node->contentType(false)->isMultipart();
947 bool isSignature = false;
948
949 qCDebug(MIMETREEPARSER_LOG) << "(" << recursionLevel << ") Looking at" << type << "/" << subType;
950
951 if (isMultipart) {
952 if (subType == "signed") {
953 isSignature = true;
954 } else if (subType == "encrypted") {
955 decryptedNode = decryptedNodeForContent(curNode);
956 }
957 } else if (type == "application") {
958 if (subType == "octet-stream") {
959 decryptedNode = decryptedNodeForContent(curNode);
960 } else if (subType == "pkcs7-signature") {
961 isSignature = true;
962 } else if (subType == "pkcs7-mime") {
963 // note: subtype pkcs7-mime can also be signed
964 // and we do NOT want to remove the signature!
965 if (encryptionState(curNode) != KMMsgNotEncrypted) {
966 decryptedNode = decryptedNodeForContent(curNode);
967 }
968 }
969 }
970
971 if (decryptedNode) {
972 qCDebug(MIMETREEPARSER_LOG) << "Current node has an associated decrypted node, adding a modified header "
973 "and then processing the children.";
974
975 Q_ASSERT(addHeaders);
976 KMime::Content headers;
977 headers.setHead(curNode->head());
978 headers.parse();
979 if (auto ct = decryptedNode->contentType(false)) {
980 headers.contentType()->from7BitString(ct->as7BitString(false));
981 } else {
983 }
984 if (auto ct = decryptedNode->contentTransferEncoding(false)) {
985 headers.contentTransferEncoding()->from7BitString(ct->as7BitString(false));
986 } else {
988 }
989 if (auto cd = decryptedNode->contentDisposition(false)) {
990 headers.contentDisposition()->from7BitString(cd->as7BitString(false));
991 } else {
993 }
994 if (auto cd = decryptedNode->contentDescription(false)) {
995 headers.contentDescription()->from7BitString(cd->as7BitString(false));
996 } else {
998 }
999 headers.assemble();
1000
1001 resultingData += headers.head() + '\n';
1002 unencryptedMessage_helper(decryptedNode, resultingData, false, recursionLevel + 1);
1003
1004 returnValue = true;
1005 } else if (isSignature) {
1006 qCDebug(MIMETREEPARSER_LOG) << "Current node is a signature, adding it as-is.";
1007 // We can't change the nodes under the signature, as that would invalidate it. Add the signature
1008 // and its child as-is
1009 if (addHeaders) {
1010 resultingData += curNode->head() + '\n';
1011 }
1012 resultingData += curNode->encodedBody();
1013 returnValue = false;
1014 } else if (isMultipart) {
1015 qCDebug(MIMETREEPARSER_LOG) << "Current node is a multipart node, adding its header and then processing all children.";
1016 // Normal multipart node, add the header and all of its children
1017 bool somethingChanged = false;
1018 if (addHeaders) {
1019 resultingData += curNode->head() + '\n';
1020 }
1021 const QByteArray boundary = curNode->contentType()->boundary();
1022 const auto contents = curNode->contents();
1023 for (KMime::Content *child : contents) {
1024 resultingData += "\n--" + boundary + '\n';
1025 const bool changed = unencryptedMessage_helper(child, resultingData, true, recursionLevel + 1);
1026 if (changed) {
1027 somethingChanged = true;
1028 }
1029 }
1030 resultingData += "\n--" + boundary + "--\n\n";
1031 returnValue = somethingChanged;
1032 } else if (curNode->bodyIsMessage()) {
1033 qCDebug(MIMETREEPARSER_LOG) << "Current node is a message, adding the header and then processing the child.";
1034 if (addHeaders) {
1035 resultingData += curNode->head() + '\n';
1036 }
1037
1038 returnValue = unencryptedMessage_helper(curNode->bodyAsMessage().data(), resultingData, true, recursionLevel + 1);
1039 } else {
1040 qCDebug(MIMETREEPARSER_LOG) << "Current node is an ordinary leaf node, adding it as-is.";
1041 if (addHeaders) {
1042 resultingData += curNode->head() + '\n';
1043 }
1044 resultingData += curNode->body();
1045 returnValue = false;
1046 }
1047 }
1048
1049 qCDebug(MIMETREEPARSER_LOG) << "(" << recursionLevel << ") done.";
1050 return returnValue;
1051}
1052
1053KMime::Message::Ptr NodeHelper::unencryptedMessage(const KMime::Message::Ptr &originalMessage)
1054{
1055 QByteArray resultingData;
1056 const bool messageChanged = unencryptedMessage_helper(originalMessage.data(), resultingData, true);
1057 if (messageChanged) {
1058#if 0
1059 qCDebug(MIMETREEPARSER_LOG) << "Resulting data is:" << resultingData;
1060 QFile bla("stripped.mbox");
1062 bla.write(resultingData);
1063 bla.close();
1064#endif
1065 KMime::Message::Ptr newMessage(new KMime::Message);
1066 newMessage->setContent(resultingData);
1067 newMessage->parse();
1068 return newMessage;
1069 } else {
1070 return {};
1071 }
1072}
1073
1074QList<KMime::Content *> NodeHelper::attachmentsOfExtraContents() const
1075{
1077 for (auto it = mExtraContents.begin(), end = mExtraContents.end(); it != end; ++it) {
1078 const auto contents = it.value();
1079 for (auto content : contents) {
1080 if (KMime::isAttachment(content)) {
1081 result.push_back(content);
1082 } else {
1083 result += content->attachments();
1084 }
1085 }
1086 }
1087 return result;
1088}
1089}
1090
1091#include "moc_nodehelper.cpp"
QString encodingForName(const QString &descriptiveName) const
QStringList availableEncodingNames() const
static KCharsets * charsets()
QString toString() const
bool removeHeader()
const Headers::ContentType * contentType() const
QByteArray encodedBody() const
Content * takeContent(Content *content)
ContentIndex index() const
QList< KMime::Content * > List
Content * parent()
QByteArray head() const
QByteArray decodedContent() const
QSharedPointer< Message > bodyAsMessage()
Content * content(const ContentIndex &index) const
QByteArray body() const
bool bodyIsMessage() const
const Headers::ContentTransferEncoding * contentTransferEncoding() const
void setContent(const QByteArray &s)
const Headers::ContentDisposition * contentDisposition() const
void appendContent(Content *content)
QList< Content * > attachments()
Content * topLevel()
void setHead(const QByteArray &head)
QByteArray encodedContent(bool useCrLf=false) const
QList< Content * > contents()
const Headers::ContentDescription * contentDescription() const
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(QByteArrayView s) override
void from7BitString(QByteArrayView s) override
interface of classes that implement status for BodyPartFormatters.
Definition bodypart.h:34
QAction * end(const QObject *recvr, const char *slot, QObject *parent)
QString path(const QString &relativePath)
QAction * next(const QObject *recvr, const char *slot, QObject *parent)
KGuiItem clear()
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)
void append(QList< T > &&value)
const_iterator constBegin() const const
const_iterator constEnd() const const
bool empty() const const
reference front()
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 toLower() 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 Mon Nov 4 2024 16:33:26 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.