File indexing completed on 2024-05-12 05:28:17
0001 // SPDX-FileCopyrightText: 2016 Sandro Knauß <knauss@kolabsys.com> 0002 // SPDX-License-Identifier: LGPL-2.0-or-later 0003 0004 #include "partmodel.h" 0005 0006 #include "../mimetreeparser/objecttreeparser.h" 0007 #include "htmlutils.h" 0008 #include <qstringliteral.h> 0009 0010 #include <KLocalizedString> 0011 #include <QDebug> 0012 #include <QRegularExpression> 0013 #include <QTextDocument> 0014 0015 // We return a pair containing the trimmed string, as well as a boolean indicating whether the string was trimmed or not 0016 std::pair<QString, bool> PartModel::trim(const QString &text) 0017 { 0018 // The delimiters have <p>.? prefixed including the .? because sometimes we get a byte order mark <feff> (seen with user-agent: 0019 // Microsoft-MacOutlook/10.1d.0.190908) We match both regulard withspace with \s and non-breaking spaces with \u00A0 0020 const QList<QRegularExpression> delimiters{ 0021 // English 0022 QRegularExpression{QStringLiteral("<p>.?-+Original(\\s|\u00A0)Message-+"), QRegularExpression::CaseInsensitiveOption}, 0023 // The remainder is not quoted 0024 QRegularExpression{QStringLiteral("<p>.?On.*wrote:"), QRegularExpression::CaseInsensitiveOption}, 0025 // The remainder is quoted 0026 QRegularExpression{QStringLiteral("> On.*wrote:"), QRegularExpression::CaseInsensitiveOption}, 0027 0028 // German 0029 // Forwarded 0030 QRegularExpression{QStringLiteral("<p>.?Von:.*</p>"), QRegularExpression::CaseInsensitiveOption}, 0031 // Reply 0032 QRegularExpression{QStringLiteral("<p>.?Am.*schrieb.*:</p>"), QRegularExpression::CaseInsensitiveOption}, 0033 // Signature 0034 QRegularExpression{QStringLiteral("<p>.?--(\\s|\u00A0)<br>"), QRegularExpression::CaseInsensitiveOption}, 0035 }; 0036 0037 for (const auto &expression : delimiters) { 0038 auto i = expression.globalMatch(text); 0039 while (i.hasNext()) { 0040 const auto match = i.next(); 0041 const int startOffset = match.capturedStart(0); 0042 // This is a very simplistic detection for an inline reply where we would have the patterns before the actual message content. 0043 // We simply ignore anything we find within the first few lines. 0044 if (startOffset >= 5) { 0045 return {text.mid(0, startOffset), true}; 0046 } else { 0047 // Search for the next delimiter 0048 continue; 0049 } 0050 } 0051 } 0052 0053 return {text, false}; 0054 } 0055 0056 static QString addCss(const QString &s) 0057 { 0058 // Get the default font from QApplication 0059 static const QString fontFamily = QFont{}.family(); 0060 // overflow:hidden ensures no scrollbars are ever shown. 0061 static const QString css = QStringLiteral("<style>\n") 0062 + QStringLiteral( 0063 "body {\n" 0064 " overflow:hidden;\n" 0065 " font-family: \"%1\" ! important;\n" 0066 " color: #31363b ! important;\n" 0067 " background-color: #fcfcfc ! important\n" 0068 "}\n") 0069 .arg(fontFamily) 0070 + QStringLiteral("blockquote { \n" 0071 " border-left: 2px solid #bdc3c7 ! important;\n" 0072 "}\n") 0073 + QStringLiteral("</style>"); 0074 0075 const QString header = QLatin1String( 0076 "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">\n" 0077 "<html><head><title></title>") 0078 + css + QLatin1String("</head>\n<body>\n"); 0079 return header + s + QStringLiteral("</body></html>"); 0080 } 0081 0082 class PartModelPrivate 0083 { 0084 public: 0085 PartModelPrivate(PartModel *q_ptr, const std::shared_ptr<MimeTreeParser::ObjectTreeParser> &parser) 0086 : q(q_ptr) 0087 , mParser(parser) 0088 { 0089 collectContents(); 0090 } 0091 0092 ~PartModelPrivate() = default; 0093 0094 void checkPart(const MimeTreeParser::MessagePart::Ptr part) 0095 { 0096 mMimeTypeCache[part.data()] = part->mimeType(); 0097 // Extract the content of the part and 0098 mContents.insert(part.data(), extractContent(part.data())); 0099 } 0100 0101 // Recursively find encapsulated messages 0102 void findEncapsulated(const MimeTreeParser::EncapsulatedRfc822MessagePart::Ptr &e) 0103 { 0104 mEncapsulatedParts[e.data()] = mParser->collectContentParts(e); 0105 for (auto subPart : mEncapsulatedParts[e.data()]) { 0106 checkPart(subPart); 0107 mParents[subPart.data()] = e.data(); 0108 if (auto encapsulatedSub = subPart.dynamicCast<MimeTreeParser::EncapsulatedRfc822MessagePart>()) { 0109 findEncapsulated(encapsulatedSub); 0110 } 0111 } 0112 } 0113 0114 QVariant extractContent(MimeTreeParser::MessagePart *messagePart) 0115 { 0116 if (auto alternativePart = dynamic_cast<MimeTreeParser::AlternativeMessagePart *>(messagePart)) { 0117 if (alternativePart->availableModes().contains(MimeTreeParser::AlternativeMessagePart::MultipartIcal)) { 0118 return alternativePart->icalContent(); 0119 } 0120 } 0121 0122 auto preprocessPlaintext = [&](const QString &text) { 0123 // We always do rich text (so we get highlighted links and stuff). 0124 // NOTE: this inserts non-breaking spaces instead of regular spaces. 0125 const auto html = Qt::convertFromPlainText(text); 0126 if (trimMail) { 0127 const auto result = PartModel::trim(html); 0128 isTrimmed = result.second; 0129 Q_EMIT q->trimMailChanged(); 0130 return HtmlUtils::linkify(result.first); 0131 } 0132 return HtmlUtils::linkify(html); 0133 }; 0134 0135 if (messagePart->isHtml()) { 0136 if (dynamic_cast<MimeTreeParser::AlternativeMessagePart *>(messagePart)) { 0137 containsHtmlAndPlain = true; 0138 Q_EMIT q->containsHtmlChanged(); 0139 if (!showHtml) { 0140 return preprocessPlaintext(messagePart->plaintextContent()); 0141 } 0142 } 0143 return addCss(mParser->resolveCidLinks(messagePart->htmlContent())); 0144 } 0145 return preprocessPlaintext(messagePart->text()); 0146 } 0147 0148 QVariant contentForPart(MimeTreeParser::MessagePart *messagePart) const 0149 { 0150 return mContents.value(messagePart); 0151 } 0152 0153 void collectContents() 0154 { 0155 mEncapsulatedParts.clear(); 0156 mParents.clear(); 0157 mContents.clear(); 0158 containsHtmlAndPlain = false; 0159 isTrimmed = false; 0160 0161 const auto parts = mParser->collectContentParts(); 0162 for (auto part : parts) { 0163 checkPart(part); 0164 if (auto encapsulatedPart = part.dynamicCast<MimeTreeParser::EncapsulatedRfc822MessagePart>()) { 0165 findEncapsulated(encapsulatedPart); 0166 } 0167 } 0168 for (auto part : parts) { 0169 if (mMimeTypeCache[part.data()] == "text/calendar") { 0170 mParts.prepend(part); 0171 } else { 0172 mParts.append(part); 0173 } 0174 } 0175 } 0176 0177 PartModel *q; 0178 QVector<MimeTreeParser::MessagePartPtr> mParts; 0179 QHash<MimeTreeParser::MessagePart *, QByteArray> mMimeTypeCache; 0180 QHash<MimeTreeParser::MessagePart *, QVector<MimeTreeParser::MessagePartPtr>> mEncapsulatedParts; 0181 QHash<MimeTreeParser::MessagePart *, MimeTreeParser::MessagePart *> mParents; 0182 QMap<MimeTreeParser::MessagePart *, QVariant> mContents; 0183 std::shared_ptr<MimeTreeParser::ObjectTreeParser> mParser; 0184 bool showHtml{true}; // show html by default 0185 bool containsHtmlAndPlain{false}; 0186 bool trimMail{false}; 0187 bool isTrimmed{false}; 0188 }; 0189 0190 PartModel::PartModel(std::shared_ptr<MimeTreeParser::ObjectTreeParser> parser) 0191 : d(std::unique_ptr<PartModelPrivate>(new PartModelPrivate(this, parser))) 0192 { 0193 } 0194 0195 PartModel::~PartModel() 0196 { 0197 } 0198 0199 void PartModel::setShowHtml(bool html) 0200 { 0201 if (d->showHtml != html) { 0202 beginResetModel(); 0203 d->showHtml = html; 0204 d->collectContents(); 0205 endResetModel(); 0206 Q_EMIT showHtmlChanged(); 0207 } 0208 } 0209 0210 bool PartModel::showHtml() const 0211 { 0212 return d->showHtml; 0213 } 0214 0215 void PartModel::setTrimMail(bool trim) 0216 { 0217 if (d->trimMail != trim) { 0218 beginResetModel(); 0219 d->trimMail = trim; 0220 d->collectContents(); 0221 endResetModel(); 0222 Q_EMIT trimMailChanged(); 0223 } 0224 } 0225 0226 bool PartModel::trimMail() const 0227 { 0228 return d->trimMail; 0229 } 0230 0231 bool PartModel::isTrimmed() const 0232 { 0233 return d->isTrimmed; 0234 } 0235 0236 bool PartModel::containsHtml() const 0237 { 0238 return d->containsHtmlAndPlain; 0239 } 0240 0241 QHash<int, QByteArray> PartModel::roleNames() const 0242 { 0243 QHash<int, QByteArray> roles; 0244 roles[TypeRole] = "type"; 0245 roles[ContentRole] = "content"; 0246 roles[IsEmbeddedRole] = "embedded"; 0247 roles[IsEncryptedRole] = "encrypted"; 0248 roles[IsSignedRole] = "signed"; 0249 roles[SecurityLevelRole] = "securityLevel"; 0250 roles[EncryptionSecurityLevelRole] = "encryptionSecurityLevel"; 0251 roles[SignatureSecurityLevelRole] = "signatureSecurityLevel"; 0252 roles[ErrorType] = "errorType"; 0253 roles[ErrorString] = "errorString"; 0254 roles[IsErrorRole] = "error"; 0255 roles[SenderRole] = "sender"; 0256 roles[SignatureDetails] = "signatureDetails"; 0257 roles[EncryptionDetails] = "encryptionDetails"; 0258 roles[DateRole] = "date"; 0259 return roles; 0260 } 0261 0262 QModelIndex PartModel::index(int row, int column, const QModelIndex &parent) const 0263 { 0264 if (row < 0 || column != 0) { 0265 return QModelIndex(); 0266 } 0267 if (parent.isValid()) { 0268 const auto part = static_cast<MimeTreeParser::MessagePart *>(parent.internalPointer()); 0269 auto encapsulatedPart = dynamic_cast<MimeTreeParser::EncapsulatedRfc822MessagePart *>(part); 0270 0271 if (encapsulatedPart) { 0272 const auto parts = d->mEncapsulatedParts[encapsulatedPart]; 0273 if (row < parts.size()) { 0274 return createIndex(row, column, parts.at(row).data()); 0275 } 0276 } 0277 return QModelIndex(); 0278 } 0279 if (row < d->mParts.size()) { 0280 return createIndex(row, column, d->mParts.at(row).data()); 0281 } 0282 return QModelIndex(); 0283 } 0284 0285 SignatureInfo *encryptionInfo(MimeTreeParser::MessagePart *messagePart) 0286 { 0287 auto signatureInfo = new SignatureInfo; 0288 const auto encryptions = messagePart->encryptions(); 0289 if (encryptions.size() > 1) { 0290 qWarning() << "Can't deal with more than one encryption"; 0291 } 0292 for (const auto &encryptionPart : encryptions) { 0293 signatureInfo->keyId = encryptionPart->partMetaData()->keyId; 0294 } 0295 return signatureInfo; 0296 }; 0297 0298 SignatureInfo *signatureInfo(MimeTreeParser::MessagePart *messagePart) 0299 { 0300 auto signatureInfo = new SignatureInfo; 0301 const auto signatures = messagePart->signatures(); 0302 if (signatures.size() > 1) { 0303 qWarning() << "Can't deal with more than one signature"; 0304 } 0305 for (const auto &signaturePart : signatures) { 0306 signatureInfo->keyId = signaturePart->partMetaData()->keyId; 0307 signatureInfo->keyMissing = signaturePart->partMetaData()->keyMissing; 0308 signatureInfo->keyExpired = signaturePart->partMetaData()->keyExpired; 0309 signatureInfo->keyRevoked = signaturePart->partMetaData()->keyRevoked; 0310 signatureInfo->sigExpired = signaturePart->partMetaData()->sigExpired; 0311 signatureInfo->crlMissing = signaturePart->partMetaData()->crlMissing; 0312 signatureInfo->crlTooOld = signaturePart->partMetaData()->crlTooOld; 0313 signatureInfo->signer = signaturePart->partMetaData()->signer; 0314 signatureInfo->signerMailAddresses = signaturePart->partMetaData()->signerMailAddresses; 0315 signatureInfo->signatureIsGood = signaturePart->partMetaData()->isGoodSignature; 0316 signatureInfo->keyIsTrusted = signaturePart->partMetaData()->keyIsTrusted; 0317 } 0318 return signatureInfo; 0319 } 0320 0321 QVariant PartModel::data(const QModelIndex &index, int role) const 0322 { 0323 if (!index.isValid()) { 0324 return QVariant(); 0325 } 0326 0327 if (index.internalPointer()) { 0328 const auto messagePart = static_cast<MimeTreeParser::MessagePart *>(index.internalPointer()); 0329 // qWarning() << "Found message part " << messagePart->metaObject()->className() << messagePart->partMetaData()->status << messagePart->error(); 0330 Q_ASSERT(messagePart); 0331 switch (role) { 0332 case Qt::DisplayRole: 0333 return QStringLiteral("Content%1"); 0334 case SenderRole: { 0335 if (auto e = dynamic_cast<MimeTreeParser::EncapsulatedRfc822MessagePart *>(messagePart)) { 0336 return e->from(); 0337 } 0338 return {}; 0339 } 0340 case DateRole: { 0341 if (auto e = dynamic_cast<MimeTreeParser::EncapsulatedRfc822MessagePart *>(messagePart)) { 0342 return e->date(); 0343 } 0344 return {}; 0345 } 0346 case TypeRole: { 0347 if (messagePart->error()) { 0348 return QStringLiteral("error"); 0349 } 0350 if (dynamic_cast<MimeTreeParser::EncapsulatedRfc822MessagePart *>(messagePart)) { 0351 return QStringLiteral("encapsulated"); 0352 } 0353 if (auto alternativePart = dynamic_cast<MimeTreeParser::AlternativeMessagePart *>(messagePart)) { 0354 if (alternativePart->availableModes().contains(MimeTreeParser::AlternativeMessagePart::MultipartIcal)) { 0355 return QStringLiteral("ical"); 0356 } 0357 } 0358 if (auto attachmentPart = dynamic_cast<MimeTreeParser::AttachmentMessagePart *>(messagePart)) { 0359 auto node = attachmentPart->node(); 0360 if (!node) { 0361 qWarning() << "no content for attachment"; 0362 return {}; 0363 } 0364 if (d->mMimeTypeCache[attachmentPart] == "text/calendar") { 0365 return QStringLiteral("ical"); 0366 } 0367 } 0368 if (!d->showHtml && d->containsHtmlAndPlain) { 0369 return QStringLiteral("plain"); 0370 } 0371 // For simple html we don't need a browser 0372 auto complexHtml = [&] { 0373 if (messagePart->isHtml()) { 0374 const auto text = messagePart->htmlContent(); 0375 if (text.contains(QStringLiteral("<!DOCTYPE html PUBLIC"))) { 0376 // We can probably deal with this if it adheres to the strict dtd 0377 //(that's what our composer produces as well) 0378 if (!text.contains(QStringLiteral("http://www.w3.org/TR/REC-html40/strict.dtd"))) { 0379 return true; 0380 } 0381 } 0382 // Blockquotes don't support any styling which would be necessary so they become readable. 0383 if (text.contains(QStringLiteral("blockquote"))) { 0384 return true; 0385 } 0386 // Media queries are too advanced 0387 if (text.contains(QStringLiteral("@media"))) { 0388 return true; 0389 } 0390 // auto css properties are not supported e.g margin-left: auto; 0391 if (text.contains(QStringLiteral(": auto;"))) { 0392 return true; 0393 } 0394 return false; 0395 } else { 0396 return false; 0397 } 0398 }(); 0399 if (complexHtml) { 0400 return QStringLiteral("html"); 0401 } 0402 return QStringLiteral("plain"); 0403 } 0404 case IsEmbeddedRole: 0405 return false; 0406 case IsErrorRole: 0407 return messagePart->error(); 0408 case ContentRole: 0409 return d->contentForPart(messagePart); 0410 case IsEncryptedRole: 0411 return messagePart->encryptionState() != MimeTreeParser::KMMsgNotEncrypted; 0412 case IsSignedRole: 0413 return messagePart->signatureState() != MimeTreeParser::KMMsgNotSigned; 0414 case SecurityLevelRole: { 0415 auto signature = messagePart->signatureState(); 0416 auto encryption = messagePart->encryptionState(); 0417 bool messageIsSigned = signature == MimeTreeParser::KMMsgPartiallySigned || signature == MimeTreeParser::KMMsgFullySigned; 0418 bool messageIsEncrypted = encryption == MimeTreeParser::KMMsgPartiallyEncrypted || encryption == MimeTreeParser::KMMsgFullyEncrypted; 0419 0420 if (messageIsSigned) { 0421 auto sigInfo = std::unique_ptr<SignatureInfo>{signatureInfo(messagePart)}; 0422 if (!sigInfo->signatureIsGood) { 0423 if (sigInfo->keyMissing || sigInfo->keyExpired) { 0424 return QStringLiteral("notsogood"); 0425 } 0426 return QStringLiteral("bad"); 0427 } 0428 } 0429 // All good 0430 if (messageIsSigned || messageIsEncrypted) { 0431 return QStringLiteral("good"); 0432 } 0433 // No info 0434 return QStringLiteral("unknown"); 0435 } 0436 case EncryptionSecurityLevelRole: { 0437 auto encryption = messagePart->encryptionState(); 0438 bool messageIsEncrypted = encryption == MimeTreeParser::KMMsgPartiallyEncrypted || encryption == MimeTreeParser::KMMsgFullyEncrypted; 0439 if (messagePart->error()) { 0440 return QStringLiteral("bad"); 0441 } 0442 // All good 0443 if (messageIsEncrypted) { 0444 return QStringLiteral("good"); 0445 } 0446 // No info 0447 return QStringLiteral("unknown"); 0448 } 0449 case SignatureSecurityLevelRole: { 0450 auto signature = messagePart->signatureState(); 0451 bool messageIsSigned = signature == MimeTreeParser::KMMsgPartiallySigned || signature == MimeTreeParser::KMMsgFullySigned; 0452 if (messageIsSigned) { 0453 auto sigInfo = std::unique_ptr<SignatureInfo>{signatureInfo(messagePart)}; 0454 if (!sigInfo->signatureIsGood) { 0455 if (sigInfo->keyMissing || sigInfo->keyExpired) { 0456 return QStringLiteral("notsogood"); 0457 } 0458 return QStringLiteral("bad"); 0459 } 0460 return QStringLiteral("good"); 0461 } 0462 // No info 0463 return QStringLiteral("unknown"); 0464 } 0465 case SignatureDetails: 0466 return QVariant::fromValue(signatureInfo(messagePart)); 0467 case EncryptionDetails: 0468 return QVariant::fromValue(encryptionInfo(messagePart)); 0469 case ErrorType: 0470 return messagePart->error(); 0471 case ErrorString: { 0472 switch (messagePart->error()) { 0473 case MimeTreeParser::MessagePart::NoKeyError: 0474 return i18n("No key available."); 0475 case MimeTreeParser::MessagePart::PassphraseError: 0476 return i18n("Wrong passphrase."); 0477 case MimeTreeParser::MessagePart::UnknownError: 0478 break; 0479 default: 0480 break; 0481 } 0482 return messagePart->errorString(); 0483 } 0484 } 0485 } 0486 return QVariant(); 0487 } 0488 0489 QModelIndex PartModel::parent(const QModelIndex &index) const 0490 { 0491 if (index.isValid()) { 0492 if (auto indexPart = static_cast<MimeTreeParser::MessagePart *>(index.internalPointer())) { 0493 for (const auto &part : d->mParts) { 0494 if (part.data() == indexPart) { 0495 return QModelIndex(); 0496 } 0497 } 0498 const auto parentPart = d->mParents[indexPart]; 0499 Q_ASSERT(parentPart); 0500 int row = 0; 0501 const auto parts = d->mEncapsulatedParts[parentPart]; 0502 for (const auto &part : parts) { 0503 if (part.data() == indexPart) { 0504 break; 0505 } 0506 row++; 0507 } 0508 return createIndex(row, 0, parentPart); 0509 } 0510 return {}; 0511 } 0512 return {}; 0513 } 0514 0515 int PartModel::rowCount(const QModelIndex &parent) const 0516 { 0517 if (parent.isValid()) { 0518 const auto part = static_cast<MimeTreeParser::MessagePart *>(parent.internalPointer()); 0519 auto encapsulatedPart = dynamic_cast<MimeTreeParser::EncapsulatedRfc822MessagePart *>(part); 0520 0521 if (encapsulatedPart) { 0522 const auto parts = d->mEncapsulatedParts[encapsulatedPart]; 0523 return parts.size(); 0524 } 0525 return 0; 0526 } 0527 return d->mParts.count(); 0528 } 0529 0530 int PartModel::columnCount(const QModelIndex &) const 0531 { 0532 return 1; 0533 }