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