1 /* Copyright (C) 2006 - 2014 Jan Kundrát <jkt@flaska.net>
2 
3    This file is part of the Trojita Qt IMAP e-mail client,
4    http://trojita.flaska.net/
5 
6    This program is free software; you can redistribute it and/or
7    modify it under the terms of the GNU General Public License as
8    published by the Free Software Foundation; either version 2 of
9    the License or (at your option) version 3 or any later version
10    accepted by the membership of KDE e.V. (or its successor approved
11    by the membership of KDE e.V.), which shall act as a proxy
12    defined in Section 14 of version 3 of the license.
13 
14    This program is distributed in the hope that it will be useful,
15    but WITHOUT ANY WARRANTY; without even the implied warranty of
16    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17    GNU General Public License for more details.
18 
19    You should have received a copy of the GNU General Public License
20    along with this program.  If not, see <http://www.gnu.org/licenses/>.
21 */
22 
23 #include "MessageComposer.h"
24 #include <QBuffer>
25 #include <QMimeData>
26 #include <QMimeDatabase>
27 #include <QUrl>
28 #include <QUuid>
29 #include "Common/Application.h"
30 #include "Composer/ComposerAttachments.h"
31 #include "Imap/Encoders.h"
32 #include "Imap/Model/ItemRoles.h"
33 #include "Imap/Model/Model.h"
34 #include "Imap/Model/Utils.h"
35 #include "UiUtils/IconLoader.h"
36 
37 namespace {
38     static QString xTrojitaAttachmentList = QStringLiteral("application/x-trojita-attachments-list");
39     static QString xTrojitaMessageList = QStringLiteral("application/x-trojita-message-list");
40     static QString xTrojitaImapPart = QStringLiteral("application/x-trojita-imap-part");
41 }
42 
43 namespace Composer {
44 
MessageComposer(Imap::Mailbox::Model * model,QObject * parent)45 MessageComposer::MessageComposer(Imap::Mailbox::Model *model, QObject *parent) :
46     QAbstractListModel(parent), m_model(model), m_shouldPreload(false), m_reportTrojitaVersions(true)
47 {
48 }
49 
~MessageComposer()50 MessageComposer::~MessageComposer()
51 {
52     qDeleteAll(m_attachments);
53 }
54 
rowCount(const QModelIndex & parent) const55 int MessageComposer::rowCount(const QModelIndex &parent) const
56 {
57     return parent.isValid() ? 0 : m_attachments.size();
58 }
59 
data(const QModelIndex & index,int role) const60 QVariant MessageComposer::data(const QModelIndex &index, int role) const
61 {
62     if (!index.isValid() || index.column() != 0 || index.row() < 0 || index.row() >= m_attachments.size())
63         return QVariant();
64 
65     switch (role) {
66     case Qt::DisplayRole:
67         return m_attachments[index.row()]->caption();
68     case Qt::ToolTipRole:
69         return m_attachments[index.row()]->tooltip();
70     case Qt::DecorationRole:
71     {
72         // This is more or less copy-pasted from Gui/AttachmentView.cpp. Unfortunately, sharing the implementation
73         // is not trivial due to the way how the static libraries are currently built.
74         QMimeType mimeType = QMimeDatabase().mimeTypeForName(QString::fromUtf8(m_attachments[index.row()]->mimeType()));
75         if (mimeType.isValid() && !mimeType.isDefault()) {
76             return QIcon::fromTheme(mimeType.iconName(), UiUtils::loadIcon(QStringLiteral("mail-attachment")));
77         } else {
78             return UiUtils::loadIcon(QStringLiteral("mail-attachment"));
79         }
80     }
81     case Imap::Mailbox::RoleAttachmentContentDispositionMode:
82         return static_cast<int>(m_attachments[index.row()]->contentDispositionMode());
83     }
84     return QVariant();
85 }
86 
supportedDropActions() const87 Qt::DropActions MessageComposer::supportedDropActions() const
88 {
89     return Qt::CopyAction | Qt::MoveAction | Qt::LinkAction;
90 }
91 
flags(const QModelIndex & index) const92 Qt::ItemFlags MessageComposer::flags(const QModelIndex &index) const
93 {
94     Qt::ItemFlags f = QAbstractListModel::flags(index);
95 
96     if (index.isValid()) {
97         f |= Qt::ItemIsDragEnabled;
98     }
99     f |= Qt::ItemIsDropEnabled;
100     return f;
101 }
102 
mimeData(const QModelIndexList & indexes) const103 QMimeData *MessageComposer::mimeData(const QModelIndexList &indexes) const
104 {
105     QByteArray encodedData;
106     QDataStream stream(&encodedData, QIODevice::WriteOnly);
107     stream.setVersion(QDataStream::Qt_4_6);
108 
109     QList<AttachmentItem*> items;
110     Q_FOREACH(const QModelIndex &index, indexes) {
111         if (index.model() != this || !index.isValid() || index.column() != 0 || index.parent().isValid())
112             continue;
113         if (index.row() < 0 || index.row() >= m_attachments.size())
114             continue;
115         items << m_attachments[index.row()];
116     }
117 
118     if (items.isEmpty())
119         return 0;
120 
121     stream << items.size();
122     Q_FOREACH(const AttachmentItem *attachment, items) {
123         attachment->asDroppableMimeData(stream);
124     }
125     QMimeData *res = new QMimeData();
126     res->setData(xTrojitaAttachmentList, encodedData);
127     return res;
128 }
129 
dropMimeData(const QMimeData * data,Qt::DropAction action,int row,int column,const QModelIndex & parent)130 bool MessageComposer::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent)
131 {
132     if (action == Qt::IgnoreAction)
133         return true;
134 
135     if (column > 0)
136         return false;
137 
138     if (!m_model)
139         return false;
140 
141     Q_UNUSED(row);
142     Q_UNUSED(parent);
143     // FIXME: would be cool to support attachment reshuffling and to respect the desired drop position
144 
145 
146     if (data->hasFormat(xTrojitaAttachmentList)) {
147         QByteArray encodedData = data->data(xTrojitaAttachmentList);
148         QDataStream stream(&encodedData, QIODevice::ReadOnly);
149         return dropAttachmentList(stream);
150     } else if (data->hasFormat(xTrojitaMessageList)) {
151         QByteArray encodedData = data->data(xTrojitaMessageList);
152         QDataStream stream(&encodedData, QIODevice::ReadOnly);
153         return dropImapMessage(stream);
154     } else if (data->hasFormat(xTrojitaImapPart)) {
155         QByteArray encodedData = data->data(xTrojitaImapPart);
156         QDataStream stream(&encodedData, QIODevice::ReadOnly);
157         return dropImapPart(stream);
158     } else if (data->hasUrls()) {
159         bool attached = false;
160         QList<QUrl> urls = data->urls();
161         foreach (const QUrl &url, urls) {
162             if (url.isLocalFile()) {
163                 // Careful here -- we definitely don't want the boolean evaluation shortcuts taking effect!
164                 // At the same time, any file being recognized and attached is enough to "satisfy" the drop
165                 attached = addFileAttachment(url.path()) || attached;
166             }
167         }
168         return attached;
169     } else {
170         return false;
171     }
172 }
173 
174 /** @short Container wrapper which calls qDeleteAll on all items which remain in the list at the time of destruction */
175 template <typename T>
176 class WillDeleteAll {
177 public:
178     T d;
~WillDeleteAll()179     ~WillDeleteAll() {
180         qDeleteAll(d);
181     }
182 };
183 
184 /** @short Handle a drag-and-drop of a list of attachments */
dropAttachmentList(QDataStream & stream)185 bool MessageComposer::dropAttachmentList(QDataStream &stream)
186 {
187     stream.setVersion(QDataStream::Qt_4_6);
188     if (stream.atEnd()) {
189         qDebug() << "drag-and-drop: cannot decode data: end of stream";
190         return false;
191     }
192     int num;
193     stream >> num;
194     if (stream.status() != QDataStream::Ok) {
195         qDebug() << "drag-and-drop: stream failed:" << stream.status();
196         return false;
197     }
198     if (num < 0) {
199         qDebug() << "drag-and-drop: invalid number of items";
200         return false;
201     }
202 
203     // A crude RAII here; there are many places where the validation might fail even though we have already allocated memory
204     WillDeleteAll<QList<AttachmentItem*>> items;
205 
206     for (int i = 0; i < num; ++i) {
207         int kind = -1;
208         stream >> kind;
209 
210         switch (kind) {
211         case AttachmentItem::ATTACHMENT_IMAP_MESSAGE:
212         {
213             QString mailbox;
214             uint uidValidity;
215             QList<uint> uids;
216             stream >> mailbox >> uidValidity >> uids;
217             if (!validateDropImapMessage(stream, mailbox, uidValidity, uids))
218                 return false;
219             if (uids.size() != 1) {
220                 qDebug() << "drag-and-drop: malformed data for a single message in a mixed list: too many UIDs";
221                 return false;
222             }
223             try {
224                 items.d << new ImapMessageAttachmentItem(m_model, mailbox, uidValidity, uids.front());
225             } catch (Imap::UnknownMessageIndex &) {
226                 return false;
227             }
228 
229             break;
230         }
231 
232         case AttachmentItem::ATTACHMENT_IMAP_PART:
233         {
234             QString mailbox;
235             uint uidValidity;
236             uint uid;
237             QByteArray trojitaPath;
238             if (!validateDropImapPart(stream, mailbox, uidValidity, uid, trojitaPath))
239                 return false;
240             try {
241                 items.d << new ImapPartAttachmentItem(m_model, mailbox, uidValidity, uid, trojitaPath);
242             } catch (Imap::UnknownMessageIndex &) {
243                 return false;
244             }
245 
246             break;
247         }
248 
249         case AttachmentItem::ATTACHMENT_FILE:
250         {
251             QString fileName;
252             stream >> fileName;
253             items.d << new FileAttachmentItem(fileName);
254             break;
255         }
256 
257         default:
258             qDebug() << "drag-and-drop: invalid kind of attachment";
259             return false;
260         }
261     }
262 
263     beginInsertRows(QModelIndex(), m_attachments.size(), m_attachments.size() + items.d.size() - 1);
264     Q_FOREACH(AttachmentItem *attachment, items.d) {
265         if (m_shouldPreload)
266             attachment->preload();
267         m_attachments << attachment;
268     }
269     items.d.clear();
270     endInsertRows();
271 
272     return true;
273 }
274 
275 /** @short Check that the data representing a list of messages is correct */
validateDropImapMessage(QDataStream & stream,QString & mailbox,uint & uidValidity,QList<uint> & uids) const276 bool MessageComposer::validateDropImapMessage(QDataStream &stream, QString &mailbox, uint &uidValidity, QList<uint> &uids) const
277 {
278     if (stream.status() != QDataStream::Ok) {
279         qDebug() << "drag-and-drop: stream failed:" << stream.status();
280         return false;
281     }
282 
283     Imap::Mailbox::TreeItemMailbox *mboxPtr = m_model->findMailboxByName(mailbox);
284     if (!mboxPtr) {
285         qDebug() << "drag-and-drop: mailbox not found";
286         return false;
287     }
288 
289     if (uids.size() < 1) {
290         qDebug() << "drag-and-drop: no UIDs passed";
291         return false;
292     }
293     if (!uidValidity) {
294         qDebug() << "drag-and-drop: invalid UIDVALIDITY";
295         return false;
296     }
297 
298     return true;
299 }
300 
301 /** @short Handle a drag-and-drop of a list of messages */
dropImapMessage(QDataStream & stream)302 bool MessageComposer::dropImapMessage(QDataStream &stream)
303 {
304     stream.setVersion(QDataStream::Qt_4_6);
305     if (stream.atEnd()) {
306         qDebug() << "drag-and-drop: cannot decode data: end of stream";
307         return false;
308     }
309     QString mailbox;
310     uint uidValidity;
311     QList<uint> uids;
312     stream >> mailbox >> uidValidity >> uids;
313     if (!validateDropImapMessage(stream, mailbox, uidValidity, uids))
314         return false;
315     if (!stream.atEnd()) {
316         qDebug() << "drag-and-drop: cannot decode data: too much data";
317         return false;
318     }
319 
320     WillDeleteAll<QList<AttachmentItem*>> items;
321     Q_FOREACH(const uint uid, uids) {
322         try {
323             items.d << new ImapMessageAttachmentItem(m_model, mailbox, uidValidity, uid);
324         } catch (Imap::UnknownMessageIndex &) {
325             return false;
326         }
327         items.d.last()->setContentDispositionMode(CDN_INLINE);
328     }
329     beginInsertRows(QModelIndex(), m_attachments.size(), m_attachments.size() + uids.size() - 1);
330     Q_FOREACH(AttachmentItem *attachment, items.d) {
331         if (m_shouldPreload)
332             attachment->preload();
333         m_attachments << attachment;
334     }
335     items.d.clear();
336     endInsertRows();
337 
338     return true;
339 }
340 
341 /** @short Check that the data representing a single message part are correct */
validateDropImapPart(QDataStream & stream,QString & mailbox,uint & uidValidity,uint & uid,QByteArray & trojitaPath) const342 bool MessageComposer::validateDropImapPart(QDataStream &stream, QString &mailbox, uint &uidValidity, uint &uid, QByteArray &trojitaPath) const
343 {
344     stream >> mailbox >> uidValidity >> uid >> trojitaPath;
345     if (stream.status() != QDataStream::Ok) {
346         qDebug() << "drag-and-drop: stream failed:" << stream.status();
347         return false;
348     }
349     Imap::Mailbox::TreeItemMailbox *mboxPtr = m_model->findMailboxByName(mailbox);
350     if (!mboxPtr) {
351         qDebug() << "drag-and-drop: mailbox not found";
352         return false;
353     }
354 
355     if (!uidValidity || !uid || trojitaPath.isEmpty()) {
356         qDebug() << "drag-and-drop: invalid data";
357         return false;
358     }
359     return true;
360 }
361 
362 /** @short Handle a drag-adn-drop of a list of message parts */
dropImapPart(QDataStream & stream)363 bool MessageComposer::dropImapPart(QDataStream &stream)
364 {
365     stream.setVersion(QDataStream::Qt_4_6);
366     if (stream.atEnd()) {
367         qDebug() << "drag-and-drop: cannot decode data: end of stream";
368         return false;
369     }
370     QString mailbox;
371     uint uidValidity;
372     uint uid;
373     QByteArray trojitaPath;
374     if (!validateDropImapPart(stream, mailbox, uidValidity, uid, trojitaPath))
375         return false;
376     if (!stream.atEnd()) {
377         qDebug() << "drag-and-drop: cannot decode data: too much data";
378         return false;
379     }
380 
381     AttachmentItem *item;
382     try {
383         item = new ImapPartAttachmentItem(m_model, mailbox, uidValidity, uid, trojitaPath);
384     } catch (Imap::UnknownMessageIndex &) {
385         return false;
386     }
387 
388     beginInsertRows(QModelIndex(), m_attachments.size(), m_attachments.size());
389     m_attachments << item;
390     if (m_shouldPreload)
391         m_attachments.back()->preload();
392     endInsertRows();
393 
394     return true;
395 }
396 
mimeTypes() const397 QStringList MessageComposer::mimeTypes() const
398 {
399     return QStringList() << xTrojitaMessageList << xTrojitaImapPart << xTrojitaAttachmentList << QStringLiteral("text/uri-list");
400 }
401 
setFrom(const Imap::Message::MailAddress & from)402 void MessageComposer::setFrom(const Imap::Message::MailAddress &from)
403 {
404     m_from = from;
405 }
406 
setRecipients(const QList<QPair<Composer::RecipientKind,Imap::Message::MailAddress>> & recipients)407 void MessageComposer::setRecipients(const QList<QPair<Composer::RecipientKind, Imap::Message::MailAddress> > &recipients)
408 {
409     m_recipients = recipients;
410 }
411 
412 /** @short Set the value for the In-Reply-To header as per RFC 5322, section 3.6.4
413 
414 The expected values to be passed here do *not* contain the angle brackets. This is in accordance with
415 the very last sentence of that section which says that the angle brackets are not part of the msg-id.
416 */
setInReplyTo(const QList<QByteArray> & inReplyTo)417 void MessageComposer::setInReplyTo(const QList<QByteArray> &inReplyTo)
418 {
419     m_inReplyTo = inReplyTo;
420 }
421 
422 /** @short Set the value for the References header as per RFC 5322, section 3.6.4
423 
424 @see setInReplyTo
425 */
setReferences(const QList<QByteArray> & references)426 void MessageComposer::setReferences(const QList<QByteArray> &references)
427 {
428     m_references = references;
429 }
430 
setTimestamp(const QDateTime & timestamp)431 void MessageComposer::setTimestamp(const QDateTime &timestamp)
432 {
433     m_timestamp = timestamp;
434 }
435 
setSubject(const QString & subject)436 void MessageComposer::setSubject(const QString &subject)
437 {
438     m_subject = subject;
439 }
440 
setOrganization(const QString & organization)441 void MessageComposer::setOrganization(const QString &organization)
442 {
443     m_organization = organization;
444 }
445 
setText(const QString & text)446 void MessageComposer::setText(const QString &text)
447 {
448     m_text = text;
449 }
450 
isReadyForSerialization() const451 bool MessageComposer::isReadyForSerialization() const
452 {
453     Q_FOREACH(const AttachmentItem *attachment, m_attachments) {
454         if (!attachment->isAvailableLocally())
455             return false;
456     }
457     return true;
458 }
459 
ensureRandomStrings() const460 void MessageComposer::ensureRandomStrings() const
461 {
462     if (m_messageId.isNull()) {
463         auto domain = m_from.host.toUtf8();
464         if (domain.isEmpty()) {
465             domain = QByteArrayLiteral("localhost");
466         }
467         m_messageId = QUuid::createUuid().toByteArray().replace("{", "").replace("}", "") + "@" + domain;
468     }
469 
470     if (m_mimeBoundary.isNull()) {
471         // Usage of "=_" is recommended by RFC2045 as it's guaranteed to never occur in a quoted-printable source.
472 
473         // We don't bother with checking that our boundary is not present in the individual parts. That's arguably wrong,
474         // but we don't have much choice if we ever plan to use CATENATE.  It also looks like this is exactly how other MUAs
475         // operate as well, so let's just join the universal dontcareism here.
476         m_mimeBoundary = QByteArray("trojita=_") + QUuid::createUuid().toByteArray().replace("{", "").replace("}", "");
477     }
478 }
479 
encodeHeaderField(const QString & text)480 QByteArray MessageComposer::encodeHeaderField(const QString &text)
481 {
482     /* This encodes an "unstructured" header field */
483     return Imap::encodeRFC2047StringWithAsciiPrefix(text);
484 }
485 
486 namespace {
487 
488 /** @short Write a list of recipients into an output buffer */
processListOfRecipientsIntoHeader(const QByteArray & prefix,const QList<QByteArray> & addresses,QByteArray & out)489 static void processListOfRecipientsIntoHeader(const QByteArray &prefix, const QList<QByteArray> &addresses, QByteArray &out)
490 {
491     // Qt and STL are different, it looks like we cannot easily use something as simple as the ostream_iterator here :(
492     if (!addresses.isEmpty()) {
493         out.append(prefix);
494         for (int i = 0; i < addresses.size() - 1; ++i)
495             out.append(addresses[i]).append(",\r\n ");
496         out.append(addresses.last()).append("\r\n");
497     }
498 }
499 
500 }
501 
writeCommonMessageBeginning(QIODevice * target) const502 void MessageComposer::writeCommonMessageBeginning(QIODevice *target) const
503 {
504     // The From header
505     target->write(QByteArray("From: ").append(m_from.asMailHeader()).append("\r\n"));
506 
507     // All recipients
508     // Got to group the headers so that both of (To, Cc) are present at most once
509     QList<QByteArray> rcptTo, rcptCc;
510     for (auto it = m_recipients.begin(); it != m_recipients.end(); ++it) {
511         switch(it->first) {
512         case Composer::ADDRESS_TO:
513             rcptTo << it->second.asMailHeader();
514             break;
515         case Composer::ADDRESS_CC:
516             rcptCc << it->second.asMailHeader();
517             break;
518         case Composer::ADDRESS_BCC:
519             break;
520         case Composer::ADDRESS_FROM:
521         case Composer::ADDRESS_SENDER:
522         case Composer::ADDRESS_REPLY_TO:
523             // These should never ever be produced by Trojita for now
524             Q_ASSERT(false);
525             break;
526         }
527     }
528 
529     QByteArray recipientHeaders;
530     processListOfRecipientsIntoHeader("To: ", rcptTo, recipientHeaders);
531     processListOfRecipientsIntoHeader("Cc: ", rcptCc, recipientHeaders);
532     target->write(recipientHeaders);
533 
534     // Other message metadata
535     target->write(encodeHeaderField(QLatin1String("Subject: ") + m_subject) + "\r\n" +
536                   "Date: " + Imap::dateTimeToRfc2822(m_timestamp).toUtf8() + "\r\n" +
537                   "MIME-Version: 1.0\r\n");
538     target->write("Message-ID: <" + m_messageId + ">\r\n");
539     writeHeaderWithMsgIds(target, "In-Reply-To", m_inReplyTo);
540     writeHeaderWithMsgIds(target, "References", m_references);
541     if (!m_organization.isEmpty()) {
542         target->write(encodeHeaderField(QLatin1String("Organization: ") + m_organization) + "\r\n");
543     }
544     if (m_reportTrojitaVersions) {
545         target->write(QStringLiteral("User-Agent: Trojita/%1; %2\r\n").arg(
546                       Common::Application::version, Imap::Mailbox::systemPlatformVersion()).toUtf8());
547     } else {
548         target->write("User-Agent: Trojita\r\n");
549     }
550 
551     // Headers depending on actual message body data
552     if (!m_attachments.isEmpty()) {
553         target->write("Content-Type: multipart/mixed;\r\n\tboundary=\"" + m_mimeBoundary + "\"\r\n"
554                       "\r\nThis is a multipart/mixed message in MIME format.\r\n\r\n"
555                       "--" + m_mimeBoundary + "\r\n");
556     }
557 
558     target->write("Content-Type: text/plain; charset=utf-8; format=flowed\r\n"
559                   "Content-Transfer-Encoding: quoted-printable\r\n"
560                   "\r\n");
561     target->write(Imap::quotedPrintableEncode(Imap::wrapFormatFlowed(m_text).toUtf8()));
562 }
563 
564 /** @short Write a header consisting of a list of message-ids
565 
566 Empty headers will not be produced, and the result is wrapped at an appropriate length.
567 
568 The header name must not contain the colon, it is added automatically.
569 */
writeHeaderWithMsgIds(QIODevice * target,const QByteArray & headerName,const QList<QByteArray> & messageIds) const570 void MessageComposer::writeHeaderWithMsgIds(QIODevice *target, const QByteArray &headerName,
571                                             const QList<QByteArray> &messageIds) const
572 {
573     if (messageIds.isEmpty())
574         return;
575 
576     target->write(headerName + ":");
577     int charCount = headerName.length() + 1;
578     for (int i = 0; i < messageIds.size(); ++i) {
579         // Wrapping shall happen at 78 columns, three bytes are eaten by "space < >"
580         if (i != 0 && charCount != 0 && charCount + messageIds[i].length() > 78 - 3) {
581             // got to wrap the header to respect a reasonably small line size
582             charCount = 0;
583             target->write("\r\n");
584         }
585         // and now just append one more item
586         target->write(" <" + messageIds[i] + ">");
587         charCount += messageIds[i].length() + 3;
588     }
589     target->write("\r\n");
590 }
591 
writeAttachmentHeader(QIODevice * target,QString * errorMessage,const AttachmentItem * attachment) const592 bool MessageComposer::writeAttachmentHeader(QIODevice *target, QString *errorMessage, const AttachmentItem *attachment) const
593 {
594     if (!attachment->isAvailableLocally() && attachment->imapUrl().isEmpty()) {
595         *errorMessage = tr("Attachment %1 is not available").arg(attachment->caption());
596         return false;
597     }
598     target->write("\r\n--" + m_mimeBoundary + "\r\n"
599                   "Content-Type: " + attachment->mimeType() + "\r\n");
600     target->write(attachment->contentDispositionHeader());
601 
602     switch (attachment->suggestedCTE()) {
603     case AttachmentItem::CTE_BASE64:
604         target->write("Content-Transfer-Encoding: base64\r\n");
605         break;
606     case AttachmentItem::CTE_7BIT:
607         target->write("Content-Transfer-Encoding: 7bit\r\n");
608         break;
609     case AttachmentItem::CTE_8BIT:
610         target->write("Content-Transfer-Encoding: 8bit\r\n");
611         break;
612     case AttachmentItem::CTE_BINARY:
613         target->write("Content-Transfer-Encoding: binary\r\n");
614         break;
615     }
616 
617     target->write("\r\n");
618     return true;
619 }
620 
writeAttachmentBody(QIODevice * target,QString * errorMessage,const AttachmentItem * attachment) const621 bool MessageComposer::writeAttachmentBody(QIODevice *target, QString *errorMessage, const AttachmentItem *attachment) const
622 {
623     if (!attachment->isAvailableLocally()) {
624         *errorMessage = tr("Attachment %1 is not available").arg(attachment->caption());
625         return false;
626     }
627     QSharedPointer<QIODevice> io = attachment->rawData();
628     if (!io) {
629         *errorMessage = tr("Attachment %1 disappeared").arg(attachment->caption());
630         return false;
631     }
632     while (!io->atEnd()) {
633         switch (attachment->suggestedCTE()) {
634         case AttachmentItem::CTE_BASE64:
635             // Base64 maps 6bit chunks into a single byte. Output shall have no more than 76 characters per line
636             // (not counting the CRLF pair).
637             target->write(io->read(76*6/8).toBase64() + "\r\n");
638             break;
639         default:
640             target->write(io->readAll());
641         }
642     }
643     return true;
644 }
645 
asRawMessage(QIODevice * target,QString * errorMessage) const646 bool MessageComposer::asRawMessage(QIODevice *target, QString *errorMessage) const
647 {
648     ensureRandomStrings();
649 
650     writeCommonMessageBeginning(target);
651 
652     if (!m_attachments.isEmpty()) {
653         Q_FOREACH(const AttachmentItem *attachment, m_attachments) {
654             if (!writeAttachmentHeader(target, errorMessage, attachment))
655                 return false;
656             if (!writeAttachmentBody(target, errorMessage, attachment))
657                 return false;
658         }
659         target->write("\r\n--" + m_mimeBoundary + "--\r\n");
660     }
661     return true;
662 }
663 
asCatenateData(QList<Imap::Mailbox::CatenatePair> & target,QString * errorMessage) const664 bool MessageComposer::asCatenateData(QList<Imap::Mailbox::CatenatePair> &target, QString *errorMessage) const
665 {
666     ensureRandomStrings();
667 
668     using namespace Imap::Mailbox;
669     target.clear();
670     target.append(qMakePair(CATENATE_TEXT, QByteArray()));
671 
672     // write the initial data
673     {
674         QBuffer io(&target.back().second);
675         io.open(QIODevice::ReadWrite);
676         writeCommonMessageBeginning(&io);
677     }
678 
679     if (!m_attachments.isEmpty()) {
680         Q_FOREACH(const AttachmentItem *attachment, m_attachments) {
681             if (target.back().first != CATENATE_TEXT) {
682                 target.append(qMakePair(CATENATE_TEXT, QByteArray()));
683             }
684             QBuffer io(&target.back().second);
685             io.open(QIODevice::Append);
686 
687             if (!writeAttachmentHeader(&io, errorMessage, attachment))
688                 return false;
689 
690             QByteArray url = attachment->imapUrl();
691             if (url.isEmpty()) {
692                 // Cannot use CATENATE here
693                 if (!writeAttachmentBody(&io, errorMessage, attachment))
694                     return false;
695             } else {
696                 target.append(qMakePair(CATENATE_URL, url));
697             }
698         }
699         if (target.back().first != CATENATE_TEXT) {
700             target.append(qMakePair(CATENATE_TEXT, QByteArray()));
701         }
702         QBuffer io(&target.back().second);
703         io.open(QIODevice::Append);
704         io.write("\r\n--" + m_mimeBoundary + "--\r\n");
705     }
706     return true;
707 }
708 
timestamp() const709 QDateTime MessageComposer::timestamp() const
710 {
711     return m_timestamp;
712 }
713 
inReplyTo() const714 QList<QByteArray> MessageComposer::inReplyTo() const
715 {
716     return m_inReplyTo;
717 }
718 
references() const719 QList<QByteArray> MessageComposer::references() const
720 {
721     return m_references;
722 }
723 
rawFromAddress() const724 QByteArray MessageComposer::rawFromAddress() const
725 {
726     return m_from.asSMTPMailbox();
727 }
728 
rawRecipientAddresses() const729 QList<QByteArray> MessageComposer::rawRecipientAddresses() const
730 {
731     QList<QByteArray> res;
732 
733     for (auto it = m_recipients.begin(); it != m_recipients.end(); ++it) {
734         res << it->second.asSMTPMailbox();
735     }
736 
737     return res;
738 }
739 
addFileAttachment(const QString & path)740 bool MessageComposer::addFileAttachment(const QString &path)
741 {
742     beginInsertRows(QModelIndex(), m_attachments.size(), m_attachments.size());
743     QScopedPointer<AttachmentItem> attachment(new FileAttachmentItem(path));
744     if (!attachment->isAvailableLocally())
745         return false;
746     if (m_shouldPreload)
747         attachment->preload();
748     m_attachments << attachment.take();
749     endInsertRows();
750     return true;
751 }
752 
removeAttachment(const QModelIndex & index)753 void MessageComposer::removeAttachment(const QModelIndex &index)
754 {
755     if (!index.isValid() || index.column() != 0 || index.row() < 0 || index.row() >= m_attachments.size())
756         return;
757 
758     beginRemoveRows(QModelIndex(), index.row(), index.row());
759     delete m_attachments.takeAt(index.row());
760     endRemoveRows();
761 }
762 
setAttachmentName(const QModelIndex & index,const QString & newName)763 void MessageComposer::setAttachmentName(const QModelIndex &index, const QString &newName)
764 {
765     if (!index.isValid() || index.column() != 0 || index.row() < 0 || index.row() >= m_attachments.size())
766         return;
767 
768     if (m_attachments[index.row()]->setPreferredFileName(newName))
769         emit dataChanged(index, index);
770 }
771 
setAttachmentContentDisposition(const QModelIndex & index,const ContentDisposition disposition)772 void MessageComposer::setAttachmentContentDisposition(const QModelIndex &index, const ContentDisposition disposition)
773 {
774     if (!index.isValid() || index.column() != 0 || index.row() < 0 || index.row() >= m_attachments.size())
775         return;
776 
777     if (m_attachments[index.row()]->setContentDispositionMode(disposition))
778         emit dataChanged(index, index);
779 }
780 
setPreloadEnabled(const bool preload)781 void MessageComposer::setPreloadEnabled(const bool preload)
782 {
783     m_shouldPreload = preload;
784 }
785 
setReplyingToMessage(const QModelIndex & index)786 void MessageComposer::setReplyingToMessage(const QModelIndex &index)
787 {
788     m_replyingTo = index;
789 }
790 
replyingToMessage() const791 QModelIndex MessageComposer::replyingToMessage() const
792 {
793     return m_replyingTo;
794 }
795 
forwardingMessage() const796 QModelIndex MessageComposer::forwardingMessage() const
797 {
798     return m_forwarding;
799 }
800 
prepareForwarding(const QModelIndex & index,const ForwardMode mode)801 void MessageComposer::prepareForwarding(const QModelIndex &index, const ForwardMode mode)
802 {
803     m_forwarding = index;
804 
805     switch (mode) {
806     case Composer::ForwardMode::FORWARD_AS_ATTACHMENT:
807     {
808         beginInsertRows(QModelIndex(), m_attachments.size(), m_attachments.size());
809         QString mailbox = m_forwarding.data(Imap::Mailbox::RoleMailboxName).toString();
810         uint uidValidity = m_forwarding.data(Imap::Mailbox::RoleMailboxUidValidity).toUInt();
811         uint uid = m_forwarding.data(Imap::Mailbox::RoleMessageUid).toUInt();
812         QScopedPointer<AttachmentItem> attachment(new ImapMessageAttachmentItem(m_model, mailbox, uidValidity, uid));
813         if (m_shouldPreload) {
814             attachment->preload();
815         }
816         attachment->setContentDispositionMode(CDN_INLINE);
817         m_attachments << attachment.take();
818         endInsertRows();
819         break;
820     }
821     }
822 }
823 
setReportTrojitaVersions(const bool reportVersions)824 void MessageComposer::setReportTrojitaVersions(const bool reportVersions)
825 {
826     m_reportTrojitaVersions = reportVersions;
827 }
828 
829 }
830