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 }