File indexing completed on 2024-05-12 05:28:16

0001 // SPDX-FileCopyrightText: 2016 Sandro Knauß <knauss@kolabsys.com>
0002 // SPDX-FileCopyCopyright: 2017 Christian Mollekopf <mollekopf@kolabsys.com>
0003 // SPDX-License-Identifier: LGPL-2.0-or-later
0004 
0005 #include "attachmentmodel.h"
0006 
0007 #include "../mimetreeparser/objecttreeparser.h"
0008 #include "mailcrypto.h"
0009 #include <QString>
0010 
0011 #include <KLocalizedString>
0012 #include <KMime/Content>
0013 #include <QDebug>
0014 #include <QDesktopServices>
0015 #include <QDir>
0016 #include <QFile>
0017 #include <QMimeDatabase>
0018 #include <QStandardPaths>
0019 #include <QUrl>
0020 
0021 #include <memory>
0022 #include <qstringliteral.h>
0023 
0024 QString sizeHuman(float size)
0025 {
0026     QStringList list;
0027     list << QStringLiteral("KB") << QStringLiteral("MB") << QStringLiteral("GB") << QStringLiteral("TB");
0028 
0029     QStringListIterator i(list);
0030     QString unit = QStringLiteral("Bytes");
0031 
0032     while (size >= 1024.0 && i.hasNext()) {
0033         unit = i.next();
0034         size /= 1024.0;
0035     }
0036 
0037     if (unit == QStringLiteral("Bytes")) {
0038         return QString().setNum(size) + QStringLiteral(" ") + unit;
0039     } else {
0040         return QString().setNum(size, 'f', 2) + QStringLiteral(" ") + unit;
0041     }
0042 }
0043 
0044 class AttachmentModelPrivate
0045 {
0046 public:
0047     AttachmentModelPrivate(AttachmentModel *q_ptr, const std::shared_ptr<MimeTreeParser::ObjectTreeParser> &parser);
0048 
0049     AttachmentModel *q;
0050     std::shared_ptr<MimeTreeParser::ObjectTreeParser> mParser;
0051     QVector<MimeTreeParser::MessagePartPtr> mAttachments;
0052 };
0053 
0054 AttachmentModelPrivate::AttachmentModelPrivate(AttachmentModel *q_ptr, const std::shared_ptr<MimeTreeParser::ObjectTreeParser> &parser)
0055     : q(q_ptr)
0056     , mParser(parser)
0057 {
0058     mAttachments = mParser->collectAttachmentParts();
0059 }
0060 
0061 AttachmentModel::AttachmentModel(std::shared_ptr<MimeTreeParser::ObjectTreeParser> parser)
0062     : d(std::unique_ptr<AttachmentModelPrivate>(new AttachmentModelPrivate(this, parser)))
0063 {
0064 }
0065 
0066 AttachmentModel::~AttachmentModel()
0067 {
0068 }
0069 
0070 QHash<int, QByteArray> AttachmentModel::roleNames() const
0071 {
0072     return {
0073         {TypeRole, QByteArrayLiteral("type")},
0074         {NameRole, QByteArrayLiteral("name")},
0075         {SizeRole, QByteArrayLiteral("size")},
0076         {IconRole, QByteArrayLiteral("iconName")},
0077         {IsEncryptedRole, QByteArrayLiteral("encrypted")},
0078         {IsSignedRole, QByteArrayLiteral("signed")},
0079     };
0080 }
0081 
0082 QModelIndex AttachmentModel::index(int row, int column, const QModelIndex &) const
0083 {
0084     if (row < 0 || column != 0) {
0085         return {};
0086     }
0087 
0088     if (row < d->mAttachments.size()) {
0089         return createIndex(row, column, d->mAttachments.at(row).data());
0090     }
0091     return {};
0092 }
0093 
0094 QVariant AttachmentModel::data(const QModelIndex &index, int role) const
0095 {
0096     if (!index.isValid()) {
0097         switch (role) {
0098         case Qt::DisplayRole:
0099             return QLatin1String("root");
0100         }
0101         return {};
0102     }
0103 
0104     if (index.internalPointer()) {
0105         const auto part = static_cast<MimeTreeParser::MessagePart *>(index.internalPointer());
0106         Q_ASSERT(part);
0107         auto node = part->node();
0108         if (!node) {
0109             qWarning() << "no content for attachment";
0110             return {};
0111         }
0112         QMimeDatabase mimeDb;
0113         const auto mimetype = mimeDb.mimeTypeForName(QString::fromLatin1(part->mimeType()));
0114         const auto content = node->encodedContent();
0115         switch (role) {
0116         case TypeRole:
0117             return mimetype.name();
0118         case NameRole:
0119             return part->filename();
0120         case IconRole:
0121             return mimetype.iconName();
0122         case SizeRole:
0123             return sizeHuman(content.size());
0124         case IsEncryptedRole:
0125             return part->encryptions().size() > 0;
0126         case IsSignedRole:
0127             return part->signatures().size() > 0;
0128         }
0129     }
0130     return QVariant();
0131 }
0132 
0133 static QString internalSaveAttachmentToDisk(const QModelIndex &index, const QString &path, bool readonly = false)
0134 {
0135     if (index.internalPointer()) {
0136         const auto part = static_cast<MimeTreeParser::MessagePart *>(index.internalPointer());
0137         Q_ASSERT(part);
0138         auto node = part->node();
0139         auto data = node->decodedContent();
0140         // This is necessary to store messages embedded messages (EncapsulatedRfc822MessagePart)
0141         if (data.isEmpty()) {
0142             data = node->encodedContent();
0143         }
0144         if (part->isText()) {
0145             // convert CRLF to LF before writing text attachments to disk
0146             data = KMime::CRLFtoLF(data);
0147         }
0148         const auto name = part->filename();
0149         QString fname = path + name;
0150 
0151         // Fallback name should we end up with an empty name
0152         if (name.isEmpty()) {
0153             fname = path + QStringLiteral("unnamed");
0154             while (QFileInfo{fname}.exists()) {
0155                 fname = fname + QStringLiteral("_1");
0156             }
0157         }
0158 
0159         // A file with that name already exists, we assume it's the right file
0160         if (QFileInfo{fname}.exists()) {
0161             return fname;
0162         }
0163         QFile f(fname);
0164         if (!f.open(QIODevice::ReadWrite)) {
0165             qWarning() << "Failed to write attachment to file:" << fname << " Error: " << f.errorString();
0166             // Kube::Fabric::Fabric{}.postMessage("notification", {{"message", QObject::tr("Failed to save attachment.")}});
0167             return {};
0168         }
0169         f.write(data);
0170         if (readonly) {
0171             // make file read-only so that nobody gets the impression that he migh edit attached files
0172             f.setPermissions(QFileDevice::ReadUser);
0173         }
0174         f.close();
0175         qInfo() << "Wrote attachment to file: " << fname;
0176         return fname;
0177     }
0178     return {};
0179 }
0180 
0181 bool AttachmentModel::saveAttachmentToDisk(const QModelIndex &index)
0182 {
0183     auto downloadDir = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
0184     if (downloadDir.isEmpty()) {
0185         downloadDir = QStringLiteral("~");
0186     }
0187     downloadDir += QStringLiteral("/kalendar/");
0188     QDir{}.mkpath(downloadDir);
0189 
0190     auto path = internalSaveAttachmentToDisk(index, downloadDir);
0191     if (path.isEmpty()) {
0192         return false;
0193     }
0194     // Kube::Fabric::Fabric{}.postMessage("notification", {{"message", tr("Saved the attachment to disk: %1").arg(path)}});
0195     return true;
0196 }
0197 
0198 bool AttachmentModel::openAttachment(const QModelIndex &index)
0199 {
0200     auto downloadDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + QStringLiteral("/kalendar/");
0201     QDir{}.mkpath(downloadDir);
0202     const auto filePath = internalSaveAttachmentToDisk(index, downloadDir, true);
0203     if (!filePath.isEmpty()) {
0204         if (!QDesktopServices::openUrl(QUrl(QStringLiteral("file://") + filePath))) {
0205             // Kube::Fabric::Fabric{}.postMessage("notification", {{"message", tr("Failed to open attachment.")}});
0206             return false;
0207         }
0208         return true;
0209     }
0210     // Kube::Fabric::Fabric{}.postMessage("notification", {{"message", tr("Failed to save attachment for opening.")}});
0211     return false;
0212 }
0213 
0214 bool AttachmentModel::importPublicKey(const QModelIndex &index)
0215 {
0216     Q_ASSERT(index.internalPointer());
0217     const auto part = static_cast<MimeTreeParser::MessagePart *>(index.internalPointer());
0218     Q_ASSERT(part);
0219     auto result = Crypto::importKey(Crypto::OpenPGP, part->node()->decodedContent());
0220 
0221     bool success = true;
0222     QString message;
0223     if (result.considered == 0) {
0224         message = i18n("No keys were found in this attachment");
0225         success = false;
0226     } else {
0227         message = i18np("one key imported", "%1 keys imported", result.imported);
0228         if (result.unchanged != 0) {
0229             message += QStringLiteral("\n") + i18np("one key was already imported", "%1 keys were already imported", result.unchanged);
0230         }
0231     }
0232 
0233     // Kube::Fabric::Fabric{}.postMessage("notification", {{"message", message}});
0234 
0235     return success;
0236 }
0237 
0238 QModelIndex AttachmentModel::parent(const QModelIndex &) const
0239 {
0240     return {};
0241 }
0242 
0243 int AttachmentModel::rowCount(const QModelIndex &parent) const
0244 {
0245     if (!parent.isValid()) {
0246         return d->mAttachments.size();
0247     }
0248     return 0;
0249 }
0250 
0251 int AttachmentModel::columnCount(const QModelIndex &) const
0252 {
0253     return 1;
0254 }