From 234daf6935043775ffce6b5a3ca78e06f56fd81e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sandro=20Knau=C3=9F?= Date: Thu, 14 Jul 2016 12:55:08 +0200 Subject: First thoughts for the new messagepart interface --- framework/domain/mimetreeparser/interface.h | 327 ++++++++++++++++++++++++++++ framework/domain/mimetreeparser/test.cpp | 146 +++++++++++++ 2 files changed, 473 insertions(+) create mode 100644 framework/domain/mimetreeparser/interface.h create mode 100644 framework/domain/mimetreeparser/test.cpp (limited to 'framework') diff --git a/framework/domain/mimetreeparser/interface.h b/framework/domain/mimetreeparser/interface.h new file mode 100644 index 00000000..a9e394db --- /dev/null +++ b/framework/domain/mimetreeparser/interface.h @@ -0,0 +1,327 @@ +/* + Copyright (c) 2016 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. +*/ + +#pragma once + +#include +#include + +class Part; +class EncryptionPart; +class SignaturePart; + +class MimePart; +class MimePartPrivate; + +class ContentPart; +class ContentPartPrivate; + +class EncryptionErrorPart; +class EncryptionErrorPartPrivate; + +class AttachmentPart; +class AttachmentPartPrivate; + +class EncapsulatedPart; +class EncapsulatedPart; + +class CertPart; +class CertPart; + +class Key; +class Signature; +class Encryption; + +class Parser; +class ParserPrivate; + +class Parser +{ +public: + Parser(const QByteArray &mimeMessage); + + std::shared_ptr getPart(QUrl url); + + QVector> collect() const; + QVector> collect() const; + QVector> collect(Part start, std::function select, std::function &)> filter) const; + +private: + std::unique_ptr d; +}; + +class Part +{ +public: + virtual QByteArray type() const = 0; + + bool hasSubParts() const; + QList subParts() const; + Part partent() const; +}; + +/* + * A MessagePart that is based on a KMime::Content + */ +class MimePart : public Part +{ +public: + /** + * Various possible values for the "Content-Disposition" header. + */ + enum Disposition { + Invalid, ///< Default, invalid value + Inline, ///< inline + Attachment, ///< attachment + Parallel ///< parallel (invalid, do not use) + }; + + // interessting header parts of a KMime::Content + QByteArray content() const; + QMimeType mimetype() const; + Disposition dispossition() const + QUrl label() const; + QByteArray cid() const; + QByteArray charset() const; + + // we wanna overrwrite the charset of the content, because some clients set the charset wrong + void setCharset(QByteArray charset); + + // Unique identifier to ecactly this KMime::Content + QByteArray link() const; + + + QByteArray type() const Q_DECL_OVERRIDE; +private: + std::unique_ptr d; +}; + +/* + * The main ContentPart + * is MimePart a good parent class? + * do we wanna need parts of the header of the connected KMime::Contents + * usecases: + * - + * for htmlonly it is representating only one MimePart (ok) + * for plaintext only also only one MimePart (ok) + * for alternative, we are represating three messageparts + * - "headers" do we return?, we can use setType to make it possible to select and than return these headers + */ +class ContentPart : public MimePart +{ +public: + enum Types { + PlainText, + Html + }; + Q_DECLARE_FLAGS(Types, Type) + + QByteArray content(Content::Type ct) const; + + // convert content with charset + QString content(Content::Type ct) const; + + Content::Types availableContent() const; + QVector signature() const; + QVector encryption() const; + + QByteArray type() const Q_DECL_OVERRIDE; + +private: + std::unique_ptr d; +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(ContentPart::Type) + +class AttachmentPart : public MimePart +{ +public: + QByteArray type() const Q_DECL_OVERRIDE; + +private: + std::unique_ptr d; +}; + +/* + * Faild to decrypt part + * thigs liks this can happen: + * decryption in progress + * have not tried at all to decrypt + * wrong passphrase + * no private key + * cryptobackend is not configured correctly (no gpg available) + * -> S/Mime and PGP have different meaning in their errors + * + * Open Questions: + * - How to make the string translateable for multiple clients, so that multiple clients can show same error messages, + * that helps users to understand what is going on ? + * - Does openpgp have translations already? + */ +class EncryptionErrorPart : public Part +{ +public: + Error errorId() const; + + CryptoBackend cryptoBackend(); + + QByteArray type() const Q_DECL_OVERRIDE; + +private: + std::unique_ptr d; +}; + +/* + * we want to request complete headers like: + * from/to... + */ + +class EncapsulatedPart :: public AttachmentPart +{ +public: + QByteArray type() const Q_DECL_OVERRIDE; + + QByteArray header(); +private: + std::unique_ptr d; +}; + +/* + * importing a cert GpgMe::ImportResult + * checking a cert (if it is a valid cert) + */ + +class CertPart :: public AttachmentPart +{ +public: + QByteArray type() const Q_DECL_OVERRIDE; + + bool checkCert() const; + Status importCert() const; + +private: + std::unique_ptr d; +}; + +/* +the ggme error class + +// class GPGMEPP_EXPORT ErrorImportResult +{ +public: + Error() : mErr(0), mMessage() {} + explicit Error(unsigned int e) : mErr(e), mMessage() {} + + const char *source() const; + const char *asString() const; + + int code() const; + int sourceID() const; + + bool isCanceled() const; + + unsigned int encodedError() const + { + return mErr; + } + int toErrno() const; + + static bool hasSystemError(); + static Error fromSystemError(unsigned int src = GPGMEPP_ERR_SOURCE_DEFAULT); + static void setSystemError(gpg_err_code_t err); + static void setErrno(int err); + static Error fromErrno(int err, unsigned int src = GPGMEPP_ERR_SOURCE_DEFAULT); + static Error fromCode(unsigned int err, unsigned int src = GPGMEPP_ERR_SOURCE_DEFAULT); + + GPGMEPP_MAKE_SAFE_BOOL_OPERATOR(mErr &&!isCanceled()) +private: + unsigned int mErr; + mutable std::string mMessage; +}; +*/ + +/* + * a used smime/PGP key + * in the end we also need things like: + bool isRevokation() const; + bool isInvalid() const; + bool isExpired() const; + + -> so we end up wrapping GpgME::Key + */ +class Key +{ + QString keyid() const; + QString name() const; + QString email() const; + QString comment() const; + QVector emails() const; + KeyTrust keyTrust() const; + CryptoBackend cryptoBackend() const; + + std::vector subkeys(); + Key parentkey() const; +}; + +class Signature +{ + Key key() const; + QDateTime creationDateTime() const; + QDateTime expirationTime() const; + bool neverExpires() const; + + bool inProgress(); //if the verfication is inProgress + + enum Validity { + Unknown, Undefined, Never, Marginal, Full, Ultimate + }; + Validity validity() const; + + // to determine if we need this in our usecase (email) + // GpgME::VerificationResult + enum Summary { + None = 0x000, + Valid = 0x001, + Green = 0x002, + Red = 0x004, + KeyRevoked = 0x008, + KeyExpired = 0x010, + SigExpired = 0x020, + KeyMissing = 0x040, + CrlMissing = 0x080, + CrlTooOld = 0x100, + BadPolicy = 0x200, + SysError = 0x400 + }; + Summary summary() const; + + const char *policyURL() const; + GpgME::Notation notation(unsigned int index) const; + std::vector notations() const; + +}; + +/* + * Normally the Keys for encryption are subkeys + * for clients the parentkeys are "more interessting", because they store the name, email etc. + * but a client may also wants show to what subkey the mail is really encrypted, an if this subkey isRevoked or something else + */ +class Encryption +{ + std::vector recipients() const; +}; \ No newline at end of file diff --git a/framework/domain/mimetreeparser/test.cpp b/framework/domain/mimetreeparser/test.cpp new file mode 100644 index 00000000..e096ea78 --- /dev/null +++ b/framework/domain/mimetreeparser/test.cpp @@ -0,0 +1,146 @@ +Usecases: + +# plaintext msg + attachment +* ContentPart => cp1 +* AttachmentPart => ap1 + +(cp1) == collect(select=NoEncapsulatedMessages) +(ap1) == collect(select=NoEncapsulatedMessages) + +(PlainText) == cp1.availableContent() + +# html msg + related attachment + normal attachment +* ContentPart => cp1 +* AttachmentPart(mimetype="*/related", cid="12345678") => ap1 +* AttachmentPart => ap2 + +(cp1) == collect(select=NoEncapsulatedMessages) +(ap1, ap2) == collect(select=NoEncapsulatedMessages) +(ap2) == collect(select=NoEncapsulatedMessages, filter=filterelated) + +ap1 == getPart("cid:12345678") + +(Html) == cp1.availableContent() + +# alternative msg + attachment +* ContentPart(html="HTML", plaintext="Text") => cp1 +* AttachmentPart => ap1 + +(cp1) == collect(select=NoEncapsulatedMessages) +(ap1) == collect(select=NoEncapsulatedMessages) + +(Html, PlainText) == cp1.availableContent() +"HTML" == cp1.content(Html) +"text" == cp1.content(Plaintext) + +# alternative msg with GPGInline +* ContentPart(html="HTML", plaintext="Text cypted") => cp1 + * TextPart(text="Text") + * TextPart(text=foo, encryption=(enc1) + +(Html, PlainText) == cp1.availableContent() + +TODO: but how to get plaintext/html content? + +# encrypted msg (not encrypted/error) with unencrypted attachment +* EncryptionErrorPart => cp1 +* AttachmentPart => ap1 + +(cp1) == collect(select=NoEncapsulatedMessages) +(ap1) == collect(select=NoEncapsulatedMessages) + +#encrypted msg (decrypted with attachment) + unencrypted attachment +* encrytion=(rec1,rec2) => enc1 + * ContentPart(encrytion = (enc1,)) => cp1 + * AttachmentPart(encryption = (enc1,)) => ap1 +* AttachmentPart => ap2 + +(cp1) == collect(select=NoEncapsulatedMessages) +(ap1, ap2) == collect(select=NoEncapsulatedMessages) + +#INLINE GPG encrypted msg + attachment +* ContentPart => cp1 + * TextPart + * TextPart(encrytion = (enc1(rec1,rec2),)) + * TextPart(signed = (sig1,)) + * TextPart +* AttachmentPart => ap1 + +(cp1) == collect(select=NoEncapsulatedMessages) +(ap1) == collect(select=NoEncapsulatedMessages) + +#forwared encrypted msg + attachments +* ContentPart => cp1 +* EncapsulatedPart => ep1 + * Encrytion=(rec1,rec2) => enc1 + * Signature => sig1 + * ContentPart(encrytion = (enc1,), signature = (sig1,)) => cp2 + * TextPart(encrytion = (enc1,), signature = (sig1,)) + * TextPart(encrytion = (enc1, enc2(rec3,rec4),), signature = (sig1,)) + * AttachmentPart(encrytion = (enc1,), signature = (sig1,)) => ap1 +* AttachmentPart => ap2 + +(cp1) = collect(select=NoEncapsulatedMessages) +(ap2) = collect(select=NoEncapsulatedMessages) + +(cp2) = collect(ep1, select=NoEncapsulatedMessages) +(ap1) = collect(ep1, select=NoEncapsulatedMessages) + +(cp1, cp2) == collect() +(ap1, ap2) == collect() + + +# plaintext msg + attachment + cert +* ContentPart => cp1 +* AttachmentPart => ap1 +* CertPart => cep1 + +(cp1) == collect(select=NoEncapsulatedMessages) +(ap1, cep1) == collect(select=NoEncapsulatedMessages) +(ap1) == collect(select=NoEncapsulatedMessages, filter=filterSubAttachmentParts) + +(cep1) == collect(select=NoEncapsulatedMessages) + + +collect function: + +bool noEncapsulatedMessages(Part part) +{ + if (is(part)) { + return false; + } + return true; +} + +bool filterRelated(T part) +{ + if (part.mimetype == related && !part.cid.isEmpty()) { + return false; //filter out related parts + } + return true; +} + +bool filterSubAttachmentParts(AttachmentPart part) +{ + if (isSubPart(part)) { + return false; // filter out CertPart f.ex. + } + return true; +} + +List collect(Part start, std::function select, std::function &)> filter) { + List col; + if (!select(start)) { + return col; + } + + if(isOrSubTypeIs(start) && filter(start.staticCast)){ + col.append(p); + } + foreach(childs as child) { + if (select(child)) { + col.expand(collect(child,select,filter); + } + } + return col; +} \ No newline at end of file -- cgit v1.2.3 From 7815e94c52d092701804521eee850ac35d7f7781 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sandro=20Knau=C3=9F?= Date: Thu, 14 Jul 2016 18:36:02 +0200 Subject: updated --- framework/domain/mimetreeparser/interface.h | 81 ++++++++++++++++++++++------- framework/domain/mimetreeparser/test.cpp | 30 ++++++----- 2 files changed, 78 insertions(+), 33 deletions(-) (limited to 'framework') diff --git a/framework/domain/mimetreeparser/interface.h b/framework/domain/mimetreeparser/interface.h index a9e394db..320030b7 100644 --- a/framework/domain/mimetreeparser/interface.h +++ b/framework/domain/mimetreeparser/interface.h @@ -23,32 +23,41 @@ #include class Part; +typedef std::shared_ptr Part::Ptr; class EncryptionPart; +typedef std::shared_ptr EncryptionPart::Ptr; class SignaturePart; +typedef std::shared_ptr SignaturePart::Ptr; class MimePart; +typedef std::shared_ptr MimePart::Ptr; class MimePartPrivate; class ContentPart; +typedef std::shared_ptr ContentPart::Ptr; class ContentPartPrivate; class EncryptionErrorPart; +typedef std::shared_ptr EncryptionErrorPart::Ptr; class EncryptionErrorPartPrivate; class AttachmentPart; +typedef std::shared_ptr AttachmentPart::Ptr; class AttachmentPartPrivate; class EncapsulatedPart; +typedef std::shared_ptr EncapsulatedPart::Ptr; class EncapsulatedPart; class CertPart; -class CertPart; +typedef std::shared_ptr CertPart::Ptr; class Key; class Signature; class Encryption; class Parser; +typedef std::shared_ptr Parser::Ptr; class ParserPrivate; class Parser @@ -56,30 +65,71 @@ class Parser public: Parser(const QByteArray &mimeMessage); - std::shared_ptr getPart(QUrl url); + Part::Ptr getPart(QUrl url); - QVector> collect() const; - QVector> collect() const; - QVector> collect(Part start, std::function select, std::function &)> filter) const; + QVector collect() const; + QVector collect() const; + QVector collect(Part start, std::function select, std::function filter) const; private: std::unique_ptr d; }; + class Part { public: virtual QByteArray type() const = 0; bool hasSubParts() const; - QList subParts() const; - Part partent() const; + QList subParts() const; + Part parent() const; + + virtual QVector signatures() const; + virtual QVector encryptions() const; }; +//A structure element, that we need to reflect, that there is a Encryption starts +// only add a new Encrption block to encryptions block +class EncryptionPart : public Part +{ +public: + QVector encryptions() const Q_DECL_OVERRIDE; + QByteArray type() const Q_DECL_OVERRIDE; +}; + +// A structure element, that we need to reflect, that there is a Signature starts +// only add a new Signature block to signature block +// With this we can a new Singature type like pep aka +/* + * add a bodypartformatter, that returns a PEPSignaturePart with all signed subparts that are signed with pep. + * subclass Signature aka PEPSignature to reflect different way of properties of PEPSignatures. + */ +class SignaturePart : public Part +{ +public: + QVector signatures() const Q_DECL_OVERRIDE; + QByteArray type() const Q_DECL_OVERRIDE; +}; + + + +class TextPart : public Part +{ +public: + QByteArray content() const; + + //Use default charset + QString encodedContent() const; + + // overwrite default charset with given charset + QString encodedContent(QByteArray charset) const; +} + /* * A MessagePart that is based on a KMime::Content */ -class MimePart : public Part +class MimePart : public TextPart { public: /** @@ -88,14 +138,12 @@ public: enum Disposition { Invalid, ///< Default, invalid value Inline, ///< inline - Attachment, ///< attachment - Parallel ///< parallel (invalid, do not use) + Attachment ///< attachment }; // interessting header parts of a KMime::Content - QByteArray content() const; QMimeType mimetype() const; - Disposition dispossition() const + Disposition disposition() const; QUrl label() const; QByteArray cid() const; QByteArray charset() const; @@ -123,7 +171,7 @@ private: * for alternative, we are represating three messageparts * - "headers" do we return?, we can use setType to make it possible to select and than return these headers */ -class ContentPart : public MimePart +class MainContentPart : public MimePart { public: enum Types { @@ -132,14 +180,9 @@ public: }; Q_DECLARE_FLAGS(Types, Type) - QByteArray content(Content::Type ct) const; - - // convert content with charset - QString content(Content::Type ct) const; + QVector content(Content::Type ct) const; Content::Types availableContent() const; - QVector signature() const; - QVector encryption() const; QByteArray type() const Q_DECL_OVERRIDE; diff --git a/framework/domain/mimetreeparser/test.cpp b/framework/domain/mimetreeparser/test.cpp index e096ea78..51aa9871 100644 --- a/framework/domain/mimetreeparser/test.cpp +++ b/framework/domain/mimetreeparser/test.cpp @@ -23,24 +23,27 @@ ap1 == getPart("cid:12345678") (Html) == cp1.availableContent() # alternative msg + attachment -* ContentPart(html="HTML", plaintext="Text") => cp1 +* ContentPart(html=[TextPart("HTML"),], plaintext=[TextPart("Text"),]) => cp1 * AttachmentPart => ap1 (cp1) == collect(select=NoEncapsulatedMessages) (ap1) == collect(select=NoEncapsulatedMessages) (Html, PlainText) == cp1.availableContent() -"HTML" == cp1.content(Html) -"text" == cp1.content(Plaintext) +[TextPart("HTML"),] == cp1.content(Html) +[TextPart("Text"),] == cp1.content(Plaintext) -# alternative msg with GPGInline -* ContentPart(html="HTML", plaintext="Text cypted") => cp1 - * TextPart(text="Text") - * TextPart(text=foo, encryption=(enc1) +# alternative msg with GPGInlin +* ContentPart( + plaintext=[TextPart("Text"), TextPart("foo", encryption=(enc1))], + html=[TextPart("HTML"),] + ) => cp1 (Html, PlainText) == cp1.availableContent() -TODO: but how to get plaintext/html content? +[TextPart("HTML"),] == cp1.content(Html) +[TextPart("Text"),TextPart("foo", encryption=(enc1))] == cp1.content(Plaintext) + # encrypted msg (not encrypted/error) with unencrypted attachment * EncryptionErrorPart => cp1 @@ -59,16 +62,15 @@ TODO: but how to get plaintext/html content? (ap1, ap2) == collect(select=NoEncapsulatedMessages) #INLINE GPG encrypted msg + attachment -* ContentPart => cp1 - * TextPart - * TextPart(encrytion = (enc1(rec1,rec2),)) - * TextPart(signed = (sig1,)) - * TextPart +* ContentPart => cp1 with + plaintext=[TextPart, TextPart(encrytion = (enc1(rec1,rec2),)), TextPart(signed = (sig1,)), TextPart] * AttachmentPart => ap1 (cp1) == collect(select=NoEncapsulatedMessages) (ap1) == collect(select=NoEncapsulatedMessages) +[TextPart, TextPart(encrytion = (enc1(rec1,rec2),)), TextPart(signed = (sig1,)), TextPart] == cp1.content(Plaintext) + #forwared encrypted msg + attachments * ContentPart => cp1 * EncapsulatedPart => ep1 @@ -87,7 +89,7 @@ TODO: but how to get plaintext/html content? (ap1) = collect(ep1, select=NoEncapsulatedMessages) (cp1, cp2) == collect() -(ap1, ap2) == collect() +(ap1, ap2) == collect()[TextPart, TextPart(encrytion = (enc1(rec1,rec2),)), TextPart(signed = (sig1,)), TextPart] # plaintext msg + attachment + cert -- cgit v1.2.3 From 2fdddd2f795da4645dc9a48fee4865324143a21b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sandro=20Knau=C3=9F?= Date: Mon, 18 Jul 2016 15:35:07 +0200 Subject: first tests with mimetreeparser interface --- framework/domain/CMakeLists.txt | 2 + framework/domain/mimetreeparser/CMakeLists.txt | 12 + framework/domain/mimetreeparser/interface.cpp | 335 +++++++++++++++++++++ framework/domain/mimetreeparser/interface.h | 281 ++++++++--------- .../domain/mimetreeparser/objecttreesource.cpp | 151 ++++++++++ framework/domain/mimetreeparser/objecttreesource.h | 57 ++++ .../domain/mimetreeparser/stringhtmlwriter.cpp | 150 +++++++++ framework/domain/mimetreeparser/stringhtmlwriter.h | 71 +++++ framework/domain/mimetreeparser/test.cpp | 148 --------- .../domain/mimetreeparser/tests/CMakeLists.txt | 10 + .../mimetreeparser/tests/data/alternative.mbox | 34 +++ .../domain/mimetreeparser/tests/data/html.mbox | 31 ++ .../domain/mimetreeparser/tests/data/htmlonly.mbox | 21 ++ .../tests/data/htmlonlyexternal.mbox | 21 ++ .../mimetreeparser/tests/data/plaintext.mbox | 17 ++ .../domain/mimetreeparser/tests/interfacetest.cpp | 47 +++ framework/domain/mimetreeparser/thoughts.txt | 148 +++++++++ 17 files changed, 1226 insertions(+), 310 deletions(-) create mode 100644 framework/domain/mimetreeparser/CMakeLists.txt create mode 100644 framework/domain/mimetreeparser/interface.cpp create mode 100644 framework/domain/mimetreeparser/objecttreesource.cpp create mode 100644 framework/domain/mimetreeparser/objecttreesource.h create mode 100644 framework/domain/mimetreeparser/stringhtmlwriter.cpp create mode 100644 framework/domain/mimetreeparser/stringhtmlwriter.h delete mode 100644 framework/domain/mimetreeparser/test.cpp create mode 100644 framework/domain/mimetreeparser/tests/CMakeLists.txt create mode 100644 framework/domain/mimetreeparser/tests/data/alternative.mbox create mode 100644 framework/domain/mimetreeparser/tests/data/html.mbox create mode 100644 framework/domain/mimetreeparser/tests/data/htmlonly.mbox create mode 100644 framework/domain/mimetreeparser/tests/data/htmlonlyexternal.mbox create mode 100644 framework/domain/mimetreeparser/tests/data/plaintext.mbox create mode 100644 framework/domain/mimetreeparser/tests/interfacetest.cpp create mode 100644 framework/domain/mimetreeparser/thoughts.txt (limited to 'framework') diff --git a/framework/domain/CMakeLists.txt b/framework/domain/CMakeLists.txt index ea293655..92a81352 100644 --- a/framework/domain/CMakeLists.txt +++ b/framework/domain/CMakeLists.txt @@ -26,3 +26,5 @@ add_subdirectory(actions/tests) install(TARGETS mailplugin DESTINATION ${QML_INSTALL_DIR}/org/kube/framework/domain) install(FILES qmldir DESTINATION ${QML_INSTALL_DIR}/org/kube/framework/domain) + +add_subdirectory(mimetreeparser) \ No newline at end of file diff --git a/framework/domain/mimetreeparser/CMakeLists.txt b/framework/domain/mimetreeparser/CMakeLists.txt new file mode 100644 index 00000000..e1c04893 --- /dev/null +++ b/framework/domain/mimetreeparser/CMakeLists.txt @@ -0,0 +1,12 @@ +set(mimetreeparser_SRCS + interface.cpp + objecttreesource.cpp + stringhtmlwriter.cpp +) + +add_library(mimetreeparser SHARED ${mimetreeparser_SRCS}) + +qt5_use_modules(mimetreeparser Core Gui) +target_link_libraries(mimetreeparser KF5::Mime KF5::MimeTreeParser) + +add_subdirectory(tests) \ No newline at end of file diff --git a/framework/domain/mimetreeparser/interface.cpp b/framework/domain/mimetreeparser/interface.cpp new file mode 100644 index 00000000..6a399015 --- /dev/null +++ b/framework/domain/mimetreeparser/interface.cpp @@ -0,0 +1,335 @@ +/* + Copyright (c) 2016 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 "interface.h" + +#include "stringhtmlwriter.h" +#include "objecttreesource.h" + +#include +#include +#include +#include + +#include +#include + +class PartPrivate +{ +public: + PartPrivate(Part *part); + void appendSubPart(Part::Ptr subpart); + + QVector subParts(); + + const std::weak_ptr &parent() const; +private: + Part *q; + std::weak_ptr mParent; + QVector mSubParts; +}; + +PartPrivate::PartPrivate(Part* part) + :q(part) +{ + +} + +void PartPrivate::appendSubPart(Part::Ptr subpart) +{ + subpart->d->mParent = Part::Ptr(q); + mSubParts.append(subpart); +} + +const std::weak_ptr &PartPrivate::parent() const +{ + return mParent; +} + +QVector< Part::Ptr > PartPrivate::subParts() +{ + return mSubParts; +} + +Part::Part() + : d(std::unique_ptr(new PartPrivate(this))) +{ + +} + +bool Part::hasSubParts() const +{ + return !subParts().isEmpty(); +} + +QVector Part::subParts() const +{ + return d->subParts(); +} + +QByteArray Part::type() const +{ + return "Part"; +} + +QVector Part::encryptions() const +{ + auto parent = d->parent().lock(); + if (parent) { + return parent->encryptions(); + } else { + return QVector(); + } +} + +QVector Part::signatures() const +{ + auto parent = d->parent().lock(); + if (parent) { + return parent->signatures(); + } else { + return QVector(); + } +} + +class ContentPrivate +{ +public: + QByteArray mContent; + QByteArray mCodec; + Part *mParent; + Content *q; +}; + +Content::Content(const QByteArray& content, ContentPart *parent) + : d(std::unique_ptr(new ContentPrivate)) +{ + d->q = this; + d->mContent = content; + d->mCodec = "utf-8"; + d->mParent = parent; +} + +Content::~Content() +{ +} + +QVector< Encryption > Content::encryptions() const +{ + if (d->mParent) { + return d->mParent->encryptions(); + } + return QVector(); +} + +QVector< Signature > Content::signatures() const +{ + if (d->mParent) { + return d->mParent->signatures(); + } + return QVector(); +} + +class ContentPartPrivate +{ +public: + void fillFrom(MimeTreeParser::TextMessagePart::Ptr part); + void fillFrom(MimeTreeParser::HtmlMessagePart::Ptr part); + void fillFrom(MimeTreeParser::AlternativeMessagePart::Ptr part); + + QVector contents() const; + + ContentPart *q; + +private: + QVector mContents; + ContentPart::Types mTypes; +}; + +void ContentPartPrivate::fillFrom(MimeTreeParser::TextMessagePart::Ptr part) +{ + mTypes = ContentPart::PlainText; + foreach (const auto &mp, part->subParts()) { + auto content = std::make_shared(mp->text().toLocal8Bit(), q); + mContents.append(content); + } +} + +void ContentPartPrivate::fillFrom(MimeTreeParser::HtmlMessagePart::Ptr part) +{ + mTypes = ContentPart::Html; +} + +void ContentPartPrivate::fillFrom(MimeTreeParser::AlternativeMessagePart::Ptr part) +{ + mTypes = ContentPart::Html | ContentPart::PlainText; +} + +ContentPart::ContentPart() + : d(std::unique_ptr(new ContentPartPrivate)) +{ + d->q = this; +} + +ContentPart::~ContentPart() +{ + +} + +QByteArray ContentPart::type() const +{ + return "ContentPart"; +} + +class MimePartPrivate +{ +public: + void fillFrom(MimeTreeParser::MessagePart::Ptr part); +}; + +QByteArray MimePart::type() const +{ + return "MimePart"; +} + +class AttachmentPartPrivate +{ +public: + void fillFrom(MimeTreeParser::AttachmentMessagePart::Ptr part); +}; + +void AttachmentPartPrivate::fillFrom(MimeTreeParser::AttachmentMessagePart::Ptr part) +{ + +} + +QByteArray AttachmentPart::type() const +{ + return "AttachmentPart"; +} + +class ParserPrivate +{ +public: + ParserPrivate(Parser *parser); + + void setMessage(const QByteArray &mimeMessage); + void createTree(MimeTreeParser::MessagePart::Ptr start, Part::Ptr tree); + + Part::Ptr mTree; +private: + Parser *q; + + MimeTreeParser::MessagePart::Ptr mPartTree; + std::shared_ptr mNodeHelper; + QString mHtml; + QMap mEmbeddedPartMap; +}; + +ParserPrivate::ParserPrivate(Parser* parser) + : q(parser) + , mNodeHelper(std::make_shared()) +{ + +} + +void ParserPrivate::setMessage(const QByteArray& mimeMessage) +{ + const auto mailData = KMime::CRLFtoLF(mimeMessage); + KMime::Message::Ptr msg(new KMime::Message); + msg->setContent(mailData); + msg->parse(); + + // render the mail + StringHtmlWriter htmlWriter; + QImage paintDevice; + ObjectTreeSource source(&htmlWriter); + MimeTreeParser::ObjectTreeParser otp(&source, mNodeHelper.get()); + + otp.parseObjectTree(msg.data()); + mPartTree = otp.parsedPart().dynamicCast(); + + mEmbeddedPartMap = htmlWriter.embeddedParts(); + mHtml = htmlWriter.html(); + + mTree = std::make_shared(); + createTree(mPartTree, mTree); +} + + +void ParserPrivate::createTree(MimeTreeParser::MessagePart::Ptr start, Part::Ptr tree) +{ + + foreach (const auto &mp, start->subParts()) { + const auto m = mp.dynamicCast(); + const auto text = mp.dynamicCast(); + const auto alternative = mp.dynamicCast(); + const auto html = mp.dynamicCast(); + const auto attachment = mp.dynamicCast(); + if (text) { + auto part = std::make_shared(); + part->d->fillFrom(text); + mTree->d->appendSubPart(part); + } else if (alternative) { + auto part = std::make_shared(); + part->d->fillFrom(alternative); + mTree->d->appendSubPart(part); + } else if (html) { + auto part = std::make_shared(); + part->d->fillFrom(html); + mTree->d->appendSubPart(part); + } else if (attachment) { + auto part = std::make_shared(); + part->d->fillFrom(attachment); + mTree->d->appendSubPart(part); + } else { + createTree(m, tree); + } + } +} + +Parser::Parser(const QByteArray& mimeMessage) + :d(std::unique_ptr(new ParserPrivate(this))) +{ + d->setMessage(mimeMessage); +} + +Parser::~Parser() +{ +} + +ContentPart::Ptr Parser::collectContentPart(const Part::Ptr &start) const +{ + foreach (const auto &part, start->subParts()) { + if (part->type() == "ContentPart") { + return std::dynamic_pointer_cast(part); + } else { + auto ret = collectContentPart(part); + if (ret) { + return ret; + } + } + } + return ContentPart::Ptr(); +} + +ContentPart::Ptr Parser::collectContentPart() const +{ + return collectContentPart(d->mTree); +} diff --git a/framework/domain/mimetreeparser/interface.h b/framework/domain/mimetreeparser/interface.h index 320030b7..82f88e73 100644 --- a/framework/domain/mimetreeparser/interface.h +++ b/framework/domain/mimetreeparser/interface.h @@ -19,104 +19,70 @@ #pragma once +#include +#include + +#include #include #include class Part; -typedef std::shared_ptr Part::Ptr; -class EncryptionPart; -typedef std::shared_ptr EncryptionPart::Ptr; -class SignaturePart; -typedef std::shared_ptr SignaturePart::Ptr; +class PartPrivate; class MimePart; -typedef std::shared_ptr MimePart::Ptr; class MimePartPrivate; class ContentPart; -typedef std::shared_ptr ContentPart::Ptr; class ContentPartPrivate; -class EncryptionErrorPart; -typedef std::shared_ptr EncryptionErrorPart::Ptr; -class EncryptionErrorPartPrivate; +class EncryptionPart; +class EncryptionPartPrivate; class AttachmentPart; -typedef std::shared_ptr AttachmentPart::Ptr; class AttachmentPartPrivate; class EncapsulatedPart; -typedef std::shared_ptr EncapsulatedPart::Ptr; -class EncapsulatedPart; +class EncapsulatedPartPrivate; class CertPart; -typedef std::shared_ptr CertPart::Ptr; +class CertPartPrivate; + +class Content; +class ContentPrivate; class Key; class Signature; class Encryption; class Parser; -typedef std::shared_ptr Parser::Ptr; class ParserPrivate; -class Parser -{ -public: - Parser(const QByteArray &mimeMessage); - - Part::Ptr getPart(QUrl url); - - QVector collect() const; - QVector collect() const; - QVector collect(Part start, std::function select, std::function filter) const; - -private: - std::unique_ptr d; -}; - - class Part { public: - virtual QByteArray type() const = 0; + typedef std::shared_ptr Ptr; + Part(); + virtual QByteArray type() const; bool hasSubParts() const; - QList subParts() const; - Part parent() const; + QVector subParts() const; + Part::Ptr parent() const; virtual QVector signatures() const; virtual QVector encryptions() const; +private: + std::unique_ptr d; + friend class ParserPrivate; + friend class PartPrivate; }; -//A structure element, that we need to reflect, that there is a Encryption starts -// only add a new Encrption block to encryptions block -class EncryptionPart : public Part -{ -public: - QVector encryptions() const Q_DECL_OVERRIDE; - QByteArray type() const Q_DECL_OVERRIDE; -}; - -// A structure element, that we need to reflect, that there is a Signature starts -// only add a new Signature block to signature block -// With this we can a new Singature type like pep aka -/* - * add a bodypartformatter, that returns a PEPSignaturePart with all signed subparts that are signed with pep. - * subclass Signature aka PEPSignature to reflect different way of properties of PEPSignatures. - */ -class SignaturePart : public Part +class Content { public: - QVector signatures() const Q_DECL_OVERRIDE; - QByteArray type() const Q_DECL_OVERRIDE; -}; - + typedef std::shared_ptr Ptr; + Content(const QByteArray &content, ContentPart *parent); + virtual ~Content(); - -class TextPart : public Part -{ -public: QByteArray content() const; //Use default charset @@ -124,14 +90,20 @@ public: // overwrite default charset with given charset QString encodedContent(QByteArray charset) const; -} + + virtual QVector signatures() const; + virtual QVector encryptions() const; +private: + std::unique_ptr d; +}; /* * A MessagePart that is based on a KMime::Content */ -class MimePart : public TextPart +class MimePart : public Part { public: + typedef std::shared_ptr Ptr; /** * Various possible values for the "Content-Disposition" header. */ @@ -154,6 +126,13 @@ public: // Unique identifier to ecactly this KMime::Content QByteArray link() const; + QByteArray content() const; + + //Use default charset + QString encodedContent() const; + + // overwrite default charset with given charset + QString encodedContent(QByteArray charset) const; QByteArray type() const Q_DECL_OVERRIDE; private: @@ -171,77 +150,84 @@ private: * for alternative, we are represating three messageparts * - "headers" do we return?, we can use setType to make it possible to select and than return these headers */ -class MainContentPart : public MimePart +class ContentPart : public Part { public: - enum Types { - PlainText, - Html + typedef std::shared_ptr Ptr; + enum Type { + PlainText = 0x0001, + Html = 0x0002 }; Q_DECLARE_FLAGS(Types, Type) - QVector content(Content::Type ct) const; + ContentPart(); + virtual ~ContentPart(); + + QVector content(Type ct) const; - Content::Types availableContent() const; + Types availableContents() const; QByteArray type() const Q_DECL_OVERRIDE; private: std::unique_ptr d; + + friend class ParserPrivate; }; -Q_DECLARE_OPERATORS_FOR_FLAGS(ContentPart::Type) +Q_DECLARE_OPERATORS_FOR_FLAGS(ContentPart::Types); class AttachmentPart : public MimePart { public: + typedef std::shared_ptr Ptr; QByteArray type() const Q_DECL_OVERRIDE; private: std::unique_ptr d; + friend class ParserPrivate; }; /* - * Faild to decrypt part - * thigs liks this can happen: - * decryption in progress - * have not tried at all to decrypt - * wrong passphrase - * no private key - * cryptobackend is not configured correctly (no gpg available) - * -> S/Mime and PGP have different meaning in their errors - * * Open Questions: * - How to make the string translateable for multiple clients, so that multiple clients can show same error messages, * that helps users to understand what is going on ? * - Does openpgp have translations already? */ -class EncryptionErrorPart : public Part +class EncryptionError { public: - Error errorId() const; - - CryptoBackend cryptoBackend(); + int errorId() const; + QString errorString() const; +}; +class EncryptionPart : public MimePart +{ +public: + typedef std::shared_ptr Ptr; QByteArray type() const Q_DECL_OVERRIDE; + EncryptionError error() const; + private: - std::unique_ptr d; + std::unique_ptr d; }; + /* * we want to request complete headers like: * from/to... */ -class EncapsulatedPart :: public AttachmentPart +class EncapsulatedPart : public AttachmentPart { public: + typedef std::shared_ptr Ptr; QByteArray type() const Q_DECL_OVERRIDE; - QByteArray header(); + //template QByteArray header(); private: - std::unique_ptr d; + std::unique_ptr d; }; /* @@ -249,64 +235,31 @@ private: * checking a cert (if it is a valid cert) */ -class CertPart :: public AttachmentPart +class CertPart : public AttachmentPart { public: + typedef std::shared_ptr Ptr; QByteArray type() const Q_DECL_OVERRIDE; - bool checkCert() const; - Status importCert() const; - -private: - std::unique_ptr d; -}; - -/* -the ggme error class - -// class GPGMEPP_EXPORT ErrorImportResult -{ -public: - Error() : mErr(0), mMessage() {} - explicit Error(unsigned int e) : mErr(e), mMessage() {} - - const char *source() const; - const char *asString() const; - - int code() const; - int sourceID() const; - - bool isCanceled() const; + enum CertType { + Pgp, + SMime + }; - unsigned int encodedError() const - { - return mErr; - } - int toErrno() const; + enum CertSubType { + Public, + Private + }; - static bool hasSystemError(); - static Error fromSystemError(unsigned int src = GPGMEPP_ERR_SOURCE_DEFAULT); - static void setSystemError(gpg_err_code_t err); - static void setErrno(int err); - static Error fromErrno(int err, unsigned int src = GPGMEPP_ERR_SOURCE_DEFAULT); - static Error fromCode(unsigned int err, unsigned int src = GPGMEPP_ERR_SOURCE_DEFAULT); + CertType certType() const; + CertSubType certSubType() const; + int keyLength() const; - GPGMEPP_MAKE_SAFE_BOOL_OPERATOR(mErr &&!isCanceled()) private: - unsigned int mErr; - mutable std::string mMessage; + std::unique_ptr d; }; -*/ -/* - * a used smime/PGP key - * in the end we also need things like: - bool isRevokation() const; - bool isInvalid() const; - bool isExpired() const; - -> so we end up wrapping GpgME::Key - */ class Key { QString keyid() const; @@ -314,8 +267,14 @@ class Key QString email() const; QString comment() const; QVector emails() const; + enum KeyTrust { + Unknown, Undefined, Never, Marginal, Full, Ultimate + }; KeyTrust keyTrust() const; - CryptoBackend cryptoBackend() const; + + bool isRevokation() const; + bool isInvalid() const; + bool isExpired() const; std::vector subkeys(); Key parentkey() const; @@ -328,35 +287,7 @@ class Signature QDateTime expirationTime() const; bool neverExpires() const; - bool inProgress(); //if the verfication is inProgress - - enum Validity { - Unknown, Undefined, Never, Marginal, Full, Ultimate - }; - Validity validity() const; - - // to determine if we need this in our usecase (email) - // GpgME::VerificationResult - enum Summary { - None = 0x000, - Valid = 0x001, - Green = 0x002, - Red = 0x004, - KeyRevoked = 0x008, - KeyExpired = 0x010, - SigExpired = 0x020, - KeyMissing = 0x040, - CrlMissing = 0x080, - CrlTooOld = 0x100, - BadPolicy = 0x200, - SysError = 0x400 - }; - Summary summary() const; - - const char *policyURL() const; - GpgME::Notation notation(unsigned int index) const; - std::vector notations() const; - + //template <> StatusObject verify() const; }; /* @@ -367,4 +298,30 @@ class Signature class Encryption { std::vector recipients() const; +}; + +class Parser +{ +public: + typedef std::shared_ptr Ptr; + Parser(const QByteArray &mimeMessage); + ~Parser(); + + Part::Ptr getPart(QUrl url); + + //template QVector collect(Part start, std::function select, std::function filter) const; + QVector collectAttachments(Part::Ptr start, std::function select, std::function filter) const; + ContentPart::Ptr collectContentPart(Part::Ptr start, std::function select, std::function filter) const; + ContentPart::Ptr collectContentPart(const Part::Ptr& start) const; + ContentPart::Ptr collectContentPart() const; + //template <> QVector collect() const; + + //template <> static StatusObject verifySignature(const Signature signature) const; + //template <> static StatusObject decrypt(const EncryptedPart part) const; + +signals: + void partsChanged(); + +private: + std::unique_ptr d; }; \ No newline at end of file diff --git a/framework/domain/mimetreeparser/objecttreesource.cpp b/framework/domain/mimetreeparser/objecttreesource.cpp new file mode 100644 index 00000000..12cf88ab --- /dev/null +++ b/framework/domain/mimetreeparser/objecttreesource.cpp @@ -0,0 +1,151 @@ +/* + Copyright (C) 2009 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net + Copyright (c) 2009 Andras Mantia + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +*/ + +#include "objecttreesource.h" + +#include +#include +#include +#include + +class ObjectSourcePrivate +{ +public: + ObjectSourcePrivate() + : mWriter(0) + , mAllowDecryption(true) + , mHtmlLoadExternal(true) + , mHtmlMail(true) + { + + } + MimeTreeParser::HtmlWriter *mWriter; + MimeTreeParser::BodyPartFormatterBaseFactory mBodyPartFormatterBaseFactory; + bool mAllowDecryption; + bool mHtmlLoadExternal; + bool mHtmlMail; +}; + +ObjectTreeSource::ObjectTreeSource(MimeTreeParser::HtmlWriter *writer) + : MimeTreeParser::Interface::ObjectTreeSource() + , d(new ObjectSourcePrivate) + { + d->mWriter = writer; + } + +ObjectTreeSource::~ObjectTreeSource() +{ + delete d; +} + +void ObjectTreeSource::setAllowDecryption(bool allowDecryption) +{ + d->mAllowDecryption = allowDecryption; +} + +MimeTreeParser::HtmlWriter *ObjectTreeSource::htmlWriter() +{ + return d->mWriter; +} + +bool ObjectTreeSource::htmlLoadExternal() const +{ + return d->mHtmlLoadExternal; +} + +void ObjectTreeSource::setHtmlLoadExternal(bool loadExternal) +{ + d->mHtmlLoadExternal = loadExternal; +} + +bool ObjectTreeSource::htmlMail() const +{ + return d->mHtmlMail; +} + +void ObjectTreeSource::setHtmlMail(bool htmlMail) +{ + d->mHtmlMail = htmlMail; +} + +bool ObjectTreeSource::decryptMessage() const +{ + return d->mAllowDecryption; +} + +bool ObjectTreeSource::showSignatureDetails() const +{ + return true; +} + +int ObjectTreeSource::levelQuote() const +{ + return 1; +} + +const QTextCodec *ObjectTreeSource::overrideCodec() +{ + return Q_NULLPTR; +} + +QString ObjectTreeSource::createMessageHeader(KMime::Message *message) +{ + return QString(); +} + +const MimeTreeParser::AttachmentStrategy *ObjectTreeSource::attachmentStrategy() +{ + return MimeTreeParser::AttachmentStrategy::smart(); +} + +QObject *ObjectTreeSource::sourceObject() +{ + return Q_NULLPTR; +} + +void ObjectTreeSource::setHtmlMode(MimeTreeParser::Util::HtmlMode mode) +{ + Q_UNUSED(mode); +} + +bool ObjectTreeSource::autoImportKeys() const +{ + return false; +} + +bool ObjectTreeSource::showEmoticons() const +{ + return false; +} + +bool ObjectTreeSource::showExpandQuotesMark() const +{ + return false; +} + +const MimeTreeParser::BodyPartFormatterBaseFactory *ObjectTreeSource::bodyPartFormatterFactory() +{ + return &(d->mBodyPartFormatterBaseFactory); +} + +MimeTreeParser::Interface::MessagePartRenderer::Ptr ObjectTreeSource::messagePartTheme(MimeTreeParser::Interface::MessagePart::Ptr msgPart) +{ + Q_UNUSED(msgPart); + return MimeTreeParser::Interface::MessagePartRenderer::Ptr(); +} diff --git a/framework/domain/mimetreeparser/objecttreesource.h b/framework/domain/mimetreeparser/objecttreesource.h new file mode 100644 index 00000000..bb0cd679 --- /dev/null +++ b/framework/domain/mimetreeparser/objecttreesource.h @@ -0,0 +1,57 @@ +/* + Copyright (C) 2009 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net + Copyright (c) 2009 Andras Mantia + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +*/ + +#ifndef MAILVIEWER_OBJECTTREEEMPTYSOURCE_H +#define MAILVIEWER_OBJECTTREEEMPTYSOURCE_H + +#include + +class QString; + +class ObjectSourcePrivate; +class ObjectTreeSource : public MimeTreeParser::Interface::ObjectTreeSource +{ +public: + ObjectTreeSource(MimeTreeParser::HtmlWriter *writer); + virtual ~ObjectTreeSource(); + void setHtmlLoadExternal(bool loadExternal); + void setHtmlMail(bool htmlMail); + bool htmlMail() const Q_DECL_OVERRIDE; + bool decryptMessage() const Q_DECL_OVERRIDE; + bool htmlLoadExternal() const Q_DECL_OVERRIDE; + bool showSignatureDetails() const Q_DECL_OVERRIDE; + void setHtmlMode(MimeTreeParser::Util::HtmlMode mode) Q_DECL_OVERRIDE; + void setAllowDecryption(bool allowDecryption); + int levelQuote() const Q_DECL_OVERRIDE; + const QTextCodec *overrideCodec() Q_DECL_OVERRIDE; + QString createMessageHeader(KMime::Message *message) Q_DECL_OVERRIDE; + const MimeTreeParser::AttachmentStrategy *attachmentStrategy() Q_DECL_OVERRIDE; + MimeTreeParser::HtmlWriter *htmlWriter() Q_DECL_OVERRIDE; + QObject *sourceObject() Q_DECL_OVERRIDE; + bool autoImportKeys() const Q_DECL_OVERRIDE; + bool showEmoticons() const Q_DECL_OVERRIDE; + bool showExpandQuotesMark() const Q_DECL_OVERRIDE; + const MimeTreeParser::BodyPartFormatterBaseFactory *bodyPartFormatterFactory() Q_DECL_OVERRIDE; + MimeTreeParser::Interface::MessagePartRendererPtr messagePartTheme(MimeTreeParser::Interface::MessagePartPtr msgPart) Q_DECL_OVERRIDE; +private: + ObjectSourcePrivate *const d; +}; + +#endif + diff --git a/framework/domain/mimetreeparser/stringhtmlwriter.cpp b/framework/domain/mimetreeparser/stringhtmlwriter.cpp new file mode 100644 index 00000000..88034492 --- /dev/null +++ b/framework/domain/mimetreeparser/stringhtmlwriter.cpp @@ -0,0 +1,150 @@ +/* -*- c++ -*- + filehtmlwriter.cpp + + This file is part of KMail, the KDE mail client. + Copyright (c) 2003 Marc Mutz + + KMail is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License, version 2, as + published by the Free Software Foundation. + + KMail 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 + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + + In addition, as a special exception, the copyright holders give + permission to link the code of this program with any edition of + the Qt library by Trolltech AS, Norway (or with modified versions + of Qt that use the same license as Qt), and distribute linked + combinations including the two. You must obey the GNU General + Public License in all respects for all of the code used other than + Qt. If you modify this file, you may extend this exception to + your version of the file, but you are not obligated to do so. If + you do not wish to do so, delete this exception statement from + your version. +*/ + +#include "stringhtmlwriter.h" + +#include +#include +#include + +StringHtmlWriter::StringHtmlWriter() + : MimeTreeParser::HtmlWriter() + , mState(Ended) +{ +} + +StringHtmlWriter::~StringHtmlWriter() +{ +} + +void StringHtmlWriter::begin(const QString &css) +{ + if (mState != Ended) { + qWarning() << "begin() called on non-ended session!"; + reset(); + } + + mState = Begun; + mExtraHead.clear(); + mHtml.clear(); + + if (!css.isEmpty()) { + write(QLatin1String("\n")); + } +} + +void StringHtmlWriter::end() +{ + if (mState != Begun) { + qWarning() << "Called on non-begun or queued session!"; + } + + if (!mExtraHead.isEmpty()) { + insertExtraHead(); + mExtraHead.clear(); + } + resolveCidUrls(); + mState = Ended; +} + +void StringHtmlWriter::reset() +{ + if (mState != Ended) { + mHtml.clear(); + mExtraHead.clear(); + mState = Begun; // don't run into end()'s warning + end(); + mState = Ended; + } +} + +void StringHtmlWriter::write(const QString &str) +{ + if (mState != Begun) { + qWarning() << "Called in Ended or Queued state!"; + } + mHtml.append(str); +} + +void StringHtmlWriter::queue(const QString &str) +{ + write(str); +} + +void StringHtmlWriter::flush() +{ + mState = Begun; // don't run into end()'s warning + end(); +} + +void StringHtmlWriter::embedPart(const QByteArray &contentId, const QString &url) +{ + write("\n"); + mEmbeddedPartMap.insert(contentId, url); +} + +void StringHtmlWriter::resolveCidUrls() +{ + for (const auto &cid : mEmbeddedPartMap.keys()) { + mHtml.replace(QString("src=\"cid:%1\"").arg(QString(cid)), QString("src=\"%1\"").arg(mEmbeddedPartMap.value(cid).toString())); + } +} + +void StringHtmlWriter::extraHead(const QString &extraHead) +{ + if (mState != Ended) { + qWarning() << "Called on non-started session!"; + } + mExtraHead.append(extraHead); +} + + +void StringHtmlWriter::insertExtraHead() +{ + const QString headTag(QStringLiteral("")); + const int index = mHtml.indexOf(headTag); + if (index != -1) { + mHtml.insert(index + headTag.length(), mExtraHead); + } +} + +QMap StringHtmlWriter::embeddedParts() const +{ + return mEmbeddedPartMap; +} + +QString StringHtmlWriter::html() const +{ + if (mState != Ended) { + qWarning() << "Called on non-ended session!"; + } + return mHtml; +} diff --git a/framework/domain/mimetreeparser/stringhtmlwriter.h b/framework/domain/mimetreeparser/stringhtmlwriter.h new file mode 100644 index 00000000..fa5b760e --- /dev/null +++ b/framework/domain/mimetreeparser/stringhtmlwriter.h @@ -0,0 +1,71 @@ +/* -*- c++ -*- + + Copyright (c) 2016 Sandro Knauß + + Kube is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License, version 2, as + published by the Free Software Foundation. + + Kube 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 + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + + In addition, as a special exception, the copyright holders give + permission to link the code of this program with any edition of + the Qt library by Trolltech AS, Norway (or with modified versions + of Qt that use the same license as Qt), and distribute linked + combinations including the two. You must obey the GNU General + Public License in all respects for all of the code used other than + Qt. If you modify this file, you may extend this exception to + your version of the file, but you are not obligated to do so. If + you do not wish to do so, delete this exception statement from + your version. +*/ + +#ifndef __KUBE_FRAMEWORK_MAIL_STRINGHTMLWRITER_H__ +#define __KUBE_FRAMEWORK_MAIL_STRINGHTMLWRITER_H__ + +#include + +#include +#include + +class QString; + +class StringHtmlWriter : public MimeTreeParser::HtmlWriter +{ +public: + explicit StringHtmlWriter(); + virtual ~StringHtmlWriter(); + + void begin(const QString &cssDefs) Q_DECL_OVERRIDE; + void end() Q_DECL_OVERRIDE; + void reset() Q_DECL_OVERRIDE; + void write(const QString &str) Q_DECL_OVERRIDE; + void queue(const QString &str) Q_DECL_OVERRIDE; + void flush() Q_DECL_OVERRIDE; + void embedPart(const QByteArray &contentId, const QString &url) Q_DECL_OVERRIDE; + void extraHead(const QString &str) Q_DECL_OVERRIDE; + + QString html() const; + QMap embeddedParts() const; +private: + void insertExtraHead(); + void resolveCidUrls(); + + QString mHtml; + QString mExtraHead; + enum State { + Begun, + Queued, + Ended + } mState; + QMap mEmbeddedPartMap; +}; + +#endif // __MESSAGEVIEWER_FILEHTMLWRITER_H__ diff --git a/framework/domain/mimetreeparser/test.cpp b/framework/domain/mimetreeparser/test.cpp deleted file mode 100644 index 51aa9871..00000000 --- a/framework/domain/mimetreeparser/test.cpp +++ /dev/null @@ -1,148 +0,0 @@ -Usecases: - -# plaintext msg + attachment -* ContentPart => cp1 -* AttachmentPart => ap1 - -(cp1) == collect(select=NoEncapsulatedMessages) -(ap1) == collect(select=NoEncapsulatedMessages) - -(PlainText) == cp1.availableContent() - -# html msg + related attachment + normal attachment -* ContentPart => cp1 -* AttachmentPart(mimetype="*/related", cid="12345678") => ap1 -* AttachmentPart => ap2 - -(cp1) == collect(select=NoEncapsulatedMessages) -(ap1, ap2) == collect(select=NoEncapsulatedMessages) -(ap2) == collect(select=NoEncapsulatedMessages, filter=filterelated) - -ap1 == getPart("cid:12345678") - -(Html) == cp1.availableContent() - -# alternative msg + attachment -* ContentPart(html=[TextPart("HTML"),], plaintext=[TextPart("Text"),]) => cp1 -* AttachmentPart => ap1 - -(cp1) == collect(select=NoEncapsulatedMessages) -(ap1) == collect(select=NoEncapsulatedMessages) - -(Html, PlainText) == cp1.availableContent() -[TextPart("HTML"),] == cp1.content(Html) -[TextPart("Text"),] == cp1.content(Plaintext) - -# alternative msg with GPGInlin -* ContentPart( - plaintext=[TextPart("Text"), TextPart("foo", encryption=(enc1))], - html=[TextPart("HTML"),] - ) => cp1 - -(Html, PlainText) == cp1.availableContent() - -[TextPart("HTML"),] == cp1.content(Html) -[TextPart("Text"),TextPart("foo", encryption=(enc1))] == cp1.content(Plaintext) - - -# encrypted msg (not encrypted/error) with unencrypted attachment -* EncryptionErrorPart => cp1 -* AttachmentPart => ap1 - -(cp1) == collect(select=NoEncapsulatedMessages) -(ap1) == collect(select=NoEncapsulatedMessages) - -#encrypted msg (decrypted with attachment) + unencrypted attachment -* encrytion=(rec1,rec2) => enc1 - * ContentPart(encrytion = (enc1,)) => cp1 - * AttachmentPart(encryption = (enc1,)) => ap1 -* AttachmentPart => ap2 - -(cp1) == collect(select=NoEncapsulatedMessages) -(ap1, ap2) == collect(select=NoEncapsulatedMessages) - -#INLINE GPG encrypted msg + attachment -* ContentPart => cp1 with - plaintext=[TextPart, TextPart(encrytion = (enc1(rec1,rec2),)), TextPart(signed = (sig1,)), TextPart] -* AttachmentPart => ap1 - -(cp1) == collect(select=NoEncapsulatedMessages) -(ap1) == collect(select=NoEncapsulatedMessages) - -[TextPart, TextPart(encrytion = (enc1(rec1,rec2),)), TextPart(signed = (sig1,)), TextPart] == cp1.content(Plaintext) - -#forwared encrypted msg + attachments -* ContentPart => cp1 -* EncapsulatedPart => ep1 - * Encrytion=(rec1,rec2) => enc1 - * Signature => sig1 - * ContentPart(encrytion = (enc1,), signature = (sig1,)) => cp2 - * TextPart(encrytion = (enc1,), signature = (sig1,)) - * TextPart(encrytion = (enc1, enc2(rec3,rec4),), signature = (sig1,)) - * AttachmentPart(encrytion = (enc1,), signature = (sig1,)) => ap1 -* AttachmentPart => ap2 - -(cp1) = collect(select=NoEncapsulatedMessages) -(ap2) = collect(select=NoEncapsulatedMessages) - -(cp2) = collect(ep1, select=NoEncapsulatedMessages) -(ap1) = collect(ep1, select=NoEncapsulatedMessages) - -(cp1, cp2) == collect() -(ap1, ap2) == collect()[TextPart, TextPart(encrytion = (enc1(rec1,rec2),)), TextPart(signed = (sig1,)), TextPart] - - -# plaintext msg + attachment + cert -* ContentPart => cp1 -* AttachmentPart => ap1 -* CertPart => cep1 - -(cp1) == collect(select=NoEncapsulatedMessages) -(ap1, cep1) == collect(select=NoEncapsulatedMessages) -(ap1) == collect(select=NoEncapsulatedMessages, filter=filterSubAttachmentParts) - -(cep1) == collect(select=NoEncapsulatedMessages) - - -collect function: - -bool noEncapsulatedMessages(Part part) -{ - if (is(part)) { - return false; - } - return true; -} - -bool filterRelated(T part) -{ - if (part.mimetype == related && !part.cid.isEmpty()) { - return false; //filter out related parts - } - return true; -} - -bool filterSubAttachmentParts(AttachmentPart part) -{ - if (isSubPart(part)) { - return false; // filter out CertPart f.ex. - } - return true; -} - -List collect(Part start, std::function select, std::function &)> filter) { - List col; - if (!select(start)) { - return col; - } - - if(isOrSubTypeIs(start) && filter(start.staticCast)){ - col.append(p); - } - foreach(childs as child) { - if (select(child)) { - col.expand(collect(child,select,filter); - } - } - return col; -} \ No newline at end of file diff --git a/framework/domain/mimetreeparser/tests/CMakeLists.txt b/framework/domain/mimetreeparser/tests/CMakeLists.txt new file mode 100644 index 00000000..d3549215 --- /dev/null +++ b/framework/domain/mimetreeparser/tests/CMakeLists.txt @@ -0,0 +1,10 @@ +add_definitions( -DMAIL_DATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/data" ) +include_directories( + ${CMAKE_CURRENT_BINARY_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/.. + ) + +add_executable(mimetreeparsertest interfacetest.cpp) +add_test(mimetreeparsertest mimetreeparsertest) +qt5_use_modules(mimetreeparsertest Core Test) +target_link_libraries(mimetreeparsertest mimetreeparser) \ No newline at end of file diff --git a/framework/domain/mimetreeparser/tests/data/alternative.mbox b/framework/domain/mimetreeparser/tests/data/alternative.mbox new file mode 100644 index 00000000..a2c58591 --- /dev/null +++ b/framework/domain/mimetreeparser/tests/data/alternative.mbox @@ -0,0 +1,34 @@ +Return-Path: +Date: Wed, 8 Jun 2016 20:34:44 -0700 +From: Konqi +To: konqi@kde.org +Subject: A random subject with alternative contenttype +MIME-Version: 1.0 +Content-Type: multipart/alternative; + boundary="----=_Part_12345678_12345678" + + +------=_Part_12345678_12345678 +Content-Type: text/plain; charset=utf-8 +Content-Transfer-Encoding: quoted-printable + +If you can see this text it means that your email client couldn't display o= +ur newsletter properly. +Please visit this link to view the newsletter on our website: http://www.go= +g.com/newsletter/ + +=2D GOG.com Team + + +------=_Part_12345678_12345678 +Content-Transfer-Encoding: 7Bit +Content-Type: text/html; charset="windows-1252" + + + +

Some HTML text

+ + +------=_Part_12345678_12345678-- diff --git a/framework/domain/mimetreeparser/tests/data/html.mbox b/framework/domain/mimetreeparser/tests/data/html.mbox new file mode 100644 index 00000000..eebd4283 --- /dev/null +++ b/framework/domain/mimetreeparser/tests/data/html.mbox @@ -0,0 +1,31 @@ +From foo@example.com Thu, 26 May 2011 01:16:54 +0100 +From: Thomas McGuire +Subject: HTML test +Date: Thu, 26 May 2011 01:16:54 +0100 +Message-ID: <1501334.pROlBb7MZF@herrwackelpudding.localhost> +X-KMail-Transport: GMX +X-KMail-Fcc: 28 +X-KMail-Drafts: 7 +X-KMail-Templates: 9 +User-Agent: KMail/4.6 beta5 (Linux/2.6.34.7-0.7-desktop; KDE/4.6.41; x86_64; git-0269848; 2011-04-19) +MIME-Version: 1.0 +Content-Type: multipart/alternative; boundary="nextPart8606278.tpV19BTJKu" +Content-Transfer-Encoding: 7Bit + + +--nextPart8606278.tpV19BTJKu +Content-Transfer-Encoding: 7Bit +Content-Type: text/plain; charset="windows-1252" + +Some HTML text +--nextPart8606278.tpV19BTJKu +Content-Transfer-Encoding: 7Bit +Content-Type: text/html; charset="windows-1252" + + + +

Some HTML text

+--nextPart8606278.tpV19BTJKu-- + diff --git a/framework/domain/mimetreeparser/tests/data/htmlonly.mbox b/framework/domain/mimetreeparser/tests/data/htmlonly.mbox new file mode 100644 index 00000000..e45b1c4d --- /dev/null +++ b/framework/domain/mimetreeparser/tests/data/htmlonly.mbox @@ -0,0 +1,21 @@ +From foo@example.com Thu, 26 May 2011 01:16:54 +0100 +From: Thomas McGuire +Subject: HTML test +Date: Thu, 26 May 2011 01:16:54 +0100 +Message-ID: <1501334.pROlBb7MZF@herrwackelpudding.localhost> +X-KMail-Transport: GMX +X-KMail-Fcc: 28 +X-KMail-Drafts: 7 +X-KMail-Templates: 9 +User-Agent: KMail/4.6 beta5 (Linux/2.6.34.7-0.7-desktop; KDE/4.6.41; x86_64; git-0269848; 2011-04-19) +MIME-Version: 1.0 +Content-Type: text/html +Content-Transfer-Encoding: 7Bit + + + + + +SOME HTML text. + + diff --git a/framework/domain/mimetreeparser/tests/data/htmlonlyexternal.mbox b/framework/domain/mimetreeparser/tests/data/htmlonlyexternal.mbox new file mode 100644 index 00000000..4eb3e2c3 --- /dev/null +++ b/framework/domain/mimetreeparser/tests/data/htmlonlyexternal.mbox @@ -0,0 +1,21 @@ +From foo@example.com Thu, 26 May 2011 01:16:54 +0100 +From: Thomas McGuire +Subject: HTML test +Date: Thu, 26 May 2011 01:16:54 +0100 +Message-ID: <1501334.pROlBb7MZF@herrwackelpudding.localhost> +X-KMail-Transport: GMX +X-KMail-Fcc: 28 +X-KMail-Drafts: 7 +X-KMail-Templates: 9 +User-Agent: KMail/4.6 beta5 (Linux/2.6.34.7-0.7-desktop; KDE/4.6.41; x86_64; git-0269848; 2011-04-19) +MIME-Version: 1.0 +Content-Type: text/html +Content-Transfer-Encoding: 7Bit + + + + + +SOME HTML text. + + diff --git a/framework/domain/mimetreeparser/tests/data/plaintext.mbox b/framework/domain/mimetreeparser/tests/data/plaintext.mbox new file mode 100644 index 00000000..c2e00a35 --- /dev/null +++ b/framework/domain/mimetreeparser/tests/data/plaintext.mbox @@ -0,0 +1,17 @@ +Return-Path: +Date: Wed, 8 Jun 2016 20:34:44 -0700 +From: Konqi +To: konqi@kde.org +Subject: A random subject with alternative contenttype +MIME-Version: 1.0 +Content-Type: text/plain; charset=utf-8 +Content-Transfer-Encoding: quoted-printable + +If you can see this text it means that your email client couldn't display o= +ur newsletter properly. +Please visit this link to view the newsletter on our website: http://www.go= +g.com/newsletter/ + +=2D GOG.com Team + + diff --git a/framework/domain/mimetreeparser/tests/interfacetest.cpp b/framework/domain/mimetreeparser/tests/interfacetest.cpp new file mode 100644 index 00000000..1e8c5302 --- /dev/null +++ b/framework/domain/mimetreeparser/tests/interfacetest.cpp @@ -0,0 +1,47 @@ +/* + Copyright (c) 2016 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 "interface.h" + +#include + +QByteArray readMailFromFile(const QString &mailFile) +{ + QFile file(QLatin1String(MAIL_DATA_DIR) + QLatin1Char('/') + mailFile); + file.open(QIODevice::ReadOnly); + Q_ASSERT(file.isOpen()); + return file.readAll(); +} + + +class InterfaceTest : public QObject +{ + Q_OBJECT +private slots: + + void testTextMail() + { + Parser parser(readMailFromFile("plaintext.mbox")); + auto contentPart = parser.collectContentPart(); + //QVERIFY((bool)contentPart); + } +}; + +QTEST_GUILESS_MAIN(InterfaceTest) +#include "interfacetest.moc" \ No newline at end of file diff --git a/framework/domain/mimetreeparser/thoughts.txt b/framework/domain/mimetreeparser/thoughts.txt new file mode 100644 index 00000000..3340347a --- /dev/null +++ b/framework/domain/mimetreeparser/thoughts.txt @@ -0,0 +1,148 @@ +Usecases: + +# plaintext msg + attachment +* ContentPart => cp1 +* AttachmentPart => ap1 + +(cp1) == collect(select=NoEncapsulatedMessages) +(ap1) == collect(select=NoEncapsulatedMessages) + +(PlainText) == cp1.availableContent() + +# html msg + related attachment + normal attachment +* ContentPart => cp1 +* AttachmentPart(mimetype="*/related", cid="12345678") => ap1 +* AttachmentPart => ap2 + +(cp1) == collect(select=NoEncapsulatedMessages) +(ap1, ap2) == collect(select=NoEncapsulatedMessages) +(ap2) == collect(select=NoEncapsulatedMessages, filter=filterelated) + +ap1 == getPart("cid:12345678") + +(Html) == cp1.availableContent() + +# alternative msg + attachment +* ContentPart(html=[Content("HTML"),], plaintext=[Content("Text"),]) => cp1 +* AttachmentPart => ap1 + +(cp1) == collect(select=NoEncapsulatedMessages) +(ap1) == collect(select=NoEncapsulatedMessages) + +(Html, PlainText) == cp1.availableContent() +[Content("HTML"),] == cp1.content(Html) +[Content("Text"),] == cp1.content(Plaintext) + +# alternative msg with GPGInlin +* ContentPart( + plaintext=[Content("Text"), Content("foo", encryption=(enc1))], + html=[Content("HTML"),] + ) => cp1 + +(Html, PlainText) == cp1.availableContent() + +[Content("HTML"),] == cp1.content(Html) +[Content("Text"),Content("foo", encryption=(enc1))] == cp1.content(Plaintext) + + +# encrypted msg (not encrypted/error) with unencrypted attachment +* EncryptionErrorPart => cp1 +* AttachmentPart => ap1 + +(cp1) == collect(select=NoEncapsulatedMessages) +(ap1) == collect(select=NoEncapsulatedMessages) + +#encrypted msg (decrypted with attachment) + unencrypted attachment +* encrytion=(rec1,rec2) => enc1 + * ContentPart(encrytion = (enc1,)) => cp1 + * AttachmentPart(encryption = (enc1,)) => ap1 +* AttachmentPart => ap2 + +(cp1) == collect(select=NoEncapsulatedMessages) +(ap1, ap2) == collect(select=NoEncapsulatedMessages) + +#INLINE GPG encrypted msg + attachment +* ContentPart => cp1 with + plaintext=[Content, Content(encrytion = (enc1(rec1,rec2),)), Content(signed = (sig1,)), Content] +* AttachmentPart => ap1 + +(cp1) == collect(select=NoEncapsulatedMessages) +(ap1) == collect(select=NoEncapsulatedMessages) + +[Content, Content(encrytion = (enc1(rec1,rec2),)), Content(signed = (sig1,)), Content] == cp1.content(Plaintext) + +#forwared encrypted msg + attachments +* ContentPart => cp1 +* EncapsulatedPart => ep1 + * Encrytion=(rec1,rec2) => enc1 + * Signature => sig1 + * ContentPart(encrytion = (enc1,), signature = (sig1,)) => cp2 + * Content(encrytion = (enc1,), signature = (sig1,)) + * Content(encrytion = (enc1, enc2(rec3,rec4),), signature = (sig1,)) + * AttachmentPart(encrytion = (enc1,), signature = (sig1,)) => ap1 +* AttachmentPart => ap2 + +(cp1) = collect(select=NoEncapsulatedMessages) +(ap2) = collect(select=NoEncapsulatedMessages) + +(cp2) = collect(ep1, select=NoEncapsulatedMessages) +(ap1) = collect(ep1, select=NoEncapsulatedMessages) + +(cp1, cp2) == collect() +(ap1, ap2) == collect()[Content, Content(encrytion = (enc1(rec1,rec2),)), Content(signed = (sig1,)), Content] + + +# plaintext msg + attachment + cert +* ContentPart => cp1 +* AttachmentPart => ap1 +* CertPart => cep1 + +(cp1) == collect(select=NoEncapsulatedMessages) +(ap1, cep1) == collect(select=NoEncapsulatedMessages) +(ap1) == collect(select=NoEncapsulatedMessages, filter=filterSubAttachmentParts) + +(cep1) == collect(select=NoEncapsulatedMessages) + + +collect function: + +bool noEncapsulatedMessages(Part part) +{ + if (is(part)) { + return false; + } + return true; +} + +bool filterRelated(T part) +{ + if (part.mimetype == related && !part.cid.isEmpty()) { + return false; //filter out related parts + } + return true; +} + +bool filterSubAttachmentParts(AttachmentPart part) +{ + if (isSubPart(part)) { + return false; // filter out CertPart f.ex. + } + return true; +} + +List collect(Part start, std::function select, std::function &)> filter) { + List col; + if (!select(start)) { + return col; + } + + if(isOrSubTypeIs(start) && filter(start.staticCast)){ + col.append(p); + } + foreach(childs as child) { + if (select(child)) { + col.expand(collect(child,select,filter); + } + } + return col; +} \ No newline at end of file -- cgit v1.2.3 From 9516f3b02f74f239ce2776abf7cf1147952065cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sandro=20Knau=C3=9F?= Date: Tue, 19 Jul 2016 14:46:23 +0200 Subject: new mimetreeparser interface Reviewers: cmollekopf Maniphest Tasks: T3208 Differential Revision: https://phabricator.kde.org/D2221 --- framework/domain/mimetreeparser/interface.cpp | 89 ++++++++++++++++------ framework/domain/mimetreeparser/interface.h | 14 ++-- .../domain/mimetreeparser/tests/data/html.mbox | 12 --- .../domain/mimetreeparser/tests/interfacetest.cpp | 25 +++++- 4 files changed, 98 insertions(+), 42 deletions(-) (limited to 'framework') diff --git a/framework/domain/mimetreeparser/interface.cpp b/framework/domain/mimetreeparser/interface.cpp index 6a399015..c239fcc0 100644 --- a/framework/domain/mimetreeparser/interface.cpp +++ b/framework/domain/mimetreeparser/interface.cpp @@ -38,26 +38,27 @@ public: QVector subParts(); - const std::weak_ptr &parent() const; + Part *parent() const; private: Part *q; - std::weak_ptr mParent; + Part *mParent; QVector mSubParts; }; PartPrivate::PartPrivate(Part* part) - :q(part) + : q(part) + , mParent(Q_NULLPTR) { } void PartPrivate::appendSubPart(Part::Ptr subpart) { - subpart->d->mParent = Part::Ptr(q); + subpart->d->mParent = q; mSubParts.append(subpart); } -const std::weak_ptr &PartPrivate::parent() const +Part *PartPrivate::parent() const { return mParent; } @@ -90,7 +91,7 @@ QByteArray Part::type() const QVector Part::encryptions() const { - auto parent = d->parent().lock(); + auto parent = d->parent(); if (parent) { return parent->encryptions(); } else { @@ -100,7 +101,7 @@ QVector Part::encryptions() const QVector Part::signatures() const { - auto parent = d->parent().lock(); + auto parent = d->parent(); if (parent) { return parent->signatures(); } else { @@ -146,6 +147,16 @@ QVector< Signature > Content::signatures() const return QVector(); } +QByteArray Content::content() const +{ + return d->mContent; +} + +QByteArray Content::charset() const +{ + return d->mCodec; +} + class ContentPartPrivate { public: @@ -153,12 +164,14 @@ public: void fillFrom(MimeTreeParser::HtmlMessagePart::Ptr part); void fillFrom(MimeTreeParser::AlternativeMessagePart::Ptr part); - QVector contents() const; + QVector content() const; ContentPart *q; + + ContentPart::Types types() const; private: - QVector mContents; + QVector mContent; ContentPart::Types mTypes; }; @@ -167,7 +180,7 @@ void ContentPartPrivate::fillFrom(MimeTreeParser::TextMessagePart::Ptr part) mTypes = ContentPart::PlainText; foreach (const auto &mp, part->subParts()) { auto content = std::make_shared(mp->text().toLocal8Bit(), q); - mContents.append(content); + mContent.append(content); } } @@ -181,6 +194,22 @@ void ContentPartPrivate::fillFrom(MimeTreeParser::AlternativeMessagePart::Ptr pa mTypes = ContentPart::Html | ContentPart::PlainText; } +ContentPart::Types ContentPartPrivate::types() const +{ + return mTypes; +} + +QVector ContentPartPrivate::content() const +{ + return mContent; +} + +QVector ContentPart::content(ContentPart::Type ct) const +{ + return d->content(); +} + + ContentPart::ContentPart() : d(std::unique_ptr(new ContentPartPrivate)) { @@ -197,6 +226,11 @@ QByteArray ContentPart::type() const return "ContentPart"; } +ContentPart::Types ContentPart::availableContents() const +{ + return d->types(); +} + class MimePartPrivate { public: @@ -230,7 +264,7 @@ public: ParserPrivate(Parser *parser); void setMessage(const QByteArray &mimeMessage); - void createTree(MimeTreeParser::MessagePart::Ptr start, Part::Ptr tree); + void createTree(const MimeTreeParser::MessagePart::Ptr& start, const Part::Ptr& tree); Part::Ptr mTree; private: @@ -273,9 +307,8 @@ void ParserPrivate::setMessage(const QByteArray& mimeMessage) } -void ParserPrivate::createTree(MimeTreeParser::MessagePart::Ptr start, Part::Ptr tree) +void ParserPrivate::createTree(const MimeTreeParser::MessagePart::Ptr &start, const Part::Ptr &tree) { - foreach (const auto &mp, start->subParts()) { const auto m = mp.dynamicCast(); const auto text = mp.dynamicCast(); @@ -316,16 +349,10 @@ Parser::~Parser() ContentPart::Ptr Parser::collectContentPart(const Part::Ptr &start) const { - foreach (const auto &part, start->subParts()) { - if (part->type() == "ContentPart") { - return std::dynamic_pointer_cast(part); - } else { - auto ret = collectContentPart(part); - if (ret) { - return ret; - } - } - } + const auto ret = collect(start, [](const Part::Ptr &p){return p->type() == "ContentPart";}, [](const ContentPart::Ptr &p){return true;}); + if (ret.size() > 0) { + return ret[0]; + }; return ContentPart::Ptr(); } @@ -333,3 +360,19 @@ ContentPart::Ptr Parser::collectContentPart() const { return collectContentPart(d->mTree); } + +template +QVector Parser::collect(const Part::Ptr &start, std::function select, std::function filter) const +{ + QVector ret; + foreach (const auto &part, start->subParts()) { + if (select(part)){ + const auto p = std::dynamic_pointer_cast(part); + if (p && filter(p)) { + ret.append(p); + } + ret += collect(part, select, filter); + } + } + return ret; +} diff --git a/framework/domain/mimetreeparser/interface.h b/framework/domain/mimetreeparser/interface.h index 82f88e73..8a0047ff 100644 --- a/framework/domain/mimetreeparser/interface.h +++ b/framework/domain/mimetreeparser/interface.h @@ -66,7 +66,7 @@ public: bool hasSubParts() const; QVector subParts() const; - Part::Ptr parent() const; + Part *parent() const; virtual QVector signatures() const; virtual QVector encryptions() const; @@ -85,6 +85,8 @@ public: QByteArray content() const; + QByteArray charset() const; + //Use default charset QString encodedContent() const; @@ -162,8 +164,8 @@ public: ContentPart(); virtual ~ContentPart(); - - QVector content(Type ct) const; + + QVector content(Type ct) const; Types availableContents() const; @@ -259,7 +261,6 @@ private: std::unique_ptr d; }; - class Key { QString keyid() const; @@ -309,7 +310,7 @@ public: Part::Ptr getPart(QUrl url); - //template QVector collect(Part start, std::function select, std::function filter) const; + template QVector collect(const Part::Ptr &start, std::function select, std::function filter) const; QVector collectAttachments(Part::Ptr start, std::function select, std::function filter) const; ContentPart::Ptr collectContentPart(Part::Ptr start, std::function select, std::function filter) const; ContentPart::Ptr collectContentPart(const Part::Ptr& start) const; @@ -324,4 +325,5 @@ signals: private: std::unique_ptr d; -}; \ No newline at end of file +}; + diff --git a/framework/domain/mimetreeparser/tests/data/html.mbox b/framework/domain/mimetreeparser/tests/data/html.mbox index eebd4283..d476a8d4 100644 --- a/framework/domain/mimetreeparser/tests/data/html.mbox +++ b/framework/domain/mimetreeparser/tests/data/html.mbox @@ -9,16 +9,6 @@ X-KMail-Drafts: 7 X-KMail-Templates: 9 User-Agent: KMail/4.6 beta5 (Linux/2.6.34.7-0.7-desktop; KDE/4.6.41; x86_64; git-0269848; 2011-04-19) MIME-Version: 1.0 -Content-Type: multipart/alternative; boundary="nextPart8606278.tpV19BTJKu" -Content-Transfer-Encoding: 7Bit - - ---nextPart8606278.tpV19BTJKu -Content-Transfer-Encoding: 7Bit -Content-Type: text/plain; charset="windows-1252" - -Some HTML text ---nextPart8606278.tpV19BTJKu Content-Transfer-Encoding: 7Bit Content-Type: text/html; charset="windows-1252" @@ -27,5 +17,3 @@ Content-Type: text/html; charset="windows-1252" p, li { white-space: pre-wrap; }

Some HTML text

---nextPart8606278.tpV19BTJKu-- - diff --git a/framework/domain/mimetreeparser/tests/interfacetest.cpp b/framework/domain/mimetreeparser/tests/interfacetest.cpp index 1e8c5302..fd828960 100644 --- a/framework/domain/mimetreeparser/tests/interfacetest.cpp +++ b/framework/domain/mimetreeparser/tests/interfacetest.cpp @@ -39,7 +39,30 @@ private slots: { Parser parser(readMailFromFile("plaintext.mbox")); auto contentPart = parser.collectContentPart(); - //QVERIFY((bool)contentPart); + QVERIFY((bool)contentPart); + QCOMPARE(contentPart->availableContents(), ContentPart::PlainText); + auto contentList = contentPart->content(ContentPart::PlainText); + QCOMPARE(contentList.size(), 1); + QCOMPARE(contentList[0]->content(), QStringLiteral("If you can see this text it means that your email client couldn't display our newsletter properly.\nPlease visit this link to view the newsletter on our website: http://www.gog.com/newsletter/\n\n- GOG.com Team\n\n").toLocal8Bit()); + QCOMPARE(contentList[0]->charset(), QStringLiteral("utf-8").toLocal8Bit()); + QCOMPARE(contentList[0]->encryptions().size(), 0); + QCOMPARE(contentList[0]->signatures().size(), 0); + } + + void testTextAlternative() + { + Parser parser(readMailFromFile("alternative.mbox")); + auto contentPart = parser.collectContentPart(); + QVERIFY((bool)contentPart); + QCOMPARE(contentPart->availableContents(), ContentPart::PlainText | ContentPart::Html); + } + + void testTextHtml() + { + Parser parser(readMailFromFile("html.mbox")); + auto contentPart = parser.collectContentPart(); + QVERIFY((bool)contentPart); + QCOMPARE(contentPart->availableContents(), ContentPart::Html); } }; -- cgit v1.2.3 From 550aa371cbf39d7d0cb735f890b338a3ac6883ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sandro=20Knau=C3=9F?= Date: Wed, 20 Jul 2016 10:04:49 +0200 Subject: add encrypted tests --- framework/domain/mimetreeparser/interface.cpp | 55 ++++------ framework/domain/mimetreeparser/interface.h | 3 + framework/domain/mimetreeparser/interface_p.h | 50 +++++++++ .../mimetreeparser/tests/data/alternative.mbox | 8 +- .../domain/mimetreeparser/tests/data/html.mbox | 6 +- .../domain/mimetreeparser/tests/data/htmlonly.mbox | 21 ---- .../tests/data/htmlonlyexternal.mbox | 21 ---- ...ed-attachment-and-non-encrypted-attachment.mbox | 115 +++++++++++++++++++++ .../data/openpgp-inline-charset-encrypted.mbox | 40 +++++++ .../mimetreeparser/tests/data/plaintext.mbox | 4 - .../mimetreeparser/tests/data/smime-encrypted.mbox | 22 ++++ .../domain/mimetreeparser/tests/interfacetest.cpp | 77 +++++++++++++- 12 files changed, 330 insertions(+), 92 deletions(-) create mode 100644 framework/domain/mimetreeparser/interface_p.h delete mode 100644 framework/domain/mimetreeparser/tests/data/htmlonly.mbox delete mode 100644 framework/domain/mimetreeparser/tests/data/htmlonlyexternal.mbox create mode 100644 framework/domain/mimetreeparser/tests/data/openpgp-encrypted-attachment-and-non-encrypted-attachment.mbox create mode 100644 framework/domain/mimetreeparser/tests/data/openpgp-inline-charset-encrypted.mbox create mode 100644 framework/domain/mimetreeparser/tests/data/smime-encrypted.mbox (limited to 'framework') diff --git a/framework/domain/mimetreeparser/interface.cpp b/framework/domain/mimetreeparser/interface.cpp index c239fcc0..e34ffda7 100644 --- a/framework/domain/mimetreeparser/interface.cpp +++ b/framework/domain/mimetreeparser/interface.cpp @@ -18,6 +18,7 @@ */ #include "interface.h" +#include "interface_p.h" #include "stringhtmlwriter.h" #include "objecttreesource.h" @@ -28,7 +29,6 @@ #include #include -#include class PartPrivate { @@ -164,34 +164,42 @@ public: void fillFrom(MimeTreeParser::HtmlMessagePart::Ptr part); void fillFrom(MimeTreeParser::AlternativeMessagePart::Ptr part); - QVector content() const; + QVector content(ContentPart::Type ct) const; ContentPart *q; ContentPart::Types types() const; - + private: - QVector mContent; + QMap> mContent; ContentPart::Types mTypes; }; void ContentPartPrivate::fillFrom(MimeTreeParser::TextMessagePart::Ptr part) { + qDebug() << "jepp"; mTypes = ContentPart::PlainText; foreach (const auto &mp, part->subParts()) { auto content = std::make_shared(mp->text().toLocal8Bit(), q); - mContent.append(content); + mContent[ContentPart::PlainText].append(content); } } void ContentPartPrivate::fillFrom(MimeTreeParser::HtmlMessagePart::Ptr part) { mTypes = ContentPart::Html; + auto content = std::make_shared(part->text().toLocal8Bit(), q); + mContent[ContentPart::Html].append(content); } void ContentPartPrivate::fillFrom(MimeTreeParser::AlternativeMessagePart::Ptr part) { mTypes = ContentPart::Html | ContentPart::PlainText; + + auto content = std::make_shared(part->htmlContent().toLocal8Bit(), q); + mContent[ContentPart::Html].append(content); + content = std::make_shared(part->plaintextContent().toLocal8Bit(), q); + mContent[ContentPart::PlainText].append(content); } ContentPart::Types ContentPartPrivate::types() const @@ -199,14 +207,14 @@ ContentPart::Types ContentPartPrivate::types() const return mTypes; } -QVector ContentPartPrivate::content() const +QVector ContentPartPrivate::content(ContentPart::Type ct) const { - return mContent; + return mContent[ct]; } QVector ContentPart::content(ContentPart::Type ct) const { - return d->content(); + return d->content(ct); } @@ -258,24 +266,6 @@ QByteArray AttachmentPart::type() const return "AttachmentPart"; } -class ParserPrivate -{ -public: - ParserPrivate(Parser *parser); - - void setMessage(const QByteArray &mimeMessage); - void createTree(const MimeTreeParser::MessagePart::Ptr& start, const Part::Ptr& tree); - - Part::Ptr mTree; -private: - Parser *q; - - MimeTreeParser::MessagePart::Ptr mPartTree; - std::shared_ptr mNodeHelper; - QString mHtml; - QMap mEmbeddedPartMap; -}; - ParserPrivate::ParserPrivate(Parser* parser) : q(parser) , mNodeHelper(std::make_shared()) @@ -292,7 +282,6 @@ void ParserPrivate::setMessage(const QByteArray& mimeMessage) // render the mail StringHtmlWriter htmlWriter; - QImage paintDevice; ObjectTreeSource source(&htmlWriter); MimeTreeParser::ObjectTreeParser otp(&source, mNodeHelper.get()); @@ -315,7 +304,11 @@ void ParserPrivate::createTree(const MimeTreeParser::MessagePart::Ptr &start, co const auto alternative = mp.dynamicCast(); const auto html = mp.dynamicCast(); const auto attachment = mp.dynamicCast(); - if (text) { + if (attachment) { + auto part = std::make_shared(); + part->d->fillFrom(attachment); + mTree->d->appendSubPart(part); + } else if (text) { auto part = std::make_shared(); part->d->fillFrom(text); mTree->d->appendSubPart(part); @@ -327,10 +320,6 @@ void ParserPrivate::createTree(const MimeTreeParser::MessagePart::Ptr &start, co auto part = std::make_shared(); part->d->fillFrom(html); mTree->d->appendSubPart(part); - } else if (attachment) { - auto part = std::make_shared(); - part->d->fillFrom(attachment); - mTree->d->appendSubPart(part); } else { createTree(m, tree); } @@ -349,7 +338,7 @@ Parser::~Parser() ContentPart::Ptr Parser::collectContentPart(const Part::Ptr &start) const { - const auto ret = collect(start, [](const Part::Ptr &p){return p->type() == "ContentPart";}, [](const ContentPart::Ptr &p){return true;}); + const auto ret = collect(start, [](const Part::Ptr &p){return p->type() == "ContentPart";}, [](const ContentPart::Ptr &p){return true;}); if (ret.size() > 0) { return ret[0]; }; diff --git a/framework/domain/mimetreeparser/interface.h b/framework/domain/mimetreeparser/interface.h index 8a0047ff..0aef7fd0 100644 --- a/framework/domain/mimetreeparser/interface.h +++ b/framework/domain/mimetreeparser/interface.h @@ -187,6 +187,7 @@ public: private: std::unique_ptr d; + friend class ParserPrivate; }; @@ -325,5 +326,7 @@ signals: private: std::unique_ptr d; + + friend class InterfaceTest; }; diff --git a/framework/domain/mimetreeparser/interface_p.h b/framework/domain/mimetreeparser/interface_p.h new file mode 100644 index 00000000..f83af6eb --- /dev/null +++ b/framework/domain/mimetreeparser/interface_p.h @@ -0,0 +1,50 @@ +/* + Copyright (c) 2016 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. +*/ + +#pragma once + +#include "interface.h" + +#include +#include + +namespace MimeTreeParser +{ + class MessagePart; + class NodeHelper; + typedef QSharedPointer MessagePartPtr; +} + +class ParserPrivate +{ +public: + ParserPrivate(Parser *parser); + + void setMessage(const QByteArray &mimeMessage); + void createTree(const MimeTreeParser::MessagePartPtr& start, const Part::Ptr& tree); + + Part::Ptr mTree; +private: + Parser *q; + + MimeTreeParser::MessagePartPtr mPartTree; + std::shared_ptr mNodeHelper; + QString mHtml; + QMap mEmbeddedPartMap; +}; \ No newline at end of file diff --git a/framework/domain/mimetreeparser/tests/data/alternative.mbox b/framework/domain/mimetreeparser/tests/data/alternative.mbox index a2c58591..6522c34b 100644 --- a/framework/domain/mimetreeparser/tests/data/alternative.mbox +++ b/framework/domain/mimetreeparser/tests/data/alternative.mbox @@ -17,18 +17,12 @@ ur newsletter properly. Please visit this link to view the newsletter on our website: http://www.go= g.com/newsletter/ -=2D GOG.com Team - ------=_Part_12345678_12345678 Content-Transfer-Encoding: 7Bit Content-Type: text/html; charset="windows-1252" - - -

Some HTML text

+

HTML text

------=_Part_12345678_12345678-- diff --git a/framework/domain/mimetreeparser/tests/data/html.mbox b/framework/domain/mimetreeparser/tests/data/html.mbox index d476a8d4..bf5c685d 100644 --- a/framework/domain/mimetreeparser/tests/data/html.mbox +++ b/framework/domain/mimetreeparser/tests/data/html.mbox @@ -12,8 +12,4 @@ MIME-Version: 1.0 Content-Transfer-Encoding: 7Bit Content-Type: text/html; charset="windows-1252" - - -

Some HTML text

+

HTML text

\ No newline at end of file diff --git a/framework/domain/mimetreeparser/tests/data/htmlonly.mbox b/framework/domain/mimetreeparser/tests/data/htmlonly.mbox deleted file mode 100644 index e45b1c4d..00000000 --- a/framework/domain/mimetreeparser/tests/data/htmlonly.mbox +++ /dev/null @@ -1,21 +0,0 @@ -From foo@example.com Thu, 26 May 2011 01:16:54 +0100 -From: Thomas McGuire -Subject: HTML test -Date: Thu, 26 May 2011 01:16:54 +0100 -Message-ID: <1501334.pROlBb7MZF@herrwackelpudding.localhost> -X-KMail-Transport: GMX -X-KMail-Fcc: 28 -X-KMail-Drafts: 7 -X-KMail-Templates: 9 -User-Agent: KMail/4.6 beta5 (Linux/2.6.34.7-0.7-desktop; KDE/4.6.41; x86_64; git-0269848; 2011-04-19) -MIME-Version: 1.0 -Content-Type: text/html -Content-Transfer-Encoding: 7Bit - - - - - -SOME HTML text. - - diff --git a/framework/domain/mimetreeparser/tests/data/htmlonlyexternal.mbox b/framework/domain/mimetreeparser/tests/data/htmlonlyexternal.mbox deleted file mode 100644 index 4eb3e2c3..00000000 --- a/framework/domain/mimetreeparser/tests/data/htmlonlyexternal.mbox +++ /dev/null @@ -1,21 +0,0 @@ -From foo@example.com Thu, 26 May 2011 01:16:54 +0100 -From: Thomas McGuire -Subject: HTML test -Date: Thu, 26 May 2011 01:16:54 +0100 -Message-ID: <1501334.pROlBb7MZF@herrwackelpudding.localhost> -X-KMail-Transport: GMX -X-KMail-Fcc: 28 -X-KMail-Drafts: 7 -X-KMail-Templates: 9 -User-Agent: KMail/4.6 beta5 (Linux/2.6.34.7-0.7-desktop; KDE/4.6.41; x86_64; git-0269848; 2011-04-19) -MIME-Version: 1.0 -Content-Type: text/html -Content-Transfer-Encoding: 7Bit - - - - - -SOME HTML text. - - diff --git a/framework/domain/mimetreeparser/tests/data/openpgp-encrypted-attachment-and-non-encrypted-attachment.mbox b/framework/domain/mimetreeparser/tests/data/openpgp-encrypted-attachment-and-non-encrypted-attachment.mbox new file mode 100644 index 00000000..2d9726ea --- /dev/null +++ b/framework/domain/mimetreeparser/tests/data/openpgp-encrypted-attachment-and-non-encrypted-attachment.mbox @@ -0,0 +1,115 @@ +From test@kolab.org Fri May 01 15:12:47 2015 +From: testkey +To: you@you.com +Subject: enc & non enc attachment +Date: Fri, 01 May 2015 17:12:47 +0200 +Message-ID: <13897561.XENKdJMSlR@tabin.local> +X-KMail-Identity: 1197256126 +User-Agent: KMail/4.13.0.1 (Linux/3.19.1-towo.1-siduction-amd64; KDE/4.14.2; x86_64; git-cd33034; 2015-04-11) +MIME-Version: 1.0 +Content-Type: multipart/mixed; boundary="nextPart1939768.sIoLGH0PD8" +Content-Transfer-Encoding: 7Bit + +This is a multi-part message in MIME format. + +--nextPart1939768.sIoLGH0PD8 +Content-Type: multipart/encrypted; boundary="nextPart2814166.CHKktCGlQ3"; protocol="application/pgp-encrypted" + + +--nextPart2814166.CHKktCGlQ3 +Content-Type: application/pgp-encrypted +Content-Disposition: attachment +Content-Transfer-Encoding: 7Bit + +Version: 1 +--nextPart2814166.CHKktCGlQ3 +Content-Type: application/octet-stream +Content-Disposition: inline; filename="msg.asc" +Content-Transfer-Encoding: 7Bit + +-----BEGIN PGP MESSAGE----- +Version: GnuPG v2 + +hIwDGJlthTT7oq0BA/9cXFQ6mN9Vxnc2B9M10odS3/6z1tsIY9oJdsiOjpfxqapX +P7nOzR/jNWdFQanXoG1SjAcY2FeZEN0c3SkxEM6R5QVF1vMh/Xsni1clI+peZyVT +Z4OSU74YCfYLg+cgDnPCF3kyNPVe6Z1pnfWOCZNCG3rpApw6UVLN63ScWC6eQIUB +DAMMzkNap8zaOwEIANKHn1svvj+hBOIZYf8R+q2Bw7cd4xEChiJ7uQLnD98j0Fh1 +85v7/8JbZx6rEDDenPp1mCciDodb0aCmi0XLuzJz2ANGTVflfq+ZA+v1pwLksWCs +0YcHLEjOJzjr3KKmvu6wqnun5J2yV69K3OW3qTTGhNvcYZulqQ617pPa48+sFCgh +nM8TMAD0ElVEwmMtrS3AWoJz52Af+R3YzpAnX8NzV317/JG+b6e2ksl3tR7TWp1q +2FOqC1sXAxuv+DIz4GgRfaK1+xYr2ckkg+H/3HJqa5LmJ7rGCyv+Epfp9u+OvdBG +PBvuCtO3tm0crmnttMw57Gy35BKutRf/8MpBj/nS6QFX0t7XOLeL4Me7/a2H20wz +HZsuRGDXMCh0lL0FYCBAwdbbYvvy0gz/5iaNvoADtaIu+VtbFNrTUN0SwuL+AIFS ++WIiaSbFt4Ng3t9YmqL6pqB7fjxI10S+PK0s7ABqe4pgbzUWWt1yzBcxfk8l/47Q +JrlvcE7HuDOhNOHfZIgUP2Dbeu+pVvHIJbmLsNWpl4s+nHhoxc9HrVhYG/MTZtQ3 +kkUWviegO6mwEZjQvgBxjWib7090sCxkO847b8A93mfQNHnuy2ZEEJ+9xyk7nIWs +4RsiNR8pYc/SMvdocyAvQMH/qSvmn/IFJ+jHhtT8UJlXJ0bHvXTHjHMqBp6fP69z +Jh1ERadWQdMaTkzQ+asl+kl/x3p6RZP8MEVbZIl/3pcV+xiFCYcFu2TETKMtbW+b +NYOlrltFxFDvyu3WeNNp0g9k0nFpD/T1OXHRBRcbUDWE4QF6NWTm6NO9wy2UYHCi +7QTSecBWgMaw7cUdwvnW6chIVoov1pm69BI9D0PoV76zCI7KzpiDsTFxdilKwbQf +K/PDnv9Adx3ERh0/F8llBHrj2UGsRs4aHSEBDBJIHDCp8+lqtsRcINQBKEU3qIjt +wf5vizdaVIgQnsD2z8QmBQ7QCCipI0ur6GKl+YWDDOSDLDUs9dK4A6xo/4Q0bsnI +rH63ti5HslGq6uArfFkewH2MWff/8Li3uGEqzpK5NhP5UpbArelK+QaQQP5SdsmW +XFwUqDS4QTCKNJXw/5SQMl8UE10l2Xaav3TkiOYTcBcvPNDovYgnMyRff/tTeFa8 +83STkvpGtkULkCntp22fydv5rg6DZ7eJrYfC2oZXdM87hHhUALUO6Y/VtVmNdNYw +F3Uim4PDuLIKt+mFqRtFqnWm+5X/AslC31qLkjH+Fbb83TY+mC9gbIn7CZGJRCjn +zzzMX2h15V/VHzNUgx9V/h28T0/z25FxoozZiJxpmhOtqoxMHp+y6nXXfMoIAD1D +963Pc7u1HS0ny54A7bqc6KKd4W9IF7HkXn3SoBwCyn0IOPoKQTDD8mW3lbBI6+h9 +vP+MAQpfD8s+3VZ9r7OKYCVmUv47ViTRlf428Co6WT7rTHjGM09tqz826fTOXA== +=6Eu9 +-----END PGP MESSAGE----- + +--nextPart2814166.CHKktCGlQ3-- + +--nextPart1939768.sIoLGH0PD8 +Content-Disposition: attachment; filename="image.png" +Content-Transfer-Encoding: base64 +Content-Type: image/png; name="image.png" + +iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAAlwSFlzAAAb +rwAAG68BXhqRHAAAAAd0SU1FB9gHFg8aNG8uqeIAAAAGYktHRAD/AP8A/6C9p5MAAAkqSURBVHja +5VV7cFTVGf/OPefeu3fv3t1NdhMSCHkKASEpyEsaGwalWEWntLV1Wu0fdOxAx9Iq0xntAwac6ehY +p+rwKLbjjLRFh9JadURKRGgFQTTECCYQE9nNgzzYZDe7m33d1+l3tpOOU61T2tF/+s1s7pzn9/t+ +v993Av/3QT6FO6WdO/d+M55Il8rMOdrT0x3Zt++3+c8EgM/nozseeviJiYmpe1zOQdM8BOOCIku/ +lIj1VrQ/0r9n9+78xwLgeAA3w4fHXV1d5Omnn6aapumlJSVVqalUJJvJZRdcu0RSfZQsaW7mjfPm +cbF9+/btEIlEaq6Z03whXyhIjDFuGIZEKSP5fMFRVcVNT2Vf0jzsmMxYGtel9rff/vM/M8bjcZpM +Jp1XX32VNDc3e7ovRP3JyZGVNdXVd1FGGwKBQEM8njiWTKV36IHgEACwibGx62LjU/cBd01Zljoc +p9DHmLbHsmyK1UuKooJt24IMcLE+y3L45eEYLS8LgWH4YXR0bAPZtGmTVFvfoBZMEzKpFKmqqmqp +qane4DhOteH3L1FkWZVlGSzLAtd1Oe4773C4LxoZvDWXh82OY2MtwAuFvCvSyDIFXdelYDDIvF4d +xPzA0AgXFStMcWPxBPGoKvXpPh6JDG5hK1Zcv1H36Xc6tsMs21EMQ69CLSts2wGkDygTyW2CP8gX +TKLIyvx0OrdDUXyLKXVUkdSne4QKtFAwuWmabjAYkDyqAgG/jziORh1EKaonkkQt2yRZRC5JHEGn +L7OKyopNqqo2IbWQjqWgLOwFBFKsuGDa4PVyIssMk1sCACCjimXbrbquYKW41zJJOpXkeARyeZNQ +SUKwHEqCKnBuAybkZeFSmssVSDKdhlBpCRgIcnQsdvKPB19sY4rMNIaH0BhQUVHKvXgpIiQF0wK/ +4QORnOEayoDzOSBMXK4BSgpeTcMECqiqTDKZHDKmct3LCI55Kp0mQgK/3yDYkgIc3kNhfHzCkRk9 +p6nk+yPD3SmWzeZiKNkciUrg2g5BjQWdSBchiEvQjzoWAFkUYPDrCjBFUEJ8AhSIRyl2jcfjEL9h +AFJODL8B6H7IZrNIt2g3B1mysShdQhmbT58+ExRdx3L5/PNomGU4kJkuA9ILYn+JP4CXOoDUoWO9 +IBhCSBCLTYCK+rqOg8CKvY6JPQhGxjkX1zyAdwrgAhTKWBDmxTUTC7Tcy5dHBiilL7cdaTsNGAwP +7o32D4Q9HnWTrvsCiqIgdWgqDkJfkKgDU1MZcBGMhbKgj2B0LIle8eNhgiBsoMwFEY7rQDqVwlo5 +esUE/AAR81gUYIUT8UR2//4/rK+pLjs3MhIFEVJN9WwXK2oM+P1BREpQO0hjwkw+BzJWY1oOXB5L +w9DIOGTQvYS4UFqigR9ZwUqEXFghVop059AjonqcAIZrqCKg31AS3OU66Adf4sabWqKvvHIYpoNh +y+Vj4xMHVEW93eUuo0izhT4oRbcSIoALbRle4AVVkfBup6g9thwCzRX1VRQmdMeqLVETEIkW2ZNx +H8oqzqAfXCGJEQ6XBQEgNQ2A7tq1C1a1tvaattOOrVFOqVSLCQhqU6QPx+DTsOU0GavLYUV20Qv4 +rEIymYNQuB48Wkg8QTA0NIQeYKB6NGTgH90jIcJEMikAi1dRRo9NLV583ek33jjpFAGIPw8++IAj +e9SIRGm5wliraVosnTWLmmemUugBkTiPSS3AtgV8VQA9A8LxdfULYXBoEKv2wMhIn2BHGFR0DZ6d +glQ6hUDT6A/RWVSSmfx5DjxRV1vzVkdHBzDAWLNmDezc+aQVqqz5dSY52Z63nLn9A33lI9myLXNL +xv0Fq3gWutMN0BToxcso+AN+cKmOXI5A9P12mKDzYNXcZXDq1F+h+IboFgzb1VAhDULeJpxwC19G +g/uMgOXVfXW1tbWCYM6mtdi8+YfiM4m/Y1UrHzkergyXz/3czImCnRjuHiW3qxpPqGFPy6SpHJC9 +IR+Sm+2N8i/dcMOMZdGeshcrS/S58+c3zU2Z8oVD50cbVfP8M4pGkymoUxLxsUzOVhtmQ+5432Rg +oj6QOLFj28/caQk+EjMXraUV1eW+8dH06StQZnlnNbQefGTD92pWfu3I6TOT8oY7brv4hWUt3xiw +2OrlDVVdRslsd2Fd469Q8sUB3c8uOW49SdHX1rbcePhoz3B7feuqlt5oZtBTv+ioSdXc7q3fHQaM +fwtg6Vd/dEvn8Qssnzg/0Ns56jRcO6Nw4d1Af+/RH0/cdv+O/fRK7KnmBXPWGsQeDPhK9oWC6hdd +R3pdUcg88Tx7U7Ej1y1qMjreGwjt/cnaF2YtvCXQe7bzxLkj+/sunT0Ry00OwHRI8DERLqeNmqGV +JZJVC6Yu7UxMOfLFlV9pWQcYp57/013rb1u9ua29b0Ch4bsl4tKLY5P1sgxNJzsHDj136KzS3NTk +9mTNusPvXJLrbnjUe/b16FDfsZ/3xC8d4/HoCQ4Anwzg91vWPL7+3pvvDM806sTY4IVyMxfrojO3 +BVubbyJMhnVVM3y+l187/nChIJ2ZpSs9hMD4qC6t6x6+0gkAoRC33/Sb8RdmXj9nzvWraivhP47g +AyHxKb1mfWkRYHCjMb30nafeeWzerU9963w3L3/02c4f7D0y0NXTx3f3D/JTb7bzxpeODu55+PGT +yy5F+ZmeD/iSrh5efeJd/hGZP5GBux+6cysY3w7H+16IVy65V6trnn3P9JqVjQ3JuSsdHhWW6hIL +NuhyUpJgEF/ofSVBeLBuVtVjd3y55SHXhQ8UBht0DR4r98Fs+IRg/zrxlz2/2A7p5yYBY93Gu+4f +H5xojLwOxfjd/WufOHhQ/IcD7eYVC5YyCjFMfkVV4NpMFvpTachoZeDaNryLnliOczsUCv1XBWD8 +YjF5MWJ9kcT757qenR7vf4bDoqWwHCvUUfPNsQQMWSZAZTlsw7nxYQQTcuDrjgQuPn7z/D7YivNt +nPPfEDzwqcU75/j6SD/f8uG5vXs5dL7Hjb+d4gp8mnF8nAOabjcac+OBAxyuNiT4HyNwGZYgu0RW +IDt/Icz4zAC0tXE4183rQ6XwU9uBXgLQ5Teg7GIv1+EqgsF/GY4DtCQALZMp2ITttmqoHzpWr756 +o/0d59+Lh3Y1HHcAAAAASUVORK5CYII= + +--nextPart1939768.sIoLGH0PD8-- + diff --git a/framework/domain/mimetreeparser/tests/data/openpgp-inline-charset-encrypted.mbox b/framework/domain/mimetreeparser/tests/data/openpgp-inline-charset-encrypted.mbox new file mode 100644 index 00000000..8bd06910 --- /dev/null +++ b/framework/domain/mimetreeparser/tests/data/openpgp-inline-charset-encrypted.mbox @@ -0,0 +1,40 @@ +From test@example.com Thu, 17 Oct 2013 02:13:03 +0200 +Return-Path: +Delivered-To: you@you.com +Received: from localhost (localhost [127.0.0.1]) + by test@example.com (Postfix) with ESMTP id B30D8120030 + for ; Thu, 17 Oct 2013 02:13:05 +0200 (CEST) +From: test +To: you@you.com +Subject: charset +Date: Thu, 17 Oct 2013 02:13:03 +0200 +Message-ID: <4081645.yGjUJ4o4Se@example.local> +User-Agent: KMail/4.12 pre (Linux/3.11-4.towo-siduction-amd64; KDE/4.11.2; x86_64; git-f7f14e3; 2013-10-15) +MIME-Version: 1.0 +Content-Transfer-Encoding: 7Bit +Content-Type: text/plain; charset="ISO-8859-15" + +-----BEGIN PGP MESSAGE----- +Version: GnuPG v2.0.22 (GNU/Linux) + +hIwDGJlthTT7oq0BBACbaRZudMigMTetPZNRgkfEXv4QQowR1jborw0dcgKKqMQ1 +6o67NkpxvmXKGJTfTVCLBX3nk6FKYo6NwlPCyU7X9X0DDk8hvaBdR9wGfrdm5YWX +GKOzcqJY1EypiMsspXeZvjzEW7O8I956c3vBb/2pM3xqYEK1kh8+d9bVH+cjf4UB +DAMMzkNap8zaOwEH/1rPShyYL8meJN+/GGgS8+Nf1BW5pSHdAPCg0dnX4QCLEx7u +GkBU6N4JGYayaCBofibOLacQPhYZdnR5Xb/Pvrx03GrzyzyDp0WyeI9nGNfkani7 +sCRWbzlMPsEvGEvJVnMLNRSk4xhPIWumL4APkw+Mgi6mf+Br8z0RhfnGwyMA53Mr +pG9VQKlq3v7/aaN40pMjAsxiytcHS515jXrb3Ko4pWbTlAr/eytOEfkLRJgSOpQT +BY7lWs+UQJqiG8Yn65vS9LMDNJgX9EOGx77Z4u9wvv4ZieOxzgbHGg5kYCoae7ba +hxZeNjYKscH+E6epbOxM/wlTdr4UTiiW9dMsH0zSwMUB891gToeXq+LDGEPTKVSX +tsJm4HS/kISJBwrCI4EUqWZML6xQ427NkZGmF2z/sD3kmL66GjspIKnb4zHmXacp +84n2KrI9s7p6AnKnQjsxvB/4/lpXPCIY5GH7KjySEJiMsHECzeN1dJSL6keykBsx +DtmYDA+dhZ6UWbwzx/78+mjNREhyp/UiSAmLzlJh89OH/xelAPvKcIosYwz4cY9N +wjralTmL+Y0aHKeZJOeqPLaXADcPFiZrCNPCH65Ey5GEtDpjLpEbjVbykPV9+YkK +7JKW6bwMraOl5zmAoR77PWMo3IoYb9q4GuqDr1V2ZGlb7eMH1gj1nfgfVintKC1X +3jFfy7aK6LIQDVKEwbi0SxVXTKStuliVUy5oX4woDOxmTEotJf1QlKZpn5oF20UP +tumYrp0SPoP8Bo4EVRVaLupduI5cYce1q/kFj9Iho/wk56MoG9PxMMfsH7oKg3AA +CqQ6/kM4oJNdN5xIf1EH5HeaNFkDy1jlLznnhwVAZKPo/9ffpg== +=bPqu +-----END PGP MESSAGE----- + + diff --git a/framework/domain/mimetreeparser/tests/data/plaintext.mbox b/framework/domain/mimetreeparser/tests/data/plaintext.mbox index c2e00a35..d185b1c1 100644 --- a/framework/domain/mimetreeparser/tests/data/plaintext.mbox +++ b/framework/domain/mimetreeparser/tests/data/plaintext.mbox @@ -11,7 +11,3 @@ If you can see this text it means that your email client couldn't display o= ur newsletter properly. Please visit this link to view the newsletter on our website: http://www.go= g.com/newsletter/ - -=2D GOG.com Team - - diff --git a/framework/domain/mimetreeparser/tests/data/smime-encrypted.mbox b/framework/domain/mimetreeparser/tests/data/smime-encrypted.mbox new file mode 100644 index 00000000..6b6d6a0d --- /dev/null +++ b/framework/domain/mimetreeparser/tests/data/smime-encrypted.mbox @@ -0,0 +1,22 @@ +From test@example.com Sat, 13 Apr 2013 01:54:30 +0200 +From: test +To: you@you.com +Subject: test +Date: Sat, 13 Apr 2013 01:54:30 +0200 +Message-ID: <1576646.QQxzHWx8dA@tabin> +X-KMail-Identity: 505942601 +User-Agent: KMail/4.10.2 (Linux/3.9.0-rc4-experimental-amd64; KDE/4.10.60; x86_64; git-fc9b82c; 2013-04-11) +MIME-Version: 1.0 +Content-Type: application/pkcs7-mime; name="smime.p7m"; smime-type="enveloped-data" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename="smime.p7m" + +MIAGCSqGSIb3DQEHA6CAMIACAQAxgfwwgfkCAQAwYjBVMQswCQYDVQQGEwJVUzENMAsGA1UECgwE +S0RBQjEWMBQGA1UEAwwNdW5pdHRlc3QgY2VydDEfMB0GCSqGSIb3DQEJARYQdGVzdEBleGFtcGxl +LmNvbQIJANNFIDoYY4XJMA0GCSqGSIb3DQEBAQUABIGAJwmmaOeidXUHSQGOf2OBIsPYafVqdORe +y54pEXbXiAfSVUWgI4a9CsiWwcDX8vlaX9ZLLr+L2VmOfr6Yc5214yxzausZVvnUFjy6LUXotuEX +tSar4EW7XI9DjaZc1l985naMsTx9JUa5GyQ9J6PGqhosAKpKMGgKkFAHaOwE1/IwgAYJKoZIhvcN +AQcBMBQGCCqGSIb3DQMHBAieDfmz3WGbN6CABHgEpsLrNn0PAZTDUfNomDypvSCl5bQH+9cKm80m +upMV2r8RBiXS7OaP4SpCxq18afDTTPatvboHIoEX92taTbq8soiAgEs6raSGtEYZNvFL0IYqm7MA +o5HCOmjiEcInyPf14lL3HnPk10FaP3hh58qTHUh4LPYtL7UECOZELYnUfUVhAAAAAAAAAAAAAA== + diff --git a/framework/domain/mimetreeparser/tests/interfacetest.cpp b/framework/domain/mimetreeparser/tests/interfacetest.cpp index fd828960..88691539 100644 --- a/framework/domain/mimetreeparser/tests/interfacetest.cpp +++ b/framework/domain/mimetreeparser/tests/interfacetest.cpp @@ -18,6 +18,7 @@ */ #include "interface.h" +#include "interface_p.h" #include @@ -33,6 +34,15 @@ QByteArray readMailFromFile(const QString &mailFile) class InterfaceTest : public QObject { Q_OBJECT +private: + void printTree(const Part::Ptr &start, QString pre) + { + foreach (const auto &part, start->subParts()) { + qWarning() << QStringLiteral("%1* %2").arg(pre).arg(QString::fromLatin1(part->type())); + printTree(part,pre + QStringLiteral(" ")); + } + } + private slots: void testTextMail() @@ -43,10 +53,13 @@ private slots: QCOMPARE(contentPart->availableContents(), ContentPart::PlainText); auto contentList = contentPart->content(ContentPart::PlainText); QCOMPARE(contentList.size(), 1); - QCOMPARE(contentList[0]->content(), QStringLiteral("If you can see this text it means that your email client couldn't display our newsletter properly.\nPlease visit this link to view the newsletter on our website: http://www.gog.com/newsletter/\n\n- GOG.com Team\n\n").toLocal8Bit()); + QCOMPARE(contentList[0]->content(), QStringLiteral("If you can see this text it means that your email client couldn't display our newsletter properly.\nPlease visit this link to view the newsletter on our website: http://www.gog.com/newsletter/").toLocal8Bit()); QCOMPARE(contentList[0]->charset(), QStringLiteral("utf-8").toLocal8Bit()); QCOMPARE(contentList[0]->encryptions().size(), 0); QCOMPARE(contentList[0]->signatures().size(), 0); + + contentList = contentPart->content(ContentPart::Html); + QCOMPARE(contentList.size(), 0); } void testTextAlternative() @@ -55,6 +68,19 @@ private slots: auto contentPart = parser.collectContentPart(); QVERIFY((bool)contentPart); QCOMPARE(contentPart->availableContents(), ContentPart::PlainText | ContentPart::Html); + auto contentList = contentPart->content(ContentPart::PlainText); + QCOMPARE(contentList.size(), 1); + QCOMPARE(contentList[0]->content(), QStringLiteral("If you can see this text it means that your email client couldn't display our newsletter properly.\nPlease visit this link to view the newsletter on our website: http://www.gog.com/newsletter/\n").toLocal8Bit()); + QCOMPARE(contentList[0]->charset(), QStringLiteral("utf-8").toLocal8Bit()); + QCOMPARE(contentList[0]->encryptions().size(), 0); + QCOMPARE(contentList[0]->signatures().size(), 0); + + contentList = contentPart->content(ContentPart::Html); + QCOMPARE(contentList.size(), 1); + QCOMPARE(contentList[0]->content(), QStringLiteral("

HTML text

\n\n").toLocal8Bit()); + QCOMPARE(contentList[0]->charset(), QStringLiteral("utf-8").toLocal8Bit()); + QCOMPARE(contentList[0]->encryptions().size(), 0); + QCOMPARE(contentList[0]->signatures().size(), 0); } void testTextHtml() @@ -63,6 +89,55 @@ private slots: auto contentPart = parser.collectContentPart(); QVERIFY((bool)contentPart); QCOMPARE(contentPart->availableContents(), ContentPart::Html); + + auto contentList = contentPart->content(ContentPart::PlainText); + QCOMPARE(contentList.size(), 0); + + contentList = contentPart->content(ContentPart::Html); + QCOMPARE(contentList.size(), 1); + QCOMPARE(contentList[0]->content(), QStringLiteral("

HTML text

").toLocal8Bit()); + QCOMPARE(contentList[0]->charset(), QStringLiteral("utf-8").toLocal8Bit()); + QCOMPARE(contentList[0]->encryptions().size(), 0); + QCOMPARE(contentList[0]->signatures().size(), 0); + } + + void testSMimeEncrypted() + { + Parser parser(readMailFromFile("smime-encrypted.mbox")); + printTree(parser.d->mTree,QString()); + auto contentPart = parser.collectContentPart(); + QVERIFY((bool)contentPart); + QCOMPARE(contentPart->availableContents(), ContentPart::PlainText); + auto contentList = contentPart->content(ContentPart::PlainText); + QCOMPARE(contentList.size(), 1); + QCOMPARE(contentList[0]->content(), QStringLiteral("The quick brown fox jumped over the lazy dog.").toLocal8Bit()); + QCOMPARE(contentList[0]->charset(), QStringLiteral("utf-8").toLocal8Bit()); + } + + void testOpenPGPEncryptedAttachment() + { + Parser parser(readMailFromFile("openpgp-encrypted-attachment-and-non-encrypted-attachment.mbox")); + printTree(parser.d->mTree,QString()); + auto contentPart = parser.collectContentPart(); + QVERIFY((bool)contentPart); + QCOMPARE(contentPart->availableContents(), ContentPart::PlainText); + auto contentList = contentPart->content(ContentPart::PlainText); + QCOMPARE(contentList.size(), 1); + QCOMPARE(contentList[0]->content(), QStringLiteral("test text").toLocal8Bit()); + QCOMPARE(contentList[0]->charset(), QStringLiteral("utf-8").toLocal8Bit()); + } + + void testOpenPPGInline() + { + Parser parser(readMailFromFile("openpgp-inline-charset-encrypted.mbox")); + printTree(parser.d->mTree,QString()); + auto contentPart = parser.collectContentPart(); + QVERIFY((bool)contentPart); + QCOMPARE(contentPart->availableContents(), ContentPart::PlainText); + auto contentList = contentPart->content(ContentPart::PlainText); + QCOMPARE(contentList.size(), 1); + QCOMPARE(contentList[0]->content(), QStringLiteral("asdasd asd asd asdf sadf sdaf sadf äöü").toLocal8Bit()); + QCOMPARE(contentList[0]->charset(), QStringLiteral("utf-8").toLocal8Bit()); } }; -- cgit v1.2.3 From cf5b3e797421e7dbf2c0d7b1efff91fc07277652 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sandro=20Knau=C3=9F?= Date: Wed, 20 Jul 2016 17:35:29 +0200 Subject: new thoughts for interface --- framework/domain/mimetreeparser/CMakeLists.txt | 2 +- framework/domain/mimetreeparser/interface.cpp | 165 ++++++++++------- framework/domain/mimetreeparser/interface.h | 246 ++++++++++++------------- 3 files changed, 220 insertions(+), 193 deletions(-) (limited to 'framework') diff --git a/framework/domain/mimetreeparser/CMakeLists.txt b/framework/domain/mimetreeparser/CMakeLists.txt index e1c04893..07ec28df 100644 --- a/framework/domain/mimetreeparser/CMakeLists.txt +++ b/framework/domain/mimetreeparser/CMakeLists.txt @@ -9,4 +9,4 @@ add_library(mimetreeparser SHARED ${mimetreeparser_SRCS}) qt5_use_modules(mimetreeparser Core Gui) target_link_libraries(mimetreeparser KF5::Mime KF5::MimeTreeParser) -add_subdirectory(tests) \ No newline at end of file +# add_subdirectory(tests) \ No newline at end of file diff --git a/framework/domain/mimetreeparser/interface.cpp b/framework/domain/mimetreeparser/interface.cpp index e34ffda7..aa7e3911 100644 --- a/framework/domain/mimetreeparser/interface.cpp +++ b/framework/domain/mimetreeparser/interface.cpp @@ -89,6 +89,16 @@ QByteArray Part::type() const return "Part"; } +QVector Part::availableContents() const +{ + return QVector(); +} + +QVector Part::content() const +{ + return QVector(); +} + QVector Part::encryptions() const { auto parent = d->parent(); @@ -118,7 +128,7 @@ public: Content *q; }; -Content::Content(const QByteArray& content, ContentPart *parent) +Content::Content(const QByteArray& content, Part *parent) : d(std::unique_ptr(new ContentPrivate)) { d->q = this; @@ -157,113 +167,141 @@ QByteArray Content::charset() const return d->mCodec; } -class ContentPartPrivate +class AlternativePartPrivate { public: - void fillFrom(MimeTreeParser::TextMessagePart::Ptr part); - void fillFrom(MimeTreeParser::HtmlMessagePart::Ptr part); void fillFrom(MimeTreeParser::AlternativeMessagePart::Ptr part); - QVector content(ContentPart::Type ct) const; + QVector content(const QByteArray &ct) const; - ContentPart *q; + AlternativePart *q; - ContentPart::Types types() const; + QVector types() const; private: - QMap> mContent; - ContentPart::Types mTypes; + QMap> mContent; + QVector mTypes; }; -void ContentPartPrivate::fillFrom(MimeTreeParser::TextMessagePart::Ptr part) +void AlternativePartPrivate::fillFrom(MimeTreeParser::AlternativeMessagePart::Ptr part) { - qDebug() << "jepp"; - mTypes = ContentPart::PlainText; - foreach (const auto &mp, part->subParts()) { - auto content = std::make_shared(mp->text().toLocal8Bit(), q); - mContent[ContentPart::PlainText].append(content); - } + mTypes = QVector() << "html" << "plaintext"; + + auto content = std::make_shared(part->htmlContent().toLocal8Bit(), q); + mContent["html"].append(content); + content = std::make_shared(part->plaintextContent().toLocal8Bit(), q); + mContent["plaintext"].append(content); } -void ContentPartPrivate::fillFrom(MimeTreeParser::HtmlMessagePart::Ptr part) +QVector AlternativePartPrivate::types() const { - mTypes = ContentPart::Html; - auto content = std::make_shared(part->text().toLocal8Bit(), q); - mContent[ContentPart::Html].append(content); + return mTypes; } -void ContentPartPrivate::fillFrom(MimeTreeParser::AlternativeMessagePart::Ptr part) +QVector AlternativePartPrivate::content(const QByteArray& ct) const { - mTypes = ContentPart::Html | ContentPart::PlainText; + return mContent[ct]; +} - auto content = std::make_shared(part->htmlContent().toLocal8Bit(), q); - mContent[ContentPart::Html].append(content); - content = std::make_shared(part->plaintextContent().toLocal8Bit(), q); - mContent[ContentPart::PlainText].append(content); +AlternativePart::AlternativePart() + : d(std::unique_ptr(new AlternativePartPrivate)) +{ + d->q = this; } -ContentPart::Types ContentPartPrivate::types() const +AlternativePart::~AlternativePart() { - return mTypes; + } -QVector ContentPartPrivate::content(ContentPart::Type ct) const +QByteArray AlternativePart::type() const { - return mContent[ct]; + return "AlternativePart"; } -QVector ContentPart::content(ContentPart::Type ct) const +QVector AlternativePart::availableContents() const { - return d->content(ct); + return d->types(); } +QVector AlternativePart::content() const +{ + return d->content(availableContents().first()); +} -ContentPart::ContentPart() - : d(std::unique_ptr(new ContentPartPrivate)) +QVector AlternativePart::content(const QByteArray& ct) const { - d->q = this; + return d->content(ct); } -ContentPart::~ContentPart() +class SinglePartPrivate { +public: + void fillFrom(MimeTreeParser::TextMessagePart::Ptr part); + void fillFrom(MimeTreeParser::HtmlMessagePart::Ptr part); + void fillFrom(MimeTreeParser::AttachmentMessagePart::Ptr part); + SinglePart *q; + + QVector mContent; + QByteArray mType; +}; +void SinglePartPrivate::fillFrom(MimeTreeParser::TextMessagePart::Ptr part) +{ + mType = "plaintext"; + mContent.clear(); + foreach (const auto &mp, part->subParts()) { + mContent.append(std::make_shared(mp->text().toLocal8Bit(), q)); + } } -QByteArray ContentPart::type() const +void SinglePartPrivate::fillFrom(MimeTreeParser::HtmlMessagePart::Ptr part) { - return "ContentPart"; + mType = "html"; + mContent.clear(); + mContent.append(std::make_shared(part->text().toLocal8Bit(), q)); } -ContentPart::Types ContentPart::availableContents() const +void SinglePartPrivate::fillFrom(MimeTreeParser::AttachmentMessagePart::Ptr part) { - return d->types(); + } -class MimePartPrivate +SinglePart::SinglePart() + : d(std::unique_ptr(new SinglePartPrivate)) { -public: - void fillFrom(MimeTreeParser::MessagePart::Ptr part); -}; + d->q = this; +} -QByteArray MimePart::type() const +SinglePart::~SinglePart() { - return "MimePart"; + } -class AttachmentPartPrivate +QVector SinglePart::availableContents() const { -public: - void fillFrom(MimeTreeParser::AttachmentMessagePart::Ptr part); -}; + return QVector() << d->mType; +} -void AttachmentPartPrivate::fillFrom(MimeTreeParser::AttachmentMessagePart::Ptr part) +QVector< Content::Ptr > SinglePart::content() const { + return d->mContent; +} +QByteArray SinglePart::type() const +{ + return "SinglePart"; } -QByteArray AttachmentPart::type() const +class MimePartPrivate +{ +public: + void fillFrom(MimeTreeParser::MessagePart::Ptr part); +}; + +QByteArray MimePart::type() const { - return "AttachmentPart"; + return "MimePart"; } ParserPrivate::ParserPrivate(Parser* parser) @@ -309,15 +347,15 @@ void ParserPrivate::createTree(const MimeTreeParser::MessagePart::Ptr &start, co part->d->fillFrom(attachment); mTree->d->appendSubPart(part); } else if (text) { - auto part = std::make_shared(); + auto part = std::make_shared(); part->d->fillFrom(text); mTree->d->appendSubPart(part); } else if (alternative) { - auto part = std::make_shared(); + auto part = std::make_shared(); part->d->fillFrom(alternative); mTree->d->appendSubPart(part); } else if (html) { - auto part = std::make_shared(); + auto part = std::make_shared(); part->d->fillFrom(html); mTree->d->appendSubPart(part); } else { @@ -336,18 +374,9 @@ Parser::~Parser() { } -ContentPart::Ptr Parser::collectContentPart(const Part::Ptr &start) const +QVector Parser::collectContentParts() const { - const auto ret = collect(start, [](const Part::Ptr &p){return p->type() == "ContentPart";}, [](const ContentPart::Ptr &p){return true;}); - if (ret.size() > 0) { - return ret[0]; - }; - return ContentPart::Ptr(); -} - -ContentPart::Ptr Parser::collectContentPart() const -{ - return collectContentPart(d->mTree); + return collect(d->mTree, [](const Part::Ptr &p){return p->availableContents().indexOf("html") > -1 || p->availableContents().indexOf("text") > -1;}, [](const Part::Ptr &p){return true;}); } template @@ -364,4 +393,4 @@ QVector Parser::collect(const Part::Ptr &start, std::function Ptr; - Part(); - virtual QByteArray type() const; + typedef std::shared_ptr Ptr; + /** + * Various possible values for the "Content-Disposition" header. + */ + enum Disposition { + Invalid, ///< Default, invalid value + Inline, ///< inline + Attachment ///< attachment + }; - bool hasSubParts() const; - QVector subParts() const; - Part *parent() const; + // interessting header parts of a KMime::Content + QMimeType mimetype() const; + Disposition disposition() const; + QUrl label() const; + QByteArray cid() const; + QByteArray charset() const; + + // Unique identifier to ecactly this KMime::Content + QByteArray link() const; + + QByteArray content() const; + //Use default charset + QString encodedContent() const; + + // overwrite default charset with given charset + QString encodedContent(QByteArray charset) const; - virtual QVector signatures() const; - virtual QVector encryptions() const; private: - std::unique_ptr d; - friend class ParserPrivate; - friend class PartPrivate; + std::unique_ptr d; }; class Content { public: typedef std::shared_ptr Ptr; - Content(const QByteArray &content, ContentPart *parent); + Content(const QByteArray &content, Part *parent); virtual ~Content(); QByteArray content() const; @@ -95,123 +118,122 @@ public: virtual QVector signatures() const; virtual QVector encryptions() const; + MailMime::Ptr mailMime() const; + virtual QByteArray type() const; private: std::unique_ptr d; }; -/* - * A MessagePart that is based on a KMime::Content - */ -class MimePart : public Part +class PlainTextContent : public Content { public: - typedef std::shared_ptr Ptr; - /** - * Various possible values for the "Content-Disposition" header. - */ - enum Disposition { - Invalid, ///< Default, invalid value - Inline, ///< inline - Attachment ///< attachment - }; + QByteArray type() const Q_DECL_OVERRIDE; +}; - // interessting header parts of a KMime::Content - QMimeType mimetype() const; - Disposition disposition() const; - QUrl label() const; - QByteArray cid() const; - QByteArray charset() const; +class HtmlContent : public Content +{ +public: + QByteArray type() const Q_DECL_OVERRIDE; +}; - // we wanna overrwrite the charset of the content, because some clients set the charset wrong - void setCharset(QByteArray charset); +/* + * importing a cert GpgMe::ImportResult + * checking a cert (if it is a valid cert) + */ - // Unique identifier to ecactly this KMime::Content - QByteArray link() const; +class CertContent : public Content +{ +public: + typedef std::shared_ptr Ptr; - QByteArray content() const; + QByteArray type() const Q_DECL_OVERRIDE; + enum CertType { + Pgp, + SMime + }; - //Use default charset - QString encodedContent() const; + enum CertSubType { + Public, + Private + }; - // overwrite default charset with given charset - QString encodedContent(QByteArray charset) const; + CertType certType() const; + CertSubType certSubType() const; + int keyLength() const; - QByteArray type() const Q_DECL_OVERRIDE; private: - std::unique_ptr d; + std::unique_ptr d; }; -/* - * The main ContentPart - * is MimePart a good parent class? - * do we wanna need parts of the header of the connected KMime::Contents - * usecases: - * - - * for htmlonly it is representating only one MimePart (ok) - * for plaintext only also only one MimePart (ok) - * for alternative, we are represating three messageparts - * - "headers" do we return?, we can use setType to make it possible to select and than return these headers - */ -class ContentPart : public Part +class Part { public: - typedef std::shared_ptr Ptr; - enum Type { - PlainText = 0x0001, - Html = 0x0002 - }; - Q_DECLARE_FLAGS(Types, Type) - - ContentPart(); - virtual ~ContentPart(); - - QVector content(Type ct) const; + typedef std::shared_ptr Ptr; + Part(); + virtual QByteArray type() const; - Types availableContents() const; + virtual QVector availableContents() const; + virtual QVector content() const; - QByteArray type() const Q_DECL_OVERRIDE; + bool hasSubParts() const; + QVector subParts() const; + Part *parent() const; + virtual QVector signatures() const; + virtual QVector encryptions() const; + virtual MailMime::Ptr mailMime() const; private: - std::unique_ptr d; - + std::unique_ptr d; friend class ParserPrivate; + friend class PartPrivate; }; -Q_DECLARE_OPERATORS_FOR_FLAGS(ContentPart::Types); - -class AttachmentPart : public MimePart +class AlternativePart : public Part { public: - typedef std::shared_ptr Ptr; + typedef std::shared_ptr Ptr; + + AlternativePart(); + virtual ~AlternativePart(); + + QVector content() const Q_DECL_OVERRIDE; + QVector availableContents() const Q_DECL_OVERRIDE; + QVector content(const QByteArray& ct) const; + QByteArray type() const Q_DECL_OVERRIDE; private: - std::unique_ptr d; + std::unique_ptr d; friend class ParserPrivate; }; -/* - * Open Questions: - * - How to make the string translateable for multiple clients, so that multiple clients can show same error messages, - * that helps users to understand what is going on ? - * - Does openpgp have translations already? - */ -class EncryptionError +class SinglePart : public Part { -public: - int errorId() const; - QString errorString() const; + public: + typedef std::shared_ptr Ptr; + + SinglePart(); + virtual ~SinglePart(); + + QVector content() const Q_DECL_OVERRIDE; + QVector availableContents() const Q_DECL_OVERRIDE; + + QByteArray type() const Q_DECL_OVERRIDE; +private: + std::unique_ptr d; + + friend class ParserPrivate; }; -class EncryptionPart : public MimePart + +class EncryptionPart : public Part { public: typedef std::shared_ptr Ptr; QByteArray type() const Q_DECL_OVERRIDE; EncryptionError error() const; - private: std::unique_ptr d; }; @@ -222,7 +244,7 @@ private: * from/to... */ -class EncapsulatedPart : public AttachmentPart +class EncapsulatedPart : public SinglePart { public: typedef std::shared_ptr Ptr; @@ -233,33 +255,11 @@ private: std::unique_ptr d; }; -/* - * importing a cert GpgMe::ImportResult - * checking a cert (if it is a valid cert) - */ - -class CertPart : public AttachmentPart +class EncryptionError { public: - typedef std::shared_ptr Ptr; - QByteArray type() const Q_DECL_OVERRIDE; - - enum CertType { - Pgp, - SMime - }; - - enum CertSubType { - Public, - Private - }; - - CertType certType() const; - CertSubType certSubType() const; - int keyLength() const; - -private: - std::unique_ptr d; + int errorId() const; + QString errorString() const; }; class Key @@ -312,10 +312,8 @@ public: Part::Ptr getPart(QUrl url); template QVector collect(const Part::Ptr &start, std::function select, std::function filter) const; - QVector collectAttachments(Part::Ptr start, std::function select, std::function filter) const; - ContentPart::Ptr collectContentPart(Part::Ptr start, std::function select, std::function filter) const; - ContentPart::Ptr collectContentPart(const Part::Ptr& start) const; - ContentPart::Ptr collectContentPart() const; + //QVector collectAttachments(Part::Ptr start, std::function select, std::function filter) const; + QVector collectContentParts() const; //template <> QVector collect() const; //template <> static StatusObject verifySignature(const Signature signature) const; -- cgit v1.2.3 From 903960116eb3329631702723bba11f5463eff573 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sandro=20Knau=C3=9F?= Date: Fri, 22 Jul 2016 13:23:17 +0200 Subject: fixes the build issues --- framework/domain/mimetreeparser/CMakeLists.txt | 2 +- framework/domain/mimetreeparser/interface.cpp | 103 ++++++++++++++------- framework/domain/mimetreeparser/interface.h | 21 +++-- .../domain/mimetreeparser/tests/interfacetest.cpp | 54 ++++++----- 4 files changed, 115 insertions(+), 65 deletions(-) (limited to 'framework') diff --git a/framework/domain/mimetreeparser/CMakeLists.txt b/framework/domain/mimetreeparser/CMakeLists.txt index 07ec28df..e1c04893 100644 --- a/framework/domain/mimetreeparser/CMakeLists.txt +++ b/framework/domain/mimetreeparser/CMakeLists.txt @@ -9,4 +9,4 @@ add_library(mimetreeparser SHARED ${mimetreeparser_SRCS}) qt5_use_modules(mimetreeparser Core Gui) target_link_libraries(mimetreeparser KF5::Mime KF5::MimeTreeParser) -# add_subdirectory(tests) \ No newline at end of file +add_subdirectory(tests) \ No newline at end of file diff --git a/framework/domain/mimetreeparser/interface.cpp b/framework/domain/mimetreeparser/interface.cpp index aa7e3911..5cf36d10 100644 --- a/framework/domain/mimetreeparser/interface.cpp +++ b/framework/domain/mimetreeparser/interface.cpp @@ -141,7 +141,7 @@ Content::~Content() { } -QVector< Encryption > Content::encryptions() const +QVector Content::encryptions() const { if (d->mParent) { return d->mParent->encryptions(); @@ -149,7 +149,7 @@ QVector< Encryption > Content::encryptions() const return QVector(); } -QVector< Signature > Content::signatures() const +QVector Content::signatures() const { if (d->mParent) { return d->mParent->signatures(); @@ -167,6 +167,19 @@ QByteArray Content::charset() const return d->mCodec; } +HtmlContent::HtmlContent(const QByteArray& content, Part* parent) + : Content(content, parent) +{ + +} + +PlainTextContent::PlainTextContent(const QByteArray& content, Part* parent) + : Content(content, parent) +{ + +} + + class AlternativePartPrivate { public: @@ -187,7 +200,7 @@ void AlternativePartPrivate::fillFrom(MimeTreeParser::AlternativeMessagePart::Pt { mTypes = QVector() << "html" << "plaintext"; - auto content = std::make_shared(part->htmlContent().toLocal8Bit(), q); + Content::Ptr content = std::make_shared(part->htmlContent().toLocal8Bit(), q); mContent["html"].append(content); content = std::make_shared(part->plaintextContent().toLocal8Bit(), q); mContent["plaintext"].append(content); @@ -224,11 +237,6 @@ QVector AlternativePart::availableContents() const return d->types(); } -QVector AlternativePart::content() const -{ - return d->content(availableContents().first()); -} - QVector AlternativePart::content(const QByteArray& ct) const { return d->content(ct); @@ -283,9 +291,12 @@ QVector SinglePart::availableContents() const return QVector() << d->mType; } -QVector< Content::Ptr > SinglePart::content() const +QVector< Content::Ptr > SinglePart::content(const QByteArray &ct) const { - return d->mContent; + if (ct == d->mType) { + return d->mContent; + } + return QVector(); } QByteArray SinglePart::type() const @@ -293,17 +304,6 @@ QByteArray SinglePart::type() const return "SinglePart"; } -class MimePartPrivate -{ -public: - void fillFrom(MimeTreeParser::MessagePart::Ptr part); -}; - -QByteArray MimePart::type() const -{ - return "MimePart"; -} - ParserPrivate::ParserPrivate(Parser* parser) : q(parser) , mNodeHelper(std::make_shared()) @@ -343,7 +343,7 @@ void ParserPrivate::createTree(const MimeTreeParser::MessagePart::Ptr &start, co const auto html = mp.dynamicCast(); const auto attachment = mp.dynamicCast(); if (attachment) { - auto part = std::make_shared(); + auto part = std::make_shared(); part->d->fillFrom(attachment); mTree->d->appendSubPart(part); } else if (text) { @@ -376,20 +376,55 @@ Parser::~Parser() QVector Parser::collectContentParts() const { - return collect(d->mTree, [](const Part::Ptr &p){return p->availableContents().indexOf("html") > -1 || p->availableContents().indexOf("text") > -1;}, [](const Part::Ptr &p){return true;}); -} - -template -QVector Parser::collect(const Part::Ptr &start, std::function select, std::function filter) const -{ - QVector ret; + return collect(d->mTree, [](const Part::Ptr &p){return p->type() != "EncapsulatedPart";}, + [](const Content::Ptr &content){ + const auto mime = content->mailMime(); + + if (!mime) { + return true; + } + + if (mime->isFirstTextPart()) { + return true; + } + const auto cd = mime->disposition(); + if (cd && cd == MailMime::Inline) { + // explict "inline" disposition: + return true; + } + if (cd && cd == MailMime::Attachment) { + // explicit "attachment" disposition: + return false; + } + + const auto ct = mime->mimetype(); + if (ct.name().trimmed().toLower() == "text" && ct.name().trimmed().isEmpty() && + (!mime || mime->filename().trimmed().isEmpty())) { + // text/* w/o filename parameter: + return true; + } + return false; + }); +} + +QVector Parser::collect(const Part::Ptr &start, std::function select, std::function filter) const +{ + QVector ret; foreach (const auto &part, start->subParts()) { - if (select(part)){ - const auto p = std::dynamic_pointer_cast(part); - if (p && filter(p)) { - ret.append(p); + QVector contents; + foreach(const auto &ct, part->availableContents()) { + foreach(const auto &content, part->content(ct)) { + if (filter(content)) { + contents.append(ct); + break; + } } - ret += collect(part, select, filter); + } + if (!contents.isEmpty()) { + ret.append(part); + } + if (select(part)){ + ret += collect(part, select, filter); } } return ret; diff --git a/framework/domain/mimetreeparser/interface.h b/framework/domain/mimetreeparser/interface.h index 5133b87e..f6ee41ee 100644 --- a/framework/domain/mimetreeparser/interface.h +++ b/framework/domain/mimetreeparser/interface.h @@ -41,9 +41,6 @@ class SinglePartPrivate; class EncryptionPart; class EncryptionPartPrivate; -class AttachmentPart; -class AttachmentPartPrivate; - class EncapsulatedPart; class EncapsulatedPartPrivate; @@ -84,6 +81,7 @@ public: QUrl label() const; QByteArray cid() const; QByteArray charset() const; + QByteArray filename() const; // Unique identifier to ecactly this KMime::Content QByteArray link() const; @@ -95,6 +93,8 @@ public: // overwrite default charset with given charset QString encodedContent(QByteArray charset) const; + bool isFirstTextPart() const; + private: std::unique_ptr d; }; @@ -127,12 +127,14 @@ private: class PlainTextContent : public Content { public: + PlainTextContent(const QByteArray &content, Part *parent); QByteArray type() const Q_DECL_OVERRIDE; }; class HtmlContent : public Content { public: + HtmlContent(const QByteArray &content, Part *parent); QByteArray type() const Q_DECL_OVERRIDE; }; @@ -145,6 +147,7 @@ class CertContent : public Content { public: typedef std::shared_ptr Ptr; + CertContent(const QByteArray &content, Part *parent); QByteArray type() const Q_DECL_OVERRIDE; enum CertType { @@ -173,7 +176,8 @@ public: virtual QByteArray type() const; virtual QVector availableContents() const; - virtual QVector content() const; + virtual QVector content(const QByteArray& ct) const; + QVector content() const; bool hasSubParts() const; QVector subParts() const; @@ -196,9 +200,8 @@ public: AlternativePart(); virtual ~AlternativePart(); - QVector content() const Q_DECL_OVERRIDE; QVector availableContents() const Q_DECL_OVERRIDE; - QVector content(const QByteArray& ct) const; + QVector content(const QByteArray& ct) const Q_DECL_OVERRIDE; QByteArray type() const Q_DECL_OVERRIDE; @@ -216,7 +219,7 @@ class SinglePart : public Part SinglePart(); virtual ~SinglePart(); - QVector content() const Q_DECL_OVERRIDE; + QVector content(const QByteArray& ct) const Q_DECL_OVERRIDE; QVector availableContents() const Q_DECL_OVERRIDE; QByteArray type() const Q_DECL_OVERRIDE; @@ -311,9 +314,9 @@ public: Part::Ptr getPart(QUrl url); - template QVector collect(const Part::Ptr &start, std::function select, std::function filter) const; - //QVector collectAttachments(Part::Ptr start, std::function select, std::function filter) const; + QVector collect(const Part::Ptr &start, std::function select, std::function filter) const; QVector collectContentParts() const; + QVector collectAttachmentParts() const; //template <> QVector collect() const; //template <> static StatusObject verifySignature(const Signature signature) const; diff --git a/framework/domain/mimetreeparser/tests/interfacetest.cpp b/framework/domain/mimetreeparser/tests/interfacetest.cpp index 88691539..822d530c 100644 --- a/framework/domain/mimetreeparser/tests/interfacetest.cpp +++ b/framework/domain/mimetreeparser/tests/interfacetest.cpp @@ -48,34 +48,38 @@ private slots: void testTextMail() { Parser parser(readMailFromFile("plaintext.mbox")); - auto contentPart = parser.collectContentPart(); + auto contentPartList = parser.collectContentParts(); + QCOMPARE(contentPartList.size(), 1); + auto contentPart = contentPartList[0]; QVERIFY((bool)contentPart); - QCOMPARE(contentPart->availableContents(), ContentPart::PlainText); - auto contentList = contentPart->content(ContentPart::PlainText); + QCOMPARE(contentPart->availableContents(), "plaintext"); + auto contentList = contentPart->content("plaintext"); QCOMPARE(contentList.size(), 1); QCOMPARE(contentList[0]->content(), QStringLiteral("If you can see this text it means that your email client couldn't display our newsletter properly.\nPlease visit this link to view the newsletter on our website: http://www.gog.com/newsletter/").toLocal8Bit()); QCOMPARE(contentList[0]->charset(), QStringLiteral("utf-8").toLocal8Bit()); QCOMPARE(contentList[0]->encryptions().size(), 0); QCOMPARE(contentList[0]->signatures().size(), 0); - contentList = contentPart->content(ContentPart::Html); + contentList = contentPart->content("html"); QCOMPARE(contentList.size(), 0); } void testTextAlternative() { Parser parser(readMailFromFile("alternative.mbox")); - auto contentPart = parser.collectContentPart(); + auto contentPartList = parser.collectContentParts(); + QCOMPARE(contentPartList.size(), 1); + auto contentPart = contentPartList[0]; QVERIFY((bool)contentPart); - QCOMPARE(contentPart->availableContents(), ContentPart::PlainText | ContentPart::Html); - auto contentList = contentPart->content(ContentPart::PlainText); + QCOMPARE(contentPart->availableContents(), QVector() << "html" << "plaintext"); + auto contentList = contentPart->content("plaintext"); QCOMPARE(contentList.size(), 1); QCOMPARE(contentList[0]->content(), QStringLiteral("If you can see this text it means that your email client couldn't display our newsletter properly.\nPlease visit this link to view the newsletter on our website: http://www.gog.com/newsletter/\n").toLocal8Bit()); QCOMPARE(contentList[0]->charset(), QStringLiteral("utf-8").toLocal8Bit()); QCOMPARE(contentList[0]->encryptions().size(), 0); QCOMPARE(contentList[0]->signatures().size(), 0); - contentList = contentPart->content(ContentPart::Html); + contentList = contentPart->content("html"); QCOMPARE(contentList.size(), 1); QCOMPARE(contentList[0]->content(), QStringLiteral("

HTML text

\n\n").toLocal8Bit()); QCOMPARE(contentList[0]->charset(), QStringLiteral("utf-8").toLocal8Bit()); @@ -86,14 +90,16 @@ private slots: void testTextHtml() { Parser parser(readMailFromFile("html.mbox")); - auto contentPart = parser.collectContentPart(); + auto contentPartList = parser.collectContentParts(); + QCOMPARE(contentPartList.size(), 1); + auto contentPart = contentPartList[0]; QVERIFY((bool)contentPart); - QCOMPARE(contentPart->availableContents(), ContentPart::Html); + QCOMPARE(contentPart->availableContents(), "html"); - auto contentList = contentPart->content(ContentPart::PlainText); + auto contentList = contentPart->content("plaintext"); QCOMPARE(contentList.size(), 0); - contentList = contentPart->content(ContentPart::Html); + contentList = contentPart->content("html"); QCOMPARE(contentList.size(), 1); QCOMPARE(contentList[0]->content(), QStringLiteral("

HTML text

").toLocal8Bit()); QCOMPARE(contentList[0]->charset(), QStringLiteral("utf-8").toLocal8Bit()); @@ -105,10 +111,12 @@ private slots: { Parser parser(readMailFromFile("smime-encrypted.mbox")); printTree(parser.d->mTree,QString()); - auto contentPart = parser.collectContentPart(); + auto contentPartList = parser.collectContentParts(); + QCOMPARE(contentPartList.size(), 1); + auto contentPart = contentPartList[0]; QVERIFY((bool)contentPart); - QCOMPARE(contentPart->availableContents(), ContentPart::PlainText); - auto contentList = contentPart->content(ContentPart::PlainText); + QCOMPARE(contentPart->availableContents(), "plaintext"); + auto contentList = contentPart->content("plaintext"); QCOMPARE(contentList.size(), 1); QCOMPARE(contentList[0]->content(), QStringLiteral("The quick brown fox jumped over the lazy dog.").toLocal8Bit()); QCOMPARE(contentList[0]->charset(), QStringLiteral("utf-8").toLocal8Bit()); @@ -118,10 +126,12 @@ private slots: { Parser parser(readMailFromFile("openpgp-encrypted-attachment-and-non-encrypted-attachment.mbox")); printTree(parser.d->mTree,QString()); - auto contentPart = parser.collectContentPart(); + auto contentPartList = parser.collectContentParts(); + QCOMPARE(contentPartList.size(), 1); + auto contentPart = contentPartList[0]; QVERIFY((bool)contentPart); - QCOMPARE(contentPart->availableContents(), ContentPart::PlainText); - auto contentList = contentPart->content(ContentPart::PlainText); + QCOMPARE(contentPart->availableContents(), "plaintext"); + auto contentList = contentPart->content("plaintext"); QCOMPARE(contentList.size(), 1); QCOMPARE(contentList[0]->content(), QStringLiteral("test text").toLocal8Bit()); QCOMPARE(contentList[0]->charset(), QStringLiteral("utf-8").toLocal8Bit()); @@ -131,10 +141,12 @@ private slots: { Parser parser(readMailFromFile("openpgp-inline-charset-encrypted.mbox")); printTree(parser.d->mTree,QString()); - auto contentPart = parser.collectContentPart(); + auto contentPartList = parser.collectContentParts(); + QCOMPARE(contentPartList.size(), 1); + auto contentPart = contentPartList[0]; QVERIFY((bool)contentPart); - QCOMPARE(contentPart->availableContents(), ContentPart::PlainText); - auto contentList = contentPart->content(ContentPart::PlainText); + QCOMPARE(contentPart->availableContents(), "plaintext"); + auto contentList = contentPart->content("plaintext"); QCOMPARE(contentList.size(), 1); QCOMPARE(contentList[0]->content(), QStringLiteral("asdasd asd asd asdf sadf sdaf sadf äöü").toLocal8Bit()); QCOMPARE(contentList[0]->charset(), QStringLiteral("utf-8").toLocal8Bit()); -- cgit v1.2.3 From 0272f1e8bb7f8c86c86958e3e022aac8b34a266c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sandro=20Knau=C3=9F?= Date: Tue, 26 Jul 2016 13:52:06 +0200 Subject: make the tests compile again --- framework/domain/mimetreeparser/interface.cpp | 107 +++++++++++++++++++++ framework/domain/mimetreeparser/interface.h | 4 +- .../domain/mimetreeparser/tests/interfacetest.cpp | 10 +- 3 files changed, 115 insertions(+), 6 deletions(-) (limited to 'framework') diff --git a/framework/domain/mimetreeparser/interface.cpp b/framework/domain/mimetreeparser/interface.cpp index 5cf36d10..a76a6cde 100644 --- a/framework/domain/mimetreeparser/interface.cpp +++ b/framework/domain/mimetreeparser/interface.cpp @@ -28,8 +28,77 @@ #include #include +#include +#include #include +class MailMimePrivate +{ +public: + KMime::Content *mNode; + MailMime *q; +}; + +MailMime::MailMime() + : d(std::unique_ptr(new MailMimePrivate())) +{ + d->q = this; +} + +bool MailMime::isFirstTextPart() const +{ + if (!d->mNode) { + return false; + } + return (d->mNode->topLevel()->textContent() == d->mNode); +} + +MailMime::Disposition MailMime::disposition() const +{ + if (!d->mNode) { + return Invalid; + } + const auto cd = d->mNode->contentDisposition(false); + if (!cd) { + return Invalid; + } + switch (cd->disposition()){ + case KMime::Headers::CDinline: + return Inline; + case KMime::Headers::CDattachment: + return Attachment; + default: + return Invalid; + } +} + +QString MailMime::filename() const +{ + if (!d->mNode) { + return QString(); + } + const auto cd = d->mNode->contentDisposition(false); + if (!cd) { + return QString(); + } + return cd->filename(); +} + +QMimeType MailMime::mimetype() const +{ + if (!d->mNode) { + return QMimeType(); + } + + const auto ct = d->mNode->contentType(false); + if (!ct) { + return QMimeType(); + } + + QMimeDatabase mimeDb; + return mimeDb.mimeTypeForName(ct->mimeType()); +} + class PartPrivate { public: @@ -39,10 +108,13 @@ public: QVector subParts(); Part *parent() const; + + const MailMime::Ptr &mailMime() const; private: Part *q; Part *mParent; QVector mSubParts; + MailMime::Ptr mMailMime; }; PartPrivate::PartPrivate(Part* part) @@ -68,6 +140,11 @@ QVector< Part::Ptr > PartPrivate::subParts() return mSubParts; } +const MailMime::Ptr& PartPrivate::mailMime() const +{ + return mMailMime; +} + Part::Part() : d(std::unique_ptr(new PartPrivate(this))) { @@ -95,6 +172,11 @@ QVector Part::availableContents() const } QVector Part::content() const +{ + return content(availableContents().first()); +} + +QVector Part::content(const QByteArray& ct) const { return QVector(); } @@ -119,6 +201,11 @@ QVector Part::signatures() const } } +MailMime::Ptr Part::mailMime() const +{ + return d->mailMime(); +} + class ContentPrivate { public: @@ -126,6 +213,7 @@ public: QByteArray mCodec; Part *mParent; Content *q; + MailMime::Ptr mMailMime; }; Content::Content(const QByteArray& content, Part *parent) @@ -167,18 +255,37 @@ QByteArray Content::charset() const return d->mCodec; } +QByteArray Content::type() const +{ + return "Content"; +} + +MailMime::Ptr Content::mailMime() const +{ + return d->mMailMime; +} + HtmlContent::HtmlContent(const QByteArray& content, Part* parent) : Content(content, parent) { } +QByteArray HtmlContent::type() const +{ + return "HtmlContent"; +} + PlainTextContent::PlainTextContent(const QByteArray& content, Part* parent) : Content(content, parent) { } +QByteArray PlainTextContent::type() const +{ + return "PlainTextContent"; +} class AlternativePartPrivate { diff --git a/framework/domain/mimetreeparser/interface.h b/framework/domain/mimetreeparser/interface.h index f6ee41ee..c71b86d6 100644 --- a/framework/domain/mimetreeparser/interface.h +++ b/framework/domain/mimetreeparser/interface.h @@ -75,13 +75,15 @@ public: Attachment ///< attachment }; + MailMime(); + // interessting header parts of a KMime::Content QMimeType mimetype() const; Disposition disposition() const; QUrl label() const; QByteArray cid() const; QByteArray charset() const; - QByteArray filename() const; + QString filename() const; // Unique identifier to ecactly this KMime::Content QByteArray link() const; diff --git a/framework/domain/mimetreeparser/tests/interfacetest.cpp b/framework/domain/mimetreeparser/tests/interfacetest.cpp index 822d530c..83de97f7 100644 --- a/framework/domain/mimetreeparser/tests/interfacetest.cpp +++ b/framework/domain/mimetreeparser/tests/interfacetest.cpp @@ -52,7 +52,7 @@ private slots: QCOMPARE(contentPartList.size(), 1); auto contentPart = contentPartList[0]; QVERIFY((bool)contentPart); - QCOMPARE(contentPart->availableContents(), "plaintext"); + QCOMPARE(contentPart->availableContents(), QVector() << "plaintext"); auto contentList = contentPart->content("plaintext"); QCOMPARE(contentList.size(), 1); QCOMPARE(contentList[0]->content(), QStringLiteral("If you can see this text it means that your email client couldn't display our newsletter properly.\nPlease visit this link to view the newsletter on our website: http://www.gog.com/newsletter/").toLocal8Bit()); @@ -94,7 +94,7 @@ private slots: QCOMPARE(contentPartList.size(), 1); auto contentPart = contentPartList[0]; QVERIFY((bool)contentPart); - QCOMPARE(contentPart->availableContents(), "html"); + QCOMPARE(contentPart->availableContents(), QVector() << "html"); auto contentList = contentPart->content("plaintext"); QCOMPARE(contentList.size(), 0); @@ -115,7 +115,7 @@ private slots: QCOMPARE(contentPartList.size(), 1); auto contentPart = contentPartList[0]; QVERIFY((bool)contentPart); - QCOMPARE(contentPart->availableContents(), "plaintext"); + QCOMPARE(contentPart->availableContents(), QVector() << "plaintext"); auto contentList = contentPart->content("plaintext"); QCOMPARE(contentList.size(), 1); QCOMPARE(contentList[0]->content(), QStringLiteral("The quick brown fox jumped over the lazy dog.").toLocal8Bit()); @@ -130,7 +130,7 @@ private slots: QCOMPARE(contentPartList.size(), 1); auto contentPart = contentPartList[0]; QVERIFY((bool)contentPart); - QCOMPARE(contentPart->availableContents(), "plaintext"); + QCOMPARE(contentPart->availableContents(), QVector() << "plaintext"); auto contentList = contentPart->content("plaintext"); QCOMPARE(contentList.size(), 1); QCOMPARE(contentList[0]->content(), QStringLiteral("test text").toLocal8Bit()); @@ -145,7 +145,7 @@ private slots: QCOMPARE(contentPartList.size(), 1); auto contentPart = contentPartList[0]; QVERIFY((bool)contentPart); - QCOMPARE(contentPart->availableContents(), "plaintext"); + QCOMPARE(contentPart->availableContents(), QVector() << "plaintext"); auto contentList = contentPart->content("plaintext"); QCOMPARE(contentList.size(), 1); QCOMPARE(contentList[0]->content(), QStringLiteral("asdasd asd asd asdf sadf sdaf sadf äöü").toLocal8Bit()); -- cgit v1.2.3 From 0f67cb5f9851af4e444c41d9f7b8d88350ec2e09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sandro=20Knau=C3=9F?= Date: Tue, 26 Jul 2016 16:01:46 +0200 Subject: Adopt changes in messagelib interface --- .../domain/mimetreeparser/objecttreesource.cpp | 22 +++++++++------------- framework/domain/mimetreeparser/objecttreesource.h | 5 ++--- 2 files changed, 11 insertions(+), 16 deletions(-) (limited to 'framework') diff --git a/framework/domain/mimetreeparser/objecttreesource.cpp b/framework/domain/mimetreeparser/objecttreesource.cpp index 12cf88ab..58667444 100644 --- a/framework/domain/mimetreeparser/objecttreesource.cpp +++ b/framework/domain/mimetreeparser/objecttreesource.cpp @@ -31,7 +31,7 @@ public: : mWriter(0) , mAllowDecryption(true) , mHtmlLoadExternal(true) - , mHtmlMail(true) + , mPreferredMode(MimeTreeParser::Util::Html) { } @@ -39,7 +39,7 @@ public: MimeTreeParser::BodyPartFormatterBaseFactory mBodyPartFormatterBaseFactory; bool mAllowDecryption; bool mHtmlLoadExternal; - bool mHtmlMail; + MimeTreeParser::Util::HtmlMode mPreferredMode; }; ObjectTreeSource::ObjectTreeSource(MimeTreeParser::HtmlWriter *writer) @@ -74,16 +74,6 @@ void ObjectTreeSource::setHtmlLoadExternal(bool loadExternal) d->mHtmlLoadExternal = loadExternal; } -bool ObjectTreeSource::htmlMail() const -{ - return d->mHtmlMail; -} - -void ObjectTreeSource::setHtmlMail(bool htmlMail) -{ - d->mHtmlMail = htmlMail; -} - bool ObjectTreeSource::decryptMessage() const { return d->mAllowDecryption; @@ -119,9 +109,15 @@ QObject *ObjectTreeSource::sourceObject() return Q_NULLPTR; } -void ObjectTreeSource::setHtmlMode(MimeTreeParser::Util::HtmlMode mode) +void ObjectTreeSource::setHtmlMode(MimeTreeParser::Util::HtmlMode mode, const QList &availableModes) { Q_UNUSED(mode); + Q_UNUSED(availableModes); +} + +MimeTreeParser::Util::HtmlMode ObjectTreeSource::preferredMode() const +{ + return d->mPreferredMode; } bool ObjectTreeSource::autoImportKeys() const diff --git a/framework/domain/mimetreeparser/objecttreesource.h b/framework/domain/mimetreeparser/objecttreesource.h index bb0cd679..42433e71 100644 --- a/framework/domain/mimetreeparser/objecttreesource.h +++ b/framework/domain/mimetreeparser/objecttreesource.h @@ -31,12 +31,11 @@ public: ObjectTreeSource(MimeTreeParser::HtmlWriter *writer); virtual ~ObjectTreeSource(); void setHtmlLoadExternal(bool loadExternal); - void setHtmlMail(bool htmlMail); - bool htmlMail() const Q_DECL_OVERRIDE; bool decryptMessage() const Q_DECL_OVERRIDE; bool htmlLoadExternal() const Q_DECL_OVERRIDE; bool showSignatureDetails() const Q_DECL_OVERRIDE; - void setHtmlMode(MimeTreeParser::Util::HtmlMode mode) Q_DECL_OVERRIDE; + void setHtmlMode(MimeTreeParser::Util::HtmlMode mode, const QList &availableModes) Q_DECL_OVERRIDE; + MimeTreeParser::Util::HtmlMode preferredMode() const Q_DECL_OVERRIDE; void setAllowDecryption(bool allowDecryption); int levelQuote() const Q_DECL_OVERRIDE; const QTextCodec *overrideCodec() Q_DECL_OVERRIDE; -- cgit v1.2.3 From a34e14a57c7726a99e63d767935379cba1ff6ea2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sandro=20Knau=C3=9F?= Date: Tue, 2 Aug 2016 09:32:12 +0200 Subject: Implement the interface --- framework/domain/mimetreeparser/interface.cpp | 83 ++++++++++++++++++++++++--- framework/domain/mimetreeparser/interface.h | 13 ++++- framework/domain/mimetreeparser/interface_p.h | 7 +++ 3 files changed, 94 insertions(+), 9 deletions(-) (limited to 'framework') diff --git a/framework/domain/mimetreeparser/interface.cpp b/framework/domain/mimetreeparser/interface.cpp index a76a6cde..4f45b883 100644 --- a/framework/domain/mimetreeparser/interface.cpp +++ b/framework/domain/mimetreeparser/interface.cpp @@ -47,12 +47,20 @@ MailMime::MailMime() bool MailMime::isFirstTextPart() const { - if (!d->mNode) { + if (!d->mNode || !d->mNode->topLevel()) { return false; } return (d->mNode->topLevel()->textContent() == d->mNode); } +bool MailMime::isTopLevelPart() const +{ + if (!d->mNode) { + return false; + } + return (d->mNode->topLevel() == d->mNode); +} + MailMime::Disposition MailMime::disposition() const { if (!d->mNode) { @@ -110,6 +118,10 @@ public: Part *parent() const; const MailMime::Ptr &mailMime() const; + void createMailMime(const MimeTreeParser::MimeMessagePart::Ptr &part); + void createMailMime(const MimeTreeParser::TextMessagePart::Ptr &part); + void createMailMime(const MimeTreeParser::AlternativeMessagePart::Ptr &part); + void createMailMime(const MimeTreeParser::HtmlMessagePart::Ptr &part); private: Part *q; Part *mParent; @@ -124,6 +136,30 @@ PartPrivate::PartPrivate(Part* part) } +void PartPrivate::createMailMime(const MimeTreeParser::HtmlMessagePart::Ptr& part) +{ + mMailMime = MailMime::Ptr(new MailMime); + mMailMime->d->mNode = part->mNode; +} + +void PartPrivate::createMailMime(const MimeTreeParser::AlternativeMessagePart::Ptr& part) +{ + mMailMime = MailMime::Ptr(new MailMime); + mMailMime->d->mNode = part->mNode; +} + +void PartPrivate::createMailMime(const MimeTreeParser::TextMessagePart::Ptr& part) +{ + mMailMime = MailMime::Ptr(new MailMime); + mMailMime->d->mNode = part->mNode; +} + +void PartPrivate::createMailMime(const MimeTreeParser::MimeMessagePart::Ptr& part) +{ + mMailMime = MailMime::Ptr(new MailMime); + mMailMime->d->mNode = part->mNode; +} + void PartPrivate::appendSubPart(Part::Ptr subpart) { subpart->d->mParent = q; @@ -262,7 +298,16 @@ QByteArray Content::type() const MailMime::Ptr Content::mailMime() const { - return d->mMailMime; + if (d->mMailMime) { + return d->mMailMime; + } else { + return d->mParent->mailMime(); + } +} + +Part *Content::parent() const +{ + return d->mParent; } HtmlContent::HtmlContent(const QByteArray& content, Part* parent) @@ -311,6 +356,7 @@ void AlternativePartPrivate::fillFrom(MimeTreeParser::AlternativeMessagePart::Pt mContent["html"].append(content); content = std::make_shared(part->plaintextContent().toLocal8Bit(), q); mContent["plaintext"].append(content); + q->reachParentD()->createMailMime(part); } QVector AlternativePartPrivate::types() const @@ -349,6 +395,11 @@ QVector AlternativePart::content(const QByteArray& ct) const return d->content(ct); } +PartPrivate* AlternativePart::reachParentD() const +{ + return Part::d.get(); +} + class SinglePartPrivate { public: @@ -367,6 +418,7 @@ void SinglePartPrivate::fillFrom(MimeTreeParser::TextMessagePart::Ptr part) mContent.clear(); foreach (const auto &mp, part->subParts()) { mContent.append(std::make_shared(mp->text().toLocal8Bit(), q)); + q->reachParentD()->createMailMime(part); } } @@ -375,11 +427,12 @@ void SinglePartPrivate::fillFrom(MimeTreeParser::HtmlMessagePart::Ptr part) mType = "html"; mContent.clear(); mContent.append(std::make_shared(part->text().toLocal8Bit(), q)); + q->reachParentD()->createMailMime(part); } void SinglePartPrivate::fillFrom(MimeTreeParser::AttachmentMessagePart::Ptr part) { - + q->reachParentD()->createMailMime(part.staticCast()); } SinglePart::SinglePart() @@ -411,6 +464,11 @@ QByteArray SinglePart::type() const return "SinglePart"; } +PartPrivate* SinglePart::reachParentD() const +{ + return Part::d.get(); +} + ParserPrivate::ParserPrivate(Parser* parser) : q(parser) , mNodeHelper(std::make_shared()) @@ -421,16 +479,16 @@ ParserPrivate::ParserPrivate(Parser* parser) void ParserPrivate::setMessage(const QByteArray& mimeMessage) { const auto mailData = KMime::CRLFtoLF(mimeMessage); - KMime::Message::Ptr msg(new KMime::Message); - msg->setContent(mailData); - msg->parse(); + mMsg = KMime::Message::Ptr(new KMime::Message); + mMsg->setContent(mailData); + mMsg->parse(); // render the mail StringHtmlWriter htmlWriter; ObjectTreeSource source(&htmlWriter); MimeTreeParser::ObjectTreeParser otp(&source, mNodeHelper.get()); - otp.parseObjectTree(msg.data()); + otp.parseObjectTree(mMsg.data()); mPartTree = otp.parsedPart().dynamicCast(); mEmbeddedPartMap = htmlWriter.embeddedParts(); @@ -494,6 +552,16 @@ QVector Parser::collectContentParts() const if (mime->isFirstTextPart()) { return true; } + + { + const auto parent = content->parent(); + if (parent) { + const auto _mime = parent->mailMime(); + if (_mime && (_mime->isTopLevelPart() || _mime->isFirstTextPart())) { + return true; + } + } + } const auto cd = mime->disposition(); if (cd && cd == MailMime::Inline) { // explict "inline" disposition: @@ -526,6 +594,7 @@ QVector Parser::collect(const Part::Ptr &start, std::function d; + + friend class PartPrivate; }; class Content @@ -122,6 +125,7 @@ public: virtual QVector encryptions() const; MailMime::Ptr mailMime() const; virtual QByteArray type() const; + Part* parent() const; private: std::unique_ptr d; }; @@ -188,8 +192,9 @@ public: virtual QVector signatures() const; virtual QVector encryptions() const; virtual MailMime::Ptr mailMime() const; -private: +protected: std::unique_ptr d; +private: friend class ParserPrivate; friend class PartPrivate; }; @@ -208,9 +213,11 @@ public: QByteArray type() const Q_DECL_OVERRIDE; private: + PartPrivate *reachParentD() const; std::unique_ptr d; friend class ParserPrivate; + friend class AlternativePartPrivate; }; class SinglePart : public Part @@ -226,9 +233,11 @@ class SinglePart : public Part QByteArray type() const Q_DECL_OVERRIDE; private: + PartPrivate *reachParentD() const; std::unique_ptr d; - friend class ParserPrivate; + friend class ParserPrivate; + friend class SinglePartPrivate; }; diff --git a/framework/domain/mimetreeparser/interface_p.h b/framework/domain/mimetreeparser/interface_p.h index f83af6eb..004a50d0 100644 --- a/framework/domain/mimetreeparser/interface_p.h +++ b/framework/domain/mimetreeparser/interface_p.h @@ -24,6 +24,12 @@ #include #include +namespace KMime +{ + class Message; + typedef QSharedPointer MessagePtr; +} + namespace MimeTreeParser { class MessagePart; @@ -44,6 +50,7 @@ private: Parser *q; MimeTreeParser::MessagePartPtr mPartTree; + KMime::MessagePtr mMsg; std::shared_ptr mNodeHelper; QString mHtml; QMap mEmbeddedPartMap; -- cgit v1.2.3 From c64288bfe549ccc95eb6b887f3b803b397ae412c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sandro=20Knau=C3=9F?= Date: Tue, 2 Aug 2016 09:33:02 +0200 Subject: Make encrypted tests pass automatically --- .../domain/mimetreeparser/tests/CMakeLists.txt | 6 +- .../mimetreeparser/tests/gnupg_home/CMakeLists.txt | 10 + .../tests/gnupg_home/dirmngr-cache.d/DIR.txt | 3 + ...crl-4E31CEB57DDD4A7B9991AB05507B1ED4293FF952.db | Bin 0 -> 2130 bytes ...crl-7F2A402CBB016A9146D613568C89D3596A4111AA.db | Bin 0 -> 2048 bytes .../mimetreeparser/tests/gnupg_home/dirmngr.conf | 8 + .../tests/gnupg_home/gpg-agent.conf.in | 10 + .../mimetreeparser/tests/gnupg_home/gpg.conf | 244 +++++++++++++++++++++ .../mimetreeparser/tests/gnupg_home/gpgsm.conf.in | 10 + .../tests/gnupg_home/pinentry-fake.sh | 9 + .../1AA8BA52430E51AE249AF0DA97D59F869E4101A8.key | Bin 0 -> 528 bytes .../mimetreeparser/tests/gnupg_home/pubring.gpg | Bin 0 -> 6757 bytes .../mimetreeparser/tests/gnupg_home/pubring.kbx | Bin 0 -> 2017 bytes .../mimetreeparser/tests/gnupg_home/scdaemon.conf | 8 + .../mimetreeparser/tests/gnupg_home/secring.gpg | Bin 0 -> 5163 bytes .../mimetreeparser/tests/gnupg_home/trustdb.gpg | Bin 0 -> 1440 bytes .../mimetreeparser/tests/gnupg_home/trustlist.txt | 11 + .../domain/mimetreeparser/tests/interfacetest.cpp | 21 +- .../tests/kdepim_add_gpg_crypto_test.cmake | 60 +++++ .../kdepim_generate_crypto_test_wrapper.cmake | 45 ++++ 20 files changed, 442 insertions(+), 3 deletions(-) create mode 100644 framework/domain/mimetreeparser/tests/gnupg_home/CMakeLists.txt create mode 100644 framework/domain/mimetreeparser/tests/gnupg_home/dirmngr-cache.d/DIR.txt create mode 100644 framework/domain/mimetreeparser/tests/gnupg_home/dirmngr-cache.d/crl-4E31CEB57DDD4A7B9991AB05507B1ED4293FF952.db create mode 100644 framework/domain/mimetreeparser/tests/gnupg_home/dirmngr-cache.d/crl-7F2A402CBB016A9146D613568C89D3596A4111AA.db create mode 100644 framework/domain/mimetreeparser/tests/gnupg_home/dirmngr.conf create mode 100644 framework/domain/mimetreeparser/tests/gnupg_home/gpg-agent.conf.in create mode 100644 framework/domain/mimetreeparser/tests/gnupg_home/gpg.conf create mode 100644 framework/domain/mimetreeparser/tests/gnupg_home/gpgsm.conf.in create mode 100755 framework/domain/mimetreeparser/tests/gnupg_home/pinentry-fake.sh create mode 100644 framework/domain/mimetreeparser/tests/gnupg_home/private-keys-v1.d/1AA8BA52430E51AE249AF0DA97D59F869E4101A8.key create mode 100644 framework/domain/mimetreeparser/tests/gnupg_home/pubring.gpg create mode 100644 framework/domain/mimetreeparser/tests/gnupg_home/pubring.kbx create mode 100644 framework/domain/mimetreeparser/tests/gnupg_home/scdaemon.conf create mode 100644 framework/domain/mimetreeparser/tests/gnupg_home/secring.gpg create mode 100644 framework/domain/mimetreeparser/tests/gnupg_home/trustdb.gpg create mode 100644 framework/domain/mimetreeparser/tests/gnupg_home/trustlist.txt create mode 100644 framework/domain/mimetreeparser/tests/kdepim_add_gpg_crypto_test.cmake create mode 100644 framework/domain/mimetreeparser/tests/kdepim_generate_crypto_test_wrapper.cmake (limited to 'framework') diff --git a/framework/domain/mimetreeparser/tests/CMakeLists.txt b/framework/domain/mimetreeparser/tests/CMakeLists.txt index d3549215..7945c5a0 100644 --- a/framework/domain/mimetreeparser/tests/CMakeLists.txt +++ b/framework/domain/mimetreeparser/tests/CMakeLists.txt @@ -1,10 +1,12 @@ +add_subdirectory(gnupg_home) add_definitions( -DMAIL_DATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/data" ) +include(${CMAKE_CURRENT_SOURCE_DIR}/kdepim_add_gpg_crypto_test.cmake) include_directories( ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/.. ) add_executable(mimetreeparsertest interfacetest.cpp) -add_test(mimetreeparsertest mimetreeparsertest) +add_gpg_crypto_test(mimetreeparsertest mimetreeparsertest) qt5_use_modules(mimetreeparsertest Core Test) -target_link_libraries(mimetreeparsertest mimetreeparser) \ No newline at end of file +target_link_libraries(mimetreeparsertest mimetreeparser) diff --git a/framework/domain/mimetreeparser/tests/gnupg_home/CMakeLists.txt b/framework/domain/mimetreeparser/tests/gnupg_home/CMakeLists.txt new file mode 100644 index 00000000..9c64a008 --- /dev/null +++ b/framework/domain/mimetreeparser/tests/gnupg_home/CMakeLists.txt @@ -0,0 +1,10 @@ +configure_file( gpg-agent.conf.in + "${CMAKE_CURRENT_BINARY_DIR}/gpg-agent.conf" @ONLY ) + +configure_file( gpgsm.conf.in + "${CMAKE_CURRENT_BINARY_DIR}/gpgsm.conf" @ONLY ) + +file( COPY + ${CMAKE_CURRENT_SOURCE_DIR} + DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/../" +) diff --git a/framework/domain/mimetreeparser/tests/gnupg_home/dirmngr-cache.d/DIR.txt b/framework/domain/mimetreeparser/tests/gnupg_home/dirmngr-cache.d/DIR.txt new file mode 100644 index 00000000..1a45a6b3 --- /dev/null +++ b/framework/domain/mimetreeparser/tests/gnupg_home/dirmngr-cache.d/DIR.txt @@ -0,0 +1,3 @@ +v:1: +c:4E31CEB57DDD4A7B9991AB05507B1ED4293FF952:CN=Test-ZS 7,O=Intevation GmbH,C=DE:ldap%3A//ca.intevation.org/cn=Test-ZS 7, o=Intevation GmbH, c=DE?certificateRevocationList:20100615T181523:20100707T181523:72FEF3FD88455A1D4C6796A6499D4422:::: +c:7F2A402CBB016A9146D613568C89D3596A4111AA:CN=Wurzel ZS 3,O=Intevation GmbH,C=DE:ldap%3A//ca.intevation.org/cn=Wurzel ZS 3, o=Intevation GmbH, c=DE?certificateRevocationList:20100625T102134:20100814T102134:44E60EEC02EF2FBF7A5C77E9BD565667:::: diff --git a/framework/domain/mimetreeparser/tests/gnupg_home/dirmngr-cache.d/crl-4E31CEB57DDD4A7B9991AB05507B1ED4293FF952.db b/framework/domain/mimetreeparser/tests/gnupg_home/dirmngr-cache.d/crl-4E31CEB57DDD4A7B9991AB05507B1ED4293FF952.db new file mode 100644 index 00000000..0b7e2dd4 Binary files /dev/null and b/framework/domain/mimetreeparser/tests/gnupg_home/dirmngr-cache.d/crl-4E31CEB57DDD4A7B9991AB05507B1ED4293FF952.db differ diff --git a/framework/domain/mimetreeparser/tests/gnupg_home/dirmngr-cache.d/crl-7F2A402CBB016A9146D613568C89D3596A4111AA.db b/framework/domain/mimetreeparser/tests/gnupg_home/dirmngr-cache.d/crl-7F2A402CBB016A9146D613568C89D3596A4111AA.db new file mode 100644 index 00000000..47474a26 Binary files /dev/null and b/framework/domain/mimetreeparser/tests/gnupg_home/dirmngr-cache.d/crl-7F2A402CBB016A9146D613568C89D3596A4111AA.db differ diff --git a/framework/domain/mimetreeparser/tests/gnupg_home/dirmngr.conf b/framework/domain/mimetreeparser/tests/gnupg_home/dirmngr.conf new file mode 100644 index 00000000..a17a0354 --- /dev/null +++ b/framework/domain/mimetreeparser/tests/gnupg_home/dirmngr.conf @@ -0,0 +1,8 @@ + +###+++--- GPGConf ---+++### +debug-level basic +log-file socket:///home/leo/kde/src/kdepim/messagecomposer/tests/gnupg_home/log-socket +###+++--- GPGConf ---+++### Tue 29 Jun 2010 10:23:13 AM EDT +# GPGConf edited this configuration file. +# It will disable options before this marked block, but it will +# never change anything below these lines. diff --git a/framework/domain/mimetreeparser/tests/gnupg_home/gpg-agent.conf.in b/framework/domain/mimetreeparser/tests/gnupg_home/gpg-agent.conf.in new file mode 100644 index 00000000..ece69255 --- /dev/null +++ b/framework/domain/mimetreeparser/tests/gnupg_home/gpg-agent.conf.in @@ -0,0 +1,10 @@ +pinentry-program @CMAKE_CURRENT_BINARY_DIR@/pinentry-fake.sh +###+++--- GPGConf ---+++### +allow-mark-trusted +debug-level basic +faked-system-time 20130110T154812 +log-file @CMAKE_CURRENT_BINARY_DIR@/gpg-agent.log +###+++--- GPGConf ---+++### Tue 29 Jun 2010 10:23:13 AM EDT +# GPGConf edited this configuration file. +# It will disable options before this marked block, but it will +# never change anything below these lines. diff --git a/framework/domain/mimetreeparser/tests/gnupg_home/gpg.conf b/framework/domain/mimetreeparser/tests/gnupg_home/gpg.conf new file mode 100644 index 00000000..f1760823 --- /dev/null +++ b/framework/domain/mimetreeparser/tests/gnupg_home/gpg.conf @@ -0,0 +1,244 @@ +# Options for GnuPG +# Copyright 1998, 1999, 2000, 2001, 2002, 2003 Free Software Foundation, Inc. +# +# This file is free software; as a special exception the author gives +# unlimited permission to copy and/or distribute it, with or without +# modifications, as long as this notice is preserved. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY, to the extent permitted by law; without even the +# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# +# Unless you specify which option file to use (with the command line +# option "--options filename"), GnuPG uses the file ~/.gnupg/gpg.conf +# by default. +# +# An options file can contain any long options which are available in +# GnuPG. If the first non white space character of a line is a '#', +# this line is ignored. Empty lines are also ignored. +# +# See the man page for a list of options. + +# Uncomment the following option to get rid of the copyright notice + +#no-greeting + +# If you have more than 1 secret key in your keyring, you may want to +# uncomment the following option and set your preferred keyid. + +#default-key 621CC013 + +# If you do not pass a recipient to gpg, it will ask for one. Using +# this option you can encrypt to a default key. Key validation will +# not be done in this case. The second form uses the default key as +# default recipient. + +#default-recipient some-user-id +#default-recipient-self + +# Use --encrypt-to to add the specified key as a recipient to all +# messages. This is useful, for example, when sending mail through a +# mail client that does not automatically encrypt mail to your key. +# In the example, this option allows you to read your local copy of +# encrypted mail that you've sent to others. + +#encrypt-to some-key-id + +# By default GnuPG creates version 3 signatures for data files. This +# is not strictly OpenPGP compliant but PGP 6 and most versions of PGP +# 7 require them. To disable this behavior, you may use this option +# or --openpgp. + +#no-force-v3-sigs + +# Because some mailers change lines starting with "From " to ">From " +# it is good to handle such lines in a special way when creating +# cleartext signatures; all other PGP versions do it this way too. + +#no-escape-from-lines + +# If you do not use the Latin-1 (ISO-8859-1) charset, you should tell +# GnuPG which is the native character set. Please check the man page +# for supported character sets. This character set is only used for +# metadata and not for the actual message which does not undergo any +# translation. Note that future version of GnuPG will change to UTF-8 +# as default character set. In most cases this option is not required +# as GnuPG is able to figure out the correct charset at runtime. + +#charset utf-8 + +# Group names may be defined like this: +# group mynames = paige 0x12345678 joe patti +# +# Any time "mynames" is a recipient (-r or --recipient), it will be +# expanded to the names "paige", "joe", and "patti", and the key ID +# "0x12345678". Note there is only one level of expansion - you +# cannot make an group that points to another group. Note also that +# if there are spaces in the recipient name, this will appear as two +# recipients. In these cases it is better to use the key ID. + +#group mynames = paige 0x12345678 joe patti + +# Lock the file only once for the lifetime of a process. If you do +# not define this, the lock will be obtained and released every time +# it is needed, which is usually preferable. + +#lock-once + +# GnuPG can send and receive keys to and from a keyserver. These +# servers can be HKP, email, or LDAP (if GnuPG is built with LDAP +# support). +# +# Example HKP keyserver: +# hkp://keys.gnupg.net +# hkp://subkeys.pgp.net +# +# Example email keyserver: +# mailto:pgp-public-keys@keys.pgp.net +# +# Example LDAP keyservers: +# ldap://keyserver.pgp.com +# +# Regular URL syntax applies, and you can set an alternate port +# through the usual method: +# hkp://keyserver.example.net:22742 +# +# Most users just set the name and type of their preferred keyserver. +# Note that most servers (with the notable exception of +# ldap://keyserver.pgp.com) synchronize changes with each other. Note +# also that a single server name may actually point to multiple +# servers via DNS round-robin. hkp://keys.gnupg.net is an example of +# such a "server", which spreads the load over a number of physical +# servers. To see the IP address of the server actually used, you may use +# the "--keyserver-options debug". + +keyserver hkp://keys.gnupg.net +#keyserver mailto:pgp-public-keys@keys.nl.pgp.net +#keyserver ldap://keyserver.pgp.com + +# Common options for keyserver functions: +# +# include-disabled : when searching, include keys marked as "disabled" +# on the keyserver (not all keyservers support this). +# +# no-include-revoked : when searching, do not include keys marked as +# "revoked" on the keyserver. +# +# verbose : show more information as the keys are fetched. +# Can be used more than once to increase the amount +# of information shown. +# +# use-temp-files : use temporary files instead of a pipe to talk to the +# keyserver. Some platforms (Win32 for one) always +# have this on. +# +# keep-temp-files : do not delete temporary files after using them +# (really only useful for debugging) +# +# http-proxy="proxy" : set the proxy to use for HTTP and HKP keyservers. +# This overrides the "http_proxy" environment variable, +# if any. +# +# auto-key-retrieve : automatically fetch keys as needed from the keyserver +# when verifying signatures or when importing keys that +# have been revoked by a revocation key that is not +# present on the keyring. +# +# no-include-attributes : do not include attribute IDs (aka "photo IDs") +# when sending keys to the keyserver. + +#keyserver-options auto-key-retrieve + +# Display photo user IDs in key listings + +# list-options show-photos + +# Display photo user IDs when a signature from a key with a photo is +# verified + +# verify-options show-photos + +# Use this program to display photo user IDs +# +# %i is expanded to a temporary file that contains the photo. +# %I is the same as %i, but the file isn't deleted afterwards by GnuPG. +# %k is expanded to the key ID of the key. +# %K is expanded to the long OpenPGP key ID of the key. +# %t is expanded to the extension of the image (e.g. "jpg"). +# %T is expanded to the MIME type of the image (e.g. "image/jpeg"). +# %f is expanded to the fingerprint of the key. +# %% is %, of course. +# +# If %i or %I are not present, then the photo is supplied to the +# viewer on standard input. If your platform supports it, standard +# input is the best way to do this as it avoids the time and effort in +# generating and then cleaning up a secure temp file. +# +# If no photo-viewer is provided, GnuPG will look for xloadimage, eog, +# or display (ImageMagick). On Mac OS X and Windows, the default is +# to use your regular JPEG image viewer. +# +# Some other viewers: +# photo-viewer "qiv %i" +# photo-viewer "ee %i" +# +# This one saves a copy of the photo ID in your home directory: +# photo-viewer "cat > ~/photoid-for-key-%k.%t" +# +# Use your MIME handler to view photos: +# photo-viewer "metamail -q -d -b -c %T -s 'KeyID 0x%k' -f GnuPG" + +# Passphrase agent +# +# We support the old experimental passphrase agent protocol as well as +# the new Assuan based one (currently available in the "newpg" package +# at ftp.gnupg.org/gcrypt/alpha/aegypten/). To make use of the agent, +# you have to run an agent as daemon and use the option +# +# use-agent +# +# which tries to use the agent but will fallback to the regular mode +# if there is a problem connecting to the agent. The normal way to +# locate the agent is by looking at the environment variable +# GPG_AGENT_INFO which should have been set during gpg-agent startup. +# In certain situations the use of this variable is not possible, thus +# the option +# +# --gpg-agent-info=::1 +# +# may be used to override it. + +# Automatic key location +# +# GnuPG can automatically locate and retrieve keys as needed using the +# auto-key-locate option. This happens when encrypting to an email +# address (in the "user@example.com" form), and there are no +# user@example.com keys on the local keyring. This option takes the +# following arguments, in the order they are to be tried: +# +# cert = locate a key using DNS CERT, as specified in RFC-4398. +# GnuPG can handle both the PGP (key) and IPGP (URL + fingerprint) +# CERT methods. +# +# pka = locate a key using DNS PKA. +# +# ldap = locate a key using the PGP Universal method of checking +# "ldap://keys.(thedomain)". For example, encrypting to +# user@example.com will check ldap://keys.example.com. +# +# keyserver = locate a key using whatever keyserver is defined using +# the keyserver option. +# +# You may also list arbitrary keyservers here by URL. +# +# Try CERT, then PKA, then LDAP, then hkp://subkeys.net: +#auto-key-locate cert pka ldap hkp://subkeys.pgp.net + +###+++--- GPGConf ---+++### +utf8-strings +#debug-level basic +#log-file socket:///home/leo/kde/src/kdepim/messagecomposer/tests/gnupg_home/log-socket +###+++--- GPGConf ---+++### Tue 29 Jun 2010 10:23:13 AM EDT +# GPGConf edited this configuration file. +# It will disable options before this marked block, but it will +# never change anything below these lines. diff --git a/framework/domain/mimetreeparser/tests/gnupg_home/gpgsm.conf.in b/framework/domain/mimetreeparser/tests/gnupg_home/gpgsm.conf.in new file mode 100644 index 00000000..92b6119d --- /dev/null +++ b/framework/domain/mimetreeparser/tests/gnupg_home/gpgsm.conf.in @@ -0,0 +1,10 @@ + +###+++--- GPGConf ---+++### +disable-crl-checks +debug-level basic +faked-system-time 20130110T154812 +log-file @CMAKE_CURRENT_BINARY_DIR@/gpgsm.log +###+++--- GPGConf ---+++### Tue 29 Jun 2010 10:23:13 AM EDT +# GPGConf edited this configuration file. +# It will disable options before this marked block, but it will +# never change anything below these lines. diff --git a/framework/domain/mimetreeparser/tests/gnupg_home/pinentry-fake.sh b/framework/domain/mimetreeparser/tests/gnupg_home/pinentry-fake.sh new file mode 100755 index 00000000..7135a942 --- /dev/null +++ b/framework/domain/mimetreeparser/tests/gnupg_home/pinentry-fake.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +echo "OK Your orders please" +while : +do + read cmd + echo "OK" + [ "$cmd" = "BYE" ] && break +done diff --git a/framework/domain/mimetreeparser/tests/gnupg_home/private-keys-v1.d/1AA8BA52430E51AE249AF0DA97D59F869E4101A8.key b/framework/domain/mimetreeparser/tests/gnupg_home/private-keys-v1.d/1AA8BA52430E51AE249AF0DA97D59F869E4101A8.key new file mode 100644 index 00000000..39ac307b Binary files /dev/null and b/framework/domain/mimetreeparser/tests/gnupg_home/private-keys-v1.d/1AA8BA52430E51AE249AF0DA97D59F869E4101A8.key differ diff --git a/framework/domain/mimetreeparser/tests/gnupg_home/pubring.gpg b/framework/domain/mimetreeparser/tests/gnupg_home/pubring.gpg new file mode 100644 index 00000000..2e00fa24 Binary files /dev/null and b/framework/domain/mimetreeparser/tests/gnupg_home/pubring.gpg differ diff --git a/framework/domain/mimetreeparser/tests/gnupg_home/pubring.kbx b/framework/domain/mimetreeparser/tests/gnupg_home/pubring.kbx new file mode 100644 index 00000000..0230f313 Binary files /dev/null and b/framework/domain/mimetreeparser/tests/gnupg_home/pubring.kbx differ diff --git a/framework/domain/mimetreeparser/tests/gnupg_home/scdaemon.conf b/framework/domain/mimetreeparser/tests/gnupg_home/scdaemon.conf new file mode 100644 index 00000000..a17a0354 --- /dev/null +++ b/framework/domain/mimetreeparser/tests/gnupg_home/scdaemon.conf @@ -0,0 +1,8 @@ + +###+++--- GPGConf ---+++### +debug-level basic +log-file socket:///home/leo/kde/src/kdepim/messagecomposer/tests/gnupg_home/log-socket +###+++--- GPGConf ---+++### Tue 29 Jun 2010 10:23:13 AM EDT +# GPGConf edited this configuration file. +# It will disable options before this marked block, but it will +# never change anything below these lines. diff --git a/framework/domain/mimetreeparser/tests/gnupg_home/secring.gpg b/framework/domain/mimetreeparser/tests/gnupg_home/secring.gpg new file mode 100644 index 00000000..cfd3387d Binary files /dev/null and b/framework/domain/mimetreeparser/tests/gnupg_home/secring.gpg differ diff --git a/framework/domain/mimetreeparser/tests/gnupg_home/trustdb.gpg b/framework/domain/mimetreeparser/tests/gnupg_home/trustdb.gpg new file mode 100644 index 00000000..70089c15 Binary files /dev/null and b/framework/domain/mimetreeparser/tests/gnupg_home/trustdb.gpg differ diff --git a/framework/domain/mimetreeparser/tests/gnupg_home/trustlist.txt b/framework/domain/mimetreeparser/tests/gnupg_home/trustlist.txt new file mode 100644 index 00000000..bbb0442d --- /dev/null +++ b/framework/domain/mimetreeparser/tests/gnupg_home/trustlist.txt @@ -0,0 +1,11 @@ +5E:7C:B2:F4:9F:70:05:43:42:32:5D:75:74:70:00:09:B9:D8:08:61 S + + + +# CN=unittest cert +# O=KDAB +# C=US +# EMail=test@example.com +24:D2:FC:A2:2E:B3:B8:0A:1E:37:71:D1:4C:C6:58:E3:21:2B:49:DC S + + diff --git a/framework/domain/mimetreeparser/tests/interfacetest.cpp b/framework/domain/mimetreeparser/tests/interfacetest.cpp index 83de97f7..615d5742 100644 --- a/framework/domain/mimetreeparser/tests/interfacetest.cpp +++ b/framework/domain/mimetreeparser/tests/interfacetest.cpp @@ -30,6 +30,19 @@ QByteArray readMailFromFile(const QString &mailFile) return file.readAll(); } +QByteArray join(QVector vec, QByteArray sep) +{ + QByteArray ret; + bool bInit = true; + foreach(const auto &entry, vec) { + if (!bInit) { + ret += sep; + } + bInit = false; + ret += entry; + } + return ret; +} class InterfaceTest : public QObject { @@ -38,7 +51,10 @@ private: void printTree(const Part::Ptr &start, QString pre) { foreach (const auto &part, start->subParts()) { - qWarning() << QStringLiteral("%1* %2").arg(pre).arg(QString::fromLatin1(part->type())); + qWarning() << QStringLiteral("%1* %2(%3)") + .arg(pre) + .arg(QString::fromLatin1(part->type())) + .arg(QString::fromLatin1(join(part->availableContents(),", "))); printTree(part,pre + QStringLiteral(" ")); } } @@ -48,6 +64,7 @@ private slots: void testTextMail() { Parser parser(readMailFromFile("plaintext.mbox")); + printTree(parser.d->mTree,QString()); auto contentPartList = parser.collectContentParts(); QCOMPARE(contentPartList.size(), 1); auto contentPart = contentPartList[0]; @@ -67,6 +84,7 @@ private slots: void testTextAlternative() { Parser parser(readMailFromFile("alternative.mbox")); + printTree(parser.d->mTree,QString()); auto contentPartList = parser.collectContentParts(); QCOMPARE(contentPartList.size(), 1); auto contentPart = contentPartList[0]; @@ -90,6 +108,7 @@ private slots: void testTextHtml() { Parser parser(readMailFromFile("html.mbox")); + printTree(parser.d->mTree,QString()); auto contentPartList = parser.collectContentParts(); QCOMPARE(contentPartList.size(), 1); auto contentPart = contentPartList[0]; diff --git a/framework/domain/mimetreeparser/tests/kdepim_add_gpg_crypto_test.cmake b/framework/domain/mimetreeparser/tests/kdepim_add_gpg_crypto_test.cmake new file mode 100644 index 00000000..17078202 --- /dev/null +++ b/framework/domain/mimetreeparser/tests/kdepim_add_gpg_crypto_test.cmake @@ -0,0 +1,60 @@ +# Copyright (c) 2013 Sandro Knauß +# +# Redistribution and use is allowed according to the terms of the BSD license. +# For details see the accompanying COPYING-CMAKE-SCRIPTS file. + +set( GNUPGHOME ${CMAKE_BINARY_DIR}/framework/domain/mimetreeparser/tests/gnupg_home ) +add_definitions( -DGNUPGHOME="\\"${GNUPGHOME}\\"" ) + +macro (ADD_GPG_CRYPTO_TEST _target _testname) + if (UNIX) + if (APPLE) + set(_library_path_variable "DYLD_LIBRARY_PATH") + elseif (CYGWIN) + set(_library_path_variable "PATH") + else (APPLE) + set(_library_path_variable "LD_LIBRARY_PATH") + endif (APPLE) + + if (APPLE) + # DYLD_LIBRARY_PATH does not work like LD_LIBRARY_PATH + # OSX already has the RPATH in libraries and executables, putting runtime directories in + # DYLD_LIBRARY_PATH actually breaks things + set(_ld_library_path "${LIBRARY_OUTPUT_PATH}/${CMAKE_CFG_INTDIR}/") + else (APPLE) + set(_ld_library_path "${LIBRARY_OUTPUT_PATH}/${CMAKE_CFG_INTDIR}/:${LIB_INSTALL_DIR}:${QT_LIBRARY_DIR}") + endif (APPLE) + set(_executable "$") + + # use add_custom_target() to have the sh-wrapper generated during build time instead of cmake time + add_custom_command(TARGET ${_target} POST_BUILD + COMMAND ${CMAKE_COMMAND} + -D_filename=${_executable}.shell -D_library_path_variable=${_library_path_variable} + -D_ld_library_path="${_ld_library_path}" -D_executable=$ + -D_gnupghome="${GNUPGHOME}" + -P ${CMAKE_SOURCE_DIR}/framework/domain/mimetreeparser/tests/kdepim_generate_crypto_test_wrapper.cmake + ) + + set_property(DIRECTORY APPEND PROPERTY ADDITIONAL_MAKE_CLEAN_FILES "${_executable}.shell" ) + add_test(NAME ${_testname} COMMAND ${_executable}.shell) + + else (UNIX) + # under windows, set the property WRAPPER_SCRIPT just to the name of the executable + # maybe later this will change to a generated batch file (for setting the PATH so that the Qt libs are found) + set(_ld_library_path "${LIBRARY_OUTPUT_PATH}/${CMAKE_CFG_INTDIR}\;${LIB_INSTALL_DIR}\;${QT_LIBRARY_DIR}") + set(_executable "$") + + # use add_custom_target() to have the batch-file-wrapper generated during build time instead of cmake time + add_custom_command(TARGET ${_target} POST_BUILD + COMMAND ${CMAKE_COMMAND} + -D_filename="${_executable}.bat" + -D_ld_library_path="${_ld_library_path}" -D_executable="${_executable}" + -D_gnupghome="${GNUPGHOME}" + -P ${CMAKE_SOURCE_DIR}/framework/domain/mimetreeparser/tests/kdepim_generate_crypto_test_wrapper.cmake + ) + + add_test(NAME ${_testname} COMMAND ${_executable}.bat) + + endif (UNIX) +endmacro (ADD_GPG_CRYPTO_TEST) + diff --git a/framework/domain/mimetreeparser/tests/kdepim_generate_crypto_test_wrapper.cmake b/framework/domain/mimetreeparser/tests/kdepim_generate_crypto_test_wrapper.cmake new file mode 100644 index 00000000..e1412f37 --- /dev/null +++ b/framework/domain/mimetreeparser/tests/kdepim_generate_crypto_test_wrapper.cmake @@ -0,0 +1,45 @@ +# Copyright (c) 2006, Alexander Neundorf, +# Copyright (c) 2013, Sandro Knauß +# +# Redistribution and use is allowed according to the terms of the BSD license. +# For details see the accompanying COPYING-CMAKE-SCRIPTS file. + + +if (UNIX) + +file(WRITE "${_filename}" +"#!/bin/sh +# created by cmake, don't edit, changes will be lost + +# don't mess with a gpg-agent already running on the system +unset GPG_AGENT_INFO + +${_library_path_variable}=${_ld_library_path}\${${_library_path_variable}:+:\$${_library_path_variable}} GNUPGHOME=${_gnupghome} gpg-agent --daemon \"${_executable}\" \"$@\" +_result=$? +_pid=`echo GETINFO pid | GNUPGHOME=${_gnupghome} gpg-connect-agent | grep 'D' | cut -d' ' -f2` +if [ ! -z \"\$_pid\" ]; then + echo \"Waiting for gpg-agent to terminate (PID: $_pid)...\" + while kill -0 \"\$_pid\"; do + sleep 1 + done +fi +exit \$_result +") + +# make it executable +# since this is only executed on UNIX, it is safe to call chmod +exec_program(chmod ARGS ug+x \"${_filename}\" OUTPUT_VARIABLE _dummy ) + +else (UNIX) + +file(TO_NATIVE_PATH "${_ld_library_path}" win_path) +file(TO_NATIVE_PATH "${_gnupghome}" win_gnupghome) + +file(WRITE "${_filename}" +" +set PATH=${win_path};$ENV{PATH} +set GNUPGHOME=${win_gnupghome};$ENV{GNUPGHOME} +gpg-agent --daemon \"${_executable}\" %* +") + +endif (UNIX) -- cgit v1.2.3 From 2d0608ddf1b84991ca7a693ce00e70b6447644d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sandro=20Knau=C3=9F?= Date: Tue, 2 Aug 2016 10:11:23 +0200 Subject: Implement collectAttachmentParts --- framework/domain/mimetreeparser/interface.cpp | 47 +++++++++++++++++++++- .../domain/mimetreeparser/tests/interfacetest.cpp | 16 ++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) (limited to 'framework') diff --git a/framework/domain/mimetreeparser/interface.cpp b/framework/domain/mimetreeparser/interface.cpp index 4f45b883..04f0fdf2 100644 --- a/framework/domain/mimetreeparser/interface.cpp +++ b/framework/domain/mimetreeparser/interface.cpp @@ -433,6 +433,9 @@ void SinglePartPrivate::fillFrom(MimeTreeParser::HtmlMessagePart::Ptr part) void SinglePartPrivate::fillFrom(MimeTreeParser::AttachmentMessagePart::Ptr part) { q->reachParentD()->createMailMime(part.staticCast()); + mType = q->mailMime()->mimetype().name().toUtf8(); + mContent.clear(); + mContent.append(std::make_shared(part->text().toLocal8Bit(), q)); } SinglePart::SinglePart() @@ -582,6 +585,49 @@ QVector Parser::collectContentParts() const }); } + +QVector Parser::collectAttachmentParts() const +{ + return collect(d->mTree, [](const Part::Ptr &p){return p->type() != "EncapsulatedPart";}, + [](const Content::Ptr &content){ + const auto mime = content->mailMime(); + + if (!mime) { + return false; + } + + if (mime->isFirstTextPart()) { + return false; + } + + { + const auto parent = content->parent(); + if (parent) { + const auto _mime = parent->mailMime(); + if (_mime && (_mime->isTopLevelPart() || _mime->isFirstTextPart())) { + return false; + } + } + } + const auto cd = mime->disposition(); + if (cd && cd == MailMime::Inline) { + // explict "inline" disposition: + return false; + } + if (cd && cd == MailMime::Attachment) { + // explicit "attachment" disposition: + return true; + } + + const auto ct = mime->mimetype(); + if (ct.name().trimmed().toLower() == "text" && ct.name().trimmed().isEmpty() && + (!mime || mime->filename().trimmed().isEmpty())) { + // text/* w/o filename parameter: + return false; + } + return true; + }); +} QVector Parser::collect(const Part::Ptr &start, std::function select, std::function filter) const { QVector ret; @@ -594,7 +640,6 @@ QVector Parser::collect(const Part::Ptr &start, std::functioncontent("html"); QCOMPARE(contentList.size(), 0); + auto contentAttachmentList = parser.collectAttachmentParts(); + QCOMPARE(contentAttachmentList.size(), 0); } void testTextAlternative() @@ -103,6 +105,8 @@ private slots: QCOMPARE(contentList[0]->charset(), QStringLiteral("utf-8").toLocal8Bit()); QCOMPARE(contentList[0]->encryptions().size(), 0); QCOMPARE(contentList[0]->signatures().size(), 0); + auto contentAttachmentList = parser.collectAttachmentParts(); + QCOMPARE(contentAttachmentList.size(), 0); } void testTextHtml() @@ -124,6 +128,8 @@ private slots: QCOMPARE(contentList[0]->charset(), QStringLiteral("utf-8").toLocal8Bit()); QCOMPARE(contentList[0]->encryptions().size(), 0); QCOMPARE(contentList[0]->signatures().size(), 0); + auto contentAttachmentList = parser.collectAttachmentParts(); + QCOMPARE(contentAttachmentList.size(), 0); } void testSMimeEncrypted() @@ -139,6 +145,8 @@ private slots: QCOMPARE(contentList.size(), 1); QCOMPARE(contentList[0]->content(), QStringLiteral("The quick brown fox jumped over the lazy dog.").toLocal8Bit()); QCOMPARE(contentList[0]->charset(), QStringLiteral("utf-8").toLocal8Bit()); + auto contentAttachmentList = parser.collectAttachmentParts(); + QCOMPARE(contentAttachmentList.size(), 0); } void testOpenPGPEncryptedAttachment() @@ -154,6 +162,12 @@ private slots: QCOMPARE(contentList.size(), 1); QCOMPARE(contentList[0]->content(), QStringLiteral("test text").toLocal8Bit()); QCOMPARE(contentList[0]->charset(), QStringLiteral("utf-8").toLocal8Bit()); + auto contentAttachmentList = parser.collectAttachmentParts(); + QCOMPARE(contentAttachmentList.size(), 2); + QCOMPARE(contentAttachmentList[0]->availableContents(), QVector() << "text/plain"); + QCOMPARE(contentAttachmentList[0]->content().size(), 1); + QCOMPARE(contentAttachmentList[1]->availableContents(), QVector() << "image/png"); + QCOMPARE(contentAttachmentList[1]->content().size(), 1); } void testOpenPPGInline() @@ -169,6 +183,8 @@ private slots: QCOMPARE(contentList.size(), 1); QCOMPARE(contentList[0]->content(), QStringLiteral("asdasd asd asd asdf sadf sdaf sadf äöü").toLocal8Bit()); QCOMPARE(contentList[0]->charset(), QStringLiteral("utf-8").toLocal8Bit()); + auto contentAttachmentList = parser.collectAttachmentParts(); + QCOMPARE(contentAttachmentList.size(), 0); } }; -- cgit v1.2.3 From 92d6fbd8f6a504da869454ca85f861e30c89a73c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sandro=20Knau=C3=9F?= Date: Tue, 2 Aug 2016 18:14:00 +0200 Subject: make signs & encrytiopn work --- framework/domain/mimetreeparser/interface.cpp | 204 +++++++++++++++++++-- framework/domain/mimetreeparser/interface.h | 34 +++- .../domain/mimetreeparser/tests/interfacetest.cpp | 12 ++ 3 files changed, 224 insertions(+), 26 deletions(-) (limited to 'framework') diff --git a/framework/domain/mimetreeparser/interface.cpp b/framework/domain/mimetreeparser/interface.cpp index 04f0fdf2..d6354c9e 100644 --- a/framework/domain/mimetreeparser/interface.cpp +++ b/framework/domain/mimetreeparser/interface.cpp @@ -122,10 +122,21 @@ public: void createMailMime(const MimeTreeParser::TextMessagePart::Ptr &part); void createMailMime(const MimeTreeParser::AlternativeMessagePart::Ptr &part); void createMailMime(const MimeTreeParser::HtmlMessagePart::Ptr &part); + + void appendEncryption(const MimeTreeParser::EncryptedMessagePart::Ptr &part); + void appendSignature(const MimeTreeParser::SignedMessagePart::Ptr &part); + + void setSignatures(const QVector &sigs); + void setEncryptions(const QVector &encs); + + const QVector &encryptions() const; + const QVector &signatures() const; private: Part *q; Part *mParent; QVector mSubParts; + QVector mEncryptions; + QVector mSignatures; MailMime::Ptr mMailMime; }; @@ -166,6 +177,27 @@ void PartPrivate::appendSubPart(Part::Ptr subpart) mSubParts.append(subpart); } +void PartPrivate::appendEncryption(const MimeTreeParser::EncryptedMessagePart::Ptr& part) +{ + mEncryptions.append(Encryption::Ptr(new Encryption)); +} + +void PartPrivate::setEncryptions(const QVector< Encryption::Ptr >& encs) +{ + mEncryptions = encs; +} + +void PartPrivate::appendSignature(const MimeTreeParser::SignedMessagePart::Ptr& part) +{ + mSignatures.append(Signature::Ptr(new Signature)); +} + + +void PartPrivate::setSignatures(const QVector< Signature::Ptr >& sigs) +{ + mSignatures = sigs; +} + Part *PartPrivate::parent() const { return mParent; @@ -181,6 +213,16 @@ const MailMime::Ptr& PartPrivate::mailMime() const return mMailMime; } +const QVector< Encryption::Ptr >& PartPrivate::encryptions() const +{ + return mEncryptions; +} + +const QVector< Signature::Ptr >& PartPrivate::signatures() const +{ + return mSignatures; +} + Part::Part() : d(std::unique_ptr(new PartPrivate(this))) { @@ -217,24 +259,24 @@ QVector Part::content(const QByteArray& ct) const return QVector(); } -QVector Part::encryptions() const +QVector Part::encryptions() const { + auto ret = d->encryptions(); auto parent = d->parent(); if (parent) { - return parent->encryptions(); - } else { - return QVector(); + ret.append(parent->encryptions()); } + return ret; } -QVector Part::signatures() const +QVector Part::signatures() const { + auto ret = d->signatures(); auto parent = d->parent(); if (parent) { - return parent->signatures(); - } else { - return QVector(); + ret.append(parent->signatures()); } + return ret; } MailMime::Ptr Part::mailMime() const @@ -250,8 +292,23 @@ public: Part *mParent; Content *q; MailMime::Ptr mMailMime; + QVector mEncryptions; + QVector mSignatures; + void appendSignature(const MimeTreeParser::SignedMessagePart::Ptr &sig); + void appendEncryption(const MimeTreeParser::EncryptedMessagePart::Ptr &enc); }; +void ContentPrivate::appendEncryption(const MimeTreeParser::EncryptedMessagePart::Ptr& enc) +{ + mEncryptions.append(Encryption::Ptr(new Encryption)); +} + +void ContentPrivate::appendSignature(const MimeTreeParser::SignedMessagePart::Ptr& sig) +{ + mSignatures.append(Signature::Ptr(new Signature)); +} + + Content::Content(const QByteArray& content, Part *parent) : d(std::unique_ptr(new ContentPrivate)) { @@ -261,24 +318,32 @@ Content::Content(const QByteArray& content, Part *parent) d->mParent = parent; } +Content::Content(ContentPrivate* d_ptr) + : d(std::unique_ptr(d_ptr)) +{ + d->q = this; +} + Content::~Content() { } -QVector Content::encryptions() const +QVector Content::encryptions() const { + auto ret = d->mEncryptions; if (d->mParent) { - return d->mParent->encryptions(); + ret.append(d->mParent->encryptions()); } - return QVector(); + return ret; } -QVector Content::signatures() const +QVector Content::signatures() const { + auto ret = d->mSignatures; if (d->mParent) { - return d->mParent->signatures(); + ret.append(d->mParent->signatures()); } - return QVector(); + return ret; } QByteArray Content::content() const @@ -327,6 +392,19 @@ PlainTextContent::PlainTextContent(const QByteArray& content, Part* parent) } +PlainTextContent::PlainTextContent(ContentPrivate* d_ptr) + : Content(d_ptr) +{ + +} + +HtmlContent::HtmlContent(ContentPrivate* d_ptr) + : Content(d_ptr) +{ + +} + + QByteArray PlainTextContent::type() const { return "PlainTextContent"; @@ -417,7 +495,23 @@ void SinglePartPrivate::fillFrom(MimeTreeParser::TextMessagePart::Ptr part) mType = "plaintext"; mContent.clear(); foreach (const auto &mp, part->subParts()) { - mContent.append(std::make_shared(mp->text().toLocal8Bit(), q)); + auto d_ptr = new ContentPrivate; + d_ptr->mContent = part->text().toLocal8Bit(); + d_ptr->mParent = q; + d_ptr->mCodec = "utf-8"; + const auto enc = mp.dynamicCast(); + auto sig = mp.dynamicCast(); + if (enc) { + d_ptr->appendEncryption(enc); + const auto s = enc->subParts(); + if (s.size() == 1) { + sig = s[0].dynamicCast(); + } + } + if (sig) { + d_ptr->appendSignature(sig); + } + mContent.append(std::make_shared(d_ptr)); q->reachParentD()->createMailMime(part); } } @@ -472,6 +566,54 @@ PartPrivate* SinglePart::reachParentD() const return Part::d.get(); } +class SignaturePrivate +{ +public: + Signature *q; +}; + +Signature::Signature() + :d(std::unique_ptr(new SignaturePrivate)) +{ + d->q = this; +} + + +Signature::Signature(SignaturePrivate *d_ptr) + :d(std::unique_ptr(d_ptr)) +{ + d->q = this; +} + +Signature::~Signature() +{ + +} + + +class EncryptionPrivate +{ +public: + Encryption *q; +}; + +Encryption::Encryption(EncryptionPrivate *d_ptr) + :d(std::unique_ptr(d_ptr)) +{ + d->q = this; +} + +Encryption::Encryption() + :d(std::unique_ptr(new EncryptionPrivate)) +{ + d->q = this; +} + +Encryption::~Encryption() +{ + +} + ParserPrivate::ParserPrivate(Parser* parser) : q(parser) , mNodeHelper(std::make_shared()) @@ -513,21 +655,43 @@ void ParserPrivate::createTree(const MimeTreeParser::MessagePart::Ptr &start, co if (attachment) { auto part = std::make_shared(); part->d->fillFrom(attachment); - mTree->d->appendSubPart(part); + tree->d->appendSubPart(part); } else if (text) { auto part = std::make_shared(); part->d->fillFrom(text); - mTree->d->appendSubPart(part); + tree->d->appendSubPart(part); } else if (alternative) { auto part = std::make_shared(); part->d->fillFrom(alternative); - mTree->d->appendSubPart(part); + tree->d->appendSubPart(part); } else if (html) { auto part = std::make_shared(); part->d->fillFrom(html); - mTree->d->appendSubPart(part); + tree->d->appendSubPart(part); } else { - createTree(m, tree); + const auto enc = mp.dynamicCast(); + const auto sig = mp.dynamicCast(); + if (enc || sig) { + auto subTree = std::make_shared(); + if (enc) { + subTree->d->appendEncryption(enc); + } + if (sig) { + subTree->d->appendSignature(sig); + } + createTree(m, subTree); + foreach(const auto &p, subTree->subParts()) { + tree->d->appendSubPart(p); + if (enc) { + p->d->setEncryptions(subTree->d->encryptions()); + } + if (sig) { + p->d->setSignatures(subTree->d->signatures()); + } + } + } else { + createTree(m, tree); + } } } } diff --git a/framework/domain/mimetreeparser/interface.h b/framework/domain/mimetreeparser/interface.h index a6a7f39d..a482a824 100644 --- a/framework/domain/mimetreeparser/interface.h +++ b/framework/domain/mimetreeparser/interface.h @@ -54,7 +54,12 @@ class EncryptionError; class Key; class Signature; +class SignaturePrivate; class Encryption; +class EncryptionPrivate; + +typedef std::shared_ptr SignaturePtr; +typedef std::shared_ptr EncryptionPtr; class Parser; class ParserPrivate; @@ -109,6 +114,7 @@ class Content public: typedef std::shared_ptr Ptr; Content(const QByteArray &content, Part *parent); + Content(ContentPrivate *d_ptr); virtual ~Content(); QByteArray content() const; @@ -121,8 +127,8 @@ public: // overwrite default charset with given charset QString encodedContent(QByteArray charset) const; - virtual QVector signatures() const; - virtual QVector encryptions() const; + QVector signatures() const; + QVector encryptions() const; MailMime::Ptr mailMime() const; virtual QByteArray type() const; Part* parent() const; @@ -134,6 +140,7 @@ class PlainTextContent : public Content { public: PlainTextContent(const QByteArray &content, Part *parent); + PlainTextContent(ContentPrivate *d_ptr); QByteArray type() const Q_DECL_OVERRIDE; }; @@ -141,6 +148,7 @@ class HtmlContent : public Content { public: HtmlContent(const QByteArray &content, Part *parent); + HtmlContent(ContentPrivate* d_ptr); QByteArray type() const Q_DECL_OVERRIDE; }; @@ -171,9 +179,8 @@ public: int keyLength() const; private: - std::unique_ptr d; + std::unique_ptr d; }; - class Part { public: @@ -189,8 +196,8 @@ public: QVector subParts() const; Part *parent() const; - virtual QVector signatures() const; - virtual QVector encryptions() const; + QVector signatures() const; + QVector encryptions() const; virtual MailMime::Ptr mailMime() const; protected: std::unique_ptr d; @@ -298,12 +305,20 @@ class Key class Signature { +public: + typedef std::shared_ptr Ptr; + Signature(); + Signature(SignaturePrivate *); + ~Signature(); + Key key() const; QDateTime creationDateTime() const; QDateTime expirationTime() const; bool neverExpires() const; //template <> StatusObject verify() const; + private: + std::unique_ptr d; }; /* @@ -313,7 +328,14 @@ class Signature */ class Encryption { +public: + typedef std::shared_ptr Ptr; + Encryption(); + Encryption(EncryptionPrivate *); + ~Encryption(); std::vector recipients() const; +private: + std::unique_ptr d; }; class Parser diff --git a/framework/domain/mimetreeparser/tests/interfacetest.cpp b/framework/domain/mimetreeparser/tests/interfacetest.cpp index fb073fc1..ac77b025 100644 --- a/framework/domain/mimetreeparser/tests/interfacetest.cpp +++ b/framework/domain/mimetreeparser/tests/interfacetest.cpp @@ -145,6 +145,8 @@ private slots: QCOMPARE(contentList.size(), 1); QCOMPARE(contentList[0]->content(), QStringLiteral("The quick brown fox jumped over the lazy dog.").toLocal8Bit()); QCOMPARE(contentList[0]->charset(), QStringLiteral("utf-8").toLocal8Bit()); + QCOMPARE(contentList[0]->encryptions().size(), 1); + QCOMPARE(contentList[0]->signatures().size(), 0); auto contentAttachmentList = parser.collectAttachmentParts(); QCOMPARE(contentAttachmentList.size(), 0); } @@ -162,12 +164,18 @@ private slots: QCOMPARE(contentList.size(), 1); QCOMPARE(contentList[0]->content(), QStringLiteral("test text").toLocal8Bit()); QCOMPARE(contentList[0]->charset(), QStringLiteral("utf-8").toLocal8Bit()); + QCOMPARE(contentList[0]->encryptions().size(), 1); + QCOMPARE(contentList[0]->signatures().size(), 1); auto contentAttachmentList = parser.collectAttachmentParts(); QCOMPARE(contentAttachmentList.size(), 2); QCOMPARE(contentAttachmentList[0]->availableContents(), QVector() << "text/plain"); QCOMPARE(contentAttachmentList[0]->content().size(), 1); + QCOMPARE(contentAttachmentList[0]->encryptions().size(), 1); + QCOMPARE(contentAttachmentList[0]->signatures().size(), 1); QCOMPARE(contentAttachmentList[1]->availableContents(), QVector() << "image/png"); QCOMPARE(contentAttachmentList[1]->content().size(), 1); + QCOMPARE(contentAttachmentList[1]->encryptions().size(), 0); + QCOMPARE(contentAttachmentList[1]->signatures().size(), 0); } void testOpenPPGInline() @@ -179,10 +187,14 @@ private slots: auto contentPart = contentPartList[0]; QVERIFY((bool)contentPart); QCOMPARE(contentPart->availableContents(), QVector() << "plaintext"); + QCOMPARE(contentPart->encryptions().size(), 0); + QCOMPARE(contentPart->signatures().size(), 0); auto contentList = contentPart->content("plaintext"); QCOMPARE(contentList.size(), 1); QCOMPARE(contentList[0]->content(), QStringLiteral("asdasd asd asd asdf sadf sdaf sadf äöü").toLocal8Bit()); QCOMPARE(contentList[0]->charset(), QStringLiteral("utf-8").toLocal8Bit()); + QCOMPARE(contentList[0]->encryptions().size(), 1); + QCOMPARE(contentList[0]->signatures().size(), 1); auto contentAttachmentList = parser.collectAttachmentParts(); QCOMPARE(contentAttachmentList.size(), 0); } -- cgit v1.2.3 From 349e404b539c1f9d1feb54658e2e6fbbd2165462 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sandro=20Knau=C3=9F?= Date: Wed, 10 Aug 2016 13:20:11 +0200 Subject: Use new mimetreeparser interface --- framework/domain/CMakeLists.txt | 2 +- framework/domain/messageparser.cpp | 49 +++++++++++++++++++++------ framework/domain/messageparser.h | 15 ++++---- framework/domain/mimetreeparser/interface.cpp | 5 +++ framework/domain/mimetreeparser/interface.h | 2 ++ framework/domain/mimetreeparser/interface_p.h | 1 - 6 files changed, 54 insertions(+), 20 deletions(-) (limited to 'framework') diff --git a/framework/domain/CMakeLists.txt b/framework/domain/CMakeLists.txt index 92a81352..07f01367 100644 --- a/framework/domain/CMakeLists.txt +++ b/framework/domain/CMakeLists.txt @@ -20,7 +20,7 @@ find_package(KF5 REQUIRED COMPONENTS Package) add_library(mailplugin SHARED ${mailplugin_SRCS}) qt5_use_modules(mailplugin Core Quick Qml WebKitWidgets) -target_link_libraries(mailplugin actionplugin settingsplugin sink KF5::MimeTreeParser KF5::Codecs KF5::Package KF5::Async KF5::IconThemes) +target_link_libraries(mailplugin actionplugin settingsplugin sink mimetreeparser KF5::MimeTreeParser KF5::Codecs KF5::Package KF5::Async KF5::IconThemes) add_subdirectory(actions/tests) diff --git a/framework/domain/messageparser.cpp b/framework/domain/messageparser.cpp index 8363e119..262be0b7 100644 --- a/framework/domain/messageparser.cpp +++ b/framework/domain/messageparser.cpp @@ -21,6 +21,10 @@ #include "stringhtmlwriter.h" #include "objecttreesource.h" +#include "mimetreeparser/interface.h" + +#include + #include #include #include @@ -29,7 +33,9 @@ #include #include -PartModel::PartModel(QSharedPointer partTree, QMap embeddedPartMap) : mPartTree(partTree), mEmbeddedPartMap(embeddedPartMap) +PartModel::PartModel(QSharedPointer partTree, std::shared_ptr parser) + : mPartTree(partTree) + , mParser(parser) { } @@ -73,8 +79,14 @@ QVariant PartModel::data(const QModelIndex &index, int role) const // qDebug() << "Getting text: " << part->property("text").toString(); // FIXME: we should have a list per part, and not one for all parts. auto text = part->property("htmlContent").toString(); - for (const auto &cid : mEmbeddedPartMap.keys()) { - text.replace(QString("src=\"cid:%1\"").arg(QString(cid)), QString("src=\"%1\"").arg(mEmbeddedPartMap.value(cid).toString())); + auto rx = QRegExp("src=(\"|')cid:([^\1]*)\1"); + int pos = 0; + while ((pos = rx.indexIn(text, pos)) != -1) { + auto repl = mParser->getPart(rx.cap(2).toUtf8()); + if (repl.isValid()) { + text.replace(rx.cap(0), QString("src=\"%1\"").arg(repl.toString())); + } + pos += rx.matchedLength(); } return text; } @@ -140,16 +152,31 @@ int PartModel::columnCount(const QModelIndex &parent) const return 1; } +class MessagePartPrivate +{ +public: + QSharedPointer mPartTree; + QString mHtml; + QMap mEmbeddedPartMap; + std::shared_ptr mNodeHelper; + std::shared_ptr mParser; +}; MessageParser::MessageParser(QObject *parent) : QObject(parent) + , d(std::unique_ptr(new MessagePartPrivate)) +{ + +} + +MessageParser::~MessageParser() { } QString MessageParser::html() const { - return mHtml; + return d->mHtml; } QVariant MessageParser::message() const @@ -161,6 +188,8 @@ void MessageParser::setMessage(const QVariant &message) { QTime time; time.start(); + d->mParser = std::shared_ptr(new Parser(message.toByteArray())); + const auto mailData = KMime::CRLFtoLF(message.toByteArray()); KMime::Message::Ptr msg(new KMime::Message); msg->setContent(mailData); @@ -170,20 +199,20 @@ void MessageParser::setMessage(const QVariant &message) // render the mail StringHtmlWriter htmlWriter; //temporary files only have the lifetime of the nodehelper, so we keep it around until the mail changes. - mNodeHelper = std::make_shared(); + d->mNodeHelper = std::make_shared(); ObjectTreeSource source(&htmlWriter); - MimeTreeParser::ObjectTreeParser otp(&source, mNodeHelper.get()); + MimeTreeParser::ObjectTreeParser otp(&source, d->mNodeHelper.get()); otp.parseObjectTree(msg.data()); - mPartTree = otp.parsedPart().dynamicCast(); + d->mPartTree = otp.parsedPart().dynamicCast(); - mEmbeddedPartMap = htmlWriter.embeddedParts(); - mHtml = htmlWriter.html(); + d->mEmbeddedPartMap = htmlWriter.embeddedParts(); + d->mHtml = htmlWriter.html(); emit htmlChanged(); } QAbstractItemModel *MessageParser::partTree() const { - return new PartModel(mPartTree, mEmbeddedPartMap); + return new PartModel(d->mPartTree, d->mParser); } diff --git a/framework/domain/messageparser.h b/framework/domain/messageparser.h index 3e0255df..9469f2b5 100644 --- a/framework/domain/messageparser.h +++ b/framework/domain/messageparser.h @@ -29,11 +29,11 @@ #include #include -namespace MimeTreeParser { - class NodeHelper; -}; class QAbstractItemModel; +class Parser; +class MessagePartPrivate; + class MessageParser : public QObject { Q_OBJECT @@ -43,6 +43,7 @@ class MessageParser : public QObject public: explicit MessageParser(QObject *parent = Q_NULLPTR); + ~MessageParser(); QString html() const; @@ -54,16 +55,13 @@ signals: void htmlChanged(); private: - QSharedPointer mPartTree; - QString mHtml; - QMap mEmbeddedPartMap; - std::shared_ptr mNodeHelper; + std::unique_ptr d; }; class PartModel : public QAbstractItemModel { Q_OBJECT public: - PartModel(QSharedPointer partTree, QMap embeddedPartMap); + PartModel(QSharedPointer partTree, std::shared_ptr parser); public: enum Roles { @@ -86,5 +84,6 @@ public: private: QSharedPointer mPartTree; QMap mEmbeddedPartMap; + std::shared_ptr mParser; }; diff --git a/framework/domain/mimetreeparser/interface.cpp b/framework/domain/mimetreeparser/interface.cpp index d6354c9e..c3ecf79c 100644 --- a/framework/domain/mimetreeparser/interface.cpp +++ b/framework/domain/mimetreeparser/interface.cpp @@ -706,6 +706,11 @@ Parser::~Parser() { } +QUrl Parser::getPart(const QByteArray &cid) +{ + return d->mEmbeddedPartMap.value(cid); +} + QVector Parser::collectContentParts() const { return collect(d->mTree, [](const Part::Ptr &p){return p->type() != "EncapsulatedPart";}, diff --git a/framework/domain/mimetreeparser/interface.h b/framework/domain/mimetreeparser/interface.h index a482a824..7eadc311 100644 --- a/framework/domain/mimetreeparser/interface.h +++ b/framework/domain/mimetreeparser/interface.h @@ -181,6 +181,7 @@ public: private: std::unique_ptr d; }; + class Part { public: @@ -346,6 +347,7 @@ public: ~Parser(); Part::Ptr getPart(QUrl url); + QUrl getPart(const QByteArray &cid); QVector collect(const Part::Ptr &start, std::function select, std::function filter) const; QVector collectContentParts() const; diff --git a/framework/domain/mimetreeparser/interface_p.h b/framework/domain/mimetreeparser/interface_p.h index 004a50d0..55d1a5cc 100644 --- a/framework/domain/mimetreeparser/interface_p.h +++ b/framework/domain/mimetreeparser/interface_p.h @@ -46,7 +46,6 @@ public: void createTree(const MimeTreeParser::MessagePartPtr& start, const Part::Ptr& tree); Part::Ptr mTree; -private: Parser *q; MimeTreeParser::MessagePartPtr mPartTree; -- cgit v1.2.3 From 1974c19eadd497e355ac985a00d0571f3e6c7712 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sandro=20Knau=C3=9F?= Date: Tue, 11 Oct 2016 16:18:50 +0200 Subject: create model for new mailviewer --- framework/domain/CMakeLists.txt | 2 + framework/domain/messageparser.cpp | 123 +---------------- framework/domain/messageparser.h | 57 ++++++++ framework/domain/messageparser_new.cpp | 148 +++++++++++++++++++++ framework/domain/messageparser_old.cpp | 140 +++++++++++++++++++ framework/domain/mimetreeparser/CMakeLists.txt | 5 +- framework/domain/mimetreeparser/interface.cpp | 14 +- framework/domain/mimetreeparser/interface.h | 2 +- .../data/openpgp-inline-encrypted+nonenc.mbox | 31 +++++ .../domain/mimetreeparser/tests/interfacetest.cpp | 27 +++- 10 files changed, 426 insertions(+), 123 deletions(-) create mode 100644 framework/domain/messageparser_new.cpp create mode 100644 framework/domain/messageparser_old.cpp create mode 100644 framework/domain/mimetreeparser/tests/data/openpgp-inline-encrypted+nonenc.mbox (limited to 'framework') diff --git a/framework/domain/CMakeLists.txt b/framework/domain/CMakeLists.txt index 07f01367..c41da377 100644 --- a/framework/domain/CMakeLists.txt +++ b/framework/domain/CMakeLists.txt @@ -7,6 +7,8 @@ set(mailplugin_SRCS stringhtmlwriter.cpp composercontroller.cpp messageparser.cpp + messageparser_new.cpp + messageparser_old.cpp mailtemplates.cpp retriever.cpp accountfactory.cpp diff --git a/framework/domain/messageparser.cpp b/framework/domain/messageparser.cpp index febd1363..ef9fb0d2 100644 --- a/framework/domain/messageparser.cpp +++ b/framework/domain/messageparser.cpp @@ -33,125 +33,6 @@ #include #include -PartModel::PartModel(QSharedPointer partTree, std::shared_ptr parser) - : mPartTree(partTree) - , mParser(parser) -{ -} - -QHash PartModel::roleNames() const -{ - QHash roles; - roles[Text] = "text"; - roles[IsHtml] = "isHtml"; - roles[IsHidden] = "isHidden"; - roles[IsEncrypted] = "isEncrypted"; - roles[IsAttachment] = "isAttachment"; - roles[HasContent] = "hasContent"; - roles[Type] = "type"; - roles[IsHidden] = "isHidden"; - return roles; -} - -QModelIndex PartModel::index(int row, int column, const QModelIndex &parent) const -{ - // qDebug() << "index " << parent << row << column << mPartTree->subParts().size(); - if (!parent.isValid()) { - if (row < mPartTree->subParts().size()) { - auto part = mPartTree->subParts().at(row); - return createIndex(row, column, part.data()); - } - } else { - auto part = static_cast(parent.internalPointer()); - auto subPart = part->subParts().at(row); - return createIndex(row, column, subPart.data()); - } - return QModelIndex(); -} - -QVariant PartModel::data(const QModelIndex &index, int role) const -{ - // qDebug() << "Getting data for index"; - if (index.isValid()) { - auto part = static_cast(index.internalPointer()); - switch (role) { - case Text: { - // qDebug() << "Getting text: " << part->property("text").toString(); - // FIXME: we should have a list per part, and not one for all parts. - auto text = part->property("htmlContent").toString(); - auto rx = QRegExp("src=(\"|')cid:([^\1]*)\1"); - int pos = 0; - while ((pos = rx.indexIn(text, pos)) != -1) { - auto repl = mParser->getPart(rx.cap(2).toUtf8()); - if (repl.isValid()) { - text.replace(rx.cap(0), QString("src=\"%1\"").arg(repl.toString())); - } - pos += rx.matchedLength(); - } - return text; - } - case IsAttachment: - return part->property("attachment").toBool(); - case IsEncrypted: - return part->property("isEncrypted").toBool(); - case IsHtml: - return part->property("isHtml").toBool(); - case HasContent: - return !part->property("htmlContent").toString().isEmpty(); - case Type: - return part->metaObject()->className(); - case IsHidden: - return false; - //return part->property("isHidden").toBool(); - - } - } - return QVariant(); -} - -QModelIndex PartModel::parent(const QModelIndex &index) const -{ - // qDebug() << "parent " << index; - if (index.isValid()) { - auto part = static_cast(index.internalPointer()); - auto parentPart = static_cast(part->parentPart()); - auto row = 0;//get the parents parent to find the index - if (!parentPart) { - parentPart = mPartTree.data(); - } - int i = 0; - for (const auto &p : parentPart->subParts()) { - if (p.data() == part) { - row = i; - break; - } - i++; - } - return createIndex(row, index.column(), parentPart); - } - return QModelIndex(); -} - -int PartModel::rowCount(const QModelIndex &parent) const -{ - // qDebug() << "Row count " << parent; - if (!parent.isValid()) { - // qDebug() << "Row count " << mPartTree->subParts().size(); - return mPartTree->subParts().size(); - } else { - auto part = static_cast(parent.internalPointer()); - if (part) { - return part->subParts().size(); - } - } - return 0; -} - -int PartModel::columnCount(const QModelIndex &parent) const -{ - // qDebug() << "Column count " << parent; - return 1; -} class MessagePartPrivate { @@ -217,3 +98,7 @@ QAbstractItemModel *MessageParser::partTree() const return new PartModel(d->mPartTree, d->mParser); } +QAbstractItemModel *MessageParser::newTree() const +{ + return new NewModel(d->mParser); +} diff --git a/framework/domain/messageparser.h b/framework/domain/messageparser.h index 9469f2b5..b3d7537d 100644 --- a/framework/domain/messageparser.h +++ b/framework/domain/messageparser.h @@ -32,6 +32,10 @@ class QAbstractItemModel; class Parser; +class Part; +typedef std::shared_ptr PartPtr; +class Content; +typedef std::shared_ptr ContentPtr; class MessagePartPrivate; class MessageParser : public QObject @@ -40,6 +44,7 @@ class MessageParser : public QObject Q_PROPERTY (QVariant message READ message WRITE setMessage) Q_PROPERTY (QString html READ html NOTIFY htmlChanged) Q_PROPERTY (QAbstractItemModel* partTree READ partTree NOTIFY htmlChanged) + Q_PROPERTY (QAbstractItemModel* newTree READ newTree NOTIFY htmlChanged) public: explicit MessageParser(QObject *parent = Q_NULLPTR); @@ -50,6 +55,7 @@ public: QVariant message() const; void setMessage(const QVariant &to); QAbstractItemModel *partTree() const; + QAbstractItemModel *newTree() const; signals: void htmlChanged(); @@ -87,3 +93,54 @@ private: std::shared_ptr mParser; }; + +class NewContentModel : public QAbstractItemModel { + Q_OBJECT +public: + NewContentModel (const PartPtr &part); + +public: + enum Roles { + TypeRole = Qt::UserRole + 1, + ContentRole, + IsEmbededRole, + SecurityLevelRole + }; + + QHash roleNames() const Q_DECL_OVERRIDE; + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; + QModelIndex parent(const QModelIndex &index) const Q_DECL_OVERRIDE; + int rowCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; + int columnCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; + +private: + const PartPtr &mPart; +}; + +class NewModel : public QAbstractItemModel { + Q_OBJECT +public: + NewModel(std::shared_ptr parser); + +public: + enum Roles { + TypeRole = Qt::UserRole + 1, + ContentsRole, + IsEmbededRole, + SecurityLevelRole + }; + + QHash roleNames() const Q_DECL_OVERRIDE; + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; + QModelIndex parent(const QModelIndex &index) const Q_DECL_OVERRIDE; + int rowCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; + int columnCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; + +private: + std::shared_ptr mParser; + QVector mParts; + QMap> mContentMap; +}; + diff --git a/framework/domain/messageparser_new.cpp b/framework/domain/messageparser_new.cpp new file mode 100644 index 00000000..d1b956f5 --- /dev/null +++ b/framework/domain/messageparser_new.cpp @@ -0,0 +1,148 @@ + +/* + 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 "messageparser.h" +#include "mimetreeparser/interface.h" + +#include + +NewModel::NewModel(std::shared_ptr parser) + : mParser(parser) +{ + mParts = mParser->collectContentParts(); + foreach(const auto &part, mParts) { + mContentMap.insert(part.get(), std::shared_ptr(new NewContentModel(part))); + } +} + +QHash NewModel::roleNames() const +{ + QHash roles; + roles[TypeRole] = "type"; + roles[ContentsRole] = "contents"; + roles[IsEmbededRole] = "embeded"; + roles[SecurityLevelRole] = "securityLevel"; + return roles; +} + +QModelIndex NewModel::index(int row, int column, const QModelIndex &parent) const +{ + if (!parent.isValid()) { + if (row < mParts.size()) { + auto part = mParts.at(row); + return createIndex(row, column, part.get()); + } + } + return QModelIndex(); +} + +QVariant NewModel::data(const QModelIndex &index, int role) const +{ + if (!index.parent().isValid()) { + auto part = static_cast(index.internalPointer()); + switch (role) { + case TypeRole: + return QString::fromLatin1(part->type()); + case IsEmbededRole: + return index.parent().isValid(); + case SecurityLevelRole: + return QStringLiteral("GRAY"); + case ContentsRole: + return QVariant::fromValue(mContentMap.value(part).get()); + } + } + return QVariant(); +} + +QModelIndex NewModel::parent(const QModelIndex &index) const +{ + return QModelIndex(); +} + +int NewModel::rowCount(const QModelIndex &parent) const +{ + if (!parent.isValid()) { + return mParts.size(); + } + return 0; +} + +int NewModel::columnCount(const QModelIndex &parent) const +{ + return 1; +} + +NewContentModel::NewContentModel(const Part::Ptr &part) + : mPart(part) +{ +} + +QHash NewContentModel::roleNames() const +{ + QHash roles; + roles[TypeRole] = "type"; + roles[ContentRole] = "content"; + roles[IsEmbededRole] = "embeded"; + roles[SecurityLevelRole] = "securityLevel"; + return roles; +} + +QModelIndex NewContentModel::index(int row, int column, const QModelIndex &parent) const +{ + if (!parent.isValid()) { + if (row < mPart->content().size()) { + auto part = mPart->content().at(row); + return createIndex(row, column, part.get()); + } + } + return QModelIndex(); +} + +QVariant NewContentModel::data(const QModelIndex &index, int role) const +{ + auto content = static_cast(index.internalPointer()); + switch (role) { + case TypeRole: + return QString::fromLatin1(content->type()); + case IsEmbededRole: + return false; + case ContentRole: + return content->encodedContent(); + case SecurityLevelRole: + return content->encryptions().size() > mPart->encryptions().size() ? "red": "black"; //test for gpg inline + } + return QVariant(); +} + +QModelIndex NewContentModel::parent(const QModelIndex &index) const +{ + return QModelIndex(); +} + +int NewContentModel::rowCount(const QModelIndex &parent) const +{ + if (!parent.isValid()) { + return mPart->content().size(); + } + return 0; +} + +int NewContentModel::columnCount(const QModelIndex &parent) const +{ + return 1; +} diff --git a/framework/domain/messageparser_old.cpp b/framework/domain/messageparser_old.cpp new file mode 100644 index 00000000..a364c8ab --- /dev/null +++ b/framework/domain/messageparser_old.cpp @@ -0,0 +1,140 @@ +/* + 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 "messageparser.h" +#include "mimetreeparser/interface.h" + +PartModel::PartModel(QSharedPointer partTree, std::shared_ptr parser) + : mPartTree(partTree) + , mParser(parser) +{ +} + +QHash PartModel::roleNames() const +{ + QHash roles; + roles[Text] = "text"; + roles[IsHtml] = "isHtml"; + roles[IsHidden] = "isHidden"; + roles[IsEncrypted] = "isEncrypted"; + roles[IsAttachment] = "isAttachment"; + roles[HasContent] = "hasContent"; + roles[Type] = "type"; + roles[IsHidden] = "isHidden"; + return roles; +} + +QModelIndex PartModel::index(int row, int column, const QModelIndex &parent) const +{ + // qDebug() << "index " << parent << row << column << mPartTree->subParts().size(); + if (!parent.isValid()) { + if (row < mPartTree->subParts().size()) { + auto part = mPartTree->subParts().at(row); + return createIndex(row, column, part.data()); + } + } else { + auto part = static_cast(parent.internalPointer()); + auto subPart = part->subParts().at(row); + return createIndex(row, column, subPart.data()); + } + return QModelIndex(); +} + +QVariant PartModel::data(const QModelIndex &index, int role) const +{ + // qDebug() << "Getting data for index"; + if (index.isValid()) { + auto part = static_cast(index.internalPointer()); + switch (role) { + case Text: { + // qDebug() << "Getting text: " << part->property("text").toString(); + // FIXME: we should have a list per part, and not one for all parts. + auto text = part->property("htmlContent").toString(); + auto rx = QRegExp("src=(\"|')cid:([^\1]*)\1"); + int pos = 0; + while ((pos = rx.indexIn(text, pos)) != -1) { + auto repl = mParser->getPart(rx.cap(2).toUtf8()); + if (repl.isValid()) { + text.replace(rx.cap(0), QString("src=\"%1\"").arg(repl.toString())); + } + pos += rx.matchedLength(); + } + return text; + } + case IsAttachment: + return part->property("attachment").toBool(); + case IsEncrypted: + return part->property("isEncrypted").toBool(); + case IsHtml: + return part->property("isHtml").toBool(); + case HasContent: + return !part->property("htmlContent").toString().isEmpty(); + case Type: + return part->metaObject()->className(); + case IsHidden: + return false; + //return part->property("isHidden").toBool(); + + } + } + return QVariant(); +} + +QModelIndex PartModel::parent(const QModelIndex &index) const +{ + // qDebug() << "parent " << index; + if (index.isValid()) { + auto part = static_cast(index.internalPointer()); + auto parentPart = static_cast(part->parentPart()); + auto row = 0;//get the parents parent to find the index + if (!parentPart) { + parentPart = mPartTree.data(); + } + int i = 0; + for (const auto &p : parentPart->subParts()) { + if (p.data() == part) { + row = i; + break; + } + i++; + } + return createIndex(row, index.column(), parentPart); + } + return QModelIndex(); +} + +int PartModel::rowCount(const QModelIndex &parent) const +{ + // qDebug() << "Row count " << parent; + if (!parent.isValid()) { + // qDebug() << "Row count " << mPartTree->subParts().size(); + return mPartTree->subParts().size(); + } else { + auto part = static_cast(parent.internalPointer()); + if (part) { + return part->subParts().size(); + } + } + return 0; +} + +int PartModel::columnCount(const QModelIndex &parent) const +{ + // qDebug() << "Column count " << parent; + return 1; +} + diff --git a/framework/domain/mimetreeparser/CMakeLists.txt b/framework/domain/mimetreeparser/CMakeLists.txt index e1c04893..64da2656 100644 --- a/framework/domain/mimetreeparser/CMakeLists.txt +++ b/framework/domain/mimetreeparser/CMakeLists.txt @@ -9,4 +9,7 @@ add_library(mimetreeparser SHARED ${mimetreeparser_SRCS}) qt5_use_modules(mimetreeparser Core Gui) target_link_libraries(mimetreeparser KF5::Mime KF5::MimeTreeParser) -add_subdirectory(tests) \ No newline at end of file +install(TARGETS mimetreeparser + DESTINATION ${LIB_INSTALL_DIR}) + +add_subdirectory(tests) diff --git a/framework/domain/mimetreeparser/interface.cpp b/framework/domain/mimetreeparser/interface.cpp index c3ecf79c..efa0fd40 100644 --- a/framework/domain/mimetreeparser/interface.cpp +++ b/framework/domain/mimetreeparser/interface.cpp @@ -30,6 +30,7 @@ #include #include +#include #include class MailMimePrivate @@ -356,6 +357,17 @@ QByteArray Content::charset() const return d->mCodec; } +QString Content::encodedContent() const +{ + return encodedContent(charset()); +} + +QString Content::encodedContent(const QByteArray &charset) const +{ + QTextCodec *codec = QTextCodec::codecForName(charset); + return codec->toUnicode(content()); +} + QByteArray Content::type() const { return "Content"; @@ -496,7 +508,7 @@ void SinglePartPrivate::fillFrom(MimeTreeParser::TextMessagePart::Ptr part) mContent.clear(); foreach (const auto &mp, part->subParts()) { auto d_ptr = new ContentPrivate; - d_ptr->mContent = part->text().toLocal8Bit(); + d_ptr->mContent = mp->text().toLocal8Bit(); d_ptr->mParent = q; d_ptr->mCodec = "utf-8"; const auto enc = mp.dynamicCast(); diff --git a/framework/domain/mimetreeparser/interface.h b/framework/domain/mimetreeparser/interface.h index 7eadc311..67246f37 100644 --- a/framework/domain/mimetreeparser/interface.h +++ b/framework/domain/mimetreeparser/interface.h @@ -125,7 +125,7 @@ public: QString encodedContent() const; // overwrite default charset with given charset - QString encodedContent(QByteArray charset) const; + QString encodedContent(const QByteArray &charset) const; QVector signatures() const; QVector encryptions() const; diff --git a/framework/domain/mimetreeparser/tests/data/openpgp-inline-encrypted+nonenc.mbox b/framework/domain/mimetreeparser/tests/data/openpgp-inline-encrypted+nonenc.mbox new file mode 100644 index 00000000..b98dc336 --- /dev/null +++ b/framework/domain/mimetreeparser/tests/data/openpgp-inline-encrypted+nonenc.mbox @@ -0,0 +1,31 @@ +From test@kolab.org Wed, 25 May 2011 23:49:40 +0100 +From: OpenPGP Test +To: test@kolab.org +Subject: inlinepgpencrypted + non enc text +Date: Wed, 25 May 2011 23:49:40 +0100 +Message-ID: <1786696.yKXrOjjflF@herrwackelpudding.localhost> +X-KMail-Transport: GMX +X-KMail-Fcc: 28 +X-KMail-Drafts: 7 +X-KMail-Templates: 9 +User-Agent: KMail/4.6 beta5 (Linux/2.6.34.7-0.7-desktop; KDE/4.6.41; x86_64; git-0269848; 2011-04-19) +MIME-Version: 1.0 +Content-Transfer-Encoding: 7Bit +Content-Type: text/plain; charset="us-ascii" + +Not encrypted not signed :( + +-----BEGIN PGP MESSAGE----- +Version: GnuPG v2.0.15 (GNU/Linux) + +hQEMAwzOQ1qnzNo7AQf/a3aNTLpQBfcUr+4AKsZQLj4h6z7e7a5AaCW8AG0wrbxN +kBYB7E5jdZh45DX/99gvoZslthWryUCX2kKZ3LtIllxKVjqNuK5hSt+SAuKkwiMR +Xcbf1KFKENKupgGSO9B2NJRbjoExdJ+fC3mGXnO3dT7xJJAo3oLE8Nivu+Bj1peY +E1wCf+vcTwVHFrA7SV8eMRb9Z9wBXmU8Q8e9ekJ7ZsRX3tMeBs6jvscVvfMf6DYY +N14snZBZuGNKT9a3DPny7IC1S0lHcaam34ogWwMi3FxPGJt/Lg52kARlkF5TDhcP +N6H0EB/iqDRjOOUoEVm8um5XOSR1FpEiAdD0DON3y9JPATnrYq7sgYZz3BVImYY+ +N/jV8fEiN0a34pcOq8NQedMuOsJHNBS5MtbQH/kJLq0MXBpXekGlHo4MKw0trISc +Rw3pW6/BFfhPJLni29g9tw== +=fRFW +-----END PGP MESSAGE----- + diff --git a/framework/domain/mimetreeparser/tests/interfacetest.cpp b/framework/domain/mimetreeparser/tests/interfacetest.cpp index ac77b025..5a3cbb87 100644 --- a/framework/domain/mimetreeparser/tests/interfacetest.cpp +++ b/framework/domain/mimetreeparser/tests/interfacetest.cpp @@ -198,7 +198,32 @@ private slots: auto contentAttachmentList = parser.collectAttachmentParts(); QCOMPARE(contentAttachmentList.size(), 0); } + + void testOpenPPGInlineWithNonEncText() + { + Parser parser(readMailFromFile("openpgp-inline-encrypted+nonenc.mbox")); + printTree(parser.d->mTree,QString()); + auto contentPartList = parser.collectContentParts(); + QCOMPARE(contentPartList.size(), 1); + auto contentPart = contentPartList[0]; + QVERIFY((bool)contentPart); + QCOMPARE(contentPart->availableContents(), QVector() << "plaintext"); + QCOMPARE(contentPart->encryptions().size(), 0); + QCOMPARE(contentPart->signatures().size(), 0); + auto contentList = contentPart->content("plaintext"); + QCOMPARE(contentList.size(), 2); + QCOMPARE(contentList[0]->content(), QStringLiteral("Not encrypted not signed :(\n\n").toLocal8Bit()); + QCOMPARE(contentList[0]->charset(), QStringLiteral("utf-8").toLocal8Bit()); + QCOMPARE(contentList[0]->encryptions().size(), 0); + QCOMPARE(contentList[0]->signatures().size(), 0); + QCOMPARE(contentList[1]->content(), QStringLiteral("some random text").toLocal8Bit()); + QCOMPARE(contentList[1]->charset(), QStringLiteral("utf-8").toLocal8Bit()); + QCOMPARE(contentList[1]->encryptions().size(), 1); + QCOMPARE(contentList[1]->signatures().size(), 0); + auto contentAttachmentList = parser.collectAttachmentParts(); + QCOMPARE(contentAttachmentList.size(), 0); + } }; QTEST_GUILESS_MAIN(InterfaceTest) -#include "interfacetest.moc" \ No newline at end of file +#include "interfacetest.moc" -- cgit v1.2.3 From b40e6c476e54c5dab834c4d01936d1f7bc33c60e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sandro=20Knau=C3=9F?= Date: Wed, 12 Oct 2016 16:41:00 +0200 Subject: Make nested mails work with mailviewer --- framework/domain/mimetreeparser/interface.cpp | 50 +- framework/domain/mimetreeparser/interface.h | 3 + .../mimetreeparser/tests/data/cid-links.mbox | 1384 ++++++++++++++++++++ .../domain/mimetreeparser/tests/interfacetest.cpp | 15 + 4 files changed, 1445 insertions(+), 7 deletions(-) create mode 100644 framework/domain/mimetreeparser/tests/data/cid-links.mbox (limited to 'framework') diff --git a/framework/domain/mimetreeparser/interface.cpp b/framework/domain/mimetreeparser/interface.cpp index efa0fd40..596dc152 100644 --- a/framework/domain/mimetreeparser/interface.cpp +++ b/framework/domain/mimetreeparser/interface.cpp @@ -36,14 +36,24 @@ class MailMimePrivate { public: - KMime::Content *mNode; + MailMimePrivate(MailMime *p); + MailMime *q; + KMime::Content *mNode; + std::shared_ptr parent; }; +MailMimePrivate::MailMimePrivate(MailMime* p) + : q(p) + , mNode(nullptr) + , parent(nullptr) +{ +} + + MailMime::MailMime() - : d(std::unique_ptr(new MailMimePrivate())) + : d(std::unique_ptr(new MailMimePrivate(this))) { - d->q = this; } bool MailMime::isFirstTextPart() const @@ -54,6 +64,14 @@ bool MailMime::isFirstTextPart() const return (d->mNode->topLevel()->textContent() == d->mNode); } +bool MailMime::isFirstPart() const +{ + if (!d->mNode || !d->mNode->parent()) { + return false; + } + return (d->mNode->parent()->contents().first() == d->mNode); +} + bool MailMime::isTopLevelPart() const { if (!d->mNode) { @@ -108,6 +126,15 @@ QMimeType MailMime::mimetype() const return mimeDb.mimeTypeForName(ct->mimeType()); } +MailMime::Ptr MailMime::parent() const +{ + if (!d->parent) { + d->parent = std::shared_ptr(new MailMime()); + d->parent->d->mNode = d->mNode->parent(); + } + return d->parent; +} + class PartPrivate { public: @@ -285,6 +312,11 @@ MailMime::Ptr Part::mailMime() const return d->mailMime(); } +Part *Part::parent() const +{ + return d->parent(); +} + class ContentPrivate { public: @@ -738,12 +770,16 @@ QVector Parser::collectContentParts() const } { - const auto parent = content->parent(); - if (parent) { - const auto _mime = parent->mailMime(); + auto _mime = content->parent()->mailMime(); + while (_mime) { if (_mime && (_mime->isTopLevelPart() || _mime->isFirstTextPart())) { return true; } + if (_mime->isFirstPart()) { + _mime = _mime->parent(); + } else { + break; + } } } const auto cd = mime->disposition(); @@ -830,4 +866,4 @@ QVector Parser::collect(const Part::Ptr &start, std::function d; diff --git a/framework/domain/mimetreeparser/tests/data/cid-links.mbox b/framework/domain/mimetreeparser/tests/data/cid-links.mbox new file mode 100644 index 00000000..40ff5282 --- /dev/null +++ b/framework/domain/mimetreeparser/tests/data/cid-links.mbox @@ -0,0 +1,1384 @@ +Message-ID: <851f01d15e53$31734730$790bc9ad@info> +From: "OculusLab" +To: +Subject: CID links for images +Date: Wed, 03 Feb 2016 07:19:17 +0200 +MIME-Version: 1.0 +Content-Type: multipart/related; + type="multipart/alternative"; + boundary="----=_NextPart_000_000F_01D15E52.0BD654A0" +X-MSMail-Priority: Normal +X-Mailer: Microsoft Windows Live Mail 14.0.8117.416 +X-MimeOLE: Produced By Microsoft MimeOLE V14.0.8117.416 + + This is a multi-part message in MIME format. + +------=_NextPart_000_000F_01D15E52.0BD654A0 +Content-Type: multipart/alternative; + boundary="----=_NextPart_000_0010_01D15E52.0BD654A0" + +------=_NextPart_000_0010_01D15E52.0BD654A0 +Content-Type: text/plain; + charset="windows-1251" +Content-Transfer-Encoding: quoted-printable + +=0D=0A=0D=0A=0D=0A=0D=0ASuperkombipackung für nur 45 Euro=0D= +=0A=0D=0A +------=_NextPart_000_0010_01D15E52.0BD654A0 +Content-Type: text/html; + charset="windows-1251" +Content-Transfer-Encoding: quoted-printable + +=0D=0A=0D=0A=0D=0A=0D=0A + +------=_NextPart_000_0010_01D15E52.0BD654A0-- + +------=_NextPart_000_000F_01D15E52.0BD654A0 +Content-Type: image/jpeg; + name="aqnaozisxya.jpeg" +Content-Transfer-Encoding: base64 +Content-ID: <9359201d15e53f31a68c307b3369b6@info> + +/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAAA8AAD/4QMqaHR0cDov +L25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENl +aGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4 +OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjUtYzAxNCA3OS4xNTE0ODEsIDIwMTMvMDMvMTMtMTI6 +MDk6MTUgICAgICAgICI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5 +OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHht +bG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6 +Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUu +Y29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBo +b3Rvc2hvcCBDQyAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6QjdCRTg5MTBD +OUNGMTFFNUJBOTdEMkQyNzU0ODI3RDciIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6QjdCRTg5 +MTFDOUNGMTFFNUJBOTdEMkQyNzU0ODI3RDciPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5z +dGFuY2VJRD0ieG1wLmlpZDpCN0JFODkwRUM5Q0YxMUU1QkE5N0QyRDI3NTQ4MjdENyIgc3RSZWY6 +ZG9jdW1lbnRJRD0ieG1wLmRpZDpCN0JFODkwRkM5Q0YxMUU1QkE5N0QyRDI3NTQ4MjdENyIvPiA8 +L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0i +ciI/Pv/uAA5BZG9iZQBkwAAAAAH/2wCEAAYEBAQFBAYFBQYJBgUGCQsIBgYICwwKCgsKCgwQDAwM +DAwMEAwODxAPDgwTExQUExMcGxsbHB8fHx8fHx8fHx8BBwcHDQwNGBAQGBoVERUaHx8fHx8fHx8f +Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fH//AABEIAvsCigMBEQACEQED +EQH/xADJAAEAAgMBAQEAAAAAAAAAAAAAAwQBAgUGBwgBAQEBAQEBAQAAAAAAAAAAAAABAgMEBQYQ +AAEEAgEDAgQCBAgKCQMACwIAAQMEEQUSIRMGMUFRIjIUYXGBQhUHkaGxUiMzFhfB0WJy0+OkZZVW +8ILSsyQ0lFU24UN1U4MlssJzhLQ1djcRAQABAgMEBQkFBwQBBAIDAAABEQIhMQNBUWEScYGRoQTw +scHRIjJSYhPhQpKyFfFygqIzUwXCI2Nzk9JDsxSDNPLD0//aAAwDAQACEQMRAD8A/VKAgICAgICA +gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA +gICAgICAgICAgICAgICAgICAg1y6Bl0DLoGXQMugZdBrIZMBOz9WZBpWkM68Zk+SIWd3QS8nQOTo +HJ0Dk6BydA5OgcnQOToGXQMugZdAy6Bl0DLoGXQMugOT4dAYnwyA7ughaWTPqgkIyaNnz16INQkN +3bL+6BJIbHhn6INozJ2fL+6DbLoI5JDYmZn6KDJGTR8s9VQiM39Xyg1GWRy6v0ygkMiYXdn6sghG +aRxd8/yItGAnldupfxMoUZ70nx/kQO9J8f5EKAzSO3r/ABMhRnuyfH+RCg0snXqgNNI/v/Igd2T4 +/wAiDLSyfFCjDSyfH+RUZ7snx/kUBpTz6oMd2TPr/Igz3JPj/Igy0knx/kQY7smX6oMd2T4/yIMj +Kb+6B3T5O2eiVKHdP4oUO5J8UqtGHlk5Yz/IpUoz3ZPiqUO7J8UKDynluqIy8h/FA7h/FKlApDZv +VAaU/ilSjPcP4oDyHjOUGO4eG6oUZ5l8UGeZY9VUatIePVRR5Dz6pUoyxnn1QBkN3fqgy5l8UQYy ++KDcXd2fPxVGUBBqgICAgICDSb+qL8nQR0//ACkP+YyEpkBAQEBAQEBEEBAQEBAQEBAL0dAb0ZFH +9HQQD6oJJP6pv0INY/Vvz/wIMS/1n8CCSL6X/N0G6CKT62UGS/qv0/4VQhQRh9Tfmglm/qyQQD9D +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 +6/oo2w6Iy3oisug1QYRGW9UVsiCKwgwgICDCAgwgyyCnfiCRmYxYhJsOzrF00lu3Fw7Ol6u8J4b+ +aX+NI1Fmxz5YZIvkkbBN8PgukTViYom1sxMfa9Xd8MuV8OlsvSRBxFh+H8qwqYWVgSMqyytDUzYR +cnfDN1d/wZQeQ2Ft7Vo5f1fQG/yW9F2tjBiZVSdlWXpfH6ghSad2/pJny7v8GfDKDqY9kG3p0Qbe +35oCDP4INmUBAd8MgrkOCz8VpGweuP4FBszoMOzKg3wRWRfOVEbCitmUGyAoMICAgINx9EVlAQao +CAgwgICDDogisIMOyDCDCBhBhAQYwgYQZwgccoMPGLoNeyPwZBloRb2QSMDMg2ZsIMogg1k+h0FM +v6+D/Of+R1Wdq6/oo2wiMt6IrLoNVRhRGW9UVsiCKwgwgwgygwgwgINZJBBsv1z6MpMrEOfanPPM +vpb1b4MuV0u1sNc8myyzRXK2kfUT/Q66acsakMaOtztFM/pE2G/zn/xLWpLNr0IsuTaRmWkbsqg6 +DjeQX+3C1YH+eX6/wD/6rdsJMudrNTLaxJJmOv8AH3L/ADf8a6ObuHq6nZEBiBhB+RM45d2/P1QW +w6C2McW6fL6N+hBI2PX+BQFUG9VFbfigMg2woDvhkGj/ABVRgmz/AIFRoPq6Df2yorHtj4KoIp+s +ojLP1RWzOg3ZQZ9lBrl1FMqh1QMug3D0/Sg2QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA +QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA +QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA +QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA +QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA +QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA +QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA +QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA +QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA +QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA +QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA +QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA +QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA +QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA +QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA +QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA +QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA +QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQRWbVWpAdi1MEFeNsyTS +kwALZx1InZmQYK5TG2FMp42tyAUsddzFpCAHZiMQzycWcmZ3SPMSji2urmnGCK5BJOTyCMQSARu8 +BMMrMLPn+jJ2Yvg/qkYk4eXX5sVpSZpFRW12zo7LXwbGlL3adgGkhlwQZB/fBsJN+llq6KZm2nGn +Zglq2q1uvHZqzBYryixRTRExgQv6OJDlnZJigRWq0ss0MUwSS13YZ4xJnKMiFiZjZuou4uztn2U2 +VEiAg5Nvy3xinParWNrVG3SiOe1TaUDsBHGHcMngF3l6B830+ibK7PKPOR70W7ZdSKWOWIJY35Ry +CxA/plibLeqt1sxNJS26JisbWyiiAgIIq1uraAjrTRzgBlEZRkxsxxvxMHcXfBCTYdvZNlTglQEB +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 +gkD6gLD9CH3Z0nKuw20ZrWq1qvHZqyhPXlFiimiJjAhf0cSHLOysxQSKAgIK02ypQ7Ctr5JONy2E +sleLiT8hh49x+TNxbj3B9XSMa8CcIrxp559CygICAgICAgICAgICAgICAgICAgICAgICAgICAgIC +AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgION5nsL2u8V2l6g/G3Xrmc +R45cMN1k4v68GyWPwUwmYicIm62J6JmInuWK40is0mkb5phHXLx/kwjrquz19C/Zu07vjuxtWgs2 +JLeCjABhmE5SNwaRpDbiLsL46N0V1MromKcs29VZnDu27uLWh71k1xm7tjfwph2tNtJd00FiGnet +8bfjF66by2JZHCzVGFo5YeRYhfEz9I+LenToumvnqcLre+bq+aHLwsf0p34T2W+XnN1sdx48xy62 +3ZszWtBZvSNamksM1iucAtMAydwY8DOTkICwvj6VdSI5r4yiL7I6IuuuiceiOpnSmZtsuzmYu66W +xMd+HGubt+JU9/X2wzTzwfsu1UeRoW2tnaSSysYOE8b2YYe2HEiYmB+PUejJdSImJzrGzLOvHdnu +IrNJ8p9H7XK8mtWNLutvqKhPHL5dHC+rJv1bhENS0Tf5kJRy/od1zssi+OScou/kmt135buu6HW6 +7kmNThT+KPd7a06LUVvlU1HkloL9ija8ZJq2mpxTyRwxxw14yrCdcSaOfvkX/wBwSznA4wt88zy3 +Zzffj+OlOHs0nfjXczZZETyThbbbGP8ADWbuqa8PZ6Vye92rXl+z2dq+0GreLs1q1iSNou7Qi59s +GIQzykd255ES+bo/VZiPZimMzfNvfbTy3LF2MTOyyLp6pvr5striW9hvtZftUCmlpwsOqtFD+07G +xkjaTYhHIRzTsJxscb4IGJwx+lb06TdSdl8Rlvtuw44xGbN2Vd9l89lMeGc5eh293a3F7yPyHXan +YM0kFfU4qlZKAXI5rBTwhIHJ4ZZohZuQjy9PwdYs92JnH257OS3zTjTpavwmNnsf6vKHm9luNpBs +aBaitdju0G2cF+G5O9+esDDSOc60hnL9w4RnzASP6untxWraYzM+zNueX36eeM8cMcUpNKRHtRfF +I2TPJdMfsw6s3U8kvxyUrRaK1bsNqNVFabZzbexVhYZBkOGZmBpfupC45LujwfDD8Vi+Zt5rqUpd +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 +QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA +QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA +QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA +QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA +QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA +QEBAQEBAQEBAQEBAQEBAQEBBpPPBXgknnkGGCIXOWWR2EAAWyRET9GZm9XdJlYiuEI5r9GGsNqax +FHWNwYJzMRB3kdhDBO+H5OTMPxVpNabWYmJiuxOoqhqt7rdqU7UDklCuXApnhmCEny4v2pTAY5cO +Ls7xkWFaYRO8nCaL6gICAghu3qVGsdq7Yiq1o+sk8xjHGLfiROzMlViHP/tf4k1H9oftuh9hzaL7 +v7qHs9x2d2DucuPLDP0yrNswzE16l6hsdfsao29faiuVTzwsQGMsb46Pgwd2dJiYzImJyVZvJvG4 +KA7GbbU4tecjwhcOxEMLyM7s4NI5ceTcX6Z9lN3Fd/DPgqD574MYmQ+RawhjblI7Xa7sI5Ycv8/R +skzK8spzQsz+V+LV6le7PuKMNO3n7WzJZhGKXD4ftm5cSx+DpNs1ptImJiux0YZoZogmhMZIpGYo +5AdiEhfqzs7dHZ0mKZrE1Qa3aUdnXezRk71djONpeJCJFGTiTg5M3Ict0Iej+zpTCJ34m2Y3LSgI +Kmz2+p1VdrO0uwUK5EwDNZlCEHN2d2FiNxbOGfonAba/Z67ZVmta61DcrE7sM9eQZY3dvXBA7srM +TGaRMSsqKjmtVoCiGeYIinPtQMZMLnI7OXAM/UWBd8N8EjcJEFbY7Klrar27snarsccbnxIvmmkG +IGwLO/UzZkjGYjbJsmd0V7DY7Klrar27snarsccbnxIvmmkGIGwLO/UzZkjGYjbJsmd0V7FlAQVr +OypVrVSpPJwsXjOOqHEn5lGDyE2WZ2bAC79UiK4cK+aPTBM0ivGnl2LKAgIKG13+h1DRvttlV17T +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 +0bkDbJqkdTZ3ZTijEasgQyWP/CyGfIjL5my4ceTlhlrTnGZjPl3fPuyypE8eLN9s8tNnNG35Lq49 +VY3Y0b2r22raDZbSK5Odo95PrnkntyxQQVD2Hbdmw0oRszNxaXtkQM/TDLFkVjTj4ox405qds0jj +ludL5pN8/Dy067bK9lZnvpnWeCPybVb7VQXLQwa+xsYWClHsrOxkHnUtcmlmsxwyPHIYA4AXJuTP +hbtmJmm2l2z93vjGd9GJieWZ/d/Nn30bQbq5L5IbR35JKY+TvUdhlJ42jbUs/Z6Px49/rx9Of4qa +cYW8bb/zzTu7l1MJu4fT9FUU+yuXtvPWi2Vhqp+UjScq85jiFtWxHAJC/wAo91nyzehdehdVNOKx +ZXbbqfmup9i3zSb+EaffNtW0Fu8+0fxqS9aDVftuWn9y9iX7nsjrwtx1/unLvfNKb/Nz5YbjlW2O +aImfhvnri/lj+XzY7Wb55ZmI32dVbce+Ij+Lodrw7YVarbuGzsikrQ7gqVKS5ZKZ8vDCwQDJMRET +8ydmHOc/ipnZbvnm66XXeiOwmKX3bo5fy2+l65YaEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQ +EBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBBpPBBYgkgnjGaCUXCWKRmIDAmwQkL9HZ29WdJh +YmmMIj1uukKsUlWEyp5+0Ioxd4cjwft5b5Pl6dPZWZrNdssxFIpsV6njvj9OCzXqaypXr3Mtbhig +jAJmJnZ+4IizHlnduqkzWKbFjCa7Vn7Cj3Hk+3i7jxdhz4Dnst17ecfR1+n0Sca1258SMKU2ZcPK +jiTeDaGzvW2VunVs14qcFOpRlrRmEPYkkMTj5ZYeknFmYWxhatvmKztma1SYikRsivfTzUdXY6TS +7PtftKhWvdh+UH3MMcvAviHNi4v09lImk1jNdlNjYtRqT2AbI6UBbGMHijuvEDzCD5yDSY5sPX0y +pGFeOfEmMuGSGt4147VjmiraqnBFYMZbAR14gGSSMuQGbCLMRCTZZ39HVrOHDImMZnesTazWzvYe +apDK9uNobTnGJd2IeXEJMt8wtzLDP06upsoVxqqy+L+MzVa1SXUUpKtN81K514ijhd3z/Rg44Dr/ +ADVeaa1rikRFKbF61UqW4HgtQx2IHcXeKUWMHcCYhfiTO3ykzO34qbarwRWtTqrcry2qcFiUoirl +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 +AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB +AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB +AQEBAQEBAQEBAQEBAQEBAQEBBX2Owqa6hYv3JGiqVYymnlfL8QBuRPhuvoykysRWXJi8qleGeezp +b9KvFAdkJrL1AjMI25O3NrBDE7t1/puH44w61dFM/L0Jb7UxTb5dPcvyb/SQ2KtWxfrV7lwRKrUl +miGWTl6cA5ZP/q5V5ZrMRsZi7CJ2Ss3b1KjWO1dsR1asTZlnmMY4xb0yRE7MyzVuIVD8l8cCpFcP +a0xqTAUkNh7ETRmEbsJkJ8uLiLkzO7emVeWa02+vJImsVhuO+0ZSyQjsapSwjJJLG00bkAQlwlIm +zlmAvlJ39H9VNldh6f2oYvJtNZjqy0LUF+vbsfahPWnrnG0nAjxl5B5PgfpDkX4Yy7XlmtOFexKx +SZ3fsVdz5jqaNK1NVnr37FKevBbqRThziexOEGZGHm4ceecO3XCWxWbd100qs4V3xbM9kVdGpu9L +cqzW6d+tZq13IZ7EM0ZxxuDZJjMXcR4t65UnCKzkRjNNqKLybxuamN2La05KZG8Q2QsRFE8gi5uD +GxceTCLu7Z9FZiYzIxyW6GwobCqFuhZit1ZM9uxAYyxlh8PgwdxfDpMTGaRMS1/ams7Pf+7h7Pd+ +37vcDj3ufa7XLOOfc+Tj68unqkRlxWcK8EIeQaE7wUA2VUr0jmIVGnjeUiid2kZo+XJ3Bxdi6dEi +K5E4Zsx77Ry7I9XFsap7OPLyURmjecWZsvmJn5t/AkRWKxkTNJpKz93U78lfvR9+IGllh5NzGMnd +hMhzlhdxfD/g6lcK7Fp3qNjyjxqtFHNY21KGKUI5YpJLEQiUcue2Yu5MzifF+L+/sryzWm1K4V2K +5+ZeOx+Sf2dluxRbJ4opowkliHuPMRCMYM58yk+Xlx4+js/ults3Vps/b3bUumLaV+99nnrh0Sth +5F4+c9qANnUKeiJSXomnjc4AD6ilHlkGH3clNldi0xptbRb3STEIxbCtIRzPWARmjJ3nYO48TYfq +fD5uPrjqryz5cM+xKx5u/Lt2LyiiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC +AgICAgICAgICAgICAgICAgICAgICAgICAgp7mOrLqrcVuqV2rJEQT1AHmUgE2CFhy2css3RE5tWz +MTg+eXK20OltaOgfb2dGeovR2KmzgsM4WHiYa0dV7cYWpCL5mdmchxj3wt31m2ebhTfWuOWym80a +RfZTD2sd1P20yRj47dkl2ut21jcwR7Y4jgjoVa8sM0X28YAJWDqzFXkiKNx/pJQZuhMu3NHNhnbf +M/zTdE8cKccKbnCyJi2K5TZEfy0mKdNZ3Y76vTeeUNjMekt15LYVKFspbhUYorFkGKE4wlCGWKw0 +nAi6sMblh8t6LjZNL6z8Mx11j0RMfZV0mPYpG+OyK+mk9W+jl6XQDF5XqdhCOwuV5v2lamubGuEJ +DNMFUGfthDX7PPtlhijEnfk/XOVu2aVjCPY/119KXYxX5o7rbo9XcgtaHbSeKbMK9eeGZ/IJr08c +McbWJqwXnk5RjOJBI/BmIGIXYsYWbJpGnXZGPD3vTMTv2w3fFZviNsW0/DZWOvGN2/ahtaGzat09 +jrrG2uWrV+EZ7mwqjV7XZqWwCTshXqGzCUosUhhh/lZn6KXRNKRSK239s2xHoS2YznGnL2RfbPrT +WKr2f3f1NGGnsjsNf+z4bcB1ZOLOFqDv8JHHhMJcCMijcmx1LC63XROrbfHu80dUbursYtibbLrZ +xu5buueWcevtxZ8u0e5s7ndHRhsDWcNLPI9eMHKYa1icpxhaYThkkAGAuLs+cM2OrLnpzSImf7l0 +9tkRE/i82GMNXxXCPgp/NWnXFY68cGv7CazttZtIS2uzebaVSuTbKoFZhGtWtMJ9ka1Qm4vILPIY +Yf5WYui3ZPLdGUR7c9c2xHoScbZ30tj+eJ9b0/ilWzX2Xk3dhOGGbaPLWchcRMCqV+RhlvmZ5GLq +3vlYj+nbH7357lu9+Z4W+Z5axFsIta2kbXXZLgeSRXDkCvK8DVj2jWWmabHbIe2/zMLuQ/rMzM7r +WjnZPw2zE9PJdHl6zWyv+aIp/KmfT3h0U3CjKNovKht9IiaR4f2oLvN0bPDs5fl6cfwU0p/p12W3 +flv+w1cfqcYtp1RZ6YWNBFfp+Rw0deNqxqHs2rFuDY0DhemUvcNzr3XGMJecp8eLOZcS+rDJpzW3 +HZbERv2YTHR5jU97DObsd3T5b9i9tTsa7yzYWypWrMOx1kNeo9WCSZnngknIozIGcYstMOCkcR/F +cborp327Zx/lp5bdzrbPtWTsivnhyPDtJcjep95QkBx8UoUyeaImxKzy92H5m6G2W5B6+mV38XNY +1qbbsOOEuXh8J067Ju89rXxqttqRatrNe3BPe8ao0IrH280nZuQ9xyGdwEuy491nzJhvXqr4mOad +WLc7prHZdtTRpbGnMxhbN1eubaeZyg8ctTeKfZznu5dvqdZbiDWyVII6wTSU5ICGOeKrF9wJkXys +ExkT8XJvVTWuia3W7cONKxNKcKbqbmtCJi62Lpyuiftr1znNccX0FhpVKugrzaqSwbFHHWKOuJjT +kaAv6U3fHZHDOHJvd8e6upNdWafNj6OvvcdOKaVtY+HDjv6ncXJ2EBAQEBAQEBAQEBAQEBAQEBAQ +EBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQ +EBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQ +EBAQEBAQEBAQEBAQEHLk8m00E1kLdqGpHWk7Tzzz1xApBi70gt/SOQlGHzExiL46+nVemPCakxE2 +xN1caRE76Rs2zlSu7NOaK08q506aY9DleSfvH8X02toXG2VGd9sbR6lztwxQTZ9ZO+7kLRB+sbM/ +szM5OzP6fC/4rX1b7reW6OT3vZmZjhTfOyMOyJlJvti3mrh5R+3dDqavyGhasNrbFyiO/jj7lzVV +bY2Dib44cYZXH/KKMV5tbwt1sc8Rf9KuF020r5475SLt9Kyjv+SjDLLX1+vt7m1XdhsQ0WhZo3fr +xKWzLWg5Y6uDScmbDu2HZXS8LzRE33W6ds5TdXHqti67rpTi1M7NqTXeS6u5rJ9hIb0YqZnFfC5x +hKvJHhzGbLuLYZ2fkxOLs7Ozuz5U1fC323xbHtc3u8uPNXd5Vrglt1ZmNseqvmxUtf51othsrMVS +3Wm1VaKs/wC1wsRlAdi0ZiFcCbIkXEGf6v1mbC7av+O1dOyJui6L5m72eWa0tiK3eW6U+pbWkT5V +pHbNfKXVrbzSWoJJ6uwrTwQxtNLLFNGYBEXLEhELuzC/Aur9Oj/Bea/w+pbNLrbomZplOe7vatmJ +mkZ+UeeJjqRX/J/G9f8Ab/tDbU6f3bcqnfsRRd0cZzHzJubY69FrT8JraleSy67lzpEzTp3HNFIn +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 +uv1Zw1tS2JjdZE804ZxHuxHCJoxFl0W2x712N08bojCv7113c5uy8ad/EgnoXPIdjDSb7aCtaoRV +pIBuO1S3OEMNGrdmkjrzSHluXJ+vzEvTpeK/36XW6Nk3YzMXzdE8vtW21nUusiJuiI2U4QzbbPLW +JnmtrdH71JiJxzzrtq+s0Pt/sa/2wFFW7QdmMwKIhDi3EXjNhMHZv1SZnZfmNSvNPNjNenv2uunE +RbERknWGxAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE +BAQEBAQEBAQEBAQEFbZavWbSlJQ2dSG9Rm496rZjCaI+JMY8gNiF8ELO2W9V00ta/Tui6yZtujbE +0nthJiubhf3Yfu1/5S03/D6v+jXt/WPGf3tX8d3rZ+nbug/uw/dr/wApab/h9X/Rp+seM/vav47v +WfTt3Qf3Yfu1/wCUtN/w+r/o0/WPGf3tX8d3rPp27oP7sP3a/wDKWm/4fV/0afrHjP72r+O71n07 +d0H92H7tf+UtN/w+r/o0/WPGf3tX8d3rPp27oP7sP3a/8pab/h9X/Rp+seM/vav47vWfTt3QP+6/ +92js7P4npsP06a+q38kafrHjP72r+O71n07d0Iav7pf3X1gIY/FNUTE+X7tSGZ8/g8gm7Ld/+b8b +dnran4pjzH0rdyb+7D92v/KWm/4fV/0ax+seM/vav47vWfTt3Qf3Yfu1/wCUtN/w+r/o0/WPGf3t +X8d3rPp27oP7sP3a/wDKWm/4fV/0afrHjP72r+O71n07d0H92H7tf+UtN/w+r/o0/WPGf3tX8d3r +Pp27oP7sP3a/8pab/h9X/Rp+seM/vav47vWfTt3Qf3Yfu1/5S03/AA+r/o0/WPGf3tX8d3rPp27o +P7sP3a/8pab/AIfV/wBGn6x4z+9q/ju9Z9O3dB/dh+7X/lLTf8Pq/wCjT9Y8Z/e1fx3es+nbuhf0 +/h3iGlsla02j1+stGDxHPTqwwSPG7sTg5RiLuLuLPj8Fx1/H6+tHLqal98Z0uumfPKxbEZQ668jQ +gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA +gICAgICAgqjttUQAY3IHCScqoE0oOxWAJxKFnz1kEgJnH1yzrp9G/wCGcq5bN/RxJmnV5emGo7rT +FLahG/XKWgPO9G0oOUAdfmlbOQb5C6l8HVnQ1KRPLNLssM+jeRjNNvl647QtzqAsVq5Xq42Lrcqc +LygxzNxcsxDnJtxF3+X2SNDUmJnlmlueGXTuZ5opE1z8vTHa3baax2y1uF2ab7V37gf1+cdn1/rM +9OPqp9G/dOVctm/o4rMxHV6cvPHaj2m70uoiCba7Ctr4pTaOOS1NHCJG/oIvI4s7/gtaPh9TVmll +t10xuiZ8xM0is5OZqPNtNd17Xrdivr4ZpZ2pd+xGLzV4p/twsDy4/LKXFx9fqHr1Xo1vAall3LbE +3TERWkThM283L0x6JZ+pGOOEV/l97snDv2uuO01hRRTDcgeKeV4IJGkBxOVncXjB84I+QE3FuvR1 +5fo31mKTWIrls39DXNHZ66efDpVv7TeN/tMtV+1af7UF2EqH3EX3DOX0s8XLnl/boun/ANTW5Ofk +u5PipNO3Im6IwlvV8g0NvYz6yrsqtjZVmzZpRTxnPG2cfPGJOY9fiyl/htW2yL7rbosnKaTSeiSb +oiabTa7/AEWoaJ9tsquvacuED25o4GMv5odwh5P19GU0fDaurX6dt11M6RM+YmYiKzk52p898T2u +62Wmo7KvLf1T4sxDNC7vgWKRwFjc3GLkwmXHDF09WdejW/xuvpadupdbMW35YT1bKY7N8YpzRzcu +39uHTgvVPJfHLlGe/U2tOzRqu42bcViI4onH6mkkEnEXb3y643+E1bLotusui67KJiaz0Qc8Y45Z +8GtryvxepUe5a3FGvUaV672JbMIRtML4KLmRMPNvcfVWzwetddy22XTdStItmtN/RxXmjHgvT3ad +eodyxPHDUAe4diQxGMQxnk5u7CzfiuNundddyxEzdu2nNFK7EFHd6XYPZahfrW3pn27jQTRydk2b +PGTg78Hx7Et6nh9SynNbdbzZViYr0byJrNNrjbb95Xg+t057c9zTsUgsBU517MBs88hMLR8ubAxC +z8iyXQWd36MvXo/4nxOpqRpxZdF1K42zlvy6uM4JN0Umfh8qdM7HoadypdqxW6c8dmpODSQWISGS +MwJsiQGLuJM7ejsvDqad1l023RMXRnE5rExOSVYUQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA +QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQfH6+m8obU6vZx6yb7+LdWS1tKYH +/ovvJ7MklywOP6MXOQer9Wjbp1NxX6m7X0ee6yb45Z0reaY28ttkRbbvnCf4uFtXGYml9fapfWOP +t4dXLSInZ7U5UWLvht4bfkuvgqWD1oamAZrZA7lfsRjZn7YiLZkeSxZeSbDYd8D15Ozc9Px1vLpX +zMc/1JpHwR7FteFLbaW9uyG9O2fqxWaxMRX8V3N2+zHC3D4Up6DyOSz4xsGpShubk5STTOLOOurx +0Za9cZXfLM8QWDPh15Sk7fT1aR4jSiNWzmj6dsfjmb7brqdPLEV2WRE5uFsXcls7ptinCLbs8vvU +uu/DnEJ/HdbY081izZ1V2XXanaTwaanFC8k8012ftFedicfkCKT+tJ/R5SfPR35+J1Y1YiIvti+/ +TibpmaREWxXk6ZmPdjdZDrdbSbt0Uu/enljt9c7OVJtru4jt+VlHrLtjyC0ceu0krVJTrxU5o444 +5Rn49ngM8hyzMxcunVsCymjp6c26VbrY0ordf7UVm6JmZimdeWIttwpunFZvm2+bqV5Y9njhXqmb +sJ4RGaSppb2po+SbTV6wyt6jXjp/F6xA7SFBRruTPExMz/01g3bp9TAP4LOpr26t2nZfd7Opfz6k +8brtvRbHVWTR04tuiJxiyIjpnOZ68LZrtiXLvQ7UtRTq+Pa++NbxfTST66xYqywyzX5onqxFHFMw +SFLFH3yISFnciH4r02XWfUuu1brK62rEXRF0TEWRPNNZjCkzyxExOUTuYtm7kikVuxumuFbojCJ/ +eumu7DDhbafU2NjSipa/YBo/GKJTNwpWBuPav5rBIEJg1h5I4e8cmY+T8mLr78uW+2y6brrPqa11 +Pet5eW32piZieWkzyxGNMKYLb92Ixp7c14ViK76zMz0wueF6HyAtTJrm2dqpqqIQQ6Da/YwU9l2W +H+mjlhtwSiwZEG5FABE7P09Hfl4/xOl9Tn5Lbr7qzfbzzdZXZMTbdHH70xHcacTGEe7TbnWs16cN +vGVeWvty8s7+pkv3LBtX12+rbfXca01OsUnOeK00deHlIxk7DE5C5O2QFs43F1n0KXxZbGN1k2X+ +1F00pE21unCmd1JpHvTtt0TExSfaikcKVxyymk9dIzVKB2S8V0Ut/W7D7Wzs5bnllT7G0c7TSDJY +GJ4Gi7s0AWCjDlGBC4i36uV11LbY174tusrbpxbpzz20pFLa1rS26beaaTMTWd7M1mLsM7sf3dlN ++EW2zStba5rl/QX9zvClsa+SGhv7lQLcJg7M2t1InOD2GZnESs2DYOBde30fqzi3LS8TbpadIuib +tK26Y/f1KW+z+7bjWPvZbJm3xWsxti2zqrN108ImK2+qrWfVbS75lugtXt1rJZXCrrI6FKrNTPXj +CD9LVmnZiiIpSk5g8oO+G+V8MrbrWWeHspbpXxndzXXRdz1n7tt9szhSk8s9OazMxfXKlOWnf0TX +DZhEL1imO00ui8aqay7Bpo7rVbn30Tg70tS7uJH65CzLDGI8scwd3xhcbdT6epqa111s6nLzRyz9 +7U9NsTNae7MJFnJp/Ttrst6ts9FIm2u+cN7h+R6va2rdzYyQX6WrvbWOpckoVO/bHXayCRq7/anD +ZeSOS65F0hLIOPTj1Xr8JrWW222RNl19unN0c11Lee+Yr7XNbSYs+aPartNW2azSPht/hxumY655 +ZjGsRK4OkOr5ho7lqTb7OveN7E+wtVQd2mqRvDRhlio1oBhD/wAZLJzmAcODcnbouf8A9jm0NS22 +NOybYpFsXbLprfMTfdPNPsWxS2ZwnDazdbldjNZiJw2W1ujClfemOzc+mL889AgICAgICAgICAgI +CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgrbLV6 +zaUpKGzqQ3qM3HvVbMYTRHxJjHkBsQvghZ2y3qumlrX6d0XWTNt0bYmk9sJMVzcL+7D92v8Aylpv ++H1f9Gvb+seM/vav47vWz9O3dB/dh+7X/lLTf8Pq/wCjT9Y8Z/e1fx3es+nbug/uw/dr/wApab/h +9X/Rp+seM/vav47vWfTt3Qf3Yfu1/wCUtN/w+r/o0/WPGf3tX8d3rPp27oP7sP3a/wDKWm/4fV/0 +afrHjP72r+O71n07d0H92H7tf+UtN/w+r/o0/WPGf3tX8d3rPp27oR2P3UfuxsRPFJ4pqRF/V46U +ERdP8qMBL+Nat/zXjLZrGtqfiun0n07d0Nov3WfuzijGMfE9O4i2Gc6NYy/SRA5P+l1J/wAz4yZr +9bU/Hd6yNO3c2/uw/dr/AMpab/h9X/RqfrHjP72r+O71n07d0H92H7tf+UtN/wAPq/6NP1jxn97V +/Hd6z6du6D+7D92v/KWm/wCH1f8ARp+seM/vav47vWfTt3Qf3Yfu1/5S03/D6v8Ao0/WPGf3tX8d +3rPp27oP7sP3a/8AKWm/4fV/0afrHjP72r+O71n07d0H92H7tf8AlLTf8Pq/6NP1jxn97V/Hd6z6 +du6D+7D92v8Aylpv+H1f9Gn6x4z+9q/ju9Z9O3dCWr+7r931SzDaq+MamvarmMsE8VGsEkcgPyEw +IQZxIXbLOyzf/lfFXRNt2rqTE4TE33Y968lu56FeBoQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE +BAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE +BAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE +BAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE +BAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE +BAQEBAQEHwOx5Z5NPMcpbS0Lm7u4xzGAtn2YRdmZl+ijw+nEU5YfBnXvmc5R/wBpvJP/AHa5/wCo +l/7Sv0NP4Y7E+tf8U9p/abyT/wB2uf8AqJf+0n0NP4Y7D61/xT2n9pvJP/drn/qJf+0n0NP4Y7D6 +1/xT2n9pvJP/AHa5/wCol/7SfQ0/hjsPrX/FPaf2m8k/92uf+ol/7SfQ0/hjsPrX/FPaf2m8k/8A +drn/AKiX/tJ9DT+GOw+tf8U9p/abyT/3a5/6iX/tJ9DT+GOw+tf8U9p/abyT/wB2uf8AqJf+0n0N +P4Y7D61/xT2n9pvJP/drn/qJf+0n0NP4Y7D61/xT2n9pvJP/AHa5/wCol/7SfQ0/hjsPrX/FPaf2 +m8k/92uf+ol/7SfQ0/hjsPrX/FPaf2m8k/8Adrn/AKiX/tJ9DT+GOw+tf8U9q/s7PneraMr9u/XG +XrGRTyOz++MsTtn8Fzst0b/di2ep0vnVtzme1VLd+WjWGyWwvtXMnAJnmm4OTerMXLGVv6WlWlLa +9EMfU1KVrNEX9pvJP/drn/qJf+0r9DT+GOxPrX/FPaf2m8k/92uf+ol/7SfQ0/hjsPrX/FPaf2m8 +k/8Adrn/AKiX/tJ9DT+GOw+tf8U9r0n7vfI95N5RWqWb09ivYGQZI5pCkb5YyNnbk74fI+y8vjNG +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 +pf3c/wDzPX//AK7/ALiReTxv9KerzvT4T+pHlsfbV8F9oQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB +AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEHzqf9z0JTGUG0KOJ3dwjKHm4t +7M5dwc/wL6cf5KaY29750/4+K4Sj/ub/AN7/AOz/AOtV/Uvl7/sT9P8Am7vtP7m/97/7P/rU/Uvl +7/sP0/5u77T+5v8A3v8A7P8A61P1L5e/7D9P+bu+0/ub/wB7/wCz/wCtT9S+Xv8AsP0/5u77T+5v +/e/+z/61P1L5e/7D9P8Am7vtP7m/97/7P/rU/Uvl7/sP0/5u77T+5v8A3v8A7P8A61P1L5e/7D9P ++bu+0/ub/wB7/wCz/wCtT9S+Xv8AsP0/5u77T+5v/e/+z/61P1L5e/7D9P8Am7vtP7m/97/7P/rU +/Uvl7/sP0/5u77T+5v8A3v8A7P8A61P1L5e/7D9P+bu+0/ub/wB7/wCz/wCtT9S+Xv8AsP0/5u77 +T+5v/e/+z/61P1L5e/7D9P8Am7vtP7m/97/7P/rU/Uvl7/sP0/5u77T+5v8A3v8A7P8A61P1L5e/ +7D9P+bu+0/ub/wB7/wCz/wCtT9S+Xv8AsP0/5u77T+5v/e/+z/61P1L5e/7D9P8Am7vtdbxj928O +k2obE7z2pIhJogaPtszmLi7v8x5+V3XHX8bOpby0o7aPg4surWr2a8L2CAgICAgICAgICAgICAgI +CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI +CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI +CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI +CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI +CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI +CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI +CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI +CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI +CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI +CAg81e2k4bO1Bc2MmpjEhGiXaB4ZGcWdzOWQDbPJ8cWIVLcemq3YTwWp/IJ42tzQ1WsUte/C3Y7n +A3cWYpHij4kxcGfrkm/BWu2cImfs86RGzOaObb2F79sSDBMZB+0K4RxvIQg4lTc+D4zgSLq/T8VL +axH/AJO6PQzM17LfzJaHk9uHSUbGxaD7i5gYJDnaICwzuRyu8YjGzM36vLK3dnEcPRDUbZ4+mU1b +yyW5NDWp1Yp7EhzAZDYZ4WeFgLIyiBchIZG/Vz+CzGPZXvokzTy4OrrNi9/XDaGLhI/MShd84kjJ +wIeXTLch9VLpwrG6rUZ0nZLj6baTWZoQtbSSHYvkrGrlhjib0fIxcgE34+vLmSXTERMxjgzPHDFF +B5pG1ODrCU/24zzfd2Y4CflniIYjZjN+P80W9Fq7Du81ViPPPno7w3pbGthu0YwkaYBlAZzeJmAm +5dSEJerN+Cl8csls1UNV5FPdsV4pagwjaGY4ZQleQSCFxFjbIRvg+XT8PzVjHsie1JmnbRQs+TMN +3W2jY44j+9h+1AnJ5ZIpBijZm+VnIn9PhlZsmteNsU65W7CZj4bvRLo/tC5PalrSMdOavUaxIMRx +yNyl5Mw5OJ+ocOjt0+LOpdNIun4f2/YtsVutjf8AsQQ+SXZghatSGUyox3jeSbhhj5Nw6Rvkvl+D +N+S1dNOadlv7WbMaRtmvc5m122ztzy2qeQrQ6wb1dvuJInZz5FzMABxkceGOBPx/hUuw5uExC2zW +kb6+h0ZfILNIaUuxHg0leaYxgNjEu2MbjnlEBcic8MzOzN+Ptu6PauiNn/qozZNYifLKq7T2189m +FC5TCucld7AkEzy4ZiEeJM4B1+b2d1N/Bd072m12ttjuVKEDSyVoHksTHK8TBzZ+DBgDciwLv7N+ +K53T7My6WR7UcXNr+S/Y1az2jKR3oVJGeSQBEpZicMkRDyb0yREbtj2XW7O796ndMuVuUdE+j1pG +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 +7BdijszgMdtngcoj4O/AhDusQEzPj6nb8FPMstZ/Hilr7WIrTuWzEReRwbIOMTR5dmcWLPHPsmyI ++avfE+hbZpNeFPP613Za8b2smpEfDuhwGTGeJN9JYy3o7ZUuxZsikU4Uc5vFK3foSFK5BUgeCWNx +6TO4kLGT56O3cN/0qzSZndMU6PKMFjZvia+XXikq6GxGdMbFzv1dfn7SJo+B54uAvKfIufEHdmwI +pdjWZzmKJTCmxUi8OEIoh+8dpa8EMUEogzOMkEhSDJhyJn+rDj/GrX0d1vL3lPT3zXuX5NTesHVk +t3AM6tgZxaOHtg7CBDjDmZZfn1fl7eiRhNenvJisU8s6oz1uxr2DelNhrt0bFmTiP9HCMbCQMxcu +Tk8bNlm91LcKRsx/Z39y3Y1nbSI+1Z22pi2TVRldu1XmaY4yHkxswEDg/VvXmpTGvCe82eW9zj8U +kOAK5XnKKGGzWr8o8kMVgREWIuXzdvj+lJxrXOYjumpGE4ZVqtUtLZpSm1W2wVZnE54ii5F3GFhI +oz5YFj4tlnEvwVv9qvGvekRSI6PMpQeGQQ1RrDYdopIPt7zCDD3uOXjkbr8hg/v16dFZn0dsUxX7 +eydi3Y02yt0nq3L4yNmJxIIOGe3IMmTyZZJ+GPl4t19Eifaid01SYwpwXIdf29pZv9zP3McUfbxj +j2nN85z1zz+CkYV6fQt2Mxwj0q2x1ezuR2Kz3gGlZZxMHg5SiBNghCTmw/k7g6lK5rWmSu/i7BaK +3Vs9mwMkclYnDmwMELQEBtybmxC3xZ1qs9899PUzEYU4R3V9br1orQRO1mZppSd3cgDtg34COSfH +5k6zdFYosOTrvHLFcKMNm4M9bXZKvGEXbdzcXFiMnM88WJ8MzMrdj00okx56+lnW+OT6+WqcFsS7 +Ncathjid+4Am5s44NuBfM/ryVnGsbJp3RQp5575aweN2oKtWKO6LSa83KhN2eogTOxBK3PEjOL46 +cUrOE7aU83qXf01dMfvYKkhzO92x1fhCIRZ9uICZ4b/rH+lZvisUIeaqUNhXq6aD9mXS/ZBEXL/w +TdzlGcfp92/H68+6t/tT/Dy+b1GyY33V75lX1ms3mqCL9nU7YSdgILXdjpyBJ2nfgYi10HAmYnb6 +nb8FZmvd5oj0H2981drRRXYtlemnp2o2vPGZSz/aswvFEMfXszSO7lxz0BmSMLacZntSmNeHr9bu +qKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC +AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC +AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC +D//Z +------=_NextPart_000_000F_01D15E52.0BD654A0-- + + diff --git a/framework/domain/mimetreeparser/tests/interfacetest.cpp b/framework/domain/mimetreeparser/tests/interfacetest.cpp index 5a3cbb87..fa372a60 100644 --- a/framework/domain/mimetreeparser/tests/interfacetest.cpp +++ b/framework/domain/mimetreeparser/tests/interfacetest.cpp @@ -223,6 +223,21 @@ private slots: auto contentAttachmentList = parser.collectAttachmentParts(); QCOMPARE(contentAttachmentList.size(), 0); } + + void testRelatedAlternative() + { + Parser parser(readMailFromFile("cid-links.mbox")); + printTree(parser.d->mTree,QString()); + auto contentPartList = parser.collectContentParts(); + QCOMPARE(contentPartList.size(), 1); + auto contentPart = contentPartList[0]; + QVERIFY((bool)contentPart); + QCOMPARE(contentPart->availableContents(), QVector() << "html" << "plaintext"); + QCOMPARE(contentPart->encryptions().size(), 0); + QCOMPARE(contentPart->signatures().size(), 0); + auto contentList = contentPart->content("plaintext"); + QCOMPARE(contentList.size(), 1); + } }; QTEST_GUILESS_MAIN(InterfaceTest) -- cgit v1.2.3 From 988f0fe074faef56c053742fb582d0bb7b980d90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sandro=20Knau=C3=9F?= Date: Mon, 17 Oct 2016 12:24:32 +0200 Subject: do not include multipart/related in attachmentlist --- framework/domain/mimetreeparser/interface.cpp | 16 ++++++++++++++-- framework/domain/mimetreeparser/tests/interfacetest.cpp | 2 ++ 2 files changed, 16 insertions(+), 2 deletions(-) (limited to 'framework') diff --git a/framework/domain/mimetreeparser/interface.cpp b/framework/domain/mimetreeparser/interface.cpp index 596dc152..ab56f800 100644 --- a/framework/domain/mimetreeparser/interface.cpp +++ b/framework/domain/mimetreeparser/interface.cpp @@ -818,12 +818,24 @@ QVector Parser::collectAttachmentParts() const } { - const auto parent = content->parent(); + QMimeDatabase mimeDb; + auto _mime = content->parent()->mailMime(); + const auto parent = _mime->parent(); if (parent) { - const auto _mime = parent->mailMime(); + const auto mimetype = parent->mimetype(); + if (mimetype == mimeDb.mimeTypeForName("multipart/related")) { + return false; + } + } + while (_mime) { if (_mime && (_mime->isTopLevelPart() || _mime->isFirstTextPart())) { return false; } + if (_mime->isFirstPart()) { + _mime = _mime->parent(); + } else { + break; + } } } const auto cd = mime->disposition(); diff --git a/framework/domain/mimetreeparser/tests/interfacetest.cpp b/framework/domain/mimetreeparser/tests/interfacetest.cpp index fa372a60..923a7446 100644 --- a/framework/domain/mimetreeparser/tests/interfacetest.cpp +++ b/framework/domain/mimetreeparser/tests/interfacetest.cpp @@ -237,6 +237,8 @@ private slots: QCOMPARE(contentPart->signatures().size(), 0); auto contentList = contentPart->content("plaintext"); QCOMPARE(contentList.size(), 1); + auto contentAttachmentList = parser.collectAttachmentParts(); + QCOMPARE(contentAttachmentList.size(), 0); } }; -- cgit v1.2.3 From b7a02699eefd84c68ff602bfea91640faec5c4ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sandro=20Knau=C3=9F?= Date: Mon, 17 Oct 2016 13:05:43 +0200 Subject: find part by cid --- framework/domain/mimetreeparser/interface.cpp | 34 ++++++++++++++++++++-- framework/domain/mimetreeparser/interface.h | 4 +-- .../domain/mimetreeparser/tests/interfacetest.cpp | 9 ++++++ 3 files changed, 43 insertions(+), 4 deletions(-) (limited to 'framework') diff --git a/framework/domain/mimetreeparser/interface.cpp b/framework/domain/mimetreeparser/interface.cpp index ab56f800..c2fd1e65 100644 --- a/framework/domain/mimetreeparser/interface.cpp +++ b/framework/domain/mimetreeparser/interface.cpp @@ -56,6 +56,14 @@ MailMime::MailMime() { } +QByteArray MailMime::cid() const +{ + if (!d->mNode || !d->mNode->contentID()) { + return QByteArray(); + } + return d->mNode->contentID()->identifier(); +} + bool MailMime::isFirstTextPart() const { if (!d->mNode || !d->mNode->topLevel()) { @@ -750,9 +758,16 @@ Parser::~Parser() { } -QUrl Parser::getPart(const QByteArray &cid) +Part::Ptr Parser::getPart(const QUrl &url) { - return d->mEmbeddedPartMap.value(cid); + if (url.scheme() == QStringLiteral("cid") && !url.path().isEmpty()) { + const auto cid = url.path(); + return find(d->mTree, [&cid](const Part::Ptr &p){ + const auto mime = p->mailMime(); + return mime->cid() == cid; + }); + } + return Part::Ptr(); } QVector Parser::collectContentParts() const @@ -857,6 +872,7 @@ QVector Parser::collectAttachmentParts() const return true; }); } + QVector Parser::collect(const Part::Ptr &start, std::function select, std::function filter) const { QVector ret; @@ -879,3 +895,17 @@ QVector Parser::collect(const Part::Ptr &start, std::function select) const +{ + foreach (const auto &part, start->subParts()) { + if (select(part)) { + return part; + } + const auto ret = find(part, select); + if (ret) { + return ret; + } + } + return Part::Ptr(); +} diff --git a/framework/domain/mimetreeparser/interface.h b/framework/domain/mimetreeparser/interface.h index 4b569546..cc6c68d2 100644 --- a/framework/domain/mimetreeparser/interface.h +++ b/framework/domain/mimetreeparser/interface.h @@ -349,10 +349,10 @@ public: Parser(const QByteArray &mimeMessage); ~Parser(); - Part::Ptr getPart(QUrl url); - QUrl getPart(const QByteArray &cid); + Part::Ptr getPart(const QUrl &url); QVector collect(const Part::Ptr &start, std::function select, std::function filter) const; + Part::Ptr find(const Part::Ptr &start, std::function select) const; QVector collectContentParts() const; QVector collectAttachmentParts() const; //template <> QVector collect() const; diff --git a/framework/domain/mimetreeparser/tests/interfacetest.cpp b/framework/domain/mimetreeparser/tests/interfacetest.cpp index 923a7446..d52606c2 100644 --- a/framework/domain/mimetreeparser/tests/interfacetest.cpp +++ b/framework/domain/mimetreeparser/tests/interfacetest.cpp @@ -240,6 +240,15 @@ private slots: auto contentAttachmentList = parser.collectAttachmentParts(); QCOMPARE(contentAttachmentList.size(), 0); } + + void testCidLink() + { + Parser parser(readMailFromFile("cid-links.mbox")); + printTree(parser.d->mTree,QString()); + QCOMPARE(parser.getPart(QUrl("cid:9359201d15e53f31a68c307b3369b6@info")), parser.d->mTree->subParts().at(1)); + QVERIFY(!parser.getPart(QUrl("cid:"))); + QVERIFY(!parser.getPart(QUrl("cid:unknown"))); + } }; QTEST_GUILESS_MAIN(InterfaceTest) -- cgit v1.2.3 From 1fe0ab05ae2bfe6505736f598bd535a71fb86a6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sandro=20Knau=C3=9F?= Date: Mon, 17 Oct 2016 15:35:49 +0200 Subject: replace cid links with actuall content of the images --- framework/domain/messageparser.h | 3 ++- framework/domain/messageparser_new.cpp | 33 +++++++++++++++++++++++---- framework/domain/messageparser_old.cpp | 23 ++++++++++++++----- framework/domain/mimetreeparser/interface.cpp | 11 ++++++++- framework/domain/mimetreeparser/interface.h | 2 ++ 5 files changed, 60 insertions(+), 12 deletions(-) (limited to 'framework') diff --git a/framework/domain/messageparser.h b/framework/domain/messageparser.h index b3d7537d..5eb355e7 100644 --- a/framework/domain/messageparser.h +++ b/framework/domain/messageparser.h @@ -97,7 +97,7 @@ private: class NewContentModel : public QAbstractItemModel { Q_OBJECT public: - NewContentModel (const PartPtr &part); + NewContentModel (const PartPtr &part, const std::shared_ptr &parser); public: enum Roles { @@ -116,6 +116,7 @@ public: private: const PartPtr &mPart; + const std::shared_ptr &mParser; }; class NewModel : public QAbstractItemModel { diff --git a/framework/domain/messageparser_new.cpp b/framework/domain/messageparser_new.cpp index d1b956f5..4395f2e3 100644 --- a/framework/domain/messageparser_new.cpp +++ b/framework/domain/messageparser_new.cpp @@ -26,7 +26,7 @@ NewModel::NewModel(std::shared_ptr parser) { mParts = mParser->collectContentParts(); foreach(const auto &part, mParts) { - mContentMap.insert(part.get(), std::shared_ptr(new NewContentModel(part))); + mContentMap.insert(part.get(), std::shared_ptr(new NewContentModel(part, mParser))); } } @@ -87,8 +87,9 @@ int NewModel::columnCount(const QModelIndex &parent) const return 1; } -NewContentModel::NewContentModel(const Part::Ptr &part) +NewContentModel::NewContentModel(const Part::Ptr &part, const std::shared_ptr &parser) : mPart(part) + , mParser(parser) { } @@ -121,8 +122,32 @@ QVariant NewContentModel::data(const QModelIndex &index, int role) const return QString::fromLatin1(content->type()); case IsEmbededRole: return false; - case ContentRole: - return content->encodedContent(); + case ContentRole: { + auto text = content->encodedContent(); + if (data(index, TypeRole).toString() == "HtmlContent") { + const auto rx = QRegExp("(src)\\s*=\\s*(\"|')(cid:[^\"']+)\\2"); + int pos = 0; + while ((pos = rx.indexIn(text, pos)) != -1) { + const auto link = QUrl(rx.cap(3).toUtf8()); + pos += rx.matchedLength(); + const auto repl = mParser->getPart(link); + if (!repl) { + continue; + } + const auto content = repl->content(); + if(content.size() < 1) { + continue; + } + const auto mailMime = content.first()->mailMime(); + const auto mimetype = mailMime->mimetype().name(); + if (mimetype.startsWith("image/")) { + const auto data = content.first()->content(); + text.replace(rx.cap(0), QString("src=\"data:%1;base64,%2\"").arg(mimetype, QString::fromLatin1(data.toBase64()))); + } + } + } + return text; + } case SecurityLevelRole: return content->encryptions().size() > mPart->encryptions().size() ? "red": "black"; //test for gpg inline } diff --git a/framework/domain/messageparser_old.cpp b/framework/domain/messageparser_old.cpp index a364c8ab..a4247d8c 100644 --- a/framework/domain/messageparser_old.cpp +++ b/framework/domain/messageparser_old.cpp @@ -64,14 +64,25 @@ QVariant PartModel::data(const QModelIndex &index, int role) const // qDebug() << "Getting text: " << part->property("text").toString(); // FIXME: we should have a list per part, and not one for all parts. auto text = part->property("htmlContent").toString(); - auto rx = QRegExp("src=(\"|')cid:([^\1]*)\1"); - int pos = 0; + const auto rx = QRegExp("(src)\\s*=\\s*(\"|')(cid:[^\"']+)\\2"); + int pos = 0; while ((pos = rx.indexIn(text, pos)) != -1) { - auto repl = mParser->getPart(rx.cap(2).toUtf8()); - if (repl.isValid()) { - text.replace(rx.cap(0), QString("src=\"%1\"").arg(repl.toString())); - } + const auto link = QUrl(rx.cap(3).toUtf8()); pos += rx.matchedLength(); + const auto repl = mParser->getPart(link); + if (!repl) { + continue; + } + const auto content = repl->content(); + if(content.size() < 1) { + continue; + } + const auto mailMime = content.first()->mailMime(); + const auto mimetype = mailMime->mimetype().name(); + if (mimetype.startsWith("image/")) { + const auto data = content.first()->content(); + text.replace(rx.cap(0), QString("src=\"data:%1;base64,%2\"").arg(mimetype, QString::fromLatin1(data.toBase64()))); + } } return text; } diff --git a/framework/domain/mimetreeparser/interface.cpp b/framework/domain/mimetreeparser/interface.cpp index c2fd1e65..b5e29e6b 100644 --- a/framework/domain/mimetreeparser/interface.cpp +++ b/framework/domain/mimetreeparser/interface.cpp @@ -143,6 +143,15 @@ MailMime::Ptr MailMime::parent() const return d->parent; } +QByteArray MailMime::decodedContent() const +{ + if (!d->mNode) { + return QByteArray(); + } + return d->mNode->decodedContent(); +} + + class PartPrivate { public: @@ -581,7 +590,7 @@ void SinglePartPrivate::fillFrom(MimeTreeParser::AttachmentMessagePart::Ptr part q->reachParentD()->createMailMime(part.staticCast()); mType = q->mailMime()->mimetype().name().toUtf8(); mContent.clear(); - mContent.append(std::make_shared(part->text().toLocal8Bit(), q)); + mContent.append(std::make_shared(q->mailMime()->decodedContent(), q)); } SinglePart::SinglePart() diff --git a/framework/domain/mimetreeparser/interface.h b/framework/domain/mimetreeparser/interface.h index cc6c68d2..f88271af 100644 --- a/framework/domain/mimetreeparser/interface.h +++ b/framework/domain/mimetreeparser/interface.h @@ -100,6 +100,8 @@ public: // overwrite default charset with given charset QString encodedContent(QByteArray charset) const; + QByteArray decodedContent() const; + bool isFirstTextPart() const; bool isFirstPart() const; bool isTopLevelPart() const; -- cgit v1.2.3 From 5cb20dd3886ee229d74068c75250691c840e89a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sandro=20Knau=C3=9F?= Date: Mon, 17 Oct 2016 16:33:03 +0200 Subject: Read correct charset out of mailpart --- framework/domain/mimetreeparser/interface.cpp | 13 ++++++++++++- framework/domain/mimetreeparser/tests/interfacetest.cpp | 12 ++++++------ 2 files changed, 18 insertions(+), 7 deletions(-) (limited to 'framework') diff --git a/framework/domain/mimetreeparser/interface.cpp b/framework/domain/mimetreeparser/interface.cpp index b5e29e6b..ceb8caf3 100644 --- a/framework/domain/mimetreeparser/interface.cpp +++ b/framework/domain/mimetreeparser/interface.cpp @@ -64,6 +64,17 @@ QByteArray MailMime::cid() const return d->mNode->contentID()->identifier(); } +QByteArray MailMime::charset() const +{ + if(!d->mNode || !d->mNode->contentType(false)) { + return QByteArray(); + } + if (d->mNode->contentType(false)) { + return d->mNode->contentType(false)->charset(); + } + return d->mNode->defaultCharset(); +} + bool MailMime::isFirstTextPart() const { if (!d->mNode || !d->mNode->topLevel()) { @@ -559,7 +570,6 @@ void SinglePartPrivate::fillFrom(MimeTreeParser::TextMessagePart::Ptr part) auto d_ptr = new ContentPrivate; d_ptr->mContent = mp->text().toLocal8Bit(); d_ptr->mParent = q; - d_ptr->mCodec = "utf-8"; const auto enc = mp.dynamicCast(); auto sig = mp.dynamicCast(); if (enc) { @@ -574,6 +584,7 @@ void SinglePartPrivate::fillFrom(MimeTreeParser::TextMessagePart::Ptr part) } mContent.append(std::make_shared(d_ptr)); q->reachParentD()->createMailMime(part); + d_ptr->mCodec = q->mailMime()->charset(); } } diff --git a/framework/domain/mimetreeparser/tests/interfacetest.cpp b/framework/domain/mimetreeparser/tests/interfacetest.cpp index d52606c2..0259471e 100644 --- a/framework/domain/mimetreeparser/tests/interfacetest.cpp +++ b/framework/domain/mimetreeparser/tests/interfacetest.cpp @@ -144,7 +144,7 @@ private slots: auto contentList = contentPart->content("plaintext"); QCOMPARE(contentList.size(), 1); QCOMPARE(contentList[0]->content(), QStringLiteral("The quick brown fox jumped over the lazy dog.").toLocal8Bit()); - QCOMPARE(contentList[0]->charset(), QStringLiteral("utf-8").toLocal8Bit()); + QCOMPARE(contentList[0]->charset(), QStringLiteral("us-ascii").toLocal8Bit()); QCOMPARE(contentList[0]->encryptions().size(), 1); QCOMPARE(contentList[0]->signatures().size(), 0); auto contentAttachmentList = parser.collectAttachmentParts(); @@ -163,7 +163,7 @@ private slots: auto contentList = contentPart->content("plaintext"); QCOMPARE(contentList.size(), 1); QCOMPARE(contentList[0]->content(), QStringLiteral("test text").toLocal8Bit()); - QCOMPARE(contentList[0]->charset(), QStringLiteral("utf-8").toLocal8Bit()); + QCOMPARE(contentList[0]->charset(), QStringLiteral("us-ascii").toLocal8Bit()); QCOMPARE(contentList[0]->encryptions().size(), 1); QCOMPARE(contentList[0]->signatures().size(), 1); auto contentAttachmentList = parser.collectAttachmentParts(); @@ -178,7 +178,7 @@ private slots: QCOMPARE(contentAttachmentList[1]->signatures().size(), 0); } - void testOpenPPGInline() + void testOpenPGPInline() { Parser parser(readMailFromFile("openpgp-inline-charset-encrypted.mbox")); printTree(parser.d->mTree,QString()); @@ -192,7 +192,7 @@ private slots: auto contentList = contentPart->content("plaintext"); QCOMPARE(contentList.size(), 1); QCOMPARE(contentList[0]->content(), QStringLiteral("asdasd asd asd asdf sadf sdaf sadf äöü").toLocal8Bit()); - QCOMPARE(contentList[0]->charset(), QStringLiteral("utf-8").toLocal8Bit()); + QCOMPARE(contentList[0]->charset(), QStringLiteral("ISO-8859-15").toLocal8Bit()); QCOMPARE(contentList[0]->encryptions().size(), 1); QCOMPARE(contentList[0]->signatures().size(), 1); auto contentAttachmentList = parser.collectAttachmentParts(); @@ -213,11 +213,11 @@ private slots: auto contentList = contentPart->content("plaintext"); QCOMPARE(contentList.size(), 2); QCOMPARE(contentList[0]->content(), QStringLiteral("Not encrypted not signed :(\n\n").toLocal8Bit()); - QCOMPARE(contentList[0]->charset(), QStringLiteral("utf-8").toLocal8Bit()); + QCOMPARE(contentList[0]->charset(), QStringLiteral("us-ascii").toLocal8Bit()); QCOMPARE(contentList[0]->encryptions().size(), 0); QCOMPARE(contentList[0]->signatures().size(), 0); QCOMPARE(contentList[1]->content(), QStringLiteral("some random text").toLocal8Bit()); - QCOMPARE(contentList[1]->charset(), QStringLiteral("utf-8").toLocal8Bit()); + QCOMPARE(contentList[1]->charset(), QStringLiteral("us-ascii").toLocal8Bit()); QCOMPARE(contentList[1]->encryptions().size(), 1); QCOMPARE(contentList[1]->signatures().size(), 0); auto contentAttachmentList = parser.collectAttachmentParts(); -- cgit v1.2.3 From c6bca32d393e7bd32574c88ea574be78cec01bf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sandro=20Knau=C3=9F?= Date: Mon, 17 Oct 2016 16:33:52 +0200 Subject: add a test for a attachment --- framework/domain/mimetreeparser/tests/interfacetest.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'framework') diff --git a/framework/domain/mimetreeparser/tests/interfacetest.cpp b/framework/domain/mimetreeparser/tests/interfacetest.cpp index 0259471e..ab6316ac 100644 --- a/framework/domain/mimetreeparser/tests/interfacetest.cpp +++ b/framework/domain/mimetreeparser/tests/interfacetest.cpp @@ -241,6 +241,17 @@ private slots: QCOMPARE(contentAttachmentList.size(), 0); } + void testAttachmentPart() + { + Parser parser(readMailFromFile("cid-links.mbox")); + const auto relatedImage = parser.d->mTree->subParts().at(1); + QCOMPARE(relatedImage->availableContents(), QVector() << "image/jpeg"); + auto contentList = relatedImage->content(); + QCOMPARE(contentList.size(), 1); + contentList = relatedImage->content("image/jpeg"); + QCOMPARE(contentList.size(), 1); + } + void testCidLink() { Parser parser(readMailFromFile("cid-links.mbox")); -- cgit v1.2.3 From d15a02d3c26c24530e8d9360629212e419c81c79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sandro=20Knau=C3=9F?= Date: Mon, 17 Oct 2016 19:06:12 +0200 Subject: fill Encryption and Signatures with content --- framework/domain/mimetreeparser/interface.cpp | 262 +++++++++++++++++---- framework/domain/mimetreeparser/interface.h | 22 +- framework/domain/mimetreeparser/interface_p.h | 2 +- .../domain/mimetreeparser/tests/interfacetest.cpp | 44 ++++ 4 files changed, 270 insertions(+), 60 deletions(-) (limited to 'framework') diff --git a/framework/domain/mimetreeparser/interface.cpp b/framework/domain/mimetreeparser/interface.cpp index ceb8caf3..0bcbfec4 100644 --- a/framework/domain/mimetreeparser/interface.cpp +++ b/framework/domain/mimetreeparser/interface.cpp @@ -23,6 +23,10 @@ #include "stringhtmlwriter.h" #include "objecttreesource.h" +#include +#include +#include + #include #include #include @@ -162,6 +166,131 @@ QByteArray MailMime::decodedContent() const return d->mNode->decodedContent(); } +class KeyPrivate +{ +public: + Key *q; + GpgME::Key mKey; +}; + +Key::Key() + :d(std::unique_ptr(new KeyPrivate)) +{ + d->q = this; +} + + +Key::Key(KeyPrivate *d_ptr) + :d(std::unique_ptr(d_ptr)) +{ + d->q = this; +} + +Key::~Key() +{ + +} + +QString Key::keyid() const +{ + return d->mKey.keyID(); +} + +QString Key::name() const +{ + //FIXME: is this the correct way to get the primary UID? + return d->mKey.userID(0).name(); +} + +QString Key::email() const +{ + return d->mKey.userID(0).email(); +} + +QString Key::comment() const +{ + return d->mKey.userID(0).comment(); +} + +class SignaturePrivate +{ +public: + Signature *q; + GpgME::Signature mSignature; + Key::Ptr mKey; +}; + +Signature::Signature() + :d(std::unique_ptr(new SignaturePrivate)) +{ + d->q = this; +} + + +Signature::Signature(SignaturePrivate *d_ptr) + :d(std::unique_ptr(d_ptr)) +{ + d->q = this; + +} + +Signature::~Signature() +{ + +} + +QDateTime Signature::creationDateTime() const +{ + QDateTime dt; + dt.setTime_t(d->mSignature.creationTime()); + return dt; +} + +QDateTime Signature::expirationDateTime() const +{ + QDateTime dt; + dt.setTime_t(d->mSignature.expirationTime()); + return dt; +} + +bool Signature::neverExpires() const +{ + return d->mSignature.neverExpires(); +} + +Key::Ptr Signature::key() const +{ + return d->mKey; +} + +class EncryptionPrivate +{ +public: + Encryption *q; + std::vector mRecipients; +}; + +Encryption::Encryption(EncryptionPrivate *d_ptr) + :d(std::unique_ptr(d_ptr)) +{ + d->q = this; +} + +Encryption::Encryption() + :d(std::unique_ptr(new EncryptionPrivate)) +{ + d->q = this; +} + +Encryption::~Encryption() +{ + +} + +std::vector Encryption::recipients() const +{ + return d->mRecipients; +} class PartPrivate { @@ -179,7 +308,9 @@ public: void createMailMime(const MimeTreeParser::AlternativeMessagePart::Ptr &part); void createMailMime(const MimeTreeParser::HtmlMessagePart::Ptr &part); + static Encryption::Ptr createEncryption(const MimeTreeParser::EncryptedMessagePart::Ptr& part); void appendEncryption(const MimeTreeParser::EncryptedMessagePart::Ptr &part); + static QVector createSignature(const MimeTreeParser::SignedMessagePart::Ptr& part); void appendSignature(const MimeTreeParser::SignedMessagePart::Ptr &part); void setSignatures(const QVector &sigs); @@ -233,9 +364,43 @@ void PartPrivate::appendSubPart(Part::Ptr subpart) mSubParts.append(subpart); } +Encryption::Ptr PartPrivate::createEncryption(const MimeTreeParser::EncryptedMessagePart::Ptr& part) +{ + Kleo::KeyListJob *job = part->mCryptoProto->keyListJob(false); // local, no sigs + if (!job) { + qWarning() << "The Crypto backend does not support listing keys. "; + return Encryption::Ptr(); + } + + auto encpriv = new EncryptionPrivate(); + foreach(const auto &recipient, part->mDecryptRecipients) { + std::vector found_keys; + const auto &keyid = recipient.keyID(); + GpgME::KeyListResult res = job->exec(QStringList(QLatin1String(keyid)), false, found_keys); + if (res.error()) { + qWarning() << "Error while searching key for Fingerprint: " << keyid; + continue; + } + if (found_keys.size() > 1) { + // Should not Happen + qWarning() << "Oops: Found more then one Key for Fingerprint: " << keyid; + } + if (found_keys.size() != 1) { + // Should not Happen at this point + qWarning() << "Oops: Found no Key for Fingerprint: " << keyid; + } else { + auto key = found_keys[0]; + auto keypriv = new KeyPrivate; + keypriv->mKey = key; + encpriv->mRecipients.push_back(Key::Ptr(new Key(keypriv))); + } + } + return Encryption::Ptr(new Encryption(encpriv)); +} + void PartPrivate::appendEncryption(const MimeTreeParser::EncryptedMessagePart::Ptr& part) { - mEncryptions.append(Encryption::Ptr(new Encryption)); + mEncryptions.append(createEncryption(part)); } void PartPrivate::setEncryptions(const QVector< Encryption::Ptr >& encs) @@ -243,9 +408,50 @@ void PartPrivate::setEncryptions(const QVector< Encryption::Ptr >& encs) mEncryptions = encs; } +QVector PartPrivate::createSignature(const MimeTreeParser::SignedMessagePart::Ptr& part) +{ + QVector sigs; + Kleo::KeyListJob *job = part->mCryptoProto->keyListJob(false); // local, no sigs + if (!job) { + qWarning() << "The Crypto backend does not support listing keys. "; + return sigs; + } + + foreach(const auto &sig, part->mSignatures) { + auto sigpriv = new SignaturePrivate(); + sigpriv->mSignature = sig; + auto signature = std::make_shared(sigpriv); + sigs.append(signature); + + std::vector found_keys; + const auto &keyid = sig.fingerprint(); + GpgME::KeyListResult res = job->exec(QStringList(QLatin1String(keyid)), false, found_keys); + if (res.error()) { + qWarning() << "Error while searching key for Fingerprint: " << keyid; + continue; + } + if (found_keys.size() > 1) { + // Should not Happen + qWarning() << "Oops: Found more then one Key for Fingerprint: " << keyid; + continue; + } + if (found_keys.size() != 1) { + // Should not Happen at this point + qWarning() << "Oops: Found no Key for Fingerprint: " << keyid; + continue; + } else { + auto key = found_keys[0]; + auto keypriv = new KeyPrivate; + keypriv->mKey = key; + sigpriv->mKey = Key::Ptr(new Key(keypriv)); + } + } + return sigs; +} + void PartPrivate::appendSignature(const MimeTreeParser::SignedMessagePart::Ptr& part) { - mSignatures.append(Signature::Ptr(new Signature)); + mSignatures.append(createSignature(part)); } @@ -361,12 +567,12 @@ public: void ContentPrivate::appendEncryption(const MimeTreeParser::EncryptedMessagePart::Ptr& enc) { - mEncryptions.append(Encryption::Ptr(new Encryption)); + mEncryptions.append(PartPrivate::createEncryption(enc)); } void ContentPrivate::appendSignature(const MimeTreeParser::SignedMessagePart::Ptr& sig) { - mSignatures.append(Signature::Ptr(new Signature)); + mSignatures.append(PartPrivate::createSignature(sig)); } @@ -638,54 +844,6 @@ PartPrivate* SinglePart::reachParentD() const return Part::d.get(); } -class SignaturePrivate -{ -public: - Signature *q; -}; - -Signature::Signature() - :d(std::unique_ptr(new SignaturePrivate)) -{ - d->q = this; -} - - -Signature::Signature(SignaturePrivate *d_ptr) - :d(std::unique_ptr(d_ptr)) -{ - d->q = this; -} - -Signature::~Signature() -{ - -} - - -class EncryptionPrivate -{ -public: - Encryption *q; -}; - -Encryption::Encryption(EncryptionPrivate *d_ptr) - :d(std::unique_ptr(d_ptr)) -{ - d->q = this; -} - -Encryption::Encryption() - :d(std::unique_ptr(new EncryptionPrivate)) -{ - d->q = this; -} - -Encryption::~Encryption() -{ - -} - ParserPrivate::ParserPrivate(Parser* parser) : q(parser) , mNodeHelper(std::make_shared()) diff --git a/framework/domain/mimetreeparser/interface.h b/framework/domain/mimetreeparser/interface.h index f88271af..3ff29d5d 100644 --- a/framework/domain/mimetreeparser/interface.h +++ b/framework/domain/mimetreeparser/interface.h @@ -53,6 +53,7 @@ class CertContentPrivate; class EncryptionError; class Key; +class KeyPrivate; class Signature; class SignaturePrivate; class Encryption; @@ -262,10 +263,9 @@ public: EncryptionError error() const; private: - std::unique_ptr d; + std::unique_ptr d; }; - /* * we want to request complete headers like: * from/to... @@ -279,7 +279,7 @@ public: //template QByteArray header(); private: - std::unique_ptr d; + std::unique_ptr d; }; class EncryptionError @@ -291,6 +291,12 @@ public: class Key { +public: + typedef std::shared_ptr Ptr; + Key(); + Key(KeyPrivate *); + ~Key(); + QString keyid() const; QString name() const; QString email() const; @@ -305,8 +311,10 @@ class Key bool isInvalid() const; bool isExpired() const; - std::vector subkeys(); + std::vector subkeys(); Key parentkey() const; +private: + std::unique_ptr d; }; class Signature @@ -317,9 +325,9 @@ public: Signature(SignaturePrivate *); ~Signature(); - Key key() const; + Key::Ptr key() const; QDateTime creationDateTime() const; - QDateTime expirationTime() const; + QDateTime expirationDateTime() const; bool neverExpires() const; //template <> StatusObject verify() const; @@ -339,7 +347,7 @@ public: Encryption(); Encryption(EncryptionPrivate *); ~Encryption(); - std::vector recipients() const; + std::vector recipients() const; private: std::unique_ptr d; }; diff --git a/framework/domain/mimetreeparser/interface_p.h b/framework/domain/mimetreeparser/interface_p.h index 55d1a5cc..8fab221a 100644 --- a/framework/domain/mimetreeparser/interface_p.h +++ b/framework/domain/mimetreeparser/interface_p.h @@ -53,4 +53,4 @@ public: std::shared_ptr mNodeHelper; QString mHtml; QMap mEmbeddedPartMap; -}; \ No newline at end of file +}; diff --git a/framework/domain/mimetreeparser/tests/interfacetest.cpp b/framework/domain/mimetreeparser/tests/interfacetest.cpp index ab6316ac..3ae32a4a 100644 --- a/framework/domain/mimetreeparser/tests/interfacetest.cpp +++ b/framework/domain/mimetreeparser/tests/interfacetest.cpp @@ -224,6 +224,50 @@ private slots: QCOMPARE(contentAttachmentList.size(), 0); } + void testEncryptionBlock() + { + Parser parser(readMailFromFile("openpgp-encrypted-attachment-and-non-encrypted-attachment.mbox")); + auto contentPartList = parser.collectContentParts(); + auto contentPart = contentPartList[0]; + auto contentList = contentPart->content("plaintext"); + QCOMPARE(contentList.size(), 1); + QCOMPARE(contentList[0]->encryptions().size(), 1); + auto enc = contentList[0]->encryptions()[0]; + QCOMPARE((int) enc->recipients().size(), 2); + + auto r = enc->recipients()[0]; + QCOMPARE(r->keyid(),QStringLiteral("14B79E26050467AA")); + QCOMPARE(r->name(),QStringLiteral("kdetest")); + QCOMPARE(r->email(),QStringLiteral("you@you.com")); + QCOMPARE(r->comment(),QStringLiteral("")); + + r = enc->recipients()[1]; + QCOMPARE(r->keyid(),QStringLiteral("8D9860C58F246DE6")); + QCOMPARE(r->name(),QStringLiteral("unittest key")); + QCOMPARE(r->email(),QStringLiteral("test@kolab.org")); + QCOMPARE(r->comment(),QStringLiteral("no password")); + } + + void testSignatureBlock() + { + Parser parser(readMailFromFile("openpgp-encrypted-attachment-and-non-encrypted-attachment.mbox")); + auto contentPartList = parser.collectContentParts(); + auto contentPart = contentPartList[0]; + auto contentList = contentPart->content("plaintext"); + QCOMPARE(contentList.size(), 1); + QCOMPARE(contentList[0]->signatures().size(), 1); + auto sig = contentList[0]->signatures()[0]; + QCOMPARE(sig->creationDateTime(), QDateTime(QDate(2015,05,01),QTime(15,12,47))); + QCOMPARE(sig->expirationDateTime(), QDateTime()); + QCOMPARE(sig->neverExpires(), true); + + auto key = sig->key(); + QCOMPARE(key->keyid(),QStringLiteral("8D9860C58F246DE6")); + QCOMPARE(key->name(),QStringLiteral("unittest key")); + QCOMPARE(key->email(),QStringLiteral("test@kolab.org")); + QCOMPARE(key->comment(),QStringLiteral("no password")); + } + void testRelatedAlternative() { Parser parser(readMailFromFile("cid-links.mbox")); -- cgit v1.2.3 From 0fa8aa51aac15233820b9f9c576584e6ff8ee151 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sandro=20Knau=C3=9F?= Date: Tue, 18 Oct 2016 15:28:45 +0200 Subject: use modeltest fromupstream to make sure, we have a valid model --- framework/domain/CMakeLists.txt | 5 +- framework/domain/messageparser.cpp | 5 +- framework/domain/modeltest.cpp | 588 +++++++++++++++++++++++++++++++++++++ framework/domain/modeltest.h | 83 ++++++ 4 files changed, 678 insertions(+), 3 deletions(-) create mode 100644 framework/domain/modeltest.cpp create mode 100644 framework/domain/modeltest.h (limited to 'framework') diff --git a/framework/domain/CMakeLists.txt b/framework/domain/CMakeLists.txt index 55bc2f24..a31bb016 100644 --- a/framework/domain/CMakeLists.txt +++ b/framework/domain/CMakeLists.txt @@ -10,6 +10,7 @@ set(mailplugin_SRCS messageparser_new.cpp messageparser_old.cpp mailtemplates.cpp + modeltest.cpp retriever.cpp accountfactory.cpp accountscontroller.cpp @@ -22,7 +23,7 @@ find_package(KF5 REQUIRED COMPONENTS Package) add_library(mailplugin SHARED ${mailplugin_SRCS}) -qt5_use_modules(mailplugin Core Quick Qml WebKitWidgets) +qt5_use_modules(mailplugin Core Quick Qml WebKitWidgets Test) target_link_libraries(mailplugin actionplugin settingsplugin sink mimetreeparser KF5::MimeTreeParser KF5::Codecs KF5::Package KF5::Async KF5::IconThemes) add_subdirectory(actions/tests) @@ -30,4 +31,4 @@ add_subdirectory(actions/tests) install(TARGETS mailplugin DESTINATION ${QML_INSTALL_DIR}/org/kube/framework/domain) install(FILES qmldir DESTINATION ${QML_INSTALL_DIR}/org/kube/framework/domain) -add_subdirectory(mimetreeparser) \ No newline at end of file +add_subdirectory(mimetreeparser) diff --git a/framework/domain/messageparser.cpp b/framework/domain/messageparser.cpp index ef9fb0d2..ec79d50b 100644 --- a/framework/domain/messageparser.cpp +++ b/framework/domain/messageparser.cpp @@ -18,6 +18,7 @@ */ #include "messageparser.h" +#include "modeltest.h" #include "stringhtmlwriter.h" #include "objecttreesource.h" @@ -100,5 +101,7 @@ QAbstractItemModel *MessageParser::partTree() const QAbstractItemModel *MessageParser::newTree() const { - return new NewModel(d->mParser); + const auto model = new NewModel(d->mParser); + new ModelTest(model, model); + return model; } diff --git a/framework/domain/modeltest.cpp b/framework/domain/modeltest.cpp new file mode 100644 index 00000000..295ff6d0 --- /dev/null +++ b/framework/domain/modeltest.cpp @@ -0,0 +1,588 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "modeltest.h" + +#include +#include + +#include +/*! + Connect to all of the models signals. Whenever anything happens recheck everything. +*/ +ModelTest::ModelTest ( QAbstractItemModel *_model, QObject *parent ) : QObject ( parent ), model ( _model ), fetchingMore ( false ) +{ + if (!model) + qFatal("%s: model must not be null", Q_FUNC_INFO); + + connect(model, SIGNAL(columnsAboutToBeInserted(QModelIndex,int,int)), + this, SLOT(runAllTests()) ); + connect(model, SIGNAL(columnsAboutToBeRemoved(QModelIndex,int,int)), + this, SLOT(runAllTests()) ); + connect(model, SIGNAL(columnsInserted(QModelIndex,int,int)), + this, SLOT(runAllTests()) ); + connect(model, SIGNAL(columnsRemoved(QModelIndex,int,int)), + this, SLOT(runAllTests()) ); + connect(model, SIGNAL(dataChanged(QModelIndex,QModelIndex)), + this, SLOT(runAllTests()) ); + connect(model, SIGNAL(headerDataChanged(Qt::Orientation,int,int)), + this, SLOT(runAllTests()) ); + connect(model, SIGNAL(layoutAboutToBeChanged()), this, SLOT(runAllTests()) ); + connect(model, SIGNAL(layoutChanged()), this, SLOT(runAllTests()) ); + connect(model, SIGNAL(modelReset()), this, SLOT(runAllTests()) ); + connect(model, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)), + this, SLOT(runAllTests()) ); + connect(model, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)), + this, SLOT(runAllTests()) ); + connect(model, SIGNAL(rowsInserted(QModelIndex,int,int)), + this, SLOT(runAllTests()) ); + connect(model, SIGNAL(rowsRemoved(QModelIndex,int,int)), + this, SLOT(runAllTests()) ); + + // Special checks for changes + connect(model, SIGNAL(layoutAboutToBeChanged()), + this, SLOT(layoutAboutToBeChanged()) ); + connect(model, SIGNAL(layoutChanged()), + this, SLOT(layoutChanged()) ); + + connect(model, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)), + this, SLOT(rowsAboutToBeInserted(QModelIndex,int,int)) ); + connect(model, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)), + this, SLOT(rowsAboutToBeRemoved(QModelIndex,int,int)) ); + connect(model, SIGNAL(rowsInserted(QModelIndex,int,int)), + this, SLOT(rowsInserted(QModelIndex,int,int)) ); + connect(model, SIGNAL(rowsRemoved(QModelIndex,int,int)), + this, SLOT(rowsRemoved(QModelIndex,int,int)) ); + connect(model, SIGNAL(dataChanged(QModelIndex,QModelIndex)), + this, SLOT(dataChanged(QModelIndex,QModelIndex)) ); + connect(model, SIGNAL(headerDataChanged(Qt::Orientation,int,int)), + this, SLOT(headerDataChanged(Qt::Orientation,int,int)) ); + + runAllTests(); +} + +void ModelTest::runAllTests() +{ + if ( fetchingMore ) + return; + nonDestructiveBasicTest(); + rowCount(); + columnCount(); + hasIndex(); + index(); + parent(); + data(); +} + +/*! + nonDestructiveBasicTest tries to call a number of the basic functions (not all) + to make sure the model doesn't outright segfault, testing the functions that makes sense. +*/ +void ModelTest::nonDestructiveBasicTest() +{ + QVERIFY( model->buddy ( QModelIndex() ) == QModelIndex() ); + model->canFetchMore ( QModelIndex() ); + QVERIFY( model->columnCount ( QModelIndex() ) >= 0 ); + QVERIFY( model->data ( QModelIndex() ) == QVariant() ); + fetchingMore = true; + model->fetchMore ( QModelIndex() ); + fetchingMore = false; + Qt::ItemFlags flags = model->flags ( QModelIndex() ); + QVERIFY( flags == Qt::ItemIsDropEnabled || flags == 0 ); + model->hasChildren ( QModelIndex() ); + model->hasIndex ( 0, 0 ); + model->headerData ( 0, Qt::Horizontal ); + model->index ( 0, 0 ); + model->itemData ( QModelIndex() ); + QVariant cache; + model->match ( QModelIndex(), -1, cache ); + model->mimeTypes(); + QVERIFY( model->parent ( QModelIndex() ) == QModelIndex() ); + QVERIFY( model->rowCount() >= 0 ); + QVariant variant; + model->setData ( QModelIndex(), variant, -1 ); + model->setHeaderData ( -1, Qt::Horizontal, QVariant() ); + model->setHeaderData ( 999999, Qt::Horizontal, QVariant() ); + QMap roles; + model->sibling ( 0, 0, QModelIndex() ); + model->span ( QModelIndex() ); + model->supportedDropActions(); +} + +/*! + Tests model's implementation of QAbstractItemModel::rowCount() and hasChildren() + + Models that are dynamically populated are not as fully tested here. + */ +void ModelTest::rowCount() +{ +// qDebug() << "rc"; + // check top row + QModelIndex topIndex = model->index ( 0, 0, QModelIndex() ); + int rows = model->rowCount ( topIndex ); + QVERIFY( rows >= 0 ); + if ( rows > 0 ) + QVERIFY( model->hasChildren ( topIndex ) ); + + QModelIndex secondLevelIndex = model->index ( 0, 0, topIndex ); + if ( secondLevelIndex.isValid() ) { // not the top level + // check a row count where parent is valid + rows = model->rowCount ( secondLevelIndex ); + QVERIFY( rows >= 0 ); + if ( rows > 0 ) + QVERIFY( model->hasChildren ( secondLevelIndex ) ); + } + + // The models rowCount() is tested more extensively in checkChildren(), + // but this catches the big mistakes +} + +/*! + Tests model's implementation of QAbstractItemModel::columnCount() and hasChildren() + */ +void ModelTest::columnCount() +{ + // check top row + QModelIndex topIndex = model->index ( 0, 0, QModelIndex() ); + QVERIFY( model->columnCount ( topIndex ) >= 0 ); + + // check a column count where parent is valid + QModelIndex childIndex = model->index ( 0, 0, topIndex ); + if ( childIndex.isValid() ) + QVERIFY( model->columnCount ( childIndex ) >= 0 ); + + // columnCount() is tested more extensively in checkChildren(), + // but this catches the big mistakes +} + +/*! + Tests model's implementation of QAbstractItemModel::hasIndex() + */ +void ModelTest::hasIndex() +{ +// qDebug() << "hi"; + // Make sure that invalid values returns an invalid index + QVERIFY( !model->hasIndex ( -2, -2 ) ); + QVERIFY( !model->hasIndex ( -2, 0 ) ); + QVERIFY( !model->hasIndex ( 0, -2 ) ); + + int rows = model->rowCount(); + int columns = model->columnCount(); + + // check out of bounds + QVERIFY( !model->hasIndex ( rows, columns ) ); + QVERIFY( !model->hasIndex ( rows + 1, columns + 1 ) ); + + if ( rows > 0 ) + QVERIFY( model->hasIndex ( 0, 0 ) ); + + // hasIndex() is tested more extensively in checkChildren(), + // but this catches the big mistakes +} + +/*! + Tests model's implementation of QAbstractItemModel::index() + */ +void ModelTest::index() +{ +// qDebug() << "i"; + // Make sure that invalid values returns an invalid index + QVERIFY( model->index ( -2, -2 ) == QModelIndex() ); + QVERIFY( model->index ( -2, 0 ) == QModelIndex() ); + QVERIFY( model->index ( 0, -2 ) == QModelIndex() ); + + int rows = model->rowCount(); + int columns = model->columnCount(); + + if ( rows == 0 ) + return; + + // Catch off by one errors + QVERIFY( model->index ( rows, columns ) == QModelIndex() ); + QVERIFY( model->index ( 0, 0 ).isValid() ); + + // Make sure that the same index is *always* returned + QModelIndex a = model->index ( 0, 0 ); + QModelIndex b = model->index ( 0, 0 ); + QVERIFY( a == b ); + + // index() is tested more extensively in checkChildren(), + // but this catches the big mistakes +} + +/*! + Tests model's implementation of QAbstractItemModel::parent() + */ +void ModelTest::parent() +{ +// qDebug() << "p"; + // Make sure the model won't crash and will return an invalid QModelIndex + // when asked for the parent of an invalid index. + QVERIFY( model->parent ( QModelIndex() ) == QModelIndex() ); + + if ( model->rowCount() == 0 ) + return; + + // Column 0 | Column 1 | + // QModelIndex() | | + // \- topIndex | topIndex1 | + // \- childIndex | childIndex1 | + + // Common error test #1, make sure that a top level index has a parent + // that is a invalid QModelIndex. + QModelIndex topIndex = model->index ( 0, 0, QModelIndex() ); + QVERIFY( model->parent ( topIndex ) == QModelIndex() ); + + // Common error test #2, make sure that a second level index has a parent + // that is the first level index. + if ( model->rowCount ( topIndex ) > 0 ) { + QModelIndex childIndex = model->index ( 0, 0, topIndex ); + QVERIFY( model->parent ( childIndex ) == topIndex ); + } + + // Common error test #3, the second column should NOT have the same children + // as the first column in a row. + // Usually the second column shouldn't have children. + QModelIndex topIndex1 = model->index ( 0, 1, QModelIndex() ); + if ( model->rowCount ( topIndex1 ) > 0 ) { + QModelIndex childIndex = model->index ( 0, 0, topIndex ); + QModelIndex childIndex1 = model->index ( 0, 0, topIndex1 ); + QVERIFY( childIndex != childIndex1 ); + } + + // Full test, walk n levels deep through the model making sure that all + // parent's children correctly specify their parent. + checkChildren ( QModelIndex() ); +} + +/*! + Called from the parent() test. + + A model that returns an index of parent X should also return X when asking + for the parent of the index. + + This recursive function does pretty extensive testing on the whole model in an + effort to catch edge cases. + + This function assumes that rowCount(), columnCount() and index() already work. + If they have a bug it will point it out, but the above tests should have already + found the basic bugs because it is easier to figure out the problem in + those tests then this one. + */ +void ModelTest::checkChildren ( const QModelIndex &parent, int currentDepth ) +{ + // First just try walking back up the tree. + QModelIndex p = parent; + while ( p.isValid() ) + p = p.parent(); + + // For models that are dynamically populated + if ( model->canFetchMore ( parent ) ) { + fetchingMore = true; + model->fetchMore ( parent ); + fetchingMore = false; + } + + int rows = model->rowCount ( parent ); + int columns = model->columnCount ( parent ); + + if ( rows > 0 ) + QVERIFY( model->hasChildren ( parent ) ); + + // Some further testing against rows(), columns(), and hasChildren() + QVERIFY( rows >= 0 ); + QVERIFY( columns >= 0 ); + if ( rows > 0 ) + QVERIFY( model->hasChildren ( parent ) ); + + qWarning() << "parent:" << model->data(parent).toString() << "rows:" << rows + << "columns:" << columns << "parent column:" << parent.column(); + + const QModelIndex topLeftChild = model->index( 0, 0, parent ); + + QVERIFY( !model->hasIndex ( rows + 1, 0, parent ) ); + for ( int r = 0; r < rows; ++r ) { + if ( model->canFetchMore ( parent ) ) { + fetchingMore = true; + model->fetchMore ( parent ); + fetchingMore = false; + } + QVERIFY( !model->hasIndex ( r, columns + 1, parent ) ); + for ( int c = 0; c < columns; ++c ) { + QVERIFY( model->hasIndex ( r, c, parent ) ); + QModelIndex index = model->index ( r, c, parent ); + // rowCount() and columnCount() said that it existed... + QVERIFY( index.isValid() ); + + qWarning() << "\tchild("<< r <<", " << c << ", " << index.column() << "):" << model->data(index).toString(); + + // index() should always return the same index when called twice in a row + QModelIndex modifiedIndex = model->index ( r, c, parent ); + QVERIFY( index == modifiedIndex ); + + // Make sure we get the same index if we request it twice in a row + QModelIndex a = model->index ( r, c, parent ); + QModelIndex b = model->index ( r, c, parent ); + QVERIFY( a == b ); + + { + const QModelIndex sibling = model->sibling( r, c, topLeftChild ); + QVERIFY( index == sibling ); + } + { + const QModelIndex sibling = topLeftChild.sibling( r, c ); + QVERIFY( index == sibling ); + } + + // Some basic checking on the index that is returned + QVERIFY( index.model() == model ); + QCOMPARE( index.row(), r ); + QCOMPARE( index.column(), c ); + // While you can technically return a QVariant usually this is a sign + // of a bug in data(). Disable if this really is ok in your model. +// QVERIFY( model->data ( index, Qt::DisplayRole ).isValid() ); + + // If the next test fails here is some somewhat useful debug you play with. + + if (model->parent(index) != parent) { + qWarning() << r << c << currentDepth << model->data(index).toString() + << model->data(parent).toString(); + qWarning() << index << parent << model->parent(index); +// And a view that you can even use to show the model. +// QTreeView view; +// view.setModel(model); +// view.show(); + } + + // Check that we can get back our real parent. + QCOMPARE( model->parent ( index ), parent ); + + // recursively go down the children + if ( model->hasChildren ( index ) && currentDepth < 10 ) { + qWarning() << r << c << "has children" << model->rowCount(index); + checkChildren ( index, ++currentDepth ); + }/* else { if (currentDepth >= 10) qDebug() << "checked 10 deep"; };*/ + + // make sure that after testing the children that the index doesn't change. + QModelIndex newerIndex = model->index ( r, c, parent ); + QVERIFY( index == newerIndex ); + } + } +} + +/*! + Tests model's implementation of QAbstractItemModel::data() + */ +void ModelTest::data() +{ + // Invalid index should return an invalid qvariant + QVERIFY( !model->data ( QModelIndex() ).isValid() ); + + if ( model->rowCount() == 0 ) + return; + + // A valid index should have a valid QVariant data + QVERIFY( model->index ( 0, 0 ).isValid() ); + + // shouldn't be able to set data on an invalid index + QVERIFY( !model->setData ( QModelIndex(), QLatin1String ( "foo" ), Qt::DisplayRole ) ); + + // General Purpose roles that should return a QString + QVariant variant = model->data ( model->index ( 0, 0 ), Qt::ToolTipRole ); + if ( variant.isValid() ) { + QVERIFY( variant.canConvert() ); + } + variant = model->data ( model->index ( 0, 0 ), Qt::StatusTipRole ); + if ( variant.isValid() ) { + QVERIFY( variant.canConvert() ); + } + variant = model->data ( model->index ( 0, 0 ), Qt::WhatsThisRole ); + if ( variant.isValid() ) { + QVERIFY( variant.canConvert() ); + } + + // General Purpose roles that should return a QSize + variant = model->data ( model->index ( 0, 0 ), Qt::SizeHintRole ); + if ( variant.isValid() ) { + QVERIFY( variant.canConvert() ); + } + + // General Purpose roles that should return a QFont + QVariant fontVariant = model->data ( model->index ( 0, 0 ), Qt::FontRole ); + if ( fontVariant.isValid() ) { + QVERIFY( fontVariant.canConvert() ); + } + + // Check that the alignment is one we know about + QVariant textAlignmentVariant = model->data ( model->index ( 0, 0 ), Qt::TextAlignmentRole ); + if ( textAlignmentVariant.isValid() ) { + int alignment = textAlignmentVariant.toInt(); + QCOMPARE( alignment, ( alignment & ( Qt::AlignHorizontal_Mask | Qt::AlignVertical_Mask ) ) ); + } + + // General Purpose roles that should return a QColor + QVariant colorVariant = model->data ( model->index ( 0, 0 ), Qt::BackgroundColorRole ); + if ( colorVariant.isValid() ) { + QVERIFY( colorVariant.canConvert() ); + } + + colorVariant = model->data ( model->index ( 0, 0 ), Qt::TextColorRole ); + if ( colorVariant.isValid() ) { + QVERIFY( colorVariant.canConvert() ); + } + + // Check that the "check state" is one we know about. + QVariant checkStateVariant = model->data ( model->index ( 0, 0 ), Qt::CheckStateRole ); + if ( checkStateVariant.isValid() ) { + int state = checkStateVariant.toInt(); + QVERIFY( state == Qt::Unchecked || + state == Qt::PartiallyChecked || + state == Qt::Checked ); + } +} + +/*! + Store what is about to be inserted to make sure it actually happens + + \sa rowsInserted() + */ +void ModelTest::rowsAboutToBeInserted ( const QModelIndex &parent, int start, int /* end */) +{ +// Q_UNUSED(end); +// qDebug() << "rowsAboutToBeInserted" << "start=" << start << "end=" << end << "parent=" << model->data ( parent ).toString() +// << "current count of parent=" << model->rowCount ( parent ); // << "display of last=" << model->data( model->index(start-1, 0, parent) ); +// qDebug() << model->index(start-1, 0, parent) << model->data( model->index(start-1, 0, parent) ); + Changing c; + c.parent = parent; + c.oldSize = model->rowCount ( parent ); + c.last = model->data ( model->index ( start - 1, 0, parent ) ); + c.next = model->data ( model->index ( start, 0, parent ) ); + insert.push ( c ); +} + +/*! + Confirm that what was said was going to happen actually did + + \sa rowsAboutToBeInserted() + */ +void ModelTest::rowsInserted ( const QModelIndex & parent, int start, int end ) +{ + Changing c = insert.pop(); + QVERIFY( c.parent == parent ); +// qDebug() << "rowsInserted" << "start=" << start << "end=" << end << "oldsize=" << c.oldSize +// << "parent=" << model->data ( parent ).toString() << "current rowcount of parent=" << model->rowCount ( parent ); + +// for (int ii=start; ii <= end; ii++) +// { +// qDebug() << "itemWasInserted:" << ii << model->data ( model->index ( ii, 0, parent )); +// } +// qDebug(); + + QVERIFY( c.oldSize + ( end - start + 1 ) == model->rowCount ( parent ) ); + QVERIFY( c.last == model->data ( model->index ( start - 1, 0, c.parent ) ) ); + + if (c.next != model->data(model->index(end + 1, 0, c.parent))) { + qDebug() << start << end; + for (int i=0; i < model->rowCount(); ++i) + qDebug() << model->index(i, 0).data().toString(); + qDebug() << c.next << model->data(model->index(end + 1, 0, c.parent)); + } + + QVERIFY( c.next == model->data ( model->index ( end + 1, 0, c.parent ) ) ); +} + +void ModelTest::layoutAboutToBeChanged() +{ + for ( int i = 0; i < qBound ( 0, model->rowCount(), 100 ); ++i ) + changing.append ( QPersistentModelIndex ( model->index ( i, 0 ) ) ); +} + +void ModelTest::layoutChanged() +{ + for ( int i = 0; i < changing.count(); ++i ) { + QPersistentModelIndex p = changing[i]; + QVERIFY( p == model->index ( p.row(), p.column(), p.parent() ) ); + } + changing.clear(); +} + +/*! + Store what is about to be inserted to make sure it actually happens + + \sa rowsRemoved() + */ +void ModelTest::rowsAboutToBeRemoved ( const QModelIndex &parent, int start, int end ) +{ +qDebug() << "ratbr" << parent << start << end; + Changing c; + c.parent = parent; + c.oldSize = model->rowCount ( parent ); + c.last = model->data ( model->index ( start - 1, 0, parent ) ); + c.next = model->data ( model->index ( end + 1, 0, parent ) ); + remove.push ( c ); +} + +/*! + Confirm that what was said was going to happen actually did + + \sa rowsAboutToBeRemoved() + */ +void ModelTest::rowsRemoved ( const QModelIndex & parent, int start, int end ) +{ + qDebug() << "rr" << parent << start << end; + Changing c = remove.pop(); + QVERIFY( c.parent == parent ); + QVERIFY( c.oldSize - ( end - start + 1 ) == model->rowCount ( parent ) ); + QVERIFY( c.last == model->data ( model->index ( start - 1, 0, c.parent ) ) ); + QVERIFY( c.next == model->data ( model->index ( start, 0, c.parent ) ) ); +} + +void ModelTest::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + QVERIFY(topLeft.isValid()); + QVERIFY(bottomRight.isValid()); + QModelIndex commonParent = bottomRight.parent(); + QCOMPARE(topLeft.parent(), commonParent); + QVERIFY(topLeft.row() <= bottomRight.row()); + QVERIFY(topLeft.column() <= bottomRight.column()); + int rowCount = model->rowCount(commonParent); + int columnCount = model->columnCount(commonParent); + QVERIFY(bottomRight.row() < rowCount); + QVERIFY(bottomRight.column() < columnCount); +} + +void ModelTest::headerDataChanged(Qt::Orientation orientation, int start, int end) +{ + QVERIFY(start >= 0); + QVERIFY(end >= 0); + QVERIFY(start <= end); + int itemCount = orientation == Qt::Vertical ? model->rowCount() : model->columnCount(); + QVERIFY(start < itemCount); + QVERIFY(end < itemCount); +} + diff --git a/framework/domain/modeltest.h b/framework/domain/modeltest.h new file mode 100644 index 00000000..735a4227 --- /dev/null +++ b/framework/domain/modeltest.h @@ -0,0 +1,83 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + + +#ifndef MODELTEST_H +#define MODELTEST_H + +#include +#include +#include + +class ModelTest : public QObject +{ + Q_OBJECT + +public: + ModelTest( QAbstractItemModel *model, QObject *parent = 0 ); + +private Q_SLOTS: + void nonDestructiveBasicTest(); + void rowCount(); + void columnCount(); + void hasIndex(); + void index(); + void parent(); + void data(); + +protected Q_SLOTS: + void runAllTests(); + void layoutAboutToBeChanged(); + void layoutChanged(); + void rowsAboutToBeInserted( const QModelIndex &parent, int start, int end ); + void rowsInserted( const QModelIndex & parent, int start, int end ); + void rowsAboutToBeRemoved( const QModelIndex &parent, int start, int end ); + void rowsRemoved( const QModelIndex & parent, int start, int end ); + void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); + void headerDataChanged(Qt::Orientation orientation, int start, int end); + +private: + void checkChildren( const QModelIndex &parent, int currentDepth = 0 ); + + QAbstractItemModel *model; + + struct Changing { + QModelIndex parent; + int oldSize; + QVariant last; + QVariant next; + }; + QStack insert; + QStack remove; + + bool fetchingMore; + + QList changing; +}; + +#endif -- cgit v1.2.3 From d708e1310cc8e65fab078eb6b5b4a325de462a24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sandro=20Knau=C3=9F?= Date: Tue, 18 Oct 2016 15:30:11 +0200 Subject: start implementing a nested model with sig->enc->part->content --- framework/domain/messageparser.h | 9 +- framework/domain/messageparser_new.cpp | 313 +++++++++++++++++++++++++++++++-- 2 files changed, 302 insertions(+), 20 deletions(-) (limited to 'framework') diff --git a/framework/domain/messageparser.h b/framework/domain/messageparser.h index 5eb355e7..e3b81dd7 100644 --- a/framework/domain/messageparser.h +++ b/framework/domain/messageparser.h @@ -33,11 +33,15 @@ class QAbstractItemModel; class Parser; class Part; +class Encryption; +class Signature; typedef std::shared_ptr PartPtr; class Content; typedef std::shared_ptr ContentPtr; class MessagePartPrivate; +class NewModelPrivate; + class MessageParser : public QObject { Q_OBJECT @@ -123,6 +127,7 @@ class NewModel : public QAbstractItemModel { Q_OBJECT public: NewModel(std::shared_ptr parser); + ~NewModel(); public: enum Roles { @@ -140,8 +145,6 @@ public: int columnCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; private: - std::shared_ptr mParser; - QVector mParts; - QMap> mContentMap; + std::unique_ptr d; }; diff --git a/framework/domain/messageparser_new.cpp b/framework/domain/messageparser_new.cpp index 4395f2e3..ffe7550f 100644 --- a/framework/domain/messageparser_new.cpp +++ b/framework/domain/messageparser_new.cpp @@ -21,8 +21,38 @@ #include -NewModel::NewModel(std::shared_ptr parser) - : mParser(parser) +Q_DECLARE_METATYPE(Part *) +Q_DECLARE_METATYPE(Content *) +Q_DECLARE_METATYPE(Signature *) +Q_DECLARE_METATYPE(Encryption *) + +class NewModelPrivate +{ +public: + NewModelPrivate(NewModel *q_ptr, const std::shared_ptr &parser); + + QSharedPointer getVar(const std::shared_ptr &sig); + QSharedPointer getVar(const std::shared_ptr &enc); + QSharedPointer getVar(const std::shared_ptr &part); + + int getPos(Signature *sig); + int getPos(Encryption *enc); + int getPos(Part *part); + + NewModel *q; + QVector mParts; + + std::shared_ptr mParser; + QMap> mContentMap; +private: + QMap, QSharedPointer> mSignatureMap; + QMap, QSharedPointer> mEncryptionMap; + QMap, QSharedPointer> mPartMap; +}; + +NewModelPrivate::NewModelPrivate(NewModel *q_ptr, const std::shared_ptr &parser) + : q(q_ptr) + , mParser(parser) { mParts = mParser->collectContentParts(); foreach(const auto &part, mParts) { @@ -30,6 +60,84 @@ NewModel::NewModel(std::shared_ptr parser) } } +QSharedPointer NewModelPrivate::getVar(const std::shared_ptr &sig) +{ + if (!mSignatureMap.contains(sig)) { + auto var = new QVariant(); + var->setValue(sig.get()); + mSignatureMap.insert(sig, QSharedPointer(var)); + } + return mSignatureMap.value(sig); +} + +QSharedPointer NewModelPrivate::getVar(const std::shared_ptr &enc) +{ + if (!mEncryptionMap.contains(enc)) { + auto var = new QVariant(); + var->setValue(enc.get()); + mEncryptionMap.insert(enc, QSharedPointer(var)); + } + return mEncryptionMap.value(enc); +} + +QSharedPointer NewModelPrivate::getVar(const std::shared_ptr &part) +{ + if (!mPartMap.contains(part)) { + auto var = new QVariant(); + var->setValue(part.get()); + mPartMap.insert(part, QSharedPointer(var)); + } + return mPartMap.value(part); +} + +int NewModelPrivate::getPos(Signature *signature) +{ + const auto first = mParts.first(); + int i = 0; + foreach(const auto &sig, first->signatures()) { + if (sig.get() == signature) { + break; + } + i++; + } + return i; +} + +int NewModelPrivate::getPos(Encryption *encryption) +{ + const auto first = mParts.first(); + int i = 0; + foreach(const auto &enc, first->encryptions()) { + if (enc.get() == encryption) { + break; + } + i++; + } + return i; +} + + +int NewModelPrivate::getPos(Part *part) +{ + int i = 0; + foreach(const auto &p, mParts) { + if (p.get() == part) { + break; + } + i++; + } + return i; +} + +NewModel::NewModel(std::shared_ptr parser) + : d(std::unique_ptr(new NewModelPrivate(this, parser))) +{ +} + +NewModel::~NewModel() +{ +} + QHash NewModel::roleNames() const { QHash roles; @@ -40,12 +148,71 @@ QHash NewModel::roleNames() const return roles; } + QModelIndex NewModel::index(int row, int column, const QModelIndex &parent) const { + if (row < 0 || column != 0) { + return QModelIndex(); + } if (!parent.isValid()) { - if (row < mParts.size()) { - auto part = mParts.at(row); - return createIndex(row, column, part.get()); + const auto first = d->mParts.first(); + if (first->signatures().size() > 0) { + if (row == 0) { + const auto sig = first->signatures().at(row); + return createIndex(row, column, d->getVar(sig).data()); + } + } else if (first->encryptions().size() > 0) { + if (row == 0) { + const auto enc = first->encryptions().at(row); + return createIndex(row, column, d->getVar(enc).data()); + } + } else { + if (row < d->mParts.size()) { + auto part = d->mParts.at(row); + return createIndex(row, column, d->getVar(part).data()); + } + } + } else { + if (!parent.internalPointer()) { + return QModelIndex(); + } + const auto data = static_cast(parent.internalPointer()); + const auto first = d->mParts.first(); + int encpos = -1; + int partpos = -1; + if (data->userType() == qMetaTypeId()) { + const auto signature = data->value(); + int i = d->getPos(signature); + + if (i+1 < first->signatures().size()) { + if (row != 0) { + return QModelIndex(); + } + const auto sig = first->signatures().at(i+1); + return createIndex(0,0, d->getVar(sig).data()); + } + + if (first->encryptions().size() > 0) { + encpos = 0; + } + } else if (data->userType() == qMetaTypeId()) { + const auto encryption = data->value(); + encpos = d->getPos(encryption) + 1; + } + + if (encpos > -1 && encpos < first->encryptions().size()) { + if (row != 0) { + return QModelIndex(); + } + const auto enc = first->encryptions().at(encpos); + return createIndex(0,0, d->getVar(enc).data()); + } + + if (row < d->mParts.size()) { + auto part = d->mParts.at(row); + auto var = new QVariant(); + var->setValue(part.get()); + return createIndex(row, column, d->getVar(part).data()); } } return QModelIndex(); @@ -53,17 +220,44 @@ QModelIndex NewModel::index(int row, int column, const QModelIndex &parent) cons QVariant NewModel::data(const QModelIndex &index, int role) const { - if (!index.parent().isValid()) { - auto part = static_cast(index.internalPointer()); - switch (role) { - case TypeRole: - return QString::fromLatin1(part->type()); - case IsEmbededRole: - return index.parent().isValid(); - case SecurityLevelRole: - return QStringLiteral("GRAY"); - case ContentsRole: - return QVariant::fromValue(mContentMap.value(part).get()); + if (!index.isValid()) { + if (role == Qt::DisplayRole) { + return QString("root"); + } + return QVariant(); + } + if (index.internalPointer()) { + const auto data = static_cast(index.internalPointer()); + if (data->userType() == qMetaTypeId()) { + const auto signature = data->value(); + int i = d->getPos(signature); + switch(role) { + case Qt::DisplayRole: + case TypeRole: + return QStringLiteral("Signature%1").arg(i); + } + } else if (data->userType() == qMetaTypeId()) { + const auto first = d->mParts.first(); + const auto encryption = data->value(); + int i = d->getPos(encryption); + switch(role) { + case Qt::DisplayRole: + case TypeRole: + return QStringLiteral("Encryption%1").arg(i); + } + } else if (data->userType() == qMetaTypeId()) { + const auto part = data->value(); + switch (role) { + case Qt::DisplayRole: + case TypeRole: + return QString::fromLatin1(part->type()); + case IsEmbededRole: + return index.parent().isValid(); + case SecurityLevelRole: + return QStringLiteral("GRAY"); + case ContentsRole: + return QVariant::fromValue(d->mContentMap.value(part).get()); + } } } return QVariant(); @@ -71,13 +265,98 @@ QVariant NewModel::data(const QModelIndex &index, int role) const QModelIndex NewModel::parent(const QModelIndex &index) const { + if (!index.internalPointer()) { + return QModelIndex(); + } + const auto data = static_cast(index.internalPointer()); + if (data->userType() == qMetaTypeId()) { + const auto signature = data->value(); + const auto first = d->mParts.first(); + int i = d->getPos(signature); + + if (i > 1) { + const auto sig = first->signatures().at(i-1); + return createIndex(0, 0, d->getVar(sig).data()); + } + + return QModelIndex(); + } else if (data->userType() == qMetaTypeId()) { + const auto encryption = data->value(); + const auto first = d->mParts.first(); + int i = d->getPos(encryption); + + if (i > 1) { + const auto enc = first->encryptions().at(i-1); + return createIndex(0, 0, d->getVar(enc).data()); + } + + if (first->signatures().size() > 0) { + const int row = first->signatures().size() - 1; + const auto sig = first->signatures().at(row); + return createIndex(0, 0, d->getVar(sig).data()); + } else { + return QModelIndex(); + } + } else if (data->userType() == qMetaTypeId()) { + const auto first = d->mParts.first(); + if (first->encryptions().size() > 0) { + const int row = first->encryptions().size() - 1; + const auto enc = first->encryptions().at(row); + auto var = new QVariant(); + var->setValue(enc.get()); + return createIndex(0, 0, d->getVar(enc).data()); + } + if (first->signatures().size() > 0) { + const int row = first->signatures().size() - 1; + const auto sig = first->signatures().at(row); + return createIndex(0, 0, d->getVar(sig).data()); + } + return QModelIndex(); + } return QModelIndex(); } int NewModel::rowCount(const QModelIndex &parent) const { if (!parent.isValid()) { - return mParts.size(); + const auto first = d->mParts.first(); + if (first->signatures().size() > 0) { + return 1; + } else if (first->encryptions().size() > 0) { + return 1; + } else { + return d->mParts.size(); + } + } else { + if (!parent.internalPointer()) { + return 0; + } + const auto data = static_cast(parent.internalPointer()); + if (data->userType() == qMetaTypeId()) { + const auto signature = data->value(); + const auto first = d->mParts.first(); + int i = d->getPos(signature); + + if (i+1 < first->signatures().size()) { + return 1; + } + + if (first->encryptions().size() > 0) { + return 1; + } + + return d->mParts.size(); + } else if (data->userType() == qMetaTypeId()) { + const auto encryption = data->value(); + const auto first = d->mParts.first(); + int i = d->getPos(encryption); + + if (i+1 < first->encryptions().size()) { + return 1; + } + + return d->mParts.size(); + } } return 0; } -- cgit v1.2.3 From 6db3df1d512866da62241107aeebd27bd4e83237 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sandro=20Knau=C3=9F?= Date: Tue, 18 Oct 2016 17:45:06 +0200 Subject: add content level --- framework/domain/messageparser_new.cpp | 71 +++++++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 2 deletions(-) (limited to 'framework') diff --git a/framework/domain/messageparser_new.cpp b/framework/domain/messageparser_new.cpp index ffe7550f..44cd6b6b 100644 --- a/framework/domain/messageparser_new.cpp +++ b/framework/domain/messageparser_new.cpp @@ -34,10 +34,14 @@ public: QSharedPointer getVar(const std::shared_ptr &sig); QSharedPointer getVar(const std::shared_ptr &enc); QSharedPointer getVar(const std::shared_ptr &part); + QSharedPointer getVar(Part *part); + QSharedPointer getVar(const std::shared_ptr &content); + QSharedPointer getVar(Content *content); int getPos(Signature *sig); int getPos(Encryption *enc); int getPos(Part *part); + int getPos(Content *content); NewModel *q; QVector mParts; @@ -47,7 +51,8 @@ public: private: QMap, QSharedPointer> mSignatureMap; QMap, QSharedPointer> mEncryptionMap; - QMap, QSharedPointer> mPartMap; + QMap> mPartMap; + QMap> mCMap; }; NewModelPrivate::NewModelPrivate(NewModel *q_ptr, const std::shared_ptr &parser) @@ -80,16 +85,37 @@ QSharedPointer NewModelPrivate::getVar(const std::shared_ptr NewModelPrivate::getVar(const std::shared_ptr &part) +{ + return getVar(part.get()); +} + +QSharedPointer NewModelPrivate::getVar(Part *part) { if (!mPartMap.contains(part)) { auto var = new QVariant(); - var->setValue(part.get()); + var->setValue(part); mPartMap.insert(part, QSharedPointer(var)); } return mPartMap.value(part); } +QSharedPointer NewModelPrivate::getVar(const std::shared_ptr &content) +{ + return getVar(content.get()); +} + +QSharedPointer NewModelPrivate::getVar(Content *content) +{ + if (!mCMap.contains(content)) { + auto var = new QVariant(); + var->setValue(content); + mCMap.insert(content, QSharedPointer(var)); + } + return mCMap.value(content); +} + int NewModelPrivate::getPos(Signature *signature) { const auto first = mParts.first(); @@ -129,6 +155,18 @@ int NewModelPrivate::getPos(Part *part) return i; } +int NewModelPrivate::getPos(Content *content) +{ + int i = 0; + foreach(const auto &c, content->parent()->content()) { + if (c.get() == content) { + break; + } + i++; + } + return i; +} + NewModel::NewModel(std::shared_ptr parser) : d(std::unique_ptr(new NewModelPrivate(this, parser))) { @@ -198,6 +236,13 @@ QModelIndex NewModel::index(int row, int column, const QModelIndex &parent) cons } else if (data->userType() == qMetaTypeId()) { const auto encryption = data->value(); encpos = d->getPos(encryption) + 1; + } else if (data->userType() == qMetaTypeId()) { + const auto part = data->value(); + if (row < part->content().size()) { + auto c = part->content().at(row); + return createIndex(row, column, d->getVar(c).data()); + } + return QModelIndex(); } if (encpos > -1 && encpos < first->encryptions().size()) { @@ -258,6 +303,14 @@ QVariant NewModel::data(const QModelIndex &index, int role) const case ContentsRole: return QVariant::fromValue(d->mContentMap.value(part).get()); } + } else if (data->userType() == qMetaTypeId()) { + const auto content = data->value(); + int i = d->getPos(content); + switch(role) { + case Qt::DisplayRole: + case TypeRole: + return QStringLiteral("Content%1").arg(i); + } } } return QVariant(); @@ -312,6 +365,17 @@ QModelIndex NewModel::parent(const QModelIndex &index) const return createIndex(0, 0, d->getVar(sig).data()); } return QModelIndex(); + } else if (data->userType() == qMetaTypeId()) { + const auto content = data->value(); + const auto parent = content->parent(); + if (!parent) { + return QModelIndex(); + } + int pos = d->getPos(parent); + if (pos < 0 || pos >= d->mParts.size()) { + return QModelIndex(); + } + return createIndex(pos, 0, d->getVar(parent).data()); } return QModelIndex(); } @@ -356,6 +420,9 @@ int NewModel::rowCount(const QModelIndex &parent) const } return d->mParts.size(); + } else if (data->userType() == qMetaTypeId()) { + const auto part = data->value(); + return part->content().size(); } } return 0; -- cgit v1.2.3 From 2e83f97afb05d6475bc39375400e7846bd77bdff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sandro=20Knau=C3=9F?= Date: Tue, 18 Oct 2016 19:00:23 +0200 Subject: create Tree beforehand --- framework/domain/messageparser_new.cpp | 292 +++++++++++++++------------------ 1 file changed, 132 insertions(+), 160 deletions(-) (limited to 'framework') diff --git a/framework/domain/messageparser_new.cpp b/framework/domain/messageparser_new.cpp index 44cd6b6b..2a3b68e9 100644 --- a/framework/domain/messageparser_new.cpp +++ b/framework/domain/messageparser_new.cpp @@ -26,11 +26,54 @@ Q_DECLARE_METATYPE(Content *) Q_DECLARE_METATYPE(Signature *) Q_DECLARE_METATYPE(Encryption *) +class Entry +{ +public: + ~Entry() + { + foreach(auto child, mChildren) { + delete child; + } + mChildren.clear(); + } + + void addChild(Entry *entry) + { + mChildren.append(entry); + entry->mParent = this; + } + + int pos() + { + if(!mParent) { + return -1; + } + int i=0; + foreach(const auto &child, mParent->mChildren) { + if (child == this) { + return i; + } + i++; + } + return -1; + } + + QSharedPointer mData; + + Entry *mParent; + QVector mChildren; +}; + class NewModelPrivate { public: NewModelPrivate(NewModel *q_ptr, const std::shared_ptr &parser); - + + void createTree(); + Entry *addSignatures(Entry *parent, QVector signatures); + Entry *addEncryptions(Entry *parent, QVector encryptions); + Entry *addPart(Entry *parent, Part *part); + QSharedPointer getVar(const std::shared_ptr &sig); QSharedPointer getVar(const std::shared_ptr &enc); QSharedPointer getVar(const std::shared_ptr &part); @@ -45,6 +88,7 @@ public: NewModel *q; QVector mParts; + Entry *mRoot; std::shared_ptr mParser; QMap> mContentMap; @@ -63,6 +107,79 @@ NewModelPrivate::NewModelPrivate(NewModel *q_ptr, const std::shared_ptr foreach(const auto &part, mParts) { mContentMap.insert(part.get(), std::shared_ptr(new NewContentModel(part, mParser))); } + createTree(); +} + +Entry * NewModelPrivate::addSignatures(Entry *parent, QVector signatures) +{ + auto ret = parent; + foreach(const auto &sig, signatures) { + auto entry = new Entry(); + entry->mData = getVar(sig); + ret = entry; + parent->addChild(entry); + } + return ret; +} + +Entry * NewModelPrivate::addEncryptions(Entry *parent, QVector encryptions) +{ + auto ret = parent; + foreach(const auto &enc, encryptions) { + auto entry = new Entry(); + entry->mData = getVar(enc); + parent->addChild(entry); + ret = entry; + } + return ret; +} + +Entry * NewModelPrivate::addPart(Entry *parent, Part *part) +{ + auto entry = new Entry(); + entry->mData = getVar(part); + parent->addChild(entry); + + foreach(const auto &content, part->content()) { + auto _entry = entry; + _entry = addSignatures(_entry, content->signatures().mid(part->signatures().size())); + _entry = addEncryptions(_entry, content->encryptions().mid(part->encryptions().size())); + auto c = new Entry(); + c->mData = getVar(content); + _entry->addChild(c); + } + + foreach(const auto &sp, part->subParts()) { + auto _entry = entry; + _entry = addSignatures(_entry, sp->signatures().mid(part->signatures().size())); + _entry = addEncryptions(_entry, sp->encryptions().mid(part->encryptions().size())); + addPart(_entry, sp.get()); + } + return entry; +} + +void NewModelPrivate::createTree() +{ + mRoot = new Entry(); + auto parent = mRoot; + Part *pPart = nullptr; + QVector signatures; + QVector encryptions; + foreach(const auto part, mParts) { + auto _parent = parent; + if (pPart != part->parent()) { + auto _parent = mRoot; + _parent = addSignatures(_parent, part->parent()->signatures()); + _parent = addEncryptions(_parent, part->parent()->encryptions()); + signatures = part->parent()->signatures(); + encryptions = part->parent()->encryptions(); + parent = _parent; + pPart = part->parent(); + } + _parent = addSignatures(_parent, part->signatures().mid(signatures.size())); + _parent = addEncryptions(_parent, part->encryptions().mid(encryptions.size())); + addPart(_parent, part.get()); + } } QSharedPointer NewModelPrivate::getVar(const std::shared_ptr &sig) @@ -192,73 +309,13 @@ QModelIndex NewModel::index(int row, int column, const QModelIndex &parent) cons if (row < 0 || column != 0) { return QModelIndex(); } - if (!parent.isValid()) { - const auto first = d->mParts.first(); - if (first->signatures().size() > 0) { - if (row == 0) { - const auto sig = first->signatures().at(row); - return createIndex(row, column, d->getVar(sig).data()); - } - } else if (first->encryptions().size() > 0) { - if (row == 0) { - const auto enc = first->encryptions().at(row); - return createIndex(row, column, d->getVar(enc).data()); - } - } else { - if (row < d->mParts.size()) { - auto part = d->mParts.at(row); - return createIndex(row, column, d->getVar(part).data()); - } - } - } else { - if (!parent.internalPointer()) { - return QModelIndex(); - } - const auto data = static_cast(parent.internalPointer()); - const auto first = d->mParts.first(); - int encpos = -1; - int partpos = -1; - if (data->userType() == qMetaTypeId()) { - const auto signature = data->value(); - int i = d->getPos(signature); - - if (i+1 < first->signatures().size()) { - if (row != 0) { - return QModelIndex(); - } - const auto sig = first->signatures().at(i+1); - return createIndex(0,0, d->getVar(sig).data()); - } - - if (first->encryptions().size() > 0) { - encpos = 0; - } - } else if (data->userType() == qMetaTypeId()) { - const auto encryption = data->value(); - encpos = d->getPos(encryption) + 1; - } else if (data->userType() == qMetaTypeId()) { - const auto part = data->value(); - if (row < part->content().size()) { - auto c = part->content().at(row); - return createIndex(row, column, d->getVar(c).data()); - } - return QModelIndex(); - } - - if (encpos > -1 && encpos < first->encryptions().size()) { - if (row != 0) { - return QModelIndex(); - } - const auto enc = first->encryptions().at(encpos); - return createIndex(0,0, d->getVar(enc).data()); - } + Entry *entry = d->mRoot; + if (parent.isValid()) { + entry = static_cast(parent.internalPointer()); + } - if (row < d->mParts.size()) { - auto part = d->mParts.at(row); - auto var = new QVariant(); - var->setValue(part.get()); - return createIndex(row, column, d->getVar(part).data()); - } + if (row < entry->mChildren.size()) { + return createIndex(row, column, entry->mChildren.at(row)); } return QModelIndex(); } @@ -272,7 +329,8 @@ QVariant NewModel::data(const QModelIndex &index, int role) const return QVariant(); } if (index.internalPointer()) { - const auto data = static_cast(index.internalPointer()); + const auto entry = static_cast(index.internalPointer()); + const auto data = entry->mData; if (data->userType() == qMetaTypeId()) { const auto signature = data->value(); int i = d->getPos(signature); @@ -321,61 +379,9 @@ QModelIndex NewModel::parent(const QModelIndex &index) const if (!index.internalPointer()) { return QModelIndex(); } - const auto data = static_cast(index.internalPointer()); - if (data->userType() == qMetaTypeId()) { - const auto signature = data->value(); - const auto first = d->mParts.first(); - int i = d->getPos(signature); - - if (i > 1) { - const auto sig = first->signatures().at(i-1); - return createIndex(0, 0, d->getVar(sig).data()); - } - - return QModelIndex(); - } else if (data->userType() == qMetaTypeId()) { - const auto encryption = data->value(); - const auto first = d->mParts.first(); - int i = d->getPos(encryption); - - if (i > 1) { - const auto enc = first->encryptions().at(i-1); - return createIndex(0, 0, d->getVar(enc).data()); - } - - if (first->signatures().size() > 0) { - const int row = first->signatures().size() - 1; - const auto sig = first->signatures().at(row); - return createIndex(0, 0, d->getVar(sig).data()); - } else { - return QModelIndex(); - } - } else if (data->userType() == qMetaTypeId()) { - const auto first = d->mParts.first(); - if (first->encryptions().size() > 0) { - const int row = first->encryptions().size() - 1; - const auto enc = first->encryptions().at(row); - auto var = new QVariant(); - var->setValue(enc.get()); - return createIndex(0, 0, d->getVar(enc).data()); - } - if (first->signatures().size() > 0) { - const int row = first->signatures().size() - 1; - const auto sig = first->signatures().at(row); - return createIndex(0, 0, d->getVar(sig).data()); - } - return QModelIndex(); - } else if (data->userType() == qMetaTypeId()) { - const auto content = data->value(); - const auto parent = content->parent(); - if (!parent) { - return QModelIndex(); - } - int pos = d->getPos(parent); - if (pos < 0 || pos >= d->mParts.size()) { - return QModelIndex(); - } - return createIndex(pos, 0, d->getVar(parent).data()); + const auto entry = static_cast(index.internalPointer()); + if (entry->mParent) { + return createIndex(entry->pos(), 0, entry->mParent); } return QModelIndex(); } @@ -383,47 +389,13 @@ QModelIndex NewModel::parent(const QModelIndex &index) const int NewModel::rowCount(const QModelIndex &parent) const { if (!parent.isValid()) { - const auto first = d->mParts.first(); - if (first->signatures().size() > 0) { - return 1; - } else if (first->encryptions().size() > 0) { - return 1; - } else { - return d->mParts.size(); - } + return d->mRoot->mChildren.size(); } else { if (!parent.internalPointer()) { return 0; } - const auto data = static_cast(parent.internalPointer()); - if (data->userType() == qMetaTypeId()) { - const auto signature = data->value(); - const auto first = d->mParts.first(); - int i = d->getPos(signature); - - if (i+1 < first->signatures().size()) { - return 1; - } - - if (first->encryptions().size() > 0) { - return 1; - } - - return d->mParts.size(); - } else if (data->userType() == qMetaTypeId()) { - const auto encryption = data->value(); - const auto first = d->mParts.first(); - int i = d->getPos(encryption); - - if (i+1 < first->encryptions().size()) { - return 1; - } - - return d->mParts.size(); - } else if (data->userType() == qMetaTypeId()) { - const auto part = data->value(); - return part->content().size(); - } + const auto entry = static_cast(parent.internalPointer()); + return entry->mChildren.size(); } return 0; } -- cgit v1.2.3 From be38c2ebaa77b116028cbe34f777e3d8e5cbc776 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sandro=20Knau=C3=9F?= Date: Tue, 18 Oct 2016 20:33:00 +0200 Subject: output content from content item --- framework/domain/messageparser.h | 1 + framework/domain/messageparser_new.cpp | 57 ++++++++++++++++++++++++++-------- 2 files changed, 45 insertions(+), 13 deletions(-) (limited to 'framework') diff --git a/framework/domain/messageparser.h b/framework/domain/messageparser.h index e3b81dd7..55c884f9 100644 --- a/framework/domain/messageparser.h +++ b/framework/domain/messageparser.h @@ -133,6 +133,7 @@ public: enum Roles { TypeRole = Qt::UserRole + 1, ContentsRole, + ContentRole, IsEmbededRole, SecurityLevelRole }; diff --git a/framework/domain/messageparser_new.cpp b/framework/domain/messageparser_new.cpp index 2a3b68e9..db4b36c5 100644 --- a/framework/domain/messageparser_new.cpp +++ b/framework/domain/messageparser_new.cpp @@ -298,6 +298,7 @@ QHash NewModel::roleNames() const QHash roles; roles[TypeRole] = "type"; roles[ContentsRole] = "contents"; + roles[ContentRole] = "content"; roles[IsEmbededRole] = "embeded"; roles[SecurityLevelRole] = "securityLevel"; return roles; @@ -330,26 +331,27 @@ QVariant NewModel::data(const QModelIndex &index, int role) const } if (index.internalPointer()) { const auto entry = static_cast(index.internalPointer()); - const auto data = entry->mData; - if (data->userType() == qMetaTypeId()) { - const auto signature = data->value(); + const auto _data = entry->mData; + if (_data->userType() == qMetaTypeId()) { + const auto signature = _data->value(); int i = d->getPos(signature); switch(role) { case Qt::DisplayRole: - case TypeRole: return QStringLiteral("Signature%1").arg(i); + case TypeRole: + return QStringLiteral("Signature"); } - } else if (data->userType() == qMetaTypeId()) { - const auto first = d->mParts.first(); - const auto encryption = data->value(); + } else if (_data->userType() == qMetaTypeId()) { + const auto encryption = _data->value(); int i = d->getPos(encryption); switch(role) { case Qt::DisplayRole: - case TypeRole: return QStringLiteral("Encryption%1").arg(i); + case TypeRole: + return QStringLiteral("Encryption"); } - } else if (data->userType() == qMetaTypeId()) { - const auto part = data->value(); + } else if (_data->userType() == qMetaTypeId()) { + const auto part = _data->value(); switch (role) { case Qt::DisplayRole: case TypeRole: @@ -361,13 +363,42 @@ QVariant NewModel::data(const QModelIndex &index, int role) const case ContentsRole: return QVariant::fromValue(d->mContentMap.value(part).get()); } - } else if (data->userType() == qMetaTypeId()) { - const auto content = data->value(); + } else if (_data->userType() == qMetaTypeId()) { + const auto content = _data->value(); int i = d->getPos(content); switch(role) { case Qt::DisplayRole: - case TypeRole: return QStringLiteral("Content%1").arg(i); + case TypeRole: + return QString::fromLatin1(content->type()); + case IsEmbededRole: + return false; + case ContentRole: { + auto text = content->encodedContent(); + if (data(index, TypeRole).toString() == "HtmlContent") { + const auto rx = QRegExp("(src)\\s*=\\s*(\"|')(cid:[^\"']+)\\2"); + int pos = 0; + while ((pos = rx.indexIn(text, pos)) != -1) { + const auto link = QUrl(rx.cap(3).toUtf8()); + pos += rx.matchedLength(); + const auto repl = d->mParser->getPart(link); + if (!repl) { + continue; + } + const auto content = repl->content(); + if(content.size() < 1) { + continue; + } + const auto mailMime = content.first()->mailMime(); + const auto mimetype = mailMime->mimetype().name(); + if (mimetype.startsWith("image/")) { + const auto data = content.first()->content(); + text.replace(rx.cap(0), QString("src=\"data:%1;base64,%2\"").arg(mimetype, QString::fromLatin1(data.toBase64()))); + } + } + } + return text; + } } } } -- cgit v1.2.3 From d2fa9b2f152e223ec31dc2812ebbfaf109549939 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sandro=20Knau=C3=9F?= Date: Wed, 19 Oct 2016 14:28:48 +0200 Subject: swtch encryption and sign creation --- framework/domain/messageparser_new.cpp | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) (limited to 'framework') diff --git a/framework/domain/messageparser_new.cpp b/framework/domain/messageparser_new.cpp index db4b36c5..da5ab2e7 100644 --- a/framework/domain/messageparser_new.cpp +++ b/framework/domain/messageparser_new.cpp @@ -142,8 +142,8 @@ Entry * NewModelPrivate::addPart(Entry *parent, Part *part) foreach(const auto &content, part->content()) { auto _entry = entry; - _entry = addSignatures(_entry, content->signatures().mid(part->signatures().size())); _entry = addEncryptions(_entry, content->encryptions().mid(part->encryptions().size())); + _entry = addSignatures(_entry, content->signatures().mid(part->signatures().size())); auto c = new Entry(); c->mData = getVar(content); _entry->addChild(c); @@ -151,8 +151,8 @@ Entry * NewModelPrivate::addPart(Entry *parent, Part *part) foreach(const auto &sp, part->subParts()) { auto _entry = entry; - _entry = addSignatures(_entry, sp->signatures().mid(part->signatures().size())); _entry = addEncryptions(_entry, sp->encryptions().mid(part->encryptions().size())); + _entry = addSignatures(_entry, sp->signatures().mid(part->signatures().size())); addPart(_entry, sp.get()); } return entry; @@ -169,15 +169,15 @@ void NewModelPrivate::createTree() auto _parent = parent; if (pPart != part->parent()) { auto _parent = mRoot; - _parent = addSignatures(_parent, part->parent()->signatures()); _parent = addEncryptions(_parent, part->parent()->encryptions()); + _parent = addSignatures(_parent, part->parent()->signatures()); signatures = part->parent()->signatures(); encryptions = part->parent()->encryptions(); parent = _parent; pPart = part->parent(); } - _parent = addSignatures(_parent, part->signatures().mid(signatures.size())); _parent = addEncryptions(_parent, part->encryptions().mid(encryptions.size())); + _parent = addSignatures(_parent, part->signatures().mid(signatures.size())); addPart(_parent, part.get()); } } @@ -340,6 +340,8 @@ QVariant NewModel::data(const QModelIndex &index, int role) const return QStringLiteral("Signature%1").arg(i); case TypeRole: return QStringLiteral("Signature"); + case SecurityLevelRole: + return QStringLiteral("RED"); } } else if (_data->userType() == qMetaTypeId()) { const auto encryption = _data->value(); @@ -349,6 +351,8 @@ QVariant NewModel::data(const QModelIndex &index, int role) const return QStringLiteral("Encryption%1").arg(i); case TypeRole: return QStringLiteral("Encryption"); + case SecurityLevelRole: + return QStringLiteral("GREEN"); } } else if (_data->userType() == qMetaTypeId()) { const auto part = _data->value(); @@ -358,8 +362,6 @@ QVariant NewModel::data(const QModelIndex &index, int role) const return QString::fromLatin1(part->type()); case IsEmbededRole: return index.parent().isValid(); - case SecurityLevelRole: - return QStringLiteral("GRAY"); case ContentsRole: return QVariant::fromValue(d->mContentMap.value(part).get()); } -- cgit v1.2.3 From 17a926c2697143937b0c1700b1d1e8081f727059 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sandro=20Knau=C3=9F?= Date: Wed, 19 Oct 2016 15:22:31 +0200 Subject: delete useless NewContentModel --- framework/domain/messageparser.h | 25 ------ framework/domain/messageparser_new.cpp | 153 +++++++++------------------------ framework/domain/modeltest.cpp | 2 +- 3 files changed, 44 insertions(+), 136 deletions(-) (limited to 'framework') diff --git a/framework/domain/messageparser.h b/framework/domain/messageparser.h index 55c884f9..559fa6f9 100644 --- a/framework/domain/messageparser.h +++ b/framework/domain/messageparser.h @@ -98,31 +98,6 @@ private: }; -class NewContentModel : public QAbstractItemModel { - Q_OBJECT -public: - NewContentModel (const PartPtr &part, const std::shared_ptr &parser); - -public: - enum Roles { - TypeRole = Qt::UserRole + 1, - ContentRole, - IsEmbededRole, - SecurityLevelRole - }; - - QHash roleNames() const Q_DECL_OVERRIDE; - QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; - QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; - QModelIndex parent(const QModelIndex &index) const Q_DECL_OVERRIDE; - int rowCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; - int columnCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; - -private: - const PartPtr &mPart; - const std::shared_ptr &mParser; -}; - class NewModel : public QAbstractItemModel { Q_OBJECT public: diff --git a/framework/domain/messageparser_new.cpp b/framework/domain/messageparser_new.cpp index da5ab2e7..57f8172f 100644 --- a/framework/domain/messageparser_new.cpp +++ b/framework/domain/messageparser_new.cpp @@ -29,6 +29,11 @@ Q_DECLARE_METATYPE(Encryption *) class Entry { public: + Entry() + : mParent(nullptr) + { + } + ~Entry() { foreach(auto child, mChildren) { @@ -36,7 +41,7 @@ public: } mChildren.clear(); } - + void addChild(Entry *entry) { mChildren.append(entry); @@ -68,6 +73,7 @@ class NewModelPrivate { public: NewModelPrivate(NewModel *q_ptr, const std::shared_ptr &parser); + ~NewModelPrivate(); void createTree(); Entry *addSignatures(Entry *parent, QVector signatures); @@ -88,10 +94,9 @@ public: NewModel *q; QVector mParts; - Entry *mRoot; + std::unique_ptr mRoot; std::shared_ptr mParser; - QMap> mContentMap; private: QMap, QSharedPointer> mSignatureMap; QMap, QSharedPointer> mEncryptionMap; @@ -101,16 +106,18 @@ private: NewModelPrivate::NewModelPrivate(NewModel *q_ptr, const std::shared_ptr &parser) : q(q_ptr) + , mRoot(std::unique_ptr(new Entry)) , mParser(parser) { mParts = mParser->collectContentParts(); - foreach(const auto &part, mParts) { - mContentMap.insert(part.get(), std::shared_ptr(new NewContentModel(part, mParser))); - } createTree(); } -Entry * NewModelPrivate::addSignatures(Entry *parent, QVector signatures) +NewModelPrivate::~NewModelPrivate() +{ +} + +Entry *NewModelPrivate::addSignatures(Entry *parent, QVector signatures) { auto ret = parent; foreach(const auto &sig, signatures) { @@ -160,15 +167,15 @@ Entry * NewModelPrivate::addPart(Entry *parent, Part *part) void NewModelPrivate::createTree() { - mRoot = new Entry(); - auto parent = mRoot; + auto root = mRoot.get(); + auto parent = root; Part *pPart = nullptr; QVector signatures; QVector encryptions; foreach(const auto part, mParts) { auto _parent = parent; if (pPart != part->parent()) { - auto _parent = mRoot; + auto _parent = root; _parent = addEncryptions(_parent, part->parent()->encryptions()); _parent = addSignatures(_parent, part->parent()->signatures()); signatures = part->parent()->signatures(); @@ -202,7 +209,6 @@ QSharedPointer NewModelPrivate::getVar(const std::shared_ptr NewModelPrivate::getVar(const std::shared_ptr &part) { return getVar(part.get()); @@ -259,7 +265,6 @@ int NewModelPrivate::getPos(Encryption *encryption) return i; } - int NewModelPrivate::getPos(Part *part) { int i = 0; @@ -297,20 +302,18 @@ QHash NewModel::roleNames() const { QHash roles; roles[TypeRole] = "type"; - roles[ContentsRole] = "contents"; roles[ContentRole] = "content"; roles[IsEmbededRole] = "embeded"; roles[SecurityLevelRole] = "securityLevel"; return roles; } - QModelIndex NewModel::index(int row, int column, const QModelIndex &parent) const { if (row < 0 || column != 0) { return QModelIndex(); } - Entry *entry = d->mRoot; + Entry *entry = d->mRoot.get(); if (parent.isValid()) { entry = static_cast(parent.internalPointer()); } @@ -324,14 +327,27 @@ QModelIndex NewModel::index(int row, int column, const QModelIndex &parent) cons QVariant NewModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) { - if (role == Qt::DisplayRole) { + switch (role) { + case Qt::DisplayRole: return QString("root"); + case IsEmbededRole: + return false; } return QVariant(); } + if (index.internalPointer()) { const auto entry = static_cast(index.internalPointer()); const auto _data = entry->mData; + if (entry == d->mRoot.get()|| !_data) { + switch (role) { + case Qt::DisplayRole: + return QString("root"); + case IsEmbededRole: + return false; + } + return QVariant(); + } if (_data->userType() == qMetaTypeId()) { const auto signature = _data->value(); int i = d->getPos(signature); @@ -342,6 +358,8 @@ QVariant NewModel::data(const QModelIndex &index, int role) const return QStringLiteral("Signature"); case SecurityLevelRole: return QStringLiteral("RED"); + case IsEmbededRole: + return data(index.parent(), IsEmbededRole); } } else if (_data->userType() == qMetaTypeId()) { const auto encryption = _data->value(); @@ -353,17 +371,17 @@ QVariant NewModel::data(const QModelIndex &index, int role) const return QStringLiteral("Encryption"); case SecurityLevelRole: return QStringLiteral("GREEN"); + case IsEmbededRole: + return data(index.parent(), IsEmbededRole); } } else if (_data->userType() == qMetaTypeId()) { const auto part = _data->value(); switch (role) { - case Qt::DisplayRole: - case TypeRole: - return QString::fromLatin1(part->type()); - case IsEmbededRole: - return index.parent().isValid(); - case ContentsRole: - return QVariant::fromValue(d->mContentMap.value(part).get()); + case Qt::DisplayRole: + case TypeRole: + return QString::fromLatin1(part->type()); + case IsEmbededRole: + return data(index.parent(), IsEmbededRole); } } else if (_data->userType() == qMetaTypeId()) { const auto content = _data->value(); @@ -374,7 +392,7 @@ QVariant NewModel::data(const QModelIndex &index, int role) const case TypeRole: return QString::fromLatin1(content->type()); case IsEmbededRole: - return false; + return data(index.parent(), IsEmbededRole); case ContentRole: { auto text = content->encodedContent(); if (data(index, TypeRole).toString() == "HtmlContent") { @@ -413,7 +431,7 @@ QModelIndex NewModel::parent(const QModelIndex &index) const return QModelIndex(); } const auto entry = static_cast(index.internalPointer()); - if (entry->mParent) { + if (entry->mParent && entry->mParent != d->mRoot.get()) { return createIndex(entry->pos(), 0, entry->mParent); } return QModelIndex(); @@ -437,88 +455,3 @@ int NewModel::columnCount(const QModelIndex &parent) const { return 1; } - -NewContentModel::NewContentModel(const Part::Ptr &part, const std::shared_ptr &parser) - : mPart(part) - , mParser(parser) -{ -} - -QHash NewContentModel::roleNames() const -{ - QHash roles; - roles[TypeRole] = "type"; - roles[ContentRole] = "content"; - roles[IsEmbededRole] = "embeded"; - roles[SecurityLevelRole] = "securityLevel"; - return roles; -} - -QModelIndex NewContentModel::index(int row, int column, const QModelIndex &parent) const -{ - if (!parent.isValid()) { - if (row < mPart->content().size()) { - auto part = mPart->content().at(row); - return createIndex(row, column, part.get()); - } - } - return QModelIndex(); -} - -QVariant NewContentModel::data(const QModelIndex &index, int role) const -{ - auto content = static_cast(index.internalPointer()); - switch (role) { - case TypeRole: - return QString::fromLatin1(content->type()); - case IsEmbededRole: - return false; - case ContentRole: { - auto text = content->encodedContent(); - if (data(index, TypeRole).toString() == "HtmlContent") { - const auto rx = QRegExp("(src)\\s*=\\s*(\"|')(cid:[^\"']+)\\2"); - int pos = 0; - while ((pos = rx.indexIn(text, pos)) != -1) { - const auto link = QUrl(rx.cap(3).toUtf8()); - pos += rx.matchedLength(); - const auto repl = mParser->getPart(link); - if (!repl) { - continue; - } - const auto content = repl->content(); - if(content.size() < 1) { - continue; - } - const auto mailMime = content.first()->mailMime(); - const auto mimetype = mailMime->mimetype().name(); - if (mimetype.startsWith("image/")) { - const auto data = content.first()->content(); - text.replace(rx.cap(0), QString("src=\"data:%1;base64,%2\"").arg(mimetype, QString::fromLatin1(data.toBase64()))); - } - } - } - return text; - } - case SecurityLevelRole: - return content->encryptions().size() > mPart->encryptions().size() ? "red": "black"; //test for gpg inline - } - return QVariant(); -} - -QModelIndex NewContentModel::parent(const QModelIndex &index) const -{ - return QModelIndex(); -} - -int NewContentModel::rowCount(const QModelIndex &parent) const -{ - if (!parent.isValid()) { - return mPart->content().size(); - } - return 0; -} - -int NewContentModel::columnCount(const QModelIndex &parent) const -{ - return 1; -} diff --git a/framework/domain/modeltest.cpp b/framework/domain/modeltest.cpp index 295ff6d0..9e1d92fa 100644 --- a/framework/domain/modeltest.cpp +++ b/framework/domain/modeltest.cpp @@ -339,7 +339,7 @@ void ModelTest::checkChildren ( const QModelIndex &parent, int currentDepth ) // rowCount() and columnCount() said that it existed... QVERIFY( index.isValid() ); - qWarning() << "\tchild("<< r <<", " << c << ", " << index.column() << "):" << model->data(index).toString(); + qWarning() << "\tchild("<< r <<", " << c << ", " << index.column() << "):" << model->data(index).toString() << index.internalPointer(); // index() should always return the same index when called twice in a row QModelIndex modifiedIndex = model->index ( r, c, parent ); -- cgit v1.2.3 From a93a649f34ffa794884019e05c57869bd3bd4672 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sandro=20Knau=C3=9F?= Date: Wed, 19 Oct 2016 15:40:25 +0200 Subject: Move addSignatures/addEncryptions/addPart to Entry class --- framework/domain/messageparser_new.cpp | 178 ++++++++++++++++----------------- 1 file changed, 89 insertions(+), 89 deletions(-) (limited to 'framework') diff --git a/framework/domain/messageparser_new.cpp b/framework/domain/messageparser_new.cpp index 57f8172f..b930f33d 100644 --- a/framework/domain/messageparser_new.cpp +++ b/framework/domain/messageparser_new.cpp @@ -26,11 +26,46 @@ Q_DECLARE_METATYPE(Content *) Q_DECLARE_METATYPE(Signature *) Q_DECLARE_METATYPE(Encryption *) +class Entry; + +class NewModelPrivate +{ +public: + NewModelPrivate(NewModel *q_ptr, const std::shared_ptr &parser); + ~NewModelPrivate(); + + void createTree(); + + QSharedPointer getVar(const std::shared_ptr &sig); + QSharedPointer getVar(const std::shared_ptr &enc); + QSharedPointer getVar(const std::shared_ptr &part); + QSharedPointer getVar(Part *part); + QSharedPointer getVar(const std::shared_ptr &content); + QSharedPointer getVar(Content *content); + + int getPos(Signature *sig); + int getPos(Encryption *enc); + int getPos(Part *part); + int getPos(Content *content); + + NewModel *q; + QVector mParts; + std::unique_ptr mRoot; + + std::shared_ptr mParser; +private: + QMap, QSharedPointer> mSignatureMap; + QMap, QSharedPointer> mEncryptionMap; + QMap> mPartMap; + QMap> mCMap; +}; + class Entry { public: - Entry() + Entry(NewModelPrivate *model) : mParent(nullptr) + , mNewModelPrivate(model) { } @@ -48,6 +83,52 @@ public: entry->mParent = this; } + Entry *addSignatures(QVector signatures) + { + auto ret = this; + foreach(const auto &sig, signatures) { + auto entry = new Entry(mNewModelPrivate); + entry->mData = mNewModelPrivate->getVar(sig); + ret->addChild(entry); + ret = entry; + } + return ret; + } + + Entry *addEncryptions(QVector encryptions) + { + auto ret = this; + foreach(const auto &enc, encryptions) { + auto entry = new Entry(mNewModelPrivate); + entry->mData = mNewModelPrivate->getVar(enc); + ret->addChild(entry); + ret = entry; + } + return ret; + } + + Entry *addPart(Part *part) + { + auto entry = new Entry(mNewModelPrivate); + entry->mData = mNewModelPrivate->getVar(part); + addChild(entry); + foreach(const auto &content, part->content()) { + auto _entry = entry; + _entry = _entry->addEncryptions(content->encryptions().mid(part->encryptions().size())); + _entry = _entry->addSignatures(content->signatures().mid(part->signatures().size())); + auto c = new Entry(mNewModelPrivate); + c->mData = mNewModelPrivate->getVar(content); + _entry->addChild(c); + } + foreach(const auto &sp, part->subParts()) { + auto _entry = entry; + _entry = _entry->addEncryptions(sp->encryptions().mid(part->encryptions().size())); + _entry = _entry->addSignatures(sp->signatures().mid(part->signatures().size())); + _entry->addPart(sp.get()); + } + return entry; + } + int pos() { if(!mParent) { @@ -67,46 +148,13 @@ public: Entry *mParent; QVector mChildren; + NewModelPrivate *mNewModelPrivate; }; -class NewModelPrivate -{ -public: - NewModelPrivate(NewModel *q_ptr, const std::shared_ptr &parser); - ~NewModelPrivate(); - - void createTree(); - Entry *addSignatures(Entry *parent, QVector signatures); - Entry *addEncryptions(Entry *parent, QVector encryptions); - Entry *addPart(Entry *parent, Part *part); - - QSharedPointer getVar(const std::shared_ptr &sig); - QSharedPointer getVar(const std::shared_ptr &enc); - QSharedPointer getVar(const std::shared_ptr &part); - QSharedPointer getVar(Part *part); - QSharedPointer getVar(const std::shared_ptr &content); - QSharedPointer getVar(Content *content); - - int getPos(Signature *sig); - int getPos(Encryption *enc); - int getPos(Part *part); - int getPos(Content *content); - - NewModel *q; - QVector mParts; - std::unique_ptr mRoot; - - std::shared_ptr mParser; -private: - QMap, QSharedPointer> mSignatureMap; - QMap, QSharedPointer> mEncryptionMap; - QMap> mPartMap; - QMap> mCMap; -}; NewModelPrivate::NewModelPrivate(NewModel *q_ptr, const std::shared_ptr &parser) : q(q_ptr) - , mRoot(std::unique_ptr(new Entry)) + , mRoot(std::unique_ptr(new Entry(this))) , mParser(parser) { mParts = mParser->collectContentParts(); @@ -117,54 +165,6 @@ NewModelPrivate::~NewModelPrivate() { } -Entry *NewModelPrivate::addSignatures(Entry *parent, QVector signatures) -{ - auto ret = parent; - foreach(const auto &sig, signatures) { - auto entry = new Entry(); - entry->mData = getVar(sig); - ret = entry; - parent->addChild(entry); - } - return ret; -} - -Entry * NewModelPrivate::addEncryptions(Entry *parent, QVector encryptions) -{ - auto ret = parent; - foreach(const auto &enc, encryptions) { - auto entry = new Entry(); - entry->mData = getVar(enc); - parent->addChild(entry); - ret = entry; - } - return ret; -} - -Entry * NewModelPrivate::addPart(Entry *parent, Part *part) -{ - auto entry = new Entry(); - entry->mData = getVar(part); - parent->addChild(entry); - - foreach(const auto &content, part->content()) { - auto _entry = entry; - _entry = addEncryptions(_entry, content->encryptions().mid(part->encryptions().size())); - _entry = addSignatures(_entry, content->signatures().mid(part->signatures().size())); - auto c = new Entry(); - c->mData = getVar(content); - _entry->addChild(c); - } - - foreach(const auto &sp, part->subParts()) { - auto _entry = entry; - _entry = addEncryptions(_entry, sp->encryptions().mid(part->encryptions().size())); - _entry = addSignatures(_entry, sp->signatures().mid(part->signatures().size())); - addPart(_entry, sp.get()); - } - return entry; -} - void NewModelPrivate::createTree() { auto root = mRoot.get(); @@ -176,16 +176,16 @@ void NewModelPrivate::createTree() auto _parent = parent; if (pPart != part->parent()) { auto _parent = root; - _parent = addEncryptions(_parent, part->parent()->encryptions()); - _parent = addSignatures(_parent, part->parent()->signatures()); + _parent = _parent->addEncryptions(part->parent()->encryptions()); + _parent = _parent->addSignatures(part->parent()->signatures()); signatures = part->parent()->signatures(); encryptions = part->parent()->encryptions(); parent = _parent; pPart = part->parent(); } - _parent = addEncryptions(_parent, part->encryptions().mid(encryptions.size())); - _parent = addSignatures(_parent, part->signatures().mid(signatures.size())); - addPart(_parent, part.get()); + _parent = _parent->addEncryptions(part->encryptions().mid(encryptions.size())); + _parent = _parent->addSignatures(part->signatures().mid(signatures.size())); + _parent->addPart(part.get()); } } -- cgit v1.2.3