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 }