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

KDE's Doxygen guidelines are available online.