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 ×tamp)
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