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 
33 namespace MimeTreeParser
34 {
35 NodeHelper::NodeHelper()
36  : mAttachmentFilesDir(new AttachmentTemporaryFilesDirs())
37 {
38  mListAttachmentTemporaryDirs.append(mAttachmentFilesDir);
39  // TODO(Andras) add methods to modify these prefixes
40 }
41 
42 NodeHelper::~NodeHelper()
43 {
44  for (auto att : mListAttachmentTemporaryDirs) {
45  if (att) {
46  att->forceCleanTempFiles();
47  delete att;
48  }
49  }
50  clear();
51 }
52 
53 void 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 
69 void 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 
100 bool NodeHelper::nodeProcessed(KMime::Content *node) const
101 {
102  if (!node) {
103  return true;
104  }
105  return mProcessedNodes.contains(node);
106 }
107 
108 static 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 
118 void 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 
144 void NodeHelper::setEncryptionState(const KMime::Content *node, const KMMsgEncryptionState state)
145 {
146  mEncryptionState[node] = state;
147 }
148 
149 KMMsgEncryptionState NodeHelper::encryptionState(const KMime::Content *node) const
150 {
151  return mEncryptionState.value(node, KMMsgNotEncrypted);
152 }
153 
154 void NodeHelper::setSignatureState(const KMime::Content *node, const KMMsgSignatureState state)
155 {
156  mSignatureState[node] = state;
157 }
158 
159 KMMsgSignatureState NodeHelper::signatureState(const KMime::Content *node) const
160 {
161  return mSignatureState.value(node, KMMsgNotSigned);
162 }
163 
164 PartMetaData NodeHelper::partMetaData(KMime::Content *node)
165 {
166  return mPartMetaDatas.value(node, PartMetaData());
167 }
168 
169 void NodeHelper::setPartMetaData(KMime::Content *node, const PartMetaData &metaData)
170 {
171  mPartMetaDatas.insert(node, metaData);
172 }
173 
174 QString 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::ReadWrite)) {
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 
197 QString 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::ReadWrite)) {
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 
245 QUrl 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 
269 QString 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 
291 void NodeHelper::forceCleanTempFiles()
292 {
293  mAttachmentFilesDir->forceCleanTempFiles();
294  delete mAttachmentFilesDir;
295  mAttachmentFilesDir = nullptr;
296 }
297 
298 void 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 
308 void NodeHelper::addTempFile(const QString &file)
309 {
310  mAttachmentFilesDir->addTempFile(file);
311 }
312 
313 bool 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 
328 QByteArray 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 
337 KMMsgEncryptionState 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 
398 KMMsgSignatureState 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 
459 void 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 
469 bool 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 
482 QList<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 
496 QList<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 
506 KMime::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 
519 QSharedPointer<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 
530 void NodeHelper::clearOverrideHeaders()
531 {
532  mHeaderOverwrite.clear();
533 }
534 
535 void 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 
543 QDateTime 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 
552 void NodeHelper::setOverrideCodec(KMime::Content *node, const QByteArray &codec)
553 {
554  if (!node) {
555  return;
556  }
557 
558  mOverrideCodecs[node] = codec;
559 }
560 
561 QByteArray 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 
589 QString 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
601 Interface::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
612 void 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 
631 bool NodeHelper::isNodeDisplayedEmbedded(KMime::Content *node) const
632 {
633  qCDebug(MIMETREEPARSER_LOG) << "IS NODE: " << mDisplayEmbeddedNodes.contains(node);
634  return mDisplayEmbeddedNodes.contains(node);
635 }
636 
637 void 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 
647 bool NodeHelper::isNodeDisplayedHidden(KMime::Content *node) const
648 {
649  return mDisplayHiddenNodes.contains(node);
650 }
651 
652 void 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 */
667 QString 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 
714 KMime::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 
740 QString NodeHelper::asHREF(const KMime::Content *node, const QString &place) const
741 {
742  return QStringLiteral("attachment:%1?place=%2").arg(persistentIndex(node), place);
743 }
744 
745 QString NodeHelper::asHREF(const KMime::Content *node) const
746 {
747  return QStringLiteral("attachment:%1").arg(persistentIndex(node));
748 }
749 
750 KMime::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 
768 QString 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 
783 QString 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 //-----------------------------------------------------------------------------
797 QString NodeHelper::encodingForName(const QString &descriptiveName)
798 {
799  const QString encoding = KCharsets::charsets()->encodingForName(descriptiveName);
800  return NodeHelper::fixEncoding(encoding);
801 }
802 
803 QStringList 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 
824 QString 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 
840 void 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 
846 void NodeHelper::cleanExtraContent(KMime::Content *topLevelNode)
847 {
848  qCDebug(MIMETREEPARSER_LOG) << "remove all extraContents for" << topLevelNode;
849  mExtraContents[topLevelNode].clear();
850 }
851 
852 QList<KMime::Content *> NodeHelper::extraContents(KMime::Content *topLevelnode) const
853 {
854  return mExtraContents.value(topLevelnode);
855 }
856 
857 void 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 
883 void 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 
904 KMime::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 
930 KMime::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 
943 bool 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 
1058 KMime::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 
1079 QList<KMime::Content *> NodeHelper::attachmentsOfExtraContents() const
1080 {
1081  QList<KMime::Content *> result;
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"
void append(const T &value)
Content * content(const ContentIndex &index) const
QByteArray encodedBody()
QByteArray body() const
bool isNull() const const
QString errorString() const const
T * data() const const
bool removeHeader()
bool contains(const Key &key) const const
QByteArray toLower() const const
QString toUpper() const const
QString encodingForName(const QString &descriptiveName) const
QByteArray mimeType() const
QList< KMime::Content * > List
Content * parent() const
virtual QFileDevice::Permissions permissions() const const override
virtual bool open(QIODevice::OpenMode mode) override
void clear()
const T value(const Key &key, const T &defaultValue) const const
QStringList split(const QString &sep, QString::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
bool bodyIsMessage() const
QMap::iterator begin()
void from7BitString(const char *s, size_t len) override
void appendContent(Content *content)
QString trimmed() const const
QStringView mid(qsizetype start) const const
QString & prepend(QChar ch)
virtual bool setPermissions(QFileDevice::Permissions permissions) override
void setHead(const QByteArray &head)
ContentIndex index() const
void push_back(const T &value)
QList::const_iterator constBegin() const const
QList< Content * > contents() const
QString toString() const
void setMimeType(const QByteArray &mimeType)
int lastIndexOf(QChar ch, int from, Qt::CaseSensitivity cs) const const
QByteArray boundary() const
Content * topLevel() const
QByteArray mediaType() const
QString tempPath()
QMimeType mimeTypeForData(const QByteArray &data) const const
QMap::iterator insert(const Key &key, const T &value)
bool empty() const const
int size() const const
QMap::iterator end()
void prepend(const T &value)
QMap::iterator find(const Key &key)
bool isEmpty() const const
QByteArray encodedContent(bool useCrLf=false)
StripTrailingSlash
SkipEmptyParts
bool isEmpty() const const
QUrl fromLocalFile(const QString &localFile)
QMap::iterator erase(QMap::iterator pos)
Headers::ContentDisposition * contentDisposition(bool create=true)
Headers::ContentTransferEncoding * contentTransferEncoding(bool create=true)
QString toLocalFile() const const
bool hasMatch() const const
QByteArray head() const
QByteArray subType() const
QStringList availableEncodingNames() const
virtual void close() override
QMap::iterator lowerBound(const Key &key)
static QByteArray defaultCharset()
interface of classes that implement status for BodyPartFormatters.
Definition: bodypart.h:33
Content * takeContent(Content *content)
const Key key(const T &value, const Key &defaultKey) const const
QString & replace(int position, int n, QChar after)
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const const
typedef ConstIterator
QByteArray decodedContent()
QSharedPointer< Message > bodyAsMessage() const
void setContent(const QByteArray &s)
QAction * clear(const QObject *recvr, const char *slot, QObject *parent)
QByteArray charset() const
QByteArray as7BitString(bool withHeaderType=true) const override
bool isEmpty() const const
QList::const_iterator constEnd() const const
QString path(QUrl::ComponentFormattingOptions options) const const
static KCharsets * charsets()
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
QString path(const QString &relativePath)
QString fromLatin1(const char *str, int size)
contentDisposition
bool isLocalFile() const const
QUrl adjusted(QUrl::FormattingOptions options) const const
virtual void from7BitString(const char *s, size_t len)
Headers::ContentType * contentType(bool create=true)
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
QAction * next(const QObject *recvr, const char *slot, QObject *parent)
QString mid(int position, int n) const const
T & front()
QList< Content * > attachments()
const QList< QKeySequence > & end()
T value(int i) const const
QString captured(int nth) const const
Headers::ContentDescription * contentDescription(bool create=true)
qint64 write(const char *data, qint64 maxSize)
QString descriptionForEncoding(QStringView encoding) const
void sort(Qt::CaseSensitivity cs)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Thu Feb 15 2024 03:55:21 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.