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"