summaryrefslogtreecommitdiffstats
path: root/framework/src
diff options
context:
space:
mode:
authorChristian Mollekopf <chrigi_1@fastmail.fm>2017-04-05 15:04:00 +0200
committerChristian Mollekopf <chrigi_1@fastmail.fm>2017-04-05 15:04:00 +0200
commit4b1798f0cdf87361869e7cf2b341acacd056c410 (patch)
tree3ff780641acdcb20b81f9b41533afd50a2525d38 /framework/src
parent71721aa4f3e85bea1a2fe504e86d99f80a3106a9 (diff)
downloadkube-4b1798f0cdf87361869e7cf2b341acacd056c410.tar.gz
kube-4b1798f0cdf87361869e7cf2b341acacd056c410.zip
Moved cpp code into src directory
Diffstat (limited to 'framework/src')
-rw-r--r--framework/src/CMakeLists.txt66
-rw-r--r--framework/src/accounts/accountfactory.cpp71
-rw-r--r--framework/src/accounts/accountfactory.h53
-rw-r--r--framework/src/accounts/accountsmodel.cpp101
-rw-r--r--framework/src/accounts/accountsmodel.h67
-rw-r--r--framework/src/accounts/gmailcontroller.cpp89
-rw-r--r--framework/src/accounts/gmailcontroller.h55
-rw-r--r--framework/src/actions/action.cpp127
-rw-r--r--framework/src/actions/action.h71
-rw-r--r--framework/src/actions/actionbroker.cpp101
-rw-r--r--framework/src/actions/actionbroker.h49
-rw-r--r--framework/src/actions/actionhandler.cpp151
-rw-r--r--framework/src/actions/actionhandler.h112
-rw-r--r--framework/src/actions/actionresult.cpp20
-rw-r--r--framework/src/actions/actionresult.h80
-rw-r--r--framework/src/actions/context.cpp91
-rw-r--r--framework/src/actions/context.h67
-rw-r--r--framework/src/actions/tests/CMakeLists.txt6
-rw-r--r--framework/src/actions/tests/actiontest.cpp102
-rw-r--r--framework/src/domain/actions/sinkactions.cpp56
-rw-r--r--framework/src/domain/actions/tests/CMakeLists.txt6
-rw-r--r--framework/src/domain/actions/tests/sinkactiontest.cpp58
-rw-r--r--framework/src/domain/attachmentmodel.cpp145
-rw-r--r--framework/src/domain/completer.cpp26
-rw-r--r--framework/src/domain/completer.h40
-rw-r--r--framework/src/domain/composercontroller.cpp307
-rw-r--r--framework/src/domain/composercontroller.h93
-rw-r--r--framework/src/domain/contactcontroller.cpp68
-rw-r--r--framework/src/domain/contactcontroller.h52
-rw-r--r--framework/src/domain/controller.cpp59
-rw-r--r--framework/src/domain/controller.h90
-rw-r--r--framework/src/domain/foldercontroller.cpp62
-rw-r--r--framework/src/domain/foldercontroller.h36
-rw-r--r--framework/src/domain/folderlistmodel.cpp145
-rw-r--r--framework/src/domain/folderlistmodel.h71
-rw-r--r--framework/src/domain/identitiesmodel.cpp99
-rw-r--r--framework/src/domain/identitiesmodel.h60
-rw-r--r--framework/src/domain/mailcontroller.cpp125
-rw-r--r--framework/src/domain/mailcontroller.h44
-rw-r--r--framework/src/domain/maillistmodel.cpp265
-rw-r--r--framework/src/domain/maillistmodel.h94
-rw-r--r--framework/src/domain/mailtemplates.cpp801
-rw-r--r--framework/src/domain/mailtemplates.h28
-rw-r--r--framework/src/domain/messageparser.cpp114
-rw-r--r--framework/src/domain/messageparser.h157
-rw-r--r--framework/src/domain/messageparser_new.cpp486
-rw-r--r--framework/src/domain/messageparser_old.cpp151
-rw-r--r--framework/src/domain/mimetreeparser/CMakeLists.txt15
-rw-r--r--framework/src/domain/mimetreeparser/interface.cpp1179
-rw-r--r--framework/src/domain/mimetreeparser/interface.h378
-rw-r--r--framework/src/domain/mimetreeparser/interface_p.h56
-rw-r--r--framework/src/domain/mimetreeparser/objecttreesource.cpp152
-rw-r--r--framework/src/domain/mimetreeparser/objecttreesource.h57
-rw-r--r--framework/src/domain/mimetreeparser/stringhtmlwriter.cpp150
-rw-r--r--framework/src/domain/mimetreeparser/stringhtmlwriter.h71
-rw-r--r--framework/src/domain/mimetreeparser/tests/CMakeLists.txt23
-rw-r--r--framework/src/domain/mimetreeparser/tests/data/alternative.mbox28
-rw-r--r--framework/src/domain/mimetreeparser/tests/data/cid-links.mbox1384
-rw-r--r--framework/src/domain/mimetreeparser/tests/data/html.mbox15
-rw-r--r--framework/src/domain/mimetreeparser/tests/data/openpgp-encrypted-attachment-and-non-encrypted-attachment.mbox115
-rw-r--r--framework/src/domain/mimetreeparser/tests/data/openpgp-inline-charset-encrypted.mbox40
-rw-r--r--framework/src/domain/mimetreeparser/tests/data/openpgp-inline-encrypted+nonenc.mbox31
-rw-r--r--framework/src/domain/mimetreeparser/tests/data/plaintext.mbox13
-rw-r--r--framework/src/domain/mimetreeparser/tests/data/smime-encrypted.mbox22
-rw-r--r--framework/src/domain/mimetreeparser/tests/gnupg_home/CMakeLists.txt10
-rw-r--r--framework/src/domain/mimetreeparser/tests/gnupg_home/dirmngr-cache.d/DIR.txt3
-rw-r--r--framework/src/domain/mimetreeparser/tests/gnupg_home/dirmngr-cache.d/crl-4E31CEB57DDD4A7B9991AB05507B1ED4293FF952.dbbin0 -> 2130 bytes
-rw-r--r--framework/src/domain/mimetreeparser/tests/gnupg_home/dirmngr-cache.d/crl-7F2A402CBB016A9146D613568C89D3596A4111AA.dbbin0 -> 2048 bytes
-rw-r--r--framework/src/domain/mimetreeparser/tests/gnupg_home/dirmngr.conf8
-rw-r--r--framework/src/domain/mimetreeparser/tests/gnupg_home/gpg-agent.conf.in10
-rw-r--r--framework/src/domain/mimetreeparser/tests/gnupg_home/gpg.conf244
-rw-r--r--framework/src/domain/mimetreeparser/tests/gnupg_home/gpgsm.conf.in10
-rwxr-xr-xframework/src/domain/mimetreeparser/tests/gnupg_home/pinentry-fake.sh9
-rw-r--r--framework/src/domain/mimetreeparser/tests/gnupg_home/private-keys-v1.d/1AA8BA52430E51AE249AF0DA97D59F869E4101A8.keybin0 -> 528 bytes
-rw-r--r--framework/src/domain/mimetreeparser/tests/gnupg_home/pubring.gpgbin0 -> 6757 bytes
-rw-r--r--framework/src/domain/mimetreeparser/tests/gnupg_home/pubring.kbxbin0 -> 2017 bytes
-rw-r--r--framework/src/domain/mimetreeparser/tests/gnupg_home/scdaemon.conf8
-rw-r--r--framework/src/domain/mimetreeparser/tests/gnupg_home/secring.gpgbin0 -> 5163 bytes
-rw-r--r--framework/src/domain/mimetreeparser/tests/gnupg_home/trustdb.gpgbin0 -> 1440 bytes
-rw-r--r--framework/src/domain/mimetreeparser/tests/gnupg_home/trustlist.txt11
-rw-r--r--framework/src/domain/mimetreeparser/tests/gpgerrortest.cpp214
-rw-r--r--framework/src/domain/mimetreeparser/tests/interfacetest.cpp310
-rw-r--r--framework/src/domain/mimetreeparser/tests/kdepim_add_gpg_crypto_test.cmake61
-rw-r--r--framework/src/domain/mimetreeparser/tests/kdepim_generate_crypto_test_wrapper.cmake45
-rw-r--r--framework/src/domain/mimetreeparser/thoughts.txt148
-rw-r--r--framework/src/domain/modeltest.cpp588
-rw-r--r--framework/src/domain/modeltest.h83
-rw-r--r--framework/src/domain/mouseproxy.cpp39
-rw-r--r--framework/src/domain/mouseproxy.h39
-rw-r--r--framework/src/domain/objecttreesource.cpp152
-rw-r--r--framework/src/domain/objecttreesource.h57
-rw-r--r--framework/src/domain/outboxcontroller.cpp47
-rw-r--r--framework/src/domain/outboxcontroller.h39
-rw-r--r--framework/src/domain/outboxmodel.cpp141
-rw-r--r--framework/src/domain/outboxmodel.h77
-rw-r--r--framework/src/domain/peoplemodel.cpp127
-rw-r--r--framework/src/domain/peoplemodel.h69
-rw-r--r--framework/src/domain/recepientautocompletionmodel.cpp134
-rw-r--r--framework/src/domain/recepientautocompletionmodel.h56
-rw-r--r--framework/src/domain/retriever.cpp55
-rw-r--r--framework/src/domain/retriever.h55
-rw-r--r--framework/src/domain/selector.cpp31
-rw-r--r--framework/src/domain/selector.h56
-rw-r--r--framework/src/domain/settings/CMakeLists.txt6
-rw-r--r--framework/src/domain/settings/accountsettings.cpp391
-rw-r--r--framework/src/domain/settings/accountsettings.h123
-rw-r--r--framework/src/domain/stringhtmlwriter.cpp150
-rw-r--r--framework/src/domain/stringhtmlwriter.h71
-rw-r--r--framework/src/frameworkplugin.cpp72
-rw-r--r--framework/src/frameworkplugin.h33
-rw-r--r--framework/src/notifications/notificationhandler.cpp80
-rw-r--r--framework/src/notifications/notificationhandler.h80
-rw-r--r--framework/src/settings/settings.cpp174
-rw-r--r--framework/src/settings/settings.h92
114 files changed, 13154 insertions, 0 deletions
diff --git a/framework/src/CMakeLists.txt b/framework/src/CMakeLists.txt
new file mode 100644
index 00000000..371be4a6
--- /dev/null
+++ b/framework/src/CMakeLists.txt
@@ -0,0 +1,66 @@
1
2find_package(Qt5 COMPONENTS REQUIRED Core Qml)
3find_package(KF5MimeTreeParser "5.1.46" CONFIG REQUIRED)
4find_package(KF5Mime "4.87.0" CONFIG REQUIRED)
5find_package(Sink CONFIG REQUIRED)
6find_package(KAsync CONFIG REQUIRED)
7find_package(QGpgme CONFIG REQUIRED)
8find_package(KF5Codecs CONFIG REQUIRED)
9find_package(KF5Package CONFIG REQUIRED)
10
11add_definitions("-Wall -std=c++0x -g")
12
13include_directories(.)
14#include_directories(SYSTEM ${KDE_INSTALL_FULL_INCLUDEDIR}/KF5/)
15#include_directories(SYSTEM ${KDE_INSTALL_FULL_INCLUDEDIR}/KF5/KMime)
16
17enable_testing()
18
19set(SRCS
20 frameworkplugin.cpp
21 actions/action.cpp
22 actions/actionhandler.cpp
23 actions/actionbroker.cpp
24 actions/actionresult.cpp
25 actions/context.cpp
26 settings/settings.cpp
27 domain/attachmentmodel.cpp
28 domain/maillistmodel.cpp
29 domain/folderlistmodel.cpp
30 domain/actions/sinkactions.cpp
31 domain/objecttreesource.cpp
32 domain/stringhtmlwriter.cpp
33 domain/composercontroller.cpp
34 domain/messageparser.cpp
35 domain/messageparser_new.cpp
36 domain/messageparser_old.cpp
37 domain/mailtemplates.cpp
38 domain/modeltest.cpp
39 domain/retriever.cpp
40 domain/outboxmodel.cpp
41 domain/identitiesmodel.cpp
42 domain/recepientautocompletionmodel.cpp
43 domain/settings/accountsettings.cpp
44 domain/selector.cpp
45 domain/completer.cpp
46 domain/controller.cpp
47 domain/outboxcontroller.cpp
48 domain/mailcontroller.cpp
49 domain/foldercontroller.cpp
50 domain/mouseproxy.cpp
51 domain/contactcontroller.cpp
52 domain/peoplemodel.cpp
53 accounts/accountfactory.cpp
54 accounts/accountsmodel.cpp
55 notifications/notificationhandler.cpp
56)
57
58add_library(frameworkplugin SHARED ${SRCS})
59qt5_use_modules(frameworkplugin Core Quick Qml WebKitWidgets Test)
60target_link_libraries(frameworkplugin sink mimetreeparser KF5::MimeTreeParser KF5::Codecs KF5::Package KAsync)
61install(TARGETS frameworkplugin DESTINATION ${FRAMEWORK_INSTALL_DIR})
62
63add_subdirectory(domain/actions/tests)
64add_subdirectory(domain/mimetreeparser)
65
66feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES)
diff --git a/framework/src/accounts/accountfactory.cpp b/framework/src/accounts/accountfactory.cpp
new file mode 100644
index 00000000..9dbb402b
--- /dev/null
+++ b/framework/src/accounts/accountfactory.cpp
@@ -0,0 +1,71 @@
1/*
2 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsystems.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19#include "accountfactory.h"
20
21#include <QDebug>
22
23#include <KPackage/PackageLoader>
24#include <KPackage/Package>
25#include <KPluginMetaData>
26
27#include "settings/settings.h"
28#include <sink/store.h>
29
30AccountFactory::AccountFactory(QObject *parent)
31 : QObject(parent)
32{
33}
34
35QString AccountFactory::name() const
36{
37 if (mName.isEmpty()) {
38 return tr("Account");
39 }
40 return mName;
41}
42
43void AccountFactory::setAccountId(const QString &accountId)
44{
45 mAccountId = accountId;
46 Sink::Store::fetchOne<Sink::ApplicationDomain::SinkAccount>(Sink::Query().filter(accountId.toUtf8()))
47 .then([this](const Sink::ApplicationDomain::SinkAccount &account) {
48 mAccountType = account.getProperty("type").toByteArray();
49 loadPackage();
50 }).exec();
51}
52
53void AccountFactory::setAccountType(const QString &type)
54{
55 mAccountType = type.toLatin1();
56 loadPackage();
57}
58
59void AccountFactory::loadPackage()
60{
61 auto package = KPackage::PackageLoader::self()->loadPackage("KPackage/GenericQML", "org.kube.accounts." + mAccountType);
62 if (!package.isValid()) {
63 qWarning() << "Failed to load account package: " << "org.kube.accounts." + mAccountType;
64 return;
65 }
66 Q_ASSERT(package.isValid());
67 mUiPath = package.filePath("mainscript");
68 mName = package.metadata().name();
69 mIcon = package.metadata().iconName();
70 emit accountLoaded();
71}
diff --git a/framework/src/accounts/accountfactory.h b/framework/src/accounts/accountfactory.h
new file mode 100644
index 00000000..b57854e5
--- /dev/null
+++ b/framework/src/accounts/accountfactory.h
@@ -0,0 +1,53 @@
1/*
2 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsystems.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19
20#pragma once
21
22#include <QObject>
23#include <QVariant>
24
25/**
26 * A factory to instantiate account-plugins.
27 */
28class AccountFactory : public QObject
29{
30 Q_OBJECT
31 Q_PROPERTY(QString accountId MEMBER mAccountId WRITE setAccountId);
32 Q_PROPERTY(QString accountType MEMBER mAccountType WRITE setAccountType);
33 Q_PROPERTY(QString name MEMBER mName READ name NOTIFY accountLoaded);
34 Q_PROPERTY(QString icon MEMBER mIcon NOTIFY accountLoaded);
35 Q_PROPERTY(QString uiPath MEMBER mUiPath NOTIFY accountLoaded);
36public:
37 explicit AccountFactory(QObject *parent = Q_NULLPTR);
38
39 void setAccountId(const QString &);
40 void setAccountType(const QString &);
41 QString name() const;
42
43signals:
44 void accountLoaded();
45
46private:
47 void loadPackage();
48 QString mAccountId;
49 QString mName;
50 QString mIcon;
51 QString mUiPath;
52 QByteArray mAccountType;
53};
diff --git a/framework/src/accounts/accountsmodel.cpp b/framework/src/accounts/accountsmodel.cpp
new file mode 100644
index 00000000..f98e8ca3
--- /dev/null
+++ b/framework/src/accounts/accountsmodel.cpp
@@ -0,0 +1,101 @@
1/*
2 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsys.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19#include "accountsmodel.h"
20#include <sink/store.h>
21
22using namespace Sink;
23using namespace Sink::ApplicationDomain;
24
25AccountsModel::AccountsModel(QObject *parent) : QIdentityProxyModel()
26{
27 Sink::Query query;
28 query.setFlags(Query::LiveQuery);
29 query.request<SinkAccount::Name>();
30 query.request<SinkAccount::Icon>();
31 query.request<SinkAccount::Status>();
32 runQuery(query);
33}
34
35AccountsModel::~AccountsModel()
36{
37
38}
39
40QHash< int, QByteArray > AccountsModel::roleNames() const
41{
42 QHash<int, QByteArray> roles;
43
44 roles[Name] = "name";
45 roles[Icon] = "icon";
46 roles[AccountId] = "accountId";
47 roles[Status] = "status";
48
49 return roles;
50}
51
52QVariant AccountsModel::data(const QModelIndex &idx, int role) const
53{
54 auto srcIdx = mapToSource(idx);
55 auto account = srcIdx.data(Sink::Store::DomainObjectRole).value<SinkAccount::Ptr>();
56 switch (role) {
57 case Name:
58 return account->getName();
59 case Icon:
60 return account->getIcon();
61 case AccountId:
62 return account->identifier();
63 case Status:
64 switch (account->getStatus()) {
65 case Sink::ApplicationDomain::ErrorStatus:
66 return ErrorStatus;
67 case Sink::ApplicationDomain::BusyStatus:
68 return BusyStatus;
69 case Sink::ApplicationDomain::ConnectedStatus:
70 return ConnectedStatus;
71 case Sink::ApplicationDomain::OfflineStatus:
72 return OfflineStatus;
73 }
74 return NoStatus;
75 }
76 return QIdentityProxyModel::data(idx, role);
77}
78
79void AccountsModel::runQuery(const Sink::Query &query)
80{
81 mModel = Sink::Store::loadModel<SinkAccount>(query);
82 setSourceModel(mModel.data());
83}
84
85void AccountsModel::setAccountId(const QByteArray &accountId)
86{
87 qWarning() << "Setting account id" << accountId;
88 //Get all folders of an account
89 Sink::Query query;
90 query.filter(accountId);
91 query.setFlags(Query::LiveQuery);
92 query.request<SinkAccount::Name>();
93 query.request<SinkAccount::Icon>();
94 query.request<SinkAccount::Status>();
95 runQuery(query);
96}
97
98QByteArray AccountsModel::accountId() const
99{
100 return {};
101}
diff --git a/framework/src/accounts/accountsmodel.h b/framework/src/accounts/accountsmodel.h
new file mode 100644
index 00000000..2c82a8b9
--- /dev/null
+++ b/framework/src/accounts/accountsmodel.h
@@ -0,0 +1,67 @@
1/*
2 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsys.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19
20#pragma once
21
22#include <QObject>
23#include <QIdentityProxyModel>
24#include <QSharedPointer>
25#include <QStringList>
26
27namespace Sink {
28 class Query;
29}
30
31class AccountsModel : public QIdentityProxyModel
32{
33 Q_OBJECT
34
35 Q_PROPERTY (QByteArray accountId READ accountId WRITE setAccountId)
36public:
37 enum Status {
38 OfflineStatus,
39 ConnectedStatus,
40 BusyStatus,
41 ErrorStatus,
42 NoStatus
43 };
44 Q_ENUMS(Status)
45
46 AccountsModel(QObject *parent = Q_NULLPTR);
47 ~AccountsModel();
48
49 QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
50
51 enum Roles {
52 Name = Qt::UserRole + 1,
53 Icon,
54 AccountId,
55 Status
56 };
57 Q_ENUMS(Roles)
58
59 QHash<int, QByteArray> roleNames() const;
60
61 void setAccountId(const QByteArray &id);
62 QByteArray accountId() const;
63
64private:
65 void runQuery(const Sink::Query &query);
66 QSharedPointer<QAbstractItemModel> mModel;
67};
diff --git a/framework/src/accounts/gmailcontroller.cpp b/framework/src/accounts/gmailcontroller.cpp
new file mode 100644
index 00000000..fda24b26
--- /dev/null
+++ b/framework/src/accounts/gmailcontroller.cpp
@@ -0,0 +1,89 @@
1/*
2 * Copyright (C) 2017 Michael Bohlender, <michael.bohlender@kdemail.net>
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License along
15 * with this program; if not, write to the Free Software Foundation, Inc.,
16 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 */
18
19#include "gmailcontroller.h"
20
21#include <sink/store.h>
22
23using namespace Sink;
24using namespace Sink::ApplicationDomain;
25
26GmailController::GmailController() : Kube::Controller(),
27action_create{new Kube::ControllerAction{this, &GmailController::create}},
28action_modify{new Kube::ControllerAction{this, &GmailController::modify}},
29action_remove{new Kube::ControllerAction{this, &GmailController::remove}}
30{
31
32}
33
34void GmailController::create() {
35
36 //account
37 auto account = ApplicationDomainType::createEntity<SinkAccount>();
38 account.setProperty("type", "imap");
39 account.setProperty("name", getName());
40 Store::create(account).exec().waitForFinished();
41
42 //imap
43 auto resource = ApplicationDomainType::createEntity<SinkResource>();
44 resource.setResourceType("sink.imap");
45 resource.setAccount(account);
46 resource.setProperty("server","imaps://imap.gmail.com:993");
47 resource.setProperty("username", getEmailAddress());
48 resource.setProperty("password", getPassword());
49 Store::create(resource).exec().waitForFinished();
50
51 //smtp
52 resource = ApplicationDomainType::createEntity<SinkResource>();
53 resource.setResourceType("sink.mailtransport");
54 resource.setAccount(account);
55 resource.setProperty("server", "smtps://smtp.gmail.com:465");
56 resource.setProperty("username", getEmailAddress());
57 resource.setProperty("password", getPassword());
58 Store::create(resource).exec().waitForFinished();
59
60 //identity
61 auto identity = ApplicationDomainType::createEntity<Identity>();
62 m_identityId = identity.identifier();
63 identity.setAccount(account);
64 identity.setName(getIdentityName());
65 identity.setAddress(getEmailAddress());
66 Store::create(identity).exec();
67}
68
69void GmailController::modify() {
70 //TODO
71}
72
73void GmailController::remove() {
74 SinkAccount account(m_accountId);
75 Store::remove(account).exec();
76}
77
78void GmailController::load(const QByteArray &id) {
79
80 m_accountId = id;
81
82 Store::fetchOne<SinkAccount>(Query().filter(m_accountId))
83 .then([this](const SinkAccount &account) {
84 setName(account.getName());
85 }).exec();
86
87 //TODO
88}
89
diff --git a/framework/src/accounts/gmailcontroller.h b/framework/src/accounts/gmailcontroller.h
new file mode 100644
index 00000000..2a5ead65
--- /dev/null
+++ b/framework/src/accounts/gmailcontroller.h
@@ -0,0 +1,55 @@
1/*
2 Copyright (C) 2017 Michael Bohlender, <michael.bohlender@kdemail.net>
3
4 This program is free software; you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or
7 (at your option) any later version.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License along
15 with this program; if not, write to the Free Software Foundation, Inc.,
16 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17*/
18
19#pragma once
20
21#include <QObject>
22#include <QString>
23#include <QByteArray>
24
25#include <domain/controller.h>
26
27class GmailController : public Kube::Controller
28{
29 Q_OBJECT
30
31 //Interface properties
32 KUBE_CONTROLLER_PROPERTY(QString, Name, name)
33
34 KUBE_CONTROLLER_PROPERTY(QString, EmailAddress, emailAddress)
35 KUBE_CONTROLLER_PROPERTY(QString, Password, password)
36 KUBE_CONTROLLER_PROPERTY(QString, IdentityName, identityName)
37
38 //Actions
39 KUBE_CONTROLLER_ACTION(create)
40 KUBE_CONTROLLER_ACTION(modify)
41 KUBE_CONTROLLER_ACTION(remove)
42
43public:
44 explicit GmailController();
45
46public slots:
47 void load(const QByteArray &id);
48
49private:
50 QByteArray m_accountId;
51 QByteArray m_smtpId;
52 QByteArray m_imapId;
53 QByteArray m_identityId;
54};
55
diff --git a/framework/src/actions/action.cpp b/framework/src/actions/action.cpp
new file mode 100644
index 00000000..1344d112
--- /dev/null
+++ b/framework/src/actions/action.cpp
@@ -0,0 +1,127 @@
1/*
2 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsys.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19#include "action.h"
20
21#include <QDebug>
22#include <QEvent>
23#include <QPointer>
24#include <QDynamicPropertyChangeEvent>
25#include <QMetaObject>
26#include <QMetaProperty>
27
28#include "actionbroker.h"
29#include "actionhandler.h"
30#include "context.h"
31
32using namespace Kube;
33
34Action::Action(QObject *parent)
35 : QObject(parent),
36 mContext(nullptr)
37{
38}
39
40Action::Action(const QByteArray &actionId, Context &context, QObject *parent)
41 : QObject(parent),
42 mContext(&context),
43 mActionId(actionId)
44{
45 setContext(&context);
46}
47
48Action::~Action()
49{
50}
51
52void Action::setContext(Context *context)
53{
54 //Get notified when any property changes
55 for (int i = context->metaObject()->propertyOffset(); i < context->metaObject()->propertyCount(); i++) {
56 auto property = context->metaObject()->property(i) ;
57 // qWarning() << "Property " << property.name() << property.hasNotifySignal() << property.notifySignal().name();
58 if (QString(property.name()) != "objectName") {
59 //We do what SIGNAL does to connect to the changed signal automatically
60 QObject::connect(context, "2"+property.notifySignal().name()+"()", this, SLOT(contextChanged()));
61 }
62 }
63 mContext = context;
64 mContext->installEventFilter(this);
65 emit readyChanged();
66}
67
68bool Action::eventFilter(QObject *obj, QEvent *e)
69{
70 if (obj == mContext) {
71 if (e->type() == QEvent::DynamicPropertyChange) {
72 contextChanged();
73 }
74 }
75 return QObject::eventFilter(obj, e);
76}
77
78void Action::contextChanged()
79{
80 emit readyChanged();
81}
82
83Context *Action::context() const
84{
85 return mContext;
86}
87
88void Action::setActionId(const QByteArray &actionId)
89{
90 mActionId = actionId;
91 emit readyChanged();
92}
93
94QByteArray Action::actionId() const
95{
96 return mActionId;
97}
98
99bool Action::ready() const
100{
101 return ActionBroker::instance().isActionReady(mActionId, mContext, mPreHandler);
102}
103
104void Action::execute()
105{
106 ActionBroker::instance().executeAction(mActionId, mContext, mPreHandler, mPostHandler);
107}
108
109ActionResult Action::executeWithResult()
110{
111 return ActionBroker::instance().executeAction(mActionId, mContext, mPreHandler, mPostHandler);
112}
113
114void Action::addPreHandler(ActionHandler *handler)
115{
116 //For cleanup
117 handler->setParent(this);
118 mPreHandler << handler;
119}
120
121void Action::addPostHandler(ActionHandler *handler)
122{
123 //For cleanup
124 handler->setParent(this);
125 mPostHandler << handler;
126}
127
diff --git a/framework/src/actions/action.h b/framework/src/actions/action.h
new file mode 100644
index 00000000..dda8c987
--- /dev/null
+++ b/framework/src/actions/action.h
@@ -0,0 +1,71 @@
1/*
2 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsys.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19#pragma once
20
21#include <QObject>
22#include "context.h"
23#include "actionresult.h"
24
25namespace Kube {
26
27class ActionHandler;
28
29class Action : public QObject
30{
31 Q_OBJECT
32 Q_PROPERTY(QByteArray actionId READ actionId WRITE setActionId)
33 //FIXME if I set the property to Context* qml fails to assign the registered type which is calle Kube::Context_QML_90 in QML...
34 Q_PROPERTY(Context* context READ context WRITE setContext)
35 Q_PROPERTY(bool ready READ ready NOTIFY readyChanged)
36
37public:
38 Action(QObject *parent = 0);
39 Action(const QByteArray &actionId, Context &context, QObject *parent = 0);
40 ~Action();
41
42 void setContext(Context *);
43 Context *context() const;
44
45 void setActionId(const QByteArray &);
46 QByteArray actionId() const;
47
48 bool ready() const;
49
50 Q_INVOKABLE void execute();
51 ActionResult executeWithResult();
52
53 void addPreHandler(ActionHandler *handler);
54 void addPostHandler(ActionHandler *handler);
55
56 bool eventFilter(QObject *obj, QEvent *e) Q_DECL_OVERRIDE;
57
58Q_SIGNALS:
59 void readyChanged();
60
61private Q_SLOTS:
62 void contextChanged();
63
64private:
65 Context *mContext;
66 QByteArray mActionId;
67 QList<QPointer<ActionHandler>> mPreHandler;
68 QList<QPointer<ActionHandler>> mPostHandler;
69};
70
71}
diff --git a/framework/src/actions/actionbroker.cpp b/framework/src/actions/actionbroker.cpp
new file mode 100644
index 00000000..f6bfdd8e
--- /dev/null
+++ b/framework/src/actions/actionbroker.cpp
@@ -0,0 +1,101 @@
1/*
2 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsys.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19
20#include "actionbroker.h"
21
22#include "context.h"
23#include "actionhandler.h"
24#include <sink/log.h>
25
26#include <QDebug>
27
28using namespace Kube;
29
30SINK_DEBUG_AREA("actionbroker")
31
32ActionBroker::ActionBroker(QObject *parent)
33 : QObject(parent)
34{
35
36}
37
38ActionBroker &ActionBroker::instance()
39{
40 static ActionBroker instance;
41 return instance;
42}
43
44bool ActionBroker::isActionReady(const QByteArray &actionId, Context *context, const QList<QPointer<ActionHandler>> &preHandler)
45{
46 if (!context) {
47 return false;
48 }
49 for (const auto handler : preHandler) {
50 if (!handler->isActionReady(context)) {
51 return false;
52 }
53 }
54
55 for (const auto handler : mHandler.values(actionId)) {
56 if (handler) {
57 if (handler->isActionReady(context)) {
58 return true;
59 }
60 }
61 }
62
63 return false;
64}
65
66ActionResult ActionBroker::executeAction(const QByteArray &actionId, Context *context, const QList<QPointer<ActionHandler>> &preHandler, const QList<QPointer<ActionHandler>> &postHandler)
67{
68 ActionResult result;
69 if (context) {
70 SinkLog() << "Executing action " << actionId;
71 SinkLog() << *context;
72 for (const auto handler : preHandler) {
73 handler->execute(context);
74 }
75 //TODO the main handler should only execute once the pre handler is done
76 for (const auto handler : mHandler.values(actionId)) {
77 if (handler) {
78 result += handler->execute(context);
79 }
80 }
81 //TODO the post handler should only execute once the main handler is done
82 for (const auto handler : postHandler) {
83 handler->execute(context);
84 }
85 } else {
86 SinkWarning() << "Can't execute without context";
87 result.setDone();
88 result.setError(1);
89 }
90 return result;
91}
92
93void ActionBroker::registerHandler(const QByteArray &actionId, ActionHandler *handler)
94{
95 mHandler.insert(actionId, handler);
96}
97
98void ActionBroker::unregisterHandler(const QByteArray &actionId, ActionHandler *handler)
99{
100 mHandler.remove(actionId, handler);
101}
diff --git a/framework/src/actions/actionbroker.h b/framework/src/actions/actionbroker.h
new file mode 100644
index 00000000..d893a3e7
--- /dev/null
+++ b/framework/src/actions/actionbroker.h
@@ -0,0 +1,49 @@
1/*
2 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsys.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19#pragma once
20
21#include <QObject>
22#include <QMultiMap>
23
24namespace Kube {
25class Context;
26class ActionHandler;
27class ActionResult;
28
29class ActionBroker : public QObject
30{
31 Q_OBJECT
32public:
33 static ActionBroker &instance();
34
35 bool isActionReady(const QByteArray &actionId, Context *context, const QList<QPointer<ActionHandler>> &preHandler);
36 ActionResult executeAction(const QByteArray &actionId, Context *context, const QList<QPointer<ActionHandler>> &preHandler, const QList<QPointer<ActionHandler>> &postHandler);
37
38 void registerHandler(const QByteArray &actionId, ActionHandler *handler);
39 void unregisterHandler(const QByteArray &actionId, ActionHandler *handler);
40
41Q_SIGNALS:
42 void readyChanged();
43
44private:
45 ActionBroker(QObject *parent = 0);
46 QMultiMap<QByteArray, QPointer<ActionHandler>> mHandler;
47};
48
49}
diff --git a/framework/src/actions/actionhandler.cpp b/framework/src/actions/actionhandler.cpp
new file mode 100644
index 00000000..99fdf66a
--- /dev/null
+++ b/framework/src/actions/actionhandler.cpp
@@ -0,0 +1,151 @@
1/*
2 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsys.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19
20#include "actionhandler.h"
21
22#include "context.h"
23#include "actionbroker.h"
24#include <QDebug>
25
26using namespace Kube;
27
28ActionHandler::ActionHandler(QObject *parent)
29 : QObject(parent)
30{
31
32}
33
34ActionHandler::~ActionHandler()
35{
36 ActionBroker::instance().unregisterHandler(mActionId, this);
37}
38
39bool ActionHandler::isActionReady(Context *context)
40{
41 if (context) {
42 QVariant returnedValue;
43 QMetaObject::invokeMethod(this, "isReady",
44 Q_RETURN_ARG(QVariant, returnedValue),
45 Q_ARG(QVariant, QVariant::fromValue(context)));
46 return returnedValue.toBool();
47 } else {
48 qWarning() << "The handler didn't get a context";
49 }
50 return false;
51}
52
53ActionResult ActionHandler::execute(Context *context)
54{
55 ActionResult result;
56 QVariant returnedValue;
57 qWarning() << "Executing the handler";
58 if (context) {
59 //The base implementation to call the handler in QML
60 QMetaObject::invokeMethod(this, "handler",
61 Q_RETURN_ARG(QVariant, returnedValue),
62 Q_ARG(QVariant, QVariant::fromValue(context)));
63 //TODO: support async handlers in QML
64 result.setDone();
65 } else {
66 qWarning() << "The handler didn't get a context";
67 result.setDone();
68 result.setError(1);
69 }
70 return result;
71}
72
73void ActionHandler::setActionId(const QByteArray &actionId)
74{
75 //Reassigning the id is not supported
76 Q_ASSERT(mActionId.isEmpty());
77 mActionId = actionId;
78 ActionBroker::instance().registerHandler(actionId, this);
79}
80
81QByteArray ActionHandler::actionId() const
82{
83 return mActionId;
84}
85
86void ActionHandler::setRequiredProperties(const QSet<QByteArray> &requiredProperties)
87{
88 mRequiredProperties = requiredProperties;
89}
90
91QSet<QByteArray> ActionHandler::requiredProperties() const
92{
93 return mRequiredProperties;
94}
95
96
97ActionHandlerHelper::ActionHandlerHelper(const Handler &handler)
98 : ActionHandler(nullptr),
99 handlerFunction(handler)
100{
101}
102
103ActionHandlerHelper::ActionHandlerHelper(const IsReadyFunction &isReady, const Handler &handler)
104 : ActionHandler(nullptr),
105 isReadyFunction(isReady),
106 handlerFunction(handler)
107{
108}
109
110ActionHandlerHelper::ActionHandlerHelper(const QByteArray &actionId, const IsReadyFunction &isReady, const Handler &handler)
111 : ActionHandler(nullptr),
112 isReadyFunction(isReady),
113 handlerFunction(handler)
114{
115 setActionId(actionId);
116}
117
118ActionHandlerHelper::ActionHandlerHelper(const QByteArray &actionId, const IsReadyFunction &isReady, const JobHandler &handler)
119 : ActionHandler(nullptr),
120 isReadyFunction(isReady),
121 jobHandlerFunction(handler)
122{
123 setActionId(actionId);
124}
125
126bool ActionHandlerHelper::isActionReady(Context *context)
127{
128 if (isReadyFunction) {
129 return isReadyFunction(context);
130 }
131 return true;
132}
133
134ActionResult ActionHandlerHelper::execute(Context *context)
135{
136 ActionResult result;
137 if (handlerFunction) {
138 handlerFunction(context);
139 result.setDone();
140 } else {
141 jobHandlerFunction(context).then([=](const KAsync::Error &error) {
142 auto modifyableResult = result;
143 if (error) {
144 qWarning() << "Job failed: " << error.errorCode << error.errorMessage;
145 modifyableResult.setError(1);
146 }
147 modifyableResult.setDone();
148 }).exec();
149 }
150 return result;
151}
diff --git a/framework/src/actions/actionhandler.h b/framework/src/actions/actionhandler.h
new file mode 100644
index 00000000..fbaedad1
--- /dev/null
+++ b/framework/src/actions/actionhandler.h
@@ -0,0 +1,112 @@
1/*
2 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsys.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19#pragma once
20
21#include <QObject>
22#include <QMultiMap>
23#include <functional>
24#include <KAsync/Async>
25
26#include "actionresult.h"
27#include "context.h"
28
29namespace Kube {
30
31class ActionHandler : public QObject
32{
33 Q_OBJECT
34 Q_PROPERTY(QByteArray actionId READ actionId WRITE setActionId)
35
36public:
37 ActionHandler(QObject *parent = 0);
38 virtual ~ActionHandler();
39
40 virtual bool isActionReady(Context *context);
41
42 virtual ActionResult execute(Context *context);
43
44 void setActionId(const QByteArray &);
45 QByteArray actionId() const;
46
47 void setRequiredProperties(const QSet<QByteArray> &requiredProperties);
48 QSet<QByteArray> requiredProperties() const;
49
50private:
51 QByteArray mActionId;
52 QSet<QByteArray> mRequiredProperties;
53};
54
55template <typename ContextType>
56class ActionHandlerBase : public ActionHandler
57{
58public:
59 ActionHandlerBase(const QByteArray &actionId)
60 : ActionHandler{}
61 {
62 setActionId(actionId);
63 }
64
65 bool isActionReady(Context *c) Q_DECL_OVERRIDE
66 {
67 auto wrapper = ContextType{*c};
68 return isActionReady(wrapper);
69 }
70
71 ActionResult execute(Context *c) Q_DECL_OVERRIDE
72 {
73 ActionResult result;
74 auto wrapper = ContextType{*c};
75 execute(wrapper)
76 .template then([=](const KAsync::Error &error) {
77 auto modifyableResult = result;
78 if (error) {
79 qWarning() << "Job failed: " << error.errorCode << error.errorMessage;
80 modifyableResult.setError(1);
81 }
82 modifyableResult.setDone();
83 }).exec();
84 return result;
85 }
86protected:
87
88 virtual bool isActionReady(ContextType &) { return true; }
89 virtual KAsync::Job<void> execute(ContextType &) = 0;
90};
91
92class ActionHandlerHelper : public ActionHandler
93{
94public:
95 typedef std::function<bool(Context *)> IsReadyFunction;
96 typedef std::function<void(Context *)> Handler;
97 typedef std::function<KAsync::Job<void>(Context *)> JobHandler;
98
99 ActionHandlerHelper(const Handler &);
100 ActionHandlerHelper(const IsReadyFunction &, const Handler &);
101 ActionHandlerHelper(const QByteArray &actionId, const IsReadyFunction &, const Handler &);
102 ActionHandlerHelper(const QByteArray &actionId, const IsReadyFunction &, const JobHandler &);
103
104 bool isActionReady(Context *) Q_DECL_OVERRIDE;
105 ActionResult execute(Context *) Q_DECL_OVERRIDE;
106private:
107 const IsReadyFunction isReadyFunction;
108 const Handler handlerFunction;
109 const JobHandler jobHandlerFunction;
110};
111
112}
diff --git a/framework/src/actions/actionresult.cpp b/framework/src/actions/actionresult.cpp
new file mode 100644
index 00000000..631c61c4
--- /dev/null
+++ b/framework/src/actions/actionresult.cpp
@@ -0,0 +1,20 @@
1/*
2 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsys.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19#include "actionresult.h"
20
diff --git a/framework/src/actions/actionresult.h b/framework/src/actions/actionresult.h
new file mode 100644
index 00000000..dcf1a9ec
--- /dev/null
+++ b/framework/src/actions/actionresult.h
@@ -0,0 +1,80 @@
1/*
2 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsys.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19#pragma once
20
21#include <QObject>
22#include <QSharedPointer>
23
24namespace Kube {
25
26struct ActionResultData
27{
28 ActionResultData() : mError(0), mDone(false) {}
29 int mError;
30 bool mDone;
31};
32
33class ActionResult : public QObject
34{
35 Q_OBJECT
36public:
37 ActionResult() : QObject(), mData(new ActionResultData()) {}
38 ActionResult(const ActionResult &rhs) : QObject(), mData(rhs.mData) {}
39 ActionResult &operator=(const ActionResult &rhs)
40 {
41 mData = rhs.mData;
42 return *this;
43 }
44 virtual ~ActionResult() {}
45
46 ActionResult &operator+=(const ActionResult &rhs)
47 {
48 if (!error() && rhs.error()) {
49 setError(rhs.error());
50 }
51 if (isDone() && rhs.isDone()) {
52 mData->mDone = false;
53 }
54 mData = rhs.mData;
55 return *this;
56 }
57
58 void setDone() {
59 mData->mDone = true;
60 }
61
62 bool isDone() const {
63 return mData->mDone;
64 }
65
66 void setError(int error) {
67 mData->mError = error;
68 }
69
70 int error() const {
71 return mData->mError;
72 }
73
74private:
75 QSharedPointer<ActionResultData> mData;
76};
77
78}
79
80Q_DECLARE_METATYPE(Kube::ActionResult);
diff --git a/framework/src/actions/context.cpp b/framework/src/actions/context.cpp
new file mode 100644
index 00000000..45b660a9
--- /dev/null
+++ b/framework/src/actions/context.cpp
@@ -0,0 +1,91 @@
1/*
2 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsys.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19#include "context.h"
20
21#include <QDebug>
22#include <QMetaProperty>
23
24using namespace Kube;
25
26Context::Context(QObject *parent)
27 : QObject(parent)
28{
29
30}
31
32Context::Context(const Context &other)
33 : QObject()
34{
35 *this = other;
36}
37
38Context &Context::operator=(const Context &other)
39{
40 for (const auto &p : other.availableProperties()) {
41 setProperty(p, other.property(p));
42 }
43 return *this;
44}
45
46void Context::clear()
47{
48 auto meta = metaObject();
49 for (auto i = meta->propertyOffset(); i < meta->propertyCount(); i++) {
50 auto property = meta->property(i);
51 setProperty(property.name(), QVariant());
52 }
53 for (const auto &p : dynamicPropertyNames()) {
54 setProperty(p, QVariant());
55 }
56}
57
58QSet<QByteArray> Context::availableProperties() const
59{
60 QSet<QByteArray> names;
61 auto meta = metaObject();
62 for (auto i = meta->propertyOffset(); i < meta->propertyCount(); i++) {
63 auto property = meta->property(i);
64 names << property.name();
65 }
66 for (const auto &p : dynamicPropertyNames()) {
67 names << p;
68 }
69 return names;
70}
71
72QDebug operator<<(QDebug dbg, const Kube::Context &context)
73{
74 dbg << "Kube::Context {\n";
75 auto metaObject = context.metaObject();
76 for (auto i = metaObject->propertyOffset(); i < metaObject->propertyCount(); i++) {
77 auto property = metaObject->property(i);
78 dbg << property.name() << context.property(property.name()) << "\n";
79 }
80 for (const auto &p : context.dynamicPropertyNames()) {
81 dbg << p << context.property(p) << "\n";
82 }
83 dbg << "\n}";
84 return dbg;
85}
86
87QDebug operator<<(QDebug dbg, const Kube::ContextWrapper &context)
88{
89 dbg << context.context;
90 return dbg;
91}
diff --git a/framework/src/actions/context.h b/framework/src/actions/context.h
new file mode 100644
index 00000000..52fbdbc1
--- /dev/null
+++ b/framework/src/actions/context.h
@@ -0,0 +1,67 @@
1/*
2 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsys.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19#pragma once
20
21#include <QObject>
22#define KUBE_CONTEXT_PROPERTY(TYPE, NAME, LOWERCASENAME) \
23 public: Q_PROPERTY(TYPE LOWERCASENAME MEMBER m##NAME NOTIFY LOWERCASENAME##Changed) \
24 Q_SIGNALS: void LOWERCASENAME##Changed(); \
25 private: TYPE m##NAME;
26
27#define KUBE_CONTEXTWRAPPER_PROPERTY(TYPE, NAME, LOWERCASENAME) \
28 public: \
29 struct NAME { \
30 static constexpr const char *name = #LOWERCASENAME; \
31 typedef TYPE Type; \
32 }; \
33 void set##NAME(const TYPE &value) { context.setProperty(NAME::name, QVariant::fromValue(value)); } \
34 void clear##NAME() { context.setProperty(NAME::name, QVariant{}); } \
35 TYPE get##NAME() const { return context.property(NAME::name).value<TYPE>(); } \
36
37
38namespace Kube {
39
40class Context : public QObject {
41 Q_OBJECT
42public:
43 Context(QObject *parent = 0);
44 Context(const Context &);
45
46 virtual ~Context(){};
47
48 Context &operator=(const Context &);
49
50 virtual void clear();
51
52 QSet<QByteArray> availableProperties() const;
53};
54
55class ContextWrapper {
56public:
57 ContextWrapper(Context &c) : context(c) {}
58 Context &context;
59};
60
61}
62
63QDebug operator<<(QDebug dbg, const Kube::Context &);
64QDebug operator<<(QDebug dbg, const Kube::ContextWrapper &);
65
66Q_DECLARE_METATYPE(Kube::Context*);
67
diff --git a/framework/src/actions/tests/CMakeLists.txt b/framework/src/actions/tests/CMakeLists.txt
new file mode 100644
index 00000000..af872a3b
--- /dev/null
+++ b/framework/src/actions/tests/CMakeLists.txt
@@ -0,0 +1,6 @@
1include_directories(${CMAKE_CURRENT_BINARY_DIR})
2cmake_policy(SET CMP0063 NEW)
3add_executable(actiontest actiontest.cpp)
4add_test(actiontest sinkactiontest)
5qt5_use_modules(actiontest Core Test)
6target_link_libraries(actiontest actionplugin)
diff --git a/framework/src/actions/tests/actiontest.cpp b/framework/src/actions/tests/actiontest.cpp
new file mode 100644
index 00000000..a4ec4432
--- /dev/null
+++ b/framework/src/actions/tests/actiontest.cpp
@@ -0,0 +1,102 @@
1#include <QTest>
2#include <QDebug>
3#include <QSignalSpy>
4
5#include <actions/action.h>
6#include <actions/context.h>
7#include <actions/actionhandler.h>
8
9#include <sink/log.h>
10
11SINK_DEBUG_AREA("actiontest")
12
13class HandlerContext : public Kube::Context {
14 Q_OBJECT
15 KUBE_CONTEXT_PROPERTY(QString, Property1, property1)
16 KUBE_CONTEXT_PROPERTY(QString, Property2, property2)
17};
18
19class HandlerContextWrapper : public Kube::ContextWrapper {
20 using Kube::ContextWrapper::ContextWrapper;
21 KUBE_CONTEXTWRAPPER_PROPERTY(QString, Property1, property1)
22 KUBE_CONTEXTWRAPPER_PROPERTY(QString, Property2, property2)
23};
24
25
26
27class Handler : public Kube::ActionHandlerBase<HandlerContextWrapper>
28{
29public:
30 Handler() : Kube::ActionHandlerBase<HandlerContextWrapper>{"org.kde.kube.test.action1"}
31 {}
32
33 //TODO default implementation checks that all defined properties are available in the context
34 // bool isReady() override {
35 // auto accountId = context->property("accountId").value<QByteArray>();
36 // return !accountId.isEmpty();
37 // }
38
39 KAsync::Job<void> execute(HandlerContextWrapper &context)
40 {
41 SinkLog() << "Executing action1";
42 SinkLog() << context;
43 executions.append(context.context);
44 return KAsync::null<void>();
45 }
46 mutable QList<Kube::Context> executions;
47};
48
49class Context1 : public Kube::ContextWrapper {
50 using Kube::ContextWrapper::ContextWrapper;
51 KUBE_CONTEXTWRAPPER_PROPERTY(QString, Property1, property1)
52 KUBE_CONTEXTWRAPPER_PROPERTY(QByteArray, Property2, property2)
53};
54
55class Context2 : public Kube::ContextWrapper {
56 using Kube::ContextWrapper::ContextWrapper;
57 KUBE_CONTEXTWRAPPER_PROPERTY(QByteArray, Property2, property2)
58};
59
60
61class ActionTest : public QObject
62{
63 Q_OBJECT
64private slots:
65
66 void initTestCase()
67 {
68 }
69
70 void testActionExecution()
71 {
72 Handler actionHandler;
73
74 HandlerContext context;
75 //Kube::Context context;
76 HandlerContextWrapper{context}.setProperty1(QString("property1"));
77 context.setProperty("property2", QVariant::fromValue(QString("property2")));
78 auto future = Kube::Action("org.kde.kube.test.action1", context).executeWithResult();
79
80 QTRY_VERIFY(future.isDone());
81 QVERIFY(!future.error());
82
83 QCOMPARE(actionHandler.executions.size(), 1);
84 QCOMPARE(actionHandler.executions.first().availableProperties().size(), 2);
85 }
86
87 void testContextCasting()
88 {
89 Kube::Context c;
90
91 Context1 context1{c};
92 context1.setProperty1("property1");
93 context1.setProperty2("property2");
94
95 auto context2 = Context2{c};
96 QCOMPARE(context2.getProperty2(), QByteArray("property2"));
97 }
98
99};
100
101QTEST_GUILESS_MAIN(ActionTest)
102#include "actiontest.moc"
diff --git a/framework/src/domain/actions/sinkactions.cpp b/framework/src/domain/actions/sinkactions.cpp
new file mode 100644
index 00000000..460cb6a1
--- /dev/null
+++ b/framework/src/domain/actions/sinkactions.cpp
@@ -0,0 +1,56 @@
1/*
2 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsys.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19
20#include <actions/actionhandler.h>
21
22#include <KMime/Message>
23#include <QFile>
24
25#include <sink/store.h>
26#include <sink/log.h>
27
28SINK_DEBUG_AREA("sinkactions")
29
30using namespace Kube;
31using namespace Sink;
32using namespace Sink::ApplicationDomain;
33
34class FolderContext : public Kube::ContextWrapper {
35 using Kube::ContextWrapper::ContextWrapper;
36 KUBE_CONTEXTWRAPPER_PROPERTY(Sink::ApplicationDomain::Folder::Ptr, Folder, folder)
37};
38
39static ActionHandlerHelper synchronizeHandler("org.kde.kube.actions.synchronize",
40 [](Context *context) -> bool {
41 return true;
42 },
43 [](Context *context_) {
44 auto context = FolderContext{*context_};
45 if (auto folder = context.getFolder()) {
46 SinkLog() << "Synchronizing folder " << folder->resourceInstanceIdentifier() << folder->identifier();
47 auto scope = SyncScope().resourceFilter(folder->resourceInstanceIdentifier()).filter<Mail::Folder>(QVariant::fromValue(folder->identifier()));
48 scope.setType<ApplicationDomain::Mail>();
49 Store::synchronize(scope).exec();
50 } else {
51 SinkLog() << "Synchronizing all";
52 Store::synchronize(SyncScope()).exec();
53 }
54 }
55);
56
diff --git a/framework/src/domain/actions/tests/CMakeLists.txt b/framework/src/domain/actions/tests/CMakeLists.txt
new file mode 100644
index 00000000..2c34d628
--- /dev/null
+++ b/framework/src/domain/actions/tests/CMakeLists.txt
@@ -0,0 +1,6 @@
1include_directories(${CMAKE_CURRENT_BINARY_DIR})
2cmake_policy(SET CMP0063 NEW)
3add_executable(sinkactiontest sinkactiontest.cpp)
4add_test(sinkactiontest sinkactiontest)
5qt5_use_modules(sinkactiontest Core Test Concurrent)
6target_link_libraries(sinkactiontest sink frameworkplugin KF5::Mime)
diff --git a/framework/src/domain/actions/tests/sinkactiontest.cpp b/framework/src/domain/actions/tests/sinkactiontest.cpp
new file mode 100644
index 00000000..79375503
--- /dev/null
+++ b/framework/src/domain/actions/tests/sinkactiontest.cpp
@@ -0,0 +1,58 @@
1#include <QTest>
2#include <QDebug>
3#include <QSignalSpy>
4#include <sink/test.h>
5#include <sink/store.h>
6#include <sink/log.h>
7#include <KMime/Message>
8
9#include <actions/action.h>
10#include <actions/context.h>
11
12using namespace Sink;
13
14class SinkActionTest : public QObject
15{
16 Q_OBJECT
17private slots:
18
19 void initTestCase()
20 {
21 Sink::Test::initTest();
22 Sink::Log::setDebugOutputLevel(Sink::Log::Trace);
23 }
24
25 void testSaveAsDraftFail()
26 {
27 Kube::Context context;
28 auto future = Kube::Action("org.kde.kube.actions.save-as-draft", context).executeWithResult();
29
30 QTRY_VERIFY(future.isDone());
31 //because of empty context
32 QVERIFY(future.error());
33 }
34
35 void testSaveAsDraftNew()
36 {
37 auto message = KMime::Message::Ptr::create();
38 message->subject(true)->fromUnicodeString(QString::fromLatin1("Foobar"), "utf8");
39 message->assemble();
40
41 auto &&account = Test::TestAccount::registerAccount();
42
43 Kube::Context context;
44 context.setProperty("message", QVariant::fromValue(message));
45 context.setProperty("accountId", QVariant::fromValue(account.identifier));
46 auto future = Kube::Action("org.kde.kube.actions.save-as-draft", context).executeWithResult();
47
48 QTRY_VERIFY(future.isDone());
49 QVERIFY(!future.error());
50 auto mails = account.entities<Sink::ApplicationDomain::Mail>();
51 QCOMPARE(mails.size(), 1);
52 auto mail = mails.first();
53 QVERIFY(mail->getProperty("draft").toBool());
54 }
55};
56
57QTEST_GUILESS_MAIN(SinkActionTest)
58#include "sinkactiontest.moc"
diff --git a/framework/src/domain/attachmentmodel.cpp b/framework/src/domain/attachmentmodel.cpp
new file mode 100644
index 00000000..e98c6fc0
--- /dev/null
+++ b/framework/src/domain/attachmentmodel.cpp
@@ -0,0 +1,145 @@
1/*
2 Copyright (c) 2016 Sandro Knauß <knauss@kolabsys.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19
20#include "messageparser.h"
21#include "mimetreeparser/interface.h"
22
23#include <QIcon>
24#include <QDebug>
25
26QString sizeHuman(const Content::Ptr &content)
27{
28 float num = content->content().size();
29 QStringList list;
30 list << "KB" << "MB" << "GB" << "TB";
31
32 QStringListIterator i(list);
33 QString unit("Bytes");
34
35 while(num >= 1024.0 && i.hasNext())
36 {
37 unit = i.next();
38 num /= 1024.0;
39 }
40
41 if (unit == "Bytes") {
42 return QString().setNum(num) + " " + unit;
43 } else {
44 return QString().setNum(num,'f',2)+" "+unit;
45 }
46}
47
48class AttachmentModelPrivate
49{
50public:
51 AttachmentModelPrivate(AttachmentModel *q_ptr, const std::shared_ptr<Parser> &parser);
52
53 AttachmentModel *q;
54 std::shared_ptr<Parser> mParser;
55 QVector<Part::Ptr> mAttachments;
56};
57
58AttachmentModelPrivate::AttachmentModelPrivate(AttachmentModel* q_ptr, const std::shared_ptr<Parser>& parser)
59 : q(q_ptr)
60 , mParser(parser)
61{
62 mAttachments = mParser->collectAttachmentParts();
63}
64
65AttachmentModel::AttachmentModel(std::shared_ptr<Parser> parser)
66 : d(std::unique_ptr<AttachmentModelPrivate>(new AttachmentModelPrivate(this, parser)))
67{
68}
69
70AttachmentModel::~AttachmentModel()
71{
72}
73
74QHash<int, QByteArray> AttachmentModel::roleNames() const
75{
76 QHash<int, QByteArray> roles;
77 roles[TypeRole] = "type";
78 roles[NameRole] = "name";
79 roles[SizeRole] = "size";
80 roles[IconRole] = "icon";
81 roles[IsEncryptedRole] = "encrypted";
82 roles[IsSignedRole] = "signed";
83 return roles;
84}
85
86QModelIndex AttachmentModel::index(int row, int column, const QModelIndex &parent) const
87{
88 if (row < 0 || column != 0) {
89 return QModelIndex();
90 }
91
92 if (row < d->mAttachments.size()) {
93 return createIndex(row, column, d->mAttachments.at(row).get());
94 }
95 return QModelIndex();
96}
97
98QVariant AttachmentModel::data(const QModelIndex &index, int role) const
99{
100 if (!index.isValid()) {
101 switch (role) {
102 case Qt::DisplayRole:
103 return QString("root");
104 }
105 return QVariant();
106 }
107
108 if (index.internalPointer()) {
109 const auto entry = static_cast<Part *>(index.internalPointer());
110 const auto content = entry->content().at(0);
111 switch(role) {
112 case TypeRole:
113 return content->mailMime()->mimetype().name();
114 case NameRole:
115 return entry->mailMime()->filename();
116 case IconRole:
117 return QIcon::fromTheme(content->mailMime()->mimetype().iconName());
118 case SizeRole:
119 return sizeHuman(content);
120 case IsEncryptedRole:
121 return content->encryptions().size() > 0;
122 case IsSignedRole:
123 return content->signatures().size() > 0;
124 }
125 }
126 return QVariant();
127}
128
129QModelIndex AttachmentModel::parent(const QModelIndex &index) const
130{
131 return QModelIndex();
132}
133
134int AttachmentModel::rowCount(const QModelIndex &parent) const
135{
136 if (!parent.isValid()) {
137 return d->mAttachments.size();
138 }
139 return 0;
140}
141
142int AttachmentModel::columnCount(const QModelIndex &parent) const
143{
144 return 1;
145}
diff --git a/framework/src/domain/completer.cpp b/framework/src/domain/completer.cpp
new file mode 100644
index 00000000..cacb4faa
--- /dev/null
+++ b/framework/src/domain/completer.cpp
@@ -0,0 +1,26 @@
1/*
2 Copyright (c) 2016 Christian Mollekofp <mollekopf@kolabsys.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19#include "completer.h"
20
21#include <QQmlEngine>
22
23Completer::Completer(QAbstractItemModel *model) : mModel{model}
24{
25 QQmlEngine::setObjectOwnership(mModel, QQmlEngine::CppOwnership);
26}
diff --git a/framework/src/domain/completer.h b/framework/src/domain/completer.h
new file mode 100644
index 00000000..a672b809
--- /dev/null
+++ b/framework/src/domain/completer.h
@@ -0,0 +1,40 @@
1/*
2 Copyright (c) 2016 Christian Mollekofp <mollekopf@kolabsys.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19#pragma once
20
21#include <QAbstractItemModel>
22#include <QString>
23#include <QQmlEngine>
24
25class Completer : public QObject {
26 Q_OBJECT
27 Q_PROPERTY (QAbstractItemModel* model READ model CONSTANT)
28 Q_PROPERTY (QString searchString WRITE setSearchString READ searchString)
29
30public:
31 Completer(QAbstractItemModel *model);
32 QAbstractItemModel *model() { return mModel; }
33 virtual void setSearchString(const QString &s) { mSearchString = s; }
34 QString searchString() const { return mSearchString; }
35
36private:
37 QAbstractItemModel *mModel = nullptr;
38 QString mSearchString;
39};
40
diff --git a/framework/src/domain/composercontroller.cpp b/framework/src/domain/composercontroller.cpp
new file mode 100644
index 00000000..3328d9eb
--- /dev/null
+++ b/framework/src/domain/composercontroller.cpp
@@ -0,0 +1,307 @@
1/*
2 Copyright (c) 2016 Michael Bohlender <michael.bohlender@kdemail.net>
3 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsys.com>
4
5 This library is free software; you can redistribute it and/or modify it
6 under the terms of the GNU Library General Public License as published by
7 the Free Software Foundation; either version 2 of the License, or (at your
8 option) any later version.
9
10 This library is distributed in the hope that it will be useful, but WITHOUT
11 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
13 License for more details.
14
15 You should have received a copy of the GNU Library General Public License
16 along with this library; see the file COPYING.LIB. If not, write to the
17 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18 02110-1301, USA.
19*/
20
21
22#include "composercontroller.h"
23#include <settings/settings.h>
24#include <KMime/Message>
25#include <KCodecs/KEmailAddress>
26#include <QVariant>
27#include <QSortFilterProxyModel>
28#include <QList>
29#include <QDebug>
30#include <QQmlEngine>
31#include <sink/store.h>
32#include <sink/log.h>
33
34#include "identitiesmodel.h"
35#include "recepientautocompletionmodel.h"
36#include "mailtemplates.h"
37
38SINK_DEBUG_AREA("composercontroller");
39
40class IdentitySelector : public Selector {
41public:
42 IdentitySelector(ComposerController &controller) : Selector(new IdentitiesModel), mController(controller)
43 {
44 }
45
46 void setCurrent(const QModelIndex &index) Q_DECL_OVERRIDE
47 {
48 if (index.isValid()) {
49 auto currentAccountId = index.data(IdentitiesModel::AccountId).toByteArray();
50
51 KMime::Types::Mailbox mb;
52 mb.setName(index.data(IdentitiesModel::Username).toString());
53 mb.setAddress(index.data(IdentitiesModel::Address).toString().toUtf8());
54 SinkLog() << "Setting current identity: " << mb.prettyAddress() << "Account: " << currentAccountId;
55
56 mController.setIdentity(mb);
57 mController.setAccountId(currentAccountId);
58 } else {
59 SinkWarning() << "No valid identity for index: " << index;
60 mController.clearIdentity();
61 mController.clearAccountId();
62 }
63
64 }
65private:
66 ComposerController &mController;
67};
68
69class RecipientCompleter : public Completer {
70public:
71 RecipientCompleter() : Completer(new RecipientAutocompletionModel)
72 {
73 }
74
75 void setSearchString(const QString &s) {
76 static_cast<RecipientAutocompletionModel*>(model())->setFilter(s);
77 Completer::setSearchString(s);
78 }
79};
80
81
82ComposerController::ComposerController()
83 : Kube::Controller(),
84 action_send{new Kube::ControllerAction{this, &ComposerController::send}},
85 action_saveAsDraft{new Kube::ControllerAction{this, &ComposerController::saveAsDraft}},
86 mRecipientCompleter{new RecipientCompleter},
87 mIdentitySelector{new IdentitySelector{*this}}
88{
89 updateSaveAsDraftAction();
90 // mSendAction->monitorProperty<To>();
91 // mSendAction->monitorProperty<Send>([] (const QString &) -> bool{
92 // //validate
93 // });
94 // registerAction<ControllerAction>(&ComposerController::send);
95 // actionDepends<ControllerAction, To, Subject>();
96 // TODO do in constructor
97
98 QObject::connect(this, &ComposerController::toChanged, &ComposerController::updateSendAction);
99 QObject::connect(this, &ComposerController::subjectChanged, &ComposerController::updateSendAction);
100 QObject::connect(this, &ComposerController::accountIdChanged, &ComposerController::updateSendAction);
101 QObject::connect(this, &ComposerController::toChanged, &ComposerController::updateSaveAsDraftAction);
102 QObject::connect(this, &ComposerController::subjectChanged, &ComposerController::updateSaveAsDraftAction);
103 QObject::connect(this, &ComposerController::accountIdChanged, &ComposerController::updateSaveAsDraftAction);
104 updateSendAction();
105}
106
107void ComposerController::clear()
108{
109 Controller::clear();
110 //Reapply account and identity from selection
111 mIdentitySelector->reapplyCurrentIndex();
112}
113
114Completer *ComposerController::recipientCompleter() const
115{
116 return mRecipientCompleter.data();
117}
118
119Selector *ComposerController::identitySelector() const
120{
121 return mIdentitySelector.data();
122}
123
124void ComposerController::setMessage(const KMime::Message::Ptr &msg)
125{
126 setTo(msg->to(true)->asUnicodeString());
127 setCc(msg->cc(true)->asUnicodeString());
128 setSubject(msg->subject(true)->asUnicodeString());
129 setBody(msg->body());
130 setExistingMessage(msg);
131}
132
133void ComposerController::loadMessage(const QVariant &message, bool loadAsDraft)
134{
135 using namespace Sink;
136 using namespace Sink::ApplicationDomain;
137
138 Query query(*message.value<Mail::Ptr>());
139 query.request<Mail::MimeMessage>();
140 Store::fetchOne<Mail>(query).then([this, loadAsDraft](const Mail &mail) {
141 setExistingMail(mail);
142
143 //TODO this should probably happen as reaction to the property being set.
144 const auto mailData = KMime::CRLFtoLF(mail.getMimeMessage());
145 if (!mailData.isEmpty()) {
146 KMime::Message::Ptr mail(new KMime::Message);
147 mail->setContent(mailData);
148 mail->parse();
149 if (loadAsDraft) {
150 setMessage(mail);
151 } else {
152 auto reply = MailTemplates::reply(mail);
153 //We assume reply
154 setMessage(reply);
155 }
156 } else {
157 qWarning() << "Retrieved empty message";
158 }
159 }).exec();
160}
161
162void ComposerController::recordForAutocompletion(const QByteArray &addrSpec, const QByteArray &displayName)
163{
164 if (auto model = static_cast<RecipientAutocompletionModel*>(recipientCompleter()->model())) {
165 model->addEntry(addrSpec, displayName);
166 }
167}
168
169void applyAddresses(const QString &list, std::function<void(const QByteArray &, const QByteArray &)> callback)
170{
171 for (const auto &to : KEmailAddress::splitAddressList(list)) {
172 QByteArray displayName;
173 QByteArray addrSpec;
174 QByteArray comment;
175 KEmailAddress::splitAddress(to.toUtf8(), displayName, addrSpec, comment);
176 callback(addrSpec, displayName);
177 }
178}
179
180KMime::Message::Ptr ComposerController::assembleMessage()
181{
182 auto mail = mExistingMessage;
183 if (!mail) {
184 mail = KMime::Message::Ptr::create();
185 }
186 applyAddresses(getTo(), [&](const QByteArray &addrSpec, const QByteArray &displayName) {
187 mail->to(true)->addAddress(addrSpec, displayName);
188 recordForAutocompletion(addrSpec, displayName);
189 });
190 applyAddresses(getCc(), [&](const QByteArray &addrSpec, const QByteArray &displayName) {
191 mail->cc(true)->addAddress(addrSpec, displayName);
192 recordForAutocompletion(addrSpec, displayName);
193 });
194 applyAddresses(getBcc(), [&](const QByteArray &addrSpec, const QByteArray &displayName) {
195 mail->bcc(true)->addAddress(addrSpec, displayName);
196 recordForAutocompletion(addrSpec, displayName);
197 });
198
199 mail->from(true)->addAddress(getIdentity());
200
201 mail->subject(true)->fromUnicodeString(getSubject(), "utf-8");
202 mail->setBody(getBody().toUtf8());
203 if (!mail->messageID()) {
204 mail->messageID(true)->generate("org.kde.kube");
205 }
206 if (!mail->date(true)->dateTime().isValid()) {
207 mail->date(true)->setDateTime(QDateTime::currentDateTimeUtc());
208 }
209
210 mail->assemble();
211 return mail;
212}
213
214void ComposerController::updateSendAction()
215{
216 auto enabled = !getTo().isEmpty() && !getSubject().isEmpty() && !getAccountId().isEmpty();
217 sendAction()->setEnabled(enabled);
218}
219
220void ComposerController::send()
221{
222 // verify<To, Subject>()
223 // && verify<Subject>();
224 auto message = assembleMessage();
225
226 auto accountId = getAccountId();
227 //SinkLog() << "Sending a mail: " << *this;
228 using namespace Sink;
229 using namespace Sink::ApplicationDomain;
230
231 Q_ASSERT(!accountId.isEmpty());
232 Query query;
233 query.containsFilter<SinkResource::Capabilities>(ResourceCapabilities::Mail::transport);
234 query.filter<SinkResource::Account>(accountId);
235 auto job = Store::fetchAll<SinkResource>(query)
236 .then([=](const QList<SinkResource::Ptr> &resources) {
237 if (!resources.isEmpty()) {
238 auto resourceId = resources[0]->identifier();
239 SinkLog() << "Sending message via resource: " << resourceId;
240 Mail mail(resourceId);
241 mail.setMimeMessage(message->encodedContent());
242 return Store::create(mail)
243 .then<void>([=] {
244 //Trigger a sync, but don't wait for it.
245 Store::synchronize(Sink::SyncScope{}.resourceFilter(resourceId)).exec();
246 });
247 }
248 SinkWarning() << "Failed to find a mailtransport resource";
249 return KAsync::error<void>(0, "Failed to find a MailTransport resource.");
250 })
251 .then([&] (const KAsync::Error &error) {
252 SinkLog() << "Message was sent: ";
253 emit done();
254 });
255 run(job);
256}
257
258void ComposerController::updateSaveAsDraftAction()
259{
260 bool enabled = !getAccountId().isEmpty();
261 sendAction()->setEnabled(enabled);
262}
263
264void ComposerController::saveAsDraft()
265{
266 SinkLog() << "Save as draft";
267 const auto accountId = getAccountId();
268 auto existingMail = getExistingMail();
269
270 auto message = assembleMessage();
271 //FIXME this is something for the validation
272 if (!message) {
273 SinkWarning() << "Failed to get the mail: ";
274 return;
275 }
276
277 using namespace Sink;
278 using namespace Sink::ApplicationDomain;
279
280 auto job = [&] {
281 if (existingMail.identifier().isEmpty()) {
282 SinkLog() << "Creating a new draft" << existingMail.identifier();
283 Query query;
284 query.containsFilter<SinkResource::Capabilities>(ResourceCapabilities::Mail::drafts);
285 query.filter<SinkResource::Account>(accountId);
286 return Store::fetchOne<SinkResource>(query)
287 .then([=](const SinkResource &resource) {
288 Mail mail(resource.identifier());
289 mail.setDraft(true);
290 mail.setMimeMessage(message->encodedContent());
291 return Store::create(mail);
292 })
293 .onError([] (const KAsync::Error &error) {
294 SinkWarning() << "Error while creating draft: " << error.errorMessage;
295 });
296 } else {
297 SinkLog() << "Modifying an existing mail" << existingMail.identifier();
298 existingMail.setDraft(true);
299 existingMail.setMimeMessage(message->encodedContent());
300 return Store::modify(existingMail);
301 }
302 }();
303 job = job.then([&] (const KAsync::Error &) {
304 emit done();
305 });
306 run(job);
307}
diff --git a/framework/src/domain/composercontroller.h b/framework/src/domain/composercontroller.h
new file mode 100644
index 00000000..92467d05
--- /dev/null
+++ b/framework/src/domain/composercontroller.h
@@ -0,0 +1,93 @@
1/*
2 Copyright (c) 2016 Michael Bohlender <michael.bohlender@kdemail.net>
3 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsys.com>
4
5 This library is free software; you can redistribute it and/or modify it
6 under the terms of the GNU Library General Public License as published by
7 the Free Software Foundation; either version 2 of the License, or (at your
8 option) any later version.
9
10 This library is distributed in the hope that it will be useful, but WITHOUT
11 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
13 License for more details.
14
15 You should have received a copy of the GNU Library General Public License
16 along with this library; see the file COPYING.LIB. If not, write to the
17 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18 02110-1301, USA.
19*/
20
21#pragma once
22
23#include <QObject>
24#include <QString>
25#include <QStringList>
26#include <QVariant>
27#include <sink/applicationdomaintype.h>
28#include <KMime/Message>
29
30#include "completer.h"
31#include "selector.h"
32#include "controller.h"
33
34inline bool operator !=(const KMime::Types::Mailbox &l, const KMime::Types::Mailbox &r)
35{
36 return !(l.prettyAddress() == r.prettyAddress());
37}
38
39Q_DECLARE_METATYPE(KMime::Types::Mailbox);
40
41namespace KMime {
42class Message;
43}
44
45class ComposerController : public Kube::Controller
46{
47 Q_OBJECT
48
49 //Interface properties
50 KUBE_CONTROLLER_PROPERTY(QString, To, to)
51 KUBE_CONTROLLER_PROPERTY(QString, Cc, cc)
52 KUBE_CONTROLLER_PROPERTY(QString, Bcc, bcc)
53 KUBE_CONTROLLER_PROPERTY(QString, Subject, subject)
54 KUBE_CONTROLLER_PROPERTY(QString, Body, body)
55
56 //Set by identitySelector
57 KUBE_CONTROLLER_PROPERTY(KMime::Types::Mailbox, Identity, identity)
58 KUBE_CONTROLLER_PROPERTY(QByteArray, AccountId, accountId)
59
60 //Set by loadMessage
61 KUBE_CONTROLLER_PROPERTY(KMime::Message::Ptr, ExistingMessage, existingMessage)
62 KUBE_CONTROLLER_PROPERTY(Sink::ApplicationDomain::Mail, ExistingMail, existingMail)
63
64 Q_PROPERTY (Completer* recipientCompleter READ recipientCompleter CONSTANT)
65 Q_PROPERTY (Selector* identitySelector READ identitySelector CONSTANT)
66 //Q_PROPERTY (QValidator* subjectValidator READ subjectValidator CONSTANT)
67
68 KUBE_CONTROLLER_ACTION(send)
69 KUBE_CONTROLLER_ACTION(saveAsDraft)
70
71public:
72 explicit ComposerController();
73
74 Completer *recipientCompleter() const;
75 Selector *identitySelector() const;
76
77 Q_INVOKABLE void loadMessage(const QVariant &draft, bool loadAsDraft);
78
79public slots:
80 virtual void clear() Q_DECL_OVERRIDE;
81
82private slots:
83 void updateSendAction();
84 void updateSaveAsDraftAction();
85
86private:
87 void recordForAutocompletion(const QByteArray &addrSpec, const QByteArray &displayName);
88 void setMessage(const QSharedPointer<KMime::Message> &msg);
89 KMime::Message::Ptr assembleMessage();
90
91 QScopedPointer<Completer> mRecipientCompleter;
92 QScopedPointer<Selector> mIdentitySelector;
93};
diff --git a/framework/src/domain/contactcontroller.cpp b/framework/src/domain/contactcontroller.cpp
new file mode 100644
index 00000000..49f78b40
--- /dev/null
+++ b/framework/src/domain/contactcontroller.cpp
@@ -0,0 +1,68 @@
1/*
2 * Copyright (C) 2017 Michael Bohlender, <michael.bohlender@kdemail.net>
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License along
15 * with this program; if not, write to the Free Software Foundation, Inc.,
16 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 */
18
19#include "contactcontroller.h"
20
21#include <sink/applicationdomaintype.h>
22
23ContactController::ContactController()
24 : Kube::Controller(),
25 action_save{new Kube::ControllerAction{this, &ContactController::save}}
26{
27 updateSaveAction();
28}
29
30void ContactController::save() {
31 qWarning() << "Saving is not implemented";
32}
33
34void ContactController::updateSaveAction()
35{
36 saveAction()->setEnabled(!getName().isEmpty());
37}
38
39void ContactController::loadContact(const QVariant &contact)
40{
41 if (auto c = contact.value<Sink::ApplicationDomain::Contact::Ptr>()) {
42 setName(c->getFn());
43 QStringList emails;
44 for (const auto &e : c->getEmails()) {
45 emails << e.email;
46 }
47 setEmails(emails);
48 }
49}
50
51QVariant ContactController::contact() const
52{
53 return QVariant{};
54}
55
56void ContactController::removeEmail(const QString &email)
57{
58 auto emails = getEmails();
59 emails.removeAll(email);
60 setEmails(emails);
61}
62
63void ContactController::addEmail(const QString &email)
64{
65 auto emails = getEmails();
66 emails.append(email);
67 setEmails(emails);
68}
diff --git a/framework/src/domain/contactcontroller.h b/framework/src/domain/contactcontroller.h
new file mode 100644
index 00000000..d1973d9c
--- /dev/null
+++ b/framework/src/domain/contactcontroller.h
@@ -0,0 +1,52 @@
1/*
2 * Copyright (C) 2017 Michael Bohlender, <michael.bohlender@kdemail.net>
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License along
15 * with this program; if not, write to the Free Software Foundation, Inc.,
16 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 */
18
19
20#pragma once
21
22#include <QObject>
23#include <QString>
24#include <QStringList>
25
26#include "controller.h"
27
28class ContactController : public Kube::Controller
29{
30 Q_OBJECT
31
32 // Input properties
33 Q_PROPERTY(QVariant contact READ contact WRITE loadContact)
34
35 //Interface properties
36 KUBE_CONTROLLER_PROPERTY(QString, Name, name)
37 KUBE_CONTROLLER_PROPERTY(QStringList, Emails, emails)
38
39 KUBE_CONTROLLER_ACTION(save)
40
41public:
42 explicit ContactController();
43
44 Q_INVOKABLE void loadContact(const QVariant &contact);
45 Q_INVOKABLE void removeEmail(const QString &email);
46 Q_INVOKABLE void addEmail(const QString &email);
47
48 QVariant contact() const;
49
50private slots:
51 void updateSaveAction();
52};
diff --git a/framework/src/domain/controller.cpp b/framework/src/domain/controller.cpp
new file mode 100644
index 00000000..52f4cd1f
--- /dev/null
+++ b/framework/src/domain/controller.cpp
@@ -0,0 +1,59 @@
1/*
2 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsys.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19#include "controller.h"
20
21#include <QQmlEngine>
22#include <QMetaProperty>
23#include <sink/log.h>
24
25using namespace Kube;
26
27ControllerAction::ControllerAction()
28 : QObject()
29{
30 QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership);
31}
32
33void ControllerAction::execute()
34{
35 emit triggered();
36}
37
38void Controller::clear()
39{
40 auto meta = metaObject();
41 for (auto i = meta->propertyOffset(); i < meta->propertyCount(); i++) {
42 auto property = meta->property(i);
43 setProperty(property.name(), QVariant());
44 }
45 for (const auto &p : dynamicPropertyNames()) {
46 setProperty(p, QVariant());
47 }
48}
49
50void Controller::run(const KAsync::Job<void> &job)
51{
52 auto jobToExec = job;
53 jobToExec.onError([] (const KAsync::Error &error) {
54 SinkWarningCtx(Sink::Log::Context{"controller"}) << "Error while executing job: " << error.errorMessage;
55 });
56 //TODO handle error
57 //TODO attach a log context to the execution that we can gather from the job?
58 jobToExec.exec();
59}
diff --git a/framework/src/domain/controller.h b/framework/src/domain/controller.h
new file mode 100644
index 00000000..bf2a7ace
--- /dev/null
+++ b/framework/src/domain/controller.h
@@ -0,0 +1,90 @@
1/*
2 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsys.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19#pragma once
20
21#include <QObject>
22#include <QVariant>
23#include <KAsync/Async>
24
25#define KUBE_CONTROLLER_PROPERTY(TYPE, NAME, LOWERCASENAME) \
26 public: Q_PROPERTY(TYPE LOWERCASENAME MEMBER m##NAME NOTIFY LOWERCASENAME##Changed) \
27 Q_SIGNALS: void LOWERCASENAME##Changed(); \
28 private: TYPE m##NAME; \
29 public: \
30 struct NAME { \
31 static constexpr const char *name = #LOWERCASENAME; \
32 typedef TYPE Type; \
33 }; \
34 void set##NAME(const TYPE &value) { setProperty(NAME::name, QVariant::fromValue(value)); } \
35 void clear##NAME() { setProperty(NAME::name, QVariant{}); } \
36 TYPE get##NAME() const { return m##NAME; } \
37
38
39#define KUBE_CONTROLLER_ACTION(NAME) \
40 Q_PROPERTY (Kube::ControllerAction* NAME##Action READ NAME##Action CONSTANT) \
41 private: QScopedPointer<Kube::ControllerAction> action_##NAME; \
42 public: Kube::ControllerAction* NAME##Action() const { Q_ASSERT(action_##NAME); return action_##NAME.data(); } \
43 private slots: void NAME(); \
44
45
46namespace Kube {
47
48class ControllerAction : public QObject {
49 Q_OBJECT
50 Q_PROPERTY(bool enabled MEMBER mEnabled NOTIFY enabledChanged)
51public:
52 ControllerAction();
53 template <typename Func>
54 ControllerAction(const typename QtPrivate::FunctionPointer<Func>::Object *obj, Func slot)
55 : ControllerAction()
56 {
57 QObject::connect(this, &ControllerAction::triggered, obj, slot);
58 }
59
60 ~ControllerAction() = default;
61
62 Q_INVOKABLE void execute();
63 void setEnabled(bool enabled) { setProperty("enabled", enabled); }
64
65signals:
66 void enabledChanged();
67 void triggered();
68
69private:
70 bool mEnabled = true;
71};
72
73class Controller : public QObject {
74 Q_OBJECT
75public:
76 Controller() = default;
77 virtual ~Controller() = default;
78
79public slots:
80 virtual void clear();
81
82signals:
83 void done();
84 void error();
85
86protected:
87 void run(const KAsync::Job<void> &job);
88};
89
90}
diff --git a/framework/src/domain/foldercontroller.cpp b/framework/src/domain/foldercontroller.cpp
new file mode 100644
index 00000000..3c10f773
--- /dev/null
+++ b/framework/src/domain/foldercontroller.cpp
@@ -0,0 +1,62 @@
1/*
2 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsys.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19#include "foldercontroller.h"
20
21#include <sink/store.h>
22#include <sink/log.h>
23
24SINK_DEBUG_AREA("foldercontroller");
25
26using namespace Sink;
27using namespace Sink::ApplicationDomain;
28
29FolderController::FolderController()
30 : Kube::Controller(),
31 action_synchronize{new Kube::ControllerAction{this, &FolderController::synchronize}},
32 action_moveToFolder{new Kube::ControllerAction{this, &FolderController::moveToFolder}}
33{
34}
35
36void FolderController::synchronize()
37{
38 auto job = [&] {
39 auto accountId = getAccountId();
40 if (auto folder = getFolder()) {
41 SinkLog() << "Synchronizing folder " << folder->resourceInstanceIdentifier() << folder->identifier();
42 auto scope = SyncScope().resourceFilter(folder->resourceInstanceIdentifier()).filter<ApplicationDomain::Mail::Folder>(QVariant::fromValue(folder->identifier()));
43 scope.setType<ApplicationDomain::Mail>();
44 return Store::synchronize(scope);
45 } else if (!accountId.isEmpty()) {
46 return Store::synchronize(SyncScope{}.resourceFilter<ApplicationDomain::SinkResource::Account>(accountId));
47 } else {
48 SinkLog() << "Synchronizing all";
49 return Store::synchronize(SyncScope());
50 }
51 }();
52 run(job);
53}
54
55void FolderController::moveToFolder()
56{
57 auto mail = getMail();
58 auto targetFolder = getFolder();
59 mail->setFolder(*targetFolder);
60 SinkLog() << "Moving to folder " << mail->identifier() << targetFolder->identifier();
61 run(Store::modify(*mail));
62}
diff --git a/framework/src/domain/foldercontroller.h b/framework/src/domain/foldercontroller.h
new file mode 100644
index 00000000..c0815546
--- /dev/null
+++ b/framework/src/domain/foldercontroller.h
@@ -0,0 +1,36 @@
1/*
2 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsys.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19#pragma once
20
21#include <QObject>
22#include "controller.h"
23#include "sink/applicationdomaintype.h"
24
25class FolderController : public Kube::Controller
26{
27 Q_OBJECT
28 KUBE_CONTROLLER_PROPERTY(Sink::ApplicationDomain::Folder::Ptr, Folder, folder)
29 KUBE_CONTROLLER_PROPERTY(Sink::ApplicationDomain::Mail::Ptr, Mail, mail)
30 KUBE_CONTROLLER_PROPERTY(QByteArray, AccountId, accountId)
31 KUBE_CONTROLLER_ACTION(synchronize)
32 KUBE_CONTROLLER_ACTION(moveToFolder)
33
34public:
35 explicit FolderController();
36};
diff --git a/framework/src/domain/folderlistmodel.cpp b/framework/src/domain/folderlistmodel.cpp
new file mode 100644
index 00000000..14405beb
--- /dev/null
+++ b/framework/src/domain/folderlistmodel.cpp
@@ -0,0 +1,145 @@
1/*
2 Copyright (c) 2016 Michael Bohlender <michael.bohlender@kdemail.net>
3 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsys.com>
4
5 This library is free software; you can redistribute it and/or modify it
6 under the terms of the GNU Library General Public License as published by
7 the Free Software Foundation; either version 2 of the License, or (at your
8 option) any later version.
9
10 This library is distributed in the hope that it will be useful, but WITHOUT
11 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
13 License for more details.
14
15 You should have received a copy of the GNU Library General Public License
16 along with this library; see the file COPYING.LIB. If not, write to the
17 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18 02110-1301, USA.
19*/
20
21#include "folderlistmodel.h"
22#include <sink/store.h>
23#include <sink/log.h>
24#include <settings/settings.h>
25
26using namespace Sink;
27using namespace Sink::ApplicationDomain;
28
29FolderListModel::FolderListModel(QObject *parent) : QSortFilterProxyModel()
30{
31 setDynamicSortFilter(true);
32 sort(0, Qt::AscendingOrder);
33
34 Query query;
35 query.setFlags(Sink::Query::LiveQuery | Sink::Query::UpdateStatus);
36 query.request<Folder::Name>().request<Folder::Icon>().request<Folder::Parent>().request<Folder::SpecialPurpose>();
37 query.requestTree<Folder::Parent>();
38 query.setId("foldertree");
39 runQuery(query);
40}
41
42FolderListModel::~FolderListModel()
43{
44
45}
46
47QHash< int, QByteArray > FolderListModel::roleNames() const
48{
49 QHash<int, QByteArray> roles;
50
51 roles[Name] = "name";
52 roles[Icon] = "icon";
53 roles[Id] = "id";
54 roles[DomainObject] = "domainObject";
55 roles[Status] = "status";
56
57 return roles;
58}
59
60QVariant FolderListModel::data(const QModelIndex &idx, int role) const
61{
62 auto srcIdx = mapToSource(idx);
63 auto folder = srcIdx.data(Sink::Store::DomainObjectRole).value<Sink::ApplicationDomain::Folder::Ptr>();
64 switch (role) {
65 case Name:
66 return folder->getName();
67 case Icon:
68 return folder->getIcon();
69 case Id:
70 return folder->identifier();
71 case DomainObject:
72 return QVariant::fromValue(folder);
73 case Status: {
74 switch (srcIdx.data(Sink::Store::StatusRole).toInt()) {
75 case Sink::ApplicationDomain::SyncStatus::SyncInProgress:
76 return InProgressStatus;
77 case Sink::ApplicationDomain::SyncStatus::SyncError:
78 return ErrorStatus;
79 case Sink::ApplicationDomain::SyncStatus::SyncSuccess:
80 return SuccessStatus;
81 }
82 return NoStatus;
83 }
84 }
85 return QSortFilterProxyModel::data(idx, role);
86}
87
88void FolderListModel::runQuery(const Query &query)
89{
90 mModel = Store::loadModel<Folder>(query);
91 setSourceModel(mModel.data());
92}
93
94void FolderListModel::setAccountId(const QVariant &accountId)
95{
96 const auto account = accountId.toString().toUtf8();
97
98 //Get all folders of an account
99 auto query = Query();
100 query.resourceFilter<SinkResource::Account>(account);
101 query.setFlags(Sink::Query::LiveQuery | Sink::Query::UpdateStatus);
102 query.requestTree<Folder::Parent>();
103 query.request<Folder::Name>()
104 .request<Folder::Icon>()
105 .request<Folder::Parent>()
106 .request<Folder::SpecialPurpose>();
107 query.requestTree<Folder::Parent>();
108 query.setId("foldertree" + account);
109 runQuery(query);
110}
111
112static int getPriority(const Sink::ApplicationDomain::Folder &folder)
113{
114 auto specialPurpose = folder.getSpecialPurpose();
115 if (specialPurpose.contains(Sink::ApplicationDomain::SpecialPurpose::Mail::inbox)) {
116 return 5;
117 } else if (specialPurpose.contains(Sink::ApplicationDomain::SpecialPurpose::Mail::drafts)) {
118 return 6;
119 } else if (specialPurpose.contains(Sink::ApplicationDomain::SpecialPurpose::Mail::sent)) {
120 return 7;
121 } else if (specialPurpose.contains(Sink::ApplicationDomain::SpecialPurpose::Mail::trash)) {
122 return 8;
123 } else if (!specialPurpose.isEmpty()) {
124 return 9;
125 }
126 return 10;
127}
128
129bool FolderListModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
130{
131 const auto leftFolder = left.data(Sink::Store::DomainObjectRole).value<Sink::ApplicationDomain::Folder::Ptr>();
132 const auto rightFolder = right.data(Sink::Store::DomainObjectRole).value<Sink::ApplicationDomain::Folder::Ptr>();
133 const auto leftPriority = getPriority(*leftFolder);
134 const auto rightPriority = getPriority(*rightFolder);
135 if (leftPriority == rightPriority) {
136 return leftFolder->getName() < rightFolder->getName();
137 }
138 return leftPriority < rightPriority;
139}
140
141QVariant FolderListModel::accountId() const
142{
143 return QVariant();
144}
145
diff --git a/framework/src/domain/folderlistmodel.h b/framework/src/domain/folderlistmodel.h
new file mode 100644
index 00000000..17645bb5
--- /dev/null
+++ b/framework/src/domain/folderlistmodel.h
@@ -0,0 +1,71 @@
1/*
2 Copyright (c) 2016 Michael Bohlender <michael.bohlender@kdemail.net>
3 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsys.com>
4
5 This library is free software; you can redistribute it and/or modify it
6 under the terms of the GNU Library General Public License as published by
7 the Free Software Foundation; either version 2 of the License, or (at your
8 option) any later version.
9
10 This library is distributed in the hope that it will be useful, but WITHOUT
11 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
13 License for more details.
14
15 You should have received a copy of the GNU Library General Public License
16 along with this library; see the file COPYING.LIB. If not, write to the
17 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18 02110-1301, USA.
19*/
20
21#pragma once
22
23#include <QObject>
24#include <QSortFilterProxyModel>
25#include <QSharedPointer>
26#include <QStringList>
27
28namespace Sink {
29 class Query;
30}
31
32class FolderListModel : public QSortFilterProxyModel
33{
34 Q_OBJECT
35
36 Q_PROPERTY (QVariant accountId READ accountId WRITE setAccountId)
37
38public:
39 enum Status {
40 NoStatus,
41 InProgressStatus,
42 ErrorStatus,
43 SuccessStatus,
44 };
45 Q_ENUMS(Status)
46
47 FolderListModel(QObject *parent = Q_NULLPTR);
48 ~FolderListModel();
49
50 QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE;
51
52 enum Roles {
53 Name = Qt::UserRole + 1,
54 Icon,
55 Id,
56 DomainObject,
57 Status
58 };
59 Q_ENUMS(Roles)
60
61 QHash<int, QByteArray> roleNames() const Q_DECL_OVERRIDE;
62
63 void setAccountId(const QVariant &accountId);
64 QVariant accountId() const;
65protected:
66 bool lessThan(const QModelIndex &left, const QModelIndex &right) const Q_DECL_OVERRIDE;
67
68private:
69 void runQuery(const Sink::Query &query);
70 QSharedPointer<QAbstractItemModel> mModel;
71};
diff --git a/framework/src/domain/identitiesmodel.cpp b/framework/src/domain/identitiesmodel.cpp
new file mode 100644
index 00000000..0d97fc6a
--- /dev/null
+++ b/framework/src/domain/identitiesmodel.cpp
@@ -0,0 +1,99 @@
1/*
2 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsys.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19#include "identitiesmodel.h"
20#include <sink/store.h>
21#include <sink/log.h>
22
23using namespace Sink;
24
25IdentitiesModel::IdentitiesModel(QObject *parent) : QIdentityProxyModel()
26{
27 Sink::Query query;
28 query.setFlags(Sink::Query::LiveQuery);
29 query.request<Sink::ApplicationDomain::Identity::Name>()
30 .request<Sink::ApplicationDomain::Identity::Address>()
31 .request<Sink::ApplicationDomain::Identity::Account>();
32 runQuery(query);
33}
34
35IdentitiesModel::~IdentitiesModel()
36{
37
38}
39
40QHash< int, QByteArray > IdentitiesModel::roleNames() const
41{
42 QHash<int, QByteArray> roles;
43
44 roles[Name] = "name";
45 roles[Username] = "username";
46 roles[Address] = "address";
47 roles[IdentityId] = "identityId";
48 roles[AccountId] = "accountId";
49 roles[AccountName] = "accountName";
50 roles[AccountIcon] = "accountIcon";
51 roles[DisplayName] = "displayName";
52
53 return roles;
54}
55
56QVariant IdentitiesModel::data(const QModelIndex &idx, int role) const
57{
58 auto srcIdx = mapToSource(idx);
59 switch (role) {
60 case Name:
61 return srcIdx.data(Sink::Store::DomainObjectRole).value<Sink::ApplicationDomain::Identity::Ptr>()->getName();
62 case Username:
63 return srcIdx.data(Sink::Store::DomainObjectRole).value<Sink::ApplicationDomain::Identity::Ptr>()->getName();
64 case Address:
65 return srcIdx.data(Sink::Store::DomainObjectRole).value<Sink::ApplicationDomain::Identity::Ptr>()->getAddress();
66 case IdentityId:
67 return srcIdx.data(Sink::Store::DomainObjectRole).value<Sink::ApplicationDomain::Identity::Ptr>()->identifier();
68 case AccountId:
69 return srcIdx.data(Sink::Store::DomainObjectRole).value<Sink::ApplicationDomain::Identity::Ptr>()->getAccount();
70 case AccountName: {
71 const auto accountId = srcIdx.data(Sink::Store::DomainObjectRole).value<Sink::ApplicationDomain::Identity::Ptr>()->getAccount();
72 return mAccountNames.value(accountId);
73 }
74 case AccountIcon: {
75 const auto accountId = srcIdx.data(Sink::Store::DomainObjectRole).value<Sink::ApplicationDomain::Identity::Ptr>()->getAccount();
76 return mAccountIcons.value(accountId);
77 }
78 case DisplayName: {
79 return data(idx, AccountName).toString() + ": " + data(idx, Username).toString() + ", " + data(idx, Address).toString();
80 }
81 }
82 return QIdentityProxyModel::data(idx, role);
83}
84
85void IdentitiesModel::runQuery(const Sink::Query &query)
86{
87 using namespace Sink::ApplicationDomain;
88 mModel = Sink::Store::loadModel<Identity>(query);
89 setSourceModel(mModel.data());
90
91 Sink::Store::fetchAll<SinkAccount>(Sink::Query{}.request<SinkAccount::Icon>().request<SinkAccount::Name>())
92 .then([this](const QList<SinkAccount::Ptr> &accounts) {
93 for (const auto &account : accounts) {
94 mAccountNames.insert(account->identifier(), account->getName());
95 mAccountIcons.insert(account->identifier(), account->getIcon());
96 }
97 emit layoutChanged();
98 }).exec();
99}
diff --git a/framework/src/domain/identitiesmodel.h b/framework/src/domain/identitiesmodel.h
new file mode 100644
index 00000000..9b172251
--- /dev/null
+++ b/framework/src/domain/identitiesmodel.h
@@ -0,0 +1,60 @@
1/*
2 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsys.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19
20#pragma once
21
22#include <QObject>
23#include <QIdentityProxyModel>
24#include <QSharedPointer>
25#include <QStringList>
26
27namespace Sink {
28 class Query;
29}
30
31class IdentitiesModel : public QIdentityProxyModel
32{
33 Q_OBJECT
34
35public:
36 IdentitiesModel(QObject *parent = Q_NULLPTR);
37 ~IdentitiesModel();
38
39 QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
40
41 enum Roles {
42 Name = Qt::UserRole + 1,
43 Username,
44 Address,
45 IdentityId,
46 AccountId,
47 AccountName,
48 AccountIcon,
49 DisplayName
50 };
51 Q_ENUMS(Roles)
52
53 QHash<int, QByteArray> roleNames() const;
54
55private:
56 void runQuery(const Sink::Query &query);
57 QSharedPointer<QAbstractItemModel> mModel;
58 QHash <QByteArray, QString> mAccountNames;
59 QHash <QByteArray, QString> mAccountIcons;
60};
diff --git a/framework/src/domain/mailcontroller.cpp b/framework/src/domain/mailcontroller.cpp
new file mode 100644
index 00000000..b912567a
--- /dev/null
+++ b/framework/src/domain/mailcontroller.cpp
@@ -0,0 +1,125 @@
1/*
2 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsys.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19#include "mailcontroller.h"
20
21#include <sink/store.h>
22#include <sink/log.h>
23#include <sink/standardqueries.h>
24
25SINK_DEBUG_AREA("mailcontroller");
26
27using namespace Sink;
28using namespace Sink::ApplicationDomain;
29
30MailController::MailController()
31 : Kube::Controller(),
32 action_markAsRead{new Kube::ControllerAction{this, &MailController::markAsRead}},
33 action_markAsUnread{new Kube::ControllerAction{this, &MailController::markAsUnread}},
34 action_markAsImportant{new Kube::ControllerAction{this, &MailController::markAsImportant}},
35 action_moveToTrash{new Kube::ControllerAction{this, &MailController::moveToTrash}},
36 action_restoreFromTrash{new Kube::ControllerAction{this, &MailController::restoreFromTrash}},
37 action_remove{new Kube::ControllerAction{this, &MailController::remove}},
38 action_moveToFolder{new Kube::ControllerAction{this, &MailController::moveToFolder}}
39{
40 QObject::connect(this, &MailController::mailChanged, &MailController::updateActions);
41 updateActions();
42}
43
44void MailController::runModification(const std::function<void(ApplicationDomain::Mail &)> &f)
45{
46 if (auto mail = getMail()) {
47 f(*mail);
48 run(Store::modify(*mail));
49 } else if (auto mail = getThreadLeader()) {
50 f(*mail);
51 run(Store::modify(Sink::StandardQueries::completeThread(*mail), *mail));
52 }
53}
54
55void MailController::updateActions()
56{
57 auto mail = getMail();
58 if (!mail) {
59 mail= getThreadLeader();
60 }
61 if (mail) {
62 action_moveToTrash->setEnabled(!mail->getTrash());
63 action_restoreFromTrash->setEnabled(mail->getTrash());
64 action_markAsRead->setEnabled(mail->getUnread());
65 action_markAsUnread->setEnabled(!mail->getUnread());
66 }
67}
68
69void MailController::markAsRead()
70{
71 runModification([] (ApplicationDomain::Mail &mail) {
72 mail.setUnread(false);
73 SinkLog() << "Mark as read " << mail.identifier();
74 });
75}
76
77void MailController::markAsUnread()
78{
79 runModification([] (ApplicationDomain::Mail &mail) {
80 mail.setUnread(true);
81 SinkLog() << "Mark as unread " << mail.identifier();
82 });
83}
84
85void MailController::markAsImportant()
86{
87 runModification([] (ApplicationDomain::Mail &mail) {
88 mail.setImportant(true);
89 SinkLog() << "Mark as important " << mail.identifier();
90 });
91}
92
93void MailController::moveToTrash()
94{
95 runModification([] (ApplicationDomain::Mail &mail) {
96 mail.setTrash(true);
97 SinkLog() << "Move to trash " << mail.identifier();
98 });
99}
100
101void MailController::restoreFromTrash()
102{
103 runModification([] (ApplicationDomain::Mail &mail) {
104 mail.setTrash(false);
105 SinkLog() << "Restore from trash " << mail.identifier();
106 });
107}
108
109void MailController::remove()
110{
111 runModification([] (ApplicationDomain::Mail &mail) {
112 mail.setTrash(true);
113 SinkLog() << "Remove " << mail.identifier();
114 });
115}
116
117void MailController::moveToFolder()
118{
119 runModification([&] (ApplicationDomain::Mail &mail) {
120 auto targetFolder = getTargetFolder();
121 mail.setFolder(*targetFolder);
122 SinkLog() << "Moving to folder " << mail.identifier() << targetFolder->identifier();
123 });
124}
125
diff --git a/framework/src/domain/mailcontroller.h b/framework/src/domain/mailcontroller.h
new file mode 100644
index 00000000..ae0f32d5
--- /dev/null
+++ b/framework/src/domain/mailcontroller.h
@@ -0,0 +1,44 @@
1/*
2 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsys.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19#pragma once
20
21#include <QObject>
22#include "controller.h"
23#include "sink/applicationdomaintype.h"
24
25class MailController : public Kube::Controller
26{
27 Q_OBJECT
28 KUBE_CONTROLLER_PROPERTY(Sink::ApplicationDomain::Mail::Ptr, Mail, mail)
29 KUBE_CONTROLLER_PROPERTY(Sink::ApplicationDomain::Mail::Ptr, ThreadLeader, threadLeader)
30 KUBE_CONTROLLER_PROPERTY(Sink::ApplicationDomain::Folder::Ptr, TargetFolder, targetFolder)
31 KUBE_CONTROLLER_ACTION(markAsRead)
32 KUBE_CONTROLLER_ACTION(markAsUnread)
33 KUBE_CONTROLLER_ACTION(markAsImportant)
34 KUBE_CONTROLLER_ACTION(moveToTrash)
35 KUBE_CONTROLLER_ACTION(restoreFromTrash)
36 KUBE_CONTROLLER_ACTION(remove)
37 KUBE_CONTROLLER_ACTION(moveToFolder)
38
39public:
40 explicit MailController();
41private slots:
42 void updateActions();
43 void runModification(const std::function<void(Sink::ApplicationDomain::Mail &)> &f);
44};
diff --git a/framework/src/domain/maillistmodel.cpp b/framework/src/domain/maillistmodel.cpp
new file mode 100644
index 00000000..83340f69
--- /dev/null
+++ b/framework/src/domain/maillistmodel.cpp
@@ -0,0 +1,265 @@
1/*
2 Copyright (c) 2016 Michael Bohlender <michael.bohlender@kdemail.net>
3 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsys.com>
4
5 This library is free software; you can redistribute it and/or modify it
6 under the terms of the GNU Library General Public License as published by
7 the Free Software Foundation; either version 2 of the License, or (at your
8 option) any later version.
9
10 This library is distributed in the hope that it will be useful, but WITHOUT
11 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
13 License for more details.
14
15 You should have received a copy of the GNU Library General Public License
16 along with this library; see the file COPYING.LIB. If not, write to the
17 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18 02110-1301, USA.
19*/
20
21#include "maillistmodel.h"
22
23#include <sink/standardqueries.h>
24
25MailListModel::MailListModel(QObject *parent)
26 : QSortFilterProxyModel()
27{
28 setDynamicSortFilter(true);
29 sort(0, Qt::DescendingOrder);
30 setFilterCaseSensitivity(Qt::CaseInsensitive);
31}
32
33MailListModel::~MailListModel()
34{
35
36}
37
38void MailListModel::setFilter(const QString &filter)
39{
40 setFilterWildcard(filter);
41}
42
43QString MailListModel::filter() const
44{
45 return {};
46}
47
48QHash< int, QByteArray > MailListModel::roleNames() const
49{
50 QHash<int, QByteArray> roles;
51
52 roles[Subject] = "subject";
53 roles[Sender] = "sender";
54 roles[SenderName] = "senderName";
55 roles[To] = "to";
56 roles[Cc] = "cc";
57 roles[Bcc] = "bcc";
58 roles[Date] = "date";
59 roles[Unread] = "unread";
60 roles[Important] = "important";
61 roles[Draft] = "draft";
62 roles[Sent] = "sent";
63 roles[Trash] = "trash";
64 roles[Id] = "id";
65 roles[MimeMessage] = "mimeMessage";
66 roles[DomainObject] = "domainObject";
67 roles[ThreadSize] = "threadSize";
68 roles[Mail] = "mail";
69 roles[Incomplete] = "incomplete";
70 roles[Status] = "status";
71
72 return roles;
73}
74
75static QString join(const QList<Sink::ApplicationDomain::Mail::Contact> &contacts)
76{
77 QStringList list;
78 for (const auto &contact : contacts) {
79 if (!contact.name.isEmpty()) {
80 list << QString("%1 <%2>").arg(contact.name).arg(contact.emailAddress);
81 } else {
82 list << contact.emailAddress;
83 }
84 }
85 return list.join(", ");
86}
87
88void MailListModel::fetchMail(Sink::ApplicationDomain::Mail::Ptr mail)
89{
90 if (mail && !mail->getFullPayloadAvailable() && !mFetchedMails.contains(mail->identifier())) {
91 qDebug() << "Fetching mail: " << mail->identifier() << mail->getSubject();
92 mFetchedMails.insert(mail->identifier());
93 Sink::Store::synchronize(Sink::SyncScope{*mail}).exec();
94 }
95}
96
97QVariant MailListModel::data(const QModelIndex &idx, int role) const
98{
99 auto srcIdx = mapToSource(idx);
100 auto mail = srcIdx.data(Sink::Store::DomainObjectRole).value<Sink::ApplicationDomain::Mail::Ptr>();
101 switch (role) {
102 case Subject:
103 return mail->getSubject();
104 case Sender:
105 return mail->getSender().emailAddress;
106 case SenderName:
107 return mail->getSender().name;
108 case To:
109 return join(mail->getTo());
110 case Cc:
111 return join(mail->getCc());
112 case Bcc:
113 return join(mail->getBcc());
114 case Date:
115 return mail->getDate();
116 case Unread:
117 return mail->getProperty("unreadCollected").toList().contains(true);
118 case Important:
119 return mail->getProperty("importantCollected").toList().contains(true);
120 case Draft:
121 return mail->getDraft();
122 case Sent:
123 return mail->getSent();
124 case Trash:
125 return mail->getTrash();
126 case Id:
127 return mail->identifier();
128 case DomainObject:
129 return QVariant::fromValue(mail);
130 case MimeMessage:
131 if (mFetchMails) {
132 const_cast<MailListModel*>(this)->fetchMail(mail);
133 }
134 return mail->getMimeMessage();
135 case ThreadSize:
136 return mail->getProperty("count").toInt();
137 case Mail:
138 return QVariant::fromValue(mail);
139 case Incomplete:
140 return !mail->getFullPayloadAvailable();
141 case Status:
142 const auto status = srcIdx.data(Sink::Store::StatusRole).toInt();
143 if (status == Sink::ApplicationDomain::SyncStatus::SyncInProgress) {
144 return InProgressStatus;
145 }
146 if (status == Sink::ApplicationDomain::SyncStatus::SyncError) {
147 return ErrorStatus;
148 }
149 return NoStatus;
150 }
151 return QSortFilterProxyModel::data(idx, role);
152}
153
154bool MailListModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
155{
156 const auto leftDate = left.data(Sink::Store::DomainObjectRole).value<Sink::ApplicationDomain::Mail::Ptr>()->getDate();
157 const auto rightDate = right.data(Sink::Store::DomainObjectRole).value<Sink::ApplicationDomain::Mail::Ptr>()->getDate();
158 return leftDate < rightDate;
159}
160
161bool MailListModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
162{
163 auto idx = sourceModel()->index(sourceRow, 0, sourceParent);
164 auto regExp = filterRegExp();
165 if (regExp.isEmpty()) {
166 return true;
167 }
168 auto mail = idx.data(Sink::Store::DomainObjectRole).value<Sink::ApplicationDomain::Mail::Ptr>();
169 return mail->getSubject().contains(regExp) ||
170 mail->getSender().name.contains(regExp);
171}
172
173void MailListModel::runQuery(const Sink::Query &query)
174{
175 m_model = Sink::Store::loadModel<Sink::ApplicationDomain::Mail>(query);
176 setSourceModel(m_model.data());
177}
178
179void MailListModel::setParentFolder(const QVariant &parentFolder)
180{
181 using namespace Sink::ApplicationDomain;
182 auto folder = parentFolder.value<Folder::Ptr>();
183 if (!folder) {
184 mCurrentQueryItem.clear();
185 setSourceModel(nullptr);
186 return;
187 }
188 if (mCurrentQueryItem == folder->identifier()) {
189 return;
190 }
191 mCurrentQueryItem = folder->identifier();
192 Sink::Query query = Sink::StandardQueries::threadLeaders(*folder);
193 if (!folder->getSpecialPurpose().contains(Sink::ApplicationDomain::SpecialPurpose::Mail::trash)) {
194 //Filter trash if this is not a trash folder
195 query.filter<Sink::ApplicationDomain::Mail::Trash>(false);
196 }
197 query.setFlags(Sink::Query::LiveQuery);
198 query.limit(100);
199 query.request<Mail::Subject>();
200 query.request<Mail::Sender>();
201 query.request<Mail::To>();
202 query.request<Mail::Cc>();
203 query.request<Mail::Bcc>();
204 query.request<Mail::Date>();
205 query.request<Mail::Unread>();
206 query.request<Mail::Important>();
207 query.request<Mail::Draft>();
208 query.request<Mail::Sent>();
209 query.request<Mail::Trash>();
210 query.request<Mail::Folder>();
211 mFetchMails = false;
212 qDebug() << "Running folder query: " << folder->resourceInstanceIdentifier() << folder->identifier();
213 //Latest mail on top
214 sort(0, Qt::DescendingOrder);
215 runQuery(query);
216}
217
218QVariant MailListModel::parentFolder() const
219{
220 return QVariant();
221}
222
223void MailListModel::setMail(const QVariant &variant)
224{
225 using namespace Sink::ApplicationDomain;
226 auto mail = variant.value<Sink::ApplicationDomain::Mail::Ptr>();
227 if (!mail) {
228 mCurrentQueryItem.clear();
229 setSourceModel(nullptr);
230 return;
231 }
232 if (mCurrentQueryItem == mail->identifier()) {
233 return;
234 }
235 mCurrentQueryItem = mail->identifier();
236 Sink::Query query = Sink::StandardQueries::completeThread(*mail);
237 query.setFlags(Sink::Query::LiveQuery | Sink::Query::UpdateStatus);
238 query.request<Mail::Subject>();
239 query.request<Mail::Sender>();
240 query.request<Mail::To>();
241 query.request<Mail::Cc>();
242 query.request<Mail::Bcc>();
243 query.request<Mail::Date>();
244 query.request<Mail::Unread>();
245 query.request<Mail::Important>();
246 query.request<Mail::Draft>();
247 query.request<Mail::Folder>();
248 query.request<Mail::Sent>();
249 query.request<Mail::Trash>();
250 query.request<Mail::MimeMessage>();
251 query.request<Mail::FullPayloadAvailable>();
252 mFetchMails = true;
253 mFetchedMails.clear();
254 qDebug() << "Running mail query: " << mail->resourceInstanceIdentifier() << mail->identifier();
255 //Latest mail at the bottom
256 sort(0, Qt::AscendingOrder);
257 runQuery(query);
258}
259
260QVariant MailListModel::mail() const
261{
262 return QVariant();
263}
264
265
diff --git a/framework/src/domain/maillistmodel.h b/framework/src/domain/maillistmodel.h
new file mode 100644
index 00000000..44347661
--- /dev/null
+++ b/framework/src/domain/maillistmodel.h
@@ -0,0 +1,94 @@
1/*
2 Copyright (c) 2016 Michael Bohlender <michael.bohlender@kdemail.net>
3 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsys.com>
4
5 This library is free software; you can redistribute it and/or modify it
6 under the terms of the GNU Library General Public License as published by
7 the Free Software Foundation; either version 2 of the License, or (at your
8 option) any later version.
9
10 This library is distributed in the hope that it will be useful, but WITHOUT
11 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
13 License for more details.
14
15 You should have received a copy of the GNU Library General Public License
16 along with this library; see the file COPYING.LIB. If not, write to the
17 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18 02110-1301, USA.
19*/
20
21#pragma once
22
23#include <sink/store.h>
24
25#include <QSortFilterProxyModel>
26#include <QSharedPointer>
27#include <QStringList>
28
29class MailListModel : public QSortFilterProxyModel
30{
31 Q_OBJECT
32 Q_PROPERTY (QVariant parentFolder READ parentFolder WRITE setParentFolder)
33 Q_PROPERTY (QVariant mail READ mail WRITE setMail)
34 Q_PROPERTY (QString filter READ filter WRITE setFilter)
35
36public:
37 enum Status {
38 NoStatus,
39 InProgressStatus,
40 ErrorStatus
41 };
42 Q_ENUMS(Status)
43
44 MailListModel(QObject *parent = Q_NULLPTR);
45 ~MailListModel();
46
47 QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE;
48
49 bool lessThan(const QModelIndex &left, const QModelIndex &right) const Q_DECL_OVERRIDE;
50 bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const Q_DECL_OVERRIDE;
51
52 enum Roles {
53 Subject = Qt::UserRole + 1,
54 Sender,
55 SenderName,
56 To,
57 Cc,
58 Bcc,
59 Date,
60 Unread,
61 Important,
62 Draft,
63 Sent,
64 Trash,
65 Id,
66 MimeMessage,
67 DomainObject,
68 ThreadSize,
69 Mail,
70 Incomplete,
71 Status
72 };
73
74 QHash<int, QByteArray> roleNames() const Q_DECL_OVERRIDE;
75
76 void runQuery(const Sink::Query &query);
77
78 void setParentFolder(const QVariant &parentFolder);
79 QVariant parentFolder() const;
80
81 void setMail(const QVariant &mail);
82 QVariant mail() const;
83
84 void setFilter(const QString &mail);
85 QString filter() const;
86
87private:
88 void fetchMail(Sink::ApplicationDomain::Mail::Ptr mail);
89
90 QSharedPointer<QAbstractItemModel> m_model;
91 bool mFetchMails = false;
92 QSet<QByteArray> mFetchedMails;
93 QByteArray mCurrentQueryItem;
94};
diff --git a/framework/src/domain/mailtemplates.cpp b/framework/src/domain/mailtemplates.cpp
new file mode 100644
index 00000000..6af381d2
--- /dev/null
+++ b/framework/src/domain/mailtemplates.cpp
@@ -0,0 +1,801 @@
1/*
2 Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com
3 Copyright (c) 2010 Leo Franchi <lfranchi@kde.org>
4 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsys.com>
5
6 This library is free software; you can redistribute it and/or modify it
7 under the terms of the GNU Library General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or (at your
9 option) any later version.
10
11 This library is distributed in the hope that it will be useful, but WITHOUT
12 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
14 License for more details.
15
16 You should have received a copy of the GNU Library General Public License
17 along with this library; see the file COPYING.LIB. If not, write to the
18 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 02110-1301, USA.
20*/
21#include "mailtemplates.h"
22
23#include <QByteArray>
24#include <QList>
25#include <QDebug>
26#include <QImage>
27#include <QWebPage>
28#include <QWebFrame>
29#include <QSysInfo>
30#include <QTextCodec>
31#include <QApplication>
32
33#include <KCodecs/KCharsets>
34#include <KMime/Types>
35
36#include "stringhtmlwriter.h"
37#include "objecttreesource.h"
38
39#include <MimeTreeParser/ObjectTreeParser>
40
41namespace KMime {
42 namespace Types {
43static bool operator==(const KMime::Types::AddrSpec &left, const KMime::Types::AddrSpec &right)
44{
45 return (left.asString() == right.asString());
46}
47
48static bool operator==(const KMime::Types::Mailbox &left, const KMime::Types::Mailbox &right)
49{
50 return (left.addrSpec().asString() == right.addrSpec().asString());
51}
52 }
53}
54
55static KMime::Types::Mailbox::List stripMyAddressesFromAddressList(const KMime::Types::Mailbox::List &list, const KMime::Types::AddrSpecList me)
56{
57 KMime::Types::Mailbox::List addresses(list);
58 for (KMime::Types::Mailbox::List::Iterator it = addresses.begin(); it != addresses.end();) {
59 if (me.contains(it->addrSpec())) {
60 it = addresses.erase(it);
61 } else {
62 ++it;
63 }
64 }
65
66 return addresses;
67}
68
69void initHeader(const KMime::Message::Ptr &message)
70{
71 message->removeHeader<KMime::Headers::To>();
72 message->removeHeader<KMime::Headers::Subject>();
73 message->date()->setDateTime(QDateTime::currentDateTime());
74
75 const QStringList extraInfo = QStringList() << QSysInfo::prettyProductName();
76 message->userAgent()->fromUnicodeString(QString("%1/%2(%3)").arg(QString::fromLocal8Bit("Kube")).arg("0.1").arg(extraInfo.join(",")), "utf-8");
77 // This will allow to change Content-Type:
78 message->contentType()->setMimeType("text/plain");
79}
80
81QString replacePrefixes(const QString &str, const QStringList &prefixRegExps,
82 bool replace, const QString &newPrefix)
83{
84 bool recognized = false;
85 // construct a big regexp that
86 // 1. is anchored to the beginning of str (sans whitespace)
87 // 2. matches at least one of the part regexps in prefixRegExps
88 QString bigRegExp = QStringLiteral("^(?:\\s+|(?:%1))+\\s*").arg(prefixRegExps.join(QStringLiteral(")|(?:")));
89 QRegExp rx(bigRegExp, Qt::CaseInsensitive);
90 if (rx.isValid()) {
91 QString tmp = str;
92 if (rx.indexIn(tmp) == 0) {
93 recognized = true;
94 if (replace) {
95 return tmp.replace(0, rx.matchedLength(), newPrefix + QLatin1String(" "));
96 }
97 }
98 } else {
99 qWarning() << "bigRegExp = \""
100 << bigRegExp << "\"\n"
101 << "prefix regexp is invalid!";
102 // try good ole Re/Fwd:
103 recognized = str.startsWith(newPrefix);
104 }
105
106 if (!recognized) {
107 return newPrefix + QLatin1String(" ") + str;
108 } else {
109 return str;
110 }
111}
112
113QString cleanSubject(const KMime::Message::Ptr &msg, const QStringList &prefixRegExps, bool replace, const QString &newPrefix)
114{
115 return replacePrefixes(msg->subject()->asUnicodeString(), prefixRegExps, replace, newPrefix);
116}
117
118QString forwardSubject(const KMime::Message::Ptr &msg)
119{
120 bool replaceForwardPrefix = true;
121 QStringList forwardPrefixes;
122 forwardPrefixes << "Fwd:";
123 forwardPrefixes << "FW:";
124 return cleanSubject(msg, forwardPrefixes, replaceForwardPrefix, QStringLiteral("Fwd:"));
125}
126
127QString replySubject(const KMime::Message::Ptr &msg)
128{
129 bool replaceReplyPrefix = true;
130 QStringList replyPrefixes;
131 //We're escaping the regex escape sequences. awesome
132 replyPrefixes << "Re\\\\s*:";
133 replyPrefixes << "Re[\\\\d+\\\\]:";
134 replyPrefixes << "Re\\\\d+:";
135 return cleanSubject(msg, replyPrefixes, replaceReplyPrefix, QStringLiteral("Re:"));
136}
137
138QByteArray getRefStr(const KMime::Message::Ptr &msg)
139{
140 QByteArray firstRef, lastRef, refStr, retRefStr;
141 int i, j;
142
143 if (auto hdr = msg->references(false)) {
144 refStr = hdr->as7BitString(false).trimmed();
145 }
146
147 if (refStr.isEmpty()) {
148 return msg->messageID()->as7BitString(false);
149 }
150
151 i = refStr.indexOf('<');
152 j = refStr.indexOf('>');
153 firstRef = refStr.mid(i, j - i + 1);
154 if (!firstRef.isEmpty()) {
155 retRefStr = firstRef + ' ';
156 }
157
158 i = refStr.lastIndexOf('<');
159 j = refStr.lastIndexOf('>');
160
161 lastRef = refStr.mid(i, j - i + 1);
162 if (!lastRef.isEmpty() && lastRef != firstRef) {
163 retRefStr += lastRef + ' ';
164 }
165
166 retRefStr += msg->messageID()->as7BitString(false);
167 return retRefStr;
168}
169
170KMime::Content *createPlainPartContent(const KMime::Message::Ptr &msg, const QString &plainBody)
171{
172 KMime::Content *textPart = new KMime::Content(msg.data());
173 textPart->contentType()->setMimeType("text/plain");
174 //FIXME This is supposed to select a charset out of the available charsets that contains all necessary characters to render the text
175 // QTextCodec *charset = selectCharset(m_charsets, plainBody);
176 // textPart->contentType()->setCharset(charset->name());
177 textPart->contentType()->setCharset("utf-8");
178 textPart->contentTransferEncoding()->setEncoding(KMime::Headers::CE8Bit);
179 textPart->fromUnicodeString(plainBody);
180 return textPart;
181}
182
183KMime::Content *createMultipartAlternativeContent(const KMime::Message::Ptr &msg, const QString &plainBody, const QString &htmlBody)
184{
185 KMime::Content *multipartAlternative = new KMime::Content(msg.data());
186 multipartAlternative->contentType()->setMimeType("multipart/alternative");
187 const QByteArray boundary = KMime::multiPartBoundary();
188 multipartAlternative->contentType()->setBoundary(boundary);
189
190 KMime::Content *textPart = createPlainPartContent(msg, plainBody);
191 multipartAlternative->addContent(textPart);
192
193 KMime::Content *htmlPart = new KMime::Content(msg.data());
194 htmlPart->contentType()->setMimeType("text/html");
195 //FIXME This is supposed to select a charset out of the available charsets that contains all necessary characters to render the text
196 // QTextCodec *charset = selectCharset(m_charsets, htmlBody);
197 // htmlPart->contentType()->setCharset(charset->name());
198 textPart->contentType()->setCharset("utf-8");
199 htmlPart->contentTransferEncoding()->setEncoding(KMime::Headers::CE8Bit);
200 htmlPart->fromUnicodeString(htmlBody);
201 multipartAlternative->addContent(htmlPart);
202
203 return multipartAlternative;
204}
205
206void addProcessedBodyToMessage(const KMime::Message::Ptr &msg, const QString &plainBody, const QString &htmlBody, bool forward)
207{
208 //FIXME
209 // MessageCore::ImageCollector ic;
210 // ic.collectImagesFrom(mOrigMsg.data());
211
212 // Now, delete the old content and set the new content, which
213 // is either only the new text or the new text with some attachments.
214 auto parts = msg->contents();
215 foreach (KMime::Content *content, parts) {
216 msg->removeContent(content, true/*delete*/);
217 }
218
219 msg->contentType()->clear(); // to get rid of old boundary
220
221 const QByteArray boundary = KMime::multiPartBoundary();
222 KMime::Content *const mainTextPart =
223 htmlBody.isEmpty() ?
224 createPlainPartContent(msg, plainBody) :
225 createMultipartAlternativeContent(msg, plainBody, htmlBody);
226 mainTextPart->assemble();
227
228 KMime::Content *textPart = mainTextPart;
229 // if (!ic.images().empty()) {
230 // textPart = createMultipartRelated(ic, mainTextPart);
231 // textPart->assemble();
232 // }
233
234 // If we have some attachments, create a multipart/mixed mail and
235 // add the normal body as well as the attachments
236 KMime::Content *mainPart = textPart;
237 //FIXME
238 // if (forward) {
239 // auto attachments = mOrigMsg->attachments();
240 // attachments += mOtp->nodeHelper()->attachmentsOfExtraContents();
241 // if (!attachments.isEmpty()) {
242 // mainPart = createMultipartMixed(attachments, textPart);
243 // mainPart->assemble();
244 // }
245 // }
246
247 msg->setBody(mainPart->encodedBody());
248 msg->setHeader(mainPart->contentType());
249 msg->setHeader(mainPart->contentTransferEncoding());
250 msg->assemble();
251 msg->parse();
252}
253
254QString plainToHtml(const QString &body)
255{
256 QString str = body;
257 str = str.toHtmlEscaped();
258 str.replace(QStringLiteral("\n"), QStringLiteral("<br />\n"));
259 return str;
260}
261
262//TODO implement this function using a DOM tree parser
263void makeValidHtml(QString &body, const QString &headElement)
264{
265 QRegExp regEx;
266 regEx.setMinimal(true);
267 regEx.setPattern(QStringLiteral("<html.*>"));
268
269 if (!body.isEmpty() && !body.contains(regEx)) {
270 regEx.setPattern(QStringLiteral("<body.*>"));
271 if (!body.contains(regEx)) {
272 body = QLatin1String("<body>") + body + QLatin1String("<br/></body>");
273 }
274 regEx.setPattern(QStringLiteral("<head.*>"));
275 if (!body.contains(regEx)) {
276 body = QLatin1String("<head>") + headElement + QLatin1String("</head>") + body;
277 }
278 body = QLatin1String("<html>") + body + QLatin1String("</html>");
279 }
280}
281
282QString stripSignature(const QString &msg)
283{
284 // Following RFC 3676, only > before --
285 // I prefer to not delete a SB instead of delete good mail content.
286 const QRegExp sbDelimiterSearch = QRegExp(QLatin1String("(^|\n)[> ]*-- \n"));
287 // The regular expression to look for prefix change
288 const QRegExp commonReplySearch = QRegExp(QLatin1String("^[ ]*>"));
289
290 QString res = msg;
291 int posDeletingStart = 1; // to start looking at 0
292
293 // While there are SB delimiters (start looking just before the deleted SB)
294 while ((posDeletingStart = res.indexOf(sbDelimiterSearch, posDeletingStart - 1)) >= 0) {
295 QString prefix; // the current prefix
296 QString line; // the line to check if is part of the SB
297 int posNewLine = -1;
298
299 // Look for the SB beginning
300 int posSignatureBlock = res.indexOf(QLatin1Char('-'), posDeletingStart);
301 // The prefix before "-- "$
302 if (res.at(posDeletingStart) == QLatin1Char('\n')) {
303 ++posDeletingStart;
304 }
305
306 prefix = res.mid(posDeletingStart, posSignatureBlock - posDeletingStart);
307 posNewLine = res.indexOf(QLatin1Char('\n'), posSignatureBlock) + 1;
308
309 // now go to the end of the SB
310 while (posNewLine < res.size() && posNewLine > 0) {
311 // handle the undefined case for mid ( x , -n ) where n>1
312 int nextPosNewLine = res.indexOf(QLatin1Char('\n'), posNewLine);
313
314 if (nextPosNewLine < 0) {
315 nextPosNewLine = posNewLine - 1;
316 }
317
318 line = res.mid(posNewLine, nextPosNewLine - posNewLine);
319
320 // check when the SB ends:
321 // * does not starts with prefix or
322 // * starts with prefix+(any substring of prefix)
323 if ((prefix.isEmpty() && line.indexOf(commonReplySearch) < 0) ||
324 (!prefix.isEmpty() && line.startsWith(prefix) &&
325 line.mid(prefix.size()).indexOf(commonReplySearch) < 0)) {
326 posNewLine = res.indexOf(QLatin1Char('\n'), posNewLine) + 1;
327 } else {
328 break; // end of the SB
329 }
330 }
331
332 // remove the SB or truncate when is the last SB
333 if (posNewLine > 0) {
334 res.remove(posDeletingStart, posNewLine - posDeletingStart);
335 } else {
336 res.truncate(posDeletingStart);
337 }
338 }
339
340 return res;
341}
342
343QString plainMessageText(MimeTreeParser::ObjectTreeParser &otp, bool aStripSignature)
344{
345 QString result = otp.plainTextContent();
346 if (result.isEmpty()) { //HTML-only mails
347 QWebPage doc;
348 doc.mainFrame()->setHtml(otp.htmlContent());
349 result = doc.mainFrame()->toPlainText();
350 }
351
352 if (aStripSignature) {
353 result = stripSignature(result);
354 }
355
356 return result;
357}
358
359QString htmlMessageText(MimeTreeParser::ObjectTreeParser &otp, bool aStripSignature, QString &headElement)
360{
361 QString htmlElement = otp.htmlContent();
362
363 if (htmlElement.isEmpty()) { //plain mails only
364 QString htmlReplace = otp.plainTextContent().toHtmlEscaped();
365 htmlReplace = htmlReplace.replace(QStringLiteral("\n"), QStringLiteral("<br />"));
366 htmlElement = QStringLiteral("<html><head></head><body>%1</body></html>\n").arg(htmlReplace);
367 }
368
369 //QWebPage relies on this
370 Q_ASSERT(QApplication::style());
371 QWebPage page;
372 page.settings()->setAttribute(QWebSettings::JavascriptEnabled, false);
373 page.settings()->setAttribute(QWebSettings::JavaEnabled, false);
374 page.settings()->setAttribute(QWebSettings::PluginsEnabled, false);
375 page.settings()->setAttribute(QWebSettings::AutoLoadImages, false);
376
377 page.currentFrame()->setHtml(htmlElement);
378
379 //TODO to be tested/verified if this is not an issue
380 page.settings()->setAttribute(QWebSettings::JavascriptEnabled, true);
381 const QString bodyElement = page.currentFrame()->evaluateJavaScript(
382 QStringLiteral("document.getElementsByTagName('body')[0].innerHTML")).toString();
383
384 headElement = page.currentFrame()->evaluateJavaScript(
385 QStringLiteral("document.getElementsByTagName('head')[0].innerHTML")).toString();
386
387 page.settings()->setAttribute(QWebSettings::JavascriptEnabled, false);
388
389 if (!bodyElement.isEmpty()) {
390 if (aStripSignature) {
391 //FIXME strip signature works partially for HTML mails
392 return stripSignature(bodyElement);
393 }
394 return bodyElement;
395 }
396
397 if (aStripSignature) {
398 //FIXME strip signature works partially for HTML mails
399 return stripSignature(htmlElement);
400 }
401 return htmlElement;
402}
403
404QString formatQuotePrefix(const QString &wildString, const QString &fromDisplayString)
405{
406 QString result;
407
408 if (wildString.isEmpty()) {
409 return wildString;
410 }
411
412 unsigned int strLength(wildString.length());
413 for (uint i = 0; i < strLength;) {
414 QChar ch = wildString[i++];
415 if (ch == QLatin1Char('%') && i < strLength) {
416 ch = wildString[i++];
417 switch (ch.toLatin1()) {
418 case 'f': { // sender's initals
419 if (fromDisplayString.isEmpty()) {
420 break;
421 }
422
423 uint j = 0;
424 const unsigned int strLength(fromDisplayString.length());
425 for (; j < strLength && fromDisplayString[j] > QLatin1Char(' '); ++j)
426 ;
427 for (; j < strLength && fromDisplayString[j] <= QLatin1Char(' '); ++j)
428 ;
429 result += fromDisplayString[0];
430 if (j < strLength && fromDisplayString[j] > QLatin1Char(' ')) {
431 result += fromDisplayString[j];
432 } else if (strLength > 1) {
433 if (fromDisplayString[1] > QLatin1Char(' ')) {
434 result += fromDisplayString[1];
435 }
436 }
437 }
438 break;
439 case '_':
440 result += QLatin1Char(' ');
441 break;
442 case '%':
443 result += QLatin1Char('%');
444 break;
445 default:
446 result += QLatin1Char('%');
447 result += ch;
448 break;
449 }
450 } else {
451 result += ch;
452 }
453 }
454 return result;
455}
456
457QString quotedPlainText(const QString &selection, const QString &fromDisplayString)
458{
459 QString content = selection;
460 // Remove blank lines at the beginning:
461 const int firstNonWS = content.indexOf(QRegExp(QLatin1String("\\S")));
462 const int lineStart = content.lastIndexOf(QLatin1Char('\n'), firstNonWS);
463 if (lineStart >= 0) {
464 content.remove(0, static_cast<unsigned int>(lineStart));
465 }
466
467 const auto quoteString = QStringLiteral("> ");
468 const QString indentStr = formatQuotePrefix(quoteString, fromDisplayString);
469 //FIXME
470 // if (TemplateParserSettings::self()->smartQuote() && mWrap) {
471 // content = MessageCore::StringUtil::smartQuote(content, mColWrap - indentStr.length());
472 // }
473 content.replace(QLatin1Char('\n'), QLatin1Char('\n') + indentStr);
474 content.prepend(indentStr);
475 content += QLatin1Char('\n');
476
477 return content;
478}
479
480QString quotedHtmlText(const QString &selection)
481{
482 QString content = selection;
483 //TODO 1) look for all the variations of <br> and remove the blank lines
484 //2) implement vertical bar for quoted HTML mail.
485 //3) After vertical bar is implemented, If a user wants to edit quoted message,
486 // then the <blockquote> tags below should open and close as when required.
487
488 //Add blockquote tag, so that quoted message can be differentiated from normal message
489 content = QLatin1String("<blockquote>") + content + QLatin1String("</blockquote>");
490 return content;
491}
492
493void applyCharset(const KMime::Message::Ptr msg, const KMime::Message::Ptr &origMsg)
494{
495 // first convert the body from its current encoding to unicode representation
496 QTextCodec *bodyCodec = KCharsets::charsets()->codecForName(QString::fromLatin1(msg->contentType()->charset()));
497 if (!bodyCodec) {
498 bodyCodec = KCharsets::charsets()->codecForName(QStringLiteral("UTF-8"));
499 }
500
501 const QString body = bodyCodec->toUnicode(msg->body());
502
503 // then apply the encoding of the original message
504 msg->contentType()->setCharset(origMsg->contentType()->charset());
505
506 QTextCodec *codec = KCharsets::charsets()->codecForName(QString::fromLatin1(msg->contentType()->charset()));
507 if (!codec) {
508 qCritical() << "Could not get text codec for charset" << msg->contentType()->charset();
509 } else if (!codec->canEncode(body)) { // charset can't encode body, fall back to preferred
510 const QStringList charsets /*= preferredCharsets() */;
511
512 QList<QByteArray> chars;
513 chars.reserve(charsets.count());
514 foreach (const QString &charset, charsets) {
515 chars << charset.toLatin1();
516 }
517
518 //FIXME
519 QByteArray fallbackCharset/* = selectCharset(chars, body)*/;
520 if (fallbackCharset.isEmpty()) { // UTF-8 as fall-through
521 fallbackCharset = "UTF-8";
522 }
523
524 codec = KCharsets::charsets()->codecForName(QString::fromLatin1(fallbackCharset));
525 msg->setBody(codec->fromUnicode(body));
526 } else {
527 msg->setBody(codec->fromUnicode(body));
528 }
529}
530
531enum ReplyStrategy {
532 ReplyList,
533 ReplySmart,
534 ReplyAll,
535 ReplyAuthor,
536 ReplyNone
537};
538
539KMime::Message::Ptr MailTemplates::reply(const KMime::Message::Ptr &origMsg)
540{
541 //FIXME
542 const bool alwaysPlain = true;
543 //FIXME
544 const ReplyStrategy replyStrategy = ReplySmart;
545 KMime::Message::Ptr msg(new KMime::Message);
546 //FIXME
547 //Personal email addresses
548 KMime::Types::AddrSpecList me;
549 KMime::Types::Mailbox::List toList;
550 KMime::Types::Mailbox::List replyToList;
551 KMime::Types::Mailbox::List mailingListAddresses;
552
553 // const uint originalIdentity = identityUoid(origMsg);
554 initHeader(msg);
555 replyToList = origMsg->replyTo()->mailboxes();
556
557 msg->contentType()->setCharset("utf-8");
558
559 if (origMsg->headerByType("List-Post") &&
560 origMsg->headerByType("List-Post")->asUnicodeString().contains(QStringLiteral("mailto:"), Qt::CaseInsensitive)) {
561
562 const QString listPost = origMsg->headerByType("List-Post")->asUnicodeString();
563 QRegExp rx(QStringLiteral("<mailto:([^@>]+)@([^>]+)>"), Qt::CaseInsensitive);
564 if (rx.indexIn(listPost, 0) != -1) { // matched
565 KMime::Types::Mailbox mailbox;
566 mailbox.fromUnicodeString(rx.cap(1) + QLatin1Char('@') + rx.cap(2));
567 mailingListAddresses << mailbox;
568 }
569 }
570
571 switch (replyStrategy) {
572 case ReplySmart: {
573 if (auto hdr = origMsg->headerByType("Mail-Followup-To")) {
574 toList << KMime::Types::Mailbox::listFrom7BitString(hdr->as7BitString(false));
575 } else if (!replyToList.isEmpty()) {
576 toList = replyToList;
577 } else if (!mailingListAddresses.isEmpty()) {
578 toList = (KMime::Types::Mailbox::List() << mailingListAddresses.at(0));
579 } else {
580 // doesn't seem to be a mailing list, reply to From: address
581 toList = origMsg->from()->mailboxes();
582
583 bool listContainsMe = false;
584 for (const auto &m : me) {
585 KMime::Types::Mailbox mailbox;
586 mailbox.setAddress(m);
587 if (toList.contains(mailbox)) {
588 listContainsMe = true;
589 }
590 }
591 if (listContainsMe) {
592 // sender seems to be one of our own identities, so we assume that this
593 // is a reply to a "sent" mail where the users wants to add additional
594 // information for the recipient.
595 toList = origMsg->to()->mailboxes();
596 }
597 }
598 // strip all my addresses from the list of recipients
599 const KMime::Types::Mailbox::List recipients = toList;
600
601 toList = stripMyAddressesFromAddressList(recipients, me);
602
603 // ... unless the list contains only my addresses (reply to self)
604 if (toList.isEmpty() && !recipients.isEmpty()) {
605 toList << recipients.first();
606 }
607 }
608 break;
609 case ReplyList: {
610 if (auto hdr = origMsg->headerByType("Mail-Followup-To")) {
611 KMime::Types::Mailbox mailbox;
612 mailbox.from7BitString(hdr->as7BitString(false));
613 toList << mailbox;
614 } else if (!mailingListAddresses.isEmpty()) {
615 toList << mailingListAddresses[ 0 ];
616 } else if (!replyToList.isEmpty()) {
617 // assume a Reply-To header mangling mailing list
618 toList = replyToList;
619 }
620
621 //FIXME
622 // strip all my addresses from the list of recipients
623 const KMime::Types::Mailbox::List recipients = toList;
624 toList = stripMyAddressesFromAddressList(recipients, me);
625 }
626 break;
627 case ReplyAll: {
628 KMime::Types::Mailbox::List recipients;
629 KMime::Types::Mailbox::List ccRecipients;
630
631 // add addresses from the Reply-To header to the list of recipients
632 if (!replyToList.isEmpty()) {
633 recipients = replyToList;
634
635 // strip all possible mailing list addresses from the list of Reply-To addresses
636 foreach (const KMime::Types::Mailbox &mailbox, mailingListAddresses) {
637 foreach (const KMime::Types::Mailbox &recipient, recipients) {
638 if (mailbox == recipient) {
639 recipients.removeAll(recipient);
640 }
641 }
642 }
643 }
644
645 if (!mailingListAddresses.isEmpty()) {
646 // this is a mailing list message
647 if (recipients.isEmpty() && !origMsg->from()->asUnicodeString().isEmpty()) {
648 // The sender didn't set a Reply-to address, so we add the From
649 // address to the list of CC recipients.
650 ccRecipients += origMsg->from()->mailboxes();
651 qDebug() << "Added" << origMsg->from()->asUnicodeString() << "to the list of CC recipients";
652 }
653
654 // if it is a mailing list, add the posting address
655 recipients.prepend(mailingListAddresses[ 0 ]);
656 } else {
657 // this is a normal message
658 if (recipients.isEmpty() && !origMsg->from()->asUnicodeString().isEmpty()) {
659 // in case of replying to a normal message only then add the From
660 // address to the list of recipients if there was no Reply-to address
661 recipients += origMsg->from()->mailboxes();
662 qDebug() << "Added" << origMsg->from()->asUnicodeString() << "to the list of recipients";
663 }
664 }
665
666 // strip all my addresses from the list of recipients
667 toList = stripMyAddressesFromAddressList(recipients, me);
668
669 // merge To header and CC header into a list of CC recipients
670 if (!origMsg->cc()->asUnicodeString().isEmpty() || !origMsg->to()->asUnicodeString().isEmpty()) {
671 KMime::Types::Mailbox::List list;
672 if (!origMsg->to()->asUnicodeString().isEmpty()) {
673 list += origMsg->to()->mailboxes();
674 }
675 if (!origMsg->cc()->asUnicodeString().isEmpty()) {
676 list += origMsg->cc()->mailboxes();
677 }
678
679 foreach (const KMime::Types::Mailbox &mailbox, list) {
680 if (!recipients.contains(mailbox) &&
681 !ccRecipients.contains(mailbox)) {
682 ccRecipients += mailbox;
683 qDebug() << "Added" << mailbox.prettyAddress() << "to the list of CC recipients";
684 }
685 }
686 }
687
688 if (!ccRecipients.isEmpty()) {
689 // strip all my addresses from the list of CC recipients
690 ccRecipients = stripMyAddressesFromAddressList(ccRecipients, me);
691
692 // in case of a reply to self, toList might be empty. if that's the case
693 // then propagate a cc recipient to To: (if there is any).
694 if (toList.isEmpty() && !ccRecipients.isEmpty()) {
695 toList << ccRecipients.at(0);
696 ccRecipients.pop_front();
697 }
698
699 foreach (const KMime::Types::Mailbox &mailbox, ccRecipients) {
700 msg->cc()->addAddress(mailbox);
701 }
702 }
703
704 if (toList.isEmpty() && !recipients.isEmpty()) {
705 // reply to self without other recipients
706 toList << recipients.at(0);
707 }
708 }
709 break;
710 case ReplyAuthor: {
711 if (!replyToList.isEmpty()) {
712 KMime::Types::Mailbox::List recipients = replyToList;
713
714 // strip the mailing list post address from the list of Reply-To
715 // addresses since we want to reply in private
716 foreach (const KMime::Types::Mailbox &mailbox, mailingListAddresses) {
717 foreach (const KMime::Types::Mailbox &recipient, recipients) {
718 if (mailbox == recipient) {
719 recipients.removeAll(recipient);
720 }
721 }
722 }
723
724 if (!recipients.isEmpty()) {
725 toList = recipients;
726 } else {
727 // there was only the mailing list post address in the Reply-To header,
728 // so use the From address instead
729 toList = origMsg->from()->mailboxes();
730 }
731 } else if (!origMsg->from()->asUnicodeString().isEmpty()) {
732 toList = origMsg->from()->mailboxes();
733 }
734 }
735 break;
736 case ReplyNone:
737 // the addressees will be set by the caller
738 break;
739 }
740
741 foreach (const KMime::Types::Mailbox &mailbox, toList) {
742 msg->to()->addAddress(mailbox);
743 }
744
745 const QByteArray refStr = getRefStr(origMsg);
746 if (!refStr.isEmpty()) {
747 msg->references()->fromUnicodeString(QString::fromLocal8Bit(refStr), "utf-8");
748 }
749
750 //In-Reply-To = original msg-id
751 msg->inReplyTo()->from7BitString(origMsg->messageID()->as7BitString(false));
752
753 msg->subject()->fromUnicodeString(replySubject(origMsg), "utf-8");
754
755 auto definedLocale = QLocale::system();
756
757 //TODO set empty source instead
758 StringHtmlWriter htmlWriter;
759 MimeTreeParser::NodeHelper nodeHelper;
760 ObjectTreeSource source(&htmlWriter);
761 MimeTreeParser::ObjectTreeParser otp(&source, &nodeHelper);
762 otp.setAllowAsync(false);
763 otp.parseObjectTree(origMsg.data());
764
765 //Add quoted body
766 QString plainBody;
767 QString htmlBody;
768
769 //On $datetime you wrote:
770 const QDateTime date = origMsg->date()->dateTime();
771 const auto dateTimeString = QString("%1 %2").arg(definedLocale.toString(date.date(), QLocale::LongFormat)).arg(definedLocale.toString(date.time(), QLocale::LongFormat));
772 const auto onDateYouWroteLine = QString("On %1 you wrote:").arg(dateTimeString);
773 plainBody.append(onDateYouWroteLine);
774 htmlBody.append(plainToHtml(onDateYouWroteLine));
775
776 //Strip signature for replies
777 const bool stripSignature = true;
778
779 //Quoted body
780 QString plainQuote = quotedPlainText(plainMessageText(otp, stripSignature), origMsg->from()->displayString());
781 if (plainQuote.endsWith(QLatin1Char('\n'))) {
782 plainQuote.chop(1);
783 }
784 plainBody.append(plainQuote);
785 QString headElement;
786 htmlBody.append(quotedHtmlText(htmlMessageText(otp, stripSignature, headElement)));
787
788 if (alwaysPlain) {
789 htmlBody.clear();
790 } else {
791 makeValidHtml(htmlBody, headElement);
792 }
793
794 addProcessedBodyToMessage(msg, plainBody, htmlBody, false);
795
796 applyCharset(msg, origMsg);
797
798 msg->assemble();
799
800 return msg;
801}
diff --git a/framework/src/domain/mailtemplates.h b/framework/src/domain/mailtemplates.h
new file mode 100644
index 00000000..6519122a
--- /dev/null
+++ b/framework/src/domain/mailtemplates.h
@@ -0,0 +1,28 @@
1/*
2 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsys.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19
20#pragma once
21
22#include <QByteArray>
23#include <KMime/Message>
24
25namespace MailTemplates
26{
27 KMime::Message::Ptr reply(const KMime::Message::Ptr &message);
28};
diff --git a/framework/src/domain/messageparser.cpp b/framework/src/domain/messageparser.cpp
new file mode 100644
index 00000000..ea2ecbf1
--- /dev/null
+++ b/framework/src/domain/messageparser.cpp
@@ -0,0 +1,114 @@
1/*
2 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsys.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19#include "messageparser.h"
20
21#include "modeltest.h"
22#include "stringhtmlwriter.h"
23#include "objecttreesource.h"
24
25#include "mimetreeparser/interface.h"
26
27#include <QRegExp>
28
29#include <QFile>
30#include <QImage>
31#include <QDebug>
32#include <QTime>
33#include <QUrl>
34#include <MimeTreeParser/ObjectTreeParser>
35#include <MimeTreeParser/MessagePart>
36
37
38class MessagePartPrivate
39{
40public:
41 QSharedPointer<MimeTreeParser::MessagePart> mPartTree;
42 QString mHtml;
43 QMap<QByteArray, QUrl> mEmbeddedPartMap;
44 std::shared_ptr<MimeTreeParser::NodeHelper> mNodeHelper;
45 std::shared_ptr<Parser> mParser;
46};
47
48MessageParser::MessageParser(QObject *parent)
49 : QObject(parent)
50 , d(std::unique_ptr<MessagePartPrivate>(new MessagePartPrivate))
51{
52
53}
54
55MessageParser::~MessageParser()
56{
57
58}
59
60QString MessageParser::html() const
61{
62 return d->mHtml;
63}
64
65QVariant MessageParser::message() const
66{
67 return QVariant();
68}
69
70void MessageParser::setMessage(const QVariant &message)
71{
72 // QTime time;
73 // time.start();
74 d->mParser = std::shared_ptr<Parser>(new Parser(message.toByteArray()));
75
76 const auto mailData = KMime::CRLFtoLF(message.toByteArray());
77 KMime::Message::Ptr msg(new KMime::Message);
78 msg->setContent(mailData);
79 msg->parse();
80 // qWarning() << "parsed: " << time.elapsed();
81
82 // render the mail
83 StringHtmlWriter htmlWriter;
84 //temporary files only have the lifetime of the nodehelper, so we keep it around until the mail changes.
85 d->mNodeHelper = std::make_shared<MimeTreeParser::NodeHelper>();
86 ObjectTreeSource source(&htmlWriter);
87 MimeTreeParser::ObjectTreeParser otp(&source, d->mNodeHelper.get());
88
89 otp.parseObjectTree(msg.data());
90 d->mPartTree = otp.parsedPart().dynamicCast<MimeTreeParser::MessagePart>();
91
92 d->mEmbeddedPartMap = htmlWriter.embeddedParts();
93 d->mHtml = htmlWriter.html();
94 emit htmlChanged();
95}
96
97QAbstractItemModel *MessageParser::partTree() const
98{
99 return new PartModel(d->mPartTree, d->mParser);
100}
101
102QAbstractItemModel *MessageParser::newTree() const
103{
104 const auto model = new NewModel(d->mParser);
105 // new ModelTest(model, model);
106 return model;
107}
108
109QAbstractItemModel *MessageParser::attachments() const
110{
111 const auto model = new AttachmentModel(d->mParser);
112 // new ModelTest(model, model);
113 return model;
114}
diff --git a/framework/src/domain/messageparser.h b/framework/src/domain/messageparser.h
new file mode 100644
index 00000000..aeeed93c
--- /dev/null
+++ b/framework/src/domain/messageparser.h
@@ -0,0 +1,157 @@
1/*
2 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsys.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19
20#pragma once
21
22#include <QObject>
23#include <QString>
24#include <QStringList>
25
26#include <QAbstractItemModel>
27#include <QModelIndex>
28
29#include <memory>
30#include <MimeTreeParser/MessagePart>
31
32class QAbstractItemModel;
33
34class Parser;
35class Part;
36class Encryption;
37class Signature;
38typedef std::shared_ptr<Part> PartPtr;
39class Content;
40typedef std::shared_ptr<Content> ContentPtr;
41class MessagePartPrivate;
42
43class NewModelPrivate;
44class AttachmentModelPrivate;
45
46class MessageParser : public QObject
47{
48 Q_OBJECT
49 Q_PROPERTY (QVariant message READ message WRITE setMessage)
50 Q_PROPERTY (QString html READ html NOTIFY htmlChanged)
51 Q_PROPERTY (QAbstractItemModel* partTree READ partTree NOTIFY htmlChanged)
52 Q_PROPERTY (QAbstractItemModel* newTree READ newTree NOTIFY htmlChanged)
53 Q_PROPERTY (QAbstractItemModel* attachments READ attachments NOTIFY htmlChanged)
54
55public:
56 explicit MessageParser(QObject *parent = Q_NULLPTR);
57 ~MessageParser();
58
59 QString html() const;
60
61 QVariant message() const;
62 void setMessage(const QVariant &to);
63 QAbstractItemModel *partTree() const;
64 QAbstractItemModel *newTree() const;
65 QAbstractItemModel *attachments() const;
66
67signals:
68 void htmlChanged();
69
70private:
71 std::unique_ptr<MessagePartPrivate> d;
72};
73
74class PartModel : public QAbstractItemModel {
75 Q_OBJECT
76public:
77 PartModel(QSharedPointer<MimeTreeParser::MessagePart> partTree, std::shared_ptr<Parser> parser);
78
79public:
80 enum Roles {
81 Text = Qt::UserRole + 1,
82 IsHtml,
83 IsEncrypted,
84 IsAttachment,
85 HasContent,
86 Type,
87 IsHidden
88 };
89
90 QHash<int, QByteArray> roleNames() const Q_DECL_OVERRIDE;
91 QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE;
92 QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE;
93 QModelIndex parent(const QModelIndex &index) const Q_DECL_OVERRIDE;
94 int rowCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE;
95 int columnCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE;
96
97private:
98 QSharedPointer<MimeTreeParser::MessagePart> mPartTree;
99 QMap<QByteArray, QUrl> mEmbeddedPartMap;
100 std::shared_ptr<Parser> mParser;
101};
102
103
104class NewModel : public QAbstractItemModel {
105 Q_OBJECT
106public:
107 NewModel(std::shared_ptr<Parser> parser);
108 ~NewModel();
109
110public:
111 enum Roles {
112 TypeRole = Qt::UserRole + 1,
113 ContentsRole,
114 ContentRole,
115 IsEmbededRole,
116 SecurityLevelRole,
117 EncryptionErrorType,
118 EncryptionErrorString
119 };
120
121 QHash<int, QByteArray> roleNames() const Q_DECL_OVERRIDE;
122 QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE;
123 QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE;
124 QModelIndex parent(const QModelIndex &index) const Q_DECL_OVERRIDE;
125 int rowCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE;
126 int columnCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE;
127
128private:
129 std::unique_ptr<NewModelPrivate> d;
130};
131
132class AttachmentModel : public QAbstractItemModel {
133 Q_OBJECT
134public:
135 AttachmentModel(std::shared_ptr<Parser> parser);
136 ~AttachmentModel();
137
138public:
139 enum Roles {
140 TypeRole = Qt::UserRole + 1,
141 IconRole,
142 NameRole,
143 SizeRole,
144 IsEncryptedRole,
145 IsSignedRole
146 };
147
148 QHash<int, QByteArray> roleNames() const Q_DECL_OVERRIDE;
149 QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE;
150 QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE;
151 QModelIndex parent(const QModelIndex &index) const Q_DECL_OVERRIDE;
152 int rowCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE;
153 int columnCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE;
154
155private:
156 std::unique_ptr<AttachmentModelPrivate> d;
157};
diff --git a/framework/src/domain/messageparser_new.cpp b/framework/src/domain/messageparser_new.cpp
new file mode 100644
index 00000000..cb399523
--- /dev/null
+++ b/framework/src/domain/messageparser_new.cpp
@@ -0,0 +1,486 @@
1/*
2 Copyright (c) 2016 Sandro Knauß <knauss@kolabsys.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19
20#include "messageparser.h"
21#include "mimetreeparser/interface.h"
22
23#include <QDebug>
24
25Q_DECLARE_METATYPE(Part *)
26Q_DECLARE_METATYPE(Content *)
27Q_DECLARE_METATYPE(Signature *)
28Q_DECLARE_METATYPE(Encryption *)
29
30class Entry;
31
32class NewModelPrivate
33{
34public:
35 NewModelPrivate(NewModel *q_ptr, const std::shared_ptr<Parser> &parser);
36 ~NewModelPrivate();
37
38 void createTree();
39
40 QSharedPointer<QVariant> getVar(const std::shared_ptr<Signature> &sig);
41 QSharedPointer<QVariant> getVar(const std::shared_ptr<Encryption> &enc);
42 QSharedPointer<QVariant> getVar(const std::shared_ptr<Part> &part);
43 QSharedPointer<QVariant> getVar(Part *part);
44 QSharedPointer<QVariant> getVar(const std::shared_ptr<Content> &content);
45 QSharedPointer<QVariant> getVar(Content *content);
46
47 int getPos(Signature *sig);
48 int getPos(Encryption *enc);
49 int getPos(Part *part);
50 int getPos(Content *content);
51
52 NewModel *q;
53 QVector<Part::Ptr> mParts;
54 std::unique_ptr<Entry> mRoot;
55
56 std::shared_ptr<Parser> mParser;
57private:
58 QMap<std::shared_ptr<Signature>, QSharedPointer<QVariant>> mSignatureMap;
59 QMap<std::shared_ptr<Encryption>, QSharedPointer<QVariant>> mEncryptionMap;
60 QMap<Part *, QSharedPointer<QVariant>> mPartMap;
61 QMap<Content *, QSharedPointer<QVariant>> mCMap;
62};
63
64class Entry
65{
66public:
67 Entry(NewModelPrivate *model)
68 : mParent(nullptr)
69 , mNewModelPrivate(model)
70 {
71 }
72
73 ~Entry()
74 {
75 foreach(auto child, mChildren) {
76 delete child;
77 }
78 mChildren.clear();
79 }
80
81 void addChild(Entry *entry)
82 {
83 mChildren.append(entry);
84 entry->mParent = this;
85 }
86
87 Entry *addSignatures(QVector<Signature::Ptr> signatures)
88 {
89 auto ret = this;
90 foreach(const auto &sig, signatures) {
91 auto entry = new Entry(mNewModelPrivate);
92 entry->mData = mNewModelPrivate->getVar(sig);
93 ret->addChild(entry);
94 ret = entry;
95 }
96 return ret;
97 }
98
99 Entry *addEncryptions(QVector<Encryption::Ptr> encryptions)
100 {
101 auto ret = this;
102 foreach(const auto &enc, encryptions) {
103 auto entry = new Entry(mNewModelPrivate);
104 entry->mData = mNewModelPrivate->getVar(enc);
105 ret->addChild(entry);
106 ret = entry;
107 }
108 return ret;
109 }
110
111 Entry *addPart(Part *part)
112 {
113 auto entry = new Entry(mNewModelPrivate);
114 entry->mData = mNewModelPrivate->getVar(part);
115 addChild(entry);
116
117 foreach(const auto &content, part->content()) {
118 auto _entry = entry;
119 _entry = _entry->addEncryptions(content->encryptions().mid(part->encryptions().size()));
120 _entry = _entry->addSignatures(content->signatures().mid(part->signatures().size()));
121 auto c = new Entry(mNewModelPrivate);
122 c->mData = mNewModelPrivate->getVar(content);
123 _entry->addChild(c);
124 }
125// foreach(const auto &content, part->availableContents()) {
126// foreach(const auto &contentPart, part->content(content)) {
127// auto _entry = entry;
128// _entry = _entry->addEncryptions(contentPart->encryptions().mid(part->encryptions().size()));
129// _entry = _entry->addSignatures(contentPart->signatures().mid(part->signatures().size()));
130// auto c = new Entry(mNewModelPrivate);
131// c->mData = mNewModelPrivate->getVar(contentPart);
132// _entry->addChild(c);
133// }
134// }
135 foreach(const auto &sp, part->subParts()) {
136 auto _entry = entry;
137 _entry = _entry->addEncryptions(sp->encryptions().mid(part->encryptions().size()));
138 _entry = _entry->addSignatures(sp->signatures().mid(part->signatures().size()));
139 _entry->addPart(sp.get());
140 }
141 return entry;
142 }
143
144 int pos()
145 {
146 if(!mParent) {
147 return -1;
148 }
149 int i=0;
150 foreach(const auto &child, mParent->mChildren) {
151 if (child == this) {
152 return i;
153 }
154 i++;
155 }
156 return -1;
157 }
158
159 QSharedPointer<QVariant> mData;
160
161 Entry *mParent;
162 QVector<Entry *> mChildren;
163 NewModelPrivate *mNewModelPrivate;
164};
165
166
167NewModelPrivate::NewModelPrivate(NewModel *q_ptr, const std::shared_ptr<Parser> &parser)
168 : q(q_ptr)
169 , mRoot(std::unique_ptr<Entry>(new Entry(this)))
170 , mParser(parser)
171{
172 mParts = mParser->collectContentParts();
173 createTree();
174}
175
176NewModelPrivate::~NewModelPrivate()
177{
178}
179
180void NewModelPrivate::createTree()
181{
182 auto root = mRoot.get();
183 auto parent = root;
184 Part *pPart = nullptr;
185 QVector<Signature::Ptr> signatures;
186 QVector<Encryption::Ptr> encryptions;
187 foreach(const auto part, mParts) {
188 auto _parent = parent;
189 if (pPart != part->parent()) {
190 auto _parent = root;
191 _parent = _parent->addEncryptions(part->parent()->encryptions());
192 _parent = _parent->addSignatures(part->parent()->signatures());
193 signatures = part->parent()->signatures();
194 encryptions = part->parent()->encryptions();
195 parent = _parent;
196 pPart = part->parent();
197 }
198 _parent = _parent->addEncryptions(part->encryptions().mid(encryptions.size()));
199 _parent = _parent->addSignatures(part->signatures().mid(signatures.size()));
200 _parent->addPart(part.get());
201 }
202}
203
204QSharedPointer<QVariant> NewModelPrivate::getVar(const std::shared_ptr<Signature> &sig)
205{
206 if (!mSignatureMap.contains(sig)) {
207 auto var = new QVariant();
208 var->setValue(sig.get());
209 mSignatureMap.insert(sig, QSharedPointer<QVariant>(var));
210 }
211 return mSignatureMap.value(sig);
212}
213
214QSharedPointer<QVariant> NewModelPrivate::getVar(const std::shared_ptr<Encryption> &enc)
215{
216 if (!mEncryptionMap.contains(enc)) {
217 auto var = new QVariant();
218 var->setValue(enc.get());
219 mEncryptionMap.insert(enc, QSharedPointer<QVariant>(var));
220 }
221 return mEncryptionMap.value(enc);
222}
223
224QSharedPointer<QVariant> NewModelPrivate::getVar(const std::shared_ptr<Part> &part)
225{
226 return getVar(part.get());
227}
228
229QSharedPointer<QVariant> NewModelPrivate::getVar(Part *part)
230{
231 if (!mPartMap.contains(part)) {
232 auto var = new QVariant();
233 var->setValue(part);
234 mPartMap.insert(part, QSharedPointer<QVariant>(var));
235 }
236 return mPartMap.value(part);
237}
238
239QSharedPointer<QVariant> NewModelPrivate::getVar(const std::shared_ptr<Content> &content)
240{
241 return getVar(content.get());
242}
243
244QSharedPointer<QVariant> NewModelPrivate::getVar(Content *content)
245{
246 if (!mCMap.contains(content)) {
247 auto var = new QVariant();
248 var->setValue(content);
249 mCMap.insert(content, QSharedPointer<QVariant>(var));
250 }
251 return mCMap.value(content);
252}
253
254int NewModelPrivate::getPos(Signature *signature)
255{
256 const auto first = mParts.first();
257 int i = 0;
258 foreach(const auto &sig, first->signatures()) {
259 if (sig.get() == signature) {
260 break;
261 }
262 i++;
263 }
264 return i;
265}
266
267int NewModelPrivate::getPos(Encryption *encryption)
268{
269 const auto first = mParts.first();
270 int i = 0;
271 foreach(const auto &enc, first->encryptions()) {
272 if (enc.get() == encryption) {
273 break;
274 }
275 i++;
276 }
277 return i;
278}
279
280int NewModelPrivate::getPos(Part *part)
281{
282 int i = 0;
283 foreach(const auto &p, mParts) {
284 if (p.get() == part) {
285 break;
286 }
287 i++;
288 }
289 return i;
290}
291
292int NewModelPrivate::getPos(Content *content)
293{
294 int i = 0;
295 foreach(const auto &c, content->parent()->content()) {
296 if (c.get() == content) {
297 break;
298 }
299 i++;
300 }
301 return i;
302}
303
304NewModel::NewModel(std::shared_ptr<Parser> parser)
305 : d(std::unique_ptr<NewModelPrivate>(new NewModelPrivate(this, parser)))
306{
307}
308
309NewModel::~NewModel()
310{
311}
312
313QHash<int, QByteArray> NewModel::roleNames() const
314{
315 QHash<int, QByteArray> roles;
316 roles[TypeRole] = "type";
317 roles[ContentRole] = "content";
318 roles[IsEmbededRole] = "embeded";
319 roles[SecurityLevelRole] = "securityLevel";
320 roles[EncryptionErrorType] = "errorType";
321 roles[EncryptionErrorString] = "errorString";
322 return roles;
323}
324
325QModelIndex NewModel::index(int row, int column, const QModelIndex &parent) const
326{
327 if (row < 0 || column != 0) {
328 return QModelIndex();
329 }
330 Entry *entry = d->mRoot.get();
331 if (parent.isValid()) {
332 entry = static_cast<Entry *>(parent.internalPointer());
333 }
334
335 if (row < entry->mChildren.size()) {
336 return createIndex(row, column, entry->mChildren.at(row));
337 }
338 return QModelIndex();
339}
340
341QVariant NewModel::data(const QModelIndex &index, int role) const
342{
343 if (!index.isValid()) {
344 switch (role) {
345 case Qt::DisplayRole:
346 return QString("root");
347 case IsEmbededRole:
348 return false;
349 }
350 return QVariant();
351 }
352
353 if (index.internalPointer()) {
354 const auto entry = static_cast<Entry *>(index.internalPointer());
355 const auto _data = entry->mData;
356 if (entry == d->mRoot.get()|| !_data) {
357 switch (role) {
358 case Qt::DisplayRole:
359 return QString("root");
360 case IsEmbededRole:
361 return false;
362 }
363 return QVariant();
364 }
365 if (_data->userType() == qMetaTypeId<Signature *>()) {
366 const auto signature = _data->value<Signature *>();
367 int i = d->getPos(signature);
368 switch(role) {
369 case Qt::DisplayRole:
370 return QStringLiteral("Signature%1").arg(i);
371 case TypeRole:
372 return QStringLiteral("Signature");
373 case SecurityLevelRole:
374 return QStringLiteral("RED");
375 case IsEmbededRole:
376 return data(index.parent(), IsEmbededRole);
377 }
378 } else if (_data->userType() == qMetaTypeId<Encryption *>()) {
379 const auto encryption = _data->value<Encryption *>();
380 int i = d->getPos(encryption);
381 switch(role) {
382 case Qt::DisplayRole:
383 return QStringLiteral("Encryption%1").arg(i);
384 case TypeRole:
385 return QStringLiteral("Encryption");
386 case SecurityLevelRole:
387 return QStringLiteral("GREEN");
388 case IsEmbededRole:
389 return data(index.parent(), IsEmbededRole);
390 case EncryptionErrorType:
391 {
392 switch(encryption->errorType()) {
393 case Encryption::NoError:
394 return QString();
395 case Encryption::PassphraseError:
396 return QStringLiteral("PassphraseError");
397 case Encryption::KeyMissing:
398 return QStringLiteral("KeyMissing");
399 default:
400 return QStringLiteral("UnknownError");
401 }
402 }
403 case EncryptionErrorString:
404 return encryption->errorString();
405 }
406 } else if (_data->userType() == qMetaTypeId<Part *>()) {
407 const auto part = _data->value<Part *>();
408 switch (role) {
409 case Qt::DisplayRole:
410 case TypeRole:
411 return QString::fromLatin1(part->type());
412 case IsEmbededRole:
413 return data(index.parent(), IsEmbededRole);
414 }
415 } else if (_data->userType() == qMetaTypeId<Content *>()) {
416 const auto content = _data->value<Content *>();
417 int i = d->getPos(content);
418 switch(role) {
419 case Qt::DisplayRole:
420 return QStringLiteral("Content%1").arg(i);
421 case TypeRole:
422 return QString::fromLatin1(content->type());
423 case IsEmbededRole:
424 return data(index.parent(), IsEmbededRole);
425 case ContentRole: {
426 auto text = content->encodedContent();
427 if (data(index, TypeRole).toString() == "HtmlContent") {
428 const auto rx = QRegExp("(src)\\s*=\\s*(\"|')(cid:[^\"']+)\\2");
429 int pos = 0;
430 while ((pos = rx.indexIn(text, pos)) != -1) {
431 const auto link = QUrl(rx.cap(3).toUtf8());
432 pos += rx.matchedLength();
433 const auto repl = d->mParser->getPart(link);
434 if (!repl) {
435 continue;
436 }
437 const auto content = repl->content();
438 if(content.size() < 1) {
439 continue;
440 }
441 const auto mailMime = content.first()->mailMime();
442 const auto mimetype = mailMime->mimetype().name();
443 if (mimetype.startsWith("image/")) {
444 const auto data = content.first()->content();
445 text.replace(rx.cap(0), QString("src=\"data:%1;base64,%2\"").arg(mimetype, QString::fromLatin1(data.toBase64())));
446 }
447 }
448 }
449 return text;
450 }
451 }
452 }
453 }
454 return QVariant();
455}
456
457QModelIndex NewModel::parent(const QModelIndex &index) const
458{
459 if (!index.internalPointer()) {
460 return QModelIndex();
461 }
462 const auto entry = static_cast<Entry *>(index.internalPointer());
463 if (entry->mParent && entry->mParent != d->mRoot.get()) {
464 return createIndex(entry->pos(), 0, entry->mParent);
465 }
466 return QModelIndex();
467}
468
469int NewModel::rowCount(const QModelIndex &parent) const
470{
471 if (!parent.isValid()) {
472 return d->mRoot->mChildren.size();
473 } else {
474 if (!parent.internalPointer()) {
475 return 0;
476 }
477 const auto entry = static_cast<Entry *>(parent.internalPointer());
478 return entry->mChildren.size();
479 }
480 return 0;
481}
482
483int NewModel::columnCount(const QModelIndex &parent) const
484{
485 return 1;
486}
diff --git a/framework/src/domain/messageparser_old.cpp b/framework/src/domain/messageparser_old.cpp
new file mode 100644
index 00000000..a4247d8c
--- /dev/null
+++ b/framework/src/domain/messageparser_old.cpp
@@ -0,0 +1,151 @@
1/*
2 This library is free software; you can redistribute it and/or modify it
3 under the terms of the GNU Library General Public License as published by
4 the Free Software Foundation; either version 2 of the License, or (at your
5 option) any later version.
6
7 This library is distributed in the hope that it will be useful, but WITHOUT
8 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
9 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
10 License for more details.
11
12 You should have received a copy of the GNU Library General Public License
13 along with this library; see the file COPYING.LIB. If not, write to the
14 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
15 02110-1301, USA.
16*/
17
18#include "messageparser.h"
19#include "mimetreeparser/interface.h"
20
21PartModel::PartModel(QSharedPointer<MimeTreeParser::MessagePart> partTree, std::shared_ptr<Parser> parser)
22 : mPartTree(partTree)
23 , mParser(parser)
24{
25}
26
27QHash<int, QByteArray> PartModel::roleNames() const
28{
29 QHash<int, QByteArray> roles;
30 roles[Text] = "text";
31 roles[IsHtml] = "isHtml";
32 roles[IsHidden] = "isHidden";
33 roles[IsEncrypted] = "isEncrypted";
34 roles[IsAttachment] = "isAttachment";
35 roles[HasContent] = "hasContent";
36 roles[Type] = "type";
37 roles[IsHidden] = "isHidden";
38 return roles;
39}
40
41QModelIndex PartModel::index(int row, int column, const QModelIndex &parent) const
42{
43 // qDebug() << "index " << parent << row << column << mPartTree->subParts().size();
44 if (!parent.isValid()) {
45 if (row < mPartTree->subParts().size()) {
46 auto part = mPartTree->subParts().at(row);
47 return createIndex(row, column, part.data());
48 }
49 } else {
50 auto part = static_cast<MimeTreeParser::MessagePart*>(parent.internalPointer());
51 auto subPart = part->subParts().at(row);
52 return createIndex(row, column, subPart.data());
53 }
54 return QModelIndex();
55}
56
57QVariant PartModel::data(const QModelIndex &index, int role) const
58{
59 // qDebug() << "Getting data for index";
60 if (index.isValid()) {
61 auto part = static_cast<MimeTreeParser::MessagePart*>(index.internalPointer());
62 switch (role) {
63 case Text: {
64 // qDebug() << "Getting text: " << part->property("text").toString();
65 // FIXME: we should have a list per part, and not one for all parts.
66 auto text = part->property("htmlContent").toString();
67 const auto rx = QRegExp("(src)\\s*=\\s*(\"|')(cid:[^\"']+)\\2");
68 int pos = 0;
69 while ((pos = rx.indexIn(text, pos)) != -1) {
70 const auto link = QUrl(rx.cap(3).toUtf8());
71 pos += rx.matchedLength();
72 const auto repl = mParser->getPart(link);
73 if (!repl) {
74 continue;
75 }
76 const auto content = repl->content();
77 if(content.size() < 1) {
78 continue;
79 }
80 const auto mailMime = content.first()->mailMime();
81 const auto mimetype = mailMime->mimetype().name();
82 if (mimetype.startsWith("image/")) {
83 const auto data = content.first()->content();
84 text.replace(rx.cap(0), QString("src=\"data:%1;base64,%2\"").arg(mimetype, QString::fromLatin1(data.toBase64())));
85 }
86 }
87 return text;
88 }
89 case IsAttachment:
90 return part->property("attachment").toBool();
91 case IsEncrypted:
92 return part->property("isEncrypted").toBool();
93 case IsHtml:
94 return part->property("isHtml").toBool();
95 case HasContent:
96 return !part->property("htmlContent").toString().isEmpty();
97 case Type:
98 return part->metaObject()->className();
99 case IsHidden:
100 return false;
101 //return part->property("isHidden").toBool();
102
103 }
104 }
105 return QVariant();
106}
107
108QModelIndex PartModel::parent(const QModelIndex &index) const
109{
110 // qDebug() << "parent " << index;
111 if (index.isValid()) {
112 auto part = static_cast<MimeTreeParser::MessagePart*>(index.internalPointer());
113 auto parentPart = static_cast<MimeTreeParser::MessagePart*>(part->parentPart());
114 auto row = 0;//get the parents parent to find the index
115 if (!parentPart) {
116 parentPart = mPartTree.data();
117 }
118 int i = 0;
119 for (const auto &p : parentPart->subParts()) {
120 if (p.data() == part) {
121 row = i;
122 break;
123 }
124 i++;
125 }
126 return createIndex(row, index.column(), parentPart);
127 }
128 return QModelIndex();
129}
130
131int PartModel::rowCount(const QModelIndex &parent) const
132{
133 // qDebug() << "Row count " << parent;
134 if (!parent.isValid()) {
135 // qDebug() << "Row count " << mPartTree->subParts().size();
136 return mPartTree->subParts().size();
137 } else {
138 auto part = static_cast<MimeTreeParser::MessagePart*>(parent.internalPointer());
139 if (part) {
140 return part->subParts().size();
141 }
142 }
143 return 0;
144}
145
146int PartModel::columnCount(const QModelIndex &parent) const
147{
148 // qDebug() << "Column count " << parent;
149 return 1;
150}
151
diff --git a/framework/src/domain/mimetreeparser/CMakeLists.txt b/framework/src/domain/mimetreeparser/CMakeLists.txt
new file mode 100644
index 00000000..64da2656
--- /dev/null
+++ b/framework/src/domain/mimetreeparser/CMakeLists.txt
@@ -0,0 +1,15 @@
1set(mimetreeparser_SRCS
2 interface.cpp
3 objecttreesource.cpp
4 stringhtmlwriter.cpp
5)
6
7add_library(mimetreeparser SHARED ${mimetreeparser_SRCS})
8
9qt5_use_modules(mimetreeparser Core Gui)
10target_link_libraries(mimetreeparser KF5::Mime KF5::MimeTreeParser)
11
12install(TARGETS mimetreeparser
13 DESTINATION ${LIB_INSTALL_DIR})
14
15add_subdirectory(tests)
diff --git a/framework/src/domain/mimetreeparser/interface.cpp b/framework/src/domain/mimetreeparser/interface.cpp
new file mode 100644
index 00000000..663a2013
--- /dev/null
+++ b/framework/src/domain/mimetreeparser/interface.cpp
@@ -0,0 +1,1179 @@
1/*
2 Copyright (c) 2016 Sandro Knauß <knauss@kolabsystems.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19
20#include "interface.h"
21#include "interface_p.h"
22
23#include "stringhtmlwriter.h"
24#include "objecttreesource.h"
25
26#include <QGpgME/KeyListJob>
27#include <QGpgME/Protocol>
28#include <gpgme++/key.h>
29#include <gpgme++/keylistresult.h>
30
31#include <KMime/Content>
32#include <MimeTreeParser/ObjectTreeParser>
33#include <MimeTreeParser/MessagePart>
34#include <MimeTreeParser/NodeHelper>
35
36#include <QMimeDatabase>
37#include <QMimeType>
38#include <QTextCodec>
39#include <QDebug>
40
41class MailMimePrivate
42{
43public:
44 MailMimePrivate(MailMime *p);
45
46 MailMime *q;
47 KMime::Content *mNode;
48 std::shared_ptr<MailMime> parent;
49};
50
51MailMimePrivate::MailMimePrivate(MailMime* p)
52 : q(p)
53 , mNode(nullptr)
54 , parent(nullptr)
55{
56}
57
58
59MailMime::MailMime()
60 : d(std::unique_ptr<MailMimePrivate>(new MailMimePrivate(this)))
61{
62}
63
64QByteArray MailMime::cid() const
65{
66 if (!d->mNode || !d->mNode->contentID()) {
67 return QByteArray();
68 }
69 return d->mNode->contentID()->identifier();
70}
71
72QByteArray MailMime::charset() const
73{
74 if(!d->mNode || !d->mNode->contentType(false)) {
75 return QByteArray();
76 }
77 if (d->mNode->contentType(false)) {
78 return d->mNode->contentType(false)->charset();
79 }
80 return d->mNode->defaultCharset();
81}
82
83bool MailMime::isFirstTextPart() const
84{
85 if (!d->mNode || !d->mNode->topLevel()) {
86 return false;
87 }
88 return (d->mNode->topLevel()->textContent() == d->mNode);
89}
90
91bool MailMime::isFirstPart() const
92{
93 if (!d->mNode || !d->mNode->parent()) {
94 return false;
95 }
96 return (d->mNode->parent()->contents().first() == d->mNode);
97}
98
99bool MailMime::isTopLevelPart() const
100{
101 if (!d->mNode) {
102 return false;
103 }
104 return (d->mNode->topLevel() == d->mNode);
105}
106
107MailMime::Disposition MailMime::disposition() const
108{
109 if (!d->mNode) {
110 return Invalid;
111 }
112 const auto cd = d->mNode->contentDisposition(false);
113 if (!cd) {
114 return Invalid;
115 }
116 switch (cd->disposition()){
117 case KMime::Headers::CDinline:
118 return Inline;
119 case KMime::Headers::CDattachment:
120 return Attachment;
121 default:
122 return Invalid;
123 }
124}
125
126QString MailMime::filename() const
127{
128 if (!d->mNode) {
129 return QString();
130 }
131 const auto cd = d->mNode->contentDisposition(false);
132 if (!cd) {
133 return QString();
134 }
135 return cd->filename();
136}
137
138QMimeType MailMime::mimetype() const
139{
140 if (!d->mNode) {
141 return QMimeType();
142 }
143
144 const auto ct = d->mNode->contentType(false);
145 if (!ct) {
146 return QMimeType();
147 }
148
149 QMimeDatabase mimeDb;
150 return mimeDb.mimeTypeForName(ct->mimeType());
151}
152
153MailMime::Ptr MailMime::parent() const
154{
155 if (!d->parent) {
156 d->parent = std::shared_ptr<MailMime>(new MailMime());
157 d->parent->d->mNode = d->mNode->parent();
158 }
159 return d->parent;
160}
161
162QByteArray MailMime::decodedContent() const
163{
164 if (!d->mNode) {
165 return QByteArray();
166 }
167 return d->mNode->decodedContent();
168}
169
170class KeyPrivate
171{
172public:
173 Key *q;
174 GpgME::Key mKey;
175 QByteArray mKeyID;
176};
177
178Key::Key()
179 :d(std::unique_ptr<KeyPrivate>(new KeyPrivate))
180{
181 d->q = this;
182}
183
184
185Key::Key(KeyPrivate *d_ptr)
186 :d(std::unique_ptr<KeyPrivate>(d_ptr))
187{
188 d->q = this;
189}
190
191Key::~Key()
192{
193
194}
195
196QString Key::keyid() const
197{
198 if (!d->mKey.isNull()) {
199 return d->mKey.keyID();
200 }
201
202 return d->mKeyID;
203}
204
205QString Key::name() const
206{
207 //FIXME: is this the correct way to get the primary UID?
208 if (!d->mKey.isNull()) {
209 return d->mKey.userID(0).name();
210 }
211
212 return QString();
213}
214
215QString Key::email() const
216{
217 if (!d->mKey.isNull()) {
218 return d->mKey.userID(0).email();
219 }
220 return QString();
221}
222
223QString Key::comment() const
224{
225 if (!d->mKey.isNull()) {
226 return d->mKey.userID(0).comment();
227 }
228 return QString();
229}
230
231class SignaturePrivate
232{
233public:
234 Signature *q;
235 GpgME::Signature mSignature;
236 Key::Ptr mKey;
237};
238
239Signature::Signature()
240 :d(std::unique_ptr<SignaturePrivate>(new SignaturePrivate))
241{
242 d->q = this;
243}
244
245
246Signature::Signature(SignaturePrivate *d_ptr)
247 :d(std::unique_ptr<SignaturePrivate>(d_ptr))
248{
249 d->q = this;
250}
251
252Signature::~Signature()
253{
254
255}
256
257QDateTime Signature::creationDateTime() const
258{
259 QDateTime dt;
260 dt.setTime_t(d->mSignature.creationTime());
261 return dt;
262}
263
264QDateTime Signature::expirationDateTime() const
265{
266 QDateTime dt;
267 dt.setTime_t(d->mSignature.expirationTime());
268 return dt;
269}
270
271bool Signature::neverExpires() const
272{
273 return d->mSignature.neverExpires();
274}
275
276Key::Ptr Signature::key() const
277{
278 return d->mKey;
279}
280
281class EncryptionPrivate
282{
283public:
284 Encryption *q;
285 std::vector<Key::Ptr> mRecipients;
286 Encryption::ErrorType mErrorType;
287 QString mErrorString;
288};
289
290Encryption::Encryption(EncryptionPrivate *d_ptr)
291 :d(std::unique_ptr<EncryptionPrivate>(d_ptr))
292{
293 d->q = this;
294}
295
296Encryption::Encryption()
297 :d(std::unique_ptr<EncryptionPrivate>(new EncryptionPrivate))
298{
299 d->q = this;
300 d->mErrorType = Encryption::NoError;
301}
302
303Encryption::~Encryption()
304{
305
306}
307
308std::vector<Key::Ptr> Encryption::recipients() const
309{
310 return d->mRecipients;
311}
312
313QString Encryption::errorString()
314{
315 return d->mErrorString;
316}
317
318Encryption::ErrorType Encryption::errorType()
319{
320 return d->mErrorType;
321}
322
323
324class PartPrivate
325{
326public:
327 PartPrivate(Part *part);
328 void appendSubPart(Part::Ptr subpart);
329
330 QVector<Part::Ptr> subParts();
331
332 Part *parent() const;
333
334 const MailMime::Ptr &mailMime() const;
335 void createMailMime(const MimeTreeParser::MimeMessagePart::Ptr &part);
336 void createMailMime(const MimeTreeParser::TextMessagePart::Ptr &part);
337 void createMailMime(const MimeTreeParser::AlternativeMessagePart::Ptr &part);
338 void createMailMime(const MimeTreeParser::HtmlMessagePart::Ptr &part);
339 void createMailMime(const MimeTreeParser::EncryptedMessagePart::Ptr &part);
340
341 static Encryption::Ptr createEncryption(const MimeTreeParser::EncryptedMessagePart::Ptr& part);
342 void appendEncryption(const MimeTreeParser::EncryptedMessagePart::Ptr &part);
343 static QVector<Signature::Ptr> createSignature(const MimeTreeParser::SignedMessagePart::Ptr& part);
344 void appendSignature(const MimeTreeParser::SignedMessagePart::Ptr &part);
345
346 void setSignatures(const QVector<Signature::Ptr> &sigs);
347 void setEncryptions(const QVector<Encryption::Ptr> &encs);
348
349 const QVector<Encryption::Ptr> &encryptions() const;
350 const QVector<Signature::Ptr> &signatures() const;
351private:
352 Part *q;
353 Part *mParent;
354 QVector<Part::Ptr> mSubParts;
355 QVector<Encryption::Ptr> mEncryptions;
356 QVector<Signature::Ptr> mSignatures;
357 MailMime::Ptr mMailMime;
358};
359
360PartPrivate::PartPrivate(Part* part)
361 : q(part)
362 , mParent(Q_NULLPTR)
363{
364
365}
366
367void PartPrivate::createMailMime(const MimeTreeParser::HtmlMessagePart::Ptr& part)
368{
369 mMailMime = MailMime::Ptr(new MailMime);
370 mMailMime->d->mNode = part->mNode;
371}
372
373void PartPrivate::createMailMime(const MimeTreeParser::AlternativeMessagePart::Ptr& part)
374{
375 mMailMime = MailMime::Ptr(new MailMime);
376 mMailMime->d->mNode = part->mNode;
377}
378
379void PartPrivate::createMailMime(const MimeTreeParser::TextMessagePart::Ptr& part)
380{
381 mMailMime = MailMime::Ptr(new MailMime);
382 mMailMime->d->mNode = part->mNode;
383}
384
385void PartPrivate::createMailMime(const MimeTreeParser::MimeMessagePart::Ptr& part)
386{
387 mMailMime = MailMime::Ptr(new MailMime);
388 mMailMime->d->mNode = part->mNode;
389}
390
391void PartPrivate::createMailMime(const MimeTreeParser::EncryptedMessagePart::Ptr& part)
392{
393 mMailMime = MailMime::Ptr(new MailMime);
394 mMailMime->d->mNode = part->mNode;
395}
396
397void PartPrivate::appendSubPart(Part::Ptr subpart)
398{
399 subpart->d->mParent = q;
400 mSubParts.append(subpart);
401}
402
403Encryption::Ptr PartPrivate::createEncryption(const MimeTreeParser::EncryptedMessagePart::Ptr& part)
404{
405 QGpgME::KeyListJob *job = part->mCryptoProto->keyListJob(false); // local, no sigs
406 if (!job) {
407 qWarning() << "The Crypto backend does not support listing keys. ";
408 return Encryption::Ptr();
409 }
410
411 auto encpriv = new EncryptionPrivate();
412 if (part->passphraseError()) {
413 encpriv->mErrorType = Encryption::PassphraseError;
414 encpriv->mErrorString = part->mMetaData.errorText;
415 } else if (part->isEncrypted() && !part->isDecryptable()) {
416 encpriv->mErrorType = Encryption::KeyMissing;
417 encpriv->mErrorString = part->mMetaData.errorText;
418 } else if (!part->isEncrypted() && !part->isDecryptable()) {
419 encpriv->mErrorType = Encryption::UnknownError;
420 encpriv->mErrorString = part->mMetaData.errorText;
421 } else {
422 encpriv->mErrorType = Encryption::NoError;
423 }
424
425 foreach(const auto &recipient, part->mDecryptRecipients) {
426 std::vector<GpgME::Key> found_keys;
427 const auto &keyid = recipient.keyID();
428 GpgME::KeyListResult res = job->exec(QStringList(QLatin1String(keyid)), false, found_keys);
429 if (res.error()) {
430 qWarning() << "Error while searching key for Fingerprint: " << keyid;
431 continue;
432 }
433 if (found_keys.size() > 1) {
434 // Should not Happen
435 qWarning() << "Oops: Found more then one Key for Fingerprint: " << keyid;
436 }
437 if (found_keys.size() != 1) {
438 // Should not Happen at this point
439 qWarning() << "Oops: Found no Key for Fingerprint: " << keyid;
440 auto keypriv = new KeyPrivate;
441 keypriv->mKeyID = keyid;
442 encpriv->mRecipients.push_back(Key::Ptr(new Key(keypriv)));
443 } else {
444 auto key = found_keys[0];
445 auto keypriv = new KeyPrivate;
446 keypriv->mKey = key;
447 encpriv->mRecipients.push_back(Key::Ptr(new Key(keypriv)));
448 }
449 }
450 return Encryption::Ptr(new Encryption(encpriv));
451}
452
453void PartPrivate::appendEncryption(const MimeTreeParser::EncryptedMessagePart::Ptr& part)
454{
455 mEncryptions.append(createEncryption(part));
456}
457
458void PartPrivate::setEncryptions(const QVector< Encryption::Ptr >& encs)
459{
460 mEncryptions = encs;
461}
462
463QVector<Signature::Ptr> PartPrivate::createSignature(const MimeTreeParser::SignedMessagePart::Ptr& part)
464{
465 QVector<Signature::Ptr> sigs;
466 QGpgME::KeyListJob *job = part->mCryptoProto->keyListJob(false); // local, no sigs
467 if (!job) {
468 qWarning() << "The Crypto backend does not support listing keys. ";
469 return sigs;
470 }
471
472 foreach(const auto &sig, part->mSignatures) {
473 auto sigpriv = new SignaturePrivate();
474 sigpriv->mSignature = sig;
475 auto signature = std::make_shared<Signature>(sigpriv);
476 sigs.append(signature);
477
478 std::vector<GpgME::Key> found_keys;
479 const auto &keyid = sig.fingerprint();
480 GpgME::KeyListResult res = job->exec(QStringList(QLatin1String(keyid)), false, found_keys);
481 if (res.error()) {
482 qWarning() << "Error while searching key for Fingerprint: " << keyid;
483 continue;
484 }
485 if (found_keys.size() > 1) {
486 // Should not Happen
487 qWarning() << "Oops: Found more then one Key for Fingerprint: " << keyid;
488 continue;
489 }
490 if (found_keys.size() != 1) {
491 // Should not Happen at this point
492 qWarning() << "Oops: Found no Key for Fingerprint: " << keyid;
493 continue;
494 } else {
495 auto key = found_keys[0];
496 auto keypriv = new KeyPrivate;
497 keypriv->mKey = key;
498 sigpriv->mKey = Key::Ptr(new Key(keypriv));
499 }
500 }
501 return sigs;
502}
503
504void PartPrivate::appendSignature(const MimeTreeParser::SignedMessagePart::Ptr& part)
505{
506 mSignatures.append(createSignature(part));
507}
508
509
510void PartPrivate::setSignatures(const QVector< Signature::Ptr >& sigs)
511{
512 mSignatures = sigs;
513}
514
515Part *PartPrivate::parent() const
516{
517 return mParent;
518}
519
520QVector< Part::Ptr > PartPrivate::subParts()
521{
522 return mSubParts;
523}
524
525const MailMime::Ptr& PartPrivate::mailMime() const
526{
527 return mMailMime;
528}
529
530const QVector< Encryption::Ptr >& PartPrivate::encryptions() const
531{
532 return mEncryptions;
533}
534
535const QVector< Signature::Ptr >& PartPrivate::signatures() const
536{
537 return mSignatures;
538}
539
540Part::Part()
541 : d(std::unique_ptr<PartPrivate>(new PartPrivate(this)))
542{
543
544}
545
546bool Part::hasSubParts() const
547{
548 return !subParts().isEmpty();
549}
550
551QVector<Part::Ptr> Part::subParts() const
552{
553 return d->subParts();
554}
555
556QByteArray Part::type() const
557{
558 return "Part";
559}
560
561QVector<QByteArray> Part::availableContents() const
562{
563 return QVector<QByteArray>();
564}
565
566QVector<Content::Ptr> Part::content() const
567{
568 return content(availableContents().first());
569}
570
571QVector<Content::Ptr> Part::content(const QByteArray& ct) const
572{
573 return QVector<Content::Ptr>();
574}
575
576QVector<Encryption::Ptr> Part::encryptions() const
577{
578 auto ret = d->encryptions();
579 auto parent = d->parent();
580 if (parent) {
581 ret.append(parent->encryptions());
582 }
583 return ret;
584}
585
586QVector<Signature::Ptr> Part::signatures() const
587{
588 auto ret = d->signatures();
589 auto parent = d->parent();
590 if (parent) {
591 ret.append(parent->signatures());
592 }
593 return ret;
594}
595
596MailMime::Ptr Part::mailMime() const
597{
598 return d->mailMime();
599}
600
601Part *Part::parent() const
602{
603 return d->parent();
604}
605
606class ContentPrivate
607{
608public:
609 QByteArray mContent;
610 QByteArray mCodec;
611 Part *mParent;
612 Content *q;
613 MailMime::Ptr mMailMime;
614 QVector<Encryption::Ptr> mEncryptions;
615 QVector<Signature::Ptr> mSignatures;
616 void appendSignature(const MimeTreeParser::SignedMessagePart::Ptr &sig);
617 void appendEncryption(const MimeTreeParser::EncryptedMessagePart::Ptr &enc);
618};
619
620void ContentPrivate::appendEncryption(const MimeTreeParser::EncryptedMessagePart::Ptr& enc)
621{
622 mEncryptions.append(PartPrivate::createEncryption(enc));
623}
624
625void ContentPrivate::appendSignature(const MimeTreeParser::SignedMessagePart::Ptr& sig)
626{
627 mSignatures.append(PartPrivate::createSignature(sig));
628}
629
630
631Content::Content(const QByteArray& content, Part *parent)
632 : d(std::unique_ptr<ContentPrivate>(new ContentPrivate))
633{
634 d->q = this;
635 d->mContent = content;
636 d->mCodec = "utf-8";
637 d->mParent = parent;
638}
639
640Content::Content(ContentPrivate* d_ptr)
641 : d(std::unique_ptr<ContentPrivate>(d_ptr))
642{
643 d->q = this;
644}
645
646Content::~Content()
647{
648}
649
650QVector<Encryption::Ptr> Content::encryptions() const
651{
652 auto ret = d->mEncryptions;
653 if (d->mParent) {
654 ret.append(d->mParent->encryptions());
655 }
656 return ret;
657}
658
659QVector<Signature::Ptr> Content::signatures() const
660{
661 auto ret = d->mSignatures;
662 if (d->mParent) {
663 ret.append(d->mParent->signatures());
664 }
665 return ret;
666}
667
668QByteArray Content::content() const
669{
670 return d->mContent;
671}
672
673QByteArray Content::charset() const
674{
675 return d->mCodec;
676}
677
678QString Content::encodedContent() const
679{
680 return QString::fromUtf8(content());
681 // TODO: should set "raw" content not the already utf8 encoded content
682 // return encodedContent(charset());
683}
684
685QString Content::encodedContent(const QByteArray &charset) const
686{
687 QTextCodec *codec = QTextCodec::codecForName(charset);
688 return codec->toUnicode(content());
689}
690
691QByteArray Content::type() const
692{
693 return "Content";
694}
695
696MailMime::Ptr Content::mailMime() const
697{
698 if (d->mMailMime) {
699 return d->mMailMime;
700 } else {
701 return d->mParent->mailMime();
702 }
703}
704
705Part *Content::parent() const
706{
707 return d->mParent;
708}
709
710HtmlContent::HtmlContent(const QByteArray& content, Part* parent)
711 : Content(content, parent)
712{
713
714}
715
716QByteArray HtmlContent::type() const
717{
718 return "HtmlContent";
719}
720
721PlainTextContent::PlainTextContent(const QByteArray& content, Part* parent)
722 : Content(content, parent)
723{
724
725}
726
727PlainTextContent::PlainTextContent(ContentPrivate* d_ptr)
728 : Content(d_ptr)
729{
730
731}
732
733HtmlContent::HtmlContent(ContentPrivate* d_ptr)
734 : Content(d_ptr)
735{
736
737}
738
739
740QByteArray PlainTextContent::type() const
741{
742 return "PlainTextContent";
743}
744
745class AlternativePartPrivate
746{
747public:
748 void fillFrom(MimeTreeParser::AlternativeMessagePart::Ptr part);
749
750 QVector<Content::Ptr> content(const QByteArray &ct) const;
751
752 AlternativePart *q;
753
754 QVector<QByteArray> types() const;
755
756private:
757 QMap<QByteArray, QVector<Content::Ptr>> mContent;
758 QVector<QByteArray> mTypes;
759};
760
761void AlternativePartPrivate::fillFrom(MimeTreeParser::AlternativeMessagePart::Ptr part)
762{
763 mTypes = QVector<QByteArray>() << "html" << "plaintext";
764
765 Content::Ptr content = std::make_shared<HtmlContent>(part->htmlContent().toLocal8Bit(), q);
766 mContent["html"].append(content);
767 content = std::make_shared<PlainTextContent>(part->plaintextContent().toLocal8Bit(), q);
768 mContent["plaintext"].append(content);
769 q->reachParentD()->createMailMime(part);
770}
771
772QVector<QByteArray> AlternativePartPrivate::types() const
773{
774 return mTypes;
775}
776
777QVector<Content::Ptr> AlternativePartPrivate::content(const QByteArray& ct) const
778{
779 return mContent[ct];
780}
781
782AlternativePart::AlternativePart()
783 : d(std::unique_ptr<AlternativePartPrivate>(new AlternativePartPrivate))
784{
785 d->q = this;
786}
787
788AlternativePart::~AlternativePart()
789{
790
791}
792
793QByteArray AlternativePart::type() const
794{
795 return "AlternativePart";
796}
797
798QVector<QByteArray> AlternativePart::availableContents() const
799{
800 return d->types();
801}
802
803QVector<Content::Ptr> AlternativePart::content(const QByteArray& ct) const
804{
805 return d->content(ct);
806}
807
808PartPrivate* AlternativePart::reachParentD() const
809{
810 return Part::d.get();
811}
812
813class SinglePartPrivate
814{
815public:
816 void fillFrom(const MimeTreeParser::TextMessagePart::Ptr &part);
817 void fillFrom(const MimeTreeParser::HtmlMessagePart::Ptr &part);
818 void fillFrom(const MimeTreeParser::AttachmentMessagePart::Ptr &part);
819 void createEncryptionFailBlock(const MimeTreeParser::EncryptedMessagePart::Ptr &part);
820 SinglePart *q;
821
822 QVector<Content::Ptr> mContent;
823 QByteArray mType;
824};
825
826void SinglePartPrivate::fillFrom(const MimeTreeParser::TextMessagePart::Ptr &part)
827{
828 mType = "plaintext";
829 mContent.clear();
830 foreach (const auto &mp, part->subParts()) {
831 auto d_ptr = new ContentPrivate;
832 d_ptr->mContent = mp->text().toUtf8(); // TODO: should set "raw" content not the already utf8 encoded content
833 d_ptr->mParent = q;
834 const auto enc = mp.dynamicCast<MimeTreeParser::EncryptedMessagePart>();
835 auto sig = mp.dynamicCast<MimeTreeParser::SignedMessagePart>();
836 if (enc) {
837 d_ptr->appendEncryption(enc);
838 if (!enc->isDecryptable()) {
839 d_ptr->mContent = QByteArray();
840 }
841 const auto s = enc->subParts();
842 if (s.size() == 1) {
843 sig = s[0].dynamicCast<MimeTreeParser::SignedMessagePart>();
844 }
845 }
846 if (sig) {
847 d_ptr->appendSignature(sig);
848 }
849 mContent.append(std::make_shared<PlainTextContent>(d_ptr));
850 q->reachParentD()->createMailMime(part);
851 d_ptr->mCodec = q->mailMime()->charset();
852 }
853}
854
855void SinglePartPrivate::fillFrom(const MimeTreeParser::HtmlMessagePart::Ptr &part)
856{
857 mType = "html";
858 mContent.clear();
859 mContent.append(std::make_shared<HtmlContent>(part->text().toUtf8(), q));
860 q->reachParentD()->createMailMime(part);
861}
862
863void SinglePartPrivate::fillFrom(const MimeTreeParser::AttachmentMessagePart::Ptr &part)
864{
865 QMimeDatabase mimeDb;
866 q->reachParentD()->createMailMime(part.staticCast<MimeTreeParser::TextMessagePart>());
867 const auto mimetype = q->mailMime()->mimetype();
868 const auto content = q->mailMime()->decodedContent();
869 mContent.clear();
870 if (mimetype == mimeDb.mimeTypeForName("text/plain")) {
871 mType = "plaintext";
872 mContent.append(std::make_shared<PlainTextContent>(content, q));
873 } else if (mimetype == mimeDb.mimeTypeForName("text/html")) {
874 mType = "html";
875 mContent.append(std::make_shared<HtmlContent>(content, q));
876 } else {
877 mType = mimetype.name().toUtf8();
878 mContent.append(std::make_shared<Content>(content, q));
879 }
880}
881
882void SinglePartPrivate::createEncryptionFailBlock(const MimeTreeParser::EncryptedMessagePart::Ptr &part)
883{
884 mType = "plaintext";
885 mContent.clear();
886 mContent.append(std::make_shared<PlainTextContent>(QByteArray(), q));
887 q->reachParentD()->createMailMime(part);
888}
889
890SinglePart::SinglePart()
891 : d(std::unique_ptr<SinglePartPrivate>(new SinglePartPrivate))
892{
893 d->q = this;
894}
895
896SinglePart::~SinglePart()
897{
898
899}
900
901QVector<QByteArray> SinglePart::availableContents() const
902{
903 return QVector<QByteArray>() << d->mType;
904}
905
906QVector< Content::Ptr > SinglePart::content(const QByteArray &ct) const
907{
908 if (ct == d->mType) {
909 return d->mContent;
910 }
911 return QVector<Content::Ptr>();
912}
913
914QByteArray SinglePart::type() const
915{
916 return "SinglePart";
917}
918
919PartPrivate* SinglePart::reachParentD() const
920{
921 return Part::d.get();
922}
923
924ParserPrivate::ParserPrivate(Parser* parser)
925 : q(parser)
926 , mNodeHelper(std::make_shared<MimeTreeParser::NodeHelper>())
927{
928
929}
930
931void ParserPrivate::setMessage(const QByteArray& mimeMessage)
932{
933 const auto mailData = KMime::CRLFtoLF(mimeMessage);
934 mMsg = KMime::Message::Ptr(new KMime::Message);
935 mMsg->setContent(mailData);
936 mMsg->parse();
937
938 // render the mail
939 StringHtmlWriter htmlWriter;
940 ObjectTreeSource source(&htmlWriter);
941 MimeTreeParser::ObjectTreeParser otp(&source, mNodeHelper.get());
942
943 otp.parseObjectTree(mMsg.data());
944 mPartTree = otp.parsedPart().dynamicCast<MimeTreeParser::MessagePart>();
945
946 mEmbeddedPartMap = htmlWriter.embeddedParts();
947 mHtml = htmlWriter.html();
948
949 mTree = std::make_shared<Part>();
950 createTree(mPartTree, mTree);
951}
952
953
954void ParserPrivate::createTree(const MimeTreeParser::MessagePart::Ptr &start, const Part::Ptr &tree)
955{
956 foreach (const auto &mp, start->subParts()) {
957 const auto m = mp.dynamicCast<MimeTreeParser::MessagePart>();
958 const auto text = mp.dynamicCast<MimeTreeParser::TextMessagePart>();
959 const auto alternative = mp.dynamicCast<MimeTreeParser::AlternativeMessagePart>();
960 const auto html = mp.dynamicCast<MimeTreeParser::HtmlMessagePart>();
961 const auto attachment = mp.dynamicCast<MimeTreeParser::AttachmentMessagePart>();
962 if (attachment) {
963 auto part = std::make_shared<SinglePart>();
964 part->d->fillFrom(attachment);
965 tree->d->appendSubPart(part);
966 } else if (text) {
967 auto part = std::make_shared<SinglePart>();
968 part->d->fillFrom(text);
969 tree->d->appendSubPart(part);
970 } else if (alternative) {
971 auto part = std::make_shared<AlternativePart>();
972 part->d->fillFrom(alternative);
973 tree->d->appendSubPart(part);
974 } else if (html) {
975 auto part = std::make_shared<SinglePart>();
976 part->d->fillFrom(html);
977 tree->d->appendSubPart(part);
978 } else {
979 const auto enc = mp.dynamicCast<MimeTreeParser::EncryptedMessagePart>();
980 const auto sig = mp.dynamicCast<MimeTreeParser::SignedMessagePart>();
981 if (enc || sig) {
982 auto subTree = std::make_shared<Part>();
983 if (enc) {
984 subTree->d->appendEncryption(enc);
985 if (!enc->isDecryptable()) {
986 auto part = std::make_shared<SinglePart>();
987 part->d->createEncryptionFailBlock(enc);
988 part->reachParentD()->setEncryptions(subTree->d->encryptions());
989 tree->d->appendSubPart(part);
990 return;
991 }
992 }
993 if (sig) {
994 subTree->d->appendSignature(sig);
995 }
996 createTree(m, subTree);
997 foreach(const auto &p, subTree->subParts()) {
998 tree->d->appendSubPart(p);
999 if (enc) {
1000 p->d->setEncryptions(subTree->d->encryptions());
1001 }
1002 if (sig) {
1003 p->d->setSignatures(subTree->d->signatures());
1004 }
1005 }
1006 } else {
1007 createTree(m, tree);
1008 }
1009 }
1010 }
1011}
1012
1013Parser::Parser(const QByteArray& mimeMessage)
1014 :d(std::unique_ptr<ParserPrivate>(new ParserPrivate(this)))
1015{
1016 d->setMessage(mimeMessage);
1017}
1018
1019Parser::~Parser()
1020{
1021}
1022
1023Part::Ptr Parser::getPart(const QUrl &url)
1024{
1025 if (url.scheme() == QStringLiteral("cid") && !url.path().isEmpty()) {
1026 const auto cid = url.path();
1027 return find(d->mTree, [&cid](const Part::Ptr &p){
1028 const auto mime = p->mailMime();
1029 return mime->cid() == cid;
1030 });
1031 }
1032 return Part::Ptr();
1033}
1034
1035QVector<Part::Ptr> Parser::collectContentParts() const
1036{
1037 return collect(d->mTree, [](const Part::Ptr &p){return p->type() != "EncapsulatedPart";},
1038 [](const Content::Ptr &content){
1039 const auto mime = content->mailMime();
1040
1041 if (!mime) {
1042 return true;
1043 }
1044
1045 if (mime->isFirstTextPart()) {
1046 return true;
1047 }
1048
1049 {
1050 auto _mime = content->parent()->mailMime();
1051 while (_mime) {
1052 if (_mime && (_mime->isTopLevelPart() || _mime->isFirstTextPart())) {
1053 return true;
1054 }
1055 if (_mime->isFirstPart()) {
1056 _mime = _mime->parent();
1057 } else {
1058 break;
1059 }
1060 }
1061 }
1062
1063 const auto ctname = mime->mimetype().name().trimmed().toLower();
1064 bool mightContent = (content->type() != "Content"); //Content we understand
1065
1066 const auto cd = mime->disposition();
1067 if (cd && cd == MailMime::Inline) {
1068 return mightContent;
1069 }
1070
1071 if (cd && cd == MailMime::Attachment) {
1072 return false;
1073 }
1074
1075 if ((ctname.startsWith("text/") || ctname.isEmpty()) &&
1076 (!mime || mime->filename().trimmed().isEmpty())) {
1077 // text/* w/o filename parameter:
1078 return true;
1079 }
1080 return false;
1081 });
1082}
1083
1084
1085QVector<Part::Ptr> Parser::collectAttachmentParts() const
1086{
1087 return collect(d->mTree, [](const Part::Ptr &p){return p->type() != "EncapsulatedPart";},
1088 [](const Content::Ptr &content){
1089 const auto mime = content->mailMime();
1090
1091 if (!mime) {
1092 return false;
1093 }
1094
1095 if (mime->isFirstTextPart()) {
1096 return false;
1097 }
1098
1099 {
1100 QMimeDatabase mimeDb;
1101 auto _mime = content->parent()->mailMime();
1102 const auto parent = _mime->parent();
1103 if (parent) {
1104 const auto mimetype = parent->mimetype();
1105 if (mimetype == mimeDb.mimeTypeForName("multipart/related")) {
1106 return false;
1107 }
1108 }
1109 while (_mime) {
1110 if (_mime && (_mime->isTopLevelPart() || _mime->isFirstTextPart())) {
1111 return false;
1112 }
1113 if (_mime->isFirstPart()) {
1114 _mime = _mime->parent();
1115 } else {
1116 break;
1117 }
1118 }
1119 }
1120
1121 const auto ctname = mime->mimetype().name().trimmed().toLower();
1122 bool mightContent = (content->type() != "Content"); //Content we understand
1123
1124 const auto cd = mime->disposition();
1125 if (cd && cd == MailMime::Inline) {
1126 // explict "inline" disposition:
1127 return !mightContent;
1128 }
1129 if (cd && cd == MailMime::Attachment) {
1130 // explicit "attachment" disposition:
1131 return true;
1132 }
1133
1134 const auto ct = mime->mimetype();
1135 if ((ctname.startsWith("text/") || ctname.isEmpty()) &&
1136 (!mime || mime->filename().trimmed().isEmpty())) {
1137 // text/* w/o filename parameter:
1138 return false;
1139 }
1140 return true;
1141 });
1142}
1143
1144QVector<Part::Ptr> Parser::collect(const Part::Ptr &start, std::function<bool(const Part::Ptr &)> select, std::function<bool(const Content::Ptr &)> filter) const
1145{
1146 QVector<Part::Ptr> ret;
1147 foreach (const auto &part, start->subParts()) {
1148 QVector<QByteArray> contents;
1149 foreach(const auto &ct, part->availableContents()) {
1150 foreach(const auto &content, part->content(ct)) {
1151 if (filter(content)) {
1152 contents.append(ct);
1153 break;
1154 }
1155 }
1156 }
1157 if (!contents.isEmpty()) {
1158 ret.append(part);
1159 }
1160 if (select(part)){
1161 ret += collect(part, select, filter);
1162 }
1163 }
1164 return ret;
1165}
1166
1167Part::Ptr Parser::find(const Part::Ptr &start, std::function<bool(const Part::Ptr &)> select) const
1168{
1169 foreach (const auto &part, start->subParts()) {
1170 if (select(part)) {
1171 return part;
1172 }
1173 const auto ret = find(part, select);
1174 if (ret) {
1175 return ret;
1176 }
1177 }
1178 return Part::Ptr();
1179}
diff --git a/framework/src/domain/mimetreeparser/interface.h b/framework/src/domain/mimetreeparser/interface.h
new file mode 100644
index 00000000..7c3ea28b
--- /dev/null
+++ b/framework/src/domain/mimetreeparser/interface.h
@@ -0,0 +1,378 @@
1/*
2 Copyright (c) 2016 Sandro Knauß <knauss@kolabsystems.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19
20#pragma once
21
22#include <functional>
23#include <memory>
24#include <vector>
25
26#include <QDateTime>
27#include <QUrl>
28#include <QMimeType>
29
30class Part;
31class PartPrivate;
32
33class MailMime;
34class MailMimePrivate;
35
36class AlternativePart;
37class AlternativePartPrivate;
38
39class SinglePart;
40class SinglePartPrivate;
41
42class EncryptionPart;
43class EncryptionPartPrivate;
44
45class EncapsulatedPart;
46class EncapsulatedPartPrivate;
47
48class Content;
49class ContentPrivate;
50
51class CertContent;
52class CertContentPrivate;
53
54class EncryptionError;
55
56class Key;
57class KeyPrivate;
58class Signature;
59class SignaturePrivate;
60class Encryption;
61class EncryptionPrivate;
62
63typedef std::shared_ptr<Signature> SignaturePtr;
64typedef std::shared_ptr<Encryption> EncryptionPtr;
65
66class Parser;
67class ParserPrivate;
68
69/*
70 * A MessagePart that is based on a KMime::Content
71 */
72class MailMime
73{
74public:
75 typedef std::shared_ptr<MailMime> Ptr;
76 /**
77 * Various possible values for the "Content-Disposition" header.
78 */
79 enum Disposition {
80 Invalid, ///< Default, invalid value
81 Inline, ///< inline
82 Attachment ///< attachment
83 };
84
85 MailMime();
86
87 // interessting header parts of a KMime::Content
88 QMimeType mimetype() const;
89 Disposition disposition() const;
90 QUrl label() const;
91 QByteArray cid() const;
92 QByteArray charset() const;
93 QString filename() const;
94
95 // Unique identifier to ecactly this KMime::Content
96 QByteArray link() const;
97
98 QByteArray content() const;
99 //Use default charset
100 QString encodedContent() const;
101
102 // overwrite default charset with given charset
103 QString encodedContent(QByteArray charset) const;
104
105 QByteArray decodedContent() const;
106
107 bool isFirstTextPart() const;
108 bool isFirstPart() const;
109 bool isTopLevelPart() const;
110
111 MailMime::Ptr parent() const;
112
113private:
114 std::unique_ptr<MailMimePrivate> d;
115
116 friend class PartPrivate;
117};
118
119class Content
120{
121public:
122 typedef std::shared_ptr<Content> Ptr;
123 Content(const QByteArray &content, Part *parent);
124 Content(ContentPrivate *d_ptr);
125 virtual ~Content();
126
127 QByteArray content() const;
128
129 QByteArray charset() const;
130
131 //Use default charset
132 QString encodedContent() const;
133
134 // overwrite default charset with given charset
135 QString encodedContent(const QByteArray &charset) const;
136
137 QVector<SignaturePtr> signatures() const;
138 QVector<EncryptionPtr> encryptions() const;
139 MailMime::Ptr mailMime() const;
140 virtual QByteArray type() const;
141 Part* parent() const;
142private:
143 std::unique_ptr<ContentPrivate> d;
144};
145
146class PlainTextContent : public Content
147{
148public:
149 PlainTextContent(const QByteArray &content, Part *parent);
150 PlainTextContent(ContentPrivate *d_ptr);
151 QByteArray type() const Q_DECL_OVERRIDE;
152};
153
154class HtmlContent : public Content
155{
156public:
157 HtmlContent(const QByteArray &content, Part *parent);
158 HtmlContent(ContentPrivate* d_ptr);
159 QByteArray type() const Q_DECL_OVERRIDE;
160};
161
162/*
163 * importing a cert GpgMe::ImportResult
164 * checking a cert (if it is a valid cert)
165 */
166
167class CertContent : public Content
168{
169public:
170 typedef std::shared_ptr<CertContent> Ptr;
171 CertContent(const QByteArray &content, Part *parent);
172
173 QByteArray type() const Q_DECL_OVERRIDE;
174 enum CertType {
175 Pgp,
176 SMime
177 };
178
179 enum CertSubType {
180 Public,
181 Private
182 };
183
184 CertType certType() const;
185 CertSubType certSubType() const;
186 int keyLength() const;
187
188private:
189 std::unique_ptr<CertContentPrivate> d;
190};
191
192class Part
193{
194public:
195 typedef std::shared_ptr<Part> Ptr;
196 Part();
197 virtual QByteArray type() const;
198
199 virtual QVector<QByteArray> availableContents() const;
200 virtual QVector<Content::Ptr> content(const QByteArray& ct) const;
201 QVector<Content::Ptr> content() const;
202
203 bool hasSubParts() const;
204 QVector<Part::Ptr> subParts() const;
205 Part *parent() const;
206
207 QVector<SignaturePtr> signatures() const;
208 QVector<EncryptionPtr> encryptions() const;
209 virtual MailMime::Ptr mailMime() const;
210protected:
211 std::unique_ptr<PartPrivate> d;
212private:
213 friend class ParserPrivate;
214 friend class PartPrivate;
215};
216
217class AlternativePart : public Part
218{
219public:
220 typedef std::shared_ptr<AlternativePart> Ptr;
221
222 AlternativePart();
223 virtual ~AlternativePart();
224
225 QVector<QByteArray> availableContents() const Q_DECL_OVERRIDE;
226 QVector<Content::Ptr> content(const QByteArray& ct) const Q_DECL_OVERRIDE;
227
228 QByteArray type() const Q_DECL_OVERRIDE;
229
230private:
231 PartPrivate *reachParentD() const;
232 std::unique_ptr<AlternativePartPrivate> d;
233
234 friend class ParserPrivate;
235 friend class AlternativePartPrivate;
236};
237
238class SinglePart : public Part
239{
240 public:
241 typedef std::shared_ptr<SinglePart> Ptr;
242
243 SinglePart();
244 virtual ~SinglePart();
245
246 QVector<Content::Ptr> content(const QByteArray& ct) const Q_DECL_OVERRIDE;
247 QVector<QByteArray> availableContents() const Q_DECL_OVERRIDE;
248
249 QByteArray type() const Q_DECL_OVERRIDE;
250private:
251 PartPrivate *reachParentD() const;
252 std::unique_ptr<SinglePartPrivate> d;
253
254 friend class ParserPrivate;
255 friend class SinglePartPrivate;
256};
257
258/*
259 * we want to request complete headers like:
260 * from/to...
261 */
262
263class EncapsulatedPart : public SinglePart
264{
265public:
266 typedef std::shared_ptr<EncapsulatedPart> Ptr;
267 QByteArray type() const Q_DECL_OVERRIDE;
268
269 //template <class T> QByteArray header<T>();
270private:
271 std::unique_ptr<EncapsulatedPartPrivate> d;
272};
273
274class EncryptionError
275{
276public:
277 int errorId() const;
278 QString errorString() const;
279};
280
281class Key
282{
283public:
284 typedef std::shared_ptr<Key> Ptr;
285 Key();
286 Key(KeyPrivate *);
287 ~Key();
288
289 QString keyid() const;
290 QString name() const;
291 QString email() const;
292 QString comment() const;
293 QVector<QString> emails() const;
294 enum KeyTrust {
295 Unknown, Undefined, Never, Marginal, Full, Ultimate
296 };
297 KeyTrust keyTrust() const;
298
299 bool isRevokation() const;
300 bool isInvalid() const;
301 bool isExpired() const;
302
303 std::vector<Key::Ptr> subkeys();
304 Key parentkey() const;
305private:
306 std::unique_ptr<KeyPrivate> d;
307};
308
309class Signature
310{
311public:
312 typedef std::shared_ptr<Signature> Ptr;
313 Signature();
314 Signature(SignaturePrivate *);
315 ~Signature();
316
317 Key::Ptr key() const;
318 QDateTime creationDateTime() const;
319 QDateTime expirationDateTime() const;
320 bool neverExpires() const;
321
322 //template <> StatusObject<SignatureVerificationResult> verify() const;
323 private:
324 std::unique_ptr<SignaturePrivate> d;
325};
326
327/*
328 * Normally the Keys for encryption are subkeys
329 * for clients the parentkeys are "more interessting", because they store the name, email etc.
330 * but a client may also wants show to what subkey the mail is really encrypted, an if this subkey isRevoked or something else
331 */
332class Encryption
333{
334public:
335 enum ErrorType {
336 NoError,
337 PassphraseError,
338 KeyMissing,
339 UnknownError
340 };
341 typedef std::shared_ptr<Encryption> Ptr;
342 Encryption();
343 Encryption(EncryptionPrivate *);
344 ~Encryption();
345 std::vector<Key::Ptr> recipients() const;
346 QString errorString();
347 ErrorType errorType();
348private:
349 std::unique_ptr<EncryptionPrivate> d;
350};
351
352class Parser
353{
354public:
355 typedef std::shared_ptr<Parser> Ptr;
356 Parser(const QByteArray &mimeMessage);
357 ~Parser();
358
359 Part::Ptr getPart(const QUrl &url);
360
361 QVector<Part::Ptr> collect(const Part::Ptr &start, std::function<bool(const Part::Ptr &)> select, std::function<bool(const Content::Ptr &)> filter) const;
362 Part::Ptr find(const Part::Ptr &start, std::function<bool(const Part::Ptr &)> select) const;
363 QVector<Part::Ptr> collectContentParts() const;
364 QVector<Part::Ptr> collectAttachmentParts() const;
365 //template <> QVector<ContentPart::Ptr> collect<ContentPart>() const;
366
367 //template <> static StatusObject<SignatureVerificationResult> verifySignature(const Signature signature) const;
368 //template <> static StatusObject<Part> decrypt(const EncryptedPart part) const;
369
370signals:
371 void partsChanged();
372
373private:
374 std::unique_ptr<ParserPrivate> d;
375
376 friend class InterfaceTest;
377};
378
diff --git a/framework/src/domain/mimetreeparser/interface_p.h b/framework/src/domain/mimetreeparser/interface_p.h
new file mode 100644
index 00000000..8fab221a
--- /dev/null
+++ b/framework/src/domain/mimetreeparser/interface_p.h
@@ -0,0 +1,56 @@
1/*
2 Copyright (c) 2016 Sandro Knauß <knauss@kolabsystems.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19
20#pragma once
21
22#include "interface.h"
23
24#include <QSharedPointer>
25#include <QMap>
26
27namespace KMime
28{
29 class Message;
30 typedef QSharedPointer<Message> MessagePtr;
31}
32
33namespace MimeTreeParser
34{
35 class MessagePart;
36 class NodeHelper;
37 typedef QSharedPointer<MessagePart> MessagePartPtr;
38}
39
40class ParserPrivate
41{
42public:
43 ParserPrivate(Parser *parser);
44
45 void setMessage(const QByteArray &mimeMessage);
46 void createTree(const MimeTreeParser::MessagePartPtr& start, const Part::Ptr& tree);
47
48 Part::Ptr mTree;
49 Parser *q;
50
51 MimeTreeParser::MessagePartPtr mPartTree;
52 KMime::MessagePtr mMsg;
53 std::shared_ptr<MimeTreeParser::NodeHelper> mNodeHelper;
54 QString mHtml;
55 QMap<QByteArray, QUrl> mEmbeddedPartMap;
56};
diff --git a/framework/src/domain/mimetreeparser/objecttreesource.cpp b/framework/src/domain/mimetreeparser/objecttreesource.cpp
new file mode 100644
index 00000000..567f3516
--- /dev/null
+++ b/framework/src/domain/mimetreeparser/objecttreesource.cpp
@@ -0,0 +1,152 @@
1/*
2 Copyright (C) 2009 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net
3 Copyright (c) 2009 Andras Mantia <andras@kdab.net>
4
5 This program is free software; you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation; either version 2 of the License, or
8 (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License along
16 with this program; if not, write to the Free Software Foundation, Inc.,
17 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18*/
19
20#include "objecttreesource.h"
21
22#include <MimeTreeParser/AttachmentStrategy>
23#include <MimeTreeParser/BodyPartFormatterBaseFactory>
24#include <MimeTreeParser/MessagePart>
25#include <MimeTreeParser/MessagePartRenderer>
26
27class ObjectSourcePrivate
28{
29public:
30 ObjectSourcePrivate()
31 : mWriter(0)
32 , mAllowDecryption(true)
33 , mHtmlLoadExternal(true)
34 , mPreferredMode(MimeTreeParser::Util::Html)
35 {
36
37 }
38 MimeTreeParser::HtmlWriter *mWriter;
39 MimeTreeParser::BodyPartFormatterBaseFactory mBodyPartFormatterBaseFactory;
40 bool mAllowDecryption;
41 bool mHtmlLoadExternal;
42 MimeTreeParser::Util::HtmlMode mPreferredMode;
43};
44
45ObjectTreeSource::ObjectTreeSource(MimeTreeParser::HtmlWriter *writer)
46 : MimeTreeParser::Interface::ObjectTreeSource()
47 , d(new ObjectSourcePrivate)
48 {
49 d->mWriter = writer;
50 }
51
52ObjectTreeSource::~ObjectTreeSource()
53{
54 delete d;
55}
56
57void ObjectTreeSource::setAllowDecryption(bool allowDecryption)
58{
59 d->mAllowDecryption = allowDecryption;
60}
61
62MimeTreeParser::HtmlWriter *ObjectTreeSource::htmlWriter()
63{
64 return d->mWriter;
65}
66
67bool ObjectTreeSource::htmlLoadExternal() const
68{
69 return d->mHtmlLoadExternal;
70}
71
72void ObjectTreeSource::setHtmlLoadExternal(bool loadExternal)
73{
74 d->mHtmlLoadExternal = loadExternal;
75}
76
77bool ObjectTreeSource::decryptMessage() const
78{
79 return d->mAllowDecryption;
80}
81
82bool ObjectTreeSource::showSignatureDetails() const
83{
84 return true;
85}
86
87int ObjectTreeSource::levelQuote() const
88{
89 return 1;
90}
91
92const QTextCodec *ObjectTreeSource::overrideCodec()
93{
94 return Q_NULLPTR;
95}
96
97QString ObjectTreeSource::createMessageHeader(KMime::Message *message)
98{
99 return QString();
100}
101
102const MimeTreeParser::AttachmentStrategy *ObjectTreeSource::attachmentStrategy()
103{
104 return MimeTreeParser::AttachmentStrategy::smart();
105}
106
107QObject *ObjectTreeSource::sourceObject()
108{
109 return Q_NULLPTR;
110}
111
112void ObjectTreeSource::setHtmlMode(MimeTreeParser::Util::HtmlMode mode, const QList<MimeTreeParser::Util::HtmlMode> &availableModes)
113{
114 Q_UNUSED(mode);
115 Q_UNUSED(availableModes);
116}
117
118MimeTreeParser::Util::HtmlMode ObjectTreeSource::preferredMode() const
119{
120 return d->mPreferredMode;
121}
122
123bool ObjectTreeSource::autoImportKeys() const
124{
125 return false;
126}
127
128bool ObjectTreeSource::showEmoticons() const
129{
130 return false;
131}
132
133bool ObjectTreeSource::showExpandQuotesMark() const
134{
135 return false;
136}
137
138bool ObjectTreeSource::isPrinting() const
139{
140 return false;
141}
142
143const MimeTreeParser::BodyPartFormatterBaseFactory *ObjectTreeSource::bodyPartFormatterFactory()
144{
145 return &(d->mBodyPartFormatterBaseFactory);
146}
147
148MimeTreeParser::Interface::MessagePartRenderer::Ptr ObjectTreeSource::messagePartTheme(MimeTreeParser::Interface::MessagePart::Ptr msgPart)
149{
150 Q_UNUSED(msgPart);
151 return MimeTreeParser::Interface::MessagePartRenderer::Ptr();
152}
diff --git a/framework/src/domain/mimetreeparser/objecttreesource.h b/framework/src/domain/mimetreeparser/objecttreesource.h
new file mode 100644
index 00000000..93812dc3
--- /dev/null
+++ b/framework/src/domain/mimetreeparser/objecttreesource.h
@@ -0,0 +1,57 @@
1/*
2 Copyright (C) 2009 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net
3 Copyright (c) 2009 Andras Mantia <andras@kdab.net>
4
5 This program is free software; you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation; either version 2 of the License, or
8 (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License along
16 with this program; if not, write to the Free Software Foundation, Inc.,
17 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18*/
19
20#ifndef MAILVIEWER_OBJECTTREEEMPTYSOURCE_H
21#define MAILVIEWER_OBJECTTREEEMPTYSOURCE_H
22
23#include <MimeTreeParser/ObjectTreeSource>
24
25class QString;
26
27class ObjectSourcePrivate;
28class ObjectTreeSource : public MimeTreeParser::Interface::ObjectTreeSource
29{
30public:
31 ObjectTreeSource(MimeTreeParser::HtmlWriter *writer);
32 virtual ~ObjectTreeSource();
33 void setHtmlLoadExternal(bool loadExternal);
34 bool decryptMessage() const Q_DECL_OVERRIDE;
35 bool htmlLoadExternal() const Q_DECL_OVERRIDE;
36 bool showSignatureDetails() const Q_DECL_OVERRIDE;
37 void setHtmlMode(MimeTreeParser::Util::HtmlMode mode, const QList<MimeTreeParser::Util::HtmlMode> &availableModes) Q_DECL_OVERRIDE;
38 MimeTreeParser::Util::HtmlMode preferredMode() const Q_DECL_OVERRIDE;
39 void setAllowDecryption(bool allowDecryption);
40 int levelQuote() const Q_DECL_OVERRIDE;
41 const QTextCodec *overrideCodec() Q_DECL_OVERRIDE;
42 QString createMessageHeader(KMime::Message *message) Q_DECL_OVERRIDE;
43 const MimeTreeParser::AttachmentStrategy *attachmentStrategy() Q_DECL_OVERRIDE;
44 MimeTreeParser::HtmlWriter *htmlWriter() Q_DECL_OVERRIDE;
45 QObject *sourceObject() Q_DECL_OVERRIDE;
46 bool autoImportKeys() const Q_DECL_OVERRIDE;
47 bool showEmoticons() const Q_DECL_OVERRIDE;
48 bool showExpandQuotesMark() const Q_DECL_OVERRIDE;
49 bool isPrinting() const Q_DECL_OVERRIDE;
50 const MimeTreeParser::BodyPartFormatterBaseFactory *bodyPartFormatterFactory() Q_DECL_OVERRIDE;
51 MimeTreeParser::Interface::MessagePartRendererPtr messagePartTheme(MimeTreeParser::Interface::MessagePartPtr msgPart) Q_DECL_OVERRIDE;
52private:
53 ObjectSourcePrivate *const d;
54};
55
56#endif
57
diff --git a/framework/src/domain/mimetreeparser/stringhtmlwriter.cpp b/framework/src/domain/mimetreeparser/stringhtmlwriter.cpp
new file mode 100644
index 00000000..88034492
--- /dev/null
+++ b/framework/src/domain/mimetreeparser/stringhtmlwriter.cpp
@@ -0,0 +1,150 @@
1/* -*- c++ -*-
2 filehtmlwriter.cpp
3
4 This file is part of KMail, the KDE mail client.
5 Copyright (c) 2003 Marc Mutz <mutz@kde.org>
6
7 KMail is free software; you can redistribute it and/or modify it
8 under the terms of the GNU General Public License, version 2, as
9 published by the Free Software Foundation.
10
11 KMail is distributed in the hope that it will be useful, but
12 WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with this program; if not, write to the Free Software
18 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19
20 In addition, as a special exception, the copyright holders give
21 permission to link the code of this program with any edition of
22 the Qt library by Trolltech AS, Norway (or with modified versions
23 of Qt that use the same license as Qt), and distribute linked
24 combinations including the two. You must obey the GNU General
25 Public License in all respects for all of the code used other than
26 Qt. If you modify this file, you may extend this exception to
27 your version of the file, but you are not obligated to do so. If
28 you do not wish to do so, delete this exception statement from
29 your version.
30*/
31
32#include "stringhtmlwriter.h"
33
34#include <QDebug>
35#include <QTextStream>
36#include <QUrl>
37
38StringHtmlWriter::StringHtmlWriter()
39 : MimeTreeParser::HtmlWriter()
40 , mState(Ended)
41{
42}
43
44StringHtmlWriter::~StringHtmlWriter()
45{
46}
47
48void StringHtmlWriter::begin(const QString &css)
49{
50 if (mState != Ended) {
51 qWarning() << "begin() called on non-ended session!";
52 reset();
53 }
54
55 mState = Begun;
56 mExtraHead.clear();
57 mHtml.clear();
58
59 if (!css.isEmpty()) {
60 write(QLatin1String("<!-- CSS Definitions \n") + css + QLatin1String("-->\n"));
61 }
62}
63
64void StringHtmlWriter::end()
65{
66 if (mState != Begun) {
67 qWarning() << "Called on non-begun or queued session!";
68 }
69
70 if (!mExtraHead.isEmpty()) {
71 insertExtraHead();
72 mExtraHead.clear();
73 }
74 resolveCidUrls();
75 mState = Ended;
76}
77
78void StringHtmlWriter::reset()
79{
80 if (mState != Ended) {
81 mHtml.clear();
82 mExtraHead.clear();
83 mState = Begun; // don't run into end()'s warning
84 end();
85 mState = Ended;
86 }
87}
88
89void StringHtmlWriter::write(const QString &str)
90{
91 if (mState != Begun) {
92 qWarning() << "Called in Ended or Queued state!";
93 }
94 mHtml.append(str);
95}
96
97void StringHtmlWriter::queue(const QString &str)
98{
99 write(str);
100}
101
102void StringHtmlWriter::flush()
103{
104 mState = Begun; // don't run into end()'s warning
105 end();
106}
107
108void StringHtmlWriter::embedPart(const QByteArray &contentId, const QString &url)
109{
110 write("<!-- embedPart(contentID=" + contentId + ", url=" + url + ") -->\n");
111 mEmbeddedPartMap.insert(contentId, url);
112}
113
114void StringHtmlWriter::resolveCidUrls()
115{
116 for (const auto &cid : mEmbeddedPartMap.keys()) {
117 mHtml.replace(QString("src=\"cid:%1\"").arg(QString(cid)), QString("src=\"%1\"").arg(mEmbeddedPartMap.value(cid).toString()));
118 }
119}
120
121void StringHtmlWriter::extraHead(const QString &extraHead)
122{
123 if (mState != Ended) {
124 qWarning() << "Called on non-started session!";
125 }
126 mExtraHead.append(extraHead);
127}
128
129
130void StringHtmlWriter::insertExtraHead()
131{
132 const QString headTag(QStringLiteral("<head>"));
133 const int index = mHtml.indexOf(headTag);
134 if (index != -1) {
135 mHtml.insert(index + headTag.length(), mExtraHead);
136 }
137}
138
139QMap<QByteArray, QUrl> StringHtmlWriter::embeddedParts() const
140{
141 return mEmbeddedPartMap;
142}
143
144QString StringHtmlWriter::html() const
145{
146 if (mState != Ended) {
147 qWarning() << "Called on non-ended session!";
148 }
149 return mHtml;
150}
diff --git a/framework/src/domain/mimetreeparser/stringhtmlwriter.h b/framework/src/domain/mimetreeparser/stringhtmlwriter.h
new file mode 100644
index 00000000..fa5b760e
--- /dev/null
+++ b/framework/src/domain/mimetreeparser/stringhtmlwriter.h
@@ -0,0 +1,71 @@
1/* -*- c++ -*-
2
3 Copyright (c) 2016 Sandro Knauß <sknauss@kde.org>
4
5 Kube is free software; you can redistribute it and/or modify it
6 under the terms of the GNU General Public License, version 2, as
7 published by the Free Software Foundation.
8
9 Kube is distributed in the hope that it will be useful, but
10 WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with this program; if not, write to the Free Software
16 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
18 In addition, as a special exception, the copyright holders give
19 permission to link the code of this program with any edition of
20 the Qt library by Trolltech AS, Norway (or with modified versions
21 of Qt that use the same license as Qt), and distribute linked
22 combinations including the two. You must obey the GNU General
23 Public License in all respects for all of the code used other than
24 Qt. If you modify this file, you may extend this exception to
25 your version of the file, but you are not obligated to do so. If
26 you do not wish to do so, delete this exception statement from
27 your version.
28*/
29
30#ifndef __KUBE_FRAMEWORK_MAIL_STRINGHTMLWRITER_H__
31#define __KUBE_FRAMEWORK_MAIL_STRINGHTMLWRITER_H__
32
33#include <MimeTreeParser/HtmlWriter>
34
35#include <QFile>
36#include <QTextStream>
37
38class QString;
39
40class StringHtmlWriter : public MimeTreeParser::HtmlWriter
41{
42public:
43 explicit StringHtmlWriter();
44 virtual ~StringHtmlWriter();
45
46 void begin(const QString &cssDefs) Q_DECL_OVERRIDE;
47 void end() Q_DECL_OVERRIDE;
48 void reset() Q_DECL_OVERRIDE;
49 void write(const QString &str) Q_DECL_OVERRIDE;
50 void queue(const QString &str) Q_DECL_OVERRIDE;
51 void flush() Q_DECL_OVERRIDE;
52 void embedPart(const QByteArray &contentId, const QString &url) Q_DECL_OVERRIDE;
53 void extraHead(const QString &str) Q_DECL_OVERRIDE;
54
55 QString html() const;
56 QMap<QByteArray, QUrl> embeddedParts() const;
57private:
58 void insertExtraHead();
59 void resolveCidUrls();
60
61 QString mHtml;
62 QString mExtraHead;
63 enum State {
64 Begun,
65 Queued,
66 Ended
67 } mState;
68 QMap<QByteArray, QUrl> mEmbeddedPartMap;
69};
70
71#endif // __MESSAGEVIEWER_FILEHTMLWRITER_H__
diff --git a/framework/src/domain/mimetreeparser/tests/CMakeLists.txt b/framework/src/domain/mimetreeparser/tests/CMakeLists.txt
new file mode 100644
index 00000000..71afb903
--- /dev/null
+++ b/framework/src/domain/mimetreeparser/tests/CMakeLists.txt
@@ -0,0 +1,23 @@
1add_subdirectory(gnupg_home)
2add_definitions( -DMAIL_DATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/data" )
3include(${CMAKE_CURRENT_SOURCE_DIR}/kdepim_add_gpg_crypto_test.cmake)
4include_directories(
5 ${CMAKE_CURRENT_BINARY_DIR}
6 ${CMAKE_CURRENT_SOURCE_DIR}/..
7 )
8
9include(ECMAddTests)
10
11add_executable(mimetreeparsertest interfacetest.cpp)
12add_gpg_crypto_test(mimetreeparsertest mimetreeparsertest)
13qt5_use_modules(mimetreeparsertest Core Test)
14target_link_libraries(mimetreeparsertest mimetreeparser)
15
16find_package(Gpgmepp 1.7.1 CONFIG)
17find_package(QGpgme 1.7.1 CONFIG)
18
19ecm_add_test(gpgerrortest.cpp
20 TEST_NAME "gpgerrortest"
21 NAME_PREFIX "mimetreeparser-"
22 LINK_LIBRARIES Qt5::Core Qt5::Test mimetreeparser Gpgmepp QGpgme
23)
diff --git a/framework/src/domain/mimetreeparser/tests/data/alternative.mbox b/framework/src/domain/mimetreeparser/tests/data/alternative.mbox
new file mode 100644
index 00000000..6522c34b
--- /dev/null
+++ b/framework/src/domain/mimetreeparser/tests/data/alternative.mbox
@@ -0,0 +1,28 @@
1Return-Path: <konqi@example.org>
2Date: Wed, 8 Jun 2016 20:34:44 -0700
3From: Konqi <konqi@example.org>
4To: konqi@kde.org
5Subject: A random subject with alternative contenttype
6MIME-Version: 1.0
7Content-Type: multipart/alternative;
8 boundary="----=_Part_12345678_12345678"
9
10
11------=_Part_12345678_12345678
12Content-Type: text/plain; charset=utf-8
13Content-Transfer-Encoding: quoted-printable
14
15If you can see this text it means that your email client couldn't display o=
16ur newsletter properly.
17Please visit this link to view the newsletter on our website: http://www.go=
18g.com/newsletter/
19
20
21------=_Part_12345678_12345678
22Content-Transfer-Encoding: 7Bit
23Content-Type: text/html; charset="windows-1252"
24
25<html><body><p><span>HTML</span> text</p></body></html>
26
27
28------=_Part_12345678_12345678--
diff --git a/framework/src/domain/mimetreeparser/tests/data/cid-links.mbox b/framework/src/domain/mimetreeparser/tests/data/cid-links.mbox
new file mode 100644
index 00000000..40ff5282
--- /dev/null
+++ b/framework/src/domain/mimetreeparser/tests/data/cid-links.mbox
@@ -0,0 +1,1384 @@
1Message-ID: <851f01d15e53$31734730$790bc9ad@info>
2From: "OculusLab" <info@findermanze.co.ua>
3To: <info@example.org>
4Subject: CID links for images
5Date: Wed, 03 Feb 2016 07:19:17 +0200
6MIME-Version: 1.0
7Content-Type: multipart/related;
8 type="multipart/alternative";
9 boundary="----=_NextPart_000_000F_01D15E52.0BD654A0"
10X-MSMail-Priority: Normal
11X-Mailer: Microsoft Windows Live Mail 14.0.8117.416
12X-MimeOLE: Produced By Microsoft MimeOLE V14.0.8117.416
13
14 This is a multi-part message in MIME format.
15
16------=_NextPart_000_000F_01D15E52.0BD654A0
17Content-Type: multipart/alternative;
18 boundary="----=_NextPart_000_0010_01D15E52.0BD654A0"
19
20------=_NextPart_000_0010_01D15E52.0BD654A0
21Content-Type: text/plain;
22 charset="windows-1251"
23Content-Transfer-Encoding: quoted-printable
24
25=0D=0A=0D=0A=0D=0A=0D=0ASuperkombipackung f&#252;r nur 45 Euro=0D=
26=0A=0D=0A
27------=_NextPart_000_0010_01D15E52.0BD654A0
28Content-Type: text/html;
29 charset="windows-1251"
30Content-Transfer-Encoding: quoted-printable
31
32<HTML><HEAD>=0D=0A<META http-equiv=3D"Content-Type" content=3D"te=
33xt/html; charset=3Dwindows-1251">=0D=0A</HEAD>=0D=0A<BODY bgColor=
34=3D#ffffff>=0D=0A<DIV align=3Dcenter><FONT size=3D2 face=3DArial>=
35<A =0D=0Ahref=3D"http://intenices.co.ua/drugs-store/index.html"><=
36STRONG><FONT =0D=0Asize=3D4>Superkombipackung f&#252;r nur 45 Eur=
37o</FONT></STRONG></A><BR><BR><A =0D=0Ahref=3D"http://intenices.co=
38.ua/drugs-store/index.html"><IMG border=3D0 hspace=3D0 alt=3D""=20=
39src=3D"cid:9359201d15e53f31a68c307b3369b6@info" width=3D650 heigh=
40t=3D763></A></FONT></DIV></BODY></HTML>
41
42------=_NextPart_000_0010_01D15E52.0BD654A0--
43
44------=_NextPart_000_000F_01D15E52.0BD654A0
45Content-Type: image/jpeg;
46 name="aqnaozisxya.jpeg"
47Content-Transfer-Encoding: base64
48Content-ID: <9359201d15e53f31a68c307b3369b6@info>
49
50/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAAA8AAD/4QMqaHR0cDov
51L25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENl
52aGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4
53OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjUtYzAxNCA3OS4xNTE0ODEsIDIwMTMvMDMvMTMtMTI6
54MDk6MTUgICAgICAgICI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5
55OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHht
56bG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6
57Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUu
58Y29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBo
59b3Rvc2hvcCBDQyAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6QjdCRTg5MTBD
60OUNGMTFFNUJBOTdEMkQyNzU0ODI3RDciIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6QjdCRTg5
61MTFDOUNGMTFFNUJBOTdEMkQyNzU0ODI3RDciPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5z
62dGFuY2VJRD0ieG1wLmlpZDpCN0JFODkwRUM5Q0YxMUU1QkE5N0QyRDI3NTQ4MjdENyIgc3RSZWY6
63ZG9jdW1lbnRJRD0ieG1wLmRpZDpCN0JFODkwRkM5Q0YxMUU1QkE5N0QyRDI3NTQ4MjdENyIvPiA8
64L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0i
65ciI/Pv/uAA5BZG9iZQBkwAAAAAH/2wCEAAYEBAQFBAYFBQYJBgUGCQsIBgYICwwKCgsKCgwQDAwM
66DAwMEAwODxAPDgwTExQUExMcGxsbHB8fHx8fHx8fHx8BBwcHDQwNGBAQGBoVERUaHx8fHx8fHx8f
67Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fH//AABEIAvsCigMBEQACEQED
68EQH/xADJAAEAAgMBAQEAAAAAAAAAAAAAAwQBAgUGBwgBAQEBAQEBAQAAAAAAAAAAAAABAgMEBQYQ
69AAEEAgEDAgQCBAgKCQMACwIAAQMEEQUSIRMGMUFRIjIUYXGBQhUHkaGxUiMzFhfB0WJy0+OkZZVW
708ILSsyQ0lFU24UN1U4MlssJzhLQ1djcRAQABAgMEBQkFBwQBBAIDAAABEQIhMQNBUWEScYGRoQTw
71scHRIjJSYhPhQpKyFfFygqIzUwXCI2Nzk9JDsxSDNPLD0//aAAwDAQACEQMRAD8A/VKAgICAgICA
72gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA
73gICAgICAgICAgICAgICAgICAg1y6Bl0DLoGXQMugZdBrIZMBOz9WZBpWkM68Zk+SIWd3QS8nQOTo
74HJ0Dk6BydA5OgcnQOToGXQMugZdAy6Bl0DLoGXQMugOT4dAYnwyA7ughaWTPqgkIyaNnz16INQkN
753bL+6BJIbHhn6INozJ2fL+6DbLoI5JDYmZn6KDJGTR8s9VQiM39Xyg1GWRy6v0ygkMiYXdn6sghG
76aRxd8/yItGAnldupfxMoUZ70nx/kQO9J8f5EKAzSO3r/ABMhRnuyfH+RCg0snXqgNNI/v/Igd2T4
77/wAiDLSyfFCjDSyfH+RUZ7snx/kUBpTz6oMd2TPr/Igz3JPj/Igy0knx/kQY7smX6oMd2T4/yIMj
78Kb+6B3T5O2eiVKHdP4oUO5J8UqtGHlk5Yz/IpUoz3ZPiqUO7J8UKDynluqIy8h/FA7h/FKlApDZv
79VAaU/ilSjPcP4oDyHjOUGO4eG6oUZ5l8UGeZY9VUatIePVRR5Dz6pUoyxnn1QBkN3fqgy5l8UQYy
80+KDcXd2fPxVGUBBqgICAgICDSb+qL8nQR0//ACkP+YyEpkBAQEBAQEBEEBAQEBAQEBAL0dAb0ZFH
819HQQD6oJJP6pv0INY/Vvz/wIMS/1n8CCSL6X/N0G6CKT62UGS/qv0/4VQhQRh9Tfmglm/qyQQD9D
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
2906/oo2w6Iy3oisug1QYRGW9UVsiCKwgwgICDCAgwgyyCnfiCRmYxYhJsOzrF00lu3Fw7Ol6u8J4b+
291aX+NI1Fmxz5YZIvkkbBN8PgukTViYom1sxMfa9Xd8MuV8OlsvSRBxFh+H8qwqYWVgSMqyytDUzYR
292cnfDN1d/wZQeQ2Ft7Vo5f1fQG/yW9F2tjBiZVSdlWXpfH6ghSad2/pJny7v8GfDKDqY9kG3p0Qbe
29335oCDP4INmUBAd8MgrkOCz8VpGweuP4FBszoMOzKg3wRWRfOVEbCitmUGyAoMICAgINx9EVlAQao
294CAgwgICDDogisIMOyDCDCBhBhAQYwgYQZwgccoMPGLoNeyPwZBloRb2QSMDMg2ZsIMogg1k+h0FM
295v6+D/Of+R1Wdq6/oo2wiMt6IrLoNVRhRGW9UVsiCKwgwgwgygwgwgINZJBBsv1z6MpMrEOfanPPM
296vpb1b4MuV0u1sNc8myyzRXK2kfUT/Q66acsakMaOtztFM/pE2G/zn/xLWpLNr0IsuTaRmWkbsqg6
297DjeQX+3C1YH+eX6/wD/6rdsJMudrNTLaxJJmOv8AH3L/ADf8a6ObuHq6nZEBiBhB+RM45d2/P1QW
298w6C2McW6fL6N+hBI2PX+BQFUG9VFbfigMg2woDvhkGj/ABVRgmz/AIFRoPq6Df2yorHtj4KoIp+s
299ojLP1RWzOg3ZQZ9lBrl1FMqh1QMug3D0/Sg2QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA
300QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA
301QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA
302QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA
303QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA
304QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA
305QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA
306QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA
307QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA
308QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA
309QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA
310QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA
311QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA
312QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA
313QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA
314QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA
315QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA
316QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQRWbVWpAdi1MEFeNsyTS
317kwALZx1InZmQYK5TG2FMp42tyAUsddzFpCAHZiMQzycWcmZ3SPMSji2urmnGCK5BJOTyCMQSARu8
318BMMrMLPn+jJ2Yvg/qkYk4eXX5sVpSZpFRW12zo7LXwbGlL3adgGkhlwQZB/fBsJN+llq6KZm2nGn
319Zglq2q1uvHZqzBYryixRTRExgQv6OJDlnZJigRWq0ss0MUwSS13YZ4xJnKMiFiZjZuou4uztn2U2
320VEiAg5Nvy3xinParWNrVG3SiOe1TaUDsBHGHcMngF3l6B830+ibK7PKPOR70W7ZdSKWOWIJY35Ry
321CxA/plibLeqt1sxNJS26JisbWyiiAgIIq1uraAjrTRzgBlEZRkxsxxvxMHcXfBCTYdvZNlTglQEB
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
354flm71A2J56MNalbhGzNLYMJLBThIwyTEZ8X7AvxzhvbCxGNld10x1cts+mVuwujjbXvl5zYy+XbX
355beQFSmgqz6myMVOWfaWagV42ijkCSWnFBJDOEju7uUpPnqLccJo0pbM7bprtyupThh141qupjM2x
356uw64z40nqwpTOZayW/tN/ToWthcarLb34zBDZmichgtRDEHOMhNhjYvl4u2PRumVNOIm3/8AHE/z
357yt8zFf3rf/irPf61WntPJNiep0zSPYidtoLFNsbOuksFRvPXjZ7NaKaYzCEcuOW5Z5PnCsRXH5NO
358fxRjNOzhFcsWbvZrEZc90dlKRXt4+znnX1taHyQfBZ68uyrBvGinhh2DTPLEB8yCHnMQRuRAPETJ
359wzyy+FnUpMxT5a7K5Vpu5tnS1ZhM148adPR5nl696y26qeOWf2lqfuLEY7VpNjLbEmOCeSAa9xze
360aPvHE/JmcH+VmZm5delsRdjsjm4YxyduF1fOxdM2xxmmPCebHhjHLltdCzpq9jzDY1gvXGjraSDt
361yw25QlYxs2WHnNGQym4Y9DJ8/rZXGb5jTvujOJj8s7MnaLY5rLdk83+jr8typqJ9h5FJWO9sLkbS
362+M0LxBVsS1h+6lKZyl/oSDr09PR/dn6Lr4n2PqzH3bsOyXPQ9qNOJ281eNJsVKG08r8lepDzHuNp
363aF2P/wDaVnVk8tmMnlsM1aCbvMJszOJvxH+b8y1q2ct19MKXzEbaRSJjCevppwY07sLYnHDtxmM+
364im7PbhSv39nHtBnnKK7sw3FA5TrvmKadtETu8b4H5TP06LF11ImbY/vU7IdIt2XT93Tr/wCWXZ8K
365/tRbk0e6ltQfa34yLYEW0s2nsucLlxipyV4oIJI5Wy4xE3FmJnyul1ttszbspht2xjXdTqmuTnE3
366XRXKa48N8U8pwzl3LGykr+W7sLFooacOor2IxORxjB2lsNJIzO/Fn6DyL8l5b5/2r99f9OHe9Fse
3673Zxr57XmfHmvbmKlHc2d9hbxfX237NueEnsyPLmYijISI/lbOX+b9bK7+K9n6sx927DhhLloY8kT
368tm6vbazrdjY8i1ks27vz1wq6ClejCCxJTA5LUEhzWJHhKPm3IGHiWQb4dVnxdsWxqU2XXW9VIphx
369mZ7MDw0zN1kT013zzTHdERP8XQ7QbG7rf3RVr9J+Nqvp4Dikxy4P2BzJxf14N836F18VFdaYnCJv
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
482gkD6gLD9CH3Z0nKuw20ZrWq1qvHZqyhPXlFiimiJjAhf0cSHLOysxQSKAgIK02ypQ7Ctr5JONy2E
483sleLiT8hh49x+TNxbj3B9XSMa8CcIrxp559CygICAgICAgICAgICAgICAgICAgICAgICAgICAgIC
484AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgION5nsL2u8V2l6g/G3Xrmc
485R45cMN1k4v68GyWPwUwmYicIm62J6JmInuWK40is0mkb5phHXLx/kwjrquz19C/Zu07vjuxtWgs2
486JLeCjABhmE5SNwaRpDbiLsL46N0V1MromKcs29VZnDu27uLWh71k1xm7tjfwph2tNtJd00FiGnet
4878bfjF66by2JZHCzVGFo5YeRYhfEz9I+LenToumvnqcLre+bq+aHLwsf0p34T2W+XnN1sdx48xy62
4883ZszWtBZvSNamksM1iucAtMAydwY8DOTkICwvj6VdSI5r4yiL7I6IuuuiceiOpnSmZtsuzmYu66W
489xMd+HGubt+JU9/X2wzTzwfsu1UeRoW2tnaSSysYOE8b2YYe2HEiYmB+PUejJdSImJzrGzLOvHdnu
490IrNJ8p9H7XK8mtWNLutvqKhPHL5dHC+rJv1bhENS0Tf5kJRy/od1zssi+OScou/kmt135buu6HW6
4917kmNThT+KPd7a06LUVvlU1HkloL9ija8ZJq2mpxTyRwxxw14yrCdcSaOfvkX/wBwSznA4wt88zy3
492Zzffj+OlOHs0nfjXczZZETyThbbbGP8ADWbuqa8PZ6Vye92rXl+z2dq+0GreLs1q1iSNou7Qi59s
493GIQzykd255ES+bo/VZiPZimMzfNvfbTy3LF2MTOyyLp6pvr5striW9hvtZftUCmlpwsOqtFD+07G
494xkjaTYhHIRzTsJxscb4IGJwx+lb06TdSdl8Rlvtuw44xGbN2Vd9l89lMeGc5eh293a3F7yPyHXan
495YM0kFfU4qlZKAXI5rBTwhIHJ4ZZohZuQjy9PwdYs92JnH257OS3zTjTpavwmNnsf6vKHm9luNpBs
496aBaitdju0G2cF+G5O9+esDDSOc60hnL9w4RnzASP6untxWraYzM+zNueX36eeM8cMcUpNKRHtRfF
497I2TPJdMfsw6s3U8kvxyUrRaK1bsNqNVFabZzbexVhYZBkOGZmBpfupC45LujwfDD8Vi+Zt5rqUpd
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
546jtmnTkupNKzti7V/PZ5orPVlOT0c1TyansNfQt3nqUbmzrAFSrtLV6wIFWtPMx2LEcMzRyvGDiOX
547w7O4u3TGbaTMRPz8PuxTLbE17WcYtmY3W8fvxj2TR6jwyWcZd9rjnlng1mxevUKxIc0rRHWhn4lL
548I5SHgpiw5O74UnGy2ds17rro80E4XzGz2e+HDmK74tWksAVHd+PXdm0jg+RuDNdtN9JN3Y7BRyH0
549bAPhvXopp48lk9ET319c9a6n3ro646IpMeiI6nKi2fkew3LacJHnry29wYDNsbOuKQq9xgCILFeO
550aXEUb5aMXFsfgKacVtid1sd911Z45R0V4rqzS6YjbMfksmnCtZnjTPOvo70e1j/dbt4trYitXY9f
551fA5oJXnFxEZWAXlIInMxBmEycWyTOsa8xSKfL24VnrdPDRMakfvelxNrJb09OSLW7G3L9z43et2+
5525YkleKWCKP7exFksQuTmbM0fEXx6dF115xvjdMds3Th1x5nLwkRMac75iOqm7hh24rA2NnV29a/t
553Dt2qFqzTgo3aN82CApRjjaCzRcgjPlLnmeDLBe2Om6RzzbGdb+iac09VIjthyif9uLvlt6caY8az
554P2Oz51XG4VPW1JrUe7vc46hVrlusEMTYea1KFeWITaJnbjy9ScR91wsit3Db0euco7crXeZpbXs4
555z6ozn1y4W22NrVeMeeQvsrAya0QioWJ7BvMGaELRuMhFy5HJl8t6ln3XS2eabJpnqf64w7O4i2k0
556/wCP/wBWPaj30myebym9Hs7sE+tua4KAxWDGKJpYq3c/ocvGfPuPkZBIffGcqaf3eOpNvVWIYr7P
557Rpc3X7fqem8bGap5RvdW1mxPTgip2IRszSWCA52laTicrkTCXbZ+OcN7MykY2dF0x1ctk+mS7C+O
558NtevmuhFvqFEPPvGLwV4huyvciltMAtKQDWJxAjxycWd+jZTSmk3Rvsn82masVi2fnj8uo4G9Y/2
559R+8LvY/aP3Mf2P8A+k/8rB9lw9/6/PDH6+VdLLT/AOzHp5//AEU/hav966uX0+7lur/NV3vPbkEm
560maoE8RlXu6wtvBzFyjqyW4+byiz5ECEXzno7ZTT/AKls7Oae3lnl668vczj9Oa+9yf8A8u6qOO3r
561qXlXlFq4QDqoa+uOdybkH3Y91+g4fMvDs4Zmz9OPZS2aWceeafhty6697V0Vvj9zH8V32rviGvvN
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
690QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA
691QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA
692QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA
693QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA
694QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA
695QEBAQEBAQEBAQEBAQEBAQEBBpPPBXgknnkGGCIXOWWR2EAAWyRET9GZm9XdJlYiuEI5r9GGsNqax
696FHWNwYJzMRB3kdhDBO+H5OTMPxVpNabWYmJiuxOoqhqt7rdqU7UDklCuXApnhmCEny4v2pTAY5cO
697Ls7xkWFaYRO8nCaL6gICAghu3qVGsdq7Yiq1o+sk8xjHGLfiROzMlViHP/tf4k1H9oftuh9hzaL7
698v7qHs9x2d2DucuPLDP0yrNswzE16l6hsdfsao29faiuVTzwsQGMsb46Pgwd2dJiYzImJyVZvJvG4
699KA7GbbU4tecjwhcOxEMLyM7s4NI5ceTcX6Z9lN3Fd/DPgqD574MYmQ+RawhjblI7Xa7sI5Ycv8/R
700skzK8spzQsz+V+LV6le7PuKMNO3n7WzJZhGKXD4ftm5cSx+DpNs1ptImJiux0YZoZogmhMZIpGYo
7015AdiEhfqzs7dHZ0mKZrE1Qa3aUdnXezRk71djONpeJCJFGTiTg5M3Ict0Iej+zpTCJ34m2Y3LSgI
702Kmz2+p1VdrO0uwUK5EwDNZlCEHN2d2FiNxbOGfonAba/Z67ZVmta61DcrE7sM9eQZY3dvXBA7srM
703TGaRMSsqKjmtVoCiGeYIinPtQMZMLnI7OXAM/UWBd8N8EjcJEFbY7Klrar27snarsccbnxIvmmkG
704IGwLO/UzZkjGYjbJsmd0V7DY7Klrar27snarsccbnxIvmmkGIGwLO/UzZkjGYjbJsmd0V7FlAQVr
705OypVrVSpPJwsXjOOqHEn5lGDyE2WZ2bAC79UiK4cK+aPTBM0ivGnl2LKAgIKG13+h1DRvttlV17T
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
930bdNduV1KcMOvGtW7qzPL8sU64z40nqwpTOZ7fn+ugvWfF45zmFi2og7155q74KrO74KE4yzkWw+c
931t1+LrGn/AFP4bvMsz/tz/D+a3y73Ifb26/i8IFflG0/kw0gc5zeYom2rD2eRFzJux+r/ADfwW9P2
932rtPjbNfwXenvNSKRqU2Up/K73hEctgtnfs2rNicNlsK0QSzyFEEIWiYQGLl2/l49Hdss3Rnx0WY/
933p28bfTK3+/MbqfkteY8xcdjo/MrexuzxS62Z6lOoFmWCEI+1EUfOEDGOV5ikd8yMXwb0V0c9Odt1
9348d2pTzRXvTU+/GyLJ77K17a27sOlnd7jff2mu6qvM5VLW0grO0tyakAj+zAmGALEQTSQ92Tr8gs5
935P0z8ymlFYx+fum3tpEzNC+aREx8NvfN+PdEV49Ex67xWtt6usvV9xZido55Ow0duS4deB4xLty2Z
93644ZCISciZzbPF26upq0mzqms5bZ80YdS2RMXZbsM/KrztefbeJ09LqXjo7vXyOVfTWYWeO0xhBJJ
937GZRv3QkywYOQDH1zhNS+Zrsvi2Z4YRu2d+4stjOvszdFf4rt+2mfVVV2UMc/7uquyLZ2bN7avrZb
938JnORi8pXIHN4oTco4e2RY4xizfzsrrdbFutbbHuxfH7a8ezcxZdM2XXThdy3YbvZu9nq7cE+2nva
939i9vYat629LUDqttiaxNMTRlPK1wOchGbxlDBng78W9mWLJikTOXPNvVNtsd03VW+MZiNtkz12zM4
940dNKUhTDfeQFZjrNZnll31gNzqwYyFxpxNLK9YcOzsDhXhY2+Mr/FZmJttnD2tO2Znrtw6aXzMdFs
941N4TNa0tvmLY6roiZj96z2u1UCfy8/FP7QjeihC1rLctucdrasSTSPTkMexUOCKGtLFMLPiIm4sxN
9421W9aItrEZbO2Ma50p1TXJNCt91szGPNFfTbTynDOXoqtFtt5JTenbvgGuCG1uZxv3OycxRs8VRoO
94372OrYkl+T0w36zrU+zddP3a3RHTt6rfP+7MONuNlvxTETPR67vNWdsS7Wxt2IvONNB3jjqTUb7nF
944ydozkjOu4u454uQi5Y+DZXKyntV3R55r6He73Y/e/wBMvJaK1a3VrRQS7O3JStyb8pCr2pY+7HDe
945EYP6WMhPiAP8jiTYbo3Rbtt3/wBqyevBm6aTd/2U6uW5L4/cu7ezrdPs79kKUUe0dpY7EteawdG+
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
1024/wDHE/ztasVik/Fblx0q+XXvl7HzStuLEFGPWl3Gadzt0I7Z0J7MLRk3GKxH84uJuJuzO2cYd2XO
1025PexypPox8t7Wzy7HkPHBpXvJrG0qftEp4NQMletau2HkeavctRFFJwlcJgEwYW5chf16u7u+r7pt
1026077o+WYw32YYeXBIti66yJw9q6Jx3TZt8t05UUZP7UW/DZN1Lag+1v6q4WwItpZtPZc6UhcYqcle
1027KCCSOVsuMRNxZiZ8q69tttbdmFNv3oxrup1TXJfDzddfbOU80V4b4p5ThnK95DVlq0LeujtW5q2x
10288YvW7EUtiY/6es0LAUbcv6JnaZ2II+Iu3qyviP8A3Plut75ur5oZ8J/7U/Fh3W+Vc3vfGqNanoqs
1029daSWSKSIJWOeeayWSBvQ5jkLj8GZ8fBTxk43Rur6XPw8exE74jzPn3jJWtV454fc1tmzavXo5Ip9
1030ec5nDJCFaWX5YHftx9qQAbkIs/sTvlNa6YrT+3XriLad+HW78sTdMzh/uTHbfNe6t3UgCfy8/FP7
1031QjeihC1rLctucdrasSTSPTkMexUOCKGtLFMLPiIm4sxN1V1oi2sRls7YxrnSnVNcmdCt91szGPNF
1032fTbTynDOXtPKLtuDxClainkik7+t704mQlwO1C0nImfPEhd2LPt6rV0R9aI2c0+lz0Zro128nocH
1033yzcXv2t5JXp7GaJq0eiAWgldnhknvyDLxZndhI43Hl06tjPRY0ory126tOrlt9NW9TD/AMV09eKW
10349BuodlvdFqL8riMettQR3LkzSE88szWIIrRvLLF3Qr4Hj9L/AE4UtxtiZ2XzHVyRPXSZr0LdhPTb
1035381PsdzxHZV21GxcorsB62eSO5Wu2CuyRmEQSEMU7nKUgcSZ2yWcu7dPRTWuiLObZSenCZj0Jp21
1036v5duHe8La2+3qVWsVJrNWrtdJsbcLz7Se7aPt12khneIm7dYxz/9k8dcezK6kTHNbOdsR1e1EZ57
1037+lvSui6+y6Mrr+6kzl1Rxjreu0rWaPlWtqjcs2IdnqJbdobM8k7PPBJXEZAaRyGPLTlkQYR/Bdb4
1038iupHwzFOvnr5oeeyZ5bLtt1a9keXnUvIZPItn5be1NVxGKnSgnqC+zs6wuUryMc7NWgm77CQsLib
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
1069RkMpTaMWFikkfkZvjGSJ3y7+6nBVKx4147Z2AbGxqqc2wB2ILkleIphcfR2kcXJsfmrbMxkTjmnP
1070Uao9gGyOlAWxjB4wuvEDziD5yLSO3Nh6+mVIwrxzJxpwQVvGvHasc0VbVU4IrBjLYCOvEAySRlyA
1071zYRZiISbLO/o6tZw4ZExjM721rQaK3VKpb1tWxVOQpzrywxnGUpu5FI4ELi5O7u7l6qbuBXPini1
1072uuiminiqwxzwRfbwyjGLGEOWftCTNlgyLfK3TorWceKUwpuWFFEBAQEBAQEBAQEBAQEBAQEBAQEB
1073AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB
1074AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB
1075AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB
1076AQEBAQEBAQEBAQEBAQEBAQEBBX2Owqa6hYv3JGiqVYymnlfL8QBuRPhuvoykysRWXJi8qleGeezp
1077b9KvFAdkJrL1AjMI25O3NrBDE7t1/puH44w61dFM/L0Jb7UxTb5dPcvyb/SQ2KtWxfrV7lwRKrUl
1078miGWTl6cA5ZP/q5V5ZrMRsZi7CJ2Ss3b1KjWO1dsR1asTZlnmMY4xb0yRE7MyzVuIVD8l8cCpFcP
1079a0xqTAUkNh7ETRmEbsJkJ8uLiLkzO7emVeWa02+vJImsVhuO+0ZSyQjsapSwjJJLG00bkAQlwlIm
1080zlmAvlJ39H9VNldh6f2oYvJtNZjqy0LUF+vbsfahPWnrnG0nAjxl5B5PgfpDkX4Yy7XlmtOFexKx
1081SZ3fsVdz5jqaNK1NVnr37FKevBbqRThziexOEGZGHm4ceecO3XCWxWbd100qs4V3xbM9kVdGpu9L
1082cqzW6d+tZq13IZ7EM0ZxxuDZJjMXcR4t65UnCKzkRjNNqKLybxuamN2La05KZG8Q2QsRFE8gi5uD
1083GxceTCLu7Z9FZiYzIxyW6GwobCqFuhZit1ZM9uxAYyxlh8PgwdxfDpMTGaRMS1/ams7Pf+7h7Pd+
108437vcDj3ufa7XLOOfc+Tj68unqkRlxWcK8EIeQaE7wUA2VUr0jmIVGnjeUiid2kZo+XJ3Bxdi6dEi
1085K5E4Zsx77Ry7I9XFsap7OPLyURmjecWZsvmJn5t/AkRWKxkTNJpKz93U78lfvR9+IGllh5NzGMnd
1086hMhzlhdxfD/g6lcK7Fp3qNjyjxqtFHNY21KGKUI5YpJLEQiUcue2Yu5MzifF+L+/sryzWm1K4V2K
10875+ZeOx+Sf2dluxRbJ4opowkliHuPMRCMYM58yk+Xlx4+js/ults3Vps/b3bUumLaV+99nnrh0Sth
10885F4+c9qANnUKeiJSXomnjc4AD6ilHlkGH3clNldi0xptbRb3STEIxbCtIRzPWARmjJ3nYO48TYfq
1089fD5uPrjqryz5cM+xKx5u/Lt2LyiiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC
1090AgICAgICAgICAgICAgICAgICAgICAgICAgp7mOrLqrcVuqV2rJEQT1AHmUgE2CFhy2css3RE5tWz
1091MTg+eXK20OltaOgfb2dGeovR2KmzgsM4WHiYa0dV7cYWpCL5mdmchxj3wt31m2ebhTfWuOWym80a
1092RfZTD2sd1P20yRj47dkl2ut21jcwR7Y4jgjoVa8sM0X28YAJWDqzFXkiKNx/pJQZuhMu3NHNhnbf
1093M/zTdE8cKccKbnCyJi2K5TZEfy0mKdNZ3Y76vTeeUNjMekt15LYVKFspbhUYorFkGKE4wlCGWKw0
1094nAi6sMblh8t6LjZNL6z8Mx11j0RMfZV0mPYpG+OyK+mk9W+jl6XQDF5XqdhCOwuV5v2lamubGuEJ
1095DNMFUGfthDX7PPtlhijEnfk/XOVu2aVjCPY/119KXYxX5o7rbo9XcgtaHbSeKbMK9eeGZ/IJr08c
1096McbWJqwXnk5RjOJBI/BmIGIXYsYWbJpGnXZGPD3vTMTv2w3fFZviNsW0/DZWOvGN2/ahtaGzat09
1097jrrG2uWrV+EZ7mwqjV7XZqWwCTshXqGzCUosUhhh/lZn6KXRNKRSK239s2xHoS2YznGnL2RfbPrT
1098WKr2f3f1NGGnsjsNf+z4bcB1ZOLOFqDv8JHHhMJcCMijcmx1LC63XROrbfHu80dUbursYtibbLrZ
1099xu5buueWcevtxZ8u0e5s7ndHRhsDWcNLPI9eMHKYa1icpxhaYThkkAGAuLs+cM2OrLnpzSImf7l0
11009tkRE/i82GMNXxXCPgp/NWnXFY68cGv7CazttZtIS2uzebaVSuTbKoFZhGtWtMJ9ka1Qm4vILPIY
1101Yf5WYui3ZPLdGUR7c9c2xHoScbZ30tj+eJ9b0/ilWzX2Xk3dhOGGbaPLWchcRMCqV+RhlvmZ5GLq
11023vlYj+nbH7357lu9+Z4W+Z5axFsIta2kbXXZLgeSRXDkCvK8DVj2jWWmabHbIe2/zMLuQ/rMzM7r
1103WjnZPw2zE9PJdHl6zWyv+aIp/KmfT3h0U3CjKNovKht9IiaR4f2oLvN0bPDs5fl6cfwU0p/p12W3
1104flv+w1cfqcYtp1RZ6YWNBFfp+Rw0deNqxqHs2rFuDY0DhemUvcNzr3XGMJecp8eLOZcS+rDJpzW3
1105HZbERv2YTHR5jU97DObsd3T5b9i9tTsa7yzYWypWrMOx1kNeo9WCSZnngknIozIGcYstMOCkcR/F
1106cborp327Zx/lp5bdzrbPtWTsivnhyPDtJcjep95QkBx8UoUyeaImxKzy92H5m6G2W5B6+mV38XNY
11071qbbsOOEuXh8J067Ju89rXxqttqRatrNe3BPe8ao0IrH280nZuQ9xyGdwEuy491nzJhvXqr4mOad
1108WLc7prHZdtTRpbGnMxhbN1eubaeZyg8ctTeKfZznu5dvqdZbiDWyVII6wTSU5ICGOeKrF9wJkXys
1109ExkT8XJvVTWuia3W7cONKxNKcKbqbmtCJi62Lpyuiftr1znNccX0FhpVKugrzaqSwbFHHWKOuJjT
1110kaAv6U3fHZHDOHJvd8e6upNdWafNj6OvvcdOKaVtY+HDjv6ncXJ2EBAQEBAQEBAQEBAQEBAQEBAQ
1111EBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQ
1112EBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQ
1113EBAQEBAQEBAQEBAQEHLk8m00E1kLdqGpHWk7Tzzz1xApBi70gt/SOQlGHzExiL46+nVemPCakxE2
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
11743TERWkThM283L0x6JZ+pGOOEV/l97snDv2uuO01hRRTDcgeKeV4IJGkBxOVncXjB84I+QE3FuvR1
11755fo31mKTWIrls39DXNHZ66efDpVv7TeN/tMtV+1af7UF2EqH3EX3DOX0s8XLnl/boun/ANTW5Ofk
1176u5PipNO3Im6IwlvV8g0NvYz6yrsqtjZVmzZpRTxnPG2cfPGJOY9fiyl/htW2yL7rbosnKaTSeiSb
1177oiabTa7/AEWoaJ9tsquvacuED25o4GMv5odwh5P19GU0fDaurX6dt11M6RM+YmYiKzk52p898T2u
117862Wmo7KvLf1T4sxDNC7vgWKRwFjc3GLkwmXHDF09WdejW/xuvpadupdbMW35YT1bKY7N8YpzRzcu
117939uHTgvVPJfHLlGe/U2tOzRqu42bcViI4onH6mkkEnEXb3y643+E1bLotusui67KJiaz0Qc8Y45Z
11808GtryvxepUe5a3FGvUaV672JbMIRtML4KLmRMPNvcfVWzwetddy22XTdStItmtN/RxXmjHgvT3ad
1181eodyxPHDUAe4diQxGMQxnk5u7CzfiuNundddyxEzdu2nNFK7EFHd6XYPZahfrW3pn27jQTRydk2b
1182PGTg78Hx7Et6nh9SynNbdbzZViYr0byJrNNrjbb95Xg+t057c9zTsUgsBU517MBs88hMLR8ubAxC
1183z8iyXQWd36MvXo/4nxOpqRpxZdF1K42zlvy6uM4JN0Umfh8qdM7HoadypdqxW6c8dmpODSQWISGS
1184MwJsiQGLuJM7ejsvDqad1l023RMXRnE5rExOSVYUQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA
1185QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQfH6+m8obU6vZx6yb7+LdWS1tKYH
1186/ovvJ7MklywOP6MXOQer9Wjbp1NxX6m7X0ee6yb45Z0reaY28ttkRbbvnCf4uFtXGYml9fapfWOP
1187t4dXLSInZ7U5UWLvht4bfkuvgqWD1oamAZrZA7lfsRjZn7YiLZkeSxZeSbDYd8D15Ozc9Px1vLpX
1188zMc/1JpHwR7FteFLbaW9uyG9O2fqxWaxMRX8V3N2+zHC3D4Up6DyOSz4xsGpShubk5STTOLOOurx
11890Za9cZXfLM8QWDPh15Sk7fT1aR4jSiNWzmj6dsfjmb7brqdPLEV2WRE5uFsXcls7ptinCLbs8vvU
1190uu/DnEJ/HdbY081izZ1V2XXanaTwaanFC8k8012ftFedicfkCKT+tJ/R5SfPR35+J1Y1YiIvti+/
1191TibpmaREWxXk6ZmPdjdZDrdbSbt0Uu/enljt9c7OVJtru4jt+VlHrLtjyC0ceu0krVJTrxU5o444
11925Rn49ngM8hyzMxcunVsCymjp6c26VbrY0ordf7UVm6JmZimdeWIttwpunFZvm2+bqV5Y9njhXqmb
1193sJ4RGaSppb2po+SbTV6wyt6jXjp/F6xA7SFBRruTPExMz/01g3bp9TAP4LOpr26t2nZfd7Opfz6k
11948brtvRbHVWTR04tuiJxiyIjpnOZ68LZrtiXLvQ7UtRTq+Pa++NbxfTST66xYqywyzX5onqxFHFMw
1195SFLFH3yISFnciH4r02XWfUuu1brK62rEXRF0TEWRPNNZjCkzyxExOUTuYtm7kikVuxumuFbojCJ/
1196eumu7DDhbafU2NjSipa/YBo/GKJTNwpWBuPav5rBIEJg1h5I4e8cmY+T8mLr78uW+2y6brrPqa11
1197Pet5eW32piZieWkzyxGNMKYLb92Ixp7c14ViK76zMz0wueF6HyAtTJrm2dqpqqIQQ6Da/YwU9l2W
1198H+mjlhtwSiwZEG5FABE7P09Hfl4/xOl9Tn5Lbr7qzfbzzdZXZMTbdHH70xHcacTGEe7TbnWs16cN
1199vGVeWvty8s7+pkv3LBtX12+rbfXca01OsUnOeK00deHlIxk7DE5C5O2QFs43F1n0KXxZbGN1k2X+
12001F00pE21unCmd1JpHvTtt0TExSfaikcKVxyymk9dIzVKB2S8V0Ut/W7D7Wzs5bnllT7G0c7TSDJY
1201GJ4Gi7s0AWCjDlGBC4i36uV11LbY174tusrbpxbpzz20pFLa1rS26beaaTMTWd7M1mLsM7sf3dlN
1202+EW2zStba5rl/QX9zvClsa+SGhv7lQLcJg7M2t1InOD2GZnESs2DYOBde30fqzi3LS8TbpadIuib
1203tK26Y/f1KW+z+7bjWPvZbJm3xWsxti2zqrN108ImK2+qrWfVbS75lugtXt1rJZXCrrI6FKrNTPXj
1204CD9LVmnZiiIpSk5g8oO+G+V8MrbrWWeHspbpXxndzXXRdz1n7tt9szhSk8s9OazMxfXKlOWnf0TX
1205DZhEL1imO00ui8aqay7Bpo7rVbn30Tg70tS7uJH65CzLDGI8scwd3xhcbdT6epqa111s6nLzRyz9
12067U9NsTNae7MJFnJp/Ttrst6ts9FIm2u+cN7h+R6va2rdzYyQX6WrvbWOpckoVO/bHXayCRq7/anD
1207ZeSOS65F0hLIOPTj1Xr8JrWW222RNl19unN0c11Lee+Yr7XNbSYs+aPartNW2azSPht/hxumY655
1208ZjGsRK4OkOr5ho7lqTb7OveN7E+wtVQd2mqRvDRhlio1oBhD/wAZLJzmAcODcnbouf8A9jm0NS22
1209NOybYpFsXbLprfMTfdPNPsWxS2ZwnDazdbldjNZiJw2W1ujClfemOzc+mL889AgICAgICAgICAgI
1210CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgrbLV6
1211zaUpKGzqQ3qM3HvVbMYTRHxJjHkBsQvghZ2y3qumlrX6d0XWTNt0bYmk9sJMVzcL+7D92v8Aylpv
1212+H1f9Gvb+seM/vav47vWz9O3dB/dh+7X/lLTf8Pq/wCjT9Y8Z/e1fx3es+nbug/uw/dr/wApab/h
12139X/Rp+seM/vav47vWfTt3Qf3Yfu1/wCUtN/w+r/o0/WPGf3tX8d3rPp27oP7sP3a/wDKWm/4fV/0
1214afrHjP72r+O71n07d0H92H7tf+UtN/w+r/o0/WPGf3tX8d3rPp27oR2P3UfuxsRPFJ4pqRF/V46U
1215ERdP8qMBL+Nat/zXjLZrGtqfiun0n07d0Nov3WfuzijGMfE9O4i2Gc6NYy/SRA5P+l1J/wAz4yZr
12169bU/Hd6yNO3c2/uw/dr/AMpab/h9X/RqfrHjP72r+O71n07d0H92H7tf+UtN/wAPq/6NP1jxn97V
1217/Hd6z6du6D+7D92v/KWm/wCH1f8ARp+seM/vav47vWfTt3Qf3Yfu1/5S03/D6v8Ao0/WPGf3tX8d
12183rPp27oP7sP3a/8AKWm/4fV/0afrHjP72r+O71n07d0H92H7tf8AlLTf8Pq/6NP1jxn97V/Hd6z6
1219du6D+7D92v8Aylpv+H1f9Gn6x4z+9q/ju9Z9O3dCWr+7r931SzDaq+MamvarmMsE8VGsEkcgPyEw
1220IQZxIXbLOyzf/lfFXRNt2rqTE4TE33Y968lu56FeBoQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE
1221BAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE
1222BAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE
1223BAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE
1224BAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE
1225BAQEBAQEHwOx5Z5NPMcpbS0Lm7u4xzGAtn2YRdmZl+ijw+nEU5YfBnXvmc5R/wBpvJP/AHa5/wCo
1226l/7Sv0NP4Y7E+tf8U9p/abyT/wB2uf8AqJf+0n0NP4Y7D61/xT2n9pvJP/drn/qJf+0n0NP4Y7D6
12271/xT2n9pvJP/AHa5/wCol/7SfQ0/hjsPrX/FPaf2m8k/92uf+ol/7SfQ0/hjsPrX/FPaf2m8k/8A
1228drn/AKiX/tJ9DT+GOw+tf8U9p/abyT/3a5/6iX/tJ9DT+GOw+tf8U9p/abyT/wB2uf8AqJf+0n0N
1229P4Y7D61/xT2n9pvJP/drn/qJf+0n0NP4Y7D61/xT2n9pvJP/AHa5/wCol/7SfQ0/hjsPrX/FPaf2
1230m8k/92uf+ol/7SfQ0/hjsPrX/FPaf2m8k/8Adrn/AKiX/tJ9DT+GOw+tf8U9q/s7PneraMr9u/XG
1231XrGRTyOz++MsTtn8Fzst0b/di2ep0vnVtzme1VLd+WjWGyWwvtXMnAJnmm4OTerMXLGVv6WlWlLa
12329EMfU1KVrNEX9pvJP/drn/qJf+0r9DT+GOxPrX/FPaf2m8k/92uf+ol/7SfQ0/hjsPrX/FPaf2m8
1233k/8Adrn/AKiX/tJ9DT+GOw+tf8U9r0n7vfI95N5RWqWb09ivYGQZI5pCkb5YyNnbk74fI+y8vjNG
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
1264ojMcUUY8YwEB9cCzM38SCxSu2qVqO1VkeKxE/KOQfVn/AEqXWxdFJyW26bZrC5F5Hu4tpJtI7Zjf
1265lZ2knwLu7OzNh2duOOjeyxOjZNvLTBuNa6LuauLnyyyTSnLKTnJITkZv1dyd8u7rpEUwc5mrVUEH
1266pf3c/wDzPX//AK7/ALiReTxv9KerzvT4T+pHlsfbV8F9oQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB
1267AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEHzqf9z0JTGUG0KOJ3dwjKHm4t
12687M5dwc/wL6cf5KaY29750/4+K4Sj/ub/AN7/AOz/AOtV/Uvl7/sT9P8Am7vtP7m/97/7P/rU/Uvl
12697/sP0/5u77T+5v8A3v8A7P8A61P1L5e/7D9P+bu+0/ub/wB7/wCz/wCtT9S+Xv8AsP0/5u77T+5v
1270/e/+z/61P1L5e/7D9P8Am7vtP7m/97/7P/rU/Uvl7/sP0/5u77T+5v8A3v8A7P8A61P1L5e/7D9P
1271+bu+0/ub/wB7/wCz/wCtT9S+Xv8AsP0/5u77T+5v/e/+z/61P1L5e/7D9P8Am7vtP7m/97/7P/rU
1272/Uvl7/sP0/5u77T+5v8A3v8A7P8A61P1L5e/7D9P+bu+0/ub/wB7/wCz/wCtT9S+Xv8AsP0/5u77
1273T+5v/e/+z/61P1L5e/7D9P8Am7vtP7m/97/7P/rU/Uvl7/sP0/5u77T+5v8A3v8A7P8A61P1L5e/
12747D9P+bu+0/ub/wB7/wCz/wCtT9S+Xv8AsP0/5u77T+5v/e/+z/61P1L5e/7D9P8Am7vtdbxj928O
1275k2obE7z2pIhJogaPtszmLi7v8x5+V3XHX8bOpby0o7aPg4surWr2a8L2CAgICAgICAgICAgICAgI
1276CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI
1277CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI
1278CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI
1279CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI
1280CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI
1281CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI
1282CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI
1283CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI
1284CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI
1285CAg81e2k4bO1Bc2MmpjEhGiXaB4ZGcWdzOWQDbPJ8cWIVLcemq3YTwWp/IJ42tzQ1WsUte/C3Y7n
1286A3cWYpHij4kxcGfrkm/BWu2cImfs86RGzOaObb2F79sSDBMZB+0K4RxvIQg4lTc+D4zgSLq/T8VL
1287axH/AJO6PQzM17LfzJaHk9uHSUbGxaD7i5gYJDnaICwzuRyu8YjGzM36vLK3dnEcPRDUbZ4+mU1b
1288yyW5NDWp1Yp7EhzAZDYZ4WeFgLIyiBchIZG/Vz+CzGPZXvokzTy4OrrNi9/XDaGLhI/MShd84kjJ
1289wIeXTLch9VLpwrG6rUZ0nZLj6baTWZoQtbSSHYvkrGrlhjib0fIxcgE34+vLmSXTERMxjgzPHDFF
1290B5pG1ODrCU/24zzfd2Y4CflniIYjZjN+P80W9Fq7Du81ViPPPno7w3pbGthu0YwkaYBlAZzeJmAm
12915dSEJerN+Cl8csls1UNV5FPdsV4pagwjaGY4ZQleQSCFxFjbIRvg+XT8PzVjHsie1JmnbRQs+TMN
12923W2jY44j+9h+1AnJ5ZIpBijZm+VnIn9PhlZsmteNsU65W7CZj4bvRLo/tC5PalrSMdOavUaxIMRx
1293yNyl5Mw5OJ+ocOjt0+LOpdNIun4f2/YtsVutjf8AsQQ+SXZghatSGUyox3jeSbhhj5Nw6Rvkvl+D
1294N+S1dNOadlv7WbMaRtmvc5m122ztzy2qeQrQ6wb1dvuJInZz5FzMABxkceGOBPx/hUuw5uExC2zW
1295kb6+h0ZfILNIaUuxHg0leaYxgNjEu2MbjnlEBcic8MzOzN+Ptu6PauiNn/qozZNYifLKq7T2189m
1296FC5TCucld7AkEzy4ZiEeJM4B1+b2d1N/Bd072m12ttjuVKEDSyVoHksTHK8TBzZ+DBgDciwLv7N+
1297K53T7My6WR7UcXNr+S/Y1az2jKR3oVJGeSQBEpZicMkRDyb0yREbtj2XW7O796ndMuVuUdE+j1pG
129882heT7dhrFY7gg0gWmKrgwImd52Do/yO3Hh6rMY+XR62pwSWvLftzeGSOtHajj7s0c1sIxdnd+Ax
1299E4v3HIRz6N6tlSufDtWIy4n9rZDhtW4KTHRqDFLLKUvE3jmjGTIhwLJCxdWcm/Napv8AiozE1yzp
1300Xz+pd8mt262llmpS9mw5QjHLxYsdyUQd8Ezt6Es0nmiN8rF0Urwq5ZeUywlTktl2xhitNsoBYXfv
1301V+DMzO/plyyPX3VrEzMxlSKdd1PsSk5ce6kykDzSN7EcJRwSObxu51rIziASSDETm7AOCEjHp6P8
1302VYis08tvqJmkV8vKnmT/ANrIn+4YaxOcNoa0Y8usgETs8o9Pbtn0/BZrhE9PdHN5qNTGMx0d8086
1303lc8huXtK9qAI4YTKuQzV7PckDlPG3bkFhBwJxJ8szv8ABaiPbtifihi6cJpulOXmdcp2CFq5xnM9
1304aJnsi07nycBJ4WEnYHP3znHXisTNba8PtbnCtdirW3fkp6vSzuEBHcnEDN5cPKzgb4MWhxH9PqOV
13050p7UR8voZrhPT/qot3fMq9SWcDavio4haB7IjK5YZzaGJxzIw59+OfZZjHtp6FmJ66OjqtpZvyWH
1306+2GKtBLLA0rycjIoj45YODdHb4l/jSMoneld3lhVVu+R2a82w40mkraxw+4leXiTiYCbuAcHy4sX
1307o5N+aW4xEzvotNkbq+f1MFv5Q2dmjFGViyU4RVojMQDDwtKZchDkIi3rnk+fRLcY67u6nrSu3hHf
1308X1IZdluY95MwVmN46ISyVSncYmdpJMkDsBZcmbpkW/HCkTS26d0x5lpWYjp9CSludlc3UY1wjfXT
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
1369oz5YFj4tlnEvwVv9qvGvekRSI6PMpQeGQQ1RrDYdopIPt7zCDD3uOXjkbr8hg/v16dFZn0dsUxX7
1370eydi3Y02yt0nq3L4yNmJxIIOGe3IMmTyZZJ+GPl4t19Eifaid01SYwpwXIdf29pZv9zP3McUfbxj
1371j2nN85z1zz+CkYV6fQt2Mxwj0q2x1ezuR2Kz3gGlZZxMHg5SiBNghCTmw/k7g6lK5rWmSu/i7BaK
13723Vs9mwMkclYnDmwMELQEBtybmxC3xZ1qs9899PUzEYU4R3V9br1orQRO1mZppSd3cgDtg34COSfH
13735k6zdFYosOTrvHLFcKMNm4M9bXZKvGEXbdzcXFiMnM88WJ8MzMrdj00okx56+lnW+OT6+WqcFsS7
1374Ncathjid+4Am5s44NuBfM/ryVnGsbJp3RQp5575aweN2oKtWKO6LSa83KhN2eogTOxBK3PEjOL46
1375cUrOE7aU83qXf01dMfvYKkhzO92x1fhCIRZ9uICZ4b/rH+lZvisUIeaqUNhXq6aD9mXS/ZBEXL/w
1376TdzlGcfp92/H68+6t/tT/Dy+b1GyY33V75lX1ms3mqCL9nU7YSdgILXdjpyBJ2nfgYi10HAmYnb6
1377nb8FZmvd5oj0H2981drRRXYtlemnp2o2vPGZSz/aswvFEMfXszSO7lxz0BmSMLacZntSmNeHr9bu
1378qKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC
1379AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC
1380AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC
1381D//Z
1382------=_NextPart_000_000F_01D15E52.0BD654A0--
1383
1384
diff --git a/framework/src/domain/mimetreeparser/tests/data/html.mbox b/framework/src/domain/mimetreeparser/tests/data/html.mbox
new file mode 100644
index 00000000..bf5c685d
--- /dev/null
+++ b/framework/src/domain/mimetreeparser/tests/data/html.mbox
@@ -0,0 +1,15 @@
1From foo@example.com Thu, 26 May 2011 01:16:54 +0100
2From: Thomas McGuire <foo@example.com>
3Subject: HTML test
4Date: Thu, 26 May 2011 01:16:54 +0100
5Message-ID: <1501334.pROlBb7MZF@herrwackelpudding.localhost>
6X-KMail-Transport: GMX
7X-KMail-Fcc: 28
8X-KMail-Drafts: 7
9X-KMail-Templates: 9
10User-Agent: KMail/4.6 beta5 (Linux/2.6.34.7-0.7-desktop; KDE/4.6.41; x86_64; git-0269848; 2011-04-19)
11MIME-Version: 1.0
12Content-Transfer-Encoding: 7Bit
13Content-Type: text/html; charset="windows-1252"
14
15<html><body><p><span>HTML</span> text</p></body></html> \ No newline at end of file
diff --git a/framework/src/domain/mimetreeparser/tests/data/openpgp-encrypted-attachment-and-non-encrypted-attachment.mbox b/framework/src/domain/mimetreeparser/tests/data/openpgp-encrypted-attachment-and-non-encrypted-attachment.mbox
new file mode 100644
index 00000000..2d9726ea
--- /dev/null
+++ b/framework/src/domain/mimetreeparser/tests/data/openpgp-encrypted-attachment-and-non-encrypted-attachment.mbox
@@ -0,0 +1,115 @@
1From test@kolab.org Fri May 01 15:12:47 2015
2From: testkey <test@kolab.org>
3To: you@you.com
4Subject: enc & non enc attachment
5Date: Fri, 01 May 2015 17:12:47 +0200
6Message-ID: <13897561.XENKdJMSlR@tabin.local>
7X-KMail-Identity: 1197256126
8User-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)
9MIME-Version: 1.0
10Content-Type: multipart/mixed; boundary="nextPart1939768.sIoLGH0PD8"
11Content-Transfer-Encoding: 7Bit
12
13This is a multi-part message in MIME format.
14
15--nextPart1939768.sIoLGH0PD8
16Content-Type: multipart/encrypted; boundary="nextPart2814166.CHKktCGlQ3"; protocol="application/pgp-encrypted"
17
18
19--nextPart2814166.CHKktCGlQ3
20Content-Type: application/pgp-encrypted
21Content-Disposition: attachment
22Content-Transfer-Encoding: 7Bit
23
24Version: 1
25--nextPart2814166.CHKktCGlQ3
26Content-Type: application/octet-stream
27Content-Disposition: inline; filename="msg.asc"
28Content-Transfer-Encoding: 7Bit
29
30-----BEGIN PGP MESSAGE-----
31Version: GnuPG v2
32
33hIwDGJlthTT7oq0BA/9cXFQ6mN9Vxnc2B9M10odS3/6z1tsIY9oJdsiOjpfxqapX
34P7nOzR/jNWdFQanXoG1SjAcY2FeZEN0c3SkxEM6R5QVF1vMh/Xsni1clI+peZyVT
35Z4OSU74YCfYLg+cgDnPCF3kyNPVe6Z1pnfWOCZNCG3rpApw6UVLN63ScWC6eQIUB
36DAMMzkNap8zaOwEIANKHn1svvj+hBOIZYf8R+q2Bw7cd4xEChiJ7uQLnD98j0Fh1
3785v7/8JbZx6rEDDenPp1mCciDodb0aCmi0XLuzJz2ANGTVflfq+ZA+v1pwLksWCs
380YcHLEjOJzjr3KKmvu6wqnun5J2yV69K3OW3qTTGhNvcYZulqQ617pPa48+sFCgh
39nM8TMAD0ElVEwmMtrS3AWoJz52Af+R3YzpAnX8NzV317/JG+b6e2ksl3tR7TWp1q
402FOqC1sXAxuv+DIz4GgRfaK1+xYr2ckkg+H/3HJqa5LmJ7rGCyv+Epfp9u+OvdBG
41PBvuCtO3tm0crmnttMw57Gy35BKutRf/8MpBj/nS6QFX0t7XOLeL4Me7/a2H20wz
42HZsuRGDXMCh0lL0FYCBAwdbbYvvy0gz/5iaNvoADtaIu+VtbFNrTUN0SwuL+AIFS
43+WIiaSbFt4Ng3t9YmqL6pqB7fjxI10S+PK0s7ABqe4pgbzUWWt1yzBcxfk8l/47Q
44JrlvcE7HuDOhNOHfZIgUP2Dbeu+pVvHIJbmLsNWpl4s+nHhoxc9HrVhYG/MTZtQ3
45kkUWviegO6mwEZjQvgBxjWib7090sCxkO847b8A93mfQNHnuy2ZEEJ+9xyk7nIWs
464RsiNR8pYc/SMvdocyAvQMH/qSvmn/IFJ+jHhtT8UJlXJ0bHvXTHjHMqBp6fP69z
47Jh1ERadWQdMaTkzQ+asl+kl/x3p6RZP8MEVbZIl/3pcV+xiFCYcFu2TETKMtbW+b
48NYOlrltFxFDvyu3WeNNp0g9k0nFpD/T1OXHRBRcbUDWE4QF6NWTm6NO9wy2UYHCi
497QTSecBWgMaw7cUdwvnW6chIVoov1pm69BI9D0PoV76zCI7KzpiDsTFxdilKwbQf
50K/PDnv9Adx3ERh0/F8llBHrj2UGsRs4aHSEBDBJIHDCp8+lqtsRcINQBKEU3qIjt
51wf5vizdaVIgQnsD2z8QmBQ7QCCipI0ur6GKl+YWDDOSDLDUs9dK4A6xo/4Q0bsnI
52rH63ti5HslGq6uArfFkewH2MWff/8Li3uGEqzpK5NhP5UpbArelK+QaQQP5SdsmW
53XFwUqDS4QTCKNJXw/5SQMl8UE10l2Xaav3TkiOYTcBcvPNDovYgnMyRff/tTeFa8
5483STkvpGtkULkCntp22fydv5rg6DZ7eJrYfC2oZXdM87hHhUALUO6Y/VtVmNdNYw
55F3Uim4PDuLIKt+mFqRtFqnWm+5X/AslC31qLkjH+Fbb83TY+mC9gbIn7CZGJRCjn
56zzzMX2h15V/VHzNUgx9V/h28T0/z25FxoozZiJxpmhOtqoxMHp+y6nXXfMoIAD1D
57963Pc7u1HS0ny54A7bqc6KKd4W9IF7HkXn3SoBwCyn0IOPoKQTDD8mW3lbBI6+h9
58vP+MAQpfD8s+3VZ9r7OKYCVmUv47ViTRlf428Co6WT7rTHjGM09tqz826fTOXA==
59=6Eu9
60-----END PGP MESSAGE-----
61
62--nextPart2814166.CHKktCGlQ3--
63
64--nextPart1939768.sIoLGH0PD8
65Content-Disposition: attachment; filename="image.png"
66Content-Transfer-Encoding: base64
67Content-Type: image/png; name="image.png"
68
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
112o/0d59+Lh3Y1HHcAAAAASUVORK5CYII=
113
114--nextPart1939768.sIoLGH0PD8--
115
diff --git a/framework/src/domain/mimetreeparser/tests/data/openpgp-inline-charset-encrypted.mbox b/framework/src/domain/mimetreeparser/tests/data/openpgp-inline-charset-encrypted.mbox
new file mode 100644
index 00000000..8bd06910
--- /dev/null
+++ b/framework/src/domain/mimetreeparser/tests/data/openpgp-inline-charset-encrypted.mbox
@@ -0,0 +1,40 @@
1From test@example.com Thu, 17 Oct 2013 02:13:03 +0200
2Return-Path: <test@example.com>
3Delivered-To: you@you.com
4Received: from localhost (localhost [127.0.0.1])
5 by test@example.com (Postfix) with ESMTP id B30D8120030
6 for <you@you.com>; Thu, 17 Oct 2013 02:13:05 +0200 (CEST)
7From: test <test@example.com>
8To: you@you.com
9Subject: charset
10Date: Thu, 17 Oct 2013 02:13:03 +0200
11Message-ID: <4081645.yGjUJ4o4Se@example.local>
12User-Agent: KMail/4.12 pre (Linux/3.11-4.towo-siduction-amd64; KDE/4.11.2; x86_64; git-f7f14e3; 2013-10-15)
13MIME-Version: 1.0
14Content-Transfer-Encoding: 7Bit
15Content-Type: text/plain; charset="ISO-8859-15"
16
17-----BEGIN PGP MESSAGE-----
18Version: GnuPG v2.0.22 (GNU/Linux)
19
20hIwDGJlthTT7oq0BBACbaRZudMigMTetPZNRgkfEXv4QQowR1jborw0dcgKKqMQ1
216o67NkpxvmXKGJTfTVCLBX3nk6FKYo6NwlPCyU7X9X0DDk8hvaBdR9wGfrdm5YWX
22GKOzcqJY1EypiMsspXeZvjzEW7O8I956c3vBb/2pM3xqYEK1kh8+d9bVH+cjf4UB
23DAMMzkNap8zaOwEH/1rPShyYL8meJN+/GGgS8+Nf1BW5pSHdAPCg0dnX4QCLEx7u
24GkBU6N4JGYayaCBofibOLacQPhYZdnR5Xb/Pvrx03GrzyzyDp0WyeI9nGNfkani7
25sCRWbzlMPsEvGEvJVnMLNRSk4xhPIWumL4APkw+Mgi6mf+Br8z0RhfnGwyMA53Mr
26pG9VQKlq3v7/aaN40pMjAsxiytcHS515jXrb3Ko4pWbTlAr/eytOEfkLRJgSOpQT
27BY7lWs+UQJqiG8Yn65vS9LMDNJgX9EOGx77Z4u9wvv4ZieOxzgbHGg5kYCoae7ba
28hxZeNjYKscH+E6epbOxM/wlTdr4UTiiW9dMsH0zSwMUB891gToeXq+LDGEPTKVSX
29tsJm4HS/kISJBwrCI4EUqWZML6xQ427NkZGmF2z/sD3kmL66GjspIKnb4zHmXacp
3084n2KrI9s7p6AnKnQjsxvB/4/lpXPCIY5GH7KjySEJiMsHECzeN1dJSL6keykBsx
31DtmYDA+dhZ6UWbwzx/78+mjNREhyp/UiSAmLzlJh89OH/xelAPvKcIosYwz4cY9N
32wjralTmL+Y0aHKeZJOeqPLaXADcPFiZrCNPCH65Ey5GEtDpjLpEbjVbykPV9+YkK
337JKW6bwMraOl5zmAoR77PWMo3IoYb9q4GuqDr1V2ZGlb7eMH1gj1nfgfVintKC1X
343jFfy7aK6LIQDVKEwbi0SxVXTKStuliVUy5oX4woDOxmTEotJf1QlKZpn5oF20UP
35tumYrp0SPoP8Bo4EVRVaLupduI5cYce1q/kFj9Iho/wk56MoG9PxMMfsH7oKg3AA
36CqQ6/kM4oJNdN5xIf1EH5HeaNFkDy1jlLznnhwVAZKPo/9ffpg==
37=bPqu
38-----END PGP MESSAGE-----
39
40
diff --git a/framework/src/domain/mimetreeparser/tests/data/openpgp-inline-encrypted+nonenc.mbox b/framework/src/domain/mimetreeparser/tests/data/openpgp-inline-encrypted+nonenc.mbox
new file mode 100644
index 00000000..b98dc336
--- /dev/null
+++ b/framework/src/domain/mimetreeparser/tests/data/openpgp-inline-encrypted+nonenc.mbox
@@ -0,0 +1,31 @@
1From test@kolab.org Wed, 25 May 2011 23:49:40 +0100
2From: OpenPGP Test <test@kolab.org>
3To: test@kolab.org
4Subject: inlinepgpencrypted + non enc text
5Date: Wed, 25 May 2011 23:49:40 +0100
6Message-ID: <1786696.yKXrOjjflF@herrwackelpudding.localhost>
7X-KMail-Transport: GMX
8X-KMail-Fcc: 28
9X-KMail-Drafts: 7
10X-KMail-Templates: 9
11User-Agent: KMail/4.6 beta5 (Linux/2.6.34.7-0.7-desktop; KDE/4.6.41; x86_64; git-0269848; 2011-04-19)
12MIME-Version: 1.0
13Content-Transfer-Encoding: 7Bit
14Content-Type: text/plain; charset="us-ascii"
15
16Not encrypted not signed :(
17
18-----BEGIN PGP MESSAGE-----
19Version: GnuPG v2.0.15 (GNU/Linux)
20
21hQEMAwzOQ1qnzNo7AQf/a3aNTLpQBfcUr+4AKsZQLj4h6z7e7a5AaCW8AG0wrbxN
22kBYB7E5jdZh45DX/99gvoZslthWryUCX2kKZ3LtIllxKVjqNuK5hSt+SAuKkwiMR
23Xcbf1KFKENKupgGSO9B2NJRbjoExdJ+fC3mGXnO3dT7xJJAo3oLE8Nivu+Bj1peY
24E1wCf+vcTwVHFrA7SV8eMRb9Z9wBXmU8Q8e9ekJ7ZsRX3tMeBs6jvscVvfMf6DYY
25N14snZBZuGNKT9a3DPny7IC1S0lHcaam34ogWwMi3FxPGJt/Lg52kARlkF5TDhcP
26N6H0EB/iqDRjOOUoEVm8um5XOSR1FpEiAdD0DON3y9JPATnrYq7sgYZz3BVImYY+
27N/jV8fEiN0a34pcOq8NQedMuOsJHNBS5MtbQH/kJLq0MXBpXekGlHo4MKw0trISc
28Rw3pW6/BFfhPJLni29g9tw==
29=fRFW
30-----END PGP MESSAGE-----
31
diff --git a/framework/src/domain/mimetreeparser/tests/data/plaintext.mbox b/framework/src/domain/mimetreeparser/tests/data/plaintext.mbox
new file mode 100644
index 00000000..d185b1c1
--- /dev/null
+++ b/framework/src/domain/mimetreeparser/tests/data/plaintext.mbox
@@ -0,0 +1,13 @@
1Return-Path: <konqi@example.org>
2Date: Wed, 8 Jun 2016 20:34:44 -0700
3From: Konqi <konqi@example.org>
4To: konqi@kde.org
5Subject: A random subject with alternative contenttype
6MIME-Version: 1.0
7Content-Type: text/plain; charset=utf-8
8Content-Transfer-Encoding: quoted-printable
9
10If you can see this text it means that your email client couldn't display o=
11ur newsletter properly.
12Please visit this link to view the newsletter on our website: http://www.go=
13g.com/newsletter/
diff --git a/framework/src/domain/mimetreeparser/tests/data/smime-encrypted.mbox b/framework/src/domain/mimetreeparser/tests/data/smime-encrypted.mbox
new file mode 100644
index 00000000..6b6d6a0d
--- /dev/null
+++ b/framework/src/domain/mimetreeparser/tests/data/smime-encrypted.mbox
@@ -0,0 +1,22 @@
1From test@example.com Sat, 13 Apr 2013 01:54:30 +0200
2From: test <test@example.com>
3To: you@you.com
4Subject: test
5Date: Sat, 13 Apr 2013 01:54:30 +0200
6Message-ID: <1576646.QQxzHWx8dA@tabin>
7X-KMail-Identity: 505942601
8User-Agent: KMail/4.10.2 (Linux/3.9.0-rc4-experimental-amd64; KDE/4.10.60; x86_64; git-fc9b82c; 2013-04-11)
9MIME-Version: 1.0
10Content-Type: application/pkcs7-mime; name="smime.p7m"; smime-type="enveloped-data"
11Content-Transfer-Encoding: base64
12Content-Disposition: attachment; filename="smime.p7m"
13
14MIAGCSqGSIb3DQEHA6CAMIACAQAxgfwwgfkCAQAwYjBVMQswCQYDVQQGEwJVUzENMAsGA1UECgwE
15S0RBQjEWMBQGA1UEAwwNdW5pdHRlc3QgY2VydDEfMB0GCSqGSIb3DQEJARYQdGVzdEBleGFtcGxl
16LmNvbQIJANNFIDoYY4XJMA0GCSqGSIb3DQEBAQUABIGAJwmmaOeidXUHSQGOf2OBIsPYafVqdORe
17y54pEXbXiAfSVUWgI4a9CsiWwcDX8vlaX9ZLLr+L2VmOfr6Yc5214yxzausZVvnUFjy6LUXotuEX
18tSar4EW7XI9DjaZc1l985naMsTx9JUa5GyQ9J6PGqhosAKpKMGgKkFAHaOwE1/IwgAYJKoZIhvcN
19AQcBMBQGCCqGSIb3DQMHBAieDfmz3WGbN6CABHgEpsLrNn0PAZTDUfNomDypvSCl5bQH+9cKm80m
20upMV2r8RBiXS7OaP4SpCxq18afDTTPatvboHIoEX92taTbq8soiAgEs6raSGtEYZNvFL0IYqm7MA
21o5HCOmjiEcInyPf14lL3HnPk10FaP3hh58qTHUh4LPYtL7UECOZELYnUfUVhAAAAAAAAAAAAAA==
22
diff --git a/framework/src/domain/mimetreeparser/tests/gnupg_home/CMakeLists.txt b/framework/src/domain/mimetreeparser/tests/gnupg_home/CMakeLists.txt
new file mode 100644
index 00000000..9c64a008
--- /dev/null
+++ b/framework/src/domain/mimetreeparser/tests/gnupg_home/CMakeLists.txt
@@ -0,0 +1,10 @@
1configure_file( gpg-agent.conf.in
2 "${CMAKE_CURRENT_BINARY_DIR}/gpg-agent.conf" @ONLY )
3
4configure_file( gpgsm.conf.in
5 "${CMAKE_CURRENT_BINARY_DIR}/gpgsm.conf" @ONLY )
6
7file( COPY
8 ${CMAKE_CURRENT_SOURCE_DIR}
9 DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/../"
10)
diff --git a/framework/src/domain/mimetreeparser/tests/gnupg_home/dirmngr-cache.d/DIR.txt b/framework/src/domain/mimetreeparser/tests/gnupg_home/dirmngr-cache.d/DIR.txt
new file mode 100644
index 00000000..1a45a6b3
--- /dev/null
+++ b/framework/src/domain/mimetreeparser/tests/gnupg_home/dirmngr-cache.d/DIR.txt
@@ -0,0 +1,3 @@
1v:1:
2c: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::::
3c: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/src/domain/mimetreeparser/tests/gnupg_home/dirmngr-cache.d/crl-4E31CEB57DDD4A7B9991AB05507B1ED4293FF952.db b/framework/src/domain/mimetreeparser/tests/gnupg_home/dirmngr-cache.d/crl-4E31CEB57DDD4A7B9991AB05507B1ED4293FF952.db
new file mode 100644
index 00000000..0b7e2dd4
--- /dev/null
+++ b/framework/src/domain/mimetreeparser/tests/gnupg_home/dirmngr-cache.d/crl-4E31CEB57DDD4A7B9991AB05507B1ED4293FF952.db
Binary files differ
diff --git a/framework/src/domain/mimetreeparser/tests/gnupg_home/dirmngr-cache.d/crl-7F2A402CBB016A9146D613568C89D3596A4111AA.db b/framework/src/domain/mimetreeparser/tests/gnupg_home/dirmngr-cache.d/crl-7F2A402CBB016A9146D613568C89D3596A4111AA.db
new file mode 100644
index 00000000..47474a26
--- /dev/null
+++ b/framework/src/domain/mimetreeparser/tests/gnupg_home/dirmngr-cache.d/crl-7F2A402CBB016A9146D613568C89D3596A4111AA.db
Binary files differ
diff --git a/framework/src/domain/mimetreeparser/tests/gnupg_home/dirmngr.conf b/framework/src/domain/mimetreeparser/tests/gnupg_home/dirmngr.conf
new file mode 100644
index 00000000..a17a0354
--- /dev/null
+++ b/framework/src/domain/mimetreeparser/tests/gnupg_home/dirmngr.conf
@@ -0,0 +1,8 @@
1
2###+++--- GPGConf ---+++###
3debug-level basic
4log-file socket:///home/leo/kde/src/kdepim/messagecomposer/tests/gnupg_home/log-socket
5###+++--- GPGConf ---+++### Tue 29 Jun 2010 10:23:13 AM EDT
6# GPGConf edited this configuration file.
7# It will disable options before this marked block, but it will
8# never change anything below these lines.
diff --git a/framework/src/domain/mimetreeparser/tests/gnupg_home/gpg-agent.conf.in b/framework/src/domain/mimetreeparser/tests/gnupg_home/gpg-agent.conf.in
new file mode 100644
index 00000000..ece69255
--- /dev/null
+++ b/framework/src/domain/mimetreeparser/tests/gnupg_home/gpg-agent.conf.in
@@ -0,0 +1,10 @@
1pinentry-program @CMAKE_CURRENT_BINARY_DIR@/pinentry-fake.sh
2###+++--- GPGConf ---+++###
3allow-mark-trusted
4debug-level basic
5faked-system-time 20130110T154812
6log-file @CMAKE_CURRENT_BINARY_DIR@/gpg-agent.log
7###+++--- GPGConf ---+++### Tue 29 Jun 2010 10:23:13 AM EDT
8# GPGConf edited this configuration file.
9# It will disable options before this marked block, but it will
10# never change anything below these lines.
diff --git a/framework/src/domain/mimetreeparser/tests/gnupg_home/gpg.conf b/framework/src/domain/mimetreeparser/tests/gnupg_home/gpg.conf
new file mode 100644
index 00000000..f1760823
--- /dev/null
+++ b/framework/src/domain/mimetreeparser/tests/gnupg_home/gpg.conf
@@ -0,0 +1,244 @@
1# Options for GnuPG
2# Copyright 1998, 1999, 2000, 2001, 2002, 2003 Free Software Foundation, Inc.
3#
4# This file is free software; as a special exception the author gives
5# unlimited permission to copy and/or distribute it, with or without
6# modifications, as long as this notice is preserved.
7#
8# This file is distributed in the hope that it will be useful, but
9# WITHOUT ANY WARRANTY, to the extent permitted by law; without even the
10# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
11#
12# Unless you specify which option file to use (with the command line
13# option "--options filename"), GnuPG uses the file ~/.gnupg/gpg.conf
14# by default.
15#
16# An options file can contain any long options which are available in
17# GnuPG. If the first non white space character of a line is a '#',
18# this line is ignored. Empty lines are also ignored.
19#
20# See the man page for a list of options.
21
22# Uncomment the following option to get rid of the copyright notice
23
24#no-greeting
25
26# If you have more than 1 secret key in your keyring, you may want to
27# uncomment the following option and set your preferred keyid.
28
29#default-key 621CC013
30
31# If you do not pass a recipient to gpg, it will ask for one. Using
32# this option you can encrypt to a default key. Key validation will
33# not be done in this case. The second form uses the default key as
34# default recipient.
35
36#default-recipient some-user-id
37#default-recipient-self
38
39# Use --encrypt-to to add the specified key as a recipient to all
40# messages. This is useful, for example, when sending mail through a
41# mail client that does not automatically encrypt mail to your key.
42# In the example, this option allows you to read your local copy of
43# encrypted mail that you've sent to others.
44
45#encrypt-to some-key-id
46
47# By default GnuPG creates version 3 signatures for data files. This
48# is not strictly OpenPGP compliant but PGP 6 and most versions of PGP
49# 7 require them. To disable this behavior, you may use this option
50# or --openpgp.
51
52#no-force-v3-sigs
53
54# Because some mailers change lines starting with "From " to ">From "
55# it is good to handle such lines in a special way when creating
56# cleartext signatures; all other PGP versions do it this way too.
57
58#no-escape-from-lines
59
60# If you do not use the Latin-1 (ISO-8859-1) charset, you should tell
61# GnuPG which is the native character set. Please check the man page
62# for supported character sets. This character set is only used for
63# metadata and not for the actual message which does not undergo any
64# translation. Note that future version of GnuPG will change to UTF-8
65# as default character set. In most cases this option is not required
66# as GnuPG is able to figure out the correct charset at runtime.
67
68#charset utf-8
69
70# Group names may be defined like this:
71# group mynames = paige 0x12345678 joe patti
72#
73# Any time "mynames" is a recipient (-r or --recipient), it will be
74# expanded to the names "paige", "joe", and "patti", and the key ID
75# "0x12345678". Note there is only one level of expansion - you
76# cannot make an group that points to another group. Note also that
77# if there are spaces in the recipient name, this will appear as two
78# recipients. In these cases it is better to use the key ID.
79
80#group mynames = paige 0x12345678 joe patti
81
82# Lock the file only once for the lifetime of a process. If you do
83# not define this, the lock will be obtained and released every time
84# it is needed, which is usually preferable.
85
86#lock-once
87
88# GnuPG can send and receive keys to and from a keyserver. These
89# servers can be HKP, email, or LDAP (if GnuPG is built with LDAP
90# support).
91#
92# Example HKP keyserver:
93# hkp://keys.gnupg.net
94# hkp://subkeys.pgp.net
95#
96# Example email keyserver:
97# mailto:pgp-public-keys@keys.pgp.net
98#
99# Example LDAP keyservers:
100# ldap://keyserver.pgp.com
101#
102# Regular URL syntax applies, and you can set an alternate port
103# through the usual method:
104# hkp://keyserver.example.net:22742
105#
106# Most users just set the name and type of their preferred keyserver.
107# Note that most servers (with the notable exception of
108# ldap://keyserver.pgp.com) synchronize changes with each other. Note
109# also that a single server name may actually point to multiple
110# servers via DNS round-robin. hkp://keys.gnupg.net is an example of
111# such a "server", which spreads the load over a number of physical
112# servers. To see the IP address of the server actually used, you may use
113# the "--keyserver-options debug".
114
115keyserver hkp://keys.gnupg.net
116#keyserver mailto:pgp-public-keys@keys.nl.pgp.net
117#keyserver ldap://keyserver.pgp.com
118
119# Common options for keyserver functions:
120#
121# include-disabled : when searching, include keys marked as "disabled"
122# on the keyserver (not all keyservers support this).
123#
124# no-include-revoked : when searching, do not include keys marked as
125# "revoked" on the keyserver.
126#
127# verbose : show more information as the keys are fetched.
128# Can be used more than once to increase the amount
129# of information shown.
130#
131# use-temp-files : use temporary files instead of a pipe to talk to the
132# keyserver. Some platforms (Win32 for one) always
133# have this on.
134#
135# keep-temp-files : do not delete temporary files after using them
136# (really only useful for debugging)
137#
138# http-proxy="proxy" : set the proxy to use for HTTP and HKP keyservers.
139# This overrides the "http_proxy" environment variable,
140# if any.
141#
142# auto-key-retrieve : automatically fetch keys as needed from the keyserver
143# when verifying signatures or when importing keys that
144# have been revoked by a revocation key that is not
145# present on the keyring.
146#
147# no-include-attributes : do not include attribute IDs (aka "photo IDs")
148# when sending keys to the keyserver.
149
150#keyserver-options auto-key-retrieve
151
152# Display photo user IDs in key listings
153
154# list-options show-photos
155
156# Display photo user IDs when a signature from a key with a photo is
157# verified
158
159# verify-options show-photos
160
161# Use this program to display photo user IDs
162#
163# %i is expanded to a temporary file that contains the photo.
164# %I is the same as %i, but the file isn't deleted afterwards by GnuPG.
165# %k is expanded to the key ID of the key.
166# %K is expanded to the long OpenPGP key ID of the key.
167# %t is expanded to the extension of the image (e.g. "jpg").
168# %T is expanded to the MIME type of the image (e.g. "image/jpeg").
169# %f is expanded to the fingerprint of the key.
170# %% is %, of course.
171#
172# If %i or %I are not present, then the photo is supplied to the
173# viewer on standard input. If your platform supports it, standard
174# input is the best way to do this as it avoids the time and effort in
175# generating and then cleaning up a secure temp file.
176#
177# If no photo-viewer is provided, GnuPG will look for xloadimage, eog,
178# or display (ImageMagick). On Mac OS X and Windows, the default is
179# to use your regular JPEG image viewer.
180#
181# Some other viewers:
182# photo-viewer "qiv %i"
183# photo-viewer "ee %i"
184#
185# This one saves a copy of the photo ID in your home directory:
186# photo-viewer "cat > ~/photoid-for-key-%k.%t"
187#
188# Use your MIME handler to view photos:
189# photo-viewer "metamail -q -d -b -c %T -s 'KeyID 0x%k' -f GnuPG"
190
191# Passphrase agent
192#
193# We support the old experimental passphrase agent protocol as well as
194# the new Assuan based one (currently available in the "newpg" package
195# at ftp.gnupg.org/gcrypt/alpha/aegypten/). To make use of the agent,
196# you have to run an agent as daemon and use the option
197#
198# use-agent
199#
200# which tries to use the agent but will fallback to the regular mode
201# if there is a problem connecting to the agent. The normal way to
202# locate the agent is by looking at the environment variable
203# GPG_AGENT_INFO which should have been set during gpg-agent startup.
204# In certain situations the use of this variable is not possible, thus
205# the option
206#
207# --gpg-agent-info=<path>:<pid>:1
208#
209# may be used to override it.
210
211# Automatic key location
212#
213# GnuPG can automatically locate and retrieve keys as needed using the
214# auto-key-locate option. This happens when encrypting to an email
215# address (in the "user@example.com" form), and there are no
216# user@example.com keys on the local keyring. This option takes the
217# following arguments, in the order they are to be tried:
218#
219# cert = locate a key using DNS CERT, as specified in RFC-4398.
220# GnuPG can handle both the PGP (key) and IPGP (URL + fingerprint)
221# CERT methods.
222#
223# pka = locate a key using DNS PKA.
224#
225# ldap = locate a key using the PGP Universal method of checking
226# "ldap://keys.(thedomain)". For example, encrypting to
227# user@example.com will check ldap://keys.example.com.
228#
229# keyserver = locate a key using whatever keyserver is defined using
230# the keyserver option.
231#
232# You may also list arbitrary keyservers here by URL.
233#
234# Try CERT, then PKA, then LDAP, then hkp://subkeys.net:
235#auto-key-locate cert pka ldap hkp://subkeys.pgp.net
236
237###+++--- GPGConf ---+++###
238utf8-strings
239#debug-level basic
240#log-file socket:///home/leo/kde/src/kdepim/messagecomposer/tests/gnupg_home/log-socket
241###+++--- GPGConf ---+++### Tue 29 Jun 2010 10:23:13 AM EDT
242# GPGConf edited this configuration file.
243# It will disable options before this marked block, but it will
244# never change anything below these lines.
diff --git a/framework/src/domain/mimetreeparser/tests/gnupg_home/gpgsm.conf.in b/framework/src/domain/mimetreeparser/tests/gnupg_home/gpgsm.conf.in
new file mode 100644
index 00000000..92b6119d
--- /dev/null
+++ b/framework/src/domain/mimetreeparser/tests/gnupg_home/gpgsm.conf.in
@@ -0,0 +1,10 @@
1
2###+++--- GPGConf ---+++###
3disable-crl-checks
4debug-level basic
5faked-system-time 20130110T154812
6log-file @CMAKE_CURRENT_BINARY_DIR@/gpgsm.log
7###+++--- GPGConf ---+++### Tue 29 Jun 2010 10:23:13 AM EDT
8# GPGConf edited this configuration file.
9# It will disable options before this marked block, but it will
10# never change anything below these lines.
diff --git a/framework/src/domain/mimetreeparser/tests/gnupg_home/pinentry-fake.sh b/framework/src/domain/mimetreeparser/tests/gnupg_home/pinentry-fake.sh
new file mode 100755
index 00000000..7135a942
--- /dev/null
+++ b/framework/src/domain/mimetreeparser/tests/gnupg_home/pinentry-fake.sh
@@ -0,0 +1,9 @@
1#!/bin/sh
2
3echo "OK Your orders please"
4while :
5do
6 read cmd
7 echo "OK"
8 [ "$cmd" = "BYE" ] && break
9done
diff --git a/framework/src/domain/mimetreeparser/tests/gnupg_home/private-keys-v1.d/1AA8BA52430E51AE249AF0DA97D59F869E4101A8.key b/framework/src/domain/mimetreeparser/tests/gnupg_home/private-keys-v1.d/1AA8BA52430E51AE249AF0DA97D59F869E4101A8.key
new file mode 100644
index 00000000..39ac307b
--- /dev/null
+++ b/framework/src/domain/mimetreeparser/tests/gnupg_home/private-keys-v1.d/1AA8BA52430E51AE249AF0DA97D59F869E4101A8.key
Binary files differ
diff --git a/framework/src/domain/mimetreeparser/tests/gnupg_home/pubring.gpg b/framework/src/domain/mimetreeparser/tests/gnupg_home/pubring.gpg
new file mode 100644
index 00000000..2e00fa24
--- /dev/null
+++ b/framework/src/domain/mimetreeparser/tests/gnupg_home/pubring.gpg
Binary files differ
diff --git a/framework/src/domain/mimetreeparser/tests/gnupg_home/pubring.kbx b/framework/src/domain/mimetreeparser/tests/gnupg_home/pubring.kbx
new file mode 100644
index 00000000..0230f313
--- /dev/null
+++ b/framework/src/domain/mimetreeparser/tests/gnupg_home/pubring.kbx
Binary files differ
diff --git a/framework/src/domain/mimetreeparser/tests/gnupg_home/scdaemon.conf b/framework/src/domain/mimetreeparser/tests/gnupg_home/scdaemon.conf
new file mode 100644
index 00000000..a17a0354
--- /dev/null
+++ b/framework/src/domain/mimetreeparser/tests/gnupg_home/scdaemon.conf
@@ -0,0 +1,8 @@
1
2###+++--- GPGConf ---+++###
3debug-level basic
4log-file socket:///home/leo/kde/src/kdepim/messagecomposer/tests/gnupg_home/log-socket
5###+++--- GPGConf ---+++### Tue 29 Jun 2010 10:23:13 AM EDT
6# GPGConf edited this configuration file.
7# It will disable options before this marked block, but it will
8# never change anything below these lines.
diff --git a/framework/src/domain/mimetreeparser/tests/gnupg_home/secring.gpg b/framework/src/domain/mimetreeparser/tests/gnupg_home/secring.gpg
new file mode 100644
index 00000000..cfd3387d
--- /dev/null
+++ b/framework/src/domain/mimetreeparser/tests/gnupg_home/secring.gpg
Binary files differ
diff --git a/framework/src/domain/mimetreeparser/tests/gnupg_home/trustdb.gpg b/framework/src/domain/mimetreeparser/tests/gnupg_home/trustdb.gpg
new file mode 100644
index 00000000..70089c15
--- /dev/null
+++ b/framework/src/domain/mimetreeparser/tests/gnupg_home/trustdb.gpg
Binary files differ
diff --git a/framework/src/domain/mimetreeparser/tests/gnupg_home/trustlist.txt b/framework/src/domain/mimetreeparser/tests/gnupg_home/trustlist.txt
new file mode 100644
index 00000000..bbb0442d
--- /dev/null
+++ b/framework/src/domain/mimetreeparser/tests/gnupg_home/trustlist.txt
@@ -0,0 +1,11 @@
15E:7C:B2:F4:9F:70:05:43:42:32:5D:75:74:70:00:09:B9:D8:08:61 S
2
3
4
5# CN=unittest cert
6# O=KDAB
7# C=US
8# EMail=test@example.com
924:D2:FC:A2:2E:B3:B8:0A:1E:37:71:D1:4C:C6:58:E3:21:2B:49:DC S
10
11
diff --git a/framework/src/domain/mimetreeparser/tests/gpgerrortest.cpp b/framework/src/domain/mimetreeparser/tests/gpgerrortest.cpp
new file mode 100644
index 00000000..4254d972
--- /dev/null
+++ b/framework/src/domain/mimetreeparser/tests/gpgerrortest.cpp
@@ -0,0 +1,214 @@
1/*
2 Copyright (c) 2016 Sandro Knauß <knauss@kolabsystems.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19
20#include "interface.h"
21#include "interface_p.h"
22
23#include <QGpgME/Protocol>
24#include <gpgme++/context.h>
25#include <gpgme++/engineinfo.h>
26#include <gpgme.h>
27
28#include <QDebug>
29#include <QDir>
30#include <QProcess>
31#include <QTest>
32
33QByteArray readMailFromFile(const QString &mailFile)
34{
35 QFile file(QLatin1String(MAIL_DATA_DIR) + QLatin1Char('/') + mailFile);
36 file.open(QIODevice::ReadOnly);
37 Q_ASSERT(file.isOpen());
38 return file.readAll();
39}
40
41void killAgent(const QString& dir)
42{
43 QProcess proc;
44 proc.setProgram(QStringLiteral("gpg-connect-agent"));
45 QStringList arguments;
46 arguments << "-S " << dir + "/S.gpg-agent";
47 proc.start();
48 proc.waitForStarted();
49 proc.write("KILLAGENT\n");
50 proc.write("BYE\n");
51 proc.closeWriteChannel();
52 proc.waitForFinished();
53}
54
55class GpgErrorTest : public QObject
56{
57 Q_OBJECT
58
59private slots:
60
61 void testGpgConfiguredCorrectly()
62 {
63 setEnv("GNUPGHOME", GNUPGHOME);
64
65 Parser parser(readMailFromFile("openpgp-inline-charset-encrypted.mbox"));
66
67 auto contentPartList = parser.collectContentParts();
68 QCOMPARE(contentPartList.size(), 1);
69 auto contentPart = contentPartList[0];
70 QCOMPARE(contentPart->availableContents(), QVector<QByteArray>() << "plaintext");
71 auto contentList = contentPart->content("plaintext");
72 QVERIFY(contentList[0]->content().startsWith("asdasd"));
73 QCOMPARE(contentList[0]->encryptions().size(), 1);
74 auto enc = contentList[0]->encryptions()[0];
75 QCOMPARE(enc->errorType(), Encryption::NoError);
76 QCOMPARE(enc->errorString(), QString());
77 QCOMPARE((int) enc->recipients().size(), 2);
78 }
79
80 void testNoGPGInstalled_data()
81 {
82 QTest::addColumn<QString>("mailFileName");
83
84 QTest::newRow("openpgp-inline-charset-encrypted") << "openpgp-inline-charset-encrypted.mbox";
85 QTest::newRow("openpgp-encrypted-attachment-and-non-encrypted-attachment") << "openpgp-encrypted-attachment-and-non-encrypted-attachment.mbox";
86 QTest::newRow("smime-encrypted") << "smime-encrypted.mbox";
87 }
88
89 void testNoGPGInstalled()
90 {
91 QFETCH(QString, mailFileName);
92
93 setEnv("PATH", "/nonexististing");
94 setGpgMEfname("/nonexisting/gpg", "");
95
96 Parser parser(readMailFromFile(mailFileName));
97 auto contentPartList = parser.collectContentParts();
98
99 QCOMPARE(contentPartList.size(), 1);
100 auto contentPart = contentPartList[0];
101 QCOMPARE(contentPart->availableContents(), QVector<QByteArray>() << "plaintext");
102 auto contentList = contentPart->content("plaintext");
103 QCOMPARE(contentList[0]->encryptions().size(), 1);
104 QVERIFY(contentList[0]->content().isEmpty());
105 auto enc = contentList[0]->encryptions()[0];
106 qDebug() << "HUHU"<< enc->errorType();
107 QCOMPARE(enc->errorType(), Encryption::UnknownError);
108 QCOMPARE(enc->errorString(), QString("Crypto plug-in \"OpenPGP\" could not decrypt the data.<br />Error: No data"));
109 QCOMPARE((int) enc->recipients().size(), 0);
110 }
111
112 void testGpgIncorrectGPGHOME_data()
113 {
114 QTest::addColumn<QString>("mailFileName");
115
116 QTest::newRow("openpgp-inline-charset-encrypted") << "openpgp-inline-charset-encrypted.mbox";
117 QTest::newRow("openpgp-encrypted-attachment-and-non-encrypted-attachment") << "openpgp-encrypted-attachment-and-non-encrypted-attachment.mbox";
118 QTest::newRow("smime-encrypted") << "smime-encrypted.mbox";
119 }
120
121 void testGpgIncorrectGPGHOME()
122 {
123 QFETCH(QString, mailFileName);
124 setEnv("GNUPGHOME", QByteArray(GNUPGHOME) + QByteArray("noexist"));
125
126 Parser parser(readMailFromFile(mailFileName));
127
128 auto contentPartList = parser.collectContentParts();
129 QCOMPARE(contentPartList.size(), 1);
130 auto contentPart = contentPartList[0];
131 QCOMPARE(contentPart->availableContents(), QVector<QByteArray>() << "plaintext");
132 auto contentList = contentPart->content("plaintext");
133 QCOMPARE(contentList[0]->encryptions().size(), 1);
134 QCOMPARE(contentList[0]->signatures().size(), 0);
135 QVERIFY(contentList[0]->content().isEmpty());
136 auto enc = contentList[0]->encryptions()[0];
137 qDebug() << enc->errorType();
138 QCOMPARE(enc->errorType(), Encryption::KeyMissing);
139 QCOMPARE(enc->errorString(), QString("Crypto plug-in \"OpenPGP\" could not decrypt the data.<br />Error: Decryption failed"));
140 QCOMPARE((int) enc->recipients().size(), 2);
141 }
142
143public Q_SLOTS:
144 void init()
145 {
146 mResetGpgmeEngine = false;
147 mModifiedEnv.clear();
148 {
149 QGpgME::openpgp(); // We need to intialize it, otherwise ctx will be a nullpointer
150 const GpgME::Context *ctx = GpgME::Context::createForProtocol(GpgME::Protocol::OpenPGP);
151 const auto engineinfo = ctx->engineInfo();
152 mGpgmeEngine_fname = engineinfo.fileName();
153 }
154 mEnv = QProcessEnvironment::systemEnvironment();
155 unsetEnv("GNUPGHOME");
156 }
157
158 void cleanup()
159 {
160 QCoreApplication::sendPostedEvents();
161
162 const QString &gnupghome = qgetenv("GNUPGHOME");
163 if (!gnupghome.isEmpty()) {
164 killAgent(gnupghome);
165 }
166
167 resetGpgMfname();
168 resetEnv();
169 }
170private:
171 void unsetEnv(const QByteArray &name)
172 {
173 mModifiedEnv << name;
174 unsetenv(name);
175 }
176
177 void setEnv(const QByteArray &name, const QByteArray &value)
178 {
179 mModifiedEnv << name;
180 setenv(name, value , 1);
181 }
182
183 void resetEnv()
184 {
185 foreach(const auto &i, mModifiedEnv) {
186 if (mEnv.contains(i)) {
187 setenv(i, mEnv.value(i).toUtf8(), 1);
188 } else {
189 unsetenv(i);
190 }
191 }
192 }
193
194 void resetGpgMfname()
195 {
196 if (mResetGpgmeEngine) {
197 gpgme_set_engine_info (GPGME_PROTOCOL_OpenPGP, mGpgmeEngine_fname, NULL);
198 }
199 }
200
201 void setGpgMEfname(const QByteArray &fname, const QByteArray &homedir)
202 {
203 mResetGpgmeEngine = true;
204 gpgme_set_engine_info (GPGME_PROTOCOL_OpenPGP, fname, homedir);
205 }
206
207 QSet<QByteArray> mModifiedEnv;
208 QProcessEnvironment mEnv;
209 bool mResetGpgmeEngine;
210 QByteArray mGpgmeEngine_fname;
211};
212
213QTEST_GUILESS_MAIN(GpgErrorTest)
214#include "gpgerrortest.moc"
diff --git a/framework/src/domain/mimetreeparser/tests/interfacetest.cpp b/framework/src/domain/mimetreeparser/tests/interfacetest.cpp
new file mode 100644
index 00000000..3ae32a4a
--- /dev/null
+++ b/framework/src/domain/mimetreeparser/tests/interfacetest.cpp
@@ -0,0 +1,310 @@
1/*
2 Copyright (c) 2016 Sandro Knauß <knauss@kolabsystems.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19
20#include "interface.h"
21#include "interface_p.h"
22
23#include <QTest>
24
25QByteArray readMailFromFile(const QString &mailFile)
26{
27 QFile file(QLatin1String(MAIL_DATA_DIR) + QLatin1Char('/') + mailFile);
28 file.open(QIODevice::ReadOnly);
29 Q_ASSERT(file.isOpen());
30 return file.readAll();
31}
32
33QByteArray join(QVector<QByteArray> vec, QByteArray sep)
34{
35 QByteArray ret;
36 bool bInit = true;
37 foreach(const auto &entry, vec) {
38 if (!bInit) {
39 ret += sep;
40 }
41 bInit = false;
42 ret += entry;
43 }
44 return ret;
45}
46
47class InterfaceTest : public QObject
48{
49 Q_OBJECT
50private:
51 void printTree(const Part::Ptr &start, QString pre)
52 {
53 foreach (const auto &part, start->subParts()) {
54 qWarning() << QStringLiteral("%1* %2(%3)")
55 .arg(pre)
56 .arg(QString::fromLatin1(part->type()))
57 .arg(QString::fromLatin1(join(part->availableContents(),", ")));
58 printTree(part,pre + QStringLiteral(" "));
59 }
60 }
61
62private slots:
63
64 void testTextMail()
65 {
66 Parser parser(readMailFromFile("plaintext.mbox"));
67 printTree(parser.d->mTree,QString());
68 auto contentPartList = parser.collectContentParts();
69 QCOMPARE(contentPartList.size(), 1);
70 auto contentPart = contentPartList[0];
71 QVERIFY((bool)contentPart);
72 QCOMPARE(contentPart->availableContents(), QVector<QByteArray>() << "plaintext");
73 auto contentList = contentPart->content("plaintext");
74 QCOMPARE(contentList.size(), 1);
75 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());
76 QCOMPARE(contentList[0]->charset(), QStringLiteral("utf-8").toLocal8Bit());
77 QCOMPARE(contentList[0]->encryptions().size(), 0);
78 QCOMPARE(contentList[0]->signatures().size(), 0);
79
80 contentList = contentPart->content("html");
81 QCOMPARE(contentList.size(), 0);
82 auto contentAttachmentList = parser.collectAttachmentParts();
83 QCOMPARE(contentAttachmentList.size(), 0);
84 }
85
86 void testTextAlternative()
87 {
88 Parser parser(readMailFromFile("alternative.mbox"));
89 printTree(parser.d->mTree,QString());
90 auto contentPartList = parser.collectContentParts();
91 QCOMPARE(contentPartList.size(), 1);
92 auto contentPart = contentPartList[0];
93 QVERIFY((bool)contentPart);
94 QCOMPARE(contentPart->availableContents(), QVector<QByteArray>() << "html" << "plaintext");
95 auto contentList = contentPart->content("plaintext");
96 QCOMPARE(contentList.size(), 1);
97 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());
98 QCOMPARE(contentList[0]->charset(), QStringLiteral("utf-8").toLocal8Bit());
99 QCOMPARE(contentList[0]->encryptions().size(), 0);
100 QCOMPARE(contentList[0]->signatures().size(), 0);
101
102 contentList = contentPart->content("html");
103 QCOMPARE(contentList.size(), 1);
104 QCOMPARE(contentList[0]->content(), QStringLiteral("<html><body><p><span>HTML</span> text</p></body></html>\n\n").toLocal8Bit());
105 QCOMPARE(contentList[0]->charset(), QStringLiteral("utf-8").toLocal8Bit());
106 QCOMPARE(contentList[0]->encryptions().size(), 0);
107 QCOMPARE(contentList[0]->signatures().size(), 0);
108 auto contentAttachmentList = parser.collectAttachmentParts();
109 QCOMPARE(contentAttachmentList.size(), 0);
110 }
111
112 void testTextHtml()
113 {
114 Parser parser(readMailFromFile("html.mbox"));
115 printTree(parser.d->mTree,QString());
116 auto contentPartList = parser.collectContentParts();
117 QCOMPARE(contentPartList.size(), 1);
118 auto contentPart = contentPartList[0];
119 QVERIFY((bool)contentPart);
120 QCOMPARE(contentPart->availableContents(), QVector<QByteArray>() << "html");
121
122 auto contentList = contentPart->content("plaintext");
123 QCOMPARE(contentList.size(), 0);
124
125 contentList = contentPart->content("html");
126 QCOMPARE(contentList.size(), 1);
127 QCOMPARE(contentList[0]->content(), QStringLiteral("<html><body><p><span>HTML</span> text</p></body></html>").toLocal8Bit());
128 QCOMPARE(contentList[0]->charset(), QStringLiteral("utf-8").toLocal8Bit());
129 QCOMPARE(contentList[0]->encryptions().size(), 0);
130 QCOMPARE(contentList[0]->signatures().size(), 0);
131 auto contentAttachmentList = parser.collectAttachmentParts();
132 QCOMPARE(contentAttachmentList.size(), 0);
133 }
134
135 void testSMimeEncrypted()
136 {
137 Parser parser(readMailFromFile("smime-encrypted.mbox"));
138 printTree(parser.d->mTree,QString());
139 auto contentPartList = parser.collectContentParts();
140 QCOMPARE(contentPartList.size(), 1);
141 auto contentPart = contentPartList[0];
142 QVERIFY((bool)contentPart);
143 QCOMPARE(contentPart->availableContents(), QVector<QByteArray>() << "plaintext");
144 auto contentList = contentPart->content("plaintext");
145 QCOMPARE(contentList.size(), 1);
146 QCOMPARE(contentList[0]->content(), QStringLiteral("The quick brown fox jumped over the lazy dog.").toLocal8Bit());
147 QCOMPARE(contentList[0]->charset(), QStringLiteral("us-ascii").toLocal8Bit());
148 QCOMPARE(contentList[0]->encryptions().size(), 1);
149 QCOMPARE(contentList[0]->signatures().size(), 0);
150 auto contentAttachmentList = parser.collectAttachmentParts();
151 QCOMPARE(contentAttachmentList.size(), 0);
152 }
153
154 void testOpenPGPEncryptedAttachment()
155 {
156 Parser parser(readMailFromFile("openpgp-encrypted-attachment-and-non-encrypted-attachment.mbox"));
157 printTree(parser.d->mTree,QString());
158 auto contentPartList = parser.collectContentParts();
159 QCOMPARE(contentPartList.size(), 1);
160 auto contentPart = contentPartList[0];
161 QVERIFY((bool)contentPart);
162 QCOMPARE(contentPart->availableContents(), QVector<QByteArray>() << "plaintext");
163 auto contentList = contentPart->content("plaintext");
164 QCOMPARE(contentList.size(), 1);
165 QCOMPARE(contentList[0]->content(), QStringLiteral("test text").toLocal8Bit());
166 QCOMPARE(contentList[0]->charset(), QStringLiteral("us-ascii").toLocal8Bit());
167 QCOMPARE(contentList[0]->encryptions().size(), 1);
168 QCOMPARE(contentList[0]->signatures().size(), 1);
169 auto contentAttachmentList = parser.collectAttachmentParts();
170 QCOMPARE(contentAttachmentList.size(), 2);
171 QCOMPARE(contentAttachmentList[0]->availableContents(), QVector<QByteArray>() << "text/plain");
172 QCOMPARE(contentAttachmentList[0]->content().size(), 1);
173 QCOMPARE(contentAttachmentList[0]->encryptions().size(), 1);
174 QCOMPARE(contentAttachmentList[0]->signatures().size(), 1);
175 QCOMPARE(contentAttachmentList[1]->availableContents(), QVector<QByteArray>() << "image/png");
176 QCOMPARE(contentAttachmentList[1]->content().size(), 1);
177 QCOMPARE(contentAttachmentList[1]->encryptions().size(), 0);
178 QCOMPARE(contentAttachmentList[1]->signatures().size(), 0);
179 }
180
181 void testOpenPGPInline()
182 {
183 Parser parser(readMailFromFile("openpgp-inline-charset-encrypted.mbox"));
184 printTree(parser.d->mTree,QString());
185 auto contentPartList = parser.collectContentParts();
186 QCOMPARE(contentPartList.size(), 1);
187 auto contentPart = contentPartList[0];
188 QVERIFY((bool)contentPart);
189 QCOMPARE(contentPart->availableContents(), QVector<QByteArray>() << "plaintext");
190 QCOMPARE(contentPart->encryptions().size(), 0);
191 QCOMPARE(contentPart->signatures().size(), 0);
192 auto contentList = contentPart->content("plaintext");
193 QCOMPARE(contentList.size(), 1);
194 QCOMPARE(contentList[0]->content(), QStringLiteral("asdasd asd asd asdf sadf sdaf sadf äöü").toLocal8Bit());
195 QCOMPARE(contentList[0]->charset(), QStringLiteral("ISO-8859-15").toLocal8Bit());
196 QCOMPARE(contentList[0]->encryptions().size(), 1);
197 QCOMPARE(contentList[0]->signatures().size(), 1);
198 auto contentAttachmentList = parser.collectAttachmentParts();
199 QCOMPARE(contentAttachmentList.size(), 0);
200 }
201
202 void testOpenPPGInlineWithNonEncText()
203 {
204 Parser parser(readMailFromFile("openpgp-inline-encrypted+nonenc.mbox"));
205 printTree(parser.d->mTree,QString());
206 auto contentPartList = parser.collectContentParts();
207 QCOMPARE(contentPartList.size(), 1);
208 auto contentPart = contentPartList[0];
209 QVERIFY((bool)contentPart);
210 QCOMPARE(contentPart->availableContents(), QVector<QByteArray>() << "plaintext");
211 QCOMPARE(contentPart->encryptions().size(), 0);
212 QCOMPARE(contentPart->signatures().size(), 0);
213 auto contentList = contentPart->content("plaintext");
214 QCOMPARE(contentList.size(), 2);
215 QCOMPARE(contentList[0]->content(), QStringLiteral("Not encrypted not signed :(\n\n").toLocal8Bit());
216 QCOMPARE(contentList[0]->charset(), QStringLiteral("us-ascii").toLocal8Bit());
217 QCOMPARE(contentList[0]->encryptions().size(), 0);
218 QCOMPARE(contentList[0]->signatures().size(), 0);
219 QCOMPARE(contentList[1]->content(), QStringLiteral("some random text").toLocal8Bit());
220 QCOMPARE(contentList[1]->charset(), QStringLiteral("us-ascii").toLocal8Bit());
221 QCOMPARE(contentList[1]->encryptions().size(), 1);
222 QCOMPARE(contentList[1]->signatures().size(), 0);
223 auto contentAttachmentList = parser.collectAttachmentParts();
224 QCOMPARE(contentAttachmentList.size(), 0);
225 }
226
227 void testEncryptionBlock()
228 {
229 Parser parser(readMailFromFile("openpgp-encrypted-attachment-and-non-encrypted-attachment.mbox"));
230 auto contentPartList = parser.collectContentParts();
231 auto contentPart = contentPartList[0];
232 auto contentList = contentPart->content("plaintext");
233 QCOMPARE(contentList.size(), 1);
234 QCOMPARE(contentList[0]->encryptions().size(), 1);
235 auto enc = contentList[0]->encryptions()[0];
236 QCOMPARE((int) enc->recipients().size(), 2);
237
238 auto r = enc->recipients()[0];
239 QCOMPARE(r->keyid(),QStringLiteral("14B79E26050467AA"));
240 QCOMPARE(r->name(),QStringLiteral("kdetest"));
241 QCOMPARE(r->email(),QStringLiteral("you@you.com"));
242 QCOMPARE(r->comment(),QStringLiteral(""));
243
244 r = enc->recipients()[1];
245 QCOMPARE(r->keyid(),QStringLiteral("8D9860C58F246DE6"));
246 QCOMPARE(r->name(),QStringLiteral("unittest key"));
247 QCOMPARE(r->email(),QStringLiteral("test@kolab.org"));
248 QCOMPARE(r->comment(),QStringLiteral("no password"));
249 }
250
251 void testSignatureBlock()
252 {
253 Parser parser(readMailFromFile("openpgp-encrypted-attachment-and-non-encrypted-attachment.mbox"));
254 auto contentPartList = parser.collectContentParts();
255 auto contentPart = contentPartList[0];
256 auto contentList = contentPart->content("plaintext");
257 QCOMPARE(contentList.size(), 1);
258 QCOMPARE(contentList[0]->signatures().size(), 1);
259 auto sig = contentList[0]->signatures()[0];
260 QCOMPARE(sig->creationDateTime(), QDateTime(QDate(2015,05,01),QTime(15,12,47)));
261 QCOMPARE(sig->expirationDateTime(), QDateTime());
262 QCOMPARE(sig->neverExpires(), true);
263
264 auto key = sig->key();
265 QCOMPARE(key->keyid(),QStringLiteral("8D9860C58F246DE6"));
266 QCOMPARE(key->name(),QStringLiteral("unittest key"));
267 QCOMPARE(key->email(),QStringLiteral("test@kolab.org"));
268 QCOMPARE(key->comment(),QStringLiteral("no password"));
269 }
270
271 void testRelatedAlternative()
272 {
273 Parser parser(readMailFromFile("cid-links.mbox"));
274 printTree(parser.d->mTree,QString());
275 auto contentPartList = parser.collectContentParts();
276 QCOMPARE(contentPartList.size(), 1);
277 auto contentPart = contentPartList[0];
278 QVERIFY((bool)contentPart);
279 QCOMPARE(contentPart->availableContents(), QVector<QByteArray>() << "html" << "plaintext");
280 QCOMPARE(contentPart->encryptions().size(), 0);
281 QCOMPARE(contentPart->signatures().size(), 0);
282 auto contentList = contentPart->content("plaintext");
283 QCOMPARE(contentList.size(), 1);
284 auto contentAttachmentList = parser.collectAttachmentParts();
285 QCOMPARE(contentAttachmentList.size(), 0);
286 }
287
288 void testAttachmentPart()
289 {
290 Parser parser(readMailFromFile("cid-links.mbox"));
291 const auto relatedImage = parser.d->mTree->subParts().at(1);
292 QCOMPARE(relatedImage->availableContents(), QVector<QByteArray>() << "image/jpeg");
293 auto contentList = relatedImage->content();
294 QCOMPARE(contentList.size(), 1);
295 contentList = relatedImage->content("image/jpeg");
296 QCOMPARE(contentList.size(), 1);
297 }
298
299 void testCidLink()
300 {
301 Parser parser(readMailFromFile("cid-links.mbox"));
302 printTree(parser.d->mTree,QString());
303 QCOMPARE(parser.getPart(QUrl("cid:9359201d15e53f31a68c307b3369b6@info")), parser.d->mTree->subParts().at(1));
304 QVERIFY(!parser.getPart(QUrl("cid:")));
305 QVERIFY(!parser.getPart(QUrl("cid:unknown")));
306 }
307};
308
309QTEST_GUILESS_MAIN(InterfaceTest)
310#include "interfacetest.moc"
diff --git a/framework/src/domain/mimetreeparser/tests/kdepim_add_gpg_crypto_test.cmake b/framework/src/domain/mimetreeparser/tests/kdepim_add_gpg_crypto_test.cmake
new file mode 100644
index 00000000..ea0ab8d2
--- /dev/null
+++ b/framework/src/domain/mimetreeparser/tests/kdepim_add_gpg_crypto_test.cmake
@@ -0,0 +1,61 @@
1# Copyright (c) 2013 Sandro Knauß <mail@sandroknauss.de>
2#
3# Redistribution and use is allowed according to the terms of the BSD license.
4# For details see the accompanying COPYING-CMAKE-SCRIPTS file.
5
6set( MIMETREEPARSERRELPATH framework/src/domain/mimetreeparser)
7set( GNUPGHOME ${CMAKE_BINARY_DIR}/${MIMETREEPARSERRELPATH}/tests/gnupg_home )
8add_definitions( -DGNUPGHOME="${GNUPGHOME}" )
9
10macro (ADD_GPG_CRYPTO_TEST _target _testname)
11 if (UNIX)
12 if (APPLE)
13 set(_library_path_variable "DYLD_LIBRARY_PATH")
14 elseif (CYGWIN)
15 set(_library_path_variable "PATH")
16 else (APPLE)
17 set(_library_path_variable "LD_LIBRARY_PATH")
18 endif (APPLE)
19
20 if (APPLE)
21 # DYLD_LIBRARY_PATH does not work like LD_LIBRARY_PATH
22 # OSX already has the RPATH in libraries and executables, putting runtime directories in
23 # DYLD_LIBRARY_PATH actually breaks things
24 set(_ld_library_path "${LIBRARY_OUTPUT_PATH}/${CMAKE_CFG_INTDIR}/")
25 else (APPLE)
26 set(_ld_library_path "${LIBRARY_OUTPUT_PATH}/${CMAKE_CFG_INTDIR}/:${LIB_INSTALL_DIR}:${QT_LIBRARY_DIR}")
27 endif (APPLE)
28 set(_executable "$<TARGET_FILE:${_target}>")
29
30 # use add_custom_target() to have the sh-wrapper generated during build time instead of cmake time
31 add_custom_command(TARGET ${_target} POST_BUILD
32 COMMAND ${CMAKE_COMMAND}
33 -D_filename=${_executable}.shell -D_library_path_variable=${_library_path_variable}
34 -D_ld_library_path="${_ld_library_path}" -D_executable=$<TARGET_FILE:${_target}>
35 -D_gnupghome="${GNUPGHOME}"
36 -P ${CMAKE_SOURCE_DIR}/${MIMETREEPARSERRELPATH}/tests/kdepim_generate_crypto_test_wrapper.cmake
37 )
38
39 set_property(DIRECTORY APPEND PROPERTY ADDITIONAL_MAKE_CLEAN_FILES "${_executable}.shell" )
40 add_test(NAME ${_testname} COMMAND ${_executable}.shell)
41
42 else (UNIX)
43 # under windows, set the property WRAPPER_SCRIPT just to the name of the executable
44 # maybe later this will change to a generated batch file (for setting the PATH so that the Qt libs are found)
45 set(_ld_library_path "${LIBRARY_OUTPUT_PATH}/${CMAKE_CFG_INTDIR}\;${LIB_INSTALL_DIR}\;${QT_LIBRARY_DIR}")
46 set(_executable "$<TARGET_FILE:${_target}>")
47
48 # use add_custom_target() to have the batch-file-wrapper generated during build time instead of cmake time
49 add_custom_command(TARGET ${_target} POST_BUILD
50 COMMAND ${CMAKE_COMMAND}
51 -D_filename="${_executable}.bat"
52 -D_ld_library_path="${_ld_library_path}" -D_executable="${_executable}"
53 -D_gnupghome="${GNUPGHOME}"
54 -P ${CMAKE_SOURCE_DIR}/${MIMETREEPARSERRELPATH}/tests/kdepim_generate_crypto_test_wrapper.cmake
55 )
56
57 add_test(NAME ${_testname} COMMAND ${_executable}.bat)
58
59 endif (UNIX)
60endmacro (ADD_GPG_CRYPTO_TEST)
61
diff --git a/framework/src/domain/mimetreeparser/tests/kdepim_generate_crypto_test_wrapper.cmake b/framework/src/domain/mimetreeparser/tests/kdepim_generate_crypto_test_wrapper.cmake
new file mode 100644
index 00000000..e1412f37
--- /dev/null
+++ b/framework/src/domain/mimetreeparser/tests/kdepim_generate_crypto_test_wrapper.cmake
@@ -0,0 +1,45 @@
1# Copyright (c) 2006, Alexander Neundorf, <neundorf@kde.org>
2# Copyright (c) 2013, Sandro Knauß <mail@sandroknauss.de>
3#
4# Redistribution and use is allowed according to the terms of the BSD license.
5# For details see the accompanying COPYING-CMAKE-SCRIPTS file.
6
7
8if (UNIX)
9
10file(WRITE "${_filename}"
11"#!/bin/sh
12# created by cmake, don't edit, changes will be lost
13
14# don't mess with a gpg-agent already running on the system
15unset GPG_AGENT_INFO
16
17${_library_path_variable}=${_ld_library_path}\${${_library_path_variable}:+:\$${_library_path_variable}} GNUPGHOME=${_gnupghome} gpg-agent --daemon \"${_executable}\" \"$@\"
18_result=$?
19_pid=`echo GETINFO pid | GNUPGHOME=${_gnupghome} gpg-connect-agent | grep 'D' | cut -d' ' -f2`
20if [ ! -z \"\$_pid\" ]; then
21 echo \"Waiting for gpg-agent to terminate (PID: $_pid)...\"
22 while kill -0 \"\$_pid\"; do
23 sleep 1
24 done
25fi
26exit \$_result
27")
28
29# make it executable
30# since this is only executed on UNIX, it is safe to call chmod
31exec_program(chmod ARGS ug+x \"${_filename}\" OUTPUT_VARIABLE _dummy )
32
33else (UNIX)
34
35file(TO_NATIVE_PATH "${_ld_library_path}" win_path)
36file(TO_NATIVE_PATH "${_gnupghome}" win_gnupghome)
37
38file(WRITE "${_filename}"
39"
40set PATH=${win_path};$ENV{PATH}
41set GNUPGHOME=${win_gnupghome};$ENV{GNUPGHOME}
42gpg-agent --daemon \"${_executable}\" %*
43")
44
45endif (UNIX)
diff --git a/framework/src/domain/mimetreeparser/thoughts.txt b/framework/src/domain/mimetreeparser/thoughts.txt
new file mode 100644
index 00000000..3340347a
--- /dev/null
+++ b/framework/src/domain/mimetreeparser/thoughts.txt
@@ -0,0 +1,148 @@
1Usecases:
2
3# plaintext msg + attachment
4* ContentPart => cp1
5* AttachmentPart => ap1
6
7(cp1) == collect<ContentPart>(select=NoEncapsulatedMessages)
8(ap1) == collect<AttachmentParts>(select=NoEncapsulatedMessages)
9
10(PlainText) == cp1.availableContent()
11
12# html msg + related attachment + normal attachment
13* ContentPart => cp1
14* AttachmentPart(mimetype="*/related", cid="12345678") => ap1
15* AttachmentPart => ap2
16
17(cp1) == collect<ContentPart>(select=NoEncapsulatedMessages)
18(ap1, ap2) == collect<AttachmentParts>(select=NoEncapsulatedMessages)
19(ap2) == collect<AttachmentParts>(select=NoEncapsulatedMessages, filter=filterelated)
20
21ap1 == getPart("cid:12345678")
22
23(Html) == cp1.availableContent()
24
25# alternative msg + attachment
26* ContentPart(html=[Content("HTML"),], plaintext=[Content("Text"),]) => cp1
27* AttachmentPart => ap1
28
29(cp1) == collect<ContentPart>(select=NoEncapsulatedMessages)
30(ap1) == collect<AttachmentParts>(select=NoEncapsulatedMessages)
31
32(Html, PlainText) == cp1.availableContent()
33[Content("HTML"),] == cp1.content(Html)
34[Content("Text"),] == cp1.content(Plaintext)
35
36# alternative msg with GPGInlin
37* ContentPart(
38 plaintext=[Content("Text"), Content("foo", encryption=(enc1))],
39 html=[Content("HTML"),]
40 ) => cp1
41
42(Html, PlainText) == cp1.availableContent()
43
44[Content("HTML"),] == cp1.content(Html)
45[Content("Text"),Content("foo", encryption=(enc1))] == cp1.content(Plaintext)
46
47
48# encrypted msg (not encrypted/error) with unencrypted attachment
49* EncryptionErrorPart => cp1
50* AttachmentPart => ap1
51
52(cp1) == collect<ContentPart>(select=NoEncapsulatedMessages)
53(ap1) == collect<AttachmentParts>(select=NoEncapsulatedMessages)
54
55#encrypted msg (decrypted with attachment) + unencrypted attachment
56* encrytion=(rec1,rec2) => enc1
57 * ContentPart(encrytion = (enc1,)) => cp1
58 * AttachmentPart(encryption = (enc1,)) => ap1
59* AttachmentPart => ap2
60
61(cp1) == collect<ContentPart>(select=NoEncapsulatedMessages)
62(ap1, ap2) == collect<AttachmentParts>(select=NoEncapsulatedMessages)
63
64#INLINE GPG encrypted msg + attachment
65* ContentPart => cp1 with
66 plaintext=[Content, Content(encrytion = (enc1(rec1,rec2),)), Content(signed = (sig1,)), Content]
67* AttachmentPart => ap1
68
69(cp1) == collect<ContentPart>(select=NoEncapsulatedMessages)
70(ap1) == collect<AttachmentParts>(select=NoEncapsulatedMessages)
71
72[Content, Content(encrytion = (enc1(rec1,rec2),)), Content(signed = (sig1,)), Content] == cp1.content(Plaintext)
73
74#forwared encrypted msg + attachments
75* ContentPart => cp1
76* EncapsulatedPart => ep1
77 * Encrytion=(rec1,rec2) => enc1
78 * Signature => sig1
79 * ContentPart(encrytion = (enc1,), signature = (sig1,)) => cp2
80 * Content(encrytion = (enc1,), signature = (sig1,))
81 * Content(encrytion = (enc1, enc2(rec3,rec4),), signature = (sig1,))
82 * AttachmentPart(encrytion = (enc1,), signature = (sig1,)) => ap1
83* AttachmentPart => ap2
84
85(cp1) = collect<ContentPart>(select=NoEncapsulatedMessages)
86(ap2) = collect<AttachmentParts>(select=NoEncapsulatedMessages)
87
88(cp2) = collect<ContentPart>(ep1, select=NoEncapsulatedMessages)
89(ap1) = collect<AttachmentParts>(ep1, select=NoEncapsulatedMessages)
90
91(cp1, cp2) == collect<ContentPart>()
92(ap1, ap2) == collect<AttachmentParts>()[Content, Content(encrytion = (enc1(rec1,rec2),)), Content(signed = (sig1,)), Content]
93
94
95# plaintext msg + attachment + cert
96* ContentPart => cp1
97* AttachmentPart => ap1
98* CertPart => cep1
99
100(cp1) == collect<ContentPart>(select=NoEncapsulatedMessages)
101(ap1, cep1) == collect<AttachmentPart>(select=NoEncapsulatedMessages)
102(ap1) == collect<AttachmentPart>(select=NoEncapsulatedMessages, filter=filterSubAttachmentParts)
103
104(cep1) == collect<CertPart>(select=NoEncapsulatedMessages)
105
106
107collect function:
108
109bool noEncapsulatedMessages(Part part)
110{
111 if (is<EncapsulatedPart>(part)) {
112 return false;
113 }
114 return true;
115}
116
117bool filterRelated(T part)
118{
119 if (part.mimetype == related && !part.cid.isEmpty()) {
120 return false; //filter out related parts
121 }
122 return true;
123}
124
125bool filterSubAttachmentParts(AttachmentPart part)
126{
127 if (isSubPart<AttachmentPart>(part)) {
128 return false; // filter out CertPart f.ex.
129 }
130 return true;
131}
132
133List<T> collect<T>(Part start, std::function<bool(const Part &)> select, std::function<bool(const std::shared_ptr<T> &)> filter) {
134 List<T> col;
135 if (!select(start)) {
136 return col;
137 }
138
139 if(isOrSubTypeIs<T>(start) && filter(start.staticCast<T>)){
140 col.append(p);
141 }
142 foreach(childs as child) {
143 if (select(child)) {
144 col.expand(collect(child,select,filter);
145 }
146 }
147 return col;
148} \ No newline at end of file
diff --git a/framework/src/domain/modeltest.cpp b/framework/src/domain/modeltest.cpp
new file mode 100644
index 00000000..2bd3d27f
--- /dev/null
+++ b/framework/src/domain/modeltest.cpp
@@ -0,0 +1,588 @@
1/****************************************************************************
2**
3** Copyright (C) 2016 The Qt Company Ltd.
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the test suite of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:GPL-EXCEPT$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see https://www.qt.io/terms-conditions. For further
15** information use the contact form at https://www.qt.io/contact-us.
16**
17** GNU General Public License Usage
18** Alternatively, this file may be used under the terms of the GNU
19** General Public License version 3 as published by the Free Software
20** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
21** included in the packaging of this file. Please review the following
22** information to ensure the GNU General Public License requirements will
23** be met: https://www.gnu.org/licenses/gpl-3.0.html.
24**
25** $QT_END_LICENSE$
26**
27****************************************************************************/
28
29#include "modeltest.h"
30
31#include <QtCore/QtCore>
32#include <QtTest/QtTest>
33
34#include <QDebug>
35/*!
36 Connect to all of the models signals. Whenever anything happens recheck everything.
37*/
38ModelTest::ModelTest ( QAbstractItemModel *_model, QObject *parent ) : QObject ( parent ), model ( _model ), fetchingMore ( false )
39{
40 if (!model)
41 qFatal("%s: model must not be null", Q_FUNC_INFO);
42
43 connect(model, SIGNAL(columnsAboutToBeInserted(QModelIndex,int,int)),
44 this, SLOT(runAllTests()) );
45 connect(model, SIGNAL(columnsAboutToBeRemoved(QModelIndex,int,int)),
46 this, SLOT(runAllTests()) );
47 connect(model, SIGNAL(columnsInserted(QModelIndex,int,int)),
48 this, SLOT(runAllTests()) );
49 connect(model, SIGNAL(columnsRemoved(QModelIndex,int,int)),
50 this, SLOT(runAllTests()) );
51 connect(model, SIGNAL(dataChanged(QModelIndex,QModelIndex)),
52 this, SLOT(runAllTests()) );
53 connect(model, SIGNAL(headerDataChanged(Qt::Orientation,int,int)),
54 this, SLOT(runAllTests()) );
55 connect(model, SIGNAL(layoutAboutToBeChanged()), this, SLOT(runAllTests()) );
56 connect(model, SIGNAL(layoutChanged()), this, SLOT(runAllTests()) );
57 connect(model, SIGNAL(modelReset()), this, SLOT(runAllTests()) );
58 connect(model, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)),
59 this, SLOT(runAllTests()) );
60 connect(model, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)),
61 this, SLOT(runAllTests()) );
62 connect(model, SIGNAL(rowsInserted(QModelIndex,int,int)),
63 this, SLOT(runAllTests()) );
64 connect(model, SIGNAL(rowsRemoved(QModelIndex,int,int)),
65 this, SLOT(runAllTests()) );
66
67 // Special checks for changes
68 connect(model, SIGNAL(layoutAboutToBeChanged()),
69 this, SLOT(layoutAboutToBeChanged()) );
70 connect(model, SIGNAL(layoutChanged()),
71 this, SLOT(layoutChanged()) );
72
73 connect(model, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)),
74 this, SLOT(rowsAboutToBeInserted(QModelIndex,int,int)) );
75 connect(model, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)),
76 this, SLOT(rowsAboutToBeRemoved(QModelIndex,int,int)) );
77 connect(model, SIGNAL(rowsInserted(QModelIndex,int,int)),
78 this, SLOT(rowsInserted(QModelIndex,int,int)) );
79 connect(model, SIGNAL(rowsRemoved(QModelIndex,int,int)),
80 this, SLOT(rowsRemoved(QModelIndex,int,int)) );
81 connect(model, SIGNAL(dataChanged(QModelIndex,QModelIndex)),
82 this, SLOT(dataChanged(QModelIndex,QModelIndex)) );
83 connect(model, SIGNAL(headerDataChanged(Qt::Orientation,int,int)),
84 this, SLOT(headerDataChanged(Qt::Orientation,int,int)) );
85
86 runAllTests();
87}
88
89void ModelTest::runAllTests()
90{
91 if ( fetchingMore )
92 return;
93 nonDestructiveBasicTest();
94 rowCount();
95 columnCount();
96 hasIndex();
97 index();
98 parent();
99 data();
100}
101
102/*!
103 nonDestructiveBasicTest tries to call a number of the basic functions (not all)
104 to make sure the model doesn't outright segfault, testing the functions that makes sense.
105*/
106void ModelTest::nonDestructiveBasicTest()
107{
108 QVERIFY( model->buddy ( QModelIndex() ) == QModelIndex() );
109 model->canFetchMore ( QModelIndex() );
110 QVERIFY( model->columnCount ( QModelIndex() ) >= 0 );
111 QVERIFY( model->data ( QModelIndex() ) == QVariant() );
112 fetchingMore = true;
113 model->fetchMore ( QModelIndex() );
114 fetchingMore = false;
115 Qt::ItemFlags flags = model->flags ( QModelIndex() );
116 QVERIFY( flags == Qt::ItemIsDropEnabled || flags == 0 );
117 model->hasChildren ( QModelIndex() );
118 model->hasIndex ( 0, 0 );
119 model->headerData ( 0, Qt::Horizontal );
120 model->index ( 0, 0 );
121 model->itemData ( QModelIndex() );
122 QVariant cache;
123 model->match ( QModelIndex(), -1, cache );
124 model->mimeTypes();
125 QVERIFY( model->parent ( QModelIndex() ) == QModelIndex() );
126 QVERIFY( model->rowCount() >= 0 );
127 QVariant variant;
128 model->setData ( QModelIndex(), variant, -1 );
129 model->setHeaderData ( -1, Qt::Horizontal, QVariant() );
130 model->setHeaderData ( 999999, Qt::Horizontal, QVariant() );
131 QMap<int, QVariant> roles;
132 model->sibling ( 0, 0, QModelIndex() );
133 model->span ( QModelIndex() );
134 model->supportedDropActions();
135}
136
137/*!
138 Tests model's implementation of QAbstractItemModel::rowCount() and hasChildren()
139
140 Models that are dynamically populated are not as fully tested here.
141 */
142void ModelTest::rowCount()
143{
144// qDebug() << "rc";
145 // check top row
146 QModelIndex topIndex = model->index ( 0, 0, QModelIndex() );
147 int rows = model->rowCount ( topIndex );
148 QVERIFY( rows >= 0 );
149 if ( rows > 0 )
150 QVERIFY( model->hasChildren ( topIndex ) );
151
152 QModelIndex secondLevelIndex = model->index ( 0, 0, topIndex );
153 if ( secondLevelIndex.isValid() ) { // not the top level
154 // check a row count where parent is valid
155 rows = model->rowCount ( secondLevelIndex );
156 QVERIFY( rows >= 0 );
157 if ( rows > 0 )
158 QVERIFY( model->hasChildren ( secondLevelIndex ) );
159 }
160
161 // The models rowCount() is tested more extensively in checkChildren(),
162 // but this catches the big mistakes
163}
164
165/*!
166 Tests model's implementation of QAbstractItemModel::columnCount() and hasChildren()
167 */
168void ModelTest::columnCount()
169{
170 // check top row
171 QModelIndex topIndex = model->index ( 0, 0, QModelIndex() );
172 QVERIFY( model->columnCount ( topIndex ) >= 0 );
173
174 // check a column count where parent is valid
175 QModelIndex childIndex = model->index ( 0, 0, topIndex );
176 if ( childIndex.isValid() )
177 QVERIFY( model->columnCount ( childIndex ) >= 0 );
178
179 // columnCount() is tested more extensively in checkChildren(),
180 // but this catches the big mistakes
181}
182
183/*!
184 Tests model's implementation of QAbstractItemModel::hasIndex()
185 */
186void ModelTest::hasIndex()
187{
188// qDebug() << "hi";
189 // Make sure that invalid values returns an invalid index
190 QVERIFY( !model->hasIndex ( -2, -2 ) );
191 QVERIFY( !model->hasIndex ( -2, 0 ) );
192 QVERIFY( !model->hasIndex ( 0, -2 ) );
193
194 int rows = model->rowCount();
195 int columns = model->columnCount();
196
197 // check out of bounds
198 QVERIFY( !model->hasIndex ( rows, columns ) );
199 QVERIFY( !model->hasIndex ( rows + 1, columns + 1 ) );
200
201 if ( rows > 0 )
202 QVERIFY( model->hasIndex ( 0, 0 ) );
203
204 // hasIndex() is tested more extensively in checkChildren(),
205 // but this catches the big mistakes
206}
207
208/*!
209 Tests model's implementation of QAbstractItemModel::index()
210 */
211void ModelTest::index()
212{
213// qDebug() << "i";
214 // Make sure that invalid values returns an invalid index
215 QVERIFY( model->index ( -2, -2 ) == QModelIndex() );
216 QVERIFY( model->index ( -2, 0 ) == QModelIndex() );
217 QVERIFY( model->index ( 0, -2 ) == QModelIndex() );
218
219 int rows = model->rowCount();
220 int columns = model->columnCount();
221
222 if ( rows == 0 )
223 return;
224
225 // Catch off by one errors
226 QVERIFY( model->index ( rows, columns ) == QModelIndex() );
227 QVERIFY( model->index ( 0, 0 ).isValid() );
228
229 // Make sure that the same index is *always* returned
230 QModelIndex a = model->index ( 0, 0 );
231 QModelIndex b = model->index ( 0, 0 );
232 QVERIFY( a == b );
233
234 // index() is tested more extensively in checkChildren(),
235 // but this catches the big mistakes
236}
237
238/*!
239 Tests model's implementation of QAbstractItemModel::parent()
240 */
241void ModelTest::parent()
242{
243// qDebug() << "p";
244 // Make sure the model won't crash and will return an invalid QModelIndex
245 // when asked for the parent of an invalid index.
246 QVERIFY( model->parent ( QModelIndex() ) == QModelIndex() );
247
248 if ( model->rowCount() == 0 )
249 return;
250
251 // Column 0 | Column 1 |
252 // QModelIndex() | |
253 // \- topIndex | topIndex1 |
254 // \- childIndex | childIndex1 |
255
256 // Common error test #1, make sure that a top level index has a parent
257 // that is a invalid QModelIndex.
258 QModelIndex topIndex = model->index ( 0, 0, QModelIndex() );
259 QVERIFY( model->parent ( topIndex ) == QModelIndex() );
260
261 // Common error test #2, make sure that a second level index has a parent
262 // that is the first level index.
263 if ( model->rowCount ( topIndex ) > 0 ) {
264 QModelIndex childIndex = model->index ( 0, 0, topIndex );
265 QVERIFY( model->parent ( childIndex ) == topIndex );
266 }
267
268 // Common error test #3, the second column should NOT have the same children
269 // as the first column in a row.
270 // Usually the second column shouldn't have children.
271 QModelIndex topIndex1 = model->index ( 0, 1, QModelIndex() );
272 if ( model->rowCount ( topIndex1 ) > 0 ) {
273 QModelIndex childIndex = model->index ( 0, 0, topIndex );
274 QModelIndex childIndex1 = model->index ( 0, 0, topIndex1 );
275 QVERIFY( childIndex != childIndex1 );
276 }
277
278 // Full test, walk n levels deep through the model making sure that all
279 // parent's children correctly specify their parent.
280 checkChildren ( QModelIndex() );
281}
282
283/*!
284 Called from the parent() test.
285
286 A model that returns an index of parent X should also return X when asking
287 for the parent of the index.
288
289 This recursive function does pretty extensive testing on the whole model in an
290 effort to catch edge cases.
291
292 This function assumes that rowCount(), columnCount() and index() already work.
293 If they have a bug it will point it out, but the above tests should have already
294 found the basic bugs because it is easier to figure out the problem in
295 those tests then this one.
296 */
297void ModelTest::checkChildren ( const QModelIndex &parent, int currentDepth )
298{
299 // First just try walking back up the tree.
300 QModelIndex p = parent;
301 while ( p.isValid() )
302 p = p.parent();
303
304 // For models that are dynamically populated
305 if ( model->canFetchMore ( parent ) ) {
306 fetchingMore = true;
307 model->fetchMore ( parent );
308 fetchingMore = false;
309 }
310
311 int rows = model->rowCount ( parent );
312 int columns = model->columnCount ( parent );
313
314 if ( rows > 0 )
315 QVERIFY( model->hasChildren ( parent ) );
316
317 // Some further testing against rows(), columns(), and hasChildren()
318 QVERIFY( rows >= 0 );
319 QVERIFY( columns >= 0 );
320 if ( rows > 0 )
321 QVERIFY( model->hasChildren ( parent ) );
322
323 qDebug() << "parent:" << model->data(parent).toString() << "rows:" << rows
324 << "columns:" << columns << "parent column:" << parent.column();
325
326 const QModelIndex topLeftChild = model->index( 0, 0, parent );
327
328 QVERIFY( !model->hasIndex ( rows + 1, 0, parent ) );
329 for ( int r = 0; r < rows; ++r ) {
330 if ( model->canFetchMore ( parent ) ) {
331 fetchingMore = true;
332 model->fetchMore ( parent );
333 fetchingMore = false;
334 }
335 QVERIFY( !model->hasIndex ( r, columns + 1, parent ) );
336 for ( int c = 0; c < columns; ++c ) {
337 QVERIFY( model->hasIndex ( r, c, parent ) );
338 QModelIndex index = model->index ( r, c, parent );
339 // rowCount() and columnCount() said that it existed...
340 QVERIFY( index.isValid() );
341
342 qDebug() << "\tchild("<< r <<", " << c << ", " << index.column() << "):" << model->data(index).toString() << index.internalPointer();
343
344 // index() should always return the same index when called twice in a row
345 QModelIndex modifiedIndex = model->index ( r, c, parent );
346 QVERIFY( index == modifiedIndex );
347
348 // Make sure we get the same index if we request it twice in a row
349 QModelIndex a = model->index ( r, c, parent );
350 QModelIndex b = model->index ( r, c, parent );
351 QVERIFY( a == b );
352
353 {
354 const QModelIndex sibling = model->sibling( r, c, topLeftChild );
355 QVERIFY( index == sibling );
356 }
357 {
358 const QModelIndex sibling = topLeftChild.sibling( r, c );
359 QVERIFY( index == sibling );
360 }
361
362 // Some basic checking on the index that is returned
363 QVERIFY( index.model() == model );
364 QCOMPARE( index.row(), r );
365 QCOMPARE( index.column(), c );
366 // While you can technically return a QVariant usually this is a sign
367 // of a bug in data(). Disable if this really is ok in your model.
368// QVERIFY( model->data ( index, Qt::DisplayRole ).isValid() );
369
370 // If the next test fails here is some somewhat useful debug you play with.
371
372 if (model->parent(index) != parent) {
373 qDebug() << r << c << currentDepth << model->data(index).toString()
374 << model->data(parent).toString();
375 qDebug() << index << parent << model->parent(index);
376// And a view that you can even use to show the model.
377// QTreeView view;
378// view.setModel(model);
379// view.show();
380 }
381
382 // Check that we can get back our real parent.
383 QCOMPARE( model->parent ( index ), parent );
384
385 // recursively go down the children
386 if ( model->hasChildren ( index ) && currentDepth < 10 ) {
387 qDebug() << r << c << "has children" << model->rowCount(index);
388 checkChildren ( index, ++currentDepth );
389 }/* else { if (currentDepth >= 10) qDebug() << "checked 10 deep"; };*/
390
391 // make sure that after testing the children that the index doesn't change.
392 QModelIndex newerIndex = model->index ( r, c, parent );
393 QVERIFY( index == newerIndex );
394 }
395 }
396}
397
398/*!
399 Tests model's implementation of QAbstractItemModel::data()
400 */
401void ModelTest::data()
402{
403 // Invalid index should return an invalid qvariant
404 QVERIFY( !model->data ( QModelIndex() ).isValid() );
405
406 if ( model->rowCount() == 0 )
407 return;
408
409 // A valid index should have a valid QVariant data
410 QVERIFY( model->index ( 0, 0 ).isValid() );
411
412 // shouldn't be able to set data on an invalid index
413 QVERIFY( !model->setData ( QModelIndex(), QLatin1String ( "foo" ), Qt::DisplayRole ) );
414
415 // General Purpose roles that should return a QString
416 QVariant variant = model->data ( model->index ( 0, 0 ), Qt::ToolTipRole );
417 if ( variant.isValid() ) {
418 QVERIFY( variant.canConvert<QString>() );
419 }
420 variant = model->data ( model->index ( 0, 0 ), Qt::StatusTipRole );
421 if ( variant.isValid() ) {
422 QVERIFY( variant.canConvert<QString>() );
423 }
424 variant = model->data ( model->index ( 0, 0 ), Qt::WhatsThisRole );
425 if ( variant.isValid() ) {
426 QVERIFY( variant.canConvert<QString>() );
427 }
428
429 // General Purpose roles that should return a QSize
430 variant = model->data ( model->index ( 0, 0 ), Qt::SizeHintRole );
431 if ( variant.isValid() ) {
432 QVERIFY( variant.canConvert<QSize>() );
433 }
434
435 // General Purpose roles that should return a QFont
436 QVariant fontVariant = model->data ( model->index ( 0, 0 ), Qt::FontRole );
437 if ( fontVariant.isValid() ) {
438 QVERIFY( fontVariant.canConvert<QFont>() );
439 }
440
441 // Check that the alignment is one we know about
442 QVariant textAlignmentVariant = model->data ( model->index ( 0, 0 ), Qt::TextAlignmentRole );
443 if ( textAlignmentVariant.isValid() ) {
444 int alignment = textAlignmentVariant.toInt();
445 QCOMPARE( alignment, ( alignment & ( Qt::AlignHorizontal_Mask | Qt::AlignVertical_Mask ) ) );
446 }
447
448 // General Purpose roles that should return a QColor
449 QVariant colorVariant = model->data ( model->index ( 0, 0 ), Qt::BackgroundColorRole );
450 if ( colorVariant.isValid() ) {
451 QVERIFY( colorVariant.canConvert<QColor>() );
452 }
453
454 colorVariant = model->data ( model->index ( 0, 0 ), Qt::TextColorRole );
455 if ( colorVariant.isValid() ) {
456 QVERIFY( colorVariant.canConvert<QColor>() );
457 }
458
459 // Check that the "check state" is one we know about.
460 QVariant checkStateVariant = model->data ( model->index ( 0, 0 ), Qt::CheckStateRole );
461 if ( checkStateVariant.isValid() ) {
462 int state = checkStateVariant.toInt();
463 QVERIFY( state == Qt::Unchecked ||
464 state == Qt::PartiallyChecked ||
465 state == Qt::Checked );
466 }
467}
468
469/*!
470 Store what is about to be inserted to make sure it actually happens
471
472 \sa rowsInserted()
473 */
474void ModelTest::rowsAboutToBeInserted ( const QModelIndex &parent, int start, int /* end */)
475{
476// Q_UNUSED(end);
477// qDebug() << "rowsAboutToBeInserted" << "start=" << start << "end=" << end << "parent=" << model->data ( parent ).toString()
478// << "current count of parent=" << model->rowCount ( parent ); // << "display of last=" << model->data( model->index(start-1, 0, parent) );
479// qDebug() << model->index(start-1, 0, parent) << model->data( model->index(start-1, 0, parent) );
480 Changing c;
481 c.parent = parent;
482 c.oldSize = model->rowCount ( parent );
483 c.last = model->data ( model->index ( start - 1, 0, parent ) );
484 c.next = model->data ( model->index ( start, 0, parent ) );
485 insert.push ( c );
486}
487
488/*!
489 Confirm that what was said was going to happen actually did
490
491 \sa rowsAboutToBeInserted()
492 */
493void ModelTest::rowsInserted ( const QModelIndex & parent, int start, int end )
494{
495 Changing c = insert.pop();
496 QVERIFY( c.parent == parent );
497// qDebug() << "rowsInserted" << "start=" << start << "end=" << end << "oldsize=" << c.oldSize
498// << "parent=" << model->data ( parent ).toString() << "current rowcount of parent=" << model->rowCount ( parent );
499
500// for (int ii=start; ii <= end; ii++)
501// {
502// qDebug() << "itemWasInserted:" << ii << model->data ( model->index ( ii, 0, parent ));
503// }
504// qDebug();
505
506 QVERIFY( c.oldSize + ( end - start + 1 ) == model->rowCount ( parent ) );
507 QVERIFY( c.last == model->data ( model->index ( start - 1, 0, c.parent ) ) );
508
509 if (c.next != model->data(model->index(end + 1, 0, c.parent))) {
510 qDebug() << start << end;
511 for (int i=0; i < model->rowCount(); ++i)
512 qDebug() << model->index(i, 0).data().toString();
513 qDebug() << c.next << model->data(model->index(end + 1, 0, c.parent));
514 }
515
516 QVERIFY( c.next == model->data ( model->index ( end + 1, 0, c.parent ) ) );
517}
518
519void ModelTest::layoutAboutToBeChanged()
520{
521 for ( int i = 0; i < qBound ( 0, model->rowCount(), 100 ); ++i )
522 changing.append ( QPersistentModelIndex ( model->index ( i, 0 ) ) );
523}
524
525void ModelTest::layoutChanged()
526{
527 for ( int i = 0; i < changing.count(); ++i ) {
528 QPersistentModelIndex p = changing[i];
529 QVERIFY( p == model->index ( p.row(), p.column(), p.parent() ) );
530 }
531 changing.clear();
532}
533
534/*!
535 Store what is about to be inserted to make sure it actually happens
536
537 \sa rowsRemoved()
538 */
539void ModelTest::rowsAboutToBeRemoved ( const QModelIndex &parent, int start, int end )
540{
541qDebug() << "ratbr" << parent << start << end;
542 Changing c;
543 c.parent = parent;
544 c.oldSize = model->rowCount ( parent );
545 c.last = model->data ( model->index ( start - 1, 0, parent ) );
546 c.next = model->data ( model->index ( end + 1, 0, parent ) );
547 remove.push ( c );
548}
549
550/*!
551 Confirm that what was said was going to happen actually did
552
553 \sa rowsAboutToBeRemoved()
554 */
555void ModelTest::rowsRemoved ( const QModelIndex & parent, int start, int end )
556{
557 qDebug() << "rr" << parent << start << end;
558 Changing c = remove.pop();
559 QVERIFY( c.parent == parent );
560 QVERIFY( c.oldSize - ( end - start + 1 ) == model->rowCount ( parent ) );
561 QVERIFY( c.last == model->data ( model->index ( start - 1, 0, c.parent ) ) );
562 QVERIFY( c.next == model->data ( model->index ( start, 0, c.parent ) ) );
563}
564
565void ModelTest::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight)
566{
567 QVERIFY(topLeft.isValid());
568 QVERIFY(bottomRight.isValid());
569 QModelIndex commonParent = bottomRight.parent();
570 QCOMPARE(topLeft.parent(), commonParent);
571 QVERIFY(topLeft.row() <= bottomRight.row());
572 QVERIFY(topLeft.column() <= bottomRight.column());
573 int rowCount = model->rowCount(commonParent);
574 int columnCount = model->columnCount(commonParent);
575 QVERIFY(bottomRight.row() < rowCount);
576 QVERIFY(bottomRight.column() < columnCount);
577}
578
579void ModelTest::headerDataChanged(Qt::Orientation orientation, int start, int end)
580{
581 QVERIFY(start >= 0);
582 QVERIFY(end >= 0);
583 QVERIFY(start <= end);
584 int itemCount = orientation == Qt::Vertical ? model->rowCount() : model->columnCount();
585 QVERIFY(start < itemCount);
586 QVERIFY(end < itemCount);
587}
588
diff --git a/framework/src/domain/modeltest.h b/framework/src/domain/modeltest.h
new file mode 100644
index 00000000..735a4227
--- /dev/null
+++ b/framework/src/domain/modeltest.h
@@ -0,0 +1,83 @@
1/****************************************************************************
2**
3** Copyright (C) 2016 The Qt Company Ltd.
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the test suite of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:GPL-EXCEPT$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see https://www.qt.io/terms-conditions. For further
15** information use the contact form at https://www.qt.io/contact-us.
16**
17** GNU General Public License Usage
18** Alternatively, this file may be used under the terms of the GNU
19** General Public License version 3 as published by the Free Software
20** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
21** included in the packaging of this file. Please review the following
22** information to ensure the GNU General Public License requirements will
23** be met: https://www.gnu.org/licenses/gpl-3.0.html.
24**
25** $QT_END_LICENSE$
26**
27****************************************************************************/
28
29
30#ifndef MODELTEST_H
31#define MODELTEST_H
32
33#include <QtCore/QObject>
34#include <QtCore/QAbstractItemModel>
35#include <QtCore/QStack>
36
37class ModelTest : public QObject
38{
39 Q_OBJECT
40
41public:
42 ModelTest( QAbstractItemModel *model, QObject *parent = 0 );
43
44private Q_SLOTS:
45 void nonDestructiveBasicTest();
46 void rowCount();
47 void columnCount();
48 void hasIndex();
49 void index();
50 void parent();
51 void data();
52
53protected Q_SLOTS:
54 void runAllTests();
55 void layoutAboutToBeChanged();
56 void layoutChanged();
57 void rowsAboutToBeInserted( const QModelIndex &parent, int start, int end );
58 void rowsInserted( const QModelIndex & parent, int start, int end );
59 void rowsAboutToBeRemoved( const QModelIndex &parent, int start, int end );
60 void rowsRemoved( const QModelIndex & parent, int start, int end );
61 void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight);
62 void headerDataChanged(Qt::Orientation orientation, int start, int end);
63
64private:
65 void checkChildren( const QModelIndex &parent, int currentDepth = 0 );
66
67 QAbstractItemModel *model;
68
69 struct Changing {
70 QModelIndex parent;
71 int oldSize;
72 QVariant last;
73 QVariant next;
74 };
75 QStack<Changing> insert;
76 QStack<Changing> remove;
77
78 bool fetchingMore;
79
80 QList<QPersistentModelIndex> changing;
81};
82
83#endif
diff --git a/framework/src/domain/mouseproxy.cpp b/framework/src/domain/mouseproxy.cpp
new file mode 100644
index 00000000..9944acb6
--- /dev/null
+++ b/framework/src/domain/mouseproxy.cpp
@@ -0,0 +1,39 @@
1/*
2 Copyright (c) 2017 Christian Mollekopf <mollekopf@kolabsys.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19#include "mouseproxy.h"
20
21MouseProxy::MouseProxy(QQuickItem *parent)
22 : QQuickItem(parent)
23{
24
25}
26
27MouseProxy::~MouseProxy()
28{
29
30}
31
32void MouseProxy::wheelEvent(QWheelEvent *event)
33{
34 if (mTarget && mForwardWheelEvents) {
35 mTarget->event(event);
36 } else {
37 QQuickItem::wheelEvent(event);
38 }
39}
diff --git a/framework/src/domain/mouseproxy.h b/framework/src/domain/mouseproxy.h
new file mode 100644
index 00000000..67b25d06
--- /dev/null
+++ b/framework/src/domain/mouseproxy.h
@@ -0,0 +1,39 @@
1/*
2 Copyright (c) 2017 Christian Mollekopf <mollekopf@kolabsys.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19#include <QQuickItem>
20
21class MouseProxy : public QQuickItem
22{
23 Q_OBJECT
24
25 Q_PROPERTY(QObject* target MEMBER mTarget)
26 Q_PROPERTY(bool forwardWheelEvents MEMBER mForwardWheelEvents)
27
28public:
29 MouseProxy(QQuickItem *parent=0);
30 ~MouseProxy();
31
32protected:
33 virtual void wheelEvent(QWheelEvent *event) override;
34
35private:
36 QObject *mTarget = nullptr;
37 bool mForwardWheelEvents = false;
38};
39
diff --git a/framework/src/domain/objecttreesource.cpp b/framework/src/domain/objecttreesource.cpp
new file mode 100644
index 00000000..567f3516
--- /dev/null
+++ b/framework/src/domain/objecttreesource.cpp
@@ -0,0 +1,152 @@
1/*
2 Copyright (C) 2009 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net
3 Copyright (c) 2009 Andras Mantia <andras@kdab.net>
4
5 This program is free software; you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation; either version 2 of the License, or
8 (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License along
16 with this program; if not, write to the Free Software Foundation, Inc.,
17 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18*/
19
20#include "objecttreesource.h"
21
22#include <MimeTreeParser/AttachmentStrategy>
23#include <MimeTreeParser/BodyPartFormatterBaseFactory>
24#include <MimeTreeParser/MessagePart>
25#include <MimeTreeParser/MessagePartRenderer>
26
27class ObjectSourcePrivate
28{
29public:
30 ObjectSourcePrivate()
31 : mWriter(0)
32 , mAllowDecryption(true)
33 , mHtmlLoadExternal(true)
34 , mPreferredMode(MimeTreeParser::Util::Html)
35 {
36
37 }
38 MimeTreeParser::HtmlWriter *mWriter;
39 MimeTreeParser::BodyPartFormatterBaseFactory mBodyPartFormatterBaseFactory;
40 bool mAllowDecryption;
41 bool mHtmlLoadExternal;
42 MimeTreeParser::Util::HtmlMode mPreferredMode;
43};
44
45ObjectTreeSource::ObjectTreeSource(MimeTreeParser::HtmlWriter *writer)
46 : MimeTreeParser::Interface::ObjectTreeSource()
47 , d(new ObjectSourcePrivate)
48 {
49 d->mWriter = writer;
50 }
51
52ObjectTreeSource::~ObjectTreeSource()
53{
54 delete d;
55}
56
57void ObjectTreeSource::setAllowDecryption(bool allowDecryption)
58{
59 d->mAllowDecryption = allowDecryption;
60}
61
62MimeTreeParser::HtmlWriter *ObjectTreeSource::htmlWriter()
63{
64 return d->mWriter;
65}
66
67bool ObjectTreeSource::htmlLoadExternal() const
68{
69 return d->mHtmlLoadExternal;
70}
71
72void ObjectTreeSource::setHtmlLoadExternal(bool loadExternal)
73{
74 d->mHtmlLoadExternal = loadExternal;
75}
76
77bool ObjectTreeSource::decryptMessage() const
78{
79 return d->mAllowDecryption;
80}
81
82bool ObjectTreeSource::showSignatureDetails() const
83{
84 return true;
85}
86
87int ObjectTreeSource::levelQuote() const
88{
89 return 1;
90}
91
92const QTextCodec *ObjectTreeSource::overrideCodec()
93{
94 return Q_NULLPTR;
95}
96
97QString ObjectTreeSource::createMessageHeader(KMime::Message *message)
98{
99 return QString();
100}
101
102const MimeTreeParser::AttachmentStrategy *ObjectTreeSource::attachmentStrategy()
103{
104 return MimeTreeParser::AttachmentStrategy::smart();
105}
106
107QObject *ObjectTreeSource::sourceObject()
108{
109 return Q_NULLPTR;
110}
111
112void ObjectTreeSource::setHtmlMode(MimeTreeParser::Util::HtmlMode mode, const QList<MimeTreeParser::Util::HtmlMode> &availableModes)
113{
114 Q_UNUSED(mode);
115 Q_UNUSED(availableModes);
116}
117
118MimeTreeParser::Util::HtmlMode ObjectTreeSource::preferredMode() const
119{
120 return d->mPreferredMode;
121}
122
123bool ObjectTreeSource::autoImportKeys() const
124{
125 return false;
126}
127
128bool ObjectTreeSource::showEmoticons() const
129{
130 return false;
131}
132
133bool ObjectTreeSource::showExpandQuotesMark() const
134{
135 return false;
136}
137
138bool ObjectTreeSource::isPrinting() const
139{
140 return false;
141}
142
143const MimeTreeParser::BodyPartFormatterBaseFactory *ObjectTreeSource::bodyPartFormatterFactory()
144{
145 return &(d->mBodyPartFormatterBaseFactory);
146}
147
148MimeTreeParser::Interface::MessagePartRenderer::Ptr ObjectTreeSource::messagePartTheme(MimeTreeParser::Interface::MessagePart::Ptr msgPart)
149{
150 Q_UNUSED(msgPart);
151 return MimeTreeParser::Interface::MessagePartRenderer::Ptr();
152}
diff --git a/framework/src/domain/objecttreesource.h b/framework/src/domain/objecttreesource.h
new file mode 100644
index 00000000..93812dc3
--- /dev/null
+++ b/framework/src/domain/objecttreesource.h
@@ -0,0 +1,57 @@
1/*
2 Copyright (C) 2009 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net
3 Copyright (c) 2009 Andras Mantia <andras@kdab.net>
4
5 This program is free software; you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation; either version 2 of the License, or
8 (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License along
16 with this program; if not, write to the Free Software Foundation, Inc.,
17 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18*/
19
20#ifndef MAILVIEWER_OBJECTTREEEMPTYSOURCE_H
21#define MAILVIEWER_OBJECTTREEEMPTYSOURCE_H
22
23#include <MimeTreeParser/ObjectTreeSource>
24
25class QString;
26
27class ObjectSourcePrivate;
28class ObjectTreeSource : public MimeTreeParser::Interface::ObjectTreeSource
29{
30public:
31 ObjectTreeSource(MimeTreeParser::HtmlWriter *writer);
32 virtual ~ObjectTreeSource();
33 void setHtmlLoadExternal(bool loadExternal);
34 bool decryptMessage() const Q_DECL_OVERRIDE;
35 bool htmlLoadExternal() const Q_DECL_OVERRIDE;
36 bool showSignatureDetails() const Q_DECL_OVERRIDE;
37 void setHtmlMode(MimeTreeParser::Util::HtmlMode mode, const QList<MimeTreeParser::Util::HtmlMode> &availableModes) Q_DECL_OVERRIDE;
38 MimeTreeParser::Util::HtmlMode preferredMode() const Q_DECL_OVERRIDE;
39 void setAllowDecryption(bool allowDecryption);
40 int levelQuote() const Q_DECL_OVERRIDE;
41 const QTextCodec *overrideCodec() Q_DECL_OVERRIDE;
42 QString createMessageHeader(KMime::Message *message) Q_DECL_OVERRIDE;
43 const MimeTreeParser::AttachmentStrategy *attachmentStrategy() Q_DECL_OVERRIDE;
44 MimeTreeParser::HtmlWriter *htmlWriter() Q_DECL_OVERRIDE;
45 QObject *sourceObject() Q_DECL_OVERRIDE;
46 bool autoImportKeys() const Q_DECL_OVERRIDE;
47 bool showEmoticons() const Q_DECL_OVERRIDE;
48 bool showExpandQuotesMark() const Q_DECL_OVERRIDE;
49 bool isPrinting() const Q_DECL_OVERRIDE;
50 const MimeTreeParser::BodyPartFormatterBaseFactory *bodyPartFormatterFactory() Q_DECL_OVERRIDE;
51 MimeTreeParser::Interface::MessagePartRendererPtr messagePartTheme(MimeTreeParser::Interface::MessagePartPtr msgPart) Q_DECL_OVERRIDE;
52private:
53 ObjectSourcePrivate *const d;
54};
55
56#endif
57
diff --git a/framework/src/domain/outboxcontroller.cpp b/framework/src/domain/outboxcontroller.cpp
new file mode 100644
index 00000000..590b6c49
--- /dev/null
+++ b/framework/src/domain/outboxcontroller.cpp
@@ -0,0 +1,47 @@
1/*
2 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsys.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19#include "outboxcontroller.h"
20
21#include <sink/store.h>
22#include <sink/log.h>
23
24OutboxController::OutboxController()
25 : Kube::Controller(),
26 mSynchronizeOutboxAction{new Kube::ControllerAction{this, &OutboxController::sendOutbox}}
27{
28}
29
30Kube::ControllerAction* OutboxController::sendOutboxAction() const
31{
32 return mSynchronizeOutboxAction.data();
33}
34
35void OutboxController::sendOutbox()
36{
37 using namespace Sink;
38 using namespace Sink::ApplicationDomain;
39 Query query;
40 query.containsFilter<SinkResource::Capabilities>(ResourceCapabilities::Mail::transport);
41 auto job = Store::fetchAll<SinkResource>(query)
42 .each([=](const SinkResource::Ptr &resource) -> KAsync::Job<void> {
43 return Store::synchronize(SyncScope{}.resourceFilter(resource->identifier()));
44 });
45 run(job);
46}
47
diff --git a/framework/src/domain/outboxcontroller.h b/framework/src/domain/outboxcontroller.h
new file mode 100644
index 00000000..4c24ad59
--- /dev/null
+++ b/framework/src/domain/outboxcontroller.h
@@ -0,0 +1,39 @@
1/*
2 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsys.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19#pragma once
20
21#include <QObject>
22#include "controller.h"
23
24class OutboxController : public Kube::Controller
25{
26 Q_OBJECT
27 Q_PROPERTY (Kube::ControllerAction* sendOutboxAction READ sendOutboxAction CONSTANT)
28
29public:
30 explicit OutboxController();
31
32 Kube::ControllerAction* sendOutboxAction() const;
33
34private slots:
35 void sendOutbox();
36
37private:
38 QScopedPointer<Kube::ControllerAction> mSynchronizeOutboxAction;
39};
diff --git a/framework/src/domain/outboxmodel.cpp b/framework/src/domain/outboxmodel.cpp
new file mode 100644
index 00000000..237648b1
--- /dev/null
+++ b/framework/src/domain/outboxmodel.cpp
@@ -0,0 +1,141 @@
1/*
2 Copyright (c) 2016 Michael Bohlender <michael.bohlender@kdemail.net>
3 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsys.com>
4
5 This library is free software; you can redistribute it and/or modify it
6 under the terms of the GNU Library General Public License as published by
7 the Free Software Foundation; either version 2 of the License, or (at your
8 option) any later version.
9
10 This library is distributed in the hope that it will be useful, but WITHOUT
11 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
13 License for more details.
14
15 You should have received a copy of the GNU Library General Public License
16 along with this library; see the file COPYING.LIB. If not, write to the
17 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18 02110-1301, USA.
19*/
20
21#include "outboxmodel.h"
22
23#include <QFile>
24#include <QDateTime>
25#include <QString>
26
27#include <sink/standardqueries.h>
28#include <sink/notifier.h>
29#include <sink/notification.h>
30
31
32OutboxModel::OutboxModel(QObject *parent)
33 : QSortFilterProxyModel(),
34 mNotifier(new Sink::Notifier{Sink::Query{}.containsFilter<Sink::ApplicationDomain::SinkResource::Capabilities>(Sink::ApplicationDomain::ResourceCapabilities::Mail::transport)}),
35 mStatus(NoStatus)
36{
37 setDynamicSortFilter(true);
38 sort(0, Qt::DescendingOrder);
39
40 using namespace Sink::ApplicationDomain;
41 auto query = Sink::StandardQueries::outboxMails();
42 query.setFlags(Sink::Query::LiveQuery | Sink::Query::UpdateStatus);
43 query.request<Mail::Subject>();
44 query.request<Mail::Date>();
45 query.request<Mail::Folder>();
46 runQuery(query);
47 connect(this, &QAbstractItemModel::rowsInserted, this, &OutboxModel::countChanged);
48 connect(this, &QAbstractItemModel::rowsRemoved, this, &OutboxModel::countChanged);
49
50 mNotifier->registerHandler([this] (const Sink::Notification &n) {
51 //TODO aggregate status from multiple resources
52 if (n.type == Sink::Notification::Status) {
53 switch (n.code) {
54 case Sink::ApplicationDomain::Status::ErrorStatus:
55 mStatus = ErrorStatus;
56 break;
57 case Sink::ApplicationDomain::Status::BusyStatus:
58 mStatus = InProgressStatus;
59 break;
60 default:
61 mStatus = NoStatus;
62 break;
63 }
64 emit statusChanged();
65 }
66
67 });
68
69}
70
71OutboxModel::~OutboxModel()
72{
73
74}
75
76QHash< int, QByteArray > OutboxModel::roleNames() const
77{
78 QHash<int, QByteArray> roles;
79
80 roles[Subject] = "subject";
81 roles[Date] = "date";
82 roles[Status] = "status";
83 roles[Id] = "id";
84 roles[MimeMessage] = "mimeMessage";
85 roles[DomainObject] = "domainObject";
86
87 return roles;
88}
89
90QVariant OutboxModel::data(const QModelIndex &idx, int role) const
91{
92 auto srcIdx = mapToSource(idx);
93 auto mail = srcIdx.data(Sink::Store::DomainObjectRole).value<Sink::ApplicationDomain::Mail::Ptr>();
94 switch (role) {
95 case Subject:
96 return mail->getSubject();
97 case Date:
98 return mail->getDate();
99 case Status: {
100 const auto status = srcIdx.data(Sink::Store::StatusRole).toInt();
101 if (status == Sink::ApplicationDomain::SyncStatus::SyncInProgress) {
102 return InProgressStatus;
103 }
104 if (status == Sink::ApplicationDomain::SyncStatus::SyncError) {
105 return ErrorStatus;
106 }
107 return PendingStatus;
108 }
109 case Id:
110 return mail->identifier();
111 case DomainObject:
112 return QVariant::fromValue(mail);
113 case MimeMessage: {
114 return mail->getMimeMessage();
115 }
116 }
117 return QSortFilterProxyModel::data(idx, role);
118}
119
120bool OutboxModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
121{
122 const auto leftDate = left.data(Sink::Store::DomainObjectRole).value<Sink::ApplicationDomain::Mail::Ptr>()->getDate();
123 const auto rightDate = right.data(Sink::Store::DomainObjectRole).value<Sink::ApplicationDomain::Mail::Ptr>()->getDate();
124 return leftDate < rightDate;
125}
126
127void OutboxModel::runQuery(const Sink::Query &query)
128{
129 mModel = Sink::Store::loadModel<Sink::ApplicationDomain::Mail>(query);
130 setSourceModel(mModel.data());
131}
132
133int OutboxModel::count() const
134{
135 return rowCount();
136}
137
138int OutboxModel::status() const
139{
140 return mStatus;
141}
diff --git a/framework/src/domain/outboxmodel.h b/framework/src/domain/outboxmodel.h
new file mode 100644
index 00000000..4be9c7f8
--- /dev/null
+++ b/framework/src/domain/outboxmodel.h
@@ -0,0 +1,77 @@
1/*
2 Copyright (c) 2016 Michael Bohlender <michael.bohlender@kdemail.net>
3 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsys.com>
4
5 This library is free software; you can redistribute it and/or modify it
6 under the terms of the GNU Library General Public License as published by
7 the Free Software Foundation; either version 2 of the License, or (at your
8 option) any later version.
9
10 This library is distributed in the hope that it will be useful, but WITHOUT
11 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
13 License for more details.
14
15 You should have received a copy of the GNU Library General Public License
16 along with this library; see the file COPYING.LIB. If not, write to the
17 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18 02110-1301, USA.
19*/
20
21#pragma once
22
23#include <sink/store.h>
24#include <sink/notifier.h>
25
26#include <QSortFilterProxyModel>
27#include <QSharedPointer>
28#include <QStringList>
29
30class OutboxModel : public QSortFilterProxyModel
31{
32 Q_OBJECT
33
34 Q_PROPERTY (int count READ count NOTIFY countChanged)
35 Q_PROPERTY (int status READ status NOTIFY statusChanged)
36
37public:
38 enum Status {
39 NoStatus,
40 PendingStatus,
41 InProgressStatus,
42 ErrorStatus
43 };
44 Q_ENUMS(Status)
45
46 OutboxModel(QObject *parent = Q_NULLPTR);
47 ~OutboxModel();
48
49 QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE;
50
51 bool lessThan(const QModelIndex &left, const QModelIndex &right) const Q_DECL_OVERRIDE;
52
53 enum Roles {
54 Subject = Qt::UserRole + 1,
55 Date,
56 Status,
57 Id,
58 MimeMessage,
59 DomainObject
60 };
61
62 QHash<int, QByteArray> roleNames() const Q_DECL_OVERRIDE;
63
64 void runQuery(const Sink::Query &query);
65
66 int count() const;
67 int status() const;
68
69signals:
70 void statusChanged();
71 void countChanged();
72
73private:
74 QSharedPointer<QAbstractItemModel> mModel;
75 QSharedPointer<Sink::Notifier> mNotifier;
76 int mStatus;
77};
diff --git a/framework/src/domain/peoplemodel.cpp b/framework/src/domain/peoplemodel.cpp
new file mode 100644
index 00000000..c9e7248c
--- /dev/null
+++ b/framework/src/domain/peoplemodel.cpp
@@ -0,0 +1,127 @@
1/*
2 Copyright (c) 2016 Michael Bohlender <michael.bohlender@kdemail.net>
3 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsys.com>
4
5 This library is free software; you can redistribute it and/or modify it
6 under the terms of the GNU Library General Public License as published by
7 the Free Software Foundation; either version 2 of the License, or (at your
8 option) any later version.
9
10 This library is distributed in the hope that it will be useful, but WITHOUT
11 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
13 License for more details.
14
15 You should have received a copy of the GNU Library General Public License
16 along with this library; see the file COPYING.LIB. If not, write to the
17 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18 02110-1301, USA.
19*/
20#include "peoplemodel.h"
21
22#include <sink/standardqueries.h>
23#include <sink/store.h>
24
25PeopleModel::PeopleModel(QObject *parent)
26 : QSortFilterProxyModel()
27{
28 using namespace Sink::ApplicationDomain;
29
30 setDynamicSortFilter(true);
31 sort(0, Qt::DescendingOrder);
32 setFilterCaseSensitivity(Qt::CaseInsensitive);
33 Sink::Query query;
34 query.setFlags(Sink::Query::LiveQuery);
35 query.request<Contact::Fn>();
36 query.request<Contact::Emails>();
37 query.request<Contact::Addressbook>();
38 query.request<Contact::Vcard>();
39 query.request<Contact::Firstname>();
40 query.request<Contact::Lastname>();
41 runQuery(query);
42}
43
44PeopleModel::~PeopleModel()
45{
46
47}
48
49void PeopleModel::setFilter(const QString &filter)
50{
51 setFilterWildcard(filter);
52}
53
54QString PeopleModel::filter() const
55{
56 return {};
57}
58
59QHash< int, QByteArray > PeopleModel::roleNames() const
60{
61 static QHash<int, QByteArray> roles = {
62 {Name, "name"},
63 {Emails, "emails"},
64 {Addressbook, "addressbook"},
65 {Type, "type"},
66 {DomainObject, "domainObject"},
67 {FirstName, "firstName"},
68 {LastName, "lastName"}
69 };
70 return roles;
71}
72
73static QStringList toStringList(const QList<Sink::ApplicationDomain::Contact::Email> &list)
74{
75 QStringList out;
76 std::transform(list.constBegin(), list.constEnd(), std::back_inserter(out), [] (const Sink::ApplicationDomain::Contact::Email &s) { return s.email; });
77 return out;
78}
79
80QVariant PeopleModel::data(const QModelIndex &idx, int role) const
81{
82 auto srcIdx = mapToSource(idx);
83 auto contact = srcIdx.data(Sink::Store::DomainObjectRole).value<Sink::ApplicationDomain::Contact::Ptr>();
84 switch (role) {
85 case Name:
86 return contact->getFn();
87 case Emails:
88 return QVariant::fromValue(toStringList(contact->getEmails()));
89 case Addressbook:
90 return contact->getAddressbook();
91 case Type:
92 return "contact";
93 case DomainObject:
94 return QVariant::fromValue(contact);
95 case FirstName:
96 return contact->getFirstname();
97 case LastName:
98 return contact->getLastname();
99 }
100 return QSortFilterProxyModel::data(idx, role);
101}
102
103bool PeopleModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
104{
105 const auto leftName = left.data(Sink::Store::DomainObjectRole).value<Sink::ApplicationDomain::Contact::Ptr>()->getFn();
106 const auto rightName = right.data(Sink::Store::DomainObjectRole).value<Sink::ApplicationDomain::Contact::Ptr>()->getFn();
107 return leftName < rightName;
108}
109
110void PeopleModel::runQuery(const Sink::Query &query)
111{
112 mModel = Sink::Store::loadModel<Sink::ApplicationDomain::Contact>(query);
113 setSourceModel(mModel.data());
114}
115
116void PeopleModel::setAddressbook(const QVariant &parentFolder)
117{
118 //TODO filter query by addressbook
119 qWarning() << "The addressbook filter is not yet implemented";
120}
121
122QVariant PeopleModel::addressbook() const
123{
124 return QVariant();
125}
126
127
diff --git a/framework/src/domain/peoplemodel.h b/framework/src/domain/peoplemodel.h
new file mode 100644
index 00000000..a59e752c
--- /dev/null
+++ b/framework/src/domain/peoplemodel.h
@@ -0,0 +1,69 @@
1/*
2 Copyright (c) 2016 Michael Bohlender <michael.bohlender@kdemail.net>
3 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsys.com>
4
5 This library is free software; you can redistribute it and/or modify it
6 under the terms of the GNU Library General Public License as published by
7 the Free Software Foundation; either version 2 of the License, or (at your
8 option) any later version.
9
10 This library is distributed in the hope that it will be useful, but WITHOUT
11 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
13 License for more details.
14
15 You should have received a copy of the GNU Library General Public License
16 along with this library; see the file COPYING.LIB. If not, write to the
17 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18 02110-1301, USA.
19*/
20
21#pragma once
22
23#include <QSortFilterProxyModel>
24#include <QSharedPointer>
25
26namespace Sink {
27 class Query;
28};
29
30/**
31 * A model that mixes addressbooks, contact groups and contacts
32 */
33class PeopleModel : public QSortFilterProxyModel
34{
35 Q_OBJECT
36 Q_PROPERTY (QVariant addressbook READ addressbook WRITE setAddressbook)
37 Q_PROPERTY (QString filter READ filter WRITE setFilter)
38
39public:
40 PeopleModel(QObject *parent = Q_NULLPTR);
41 ~PeopleModel();
42
43 QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE;
44
45 bool lessThan(const QModelIndex &left, const QModelIndex &right) const Q_DECL_OVERRIDE;
46
47 enum Roles {
48 Name = Qt::UserRole + 1,
49 Type,
50 Emails,
51 Addressbook,
52 DomainObject,
53 FirstName,
54 LastName
55 };
56
57 QHash<int, QByteArray> roleNames() const Q_DECL_OVERRIDE;
58
59 void runQuery(const Sink::Query &query);
60
61 void setAddressbook(const QVariant &parentFolder);
62 QVariant addressbook() const;
63
64 void setFilter(const QString &mail);
65 QString filter() const;
66
67private:
68 QSharedPointer<QAbstractItemModel> mModel;
69};
diff --git a/framework/src/domain/recepientautocompletionmodel.cpp b/framework/src/domain/recepientautocompletionmodel.cpp
new file mode 100644
index 00000000..2f52db07
--- /dev/null
+++ b/framework/src/domain/recepientautocompletionmodel.cpp
@@ -0,0 +1,134 @@
1/*
2 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsys.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19#include "recepientautocompletionmodel.h"
20
21#include <QStandardItemModel>
22#include <QSettings>
23#include <QStandardPaths>
24#include <QSet>
25#include <QDebug>
26#include <QTimer>
27#include <sink/store.h>
28#include <sink/applicationdomaintype.h>
29
30using namespace Sink::ApplicationDomain;
31
32RecipientAutocompletionModel::RecipientAutocompletionModel(QObject *parent)
33 : QSortFilterProxyModel(),
34 mSourceModel(new QStandardItemModel),
35 mTimer(new QTimer)
36{
37 setSourceModel(mSourceModel.data());
38 setDynamicSortFilter(true);
39 setFilterCaseSensitivity(Qt::CaseInsensitive);
40 mTimer->setSingleShot(true);
41 QObject::connect(mTimer.data(), &QTimer::timeout, this, &RecipientAutocompletionModel::save);
42
43 load();
44}
45
46RecipientAutocompletionModel::~RecipientAutocompletionModel()
47{
48 save();
49}
50
51static QString getPath()
52{
53 return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + "/kube/recepientautocompletion.ini";
54}
55
56void RecipientAutocompletionModel::save()
57{
58 QSet<QString> list;
59 for (int row = 0; row < mSourceModel->rowCount(); row++) {
60 list << mSourceModel->item(row)->data(Text).toString();
61 }
62
63 qWarning() << "Path " << getPath();
64 QSettings settings(getPath(), QSettings::IniFormat);
65 settings.setValue("list", QStringList{list.toList()});
66}
67
68void RecipientAutocompletionModel::load()
69{
70 qWarning() << "Path " << getPath();
71 QSettings settings(getPath(), QSettings::IniFormat);
72 auto list = settings.value("list").toStringList();
73 auto add = [] (const QString &n) {
74 auto item = new QStandardItem{n};
75 item->setData(n, Text);
76 return item;
77 };
78 for (const auto &entry : list) {
79 mSourceModel->appendRow(add(entry));
80 }
81 Sink::Query query;
82 query.request<Contact::Fn>();
83 query.request<Contact::Emails>();
84 Sink::Store::fetchAll<Contact>(query)
85 .then([this] (const QList<Contact::Ptr> &list) {
86 for (const auto &c : list) {
87 for (const auto &email : c->getEmails()) {
88 addToModel(email.email, c->getFn());
89 }
90 }
91 }).exec();
92}
93
94QHash< int, QByteArray > RecipientAutocompletionModel::roleNames() const
95{
96 QHash<int, QByteArray> roles;
97 roles[Text] = "text";
98 roles[Color] = "color";
99 return roles;
100}
101
102
103bool RecipientAutocompletionModel::addToModel(const QString &address, const QString &name)
104{
105 auto add = [] (const QString &n) {
106 auto item = new QStandardItem{n};
107 item->setData(n, Text);
108 return item;
109 };
110 auto formattedName = [&] () {
111 if (name.isEmpty()) {
112 return QString(address);
113 }
114 return QString("%1 <%2>").arg(QString(address), QString(name));
115 }();
116 auto matches = mSourceModel->findItems(formattedName);
117 if (matches.isEmpty()) {
118 mSourceModel->appendRow(add(formattedName));
119 return true;
120 }
121 return false;
122}
123
124void RecipientAutocompletionModel::addEntry(const QByteArray &address, const QByteArray &name)
125{
126 if (addToModel(address, name)) {
127 mTimer->start(100);
128 }
129}
130
131void RecipientAutocompletionModel::setFilter(const QString &filter)
132{
133 setFilterWildcard("*" + filter +"*");
134}
diff --git a/framework/src/domain/recepientautocompletionmodel.h b/framework/src/domain/recepientautocompletionmodel.h
new file mode 100644
index 00000000..22c6f108
--- /dev/null
+++ b/framework/src/domain/recepientautocompletionmodel.h
@@ -0,0 +1,56 @@
1/*
2 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsys.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19
20#pragma once
21
22#include <QSortFilterProxyModel>
23#include <QScopedPointer>
24
25class QStandardItemModel;
26class QTimer;
27
28class RecipientAutocompletionModel : public QSortFilterProxyModel
29{
30 Q_OBJECT
31
32public:
33 RecipientAutocompletionModel(QObject *parent = Q_NULLPTR);
34 ~RecipientAutocompletionModel();
35
36 enum Roles {
37 Text = Qt::UserRole + 1,
38 Color
39 };
40 Q_ENUMS(Roles)
41
42 QHash<int, QByteArray> roleNames() const Q_DECL_OVERRIDE;
43
44 bool addToModel(const QString &address, const QString &name);
45 void addEntry(const QByteArray &address, const QByteArray &name);
46 void setFilter(const QString &);
47
48private slots:
49 void save();
50
51private:
52 void load();
53
54 QScopedPointer<QStandardItemModel> mSourceModel;
55 QScopedPointer<QTimer> mTimer;
56};
diff --git a/framework/src/domain/retriever.cpp b/framework/src/domain/retriever.cpp
new file mode 100644
index 00000000..b8e29523
--- /dev/null
+++ b/framework/src/domain/retriever.cpp
@@ -0,0 +1,55 @@
1/*
2 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsystems.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19
20
21#include "retriever.h"
22
23Retriever::Retriever(QObject *parent) : QObject(parent)
24{
25}
26
27QAbstractItemModel* Retriever::model() const
28{
29 return mModel;
30}
31
32void Retriever::setModel(QAbstractItemModel* model)
33{
34 mValue = QVariant();
35 mModel = model;
36 connect(model, &QAbstractItemModel::rowsInserted, this, &Retriever::onRowsInserted);
37 connect(model, &QAbstractItemModel::modelReset, this, &Retriever::onModelReset);
38 if (model->rowCount(QModelIndex())) {
39 mValue = model->index(0, 0, QModelIndex()).data(mModel->roleNames().key(mPropertyName.toLatin1()));
40 emit valueChanged();
41 }
42}
43
44void Retriever::onRowsInserted(const QModelIndex &parent, int first, int last)
45{
46 if (!mValue.isValid()) {
47 mValue = mModel->index(0, 0, QModelIndex()).data(mModel->roleNames().key(mPropertyName.toLatin1()));
48 emit valueChanged();
49 }
50}
51
52void Retriever::onModelReset()
53{
54 mValue = QVariant();
55}
diff --git a/framework/src/domain/retriever.h b/framework/src/domain/retriever.h
new file mode 100644
index 00000000..e454532d
--- /dev/null
+++ b/framework/src/domain/retriever.h
@@ -0,0 +1,55 @@
1/*
2 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsystems.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19
20#pragma once
21
22#include <QObject>
23#include <QAbstractItemModel>
24#include <QVariant>
25
26/**
27 * A wrapper for a QAbstractItemModel to retrieve a value from a single index via property binding
28 *
29 * Assign a model that retrieves the index, set the property your interested in, and propery-bind "value".
30 */
31class Retriever : public QObject
32{
33 Q_OBJECT
34 Q_PROPERTY(QAbstractItemModel* model READ model WRITE setModel)
35 Q_PROPERTY(QString propertyName MEMBER mPropertyName)
36 Q_PROPERTY(QVariant value MEMBER mValue NOTIFY valueChanged)
37
38public:
39 explicit Retriever(QObject *parent = Q_NULLPTR);
40
41 QAbstractItemModel* model() const;
42 void setModel(QAbstractItemModel* model);
43
44signals:
45 void valueChanged();
46
47private slots:
48 void onRowsInserted(const QModelIndex &parent, int first, int last);
49 void onModelReset();
50
51private:
52 QString mPropertyName;
53 QVariant mValue;
54 QAbstractItemModel *mModel;
55};
diff --git a/framework/src/domain/selector.cpp b/framework/src/domain/selector.cpp
new file mode 100644
index 00000000..d021095b
--- /dev/null
+++ b/framework/src/domain/selector.cpp
@@ -0,0 +1,31 @@
1/*
2 Copyright (c) 2016 Christian Mollekofp <mollekopf@kolabsys.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19#include "selector.h"
20
21#include <QQmlEngine>
22
23Selector::Selector(QAbstractItemModel *model) : mModel{model}
24{
25 QQmlEngine::setObjectOwnership(mModel, QQmlEngine::CppOwnership);
26}
27
28void Selector::reapplyCurrentIndex()
29{
30 setCurrentIndex(currentIndex());
31}
diff --git a/framework/src/domain/selector.h b/framework/src/domain/selector.h
new file mode 100644
index 00000000..358eaa9a
--- /dev/null
+++ b/framework/src/domain/selector.h
@@ -0,0 +1,56 @@
1/*
2 Copyright (c) 2016 Christian Mollekofp <mollekopf@kolabsys.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19#pragma once
20
21#include <QAbstractItemModel>
22#include <QString>
23
24/**
25 * Exposes a model and maintains a current index selection.
26 */
27class Selector : public QObject {
28 Q_OBJECT
29 Q_PROPERTY (int currentIndex READ currentIndex WRITE setCurrentIndex)
30 Q_PROPERTY (QAbstractItemModel* model READ model CONSTANT)
31
32public:
33 Selector(QAbstractItemModel *model);
34
35 virtual QAbstractItemModel *model() { return mModel; }
36
37 void setCurrentIndex(int i) {
38 mCurrentIndex = i;
39 Q_ASSERT(mModel);
40 if (i >= 0) {
41 setCurrent(mModel->index(mCurrentIndex, 0));
42 } else {
43 setCurrent(QModelIndex());
44 }
45 }
46
47 void reapplyCurrentIndex();
48
49 int currentIndex() { return mCurrentIndex; }
50
51 virtual void setCurrent(const QModelIndex &) = 0;
52private:
53 QAbstractItemModel *mModel = nullptr;
54 int mCurrentIndex = 0;
55};
56
diff --git a/framework/src/domain/settings/CMakeLists.txt b/framework/src/domain/settings/CMakeLists.txt
new file mode 100644
index 00000000..dc9d01b1
--- /dev/null
+++ b/framework/src/domain/settings/CMakeLists.txt
@@ -0,0 +1,6 @@
1include_directories(${CMAKE_CURRENT_BINARY_DIR})
2cmake_policy(SET CMP0063 NEW)
3add_executable(sinkactiontest sinkactiontest.cpp)
4add_test(sinkactiontest sinkactiontest)
5qt5_use_modules(sinkactiontest Core Test Concurrent)
6target_link_libraries(sinkactiontest sink actionplugin KF5::Mime mailplugin)
diff --git a/framework/src/domain/settings/accountsettings.cpp b/framework/src/domain/settings/accountsettings.cpp
new file mode 100644
index 00000000..d1019e1f
--- /dev/null
+++ b/framework/src/domain/settings/accountsettings.cpp
@@ -0,0 +1,391 @@
1/*
2 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsys.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19#include "accountsettings.h"
20
21#include <sink/store.h>
22#include <sink/log.h>
23#include <QDebug>
24#include <QDir>
25#include <QUrl>
26
27using namespace Sink;
28using namespace Sink::ApplicationDomain;
29
30SINK_DEBUG_AREA("accountsettings")
31
32AccountSettings::AccountSettings(QObject *parent)
33 : QObject(parent)
34{
35}
36
37void AccountSettings::setAccountType(const QByteArray &type)
38{
39 mAccountType = type;
40}
41
42QByteArray AccountSettings::accountType() const
43{
44 return mAccountType;
45}
46
47void AccountSettings::setAccountIdentifier(const QByteArray &id)
48{
49 if (id.isEmpty()) {
50 return;
51 }
52 mAccountIdentifier = id;
53
54 //Clear
55 mIcon = QString();
56 mName = QString();
57 mImapServer = QString();
58 mImapUsername = QString();
59 mImapPassword = QString();
60 mSmtpServer = QString();
61 mSmtpUsername = QString();
62 mSmtpPassword = QString();
63 mCardDavServer = QString();
64 mCardDavUsername = QString();
65 mCardDavPassword = QString();
66 emit changed();
67 emit imapResourceChanged();
68 emit smtpResourceChanged();
69 emit cardDavResourceChanged();
70
71 load();
72
73}
74
75QByteArray AccountSettings::accountIdentifier() const
76{
77 return mAccountIdentifier;
78}
79
80void AccountSettings::setPath(const QUrl &path)
81{
82 auto normalizedPath = path.path();
83 if (mPath != normalizedPath) {
84 mPath = normalizedPath;
85 emit pathChanged();
86 }
87}
88
89QUrl AccountSettings::path() const
90{
91 return QUrl(mPath);
92}
93
94QValidator *AccountSettings::pathValidator() const
95{
96 class PathValidator : public QValidator {
97 State validate(QString &input, int &pos) const {
98 Q_UNUSED(pos);
99 if (!input.isEmpty() && QDir(input).exists()) {
100 return Acceptable;
101 } else {
102 return Intermediate;
103 }
104 }
105 };
106 static PathValidator *pathValidator = new PathValidator;
107 return pathValidator;
108}
109
110QValidator *AccountSettings::imapServerValidator() const
111{
112 class ImapServerValidator : public QValidator {
113 State validate(QString &input, int &pos) const {
114 Q_UNUSED(pos);
115 // imaps://mainserver.example.net:475
116 const QUrl url(input);
117 static QSet<QString> validProtocols = QSet<QString>() << "imap" << "imaps";
118 if (url.isValid() && validProtocols.contains(url.scheme().toLower())) {
119 return Acceptable;
120 } else {
121 return Intermediate;
122 }
123 }
124 };
125 static ImapServerValidator *validator = new ImapServerValidator;
126 return validator;
127}
128
129QValidator *AccountSettings::smtpServerValidator() const
130{
131 class SmtpServerValidator : public QValidator {
132 State validate(QString &input, int &pos) const {
133 Q_UNUSED(pos);
134 // smtps://mainserver.example.net:475
135 const QUrl url(input);
136 static QSet<QString> validProtocols = QSet<QString>() << "smtp" << "smtps";
137 if (url.isValid() && validProtocols.contains(url.scheme().toLower())) {
138 return Acceptable;
139 } else {
140 return Intermediate;
141 }
142 }
143 };
144 static SmtpServerValidator *validator = new SmtpServerValidator;
145 return validator;
146}
147
148void AccountSettings::saveAccount()
149{
150 if (mAccountIdentifier.isEmpty()) {
151 auto account = ApplicationDomainType::createEntity<SinkAccount>();
152 mAccountIdentifier = account.identifier();
153 Q_ASSERT(!mAccountType.isEmpty());
154 account.setAccountType(mAccountType);
155 account.setName(mName);
156 account.setIcon(mIcon);
157 Store::create(account)
158 .onError([](const KAsync::Error &error) {
159 qWarning() << "Error while creating account: " << error.errorMessage;;
160 })
161 .exec();
162 } else {
163 qDebug() << "Saving account " << mAccountIdentifier << mMailtransportIdentifier;
164 Q_ASSERT(!mAccountIdentifier.isEmpty());
165 SinkAccount account(mAccountIdentifier);
166 account.setAccountType(mAccountType);
167 account.setName(mName);
168 account.setIcon(mIcon);
169 Q_ASSERT(!account.identifier().isEmpty());
170 Store::modify(account)
171 .onError([](const KAsync::Error &error) {
172 qWarning() << "Error while creating account: " << error.errorMessage;;
173 })
174 .exec();
175 }
176}
177
178void AccountSettings::loadAccount()
179{
180 Q_ASSERT(!mAccountIdentifier.isEmpty());
181 Store::fetchOne<SinkAccount>(Query().filter(mAccountIdentifier).request<SinkAccount::Icon>().request<SinkAccount::Name>().request<SinkAccount::AccountType>())
182 .then([this](const SinkAccount &account) {
183 mAccountType = account.getAccountType().toLatin1();
184 mIcon = account.getIcon();
185 mName = account.getName();
186 emit changed();
187 }).exec();
188}
189
190void AccountSettings::loadImapResource()
191{
192 Store::fetchOne<SinkResource>(Query().filter<SinkResource::Account>(mAccountIdentifier).containsFilter<SinkResource::Capabilities>(ResourceCapabilities::Mail::storage))
193 .then([this](const SinkResource &resource) {
194 mImapIdentifier = resource.identifier();
195 mImapServer = resource.getProperty("server").toString();
196 mImapUsername = resource.getProperty("username").toString();
197 mImapPassword = resource.getProperty("password").toString();
198 emit imapResourceChanged();
199 }).onError([](const KAsync::Error &error) {
200 qWarning() << "Failed to find the imap resource: " << error.errorMessage;
201 }).exec();
202}
203
204void AccountSettings::loadMaildirResource()
205{
206 Store::fetchOne<SinkResource>(Query().filter<SinkResource::Account>(mAccountIdentifier).containsFilter<SinkResource::Capabilities>(ResourceCapabilities::Mail::storage))
207 .then([this](const SinkResource &resource) {
208 mMaildirIdentifier = resource.identifier();
209 auto path = resource.getProperty("path").toString();
210 if (mPath != path) {
211 mPath = path;
212 emit pathChanged();
213 }
214 }).onError([](const KAsync::Error &error) {
215 SinkWarning() << "Failed to find the maildir resource: " << error.errorMessage;
216 }).exec();
217}
218
219void AccountSettings::loadMailtransportResource()
220{
221 Store::fetchOne<SinkResource>(Query().filter<SinkResource::Account>(mAccountIdentifier).containsFilter<SinkResource::Capabilities>(ResourceCapabilities::Mail::transport))
222 .then([this](const SinkResource &resource) {
223 mMailtransportIdentifier = resource.identifier();
224 mSmtpServer = resource.getProperty("server").toString();
225 mSmtpUsername = resource.getProperty("username").toString();
226 mSmtpPassword = resource.getProperty("password").toString();
227 emit smtpResourceChanged();
228 }).onError([](const KAsync::Error &error) {
229 SinkWarning() << "Failed to find the smtp resource: " << error.errorMessage;
230 }).exec();
231}
232
233void AccountSettings::loadIdentity()
234{
235 //FIXME this assumes that we only ever have one identity per account
236 Store::fetchOne<Identity>(Query().filter<Identity::Account>(mAccountIdentifier))
237 .then([this](const Identity &identity) {
238 mIdentityIdentifier = identity.identifier();
239 mUsername = identity.getName();
240 mEmailAddress = identity.getAddress();
241 emit identityChanged();
242 }).onError([](const KAsync::Error &error) {
243 SinkWarning() << "Failed to find the identity resource: " << error.errorMessage;
244 }).exec();
245}
246
247void AccountSettings::loadCardDavResource()
248{
249 Store::fetchOne<SinkResource>(Query().filter<SinkResource::Account>(mAccountIdentifier).containsFilter<SinkResource::Capabilities>(ResourceCapabilities::Mail::storage))
250 .then([this](const SinkResource &resource) {
251 mCardDavIdentifier = resource.identifier();
252 mCardDavServer = resource.getProperty("server").toString();
253 mCardDavUsername = resource.getProperty("username").toString();
254 mCardDavPassword = resource.getProperty("password").toString();
255 emit cardDavResourceChanged();
256 }).onError([](const KAsync::Error &error) {
257 qWarning() << "Failed to find the CardDAV resource: " << error.errorMessage;
258 }).exec();
259}
260
261
262template<typename ResourceType>
263static QByteArray saveResource(const QByteArray &accountIdentifier, const QByteArray &identifier, const std::map<QByteArray, QVariant> &properties)
264{
265 if (!identifier.isEmpty()) {
266 SinkResource resource(identifier);
267 for (const auto &pair : properties) {
268 resource.setProperty(pair.first, pair.second);
269 }
270 Store::modify(resource)
271 .onError([](const KAsync::Error &error) {
272 SinkWarning() << "Error while modifying resource: " << error.errorMessage;
273 })
274 .exec();
275 } else {
276 auto resource = ResourceType::create(accountIdentifier);
277 auto newIdentifier = resource.identifier();
278 for (const auto &pair : properties) {
279 resource.setProperty(pair.first, pair.second);
280 }
281 Store::create(resource)
282 .onError([](const KAsync::Error &error) {
283 SinkWarning() << "Error while creating resource: " << error.errorMessage;
284 })
285 .exec();
286 return newIdentifier;
287 }
288 return identifier;
289}
290
291void AccountSettings::saveImapResource()
292{
293 mImapIdentifier = saveResource<ImapResource>(mAccountIdentifier, mImapIdentifier, {
294 {"server", mImapServer},
295 {"username", mImapUsername},
296 {"password", mImapPassword},
297 });
298}
299
300void AccountSettings::saveCardDavResource()
301{
302 mCardDavIdentifier = saveResource<CardDavResource>(mAccountIdentifier, mCardDavIdentifier, {
303 {"server", mCardDavServer},
304 {"username", mCardDavUsername},
305 {"password", mCardDavPassword},
306 });
307}
308
309void AccountSettings::saveMaildirResource()
310{
311 mMaildirIdentifier = saveResource<MaildirResource>(mAccountIdentifier, mMaildirIdentifier, {
312 {"path", mPath},
313 });
314}
315
316void AccountSettings::saveMailtransportResource()
317{
318 mMailtransportIdentifier = saveResource<MailtransportResource>(mAccountIdentifier, mMailtransportIdentifier, {
319 {"server", mSmtpServer},
320 {"username", mSmtpUsername},
321 {"password", mSmtpPassword},
322 });
323}
324
325void AccountSettings::saveIdentity()
326{
327 if (!mIdentityIdentifier.isEmpty()) {
328 Identity identity(mMailtransportIdentifier);
329 identity.setName(mUsername);
330 identity.setAddress(mEmailAddress);
331 Store::modify(identity)
332 .onError([](const KAsync::Error &error) {
333 SinkWarning() << "Error while modifying identity: " << error.errorMessage;
334 })
335 .exec();
336 } else {
337 auto identity = ApplicationDomainType::createEntity<Identity>();
338 mIdentityIdentifier = identity.identifier();
339 identity.setAccount(mAccountIdentifier);
340 identity.setName(mUsername);
341 identity.setAddress(mEmailAddress);
342 Store::create(identity)
343 .onError([](const KAsync::Error &error) {
344 SinkWarning() << "Error while creating identity: " << error.errorMessage;
345 })
346 .exec();
347 }
348}
349
350void AccountSettings::removeResource(const QByteArray &identifier)
351{
352 if (identifier.isEmpty()) {
353 SinkWarning() << "We're missing an identifier";
354 } else {
355 SinkResource resource(identifier);
356 Store::remove(resource)
357 .onError([](const KAsync::Error &error) {
358 SinkWarning() << "Error while removing resource: " << error.errorMessage;
359 })
360 .exec();
361 }
362}
363
364void AccountSettings::removeAccount()
365{
366 if (mAccountIdentifier.isEmpty()) {
367 SinkWarning() << "We're missing an identifier";
368 } else {
369 SinkAccount account(mAccountIdentifier);
370 Store::remove(account)
371 .onError([](const KAsync::Error &error) {
372 SinkWarning() << "Error while removing account: " << error.errorMessage;
373 })
374 .exec();
375 }
376}
377
378void AccountSettings::removeIdentity()
379{
380 if (mIdentityIdentifier.isEmpty()) {
381 SinkWarning() << "We're missing an identifier";
382 } else {
383 Identity identity(mIdentityIdentifier);
384 Store::remove(identity)
385 .onError([](const KAsync::Error &error) {
386 SinkWarning() << "Error while removing identity: " << error.errorMessage;
387 })
388 .exec();
389 }
390}
391
diff --git a/framework/src/domain/settings/accountsettings.h b/framework/src/domain/settings/accountsettings.h
new file mode 100644
index 00000000..077b7784
--- /dev/null
+++ b/framework/src/domain/settings/accountsettings.h
@@ -0,0 +1,123 @@
1/*
2 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsys.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19#pragma once
20
21#include <QObject>
22#include <QValidator>
23
24class AccountSettings : public QObject
25{
26 Q_OBJECT
27 Q_PROPERTY(QByteArray accountIdentifier READ accountIdentifier WRITE setAccountIdentifier)
28 Q_PROPERTY(QByteArray accountType READ accountType WRITE setAccountType)
29 Q_PROPERTY(QString icon MEMBER mIcon NOTIFY changed)
30 Q_PROPERTY(QString accountName MEMBER mName NOTIFY changed)
31
32 Q_PROPERTY(QString userName MEMBER mUsername NOTIFY identityChanged)
33 Q_PROPERTY(QString emailAddress MEMBER mEmailAddress NOTIFY identityChanged)
34
35 Q_PROPERTY(QString imapServer MEMBER mImapServer NOTIFY imapResourceChanged)
36 Q_PROPERTY(QValidator* imapServerValidator READ imapServerValidator CONSTANT)
37 Q_PROPERTY(QString imapUsername MEMBER mImapUsername NOTIFY imapResourceChanged)
38 Q_PROPERTY(QString imapPassword MEMBER mImapPassword NOTIFY imapResourceChanged)
39
40 Q_PROPERTY(QString smtpServer MEMBER mSmtpServer NOTIFY smtpResourceChanged)
41 Q_PROPERTY(QValidator* smtpServerValidator READ smtpServerValidator CONSTANT)
42 Q_PROPERTY(QString smtpUsername MEMBER mSmtpUsername NOTIFY smtpResourceChanged)
43 Q_PROPERTY(QString smtpPassword MEMBER mSmtpPassword NOTIFY smtpResourceChanged)
44
45 Q_PROPERTY(QUrl path READ path WRITE setPath NOTIFY pathChanged)
46 Q_PROPERTY(QValidator* pathValidator READ pathValidator CONSTANT)
47
48public:
49 AccountSettings(QObject *parent = 0);
50
51 void setAccountIdentifier(const QByteArray &);
52 QByteArray accountIdentifier() const;
53
54 void setAccountType(const QByteArray &);
55 QByteArray accountType() const;
56
57 void setPath(const QUrl &);
58 QUrl path() const;
59
60 virtual QValidator *imapServerValidator() const;
61 virtual QValidator *smtpServerValidator() const;
62 virtual QValidator *pathValidator() const;
63
64 Q_INVOKABLE virtual void load() = 0;
65 Q_INVOKABLE virtual void save() = 0;
66 Q_INVOKABLE virtual void remove() = 0;
67
68signals:
69 void imapResourceChanged();
70 void smtpResourceChanged();
71 void identityChanged();
72 void pathChanged();
73 void changed();
74 void cardDavResourceChanged();
75
76protected:
77 void saveAccount();
78 void saveImapResource();
79 void saveMaildirResource();
80 void saveMailtransportResource();
81 void saveIdentity();
82 void saveCardDavResource();
83
84 void loadAccount();
85 void loadImapResource();
86 void loadMaildirResource();
87 void loadMailtransportResource();
88 void loadIdentity();
89 void loadCardDavResource();
90
91 void removeAccount();
92 void removeResource(const QByteArray &identifier);
93
94 void removeIdentity();
95
96 QByteArray mAccountIdentifier;
97 QByteArray mAccountType;
98 QString mIcon;
99 QString mName;
100
101 QByteArray mImapIdentifier;
102 QString mImapServer;
103 QString mImapUsername;
104 QString mImapPassword;
105
106 QByteArray mMaildirIdentifier;
107 QString mPath;
108
109 QByteArray mMailtransportIdentifier;
110 QString mSmtpServer;
111 QString mSmtpUsername;
112 QString mSmtpPassword;
113
114 QByteArray mIdentityIdentifier;
115 QString mUsername;
116 QString mEmailAddress;
117
118 QByteArray mCardDavIdentifier;
119 QString mCardDavServer;
120 QString mCardDavUsername;
121 QString mCardDavPassword;
122};
123
diff --git a/framework/src/domain/stringhtmlwriter.cpp b/framework/src/domain/stringhtmlwriter.cpp
new file mode 100644
index 00000000..88034492
--- /dev/null
+++ b/framework/src/domain/stringhtmlwriter.cpp
@@ -0,0 +1,150 @@
1/* -*- c++ -*-
2 filehtmlwriter.cpp
3
4 This file is part of KMail, the KDE mail client.
5 Copyright (c) 2003 Marc Mutz <mutz@kde.org>
6
7 KMail is free software; you can redistribute it and/or modify it
8 under the terms of the GNU General Public License, version 2, as
9 published by the Free Software Foundation.
10
11 KMail is distributed in the hope that it will be useful, but
12 WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with this program; if not, write to the Free Software
18 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19
20 In addition, as a special exception, the copyright holders give
21 permission to link the code of this program with any edition of
22 the Qt library by Trolltech AS, Norway (or with modified versions
23 of Qt that use the same license as Qt), and distribute linked
24 combinations including the two. You must obey the GNU General
25 Public License in all respects for all of the code used other than
26 Qt. If you modify this file, you may extend this exception to
27 your version of the file, but you are not obligated to do so. If
28 you do not wish to do so, delete this exception statement from
29 your version.
30*/
31
32#include "stringhtmlwriter.h"
33
34#include <QDebug>
35#include <QTextStream>
36#include <QUrl>
37
38StringHtmlWriter::StringHtmlWriter()
39 : MimeTreeParser::HtmlWriter()
40 , mState(Ended)
41{
42}
43
44StringHtmlWriter::~StringHtmlWriter()
45{
46}
47
48void StringHtmlWriter::begin(const QString &css)
49{
50 if (mState != Ended) {
51 qWarning() << "begin() called on non-ended session!";
52 reset();
53 }
54
55 mState = Begun;
56 mExtraHead.clear();
57 mHtml.clear();
58
59 if (!css.isEmpty()) {
60 write(QLatin1String("<!-- CSS Definitions \n") + css + QLatin1String("-->\n"));
61 }
62}
63
64void StringHtmlWriter::end()
65{
66 if (mState != Begun) {
67 qWarning() << "Called on non-begun or queued session!";
68 }
69
70 if (!mExtraHead.isEmpty()) {
71 insertExtraHead();
72 mExtraHead.clear();
73 }
74 resolveCidUrls();
75 mState = Ended;
76}
77
78void StringHtmlWriter::reset()
79{
80 if (mState != Ended) {
81 mHtml.clear();
82 mExtraHead.clear();
83 mState = Begun; // don't run into end()'s warning
84 end();
85 mState = Ended;
86 }
87}
88
89void StringHtmlWriter::write(const QString &str)
90{
91 if (mState != Begun) {
92 qWarning() << "Called in Ended or Queued state!";
93 }
94 mHtml.append(str);
95}
96
97void StringHtmlWriter::queue(const QString &str)
98{
99 write(str);
100}
101
102void StringHtmlWriter::flush()
103{
104 mState = Begun; // don't run into end()'s warning
105 end();
106}
107
108void StringHtmlWriter::embedPart(const QByteArray &contentId, const QString &url)
109{
110 write("<!-- embedPart(contentID=" + contentId + ", url=" + url + ") -->\n");
111 mEmbeddedPartMap.insert(contentId, url);
112}
113
114void StringHtmlWriter::resolveCidUrls()
115{
116 for (const auto &cid : mEmbeddedPartMap.keys()) {
117 mHtml.replace(QString("src=\"cid:%1\"").arg(QString(cid)), QString("src=\"%1\"").arg(mEmbeddedPartMap.value(cid).toString()));
118 }
119}
120
121void StringHtmlWriter::extraHead(const QString &extraHead)
122{
123 if (mState != Ended) {
124 qWarning() << "Called on non-started session!";
125 }
126 mExtraHead.append(extraHead);
127}
128
129
130void StringHtmlWriter::insertExtraHead()
131{
132 const QString headTag(QStringLiteral("<head>"));
133 const int index = mHtml.indexOf(headTag);
134 if (index != -1) {
135 mHtml.insert(index + headTag.length(), mExtraHead);
136 }
137}
138
139QMap<QByteArray, QUrl> StringHtmlWriter::embeddedParts() const
140{
141 return mEmbeddedPartMap;
142}
143
144QString StringHtmlWriter::html() const
145{
146 if (mState != Ended) {
147 qWarning() << "Called on non-ended session!";
148 }
149 return mHtml;
150}
diff --git a/framework/src/domain/stringhtmlwriter.h b/framework/src/domain/stringhtmlwriter.h
new file mode 100644
index 00000000..fa5b760e
--- /dev/null
+++ b/framework/src/domain/stringhtmlwriter.h
@@ -0,0 +1,71 @@
1/* -*- c++ -*-
2
3 Copyright (c) 2016 Sandro Knauß <sknauss@kde.org>
4
5 Kube is free software; you can redistribute it and/or modify it
6 under the terms of the GNU General Public License, version 2, as
7 published by the Free Software Foundation.
8
9 Kube is distributed in the hope that it will be useful, but
10 WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with this program; if not, write to the Free Software
16 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
18 In addition, as a special exception, the copyright holders give
19 permission to link the code of this program with any edition of
20 the Qt library by Trolltech AS, Norway (or with modified versions
21 of Qt that use the same license as Qt), and distribute linked
22 combinations including the two. You must obey the GNU General
23 Public License in all respects for all of the code used other than
24 Qt. If you modify this file, you may extend this exception to
25 your version of the file, but you are not obligated to do so. If
26 you do not wish to do so, delete this exception statement from
27 your version.
28*/
29
30#ifndef __KUBE_FRAMEWORK_MAIL_STRINGHTMLWRITER_H__
31#define __KUBE_FRAMEWORK_MAIL_STRINGHTMLWRITER_H__
32
33#include <MimeTreeParser/HtmlWriter>
34
35#include <QFile>
36#include <QTextStream>
37
38class QString;
39
40class StringHtmlWriter : public MimeTreeParser::HtmlWriter
41{
42public:
43 explicit StringHtmlWriter();
44 virtual ~StringHtmlWriter();
45
46 void begin(const QString &cssDefs) Q_DECL_OVERRIDE;
47 void end() Q_DECL_OVERRIDE;
48 void reset() Q_DECL_OVERRIDE;
49 void write(const QString &str) Q_DECL_OVERRIDE;
50 void queue(const QString &str) Q_DECL_OVERRIDE;
51 void flush() Q_DECL_OVERRIDE;
52 void embedPart(const QByteArray &contentId, const QString &url) Q_DECL_OVERRIDE;
53 void extraHead(const QString &str) Q_DECL_OVERRIDE;
54
55 QString html() const;
56 QMap<QByteArray, QUrl> embeddedParts() const;
57private:
58 void insertExtraHead();
59 void resolveCidUrls();
60
61 QString mHtml;
62 QString mExtraHead;
63 enum State {
64 Begun,
65 Queued,
66 Ended
67 } mState;
68 QMap<QByteArray, QUrl> mEmbeddedPartMap;
69};
70
71#endif // __MESSAGEVIEWER_FILEHTMLWRITER_H__
diff --git a/framework/src/frameworkplugin.cpp b/framework/src/frameworkplugin.cpp
new file mode 100644
index 00000000..f491bae7
--- /dev/null
+++ b/framework/src/frameworkplugin.cpp
@@ -0,0 +1,72 @@
1/*
2 Copyright (c) 2016 Michael Bohlender <michael.bohlender@kdemail.net>
3 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsys.com>
4
5 This library is free software; you can redistribute it and/or modify it
6 under the terms of the GNU Library General Public License as published by
7 the Free Software Foundation; either version 2 of the License, or (at your
8 option) any later version.
9
10 This library is distributed in the hope that it will be useful, but WITHOUT
11 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
13 License for more details.
14
15 You should have received a copy of the GNU Library General Public License
16 along with this library; see the file COPYING.LIB. If not, write to the
17 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18 02110-1301, USA.
19*/
20
21#include "frameworkplugin.h"
22
23#include "domain/maillistmodel.h"
24#include "domain/folderlistmodel.h"
25#include "domain/composercontroller.h"
26#include "domain/messageparser.h"
27#include "domain/retriever.h"
28#include "domain/outboxmodel.h"
29#include "domain/outboxcontroller.h"
30#include "domain/mailcontroller.h"
31#include "domain/foldercontroller.h"
32#include "domain/mouseproxy.h"
33#include "domain/contactcontroller.h"
34#include "domain/peoplemodel.h"
35#include "accounts/accountsmodel.h"
36#include "accounts/accountfactory.h"
37#include "settings/settings.h"
38#include "notifications/notificationhandler.h"
39#include "actions/action.h"
40#include "actions/context.h"
41#include "actions/actionhandler.h"
42#include "actions/actionresult.h"
43
44#include <QtQml>
45
46void FrameworkPlugin::registerTypes (const char *uri)
47{
48 qmlRegisterType<FolderListModel>(uri, 1, 0, "FolderListModel");
49 qmlRegisterType<MailListModel>(uri, 1, 0, "MailListModel");
50 qmlRegisterType<ComposerController>(uri, 1, 0, "ComposerController");
51 qmlRegisterType<MessageParser>(uri, 1, 0, "MessageParser");
52 qmlRegisterType<Retriever>(uri, 1, 0, "Retriever");
53 qmlRegisterType<OutboxController>(uri, 1, 0, "OutboxController");
54 qmlRegisterType<OutboxModel>(uri, 1, 0, "OutboxModel");
55 qmlRegisterType<MailController>(uri, 1, 0, "MailController");
56 qmlRegisterType<FolderController>(uri, 1, 0, "FolderController");
57 qmlRegisterType<MouseProxy>(uri, 1, 0, "MouseProxy");
58 qmlRegisterType<ContactController>(uri, 1, 0,"ContactController");
59 qmlRegisterType<PeopleModel>(uri, 1, 0,"PeopleModel");
60
61 qmlRegisterType<AccountFactory>(uri, 1, 0, "AccountFactory");
62 qmlRegisterType<AccountsModel>(uri, 1, 0, "AccountsModel");
63
64 qmlRegisterType<Kube::Settings>(uri, 1, 0, "Settings");
65 qmlRegisterType<Kube::NotificationHandler>(uri, 1, 0, "NotificationHandler");
66 qmlRegisterType<Kube::Notification>(uri, 1, 0, "Notification");
67
68 qmlRegisterType<Kube::Context>(uri, 1, 0, "Context");
69 qmlRegisterType<Kube::Action>(uri, 1, 0, "Action");
70 qmlRegisterType<Kube::ActionHandler>(uri, 1, 0, "ActionHandler");
71 qmlRegisterType<Kube::ActionResult>(uri, 1, 0, "ActionResult");
72}
diff --git a/framework/src/frameworkplugin.h b/framework/src/frameworkplugin.h
new file mode 100644
index 00000000..519e0ba9
--- /dev/null
+++ b/framework/src/frameworkplugin.h
@@ -0,0 +1,33 @@
1/*
2 Copyright (c) 2016 Michael Bohlender <michael.bohlender@kdemail.net>
3 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsys.com>
4
5 This library is free software; you can redistribute it and/or modify it
6 under the terms of the GNU Library General Public License as published by
7 the Free Software Foundation; either version 2 of the License, or (at your
8 option) any later version.
9
10 This library is distributed in the hope that it will be useful, but WITHOUT
11 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
13 License for more details.
14
15 You should have received a copy of the GNU Library General Public License
16 along with this library; see the file COPYING.LIB. If not, write to the
17 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18 02110-1301, USA.
19*/
20
21#pragma once
22
23#include <QQmlEngine>
24#include <QQmlExtensionPlugin>
25
26class FrameworkPlugin : public QQmlExtensionPlugin
27{
28 Q_OBJECT
29 Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface")
30
31public:
32 virtual void registerTypes(const char *uri);
33};
diff --git a/framework/src/notifications/notificationhandler.cpp b/framework/src/notifications/notificationhandler.cpp
new file mode 100644
index 00000000..093d638a
--- /dev/null
+++ b/framework/src/notifications/notificationhandler.cpp
@@ -0,0 +1,80 @@
1/*
2 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsys.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19
20#include "notificationhandler.h"
21
22#include <QDebug>
23
24#include <sink/notifier.h>
25#include <sink/notification.h>
26#include <sink/query.h>
27
28using namespace Kube;
29
30NotificationHandler::NotificationHandler(QObject *parent)
31 : QObject(parent)
32{
33 Sink::Query query{Sink::Query::LiveQuery};
34 mNotifier.reset(new Sink::Notifier{query});
35 mNotifier->registerHandler([this] (const Sink::Notification &notification) {
36 notify(notification);
37 });
38
39}
40
41void NotificationHandler::notify(const Sink::Notification &notification)
42{
43 Notification n;
44 qWarning() << "Received notification: " << notification;
45 if (notification.type == Sink::Notification::Warning) {
46 n.mType = Notification::Warning;
47 if (notification.code == Sink::ApplicationDomain::TransmissionError) {
48 n.mMessage = "Failed to send message.";
49 } else {
50 return;
51 }
52 } else if (notification.type == Sink::Notification::Status) {
53 if (notification.code == Sink::ApplicationDomain::ErrorStatus) {
54 //A resource entered error status
55 n.mType = Notification::Warning;
56 n.mMessage = "A resource experienced an error.";
57 } else {
58 return;
59 }
60 } else if (notification.type == Sink::Notification::Error) {
61 if (notification.code == Sink::ApplicationDomain::ConnectionError) {
62 n.mType = Notification::Warning;
63 n.mMessage = "Failed to connect to server.";
64 } else {
65 return;
66 }
67 } else if (notification.type == Sink::Notification::Info) {
68 n.mType = Notification::Info;
69 if (notification.code == Sink::ApplicationDomain::TransmissionSuccess) {
70 n.mMessage = "A message has been sent.";
71 } else {
72 return;
73 }
74 } else {
75 return;
76 }
77 //The base implementation to call the handler in QML
78 QMetaObject::invokeMethod(this, "handler", Q_ARG(QVariant, QVariant::fromValue(&n)));
79}
80
diff --git a/framework/src/notifications/notificationhandler.h b/framework/src/notifications/notificationhandler.h
new file mode 100644
index 00000000..fa992e87
--- /dev/null
+++ b/framework/src/notifications/notificationhandler.h
@@ -0,0 +1,80 @@
1/*
2 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsys.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19#pragma once
20
21#include <QObject>
22#include <QVariant>
23#include <functional>
24#include <sink/notifier.h>
25
26namespace Sink {
27 class Notification;
28}
29
30namespace Kube {
31
32class Notification : public QObject
33{
34 Q_OBJECT
35
36public:
37 enum Type {
38 Info,
39 Warning
40 };
41 Q_ENUMS(Type)
42
43 Q_PROPERTY(Type type MEMBER mType CONSTANT)
44 Q_PROPERTY(QString message MEMBER mMessage CONSTANT)
45
46 Notification() = default;
47 ~Notification() = default;
48
49 Type mType;
50 QString mMessage;
51};
52
53/**
54 * A notification handler.
55 *
56 * Can beinstantiated from QML like so:
57 * NotificationHandler {
58 * function handler(notification) {
59 * ...do something
60 * }
61 * }
62 *
63 * The handler will listen for all notifications by default.
64 */
65class NotificationHandler : public QObject
66{
67 Q_OBJECT
68
69public:
70 NotificationHandler(QObject *parent = 0);
71
72 virtual void notify(const Sink::Notification &notification);
73
74private:
75 QScopedPointer<Sink::Notifier> mNotifier;
76};
77
78}
79
80Q_DECLARE_METATYPE(Kube::Notification*);
diff --git a/framework/src/settings/settings.cpp b/framework/src/settings/settings.cpp
new file mode 100644
index 00000000..00d1b9c5
--- /dev/null
+++ b/framework/src/settings/settings.cpp
@@ -0,0 +1,174 @@
1/*
2 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsys.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19#include "settings.h"
20
21#include <QDebug>
22#include <QStandardPaths>
23#include <QMetaObject>
24#include <QMetaProperty>
25#include <QFile>
26
27using namespace Kube;
28
29Settings::Settings(QObject *parent)
30 : QObject(parent),
31 mLoaded(false)
32{
33
34}
35
36Settings::Settings(const QByteArray &id, QObject *parent)
37 : QObject(parent),
38 mIdentifier(id),
39 mLoaded(false)
40{
41 load();
42}
43
44Settings::Settings(const Settings &other)
45 : QObject(other.parent()),
46 mIdentifier(other.mIdentifier),
47 mLoaded(false)
48{
49 load();
50}
51
52Settings::~Settings()
53{
54}
55
56QSharedPointer<QSettings> Settings::getSettings()
57{
58 return QSharedPointer<QSettings>::create(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QString("/kube/%1.ini").arg(QString::fromLatin1(mIdentifier)), QSettings::IniFormat);
59}
60
61void Settings::save()
62{
63 qWarning() << "Saving" << mIdentifier;
64 auto settings = getSettings();
65
66 for (const auto &p : dynamicPropertyNames()) {
67 qWarning() << "setting " << p << property(p);
68 if (p == "identifier" || p == "name") {
69 continue;
70 }
71 settings->setValue(p, property(p));
72 }
73 for (int i = metaObject()->propertyOffset(); i < metaObject()->propertyCount(); i++) {
74 const auto p = metaObject()->property(i).name();
75 if (p == QByteArray("identifier") || p == QByteArray("name")) {
76 continue;
77 }
78 qWarning() << "setting " << p << property(p);
79 settings->setValue(p, property(p));
80 }
81 settings->sync();
82}
83
84void Settings::remove()
85{
86 QFile::remove(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QString("/kube/%1.ini").arg(QString::fromLatin1(mIdentifier)));
87}
88
89void Settings::load()
90{
91 if (mLoaded || mIdentifier.isEmpty() || mIdentifier.endsWith(".")) {
92 return;
93 }
94 mLoaded = true;
95 for (int i = metaObject()->propertyOffset(); i < metaObject()->propertyCount(); i++) {
96 auto p = metaObject()->property(i).name();
97 setProperty(p, QVariant());
98 }
99 auto settings = getSettings();
100 for (const auto &p : settings->allKeys()) {
101 qWarning() << "loading " << p << settings->value(p);
102 setProperty(p.toLatin1(), settings->value(p));
103 }
104}
105
106void Settings::setIdentifier(const QByteArray &id)
107{
108 mIdentifier = id;
109 load();
110}
111
112QByteArray Settings::identifier() const
113{
114 return mIdentifier;
115}
116
117ApplicationContext::ApplicationContext()
118 : Settings("applicationcontext")
119{
120
121}
122
123Account ApplicationContext::currentAccount() const
124{
125 return Account(property("currentAccountId").toByteArray());
126}
127
128Account::Account(const QByteArray &identifier)
129 : Settings("account." + identifier)
130{
131
132}
133
134Identity Account::primaryIdentity() const
135{
136 return Identity(property("primaryIdentityId").toByteArray());
137}
138
139QByteArray Account::type() const
140{
141 return property("type").toByteArray();
142}
143
144Identity::Identity(const QByteArray &identifier)
145 : Settings("identity." + identifier)
146{
147
148}
149
150Transport Identity::transport() const
151{
152 return Transport(property("transportId").toByteArray());
153}
154
155Transport::Transport(const QByteArray &identifier)
156 : Settings("transport." + identifier)
157{
158
159}
160
161QByteArray Transport::username() const
162{
163 return property("username").toByteArray();
164}
165
166QByteArray Transport::password() const
167{
168 return property("password").toByteArray();
169}
170
171QByteArray Transport::server() const
172{
173 return property("server").toByteArray();
174}
diff --git a/framework/src/settings/settings.h b/framework/src/settings/settings.h
new file mode 100644
index 00000000..c5cff76b
--- /dev/null
+++ b/framework/src/settings/settings.h
@@ -0,0 +1,92 @@
1/*
2 Copyright (c) 2016 Christian Mollekopf <mollekopf@kolabsys.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19#pragma once
20
21#include <QObject>
22#include <QByteArray>
23#include <QSettings>
24#include <QSharedPointer>
25
26namespace Kube {
27
28class Settings : public QObject {
29 Q_OBJECT
30 Q_PROPERTY(QByteArray identifier READ identifier WRITE setIdentifier)
31public:
32 Settings(QObject *parent = 0);
33 Settings(const QByteArray &id, QObject *parent = 0);
34 virtual ~Settings();
35 Settings(const Settings&);
36
37 void setIdentifier(const QByteArray &id);
38 QByteArray identifier() const;
39
40 Q_INVOKABLE void save();
41 Q_INVOKABLE void remove();
42private:
43 void load();
44 QSharedPointer<QSettings> getSettings();
45 QByteArray mIdentifier;
46 bool mLoaded;
47};
48
49class Account;
50class Identity;
51class Transport;
52
53class ApplicationContext : public Settings
54{
55 Q_OBJECT
56public:
57 ApplicationContext();
58 Account currentAccount() const;
59
60};
61
62class Account : public Settings
63{
64 Q_OBJECT
65public:
66 Account(const QByteArray &identifier);
67 Identity primaryIdentity() const;
68 QByteArray type() const;
69};
70
71class Identity : public Settings
72{
73 Q_OBJECT
74public:
75 Identity(const QByteArray &identifier);
76 Transport transport() const;
77};
78
79class Transport : public Settings
80{
81 Q_OBJECT
82public:
83 Transport(const QByteArray &identifier);
84 QByteArray username() const;
85 QByteArray password() const;
86 QByteArray server() const;
87};
88
89}
90
91Q_DECLARE_METATYPE(Kube::Settings*);
92