File indexing completed on 2025-03-09 04:54:39

0001 /*
0002  * SPDX-FileCopyrightText: 2005 Till Adam <adam@kde.org>
0003  * SPDX-License-Identifier: GPL-2.0-or-later WITH Qt-Commercial-exception-1.0
0004  */
0005 
0006 #include "messageviewer/messageviewerutil.h"
0007 #include "MessageCore/MessageCoreSettings"
0008 #include "MessageCore/NodeHelper"
0009 #include "MessageCore/StringUtil"
0010 #include "messageviewer_debug.h"
0011 #include "messageviewerutil_p.h"
0012 #include <MimeTreeParser/NodeHelper>
0013 
0014 #include <PimCommon/RenameFileDialog>
0015 
0016 #include <Gravatar/GravatarCache>
0017 #include <gravatar/gravatarsettings.h>
0018 
0019 #include <KMbox/MBox>
0020 
0021 #include <KFileWidget>
0022 #include <KIO/FileCopyJob>
0023 #include <KIO/StatJob>
0024 #include <KJobWidgets>
0025 #include <KLocalizedString>
0026 #include <KMessageBox>
0027 #include <KMime/Message>
0028 #include <KRecentDirs>
0029 #include <QAction>
0030 #include <QActionGroup>
0031 #include <QDBusConnectionInterface>
0032 #include <QDesktopServices>
0033 #include <QFileDialog>
0034 #include <QIcon>
0035 #include <QRegularExpression>
0036 #include <QTemporaryFile>
0037 #include <QWidget>
0038 
0039 using namespace MessageViewer;
0040 /** Checks whether @p str contains external references. To be precise,
0041     we only check whether @p str contains 'xxx="http[s]:' where xxx is
0042     not href. Obfuscated external references are ignored on purpose.
0043 */
0044 
0045 bool Util::containsExternalReferences(const QString &str, const QString &extraHead)
0046 {
0047     const bool hasBaseInHeader = extraHead.contains(QLatin1StringView("<base href=\""), Qt::CaseInsensitive);
0048     if (hasBaseInHeader
0049         && (str.contains(QLatin1StringView("href=\"/"), Qt::CaseInsensitive) || str.contains(QLatin1StringView("<img src=\"/"), Qt::CaseInsensitive))) {
0050         return true;
0051     }
0052     int httpPos = str.indexOf(QLatin1StringView("\"http:"), Qt::CaseInsensitive);
0053     int httpsPos = str.indexOf(QLatin1StringView("\"https:"), Qt::CaseInsensitive);
0054     while (httpPos >= 0 || httpsPos >= 0) {
0055         // pos = index of next occurrence of "http: or "https: whichever comes first
0056         const int pos = (httpPos < httpsPos) ? ((httpPos >= 0) ? httpPos : httpsPos) : ((httpsPos >= 0) ? httpsPos : httpPos);
0057         // look backwards for "href"
0058         if (pos > 5) {
0059             int hrefPos = str.lastIndexOf(QLatin1StringView("href"), pos - 5, Qt::CaseInsensitive);
0060             // if no 'href' is found or the distance between 'href' and '"http[s]:'
0061             // is larger than 7 (7 is the distance in 'href = "http[s]:') then
0062             // we assume that we have found an external reference
0063             if ((hrefPos == -1) || (pos - hrefPos > 7)) {
0064                 // HTML messages created by KMail itself for now contain the following:
0065                 // <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
0066                 // Make sure not to show an external references warning for this string
0067                 int dtdPos = str.indexOf(QLatin1StringView("http://www.w3.org/TR/html4/loose.dtd"), pos + 1);
0068                 if (dtdPos != (pos + 1)) {
0069                     return true;
0070                 }
0071             }
0072         }
0073         // find next occurrence of "http: or "https:
0074         if (pos == httpPos) {
0075             httpPos = str.indexOf(QLatin1StringView("\"http:"), httpPos + 6, Qt::CaseInsensitive);
0076         } else {
0077             httpsPos = str.indexOf(QLatin1StringView("\"https:"), httpsPos + 7, Qt::CaseInsensitive);
0078         }
0079     }
0080     QRegularExpressionMatch rmatch;
0081 
0082     const int startImgIndex = str.indexOf(QLatin1StringView("<img "));
0083     QString newStringImg;
0084     if (startImgIndex != -1) {
0085         for (int i = startImgIndex, total = str.length(); i < total; ++i) {
0086             const QChar charStr = str.at(i);
0087             if (charStr == QLatin1Char('>')) {
0088                 newStringImg += charStr;
0089                 break;
0090             } else {
0091                 newStringImg += charStr;
0092             }
0093         }
0094         if (!newStringImg.isEmpty()) {
0095             static QRegularExpression image1RegularExpression =
0096                 QRegularExpression(QStringLiteral("<img.*src=\"https?:/.*\".*>"), QRegularExpression::CaseInsensitiveOption);
0097             const bool containsReg2 = newStringImg.contains(image1RegularExpression, &rmatch);
0098             if (!containsReg2) {
0099                 static QRegularExpression image2RegularExpression =
0100                     QRegularExpression(QStringLiteral("<img.*src=https?:/.*>"), QRegularExpression::CaseInsensitiveOption);
0101                 const bool containsReg = newStringImg.contains(image2RegularExpression, &rmatch);
0102                 return containsReg;
0103             } else {
0104                 return true;
0105             }
0106         }
0107     }
0108     return false;
0109 }
0110 
0111 bool Util::checkOverwrite(const QUrl &url, QWidget *w)
0112 {
0113     bool fileExists = false;
0114     if (url.isLocalFile()) {
0115         fileExists = QFile::exists(url.toLocalFile());
0116     } else {
0117         auto job = KIO::stat(url, KIO::StatJob::DestinationSide, KIO::StatBasic);
0118         KJobWidgets::setWindow(job, w);
0119         fileExists = job->exec();
0120     }
0121     if (fileExists) {
0122         if (KMessageBox::Cancel
0123             == KMessageBox::warningContinueCancel(w,
0124                                                   i18n("A file named \"%1\" already exists. "
0125                                                        "Are you sure you want to overwrite it?",
0126                                                        url.toDisplayString()),
0127                                                   i18nc("@title:window", "Overwrite File?"),
0128                                                   KStandardGuiItem::overwrite())) {
0129             return false;
0130         }
0131     }
0132     return true;
0133 }
0134 
0135 bool Util::handleUrlWithQDesktopServices(const QUrl &url)
0136 {
0137 #if defined Q_OS_WIN || defined Q_OS_MACX
0138     QDesktopServices::openUrl(url);
0139     return true;
0140 #else
0141     // Always handle help through khelpcenter or browser
0142     if (url.scheme() == QLatin1StringView("help")) {
0143         QDesktopServices::openUrl(url);
0144         return true;
0145     }
0146     return false;
0147 #endif
0148 }
0149 
0150 KMime::Content::List Util::allContents(const KMime::Content *message)
0151 {
0152     KMime::Content::List result;
0153     KMime::Content *child = MessageCore::NodeHelper::firstChild(message);
0154     if (child) {
0155         result += child;
0156         result += allContents(child);
0157     }
0158     KMime::Content *next = MessageCore::NodeHelper::nextSibling(message);
0159     if (next) {
0160         result += next;
0161         result += allContents(next);
0162     }
0163 
0164     return result;
0165 }
0166 
0167 bool Util::saveContents(QWidget *parent, const KMime::Content::List &contents, QList<QUrl> &urlList)
0168 {
0169     QUrl url;
0170     QUrl dirUrl;
0171     QString recentDirClass;
0172     QUrl currentFolder;
0173     const bool multiple = (contents.count() > 1);
0174     if (multiple) {
0175         // get the dir
0176         dirUrl = QFileDialog::getExistingDirectoryUrl(parent,
0177                                                       i18n("Save Attachments To"),
0178                                                       KFileWidget::getStartUrl(QUrl(QStringLiteral("kfiledialog:///attachmentDir")), recentDirClass));
0179         if (!dirUrl.isValid()) {
0180             return false;
0181         }
0182 
0183         // we may not get a slash-terminated url out of KFileDialog
0184         if (!dirUrl.path().endsWith(QLatin1Char('/'))) {
0185             dirUrl.setPath(dirUrl.path() + QLatin1Char('/'));
0186         }
0187         currentFolder = dirUrl;
0188     } else {
0189         // only one item, get the desired filename
0190         KMime::Content *content = contents.first();
0191         QString fileName = MimeTreeParser::NodeHelper::fileName(content);
0192         fileName = MessageCore::StringUtil::cleanFileName(fileName);
0193         if (fileName.isEmpty()) {
0194             fileName = i18nc("filename for an unnamed attachment", "attachment.1");
0195         }
0196 
0197         QUrl localUrl = KFileWidget::getStartUrl(QUrl(QStringLiteral("kfiledialog:///attachmentDir")), recentDirClass);
0198         localUrl.setPath(localUrl.path() + QLatin1Char('/') + fileName);
0199         QFileDialog::Options options = QFileDialog::DontConfirmOverwrite;
0200         url = QFileDialog::getSaveFileUrl(parent, i18n("Save Attachment"), localUrl, QString(), nullptr, options);
0201         if (url.isEmpty()) {
0202             return false;
0203         }
0204         currentFolder = KIO::upUrl(url);
0205     }
0206 
0207     if (!recentDirClass.isEmpty()) {
0208         KRecentDirs::add(recentDirClass, currentFolder.path());
0209     }
0210 
0211     QMap<QString, int> renameNumbering;
0212 
0213     bool globalResult = true;
0214     int unnamedAtmCount = 0;
0215     PimCommon::RenameFileDialog::RenameFileDialogResult result = PimCommon::RenameFileDialog::RENAMEFILE_IGNORE;
0216     for (KMime::Content *content : std::as_const(contents)) {
0217         QUrl curUrl;
0218         if (!dirUrl.isEmpty()) {
0219             curUrl = dirUrl;
0220             QString fileName = MimeTreeParser::NodeHelper::fileName(content);
0221             fileName = MessageCore::StringUtil::cleanFileName(fileName);
0222             if (fileName.isEmpty()) {
0223                 ++unnamedAtmCount;
0224                 fileName = i18nc("filename for the %1-th unnamed attachment", "attachment.%1", unnamedAtmCount);
0225             }
0226             if (!curUrl.path().endsWith(QLatin1Char('/'))) {
0227                 curUrl.setPath(curUrl.path() + QLatin1Char('/'));
0228             }
0229             curUrl.setPath(curUrl.path() + fileName);
0230         } else {
0231             curUrl = url;
0232         }
0233         if (!curUrl.isEmpty()) {
0234             // Bug #312954
0235             if (multiple && (curUrl.fileName() == QLatin1StringView("smime.p7s"))) {
0236                 continue;
0237             }
0238             // Rename the file if we have already saved one with the same name:
0239             // try appending a number before extension (e.g. "pic.jpg" => "pic_2.jpg")
0240             const QString origFile = curUrl.fileName();
0241             QString file = origFile;
0242 
0243             while (renameNumbering.contains(file)) {
0244                 file = origFile;
0245                 int num = renameNumbering[file] + 1;
0246                 int dotIdx = file.lastIndexOf(QLatin1Char('.'));
0247                 file.insert((dotIdx >= 0) ? dotIdx : file.length(), QLatin1Char('_') + QString::number(num));
0248             }
0249             curUrl = curUrl.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash);
0250             curUrl.setPath(curUrl.path() + QLatin1Char('/') + file);
0251 
0252             // Increment the counter for both the old and the new filename
0253             if (!renameNumbering.contains(origFile)) {
0254                 renameNumbering[origFile] = 1;
0255             } else {
0256                 renameNumbering[origFile]++;
0257             }
0258 
0259             if (file != origFile) {
0260                 if (!renameNumbering.contains(file)) {
0261                     renameNumbering[file] = 1;
0262                 } else {
0263                     renameNumbering[file]++;
0264                 }
0265             }
0266 
0267             if (!(result == PimCommon::RenameFileDialog::RENAMEFILE_OVERWRITEALL || result == PimCommon::RenameFileDialog::RENAMEFILE_IGNOREALL)) {
0268                 bool fileExists = false;
0269                 if (curUrl.isLocalFile()) {
0270                     fileExists = QFile::exists(curUrl.toLocalFile());
0271                 } else {
0272                     auto job = KIO::stat(url, KIO::StatJob::DestinationSide, KIO::StatDetail::StatBasic);
0273                     KJobWidgets::setWindow(job, parent);
0274                     fileExists = job->exec();
0275                 }
0276                 if (fileExists) {
0277                     QPointer<PimCommon::RenameFileDialog> dlg = new PimCommon::RenameFileDialog(curUrl, multiple, parent);
0278                     result = static_cast<PimCommon::RenameFileDialog::RenameFileDialogResult>(dlg->exec());
0279                     if (result == PimCommon::RenameFileDialog::RENAMEFILE_IGNORE || result == PimCommon::RenameFileDialog::RENAMEFILE_IGNOREALL) {
0280                         delete dlg;
0281                         continue;
0282                     } else if (result == PimCommon::RenameFileDialog::RENAMEFILE_RENAME) {
0283                         if (dlg) {
0284                             curUrl = dlg->newName();
0285                         }
0286                     }
0287                     delete dlg;
0288                 }
0289             }
0290             // save
0291             if (result != PimCommon::RenameFileDialog::RENAMEFILE_IGNOREALL) {
0292                 const bool resultSave = saveContent(parent, content, curUrl);
0293                 if (!resultSave) {
0294                     globalResult = resultSave;
0295                 } else {
0296                     urlList.append(curUrl);
0297                 }
0298             }
0299         }
0300     }
0301 
0302     return globalResult;
0303 }
0304 
0305 bool Util::saveContent(QWidget *parent, KMime::Content *content, const QUrl &url)
0306 {
0307     // FIXME: This is all horribly broken. First of all, creating a NodeHelper and then immediately
0308     //        reading out the encryption/signature state will not work at all.
0309     //        Then, topLevel() will not work for attachments that are inside encrypted parts.
0310     //        What should actually be done is either passing in an ObjectTreeParser that has already
0311     //        parsed the message, or creating an OTP here (which would have the downside that the
0312     //        password dialog for decrypting messages is shown twice)
0313 #if 0 // totally broken
0314     KMime::Content *topContent = content->topLevel();
0315     MimeTreeParser::NodeHelper *mNodeHelper = new MimeTreeParser::NodeHelper;
0316     bool bSaveEncrypted = false;
0317     bool bEncryptedParts = mNodeHelper->encryptionState(content)
0318                            != MimeTreeParser::KMMsgNotEncrypted;
0319     if (bEncryptedParts) {
0320         if (KMessageBox::questionTwoActions(parent,
0321                                        i18n(
0322                                            "The part %1 of the message is encrypted. Do you want to keep the encryption when saving?",
0323                                            url.fileName()),
0324                                        i18n("KMail Question"), KGuiItem(i18n("Keep Encryption")),
0325                                        KGuiItem(i18n("Do Not Keep")))
0326             == KMessageBox::ButtonCode::PrimaryAction) {
0327             bSaveEncrypted = true;
0328         }
0329     }
0330 
0331     bool bSaveWithSig = true;
0332     if (mNodeHelper->signatureState(content) != MessageViewer::MimeTreeParser::KMMsgNotSigned) {
0333         if (KMessageBox::questionTwoActions(parent,
0334                                        i18n(
0335                                            "The part %1 of the message is signed. Do you want to keep the signature when saving?",
0336                                            url.fileName()),
0337                                        i18n("KMail Question"), KGuiItem(i18n("Keep Signature")),
0338                                        KGuiItem(i18n("Do Not Keep")))
0339             != KMessageBox::Yes) {
0340             bSaveWithSig = false;
0341         }
0342     }
0343 
0344     QByteArray data;
0345     if (bSaveEncrypted || !bEncryptedParts) {
0346         KMime::Content *dataNode = content;
0347         QByteArray rawDecryptedBody;
0348         bool gotRawDecryptedBody = false;
0349         if (!bSaveWithSig) {
0350             if (topContent->contentType()->mimeType() == "multipart/signed") {
0351                 // carefully look for the part that is *not* the signature part:
0352                 if (MimeTreeParser::ObjectTreeParser::findType(topContent,
0353                                                                "application/pgp-signature", true,
0354                                                                false)) {
0355                     dataNode = MimeTreeParser::ObjectTreeParser ::findTypeNot(topContent,
0356                                                                               "application",
0357                                                                               "pgp-signature", true,
0358                                                                               false);
0359                 } else if (MimeTreeParser::ObjectTreeParser::findType(topContent,
0360                                                                       "application/pkcs7-mime",
0361                                                                       true, false)) {
0362                     dataNode = MimeTreeParser::ObjectTreeParser ::findTypeNot(topContent,
0363                                                                               "application",
0364                                                                               "pkcs7-mime", true,
0365                                                                               false);
0366                 } else {
0367                     dataNode = MimeTreeParser::ObjectTreeParser ::findTypeNot(topContent,
0368                                                                               "multipart", "", true,
0369                                                                               false);
0370                 }
0371             } else {
0372                 EmptySource emptySource;
0373                 MimeTreeParser::ObjectTreeParser otp(&emptySource, 0, 0, false, false);
0374 
0375                 // process this node and all it's siblings and descendants
0376                 mNodeHelper->setNodeUnprocessed(dataNode, true);
0377                 otp.parseObjectTree(dataNode);
0378 
0379                 rawDecryptedBody = otp.rawDecryptedBody();
0380                 gotRawDecryptedBody = true;
0381             }
0382         }
0383         QByteArray cstr = gotRawDecryptedBody
0384                           ? rawDecryptedBody
0385                           : dataNode->decodedContent();
0386         data = KMime::CRLFtoLF(cstr);
0387     }
0388 #else
0389     const QByteArray data = content->decodedContent();
0390     qCWarning(MESSAGEVIEWER_LOG) << "Port the encryption/signature handling when saving a KMime::Content.";
0391 #endif
0392     QDataStream ds;
0393     QFile file;
0394     QTemporaryFile tf;
0395     if (url.isLocalFile()) {
0396         // save directly
0397         file.setFileName(url.toLocalFile());
0398         if (!file.open(QIODevice::WriteOnly)) {
0399             KMessageBox::error(parent,
0400                                xi18nc("1 = file name, 2 = error string",
0401                                       "<qt>Could not write to the file<br /><filename>%1</filename><br /><br />%2</qt>",
0402                                       file.fileName(),
0403                                       file.errorString()),
0404                                i18nc("@title:window", "Error saving attachment"));
0405             return false;
0406         }
0407         ds.setDevice(&file);
0408     } else {
0409         // tmp file for upload
0410         tf.open();
0411         ds.setDevice(&tf);
0412     }
0413 
0414     const int bytesWritten = ds.writeRawData(data.data(), data.size());
0415     if (bytesWritten != data.size()) {
0416         auto f = static_cast<QFile *>(ds.device());
0417         KMessageBox::error(parent,
0418                            xi18nc("1 = file name, 2 = error string",
0419                                   "<qt>Could not write to the file<br /><filename>%1</filename><br /><br />%2</qt>",
0420                                   f->fileName(),
0421                                   f->errorString()),
0422                            i18n("Error saving attachment"));
0423         // Remove the newly created empty or partial file
0424         f->remove();
0425         return false;
0426     }
0427 
0428     if (!url.isLocalFile()) {
0429         // QTemporaryFile::fileName() is only defined while the file is open
0430         QString tfName = tf.fileName();
0431         tf.close();
0432         auto job = KIO::file_copy(QUrl::fromLocalFile(tfName), url);
0433         KJobWidgets::setWindow(job, parent);
0434         if (!job->exec()) {
0435             KMessageBox::error(parent,
0436                                xi18nc("1 = file name, 2 = error string",
0437                                       "<qt>Could not write to the file<br /><filename>%1</filename><br /><br />%2</qt>",
0438                                       url.toDisplayString(),
0439                                       job->errorString()),
0440                                i18n("Error saving attachment"));
0441             return false;
0442         }
0443     } else {
0444         file.close();
0445     }
0446 
0447     return true;
0448 }
0449 
0450 bool Util::saveAttachments(const KMime::Content::List &contents, QWidget *parent, QList<QUrl> &urlList)
0451 {
0452     if (contents.isEmpty()) {
0453         KMessageBox::information(parent, i18n("Found no attachments to save."));
0454         return false;
0455     }
0456 
0457     return Util::saveContents(parent, contents, urlList);
0458 }
0459 
0460 QString Util::generateFileNameForExtension(const Akonadi::Item &msgBase, const QString &extension)
0461 {
0462     QString fileName;
0463 
0464     if (msgBase.hasPayload<KMime::Message::Ptr>()) {
0465         fileName = MessageCore::StringUtil::cleanFileName(MessageCore::StringUtil::cleanSubject(msgBase.payload<KMime::Message::Ptr>().data()).trimmed());
0466         fileName.remove(QLatin1Char('\"'));
0467     } else {
0468         fileName = i18n("message");
0469     }
0470 
0471     if (!fileName.endsWith(extension)) {
0472         fileName += extension;
0473     }
0474     return fileName;
0475 }
0476 
0477 QString Util::generateMboxFileName(const Akonadi::Item &msgBase)
0478 {
0479     return Util::generateFileNameForExtension(msgBase, QStringLiteral(".mbox"));
0480 }
0481 
0482 bool Util::saveMessageInMboxAndGetUrl(QUrl &url, const Akonadi::Item::List &retrievedMsgs, QWidget *parent, bool appendMessages)
0483 {
0484     if (retrievedMsgs.isEmpty()) {
0485         return false;
0486     }
0487     const Akonadi::Item msgBase = retrievedMsgs.first();
0488     QString fileName = generateMboxFileName(msgBase);
0489 
0490     const QString filter = i18n("email messages (*.mbox);;all files (*)");
0491 
0492     QString fileClass;
0493     const QUrl startUrl = KFileWidget::getStartUrl(QUrl(QStringLiteral("kfiledialog:///savemessage")), fileClass);
0494     QUrl localUrl;
0495     localUrl.setPath(startUrl.path() + QLatin1Char('/') + fileName);
0496     QFileDialog::Options opt;
0497     if (appendMessages) {
0498         opt |= QFileDialog::DontConfirmOverwrite;
0499     }
0500     QUrl dirUrl = QFileDialog::getSaveFileUrl(parent,
0501                                               i18np("Save Message", "Save Messages", retrievedMsgs.count()),
0502                                               QUrl::fromLocalFile(localUrl.toString()),
0503                                               filter,
0504                                               nullptr,
0505                                               opt);
0506     if (!dirUrl.isEmpty()) {
0507         QFile file;
0508         QTemporaryFile tf;
0509         QString localFileName;
0510         if (dirUrl.isLocalFile()) {
0511             // save directly
0512             file.setFileName(dirUrl.toLocalFile());
0513             localFileName = file.fileName();
0514             if (!appendMessages) {
0515                 QFile::remove(localFileName);
0516             }
0517         } else {
0518             // tmp file for upload
0519             tf.open();
0520             localFileName = tf.fileName();
0521         }
0522 
0523         KMBox::MBox mbox;
0524         if (!mbox.load(localFileName)) {
0525             if (appendMessages) {
0526                 KMessageBox::error(parent, i18n("File %1 could not be loaded.", localFileName), i18nc("@title:window", "Error loading message"));
0527             } else {
0528                 KMessageBox::error(parent, i18n("File %1 could not be created.", localFileName), i18nc("@title:window", "Error saving message"));
0529             }
0530             return false;
0531         }
0532         for (const Akonadi::Item &item : std::as_const(retrievedMsgs)) {
0533             if (item.hasPayload<KMime::Message::Ptr>()) {
0534                 mbox.appendMessage(item.payload<KMime::Message::Ptr>());
0535             }
0536         }
0537 
0538         if (!mbox.save()) {
0539             KMessageBox::error(parent, i18n("We cannot save message."), i18n("Error saving message"));
0540             return false;
0541         }
0542         localUrl = QUrl::fromLocalFile(localFileName);
0543         if (localUrl.isLocalFile()) {
0544             KRecentDirs::add(fileClass, localUrl.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path());
0545         }
0546 
0547         if (!dirUrl.isLocalFile()) {
0548             // QTemporaryFile::fileName() is only defined while the file is open
0549             QString tfName = tf.fileName();
0550             tf.close();
0551             auto job = KIO::file_copy(QUrl::fromLocalFile(tfName), dirUrl);
0552             KJobWidgets::setWindow(job, parent);
0553             if (!job->exec()) {
0554                 KMessageBox::error(parent,
0555                                    xi18nc("1 = file name, 2 = error string",
0556                                           "<qt>Could not write to the file<br /><filename>%1</filename><br /><br />%2</qt>",
0557                                           url.toDisplayString(),
0558                                           job->errorString()),
0559                                    i18nc("@title:window", "Error saving message"));
0560                 return false;
0561             }
0562         } else {
0563             file.close();
0564         }
0565         url = localUrl;
0566     }
0567     return true;
0568 }
0569 
0570 bool Util::saveMessageInMbox(const Akonadi::Item::List &retrievedMsgs, QWidget *parent, bool appendMessages)
0571 {
0572     QUrl url;
0573     return saveMessageInMboxAndGetUrl(url, retrievedMsgs, parent, appendMessages);
0574 }
0575 
0576 bool Util::deleteAttachment(KMime::Content *node)
0577 {
0578     Q_ASSERT(node);
0579 
0580     auto parentNode = node->parent();
0581     if (!parentNode) {
0582         return false;
0583     }
0584 
0585     QString filename;
0586     QString name;
0587     QByteArray mimetype;
0588     if (auto cd = node->contentDisposition(false)) {
0589         filename = cd->filename();
0590     }
0591 
0592     if (auto ct = node->contentType(false)) {
0593         name = ct->name();
0594         mimetype = ct->mimeType();
0595     }
0596 
0597     if (mimetype == "text/x-moz-deleted") {
0598         // The attachment has already been deleted, no need to delete the deletion attachment
0599         return false;
0600     }
0601 
0602     // text/plain part:
0603     const auto newName = i18nc("Argument is the original name of the deleted attachment", "Deleted: %1", name);
0604     auto deletePart = new KMime::Content(parentNode);
0605     auto deleteCt = deletePart->contentType(true);
0606     deleteCt->setMimeType("text/x-moz-deleted");
0607     deleteCt->setName(newName, "utf8");
0608     deletePart->contentDisposition(true)->setDisposition(KMime::Headers::CDattachment);
0609     deletePart->contentDisposition(false)->setFilename(newName);
0610 
0611     deleteCt->setCharset("utf-8");
0612     deletePart->contentTransferEncoding()->setEncoding(KMime::Headers::CEquPr);
0613     QByteArray bodyMessage = QByteArrayLiteral("\n");
0614     bodyMessage += i18n("You deleted an attachment from this message. The original MIME headers for the attachment were:").toUtf8() + ("\n");
0615     bodyMessage += ("\nContent-Type: ") + mimetype;
0616     bodyMessage += ("\nname=\"") + name.toUtf8() + "\"";
0617     bodyMessage += ("\nfilename=\"") + filename.toUtf8() + "\"";
0618     deletePart->setBody(bodyMessage);
0619     parentNode->replaceContent(node, deletePart);
0620 
0621     parentNode->assemble();
0622 
0623     return true;
0624 }
0625 
0626 int Util::deleteAttachments(const KMime::Content::List &nodes)
0627 {
0628     int updatedCount = 0;
0629     for (const auto node : nodes) {
0630         if (deleteAttachment(node)) {
0631             ++updatedCount;
0632         }
0633     }
0634     return updatedCount;
0635 }
0636 
0637 QAction *Util::createAppAction(const KService::Ptr &service, bool singleOffer, QActionGroup *actionGroup, QObject *parent)
0638 {
0639     QString actionName(service->name().replace(QLatin1Char('&'), QStringLiteral("&&")));
0640     if (singleOffer) {
0641         actionName = i18n("Open &with %1", actionName);
0642     } else {
0643         actionName = i18nc("@item:inmenu Open With, %1 is application name", "%1", actionName);
0644     }
0645 
0646     auto act = new QAction(parent);
0647     act->setIcon(QIcon::fromTheme(service->icon()));
0648     act->setText(actionName);
0649     actionGroup->addAction(act);
0650     act->setData(QVariant::fromValue(service));
0651     return act;
0652 }
0653 
0654 bool Util::excludeExtraHeader(const QString &s)
0655 {
0656     static QRegularExpression divRef(QStringLiteral("</div>"), QRegularExpression::CaseInsensitiveOption);
0657     if (s.contains(divRef)) {
0658         return true;
0659     }
0660     static QRegularExpression bodyRef(QStringLiteral("body.s*>.s*div"), QRegularExpression::CaseInsensitiveOption);
0661     if (s.contains(bodyRef)) {
0662         return true;
0663     }
0664     return false;
0665 }
0666 
0667 void Util::addHelpTextAction(QAction *act, const QString &text)
0668 {
0669     act->setStatusTip(text);
0670     act->setToolTip(text);
0671     if (act->whatsThis().isEmpty()) {
0672         act->setWhatsThis(text);
0673     }
0674 }
0675 
0676 void Util::readGravatarConfig()
0677 {
0678     Gravatar::GravatarCache::self()->setMaximumSize(Gravatar::GravatarSettings::self()->gravatarCacheSize());
0679     if (!Gravatar::GravatarSettings::self()->gravatarSupportEnabled()) {
0680         Gravatar::GravatarCache::self()->clear();
0681     }
0682 }
0683 
0684 QString Util::parseBodyStyle(const QString &style)
0685 {
0686     const int indexStyle = style.indexOf(QLatin1StringView("style=\""));
0687     if (indexStyle != -1) {
0688         // qDebug() << " style " << style;
0689         const int indexEnd = style.indexOf(QLatin1Char('"'), indexStyle + 7);
0690         if (indexEnd != -1) {
0691             const QStringView styleStr = QStringView(style).mid(indexStyle + 7, indexEnd - (indexStyle + 7));
0692             const auto lstStyle = styleStr.split(QLatin1Char(';'), Qt::SkipEmptyParts);
0693             QStringList lst;
0694             for (const auto &style : lstStyle) {
0695                 // qDebug() << " style : " << style;
0696                 if (!style.trimmed().contains(QLatin1StringView("white-space")) && !style.trimmed().contains(QLatin1StringView("text-align"))) {
0697                     lst.append(style.toString().trimmed());
0698                 }
0699             }
0700             if (!lst.isEmpty()) {
0701                 // qDebug() << " lst " << lst;
0702                 return QStringLiteral(" style=\"%1").arg(lst.join(QLatin1Char(';'))) + QStringLiteral(";\"");
0703             }
0704         }
0705     }
0706     return {};
0707 }
0708 
0709 // FIXME this used to go through the full webkit parser to extract the body and head blocks
0710 // until we have that back, at least attempt to fix some of the damage
0711 // yes, "parsing" HTML with regexps is very very wrong, but it's still better than not filtering
0712 // this at all...
0713 Util::HtmlMessageInfo Util::processHtml(const QString &htmlSource)
0714 {
0715     Util::HtmlMessageInfo messageInfo;
0716     QString s = htmlSource.trimmed();
0717     static QRegularExpression docTypeRegularExpression = QRegularExpression(QStringLiteral("<!DOCTYPE[^>]*>"), QRegularExpression::CaseInsensitiveOption);
0718     QRegularExpressionMatch matchDocType;
0719     const int indexDoctype = s.indexOf(docTypeRegularExpression, 0, &matchDocType);
0720     QString textBeforeDoctype;
0721     if (indexDoctype > 0) {
0722         textBeforeDoctype = s.left(indexDoctype);
0723         s.remove(textBeforeDoctype);
0724     }
0725     const QString capturedString = matchDocType.captured();
0726     if (!capturedString.isEmpty()) {
0727         s = s.remove(capturedString).trimmed();
0728     }
0729     static QRegularExpression htmlRegularExpression = QRegularExpression(QStringLiteral("<html[^>]*>"), QRegularExpression::CaseInsensitiveOption);
0730     s = s.remove(htmlRegularExpression).trimmed();
0731     // head
0732     static QRegularExpression headEndRegularExpression = QRegularExpression(QStringLiteral("^<head/>"), QRegularExpression::CaseInsensitiveOption);
0733     s = s.remove(headEndRegularExpression).trimmed();
0734     const int startIndex = s.indexOf(QLatin1StringView("<head>"), Qt::CaseInsensitive);
0735     if (startIndex >= 0) {
0736         const auto endIndex = s.indexOf(QLatin1StringView("</head>"), Qt::CaseInsensitive);
0737 
0738         if (endIndex < 0) {
0739             messageInfo.htmlSource = htmlSource;
0740             return messageInfo;
0741         }
0742         const int index = startIndex + 6;
0743         messageInfo.extraHead = s.mid(index, endIndex - index);
0744         if (MessageViewer::Util::excludeExtraHeader(messageInfo.extraHead)) {
0745             messageInfo.extraHead.clear();
0746         }
0747         s = s.remove(startIndex, endIndex - startIndex + 7).trimmed();
0748         // qDebug() << "BEFORE messageInfo.extraHead**********" << messageInfo.extraHead;
0749         static QRegularExpression styleBodyRegularExpression =
0750             QRegularExpression(QStringLiteral("<style>\\s*body\\s*{"), QRegularExpression::CaseInsensitiveOption | QRegularExpression::MultilineOption);
0751         QRegularExpressionMatch matchBodyStyle;
0752         const int bodyStyleStartIndex = messageInfo.extraHead.indexOf(styleBodyRegularExpression, 0, &matchBodyStyle);
0753         if (bodyStyleStartIndex > 0) {
0754             const auto endIndex = messageInfo.extraHead.indexOf(QLatin1StringView("</style>"), bodyStyleStartIndex, Qt::CaseInsensitive);
0755             // qDebug() << " endIndex " << endIndex;
0756             messageInfo.extraHead = messageInfo.extraHead.remove(bodyStyleStartIndex, endIndex - bodyStyleStartIndex + 8);
0757         }
0758         // qDebug() << "AFTER messageInfo.extraHead**********" << messageInfo.extraHead;
0759     }
0760     // body
0761     static QRegularExpression body = QRegularExpression(QStringLiteral("<body[^>]*>"), QRegularExpression::CaseInsensitiveOption);
0762     QRegularExpressionMatch matchBody;
0763     const int bodyStartIndex = s.indexOf(body, 0, &matchBody);
0764     if (bodyStartIndex >= 0) {
0765         // qDebug() << "matchBody  " << matchBody.capturedTexts();
0766         s = s.remove(bodyStartIndex, matchBody.capturedLength()).trimmed();
0767         // Parse style
0768         messageInfo.bodyStyle = matchBody.captured();
0769     }
0770     // Some mail has </div>$ at end
0771     static QRegularExpression htmlDivRegularExpression =
0772         QRegularExpression(QStringLiteral("(</html></div>|</html>)$"), QRegularExpression::CaseInsensitiveOption);
0773     s = s.remove(htmlDivRegularExpression).trimmed();
0774     // s = s.remove(QRegularExpression(QStringLiteral("</html>$"), QRegularExpression::CaseInsensitiveOption)).trimmed();
0775     static QRegularExpression bodyEndRegularExpression = QRegularExpression(QStringLiteral("</body>$"), QRegularExpression::CaseInsensitiveOption);
0776     s = s.remove(bodyEndRegularExpression).trimmed();
0777     messageInfo.htmlSource = textBeforeDoctype + s;
0778     return messageInfo;
0779 }
0780 
0781 QByteArray Util::htmlCodec(const QByteArray &data, const QByteArray &codec)
0782 {
0783     QByteArray currentCodec = codec;
0784     if (currentCodec.isEmpty()) {
0785         currentCodec = QByteArray("UTF-8");
0786     }
0787     if (currentCodec == QByteArray("us-ascii")) {
0788         currentCodec = QByteArray("iso-8859-1");
0789     }
0790     if (data.contains("charset=\"utf-8\"") || data.contains("charset=\"UTF-8\"") || data.contains("charset=UTF-8")) {
0791         currentCodec = QByteArray("UTF-8");
0792     }
0793 
0794     // qDebug() << " codec ******************************************: " << codec << " currentCodec : " <<currentCodec;
0795     return currentCodec;
0796 }
0797 QStringConverter::Encoding Util::htmlEncoding(const QByteArray &data, const QByteArray &codec)
0798 {
0799     QByteArray currentCodec = codec;
0800     if (currentCodec.isEmpty()) {
0801         return QStringConverter::Utf8;
0802     }
0803     if (currentCodec == QByteArray("us-ascii")) {
0804         return QStringConverter::Latin1;
0805     }
0806     if (data.contains("charset=\"utf-8\"") || data.contains("charset=\"UTF-8\"") || data.contains("charset=UTF-8")) {
0807         return QStringConverter::Utf8;
0808     }
0809 
0810     // qDebug() << " codec ******************************************: " << codec << " currentCodec : " <<currentCodec;
0811     // TODO verify
0812     return QStringConverter::System;
0813 }
0814 
0815 QDebug operator<<(QDebug d, const Util::HtmlMessageInfo &t)
0816 {
0817     d << " htmlSource " << t.htmlSource;
0818     d << " extraHead " << t.extraHead;
0819     d << " bodyStyle " << t.bodyStyle;
0820     return d;
0821 }