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

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_core_debug.h"
0008 
0009 #include <QGpgME/ImportJob>
0010 #include <QGpgME/Protocol>
0011 
0012 #include <KLocalizedString>
0013 #include <KMime/Content>
0014 
0015 #include <QDesktopServices>
0016 #include <QDir>
0017 #include <QFile>
0018 #include <QGuiApplication>
0019 #include <QIcon>
0020 #include <QMimeDatabase>
0021 #include <QMimeType>
0022 #include <QRegularExpression>
0023 #include <QStandardPaths>
0024 #include <QTemporaryDir>
0025 #include <QUrl>
0026 
0027 #ifdef Q_OS_WIN
0028 #include <cstdio>
0029 #include <string>
0030 #include <vector>
0031 #include <windows.h>
0032 #endif
0033 
0034 namespace
0035 {
0036 
0037 QString sizeHuman(float size)
0038 {
0039     QStringList list;
0040     list << QStringLiteral("KB") << QStringLiteral("MB") << QStringLiteral("GB") << QStringLiteral("TB");
0041 
0042     QStringListIterator i(list);
0043     QString unit = QStringLiteral("Bytes");
0044 
0045     while (size >= 1024.0 && i.hasNext()) {
0046         unit = i.next();
0047         size /= 1024.0;
0048     }
0049 
0050     if (unit == QStringLiteral("Bytes")) {
0051         return QString().setNum(size) + QStringLiteral(" ") + unit;
0052     } else {
0053         return QString().setNum(size, 'f', 2) + QStringLiteral(" ") + unit;
0054     }
0055 }
0056 
0057 // SPDX-SnippetBegin
0058 // Copyright (C) 2016 The Qt Company Ltd.
0059 // SPDX-License-Identifier: GPL-3.0-only
0060 
0061 #define WINDOWS_DEVICES_PATTERN "(CON|AUX|PRN|NUL|COM[1-9]|LPT[1-9])(\\..*)?"
0062 
0063 // Naming a file like a device name will break on Windows, even if it is
0064 // "com1.txt". Since we are cross-platform, we generally disallow such file
0065 //  names.
0066 const QRegularExpression &windowsDeviceNoSubDirPattern()
0067 {
0068     static const QRegularExpression rc(QStringLiteral("^" WINDOWS_DEVICES_PATTERN "$"), QRegularExpression::CaseInsensitiveOption);
0069     Q_ASSERT(rc.isValid());
0070     return rc;
0071 }
0072 
0073 const QRegularExpression &windowsDeviceSubDirPattern()
0074 {
0075     static const QRegularExpression rc(QStringLiteral("^.*[/\\\\]" WINDOWS_DEVICES_PATTERN "$"), QRegularExpression::CaseInsensitiveOption);
0076     Q_ASSERT(rc.isValid());
0077     return rc;
0078 }
0079 
0080 /* Validate a file base name, check for forbidden characters/strings. */
0081 
0082 #define SLASHES "/\\"
0083 
0084 static const char notAllowedCharsSubDir[] = ",^@={}[]~!?:&*\"|#%<>$\"'();`' ";
0085 static const char notAllowedCharsNoSubDir[] = ",^@={}[]~!?:&*\"|#%<>$\"'();`' " SLASHES;
0086 
0087 static const char *notAllowedSubStrings[] = {".."};
0088 
0089 bool validateFileName(const QString &name, bool allowDirectories)
0090 {
0091     if (name.isEmpty()) {
0092         return false;
0093     }
0094 
0095     // Characters
0096     const char *notAllowedChars = allowDirectories ? notAllowedCharsSubDir : notAllowedCharsNoSubDir;
0097     for (const char *c = notAllowedChars; *c; c++) {
0098         if (name.contains(QLatin1Char(*c))) {
0099             return false;
0100         }
0101     }
0102 
0103     // Substrings
0104     const int notAllowedSubStringCount = sizeof(notAllowedSubStrings) / sizeof(const char *);
0105     for (int s = 0; s < notAllowedSubStringCount; s++) {
0106         const QLatin1StringView notAllowedSubString(notAllowedSubStrings[s]);
0107         if (name.contains(notAllowedSubString)) {
0108             return false;
0109         }
0110     }
0111 
0112     // Windows devices
0113     bool matchesWinDevice = name.contains(windowsDeviceNoSubDirPattern());
0114     if (!matchesWinDevice && allowDirectories) {
0115         matchesWinDevice = name.contains(windowsDeviceSubDirPattern());
0116     }
0117     return !matchesWinDevice;
0118 }
0119 // SPDX-SnippetEnd
0120 }
0121 
0122 #ifdef Q_OS_WIN
0123 struct WindowFile {
0124     std::wstring fileName;
0125     std::wstring dirName;
0126     HANDLE handle;
0127 };
0128 #endif
0129 
0130 class AttachmentModelPrivate
0131 {
0132 public:
0133     AttachmentModelPrivate(AttachmentModel *q_ptr, const std::shared_ptr<MimeTreeParser::ObjectTreeParser> &parser);
0134 
0135     AttachmentModel *q;
0136     QMimeDatabase mimeDb;
0137     std::shared_ptr<MimeTreeParser::ObjectTreeParser> mParser;
0138     MimeTreeParser::MessagePart::List mAttachments;
0139 
0140 #ifdef Q_OS_WIN
0141     std::vector<WindowFile> mOpenFiles;
0142 #endif
0143 };
0144 
0145 AttachmentModelPrivate::AttachmentModelPrivate(AttachmentModel *q_ptr, const std::shared_ptr<MimeTreeParser::ObjectTreeParser> &parser)
0146     : q(q_ptr)
0147     , mParser(parser)
0148 {
0149     mAttachments = mParser->collectAttachmentParts();
0150 }
0151 
0152 AttachmentModel::AttachmentModel(std::shared_ptr<MimeTreeParser::ObjectTreeParser> parser)
0153     : QAbstractTableModel()
0154     , d(std::unique_ptr<AttachmentModelPrivate>(new AttachmentModelPrivate(this, parser)))
0155 {
0156 }
0157 
0158 AttachmentModel::~AttachmentModel()
0159 {
0160 #ifdef Q_OS_WIN
0161     for (const auto &file : d->mOpenFiles) {
0162         // As owner of the file we need to close our handle first
0163         // With FILE_SHARE_DELETE we have ensured that all _other_ processes must
0164         // have opened the file with FILE_SHARE_DELETE, too.
0165         if (!CloseHandle(file.handle)) {
0166             // Always get the last error before calling any Qt functions that may
0167             // use Windows system calls.
0168             DWORD err = GetLastError();
0169             qWarning() << "Unable to close handle for file" << QString::fromStdWString(file.fileName) << err;
0170         }
0171 
0172         if (!DeleteFileW(file.fileName.c_str())) {
0173             DWORD err = GetLastError();
0174             qWarning() << "Unable to delete file" << QString::fromStdWString(file.fileName) << err;
0175         }
0176 
0177         if (!RemoveDirectoryW(file.dirName.c_str())) {
0178             DWORD err = GetLastError();
0179             qWarning() << "Unable to delete temporary directory" << QString::fromStdWString(file.dirName) << err;
0180         }
0181     }
0182 #endif
0183 }
0184 
0185 QHash<int, QByteArray> AttachmentModel::roleNames() const
0186 {
0187     return {
0188         {TypeRole, QByteArrayLiteral("type")},
0189         {NameRole, QByteArrayLiteral("name")},
0190         {SizeRole, QByteArrayLiteral("size")},
0191         {IconRole, QByteArrayLiteral("iconName")},
0192         {IsEncryptedRole, QByteArrayLiteral("encrypted")},
0193         {IsSignedRole, QByteArrayLiteral("signed")},
0194     };
0195 }
0196 
0197 QVariant AttachmentModel::headerData(int section, Qt::Orientation orientation, int role) const
0198 {
0199     if (orientation == Qt::Horizontal && role == Qt::DisplayRole) {
0200         switch (section) {
0201         case NameColumn:
0202             return i18ndc("mimetreeparser", "@title:column", "Name");
0203         case SizeColumn:
0204             return i18ndc("mimetreeparser", "@title:column", "Size");
0205         case IsEncryptedColumn:
0206             return i18ndc("mimetreeparser", "@title:column", "Encrypted");
0207         case IsSignedColumn:
0208             return i18ndc("mimetreeparser", "@title:column", "Signed");
0209         }
0210     }
0211     return {};
0212 }
0213 
0214 QVariant AttachmentModel::data(const QModelIndex &index, int role) const
0215 {
0216     const auto row = index.row();
0217     const auto column = index.column();
0218 
0219     const auto part = d->mAttachments.at(row);
0220     Q_ASSERT(part);
0221     auto node = part->node();
0222     if (!node) {
0223         qWarning() << "no content for attachment";
0224         return {};
0225     }
0226     const auto mimetype = d->mimeDb.mimeTypeForName(QString::fromLatin1(part->mimeType()));
0227     const auto content = node->encodedContent();
0228 
0229     switch (column) {
0230     case NameColumn:
0231         switch (role) {
0232         case TypeRole:
0233             return mimetype.name();
0234         case Qt::DisplayRole:
0235         case NameRole:
0236             return part->filename();
0237         case IconRole:
0238             return mimetype.iconName();
0239         case Qt::DecorationRole:
0240             return QIcon::fromTheme(mimetype.iconName());
0241         case SizeRole:
0242             return sizeHuman(content.size());
0243         case IsEncryptedRole:
0244             return part->encryptions().size() > 0;
0245         case IsSignedRole:
0246             return part->signatures().size() > 0;
0247         case AttachmentPartRole:
0248             return QVariant::fromValue(part);
0249         default:
0250             return {};
0251         }
0252     case SizeColumn:
0253         switch (role) {
0254         case Qt::DisplayRole:
0255             return sizeHuman(content.size());
0256         default:
0257             return {};
0258         }
0259     case IsEncryptedColumn:
0260         switch (role) {
0261         case Qt::CheckStateRole:
0262             return part->encryptions().size() > 0 ? Qt::Checked : Qt::Unchecked;
0263         default:
0264             return {};
0265         }
0266     case IsSignedColumn:
0267         switch (role) {
0268         case Qt::CheckStateRole:
0269             return part->signatures().size() > 0 ? Qt::Checked : Qt::Unchecked;
0270         default:
0271             return {};
0272         }
0273     default:
0274         return {};
0275     }
0276 }
0277 
0278 QString AttachmentModel::saveAttachmentToPath(const int row, const QString &path)
0279 {
0280     const auto part = d->mAttachments.at(row);
0281     return saveAttachmentToPath(part, path);
0282 }
0283 
0284 QString AttachmentModel::saveAttachmentToPath(const MimeTreeParser::MessagePart::Ptr &part, const QString &path)
0285 {
0286     Q_ASSERT(part);
0287     auto node = part->node();
0288     auto data = node->decodedContent();
0289     // This is necessary to store messages embedded messages (EncapsulatedRfc822MessagePart)
0290     if (data.isEmpty()) {
0291         data = node->encodedContent();
0292     }
0293     if (part->isText()) {
0294         // convert CRLF to LF before writing text attachments to disk
0295         data = KMime::CRLFtoLF(data);
0296     }
0297 
0298     QFile f(path);
0299     if (!f.open(QIODevice::ReadWrite)) {
0300         qCWarning(MIMETREEPARSER_CORE_LOG) << "Failed to write attachment to file:" << path << " Error: " << f.errorString();
0301         Q_EMIT errorOccurred(i18ndc("mimetreeparser", "@info", "Failed to save attachment."));
0302         return {};
0303     }
0304     f.write(data);
0305     f.close();
0306     qCInfo(MIMETREEPARSER_CORE_LOG) << "Wrote attachment to file: " << path;
0307     return path;
0308 }
0309 
0310 bool AttachmentModel::openAttachment(const int row)
0311 {
0312     const auto part = d->mAttachments.at(row);
0313     return openAttachment(part);
0314 }
0315 
0316 bool AttachmentModel::openAttachment(const MimeTreeParser::MessagePart::Ptr &message)
0317 {
0318     QString fileName = message->filename();
0319     QTemporaryDir tempDir(QDir::tempPath() + QLatin1Char('/') + qGuiApp->applicationName() + QStringLiteral(".XXXXXX"));
0320     // TODO: We need some cleanup here. Otherwise the files will stay forever on Windows.
0321     tempDir.setAutoRemove(false);
0322     if (message->filename().isEmpty() || !validateFileName(fileName, false)) {
0323         const auto mimetype = d->mimeDb.mimeTypeForName(QString::fromLatin1(message->mimeType()));
0324         fileName = tempDir.filePath(i18n("attachment") + QLatin1Char('.') + mimetype.preferredSuffix());
0325     } else {
0326         fileName = tempDir.filePath(message->filename());
0327     }
0328 
0329     const auto filePath = saveAttachmentToPath(message, fileName);
0330     if (filePath.isEmpty()) {
0331         Q_EMIT errorOccurred(i18ndc("mimetreeparser", "@info", "Failed to write attachment for opening."));
0332         return false;
0333     }
0334 
0335 #ifdef Q_OS_WIN
0336     std::wstring fileNameStr = filePath.toStdWString();
0337 
0338     HANDLE hFile = CreateFileW(fileNameStr.c_str(),
0339                                GENERIC_READ,
0340                                FILE_SHARE_READ | FILE_SHARE_DELETE, // allow other processes to delete it
0341                                NULL,
0342                                OPEN_EXISTING,
0343                                FILE_ATTRIBUTE_NORMAL, // Using FILE_FLAG_DELETE_ON_CLOSE causes some
0344                                                       // applications like windows zip not to open the
0345                                                       // file.
0346                                NULL // no template
0347     );
0348 
0349     if (hFile == INVALID_HANDLE_VALUE) {
0350         Q_EMIT errorOccurred(i18ndc("mimetreeparser", "@info", "Failed to open attachment."));
0351         QFile file(fileName);
0352         file.remove();
0353         return false;
0354     }
0355 
0356     d->mOpenFiles.push_back({fileNameStr, tempDir.path().toStdWString(), hFile});
0357 #endif
0358 
0359     if (!QDesktopServices::openUrl(QUrl::fromLocalFile(filePath))) {
0360         Q_EMIT errorOccurred(i18ndc("mimetreeparser", "@info", "Failed to open attachment."));
0361         return false;
0362     }
0363 
0364     return true;
0365 }
0366 
0367 bool AttachmentModel::importPublicKey(const int row)
0368 {
0369     const auto part = d->mAttachments.at(row);
0370     return importPublicKey(part);
0371 }
0372 
0373 bool AttachmentModel::importPublicKey(const MimeTreeParser::MessagePart::Ptr &part)
0374 {
0375     Q_ASSERT(part);
0376     const QByteArray certData = part->node()->decodedContent();
0377     QGpgME::ImportJob *importJob = QGpgME::openpgp()->importJob();
0378 
0379     connect(importJob, &QGpgME::AbstractImportJob::result, this, [this](const GpgME::ImportResult &result) {
0380         if (result.numConsidered() == 0) {
0381             Q_EMIT errorOccurred(i18ndc("mimetreeparser", "@info", "No certificates were found in this attachment"));
0382             return;
0383         } else {
0384             QString message = i18ndcp("mimetreeparser", "@info", "one certificate imported", "%1 certificates imported", result.numImported());
0385             if (result.numUnchanged() != 0) {
0386                 message += QStringLiteral("\n")
0387                     + i18ndcp("mimetreeparser",
0388                               "@info",
0389                               "one certificate was already imported",
0390                               "%1 certificates were already imported",
0391                               result.numUnchanged());
0392             }
0393             Q_EMIT info(message);
0394         }
0395     });
0396     GpgME::Error err = importJob->start(certData);
0397     return !err;
0398 }
0399 
0400 int AttachmentModel::rowCount(const QModelIndex &parent) const
0401 {
0402     if (!parent.isValid()) {
0403         return d->mAttachments.size();
0404     }
0405     return 0;
0406 }
0407 
0408 int AttachmentModel::columnCount(const QModelIndex &parent) const
0409 {
0410     if (!parent.isValid()) {
0411         return ColumnCount;
0412     }
0413     return 0;
0414 }
0415 
0416 #include "moc_attachmentmodel.cpp"