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

KDE's Doxygen guidelines are available online.