File indexing completed on 2024-06-16 05:01:21
0001 /* Copyright (C) 2006 - 2014 Jan Kundrát <jkt@flaska.net> 0002 0003 This file is part of the Trojita Qt IMAP e-mail client, 0004 http://trojita.flaska.net/ 0005 0006 This program is free software; you can redistribute it and/or 0007 modify it under the terms of the GNU General Public License as 0008 published by the Free Software Foundation; either version 2 of 0009 the License or (at your option) version 3 or any later version 0010 accepted by the membership of KDE e.V. (or its successor approved 0011 by the membership of KDE e.V.), which shall act as a proxy 0012 defined in Section 14 of version 3 of the license. 0013 0014 This program is distributed in the hope that it will be useful, 0015 but WITHOUT ANY WARRANTY; without even the implied warranty of 0016 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 0017 GNU General Public License for more details. 0018 0019 You should have received a copy of the GNU General Public License 0020 along with this program. If not, see <http://www.gnu.org/licenses/>. 0021 */ 0022 0023 #include "ComposerAttachments.h" 0024 #include <QBuffer> 0025 #include <QFileInfo> 0026 #include <QMimeData> 0027 #include <QMimeDatabase> 0028 #include <QProcess> 0029 #include <QUrl> 0030 #include "Composer/MessageComposer.h" 0031 #include "Imap/Encoders.h" 0032 #include "Imap/Model/FullMessageCombiner.h" 0033 #include "Imap/Model/ItemRoles.h" 0034 #include "Imap/Model/MailboxTree.h" 0035 #include "Imap/Model/Model.h" 0036 #include "Imap/Network/MsgPartNetAccessManager.h" 0037 #include "UiUtils/Formatting.h" 0038 0039 using namespace Imap::Mailbox; 0040 0041 namespace Composer { 0042 0043 QByteArray contentDispositionToByteArray(const ContentDisposition cdn) 0044 { 0045 switch (cdn) { 0046 case CDN_INLINE: 0047 return "inline"; 0048 case CDN_ATTACHMENT: 0049 return "attachment"; 0050 } 0051 Q_ASSERT(false); 0052 // failsafe from RFC 2183 0053 return "attachment"; 0054 } 0055 0056 /** @short Parse a part's Content-Transfer-Encoding to our enum */ 0057 AttachmentItem::ContentTransferEncoding partCTE(const QModelIndex &index) 0058 { 0059 QByteArray cte = index.data(RolePartTransferEncoding).toByteArray(); 0060 if (cte == "7bit") { 0061 return AttachmentItem::ContentTransferEncoding::SevenBit; 0062 } else if (cte == "8bit") { 0063 return AttachmentItem::ContentTransferEncoding::EightBit; 0064 } else if (cte == "binary") { 0065 return AttachmentItem::ContentTransferEncoding::Binary; 0066 } else if (cte == "base64") { 0067 return AttachmentItem::ContentTransferEncoding::Base64; 0068 } else if (cte == "quoted-printable") { 0069 return AttachmentItem::ContentTransferEncoding::QuotedPrintable; 0070 } else { 0071 // https://tools.ietf.org/html/rfc2045#section-6.1 0072 return AttachmentItem::ContentTransferEncoding::SevenBit; 0073 } 0074 0075 } 0076 0077 /** @short Return a CTE suitable for transmission of the specified MIME container */ 0078 AttachmentItem::ContentTransferEncoding containerPartCTE(const QModelIndex &index) 0079 { 0080 const auto cte = partCTE(index); 0081 switch (cte) { 0082 case AttachmentItem::ContentTransferEncoding::SevenBit: 0083 case AttachmentItem::ContentTransferEncoding::EightBit: 0084 case AttachmentItem::ContentTransferEncoding::Binary: 0085 return cte; 0086 case AttachmentItem::ContentTransferEncoding::Base64: 0087 case AttachmentItem::ContentTransferEncoding::QuotedPrintable: 0088 // Well, we're pretty screwed here :(, the original message is either gone now (which is the better outcome), 0089 // or it does not specify a valid an allowed content encoding. 0090 // The composite types, and message/rfc822 is one of them, are not allowed to be encoded in anything but 0091 // 7bit, 8bit and binary (http://tools.ietf.org/html/rfc2045#page-17). 0092 // Let's assume "7bit", which is the default in RFC 2045. 0093 return AttachmentItem::ContentTransferEncoding::SevenBit; 0094 } 0095 Q_UNREACHABLE(); 0096 } 0097 0098 AttachmentItem::AttachmentItem(): m_contentDisposition(CDN_ATTACHMENT) 0099 { 0100 } 0101 0102 AttachmentItem::~AttachmentItem() 0103 { 0104 } 0105 0106 QByteArray AttachmentItem::contentDispositionHeader() const 0107 { 0108 // Looks like Thunderbird ignores attachments with funky MIME type sent with "Content-Disposition: attachment" 0109 // when they are not marked with the "filename" option. 0110 // Either I'm having a really, really bad day and I'm missing something, or they made a rather stupid bug. 0111 0112 QString shortFileName = contentDispositionFilename(); 0113 if (shortFileName.isEmpty()) 0114 shortFileName = QStringLiteral("attachment"); 0115 return "Content-Disposition: " + contentDispositionToByteArray(m_contentDisposition) + ";\r\n\t" + 0116 Imap::encodeRfc2231Parameter("filename", shortFileName) + "\r\n"; 0117 } 0118 0119 ContentDisposition AttachmentItem::contentDispositionMode() const 0120 { 0121 return m_contentDisposition; 0122 } 0123 0124 bool AttachmentItem::setContentDispositionMode(const ContentDisposition contentDisposition) 0125 { 0126 m_contentDisposition = contentDisposition; 0127 return true; 0128 } 0129 0130 FileAttachmentItem::FileAttachmentItem(const QString &fileName): 0131 fileName(fileName) 0132 { 0133 } 0134 0135 FileAttachmentItem::~FileAttachmentItem() 0136 { 0137 } 0138 0139 QString FileAttachmentItem::caption() const 0140 { 0141 QString realFileName = QFileInfo(fileName).fileName(); 0142 if (!preferredName.isEmpty() && realFileName != preferredName) { 0143 //: Translators: %1 and %2 are file names of an attachment. 0144 //: %1 is the name that will be present in the e-mail and 0145 //: %2 is the original name on disk. 0146 return MessageComposer::tr("%1\n(from %2)").arg(preferredName, realFileName); 0147 } else { 0148 return realFileName; 0149 } 0150 } 0151 0152 QString FileAttachmentItem::tooltip() const 0153 { 0154 QFileInfo f(fileName); 0155 0156 if (!f.exists()) 0157 return MessageComposer::tr("File does not exist"); 0158 0159 if (!f.isReadable()) 0160 return MessageComposer::tr("File is not readable"); 0161 0162 return UiUtils::Formatting::htmlEscaped( 0163 MessageComposer::tr("%1: %2, %3").arg( 0164 fileName, 0165 QString::fromUtf8(mimeType()), 0166 UiUtils::Formatting::prettySize(f.size()) 0167 )); 0168 } 0169 0170 bool FileAttachmentItem::isAvailableLocally() const 0171 { 0172 QFileInfo info(fileName); 0173 return info.isFile() && info.isReadable(); 0174 } 0175 0176 QSharedPointer<QIODevice> FileAttachmentItem::rawData() const 0177 { 0178 QSharedPointer<QIODevice> io(new QFile(fileName)); 0179 io->open(QIODevice::ReadOnly); 0180 return io; 0181 } 0182 0183 QByteArray FileAttachmentItem::mimeType() const 0184 { 0185 if (!m_cachedMime.isEmpty()) 0186 return m_cachedMime; 0187 0188 QMimeDatabase mimeDb; 0189 m_cachedMime = mimeDb.mimeTypeForFile(fileName).name().toUtf8(); 0190 return m_cachedMime; 0191 } 0192 0193 QString FileAttachmentItem::contentDispositionFilename() const 0194 { 0195 if (!preferredName.isEmpty()) 0196 return preferredName; 0197 QString shortFileName = QFileInfo(fileName).fileName(); 0198 if (shortFileName.isEmpty()) 0199 shortFileName = QStringLiteral("attachment"); 0200 return shortFileName; 0201 } 0202 0203 bool FileAttachmentItem::setPreferredFileName(const QString &name) 0204 { 0205 preferredName = name; 0206 return true; 0207 } 0208 0209 AttachmentItem::ContentTransferEncoding FileAttachmentItem::suggestedCTE() const 0210 { 0211 return AttachmentItem::ContentTransferEncoding::Base64; 0212 } 0213 0214 QByteArray FileAttachmentItem::imapUrl() const 0215 { 0216 // It's a local item, it cannot really be on an IMAP server 0217 return QByteArray(); 0218 } 0219 0220 void FileAttachmentItem::preload() const 0221 { 0222 // Don't need to do anything 0223 // We could possibly leave this file open to prevent eventual deletion, but it's probably not worth the effort. 0224 } 0225 0226 void FileAttachmentItem::asDroppableMimeData(QDataStream &stream) const 0227 { 0228 stream << ATTACHMENT_FILE << fileName; 0229 } 0230 0231 0232 ImapMessageAttachmentItem::ImapMessageAttachmentItem(Model *model, const QString &mailbox, const uint uidValidity, const uint uid): 0233 fullMessageCombiner(0) 0234 { 0235 Q_ASSERT(model); 0236 TreeItemMailbox *mboxPtr = model->findMailboxByName(mailbox); 0237 if (!mboxPtr) 0238 throw Imap::UnknownMessageIndex("No such mailbox"); 0239 0240 if (mboxPtr->syncState.uidValidity() != uidValidity) 0241 throw Imap::UnknownMessageIndex("UIDVALIDITY mismatch"); 0242 0243 QList<TreeItemMessage*> messages = model->findMessagesByUids(mboxPtr, Imap::Uids() << uid); 0244 if (messages.isEmpty()) 0245 throw Imap::UnknownMessageIndex("No such UID"); 0246 0247 Q_ASSERT(messages.size() == 1); 0248 index = messages.front()->toIndex(model); 0249 fullMessageCombiner = new FullMessageCombiner(index); 0250 } 0251 0252 ImapMessageAttachmentItem::~ImapMessageAttachmentItem() 0253 { 0254 delete fullMessageCombiner; 0255 } 0256 0257 QString ImapMessageAttachmentItem::caption() const 0258 { 0259 if (!index.isValid()) 0260 return MessageComposer::tr("Message not available"); 0261 QString subject = index.data(RoleMessageSubject).toString(); 0262 if (!preferredName.isEmpty() && subject + QLatin1String(".eml") != preferredName) { 0263 return MessageComposer::tr("%1\n(%2)").arg(preferredName, subject); 0264 } else { 0265 return subject; 0266 } 0267 } 0268 0269 QString ImapMessageAttachmentItem::tooltip() const 0270 { 0271 if (!index.isValid()) 0272 return QString(); 0273 return MessageComposer::tr("IMAP message %1").arg(QString::fromUtf8(imapUrl())); 0274 } 0275 0276 QString ImapMessageAttachmentItem::contentDispositionFilename() const 0277 { 0278 if (!preferredName.isEmpty()) 0279 return preferredName; 0280 if (!index.isValid()) 0281 return QStringLiteral("attachment.eml"); 0282 return index.data(RoleMessageSubject).toString() + QLatin1String(".eml"); 0283 } 0284 0285 bool ImapMessageAttachmentItem::setPreferredFileName(const QString &name) 0286 { 0287 preferredName = name; 0288 return true; 0289 } 0290 0291 QByteArray ImapMessageAttachmentItem::mimeType() const 0292 { 0293 return "message/rfc822"; 0294 } 0295 0296 bool ImapMessageAttachmentItem::isAvailableLocally() const 0297 { 0298 return fullMessageCombiner->loaded(); 0299 } 0300 0301 QSharedPointer<QIODevice> ImapMessageAttachmentItem::rawData() const 0302 { 0303 if (!index.isValid()) 0304 return QSharedPointer<QIODevice>(); 0305 0306 QSharedPointer<QIODevice> io(new QBuffer()); 0307 // This can probably be optimized to allow zero-copy operation through a pair of two QIODevices 0308 static_cast<QBuffer*>(io.data())->setData(fullMessageCombiner->data()); 0309 io->open(QIODevice::ReadOnly); 0310 return io; 0311 } 0312 0313 AttachmentItem::ContentTransferEncoding ImapMessageAttachmentItem::suggestedCTE() const 0314 { 0315 // The relevant thing is the CTE of the root MIME part, not the message itself. 0316 // It's not even supported by Trojita for TreeItemMessage. 0317 0318 QModelIndex rootPart = index.model()->index(0, 0, index); 0319 if (rootPart.data(RolePartIsTopLevelMultipart).toBool()) { 0320 // This was a desperate attempt; the BODYSTRUCTURE does *not* contain the body-fld-enc field for multiparts, 0321 // so if our message happens to have a top-level multipart, we're out of luck and will produce an invalid result. 0322 // See http://mailman2.u.washington.edu/pipermail/imap-protocol/2013-October/002109.html for details. 0323 // Let's try to "play it safe" and assume that the children *might* contain 8bit data. We are still hoping for 0324 // the best (i.e. if the message was actually using the "binary" CTE, we would be screwed), but I guess this is 0325 // better than potentially lying by claiming that this is just a 7bit message. Suggestions welcome. 0326 return AttachmentItem::ContentTransferEncoding::EightBit; 0327 } else { 0328 return containerPartCTE(rootPart); 0329 } 0330 } 0331 0332 QByteArray ImapMessageAttachmentItem::imapUrl() const 0333 { 0334 return index.data(RoleIMAPRelativeUrl).toByteArray(); 0335 } 0336 0337 void ImapMessageAttachmentItem::preload() const 0338 { 0339 fullMessageCombiner->load(); 0340 } 0341 0342 void ImapMessageAttachmentItem::asDroppableMimeData(QDataStream &stream) const 0343 { 0344 stream << ATTACHMENT_IMAP_MESSAGE << index.data(RoleMailboxName).toString() << 0345 index.data(RoleMailboxUidValidity).toUInt() << (QList<uint>() <<index.data(RoleMessageUid).toUInt()); 0346 } 0347 0348 0349 ImapPartAttachmentItem::ImapPartAttachmentItem(Model *model, const QString &mailbox, const uint uidValidity, const uint uid, 0350 const QByteArray &trojitaPath) 0351 { 0352 TreeItemMailbox *mboxPtr = model->findMailboxByName(mailbox); 0353 if (!mboxPtr) 0354 throw Imap::UnknownMessageIndex("No such mailbox"); 0355 0356 if (mboxPtr->syncState.uidValidity() != uidValidity) 0357 throw Imap::UnknownMessageIndex("UIDVALIDITY mismatch"); 0358 0359 QList<TreeItemMessage*> messages = model->findMessagesByUids(mboxPtr, Imap::Uids() << uid); 0360 if (messages.isEmpty()) 0361 throw Imap::UnknownMessageIndex("UID not found"); 0362 0363 Q_ASSERT(messages.size() == 1); 0364 0365 QModelIndex partIndex = Imap::Network::MsgPartNetAccessManager::pathToPart(messages.front()->toIndex(model), trojitaPath); 0366 if (!partIndex.isValid()) 0367 throw Imap::UnknownMessageIndex("No such part"); 0368 0369 if (partIndex.data(Imap::Mailbox::RolePartMimeType).toString().startsWith(QLatin1String("multipart/"))) { 0370 // Yes, we absolutely do abuse this exception now. Any better ideas? 0371 throw Imap::UnknownMessageIndex("Cannot attach multipart/* MIME containers"); 0372 } 0373 index = partIndex; 0374 } 0375 0376 ImapPartAttachmentItem::~ImapPartAttachmentItem() 0377 { 0378 } 0379 0380 QString ImapPartAttachmentItem::caption() const 0381 { 0382 QString partName = index.data(RolePartFileName).toString(); 0383 if (!index.isValid() || (preferredName.isEmpty() && partName.isEmpty())) { 0384 return MessageComposer::tr("IMAP part %1").arg(QString::fromUtf8(imapUrl())); 0385 } else if (!preferredName.isEmpty()) { 0386 return preferredName; 0387 } else { 0388 return partName; 0389 } 0390 } 0391 0392 QString ImapPartAttachmentItem::tooltip() const 0393 { 0394 if (!index.isValid()) 0395 return QString(); 0396 return MessageComposer::tr("%1, %2").arg(index.data(RolePartMimeType).toString(), 0397 UiUtils::Formatting::prettySize(index.data(RolePartOctets).toULongLong())); 0398 } 0399 0400 QByteArray ImapPartAttachmentItem::mimeType() const 0401 { 0402 return index.data(RolePartMimeType).toString().toUtf8(); 0403 } 0404 0405 QString ImapPartAttachmentItem::contentDispositionFilename() const 0406 { 0407 if (!preferredName.isEmpty()) 0408 return preferredName; 0409 QString res = index.data(RolePartFileName).toString(); 0410 return res.isEmpty() ? QStringLiteral("attachment") : res; 0411 } 0412 0413 bool ImapPartAttachmentItem::setPreferredFileName(const QString &name) 0414 { 0415 preferredName = name; 0416 return true; 0417 } 0418 0419 AttachmentItem::ContentTransferEncoding ImapPartAttachmentItem::suggestedCTE() const 0420 { 0421 auto mimeType = index.data(RolePartMimeType).toString(); 0422 if (mimeType.startsWith(QLatin1String("message/")) || mimeType.startsWith(QLatin1String("multipart/"))) { 0423 // https://tools.ietf.org/html/rfc2045#page-17 0424 return containerPartCTE(index); 0425 } else { 0426 return partCTE(index); 0427 } 0428 } 0429 0430 QSharedPointer<QIODevice> ImapPartAttachmentItem::rawData() const 0431 { 0432 if (!index.isValid() || !index.data(RoleIsFetched).toBool()) 0433 return QSharedPointer<QIODevice>(); 0434 0435 QSharedPointer<QIODevice> io(new QBuffer()); 0436 static_cast<QBuffer*>(io.data())->setData(index.data(RolePartData).toByteArray()); 0437 io->open(QIODevice::ReadOnly); 0438 return io; 0439 } 0440 0441 bool ImapPartAttachmentItem::isAvailableLocally() const 0442 { 0443 return index.data(RoleIsFetched).toBool(); 0444 } 0445 0446 QByteArray ImapPartAttachmentItem::imapUrl() const 0447 { 0448 Q_ASSERT(index.isValid()); 0449 return index.data(RoleIMAPRelativeUrl).toByteArray(); 0450 } 0451 0452 void ImapPartAttachmentItem::preload() const 0453 { 0454 index.data(RolePartData); 0455 } 0456 0457 void ImapPartAttachmentItem::asDroppableMimeData(QDataStream &stream) const 0458 { 0459 Q_ASSERT(index.isValid()); 0460 stream << ATTACHMENT_IMAP_PART << index.data(RoleMailboxName).toString() << index.data(RoleMailboxUidValidity).toUInt() << 0461 index.data(RoleMessageUid).toUInt() << index.data(RolePartPathToPart).toString(); 0462 } 0463 0464 }