Messagelib

messageviewerutil.cpp
1 /*******************************************************************************
2 **
3 ** Filename : util
4 ** Created on : 03 April, 2005
5 ** SPDX-FileCopyrightText : 2005 Till Adam <[email protected]>
6 **
7 *******************************************************************************/
8 
9 /*******************************************************************************
10 **
11 ** This program is free software; you can redistribute it and/or modify
12 ** it under the terms of the GNU General Public License as published by
13 ** the Free Software Foundation; either version 2 of the License, or
14 ** (at your option) any later version.
15 **
16 ** It is distributed in the hope that it will be useful, but
17 ** WITHOUT ANY WARRANTY; without even the implied warranty of
18 ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
19 ** General Public License for more details.
20 **
21 ** You should have received a copy of the GNU General Public License
22 ** along with this program; if not, write to the Free Software
23 ** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
24 **
25 ** In addition, as a special exception, the copyright holders give
26 ** permission to link the code of this program with any edition of
27 ** the Qt library by Trolltech AS, Norway (or with modified versions
28 ** of Qt that use the same license as Qt), and distribute linked
29 ** combinations including the two. You must obey the GNU General
30 ** Public License in all respects for all of the code used other than
31 ** Qt. If you modify this file, you may extend this exception to
32 ** your version of the file, but you are not obligated to do so. If
33 ** you do not wish to do so, delete this exception statement from
34 ** your version.
35 **
36 *******************************************************************************/
37 
38 #include "messageviewer/messageviewerutil.h"
39 #include "MessageCore/MessageCoreSettings"
40 #include "MessageCore/NodeHelper"
41 #include "MessageCore/StringUtil"
42 #include "messageviewer_debug.h"
43 #include "messageviewerutil_p.h"
44 #include <KPIMTextEdit/TextToSpeech>
45 #include <MimeTreeParser/NodeHelper>
46 
47 #include <PimCommon/RenameFileDialog>
48 
49 #include <Gravatar/GravatarCache>
50 #include <gravatar/gravatarsettings.h>
51 
52 
53 #include <KMbox/MBox>
54 
55 #include <KCharsets>
56 #include <KFileWidget>
57 #include <KIO/FileCopyJob>
58 #include <KIO/StatJob>
59 #include <KJobWidgets>
60 #include <KLocalizedString>
61 #include <KMessageBox>
62 #include <KMime/Message>
63 #include <KRecentDirs>
64 #include <QAction>
65 #include <QActionGroup>
66 #include <QDBusConnectionInterface>
67 #include <QDesktopServices>
68 #include <QFileDialog>
69 #include <QIcon>
70 #include <QRegularExpression>
71 #include <QTemporaryFile>
72 #include <QWidget>
73 #include <ktoolinvocation.h>
74 
75 using namespace MessageViewer;
76 /** Checks whether @p str contains external references. To be precise,
77  we only check whether @p str contains 'xxx="http[s]:' where xxx is
78  not href. Obfuscated external references are ignored on purpose.
79 */
80 
81 bool Util::containsExternalReferences(const QString &str, const QString &extraHead)
82 {
83  const bool hasBaseInHeader = extraHead.contains(QLatin1String("<base href=\""), Qt::CaseInsensitive);
84  if (hasBaseInHeader && (str.contains(QLatin1String("href=\"/"), Qt::CaseInsensitive) || str.contains(QLatin1String("<img src=\"/"), Qt::CaseInsensitive))) {
85  return true;
86  }
87  int httpPos = str.indexOf(QLatin1String("\"http:"), Qt::CaseInsensitive);
88  int httpsPos = str.indexOf(QLatin1String("\"https:"), Qt::CaseInsensitive);
89  while (httpPos >= 0 || httpsPos >= 0) {
90  // pos = index of next occurrence of "http: or "https: whichever comes first
91  int pos = (httpPos < httpsPos) ? ((httpPos >= 0) ? httpPos : httpsPos) : ((httpsPos >= 0) ? httpsPos : httpPos);
92  // look backwards for "href"
93  if (pos > 5) {
94  int hrefPos = str.lastIndexOf(QLatin1String("href"), pos - 5, Qt::CaseInsensitive);
95  // if no 'href' is found or the distance between 'href' and '"http[s]:'
96  // is larger than 7 (7 is the distance in 'href = "http[s]:') then
97  // we assume that we have found an external reference
98  if ((hrefPos == -1) || (pos - hrefPos > 7)) {
99  // HTML messages created by KMail itself for now contain the following:
100  // <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
101  // Make sure not to show an external references warning for this string
102  int dtdPos = str.indexOf(QLatin1String("http://www.w3.org/TR/html4/loose.dtd"), pos + 1);
103  if (dtdPos != (pos + 1)) {
104  return true;
105  }
106  }
107  }
108  // find next occurrence of "http: or "https:
109  if (pos == httpPos) {
110  httpPos = str.indexOf(QLatin1String("\"http:"), httpPos + 6, Qt::CaseInsensitive);
111  } else {
112  httpsPos = str.indexOf(QLatin1String("\"https:"), httpsPos + 7, Qt::CaseInsensitive);
113  }
114  }
116 
117  const int startImgIndex = str.indexOf(QLatin1String("<img "));
118  QString newStringImg;
119  if (startImgIndex != -1) {
120  for (int i = startImgIndex, total = str.length(); i < total; ++i) {
121  const QChar charStr = str.at(i);
122  if (charStr == QLatin1Char('>')) {
123  newStringImg += charStr;
124  break;
125  } else {
126  newStringImg += charStr;
127  }
128  }
129  if (!newStringImg.isEmpty()) {
130  static QRegularExpression image1RegularExpression =
131  QRegularExpression(QStringLiteral("<img.*src=\"https?:/.*\".*>"), QRegularExpression::CaseInsensitiveOption);
132  const bool containsReg2 = newStringImg.contains(image1RegularExpression, &rmatch);
133  if (!containsReg2) {
134  static QRegularExpression image2RegularExpression =
135  QRegularExpression(QStringLiteral("<img.*src=https?:/.*>"), QRegularExpression::CaseInsensitiveOption);
136  const bool containsReg = newStringImg.contains(image2RegularExpression, &rmatch);
137  return containsReg;
138  } else {
139  return true;
140  }
141  }
142  }
143  return false;
144 }
145 
146 bool Util::checkOverwrite(const QUrl &url, QWidget *w)
147 {
148  bool fileExists = false;
149  if (url.isLocalFile()) {
150  fileExists = QFile::exists(url.toLocalFile());
151  } else {
152  auto job = KIO::statDetails(url, KIO::StatJob::DestinationSide, KIO::StatBasic);
153  KJobWidgets::setWindow(job, w);
154  fileExists = job->exec();
155  }
156  if (fileExists) {
157  if (KMessageBox::Cancel
159  i18n("A file named \"%1\" already exists. "
160  "Are you sure you want to overwrite it?",
161  url.toDisplayString()),
162  i18n("Overwrite File?"),
164  return false;
165  }
166  }
167  return true;
168 }
169 
170 bool Util::handleUrlWithQDesktopServices(const QUrl &url)
171 {
172 #if defined Q_OS_WIN || defined Q_OS_MACX
174  return true;
175 #else
176  // Always handle help through khelpcenter or browser
177  if (url.scheme() == QLatin1String("help")) {
179  return true;
180  }
181  return false;
182 #endif
183 }
184 
185 KMime::Content::List Util::allContents(const KMime::Content *message)
186 {
187  KMime::Content::List result;
189  if (child) {
190  result += child;
191  result += allContents(child);
192  }
194  if (next) {
195  result += next;
196  result += allContents(next);
197  }
198 
199  return result;
200 }
201 
202 bool Util::saveContents(QWidget *parent, const KMime::Content::List &contents, QList<QUrl> &urlList)
203 {
204  QUrl url;
205  QUrl dirUrl;
206  QString recentDirClass;
207  QUrl currentFolder;
208  const bool multiple = (contents.count() > 1);
209  if (multiple) {
210  // get the dir
211  dirUrl = QFileDialog::getExistingDirectoryUrl(parent,
212  i18n("Save Attachments To"),
213  KFileWidget::getStartUrl(QUrl(QStringLiteral("kfiledialog:///attachmentDir")), recentDirClass));
214  if (!dirUrl.isValid()) {
215  return false;
216  }
217 
218  // we may not get a slash-terminated url out of KFileDialog
219  if (!dirUrl.path().endsWith(QLatin1Char('/'))) {
220  dirUrl.setPath(dirUrl.path() + QLatin1Char('/'));
221  }
222  currentFolder = dirUrl;
223  } else {
224  // only one item, get the desired filename
225  KMime::Content *content = contents.first();
226  QString fileName = MimeTreeParser::NodeHelper::fileName(content);
227  fileName = MessageCore::StringUtil::cleanFileName(fileName);
228  if (fileName.isEmpty()) {
229  fileName = i18nc("filename for an unnamed attachment", "attachment.1");
230  }
231 
232  QUrl localUrl = KFileWidget::getStartUrl(QUrl(QStringLiteral("kfiledialog:///attachmentDir")), recentDirClass);
233  localUrl.setPath(localUrl.path() + QLatin1Char('/') + fileName);
235  url = QFileDialog::getSaveFileUrl(parent, i18n("Save Attachment"), localUrl, QString(), nullptr, options);
236  if (url.isEmpty()) {
237  return false;
238  }
239  currentFolder = KIO::upUrl(url);
240  }
241 
242  if (!recentDirClass.isEmpty()) {
243  KRecentDirs::add(recentDirClass, currentFolder.path());
244  }
245 
246  QMap<QString, int> renameNumbering;
247 
248  bool globalResult = true;
249  int unnamedAtmCount = 0;
250  PimCommon::RenameFileDialog::RenameFileDialogResult result = PimCommon::RenameFileDialog::RENAMEFILE_IGNORE;
251  for (KMime::Content *content : std::as_const(contents)) {
252  QUrl curUrl;
253  if (!dirUrl.isEmpty()) {
254  curUrl = dirUrl;
255  QString fileName = MimeTreeParser::NodeHelper::fileName(content);
256  fileName = MessageCore::StringUtil::cleanFileName(fileName);
257  if (fileName.isEmpty()) {
258  ++unnamedAtmCount;
259  fileName = i18nc("filename for the %1-th unnamed attachment", "attachment.%1", unnamedAtmCount);
260  }
261  if (!curUrl.path().endsWith(QLatin1Char('/'))) {
262  curUrl.setPath(curUrl.path() + QLatin1Char('/'));
263  }
264  curUrl.setPath(curUrl.path() + fileName);
265  } else {
266  curUrl = url;
267  }
268  if (!curUrl.isEmpty()) {
269  // Bug #312954
270  if (multiple && (curUrl.fileName() == QLatin1String("smime.p7s"))) {
271  continue;
272  }
273  // Rename the file if we have already saved one with the same name:
274  // try appending a number before extension (e.g. "pic.jpg" => "pic_2.jpg")
275  QString origFile = curUrl.fileName();
276  QString file = origFile;
277 
278  while (renameNumbering.contains(file)) {
279  file = origFile;
280  int num = renameNumbering[file] + 1;
281  int dotIdx = file.lastIndexOf(QLatin1Char('.'));
282  file.insert((dotIdx >= 0) ? dotIdx : file.length(), QLatin1Char('_') + QString::number(num));
283  }
285  curUrl.setPath(curUrl.path() + QLatin1Char('/') + file);
286 
287  // Increment the counter for both the old and the new filename
288  if (!renameNumbering.contains(origFile)) {
289  renameNumbering[origFile] = 1;
290  } else {
291  renameNumbering[origFile]++;
292  }
293 
294  if (file != origFile) {
295  if (!renameNumbering.contains(file)) {
296  renameNumbering[file] = 1;
297  } else {
298  renameNumbering[file]++;
299  }
300  }
301 
302  if (!(result == PimCommon::RenameFileDialog::RENAMEFILE_OVERWRITEALL || result == PimCommon::RenameFileDialog::RENAMEFILE_IGNOREALL)) {
303  bool fileExists = false;
304  if (curUrl.isLocalFile()) {
305  fileExists = QFile::exists(curUrl.toLocalFile());
306  } else {
307  auto job = KIO::statDetails(url, KIO::StatJob::DestinationSide, KIO::StatDetail::StatBasic);
308  KJobWidgets::setWindow(job, parent);
309  fileExists = job->exec();
310  }
311  if (fileExists) {
312  QPointer<PimCommon::RenameFileDialog> dlg = new PimCommon::RenameFileDialog(curUrl, multiple, parent);
313  result = static_cast<PimCommon::RenameFileDialog::RenameFileDialogResult>(dlg->exec());
314  if (result == PimCommon::RenameFileDialog::RENAMEFILE_IGNORE || result == PimCommon::RenameFileDialog::RENAMEFILE_IGNOREALL) {
315  delete dlg;
316  continue;
317  } else if (result == PimCommon::RenameFileDialog::RENAMEFILE_RENAME) {
318  if (dlg) {
319  curUrl = dlg->newName();
320  }
321  }
322  delete dlg;
323  }
324  }
325  // save
326  if (result != PimCommon::RenameFileDialog::RENAMEFILE_IGNOREALL) {
327  const bool resultSave = saveContent(parent, content, curUrl);
328  if (!resultSave) {
329  globalResult = resultSave;
330  } else {
331  urlList.append(curUrl);
332  }
333  }
334  }
335  }
336 
337  return globalResult;
338 }
339 
340 bool Util::saveContent(QWidget *parent, KMime::Content *content, const QUrl &url)
341 {
342  // FIXME: This is all horribly broken. First of all, creating a NodeHelper and then immediately
343  // reading out the encryption/signature state will not work at all.
344  // Then, topLevel() will not work for attachments that are inside encrypted parts.
345  // What should actually be done is either passing in an ObjectTreeParser that has already
346  // parsed the message, or creating an OTP here (which would have the downside that the
347  // password dialog for decrypting messages is shown twice)
348 #if 0 // totally broken
349  KMime::Content *topContent = content->topLevel();
351  bool bSaveEncrypted = false;
352  bool bEncryptedParts = mNodeHelper->encryptionState(content)
353  != MimeTreeParser::KMMsgNotEncrypted;
354  if (bEncryptedParts) {
355  if (KMessageBox::questionYesNo(parent,
356  i18n(
357  "The part %1 of the message is encrypted. Do you want to keep the encryption when saving?",
358  url.fileName()),
359  i18n("KMail Question"), KGuiItem(i18n("Keep Encryption")),
360  KGuiItem(i18n("Do Not Keep")))
361  == KMessageBox::Yes) {
362  bSaveEncrypted = true;
363  }
364  }
365 
366  bool bSaveWithSig = true;
367  if (mNodeHelper->signatureState(content) != MessageViewer::MimeTreeParser::KMMsgNotSigned) {
368  if (KMessageBox::questionYesNo(parent,
369  i18n(
370  "The part %1 of the message is signed. Do you want to keep the signature when saving?",
371  url.fileName()),
372  i18n("KMail Question"), KGuiItem(i18n("Keep Signature")),
373  KGuiItem(i18n("Do Not Keep")))
374  != KMessageBox::Yes) {
375  bSaveWithSig = false;
376  }
377  }
378 
379  QByteArray data;
380  if (bSaveEncrypted || !bEncryptedParts) {
381  KMime::Content *dataNode = content;
382  QByteArray rawDecryptedBody;
383  bool gotRawDecryptedBody = false;
384  if (!bSaveWithSig) {
385  if (topContent->contentType()->mimeType() == "multipart/signed") {
386  // carefully look for the part that is *not* the signature part:
387  if (MimeTreeParser::ObjectTreeParser::findType(topContent,
388  "application/pgp-signature", true,
389  false)) {
390  dataNode = MimeTreeParser::ObjectTreeParser ::findTypeNot(topContent,
391  "application",
392  "pgp-signature", true,
393  false);
394  } else if (MimeTreeParser::ObjectTreeParser::findType(topContent,
395  "application/pkcs7-mime",
396  true, false)) {
397  dataNode = MimeTreeParser::ObjectTreeParser ::findTypeNot(topContent,
398  "application",
399  "pkcs7-mime", true,
400  false);
401  } else {
402  dataNode = MimeTreeParser::ObjectTreeParser ::findTypeNot(topContent,
403  "multipart", "", true,
404  false);
405  }
406  } else {
407  EmptySource emptySource;
408  MimeTreeParser::ObjectTreeParser otp(&emptySource, 0, 0, false, false);
409 
410  // process this node and all it's siblings and descendants
411  mNodeHelper->setNodeUnprocessed(dataNode, true);
412  otp.parseObjectTree(dataNode);
413 
414  rawDecryptedBody = otp.rawDecryptedBody();
415  gotRawDecryptedBody = true;
416  }
417  }
418  QByteArray cstr = gotRawDecryptedBody
419  ? rawDecryptedBody
420  : dataNode->decodedContent();
421  data = KMime::CRLFtoLF(cstr);
422  }
423 #else
424  const QByteArray data = content->decodedContent();
425  qCWarning(MESSAGEVIEWER_LOG) << "Port the encryption/signature handling when saving a KMime::Content.";
426 #endif
427  QDataStream ds;
428  QFile file;
429  QTemporaryFile tf;
430  if (url.isLocalFile()) {
431  // save directly
432  file.setFileName(url.toLocalFile());
433  if (!file.open(QIODevice::WriteOnly)) {
434  KMessageBox::error(parent,
435  xi18nc("1 = file name, 2 = error string",
436  "<qt>Could not write to the file<br /><filename>%1</filename><br /><br />%2</qt>",
437  file.fileName(),
438  file.errorString()),
439  i18n("Error saving attachment"));
440  return false;
441  }
442  ds.setDevice(&file);
443  } else {
444  // tmp file for upload
445  tf.open();
446  ds.setDevice(&tf);
447  }
448 
449  const int bytesWritten = ds.writeRawData(data.data(), data.size());
450  if (bytesWritten != data.size()) {
451  auto f = static_cast<QFile *>(ds.device());
452  KMessageBox::error(parent,
453  xi18nc("1 = file name, 2 = error string",
454  "<qt>Could not write to the file<br /><filename>%1</filename><br /><br />%2</qt>",
455  f->fileName(),
456  f->errorString()),
457  i18n("Error saving attachment"));
458  // Remove the newly created empty or partial file
459  f->remove();
460  return false;
461  }
462 
463  if (!url.isLocalFile()) {
464  // QTemporaryFile::fileName() is only defined while the file is open
465  QString tfName = tf.fileName();
466  tf.close();
467  auto job = KIO::file_copy(QUrl::fromLocalFile(tfName), url);
468  KJobWidgets::setWindow(job, parent);
469  if (!job->exec()) {
470  KMessageBox::error(parent,
471  xi18nc("1 = file name, 2 = error string",
472  "<qt>Could not write to the file<br /><filename>%1</filename><br /><br />%2</qt>",
473  url.toDisplayString(),
474  job->errorString()),
475  i18n("Error saving attachment"));
476  return false;
477  }
478  } else {
479  file.close();
480  }
481 
482  return true;
483 }
484 
485 bool Util::saveAttachments(const KMime::Content::List &contents, QWidget *parent, QList<QUrl> &urlList)
486 {
487  if (contents.isEmpty()) {
488  KMessageBox::information(parent, i18n("Found no attachments to save."));
489  return false;
490  }
491 
492  return Util::saveContents(parent, contents, urlList);
493 }
494 
495 QString Util::generateFileNameForExtension(const Akonadi::Item &msgBase, const QString &extension)
496 {
497  QString fileName;
498 
499  if (msgBase.hasPayload<KMime::Message::Ptr>()) {
501  fileName.remove(QLatin1Char('\"'));
502  } else {
503  fileName = i18n("message");
504  }
505 
506  if (!fileName.endsWith(extension)) {
507  fileName += extension;
508  }
509  return fileName;
510 }
511 
512 QString Util::generateMboxFileName(const Akonadi::Item &msgBase)
513 {
514  return Util::generateFileNameForExtension(msgBase, QStringLiteral(".mbox"));
515 }
516 
517 bool Util::saveMessageInMboxAndGetUrl(QUrl &url, const Akonadi::Item::List &retrievedMsgs, QWidget *parent, bool appendMessages)
518 {
519  if (retrievedMsgs.isEmpty()) {
520  return false;
521  }
522  const Akonadi::Item msgBase = retrievedMsgs.first();
523  QString fileName = generateMboxFileName(msgBase);
524 
525  const QString filter = i18n("email messages (*.mbox);;all files (*)");
526 
527  QString fileClass;
528  const QUrl startUrl = KFileWidget::getStartUrl(QUrl(QStringLiteral("kfiledialog:///savemessage")), fileClass);
529  QUrl localUrl;
530  localUrl.setPath(startUrl.path() + QLatin1Char('/') + fileName);
532  if (appendMessages) {
534  }
535  QUrl dirUrl = QFileDialog::getSaveFileUrl(parent,
536  i18np("Save Message", "Save Messages", retrievedMsgs.count()),
537  QUrl::fromLocalFile(localUrl.toString()),
538  filter,
539  nullptr,
540  opt);
541  if (!dirUrl.isEmpty()) {
542  QFile file;
543  QTemporaryFile tf;
544  QString localFileName;
545  if (dirUrl.isLocalFile()) {
546  // save directly
547  file.setFileName(dirUrl.toLocalFile());
548  localFileName = file.fileName();
549  if (!appendMessages) {
550  QFile::remove(localFileName);
551  }
552  } else {
553  // tmp file for upload
554  tf.open();
555  localFileName = tf.fileName();
556  }
557 
558  KMBox::MBox mbox;
559  if (!mbox.load(localFileName)) {
560  if (appendMessages) {
561  KMessageBox::error(parent, i18n("File %1 could not be loaded.", localFileName), i18n("Error loading message"));
562  } else {
563  KMessageBox::error(parent, i18n("File %1 could not be created.", localFileName), i18n("Error saving message"));
564  }
565  return false;
566  }
567  for (const Akonadi::Item &item : std::as_const(retrievedMsgs)) {
568  if (item.hasPayload<KMime::Message::Ptr>()) {
569  mbox.appendMessage(item.payload<KMime::Message::Ptr>());
570  }
571  }
572 
573  if (!mbox.save()) {
574  KMessageBox::error(parent, i18n("We cannot save message."), i18n("Error saving message"));
575  return false;
576  }
577  localUrl = QUrl::fromLocalFile(localFileName);
578  if (localUrl.isLocalFile()) {
580  }
581 
582  if (!dirUrl.isLocalFile()) {
583  // QTemporaryFile::fileName() is only defined while the file is open
584  QString tfName = tf.fileName();
585  tf.close();
586  auto job = KIO::file_copy(QUrl::fromLocalFile(tfName), dirUrl);
587  KJobWidgets::setWindow(job, parent);
588  if (!job->exec()) {
589  KMessageBox::error(parent,
590  xi18nc("1 = file name, 2 = error string",
591  "<qt>Could not write to the file<br /><filename>%1</filename><br /><br />%2</qt>",
592  url.toDisplayString(),
593  job->errorString()),
594  i18n("Error saving message"));
595  return false;
596  }
597  } else {
598  file.close();
599  }
600  url = localUrl;
601  }
602  return true;
603 }
604 
605 bool Util::saveMessageInMbox(const Akonadi::Item::List &retrievedMsgs, QWidget *parent, bool appendMessages)
606 {
607  QUrl url;
608  return saveMessageInMboxAndGetUrl(url, retrievedMsgs, parent, appendMessages);
609 }
610 
611 QAction *Util::createAppAction(const KService::Ptr &service, bool singleOffer, QActionGroup *actionGroup, QObject *parent)
612 {
613  QString actionName(service->name().replace(QLatin1Char('&'), QStringLiteral("&&")));
614  if (singleOffer) {
615  actionName = i18n("Open &with %1", actionName);
616  } else {
617  actionName = i18nc("@item:inmenu Open With, %1 is application name", "%1", actionName);
618  }
619 
620  auto act = new QAction(parent);
621  act->setIcon(QIcon::fromTheme(service->icon()));
622  act->setText(actionName);
623  actionGroup->addAction(act);
624  act->setData(QVariant::fromValue(service));
625  return act;
626 }
627 
628 bool Util::excludeExtraHeader(const QString &s)
629 {
630  static QRegularExpression divRef(QStringLiteral("</div>"), QRegularExpression::CaseInsensitiveOption);
631  if (s.contains(divRef)) {
632  return true;
633  }
634  static QRegularExpression bodyRef(QStringLiteral("body.s*>.s*div"), QRegularExpression::CaseInsensitiveOption);
635  if (s.contains(bodyRef)) {
636  return true;
637  }
638  return false;
639 }
640 
641 void Util::addHelpTextAction(QAction *act, const QString &text)
642 {
643  act->setStatusTip(text);
644  act->setToolTip(text);
645  if (act->whatsThis().isEmpty()) {
646  act->setWhatsThis(text);
647  }
648 }
649 
651 {
652  if (_str.isEmpty()) {
653  return nullptr;
654  }
655  const QByteArray codec = _str.toLower();
657 }
658 
659 void Util::readGravatarConfig()
660 {
661  Gravatar::GravatarCache::self()->setMaximumSize(Gravatar::GravatarSettings::self()->gravatarCacheSize());
662  if (!Gravatar::GravatarSettings::self()->gravatarSupportEnabled()) {
663  Gravatar::GravatarCache::self()->clear();
664  }
665 }
666 
667 QString Util::parseBodyStyle(const QString &style)
668 {
669  const int indexStyle = style.indexOf(QLatin1String("style=\""));
670  if (indexStyle != -1) {
671  // qDebug() << " style " << style;
672  const int indexEnd = style.indexOf(QLatin1Char('"'), indexStyle + 7);
673  if (indexEnd != -1) {
674  const QStringView styleStr = QStringView(style).mid(indexStyle + 7, indexEnd - (indexStyle + 7));
675  const auto lstStyle = styleStr.split(QLatin1Char(';'), Qt::SkipEmptyParts);
676  QStringList lst;
677  for (const auto &style : lstStyle) {
678  if (!style.trimmed().contains(QLatin1String("white-space"))) {
679  lst.append(style.toString().trimmed());
680  }
681  }
682  if (!lst.isEmpty()) {
683  return QStringLiteral(" style=\"%1").arg(lst.join(QLatin1Char(';'))) + QStringLiteral(";\"");
684  }
685  }
686  }
687  return {};
688 }
689 
690 // FIXME this used to go through the full webkit parser to extract the body and head blocks
691 // until we have that back, at least attempt to fix some of the damage
692 // yes, "parsing" HTML with regexps is very very wrong, but it's still better than not filtering
693 // this at all...
694 Util::HtmlMessageInfo Util::processHtml(const QString &htmlSource)
695 {
696  Util::HtmlMessageInfo messageInfo;
697  QString s = htmlSource.trimmed();
698  static QRegularExpression docTypeRegularExpression = QRegularExpression(QStringLiteral("<!DOCTYPE[^>]*>"), QRegularExpression::CaseInsensitiveOption);
699  QRegularExpressionMatch matchDocType;
700  const int indexDoctype = s.indexOf(docTypeRegularExpression, 0, &matchDocType);
701  QString textBeforeDoctype;
702  if (indexDoctype > 0) {
703  textBeforeDoctype = s.left(indexDoctype);
704  s.remove(textBeforeDoctype);
705  }
706  const QString capturedString = matchDocType.captured();
707  if (!capturedString.isEmpty()) {
708  s = s.remove(capturedString).trimmed();
709  }
710  static QRegularExpression htmlRegularExpression = QRegularExpression(QStringLiteral("<html[^>]*>"), QRegularExpression::CaseInsensitiveOption);
711  s = s.remove(htmlRegularExpression).trimmed();
712  // head
713  static QRegularExpression headEndRegularExpression = QRegularExpression(QStringLiteral("^<head/>"), QRegularExpression::CaseInsensitiveOption);
714  s = s.remove(headEndRegularExpression).trimmed();
715  const int startIndex = s.indexOf(QLatin1String("<head>"), Qt::CaseInsensitive);
716  if (startIndex >= 0) {
717  const auto endIndex = s.indexOf(QLatin1String("</head>"), Qt::CaseInsensitive);
718 
719  if (endIndex < 0) {
720  messageInfo.htmlSource = htmlSource;
721  return messageInfo;
722  }
723  const int index = startIndex + 6;
724  messageInfo.extraHead = s.mid(index, endIndex - index);
725  if (MessageViewer::Util::excludeExtraHeader(messageInfo.extraHead)) {
726  messageInfo.extraHead.clear();
727  }
728  s = s.remove(startIndex, endIndex - startIndex + 7).trimmed();
729  }
730  // body
731  static QRegularExpression body = QRegularExpression(QStringLiteral("<body[^>]*>"), QRegularExpression::CaseInsensitiveOption);
732  QRegularExpressionMatch matchBody;
733  const int bodyStartIndex = s.indexOf(body, 0, &matchBody);
734  if (bodyStartIndex >= 0) {
735  // qDebug() << "matchBody " << matchBody.capturedTexts();
736  s = s.remove(bodyStartIndex, matchBody.capturedLength()).trimmed();
737  // Parse style
738  messageInfo.bodyStyle = matchBody.captured();
739  }
740  // Some mail has </div>$ at end
741  static QRegularExpression htmlDivRegularExpression =
742  QRegularExpression(QStringLiteral("(</html></div>|</html>)$"), QRegularExpression::CaseInsensitiveOption);
743  s = s.remove(htmlDivRegularExpression).trimmed();
744  // s = s.remove(QRegularExpression(QStringLiteral("</html>$"), QRegularExpression::CaseInsensitiveOption)).trimmed();
745  static QRegularExpression bodyEndRegularExpression = QRegularExpression(QStringLiteral("</body>$"), QRegularExpression::CaseInsensitiveOption);
746  s = s.remove(bodyEndRegularExpression).trimmed();
747  messageInfo.htmlSource = textBeforeDoctype + s;
748  return messageInfo;
749 }
750 
751 QByteArray Util::htmlCodec(const QByteArray &data, const QByteArray &codec)
752 {
753  QByteArray currentCodec = codec;
754  if (currentCodec.isEmpty()) {
755  currentCodec = QByteArray("UTF-8");
756  }
757  if (currentCodec == QByteArray("us-ascii")) {
758  currentCodec = QByteArray("iso-8859-1");
759  }
760  if (data.contains("charset=\"utf-8\"") || data.contains("charset=\"UTF-8\"") || data.contains("charset=UTF-8")) {
761  currentCodec = QByteArray("UTF-8");
762  }
763 
764  // qDebug() << " codec ******************************************: " << codec << " currentCodec : " <<currentCodec;
765  return currentCodec;
766 }
767 
768 QDebug operator<<(QDebug d, const Util::HtmlMessageInfo &t)
769 {
770  d << " htmlSource " << t.htmlSource;
771  d << " extraHead " << t.extraHead;
772  d << " bodyStyle " << t.bodyStyle;
773  return d;
774 }
KJOBWIDGETS_EXPORT void setWindow(KJob *job, QWidget *widget)
RemoveFilename
QTextCodec * codecForName(const QString &name) const
QString captured(int nth) const const
int indexOf(QChar ch, int from, Qt::CaseSensitivity cs) const const
bool save(const QString &fileName=QString())
QString toDisplayString(QUrl::FormattingOptions options) const const
QByteArray toLower() const const
QString xi18nc(const char *context, const char *text, const TYPE &arg...)
bool remove()
QString errorString() const const
void setDevice(QIODevice *d)
virtual QString fileName() const const override
bool isEmpty() const const
void setFileName(const QString &name)
The EmptySource class.
KIOFILEWIDGETS_EXPORT void add(const QString &fileClass, const QString &directory)
typedef Options
QAction * addAction(QAction *action)
static QUrl getStartUrl(const QUrl &startDir, QString &recentDirClass)
T & first()
QString i18np(const char *singular, const char *plural, const TYPE &arg...)
QString join(const QString &separator) const const
bool exists() const const
QString & remove(int position, int n)
KGuiItem overwrite()
QByteArray mimeType() const
bool isEmpty() const const
T * data() const const
QString toString(QUrl::FormattingOptions options) const const
T payload() const
void setToolTip(const QString &tip)
int lastIndexOf(QChar ch, int from, Qt::CaseSensitivity cs) const const
void clear()
MBoxEntry appendMessage(const KMime::Message::Ptr &message)
void setPath(const QString &path, QUrl::ParsingMode mode)
QString number(int n, int base)
QByteArray decodedContent()
void append(const T &value)
QString & insert(int position, QChar ch)
ButtonCode warningContinueCancel(QWidget *parent, const QString &text, const QString &caption=QString(), const KGuiItem &buttonContinue=KStandardGuiItem::cont(), const KGuiItem &buttonCancel=KStandardGuiItem::cancel(), const QString &dontAskAgainName=QString(), Options options=Notify)
QString i18nc(const char *context, const char *text, const TYPE &arg...)
CaseInsensitive
bool isEmpty() const const
bool isEmpty() const const
QString trimmed() const const
MESSAGECORE_EXPORT KMime::Content * firstChild(const KMime::Content *node)
Returns the first child node of the given node.
void error(QWidget *parent, const QString &text, const QString &caption=QString(), Options options=Notify)
void parseObjectTree(KMime::Content *node, bool parseOnlySingleNode=false)
Parse beginning at a given node and recursively parsing the children of that node and it&#39;s next sibli...
QString path(QUrl::ComponentFormattingOptions options) const const
bool endsWith(const QString &s, Qt::CaseSensitivity cs) const const
virtual bool open(QIODevice::OpenMode mode) override
QString scheme() const const
QString toLocalFile() const const
Headers::ContentType * contentType(bool create=true)
void setData(const QVariant &userData)
ButtonCode questionYesNo(QWidget *parent, const QString &text, const QString &caption=QString(), const KGuiItem &buttonYes=KStandardGuiItem::yes(), const KGuiItem &buttonNo=KStandardGuiItem::no(), const QString &dontAskAgainName=QString(), Options options=Notify)
MESSAGEVIEWER_EXPORT bool containsExternalReferences(const QString &str, const QString &extraHead)
Checks whether str contains external references.
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
SkipEmptyParts
static KCharsets * charsets()
QUrl getSaveFileUrl(QWidget *parent, const QString &caption, const QUrl &dir, const QString &filter, QString *selectedFilter, QFileDialog::Options options, const QStringList &supportedSchemes)
QVariant fromValue(const T &value)
virtual QString fileName() const const override
QString i18n(const char *text, const TYPE &arg...)
Content * topLevel() const
QDataStream & operator<<(QDataStream &out, const KDateTime::Spec &spec)
bool isValid() const const
void setStatusTip(const QString &statusTip)
QString mid(int position, int n) const const
QUrl adjusted(QUrl::FormattingOptions options) const const
static QString fileName(const KMime::Content *node)
Returns a usable filename for a node, that can be the filename from the content disposition header...
bool isEmpty() const const
virtual void close() override
QUrl getExistingDirectoryUrl(QWidget *parent, const QString &caption, const QUrl &dir, QFileDialog::Options options, const QStringList &supportedSchemes)
const QChar at(int position) const const
int count(const T &value) const const
bool contains(char ch) const const
int writeRawData(const char *s, int len)
int length() const const
Parses messages and generates HTML display code out of them.
char * data()
QString left(int n) const const
QString cleanFileName(const QString &name)
Cleans a filename by replacing characters not allowed or wanted on the filesystem e...
Definition: stringutil.cpp:686
QIcon fromTheme(const QString &name)
QString cleanSubject(KMime::Message *msg)
Return this mails subject, with all "forward" and "reply" prefixes removed.
Definition: stringutil.cpp:722
QIODevice * device() const const
void information(QWidget *parent, const QString &text, const QString &caption=QString(), const QString &dontShowAgainName=QString(), Options options=Notify)
bool hasPayload() const
bool openUrl(const QUrl &url)
int size() const const
int capturedLength(int nth) const const
MESSAGECORE_EXPORT KMime::Content * nextSibling(const KMime::Content *node)
Returns the next sibling node of the given node.
QString fileName(QUrl::ComponentFormattingOptions options) const const
QStringView mid(qsizetype start) const const
QUrl fromLocalFile(const QString &localFile)
MESSAGEVIEWER_EXPORT const QTextCodec * codecForName(const QByteArray &_str)
Return a QTextCodec for the specified charset.
bool isLocalFile() const const
bool load(const QString &fileName)
This file is part of the KDE documentation.
Documentation copyright © 1996-2022 The KDE developers.
Generated on Tue Jan 18 2022 23:06:04 by doxygen 1.8.11 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.