Branch 'kolab/integration/4.13.0' - 17 commits - akonadi/CMakeLists.txt akonadi/collectionfetchscope.cpp akonadi/collectionsync.cpp akonadi/entitycache_p.h akonadi/item.cpp akonadi/itemfetchscope.cpp akonadi/itemfetchscope.h akonadi/itemfetchscope_p.h akonadi/item.h akonadi/item_p.h akonadi/itemsearchjob.cpp akonadi/monitor.h akonadi/monitor_p.cpp akonadi/monitor_p.h akonadi/protocolhelper.cpp akonadi/protocolhelper_p.h akonadi/qtest_akonadi.h akonadi/relation.cpp akonadi/relationcreatejob.cpp akonadi/relationcreatejob.h akonadi/relationdeletejob.cpp akonadi/relationdeletejob.h akonadi/relationfetchjob.cpp akonadi/relationfetchjob.h akonadi/relation.h akonadi/resourcebase.cpp akonadi/resourcebase.h akonadi/resourcescheduler.cpp akonadi/resourcescheduler_p.h akonadi/searchcreatejob.cpp akonadi/session.cpp akonadi/tagmodifyjob.cpp akonadi/tagsync.cpp akonadi/tagsync.h akonadi/tests kimap/fetchjob.cpp kimap/tests

Christian Mollekopf mollekopf at kolabsys.com
Fri Aug 29 13:14:52 CEST 2014


 akonadi/CMakeLists.txt                   |    9 +
 akonadi/collectionfetchscope.cpp         |    1 
 akonadi/collectionsync.cpp               |    2 
 akonadi/entitycache_p.h                  |    5 
 akonadi/item.cpp                         |    6 
 akonadi/item.h                           |    8 
 akonadi/item_p.h                         |    1 
 akonadi/itemfetchscope.cpp               |   10 +
 akonadi/itemfetchscope.h                 |   18 ++
 akonadi/itemfetchscope_p.h               |    3 
 akonadi/itemsearchjob.cpp                |    2 
 akonadi/monitor.h                        |   37 ++++
 akonadi/monitor_p.cpp                    |   93 ++++++++++
 akonadi/monitor_p.h                      |    2 
 akonadi/protocolhelper.cpp               |   38 ++++
 akonadi/protocolhelper_p.h               |    1 
 akonadi/qtest_akonadi.h                  |    3 
 akonadi/relation.cpp                     |  136 +++++++++++++++
 akonadi/relation.h                       |  133 +++++++++++++++
 akonadi/relationcreatejob.cpp            |   86 +++++++++
 akonadi/relationcreatejob.h              |   62 +++++++
 akonadi/relationdeletejob.cpp            |   84 +++++++++
 akonadi/relationdeletejob.h              |   62 +++++++
 akonadi/relationfetchjob.cpp             |  164 ++++++++++++++++++
 akonadi/relationfetchjob.h               |   79 ++++++++
 akonadi/resourcebase.cpp                 |   55 ++++++
 akonadi/resourcebase.h                   |   11 +
 akonadi/resourcescheduler.cpp            |   16 +
 akonadi/resourcescheduler_p.h            |    4 
 akonadi/searchcreatejob.cpp              |    2 
 akonadi/session.cpp                      |    2 
 akonadi/tagmodifyjob.cpp                 |    2 
 akonadi/tagsync.cpp                      |  243 +++++++++++++++++++++++++++
 akonadi/tagsync.h                        |   61 ++++++
 akonadi/tests/CMakeLists.txt             |   13 +
 akonadi/tests/actionstatemanagertest.cpp |   13 +
 akonadi/tests/protocolhelpertest.cpp     |    6 
 akonadi/tests/relationtest.cpp           |  165 ++++++++++++++++++
 akonadi/tests/tagsynctest.cpp            |  273 +++++++++++++++++++++++++++++++
 akonadi/tests/tagtest.cpp                |  114 ++++++++++++
 akonadi/tests/virtualresource.cpp        |  111 ++++++++++++
 akonadi/tests/virtualresource.h          |   53 ++++++
 kimap/fetchjob.cpp                       |    2 
 kimap/tests/kimaptest/fakeserver.cpp     |    3 
 44 files changed, 2173 insertions(+), 21 deletions(-)

New commits:
commit 0211264bb762cc765d853c93fd728926ccccff38
Author: Christian Mollekopf <chrigi_1 at fastmail.fm>
Date:   Wed Aug 27 10:39:38 2014 +0200

    KIMAP-FakeServer: Allow skipping client parts.

diff --git a/kimap/tests/kimaptest/fakeserver.cpp b/kimap/tests/kimaptest/fakeserver.cpp
index 5b374ed..2619b7d 100644
--- a/kimap/tests/kimaptest/fakeserver.cpp
+++ b/kimap/tests/kimaptest/fakeserver.cpp
@@ -231,6 +231,9 @@ void FakeServer::readClientPart( int scenarioNumber )
             scenario.first().startsWith( "C: " ) ) {
       QByteArray received = "C: "+clientParser->readUntilCommandEnd().trimmed();
       QByteArray expected = scenario.takeFirst();
+      if (expected.contains("C: SKIP")) {
+        continue;
+      }
       compareReceived(received, expected);
       if (received.contains("STARTTLS")) {
         m_starttls = true;


commit 32f261e71f80629d8cd404334985fec9f9694aee
Author: Christian Mollekopf <chrigi_1 at fastmail.fm>
Date:   Tue Aug 26 19:30:08 2014 +0200

    tagsync for resourcescheduler

diff --git a/akonadi/resourcebase.cpp b/akonadi/resourcebase.cpp
index d11f216..5568560 100644
--- a/akonadi/resourcebase.cpp
+++ b/akonadi/resourcebase.cpp
@@ -26,6 +26,7 @@
 #include "collectionsync_p.h"
 #include "dbusconnectionpool.h"
 #include "itemsync.h"
+#include "tagsync.h"
 #include "kdepimlibs-version.h"
 #include "resourcescheduler_p.h"
 #include "tracerinterface.h"
@@ -76,6 +77,7 @@ public:
         , mItemSyncFetchScope(0)
         , mItemTransactionMode(ItemSync::SingleTransaction)
         , mCollectionSyncer(0)
+        , mTagSyncer(0)
         , mHierarchicalRid(false)
         , mUnemittedProgress(0)
         , mAutomaticProgressReporting(true)
@@ -141,6 +143,7 @@ public:
     void slotSynchronizeCollectionAttributes(const Collection &col);
     void slotCollectionListForAttributesDone(KJob *job);
     void slotCollectionAttributesSyncDone(KJob *job);
+    void slotSynchronizeTags();
 
     void slotItemSyncDone(KJob *job);
 
@@ -160,6 +163,7 @@ public:
     void slotRecursiveMoveReplay(RecursiveMover *mover);
     void slotRecursiveMoveReplayResult(KJob *job);
 
+    void slotTagSyncDone(KJob *job);
     void slotSessionReconnected()
     {
         Q_Q(ResourceBase);
@@ -437,6 +441,7 @@ public:
     ItemFetchScope *mItemSyncFetchScope;
     ItemSync::TransactionMode mItemTransactionMode;
     CollectionSync *mCollectionSyncer;
+    TagSync *mTagSyncer;
     bool mHierarchicalRid;
     QTimer mProgressEmissionCompressor;
     int mUnemittedProgress;
@@ -473,6 +478,8 @@ ResourceBase::ResourceBase(const QString &id)
             SLOT(slotSynchronizeCollection(Akonadi::Collection)));
     connect(d->scheduler, SIGNAL(executeCollectionAttributesSync(Akonadi::Collection)),
             SLOT(slotSynchronizeCollectionAttributes(Akonadi::Collection)));
+    connect(d->scheduler, SIGNAL(executeTagSync()),
+            SLOT(slotSynchronizeTags()));
     connect(d->scheduler, SIGNAL(executeItemFetch(Akonadi::Item,QSet<QByteArray>)),
             SLOT(slotPrepareItemRetrieval(Akonadi::Item)));
     connect(d->scheduler, SIGNAL(executeResourceCollectionDeletion()),
@@ -924,6 +931,12 @@ void ResourceBasePrivate::slotSynchronizeCollectionAttributes(const Collection &
     QMetaObject::invokeMethod(q, "retrieveCollectionAttributes", Q_ARG(Akonadi::Collection, col));
 }
 
+void ResourceBasePrivate::slotSynchronizeTags()
+{
+    Q_Q(ResourceBase);
+    QMetaObject::invokeMethod(q, "retrieveTags");
+}
+
 void ResourceBasePrivate::slotPrepareItemRetrieval(const Akonadi::Item &item)
 {
     Q_Q(ResourceBase);
@@ -1032,6 +1045,11 @@ void ResourceBase::synchronizeCollectionTree()
     d_func()->scheduler->scheduleCollectionTreeSync();
 }
 
+void ResourceBase::synchronizeTags()
+{
+    d_func()->scheduler->scheduleTagSync();
+}
+
 void ResourceBase::cancelTask()
 {
     Q_D(ResourceBase);
@@ -1242,6 +1260,12 @@ void ResourceBase::retrieveCollectionAttributes(const Collection &collection)
     collectionAttributesRetrieved(collection);
 }
 
+void ResourceBase::retrieveTags()
+{
+    Q_D(ResourceBase);
+    d->scheduler->taskDone();
+}
+
 void Akonadi::ResourceBase::abortActivity()
 {
 }
@@ -1291,5 +1315,36 @@ QString ResourceBase::dumpMemoryInfoToString() const
     return d->dumpMemoryInfoToString();
 }
 
+void ResourceBase::tagsRetrieved(const Tag::List &tags, const QHash<QString, Item::List> &tagMembers)
+{
+    Q_D(ResourceBase);
+    Q_ASSERT_X(d->scheduler->currentTask().type == ResourceScheduler::SyncTags ||
+               d->scheduler->currentTask().type == ResourceScheduler::SyncAll ||
+               d->scheduler->currentTask().type == ResourceScheduler::Custom,
+               "ResourceBase::tagsRetrieved()",
+               "Calling tagsRetrieved() although no tag retrieval is in progress");
+    if (!d->mTagSyncer) {
+        d->mTagSyncer = new TagSync(this);
+        connect(d->mTagSyncer, SIGNAL(percent(KJob*,ulong)), SLOT(slotPercent(KJob*,ulong)));
+        connect(d->mTagSyncer, SIGNAL(result(KJob*)), SLOT(slotTagSyncDone(KJob*)));
+    }
+    d->mTagSyncer->setFullTagList(tags);
+    d->mTagSyncer->setTagMembers(tagMembers);
+}
+
+void ResourceBasePrivate::slotTagSyncDone(KJob *job)
+{
+    Q_Q(ResourceBase);
+    mTagSyncer = 0;
+    if (job->error()) {
+        if (job->error() != Job::UserCanceled) {
+            kWarning() << "TagSync failed: " << job->errorString();
+            emit q->error(job->errorString());
+        }
+    }
+    scheduler->taskDone();
+}
+
+
 #include "resourcebase.moc"
 #include "moc_resourcebase.cpp"
diff --git a/akonadi/resourcebase.h b/akonadi/resourcebase.h
index 19c3186..a66e85a 100644
--- a/akonadi/resourcebase.h
+++ b/akonadi/resourcebase.h
@@ -270,6 +270,8 @@ protected Q_SLOTS:
      */
     virtual void retrieveCollections() = 0;
 
+    virtual void retrieveTags();
+
     /**
      * Retrieve the attributes of a single collection from the backend. The
      * collection to retrieve attributes for is provided as @p collection.
@@ -427,6 +429,8 @@ protected:
     */
     void collectionsRetrieved(const Collection::List &collections);
 
+    void tagsRetrieved(const Tag::List &tags, const QHash<QString, Item::List> &tagMembers);
+
     /**
      * Call this to supply incrementally retrieved collections from the remote server.
      *
@@ -631,6 +635,11 @@ protected:
     void synchronizeCollectionTree();
 
     /**
+     * Refetches Tags.
+     */
+    void synchronizeTags();
+
+    /**
      * Stops the execution of the current task and continues with the next one.
      */
     void cancelTask();
@@ -781,6 +790,8 @@ private:
     Q_PRIVATE_SLOT(d_func(), void slotSessionReconnected())
     Q_PRIVATE_SLOT(d_func(), void slotRecursiveMoveReplay(RecursiveMover *))
     Q_PRIVATE_SLOT(d_func(), void slotRecursiveMoveReplayResult(KJob *))
+    Q_PRIVATE_SLOT(d_func(), void slotTagSyncDone(KJob *))
+    Q_PRIVATE_SLOT(d_func(), void slotSynchronizeTags())
 };
 
 }
diff --git a/akonadi/resourcescheduler.cpp b/akonadi/resourcescheduler.cpp
index dad51a6..52e43a8 100644
--- a/akonadi/resourcescheduler.cpp
+++ b/akonadi/resourcescheduler.cpp
@@ -68,6 +68,18 @@ void ResourceScheduler::scheduleCollectionTreeSync()
   scheduleNext();
 }
 
+void ResourceScheduler::scheduleTagSync()
+{
+  Task t;
+  t.type = SyncTags;
+  TaskList& queue = queueForTaskType( t.type );
+  if ( queue.contains( t ) || mCurrentTask == t )
+    return;
+  queue << t;
+  signalTaskToTracker( t, "SyncTags" );
+  scheduleNext();
+}
+
 void ResourceScheduler::scheduleSync(const Collection & col)
 {
   Task t;
@@ -318,6 +330,9 @@ void ResourceScheduler::executeNext()
     case SyncCollectionAttributes:
       emit executeCollectionAttributesSync( mCurrentTask.collection );
       break;
+    case SyncTags:
+      emit executeTagSync();
+      break;
     case FetchItem:
       emit executeItemFetch( mCurrentTask.item, mCurrentTask.itemParts );
       break;
@@ -532,6 +547,7 @@ static const char s_taskTypes[][27] = {
       "SyncCollectionTree",
       "SyncCollection",
       "SyncCollectionAttributes",
+      "SyncTags",
       "FetchItem",
       "ChangeReplay",
       "RecursiveMoveReplay",
diff --git a/akonadi/resourcescheduler_p.h b/akonadi/resourcescheduler_p.h
index 3dfaa5f..676a7f4 100644
--- a/akonadi/resourcescheduler_p.h
+++ b/akonadi/resourcescheduler_p.h
@@ -54,6 +54,7 @@ public:
         SyncCollectionTree,
         SyncCollection,
         SyncCollectionAttributes,
+        SyncTags,
         FetchItem,
         ChangeReplay,
         RecursiveMoveReplay,
@@ -122,6 +123,8 @@ public:
     */
     void scheduleAttributesSync(const Collection &collection);
 
+    void scheduleTagSync();
+
     /**
       Schedules fetching of a single PIM item.
       @param item The item to fetch.
@@ -227,6 +230,7 @@ Q_SIGNALS:
     void executeCollectionAttributesSync(const Akonadi::Collection &col);
     void executeCollectionSync(const Akonadi::Collection &col);
     void executeCollectionTreeSync();
+    void executeTagSync();
     void executeItemFetch(const Akonadi::Item &item, const QSet<QByteArray> &parts);
     void executeResourceCollectionDeletion();
     void executeCacheInvalidation(const Akonadi::Collection &collection);


commit ad79a93155045eb201a3604c8d25160b7f252eb4
Author: Christian Mollekopf <chrigi_1 at fastmail.fm>
Date:   Tue Aug 26 03:02:03 2014 +0200

    VirtualResource: An interface for unittests to create resource collections

diff --git a/akonadi/tests/CMakeLists.txt b/akonadi/tests/CMakeLists.txt
index 61133bf..d0a4ae3 100644
--- a/akonadi/tests/CMakeLists.txt
+++ b/akonadi/tests/CMakeLists.txt
@@ -74,6 +74,17 @@ add_library(akonaditestfake STATIC
   inspectablechangerecorder.cpp
 )
 
+add_library(akonaditest ${LIBRARY_TYPE} 
+    virtualresource.cpp
+    ../resourcescheduler.cpp
+)
+target_link_libraries(akonaditest akonadi-kde ${KDE4_KDEUI_LIBS})
+install(TARGETS akonaditest ${INSTALL_TARGETS_DEFAULT_ARGS})
+install(FILES
+    virtualresource.h
+    DESTINATION ${INCLUDE_INSTALL_DIR}/akonadi COMPONENT Devel
+)
+
 # demo applications
 add_akonadi_demo(itemdumper.cpp)
 add_akonadi_demo(subscriber.cpp)
diff --git a/akonadi/tests/virtualresource.cpp b/akonadi/tests/virtualresource.cpp
new file mode 100644
index 0000000..a675d81
--- /dev/null
+++ b/akonadi/tests/virtualresource.cpp
@@ -0,0 +1,111 @@
+/*
+    Copyright (c) 2014 Christian Mollekopf <mollekopf at kolabsys.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+#include "virtualresource.h"
+
+#include <akonadi/collectioncreatejob.h>
+#include <akonadi/collectiondeletejob.h>
+#include <akonadi/itemcreatejob.h>
+#include <akonadi/resourceselectjob_p.h>
+#include <akonadi/servermanager.h>
+#include <akonadi/session_p.h>
+#include <qdbusinterface.h>
+#include <dbusconnectionpool.h>
+#include <QStringList>
+
+#define EXEC(job) \
+do { \
+  if (!job->exec()) { \
+      kFatal() << "Job failed: " << job->errorString(); \
+  } \
+} while ( 0 )
+
+using namespace Akonadi;
+
+VirtualResource::VirtualResource(const QString &name, QObject *parent)
+    : QObject(parent),
+    mResourceName(name)
+{
+    // QDBusInterface *interface = new QDBusInterface(ServerManager::serviceName(ServerManager::Control),
+    //                                QString::fromLatin1("/"),
+    //                                QString::fromLatin1("org.freedesktop.Akonadi.AgentManager"),
+    //                                DBusConnectionPool::threadConnection(), this);
+    // if (interface->isValid()) {
+    //     const QDBusMessage reply = interface->call(QString::fromUtf8("createAgentInstance"), name, QStringList());
+    //     if (reply.type() == QDBusMessage::ErrorMessage) {
+    //         // This means that the resource doesn't provide a synchronizeCollectionAttributes method, so we just finish the job
+    //         return;
+    //     }
+    // } else {
+    //     Q_ASSERT(false);
+    // }
+    // mSession = new Akonadi::Session(name.toLatin1(), this);
+
+    // Since this is in the same process as the test, all jobs in the test get executed in the resource session by default
+    SessionPrivate::createDefaultSession(name.toLatin1());
+    mSession = Session::defaultSession();
+    ResourceSelectJob *select = new ResourceSelectJob(name, mSession);
+    EXEC(select);
+}
+
+VirtualResource::~VirtualResource()
+{
+    if (mRootCollection.isValid()) {
+        CollectionDeleteJob *d = new CollectionDeleteJob(mRootCollection, mSession);
+        EXEC(d);
+    }
+}
+
+Akonadi::Collection VirtualResource::createCollection(const Akonadi::Collection &collection)
+{
+    // kDebug() << collection.name() << collection.parentCollection().remoteId();
+    // kDebug() << "contentMimeTypes: " << collection.contentMimeTypes();
+    
+    Q_ASSERT(!collection.name().isEmpty());
+    Collection col = collection;
+    if (!col.parentCollection().isValid()) {
+        col.setParentCollection(mRootCollection);
+    }
+    CollectionCreateJob *create = new CollectionCreateJob(col, mSession);
+    EXEC(create);
+    return create->collection();
+}
+Akonadi::Collection VirtualResource::createRootCollection(const Akonadi::Collection &collection)
+{
+    kDebug() << collection.name();
+    mRootCollection = createCollection(collection);
+    return mRootCollection;
+}
+
+Akonadi::Item VirtualResource::createItem(const Akonadi::Item &item, const Collection &parent)
+{
+    ItemCreateJob *create = new ItemCreateJob(item, parent, mSession);
+    EXEC(create);
+    return create->item();
+}
+
+void VirtualResource::reset()
+{
+    Q_ASSERT(mRootCollection.isValid());
+    Akonadi::Collection col = mRootCollection;
+    CollectionDeleteJob *d = new CollectionDeleteJob(mRootCollection, mSession);
+    EXEC(d);
+    col.setId(-1);
+    createRootCollection(col);
+}
+
diff --git a/akonadi/tests/virtualresource.h b/akonadi/tests/virtualresource.h
new file mode 100644
index 0000000..6b03df2
--- /dev/null
+++ b/akonadi/tests/virtualresource.h
@@ -0,0 +1,53 @@
+/*
+    Copyright (c) 2014 Christian Mollekopf <mollekopf at kolabsys.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef VIRTUALRESOURCE_H
+#define VIRTUALRESOURCE_H
+
+#include <akonadi/collection.h>
+#include <akonadi/item.h>
+#include <akonadi/session.h>
+
+namespace Akonadi {
+
+/**
+ * For testing only.
+ *
+ */
+class AKONADI_EXPORT VirtualResource : public QObject
+{
+    Q_OBJECT
+public:
+    VirtualResource(const QString &name, QObject *parent = 0);
+    ~VirtualResource();
+
+    Akonadi::Collection createCollection(const Akonadi::Collection &collection);
+    Akonadi::Collection createRootCollection(const Akonadi::Collection &collection);
+    Akonadi::Item createItem(const Akonadi::Item &item, const Akonadi::Collection &parent);
+
+    void reset();
+private:
+    Akonadi::Collection mRootCollection;
+    QString mResourceName;
+    Akonadi::Session *mSession;
+};
+
+}
+
+#endif


commit cd27dcb68d86dfbed0e575dff56f969f92e1b9ae
Author: Christian Mollekopf <chrigi_1 at fastmail.fm>
Date:   Tue Aug 26 03:00:51 2014 +0200

    KIMAP-FetchJob: Emit headersReceived on full message retrieval as advertised in the docs.

diff --git a/kimap/fetchjob.cpp b/kimap/fetchjob.cpp
index f3a6125..bf2af67 100644
--- a/kimap/fetchjob.cpp
+++ b/kimap/fetchjob.cpp
@@ -61,7 +61,7 @@ namespace KIMAP
                                  pendingUids, pendingAttributes,
                                  pendingParts );
         }
-        if ( !pendingSizes.isEmpty() || !pendingFlags.isEmpty() ) {
+        if ( !pendingSizes.isEmpty() || !pendingFlags.isEmpty() || !pendingMessages.isEmpty() ) {
           emit q->headersReceived( selectedMailBox,
                                    pendingUids, pendingSizes,
                                    pendingFlags, pendingMessages );


commit 4ddf61c67641f725be942be06ff2b13da5d2a992
Author: Christian Mollekopf <chrigi_1 at fastmail.fm>
Date:   Mon Aug 25 16:15:22 2014 +0200

    Detect if we failed to start with QTEST_AKONADIMAIN.
    
    This used to silently block because it couldn't connect to the server
    because the environment variables weren't set-up correctly.

diff --git a/akonadi/qtest_akonadi.h b/akonadi/qtest_akonadi.h
index 4ddd1a5..8cb12ea 100644
--- a/akonadi/qtest_akonadi.h
+++ b/akonadi/qtest_akonadi.h
@@ -65,6 +65,9 @@ void checkTestIsIsolated() {
     Q_ASSERT_X(!qgetenv("TESTRUNNER_DB_ENVIRONMENT").isEmpty(),
                "AkonadiTest::checkTestIsIsolated",
                "This test must be run using ctest, in order to use the testrunner environment. Aborting, to avoid messing up your real akonadi");
+    Q_ASSERT_X(qgetenv("XDG_DATA_HOME").contains("testrunner"),
+               "AkonadiTest::checkTestIsIsolated",
+               "Did you forget to run the test using QTEST_AKONADIMAIN?");
 }
 
 /**


commit 9028b5ef9395f1fb3d83e0ee4eae996c8f96241d
Author: Christian Mollekopf <chrigi_1 at fastmail.fm>
Date:   Mon Aug 25 15:19:40 2014 +0200

    Print warnings for problems.

diff --git a/akonadi/session.cpp b/akonadi/session.cpp
index ec6bc28..313c6b5 100644
--- a/akonadi/session.cpp
+++ b/akonadi/session.cpp
@@ -118,7 +118,7 @@ void SessionPrivate::reconnect()
         const QString connectionConfigFile = connectionFile();
         const QFileInfo fileInfo(connectionConfigFile);
         if (!fileInfo.exists()) {
-            kDebug() << "Akonadi Client Session: connection config file '"
+            kWarning() << "Akonadi Client Session: connection config file '"
                      "akonadi/akonadiconnectionrc' can not be found in"
                      << XdgBaseDirs::homePath("config") << "nor in any of"
                      << XdgBaseDirs::systemPathList("config");


commit 86c5a9eca962b9c708010852d40835c76399e95d
Author: Christian Mollekopf <chrigi_1 at fastmail.fm>
Date:   Mon Aug 25 01:33:15 2014 +0200

    TagModifyJob: Allow unsetting the remoteId

diff --git a/akonadi/tagmodifyjob.cpp b/akonadi/tagmodifyjob.cpp
index c4f8432..ddd3cbc 100644
--- a/akonadi/tagmodifyjob.cpp
+++ b/akonadi/tagmodifyjob.cpp
@@ -47,7 +47,7 @@ void TagModifyJob::doStart()
     Q_D(TagModifyJob);
 
     QList<QByteArray> list;
-    if (!d->mTag.remoteId().isEmpty()) {
+    if (!d->mTag.remoteId().isNull()) {
         list << "REMOTEID";
         list << ImapParser::quote(d->mTag.remoteId());
     }
diff --git a/akonadi/tests/tagtest.cpp b/akonadi/tests/tagtest.cpp
index 7570b8c..1ddf340 100644
--- a/akonadi/tests/tagtest.cpp
+++ b/akonadi/tests/tagtest.cpp
@@ -51,9 +51,11 @@ private Q_SLOTS:
     void testRID();
     void testDelete();
     void testModify();
+    void testModifyFromResource();
     void testCreateMerge();
     void testAttributes();
     void testTagItem();
+    void testCreateItem();
     void testRIDIsolation();
     void testFetchTagIdWithItem();
     void testFetchFullTagWithItem();
@@ -291,6 +293,36 @@ void TagTest::testModify()
     AKVERIFYEXEC(deleteJob);
 }
 
+void TagTest::testModifyFromResource()
+{
+    ResourceSelectJob *select = new ResourceSelectJob(QLatin1String("akonadi_knut_resource_0"));
+    AKVERIFYEXEC(select);
+
+    Tag tag;
+    {
+        tag.setGid("gid");
+        tag.setRemoteId("rid");
+        TagCreateJob *createjob = new TagCreateJob(tag, this);
+        AKVERIFYEXEC(createjob);
+        QVERIFY(createjob->tag().isValid());
+        tag = createjob->tag();
+    }
+
+    {
+        tag.setRemoteId(QByteArray(""));
+        TagModifyJob *modJob = new TagModifyJob(tag, this);
+        AKVERIFYEXEC(modJob);
+
+        TagFetchJob *fetchJob = new TagFetchJob(this);
+        AKVERIFYEXEC(fetchJob);
+        QCOMPARE(fetchJob->tags().size(), 1);
+        QVERIFY(fetchJob->tags().first().remoteId().isEmpty());
+    }
+
+    TagDeleteJob *deleteJob = new TagDeleteJob(tag, this);
+    AKVERIFYEXEC(deleteJob);
+}
+
 void TagTest::testCreateMerge()
 {
     Tag tag;
@@ -412,8 +444,11 @@ void TagTest::testTagItem()
     AKVERIFYEXEC(deleteJob);
 }
 
-void TagTest::testFetchTagIdWithItem()
+void TagTest::testCreateItem()
 {
+    // Akonadi::Monitor monitor;
+    // monitor.itemFetchScope().setFetchTags(true);
+    // monitor.setAllMonitored(true);
     const Collection res3 = Collection( collectionIdFromPath( "res3" ) );
     Tag tag;
     {
@@ -422,18 +457,51 @@ void TagTest::testFetchTagIdWithItem()
         tag = createjob->tag();
     }
 
+    // QSignalSpy tagsSpy(&monitor, SIGNAL(itemsTagsChanged(Akonadi::Item::List,QSet<Akonadi::Tag>,QSet<Akonadi::Tag>)));
+    // QVERIFY(tagsSpy.isValid());
+
     Item item1;
     {
-        item1.setMimeType( "application/octet-stream" );
+        item1.setMimeType("application/octet-stream");
+        item1.setTag(tag);
         ItemCreateJob *append = new ItemCreateJob(item1, res3, this);
         AKVERIFYEXEC(append);
         item1 = append->item();
+    }
 
-        // FIXME This should also be possible with create, but isn't
-        item1.setTag(tag);
 
-        ItemModifyJob *modJob = new ItemModifyJob(item1, this);
-        AKVERIFYEXEC(modJob);
+    // QTRY_VERIFY(tagsSpy.count() >= 1);
+    // QTest::qWait(10);
+    // kDebug() << tagsSpy.count();
+    // QTRY_COMPARE(tagsSpy.last().first().value<Akonadi::Item::List>().first().id(), item1.id());
+    // QTRY_COMPARE(tagsSpy.last().at(1).value< QSet<Tag> >().size(), 1); //1 added tag
+
+    ItemFetchJob *fetchJob = new ItemFetchJob(item1, this);
+    fetchJob->fetchScope().setFetchTags(true);
+    AKVERIFYEXEC(fetchJob);
+    QCOMPARE(fetchJob->items().first().tags().size(), 1);
+
+    TagDeleteJob *deleteJob = new TagDeleteJob(tag, this);
+    AKVERIFYEXEC(deleteJob);
+}
+
+void TagTest::testFetchTagIdWithItem()
+{
+    const Collection res3 = Collection( collectionIdFromPath( "res3" ) );
+    Tag tag;
+    {
+        TagCreateJob *createjob = new TagCreateJob(Tag("gid1"), this);
+        AKVERIFYEXEC(createjob);
+        tag = createjob->tag();
+    }
+
+    Item item1;
+    {
+        item1.setMimeType( "application/octet-stream" );
+        item1.setTag(tag);
+        ItemCreateJob *append = new ItemCreateJob(item1, res3, this);
+        AKVERIFYEXEC(append);
+        item1 = append->item();
     }
 
     ItemFetchJob *fetchJob = new ItemFetchJob(item1, this);


commit 20acbaf079f231b9a8a8f249948fefc14a781af2
Author: Christian Mollekopf <chrigi_1 at fastmail.fm>
Date:   Mon Aug 25 01:32:46 2014 +0200

    TagSync

diff --git a/akonadi/CMakeLists.txt b/akonadi/CMakeLists.txt
index 7f2417f..622a41c 100644
--- a/akonadi/CMakeLists.txt
+++ b/akonadi/CMakeLists.txt
@@ -207,6 +207,7 @@ set( akonadikde_LIB_SRC
   tageditwidget.cpp
   tagmanagementdialog.cpp
   tagselectiondialog.cpp
+  tagsync.cpp
   tagwidget.cpp
   unlinkjob.cpp
 # Temporary until ported to Qt-plugin framework
@@ -430,6 +431,7 @@ install( FILES
   tagwidget.h
   tagmanagementdialog.h
   tagselectiondialog.h
+  tagsync.h
   trashjob.h
   trashrestorejob.h
   trashsettings.h
diff --git a/akonadi/tagsync.cpp b/akonadi/tagsync.cpp
new file mode 100644
index 0000000..f534dba
--- /dev/null
+++ b/akonadi/tagsync.cpp
@@ -0,0 +1,243 @@
+/*
+    Copyright (c) 2014 Christian Mollekopf <mollekopf at kolabsys.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+namespace Akonadi {
+    class Item;
+}
+
+unsigned int qHash(const Akonadi::Item &item);
+
+#include "tagsync.h"
+
+#include <akonadi/itemfetchjob.h>
+#include <akonadi/itemfetchscope.h>
+#include <akonadi/itemmodifyjob.h>
+#include <akonadi/tagfetchjob.h>
+#include <akonadi/tagcreatejob.h>
+#include <akonadi/tagmodifyjob.h>
+
+using namespace Akonadi;
+
+//We want to compare items by remoteId and not by id
+uint qHash(const Item &item)
+{
+    if (item.isValid()) {
+        return qHash(item.id());
+    }
+    Q_ASSERT(!item.remoteId().isEmpty());
+    return qHash(item.remoteId());
+}
+
+bool operator==(const Item &left, const Item &right)
+{
+    if (left.isValid() && right.isValid() && (left.id() == right.id())) {
+        return true;
+    }
+    if (!left.remoteId().isEmpty() && !right.remoteId().isEmpty() && (left.remoteId() == right.remoteId())) {
+        return true;
+    }
+    return false;
+}
+
+TagSync::TagSync(QObject *parent)
+    : Job(parent),
+    mDeliveryDone(false),
+    mTagMembersDeliveryDone(false),
+    mLocalTagsFetched(false)
+{
+
+}
+
+TagSync::~TagSync()
+{
+
+}
+
+void TagSync::setFullTagList(const Akonadi::Tag::List &tags)
+{
+    mRemoteTags = tags;
+    mDeliveryDone = true;
+    diffTags();
+}
+
+void TagSync::setTagMembers(const QHash<QString, Akonadi::Item::List> &ridMemberMap)
+{
+    mRidMemberMap = ridMemberMap;
+    mTagMembersDeliveryDone = true;
+    diffTags();
+}
+
+void TagSync::doStart()
+{
+    // kDebug();
+    //This should include all tags, including the ones that don't have a remote id
+    Akonadi::TagFetchJob *fetch = new Akonadi::TagFetchJob(this);
+    connect(fetch, SIGNAL(result(KJob*)), this, SLOT(onLocalTagFetchDone(KJob*)));
+}
+
+void TagSync::onLocalTagFetchDone(KJob *job)
+{
+    // kDebug();
+    TagFetchJob *fetch = static_cast<TagFetchJob*>(job);
+    mLocalTags = fetch->tags();
+    mLocalTagsFetched = true;
+    diffTags();
+}
+
+void TagSync::diffTags()
+{
+    if (!mDeliveryDone || !mTagMembersDeliveryDone || !mLocalTagsFetched) {
+        kDebug() << "waiting for delivery: " << mDeliveryDone << mLocalTagsFetched;
+        return;
+    }
+    // kDebug() << "diffing";
+    QHash<QByteArray, Akonadi::Tag> tagByGid;
+    QHash<QByteArray, Akonadi::Tag> tagByRid;
+    QHash<Akonadi::Tag::Id, Akonadi::Tag> tagById;
+    Q_FOREACH (const Akonadi::Tag &localTag, mLocalTags) {
+        tagByRid.insert(localTag.remoteId(), localTag);
+        tagByGid.insert(localTag.gid(), localTag);
+        if (!localTag.remoteId().isEmpty()) {
+            tagById.insert(localTag.id(), localTag);
+        }
+    }
+    Q_FOREACH (const Akonadi::Tag &remoteTag, mRemoteTags) {
+        if (tagByRid.contains(remoteTag.remoteId())) {
+            //Tag still exists, check members
+            Tag tag = tagByRid.value(remoteTag.remoteId());
+            ItemFetchJob *itemFetch = new ItemFetchJob(tag, this);
+            itemFetch->setProperty("tag", QVariant::fromValue(tag));
+            itemFetch->setProperty("merge", false);
+            connect(itemFetch, SIGNAL(result(KJob*)), this, SLOT(onTagItemsFetchDone(KJob*)));
+            connect(itemFetch, SIGNAL(result(KJob*)), this, SLOT(onJobDone(KJob*)));
+            tagById.remove(tagByRid.value(remoteTag.remoteId()).id());
+        } else if (tagByGid.contains(remoteTag.gid())) {
+            //Tag exists but has no rid
+            //Merge members and set rid
+            Tag tag = tagByGid.value(remoteTag.gid());
+            tag.setRemoteId(remoteTag.remoteId());
+            ItemFetchJob *itemFetch = new ItemFetchJob(tag, this);
+            itemFetch->setProperty("tag", QVariant::fromValue(tag));
+            itemFetch->setProperty("merge", true);
+            connect(itemFetch, SIGNAL(result(KJob*)), this, SLOT(onTagItemsFetchDone(KJob*)));
+            connect(itemFetch, SIGNAL(result(KJob*)), this, SLOT(onJobDone(KJob*)));
+            tagById.remove(tagByGid.value(remoteTag.gid()).id());
+        } else {
+            //New tag, create
+            TagCreateJob *createJob = new TagCreateJob(remoteTag, this);
+            createJob->setMergeIfExisting(true);
+            connect(createJob, SIGNAL(result(KJob*)), this, SLOT(onCreateTagDone(KJob*)));
+            connect(createJob, SIGNAL(result(KJob*)), this, SLOT(onJobDone(KJob*)));
+            //TODO add tags
+        }
+    }
+    Q_FOREACH (const Akonadi::Tag::Id &removedTag, tagById.keys()) {
+        //Removed remotely, unset rid
+        Tag tag = tagById.value(removedTag);
+        tag.setRemoteId(QByteArray(""));
+        TagModifyJob *modJob = new TagModifyJob(tag, this);
+        connect(modJob, SIGNAL(result(KJob*)), this, SLOT(onJobDone(KJob*)));
+    }
+    checkDone();
+}
+
+static QSet<QString> ridSet(const Akonadi::Item::List &list)
+{
+    QSet<QString> set;
+    Q_FOREACH (const Akonadi::Item &item, list) {
+        set << item.remoteId();
+    }
+    return set;
+}
+
+void TagSync::onCreateTagDone(KJob *job)
+{
+    if (job->error()) {
+        kWarning() << "ItemFetch failed: " << job->errorString();
+        // cancelTask(job->errorString());
+        return;
+    }
+
+    Akonadi::Tag tag = static_cast<Akonadi::TagCreateJob*>(job)->tag();
+    const Item::List remoteMembers = mRidMemberMap.value(QString::fromLatin1(tag.remoteId()));
+    Q_FOREACH (Item item, remoteMembers) {
+        item.setTag(tag);
+        ItemModifyJob *modJob = new ItemModifyJob(item, this);
+        connect(modJob, SIGNAL(result(KJob*)), this, SLOT(onJobDone(KJob*)));
+        kDebug() << "setting tag " << item.remoteId();
+    }
+}
+
+void TagSync::onTagItemsFetchDone(KJob *job)
+{
+    if (job->error()) {
+        kWarning() << "ItemFetch failed: " << job->errorString();
+        // cancelTask(job->errorString());
+        return;
+    }
+
+    const Akonadi::Item::List items = static_cast<Akonadi::ItemFetchJob*>(job)->items();
+    const Akonadi::Tag tag = job->property("tag").value<Akonadi::Tag>();
+    const bool merge = job->property("merge").toBool();
+    const QSet<Item> localMembers = items.toSet();
+    const QSet<Item> remoteMembers = mRidMemberMap.value(QString::fromLatin1(tag.remoteId())).toSet();
+    const QSet<Item> toAdd = remoteMembers - localMembers;
+    const QSet<Item> toRemove = localMembers - remoteMembers;
+    if (!merge) {
+        Q_FOREACH (Item item, toRemove) {
+            item.clearTag(tag);
+            ItemModifyJob *modJob = new ItemModifyJob(item, this);
+            connect(modJob, SIGNAL(result(KJob*)), this, SLOT(onJobDone(KJob*)));
+            kDebug() << "removing tag " << item.remoteId();
+        }
+    }
+    Q_FOREACH (Item item, toAdd) {
+        item.setTag(tag);
+        ItemModifyJob *modJob = new ItemModifyJob(item, this);
+        connect(modJob, SIGNAL(result(KJob*)), this, SLOT(onJobDone(KJob*)));
+        kDebug() << "setting tag " << item.remoteId();
+    }
+}
+
+void TagSync::onJobDone(KJob *job)
+{
+    checkDone();
+}
+
+void TagSync::slotResult(KJob *job)
+{
+    if (job->error()) {
+        kWarning() << "Error during CollectionSync: " << job->errorString() << job->metaObject()->className();
+        // pretent there were no errors
+        Akonadi::Job::removeSubjob(job);
+    } else {
+        Akonadi::Job::slotResult(job);
+    }
+}
+
+void TagSync::checkDone()
+{
+    if (hasSubjobs()) {
+        kDebug() << "Still going";
+        return;
+    }
+    kDebug() << "done";
+    emitResult();
+}
+
+#include "tagsync.moc"
diff --git a/akonadi/tagsync.h b/akonadi/tagsync.h
new file mode 100644
index 0000000..44146d2
--- /dev/null
+++ b/akonadi/tagsync.h
@@ -0,0 +1,61 @@
+/*
+    Copyright (c) 2014 Christian Mollekopf <mollekopf at kolabsys.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+#ifndef TAGSYNC_H
+#define TAGSYNC_H
+
+#include "akonadi_export.h"
+#include <Akonadi/Job>
+#include <Akonadi/Tag>
+#include <Akonadi/Item>
+
+class AKONADI_EXPORT TagSync : public Akonadi::Job
+{
+    Q_OBJECT
+public:
+    TagSync(QObject *parent = 0);
+    virtual ~TagSync();
+
+    void setFullTagList(const Akonadi::Tag::List &tags);
+    void setTagMembers(const QHash<QString, Akonadi::Item::List> &ridMemberMap);
+
+protected:
+    void doStart();
+
+private Q_SLOTS:
+    void onLocalTagFetchDone(KJob *job);
+    // void onItemsFetchDone(KJob *job);
+    void onCreateTagDone(KJob *job);
+    void onTagItemsFetchDone(KJob *job);
+    void onJobDone(KJob *job);
+    void slotResult(KJob *job);
+
+private:
+    void diffTags();
+    void checkDone();
+
+private:
+    Akonadi::Tag::List mRemoteTags;
+    Akonadi::Tag::List mLocalTags;
+    bool mDeliveryDone;
+    bool mTagMembersDeliveryDone;
+    bool mLocalTagsFetched;
+    QHash<QString, Akonadi::Item::List> mRidMemberMap;
+};
+
+#endif
diff --git a/akonadi/tests/CMakeLists.txt b/akonadi/tests/CMakeLists.txt
index 7d66e4a..61133bf 100644
--- a/akonadi/tests/CMakeLists.txt
+++ b/akonadi/tests/CMakeLists.txt
@@ -152,5 +152,6 @@ add_akonadi_isolated_test(lazypopulationtest.cpp)
 add_akonadi_isolated_test(favoriteproxytest.cpp)
 add_akonadi_isolated_test_advanced(itemsearchjobtest.cpp testsearchplugin/testsearchplugin.cpp "")
 add_akonadi_isolated_test(tagtest.cpp)
+add_akonadi_isolated_test(tagsynctest.cpp)
 add_akonadi_isolated_test(relationtest.cpp)
 add_akonadi_isolated_test(etmpopulationtest.cpp)
diff --git a/akonadi/tests/tagsynctest.cpp b/akonadi/tests/tagsynctest.cpp
new file mode 100644
index 0000000..5f8558c
--- /dev/null
+++ b/akonadi/tests/tagsynctest.cpp
@@ -0,0 +1,273 @@
+/*
+    Copyright (c) 2014 Christian Mollekopf <mollekopf at kolabsys.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "test_utils.h"
+
+#include <akonadi/agentmanager.h>
+#include <akonadi/agentinstance.h>
+#include <akonadi/control.h>
+#include <akonadi/tagfetchjob.h>
+#include <akonadi/tagdeletejob.h>
+#include <akonadi/tagcreatejob.h>
+#include <akonadi/tag.h>
+#include <akonadi/tagsync.h>
+#include <akonadi/resourceselectjob_p.h>
+#include <akonadi/itemcreatejob.h>
+#include <akonadi/itemfetchjob.h>
+#include <akonadi/itemfetchscope.h>
+
+#include <QtCore/QObject>
+#include <QSignalSpy>
+
+#include <qtest_akonadi.h>
+
+using namespace Akonadi;
+
+bool operator==(const Tag &left, const Tag &right)
+{
+    if (left.gid() == right.gid()) {
+        return true;
+    }
+    qDebug() << left.gid();
+    qDebug() << right.gid();
+    return false;
+}
+
+class TagSyncTest : public QObject
+{
+    Q_OBJECT
+private Q_SLOTS:
+    void initTestCase()
+    {
+        AkonadiTest::checkTestIsIsolated();
+        Control::start();
+        AkonadiTest::setAllResourcesOffline();
+        cleanTags();
+    }
+
+    Tag::List getTags()
+    {
+        TagFetchJob *fetchJob = new TagFetchJob();
+        bool ret = fetchJob->exec();
+        Q_ASSERT(ret);
+        return fetchJob->tags();
+    }
+
+    Tag::List getTagsWithRid()
+    {
+        Tag::List tags;
+        Q_FOREACH(const Tag &t, getTags()) {
+            if (!t.remoteId().isEmpty()) {
+                tags << t;
+                kDebug() << t.remoteId();
+            }
+        }
+        return tags;
+    }
+
+    void cleanTags()
+    {
+        Q_FOREACH(const Tag &t, getTags()) {
+            TagDeleteJob *job = new TagDeleteJob(t);
+            bool ret = job->exec();
+            Q_ASSERT(ret);
+        }
+    }
+
+    void newTag()
+    {
+        ResourceSelectJob *select = new ResourceSelectJob(QLatin1String("akonadi_knut_resource_0"));
+        AKVERIFYEXEC(select);
+
+        Tag::List remoteTags;
+
+        Tag tag1("tag1");
+        tag1.setRemoteId("rid1");
+        remoteTags << tag1;
+
+        TagSync* syncer = new TagSync(this);
+        syncer->setFullTagList(remoteTags);
+        syncer->setTagMembers(QHash<QString, Item::List>());
+        AKVERIFYEXEC(syncer);
+
+        Tag::List resultTags = getTags();
+        QCOMPARE(resultTags.count(), remoteTags.count());
+        QCOMPARE(resultTags, remoteTags);
+        cleanTags();
+    }
+
+    void newTagWithItems()
+    {
+        const Collection res3 = Collection( collectionIdFromPath( "res3" ) );
+        ResourceSelectJob *select = new ResourceSelectJob(QLatin1String("akonadi_knut_resource_0"));
+        AKVERIFYEXEC(select);
+
+        Tag::List remoteTags;
+
+        Tag tag1("tag1");
+        tag1.setRemoteId("rid1");
+        remoteTags << tag1;
+
+        Item item1;
+        {
+            item1.setMimeType("application/octet-stream");
+            item1.setRemoteId("item1");
+            ItemCreateJob *append = new ItemCreateJob(item1, res3, this);
+            AKVERIFYEXEC(append);
+            item1 = append->item();
+        }
+
+        QHash<QString, Item::List> tagMembers;
+        tagMembers.insert(tag1.remoteId(), Item::List() << item1);
+
+        TagSync* syncer = new TagSync(this);
+        syncer->setFullTagList(remoteTags);
+        syncer->setTagMembers(tagMembers);
+        AKVERIFYEXEC(syncer);
+
+        Tag::List resultTags = getTags();
+        QCOMPARE(resultTags.count(), remoteTags.count());
+        QCOMPARE(resultTags, remoteTags);
+
+        //We need the id of the fetch
+        tag1 = resultTags.first();
+
+        ItemFetchJob *fetchJob = new ItemFetchJob(tag1);
+        AKVERIFYEXEC(fetchJob);
+        QCOMPARE(tagMembers.value(tag1.remoteId()).count(), fetchJob->items().count());
+        QCOMPARE(tagMembers.value(tag1.remoteId()), fetchJob->items());
+
+        cleanTags();
+    }
+
+    void existingTag()
+    {
+        ResourceSelectJob *select = new ResourceSelectJob(QLatin1String("akonadi_knut_resource_0"));
+        AKVERIFYEXEC(select);
+
+        Tag tag1("tag1");
+        tag1.setRemoteId("rid1");
+
+        TagCreateJob *createJob = new TagCreateJob(tag1, this);
+        AKVERIFYEXEC(createJob);
+
+        Tag::List remoteTags;
+        remoteTags << tag1;
+
+        TagSync* syncer = new TagSync(this);
+        syncer->setFullTagList(remoteTags);
+        syncer->setTagMembers(QHash<QString, Item::List>());
+        AKVERIFYEXEC(syncer);
+
+        Tag::List resultTags = getTags();
+        QCOMPARE(resultTags.count(), remoteTags.count());
+        QCOMPARE(resultTags, remoteTags);
+        cleanTags();
+    }
+
+    void existingTagWithItems()
+    {
+        const Collection res3 = Collection( collectionIdFromPath( "res3" ) );
+
+        ResourceSelectJob *select = new ResourceSelectJob(QLatin1String("akonadi_knut_resource_0"));
+        AKVERIFYEXEC(select);
+
+        Tag tag1("tag1");
+        tag1.setRemoteId("rid1");
+
+        TagCreateJob *createJob = new TagCreateJob(tag1, this);
+        AKVERIFYEXEC(createJob);
+
+        Tag::List remoteTags;
+        remoteTags << tag1;
+
+        Item item1;
+        {
+            item1.setMimeType("application/octet-stream");
+            item1.setRemoteId("item1");
+            ItemCreateJob *append = new ItemCreateJob(item1, res3, this);
+            AKVERIFYEXEC(append);
+            item1 = append->item();
+        }
+
+        Item item2;
+        {
+            item2.setMimeType("application/octet-stream");
+            item2.setRemoteId("item2");
+            item2.setTag(tag1);
+            ItemCreateJob *append = new ItemCreateJob(item2, res3, this);
+            AKVERIFYEXEC(append);
+            item2 = append->item();
+        }
+
+        QHash<QString, Item::List> tagMembers;
+        tagMembers.insert(tag1.remoteId(), Item::List() << item1);
+
+        TagSync* syncer = new TagSync(this);
+        syncer->setFullTagList(remoteTags);
+        syncer->setTagMembers(tagMembers);
+        AKVERIFYEXEC(syncer);
+
+        Tag::List resultTags = getTags();
+        QCOMPARE(resultTags.count(), remoteTags.count());
+        QCOMPARE(resultTags, remoteTags);
+        {
+            ItemFetchJob *fetchJob = new ItemFetchJob(item1, this);
+            fetchJob->fetchScope().setFetchTags(true);
+            AKVERIFYEXEC(fetchJob);
+            QCOMPARE(fetchJob->items().first().tags().count(), 1);
+        }
+        {
+            ItemFetchJob *fetchJob = new ItemFetchJob(item2, this);
+            fetchJob->fetchScope().setFetchTags(true);
+            AKVERIFYEXEC(fetchJob);
+            QCOMPARE(fetchJob->items().first().tags().count(), 0);
+        }
+
+        cleanTags();
+    }
+
+    void removeTag()
+    {
+        ResourceSelectJob *select = new ResourceSelectJob(QLatin1String("akonadi_knut_resource_0"));
+        AKVERIFYEXEC(select);
+
+        Tag tag1("tag1");
+        tag1.setRemoteId("rid1");
+
+        TagCreateJob *createJob = new TagCreateJob(tag1, this);
+        AKVERIFYEXEC(createJob);
+
+        Tag::List remoteTags;
+
+        TagSync* syncer = new TagSync(this);
+        syncer->setFullTagList(remoteTags);
+        syncer->setTagMembers(QHash<QString, Item::List>());
+        AKVERIFYEXEC(syncer);
+
+        Tag::List resultTags = getTagsWithRid();
+        QCOMPARE(resultTags.count(), remoteTags.count());
+        QCOMPARE(resultTags, remoteTags);
+        cleanTags();
+    }
+};
+
+QTEST_AKONADIMAIN(TagSyncTest, NoGUI)
+
+#include "tagsynctest.moc"


commit a36fe428ec49831ede5c19b63bb4a07075a23823
Author: Christian Mollekopf <chrigi_1 at fastmail.fm>
Date:   Fri Aug 22 13:32:52 2014 +0200

    CollectionSync: Only modify collections that have actually changed.
    
    We used to unconditionally modify all if at least one changed.

diff --git a/akonadi/collectionsync.cpp b/akonadi/collectionsync.cpp
index 4185a50..ff16d7f 100644
--- a/akonadi/collectionsync.cpp
+++ b/akonadi/collectionsync.cpp
@@ -454,7 +454,7 @@ public:
             }
         }
 
-        {
+        if (checkLocalCollection(localNode, remoteNode)) {
             // ### HACK to work around the implicit move attempts of CollectionModifyJob
             // which we do explicitly below
             Collection c(upd);


commit 1da801b81c156ff24a9b29e6ebf3992c9997d9ba
Author: Christian Mollekopf <chrigi_1 at fastmail.fm>
Date:   Fri Aug 22 13:31:25 2014 +0200

    TagTest: added test to fetch items by tag.

diff --git a/akonadi/tests/tagtest.cpp b/akonadi/tests/tagtest.cpp
index 915237c..7570b8c 100644
--- a/akonadi/tests/tagtest.cpp
+++ b/akonadi/tests/tagtest.cpp
@@ -60,6 +60,7 @@ private Q_SLOTS:
     void testModifyItemWithTagByGID();
     void testModifyItemWithTagByRID();
     void testMonitor();
+    void testFetchItemsByTag();
 };
 
 void TagTest::initTestCase()
@@ -609,6 +610,39 @@ void TagTest::testMonitor()
   }
 }
 
+void TagTest::testFetchItemsByTag()
+{
+    const Collection res3 = Collection( collectionIdFromPath( "res3" ) );
+    Tag tag;
+    {
+        TagCreateJob *createjob = new TagCreateJob(Tag("gid1"), this);
+        AKVERIFYEXEC(createjob);
+        tag = createjob->tag();
+    }
+
+    Item item1;
+    {
+        item1.setMimeType( "application/octet-stream" );
+        ItemCreateJob *append = new ItemCreateJob(item1, res3, this);
+        AKVERIFYEXEC(append);
+        item1 = append->item();
+        //FIXME This should also be possible with create, but isn't
+        item1.setTag(tag);
+    }
+
+    ItemModifyJob *modJob = new ItemModifyJob(item1, this);
+    AKVERIFYEXEC(modJob);
+
+    ItemFetchJob *fetchJob = new ItemFetchJob(tag, this);
+    AKVERIFYEXEC(fetchJob);
+    QCOMPARE(fetchJob->items().size(), 1);
+    Item i = fetchJob->items().first();
+    QCOMPARE(i, item1);
+
+    TagDeleteJob *deleteJob = new TagDeleteJob(tag, this);
+    AKVERIFYEXEC(deleteJob);
+}
+
 #include "tagtest.moc"
 
 QTEST_AKONADIMAIN(TagTest, NoGUI)


commit 6846f6dcbc5681a91174267ef97029eb5885a2f1
Author: Christian Mollekopf <chrigi_1 at fastmail.fm>
Date:   Wed Aug 20 17:08:15 2014 +0200

    Disable remote search by default.
    
    Looks like a previous patch somehow got reverted...

diff --git a/akonadi/itemsearchjob.cpp b/akonadi/itemsearchjob.cpp
index aef8472..431b609 100644
--- a/akonadi/itemsearchjob.cpp
+++ b/akonadi/itemsearchjob.cpp
@@ -37,7 +37,7 @@ public:
         : JobPrivate(parent)
         , mQuery(query)
         , mRecursive(false)
-        , mRemote(true)
+        , mRemote(false)
         , mEmitTimer(0)
     {
     }
diff --git a/akonadi/searchcreatejob.cpp b/akonadi/searchcreatejob.cpp
index 2bedbd5..4f0b955 100644
--- a/akonadi/searchcreatejob.cpp
+++ b/akonadi/searchcreatejob.cpp
@@ -38,7 +38,7 @@ public:
         , mName(name)
         , mQuery(query)
         , mRecursive(false)
-        , mRemote(true)
+        , mRemote(false)
     {
     }
 


commit 2b20f479e02657f3dffe4d44b3a6ed1fc6ddb5e6
Author: Christian Mollekopf <chrigi_1 at fastmail.fm>
Date:   Tue Aug 19 22:31:12 2014 +0200

    EntityCache: If we don't do error handling, at least print the error.

diff --git a/akonadi/entitycache_p.h b/akonadi/entitycache_p.h
index f296686..48fb5c1 100644
--- a/akonadi/entitycache_p.h
+++ b/akonadi/entitycache_p.h
@@ -184,7 +184,10 @@ class EntityCache : public EntityCacheBase
 
     void processResult( KJob* job )
     {
-      // Error handling?
+      if (job->error()) {
+        //This can happen if we have stale notifications for items that have already been removed
+        kWarning() << "An error occured: " << job->errorString();
+      }
       typename T::Id id = job->property( "EntityCacheNode" ).template value<typename T::Id>();
       EntityCacheNode<T> *node = cacheNodeForId( id );
       if ( !node ) {


commit 2c3d657dce223cc819c3146fc8e8fdaf4919c293
Author: Christian Mollekopf <chrigi_1 at fastmail.fm>
Date:   Tue Aug 19 22:30:19 2014 +0200

    ProtocolHelper: It should be possible to fetch tags even with the root collection set.

diff --git a/akonadi/protocolhelper.cpp b/akonadi/protocolhelper.cpp
index c0027c7..3cd4197 100644
--- a/akonadi/protocolhelper.cpp
+++ b/akonadi/protocolhelper.cpp
@@ -386,7 +386,7 @@ QByteArray ProtocolHelper::commandContextToByteArray(const Akonadi::Collection &
     }
 
     if (collection == Collection::root()) {
-        if (requestedItems.isEmpty()) {   // collection content listing
+        if (requestedItems.isEmpty() && !tag.isValid()) {   // collection content listing
             throw Exception("Cannot perform item operations on root collection.");
         }
     } else {


commit c10aba63f8500b513ad9a16a923f7d133d7257fa
Author: Christian Mollekopf <chrigi_1 at fastmail.fm>
Date:   Tue Aug 19 22:29:44 2014 +0200

    CollectionFetchScope: set fetchid only when we request attributes.

diff --git a/akonadi/collectionfetchscope.cpp b/akonadi/collectionfetchscope.cpp
index 560b31d..8265d3c 100644
--- a/akonadi/collectionfetchscope.cpp
+++ b/akonadi/collectionfetchscope.cpp
@@ -172,6 +172,7 @@ QSet<QByteArray> CollectionFetchScope::attributes() const
 
 void CollectionFetchScope::fetchAttribute(const QByteArray &type, bool fetch)
 {
+    d->fetchIdOnly = false;
     if (fetch) {
         d->attributes.insert(type);
     } else {


commit 666a3389c5e1e4122c7e48417f77267de78fb10c
Author: Christian Mollekopf <chrigi_1 at fastmail.fm>
Date:   Tue Aug 19 00:03:12 2014 +0200

    Fixed protocolhelpertest.

diff --git a/akonadi/tests/protocolhelpertest.cpp b/akonadi/tests/protocolhelpertest.cpp
index f4a4d93..2a2294f 100644
--- a/akonadi/tests/protocolhelpertest.cpp
+++ b/akonadi/tests/protocolhelpertest.cpp
@@ -175,13 +175,15 @@ class ProtocolHelperTest : public QObject
       QTest::newRow( "empty" ) << Collection() << QByteArray();
       QTest::newRow( "root" ) << Collection::root() << QByteArray( "(0 \"\")" );
       Collection c;
+      c.setId(1);
       c.setParentCollection( Collection::root() );
       c.setRemoteId( "r1" );
-      QTest::newRow( "one level" ) << c << QByteArray( "(-23 \"r1\") (0 \"\")" );
+      QTest::newRow( "one level" ) << c << QByteArray( "(1 \"r1\") (0 \"\")" );
       Collection c2;
+      c2.setId(2);
       c2.setParentCollection( c );
       c2.setRemoteId( "r2" );
-      QTest::newRow( "two level ok" ) << c2 << QByteArray( "(-24 \"r2\") (-23 \"r1\") (0 \"\")" );
+      QTest::newRow( "two level ok" ) << c2 << QByteArray( "(2 \"r2\") (1 \"r1\") (0 \"\")" );
     }
 
     void testHRidToByteArray()


commit 3589b73236e7e93b7dbef07fcc2ff60043866e5d
Author: Christian Mollekopf <chrigi_1 at fastmail.fm>
Date:   Tue Aug 19 00:02:55 2014 +0200

    Adapted actionstatemanagertest to new action

diff --git a/akonadi/tests/actionstatemanagertest.cpp b/akonadi/tests/actionstatemanagertest.cpp
index 74acdf3..6d83b73 100644
--- a/akonadi/tests/actionstatemanagertest.cpp
+++ b/akonadi/tests/actionstatemanagertest.cpp
@@ -178,6 +178,7 @@ class ActionStateManagerTest : public QObject
         map.insert( StandardActionManager::RestoreItemsFromTrash, false );
         map.insert( StandardActionManager::MoveToTrashRestoreCollection, false );
         map.insert( StandardActionManager::MoveToTrashRestoreItem, false );
+        map.insert( StandardActionManager::SynchronizeCollectionTree, false );
 
         QTest::newRow( "nothing selected" ) << collectionList << map;
       }
@@ -219,6 +220,7 @@ class ActionStateManagerTest : public QObject
         map.insert( StandardActionManager::RestoreItemsFromTrash, false );
         map.insert( StandardActionManager::MoveToTrashRestoreCollection, false );
         map.insert( StandardActionManager::MoveToTrashRestoreItem, false );
+        map.insert( StandardActionManager::SynchronizeCollectionTree, false );
 
         QTest::newRow( "root collection selected" ) << collectionList << map;
       }
@@ -260,6 +262,7 @@ class ActionStateManagerTest : public QObject
         map.insert( StandardActionManager::RestoreItemsFromTrash, false );
         map.insert( StandardActionManager::MoveToTrashRestoreCollection, false );
         map.insert( StandardActionManager::MoveToTrashRestoreItem, false );
+        map.insert( StandardActionManager::SynchronizeCollectionTree, true );
 
         QTest::newRow( "read-only resource collection selected" ) << collectionList << map;
       }
@@ -301,6 +304,7 @@ class ActionStateManagerTest : public QObject
         map.insert( StandardActionManager::RestoreItemsFromTrash, false );
         map.insert( StandardActionManager::MoveToTrashRestoreCollection, false );
         map.insert( StandardActionManager::MoveToTrashRestoreItem, false );
+        map.insert( StandardActionManager::SynchronizeCollectionTree, true );
 
         QTest::newRow( "writable resource collection selected" ) << collectionList << map;
       }
@@ -342,6 +346,7 @@ class ActionStateManagerTest : public QObject
         map.insert( StandardActionManager::RestoreItemsFromTrash, false );
         map.insert( StandardActionManager::MoveToTrashRestoreCollection, false );
         map.insert( StandardActionManager::MoveToTrashRestoreItem, false );
+        map.insert( StandardActionManager::SynchronizeCollectionTree, true );
 
         QTest::newRow( "non-configurable resource collection selected" ) << collectionList << map;
       }
@@ -383,6 +388,7 @@ class ActionStateManagerTest : public QObject
         map.insert( StandardActionManager::RestoreItemsFromTrash, false );
         map.insert( StandardActionManager::MoveToTrashRestoreCollection, false );
         map.insert( StandardActionManager::MoveToTrashRestoreItem, false );
+        map.insert( StandardActionManager::SynchronizeCollectionTree, false );
 
         QTest::newRow( "read-only folder collection selected" ) << collectionList << map;
       }
@@ -424,6 +430,7 @@ class ActionStateManagerTest : public QObject
         map.insert( StandardActionManager::RestoreItemsFromTrash, false );
         map.insert( StandardActionManager::MoveToTrashRestoreCollection, true );
         map.insert( StandardActionManager::MoveToTrashRestoreItem, false );
+        map.insert( StandardActionManager::SynchronizeCollectionTree, false );
 
         QTest::newRow( "writable folder collection selected" ) << collectionList << map;
       }
@@ -465,6 +472,7 @@ class ActionStateManagerTest : public QObject
         map.insert( StandardActionManager::RestoreItemsFromTrash, false );
         map.insert( StandardActionManager::MoveToTrashRestoreCollection, true );
         map.insert( StandardActionManager::MoveToTrashRestoreItem, false );
+        map.insert( StandardActionManager::SynchronizeCollectionTree, false );
 
         QTest::newRow( "favorite writable folder collection selected" ) << collectionList << map;
       }
@@ -506,6 +514,7 @@ class ActionStateManagerTest : public QObject
         map.insert( StandardActionManager::RestoreItemsFromTrash, false );
         map.insert( StandardActionManager::MoveToTrashRestoreCollection, true );
         map.insert( StandardActionManager::MoveToTrashRestoreItem, false );
+        map.insert( StandardActionManager::SynchronizeCollectionTree, false );
 
         QTest::newRow( "structural folder collection selected" ) << collectionList << map;
       }
@@ -548,6 +557,8 @@ class ActionStateManagerTest : public QObject
         map.insert( StandardActionManager::RestoreItemsFromTrash, false );
         map.insert( StandardActionManager::MoveToTrashRestoreCollection, false );
         map.insert( StandardActionManager::MoveToTrashRestoreItem, false );
+        map.insert( StandardActionManager::MoveToTrashRestoreItem, false );
+        map.insert( StandardActionManager::SynchronizeCollectionTree, false );
 
         QTest::newRow( "root collection and writable resource collection selected" ) << collectionList << map;
       }
@@ -566,7 +577,7 @@ class ActionStateManagerTest : public QObject
       QHashIterator<StandardActionManager::Type, bool> it( stateMap );
       while ( it.hasNext() ) {
         it.next();
-        //qDebug() << it.key();
+        qDebug() << it.key();
         QVERIFY( mStateMap.contains( it.key() ) );
         QCOMPARE( it.value(), mStateMap.value( it.key() ) );
       }


commit 5bdcb266a5ea99715157d443aadb822f2ae9e483
Author: Christian Mollekopf <chrigi_1 at fastmail.fm>
Date:   Thu Aug 28 11:52:48 2014 +0200

    Relations support

diff --git a/akonadi/CMakeLists.txt b/akonadi/CMakeLists.txt
index 5b748c3..7f2417f 100644
--- a/akonadi/CMakeLists.txt
+++ b/akonadi/CMakeLists.txt
@@ -191,6 +191,10 @@ set( akonadikde_LIB_SRC
   transportresourcebase.cpp
   typepluginloader.cpp
   attributeentity.cpp
+  relation.cpp
+  relationcreatejob.cpp
+  relationdeletejob.cpp
+  relationfetchjob.cpp
   tag.cpp
   tagmodel.cpp
   tagmodel_p.cpp
@@ -412,6 +416,9 @@ install( FILES
   specialcollectionsdiscoveryjob.h
   standardactionmanager.h
   statisticsproxymodel.h
+  relation.h
+  relationcreatejob.h
+  relationdeletejob.h
   tagattribute.h
   tag.h
   tagmodel.h
diff --git a/akonadi/item.cpp b/akonadi/item.cpp
index a1d024e..38681ba 100644
--- a/akonadi/item.cpp
+++ b/akonadi/item.cpp
@@ -267,6 +267,12 @@ Tag::List Item::tags() const
     return d->mTags;
 }
 
+Relation::List Item::relations() const
+{
+    Q_D(const Item);
+    return d->mRelations;
+}
+
 QSet<QByteArray> Item::loadedPayloadParts() const
 {
     return ItemSerializer::parts(*this);
diff --git a/akonadi/item.h b/akonadi/item.h
index 73c8225..c7a87a3 100644
--- a/akonadi/item.h
+++ b/akonadi/item.h
@@ -26,6 +26,7 @@
 #include <akonadi/entity.h>
 #include <akonadi/exception.h>
 #include <akonadi/tag.h>
+#include <akonadi/relation.h>
 #include <akonadi/collection.h>
 #include "itempayloadinternals_p.h"
 
@@ -229,6 +230,13 @@ public:
     void clearTags();
 
     /**
+     * Returns all relations of this item.
+     * @since 4.15
+     * @see RelationCreateJob, RelationDeleteJob to modify relations
+     */
+    Relation::List relations() const;
+
+    /**
      * Sets the payload based on the canonical representation normally
      * used for data of this mime type.
      *
diff --git a/akonadi/item_p.h b/akonadi/item_p.h
index d05ca44..8a2ea4c 100644
--- a/akonadi/item_p.h
+++ b/akonadi/item_p.h
@@ -414,6 +414,7 @@ public:
     int mRevision;
     Item::Flags mFlags;
     Tag::List mTags;
+    Relation::List mRelations;
     Entity::Id mCollectionId;
     Collection::List mVirtualReferences;
     qint64 mSize;
diff --git a/akonadi/itemfetchscope.cpp b/akonadi/itemfetchscope.cpp
index 3baaf1a..c142c5f 100644
--- a/akonadi/itemfetchscope.cpp
+++ b/akonadi/itemfetchscope.cpp
@@ -218,3 +218,13 @@ bool ItemFetchScope::fetchVirtualReferences() const
 {
     return d->mFetchVRefs;
 }
+
+void ItemFetchScope::setFetchRelations(bool fetchRelations)
+{
+    d->mFetchRelations = fetchRelations;
+}
+
+bool ItemFetchScope::fetchRelations() const
+{
+    return d->mFetchRelations;
+}
diff --git a/akonadi/itemfetchscope.h b/akonadi/itemfetchscope.h
index bb01461..32ef450 100644
--- a/akonadi/itemfetchscope.h
+++ b/akonadi/itemfetchscope.h
@@ -405,6 +405,24 @@ public:
      */
     bool fetchVirtualReferences() const;
 
+    /**
+     * Fetch relations for items.
+     *
+     * The default is @c false.
+     *
+     * @param fetchTags whether or not to load relations.
+     * @since 4.15
+     */
+    void setFetchRelations(bool fetchRelations);
+
+    /**
+     * Returns whether relations should be retrieved.
+     *
+     * @see setFetchRelations()
+     * @since 4.15
+     */
+    bool fetchRelations() const;
+
 private:
     //@cond PRIVATE
     QSharedDataPointer<ItemFetchScopePrivate> d;
diff --git a/akonadi/itemfetchscope_p.h b/akonadi/itemfetchscope_p.h
index 3dda8bf..a7eff17 100644
--- a/akonadi/itemfetchscope_p.h
+++ b/akonadi/itemfetchscope_p.h
@@ -46,6 +46,7 @@ public:
         , mFetchGid(false)
         , mFetchTags(false)
         , mFetchVRefs(false)
+        , mFetchRelations(false)
     {
         mTagFetchScope.setFetchIdOnly(true);
     }
@@ -68,6 +69,7 @@ public:
         mFetchTags = other.mFetchTags;
         mTagFetchScope = other.mTagFetchScope;
         mFetchVRefs = other.mFetchVRefs;
+        mFetchRelations = other.mFetchRelations;
     }
 
 public:
@@ -86,6 +88,7 @@ public:
     bool mFetchTags;
     TagFetchScope mTagFetchScope;
     bool mFetchVRefs;
+    bool mFetchRelations;
 };
 
 }
diff --git a/akonadi/monitor.h b/akonadi/monitor.h
index 2fb6bc2..e737a42 100644
--- a/akonadi/monitor.h
+++ b/akonadi/monitor.h
@@ -23,6 +23,7 @@
 #include <akonadi/tag.h>
 #include <akonadi/collection.h>
 #include <akonadi/item.h>
+#include <akonadi/relation.h>
 
 #include <QtCore/QObject>
 
@@ -82,7 +83,8 @@ public:
          */
         Collections = 1,
         Items,
-        Tags
+        Tags,
+        Relations
     };
 
     /**
@@ -427,6 +429,17 @@ Q_SIGNALS:
                           const QSet<Akonadi::Tag> &removedTags);
 
     /**
+     * This signal is emitted if relations of monitored items have changed.
+     *
+     * @param items Items that were changed
+     * @param addedRelations Relations that have been added to each item in @p items.
+     * @param removedRelations Relations that have been removed from each item in @p items
+     * @since 4.15
+     */
+    void itemsRelationsChanged(const Akonadi::Item::List &items, const Akonadi::Relation::List &addedRelations,
+                          const Akonadi::Relation::List &removedRelations);
+
+    /**
      * This signal is emitted if a monitored item has been moved between two collections
      *
      * @param item The moved item.
@@ -617,6 +630,28 @@ Q_SIGNALS:
     void tagRemoved(const Akonadi::Tag &tag);
 
     /**
+     * This signal is emitted if a relation has been added to Akonadi storage.
+     *
+     * The monitor will also emit itemRelationsChanged() signal for all monitored items
+     * hat are affected by @p relation.
+     *
+     * @param relation The added relation
+     * @since 4.13
+     */
+    void relationAdded(const Akonadi::Relation &relation);
+
+    /**
+     * This signal is emitted if a monitored relation is removed from the server storage.
+     *
+     * The monitor will also emit itemRelationsChanged() signal for all monitored items
+     * that were affected by @p relation.
+     *
+     * @param relation The removed relation.
+     * @since 4.13
+     */
+    void relationRemoved(const Akonadi::Relation &relation);
+
+    /**
      * This signal is emitted if the Monitor starts or stops monitoring @p collection explicitly.
      * @param collection The collection
      * @param monitored Whether the collection is now being monitored or not.
diff --git a/akonadi/monitor_p.cpp b/akonadi/monitor_p.cpp
index b514efd..c439611 100644
--- a/akonadi/monitor_p.cpp
+++ b/akonadi/monitor_p.cpp
@@ -232,6 +232,11 @@ void MonitorPrivate::checkBatchSupport(const NotificationMessageV3 &msg, bool &n
             batchSupported = true;
             needsSplit = false;
             return;
+        case NotificationMessageV2::ModifyRelations:
+            // Relations were added after batch notifications, so they are always supported
+            batchSupported = true;
+            needsSplit = false;
+            return;
         case NotificationMessageV2::Move:
             needsSplit = isBatch && q_ptr->receivers(SIGNAL(itemMoved(Akonadi::Item,Akonadi::Collection,Akonadi::Collection))) > 0;
             batchSupported = q_ptr->receivers(SIGNAL(itemsMoved(Akonadi::Item::List,Akonadi::Collection,Akonadi::Collection))) > 0;
@@ -260,6 +265,9 @@ void MonitorPrivate::checkBatchSupport(const NotificationMessageV3 &msg, bool &n
     } else if (msg.type() == NotificationMessageV2::Tags) {
         needsSplit = isBatch;
         batchSupported = false;
+    } else if (msg.type() == NotificationMessageV2::Relations) {
+        needsSplit = isBatch;
+        batchSupported = false;
     }
 }
 
@@ -303,7 +311,7 @@ bool MonitorPrivate::acceptNotification(const Akonadi::NotificationMessageV3 &ms
         return false;
     }
 
-    if (msg.entities().count() == 0) {
+    if (msg.entities().count() == 0 && msg.type() != NotificationMessageV2::Relations) {
         return false;
     }
 
@@ -378,6 +386,8 @@ bool MonitorPrivate::acceptNotification(const Akonadi::NotificationMessageV3 &ms
             return false;
         }
         return true;
+    case NotificationMessageV2::Relations:
+        return true;
     }
     Q_ASSERT(false);
     return false;
@@ -418,6 +428,9 @@ bool MonitorPrivate::ensureDataAvailable(const NotificationMessageV3 &msg)
         }
         return true;
     }
+    if (msg.type() == NotificationMessageV2::Relations) {
+        return true;
+    }
 
     bool allCached = true;
     if (fetchCollection) {
@@ -490,6 +503,22 @@ bool MonitorPrivate::emitNotification(const NotificationMessageV3 &msg)
         //In case of a Remove notification this will return a list of invalid entities (we'll deal later with them)
         const Tag::List tags = tagCache->retrieve(msg.uids());
         someoneWasListening = emitTagsNotification(msg, tags);
+    } else if (msg.type() == NotificationMessageV2::Relations) {
+        Relation rel;
+        Q_FOREACH (const QByteArray & part, msg.itemParts()) {
+            QList<QByteArray> splitPart = part.split(' ');
+            Q_ASSERT(splitPart.size() == 2);
+            if (splitPart.first() == "LEFT") {
+                rel.setLeft(Akonadi::Item(splitPart.at(1).toLongLong()));
+            } else if (splitPart.first() == "RIGHT") {
+                rel.setRight(Akonadi::Item(splitPart.at(1).toLongLong()));
+            } else if (splitPart.first() == "TYPE") {
+                rel.setType(splitPart.at(1));
+            } else if (splitPart.first() == "RID") {
+                rel.setRemoteId(splitPart.at(1));
+            }
+        }
+        someoneWasListening = emitRelationsNotification(msg, Relation::List() << rel);
     } else {
         const Collection parent = collectionCache->retrieve(msg.parentCollection());
         Collection destParent;
@@ -741,8 +770,23 @@ void MonitorPrivate::dispatchNotifications()
     }
 }
 
-bool MonitorPrivate::emitItemsNotification(const NotificationMessageV3 &msg, const Item::List &items, const Collection &collection, const Collection &collectionDest)
+static Relation::List extractRelations(QSet<QByteArray> &flags)
 {
+    Relation::List relations;
+    Q_FOREACH (const QByteArray &flag, flags) {
+        if (flag.startsWith("RELATION")) {
+            flags.remove(flag);
+            const QList<QByteArray> parts = flag.split(' ');
+            Q_ASSERT(parts.size() == 4);
+            relations << Relation(parts[1], Item(parts[2].toLongLong()), Item(parts[3].toLongLong()));
+        }
+    }
+    return relations;
+}
+
+bool MonitorPrivate::emitItemsNotification(const NotificationMessageV3 &msg_, const Item::List &items, const Collection &collection, const Collection &collectionDest)
+{
+    NotificationMessageV3 msg = msg_;
     Q_ASSERT(msg.type() == NotificationMessageV2::Items);
     Collection col = collection;
     Collection colDest = collectionDest;
@@ -758,6 +802,17 @@ bool MonitorPrivate::emitItemsNotification(const NotificationMessageV3 &msg, con
         }
     }
 
+    Relation::List addedRelations, removedRelations;
+    if (msg.operation() == NotificationMessageV2::ModifyRelations) {
+        QSet<QByteArray> addedFlags = msg.addedFlags();
+        addedRelations = extractRelations(addedFlags);
+        msg.setAddedFlags(addedFlags);
+
+        QSet<QByteArray> removedFlags = msg.removedFlags();
+        removedRelations = extractRelations(removedFlags);
+        msg.setRemovedFlags(removedFlags);
+    }
+
     Tag::List addedTags, removedTags;
     if (msg.operation() == NotificationMessageV2::ModifyTags) {
         addedTags = tagCache->retrieve(msg.addedTags().toList());
@@ -895,6 +950,12 @@ bool MonitorPrivate::emitItemsNotification(const NotificationMessageV3 &msg, con
             return true;
         }
         return false;
+    case NotificationMessageV2::ModifyRelations:
+        if (q_ptr->receivers(SIGNAL(itemsRelationsChanged(Akonadi::Item::List,QSet<Akonadi::Relation>,QSet<Akonadi::Relation>))) > 0) {
+            emit q_ptr->itemsRelationsChanged(its, addedRelations, removedRelations);
+            return true;
+        }
+        return false;
     default:
         kDebug() << "Unknown operation type" << msg.operation() << "in item change notification";
     }
@@ -1026,6 +1087,34 @@ bool MonitorPrivate::emitTagsNotification(const NotificationMessageV3 &msg, cons
     return false;
 }
 
+bool MonitorPrivate::emitRelationsNotification(const NotificationMessageV3 &msg, const Relation::List &relations)
+{
+    Q_ASSERT(msg.type() == NotificationMessageV2::Relations);
+
+    switch (msg.operation()) {
+    case NotificationMessageV2::Add:
+        if (q_ptr->receivers(SIGNAL(relationAdded(Akonadi::Relation))) == 0) {
+            return false;
+        }
+        Q_FOREACH (const Relation &relation, relations) {
+            Q_EMIT q_ptr->relationAdded(relation);
+        }
+        return true;
+    case NotificationMessageV2::Remove:
+        if (q_ptr->receivers(SIGNAL(relationRemoved(Akonadi::Relation))) == 0) {
+            return false;
+        }
+        Q_FOREACH (const Relation &relation, relations) {
+            Q_EMIT q_ptr->relationRemoved(relation);
+        }
+        return true;
+    default:
+        kDebug() << "Unknown operation type" << msg.operation() << "in tag change notification";
+    }
+
+    return false;
+}
+
 void MonitorPrivate::invalidateCaches(const NotificationMessageV3 &msg)
 {
     // remove invalidates
diff --git a/akonadi/monitor_p.h b/akonadi/monitor_p.h
index fb9f4d2..03e1aee 100644
--- a/akonadi/monitor_p.h
+++ b/akonadi/monitor_p.h
@@ -158,6 +158,8 @@ public:
 
     bool emitTagsNotification(const NotificationMessageV3 &msg, const Tag::List &tags);
 
+    bool emitRelationsNotification(const NotificationMessageV3 &msg, const Relation::List &relations);
+
     void serverStateChanged(Akonadi::ServerManager::State state);
 
     /**
diff --git a/akonadi/protocolhelper.cpp b/akonadi/protocolhelper.cpp
index 60e2440..c0027c7 100644
--- a/akonadi/protocolhelper.cpp
+++ b/akonadi/protocolhelper.cpp
@@ -22,6 +22,7 @@
 #include "attributefactory.h"
 #include "collectionstatistics.h"
 #include "entity_p.h"
+#include "item_p.h"
 #include "exception.h"
 #include "itemserializer_p.h"
 #include "itemserializerplugin.h"
@@ -109,7 +110,6 @@ void ProtocolHelper::parseAncestors( const QByteArray &data, Entity *entity, int
 
   static const Collection::Id rootCollectionId = Collection::root().id();
   QVarLengthArray<QByteArray, 16> ancestors;
-  // QVarLengthArray<QByteArray, 16> parentIds;
   QList<QByteArray> parentIds;
 
   ImapParser::parseParenthesizedList( data, ancestors );
@@ -465,6 +465,8 @@ QByteArray ProtocolHelper::itemFetchScopeToByteArray( const ItemFetchScope &fetc
     command += " VIRTREF";
   if ( fetchScope.fetchModificationTime() )
     command += " DATETIME";
+  if ( fetchScope.fetchRelations() )
+    command += " RELATIONS";
   foreach ( const QByteArray &part, fetchScope.payloadParts() )
     command += ' ' + ProtocolHelper::encodePartIdentifier( ProtocolHelper::PartPayload, part );
   foreach ( const QByteArray &part, fetchScope.attributes() )
@@ -586,6 +588,18 @@ void ProtocolHelper::parseItemFetchResult( const QList<QByteArray> &lineTokens,
         }
       }
       item.setTags( tags );
+    } else if ( key == "RELATIONS" ) {
+        Relation::List relations;
+        QList<QByteArray> data;
+        ImapParser::parseParenthesizedList( lineTokens[i + 1], data );
+        Q_FOREACH (const QByteArray &d, data) {
+            QList<QByteArray> parts;
+            ImapParser::parseParenthesizedList( d, parts );
+            Relation relation;
+            parseRelationFetchResult(parts, relation);
+            relations << relation;
+        }
+        item.d_func()->mRelations = relations;
     } else if ( key == "VIRTREF" ) {
       ImapSet set;
       ImapParser::parseSequenceSet( lineTokens[i + 1], set );
@@ -695,6 +709,26 @@ void ProtocolHelper::parseTagFetchResult( const QList<QByteArray> &lineTokens, T
   }
 }
 
+void ProtocolHelper::parseRelationFetchResult( const QList<QByteArray> &lineTokens, Relation &relation )
+{
+  for (int i = 0; i < lineTokens.count() - 1; i += 2) {
+    const QByteArray key = lineTokens.value(i);
+    const QByteArray value = lineTokens.value(i + 1);
+
+    if (key == "LEFT") {
+      relation.setLeft(Akonadi::Item(value.toLongLong()));
+    } else if (key == "RIGHT") {
+      relation.setRight(Akonadi::Item(value.toLongLong()));
+    } else if (key == "REMOTEID") {
+      relation.setRemoteId(value);
+    } else if ( key == "TYPE" ) {
+      relation.setType(value);
+    } else {
+      kWarning() << "Unknown relation attribute " << key;
+    }
+  }
+}
+
 QString ProtocolHelper::akonadiStoragePath()
 {
     QString fullRelPath = QLatin1String("akonadi");
diff --git a/akonadi/protocolhelper_p.h b/akonadi/protocolhelper_p.h
index f3612f6..150965f 100644
--- a/akonadi/protocolhelper_p.h
+++ b/akonadi/protocolhelper_p.h
@@ -234,6 +234,7 @@ public:
      */
     static void parseItemFetchResult(const QList<QByteArray> &lineTokens, Item &item, ProtocolHelperValuePool *valuePool = 0);
     static void parseTagFetchResult(const QList<QByteArray> &lineTokens, Tag &tag);
+    static void parseRelationFetchResult(const QList<QByteArray> &lineTokens, Relation &tag);
 
     static QString akonadiStoragePath();
     static QString absolutePayloadFilePath(const QString &fileName);
diff --git a/akonadi/relation.cpp b/akonadi/relation.cpp
new file mode 100644
index 0000000..566b4f3
--- /dev/null
+++ b/akonadi/relation.cpp
@@ -0,0 +1,136 @@
+/*
+    Copyright (c) 2014 Christian Mollekopf <mollekopf at kolabsys.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "relation.h"
+
+#include <akonadi/item.h>
+
+using namespace Akonadi;
+
+const char *Akonadi::Relation::GENERIC = "GENERIC";
+
+struct Relation::Private {
+    Item left;
+    Item right;
+    QByteArray type;
+    QByteArray remoteId;
+};
+
+Relation::Relation()
+    : d(new Private)
+{
+
+}
+
+Relation::Relation(const QByteArray &type, const Item &left, const Item &right)
+    : d(new Private)
+{
+    d->left = left;
+    d->right = right;
+    d->type = type;
+}
+
+Relation::Relation(const Relation &other)
+    : d(new Private)
+{
+    operator=(other);
+}
+
+Relation::~Relation()
+{
+}
+
+Relation &Relation::operator=(const Relation &other)
+{
+    d->left = other.d->left;
+    d->right = other.d->right;
+    d->type = other.d->type;
+    return *this;
+}
+
+bool Relation::operator==(const Relation &other) const
+{
+    if (isValid() && other.isValid()) {
+        return d->left == other.d->left
+            && d->right == other.d->right
+            && d->type == other.d->type;
+    }
+    return false;
+}
+
+bool Relation::operator!=(const Relation &other) const
+{
+    return !operator==(other);
+}
+
+void Relation::setLeft(const Item &left)
+{
+    d->left = left;
+}
+
+Item Relation::left() const
+{
+    return d->left;
+}
+
+void Relation::setRight(const Item &right)
+{
+    d->right = right;
+}
+
+Item Relation::right() const
+{
+    return d->right;
+}
+
+void Relation::setType(const QByteArray &type) const
+{
+    d->type = type;
+}
+
+QByteArray Relation::type() const
+{
+    return d->type;
+}
+
+void Relation::setRemoteId(const QByteArray &remoteId) const
+{
+    d->remoteId = remoteId;
+}
+
+QByteArray Relation::remoteId() const
+{
+    return d->remoteId;
+}
+
+bool Relation::isValid() const
+{
+    return (d->left.isValid() || !d->left.remoteId().isEmpty()) && (d->right.isValid() || !d->left.remoteId().isEmpty()) && !d->type.isEmpty();
+}
+
+uint qHash(const Relation &relation)
+{
+    return (3*qHash(relation.left())+qHash(relation.right())+qHash(relation.type()));
+}
+
+QDebug &operator<<(QDebug &debug, const Relation &relation)
+{
+    debug << "Akonadi::Relation( TYPE " << relation.type() << ", LEFT " << relation.left().id() << ", RIGHT " << relation.right().id() << ")";
+    return debug;
+}
diff --git a/akonadi/relation.h b/akonadi/relation.h
new file mode 100644
index 0000000..7d2a78a
--- /dev/null
+++ b/akonadi/relation.h
@@ -0,0 +1,133 @@
+/*
+    Copyright (c) 2014 Christian Mollekopf <mollekopf at kolabsys.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef AKONADI_RELATION_H
+#define AKONADI_RELATION_H
+
+#include "akonadi_export.h"
+
+namespace Akonadi {
+class Relation;
+}
+
+AKONADI_EXPORT unsigned int qHash(const Akonadi::Relation &);
+
+#include <QSharedPointer>
+#include <QByteArray>
+#include <QList>
+#include <QDebug>
+
+namespace Akonadi {
+class Item;
+
+/**
+ * An Akonadi Relation.
+ *
+ * A Relation object represents an relation between two Akonadi items.
+ *
+ * An example usecase could be a association of a note with an email. The note (that for instance contains personal notes for the email),
+ * can be stored independently but is easily retrieved by asking for relations the email.
+ * 
+ * The relation type allows to distinguish various types of relations that could for instance be bidirectional or not.
+ * 
+ * @since 4.15
+ */
+class AKONADI_EXPORT Relation
+{
+public:
+    typedef QList<Relation> List;
+
+    /**
+     * The GENERIC type represents a generic relation between two items.
+     */
+    static const char *GENERIC;
+
+    /**
+     * Creates an invalid relation.
+     */
+    Relation();
+
+    /**
+     * Creates a relation
+     */
+    explicit Relation(const QByteArray &type, const Item &left, const Item &right);
+
+    Relation(const Relation &other);
+    ~Relation();
+
+    Relation &operator=(const Relation &);
+    bool operator==(const Relation &) const;
+    bool operator!=(const Relation &) const;
+
+    /**
+     * Sets the @p item of the left side of the relation.
+     */
+    void setLeft(const Item &item);
+
+    /**
+     * Returns the identifier of the left side of the relation.
+     */
+    Item left() const;
+
+    /**
+     * Sets the @p item of the right side of the relation.
+     */
+    void setRight(const Akonadi::Item &item);
+
+    /**
+     * Returns the identifier of the right side of the relation.
+     */
+    Item right() const;
+
+    /**
+     * Sets the type of the relation.
+     */
+    void setType(const QByteArray &type) const;
+
+    /**
+     * Returns the type of the relation.
+     */
+    QByteArray type() const;
+
+    /**
+     * Sets the type of the relation.
+     */
+    void setRemoteId(const QByteArray &type) const;
+
+    /**
+     * Returns the type of the relation.
+     */
+    QByteArray remoteId() const;
+
+    bool isValid() const;
+
+private:
+    class Private;
+    QSharedPointer<Private> d;
+};
+
+}
+
+AKONADI_EXPORT QDebug &operator<<(QDebug &debug, const Akonadi::Relation &tag);
+
+Q_DECLARE_METATYPE(Akonadi::Relation)
+Q_DECLARE_METATYPE(Akonadi::Relation::List)
+Q_DECLARE_METATYPE(QSet<Akonadi::Relation>)
+
+#endif
diff --git a/akonadi/relationcreatejob.cpp b/akonadi/relationcreatejob.cpp
new file mode 100644
index 0000000..4cc8a9a
--- /dev/null
+++ b/akonadi/relationcreatejob.cpp
@@ -0,0 +1,86 @@
+/*
+    Copyright (c) 2014 Christian Mollekopf <mollekopf at kolabsys.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "relationcreatejob.h"
+#include "job_p.h"
+#include "relation.h"
+#include "protocolhelper_p.h"
+#include <KLocalizedString>
+
+using namespace Akonadi;
+
+struct Akonadi::RelationCreateJobPrivate : public JobPrivate
+{
+    RelationCreateJobPrivate(RelationCreateJob *parent)
+        : JobPrivate(parent)
+    {
+    }
+
+    Relation mRelation;
+};
+
+RelationCreateJob::RelationCreateJob(const Akonadi::Relation &relation, QObject *parent)
+    : Job(new RelationCreateJobPrivate(this), parent)
+{
+    Q_D(RelationCreateJob);
+    d->mRelation = relation;
+}
+
+void RelationCreateJob::doStart()
+{
+    Q_D(RelationCreateJob);
+
+    if (!d->mRelation.isValid()) {
+        kWarning() << "The relation is invalid";
+        setError(Job::Unknown);
+        setErrorText(i18n("Failed to create relation."));
+        emitResult();
+        return;
+    }
+
+    QByteArray command = d->newTag() + " UID RELATIONSTORE ";
+
+    QList<QByteArray> list;
+    list << "LEFT";
+    list << QByteArray::number(d->mRelation.left().id());
+    list << "RIGHT";
+    list << QByteArray::number(d->mRelation.right().id());
+    list << "TYPE";
+    list << ImapParser::quote(d->mRelation.type());
+    if (!d->mRelation.remoteId().isEmpty()) {
+        list << "REMOTEID";
+        list << d->mRelation.remoteId();
+    }
+
+    command += ImapParser::join(list, " ") + "\n";
+
+    d->writeData(command);
+}
+
+void RelationCreateJob::doHandleResponse(const QByteArray &tag, const QByteArray &data)
+{
+    Q_D(RelationCreateJob);
+    kWarning() << "Unhandled response: " << tag << data;
+}
+
+Relation RelationCreateJob::relation() const
+{
+    Q_D(const RelationCreateJob);
+    return d->mRelation;
+}
diff --git a/akonadi/relationcreatejob.h b/akonadi/relationcreatejob.h
new file mode 100644
index 0000000..5feac15
--- /dev/null
+++ b/akonadi/relationcreatejob.h
@@ -0,0 +1,62 @@
+/*
+    Copyright (c) 2014 Christian Mollekopf <mollekopf at kolabsys.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef AKONADI_RELATIONCREATEJOB_H
+#define AKONADI_RELATIONCREATEJOB_H
+
+#include <akonadi/job.h>
+
+namespace Akonadi {
+
+class Relation;
+class RelationCreateJobPrivate;
+
+/**
+ * @short Job that creates a new relation in the Akonadi storage.
+ * @since 4.15
+ */
+class AKONADI_EXPORT RelationCreateJob : public Job
+{
+    Q_OBJECT
+
+public:
+    /**
+     * Creates a new relation create job.
+     *
+     * @param relation The relation to create.
+     * @param parent The parent object.
+     */
+    explicit RelationCreateJob(const Relation &relation, QObject *parent = 0);
+
+    /**
+     * Returns the relation.
+     */
+    Relation relation() const;
+
+protected:
+    virtual void doStart();
+    virtual void doHandleResponse(const QByteArray &tag, const QByteArray &data);
+
+private:
+    Q_DECLARE_PRIVATE(RelationCreateJob)
+};
+
+}
+
+#endif
diff --git a/akonadi/relationdeletejob.cpp b/akonadi/relationdeletejob.cpp
new file mode 100644
index 0000000..8c65d41
--- /dev/null
+++ b/akonadi/relationdeletejob.cpp
@@ -0,0 +1,84 @@
+/*
+    Copyright (c) 2014 Christian Mollekopf <mollekopf at kolabsys.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "relationdeletejob.h"
+#include "job_p.h"
+#include "relation.h"
+#include "protocolhelper_p.h"
+#include <KLocalizedString>
+
+using namespace Akonadi;
+
+struct Akonadi::RelationDeleteJobPrivate : public JobPrivate
+{
+    RelationDeleteJobPrivate(RelationDeleteJob *parent)
+        : JobPrivate(parent)
+    {
+    }
+
+    Relation mRelation;
+};
+
+RelationDeleteJob::RelationDeleteJob(const Akonadi::Relation &relation, QObject *parent)
+    : Job(new RelationDeleteJobPrivate(this), parent)
+{
+    Q_D(RelationDeleteJob);
+    d->mRelation = relation;
+}
+
+void RelationDeleteJob::doStart()
+{
+    Q_D(RelationDeleteJob);
+
+    if (!d->mRelation.isValid()) {
+        kWarning() << "The relation is invalid";
+        setError(Job::Unknown);
+        setErrorText(i18n("Failed to create relation."));
+        emitResult();
+        return;
+    }
+
+    QByteArray command = d->newTag() + " UID RELATIONREMOVE ";
+
+    QList<QByteArray> list;
+    list << "LEFT";
+    list << QByteArray::number(d->mRelation.left().id());
+    list << "RIGHT";
+    list << QByteArray::number(d->mRelation.right().id());
+    if (!d->mRelation.type().isEmpty()) {
+        list << "TYPE";
+        list << ImapParser::quote(d->mRelation.type());
+    }
+
+    command += ImapParser::join(list, " ") + "\n";
+
+    d->writeData(command);
+}
+
+void RelationDeleteJob::doHandleResponse(const QByteArray &tag, const QByteArray &data)
+{
+    Q_D(RelationDeleteJob);
+    kWarning() << "Unhandled response: " << tag << data;
+}
+
+Relation RelationDeleteJob::relation() const
+{
+    Q_D(const RelationDeleteJob);
+    return d->mRelation;
+}
diff --git a/akonadi/relationdeletejob.h b/akonadi/relationdeletejob.h
new file mode 100644
index 0000000..6dbd210
--- /dev/null
+++ b/akonadi/relationdeletejob.h
@@ -0,0 +1,62 @@
+/*
+    Copyright (c) 2014 Christian Mollekopf <mollekopf at kolabsys.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef AKONADI_RELATIONDELETEJOB_H
+#define AKONADI_RELATIONDELETEJOB_H
+
+#include <akonadi/job.h>
+
+namespace Akonadi {
+
+class Relation;
+class RelationDeleteJobPrivate;
+
+/**
+ * @short Job that deletes a relation in the Akonadi storage.
+ * @since 4.15
+ */
+class AKONADI_EXPORT RelationDeleteJob : public Job
+{
+    Q_OBJECT
+
+public:
+    /**
+     * Creates a new relation delete job.
+     *
+     * @param relation The relation to delete.
+     * @param parent The parent object.
+     */
+    explicit RelationDeleteJob(const Relation &relation, QObject *parent = 0);
+
+    /**
+     * Returns the relation.
+     */
+    Relation relation() const;
+
+protected:
+    virtual void doStart();
+    virtual void doHandleResponse(const QByteArray &tag, const QByteArray &data);
+
+private:
+    Q_DECLARE_PRIVATE(RelationDeleteJob)
+};
+
+}
+
+#endif
diff --git a/akonadi/relationfetchjob.cpp b/akonadi/relationfetchjob.cpp
new file mode 100644
index 0000000..e248afb
--- /dev/null
+++ b/akonadi/relationfetchjob.cpp
@@ -0,0 +1,164 @@
+/*
+    Copyright (c) 2014 Christian Mollekopf <mollekopf at kolabsys.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "relationfetchjob.h"
+#include "job_p.h"
+#include "relation.h"
+#include "protocolhelper_p.h"
+#include <QTimer>
+
+using namespace Akonadi;
+
+class Akonadi::RelationFetchJobPrivate : public JobPrivate
+{
+public:
+    RelationFetchJobPrivate(RelationFetchJob *parent)
+        : JobPrivate(parent)
+        , mEmitTimer(0)
+    {
+    }
+
+    void init()
+    {
+        Q_Q(RelationFetchJob);
+        mEmitTimer = new QTimer(q);
+        mEmitTimer->setSingleShot(true);
+        mEmitTimer->setInterval(100);
+        q->connect(mEmitTimer, SIGNAL(timeout()), q, SLOT(timeout()));
+    }
+
+    void aboutToFinish()
+    {
+      timeout();
+    }
+
+    void timeout()
+    {
+        Q_Q(RelationFetchJob);
+        mEmitTimer->stop(); // in case we are called by result()
+        if (!mPendingRelations.isEmpty()) {
+            if (!q->error()) {
+                emit q->relationsReceived(mPendingRelations);
+            }
+            mPendingRelations.clear();
+        }
+    }
+
+    Q_DECLARE_PUBLIC(RelationFetchJob)
+
+    Relation::List mResultRelations;
+    Relation::List mPendingRelations; // relation pending for emitting itemsReceived()
+    QTimer *mEmitTimer;
+    QStringList mTypes;
+    QString mResource;
+    Relation mRequestedRelation;
+};
+
+RelationFetchJob::RelationFetchJob(const Relation &relation, QObject *parent)
+    : Job(new RelationFetchJobPrivate(this), parent)
+{
+    Q_D(RelationFetchJob);
+    d->init();
+    d->mRequestedRelation = relation;
+}
+
+RelationFetchJob::RelationFetchJob(const QStringList &types, QObject *parent)
+    : Job(new RelationFetchJobPrivate(this), parent)
+{
+    Q_D(RelationFetchJob);
+    d->init();
+    d->mTypes = types;
+}
+
+void RelationFetchJob::doStart()
+{
+    Q_D(RelationFetchJob);
+
+    QByteArray command = d->newTag();
+    command += " UID RELATIONFETCH ";
+
+    QList<QByteArray> filter;
+    if (!d->mResource.isEmpty()) {
+        filter.append("RESOURCE");
+        filter.append(d->mResource.toUtf8());
+    }
+    if (!d->mTypes.isEmpty()) {
+        filter.append("TYPE");
+        QList<QByteArray> types;
+        foreach (const QString &t, d->mTypes) {
+            types.append(t.toUtf8());
+        }
+        filter.append('(' + ImapParser::join(types, " ") + ')');
+    } else if (!d->mRequestedRelation.type().isEmpty()) {
+        filter.append("TYPE");
+        filter.append('(' + d->mRequestedRelation.type() + ')');
+    }
+    if (d->mRequestedRelation.left().id() >= 0) {
+        filter << "LEFT" << QByteArray::number(d->mRequestedRelation.left().id());
+    }
+    if (d->mRequestedRelation.right().id() >= 0) {
+        filter << "RIGHT" << QByteArray::number(d->mRequestedRelation.right().id());
+    }
+
+    command += "(" + ImapParser::join(filter, " ") + ")\n";
+
+    kDebug() << command;
+    d->writeData(command);
+}
+
+void RelationFetchJob::doHandleResponse(const QByteArray &tag, const QByteArray &data)
+{
+    Q_D(RelationFetchJob);
+
+    if (tag == "*") {
+        int begin = data.indexOf("RELATIONFETCH");
+        if (begin >= 0) {
+            // split fetch response into key/value pairs
+            QList<QByteArray> fetchResponse;
+            ImapParser::parseParenthesizedList(data, fetchResponse, begin + 8);
+
+            Relation rel;
+            ProtocolHelper::parseRelationFetchResult(fetchResponse, rel);
+
+            if (rel.isValid()) {
+                d->mResultRelations.append(rel);
+                d->mPendingRelations.append(rel);
+                if (!d->mEmitTimer->isActive()) {
+                    d->mEmitTimer->start();
+                }
+            }
+            return;
+        }
+    }
+    kDebug() << "Unhandled response: " << tag << data;
+}
+
+Relation::List RelationFetchJob::relations() const
+{
+    Q_D(const RelationFetchJob);
+    return d->mResultRelations;
+}
+
+void RelationFetchJob::setResource(const QString &identifier)
+{
+    Q_D(RelationFetchJob);
+    d->mResource = identifier;
+}
+
+#include "moc_relationfetchjob.cpp"
diff --git a/akonadi/relationfetchjob.h b/akonadi/relationfetchjob.h
new file mode 100644
index 0000000..27c60ba
--- /dev/null
+++ b/akonadi/relationfetchjob.h
@@ -0,0 +1,79 @@
+/*
+    Copyright (c) 2014 Christian Mollekopf <mollekopf at kolabsys.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef AKONADI_RELATIONFETCHJOB_H
+#define AKONADI_RELATIONFETCHJOB_H
+
+#include <akonadi/job.h>
+#include <akonadi/relation.h>
+
+namespace Akonadi {
+
+class Relation;
+class RelationFetchJobPrivate;
+
+/**
+ * @short Job that to fetch relations from Akonadi storage.
+ * @since 4.15
+ */
+class AKONADI_EXPORT RelationFetchJob : public Job
+{
+    Q_OBJECT
+
+public:
+    /**
+     * Creates a new relation fetch job.
+     *
+     * @param relation The relation to fetch.
+     * @param parent The parent object.
+     */
+    explicit RelationFetchJob(const Relation &relation, QObject *parent = 0);
+
+    explicit RelationFetchJob(const QStringList &types, QObject *parent = 0);
+
+    void setResource(const QString &identifier);
+
+    /**
+     * Returns the relations.
+     */
+    Relation::List relations() const;
+
+Q_SIGNALS:
+    /**
+     * This signal is emitted whenever new relations have been fetched completely.
+     *
+     * @param relations The fetched relations.
+     */
+    void relationsReceived(const Akonadi::Relation::List &relations);
+
+protected:
+    virtual void doStart();
+    virtual void doHandleResponse(const QByteArray &tag, const QByteArray &data);
+
+private:
+    Q_DECLARE_PRIVATE(RelationFetchJob)
+
+    //@cond PRIVATE
+    Q_PRIVATE_SLOT(d_func(), void timeout())
+    //@endcond
+};
+
+}
+
+#endif
diff --git a/akonadi/tests/CMakeLists.txt b/akonadi/tests/CMakeLists.txt
index a67b635..7d66e4a 100644
--- a/akonadi/tests/CMakeLists.txt
+++ b/akonadi/tests/CMakeLists.txt
@@ -152,4 +152,5 @@ add_akonadi_isolated_test(lazypopulationtest.cpp)
 add_akonadi_isolated_test(favoriteproxytest.cpp)
 add_akonadi_isolated_test_advanced(itemsearchjobtest.cpp testsearchplugin/testsearchplugin.cpp "")
 add_akonadi_isolated_test(tagtest.cpp)
+add_akonadi_isolated_test(relationtest.cpp)
 add_akonadi_isolated_test(etmpopulationtest.cpp)
diff --git a/akonadi/tests/relationtest.cpp b/akonadi/tests/relationtest.cpp
new file mode 100644
index 0000000..5deeb6a
--- /dev/null
+++ b/akonadi/tests/relationtest.cpp
@@ -0,0 +1,165 @@
+/*
+    Copyright (c) 2014 Christian Mollekopf <mollekopf at kolabsys.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include <QObject>
+
+#include "test_utils.h"
+
+#include <akonadi/control.h>
+#include <akonadi/relationcreatejob.h>
+#include <akonadi/relationfetchjob.h>
+#include <akonadi/relationdeletejob.h>
+#include <tagmodifyjob.h>
+#include <resourceselectjob_p.h>
+#include <akonadi/qtest_akonadi.h>
+#include <akonadi/item.h>
+#include <akonadi/itemcreatejob.h>
+#include <akonadi/itemmodifyjob.h>
+#include <akonadi/itemfetchjob.h>
+#include <akonadi/itemfetchscope.h>
+#include <akonadi/monitor.h>
+#include <akonadi/attributefactory.h>
+
+using namespace Akonadi;
+
+class RelationTest : public QObject
+{
+    Q_OBJECT
+
+private Q_SLOTS:
+    void initTestCase();
+
+    void testCreateFetch();
+    void testMonitor();
+};
+
+void RelationTest::initTestCase()
+{
+    AkonadiTest::checkTestIsIsolated();
+    AkonadiTest::setAllResourcesOffline();
+    qRegisterMetaType<Akonadi::Relation>();
+    qRegisterMetaType<QSet<Akonadi::Relation> >();
+    qRegisterMetaType<Akonadi::Item::List>();
+}
+
+void RelationTest::testCreateFetch()
+{
+    const Collection res3 = Collection( collectionIdFromPath( "res3" ) );
+    Item item1;
+    {
+        item1.setMimeType( "application/octet-stream" );
+        ItemCreateJob *append = new ItemCreateJob(item1, res3, this);
+        AKVERIFYEXEC(append);
+        item1 = append->item();
+    }
+    Item item2;
+    {
+        item2.setMimeType( "application/octet-stream" );
+        ItemCreateJob *append = new ItemCreateJob(item2, res3, this);
+        AKVERIFYEXEC(append);
+        item2 = append->item();
+    }
+
+    Relation rel(Relation::GENERIC, item1, item2);
+    RelationCreateJob *createjob = new RelationCreateJob(rel, this);
+    AKVERIFYEXEC(createjob);
+
+    //Test fetch & create
+    {
+        RelationFetchJob *fetchJob = new RelationFetchJob(QStringList(), this);
+        AKVERIFYEXEC(fetchJob);
+        QCOMPARE(fetchJob->relations().size(), 1);
+        QCOMPARE(fetchJob->relations().first().type(), QByteArray(Relation::GENERIC));
+    }
+
+    //Test item fetch
+    {
+        ItemFetchJob *fetchJob = new ItemFetchJob(item1);
+        fetchJob->fetchScope().setFetchRelations(true);
+        AKVERIFYEXEC(fetchJob);
+        QCOMPARE(fetchJob->items().first().relations().size(), 1);
+    }
+
+    {
+        ItemFetchJob *fetchJob = new ItemFetchJob(item2);
+        fetchJob->fetchScope().setFetchRelations(true);
+        AKVERIFYEXEC(fetchJob);
+        QCOMPARE(fetchJob->items().first().relations().size(), 1);
+    }
+
+    //Test delete
+    {
+        RelationDeleteJob *deleteJob = new RelationDeleteJob(rel, this);
+        AKVERIFYEXEC(deleteJob);
+
+        RelationFetchJob *fetchJob = new RelationFetchJob(QStringList(), this);
+        AKVERIFYEXEC(fetchJob);
+        QCOMPARE(fetchJob->relations().size(), 0);
+    }
+}
+
+void RelationTest::testMonitor()
+{
+    Akonadi::Monitor monitor;
+    monitor.setTypeMonitored(Akonadi::Monitor::Relations);
+
+    const Collection res3 = Collection( collectionIdFromPath( "res3" ) );
+    Item item1;
+    {
+        item1.setMimeType( "application/octet-stream" );
+        ItemCreateJob *append = new ItemCreateJob(item1, res3, this);
+        AKVERIFYEXEC(append);
+        item1 = append->item();
+    }
+    Item item2;
+    {
+        item2.setMimeType( "application/octet-stream" );
+        ItemCreateJob *append = new ItemCreateJob(item2, res3, this);
+        AKVERIFYEXEC(append);
+        item2 = append->item();
+    }
+
+    Relation rel(Relation::GENERIC, item1, item2);
+
+    {
+        QSignalSpy addedSpy(&monitor, SIGNAL(relationAdded(Akonadi::Relation)));
+        QVERIFY(addedSpy.isValid());
+
+        RelationCreateJob *createjob = new RelationCreateJob(rel, this);
+        AKVERIFYEXEC(createjob);
+
+        //We usually pick up signals from the previous tests as well (due to server-side notification caching)
+        QTRY_VERIFY(addedSpy.count() >= 1);
+        QTRY_COMPARE(addedSpy.last().first().value<Akonadi::Relation>(), rel);
+    }
+
+    {
+        QSignalSpy removedSpy(&monitor, SIGNAL(relationRemoved(Akonadi::Relation)));
+        QVERIFY(removedSpy.isValid());
+        RelationDeleteJob *deleteJob = new RelationDeleteJob(rel, this);
+        AKVERIFYEXEC(deleteJob);
+        QTRY_VERIFY(removedSpy.count() >= 1);
+        QTRY_COMPARE(removedSpy.last().first().value<Akonadi::Relation>(), rel);
+    }
+}
+
+
+#include "relationtest.moc"
+
+QTEST_AKONADIMAIN(RelationTest, NoGUI)




More information about the commits mailing list