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