Branch 'kolab/integration/4.13.0' - 5 commits - resources/imap resources/kolab resources/shared

Christian Mollekopf mollekopf at kolabsys.com
Fri Aug 29 13:15:53 CEST 2014


 resources/imap/CMakeLists.txt                                                     |    1 
 resources/imap/batchfetcher.cpp                                                   |    6 
 resources/imap/imapresourcebase.cpp                                               |    3 
 resources/imap/imapresourcebase.h                                                 |    4 
 resources/imap/replacemessagejob.cpp                                              |  185 ++++++++
 resources/imap/replacemessagejob.h                                                |   61 ++
 resources/imap/resourcestate.cpp                                                  |   30 +
 resources/imap/resourcestate.h                                                    |   14 
 resources/imap/resourcestateinterface.h                                           |    8 
 resources/imap/resourcetask.cpp                                                   |    6 
 resources/imap/resourcetask.h                                                     |    2 
 resources/imap/retrieveitemstask.cpp                                              |   44 +
 resources/imap/sessionpool.cpp                                                    |   36 +
 resources/imap/sessionpool.h                                                      |    4 
 resources/imap/tests/dummyresourcestate.cpp                                       |   58 ++
 resources/imap/tests/dummyresourcestate.h                                         |   20 
 resources/imap/tests/testretrieveitemstask.cpp                                    |   42 +
 resources/imap/tests/testsessionpool.cpp                                          |   42 +
 resources/kolab/CMakeLists.txt                                                    |   57 +-
 resources/kolab/kolabaddtagtask.cpp                                               |  193 ++++++++
 resources/kolab/kolabaddtagtask.h                                                 |   49 ++
 resources/kolab/kolabchangeitemstagstask.cpp                                      |  135 +++++
 resources/kolab/kolabchangeitemstagstask.h                                        |   54 ++
 resources/kolab/kolabchangetagtask.cpp                                            |   81 +++
 resources/kolab/kolabchangetagtask.h                                              |   49 ++
 resources/kolab/kolabhelpers.cpp                                                  |   16 
 resources/kolab/kolabhelpers.h                                                    |    3 
 resources/kolab/kolabrelationresourcetask.cpp                                     |   77 +++
 resources/kolab/kolabrelationresourcetask.h                                       |   53 ++
 resources/kolab/kolabremovetagtask.cpp                                            |   93 ++++
 resources/kolab/kolabremovetagtask.h                                              |   44 +
 resources/kolab/kolabresource.cpp                                                 |   36 +
 resources/kolab/kolabresource.h                                                   |    6 
 resources/kolab/kolabresourcestate.cpp                                            |    5 
 resources/kolab/kolabretrievetagstask.cpp                                         |  152 ++++++
 resources/kolab/kolabretrievetagstask.h                                           |   56 ++
 resources/kolab/tagchangehelper.cpp                                               |  164 +++++++
 resources/kolab/tagchangehelper.h                                                 |   71 +++
 resources/kolab/tests/CMakeLists.txt                                              |   26 +
 resources/kolab/tests/imaptestbase.cpp                                            |  137 +++++
 resources/kolab/tests/imaptestbase.h                                              |   95 ++++
 resources/kolab/tests/testchangeitemstagstask.cpp                                 |  229 ++++++++++
 resources/kolab/tests/testretrievetagstask.cpp                                    |  184 ++++++++
 resources/kolab/tests/unittestenv/config-mysql-db.xml                             |    8 
 resources/kolab/tests/unittestenv/config-mysql-fs.xml                             |    8 
 resources/kolab/tests/unittestenv/config-postgresql-db.xml                        |    8 
 resources/kolab/tests/unittestenv/config-postgresql-fs.xml                        |    8 
 resources/kolab/tests/unittestenv/config-sqlite-db.xml                            |    8 
 resources/kolab/tests/unittestenv/kdehome/share/config/akonadi-firstrunrc         |    4 
 resources/kolab/tests/unittestenv/kdehome/share/config/akonadi_knut_resource_0rc  |    4 
 resources/kolab/tests/unittestenv/kdehome/share/config/kdebugrc                   |   80 +++
 resources/kolab/tests/unittestenv/kdehome/share/config/kdedrc                     |    3 
 resources/kolab/tests/unittestenv/kdehome/testdata-res1.xml                       |   37 +
 resources/kolab/tests/unittestenv/xdgconfig-mysql.db/akonadi/akonadiserverrc      |    5 
 resources/kolab/tests/unittestenv/xdgconfig-mysql.fs/akonadi/akonadiserverrc      |    6 
 resources/kolab/tests/unittestenv/xdgconfig-postgresql.db/akonadi/akonadiserverrc |    9 
 resources/kolab/tests/unittestenv/xdgconfig-postgresql.fs/akonadi/akonadiserverrc |   10 
 resources/kolab/tests/unittestenv/xdgconfig-sqlite.db/akonadi/akonadiserverrc     |   10 
 resources/shared/imapaclattribute.cpp                                             |    2 
 resources/shared/tests/imapaclattributetest.cpp                                   |   53 +-
 60 files changed, 2833 insertions(+), 61 deletions(-)

New commits:
commit a507d766294a987f851665f5b7947e98e9f840fc
Author: Christian Mollekopf <chrigi_1 at fastmail.fm>
Date:   Thu Aug 28 16:07:49 2014 +0200

    IMAP-Resource: Fixed crash in SessionPool
    
    If we're unlucky enough that we receive a disconnect call before a session
    has been declared ready, we end up declaring the about to be deleted session.
    This then results in a dangling pointer and eventually in a crash.

diff --git a/resources/imap/imapresourcebase.cpp b/resources/imap/imapresourcebase.cpp
index 62d5031..78ad4a1 100644
--- a/resources/imap/imapresourcebase.cpp
+++ b/resources/imap/imapresourcebase.cpp
@@ -321,6 +321,9 @@ void ImapResourceBase::onConnectDone( int errorCode, const QString &errorString
   case SessionPool::NoAvailableSessionError:
     kFatal() << "Shouldn't happen";
     return;
+  case SessionPool::CancelledError:
+    kWarning() << "Session login cancelled";
+    return;
   }
 }
 
diff --git a/resources/imap/sessionpool.cpp b/resources/imap/sessionpool.cpp
index a2e8893..1ce945b 100644
--- a/resources/imap/sessionpool.cpp
+++ b/resources/imap/sessionpool.cpp
@@ -218,6 +218,13 @@ void SessionPool::killSession( KIMAP::Session *session, SessionTermination termi
 
 void SessionPool::declareSessionReady( KIMAP::Session *session )
 {
+  //This can happen if we happen to disconnect while capabilities and namespace are being retrieved,
+  //resulting in us keeping a dangling pointer to a deleted session
+  if (!m_connectingPool.contains( session )) {
+    kWarning() << "Tried to declare a removed session ready";
+    return;
+  }
+
   m_pendingInitialSession = 0;
 
   if ( !m_initialConnectDone ) {
@@ -375,6 +382,11 @@ void SessionPool::onPasswordRequestDone( int resultType, const QString &password
 void SessionPool::onLoginDone( KJob *job )
 {
   KIMAP::LoginJob *login = static_cast<KIMAP::LoginJob*>( job );
+  //Can happen if we disonnected meanwhile
+  if (!m_connectingPool.contains(login->session())) {
+    emit connectDone( CancelledError, i18n( "Disconnected during login.") );
+    return;
+  }
 
   if ( job->error() == 0 ) {
     if ( m_initialConnectDone ) {
@@ -411,6 +423,11 @@ void SessionPool::onLoginDone( KJob *job )
 void SessionPool::onCapabilitiesTestDone( KJob *job )
 {
   KIMAP::CapabilitiesJob *capJob = qobject_cast<KIMAP::CapabilitiesJob*>( job );
+  //Can happen if we disonnected meanwhile
+  if (!m_connectingPool.contains(capJob->session())) {
+    emit connectDone( CancelledError, i18n( "Disconnected during login.") );
+    return;
+  }
 
   if ( job->error() ) {
     if ( m_account ) {
@@ -465,6 +482,11 @@ void SessionPool::onCapabilitiesTestDone( KJob *job )
 void SessionPool::onNamespacesTestDone( KJob *job )
 {
   KIMAP::NamespaceJob *nsJob = qobject_cast<KIMAP::NamespaceJob*>( job );
+  //Can happen if we disonnected meanwhile
+  if (!m_connectingPool.contains(nsJob->session())) {
+    emit connectDone( CancelledError, i18n( "Disconnected during login.") );
+    return;
+  }
   m_personalNamespaces = nsJob->personalNamespaces();
   m_userNamespaces = nsJob->userNamespaces();
   m_sharedNamespaces = nsJob->sharedNamespaces();
diff --git a/resources/imap/sessionpool.h b/resources/imap/sessionpool.h
index 3393c05..6612b9d 100644
--- a/resources/imap/sessionpool.h
+++ b/resources/imap/sessionpool.h
@@ -52,7 +52,8 @@ public:
     CapabilitiesTestError,
     IncompatibleServerError,
     NoAvailableSessionError,
-    CouldNotConnectError
+    CouldNotConnectError,
+    CancelledError
   };
 
   enum SessionTermination {
diff --git a/resources/imap/tests/testsessionpool.cpp b/resources/imap/tests/testsessionpool.cpp
index fdca455..d4201e0 100644
--- a/resources/imap/tests/testsessionpool.cpp
+++ b/resources/imap/tests/testsessionpool.cpp
@@ -461,13 +461,45 @@ private slots:
     server.quit();
   }
 
+  void shouldCleanupOnClosingDuringLogin_data()
+  {
+    QTest::addColumn< QList<QByteArray> >( "scenario" );
+
+    {
+        QList<QByteArray> scenario;
+        scenario << FakeServer::greeting()
+                << "C: A000001 LOGIN \"test at kdab.com\" \"foobar\"";
+
+        QTest::newRow( "during login" ) << scenario;
+    }
+    {
+        QList<QByteArray> scenario;
+        scenario << FakeServer::greeting()
+                << "C: A000001 LOGIN \"test at kdab.com\" \"foobar\""
+                << "S: A000001 OK User Logged in"
+                << "C: A000002 CAPABILITY";
+
+        QTest::newRow( "during capability" ) << scenario;
+    }
+    {
+        QList<QByteArray> scenario;
+        scenario << FakeServer::greeting()
+                << "C: A000001 LOGIN \"test at kdab.com\" \"foobar\""
+                << "S: A000001 OK User Logged in"
+                << "C: A000002 CAPABILITY"
+                << "S: * CAPABILITY IMAP4 IMAP4rev1 NAMESPACE UIDPLUS IDLE"
+                << "S: A000002 OK Completed"
+                << "C: A000003 NAMESPACE";
+        QTest::newRow( "during namespace" ) << scenario;
+    }
+  }
+
   void shouldCleanupOnClosingDuringLogin()
   {
+    QFETCH( QList<QByteArray>, scenario );
+
     FakeServer server;
-    server.addScenario( QList<QByteArray>()
-                        << FakeServer::greeting()
-                        << "C: A000001 LOGIN \"test at kdab.com\" \"foobar\""
-    );
+    server.addScenario(scenario);
 
     server.startAndWait();
 
@@ -493,7 +525,7 @@ private slots:
 
     QTest::qWait( 100 );
     QCOMPARE( connectSpy.count(), 1 ); // We're informed that connect failed
-    QCOMPARE( connectSpy.at( 0 ).at( 0 ).toInt(), int(SessionPool::CouldNotConnectError) );
+    QCOMPARE( connectSpy.at( 0 ).at( 0 ).toInt(), int(SessionPool::CancelledError) );
     QCOMPARE( lostSpy.count(), 0 ); // We're not supposed to know the session pointer, so no connectionLost emitted
 
     // Make the session->deleteLater work, it can't happen in qWait (nested event loop)


commit f75803dd945f6f4fe327770cd294fca8f61a2e4c
Author: Christian Mollekopf <chrigi_1 at fastmail.fm>
Date:   Wed Aug 27 15:50:54 2014 +0200

    ImapAclAttribute: Fixed myrights.

diff --git a/resources/shared/imapaclattribute.cpp b/resources/shared/imapaclattribute.cpp
index cd64f14..86014e1 100644
--- a/resources/shared/imapaclattribute.cpp
+++ b/resources/shared/imapaclattribute.cpp
@@ -138,7 +138,7 @@ void ImapAclAttribute::deserialize( const QByteArray &data )
       parts << data.mid(lastPos, pos-lastPos);
       lastPos = pos + 4;
   }
-  parts << data.right(lastPos + 4);
+  parts << data.mid(lastPos);
 
   if (parts.size() < 2) {
       return;
diff --git a/resources/shared/tests/imapaclattributetest.cpp b/resources/shared/tests/imapaclattributetest.cpp
index 838360e..71c03ba 100644
--- a/resources/shared/tests/imapaclattributetest.cpp
+++ b/resources/shared/tests/imapaclattributetest.cpp
@@ -26,6 +26,7 @@ using namespace Akonadi;
 typedef QMap<QByteArray, KIMAP::Acl::Rights> ImapAcl;
 
 Q_DECLARE_METATYPE( ImapAcl )
+Q_DECLARE_METATYPE( KIMAP::Acl::Rights )
 
 class ImapAclAttributeTest : public QObject
 {
@@ -35,32 +36,56 @@ class ImapAclAttributeTest : public QObject
     void testDeserialize_data()
     {
       QTest::addColumn<ImapAcl>( "rights" );
+      QTest::addColumn<KIMAP::Acl::Rights>( "myRights" );
       QTest::addColumn<QByteArray>( "serialized" );
 
-      ImapAcl acl;
-      QTest::newRow( "empty" ) << acl << QByteArray( " %% " );
-
-      acl.insert( "user at host", KIMAP::Acl::None );
-      QTest::newRow( "none" ) << acl << QByteArray( "user at host  %% " );
-
-      acl.insert( "user at host", KIMAP::Acl::Lookup );
-      QTest::newRow( "lookup" ) << acl << QByteArray( "user at host l %% " );
-
-      acl.insert( "user at host", KIMAP::Acl::Lookup | KIMAP::Acl::Read );
-      QTest::newRow( "lookup/read" ) << acl << QByteArray( "user at host lr %% " );
-
-      acl.insert( "otheruser at host", KIMAP::Acl::Lookup | KIMAP::Acl::Read );
-      QTest::newRow( "lookup/read" ) << acl << QByteArray( "otheruser at host lr % user at host lr %% " );
+      KIMAP::Acl::Rights rights = KIMAP::Acl::None;
+
+      {
+        ImapAcl acl;
+        QTest::newRow( "empty" ) << acl << KIMAP::Acl::Rights(KIMAP::Acl::None) << QByteArray( " %% " );
+      }
+
+      {
+        ImapAcl acl;
+        acl.insert( "user at host", rights );
+        QTest::newRow( "none" ) << acl << KIMAP::Acl::Rights(KIMAP::Acl::None) << QByteArray( "user at host  %% " );
+      }
+
+      {
+        ImapAcl acl;
+        acl.insert( "user at host", KIMAP::Acl::Lookup );
+        QTest::newRow( "lookup" ) << acl << KIMAP::Acl::Rights(KIMAP::Acl::None) << QByteArray( "user at host l %% " );
+      }
+
+      {
+        ImapAcl acl;
+        acl.insert( "user at host", KIMAP::Acl::Lookup | KIMAP::Acl::Read );
+        QTest::newRow( "lookup/read" ) << acl << KIMAP::Acl::Rights(KIMAP::Acl::None) << QByteArray( "user at host lr %% " );
+      }
+
+      {
+        ImapAcl acl;
+        acl.insert( "user at host", KIMAP::Acl::Lookup | KIMAP::Acl::Read );
+        acl.insert( "otheruser at host", KIMAP::Acl::Lookup | KIMAP::Acl::Read );
+        QTest::newRow( "lookup/read" ) << acl << KIMAP::Acl::Rights(KIMAP::Acl::None) << QByteArray( "otheruser at host lr % user at host lr %% " );
+      }
+
+      {
+        QTest::newRow( "myrights" ) << ImapAcl() << KIMAP::Acl::rightsFromString("lrswipckxtdaen") << QByteArray( " %%  %% lrswipckxtdaen" );
+      }
     }
 
     void testDeserialize()
     {
       QFETCH( ImapAcl, rights );
+      QFETCH( KIMAP::Acl::Rights, myRights );
       QFETCH( QByteArray, serialized );
 
       ImapAclAttribute deserializeAttr;
       deserializeAttr.deserialize( serialized );
       QCOMPARE( deserializeAttr.rights(), rights );
+      QCOMPARE( deserializeAttr.myRights(), myRights );
     }
 
     void testSerializeDeserialize_data()


commit 926c7910624f61c27bbee36e849231ce71d8be75
Author: Christian Mollekopf <chrigi_1 at fastmail.fm>
Date:   Tue Aug 26 17:14:00 2014 +0200

    IMAP-Resource: Detect destroyed sessions.

diff --git a/resources/imap/sessionpool.cpp b/resources/imap/sessionpool.cpp
index 8208599..a2e8893 100644
--- a/resources/imap/sessionpool.cpp
+++ b/resources/imap/sessionpool.cpp
@@ -352,6 +352,7 @@ void SessionPool::onPasswordRequestDone( int resultType, const QString &password
     session = m_pendingInitialSession;
   } else {
     session = new KIMAP::Session( m_account->server(), m_account->port(), this );
+    QObject::connect(session, SIGNAL(destroyed(QObject*)), this, SLOT(onSessionDestroyed(QObject*)));
     session->setUiProxy( m_sessionUiProxy );
     session->setTimeout( m_account->timeout() );
     m_connectingPool << session;
@@ -519,3 +520,16 @@ void SessionPool::onConnectionLost()
       m_pendingInitialSession = 0;
 }
 
+void SessionPool::onSessionDestroyed(QObject *object)
+{
+  //Safety net for bugs that cause dangling session pointers
+  KIMAP::Session *session = static_cast<KIMAP::Session*>(object);
+  if (m_unusedPool.contains(session) || m_reservedPool.contains(session) || m_connectingPool.contains(session)) {
+    kWarning() << "Session destroyed while still in pool" << session;
+    m_unusedPool.removeAll(session);
+    m_reservedPool.removeAll(session);
+    m_connectingPool.removeAll(session);
+    Q_ASSERT(false);
+  }
+}
+
diff --git a/resources/imap/sessionpool.h b/resources/imap/sessionpool.h
index 2af60b6..3393c05 100644
--- a/resources/imap/sessionpool.h
+++ b/resources/imap/sessionpool.h
@@ -105,6 +105,7 @@ private slots:
   void onNamespacesTestDone( KJob *job );
 
   void onSessionStateChanged(KIMAP::Session::State newState, KIMAP::Session::State oldState);
+  void onSessionDestroyed(QObject*);
 
 private:
   void onConnectionLost();


commit 28736dc2a59a704adae5688885a1c300d2b5e48a
Author: Christian Mollekopf <chrigi_1 at fastmail.fm>
Date:   Tue Aug 26 16:36:20 2014 +0200

    IMAP-Resource: Detect invalid UIDNEXT values and fetch everything
    instead.
    
    This can in particular happen if the server doesn't deliver UIDNEXT at all.
    
    Instead of failing we simply refetch everything if UIDNEXT is missing.

diff --git a/resources/imap/batchfetcher.cpp b/resources/imap/batchfetcher.cpp
index d250326..8aa2b4d 100644
--- a/resources/imap/batchfetcher.cpp
+++ b/resources/imap/batchfetcher.cpp
@@ -110,6 +110,12 @@ void BatchFetcher::fetchNextBatch()
 
         //Take a chunk from the set
         Q_FOREACH (const KIMAP::ImapInterval &interval, m_currentSet.intervals()) {
+            if (!interval.hasDefinedEnd()) {
+                //If we get an interval without a defined end we simply fetch everything
+                toFetch.add(interval);
+                newSet = KIMAP::ImapSet();
+                break;
+            }
             const qint64 wantedItems = m_batchSize - counter;
             if (counter < m_batchSize) {
                 if (interval.size() <= wantedItems) {
diff --git a/resources/imap/retrieveitemstask.cpp b/resources/imap/retrieveitemstask.cpp
index a90fc18..9b111ba 100644
--- a/resources/imap/retrieveitemstask.cpp
+++ b/resources/imap/retrieveitemstask.cpp
@@ -229,10 +229,15 @@ void RetrieveItemsTask::onFinalSelectDone(KJob *job)
     const QString mailBox = select->mailBox();
     const int messageCount = select->messageCount();
     const qint64 uidValidity = select->uidValidity();
-    const qint64 nextUid = select->nextUid();
+    qint64 nextUid = select->nextUid();
     quint64 highestModSeq = select->highestModSequence();
     const QList<QByteArray> flags = select->permanentFlags();
 
+    if (nextUid < 0) {
+        kWarning() << "Server bug: Your IMAP Server delivered an invalid UIDNEXT value";
+        nextUid = 0;
+    }
+
     //The select job retrieves highestmodseq whenever it's available, but in case of no CONDSTORE support we ignore it
     if (!serverSupportsCondstore()) {
         highestModSeq = 0;
@@ -259,16 +264,15 @@ void RetrieveItemsTask::onFinalSelectDone(KJob *job)
 
     // Get the current uid next value and store it
     int oldNextUid = 0;
-    if (!col.hasAttribute("uidnext")) {
-        UidNextAttribute* currentNextUid  = new UidNextAttribute(nextUid);
-        col.addAttribute(currentNextUid);
-        modifyNeeded = true;
-    } else {
-        UidNextAttribute* currentNextUid =
-        static_cast<UidNextAttribute*>(col.attribute("uidnext"));
-        oldNextUid = currentNextUid->uidNext();
-        if (oldNextUid != nextUid) {
-            currentNextUid->setUidNext(nextUid);
+    if (nextUid > 0) { //this can happen with faulty servers that don't deliver uidnext
+        if (UidNextAttribute* currentNextUid = col.attribute<UidNextAttribute>()) {
+            oldNextUid = currentNextUid->uidNext();
+            if (oldNextUid != nextUid) {
+                currentNextUid->setUidNext(nextUid);
+                modifyNeeded = true;
+            }
+        } else {
+            col.attribute<UidNextAttribute>(Akonadi::Collection::AddIfMissing)->setUidNext(nextUid);
             modifyNeeded = true;
         }
     }
@@ -358,17 +362,17 @@ void RetrieveItemsTask::onFinalSelectDone(KJob *job)
             kDebug( 5327 ) << "No messages present so we are done";
         }
         taskComplete();
-    } else if (oldUidValidity != uidValidity) {
+    } else if (oldUidValidity != uidValidity || nextUid <= 0) {
         //If uidvalidity has changed our local cache is worthless and has to be refetched completely
-        if (oldUidValidity != 0) {
-            kDebug( 5327 ) << "UIDVALIDITY check failed (" << oldUidValidity << "|"
-                            << uidValidity << ") refetching " << mailBox;
-        } else {
-            kDebug( 5327 ) << "Fetching complete mailbox " << mailBox;
+        if (oldUidValidity != 0 && oldUidValidity != uidValidity) {
+            kDebug(5327) << "UIDVALIDITY check failed (" << oldUidValidity << "|" << uidValidity << ")";
         }
-
+        if (nextUid <= 0) {
+            kDebug(5327) << "Invalid UIDNEXT";
+        }
+        kDebug(5327) << "Fetching complete mailbox " << mailBox;
         setTotalItems(messageCount);
-        retrieveItems(KIMAP::ImapSet(1, nextUid), scope, false, true);
+        retrieveItems(KIMAP::ImapSet(1, 0), scope, false, true);
     } else if (!m_messageUidsMissingBody.isEmpty()) {
         //fetch missing uids
         m_fetchedMissingBodies = 0;
@@ -393,7 +397,7 @@ void RetrieveItemsTask::onFinalSelectDone(KJob *job)
         kWarning() << "Detected inconsistency in local cache, we're missing some messages. Server: " << messageCount << " Local: "<< realMessageCount;
         kWarning() << "Refetching complete mailbox.";
         setTotalItems(messageCount);
-        retrieveItems(KIMAP::ImapSet(1, nextUid), scope, false, true);
+        retrieveItems(KIMAP::ImapSet(1, 0), scope, false, true);
     } else if (nextUid > oldNextUid) {
         //New messages are available. Fetch new messages, and then check for changed flags and removed messages
         kDebug( 5327 ) << "Fetching new messages: UidNext: " << nextUid << " Old UidNext: " << oldNextUid;
diff --git a/resources/imap/tests/testretrieveitemstask.cpp b/resources/imap/tests/testretrieveitemstask.cpp
index 3e2bc49..d5092cb 100644
--- a/resources/imap/tests/testretrieveitemstask.cpp
+++ b/resources/imap/tests/testretrieveitemstask.cpp
@@ -407,11 +407,10 @@ private slots:
              << "S: * OK [ UIDVALIDITY 1149151135  ]"
              << "S: * OK [ UIDNEXT 9  ]"
              << "S: A000005 OK select done"
-             << "C: A000006 UID SEARCH UID 1:9"
+             << "C: A000006 UID SEARCH UID 1:*"
              << "S: * SEARCH 1 2 3 4 5 6 7 8 9"
              << "S: A000006 OK search done"
              << "C: A000007 UID FETCH 1:9 (RFC822.SIZE INTERNALDATE BODY.PEEK[] FLAGS UID)"
-             << "S: * 1 FETCH ( FLAGS (\\Seen) UID 2321 )"
              << "S: * 1 FETCH ( FLAGS (\\Seen) UID 2321 INTERNALDATE \"29-Jun-2010 15:26:42 +0200\" "
                 "RFC822.SIZE 75 BODY[] {75}\r\n"
                 "From: Foo <foo at kde.org>\r\n"
@@ -524,6 +523,45 @@ private slots:
     //fetch only changed flags
     QTest::newRow( "remote message deleted" ) << collection << scenario << callNames;
 
+    collection = createCollectionChain(QLatin1String("/INBOX/Foo") );
+    collection.attribute<UidValidityAttribute>(Akonadi::Entity::AddIfMissing)->setUidValidity(1149151135);
+    collection.setCachePolicy( policy );
+    collection.attribute<UidNextAttribute>( Akonadi::Collection::AddIfMissing )->setUidNext( -1 );
+    collection.attribute<HighestModSeqAttribute>( Akonadi::Entity::AddIfMissing )->setHighestModSeq( 123456789 );
+    stats.setCount( 0 );
+    collection.setStatistics( stats );
+    scenario.clear();
+    scenario << defaultPoolConnectionScenario()
+             << "C: A000003 SELECT \"INBOX/Foo\""
+             << "S: A000003 OK select done"
+             << "C: A000004 EXPUNGE"
+             << "S: A000004 OK expunge done"
+             << "C: A000005 SELECT \"INBOX/Foo\""
+             << "S: * FLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen)"
+             << "S: * OK [ PERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen) ]"
+             << "S: * 1 EXISTS"
+             << "S: * 0 RECENT"
+             << "S: * OK [ UIDVALIDITY 1149151135  ]"
+             << "S: A000005 OK select done"
+             << "C: A000006 UID SEARCH UID 1:*"
+             << "S: * SEARCH 1 2 3 4 5 6 7 8 9"
+             << "S: A000006 OK search done"
+             << "C: A000007 UID FETCH 1:9 (RFC822.SIZE INTERNALDATE BODY.PEEK[] FLAGS UID)"
+             << "S: * 1 FETCH ( FLAGS (\\Seen) UID 2321 INTERNALDATE \"29-Jun-2010 15:26:42 +0200\" "
+                "RFC822.SIZE 75 BODY[] {75}\r\n"
+                "From: Foo <foo at kde.org>\r\n"
+                "To: Bar <bar at kde.org>\r\n"
+                "Subject: Test Mail\r\n"
+                "\r\n"
+                "Test\r\n"
+                " )"
+             << "S: A000007 OK fetch done";
+
+    callNames.clear();
+    callNames << "itemsRetrieved" << "applyCollectionChanges" << "itemsRetrievalDone" ;
+
+    QTest::newRow( "missing uidnext" ) << collection << scenario << callNames;
+
   }
 
   void shouldIntrospectCollection()


commit 5a6fc8d51e52dce81bfd0249d1f44b1601aa194e
Author: Christian Mollekopf <chrigi_1 at fastmail.fm>
Date:   Tue Aug 19 10:38:02 2014 +0200

    Tag support for the kolab resource.
    
    Thanks to Kevin Krammer for his help.

diff --git a/resources/imap/CMakeLists.txt b/resources/imap/CMakeLists.txt
index e22a242..20b5634 100644
--- a/resources/imap/CMakeLists.txt
+++ b/resources/imap/CMakeLists.txt
@@ -49,6 +49,7 @@ set( imapresource_LIB_SRCS
   imapidlemanager.cpp
   resourcestate.cpp
   collectionmetadatahelper.cpp
+  replacemessagejob.cpp
   ${AKONADI_COLLECTIONATTRIBUTES_SHARED_SOURCES}
   ${AKONADI_IMAPATTRIBUTES_SHARED_SOURCES}
 )
diff --git a/resources/imap/imapresourcebase.h b/resources/imap/imapresourcebase.h
index 0ab176a..5e5979d 100644
--- a/resources/imap/imapresourcebase.h
+++ b/resources/imap/imapresourcebase.h
@@ -48,8 +48,8 @@ class SubscriptionDialog;
 class Settings;
 
 class ImapResourceBase : public Akonadi::ResourceBase,
-                         public Akonadi::AgentBase::ObserverV3,
-                        public Akonadi::AgentSearchInterface
+                         public Akonadi::AgentBase::ObserverV4,
+                         public Akonadi::AgentSearchInterface
 {
   Q_OBJECT
   Q_CLASSINFO("D-Bus Interface", "org.kde.Akonadi.ImapResourceBase")
diff --git a/resources/imap/replacemessagejob.cpp b/resources/imap/replacemessagejob.cpp
new file mode 100644
index 0000000..57e61b1
--- /dev/null
+++ b/resources/imap/replacemessagejob.cpp
@@ -0,0 +1,185 @@
+/*
+    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 "replacemessagejob.h"
+
+#include <KIMAP/AppendJob>
+#include <KIMAP/SearchJob>
+#include <KIMAP/SelectJob>
+#include <KIMAP/StoreJob>
+#include <KIMAP/ImapSet>
+#include <KDebug>
+#include <KMime/Message>
+
+#include "imapflags.h"
+
+ReplaceMessageJob::ReplaceMessageJob(const KMime::Message::Ptr &msg, KIMAP::Session *session, const QString &mailbox, qint64 uidNext, qint64 oldUid, QObject *parent)
+    : KJob(parent),
+    mSession(session),
+    mMessage(msg),
+    mMailbox(mailbox),
+    mUidNext(uidNext),
+    mOldUid(oldUid),
+    mNewUid(-1),
+    mMessageId(msg->messageID()->asUnicodeString().toUtf8())
+{
+}
+
+void ReplaceMessageJob::start()
+{
+    KIMAP::AppendJob *job = new KIMAP::AppendJob(mSession);
+    job->setMailBox(mMailbox);
+    job->setContent(mMessage->encodedContent(true));
+    job->setInternalDate(mMessage->date()->dateTime());
+    connect(job, SIGNAL(result(KJob*)), SLOT(onAppendMessageDone(KJob*)));
+    job->start();
+}
+
+void ReplaceMessageJob::onAppendMessageDone(KJob *job)
+{
+    KIMAP::AppendJob *append = qobject_cast<KIMAP::AppendJob*>(job);
+
+    if (append->error()) {
+        kWarning() << append->errorString();
+        setError(KJob::UserDefinedError);
+        emitResult();
+        return;
+    }
+
+    // We get it directly if UIDPLUS is supported...
+    mNewUid = append->uid();
+
+    if (mNewUid > 0 && mOldUid <= 0) {
+        //We have the uid an no message to delete, we're done
+        emitResult();
+        return;
+    }
+
+    if (mSession->selectedMailBox() != mMailbox) {
+        //For search and delete we need to select the right mailbox first
+        KIMAP::SelectJob *select = new KIMAP::SelectJob(mSession);
+        select->setMailBox(mMailbox);
+        connect(select, SIGNAL(result(KJob*)), this, SLOT(onSelectDone(KJob*)));
+        select->start();
+    } else {
+        if (mNewUid > 0) {
+            triggerDeleteJobIfNecessary();
+        } else {
+            triggerSearchJob();
+        }
+    }
+}
+
+void ReplaceMessageJob::onSelectDone(KJob *job)
+{
+    if (job->error()) {
+        kWarning() << job->errorString();
+        setError(KJob::UserDefinedError);
+        emitResult();
+    } else {
+        if (mNewUid > 0) {
+            triggerDeleteJobIfNecessary();
+        } else {
+            triggerSearchJob();
+        }
+    }
+}
+
+void ReplaceMessageJob::triggerSearchJob()
+{
+    KIMAP::SearchJob *search = new KIMAP::SearchJob(mSession);
+
+    search->setUidBased(true);
+    search->setSearchLogic(KIMAP::SearchJob::And);
+
+    if (!mMessageId.isEmpty()) {
+        QByteArray header = "Message-ID ";
+        header += mMessageId;
+
+        search->addSearchCriteria(KIMAP::SearchJob::Header, header);
+    } else {
+        search->addSearchCriteria(KIMAP::SearchJob::New);
+
+        if (mUidNext < 0) {
+            kWarning() << "Could not determine the UID for the newly created message on the server";
+            search->deleteLater();
+            setError(KJob::UserDefinedError);
+            emitResult();
+            return;
+        }
+        search->addSearchCriteria(KIMAP::SearchJob::Uid, KIMAP::ImapInterval(mUidNext).toImapSequence());
+    }
+
+    connect(search, SIGNAL(result(KJob*)),
+            this, SLOT(onSearchDone(KJob*)));
+
+    search->start();
+}
+
+void ReplaceMessageJob::onSearchDone(KJob *job)
+{
+    if (job->error()) {
+        kWarning() << job->errorString();
+        setError(KJob::UserDefinedError);
+        emitResult();
+        return;
+    }
+
+    KIMAP::SearchJob *search = static_cast<KIMAP::SearchJob*>(job);
+
+    if (search->results().count() == 1) {
+        mNewUid = search->results().first();
+    } else {
+        kWarning() << "Failed to find uid for message. Got 0 or too many results: " << search->results().count();
+        setError(KJob::UserDefinedError);
+        emitResult();
+        return;
+    }
+    triggerDeleteJobIfNecessary();
+}
+
+void ReplaceMessageJob::triggerDeleteJobIfNecessary()
+{
+    if (mOldUid <= 0) {
+        //Nothing to do, we're done
+        emitResult();
+    } else {
+        KIMAP::StoreJob *store = new KIMAP::StoreJob(mSession);
+        store->setUidBased(true);
+        store->setSequenceSet(KIMAP::ImapSet(mOldUid));
+        store->setFlags(QList<QByteArray>() << ImapFlags::Deleted);
+        store->setMode(KIMAP::StoreJob::AppendFlags);
+        connect(store, SIGNAL(result(KJob*)), this, SLOT(onDeleteDone(KJob*)));
+        store->start();
+    }
+}
+
+void ReplaceMessageJob::onDeleteDone(KJob *job)
+{
+    if (job->error()) {
+        kWarning() << job->errorString();
+    }
+    emitResult();
+}
+
+
+qint64 ReplaceMessageJob::newUid() const
+{
+    return mNewUid;
+}
diff --git a/resources/imap/replacemessagejob.h b/resources/imap/replacemessagejob.h
new file mode 100644
index 0000000..d9ebc12
--- /dev/null
+++ b/resources/imap/replacemessagejob.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 REPLACEMESSAGEJOB_H
+#define REPLACEMESSAGEJOB_H
+
+#include <KJob>
+#include <KMime/Message>
+#include <KIMAP/Session>
+
+/**
+ * This job appends a message, marks the old one as deleted, and returns the uid of the appended message.
+ */
+class ReplaceMessageJob : public KJob
+{
+    Q_OBJECT
+public:
+    ReplaceMessageJob(const KMime::Message::Ptr &msg, KIMAP::Session *session, const QString &mailbox, qint64 uidNext = -1, qint64 oldUid = -1, QObject *parent = 0);
+
+    qint64 newUid() const;
+
+    void start();
+
+private:
+    void triggerSearchJob();
+    void triggerDeleteJobIfNecessary();
+
+private Q_SLOTS:
+    void onAppendMessageDone(KJob *job);
+    void onSelectDone(KJob *job);
+    void onSearchDone(KJob *job);
+    void onDeleteDone(KJob *job);
+
+private:
+    KIMAP::Session *mSession;
+    const KMime::Message::Ptr mMessage;
+    const QString mMailbox;
+    qint64 mUidNext;
+    qint64 mOldUid;
+    qint64 mNewUid;
+    const QByteArray mMessageId;
+};
+
+#endif
+
diff --git a/resources/imap/resourcestate.cpp b/resources/imap/resourcestate.cpp
index fbff0ce..ad6172e 100644
--- a/resources/imap/resourcestate.cpp
+++ b/resources/imap/resourcestate.cpp
@@ -53,6 +53,11 @@ QString ResourceState::resourceName() const
   return m_resource->name();
 }
 
+QString ResourceState::resourceIdentifier() const
+{
+  return m_resource->identifier();
+}
+
 QStringList ResourceState::serverCapabilities() const
 {
   return m_resource->m_pool->serverCapabilities();
@@ -150,6 +155,21 @@ QSet<QByteArray> ResourceState::removedFlags() const
   return m_arguments.removedFlags;
 }
 
+Akonadi::Tag ResourceState::tag() const
+{
+    return m_arguments.tag;
+}
+
+QSet<Akonadi::Tag> ResourceState::addedTags() const
+{
+    return m_arguments.addedTags;
+}
+
+QSet<Akonadi::Tag> ResourceState::removedTags() const
+{
+    return m_arguments.removedTags;
+}
+
 QString ResourceState::rootRemoteId() const
 {
   return m_resource->settings()->rootRemoteId();
@@ -254,6 +274,11 @@ void ResourceState::collectionChangeCommitted( const Akonadi::Collection &collec
   m_resource->changeCommitted( collection );
 }
 
+void ResourceState::tagChangeCommitted(const Akonadi::Tag &tag)
+{
+  m_resource->changeCommitted( tag );
+}
+
 void ResourceState::changeProcessed()
 {
   m_resource->changeProcessed();
@@ -386,3 +411,8 @@ MessageHelper::Ptr ResourceState::messageHelper() const
 {
   return MessageHelper::Ptr(new MessageHelper());
 }
+
+void ResourceState::tagsRetrieved( const Akonadi::Tag::List &tags, const QHash<QString, Akonadi::Item::List> &tagMembers )
+{
+  m_resource->tagsRetrieved(tags, tagMembers);
+}
diff --git a/resources/imap/resourcestate.h b/resources/imap/resourcestate.h
index f6026bb..e615f52 100644
--- a/resources/imap/resourcestate.h
+++ b/resources/imap/resourcestate.h
@@ -35,18 +35,23 @@ struct TaskArguments {
     TaskArguments(const Akonadi::Item::List &_items): items(_items) {}
     TaskArguments(const Akonadi::Item::List &_items, const QSet<QByteArray> &_addedFlags, const QSet<QByteArray> &_removedFlags): items(_items), addedFlags(_addedFlags), removedFlags(_removedFlags) {}
     TaskArguments(const Akonadi::Item::List &_items, const Akonadi::Collection &_sourceCollection, const Akonadi::Collection &_targetCollection): items(_items), sourceCollection(_sourceCollection), targetCollection(_targetCollection){}
+    TaskArguments(const Akonadi::Item::List &_items, const QSet<Akonadi::Tag> &_addedTags, const QSet<Akonadi::Tag> &_removedTags): items(_items), addedTags(_addedTags), removedTags(_removedTags) {}
     TaskArguments(const Akonadi::Collection &_collection): collection(_collection){}
     TaskArguments(const Akonadi::Collection &_collection, const Akonadi::Collection &_parentCollection): collection(_collection), parentCollection(_parentCollection){}
     TaskArguments(const Akonadi::Collection &_collection, const Akonadi::Collection &_sourceCollection, const Akonadi::Collection &_targetCollection): collection(_collection), sourceCollection(_sourceCollection), targetCollection(_targetCollection){}
     TaskArguments(const Akonadi::Collection &_collection, const QSet<QByteArray> &_parts): collection(_collection), parts(_parts){}
+    TaskArguments(const Akonadi::Tag &_tag) : tag(_tag) {}
     Akonadi::Collection collection;
     Akonadi::Item::List items;
     Akonadi::Collection parentCollection; //only used as parent of a collection
     Akonadi::Collection sourceCollection;
     Akonadi::Collection targetCollection;
+    Akonadi::Tag tag;
     QSet<QByteArray> parts;
     QSet<QByteArray> addedFlags;
     QSet<QByteArray> removedFlags;
+    QSet<Akonadi::Tag> addedTags;
+    QSet<Akonadi::Tag> removedTags;
 };
 
 class ResourceState : public ResourceStateInterface
@@ -59,6 +64,7 @@ public:
 
   virtual QString userName() const;
   virtual QString resourceName() const;
+  virtual QString resourceIdentifier() const;
   virtual QStringList serverCapabilities() const;
   virtual QList<KIMAP::MailBoxDescriptor> serverNamespaces() const;
   virtual QList<KIMAP::MailBoxDescriptor> personalNamespaces() const;
@@ -83,6 +89,10 @@ public:
   virtual QSet<QByteArray> addedFlags() const;
   virtual QSet<QByteArray> removedFlags() const;
 
+  virtual Akonadi::Tag tag() const;
+  virtual QSet<Akonadi::Tag> addedTags() const;
+  virtual QSet<Akonadi::Tag> removedTags() const;
+
   virtual QString rootRemoteId() const;
 
   virtual void setIdleCollection( const Akonadi::Collection &collection );
@@ -103,8 +113,12 @@ public:
 
   virtual void collectionsRetrieved( const Akonadi::Collection::List &collections );
 
+  virtual void tagsRetrieved( const Akonadi::Tag::List &tags, const QHash<QString, Akonadi::Item::List> & );
+
   virtual void collectionChangeCommitted( const Akonadi::Collection &collection );
 
+  virtual void tagChangeCommitted( const Akonadi::Tag &tag );
+
   virtual void changeProcessed();
 
   virtual void searchFinished( const QVector<qint64> &result, bool isRid = true );
diff --git a/resources/imap/resourcestateinterface.h b/resources/imap/resourcestateinterface.h
index d61af67..2ead8b6 100644
--- a/resources/imap/resourcestateinterface.h
+++ b/resources/imap/resourcestateinterface.h
@@ -41,6 +41,7 @@ public:
 
   virtual QString userName() const = 0;
   virtual QString resourceName() const = 0;
+  virtual QString resourceIdentifier() const = 0;
   virtual QStringList serverCapabilities() const = 0;
   virtual QList<KIMAP::MailBoxDescriptor> serverNamespaces() const = 0;
   virtual QList<KIMAP::MailBoxDescriptor> personalNamespaces() const = 0;
@@ -65,6 +66,10 @@ public:
   virtual QSet<QByteArray> addedFlags() const = 0;
   virtual QSet<QByteArray> removedFlags() const = 0;
 
+  virtual Akonadi::Tag tag() const = 0;
+  virtual QSet<Akonadi::Tag> addedTags() const = 0;
+  virtual QSet<Akonadi::Tag> removedTags() const = 0;
+
   virtual QString rootRemoteId() const = 0;
   static QString mailBoxForCollection( const Akonadi::Collection &collection, bool showWarnings = true );
 
@@ -88,6 +93,8 @@ public:
 
   virtual void collectionChangeCommitted( const Akonadi::Collection &collection ) = 0;
 
+  virtual void tagChangeCommitted( const Akonadi::Tag &tag ) = 0;
+
   virtual void changeProcessed() = 0;
 
   virtual void searchFinished( const QVector<qint64> &result, bool isRid = true ) = 0;
@@ -112,6 +119,7 @@ public:
   virtual int batchSize() const = 0;
 
   virtual MessageHelper::Ptr messageHelper() const = 0;
+  virtual void tagsRetrieved( const Akonadi::Tag::List &tags, const QHash<QString, Akonadi::Item::List> & ) = 0;
 
 };
 
diff --git a/resources/imap/resourcetask.cpp b/resources/imap/resourcetask.cpp
index d887cae..5df0ab9 100644
--- a/resources/imap/resourcetask.cpp
+++ b/resources/imap/resourcetask.cpp
@@ -321,6 +321,12 @@ void ResourceTask::changeCommitted( const Akonadi::Collection &collection )
   deleteLater();
 }
 
+void ResourceTask::changeCommitted( const Akonadi::Tag &tag )
+{
+    m_resource->tagChangeCommitted( tag );
+    deleteLater();
+}
+
 void ResourceTask::changeProcessed()
 {
   m_resource->changeProcessed();
diff --git a/resources/imap/resourcetask.h b/resources/imap/resourcetask.h
index 72280c8..4c47358 100644
--- a/resources/imap/resourcetask.h
+++ b/resources/imap/resourcetask.h
@@ -115,6 +115,8 @@ protected:
 
   void changeCommitted( const Akonadi::Collection &collection );
 
+  void changeCommitted( const Akonadi::Tag &tag );
+
   void changeProcessed();
 
   void searchFinished( const QVector<qint64> &result, bool isRid = true );
diff --git a/resources/imap/tests/dummyresourcestate.cpp b/resources/imap/tests/dummyresourcestate.cpp
index 12a656a..6dec2ec 100644
--- a/resources/imap/tests/dummyresourcestate.cpp
+++ b/resources/imap/tests/dummyresourcestate.cpp
@@ -20,9 +20,10 @@
 */
 
 #include "dummyresourcestate.h"
-
 Q_DECLARE_METATYPE(QList<qint64>)
 Q_DECLARE_METATYPE(QVector<qint64>);
+Q_DECLARE_METATYPE(QString);
+Q_DECLARE_METATYPE(TagListAndMembers);
 
 DummyResourceState::DummyResourceState()
   : m_automaticExpunge( true ), m_subscriptionEnabled( true ),
@@ -30,6 +31,7 @@ DummyResourceState::DummyResourceState()
 {
   qRegisterMetaType<QList<qint64> >();
   qRegisterMetaType<QVector<qint64> >();
+  qRegisterMetaType<TagListAndMembers>();
 }
 
 DummyResourceState::~DummyResourceState()
@@ -57,6 +59,16 @@ QString DummyResourceState::resourceName() const
   return m_resourceName;
 }
 
+void DummyResourceState::setResourceIdentifier(const QString &identifier)
+{
+  m_resourceIdentifier = identifier;
+}
+
+QString DummyResourceState::resourceIdentifier() const
+{
+  return m_resourceIdentifier;
+}
+
 void DummyResourceState::setServerCapabilities( const QStringList &capabilities )
 {
   m_capabilities = capabilities;
@@ -195,7 +207,37 @@ void DummyResourceState::setParts( const QSet<QByteArray> &parts )
 
 QSet<QByteArray> DummyResourceState::parts() const
 {
-  return m_parts;
+    return m_parts;
+}
+
+void DummyResourceState::setTag(const Akonadi::Tag &tag)
+{
+    m_tag = tag;
+}
+
+Akonadi::Tag DummyResourceState::tag() const
+{
+    return m_tag;
+}
+
+void DummyResourceState::setAddedTags(const QSet<Akonadi::Tag> &addedTags)
+{
+    m_addedTags = addedTags;
+}
+
+QSet<Akonadi::Tag> DummyResourceState::addedTags() const
+{
+    return m_addedTags;
+}
+
+void DummyResourceState::setRemovedTags(const QSet<Akonadi::Tag> &removedTags)
+{
+    m_removedTags = removedTags;
+}
+
+QSet<Akonadi::Tag> DummyResourceState::removedTags() const
+{
+    return m_removedTags;
 }
 
 QString DummyResourceState::rootRemoteId() const
@@ -272,7 +314,17 @@ void DummyResourceState::collectionsRetrieved( const Akonadi::Collection::List &
 
 void DummyResourceState::collectionChangeCommitted( const Akonadi::Collection &collection )
 {
-  recordCall( "collectionChangeCommitted", QVariant::fromValue( collection ) );
+    recordCall( "collectionChangeCommitted", QVariant::fromValue( collection ) );
+}
+
+void DummyResourceState::tagsRetrieved( const Akonadi::Tag::List &tags, const QHash<QString, Akonadi::Item::List> &items )
+{
+  recordCall( "tagsRetrieved",  QVariant::fromValue( qMakePair(tags, items) ) );
+}
+
+void DummyResourceState::tagChangeCommitted(const Akonadi::Tag &tag)
+{
+  recordCall( "tagChangeCommitted", QVariant::fromValue( tag ) );
 }
 
 void DummyResourceState::changeProcessed()
diff --git a/resources/imap/tests/dummyresourcestate.h b/resources/imap/tests/dummyresourcestate.h
index c12f8d5..8ef77d6 100644
--- a/resources/imap/tests/dummyresourcestate.h
+++ b/resources/imap/tests/dummyresourcestate.h
@@ -27,6 +27,8 @@
 
 #include "resourcestateinterface.h"
 
+typedef QPair<Akonadi::Tag::List, QHash<QString, Akonadi::Item::List> > TagListAndMembers;
+
 class DummyResourceState : public ResourceStateInterface
 {
 public:
@@ -41,6 +43,9 @@ public:
   void setResourceName( const QString &name );
   virtual QString resourceName() const;
 
+  void setResourceIdentifier( const QString &identifier );
+  virtual QString resourceIdentifier() const;
+
   void setServerCapabilities( const QStringList &capabilities );
   virtual QStringList serverCapabilities() const;
 
@@ -78,6 +83,13 @@ public:
   void setParts( const QSet<QByteArray> &parts );
   virtual QSet<QByteArray> parts() const;
 
+  void setTag( const Akonadi::Tag &tag );
+  virtual Akonadi::Tag tag() const;
+  void setAddedTags( const QSet<Akonadi::Tag> &addedTags );
+  virtual QSet<Akonadi::Tag> addedTags() const;
+  void setRemovedTags( const QSet<Akonadi::Tag> &removedTags );
+  virtual QSet<Akonadi::Tag> removedTags() const;
+
   virtual QString rootRemoteId() const;
 
   virtual void setIdleCollection( const Akonadi::Collection &collection );
@@ -103,6 +115,9 @@ public:
 
   virtual void collectionChangeCommitted( const Akonadi::Collection &collection );
 
+  virtual void tagsRetrieved( const Akonadi::Tag::List &tags, const QHash<QString, Akonadi::Item::List> & );
+  virtual void tagChangeCommitted( const Akonadi::Tag &tag );
+
   virtual void searchFinished( const QVector<qint64> &result, bool isRid = true );
 
   virtual void changeProcessed();
@@ -135,6 +150,7 @@ private:
 
   QString m_userName;
   QString m_resourceName;
+  QString m_resourceIdentifier;
   QStringList m_capabilities;
   QList<KIMAP::MailBoxDescriptor> m_namespaces;
 
@@ -154,6 +170,10 @@ private:
 
   QSet<QByteArray> m_parts;
 
+  Akonadi::Tag m_tag;
+  QSet<Akonadi::Tag> m_addedTags;
+  QSet<Akonadi::Tag> m_removedTags;
+
   QList< QPair<QByteArray, QVariant> > m_calls;
 };
 
diff --git a/resources/kolab/CMakeLists.txt b/resources/kolab/CMakeLists.txt
index a787615..4c410c8 100644
--- a/resources/kolab/CMakeLists.txt
+++ b/resources/kolab/CMakeLists.txt
@@ -7,32 +7,63 @@ include_directories(
     ${Libkolab_INCLUDES}
     ${Libkolabxml_INCLUDES}
 )
-add_definitions( -DQT_NO_CAST_FROM_ASCII )
-add_definitions( -DQT_NO_CAST_TO_ASCII )
+#add_definitions( -DQT_NO_CAST_FROM_ASCII )
+#add_definitions( -DQT_NO_CAST_TO_ASCII )
 
 
 set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${KDE4_ENABLE_EXCEPTIONS}" )
 
 ########### next target ###############
 
+set(kolabresource_LIB_SRCS
+    kolabretrievecollectionstask.cpp
+    kolabhelpers.cpp
+    kolabmessagehelper.cpp
+    kolabaddtagtask.cpp
+    kolabchangeitemstagstask.cpp
+    kolabchangetagtask.cpp
+    kolabrelationresourcetask.cpp
+    kolabremovetagtask.cpp
+    tagchangehelper.cpp
+    kolabretrievetagstask.cpp
+)
+
+kde4_add_kcfg_files(kolabresource_LIB_SRCS ../imap/settingsbase.kcfgc)
+
+kde4_add_library(kolabresource STATIC ${kolabresource_LIB_SRCS})
+target_link_libraries(kolabresource
+    ${KDEPIMLIBS_AKONADI_LIBS}
+    ${QT_QTDBUS_LIBRARY}
+    ${QT_QTCORE_LIBRARY}
+    ${QT_QTGUI_LIBRARY}
+    ${QT_QTNETWORK_LIBRARY}
+    ${KDEPIMLIBS_KIMAP_LIBS}
+    ${KDEPIMLIBS_MAILTRANSPORT_LIBS}
+    ${KDE4_KIO_LIBS}
+    ${KDEPIMLIBS_KMIME_LIBS}
+    ${KDEPIMLIBS_AKONADI_KMIME_LIBS}
+    ${KDEPIMLIBS_KPIMIDENTITIES_LIBS}
+    ${Libkolab_LIBRARIES}
+    ${Libkolabxml_LIBRARIES}
+    ${KDEPIMLIBS_KABC_LIBS}
+    ${KDEPIMLIBS_KCALCORE_LIBS}
+)
+
+
+
 set(kolabresource_SRCS
+    kolabresource.cpp
+    kolabresourcestate.cpp
     ../imap/imapresource.cpp
     ../imap/settingspasswordrequester.cpp
     ../imap/setupserver.cpp
     ../imap/serverinfodialog.cpp
-    kolabretrievecollectionstask.cpp
-    kolabresource.cpp
-    kolabresourcestate.cpp
-    kolabhelpers.cpp
-    kolabmessagehelper.cpp
 )
 
-kde4_add_kcfg_files(kolabresource_SRCS ../imap/settingsbase.kcfgc)
-
 if (KDEPIM_MOBILE_UI)
-kde4_add_ui_files(kolabresource_SRCS ../imap/setupserverview_mobile.ui)
+    kde4_add_ui_files(kolabresource_SRCS ../imap/setupserverview_mobile.ui)
 else ()
-kde4_add_ui_files(kolabresource_SRCS ../imap/setupserverview_desktop.ui)
+    kde4_add_ui_files(kolabresource_SRCS ../imap/setupserverview_desktop.ui)
 endif ()
 kde4_add_ui_files(kolabresource_SRCS ../imap/serverinfo.ui)
 
@@ -50,6 +81,7 @@ target_link_libraries(akonadi_kolab_resource
     ${KDEPIMLIBS_AKONADI_KMIME_LIBS}
     ${KDEPIMLIBS_KPIMIDENTITIES_LIBS}
     imapresource
+    kolabresource
     folderarchivesettings
     ${Libkolab_LIBRARIES}
     ${Libkolabxml_LIBRARIES}
@@ -60,4 +92,5 @@ target_link_libraries(akonadi_kolab_resource
 install(FILES kolabresource.desktop DESTINATION "${CMAKE_INSTALL_PREFIX}/share/akonadi/agents")
 install(TARGETS akonadi_kolab_resource ${INSTALL_TARGETS_DEFAULT_ARGS})
 
-add_subdirectory(wizard)
\ No newline at end of file
+add_subdirectory(wizard)
+add_subdirectory(tests)
diff --git a/resources/kolab/kolabaddtagtask.cpp b/resources/kolab/kolabaddtagtask.cpp
new file mode 100644
index 0000000..d830d3e
--- /dev/null
+++ b/resources/kolab/kolabaddtagtask.cpp
@@ -0,0 +1,193 @@
+/*
+    Copyright (c) 2014 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info at kdab.com>
+    Author: Kevin Krammer <kevin.krammer at kdab.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 "kolabaddtagtask.h"
+
+#include "../imap/uidnextattribute.h"
+
+#include <kolabobject.h>
+
+#include <kimap/appendjob.h>
+#include <kimap/imapset.h>
+#include <kimap/searchjob.h>
+#include <kimap/selectjob.h>
+#include <kimap/session.h>
+
+#include <KDE/KLocalizedString>
+
+#include <QUuid>
+
+KolabAddTagTask::KolabAddTagTask(ResourceStateInterface::Ptr resource, QObject *parent)
+    : KolabRelationResourceTask(resource, parent)
+{
+}
+
+void KolabAddTagTask::startRelationTask(KIMAP::Session *session)
+{
+    kDebug() << "converted tag";
+
+    const QLatin1String productId("Akonadi-Kolab-Resource");
+    const KMime::Message::Ptr message = Kolab::KolabObjectWriter::writeTag(resourceState()->tag(), QStringList(), Kolab::KolabV3, productId);
+    mMessageId = message->messageID()->asUnicodeString().toUtf8();
+
+    KIMAP::AppendJob *job = new KIMAP::AppendJob(session);
+    job->setMailBox(mailBoxForCollection(relationCollection()));
+    job->setContent(message->encodedContent(true));
+    job->setInternalDate(message->date()->dateTime());
+    connect(job, SIGNAL(result(KJob*)), SLOT(onAppendMessageDone(KJob*)));
+    job->start();
+}
+
+void KolabAddTagTask::applyFoundUid(qint64 uid)
+{
+    Akonadi::Tag tag = resourceState()->tag();
+
+    //If we failed to get the remoteid the tag remains local only
+    if (uid > 0) {
+      tag.setRemoteId(QByteArray::number(uid));
+    }
+
+    kDebug() << "comitting new tag";
+    changeCommitted(tag);
+
+    Akonadi::Collection c = relationCollection();
+
+    // Get the current uid next value and store it
+    UidNextAttribute *uidAttr = 0;
+    int oldNextUid = 0;
+    if (c.hasAttribute("uidnext")) {
+      uidAttr = static_cast<UidNextAttribute*>(c.attribute("uidnext"));
+      oldNextUid = uidAttr->uidNext();
+    }
+
+    // If the uid we just got back is the expected next one of the box
+    // then update the property to the probable next uid to keep the cache in sync.
+    // If not something happened in our back, so we don't update and a refetch will
+    // happen at some point.
+    if (uid == oldNextUid) {
+      if (uidAttr == 0) {
+        uidAttr = new UidNextAttribute(uid + 1);
+        c.addAttribute(uidAttr);
+      } else {
+        uidAttr->setUidNext(uid + 1);
+      }
+
+      applyCollectionChanges(c);
+    }
+}
+
+void KolabAddTagTask::triggerSearchJob(KIMAP::Session *session)
+{
+    KIMAP::SearchJob *search = new KIMAP::SearchJob(session);
+
+    search->setUidBased(true);
+    search->setSearchLogic(KIMAP::SearchJob::And);
+
+    if (!mMessageId.isEmpty()) {
+      QByteArray header = "Message-ID ";
+      header += mMessageId;
+
+      search->addSearchCriteria(KIMAP::SearchJob::Header, header);
+    } else {
+      search->addSearchCriteria(KIMAP::SearchJob::New);
+
+      UidNextAttribute *uidNext = relationCollection().attribute<UidNextAttribute>();
+      if (!uidNext) {
+        cancelTask(i18n("Could not determine the UID for the newly created message on the server"));
+        search->deleteLater();
+        return;
+      }
+      KIMAP::ImapInterval interval(uidNext->uidNext());
+
+      search->addSearchCriteria(KIMAP::SearchJob::Uid, interval.toImapSequence());
+    }
+
+    connect(search, SIGNAL(result(KJob*)),
+            this, SLOT(onSearchDone(KJob*)));
+
+    search->start();
+}
+
+void KolabAddTagTask::onAppendMessageDone(KJob *job)
+
+{
+    KIMAP::AppendJob *append = qobject_cast<KIMAP::AppendJob*>(job);
+
+    if (append->error()) {
+      kWarning() << append->errorString();
+      cancelTask(append->errorString());
+      return;
+    }
+
+    qint64 uid = append->uid();
+    kDebug() << "appended message with uid: " << uid;
+
+    if (uid > 0) {
+      // We got it directly if UIDPLUS is supported...
+      applyFoundUid(uid);
+
+    } else {
+      // ... otherwise prepare searching for the message
+      KIMAP::Session *session = append->session();
+      const QString mailBox = append->mailBox();
+
+      if (session->selectedMailBox() != mailBox) {
+        KIMAP::SelectJob *select = new KIMAP::SelectJob(session);
+        select->setMailBox(mailBox);
+
+        connect(select, SIGNAL(result(KJob*)),
+                this, SLOT(onPreSearchSelectDone(KJob*)));
+
+        select->start();
+
+      } else {
+        triggerSearchJob(session);
+      }
+    }
+}
+
+void KolabAddTagTask::onPreSearchSelectDone(KJob *job)
+{
+    if ( job->error() ) {
+      kWarning() << job->errorString();
+      cancelTask(job->errorString());
+    } else {
+      KIMAP::SelectJob *select = static_cast<KIMAP::SelectJob*>(job);
+      triggerSearchJob(select->session());
+    }
+}
+
+void KolabAddTagTask::onSearchDone(KJob *job)
+{
+    if (job->error()) {
+        kWarning() << job->errorString();
+        cancelTask(job->errorString());
+        return;
+    }
+
+    KIMAP::SearchJob *search = static_cast<KIMAP::SearchJob*>(job);
+
+    qint64 uid = 0;
+    if (search->results().count() == 1)
+      uid = search->results().first();
+
+    applyFoundUid(uid);
+}
diff --git a/resources/kolab/kolabaddtagtask.h b/resources/kolab/kolabaddtagtask.h
new file mode 100644
index 0000000..b8ed127
--- /dev/null
+++ b/resources/kolab/kolabaddtagtask.h
@@ -0,0 +1,49 @@
+/*
+    Copyright (c) 2014 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info at kdab.com>
+    Author: Kevin Krammer <kevin.krammer at kdab.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 KOLABADDTAGTASK_H
+#define KOLABADDTAGTASK_H
+
+#include "kolabrelationresourcetask.h"
+
+class KolabAddTagTask : public KolabRelationResourceTask
+{
+    Q_OBJECT
+public:
+    explicit KolabAddTagTask(ResourceStateInterface::Ptr resource, QObject *parent = 0);
+
+protected:
+    virtual void startRelationTask(KIMAP::Session *session);
+
+private:
+    QByteArray mMessageId;
+
+private:
+    void applyFoundUid(qint64 uid);
+    void triggerSearchJob(KIMAP::Session *session);
+
+private slots:
+    void onAppendMessageDone(KJob *job);
+    void onPreSearchSelectDone(KJob *job);
+    void onSearchDone(KJob *job);
+};
+
+#endif // KOLABADDTAGTASK_H
diff --git a/resources/kolab/kolabchangeitemstagstask.cpp b/resources/kolab/kolabchangeitemstagstask.cpp
new file mode 100644
index 0000000..3b47955
--- /dev/null
+++ b/resources/kolab/kolabchangeitemstagstask.cpp
@@ -0,0 +1,135 @@
+/*
+    Copyright (c) 2014 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info at kdab.com>
+    Author: Kevin Krammer <kevin.krammer at kdab.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 "kolabchangeitemstagstask.h"
+
+#include "tagchangehelper.h"
+
+#include <akonadi/itemfetchjob.h>
+#include <akonadi/itemfetchscope.h>
+#include <akonadi/tagfetchjob.h>
+
+KolabChangeItemsTagsTask::KolabChangeItemsTagsTask(ResourceStateInterface::Ptr resource, QSharedPointer<TagConverter> tagConverter, QObject *parent)
+    : KolabRelationResourceTask(resource, parent)
+    , mTagConverter(tagConverter)
+{
+}
+
+void KolabChangeItemsTagsTask::startRelationTask(KIMAP::Session *session)
+{
+    mSession = session;
+
+    //It's entierly possible that we don't have an rid yet
+
+    // compile a set of changed tags
+    Q_FOREACH (const Akonadi::Tag &tag, resourceState()->addedTags()) {
+        mChangedTags.append(tag);
+    }
+    Q_FOREACH (const Akonadi::Tag &tag, resourceState()->removedTags()) {
+        mChangedTags.append(tag);
+    }
+    kDebug() << mChangedTags;
+
+    processNextTag();
+}
+
+void KolabChangeItemsTagsTask::processNextTag()
+{
+    if (mChangedTags.isEmpty()) {
+        changeProcessed();
+        return;
+    }
+
+    // "take first"
+    const Akonadi::Tag tag = mChangedTags.takeFirst();
+
+    //We have to fetch it again in case it changed since the notification was emitted (which is likely)
+    //Otherwise we get an empty remoteid for new tags that were immediately applied on an item
+    Akonadi::TagFetchJob *fetch = new Akonadi::TagFetchJob(tag);
+    connect(fetch, SIGNAL(result(KJob*)), this, SLOT(onTagFetchDone(KJob*)));
+}
+
+void KolabChangeItemsTagsTask::onTagFetchDone(KJob *job)
+{
+    if (job->error()) {
+        kWarning() << "TagFetch failed: " << job->errorString();
+        // TODO: we could continue for the other tags?
+        cancelTask(job->errorString());
+        return;
+    }
+
+    const Akonadi::Tag::List tags = static_cast<Akonadi::TagFetchJob*>(job)->tags();
+    Q_ASSERT(tags.size() == 1);
+    if (tags.size() != 1) {
+        kWarning() << "Invalid number of tags retrieved: " << tags.size();
+        // TODO: we could continue for the other tags?
+        cancelTask(job->errorString());
+        return;
+    }
+
+    Akonadi::ItemFetchJob *fetch = new Akonadi::ItemFetchJob(tags.first());
+    // fetch->fetchScope().setCacheOnly(true);
+    // TODO: does the fetch already limit to resource local items?
+    fetch->fetchScope().setAncestorRetrieval(Akonadi::ItemFetchScope::All);
+    fetch->setProperty("tag", QVariant::fromValue(tags.first()));
+    connect(fetch, SIGNAL(result(KJob*)), this, SLOT(onItemsFetchDone(KJob*)));
+}
+
+void KolabChangeItemsTagsTask::onItemsFetchDone(KJob *job)
+{
+    if (job->error()) {
+        kWarning() << "ItemFetch failed: " << job->errorString();
+        // TODO: we could continue for the other tags?
+        cancelTask(job->errorString());
+        return;
+    }
+
+    const Akonadi::Item::List items = static_cast<Akonadi::ItemFetchJob*>(job)->items();
+    kDebug() << items.size();
+
+    TagChangeHelper *changeHelper = new TagChangeHelper(this);
+
+    connect(changeHelper, SIGNAL(applyCollectionChanges(Akonadi::Collection)),
+            this, SLOT(onApplyCollectionChanged(Akonadi::Collection)));
+    connect(changeHelper, SIGNAL(cancelTask(QString)), this, SLOT(onCancelTask(QString)));
+    connect(changeHelper, SIGNAL(changeCommitted()), this, SLOT(onChangeCommitted()));
+
+    const Akonadi::Tag tag = job->property("tag").value<Akonadi::Tag>();
+    Q_ASSERT(tag.isValid());
+    changeHelper->start(tag, mTagConverter->createMessage(resourceState()->tag(), items), mSession);
+}
+
+void KolabChangeItemsTagsTask::onApplyCollectionChanged(const Akonadi::Collection &collection)
+{
+    mRelationCollection = collection;
+    applyCollectionChanges(collection);
+}
+
+void KolabChangeItemsTagsTask::onCancelTask(const QString &errorText)
+{
+    // TODO: we could continue for the other tags?
+    cancelTask(errorText);
+}
+
+void KolabChangeItemsTagsTask::onChangeCommitted()
+{
+    processNextTag();
+}
diff --git a/resources/kolab/kolabchangeitemstagstask.h b/resources/kolab/kolabchangeitemstagstask.h
new file mode 100644
index 0000000..d8d5458
--- /dev/null
+++ b/resources/kolab/kolabchangeitemstagstask.h
@@ -0,0 +1,54 @@
+/*
+    Copyright (c) 2014 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info at kdab.com>
+    Author: Kevin Krammer <kevin.krammer at kdab.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 KOLABCHANGEITEMSTAGSTASK_H
+#define KOLABCHANGEITEMSTAGSTASK_H
+
+#include "kolabrelationresourcetask.h"
+#include "tagchangehelper.h"
+
+class KolabChangeItemsTagsTask : public KolabRelationResourceTask
+{
+    Q_OBJECT
+public:
+    explicit KolabChangeItemsTagsTask(ResourceStateInterface::Ptr resource, QSharedPointer<TagConverter> tagConverter, QObject *parent = 0);
+
+protected:
+    virtual void startRelationTask(KIMAP::Session *session);
+
+private:
+    KIMAP::Session *mSession;
+    QList<Akonadi::Tag> mChangedTags;
+    QSharedPointer<TagConverter> mTagConverter;
+
+private:
+    void processNextTag();
+
+private Q_SLOTS:
+    void onItemsFetchDone(KJob *job);
+    void onTagFetchDone(KJob *job);
+
+    void onApplyCollectionChanged(const Akonadi::Collection &collection);
+    void onCancelTask(const QString &errorText);
+    void onChangeCommitted();
+};
+
+#endif // KOLABCHANGEITEMSTAGSTASK_H
diff --git a/resources/kolab/kolabchangetagtask.cpp b/resources/kolab/kolabchangetagtask.cpp
new file mode 100644
index 0000000..845d445
--- /dev/null
+++ b/resources/kolab/kolabchangetagtask.cpp
@@ -0,0 +1,81 @@
+/*
+    Copyright (c) 2014 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info at kdab.com>
+    Author: Kevin Krammer <kevin.krammer at kdab.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 "kolabchangetagtask.h"
+
+#include "tagchangehelper.h"
+
+#include <akonadi/itemfetchjob.h>
+#include <akonadi/itemfetchscope.h>
+
+KolabChangeTagTask::KolabChangeTagTask(ResourceStateInterface::Ptr resource, QSharedPointer<TagConverter> tagConverter, QObject *parent)
+    : KolabRelationResourceTask(resource, parent)
+    , mSession(0)
+    , mTagConverter(tagConverter)
+{
+}
+
+void KolabChangeTagTask::startRelationTask(KIMAP::Session *session)
+{
+    mSession = session;
+
+    Akonadi::ItemFetchJob *fetch = new Akonadi::ItemFetchJob(resourceState()->tag());
+    fetch->fetchScope().setCacheOnly(true);
+    // TODO: does the fetch already limit to resource local items?
+    fetch->fetchScope().setAncestorRetrieval(Akonadi::ItemFetchScope::All);
+    connect(fetch, SIGNAL(result(KJob*)), this, SLOT(onItemsFetchDone(KJob*)));
+}
+
+void KolabChangeTagTask::onItemsFetchDone(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();
+
+    TagChangeHelper *changeHelper = new TagChangeHelper(this);
+
+    connect(changeHelper, SIGNAL(applyCollectionChanges(Akonadi::Collection)),
+            this, SLOT(onApplyCollectionChanged(Akonadi::Collection)));
+    connect(changeHelper, SIGNAL(cancelTask(QString)), this, SLOT(onCancelTask(QString)));
+    connect(changeHelper, SIGNAL(changeCommitted()), this, SLOT(onChangeCommitted()));
+
+    changeHelper->start(resourceState()->tag(), mTagConverter->createMessage(resourceState()->tag(), items), mSession);
+}
+
+void KolabChangeTagTask::onApplyCollectionChanged(const Akonadi::Collection &collection)
+{
+    mRelationCollection = collection;
+    applyCollectionChanges(collection);
+}
+
+void KolabChangeTagTask::onCancelTask(const QString &errorText)
+{
+    cancelTask(errorText);
+}
+
+void KolabChangeTagTask::onChangeCommitted()
+{
+    changeCommitted(resourceState()->tag());
+}
diff --git a/resources/kolab/kolabchangetagtask.h b/resources/kolab/kolabchangetagtask.h
new file mode 100644
index 0000000..51fadd9
--- /dev/null
+++ b/resources/kolab/kolabchangetagtask.h
@@ -0,0 +1,49 @@
+/*
+    Copyright (c) 2014 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info at kdab.com>
+    Author: Kevin Krammer <kevin.krammer at kdab.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 KOLABCHANGETAGTASK_H
+#define KOLABCHANGETAGTASK_H
+
+#include "kolabrelationresourcetask.h"
+#include "tagchangehelper.h"
+
+class KolabChangeTagTask : public KolabRelationResourceTask
+{
+    Q_OBJECT
+public:
+    explicit KolabChangeTagTask(ResourceStateInterface::Ptr resource, QSharedPointer<TagConverter> tagConverter, QObject *parent = 0);
+
+protected:
+    virtual void startRelationTask(KIMAP::Session *session);
+
+private:
+    KIMAP::Session *mSession;
+    QSharedPointer<TagConverter> mTagConverter;
+
+private Q_SLOTS:
+    void onItemsFetchDone(KJob *job);
+
+    void onApplyCollectionChanged(const Akonadi::Collection &collection);
+    void onCancelTask(const QString &errorText);
+    void onChangeCommitted();
+};
+
+#endif // KOLABCHANGETAGTASK_H
diff --git a/resources/kolab/kolabhelpers.cpp b/resources/kolab/kolabhelpers.cpp
index d9d1c6d..a2ef951 100644
--- a/resources/kolab/kolabhelpers.cpp
+++ b/resources/kolab/kolabhelpers.cpp
@@ -304,6 +304,19 @@ Kolab::ObjectType KolabHelpers::getKolabTypeFromMimeType(const QString &type)
     return Kolab::InvalidObject;
 }
 
+QString KolabHelpers::getMimeType(Kolab::FolderType type)
+{
+    switch (type) {
+        case Kolab::MailType:
+            return KMime::Message::mimeType();
+        case Kolab::ConfigurationType:
+            return QLatin1String(KOLAB_TYPE_RELATION);
+        default:
+            kDebug() << "unhandled folder type: " << type;
+    }
+    return QString();
+}
+
 QStringList KolabHelpers::getContentMimeTypes(Kolab::FolderType type)
 {
     QStringList contentTypes;
@@ -323,6 +336,9 @@ QStringList KolabHelpers::getContentMimeTypes(Kolab::FolderType type)
         case Kolab::MailType:
             contentTypes << KMime::Message::mimeType();
             break;
+        case Kolab::ConfigurationType:
+            contentTypes << QLatin1String(KOLAB_TYPE_RELATION);
+            break;
         default:
             kDebug() << "unhandled folder type: " << type;
     }
diff --git a/resources/kolab/kolabhelpers.h b/resources/kolab/kolabhelpers.h
index 4ceacb2..28162a4 100644
--- a/resources/kolab/kolabhelpers.h
+++ b/resources/kolab/kolabhelpers.h
@@ -36,9 +36,10 @@ public:
     static Kolab::ObjectType getKolabTypeFromMimeType(const QString &type);
     static QByteArray kolabTypeForMimeType( const QStringList &contentMimeTypes );
     static QStringList getContentMimeTypes(Kolab::FolderType type);
+    static QString getMimeType(Kolab::FolderType type);
     static QString getIcon(Kolab::FolderType type);
     //Returns true if the folder type shouldn't be ignored
     static bool isHandledType(Kolab::FolderType type);
 };
 
-#endif
\ No newline at end of file
+#endif
diff --git a/resources/kolab/kolabrelationresourcetask.cpp b/resources/kolab/kolabrelationresourcetask.cpp
new file mode 100644
index 0000000..55596e7
--- /dev/null
+++ b/resources/kolab/kolabrelationresourcetask.cpp
@@ -0,0 +1,77 @@
+/*
+    Copyright (c) 2014 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info at kdab.com>
+    Author: Kevin Krammer <kevin.krammer at kdab.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 "kolabrelationresourcetask.h"
+
+#include "kolabhelpers.h"
+
+#include <akonadi/collectionfetchjob.h>
+#include <akonadi/collectionfetchscope.h>
+
+#include <KDE/KLocalizedString>
+
+KolabRelationResourceTask::KolabRelationResourceTask(ResourceStateInterface::Ptr resource, QObject *parent)
+    : ResourceTask(DeferIfNoSession, resource, parent)
+    , mImapSession(0)
+{
+}
+
+Akonadi::Collection KolabRelationResourceTask::relationCollection() const
+{
+    return mRelationCollection;
+}
+
+void KolabRelationResourceTask::doStart(KIMAP::Session *session)
+{
+    mImapSession = session;
+
+    // need to find the configuration collection.
+
+    Akonadi::Collection topLevelCollection;
+    topLevelCollection.setRemoteId(rootRemoteId());
+    topLevelCollection.setParentCollection(Akonadi::Collection::root());
+
+    Akonadi::CollectionFetchJob *fetchJob = new Akonadi::CollectionFetchJob(topLevelCollection, Akonadi::CollectionFetchJob::Recursive);
+    fetchJob->fetchScope().setContentMimeTypes(QStringList() << KolabHelpers::getMimeType(Kolab::ConfigurationType));
+    fetchJob->fetchScope().setAncestorRetrieval(Akonadi::CollectionFetchScope::All);
+    fetchJob->fetchScope().setListFilter(Akonadi::CollectionFetchScope::NoFilter);
+    connect(fetchJob, SIGNAL(result(KJob*)), this, SLOT(onCollectionFetchResult(KJob*)));
+}
+
+void KolabRelationResourceTask::onCollectionFetchResult(KJob *job)
+{
+    if (job->error() == 0) {
+        Akonadi::CollectionFetchJob *fetchJob = qobject_cast<Akonadi::CollectionFetchJob*>(job);
+        Q_ASSERT(fetchJob != 0);
+
+        Q_FOREACH (const Akonadi::Collection &collection, fetchJob->collections()) {
+            const QString mailBox = mailBoxForCollection(collection);
+            if (!mailBox.isEmpty()) {
+                mRelationCollection = collection;
+                startRelationTask(mImapSession);
+                return;
+            }
+        }
+    }
+
+    kWarning() << "Couldn't find collection for relations";
+    cancelTask(i18n("No mailbox for storing relations available, cancelling tag operation."));
+}
diff --git a/resources/kolab/kolabrelationresourcetask.h b/resources/kolab/kolabrelationresourcetask.h
new file mode 100644
index 0000000..51707da
--- /dev/null
+++ b/resources/kolab/kolabrelationresourcetask.h
@@ -0,0 +1,53 @@
+/*
+    Copyright (c) 2014 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info at kdab.com>
+    Author: Kevin Krammer <kevin.krammer at kdab.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 KOLABRELATIONRESOURCETASK_H
+#define KOLABRELATIONRESOURCETASK_H
+
+#include <resourcetask.h>
+
+class KolabRelationResourceTask : public ResourceTask
+{
+    Q_OBJECT
+public:
+    explicit KolabRelationResourceTask(ResourceStateInterface::Ptr resource, QObject *parent = 0);
+
+    Akonadi::Collection relationCollection() const;
+
+    using ResourceTask::mailBoxForCollection;
+    using ResourceTask::resourceState;
+
+protected:
+    Akonadi::Collection mRelationCollection;
+
+protected:
+    virtual void doStart(KIMAP::Session *session);
+
+    virtual void startRelationTask(KIMAP::Session *session) = 0;
+
+private:
+    KIMAP::Session *mImapSession;
+
+private Q_SLOTS:
+    void onCollectionFetchResult(KJob *job);
+};
+
+#endif // KOLABRELATIONRESOURCETASK_H
diff --git a/resources/kolab/kolabremovetagtask.cpp b/resources/kolab/kolabremovetagtask.cpp
new file mode 100644
index 0000000..3d0ed00
--- /dev/null
+++ b/resources/kolab/kolabremovetagtask.cpp
@@ -0,0 +1,93 @@
+/*
+    Copyright (c) 2014 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info at kdab.com>
+    Author: Kevin Krammer <kevin.krammer at kdab.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 "kolabremovetagtask.h"
+
+#include <imapflags.h>
+
+#include <kimap/selectjob.h>
+#include <kimap/session.h>
+#include <kimap/storejob.h>
+
+KolabRemoveTagTask::KolabRemoveTagTask(ResourceStateInterface::Ptr resource, QObject *parent)
+    : KolabRelationResourceTask(resource, parent)
+{
+}
+
+void KolabRemoveTagTask::startRelationTask(KIMAP::Session *session)
+{
+    // The imap specs do not allow for a single message to be deleted. We can only
+    // set the \Deleted flag. The message will actually be deleted when EXPUNGE will
+    // be issued on the next retrieveItems().
+
+    const QString mailBox = mailBoxForCollection(relationCollection());
+
+    kDebug(5327) << "Deleting tag " << resourceState()->tag().name() << " from " << mailBox;
+
+    if (session->selectedMailBox() != mailBox) {
+      KIMAP::SelectJob *select = new KIMAP::SelectJob(session);
+      select->setMailBox(mailBox);
+
+      connect(select, SIGNAL(result(KJob*)),
+              this, SLOT(onSelectDone(KJob*)));
+
+      select->start();
+
+    } else {
+      triggerStoreJob(session);
+    }
+}
+
+void KolabRemoveTagTask::triggerStoreJob(KIMAP::Session *session)
+{
+    KIMAP::ImapSet set;
+    set.add(resourceState()->tag().remoteId().toLong());
+
+    KIMAP::StoreJob *store = new KIMAP::StoreJob(session);
+    store->setUidBased(true);
+    store->setSequenceSet(set);
+    store->setFlags(QList<QByteArray>() << ImapFlags::Deleted);
+    store->setMode(KIMAP::StoreJob::AppendFlags);
+    connect(store, SIGNAL(result(KJob*)), SLOT(onStoreFlagsDone(KJob*)));
+    store->start();
+}
+
+void KolabRemoveTagTask::onSelectDone(KJob *job)
+{
+    if (job->error()) {
+      kWarning() << "Failed to select mailbox: " << job->errorString();
+      cancelTask(job->errorString());
+    } else {
+      KIMAP::SelectJob *select = static_cast<KIMAP::SelectJob*>(job);
+      triggerStoreJob(select->session());
+    }
+}
+
+void KolabRemoveTagTask::onStoreFlagsDone(KJob *job)
+{
+    //TODO use UID EXPUNGE if available
+    if (job->error()) {
+      kWarning() << "Failed to append flags: " << job->errorString();
+      cancelTask(job->errorString());
+    } else {
+      changeProcessed();
+    }
+}
diff --git a/resources/kolab/kolabremovetagtask.h b/resources/kolab/kolabremovetagtask.h
new file mode 100644
index 0000000..31b5eec
--- /dev/null
+++ b/resources/kolab/kolabremovetagtask.h
@@ -0,0 +1,44 @@
+/*
+    Copyright (c) 2014 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info at kdab.com>
+    Author: Kevin Krammer <kevin.krammer at kdab.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 KOLABREMOVETAGTASK_H
+#define KOLABREMOVETAGTASK_H
+
+#include "kolabrelationresourcetask.h"
+
+class KolabRemoveTagTask : public KolabRelationResourceTask
+{
+    Q_OBJECT
+public:
+    explicit KolabRemoveTagTask(ResourceStateInterface::Ptr resource, QObject *parent = 0);
+
+protected:
+    virtual void startRelationTask(KIMAP::Session *session);
+
+private:
+    void triggerStoreJob(KIMAP::Session *session);
+
+private Q_SLOTS:
+    void onSelectDone(KJob *job);
+    void onStoreFlagsDone(KJob *job);
+};
+
+#endif // KOLABREMOVETAGTASK_H
diff --git a/resources/kolab/kolabresource.cpp b/resources/kolab/kolabresource.cpp
index 2d6db5e..dd25447 100644
--- a/resources/kolab/kolabresource.cpp
+++ b/resources/kolab/kolabresource.cpp
@@ -35,6 +35,11 @@
 #include "kolabresourcestate.h"
 #include "kolabhelpers.h"
 #include "settings.h"
+#include "kolabaddtagtask.h"
+#include "kolabchangeitemstagstask.h"
+#include "kolabchangetagtask.h"
+#include "kolabremovetagtask.h"
+#include "kolabretrievetagstask.h"
 
 KolabResource::KolabResource(const QString& id)
     :ImapResource(id)
@@ -69,6 +74,7 @@ void KolabResource::retrieveCollections()
 {
     emit status(AgentBase::Running, i18nc("@info:status", "Retrieving folders"));
     startTask(new KolabRetrieveCollectionsTask(createResourceState(TaskArguments()), this));
+    synchronizeTags();
 }
 
 void KolabResource::retrieveItems(const Akonadi::Collection &col)
@@ -187,4 +193,34 @@ void KolabResource::collectionChanged(const Akonadi::Collection& collection, con
     startTask(task);
 }
 
+void KolabResource::tagAdded(const Akonadi::Tag &tag)
+{
+    KolabAddTagTask *task = new KolabAddTagTask(createResourceState(TaskArguments(tag)), this);
+    startTask(task);
+}
+
+void KolabResource::tagChanged(const Akonadi::Tag &tag)
+{
+    KolabChangeTagTask *task = new KolabChangeTagTask(createResourceState(TaskArguments(tag)), QSharedPointer<TagConverter>(new TagConverter), this);
+    startTask(task);
+}
+
+void KolabResource::tagRemoved(const Akonadi::Tag &tag)
+{
+    KolabRemoveTagTask *task = new KolabRemoveTagTask(createResourceState(TaskArguments(tag)), this);
+    startTask(task);
+}
+
+void KolabResource::itemsTagsChanged(const Akonadi::Item::List &items, const QSet<Akonadi::Tag> &addedTags, const QSet<Akonadi::Tag> &removedTags)
+{
+    KolabChangeItemsTagsTask *task = new KolabChangeItemsTagsTask(createResourceState(TaskArguments(items, addedTags, removedTags)), QSharedPointer<TagConverter>(new TagConverter), this);
+    startTask(task);
+}
+
+void KolabResource::retrieveTags()
+{
+    KolabRetrieveTagTask *task = new KolabRetrieveTagTask(createResourceState(TaskArguments()), this);
+    startTask(task);
+}
+
 AKONADI_RESOURCE_MAIN( KolabResource )
diff --git a/resources/kolab/kolabresource.h b/resources/kolab/kolabresource.h
index 2604d98..1f9bad8 100644
--- a/resources/kolab/kolabresource.h
+++ b/resources/kolab/kolabresource.h
@@ -51,10 +51,16 @@ protected:
     virtual void collectionChanged(const Akonadi::Collection &collection, const QSet<QByteArray> &parts);
     //collectionRemoved & collectionMoved do not require adjustments since they don't change the annotations
 
+    virtual void tagAdded(const Akonadi::Tag &tag);
+    virtual void tagChanged(const Akonadi::Tag &tag);
+    virtual void tagRemoved(const Akonadi::Tag &tag);
+    virtual void itemsTagsChanged(const Akonadi::Item::List &items, const QSet<Akonadi::Tag> &addedTags, const QSet<Akonadi::Tag> &removedTags);
+
     virtual QString defaultName();
 
 private Q_SLOTS:
     void onItemRetrievalCollectionFetchDone(KJob *job);
+    void retrieveTags();
 };
 
 #endif
diff --git a/resources/kolab/kolabresourcestate.cpp b/resources/kolab/kolabresourcestate.cpp
index 60b0478..86e9789 100644
--- a/resources/kolab/kolabresourcestate.cpp
+++ b/resources/kolab/kolabresourcestate.cpp
@@ -64,6 +64,11 @@ static Akonadi::Collection processAnnotations(const Akonadi::Collection &collect
                 col.setCachePolicy(cachePolicy);
             }
         }
+        if (folderType == Kolab::ConfigurationType) {
+            //we want to hide this folder from indexing and display, but still have the data available locally.
+            col.setEnabled(false);
+            col.setShouldList(Akonadi::Collection::ListSync, true);
+        }
         if (!KolabHelpers::isHandledType(folderType)) {
             //If we don't handle the folder, make sure we don't download the messages
             col.attribute<NoSelectAttribute>(Akonadi::Entity::AddIfMissing);
diff --git a/resources/kolab/kolabretrievetagstask.cpp b/resources/kolab/kolabretrievetagstask.cpp
new file mode 100644
index 0000000..0b55c5e
--- /dev/null
+++ b/resources/kolab/kolabretrievetagstask.cpp
@@ -0,0 +1,152 @@
+/*
+    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 "kolabretrievetagstask.h"
+
+#include "tagchangehelper.h"
+
+#include <kimap/selectjob.h>
+#include <kimap/fetchjob.h>
+#include <kolabobject.h>
+
+KolabRetrieveTagTask::KolabRetrieveTagTask(ResourceStateInterface::Ptr resource, QObject *parent)
+    : KolabRelationResourceTask(resource, parent)
+    , mSession(0)
+{
+}
+
+void KolabRetrieveTagTask::startRelationTask(KIMAP::Session *session)
+{
+    mSession = session;
+    const QString mailBox = mailBoxForCollection(relationCollection());
+
+    KIMAP::SelectJob *select = new KIMAP::SelectJob(session);
+    select->setMailBox(mailBox);
+    connect( select, SIGNAL(result(KJob*)),
+            this, SLOT(onFinalSelectDone(KJob*)) );
+    select->start();
+}
+
+void KolabRetrieveTagTask::onFinalSelectDone(KJob *job)
+{
+    if (job->error()) {
+        kWarning() << job->errorString();
+        cancelTask(job->errorString());
+        return;
+    }
+
+    KIMAP::SelectJob *select = static_cast<KIMAP::SelectJob*>(job);
+    KIMAP::FetchJob *fetch = new KIMAP::FetchJob(select->session());
+
+    KIMAP::ImapSet set;
+    set.add(KIMAP::ImapInterval(1));
+    fetch->setSequenceSet(set);
+    fetch->setUidBased(false);
+
+    KIMAP::FetchJob::FetchScope scope;
+    scope.parts.clear();
+    scope.mode = KIMAP::FetchJob::FetchScope::Full;
+    fetch->setScope(scope);
+
+    connect(fetch, SIGNAL(headersReceived(QString,
+                                          QMap<qint64,qint64>,
+                                          QMap<qint64,qint64>,
+                                          QMap<qint64,KIMAP::MessageAttribute>,
+                                          QMap<qint64,KIMAP::MessageFlags>,
+                                          QMap<qint64,KIMAP::MessagePtr>)),
+            this, SLOT(onHeadersReceived(QString,
+                                         QMap<qint64,qint64>,
+                                         QMap<qint64,qint64>,
+                                         QMap<qint64,KIMAP::MessageAttribute>,
+                                         QMap<qint64,KIMAP::MessageFlags>,
+                                         QMap<qint64,KIMAP::MessagePtr>)));
+    connect(fetch, SIGNAL(result(KJob*)),
+            this, SLOT(onHeadersFetchDone(KJob*)));
+    fetch->start();
+}
+
+void KolabRetrieveTagTask::onHeadersReceived(const QString &mailBox,
+                                     const QMap<qint64, qint64> &uids,
+                                     const QMap<qint64, qint64> &sizes,
+                                     const QMap<qint64, KIMAP::MessageAttribute> &attrs,
+                                     const QMap<qint64, KIMAP::MessageFlags> &flags,
+                                     const QMap<qint64, KIMAP::MessagePtr> &messages)
+{
+    KIMAP::FetchJob *fetch = static_cast<KIMAP::FetchJob*>( sender() );
+    Q_ASSERT(fetch);
+
+    foreach (qint64 number, uids.keys()) { //krazy:exclude=foreach
+        const KMime::Message::Ptr msg = messages[number];
+        const Kolab::KolabObjectReader reader(msg);
+        switch (reader.getType()) {
+            case Kolab::RelationConfigurationObject: {
+                Akonadi::Tag tag = reader.getTag();
+                tag.setRemoteId(QByteArray::number(uids[number]));
+                mTags << tag;
+
+                Akonadi::Item::List members;
+                Q_FOREACH (const QString &memberUrl, reader.getTagMembers()) {
+                    Kolab::RelationMember member = Kolab::parseMemberUrl(memberUrl);
+                    //TODO implement fallback to search if uid is not available
+                    //TODO should we create a dummy item if it isn't yet available?
+                    if (member.uid < 0) {
+                        kWarning() << "Failed to parse uid: " << memberUrl;
+                        continue;
+                    }
+                    Akonadi::Item i;
+                    i.setRemoteId(QString::number(member.uid));
+                    kDebug() << "got member: " << member.uid << member.mailbox;
+                    Akonadi::Collection parent;
+                    {
+                        //The root collection is not part of the mailbox path
+                        Akonadi::Collection col;
+                        col.setRemoteId(rootRemoteId());
+                        col.setParentCollection(Akonadi::Collection::root());
+                        parent = col;
+                    }
+                    Q_FOREACH(const QByteArray part, member.mailbox) {
+                        Akonadi::Collection col;
+                        col.setRemoteId(separatorCharacter() + QString::fromLatin1(part));
+                        col.setParentCollection(parent);
+                        parent = col;
+                    }
+                    i.setParentCollection(parent);
+                    members << i;
+                }
+                mTagMembers.insert(QString::fromLatin1(tag.remoteId()), members);
+            }
+                break;
+            default:
+                break;
+        }
+    }
+}
+
+void KolabRetrieveTagTask::onHeadersFetchDone(KJob *job)
+{
+    if (job->error()) {
+        kWarning() << "Fetch job failed " << job->errorString();
+        cancelTask(job->errorString());
+        return;
+    }
+    kDebug() << "Fetched tags: " << mTags.size() << mTagMembers.keys().size();
+    resourceState()->tagsRetrieved(mTags, mTagMembers);
+    deleteLater();
+}
+
diff --git a/resources/kolab/kolabretrievetagstask.h b/resources/kolab/kolabretrievetagstask.h
new file mode 100644
index 0000000..d790dbd
--- /dev/null
+++ b/resources/kolab/kolabretrievetagstask.h
@@ -0,0 +1,56 @@
+/*
+    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 KOLABRETRIEVETAGSTASK_H
+#define KOLABRETRIEVETAGSTASK_H
+
+#include "kolabrelationresourcetask.h"
+#include <akonadi/tag.h>
+
+class KolabRetrieveTagTask : public KolabRelationResourceTask
+{
+    Q_OBJECT
+public:
+    explicit KolabRetrieveTagTask(ResourceStateInterface::Ptr resource, QObject *parent = 0);
+
+protected:
+    virtual void startRelationTask(KIMAP::Session *session);
+
+private:
+    KIMAP::Session *mSession;
+    Akonadi::Tag::List mTags;
+    QHash<QString, Akonadi::Item::List> mTagMembers;
+
+private Q_SLOTS:
+    // void onItemsFetchDone(KJob *job);
+    void onFinalSelectDone(KJob *job);
+    void onHeadersReceived(const QString &mailBox,
+                            const QMap<qint64, qint64> &uids,
+                            const QMap<qint64, qint64> &sizes,
+                            const QMap<qint64, KIMAP::MessageAttribute> &attrs,
+                            const QMap<qint64, KIMAP::MessageFlags> &flags,
+                            const QMap<qint64, KIMAP::MessagePtr> &messages);
+    void onHeadersFetchDone(KJob *job);
+
+    // void onApplyCollectionChanged(const Akonadi::Collection &collection);
+    // void onCancelTask(const QString &errorText);
+    // void onChangeCommitted();
+};
+
+#endif // KOLABCHANGETAGTASK_H
diff --git a/resources/kolab/tagchangehelper.cpp b/resources/kolab/tagchangehelper.cpp
new file mode 100644
index 0000000..64c9764
--- /dev/null
+++ b/resources/kolab/tagchangehelper.cpp
@@ -0,0 +1,164 @@
+/*
+    Copyright (c) 2014 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info at kdab.com>
+    Author: Kevin Krammer <kevin.krammer at kdab.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 "tagchangehelper.h"
+
+#include "kolabrelationresourcetask.h"
+
+#include <imapflags.h>
+#include <uidnextattribute.h>
+
+#include <kolabobject.h>
+
+#include <kimap/appendjob.h>
+#include <kimap/searchjob.h>
+#include <kimap/selectjob.h>
+#include <kimap/session.h>
+#include <kimap/storejob.h>
+#include <replacemessagejob.h>
+#include <akonadi/tagmodifyjob.h>
+
+#include <KDE/KLocalizedString>
+
+TagChangeHelper::TagChangeHelper(KolabRelationResourceTask *parent)
+    : QObject(parent)
+    , mTask(parent)
+{
+}
+
+static QList<QByteArray> ancestorChain(const Akonadi::Collection &col)
+{
+    Q_ASSERT(col.isValid());
+    if (col.parentCollection() == Akonadi::Collection::root() || col == Akonadi::Collection::root() || !col.isValid()) {
+        return QList<QByteArray>();
+    }
+    QList<QByteArray> ancestors = ancestorChain(col.parentCollection());
+    Q_ASSERT(!col.remoteId().isEmpty());
+    ancestors << col.remoteId().toLatin1().mid(1); //We strip the first character which is always the separator
+    return ancestors;
+}
+
+QString TagConverter::createMemberUrl(const Akonadi::Item &item)
+{
+    Kolab::RelationMember member;
+    member.uid = item.remoteId().toLong();
+    member.user = QLatin1String("user at example.org");
+    member.subject = QLatin1String("subject");
+    member.messageId = QLatin1String("messageid");
+    member.mailbox = ancestorChain(item.parentCollection());
+    return Kolab::generateMemberUrl(member);
+}
+
+KMime::Message::Ptr TagConverter::createMessage(const Akonadi::Tag &tag, const Akonadi::Item::List &items)
+{
+    QStringList itemRemoteIds;
+    itemRemoteIds.reserve(items.count());
+    Q_FOREACH (const Akonadi::Item &item, items) {
+        itemRemoteIds << createMemberUrl(item);
+    }
+
+    // save message to the server.
+    const QLatin1String productId("Akonadi-Kolab-Resource");
+    const KMime::Message::Ptr message = Kolab::KolabObjectWriter::writeTag(tag, itemRemoteIds, Kolab::KolabV3, productId);
+    return message;
+}
+
+void TagChangeHelper::start(const Akonadi::Tag &tag, const KMime::Message::Ptr &message, KIMAP::Session *session)
+{
+    Q_ASSERT(tag.isValid());
+    const QString mailBox = mTask->mailBoxForCollection(mTask->relationCollection());
+    const qint64 oldUid = tag.remoteId().toLongLong();
+    kDebug(5327) << mailBox << oldUid;
+
+    qint64 uidNext = -1;
+    // Using uidnext here is mutually exclusive with doing the item sync
+    // if (UidNextAttribute *uidNextAttr = mTask->relationCollection().attribute<UidNextAttribute>()) {
+    //     uidNext = uidNextAttr->uidNext();
+    // }
+
+    ReplaceMessageJob *append = new ReplaceMessageJob(message, session, mailBox, uidNext, oldUid, this);
+    connect(append, SIGNAL(result(KJob*)), this, SLOT(onReplaceDone(KJob*)));
+    append->setProperty("tag", QVariant::fromValue(tag));
+    append->start();
+}
+
+void TagChangeHelper::recordNewUid(qint64 newUid, Akonadi::Tag tag)
+{
+    Q_ASSERT(newUid > 0);
+    Q_ASSERT(tag.isValid());
+
+    Akonadi::Collection c = mTask->relationCollection();
+
+    // Get the current uid next value and store it
+    // UidNextAttribute *uidAttr = 0;
+    // int oldNextUid = 0;
+    // if (c.hasAttribute("uidnext")) {
+    //     uidAttr = static_cast<UidNextAttribute*>(c.attribute("uidnext"));
+    //     oldNextUid = uidAttr->uidNext();
+    // }
+
+    // If the uid we just got back is the expected next one of the box
+    // then update the property to the probable next uid to keep the cache in sync.
+    // If not something happened in our back, so we don't update and a refetch will
+    // happen at some point.
+    // if (newUid == oldNextUid) {
+    //     if (uidAttr == 0) {
+    //         uidAttr = new UidNextAttribute(newUid + 1);
+    //         c.addAttribute(uidAttr);
+    //     } else {
+    //         uidAttr->setUidNext(newUid + 1);
+    //     }
+
+    //     emit applyCollectionChanges(c);
+    // }
+
+    const QByteArray remoteId =  QByteArray::number(newUid);
+    kDebug(5327) << "Setting remote ID to " << remoteId << " on tag with local id: " << tag.id();
+    tag.setRemoteId(remoteId);
+    Akonadi::TagModifyJob *modJob = new Akonadi::TagModifyJob(tag);
+    connect(modJob, SIGNAL(result(KJob*)), this, SLOT(onModifyDone(KJob*)));
+}
+
+void TagChangeHelper::onReplaceDone(KJob *job)
+{
+    if (job->error()) {
+        kWarning() << "Replace failed: " << job->errorString();
+    }
+    ReplaceMessageJob *replaceJob = static_cast<ReplaceMessageJob*>(job);
+    const qint64 newUid = replaceJob->newUid();
+    const Akonadi::Tag tag = job->property("tag").value<Akonadi::Tag>();
+    if (newUid > 0) {
+        recordNewUid(newUid, tag);
+    } else {
+        emit cancelTask(job->errorString());
+    }
+}
+
+void TagChangeHelper::onModifyDone(KJob *job)
+{
+    if (job->error()) {
+        kWarning() << "Modify failed: " << job->errorString();
+        emit cancelTask(job->errorString());
+        return;
+    }
+    emit changeCommitted();
+}
+
diff --git a/resources/kolab/tagchangehelper.h b/resources/kolab/tagchangehelper.h
new file mode 100644
index 0000000..5280cf6
--- /dev/null
+++ b/resources/kolab/tagchangehelper.h
@@ -0,0 +1,71 @@
+/*
+    Copyright (c) 2014 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info at kdab.com>
+    Author: Kevin Krammer <kevin.krammer at kdab.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 TAGCHANGEHELPER_H
+#define TAGCHANGEHELPER_H
+
+#include <akonadi/collection.h>
+#include <akonadi/item.h>
+#include <kmime/kmime_message.h>
+
+#include <QObject>
+
+namespace KIMAP {
+class Session;
+}
+
+namespace Akonadi {
+class Tag;
+}
+
+class KolabRelationResourceTask;
+
+struct TagConverter
+{
+    static QString createMemberUrl(const Akonadi::Item &item);
+    virtual KMime::Message::Ptr createMessage(const Akonadi::Tag &tag, const Akonadi::Item::List &items);
+};
+
+class TagChangeHelper : public QObject
+{
+    Q_OBJECT
+public:
+    explicit TagChangeHelper(KolabRelationResourceTask *parent = 0);
+
+    void start(const Akonadi::Tag &tag, const KMime::Message::Ptr &message, KIMAP::Session *session);
+
+Q_SIGNALS:
+    void applyCollectionChanges(const Akonadi::Collection &collection);
+    void cancelTask(const QString &errorText);
+    void changeCommitted();
+
+private:
+    KolabRelationResourceTask *const mTask;
+
+private:
+    void recordNewUid(qint64 newUid, Akonadi::Tag tag);
+
+private Q_SLOTS:
+    void onReplaceDone(KJob *job);
+    void onModifyDone(KJob *job);
+};
+
+#endif // TAGCHANGEHELPER_H
diff --git a/resources/kolab/tests/CMakeLists.txt b/resources/kolab/tests/CMakeLists.txt
new file mode 100644
index 0000000..4127aa1
--- /dev/null
+++ b/resources/kolab/tests/CMakeLists.txt
@@ -0,0 +1,26 @@
+set(EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR})
+
+# if kdepimlibs was built without -DKDE4_BUILD_TESTS, kimaptest doesn't exist.
+find_path(KIMAPTEST_INCLUDE_DIR NAMES kimaptest/fakeserver.h)
+find_library(KIMAPTEST_LIBRARY NAMES kimaptest)
+
+set(EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR})
+include(AkonadiMacros)
+set(KDEPIMLIBS_RUN_ISOLATED_TESTS TRUE)
+set(KDEPIMLIBS_RUN_SQLITE_ISOLATED_TESTS TRUE)
+
+if(KIMAPTEST_INCLUDE_DIR AND KIMAPTEST_LIBRARY)
+    MACRO(KOLAB_RESOURCE_ISOLATED_TESTS)
+        FOREACH(_testname ${ARGN})
+            include_directories(${CMAKE_CURRENT_SOURCE_DIR}/.. ${CMAKE_CURRENT_BINARY_DIR}/.. ../../imap/tests/)
+            add_akonadi_isolated_test_advanced(${_testname}.cpp "../../imap/tests/dummypasswordrequester.cpp;../../imap/tests/dummyresourcestate.cpp;../../imap/tests/imaptestbase.cpp" "${KDE4_KDECORE_LIBS};${KDEPIMLIBS_KIMAP_LIBS};${KIMAPTEST_LIBRARY};${QT_QTTEST_LIBRARY};imapresource;kolabresource;akonaditest")
+            set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${KDE4_ENABLE_EXCEPTIONS}")
+        ENDFOREACH(_testname)
+    ENDMACRO(KOLAB_RESOURCE_ISOLATED_TESTS)
+
+    KOLAB_RESOURCE_ISOLATED_TESTS (
+        testretrievetagstask
+        testchangeitemstagstask
+    )
+endif()
+
diff --git a/resources/kolab/tests/imaptestbase.cpp b/resources/kolab/tests/imaptestbase.cpp
new file mode 100644
index 0000000..94625d8
--- /dev/null
+++ b/resources/kolab/tests/imaptestbase.cpp
@@ -0,0 +1,137 @@
+/*
+   Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info at kdab.com>
+   Author: Kevin Ottens <kevin at kdab.com>
+
+   This program is free software; you can redistribute it and/or
+   modify it under the terms of the GNU General Public
+   License as published by the Free Software Foundation; either
+   version 2 of the License, or ( at your option ) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program; if not, write to the Free Software
+   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "imaptestbase.h"
+
+ImapTestBase::ImapTestBase( QObject *parent )
+  : QObject( parent )
+{
+
+}
+
+QString ImapTestBase::defaultUserName() const
+{
+  return QLatin1String("test at kdab.com");
+}
+
+QString ImapTestBase::defaultPassword() const
+{
+  return QLatin1String("foobar");
+}
+
+ImapAccount *ImapTestBase::createDefaultAccount() const
+{
+  ImapAccount *account = new ImapAccount;
+
+  account->setServer( QLatin1String("127.0.0.1") );
+  account->setPort( 5989 );
+  account->setUserName( defaultUserName() );
+  account->setSubscriptionEnabled( true );
+  account->setEncryptionMode( KIMAP::LoginJob::Unencrypted );
+  account->setAuthenticationMode( KIMAP::LoginJob::ClearText );
+
+  return account;
+}
+
+DummyPasswordRequester *ImapTestBase::createDefaultRequester()
+{
+  DummyPasswordRequester *requester = new DummyPasswordRequester( this );
+  requester->setPassword( defaultPassword() );
+  return requester;
+}
+
+void ImapTestBase::setupTestCase()
+{
+  qRegisterMetaType<ImapAccount*>();
+  qRegisterMetaType<DummyPasswordRequester*>();
+  qRegisterMetaType<DummyResourceState::Ptr>();
+  qRegisterMetaType<KIMAP::Session*>();
+}
+
+QList<QByteArray> ImapTestBase::defaultAuthScenario() const
+{
+  QList<QByteArray> scenario;
+
+  scenario << FakeServer::greeting()
+           << "C: A000001 LOGIN \"test at kdab.com\" \"foobar\""
+           << "S: A000001 OK User Logged in";
+
+  return scenario;
+}
+
+QList<QByteArray> ImapTestBase::defaultPoolConnectionScenario( const QList<QByteArray> &customCapabilities ) const
+{
+  QList<QByteArray> scenario;
+
+  QByteArray caps = "S: * CAPABILITY IMAP4 IMAP4rev1 UIDPLUS IDLE";
+  Q_FOREACH ( const QByteArray &cap, customCapabilities ) {
+    caps += " " + cap;
+  }
+
+  scenario << defaultAuthScenario()
+           << "C: A000002 CAPABILITY"
+           << caps
+           << "S: A000002 OK Completed";
+
+  return scenario;
+}
+
+bool ImapTestBase::waitForSignal( QObject *obj, const char *member, int timeout ) const
+{
+  QEventLoop loop;
+  QTimer timer;
+
+  connect( &timer, SIGNAL(timeout()), &loop, SLOT(quit()) );
+
+  QSignalSpy spy( obj, member );
+  connect( obj, member, &loop, SLOT(quit()) );
+
+  timer.setSingleShot( true );
+  timer.start( timeout );
+  loop.exec();
+  timer.stop();
+
+  return spy.count()==1;
+}
+
+Akonadi::Collection ImapTestBase::createCollectionChain( const QString &remoteId ) const
+{
+    QChar separator = remoteId.length() > 0 ? remoteId.at(0) : QLatin1Char('/');
+
+    Akonadi::Collection parent( 1 );
+    parent.setRemoteId( QLatin1String("root-id") );
+    parent.setParentCollection( Akonadi::Collection::root() );
+    Akonadi::Entity::Id id = 2;
+
+    Akonadi::Collection collection = parent;
+
+    const QStringList collections = remoteId.split( separator, QString::SkipEmptyParts );
+    Q_FOREACH ( const QString &colId, collections ) {
+        collection = Akonadi::Collection( id );
+        collection.setRemoteId( separator + colId );
+        collection.setParentCollection( parent );
+
+        parent = collection;
+        id++;
+    }
+
+    return collection;
+}
+
+
diff --git a/resources/kolab/tests/imaptestbase.h b/resources/kolab/tests/imaptestbase.h
new file mode 100644
index 0000000..cdb92c0
--- /dev/null
+++ b/resources/kolab/tests/imaptestbase.h
@@ -0,0 +1,95 @@
+/*
+   Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info at kdab.com>
+   Author: Kevin Ottens <kevin at kdab.com>
+
+   This program is free software; you can redistribute it and/or
+   modify it under the terms of the GNU General Public
+   License as published by the Free Software Foundation; either
+   version 2 of the License, or ( at your option ) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program; if not, write to the Free Software
+   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#ifndef IMAPTESTBASE_H
+#define IMAPTESTBASE_H
+
+#include <qtest_kde.h>
+
+#include <kimaptest/fakeserver.h>
+
+#include "dummypasswordrequester.h"
+#include "dummyresourcestate.h"
+#include "imapaccount.h"
+#include "resourcetask.h"
+#include "sessionpool.h"
+
+Q_DECLARE_METATYPE(ImapAccount*)
+Q_DECLARE_METATYPE(DummyPasswordRequester*)
+Q_DECLARE_METATYPE(DummyResourceState::Ptr)
+Q_DECLARE_METATYPE(KIMAP::Session*)
+Q_DECLARE_METATYPE(QVariant)
+
+class ImapTestBase : public QObject
+{
+  Q_OBJECT
+
+public:
+  ImapTestBase( QObject *parent = 0 );
+
+protected:
+  QString defaultUserName() const;
+  QString defaultPassword() const;
+  ImapAccount *createDefaultAccount() const;
+  DummyPasswordRequester *createDefaultRequester();
+  QList<QByteArray> defaultAuthScenario() const;
+  QList<QByteArray> defaultPoolConnectionScenario( const QList<QByteArray> &customCapabilities = QList<QByteArray>() ) const;
+
+  bool waitForSignal( QObject *obj, const char *member, int timeout = 500 ) const;
+
+  Akonadi::Collection createCollectionChain( const QString &remoteId ) const;
+
+private slots:
+  void setupTestCase();
+};
+
+// Taken from Qt 5:
+#if QT_VERSION < 0x050000
+
+// Will try to wait for the expression to become true while allowing event processing
+#define QTRY_VERIFY(__expr) \
+do { \
+    const int __step = 50; \
+    const int __timeout = 5000; \
+    if ( !( __expr ) ) { \
+        QTest::qWait( 0 ); \
+    } \
+    for ( int __i = 0; __i < __timeout && !( __expr ); __i += __step ) { \
+        QTest::qWait( __step ); \
+    } \
+    QVERIFY( __expr ); \
+} while ( 0 )
+
+// Will try to wait for the comparison to become successful while allowing event processing
+#define QTRY_COMPARE(__expr, __expected) \
+do { \
+    const int __step = 50; \
+    const int __timeout = 5000; \
+    if ( ( __expr ) != ( __expected ) ) { \
+        QTest::qWait( 0 ); \
+    } \
+    for ( int __i = 0; __i < __timeout && ( ( __expr ) != ( __expected ) ); __i += __step ) { \
+        QTest::qWait( __step ); \
+    } \
+    QCOMPARE( __expr, __expected ); \
+} while ( 0 )
+
+#endif
+
+#endif
diff --git a/resources/kolab/tests/testchangeitemstagstask.cpp b/resources/kolab/tests/testchangeitemstagstask.cpp
new file mode 100644
index 0000000..23bda44
--- /dev/null
+++ b/resources/kolab/tests/testchangeitemstagstask.cpp
@@ -0,0 +1,229 @@
+/*
+
+   This program is free software; you can redistribute it and/or
+   modify it under the terms of the GNU General Public
+   License as published by the Free Software Foundation; either
+   version 2 of the License, or (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program; if not, write to the Free Software
+   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+namespace Akonadi {
+    class Tag;
+};
+
+unsigned int qHash(const Akonadi::Tag &tag);
+
+#include "imaptestbase.h"
+
+#include <akonadi/tag.h>
+
+#include <akonadi/collectionquotaattribute.h>
+#include <akonadi/attributefactory.h>
+#include <akonadi/qtest_akonadi.h>
+#include <akonadi/servermanager.h>
+#include <akonadi/collectioncreatejob.h>
+#include <akonadi/virtualresource.h>
+#include <akonadi/tagcreatejob.h>
+#include "kolabhelpers.h"
+#include <kolab/kolabobject.h>
+
+#include "kolabchangeitemstagstask.h"
+
+using namespace Akonadi;
+
+typedef QHash<QString, Akonadi::Item::List> Members;
+
+Q_DECLARE_METATYPE(TagListAndMembers);
+Q_DECLARE_METATYPE(Members);
+
+struct TestTagConverter : public TagConverter
+{
+    virtual KMime::Message::Ptr createMessage(const Akonadi::Tag &tag, const Akonadi::Item::List &items)
+    {
+        return KMime::Message::Ptr(new KMime::Message());
+    }
+};
+
+class TestChangeItemsTagsTask : public ImapTestBase
+{
+    Q_OBJECT
+
+private slots:
+
+    void initTestCase()
+    {
+        AkonadiTest::checkTestIsIsolated();
+    }
+
+    void testRetrieveTags_data()
+    {
+        Akonadi::VirtualResource *resource = new Akonadi::VirtualResource(QLatin1String("akonadi_knut_resource_0"), this);
+
+        Akonadi::Collection root;
+        root.setName(QLatin1String("akonadi_knut_resource_0"));
+        root.setContentMimeTypes(QStringList() << Akonadi::Collection::mimeType());
+        root.setParentCollection(Akonadi::Collection::root());
+        root.setRemoteId("root-id");
+        root = resource->createRootCollection(root);
+
+        Akonadi::Collection col;
+        col.setName("Configuration");
+        col.setContentMimeTypes(QStringList() << KolabHelpers::getMimeType(Kolab::ConfigurationType));
+        col.setRemoteId("/configuration");
+        col = resource->createCollection(col);
+
+        Akonadi::Collection mailcol;
+        mailcol.setName("INBOX");
+        mailcol.setContentMimeTypes(QStringList() << KMime::Message::mimeType());
+        mailcol.setRemoteId("/INBOX");
+        mailcol = resource->createCollection(mailcol);
+
+        Akonadi::Tag tag("tagname");
+        {
+            Akonadi::TagCreateJob *createJob = new Akonadi::TagCreateJob(tag);
+            AKVERIFYEXEC(createJob);
+            tag = createJob->tag();
+        }
+
+        Akonadi::Item item(KMime::Message::mimeType());
+        item.setRemoteId("20");
+        item.setTag(tag);
+        item = resource->createItem(item, mailcol);
+
+        QTest::addColumn< QList<QByteArray> >("scenario");
+        QTest::addColumn<QStringList>("callNames");
+        QTest::addColumn<Akonadi::Tag::List>("expectedTags");
+        QTest::addColumn<Members>("expectedMembers");
+        QTest::addColumn<DummyResourceState::Ptr>("resourceState");
+
+        {
+            QList<QByteArray> scenario;
+            scenario << defaultPoolConnectionScenario();
+
+            QStringList callNames;
+            callNames << "changeProcessed";
+
+            QHash<QString, Akonadi::Item::List> expectedMembers;
+
+            DummyResourceState::Ptr state = DummyResourceState::Ptr(new DummyResourceState);
+            state->setServerCapabilities(QStringList() << "METADATA" << "ACL");
+            state->setUserName("Hans");
+
+            QTest::newRow("nothing changed") << scenario << callNames << Akonadi::Tag::List() << expectedMembers << state;
+        }
+        {
+            KMime::Message::Ptr msg(new KMime::Message());
+
+            const QByteArray &content = msg->encodedContent(true);
+            QList<QByteArray> scenario;
+            scenario << defaultPoolConnectionScenario()
+                    << "C: A000003 APPEND \"configuration\"  {"+ QByteArray::number(content.size()) + "}"
+                    << "S: A000003 OK append done [ APPENDUID 1239890035 65 ]";
+
+            QStringList callNames;
+            callNames << "changeProcessed";
+
+            Akonadi::Tag expectedTag = tag;
+            expectedTag.setRemoteId("7");
+
+            QHash<QString, Akonadi::Item::List> expectedMembers;
+            Akonadi::Item member;
+            member.setRemoteId("20");
+            member.setParentCollection(createCollectionChain("/INBOX"));
+            expectedMembers.insert(expectedTag.remoteId(), (Akonadi::Item::List() << member));
+
+            DummyResourceState::Ptr state = DummyResourceState::Ptr(new DummyResourceState);
+            state->setServerCapabilities(QStringList() << "METADATA" << "ACL");
+            state->setUserName("Hans");
+            state->setAddedTags(QSet<Akonadi::Tag>() << tag);
+
+            QTest::newRow("list single tag") << scenario << callNames << (Akonadi::Tag::List() << expectedTag) << expectedMembers << state;
+        }
+    }
+
+    void testRetrieveTags()
+    {
+        QFETCH(QList<QByteArray>, scenario);
+        QFETCH(QStringList, callNames);
+        QFETCH(Akonadi::Tag::List, expectedTags);
+        QFETCH(Members, expectedMembers);
+        QFETCH(DummyResourceState::Ptr, resourceState);
+
+        FakeServer server;
+        server.setScenario(scenario);
+        server.startAndWait();
+
+        SessionPool pool(1);
+
+        pool.setPasswordRequester(createDefaultRequester());
+        QVERIFY(pool.connect(createDefaultAccount()));
+        QVERIFY(waitForSignal(&pool, SIGNAL(connectDone(int,QString))));
+
+        KolabChangeItemsTagsTask *task = new KolabChangeItemsTagsTask(resourceState, QSharedPointer<TestTagConverter>(new TestTagConverter));
+
+        task->start(&pool);
+
+        QTRY_COMPARE(resourceState->calls().count(), callNames.size());
+        for (int i = 0; i < callNames.size(); i++) {
+            QString command = QString::fromUtf8(resourceState->calls().at(i).first);
+            QVariant parameter = resourceState->calls().at(i).second;
+
+            if (command == "cancelTask" && callNames[i] != "cancelTask") {
+                kDebug() << "Got a cancel:" << parameter.toString();
+            }
+
+            QCOMPARE(command, callNames[i]);
+
+            if (command == "tagsRetrieved") {
+                QPair<Akonadi::Tag::List, QHash<QString, Akonadi::Item::List> > pair = parameter.value<TagListAndMembers>();
+                Akonadi::Tag::List tags = pair.first;
+                QHash<QString, Akonadi::Item::List> members = pair.second;
+                QCOMPARE(tags.size(), expectedTags.size());
+                for (int i = 0 ; i < tags.size(); i++) {
+                    QCOMPARE(tags[i].name(), expectedTags[i].name());
+                    QCOMPARE(tags[i].remoteId(), expectedTags[i].remoteId());
+                    const Akonadi::Item::List memberlist = members.value(tags[i].remoteId());
+                    const Akonadi::Item::List expectedMemberlist = expectedMembers.value(tags[i].remoteId());
+                    QCOMPARE(memberlist.size(), expectedMemberlist.size());
+                    for (int i = 0 ; i < expectedMemberlist.size(); i++) {
+                        QCOMPARE(memberlist[i].remoteId(), expectedMemberlist[i].remoteId());
+                        Akonadi::Collection parent = memberlist[i].parentCollection();
+                        Akonadi::Collection expectedParent = expectedMemberlist[i].parentCollection();
+                        while (expectedParent.isValid()) {
+                            QCOMPARE(parent.remoteId(), expectedParent.remoteId());
+                            expectedParent = expectedParent.parentCollection();
+                            parent = parent.parentCollection();
+                        }
+                    }
+                }
+            }
+        }
+
+        QVERIFY(server.isAllScenarioDone());
+
+        server.quit();
+    }
+
+    void testTagConverter()
+    {
+        TagConverter converter;
+        Akonadi::Item item;
+        item.setRemoteId(QLatin1String("20"));
+        item.setParentCollection(createCollectionChain("/INBOX"));
+        const QString member = TagConverter::createMemberUrl(item);
+        const QString expected = QLatin1String("imap:/user/localuser at localhost/INBOX/20?message-id=messageid&subject=subject&date=");
+        QCOMPARE(member, expected);
+    }
+};
+
+QTEST_AKONADIMAIN(TestChangeItemsTagsTask, NoGUI)
+
+#include "testchangeitemstagstask.moc"
diff --git a/resources/kolab/tests/testretrievetagstask.cpp b/resources/kolab/tests/testretrievetagstask.cpp
new file mode 100644
index 0000000..6ae2609
--- /dev/null
+++ b/resources/kolab/tests/testretrievetagstask.cpp
@@ -0,0 +1,184 @@
+/*
+
+   This program is free software; you can redistribute it and/or
+   modify it under the terms of the GNU General Public
+   License as published by the Free Software Foundation; either
+   version 2 of the License, or (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program; if not, write to the Free Software
+   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "imaptestbase.h"
+
+#include "kolabretrievetagstask.h"
+
+#include <akonadi/collectionquotaattribute.h>
+#include <akonadi/attributefactory.h>
+#include <akonadi/qtest_akonadi.h>
+#include <akonadi/servermanager.h>
+#include <akonadi/collectioncreatejob.h>
+#include <akonadi/virtualresource.h>
+#include "kolabhelpers.h"
+#include <kolab/kolabobject.h>
+
+typedef QHash<QString, Akonadi::Item::List> Members;
+
+Q_DECLARE_METATYPE(TagListAndMembers);
+Q_DECLARE_METATYPE(Members);
+
+class TestRetrieveTagsTask : public ImapTestBase
+{
+    Q_OBJECT
+
+private slots:
+
+    void initTestCase()
+    {
+        AkonadiTest::checkTestIsIsolated();
+    }
+
+    void testRetrieveTags_data()
+    {
+        Akonadi::VirtualResource *resource = new Akonadi::VirtualResource(QLatin1String("akonadi_knut_resource_0"), this);
+
+        Akonadi::Collection root;
+        root.setName(QLatin1String("akonadi_knut_resource_0"));
+        root.setContentMimeTypes(QStringList() << Akonadi::Collection::mimeType());
+        root.setParentCollection(Akonadi::Collection::root());
+        root.setRemoteId("root-id");
+        root = resource->createRootCollection(root);
+
+        Akonadi::Collection col;
+        col.setName("Configuration");
+        col.setContentMimeTypes(QStringList() << KolabHelpers::getMimeType(Kolab::ConfigurationType));
+        col.setRemoteId("/configuration");
+        col = resource->createCollection(col);
+
+        Akonadi::Collection mailcol;
+        mailcol.setName("INBOX");
+        mailcol.setContentMimeTypes(QStringList() << KMime::Message::mimeType());
+        mailcol.setRemoteId("/INBOX");
+        mailcol = resource->createCollection(mailcol);
+
+        Akonadi::Item item(KMime::Message::mimeType());
+        item.setRemoteId("20");
+        item = resource->createItem(item, mailcol);
+
+        QTest::addColumn< QList<QByteArray> >("scenario");
+        QTest::addColumn<QStringList>("callNames");
+        QTest::addColumn<Akonadi::Tag::List>("expectedTags");
+        QTest::addColumn<Members>("expectedMembers");
+
+        QList<QByteArray> scenario;
+        QStringList callNames;
+
+        Akonadi::Tag tag;
+        tag.setName("tagname");
+        Kolab::KolabObjectWriter writer;
+        QStringList members;
+        members << QLatin1String("imap:///user/john.doe%40example.org/INBOX/20?message-id=%3Cf06aa3345a25005380b47547ad161d36%40lhm.klab.cc%3E&date=Tue%2C+12+Aug+2014+20%3A42%3A59+%2B0200&subject=Re%3A+test");
+        KMime::Message::Ptr msg = writer.writeTag(tag, members);
+        // kDebug() << msg->encodedContent();
+
+        const QByteArray &content = msg->encodedContent(true);
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                << "C: A000003 SELECT \"configuration\""
+                << "S: A000003 OK select done"
+                << "C: A000004 FETCH 1:* (RFC822.SIZE INTERNALDATE BODY.PEEK[] FLAGS UID)"
+                << "S: * 1 FETCH ( FLAGS (\\Seen) UID 7 INTERNALDATE \"29-Jun-2010 15:26:42 +0200\" "
+                    "RFC822.SIZE 75 BODY[] {" + QByteArray::number(content.size()) + "}\r\n"
+                    + content + " )"
+                << "S: A000004 OK fetch done";
+
+        callNames.clear();
+        callNames << "tagsRetrieved";
+
+        Akonadi::Tag expectedTag = tag;
+        expectedTag.setRemoteId("7");
+
+        QHash<QString, Akonadi::Item::List> expectedMembers;
+        Akonadi::Item member;
+        member.setRemoteId("20");
+        member.setParentCollection(createCollectionChain("/INBOX"));
+        expectedMembers.insert(expectedTag.remoteId(), (Akonadi::Item::List() << member));
+
+        QTest::newRow("list single tag") << scenario << callNames << (Akonadi::Tag::List() << expectedTag) << expectedMembers;
+    }
+
+    void testRetrieveTags()
+    {
+        QFETCH(QList<QByteArray>, scenario);
+        QFETCH(QStringList, callNames);
+        QFETCH(Akonadi::Tag::List, expectedTags);
+        QFETCH(Members, expectedMembers);
+
+        FakeServer server;
+        server.setScenario(scenario);
+        server.startAndWait();
+
+        SessionPool pool(1);
+
+        pool.setPasswordRequester(createDefaultRequester());
+        QVERIFY(pool.connect(createDefaultAccount()));
+        QVERIFY(waitForSignal(&pool, SIGNAL(connectDone(int,QString))));
+
+        DummyResourceState::Ptr state = DummyResourceState::Ptr(new DummyResourceState);
+        state->setServerCapabilities(QStringList() << "METADATA" << "ACL");
+        state->setUserName("Hans");
+        KolabRetrieveTagTask *task = new KolabRetrieveTagTask(state);
+
+        task->start(&pool);
+
+        QTRY_COMPARE(state->calls().count(), callNames.size());
+        for (int i = 0; i < callNames.size(); i++) {
+            QString command = QString::fromUtf8(state->calls().at(i).first);
+            QVariant parameter = state->calls().at(i).second;
+
+            if (command == "cancelTask" && callNames[i] != "cancelTask") {
+                kDebug() << "Got a cancel:" << parameter.toString();
+            }
+
+            QCOMPARE(command, callNames[i]);
+
+            if (command == "tagsRetrieved") {
+                QPair<Akonadi::Tag::List, QHash<QString, Akonadi::Item::List> > pair = parameter.value<TagListAndMembers>();
+                Akonadi::Tag::List tags = pair.first;
+                QHash<QString, Akonadi::Item::List> members = pair.second;
+                QCOMPARE(tags.size(), expectedTags.size());
+                for (int i = 0 ; i < tags.size(); i++) {
+                    QCOMPARE(tags[i].name(), expectedTags[i].name());
+                    QCOMPARE(tags[i].remoteId(), expectedTags[i].remoteId());
+                    const Akonadi::Item::List memberlist = members.value(tags[i].remoteId());
+                    const Akonadi::Item::List expectedMemberlist = expectedMembers.value(tags[i].remoteId());
+                    QCOMPARE(memberlist.size(), expectedMemberlist.size());
+                    for (int i = 0 ; i < expectedMemberlist.size(); i++) {
+                        QCOMPARE(memberlist[i].remoteId(), expectedMemberlist[i].remoteId());
+                        Akonadi::Collection parent = memberlist[i].parentCollection();
+                        Akonadi::Collection expectedParent = expectedMemberlist[i].parentCollection();
+                        while (expectedParent.isValid()) {
+                            QCOMPARE(parent.remoteId(), expectedParent.remoteId());
+                            expectedParent = expectedParent.parentCollection();
+                            parent = parent.parentCollection();
+                        }
+                    }
+                }
+            }
+        }
+
+        QVERIFY(server.isAllScenarioDone());
+
+        server.quit();
+    }
+};
+
+QTEST_AKONADIMAIN(TestRetrieveTagsTask, NoGUI)
+
+#include "testretrievetagstask.moc"
diff --git a/resources/kolab/tests/unittestenv/config-mysql-db.xml b/resources/kolab/tests/unittestenv/config-mysql-db.xml
new file mode 100644
index 0000000..011bc57
--- /dev/null
+++ b/resources/kolab/tests/unittestenv/config-mysql-db.xml
@@ -0,0 +1,8 @@
+<config>
+  <kdehome>kdehome</kdehome>
+  <confighome>xdgconfig-mysql.db</confighome>
+  <datahome>xdglocal</datahome>
+  <agent synchronize="true">akonadi_knut_resource</agent>
+  <envvar name="AKONADI_DISABLE_AGENT_AUTOSTART">true</envvar>
+  <envvar name="TESTRUNNER_DB_ENVIRONMENT">mysql</envvar>
+</config>
diff --git a/resources/kolab/tests/unittestenv/config-mysql-fs.xml b/resources/kolab/tests/unittestenv/config-mysql-fs.xml
new file mode 100644
index 0000000..4988fca
--- /dev/null
+++ b/resources/kolab/tests/unittestenv/config-mysql-fs.xml
@@ -0,0 +1,8 @@
+<config>
+  <kdehome>kdehome</kdehome>
+  <confighome>xdgconfig-mysql.fs</confighome>
+  <datahome>xdglocal</datahome>
+  <agent synchronize="true">akonadi_knut_resource</agent>
+  <envvar name="AKONADI_DISABLE_AGENT_AUTOSTART">true</envvar>
+  <envvar name="TESTRUNNER_DB_ENVIRONMENT">mysql</envvar>
+</config>
diff --git a/resources/kolab/tests/unittestenv/config-postgresql-db.xml b/resources/kolab/tests/unittestenv/config-postgresql-db.xml
new file mode 100644
index 0000000..615e0d1
--- /dev/null
+++ b/resources/kolab/tests/unittestenv/config-postgresql-db.xml
@@ -0,0 +1,8 @@
+<config>
+  <kdehome>kdehome</kdehome>
+  <confighome>xdgconfig-postgresql.db</confighome>
+  <datahome>xdglocal</datahome>
+  <agent synchronize="true">akonadi_knut_resource</agent>
+  <envvar name="AKONADI_DISABLE_AGENT_AUTOSTART">true</envvar>
+  <envvar name="TESTRUNNER_DB_ENVIRONMENT">postgresql</envvar>
+</config>
diff --git a/resources/kolab/tests/unittestenv/config-postgresql-fs.xml b/resources/kolab/tests/unittestenv/config-postgresql-fs.xml
new file mode 100644
index 0000000..3ba8d74
--- /dev/null
+++ b/resources/kolab/tests/unittestenv/config-postgresql-fs.xml
@@ -0,0 +1,8 @@
+<config>
+  <kdehome>kdehome</kdehome>
+  <confighome>xdgconfig-postgresql.fs</confighome>
+  <datahome>xdglocal</datahome>
+  <agent synchronize="true">akonadi_knut_resource</agent>
+  <envvar name="AKONADI_DISABLE_AGENT_AUTOSTART">true</envvar>
+  <envvar name="TESTRUNNER_DB_ENVIRONMENT">postgresql</envvar>
+</config>
diff --git a/resources/kolab/tests/unittestenv/config-sqlite-db.xml b/resources/kolab/tests/unittestenv/config-sqlite-db.xml
new file mode 100644
index 0000000..c36076f
--- /dev/null
+++ b/resources/kolab/tests/unittestenv/config-sqlite-db.xml
@@ -0,0 +1,8 @@
+<config>
+  <kdehome>kdehome</kdehome>
+  <confighome>xdgconfig-sqlite.db</confighome>
+  <datahome>xdglocal</datahome>
+  <agent synchronize="true">akonadi_knut_resource</agent>
+  <envvar name="AKONADI_DISABLE_AGENT_AUTOSTART">true</envvar>
+  <envvar name="TESTRUNNER_DB_ENVIRONMENT">sqlite</envvar>
+</config>
diff --git a/resources/kolab/tests/unittestenv/kdehome/share/config/akonadi-firstrunrc b/resources/kolab/tests/unittestenv/kdehome/share/config/akonadi-firstrunrc
new file mode 100644
index 0000000..c5e90d8
--- /dev/null
+++ b/resources/kolab/tests/unittestenv/kdehome/share/config/akonadi-firstrunrc
@@ -0,0 +1,4 @@
+[ProcessedDefaults]
+defaultaddressbook=done
+defaultcalendar=done
+defaultnotebook=done
diff --git a/resources/kolab/tests/unittestenv/kdehome/share/config/akonadi_knut_resource_0rc b/resources/kolab/tests/unittestenv/kdehome/share/config/akonadi_knut_resource_0rc
new file mode 100644
index 0000000..61bb2f0
--- /dev/null
+++ b/resources/kolab/tests/unittestenv/kdehome/share/config/akonadi_knut_resource_0rc
@@ -0,0 +1,4 @@
+[General]
+DataFile[$e]=$KDEHOME/testdata-res1.xml
+FileWatchingEnabled=false
+
diff --git a/resources/kolab/tests/unittestenv/kdehome/share/config/kdebugrc b/resources/kolab/tests/unittestenv/kdehome/share/config/kdebugrc
new file mode 100755
index 0000000..bbd212c
--- /dev/null
+++ b/resources/kolab/tests/unittestenv/kdehome/share/config/kdebugrc
@@ -0,0 +1,80 @@
+DisableAll=false
+
+[0]
+AbortFatal=true
+ErrorFilename[$e]=kdebug.dbg
+ErrorOutput=2
+FatalFilename[$e]=kdebug.dbg
+FatalOutput=2
+InfoFilename[$e]=kdebug.dbg
+InfoOutput=2
+WarnFilename[$e]=kdebug.dbg
+WarnOutput=2
+
+[264]
+AbortFatal=true
+ErrorFilename[$e]=kdebug.dbg
+ErrorOutput=2
+FatalFilename[$e]=kdebug.dbg
+FatalOutput=2
+InfoFilename[$e]=kdebug.dbg
+WarnFilename[$e]=kdebug.dbg
+WarnOutput=2
+
+[5250]
+InfoOutput=2
+
+[7009]
+AbortFatal=true
+ErrorFilename[$e]=kdebug.dbg
+ErrorOutput=2
+FatalFilename[$e]=kdebug.dbg
+FatalOutput=2
+InfoFilename[$e]=kdebug.dbg
+InfoOutput=2
+WarnFilename[$e]=kdebug.dbg
+WarnOutput=2
+
+[7011]
+AbortFatal=true
+ErrorFilename[$e]=kdebug.dbg
+ErrorOutput=2
+FatalFilename[$e]=kdebug.dbg
+FatalOutput=2
+InfoFilename[$e]=kdebug.dbg
+InfoOutput=2
+WarnFilename[$e]=kdebug.dbg
+WarnOutput=2
+
+[7012]
+AbortFatal=true
+ErrorFilename[$e]=kdebug.dbg
+ErrorOutput=2
+FatalFilename[$e]=kdebug.dbg
+FatalOutput=2
+InfoFilename[$e]=kdebug.dbg
+InfoOutput=2
+WarnFilename[$e]=kdebug.dbg
+WarnOutput=2
+
+[7014]
+AbortFatal=true
+ErrorFilename[$e]=kdebug.dbg
+ErrorOutput=2
+FatalFilename[$e]=kdebug.dbg
+FatalOutput=2
+InfoFilename[$e]=kdebug.dbg
+InfoOutput=2
+WarnFilename[$e]=kdebug.dbg
+WarnOutput=2
+
+[7021]
+AbortFatal=true
+ErrorFilename[$e]=kdebug.dbg
+ErrorOutput=2
+FatalFilename[$e]=kdebug.dbg
+FatalOutput=2
+InfoFilename[$e]=kdebug.dbg
+InfoOutput=2
+WarnFilename[$e]=kdebug.dbg
+WarnOutput=2
diff --git a/resources/kolab/tests/unittestenv/kdehome/share/config/kdedrc b/resources/kolab/tests/unittestenv/kdehome/share/config/kdedrc
new file mode 100644
index 0000000..41d1781
--- /dev/null
+++ b/resources/kolab/tests/unittestenv/kdehome/share/config/kdedrc
@@ -0,0 +1,3 @@
+[General]
+CheckSycoca=false
+CheckFileStamps=false
diff --git a/resources/kolab/tests/unittestenv/kdehome/testdata-res1.xml b/resources/kolab/tests/unittestenv/kdehome/testdata-res1.xml
new file mode 100644
index 0000000..00ed642
--- /dev/null
+++ b/resources/kolab/tests/unittestenv/kdehome/testdata-res1.xml
@@ -0,0 +1,37 @@
+<knut>
+ <collection rid="1" name="res1" content="inode/directory">
+  <collection rid="2" name="INBOX" content="inode/directory,message/rfc822">
+   <item rid="A" mimetype="application/octet-stream">
+    <payload>testmailbody</payload>
+    <attribute type="HEAD">From: <test at user.tst></attribute>
+    <flag>\SEEN</flag>
+    <flag>\FLAGGED</flag>
+    <flag>\DRAFT</flag>
+   </item>
+  </collection>
+  <collection rid="3" name="Calendar" content="inode/directory,message/rfc822">
+   <attribute type="collectionannotations">/shared/vendor/kolab/folder-type event</attribute>
+  </collection>
+  <collection rid="4" name="Contact" content="inode/directory,message/rfc822">
+   <attribute type="collectionannotations">/shared/vendor/kolab/folder-type contact</attribute>
+  </collection>
+  <collection rid="5" name="Notes" content="inode/directory,message/rfc822">
+   <attribute type="collectionannotations">/shared/vendor/kolab/folder-type note</attribute>
+  </collection>
+  <collection rid="6" name="Tasks" content="inode/directory,message/rfc822">
+   <attribute type="collectionannotations">/shared/vendor/kolab/folder-type task</attribute>
+  </collection>
+  <collection rid="7" name="Journal" content="inode/directory,message/rfc822">
+   <attribute type="collectionannotations">/shared/vendor/kolab/folder-type journal</attribute>
+  </collection>
+  <collection rid="8" name="Configuration" content="inode/directory,message/rfc822">
+   <attribute type="collectionannotations">/shared/vendor/kolab/folder-type configuration</attribute>
+  </collection>
+  <collection rid="9" name="Freebusy" content="inode/directory,message/rfc822">
+   <attribute type="collectionannotations">/shared/vendor/kolab/folder-type freebusy</attribute>
+  </collection>
+  <collection rid="10" name="Files" content="inode/directory,message/rfc822">
+   <attribute type="collectionannotations">/shared/vendor/kolab/folder-type file</attribute>
+  </collection>
+ </collection>
+</knut>
diff --git a/resources/kolab/tests/unittestenv/xdgconfig-mysql.db/akonadi/akonadiserverrc b/resources/kolab/tests/unittestenv/xdgconfig-mysql.db/akonadi/akonadiserverrc
new file mode 100644
index 0000000..fa9b2d4
--- /dev/null
+++ b/resources/kolab/tests/unittestenv/xdgconfig-mysql.db/akonadi/akonadiserverrc
@@ -0,0 +1,5 @@
+[%General]
+ExternalPayload=false
+
+[Search]
+Manager=Dummy
diff --git a/resources/kolab/tests/unittestenv/xdgconfig-mysql.fs/akonadi/akonadiserverrc b/resources/kolab/tests/unittestenv/xdgconfig-mysql.fs/akonadi/akonadiserverrc
new file mode 100644
index 0000000..a7bb0c2
--- /dev/null
+++ b/resources/kolab/tests/unittestenv/xdgconfig-mysql.fs/akonadi/akonadiserverrc
@@ -0,0 +1,6 @@
+[%General]
+SizeThreshold=0
+ExternalPayload=true
+
+[Search]
+Manager=Dummy
diff --git a/resources/kolab/tests/unittestenv/xdgconfig-postgresql.db/akonadi/akonadiserverrc b/resources/kolab/tests/unittestenv/xdgconfig-postgresql.db/akonadi/akonadiserverrc
new file mode 100644
index 0000000..b2c8b1a
--- /dev/null
+++ b/resources/kolab/tests/unittestenv/xdgconfig-postgresql.db/akonadi/akonadiserverrc
@@ -0,0 +1,9 @@
+[%General]
+Driver=QPSQL
+ExternalPayload=false
+
+[Search]
+Manager=Dummy
+
+[QPSQL]
+StartServer=true
diff --git a/resources/kolab/tests/unittestenv/xdgconfig-postgresql.fs/akonadi/akonadiserverrc b/resources/kolab/tests/unittestenv/xdgconfig-postgresql.fs/akonadi/akonadiserverrc
new file mode 100644
index 0000000..8333c73
--- /dev/null
+++ b/resources/kolab/tests/unittestenv/xdgconfig-postgresql.fs/akonadi/akonadiserverrc
@@ -0,0 +1,10 @@
+[%General]
+Driver=QPSQL
+SizeThreshold=0
+ExternalPayload=true
+
+[Search]
+Manager=Dummy
+
+[QPSQL]
+StartServer=true
diff --git a/resources/kolab/tests/unittestenv/xdgconfig-sqlite.db/akonadi/akonadiserverrc b/resources/kolab/tests/unittestenv/xdgconfig-sqlite.db/akonadi/akonadiserverrc
new file mode 100644
index 0000000..33e7f81
--- /dev/null
+++ b/resources/kolab/tests/unittestenv/xdgconfig-sqlite.db/akonadi/akonadiserverrc
@@ -0,0 +1,10 @@
+[%General]
+# This is a slightly adjusted version of the QSQLITE driver from Qt
+# It is provided by akonadi itself
+Driver=QSQLITE3
+
+[Debug]
+Tracer=null
+
+[Search]
+Manager=Dummy





More information about the commits mailing list