From 638e75d6f3d00fb473fd45e325fcfb34c6340c65 Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Fri, 10 Jun 2016 15:49:48 +0200 Subject: Create the drafts folder if necessary and merge it with the source version --- common/synchronizer.cpp | 61 +++++++++++++ common/synchronizer.h | 3 + examples/imapresource/imapresource.cpp | 128 +++++++++++++++++++++++++--- examples/imapresource/tests/resetmailbox.sh | 1 + tests/mailsynctest.cpp | 17 ++-- 5 files changed, 194 insertions(+), 16 deletions(-) diff --git a/common/synchronizer.cpp b/common/synchronizer.cpp index 0314997..b127ec5 100644 --- a/common/synchronizer.cpp +++ b/common/synchronizer.cpp @@ -164,6 +164,59 @@ void Synchronizer::createOrModify(const QByteArray &bufferType, const QByteArray } } +template +void Synchronizer::createOrModify(const QByteArray &bufferType, const QByteArray &remoteId, const DomainType &entity, const QHash &mergeCriteria) +{ + + Trace() << "Create or modify" << bufferType << remoteId; + auto mainDatabase = Storage::mainDatabase(transaction(), bufferType); + const auto sinkId = syncStore().resolveRemoteId(bufferType, remoteId); + const auto found = mainDatabase.contains(sinkId); + auto adaptorFactory = Sink::AdaptorFactoryRegistry::instance().getFactory(mResourceType, bufferType); + if (!found) { + if (!mergeCriteria.isEmpty()) { + Sink::Query query; + query.propertyFilter = mergeCriteria; + bool merge = false; + Sink::EntityReader reader(mResourceInstanceIdentifier, mResourceType, transaction()); + reader.query(query, + [this, bufferType, remoteId, &merge](const DomainType &o) -> bool{ + merge = true; + Trace() << "Merging local entity with remote entity: " << o.identifier() << remoteId; + syncStore().recordRemoteId(bufferType, o.identifier(), remoteId); + return false; + }); + if (!merge) { + Trace() << "Found a new entity: " << remoteId; + createEntity( + sinkId, bufferType, entity, *adaptorFactory, [this](const QByteArray &buffer) { enqueueCommand(Sink::Commands::CreateEntityCommand, buffer); }); + } + } else { + Trace() << "Found a new entity: " << remoteId; + createEntity( + sinkId, bufferType, entity, *adaptorFactory, [this](const QByteArray &buffer) { enqueueCommand(Sink::Commands::CreateEntityCommand, buffer); }); + } + } else { // modification + qint64 retrievedRevision = 0; + if (auto current = EntityReaderUtils::getLatest(mainDatabase, sinkId, *adaptorFactory, retrievedRevision)) { + bool changed = false; + for (const auto &property : entity.changedProperties()) { + if (entity.getProperty(property) != current->getProperty(property)) { + Trace() << "Property changed " << sinkId << property; + changed = true; + } + } + if (changed) { + Trace() << "Found a modified entity: " << remoteId; + modifyEntity(sinkId, Sink::Storage::maxRevision(transaction()), bufferType, entity, *adaptorFactory, + [this](const QByteArray &buffer) { enqueueCommand(Sink::Commands::ModifyEntityCommand, buffer); }); + } + } else { + Warning() << "Failed to get current entity"; + } + } +} + KAsync::Job Synchronizer::synchronize() { Trace() << "Synchronizing"; @@ -202,3 +255,11 @@ Sink::Storage::Transaction &Synchronizer::syncTransaction() } return mSyncTransaction; } + +#define REGISTER_TYPE(T) \ + template void Synchronizer::createOrModify(const QByteArray &bufferType, const QByteArray &remoteId, const T &entity, const QHash &mergeCriteria) + +REGISTER_TYPE(ApplicationDomain::Event); +REGISTER_TYPE(ApplicationDomain::Mail); +REGISTER_TYPE(ApplicationDomain::Folder); + diff --git a/common/synchronizer.h b/common/synchronizer.h index 17e7003..8442aa2 100644 --- a/common/synchronizer.h +++ b/common/synchronizer.h @@ -23,6 +23,7 @@ #include #include #include +#include #include "storage.h" @@ -81,6 +82,8 @@ protected: * Depending on whether the entity is locally available, or has changed. */ void createOrModify(const QByteArray &bufferType, const QByteArray &remoteId, const Sink::ApplicationDomain::ApplicationDomainType &entity); + template + void createOrModify(const QByteArray &bufferType, const QByteArray &remoteId, const DomainType &entity, const QHash &mergeCriteria); virtual KAsync::Job synchronizeWithSource() = 0; diff --git a/examples/imapresource/imapresource.cpp b/examples/imapresource/imapresource.cpp index fee479a..ac75432 100644 --- a/examples/imapresource/imapresource.cpp +++ b/examples/imapresource/imapresource.cpp @@ -39,12 +39,14 @@ #include "sourcewriteback.h" #include "entitystore.h" #include "remoteidmap.h" +#include "query.h" #include #include #include #include #include "imapserverproxy.h" +#include "entityreader.h" //This is the resources entity type, and not the domain type #define ENTITY_TYPE_MAIL "mail" @@ -56,6 +58,77 @@ using namespace Imap; using namespace Sink; +static QHash specialPurposeFolders() +{ + QHash hash; + //FIXME localize + hash.insert("drafts", "Drafts"); + return hash; +} + +static QHash specialPurposeNames() +{ + QHash hash; + for (const auto &value : specialPurposeFolders().values()) { + hash.insert(value.toLower(), specialPurposeFolders().key(value)); + } + return hash; +} + +//specialpurpose, name +static QHash sSpecialPurposeFolders = specialPurposeFolders(); +//Lowercase-name, specialpurpose +static QHash sSpecialPurposeNames = specialPurposeNames(); + +class DraftsProcessor : public Sink::Preprocessor +{ +public: + DraftsProcessor() {} + + QByteArray ensureDraftsFolder(Sink::Storage::Transaction &transaction) + { + if (mDraftsFolder.isEmpty()) { + //Try to find an existing drafts folder + Sink::EntityReader reader(mResourceInstanceIdentifier, mResourceType, transaction); + reader.query(Sink::Query().filter(Query::Comparator("drafts", Query::Comparator::Contains)), + [this](const ApplicationDomain::Folder &f) -> bool{ + mDraftsFolder = f.identifier(); + return false; + }); + if (mDraftsFolder.isEmpty()) { + Trace() << "Failed to find a drafts folder, creating a new one"; + auto folder = ApplicationDomain::Folder::create(mResourceInstanceIdentifier); + folder.setSpecialPurpose(QByteArrayList() << "drafts"); + folder.setName(sSpecialPurposeFolders.value("drafts")); + folder.setIcon("folder"); + //This processes the pipeline synchronously + createEntity(folder); + mDraftsFolder = folder.identifier(); + } + } + return mDraftsFolder; + } + + void newEntity(const QByteArray &uid, qint64 revision, Sink::ApplicationDomain::BufferAdaptor &newEntity, Sink::Storage::Transaction &transaction) Q_DECL_OVERRIDE + { + if (newEntity.getProperty("draft").toBool()) { + newEntity.setProperty("folder", ensureDraftsFolder(transaction)); + } + } + + void modifiedEntity(const QByteArray &uid, qint64 revision, const Sink::ApplicationDomain::BufferAdaptor &oldEntity, Sink::ApplicationDomain::BufferAdaptor &newEntity, + Sink::Storage::Transaction &transaction) Q_DECL_OVERRIDE + { + if (newEntity.getProperty("draft").toBool()) { + newEntity.setProperty("folder", ensureDraftsFolder(transaction)); + } + } + + QByteArray mDraftsFolder; + QByteArray mResourceInstanceIdentifier; + QByteArray mResourceType; +}; + class MailPropertyExtractor : public Sink::Preprocessor { public: @@ -97,10 +170,6 @@ public: updatedIndexedProperties(newEntity); } - void deletedEntity(const QByteArray &uid, qint64 revision, const Sink::ApplicationDomain::BufferAdaptor &oldEntity, Sink::Storage::Transaction &transaction) Q_DECL_OVERRIDE - { - } - }; static qint64 uidFromMailRid(const QByteArray &remoteId) @@ -142,13 +211,19 @@ public: const auto remoteId = folderPath.toUtf8(); const auto bufferType = ENTITY_TYPE_FOLDER; Sink::ApplicationDomain::Folder folder; - folder.setProperty("name", folderName); - folder.setProperty("icon", icon); + folder.setProperty(ApplicationDomain::Folder::Name::name, folderName); + folder.setProperty(ApplicationDomain::Folder::Icon::name, icon); + QHash mergeCriteria; + if (sSpecialPurposeNames.contains(folderName.toLower())) { + auto type = sSpecialPurposeNames.value(folderName.toLower()); + folder.setProperty(ApplicationDomain::Folder::SpecialPurpose::name, QVariant::fromValue(QByteArrayList() << type)); + mergeCriteria.insert(ApplicationDomain::Folder::SpecialPurpose::name, Query::Comparator(type, Query::Comparator::Contains)); + } if (!parentFolderRid.isEmpty()) { folder.setProperty("parent", syncStore().resolveRemoteId(ENTITY_TYPE_FOLDER, parentFolderRid.toUtf8())); } - createOrModify(bufferType, remoteId, folder); + createOrModify(bufferType, remoteId, folder, mergeCriteria); return remoteId; } @@ -434,14 +509,47 @@ public: } Trace() << "Creating a new folder: " << parentFolder << folder.getName(); auto rid = QSharedPointer::create(); - return login.then(imap->createSubfolder(parentFolder, folder.getName())) + auto createFolder = login.then(imap->createSubfolder(parentFolder, folder.getName())) .then([imap, rid](const QString &createdFolder) { Trace() << "Finished creating a new folder: " << createdFolder; *rid = createdFolder.toUtf8(); - }) + }); + if (folder.getSpecialPurpose().isEmpty()) { + return createFolder + .then([rid](){ + return *rid; + }); + } else { //We try to merge special purpose folders first + auto specialPurposeFolders = QSharedPointer>::create(); + auto mergeJob = imap->login(mUser, mPassword) + .then(imap->fetchFolders([=](const QVector &folders) { + for (const auto &f : folders) { + if (sSpecialPurposeNames.contains(f.pathParts.last().toLower())) { + specialPurposeFolders->insert(sSpecialPurposeNames.value(f.pathParts.last().toLower()), f.path); + }; + } + })) + .then>([specialPurposeFolders, folder, imap, parentFolder, rid]() -> KAsync::Job { + for (const auto &purpose : folder.getSpecialPurpose()) { + if (specialPurposeFolders->contains(purpose)) { + auto f = specialPurposeFolders->value(purpose); + Trace() << "Merging specialpurpose folder with: " << f << " with purpose: " << purpose; + *rid = f.toUtf8(); + return KAsync::null(); + } + Trace() << "No match found for merging, creating a new folder"; + return imap->createSubfolder(parentFolder, folder.getName()) + .then([imap, rid](const QString &createdFolder) { + Trace() << "Finished creating a new folder: " << createdFolder; + *rid = createdFolder.toUtf8(); + }); + + }) .then([rid](){ return *rid; }); + return mergeJob; + } } else if (operation == Sink::Operation_Removal) { Trace() << "Removing a folder: " << oldRemoteId; return login.then(imap->remove(oldRemoteId)) @@ -495,7 +603,7 @@ ImapResource::ImapResource(const QByteArray &instanceIdentifier, const QSharedPo changereplay->mPassword = mPassword; setupChangereplay(changereplay); - setupPreprocessors(ENTITY_TYPE_MAIL, QVector() << new MailPropertyExtractor << new DefaultIndexUpdater); + setupPreprocessors(ENTITY_TYPE_MAIL, QVector() << new DraftsProcessor << new MailPropertyExtractor << new DefaultIndexUpdater); setupPreprocessors(ENTITY_TYPE_FOLDER, QVector() << new DefaultIndexUpdater); } diff --git a/examples/imapresource/tests/resetmailbox.sh b/examples/imapresource/tests/resetmailbox.sh index 5e52d6f..8834b51 100644 --- a/examples/imapresource/tests/resetmailbox.sh +++ b/examples/imapresource/tests/resetmailbox.sh @@ -3,6 +3,7 @@ sudo echo "sam user.doe.* cyrus c" | cyradm --auth PLAIN -u cyrus -w admin localhost sudo echo "dm user.doe.*" | cyradm --auth PLAIN -u cyrus -w admin localhost sudo echo "cm user.doe.test" | cyradm --auth PLAIN -u cyrus -w admin localhost +sudo echo "cm user.doe.Drafts" | cyradm --auth PLAIN -u cyrus -w admin localhost sudo echo "sam user.doe cyrus c" | cyradm --auth PLAIN -u cyrus -w admin localhost sudo cp /work/source/Sink/examples/imapresource/tests/data/1365777830.R28.localhost.localdomain\:2\,S /var/spool/imap/d/user/doe/test/1. sudo chown cyrus:mail /var/spool/imap/d/user/doe/test/1. diff --git a/tests/mailsynctest.cpp b/tests/mailsynctest.cpp index 6c91381..ff56030 100644 --- a/tests/mailsynctest.cpp +++ b/tests/mailsynctest.cpp @@ -80,7 +80,7 @@ void MailSyncTest::testListFolders() Sink::Query query; query.resources << mResourceInstanceIdentifier; - query.request(); + query.request().request(); // Ensure all local data is processed VERIFYEXEC(Store::synchronize(query)); @@ -88,16 +88,21 @@ void MailSyncTest::testListFolders() auto job = Store::fetchAll(query).then>([=](const QList &folders) { QStringList names; + QHash specialPurposeFolders; for (const auto &folder : folders) { names << folder->getName(); + for (const auto &purpose : folder->getSpecialPurpose()) { + specialPurposeFolders.insert(purpose, folder->identifier()); + } } //Workaround for maildir if (names.contains("maildir1")) { names.removeAll("maildir1"); } if (mCapabilities.contains(ResourceCapabilities::Mail::drafts)) { - QVERIFY(names.contains("drafts")); - names.removeAll("drafts"); + QVERIFY(names.contains("Drafts")); + names.removeAll("Drafts"); + QVERIFY(specialPurposeFolders.contains("drafts")); } QCOMPARE(names.size(), 2); QVERIFY(names.contains("INBOX")); @@ -183,8 +188,8 @@ void MailSyncTest::testListFolderHierarchy() names.removeAll("maildir1"); } if (mCapabilities.contains(ResourceCapabilities::Mail::drafts)) { - QVERIFY(names.contains("drafts")); - names.removeAll("drafts"); + QVERIFY(names.contains("Drafts")); + names.removeAll("Drafts"); } QCOMPARE(names.size(), 3); QCOMPARE(map.value("sub")->getParent(), map.value("test")->identifier()); @@ -195,7 +200,7 @@ void MailSyncTest::testListFolderHierarchy() void MailSyncTest::testListNewSubFolder() { if (!mCapabilities.contains(ResourceCapabilities::Mail::folderhierarchy)) { - QSKIP("Missing capability folder.hierarchy"); + QSKIP("Missing capability mail.folderhierarchy"); } Sink::Query query; query.resources << mResourceInstanceIdentifier; -- cgit v1.2.3