summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChristian Mollekopf <chrigi_1@fastmail.fm>2016-06-10 15:49:48 +0200
committerChristian Mollekopf <chrigi_1@fastmail.fm>2016-06-10 15:49:48 +0200
commit638e75d6f3d00fb473fd45e325fcfb34c6340c65 (patch)
tree4db057d7dff07c9e03cf5a732c660e705c17327a
parentce0feb3ef62c9438b0aedd601461cbb340faa021 (diff)
downloadsink-638e75d6f3d00fb473fd45e325fcfb34c6340c65.tar.gz
sink-638e75d6f3d00fb473fd45e325fcfb34c6340c65.zip
Create the drafts folder if necessary and merge it with the source
version
-rw-r--r--common/synchronizer.cpp61
-rw-r--r--common/synchronizer.h3
-rw-r--r--examples/imapresource/imapresource.cpp128
-rw-r--r--examples/imapresource/tests/resetmailbox.sh1
-rw-r--r--tests/mailsynctest.cpp17
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
164 } 164 }
165} 165}
166 166
167template<typename DomainType>
168void Synchronizer::createOrModify(const QByteArray &bufferType, const QByteArray &remoteId, const DomainType &entity, const QHash<QByteArray, Sink::Query::Comparator> &mergeCriteria)
169{
170
171 Trace() << "Create or modify" << bufferType << remoteId;
172 auto mainDatabase = Storage::mainDatabase(transaction(), bufferType);
173 const auto sinkId = syncStore().resolveRemoteId(bufferType, remoteId);
174 const auto found = mainDatabase.contains(sinkId);
175 auto adaptorFactory = Sink::AdaptorFactoryRegistry::instance().getFactory(mResourceType, bufferType);
176 if (!found) {
177 if (!mergeCriteria.isEmpty()) {
178 Sink::Query query;
179 query.propertyFilter = mergeCriteria;
180 bool merge = false;
181 Sink::EntityReader<DomainType> reader(mResourceInstanceIdentifier, mResourceType, transaction());
182 reader.query(query,
183 [this, bufferType, remoteId, &merge](const DomainType &o) -> bool{
184 merge = true;
185 Trace() << "Merging local entity with remote entity: " << o.identifier() << remoteId;
186 syncStore().recordRemoteId(bufferType, o.identifier(), remoteId);
187 return false;
188 });
189 if (!merge) {
190 Trace() << "Found a new entity: " << remoteId;
191 createEntity(
192 sinkId, bufferType, entity, *adaptorFactory, [this](const QByteArray &buffer) { enqueueCommand(Sink::Commands::CreateEntityCommand, buffer); });
193 }
194 } else {
195 Trace() << "Found a new entity: " << remoteId;
196 createEntity(
197 sinkId, bufferType, entity, *adaptorFactory, [this](const QByteArray &buffer) { enqueueCommand(Sink::Commands::CreateEntityCommand, buffer); });
198 }
199 } else { // modification
200 qint64 retrievedRevision = 0;
201 if (auto current = EntityReaderUtils::getLatest(mainDatabase, sinkId, *adaptorFactory, retrievedRevision)) {
202 bool changed = false;
203 for (const auto &property : entity.changedProperties()) {
204 if (entity.getProperty(property) != current->getProperty(property)) {
205 Trace() << "Property changed " << sinkId << property;
206 changed = true;
207 }
208 }
209 if (changed) {
210 Trace() << "Found a modified entity: " << remoteId;
211 modifyEntity(sinkId, Sink::Storage::maxRevision(transaction()), bufferType, entity, *adaptorFactory,
212 [this](const QByteArray &buffer) { enqueueCommand(Sink::Commands::ModifyEntityCommand, buffer); });
213 }
214 } else {
215 Warning() << "Failed to get current entity";
216 }
217 }
218}
219
167KAsync::Job<void> Synchronizer::synchronize() 220KAsync::Job<void> Synchronizer::synchronize()
168{ 221{
169 Trace() << "Synchronizing"; 222 Trace() << "Synchronizing";
@@ -202,3 +255,11 @@ Sink::Storage::Transaction &Synchronizer::syncTransaction()
202 } 255 }
203 return mSyncTransaction; 256 return mSyncTransaction;
204} 257}
258
259#define REGISTER_TYPE(T) \
260 template void Synchronizer::createOrModify(const QByteArray &bufferType, const QByteArray &remoteId, const T &entity, const QHash<QByteArray, Sink::Query::Comparator> &mergeCriteria)
261
262REGISTER_TYPE(ApplicationDomain::Event);
263REGISTER_TYPE(ApplicationDomain::Mail);
264REGISTER_TYPE(ApplicationDomain::Folder);
265
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 @@
23#include <QObject> 23#include <QObject>
24#include <Async/Async> 24#include <Async/Async>
25#include <domainadaptor.h> 25#include <domainadaptor.h>
26#include <query.h>
26 27
27#include "storage.h" 28#include "storage.h"
28 29
@@ -81,6 +82,8 @@ protected:
81 * Depending on whether the entity is locally available, or has changed. 82 * Depending on whether the entity is locally available, or has changed.
82 */ 83 */
83 void createOrModify(const QByteArray &bufferType, const QByteArray &remoteId, const Sink::ApplicationDomain::ApplicationDomainType &entity); 84 void createOrModify(const QByteArray &bufferType, const QByteArray &remoteId, const Sink::ApplicationDomain::ApplicationDomainType &entity);
85 template <typename DomainType>
86 void createOrModify(const QByteArray &bufferType, const QByteArray &remoteId, const DomainType &entity, const QHash<QByteArray, Sink::Query::Comparator> &mergeCriteria);
84 87
85 virtual KAsync::Job<void> synchronizeWithSource() = 0; 88 virtual KAsync::Job<void> synchronizeWithSource() = 0;
86 89
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 @@
39#include "sourcewriteback.h" 39#include "sourcewriteback.h"
40#include "entitystore.h" 40#include "entitystore.h"
41#include "remoteidmap.h" 41#include "remoteidmap.h"
42#include "query.h"
42#include <QDate> 43#include <QDate>
43#include <QUuid> 44#include <QUuid>
44#include <QDir> 45#include <QDir>
45#include <QDirIterator> 46#include <QDirIterator>
46 47
47#include "imapserverproxy.h" 48#include "imapserverproxy.h"
49#include "entityreader.h"
48 50
49//This is the resources entity type, and not the domain type 51//This is the resources entity type, and not the domain type
50#define ENTITY_TYPE_MAIL "mail" 52#define ENTITY_TYPE_MAIL "mail"
@@ -56,6 +58,77 @@
56using namespace Imap; 58using namespace Imap;
57using namespace Sink; 59using namespace Sink;
58 60
61static QHash<QByteArray, QString> specialPurposeFolders()
62{
63 QHash<QByteArray, QString> hash;
64 //FIXME localize
65 hash.insert("drafts", "Drafts");
66 return hash;
67}
68
69static QHash<QString, QByteArray> specialPurposeNames()
70{
71 QHash<QString, QByteArray> hash;
72 for (const auto &value : specialPurposeFolders().values()) {
73 hash.insert(value.toLower(), specialPurposeFolders().key(value));
74 }
75 return hash;
76}
77
78//specialpurpose, name
79static QHash<QByteArray, QString> sSpecialPurposeFolders = specialPurposeFolders();
80//Lowercase-name, specialpurpose
81static QHash<QString, QByteArray> sSpecialPurposeNames = specialPurposeNames();
82
83class DraftsProcessor : public Sink::Preprocessor
84{
85public:
86 DraftsProcessor() {}
87
88 QByteArray ensureDraftsFolder(Sink::Storage::Transaction &transaction)
89 {
90 if (mDraftsFolder.isEmpty()) {
91 //Try to find an existing drafts folder
92 Sink::EntityReader<ApplicationDomain::Folder> reader(mResourceInstanceIdentifier, mResourceType, transaction);
93 reader.query(Sink::Query().filter<ApplicationDomain::Folder::SpecialPurpose>(Query::Comparator("drafts", Query::Comparator::Contains)),
94 [this](const ApplicationDomain::Folder &f) -> bool{
95 mDraftsFolder = f.identifier();
96 return false;
97 });
98 if (mDraftsFolder.isEmpty()) {
99 Trace() << "Failed to find a drafts folder, creating a new one";
100 auto folder = ApplicationDomain::Folder::create(mResourceInstanceIdentifier);
101 folder.setSpecialPurpose(QByteArrayList() << "drafts");
102 folder.setName(sSpecialPurposeFolders.value("drafts"));
103 folder.setIcon("folder");
104 //This processes the pipeline synchronously
105 createEntity(folder);
106 mDraftsFolder = folder.identifier();
107 }
108 }
109 return mDraftsFolder;
110 }
111
112 void newEntity(const QByteArray &uid, qint64 revision, Sink::ApplicationDomain::BufferAdaptor &newEntity, Sink::Storage::Transaction &transaction) Q_DECL_OVERRIDE
113 {
114 if (newEntity.getProperty("draft").toBool()) {
115 newEntity.setProperty("folder", ensureDraftsFolder(transaction));
116 }
117 }
118
119 void modifiedEntity(const QByteArray &uid, qint64 revision, const Sink::ApplicationDomain::BufferAdaptor &oldEntity, Sink::ApplicationDomain::BufferAdaptor &newEntity,
120 Sink::Storage::Transaction &transaction) Q_DECL_OVERRIDE
121 {
122 if (newEntity.getProperty("draft").toBool()) {
123 newEntity.setProperty("folder", ensureDraftsFolder(transaction));
124 }
125 }
126
127 QByteArray mDraftsFolder;
128 QByteArray mResourceInstanceIdentifier;
129 QByteArray mResourceType;
130};
131
59class MailPropertyExtractor : public Sink::Preprocessor 132class MailPropertyExtractor : public Sink::Preprocessor
60{ 133{
61public: 134public:
@@ -97,10 +170,6 @@ public:
97 updatedIndexedProperties(newEntity); 170 updatedIndexedProperties(newEntity);
98 } 171 }
99 172
100 void deletedEntity(const QByteArray &uid, qint64 revision, const Sink::ApplicationDomain::BufferAdaptor &oldEntity, Sink::Storage::Transaction &transaction) Q_DECL_OVERRIDE
101 {
102 }
103
104}; 173};
105 174
106static qint64 uidFromMailRid(const QByteArray &remoteId) 175static qint64 uidFromMailRid(const QByteArray &remoteId)
@@ -142,13 +211,19 @@ public:
142 const auto remoteId = folderPath.toUtf8(); 211 const auto remoteId = folderPath.toUtf8();
143 const auto bufferType = ENTITY_TYPE_FOLDER; 212 const auto bufferType = ENTITY_TYPE_FOLDER;
144 Sink::ApplicationDomain::Folder folder; 213 Sink::ApplicationDomain::Folder folder;
145 folder.setProperty("name", folderName); 214 folder.setProperty(ApplicationDomain::Folder::Name::name, folderName);
146 folder.setProperty("icon", icon); 215 folder.setProperty(ApplicationDomain::Folder::Icon::name, icon);
216 QHash<QByteArray, Query::Comparator> mergeCriteria;
217 if (sSpecialPurposeNames.contains(folderName.toLower())) {
218 auto type = sSpecialPurposeNames.value(folderName.toLower());
219 folder.setProperty(ApplicationDomain::Folder::SpecialPurpose::name, QVariant::fromValue(QByteArrayList() << type));
220 mergeCriteria.insert(ApplicationDomain::Folder::SpecialPurpose::name, Query::Comparator(type, Query::Comparator::Contains));
221 }
147 222
148 if (!parentFolderRid.isEmpty()) { 223 if (!parentFolderRid.isEmpty()) {
149 folder.setProperty("parent", syncStore().resolveRemoteId(ENTITY_TYPE_FOLDER, parentFolderRid.toUtf8())); 224 folder.setProperty("parent", syncStore().resolveRemoteId(ENTITY_TYPE_FOLDER, parentFolderRid.toUtf8()));
150 } 225 }
151 createOrModify(bufferType, remoteId, folder); 226 createOrModify(bufferType, remoteId, folder, mergeCriteria);
152 return remoteId; 227 return remoteId;
153 } 228 }
154 229
@@ -434,14 +509,47 @@ public:
434 } 509 }
435 Trace() << "Creating a new folder: " << parentFolder << folder.getName(); 510 Trace() << "Creating a new folder: " << parentFolder << folder.getName();
436 auto rid = QSharedPointer<QByteArray>::create(); 511 auto rid = QSharedPointer<QByteArray>::create();
437 return login.then<QString>(imap->createSubfolder(parentFolder, folder.getName())) 512 auto createFolder = login.then<QString>(imap->createSubfolder(parentFolder, folder.getName()))
438 .then<void, QString>([imap, rid](const QString &createdFolder) { 513 .then<void, QString>([imap, rid](const QString &createdFolder) {
439 Trace() << "Finished creating a new folder: " << createdFolder; 514 Trace() << "Finished creating a new folder: " << createdFolder;
440 *rid = createdFolder.toUtf8(); 515 *rid = createdFolder.toUtf8();
441 }) 516 });
517 if (folder.getSpecialPurpose().isEmpty()) {
518 return createFolder
519 .then<QByteArray>([rid](){
520 return *rid;
521 });
522 } else { //We try to merge special purpose folders first
523 auto specialPurposeFolders = QSharedPointer<QHash<QByteArray, QString>>::create();
524 auto mergeJob = imap->login(mUser, mPassword)
525 .then<void>(imap->fetchFolders([=](const QVector<Imap::Folder> &folders) {
526 for (const auto &f : folders) {
527 if (sSpecialPurposeNames.contains(f.pathParts.last().toLower())) {
528 specialPurposeFolders->insert(sSpecialPurposeNames.value(f.pathParts.last().toLower()), f.path);
529 };
530 }
531 }))
532 .then<void, KAsync::Job<void>>([specialPurposeFolders, folder, imap, parentFolder, rid]() -> KAsync::Job<void> {
533 for (const auto &purpose : folder.getSpecialPurpose()) {
534 if (specialPurposeFolders->contains(purpose)) {
535 auto f = specialPurposeFolders->value(purpose);
536 Trace() << "Merging specialpurpose folder with: " << f << " with purpose: " << purpose;
537 *rid = f.toUtf8();
538 return KAsync::null<void>();
539 }
540 Trace() << "No match found for merging, creating a new folder";
541 return imap->createSubfolder(parentFolder, folder.getName())
542 .then<void, QString>([imap, rid](const QString &createdFolder) {
543 Trace() << "Finished creating a new folder: " << createdFolder;
544 *rid = createdFolder.toUtf8();
545 });
546
547 })
442 .then<QByteArray>([rid](){ 548 .then<QByteArray>([rid](){
443 return *rid; 549 return *rid;
444 }); 550 });
551 return mergeJob;
552 }
445 } else if (operation == Sink::Operation_Removal) { 553 } else if (operation == Sink::Operation_Removal) {
446 Trace() << "Removing a folder: " << oldRemoteId; 554 Trace() << "Removing a folder: " << oldRemoteId;
447 return login.then<void>(imap->remove(oldRemoteId)) 555 return login.then<void>(imap->remove(oldRemoteId))
@@ -495,7 +603,7 @@ ImapResource::ImapResource(const QByteArray &instanceIdentifier, const QSharedPo
495 changereplay->mPassword = mPassword; 603 changereplay->mPassword = mPassword;
496 setupChangereplay(changereplay); 604 setupChangereplay(changereplay);
497 605
498 setupPreprocessors(ENTITY_TYPE_MAIL, QVector<Sink::Preprocessor*>() << new MailPropertyExtractor << new DefaultIndexUpdater<Sink::ApplicationDomain::Mail>); 606 setupPreprocessors(ENTITY_TYPE_MAIL, QVector<Sink::Preprocessor*>() << new DraftsProcessor << new MailPropertyExtractor << new DefaultIndexUpdater<Sink::ApplicationDomain::Mail>);
499 setupPreprocessors(ENTITY_TYPE_FOLDER, QVector<Sink::Preprocessor*>() << new DefaultIndexUpdater<Sink::ApplicationDomain::Folder>); 607 setupPreprocessors(ENTITY_TYPE_FOLDER, QVector<Sink::Preprocessor*>() << new DefaultIndexUpdater<Sink::ApplicationDomain::Folder>);
500} 608}
501 609
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 @@
3sudo echo "sam user.doe.* cyrus c" | cyradm --auth PLAIN -u cyrus -w admin localhost 3sudo echo "sam user.doe.* cyrus c" | cyradm --auth PLAIN -u cyrus -w admin localhost
4sudo echo "dm user.doe.*" | cyradm --auth PLAIN -u cyrus -w admin localhost 4sudo echo "dm user.doe.*" | cyradm --auth PLAIN -u cyrus -w admin localhost
5sudo echo "cm user.doe.test" | cyradm --auth PLAIN -u cyrus -w admin localhost 5sudo echo "cm user.doe.test" | cyradm --auth PLAIN -u cyrus -w admin localhost
6sudo echo "cm user.doe.Drafts" | cyradm --auth PLAIN -u cyrus -w admin localhost
6sudo echo "sam user.doe cyrus c" | cyradm --auth PLAIN -u cyrus -w admin localhost 7sudo echo "sam user.doe cyrus c" | cyradm --auth PLAIN -u cyrus -w admin localhost
7sudo cp /work/source/Sink/examples/imapresource/tests/data/1365777830.R28.localhost.localdomain\:2\,S /var/spool/imap/d/user/doe/test/1. 8sudo cp /work/source/Sink/examples/imapresource/tests/data/1365777830.R28.localhost.localdomain\:2\,S /var/spool/imap/d/user/doe/test/1.
8sudo chown cyrus:mail /var/spool/imap/d/user/doe/test/1. 9sudo 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()
80 80
81 Sink::Query query; 81 Sink::Query query;
82 query.resources << mResourceInstanceIdentifier; 82 query.resources << mResourceInstanceIdentifier;
83 query.request<Folder::Name>(); 83 query.request<Folder::Name>().request<Folder::SpecialPurpose>();
84 84
85 // Ensure all local data is processed 85 // Ensure all local data is processed
86 VERIFYEXEC(Store::synchronize(query)); 86 VERIFYEXEC(Store::synchronize(query));
@@ -88,16 +88,21 @@ void MailSyncTest::testListFolders()
88 88
89 auto job = Store::fetchAll<Folder>(query).then<void, QList<Folder::Ptr>>([=](const QList<Folder::Ptr> &folders) { 89 auto job = Store::fetchAll<Folder>(query).then<void, QList<Folder::Ptr>>([=](const QList<Folder::Ptr> &folders) {
90 QStringList names; 90 QStringList names;
91 QHash<QByteArray, QByteArray> specialPurposeFolders;
91 for (const auto &folder : folders) { 92 for (const auto &folder : folders) {
92 names << folder->getName(); 93 names << folder->getName();
94 for (const auto &purpose : folder->getSpecialPurpose()) {
95 specialPurposeFolders.insert(purpose, folder->identifier());
96 }
93 } 97 }
94 //Workaround for maildir 98 //Workaround for maildir
95 if (names.contains("maildir1")) { 99 if (names.contains("maildir1")) {
96 names.removeAll("maildir1"); 100 names.removeAll("maildir1");
97 } 101 }
98 if (mCapabilities.contains(ResourceCapabilities::Mail::drafts)) { 102 if (mCapabilities.contains(ResourceCapabilities::Mail::drafts)) {
99 QVERIFY(names.contains("drafts")); 103 QVERIFY(names.contains("Drafts"));
100 names.removeAll("drafts"); 104 names.removeAll("Drafts");
105 QVERIFY(specialPurposeFolders.contains("drafts"));
101 } 106 }
102 QCOMPARE(names.size(), 2); 107 QCOMPARE(names.size(), 2);
103 QVERIFY(names.contains("INBOX")); 108 QVERIFY(names.contains("INBOX"));
@@ -183,8 +188,8 @@ void MailSyncTest::testListFolderHierarchy()
183 names.removeAll("maildir1"); 188 names.removeAll("maildir1");
184 } 189 }
185 if (mCapabilities.contains(ResourceCapabilities::Mail::drafts)) { 190 if (mCapabilities.contains(ResourceCapabilities::Mail::drafts)) {
186 QVERIFY(names.contains("drafts")); 191 QVERIFY(names.contains("Drafts"));
187 names.removeAll("drafts"); 192 names.removeAll("Drafts");
188 } 193 }
189 QCOMPARE(names.size(), 3); 194 QCOMPARE(names.size(), 3);
190 QCOMPARE(map.value("sub")->getParent(), map.value("test")->identifier()); 195 QCOMPARE(map.value("sub")->getParent(), map.value("test")->identifier());
@@ -195,7 +200,7 @@ void MailSyncTest::testListFolderHierarchy()
195void MailSyncTest::testListNewSubFolder() 200void MailSyncTest::testListNewSubFolder()
196{ 201{
197 if (!mCapabilities.contains(ResourceCapabilities::Mail::folderhierarchy)) { 202 if (!mCapabilities.contains(ResourceCapabilities::Mail::folderhierarchy)) {
198 QSKIP("Missing capability folder.hierarchy"); 203 QSKIP("Missing capability mail.folderhierarchy");
199 } 204 }
200 Sink::Query query; 205 Sink::Query query;
201 query.resources << mResourceInstanceIdentifier; 206 query.resources << mResourceInstanceIdentifier;