Messagelib

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

KDE's Doxygen guidelines are available online.