Messagelib

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

KDE's Doxygen guidelines are available online.