diff --git a/components/kube/qml/Kube.qml b/components/kube/qml/Kube.qml --- a/components/kube/qml/Kube.qml +++ b/components/kube/qml/Kube.qml @@ -225,12 +225,17 @@ Kube.Listener { filter: Kube.Messages.reply - onMessageReceived: kubeViews.replaceView("composer", {message: message.mail, loadAsDraft: false}) + onMessageReceived: kubeViews.replaceView("composer", {message: message.mail, loadType: Kube.ComposerController.Reply}) + } + + Kube.Listener { + filter: Kube.Messages.forward + onMessageReceived: kubeViews.replaceView("composer", {message: message.mail, loadType: Kube.ComposerController.Forward}) } Kube.Listener { filter: Kube.Messages.edit - onMessageReceived: kubeViews.replaceView("composer", {message: message.mail, loadAsDraft: true}) + onMessageReceived: kubeViews.replaceView("composer", {message: message.mail, loadType: Kube.ComposerController.Draft}) } Kube.Listener { diff --git a/framework/qml/Icons.qml b/framework/qml/Icons.qml --- a/framework/qml/Icons.qml +++ b/framework/qml/Icons.qml @@ -41,6 +41,7 @@ property string edit: "document-edit" property string edit_inverted: "document-edit-inverted" property string replyToSender: "mail-reply-sender" + property string forward: "mail-forward" property string outbox: "mail-folder-outbox" property string outbox_inverted: "mail-folder-outbox-inverted" property string copy: "edit-copy" diff --git a/framework/qml/MailViewer.qml b/framework/qml/MailViewer.qml --- a/framework/qml/MailViewer.qml +++ b/framework/qml/MailViewer.qml @@ -370,24 +370,40 @@ } } - Kube.IconButton { - visible: !model.trash - anchors{ + Grid { + anchors { verticalCenter: parent.verticalCenter right: parent.right rightMargin: Kube.Units.largeSpacing } - activeFocusOnTab: false + columns: 2 - iconName: model.draft ? Kube.Icons.edit : Kube.Icons.replyToSender - onClicked: { - if (model.draft) { - Kube.Fabric.postMessage(Kube.Messages.edit, {"mail": model.mail, "isDraft": model.draft}) - } else { - Kube.Fabric.postMessage(Kube.Messages.reply, {"mail": model.mail, "isDraft": model.draft}) + Kube.IconButton { + visible: !model.trash + activeFocusOnTab: false + + iconName: model.draft ? Kube.Icons.edit : Kube.Icons.replyToSender + onClicked: { + if (model.draft) { + Kube.Fabric.postMessage(Kube.Messages.edit, {"mail": model.mail}) + } else { + Kube.Fabric.postMessage(Kube.Messages.reply, {"mail": model.mail}) + } } } + + Kube.IconButton { + visible: !model.trash && !model.draft + activeFocusOnTab: false + + iconName: Kube.Icons.forward + onClicked: { + Kube.Fabric.postMessage(Kube.Messages.forward, {"mail": model.mail}) + } + } + } + } Rectangle { anchors.fill: parent diff --git a/framework/qml/Messages.qml b/framework/qml/Messages.qml --- a/framework/qml/Messages.qml +++ b/framework/qml/Messages.qml @@ -44,6 +44,7 @@ property string search: "search" property string synchronize: "synchronize" property string reply: "reply" + property string forward: "forward" property string edit: "edit" property string compose: "compose" property string sendOutbox: "sendOutbox" diff --git a/framework/src/domain/composercontroller.h b/framework/src/domain/composercontroller.h --- a/framework/src/domain/composercontroller.h +++ b/framework/src/domain/composercontroller.h @@ -81,20 +81,31 @@ KUBE_CONTROLLER_ACTION(saveAsDraft) public: + enum LoadType { + Draft, + Reply, + Forward, + }; + Q_ENUMS(LoadType); + explicit ComposerController(); Completer *recipientCompleter() const; Selector *identitySelector() const; - Q_INVOKABLE void loadMessage(const QVariant &draft, bool loadAsDraft); + Q_INVOKABLE void loadDraft(const QVariant &message); + Q_INVOKABLE void loadReply(const QVariant &message); + Q_INVOKABLE void loadForward(const QVariant &message); public slots: virtual void clear() Q_DECL_OVERRIDE; private slots: void findPersonalKey(); private: + void loadMessage(const QVariant &message, std::function callback); + void recordForAutocompletion(const QByteArray &addrSpec, const QByteArray &displayName); void setMessage(const QSharedPointer &msg); void addAttachmentPart(KMime::Content *partToAttach); diff --git a/framework/src/domain/composercontroller.cpp b/framework/src/domain/composercontroller.cpp --- a/framework/src/domain/composercontroller.cpp +++ b/framework/src/domain/composercontroller.cpp @@ -272,14 +272,8 @@ void ComposerController::addAttachmentPart(KMime::Content *partToAttach) { QVariantMap map; - if (partToAttach->contentType()->mimeType() == "multipart/digest" || - partToAttach->contentType()->mimeType() == "message/rfc822") { - // if it is a digest or a full message, use the encodedContent() of the attachment, - // which already has the proper headers - map.insert("content", partToAttach->encodedContent()); - } else { - map.insert("content", partToAttach->decodedContent()); - } + // May need special care for the multipart/digest MIME type + map.insert("content", partToAttach->decodedContent()); map.insert("mimetype", partToAttach->contentType()->mimeType()); QMimeDatabase db; @@ -337,41 +331,57 @@ setExistingMessage(msg); } -void ComposerController::loadMessage(const QVariant &message, bool loadAsDraft) +void ComposerController::loadDraft(const QVariant &message) { + loadMessage(message, [this] (const KMime::Message::Ptr &mail) { + mRemoveDraft = true; + setMessage(mail); + }); +} + +void ComposerController::loadReply(const QVariant &message) { + loadMessage(message, [this] (const KMime::Message::Ptr &mail) { + //Find all personal email addresses to exclude from reply + KMime::Types::AddrSpecList me; + auto list = static_cast(mIdentitySelector.data())->getAllAddresses(); + for (const auto &a : list) { + KMime::Types::Mailbox mb; + mb.setAddress(a); + me << mb.addrSpec(); + } + + MailTemplates::reply(mail, [this] (const KMime::Message::Ptr &reply) { + //We assume reply + setMessage(reply); + }, me); + }); +} + +void ComposerController::loadForward(const QVariant &message) { + loadMessage(message, [this] (const KMime::Message::Ptr &mail) { + MailTemplates::forward(mail, [this] (const KMime::Message::Ptr &fwdMessage) { + setMessage(fwdMessage); + }); + }); +} + +void ComposerController::loadMessage(const QVariant &message, std::function callback) { using namespace Sink; using namespace Sink::ApplicationDomain; auto msg = message.value(); Q_ASSERT(msg); Query query(*msg); query.request(); - Store::fetchOne(query).then([this, loadAsDraft](const Mail &mail) { - mRemoveDraft = loadAsDraft; + Store::fetchOne(query).then([this, callback](const Mail &mail) { setExistingMail(mail); const auto mailData = KMime::CRLFtoLF(mail.getMimeMessage()); if (!mailData.isEmpty()) { KMime::Message::Ptr mail(new KMime::Message); mail->setContent(mailData); mail->parse(); - if (loadAsDraft) { - setMessage(mail); - } else { - //Find all personal email addresses to exclude from reply - KMime::Types::AddrSpecList me; - auto list = static_cast(mIdentitySelector.data())->getAllAddresses(); - for (const auto &a : list) { - KMime::Types::Mailbox mb; - mb.setAddress(a); - me << mb.addrSpec(); - } - - MailTemplates::reply(mail, [this] (const KMime::Message::Ptr &reply) { - //We assume reply - setMessage(reply); - }, me); - } + callback(mail); } else { qWarning() << "Retrieved empty message"; } diff --git a/framework/src/domain/mime/mailtemplates.h b/framework/src/domain/mime/mailtemplates.h --- a/framework/src/domain/mime/mailtemplates.h +++ b/framework/src/domain/mime/mailtemplates.h @@ -35,6 +35,7 @@ namespace MailTemplates { void reply(const KMime::Message::Ptr &origMsg, const std::function &callback, const KMime::Types::AddrSpecList &me = {}); + void forward(const KMime::Message::Ptr &origMsg, const std::function &callback); QString plaintextContent(const KMime::Message::Ptr &origMsg); QString body(const KMime::Message::Ptr &msg, bool &isHtml); KMime::Message::Ptr createMessage(KMime::Message::Ptr existingMessage, const QStringList &to, const QStringList &cc, const QStringList &bcc, const KMime::Types::Mailbox &from, const QString &subject, const QString &body, bool htmlBody, const QList &attachments, const std::vector &signingKeys = {}, const std::vector &encryptionKeys = {}); diff --git a/framework/src/domain/mime/mailtemplates.cpp b/framework/src/domain/mime/mailtemplates.cpp --- a/framework/src/domain/mime/mailtemplates.cpp +++ b/framework/src/domain/mime/mailtemplates.cpp @@ -864,6 +864,34 @@ }); } +void MailTemplates::forward(const KMime::Message::Ptr &origMsg, const std::function &callback) +{ + KMime::Message::Ptr wrapperMsg(new KMime::Message); + + wrapperMsg->to()->clear(); + wrapperMsg->cc()->clear(); + + wrapperMsg->subject()->fromUnicodeString(forwardSubject(origMsg->subject()->asUnicodeString()), "utf-8"); + + const QByteArray refStr = getRefStr(origMsg); + if (!refStr.isEmpty()) { + wrapperMsg->references()->fromUnicodeString(QString::fromLocal8Bit(refStr), "utf-8"); + } + + KMime::Content* fwdAttachment = new KMime::Content; + + fwdAttachment->contentDisposition()->setDisposition(KMime::Headers::CDinline); + fwdAttachment->contentType()->setMimeType("message/rfc822"); + fwdAttachment->contentDisposition()->setFilename(origMsg->subject()->asUnicodeString() + ".eml"); + // The mail was parsed in loadMessage before, so no need to assemble it + fwdAttachment->setBody(origMsg->encodedContent()); + + wrapperMsg->addContent(fwdAttachment); + wrapperMsg->assemble(); + + callback(wrapperMsg); +} + QString MailTemplates::plaintextContent(const KMime::Message::Ptr &msg) { MimeTreeParser::ObjectTreeParser otp; @@ -899,10 +927,14 @@ } else { part->contentDisposition(true)->setDisposition(KMime::Headers::CDattachment); } + part->contentType(true)->setMimeType(mimeType); part->contentType(true)->setName(name, "utf-8"); - //Just always encode attachments base64 so it's safe for binary data - part->contentTransferEncoding(true)->setEncoding(KMime::Headers::CEbase64); + // Just always encode attachments base64 so it's safe for binary data, + // except when it's another message + if(mimeType != "message/rfc822") { + part->contentTransferEncoding(true)->setEncoding(KMime::Headers::CEbase64); + } part->setBody(content); return part; } diff --git a/framework/src/domain/mime/tests/mailtemplatetest.cpp b/framework/src/domain/mime/tests/mailtemplatetest.cpp --- a/framework/src/domain/mime/tests/mailtemplatetest.cpp +++ b/framework/src/domain/mime/tests/mailtemplatetest.cpp @@ -224,6 +224,30 @@ QCOMPARE(result->cc()->addresses(), l); } + void testForwardAsAttachment() + { + auto msg = readMail("plaintext.mbox"); + KMime::Message::Ptr result; + MailTemplates::forward(msg, [&] (const KMime::Message::Ptr &r) { + result = r; + }); + QTRY_VERIFY(result); + QCOMPARE(result->subject(false)->asUnicodeString(), {"FW: A random subject with alternative contenttype"}); + QCOMPARE(result->to()->addresses(), {}); + QCOMPARE(result->cc()->addresses(), {}); + + auto attachments = result->attachments(); + QCOMPARE(attachments.size(), 1); + auto attachment = attachments[0]; + QCOMPARE(attachment->contentDisposition(false)->disposition(), KMime::Headers::CDinline); + QCOMPARE(attachment->contentDisposition(false)->filename(), {"A random subject with alternative contenttype.eml"}); + QVERIFY(attachment->bodyIsMessage()); + + attachment->parse(); + auto origMsg = attachment->bodyAsMessage(); + QCOMPARE(origMsg->subject(false)->asUnicodeString(), {"A random subject with alternative contenttype"}); + } + void testCreatePlainMail() { QStringList to = {{"to@example.org"}}; diff --git a/icons/breeze/icons/actions/16/mail-forward-inverted.svg b/icons/breeze/icons/actions/16/mail-forward-inverted.svg new file mode 100644 --- /dev/null +++ b/icons/breeze/icons/actions/16/mail-forward-inverted.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/icons/breeze/icons/actions/16/mail-forward.svg b/icons/breeze/icons/actions/16/mail-forward.svg new file mode 100644 --- /dev/null +++ b/icons/breeze/icons/actions/16/mail-forward.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/icons/breeze/icons/actions/22/mail-forward-inverted.svg b/icons/breeze/icons/actions/22/mail-forward-inverted.svg new file mode 100644 --- /dev/null +++ b/icons/breeze/icons/actions/22/mail-forward-inverted.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/icons/breeze/icons/actions/22/mail-forward.svg b/icons/breeze/icons/actions/22/mail-forward.svg new file mode 100644 --- /dev/null +++ b/icons/breeze/icons/actions/22/mail-forward.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/icons/breeze/icons/actions/24/mail-forward-inverted.svg b/icons/breeze/icons/actions/24/mail-forward-inverted.svg new file mode 100644 --- /dev/null +++ b/icons/breeze/icons/actions/24/mail-forward-inverted.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/icons/breeze/icons/actions/24/mail-forward.svg b/icons/breeze/icons/actions/24/mail-forward.svg new file mode 100644 --- /dev/null +++ b/icons/breeze/icons/actions/24/mail-forward.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/icons/breeze/icons/actions/32/mail-forward-inverted.svg b/icons/breeze/icons/actions/32/mail-forward-inverted.svg new file mode 100644 --- /dev/null +++ b/icons/breeze/icons/actions/32/mail-forward-inverted.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/icons/breeze/icons/actions/32/mail-forward.svg b/icons/breeze/icons/actions/32/mail-forward.svg new file mode 100644 --- /dev/null +++ b/icons/breeze/icons/actions/32/mail-forward.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/icons/copybreeze.sh b/icons/copybreeze.sh --- a/icons/copybreeze.sh +++ b/icons/copybreeze.sh @@ -24,6 +24,7 @@ "mail-mark-important.svg", "mail-mark-unread-new.svg", "mail-reply-sender.svg", + "mail-forward.svg", "mail-folder-outbox.svg", "network-disconnect.svg", "view-refresh.svg", diff --git a/views/composer/qml/View.qml b/views/composer/qml/View.qml --- a/views/composer/qml/View.qml +++ b/views/composer/qml/View.qml @@ -30,7 +30,7 @@ id: root property bool newMessage: false - property bool loadAsDraft: false + property int loadType: Kube.ComposerController.Draft property variant message: {} property variant recipients: [] @@ -58,11 +58,21 @@ function loadMessage(message, loadAsDraft) { if (message) { - composerController.loadMessage(message, loadAsDraft) - //Forward focus for replies directly - if (!loadAsDraft) { - subject.forceActiveFocus() + + switch(loadType) { + case Kube.ComposerController.Draft: + composerController.loadDraft(message) + break; + case Kube.ComposerController.Reply: + composerController.loadReply(message) + subject.forceActiveFocus() + break; + case Kube.ComposerController.Forward: + composerController.loadForward(message) + subject.forceActiveFocus() + break; } + } else if (newMessage) { composerController.clear() if (root.recipients) {