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("&gt; 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 }