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

KDE's Doxygen guidelines are available online.