File indexing completed on 2024-12-22 05:05:20

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