diff --git a/messagecomposer/autotests/data/non-protected_headers.mbox b/messagecomposer/autotests/data/non-protected_headers.mbox new file mode 100644 --- /dev/null +++ b/messagecomposer/autotests/data/non-protected_headers.mbox @@ -0,0 +1,3 @@ +Content-Type: text/plain + +one flew over the cuckoo's nest diff --git a/messagecomposer/autotests/data/protected_headers-non-obvoscate.mbox b/messagecomposer/autotests/data/protected_headers-non-obvoscate.mbox new file mode 100644 --- /dev/null +++ b/messagecomposer/autotests/data/protected_headers-non-obvoscate.mbox @@ -0,0 +1,6 @@ +Content-Type: text/plain; protected-headers="v1" +To: to@test.de, to2@test.de +Cc: cc@test.de, cc2@test.de +Subject: =?UTF-8?B?YXNkZmdoamtsw7Y=?= + +one flew over the cuckoo's nest diff --git a/messagecomposer/autotests/data/protected_headers-obvoscate.mbox b/messagecomposer/autotests/data/protected_headers-obvoscate.mbox new file mode 100644 --- /dev/null +++ b/messagecomposer/autotests/data/protected_headers-obvoscate.mbox @@ -0,0 +1,16 @@ +Content-Type: multipart/mixed; boundary="123456789"; protected-headers="v1" +To: to@test.de, to2@test.de +Cc: cc@test.de, cc2@test.de +Subject: =?UTF-8?B?YXNkZmdoamtsw7Y=?= + +--123456789 +Content-Disposition: inline +Content-Transfer-Encoding: quoted-printable +Content-Type: text/plain; charset="UTF-8"; protected-headers="v1" + +Subject: asdfghjkl=C3=B6 +--123456789 +Content-Type: text/plain + +one flew over the cuckoo's nest +--123456789-- diff --git a/messagecomposer/autotests/encryptjobtest.h b/messagecomposer/autotests/encryptjobtest.h --- a/messagecomposer/autotests/encryptjobtest.h +++ b/messagecomposer/autotests/encryptjobtest.h @@ -40,8 +40,12 @@ private Q_SLOTS: void testContentDirect(); void testContentChained(); + void testContentSubjobChained(); void testHeaders(); + void testProtectedHeaders_data(); + void testProtectedHeaders(); + private: void checkEncryption(MessageComposer::EncryptJob *eJob); }; diff --git a/messagecomposer/autotests/encryptjobtest.cpp b/messagecomposer/autotests/encryptjobtest.cpp --- a/messagecomposer/autotests/encryptjobtest.cpp +++ b/messagecomposer/autotests/encryptjobtest.cpp @@ -30,21 +30,32 @@ #include #include +#include #include #include -#include +#include +#include #include #include +#include +#include + +#include +#include + #include #include #include #include +#include QTEST_MAIN(EncryptJobTest) +using namespace MessageComposer; + void EncryptJobTest::initTestCase() { MessageComposer::Test::setupEnv(); @@ -102,7 +113,6 @@ VERIFYEXEC(mainTextJob); std::vector< GpgME::Key > keys = MessageComposer::Test::getKeys(); - qDebug() << "done getting keys"; MessageComposer::EncryptJob *eJob = new MessageComposer::EncryptJob(composer); QStringList recipients; @@ -116,6 +126,35 @@ checkEncryption(eJob); } +void EncryptJobTest::testContentSubjobChained() +{ + std::vector< GpgME::Key > keys = MessageComposer::Test::getKeys(); + + QByteArray data(QString::fromLocal8Bit("one flew over the cuckoo's nest").toUtf8()); + KMime::Message skeletonMessage; + + KMime::Content *content = new KMime::Content; + content->contentType(true)->setMimeType("text/plain"); + content->setBody(data); + + auto tJob = new TransparentJob; + tJob->setContent(content); + + QStringList recipients; + recipients << QString::fromLocal8Bit("test@kolab.org"); + + Composer composer; + auto eJob = new MessageComposer::EncryptJob(&composer); + + eJob->setCryptoMessageFormat(Kleo::OpenPGPMIMEFormat); + eJob->setRecipients(recipients); + eJob->setEncryptionKeys(keys); + eJob->setSkeletonMessage(&skeletonMessage); + eJob->appendSubjob(tJob); + + checkEncryption(eJob); +} + void EncryptJobTest::testHeaders() { std::vector< GpgME::Key > keys = MessageComposer::Test::getKeys(); @@ -151,6 +190,95 @@ QCOMPARE(result->contentTransferEncoding()->encoding(), KMime::Headers::CE7Bit); } +void EncryptJobTest::testProtectedHeaders_data() +{ + QTest::addColumn("protectedHeaders"); + QTest::addColumn("protectedHeadersObvoscate"); + QTest::addColumn("referenceFile"); + + QTest::newRow("simple-obvoscate") << true << true << QStringLiteral("protected_headers-obvoscate.mbox"); + QTest::newRow("simple-non-obvoscate") << true << false << QStringLiteral("protected_headers-non-obvoscate.mbox"); + QTest::newRow("non-protected_headers") << false << false << QStringLiteral("non-protected_headers.mbox"); +} + +void EncryptJobTest::testProtectedHeaders() +{ + QFETCH(bool,protectedHeaders); + QFETCH(bool, protectedHeadersObvoscate); + QFETCH(QString, referenceFile); + + std::vector< GpgME::Key > keys = MessageComposer::Test::getKeys(); + + MessageComposer::Composer composer; + MessageComposer::EncryptJob *eJob = new MessageComposer::EncryptJob(&composer); + + QVERIFY(eJob); + + const QByteArray data(QString::fromLocal8Bit("one flew over the cuckoo's nest").toUtf8()); + const QString subject(QStringLiteral("asdfghjklö")); + + KMime::Content *content = new KMime::Content; + content->contentType(true)->setMimeType("text/plain"); + content->setBody(data); + + KMime::Message skeletonMessage; + skeletonMessage.contentType(true)->setMimeType("foo/bla"); + skeletonMessage.to(true)->from7BitString("to@test.de, to2@test.de"); + skeletonMessage.cc(true)->from7BitString("cc@test.de, cc2@test.de"); + skeletonMessage.bcc(true)->from7BitString("bcc@test.de, bcc2@test.de"); + skeletonMessage.subject(true)->fromUnicodeString(subject, "utf-8"); + + QStringList recipients; + recipients << QString::fromLocal8Bit("test@kolab.org"); + + eJob->setContent(content); + eJob->setCryptoMessageFormat(Kleo::OpenPGPMIMEFormat); + eJob->setRecipients(recipients); + eJob->setEncryptionKeys(keys); + eJob->setSkeletonMessage(&skeletonMessage); + eJob->setProtectedHeaders(protectedHeaders); + eJob->setProtectedHeadersObvoscate(protectedHeadersObvoscate); + + VERIFYEXEC(eJob); + + if (protectedHeadersObvoscate) { + QCOMPARE(skeletonMessage.subject()->as7BitString(false), "..."); + } else { + QCOMPARE(skeletonMessage.subject()->asUnicodeString(), subject); + } + + KMime::Content *result = eJob->content(); + result->assemble(); + + KMime::Content *encPart = MessageComposer::Util::findTypeInMessage(result, "application", "octet-stream"); + KMime::Content tempNode; + { + QByteArray plainText; + auto job = QGpgME::openpgp()->decryptVerifyJob(); + job->exec(encPart->encodedBody(), plainText); + + tempNode.setContent(KMime::CRLFtoLF(plainText.constData())); + tempNode.parse(); + } + if (protectedHeadersObvoscate) { + tempNode.contentType(false)->setBoundary("123456789"); + tempNode.assemble(); + } + + delete result; + + QFile f(referenceFile); + QVERIFY(f.open(QIODevice::WriteOnly | QIODevice::Truncate)); + const QByteArray encodedContent(tempNode.encodedContent()); + f.write(encodedContent); + if (!encodedContent.endsWith('\n')) { + f.write("\n"); + } + f.close(); + + Test::compareFile(referenceFile, QStringLiteral(MAIL_DATA_DIR "/")+referenceFile); +} + void EncryptJobTest::checkEncryption(MessageComposer::EncryptJob *eJob) { VERIFYEXEC(eJob); diff --git a/messagecomposer/autotests/setupenv.h b/messagecomposer/autotests/setupenv.h --- a/messagecomposer/autotests/setupenv.h +++ b/messagecomposer/autotests/setupenv.h @@ -23,6 +23,8 @@ #include +#include + namespace MessageComposer { namespace Test { /** @@ -37,6 +39,17 @@ * Returns list of keys used in various crypto routines */ std::vector getKeys(bool smime = false); + +/** +* Loads a message from filename and returns a message pointer +*/ +KMime::Message::Ptr loadMessageFromFile(const QString &filename); + +/** +* compare two mails via files. +* If the files are not euqal print diff output. +*/ +void compareFile(const QString &outFile, const QString &referenceFile); } } diff --git a/messagecomposer/autotests/setupenv.cpp b/messagecomposer/autotests/setupenv.cpp --- a/messagecomposer/autotests/setupenv.cpp +++ b/messagecomposer/autotests/setupenv.cpp @@ -24,9 +24,11 @@ #include #include -#include #include +#include +#include #include +#include using namespace MessageComposer; @@ -75,3 +77,34 @@ return keys; } + +KMime::Message::Ptr Test::loadMessageFromFile(const QString &filename) +{ + QFile file(QLatin1String(QByteArray(MAIL_DATA_DIR "/" + filename.toLatin1()))); + const bool opened = file.open(QIODevice::ReadOnly); + Q_ASSERT(opened); + Q_UNUSED(opened); + const QByteArray data = KMime::CRLFtoLF(file.readAll()); + Q_ASSERT(!data.isEmpty()); + KMime::Message::Ptr msg(new KMime::Message); + msg->setContent(data); + msg->parse(); + return msg; +} + +void Test::compareFile(const QString &outFile, const QString &referenceFile) +{ + QVERIFY(QFile::exists(outFile)); + + // compare to reference file + const auto args = QStringList() + << QStringLiteral("-u") + << referenceFile + << outFile; + QProcess proc; + proc.setProcessChannelMode(QProcess::ForwardedChannels); + proc.start(QStringLiteral("diff"), args); + QVERIFY(proc.waitForFinished()); + + QCOMPARE(proc.exitCode(), 0); +} diff --git a/messagecomposer/src/CMakeLists.txt b/messagecomposer/src/CMakeLists.txt --- a/messagecomposer/src/CMakeLists.txt +++ b/messagecomposer/src/CMakeLists.txt @@ -35,6 +35,7 @@ job/savecontactpreferencejob.cpp job/attachmentvcardfromaddressbookjob.cpp job/attachmentclipboardjob.cpp + job/protectedheaders.cpp ) set( messagecomposer_statusbarwidget_src diff --git a/messagecomposer/src/composer/composer.cpp b/messagecomposer/src/composer/composer.cpp --- a/messagecomposer/src/composer/composer.cpp +++ b/messagecomposer/src/composer/composer.cpp @@ -278,6 +278,7 @@ eJob->setCryptoMessageFormat(format); eJob->setEncryptionKeys(recipients.second); eJob->setRecipients(recipients.first); + eJob->setSkeletonMessage(skeletonMessage); subJob = eJob; } qCDebug(MESSAGECOMPOSER_LOG) << "subJob" << subJob; diff --git a/messagecomposer/src/job/encryptjob.h b/messagecomposer/src/job/encryptjob.h --- a/messagecomposer/src/job/encryptjob.h +++ b/messagecomposer/src/job/encryptjob.h @@ -54,11 +54,17 @@ void setCryptoMessageFormat(Kleo::CryptoMessageFormat format); void setEncryptionKeys(const std::vector &keys) override; void setRecipients(const QStringList &rec) override; + void setSkeletonMessage(KMime::Message *skeletonMessage); + + void setProtectedHeaders(bool protectedHeaders); + void setProtectedHeadersObvoscate(bool protectedHeadersObvoscate); std::vector encryptionKeys() const override; QStringList recipients() const override; protected Q_SLOTS: + void doStart() override; + void slotResult(KJob *job) override; void process() override; private: diff --git a/messagecomposer/src/job/encryptjob.cpp b/messagecomposer/src/job/encryptjob.cpp --- a/messagecomposer/src/job/encryptjob.cpp +++ b/messagecomposer/src/job/encryptjob.cpp @@ -21,6 +21,7 @@ #include "job/encryptjob.h" #include "contentjobbase_p.h" +#include "job/protectedheaders.h" #include "utils/util_p.h" #include @@ -30,9 +31,6 @@ #include "messagecomposer_debug.h" -#include -#include - #include #include #include @@ -52,6 +50,10 @@ std::vector keys; Kleo::CryptoMessageFormat format; KMime::Content *content = nullptr; + KMime::Message *skeletonMessage = nullptr; + + bool protectedHeaders = true; + bool protectedHeadersObvoscate = false; // copied from messagecomposer.cpp bool binaryHint(Kleo::CryptoMessageFormat f) @@ -122,6 +124,27 @@ d->recipients = recipients; } +void EncryptJob::setSkeletonMessage(KMime::Message* skeletonMessage) +{ + Q_D(EncryptJob); + + d->skeletonMessage = skeletonMessage; +} + +void EncryptJob::setProtectedHeaders(bool protectedHeaders) +{ + Q_D(EncryptJob); + + d->protectedHeaders = protectedHeaders; +} + +void EncryptJob::setProtectedHeadersObvoscate(bool protectedHeadersObvoscate) +{ + Q_D(EncryptJob); + + d->protectedHeadersObvoscate = protectedHeadersObvoscate; +} + QStringList EncryptJob::recipients() const { Q_D(const EncryptJob); @@ -136,7 +159,7 @@ return d->keys; } -void EncryptJob::process() +void EncryptJob::doStart() { Q_D(EncryptJob); Q_ASSERT(d->resultContent == nullptr); // Not processed before. @@ -146,15 +169,61 @@ return; } + // if setContent hasn't been called, we assume that a subjob was added + // and we want to use that + if (!d->content || !d->content->hasContent()) { + if (d->subjobContents.size() == 1) { + d->content = d->subjobContents.first(); + } + } + + if (d->protectedHeaders && d->skeletonMessage && d->format & Kleo::OpenPGPMIMEFormat) { + ProtectedHeadersJob *pJob = new ProtectedHeadersJob; + pJob->setContent(d->content); + pJob->setSkeletonMessage(d->skeletonMessage); + pJob->setObvoscate(d->protectedHeadersObvoscate); + QObject::connect(pJob, &ProtectedHeadersJob::finished, this, [d, pJob](KJob *job) { + if (job->error()) { + return; + } + d->content = pJob->content(); + }); + appendSubjob(pJob); + } + + ContentJobBase::doStart(); +} + +void EncryptJob::slotResult(KJob *job) +{ + Q_D(EncryptJob); + if (error()) { + ContentJobBase::slotResult(job); + return; + } + if (subjobs().size() == 2) { + auto pjob = static_cast(subjobs().last()); + if (pjob) { + Q_ASSERT(dynamic_cast(job)); + auto cjob = static_cast(job); + pjob->setContent(cjob->content()); + } + } + + ContentJobBase::slotResult(job); +} + +void EncryptJob::process() +{ + Q_D(EncryptJob); + // if setContent hasn't been called, we assume that a subjob was added // and we want to use that if (!d->content || !d->content->hasContent()) { Q_ASSERT(d->subjobContents.size() == 1); d->content = d->subjobContents.first(); } - //d->resultContent = new KMime::Content; - const QGpgME::Protocol *proto = nullptr; if (d->format & Kleo::AnyOpenPGP) { proto = QGpgME::openpgp(); diff --git a/messagecomposer/src/job/encryptjob.h b/messagecomposer/src/job/protectedheaders.h copy from messagecomposer/src/job/encryptjob.h copy to messagecomposer/src/job/protectedheaders.h --- a/messagecomposer/src/job/encryptjob.h +++ b/messagecomposer/src/job/protectedheaders.h @@ -1,6 +1,5 @@ /* - Copyright (C) 2009 Klaralvdalens Datakonsult AB, a KDAB Group company, info@kdab.net - Copyright (c) 2009 Leo Franchi + Copyright (C) 2020 Sandro Knauß This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by @@ -18,51 +17,46 @@ 02110-1301, USA. */ -#ifndef MESSAGECOMPOSER_ENCRYPTJOB_H -#define MESSAGECOMPOSER_ENCRYPTJOB_H +#ifndef MESSAGECOMPOSER_PROTECTEDHEADERSJOB_H +#define MESSAGECOMPOSER_PROTECTEDHEADERSJOB_H -#include "abstractencryptjob.h" #include "contentjobbase.h" #include "infopart.h" #include "messagecomposer_export.h" -#include - -#include -#include - namespace KMime { class Content; } namespace MessageComposer { -class EncryptJobPrivate; +class ProtectedHeadersJobPrivate; /** - Encrypt the contents of a message . - Used as a subjob of CryptoMessage + Copies headers from skeleton message to content. + It is used for Protected Headers for Cryptographic E-mail + currently a draft for RFC: + https://datatracker.ietf.org/doc/draft-autocrypt-lamps-protected-headers/ + Used as a subjob of EncryptJob/SignJob/SignEncryptJob */ -class MESSAGECOMPOSER_EXPORT EncryptJob : public ContentJobBase, public MessageComposer::AbstractEncryptJob +class MESSAGECOMPOSER_EXPORT ProtectedHeadersJob : public ContentJobBase { Q_OBJECT public: - explicit EncryptJob(QObject *parent = nullptr); - ~EncryptJob() override; + explicit ProtectedHeadersJob(QObject *parent = nullptr); + ~ProtectedHeadersJob() override; void setContent(KMime::Content *content); - void setCryptoMessageFormat(Kleo::CryptoMessageFormat format); - void setEncryptionKeys(const std::vector &keys) override; - void setRecipients(const QStringList &rec) override; + void setSkeletonMessage(KMime::Message *skeletonMessage); - std::vector encryptionKeys() const override; - QStringList recipients() const override; + void setObvoscate(bool obvoscate); protected Q_SLOTS: + void doStart() override; void process() override; private: - Q_DECLARE_PRIVATE(EncryptJob) + Q_DECLARE_PRIVATE(ProtectedHeadersJob) }; } diff --git a/messagecomposer/src/job/protectedheaders.cpp b/messagecomposer/src/job/protectedheaders.cpp new file mode 100644 --- /dev/null +++ b/messagecomposer/src/job/protectedheaders.cpp @@ -0,0 +1,167 @@ +/* + Copyright (C) 2020 Sandro Knauß + + This library is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your + option) any later version. + + This library is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public + License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + 02110-1301, USA. +*/ + +#include "job/protectedheaders.h" + +#include "contentjobbase_p.h" +#include "job/singlepartjob.h" +#include "utils/util_p.h" + +#include "messagecomposer_debug.h" + +#include +#include + +using namespace MessageComposer; + +class MessageComposer::ProtectedHeadersJobPrivate : public ContentJobBasePrivate +{ +public: + ProtectedHeadersJobPrivate(ProtectedHeadersJob *qq) + : ContentJobBasePrivate(qq) + { + } + + KMime::Content *content = nullptr; + KMime::Message *skeletonMessage = nullptr; + + bool obvoscate = false; + + Q_DECLARE_PUBLIC(ProtectedHeadersJob) +}; + +ProtectedHeadersJob::ProtectedHeadersJob(QObject *parent) + : ContentJobBase(*new ProtectedHeadersJobPrivate(this), parent) +{ +} + +ProtectedHeadersJob::~ProtectedHeadersJob() +{ +} + +void ProtectedHeadersJob::setContent(KMime::Content *content) +{ + Q_D(ProtectedHeadersJob); + + d->content = content; + if (content) + { + d->content->assemble(); + } +} + +void ProtectedHeadersJob::setSkeletonMessage(KMime::Message *skeletonMessage) +{ + Q_D(ProtectedHeadersJob); + + d->skeletonMessage = skeletonMessage; +} + +void ProtectedHeadersJob::setObvoscate(bool obvoscate) +{ + Q_D(ProtectedHeadersJob); + + d->obvoscate = obvoscate; +} + +void ProtectedHeadersJob::doStart() { + Q_D(ProtectedHeadersJob); + Q_ASSERT(d->resultContent == nullptr); // Not processed before. + Q_ASSERT(d->skeletonMessage); // We need a skeletonMessage to proceed + + auto subject = d->skeletonMessage->header(); + if (d->obvoscate && subject) { + // Create protected header lagacy mimepart with replaced headers + SinglepartJob *cjob = new SinglepartJob; + cjob->contentType()->setMimeType("text/plain"); + cjob->contentType()->setCharset(subject->rfc2047Charset()); + cjob->contentType()->setParameter(QStringLiteral("protected-headers"), QStringLiteral("v1")); + cjob->contentDisposition()->setDisposition(KMime::Headers::contentDisposition::CDinline); + cjob->setData(subject->type() + QByteArray(": ") + subject->asUnicodeString().toUtf8()); + + QObject::connect(cjob, &SinglepartJob::finished, this, [d, cjob](KJob *job) { + KMime::Content *mixedPart = new KMime::Content(); + const QByteArray boundary = KMime::multiPartBoundary(); + mixedPart->contentType()->setMimeType("multipart/mixed"); + mixedPart->contentType()->setBoundary(boundary); + mixedPart->addContent(cjob->content()); + + // if setContent hasn't been called, we assume that a subjob was added + // and we want to use that + if (!d->content || !d->content->hasContent()) { + Q_ASSERT(d->subjobContents.size() == 1); + d->content = d->subjobContents.first(); + } + + mixedPart->addContent(d->content); + d->content = mixedPart; + }); + appendSubjob(cjob); + } + + ContentJobBase::doStart(); +} + +void ProtectedHeadersJob::process() +{ + Q_D(ProtectedHeadersJob); + + // if setContent hasn't been called, we assume that a subjob was added + // and we want to use that + if (!d->content || !d->content->hasContent()) { + Q_ASSERT(d->subjobContents.size() == 1); + d->content = d->subjobContents.first(); + } + + auto subject = d->skeletonMessage->header(); + const auto headers = d->skeletonMessage->headers(); + for (const auto &header: headers) { + const QByteArray headerType(header->type()); + if (headerType.startsWith("X-KMail-")) { + continue; + } + if (headerType == "MIME-Version") { + continue; + } + if (headerType == "Bcc") { + continue; + } + if (headerType.startsWith("Content-")) { + continue; + } + if (headerType == "Subject") { + KMime::Headers::Subject *copySubject = new KMime::Headers::Subject(); + copySubject->from7BitString(subject->as7BitString(false)); + d->content->appendHeader(copySubject); + } else { + d->content->appendHeader(header); + } + } + + if (d->obvoscate && subject) { + subject->clear(); + subject->from7BitString("..."); + } + auto contentType = d->content->header(); + contentType->setParameter(QStringLiteral("protected-headers"), QStringLiteral("v1")); + + d->resultContent = d->content; + + emitResult(); +}