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/StringUtil"
9#include "messageviewer_debug.h"
10#include "messageviewerutil_p.h"
11#include <MimeTreeParser/NodeHelper>
12
13#include <PimCommon/RenameFileDialog>
14
15#include <Gravatar/GravatarCache>
16#include <gravatar/gravatarsettings.h>
17
18#include <KMbox/MBox>
19
20#include <KFileWidget>
21#include <KIO/FileCopyJob>
22#include <KIO/StatJob>
23#include <KJobWidgets>
24#include <KLocalizedString>
25#include <KMessageBox>
26#include <KMime/Message>
27#include <KRecentDirs>
28#include <QAction>
29#include <QActionGroup>
30#include <QDBusConnectionInterface>
31#include <QDesktopServices>
32#include <QFileDialog>
33#include <QIcon>
34#include <QRegularExpression>
35#include <QTemporaryFile>
36#include <QWidget>
37
38using namespace MessageViewer;
39/** Checks whether @p str contains external references. To be precise,
40 we only check whether @p str contains 'xxx="http[s]:' where xxx is
41 not href. Obfuscated external references are ignored on purpose.
42*/
43
44bool Util::containsExternalReferences(const QString &str, const QString &extraHead)
45{
46 const bool hasBaseInHeader = extraHead.contains(QLatin1StringView("<base href=\""), Qt::CaseInsensitive);
47 if (hasBaseInHeader
48 && (str.contains(QLatin1StringView("href=\"/"), Qt::CaseInsensitive) || str.contains(QLatin1StringView("<img src=\"/"), Qt::CaseInsensitive))) {
49 return true;
50 }
51 int httpPos = str.indexOf(QLatin1StringView("\"http:"), Qt::CaseInsensitive);
52 int httpsPos = str.indexOf(QLatin1StringView("\"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(QLatin1StringView("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(QLatin1StringView("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(QLatin1StringView("\"http:"), httpPos + 6, Qt::CaseInsensitive);
75 } else {
76 httpsPos = str.indexOf(QLatin1StringView("\"https:"), httpsPos + 7, Qt::CaseInsensitive);
77 }
78 }
80
81 const int startImgIndex = str.indexOf(QLatin1StringView("<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
110bool 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);
118 fileExists = job->exec();
119 }
120 if (fileExists) {
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
134bool 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() == QLatin1StringView("help")) {
143 return true;
144 }
145 return false;
146#endif
147}
148
149bool Util::saveContents(QWidget *parent, const KMime::Content::List &contents, QList<QUrl> &urlList)
150{
151 QUrl url;
152 QUrl dirUrl;
153 QString recentDirClass;
154 QUrl currentFolder;
155 const bool multiple = (contents.count() > 1);
156 if (multiple) {
157 // get the dir
159 i18n("Save Attachments To"),
160 KFileWidget::getStartUrl(QUrl(QStringLiteral("kfiledialog:///attachmentDir")), recentDirClass));
161 if (!dirUrl.isValid()) {
162 return false;
163 }
164
165 // we may not get a slash-terminated url out of KFileDialog
166 if (!dirUrl.path().endsWith(QLatin1Char('/'))) {
167 dirUrl.setPath(dirUrl.path() + QLatin1Char('/'));
168 }
169 currentFolder = dirUrl;
170 } else {
171 // only one item, get the desired filename
172 KMime::Content *content = contents.first();
174 fileName = MessageCore::StringUtil::cleanFileName(fileName);
175 if (fileName.isEmpty()) {
176 fileName = i18nc("filename for an unnamed attachment", "attachment.1");
177 }
178
179 QUrl localUrl = KFileWidget::getStartUrl(QUrl(QStringLiteral("kfiledialog:///attachmentDir")), recentDirClass);
180 localUrl.setPath(localUrl.path() + QLatin1Char('/') + fileName);
182 url = QFileDialog::getSaveFileUrl(parent, i18n("Save Attachment"), localUrl, QString(), nullptr, options);
183 if (url.isEmpty()) {
184 return false;
185 }
186 currentFolder = KIO::upUrl(url);
187 }
188
189 if (!recentDirClass.isEmpty()) {
190 KRecentDirs::add(recentDirClass, currentFolder.path());
191 }
192
193 QMap<QString, int> renameNumbering;
194
195 bool globalResult = true;
196 int unnamedAtmCount = 0;
197 PimCommon::RenameFileDialog::RenameFileDialogResult result = PimCommon::RenameFileDialog::RENAMEFILE_IGNORE;
198 for (KMime::Content *content : std::as_const(contents)) {
199 QUrl curUrl;
200 if (!dirUrl.isEmpty()) {
201 curUrl = dirUrl;
203 fileName = MessageCore::StringUtil::cleanFileName(fileName);
204 if (fileName.isEmpty()) {
205 ++unnamedAtmCount;
206 fileName = i18nc("filename for the %1-th unnamed attachment", "attachment.%1", unnamedAtmCount);
207 }
208 if (!curUrl.path().endsWith(QLatin1Char('/'))) {
209 curUrl.setPath(curUrl.path() + QLatin1Char('/'));
210 }
211 curUrl.setPath(curUrl.path() + fileName);
212 } else {
213 curUrl = url;
214 }
215 if (!curUrl.isEmpty()) {
216 // Bug #312954
217 if (multiple && (curUrl.fileName() == QLatin1StringView("smime.p7s"))) {
218 continue;
219 }
220 // Rename the file if we have already saved one with the same name:
221 // try appending a number before extension (e.g. "pic.jpg" => "pic_2.jpg")
222 const QString origFile = curUrl.fileName();
223 QString file = origFile;
224
225 while (renameNumbering.contains(file)) {
226 file = origFile;
227 int num = renameNumbering[file] + 1;
228 int dotIdx = file.lastIndexOf(QLatin1Char('.'));
229 file.insert((dotIdx >= 0) ? dotIdx : file.length(), QLatin1Char('_') + QString::number(num));
230 }
232 curUrl.setPath(curUrl.path() + QLatin1Char('/') + file);
233
234 // Increment the counter for both the old and the new filename
235 if (!renameNumbering.contains(origFile)) {
236 renameNumbering[origFile] = 1;
237 } else {
238 renameNumbering[origFile]++;
239 }
240
241 if (file != origFile) {
242 if (!renameNumbering.contains(file)) {
243 renameNumbering[file] = 1;
244 } else {
245 renameNumbering[file]++;
246 }
247 }
248
249 if (!(result == PimCommon::RenameFileDialog::RENAMEFILE_OVERWRITEALL || result == PimCommon::RenameFileDialog::RENAMEFILE_IGNOREALL)) {
250 bool fileExists = false;
251 if (curUrl.isLocalFile()) {
253 } else {
254 auto job = KIO::stat(url, KIO::StatJob::DestinationSide, KIO::StatDetail::StatBasic);
255 KJobWidgets::setWindow(job, parent);
256 fileExists = job->exec();
257 }
258 if (fileExists) {
259 QPointer<PimCommon::RenameFileDialog> dlg = new PimCommon::RenameFileDialog(curUrl, multiple, parent);
260 result = static_cast<PimCommon::RenameFileDialog::RenameFileDialogResult>(dlg->exec());
261 if (result == PimCommon::RenameFileDialog::RENAMEFILE_IGNORE || result == PimCommon::RenameFileDialog::RENAMEFILE_IGNOREALL) {
262 delete dlg;
263 continue;
264 } else if (result == PimCommon::RenameFileDialog::RENAMEFILE_RENAME) {
265 if (dlg) {
266 curUrl = dlg->newName();
267 }
268 }
269 delete dlg;
270 }
271 }
272 // save
273 if (result != PimCommon::RenameFileDialog::RENAMEFILE_IGNOREALL) {
274 const bool resultSave = saveContent(parent, content, curUrl);
275 if (!resultSave) {
276 globalResult = resultSave;
277 } else {
278 urlList.append(curUrl);
279 }
280 }
281 }
282 }
283
284 return globalResult;
285}
286
287bool Util::saveContent(QWidget *parent, KMime::Content *content, const QUrl &url)
288{
289 // FIXME: This is all horribly broken. First of all, creating a NodeHelper and then immediately
290 // reading out the encryption/signature state will not work at all.
291 // Then, topLevel() will not work for attachments that are inside encrypted parts.
292 // What should actually be done is either passing in an ObjectTreeParser that has already
293 // parsed the message, or creating an OTP here (which would have the downside that the
294 // password dialog for decrypting messages is shown twice)
295#if 0 // totally broken
296 KMime::Content *topContent = content->topLevel();
298 bool bSaveEncrypted = false;
299 bool bEncryptedParts = mNodeHelper->encryptionState(content)
300 != MimeTreeParser::KMMsgNotEncrypted;
301 if (bEncryptedParts) {
303 i18n(
304 "The part %1 of the message is encrypted. Do you want to keep the encryption when saving?",
305 url.fileName()),
306 i18n("KMail Question"), KGuiItem(i18nc("@action:button", "Keep Encryption")),
307 KGuiItem(i18nc("@action:button", "Do Not Keep")))
308 == KMessageBox::ButtonCode::PrimaryAction) {
309 bSaveEncrypted = true;
310 }
311 }
312
313 bool bSaveWithSig = true;
314 if (mNodeHelper->signatureState(content) != MessageViewer::MimeTreeParser::KMMsgNotSigned) {
316 i18n(
317 "The part %1 of the message is signed. Do you want to keep the signature when saving?",
318 url.fileName()),
319 i18n("KMail Question"), KGuiItem(i18nc("@action:button", "Keep Signature")),
320 KGuiItem(i18nc("@action:button", "Do Not Keep")))
321 != KMessageBox::Yes) {
322 bSaveWithSig = false;
323 }
324 }
325
326 QByteArray data;
327 if (bSaveEncrypted || !bEncryptedParts) {
328 KMime::Content *dataNode = content;
329 QByteArray rawDecryptedBody;
330 bool gotRawDecryptedBody = false;
331 if (!bSaveWithSig) {
332 if (topContent->contentType()->mimeType() == "multipart/signed") {
333 // carefully look for the part that is *not* the signature part:
334 if (MimeTreeParser::ObjectTreeParser::findType(topContent,
335 "application/pgp-signature", true,
336 false)) {
337 dataNode = MimeTreeParser::ObjectTreeParser ::findTypeNot(topContent,
338 "application",
339 "pgp-signature", true,
340 false);
341 } else if (MimeTreeParser::ObjectTreeParser::findType(topContent,
342 "application/pkcs7-mime",
343 true, false)) {
344 dataNode = MimeTreeParser::ObjectTreeParser ::findTypeNot(topContent,
345 "application",
346 "pkcs7-mime", true,
347 false);
348 } else {
349 dataNode = MimeTreeParser::ObjectTreeParser ::findTypeNot(topContent,
350 "multipart", "", true,
351 false);
352 }
353 } else {
354 EmptySource emptySource;
355 MimeTreeParser::ObjectTreeParser otp(&emptySource, 0, 0, false, false);
356
357 // process this node and all it's siblings and descendants
358 mNodeHelper->setNodeUnprocessed(dataNode, true);
359 otp.parseObjectTree(dataNode);
360
361 rawDecryptedBody = otp.rawDecryptedBody();
362 gotRawDecryptedBody = true;
363 }
364 }
365 QByteArray cstr = gotRawDecryptedBody
366 ? rawDecryptedBody
367 : dataNode->decodedContent();
368 data = KMime::CRLFtoLF(cstr);
369 }
370#else
371 const QByteArray data = content->decodedContent();
372 qCWarning(MESSAGEVIEWER_LOG) << "Port the encryption/signature handling when saving a KMime::Content.";
373#endif
374 QDataStream ds;
375 QFile file;
377 if (url.isLocalFile()) {
378 // save directly
379 file.setFileName(url.toLocalFile());
380 if (!file.open(QIODevice::WriteOnly)) {
381 KMessageBox::error(parent,
382 xi18nc("1 = file name, 2 = error string",
383 "<qt>Could not write to the file<br /><filename>%1</filename><br /><br />%2</qt>",
384 file.fileName(),
385 file.errorString()),
386 i18nc("@title:window", "Error saving attachment"));
387 return false;
388 }
389 ds.setDevice(&file);
390 } else {
391 // tmp file for upload
392 tf.open();
393 ds.setDevice(&tf);
394 }
395
396 const int bytesWritten = ds.writeRawData(data.data(), data.size());
397 if (bytesWritten != data.size()) {
398 auto f = static_cast<QFile *>(ds.device());
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 f->fileName(),
403 f->errorString()),
404 i18n("Error saving attachment"));
405 // Remove the newly created empty or partial file
406 f->remove();
407 return false;
408 }
409
410 if (!url.isLocalFile()) {
411 // QTemporaryFile::fileName() is only defined while the file is open
412 QString tfName = tf.fileName();
413 tf.close();
414 auto job = KIO::file_copy(QUrl::fromLocalFile(tfName), url);
415 KJobWidgets::setWindow(job, parent);
416 if (!job->exec()) {
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 url.toDisplayString(),
421 job->errorString()),
422 i18n("Error saving attachment"));
423 return false;
424 }
425 } else {
426 file.close();
427 }
428
429 return true;
430}
431
432bool Util::saveAttachments(const KMime::Content::List &contents, QWidget *parent, QList<QUrl> &urlList)
433{
434 if (contents.isEmpty()) {
435 KMessageBox::information(parent, i18n("Found no attachments to save."));
436 return false;
437 }
438
439 return Util::saveContents(parent, contents, urlList);
440}
441
442QString Util::generateFileNameForExtension(const Akonadi::Item &msgBase, const QString &extension)
443{
444 QString fileName;
445
446 if (msgBase.hasPayload<KMime::Message::Ptr>()) {
448 fileName.remove(QLatin1Char('\"'));
449 } else {
450 fileName = i18n("message");
451 }
452
453 if (!fileName.endsWith(extension)) {
454 fileName += extension;
455 }
456 return fileName;
457}
458
459QString Util::generateMboxFileName(const Akonadi::Item &msgBase)
460{
461 return Util::generateFileNameForExtension(msgBase, QStringLiteral(".mbox"));
462}
463
464bool Util::saveMessageInMboxAndGetUrl(QUrl &url, const Akonadi::Item::List &retrievedMsgs, QWidget *parent, bool appendMessages)
465{
466 if (retrievedMsgs.isEmpty()) {
467 return false;
468 }
469 const Akonadi::Item msgBase = retrievedMsgs.first();
470 QString fileName = generateMboxFileName(msgBase);
471
472 const QString filter = i18n("email messages (*.mbox);;all files (*)");
473
474 QString fileClass;
475 const QUrl startUrl = KFileWidget::getStartUrl(QUrl(QStringLiteral("kfiledialog:///savemessage")), fileClass);
476 QUrl localUrl;
477 localUrl.setPath(startUrl.path() + QLatin1Char('/') + fileName);
479 if (appendMessages) {
481 }
482 QUrl dirUrl = QFileDialog::getSaveFileUrl(parent,
483 i18np("Save Message", "Save Messages", retrievedMsgs.count()),
484 QUrl::fromLocalFile(localUrl.toString()),
485 filter,
486 nullptr,
487 opt);
488 if (!dirUrl.isEmpty()) {
489 QFile file;
491 QString localFileName;
492 if (dirUrl.isLocalFile()) {
493 // save directly
494 file.setFileName(dirUrl.toLocalFile());
495 localFileName = file.fileName();
496 if (!appendMessages) {
497 QFile::remove(localFileName);
498 }
499 } else {
500 // tmp file for upload
501 tf.open();
502 localFileName = tf.fileName();
503 }
504
505 KMBox::MBox mbox;
506 if (!mbox.load(localFileName)) {
507 if (appendMessages) {
508 KMessageBox::error(parent, i18n("File %1 could not be loaded.", localFileName), i18nc("@title:window", "Error loading message"));
509 } else {
510 KMessageBox::error(parent, i18n("File %1 could not be created.", localFileName), i18nc("@title:window", "Error saving message"));
511 }
512 return false;
513 }
514 for (const Akonadi::Item &item : std::as_const(retrievedMsgs)) {
515 if (item.hasPayload<KMime::Message::Ptr>()) {
516 mbox.appendMessage(item.payload<KMime::Message::Ptr>());
517 }
518 }
519
520 if (!mbox.save()) {
521 KMessageBox::error(parent, i18n("We cannot save message."), i18n("Error saving message"));
522 return false;
523 }
524 localUrl = QUrl::fromLocalFile(localFileName);
525 if (localUrl.isLocalFile()) {
527 }
528
529 if (!dirUrl.isLocalFile()) {
530 // QTemporaryFile::fileName() is only defined while the file is open
531 QString tfName = tf.fileName();
532 tf.close();
533 auto job = KIO::file_copy(QUrl::fromLocalFile(tfName), dirUrl);
534 KJobWidgets::setWindow(job, parent);
535 if (!job->exec()) {
536 KMessageBox::error(parent,
537 xi18nc("1 = file name, 2 = error string",
538 "<qt>Could not write to the file<br /><filename>%1</filename><br /><br />%2</qt>",
539 url.toDisplayString(),
540 job->errorString()),
541 i18nc("@title:window", "Error saving message"));
542 return false;
543 }
544 } else {
545 file.close();
546 }
547 url = localUrl;
548 }
549 return true;
550}
551
552bool Util::saveMessageInMbox(const Akonadi::Item::List &retrievedMsgs, QWidget *parent, bool appendMessages)
553{
554 QUrl url;
555 return saveMessageInMboxAndGetUrl(url, retrievedMsgs, parent, appendMessages);
556}
557
559{
560 Q_ASSERT(node);
561
562 auto parentNode = node->parent();
563 if (!parentNode) {
564 return false;
565 }
566
567 QString filename;
568 QString name;
569 QByteArray mimetype;
570 if (auto cd = node->contentDisposition(false)) {
571 filename = cd->filename();
572 }
573
574 if (auto ct = node->contentType(false)) {
575 name = ct->name();
576 mimetype = ct->mimeType();
577 }
578
579 if (mimetype == "text/x-moz-deleted") {
580 // The attachment has already been deleted, no need to delete the deletion attachment
581 return false;
582 }
583
584 // text/plain part:
585 const auto newName = i18nc("Argument is the original name of the deleted attachment", "Deleted: %1", name);
586 auto deletePart = new KMime::Content(parentNode);
587 auto deleteCt = deletePart->contentType(true);
588 deleteCt->setMimeType("text/x-moz-deleted");
589 deleteCt->setName(newName);
590 deletePart->contentDisposition(true)->setDisposition(KMime::Headers::CDattachment);
591 deletePart->contentDisposition(false)->setFilename(newName);
592
593 deleteCt->setCharset("utf-8");
594 deletePart->contentTransferEncoding()->setEncoding(KMime::Headers::CEquPr);
595 QByteArray bodyMessage = QByteArrayLiteral("\n");
596 bodyMessage += i18n("You deleted an attachment from this message. The original MIME headers for the attachment were:").toUtf8() + ("\n");
597 bodyMessage += ("\nContent-Type: ") + mimetype;
598 bodyMessage += ("\nname=\"") + name.toUtf8() + "\"";
599 bodyMessage += ("\nfilename=\"") + filename.toUtf8() + "\"";
600 deletePart->setBody(bodyMessage);
601 parentNode->replaceContent(node, deletePart);
602
603 parentNode->assemble();
604
605 return true;
606}
607
609{
610 int updatedCount = 0;
611 for (const auto node : nodes) {
612 if (deleteAttachment(node)) {
613 ++updatedCount;
614 }
615 }
616 return updatedCount;
617}
618
619QAction *Util::createAppAction(const KService::Ptr &service, bool singleOffer, QActionGroup *actionGroup, QObject *parent)
620{
621 QString actionName(service->name().replace(QLatin1Char('&'), QStringLiteral("&&")));
622 if (singleOffer) {
623 actionName = i18n("Open &with %1", actionName);
624 } else {
625 actionName = i18nc("@item:inmenu Open With, %1 is application name", "%1", actionName);
626 }
627
628 auto act = new QAction(parent);
629 act->setIcon(QIcon::fromTheme(service->icon()));
630 act->setText(actionName);
631 actionGroup->addAction(act);
632 act->setData(QVariant::fromValue(service));
633 return act;
634}
635
636bool Util::excludeExtraHeader(const QString &s)
637{
638 static QRegularExpression divRef(QStringLiteral("</div>"), QRegularExpression::CaseInsensitiveOption);
639 if (s.contains(divRef)) {
640 return true;
641 }
642 static QRegularExpression bodyRef(QStringLiteral("body.s*>.s*div"), QRegularExpression::CaseInsensitiveOption);
643 if (s.contains(bodyRef)) {
644 return true;
645 }
646 return false;
647}
648
649void Util::addHelpTextAction(QAction *act, const QString &text)
650{
651 act->setStatusTip(text);
652 act->setToolTip(text);
653 if (act->whatsThis().isEmpty()) {
654 act->setWhatsThis(text);
655 }
656}
657
658void Util::readGravatarConfig()
659{
660 Gravatar::GravatarCache::self()->setMaximumSize(Gravatar::GravatarSettings::self()->gravatarCacheSize());
661 if (!Gravatar::GravatarSettings::self()->gravatarSupportEnabled()) {
662 Gravatar::GravatarCache::self()->clear();
663 }
664}
665
666QString Util::parseBodyStyle(const QString &style)
667{
668 const int indexStyle = style.indexOf(QLatin1StringView("style=\""));
669 if (indexStyle != -1) {
670 // qDebug() << " style " << style;
671 const int indexEnd = style.indexOf(QLatin1Char('"'), indexStyle + 7);
672 if (indexEnd != -1) {
673 const QStringView styleStr = QStringView(style).mid(indexStyle + 7, indexEnd - (indexStyle + 7));
674 const auto lstStyle = styleStr.split(QLatin1Char(';'), Qt::SkipEmptyParts);
675 QStringList lst;
676 for (const auto &style : lstStyle) {
677 // qDebug() << " style : " << style;
678 if (!style.trimmed().contains(QLatin1StringView("white-space")) && !style.trimmed().contains(QLatin1StringView("text-align"))) {
679 lst.append(style.toString().trimmed());
680 }
681 }
682 if (!lst.isEmpty()) {
683 // qDebug() << " lst " << lst;
684 return QStringLiteral(" style=\"%1").arg(lst.join(QLatin1Char(';'))) + QStringLiteral(";\"");
685 }
686 }
687 }
688 return {};
689}
690
691// FIXME this used to go through the full webkit parser to extract the body and head blocks
692// until we have that back, at least attempt to fix some of the damage
693// yes, "parsing" HTML with regexps is very very wrong, but it's still better than not filtering
694// this at all...
695Util::HtmlMessageInfo Util::processHtml(const QString &htmlSource)
696{
697 Util::HtmlMessageInfo messageInfo;
698 QString s = htmlSource.trimmed();
699 static QRegularExpression docTypeRegularExpression = QRegularExpression(QStringLiteral("<!DOCTYPE[^>]*>"), QRegularExpression::CaseInsensitiveOption);
700 QRegularExpressionMatch matchDocType;
701 const int indexDoctype = s.indexOf(docTypeRegularExpression, 0, &matchDocType);
702 QString textBeforeDoctype;
703 if (indexDoctype > 0) {
704 textBeforeDoctype = s.left(indexDoctype);
705 s.remove(textBeforeDoctype);
706 }
707 const QString capturedString = matchDocType.captured();
708 if (!capturedString.isEmpty()) {
709 s = s.remove(capturedString).trimmed();
710 }
711 static QRegularExpression htmlRegularExpression = QRegularExpression(QStringLiteral("<html[^>]*>"), QRegularExpression::CaseInsensitiveOption);
712 s = s.remove(htmlRegularExpression).trimmed();
713 // head
714 static QRegularExpression headEndRegularExpression = QRegularExpression(QStringLiteral("^<head/>"), QRegularExpression::CaseInsensitiveOption);
715 s = s.remove(headEndRegularExpression).trimmed();
716 const int startIndex = s.indexOf(QLatin1StringView("<head>"), Qt::CaseInsensitive);
717 if (startIndex >= 0) {
718 const auto endIndex = s.indexOf(QLatin1StringView("</head>"), Qt::CaseInsensitive);
719
720 if (endIndex < 0) {
721 messageInfo.htmlSource = htmlSource;
722 return messageInfo;
723 }
724 const int index = startIndex + 6;
725 messageInfo.extraHead = s.mid(index, endIndex - index);
726 if (MessageViewer::Util::excludeExtraHeader(messageInfo.extraHead)) {
727 messageInfo.extraHead.clear();
728 }
729 s = s.remove(startIndex, endIndex - startIndex + 7).trimmed();
730 // qDebug() << "BEFORE messageInfo.extraHead**********" << messageInfo.extraHead;
731 static QRegularExpression styleBodyRegularExpression =
733 QRegularExpressionMatch matchBodyStyle;
734 const int bodyStyleStartIndex = messageInfo.extraHead.indexOf(styleBodyRegularExpression, 0, &matchBodyStyle);
735 if (bodyStyleStartIndex > 0) {
736 const auto endIndex = messageInfo.extraHead.indexOf(QLatin1StringView("</style>"), bodyStyleStartIndex, Qt::CaseInsensitive);
737 // qDebug() << " endIndex " << endIndex;
738 messageInfo.extraHead = messageInfo.extraHead.remove(bodyStyleStartIndex, endIndex - bodyStyleStartIndex + 8);
739 }
740 // qDebug() << "AFTER messageInfo.extraHead**********" << messageInfo.extraHead;
741 }
742 // body
743 static QRegularExpression body = QRegularExpression(QStringLiteral("<body[^>]*>"), QRegularExpression::CaseInsensitiveOption);
744 QRegularExpressionMatch matchBody;
745 const int bodyStartIndex = s.indexOf(body, 0, &matchBody);
746 if (bodyStartIndex >= 0) {
747 // qDebug() << "matchBody " << matchBody.capturedTexts();
748 s = s.remove(bodyStartIndex, matchBody.capturedLength()).trimmed();
749 // Parse style
750 messageInfo.bodyStyle = matchBody.captured();
751 }
752 // Some mail has </div>$ at end
753 static QRegularExpression htmlDivRegularExpression =
754 QRegularExpression(QStringLiteral("(</html></div>|</html>)$"), QRegularExpression::CaseInsensitiveOption);
755 s = s.remove(htmlDivRegularExpression).trimmed();
756 // s = s.remove(QRegularExpression(QStringLiteral("</html>$"), QRegularExpression::CaseInsensitiveOption)).trimmed();
757 static QRegularExpression bodyEndRegularExpression = QRegularExpression(QStringLiteral("</body>$"), QRegularExpression::CaseInsensitiveOption);
758 s = s.remove(bodyEndRegularExpression).trimmed();
759 messageInfo.htmlSource = textBeforeDoctype + s;
760 return messageInfo;
761}
762
763QDebug operator<<(QDebug d, const Util::HtmlMessageInfo &t)
764{
765 d << " htmlSource " << t.htmlSource;
766 d << " extraHead " << t.extraHead;
767 d << " bodyStyle " << t.bodyStyle;
768 return d;
769}
bool hasPayload() const
T payload() 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())
const Headers::ContentType * contentType() const
Content * parent()
QByteArray decodedContent() const
const Headers::ContentDisposition * contentDisposition() const
Content * topLevel()
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)
KCALENDARCORE_EXPORT QDataStream & operator<<(QDataStream &out, const KCalendarCore::Alarm::Ptr &)
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()
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 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)
char * data()
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
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 Fri Jul 26 2024 11:54:19 by doxygen 1.11.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.