summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChristian Mollekopf <chrigi_1@fastmail.fm>2016-05-22 13:10:39 +0200
committerChristian Mollekopf <chrigi_1@fastmail.fm>2016-05-22 13:10:39 +0200
commit6864e4accaafa4fa90332719bff5a85a0e92b242 (patch)
tree31c0d6df98112674e55ea5ad636c2ad780add49c
parentde52c17a7a08e72affc4c182fb1650d18d8b3b2b (diff)
downloadsink-6864e4accaafa4fa90332719bff5a85a0e92b242.tar.gz
sink-6864e4accaafa4fa90332719bff5a85a0e92b242.zip
ImapResource prototype
-rw-r--r--common/domain/applicationdomaintype.cpp11
-rw-r--r--common/domain/applicationdomaintype.h3
-rw-r--r--examples/CMakeLists.txt1
-rw-r--r--examples/imapresource/CMakeLists.txt18
-rw-r--r--examples/imapresource/domainadaptor.cpp35
-rw-r--r--examples/imapresource/domainadaptor.h38
-rw-r--r--examples/imapresource/facade.cpp44
-rw-r--r--examples/imapresource/facade.h36
-rw-r--r--examples/imapresource/imapresource.cpp277
-rw-r--r--examples/imapresource/imapresource.h71
-rw-r--r--examples/imapresource/imapserverproxy.cpp209
-rw-r--r--examples/imapresource/imapserverproxy.h62
-rw-r--r--examples/imapresource/tests/CMakeLists.txt21
-rw-r--r--examples/imapresource/tests/imapresourcetest.cpp162
-rw-r--r--examples/imapresource/tests/resetmailbox.sh10
15 files changed, 998 insertions, 0 deletions
diff --git a/common/domain/applicationdomaintype.cpp b/common/domain/applicationdomaintype.cpp
index 166d3e6..7be0295 100644
--- a/common/domain/applicationdomaintype.cpp
+++ b/common/domain/applicationdomaintype.cpp
@@ -261,6 +261,17 @@ namespace MailtransportResource {
261 } 261 }
262} 262}
263 263
264namespace ImapResource {
265 SinkResource create(const QByteArray &account)
266 {
267 auto &&resource = ApplicationDomainType::createEntity<SinkResource>();
268 resource.setProperty("type", "org.kde.imap");
269 resource.setProperty("account", account);
270 resource.setProperty("capabilities", QVariant::fromValue(QByteArrayList() << "storage" << "drafts"));
271 return resource;
272 }
273}
274
264template<> 275template<>
265QByteArray getTypeName<Event>() 276QByteArray getTypeName<Event>()
266{ 277{
diff --git a/common/domain/applicationdomaintype.h b/common/domain/applicationdomaintype.h
index 4eec4a3..1b0ae9d 100644
--- a/common/domain/applicationdomaintype.h
+++ b/common/domain/applicationdomaintype.h
@@ -245,6 +245,9 @@ namespace MaildirResource {
245namespace MailtransportResource { 245namespace MailtransportResource {
246 SinkResource SINK_EXPORT create(const QByteArray &account); 246 SinkResource SINK_EXPORT create(const QByteArray &account);
247}; 247};
248namespace ImapResource {
249 SinkResource SINK_EXPORT create(const QByteArray &account);
250};
248 251
249/** 252/**
250 * All types need to be registered here an MUST return a different name. 253 * All types need to be registered here an MUST return a different name.
diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt
index d5fcacf..59753bb 100644
--- a/examples/CMakeLists.txt
+++ b/examples/CMakeLists.txt
@@ -7,5 +7,6 @@ add_subdirectory(dummyresource)
7if (BUILD_MAILDIR) 7if (BUILD_MAILDIR)
8 # a maildir resource implementation 8 # a maildir resource implementation
9 add_subdirectory(maildirresource) 9 add_subdirectory(maildirresource)
10 add_subdirectory(imapresource)
10endif() 11endif()
11add_subdirectory(mailtransportresource) 12add_subdirectory(mailtransportresource)
diff --git a/examples/imapresource/CMakeLists.txt b/examples/imapresource/CMakeLists.txt
new file mode 100644
index 0000000..d5320a6
--- /dev/null
+++ b/examples/imapresource/CMakeLists.txt
@@ -0,0 +1,18 @@
1project(sink_resource_imap)
2
3add_definitions(-DQT_PLUGIN)
4include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR})
5
6find_package(KF5 COMPONENTS REQUIRED Mime IMAP KIO)
7find_package(KF5CoreAddons REQUIRED)
8
9include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR})
10
11add_library(${PROJECT_NAME} SHARED facade.cpp imapresource.cpp domainadaptor.cpp imapserverproxy.cpp)
12qt5_use_modules(${PROJECT_NAME} Core Network)
13#We need CoreAddons for KJob and KIOCore for KTcpSocket. Both used in KIMAP
14target_link_libraries(${PROJECT_NAME} sink KF5::Mime KF5::IMAP KF5::CoreAddons KF5::KIOCore)
15
16install(TARGETS ${PROJECT_NAME} LIBRARY DESTINATION ${SINK_RESOURCE_PLUGINS_PATH})
17
18add_subdirectory(tests)
diff --git a/examples/imapresource/domainadaptor.cpp b/examples/imapresource/domainadaptor.cpp
new file mode 100644
index 0000000..4e74ad2
--- /dev/null
+++ b/examples/imapresource/domainadaptor.cpp
@@ -0,0 +1,35 @@
1/*
2 * Copyright (C) 2015 Christian Mollekopf <chrigi_1@fastmail.fm>
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the
16 * Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 */
19
20#include "domainadaptor.h"
21
22using namespace flatbuffers;
23
24ImapMailAdaptorFactory::ImapMailAdaptorFactory()
25 : DomainTypeAdaptorFactory()
26{
27
28}
29
30ImapFolderAdaptorFactory::ImapFolderAdaptorFactory()
31 : DomainTypeAdaptorFactory()
32{
33
34}
35
diff --git a/examples/imapresource/domainadaptor.h b/examples/imapresource/domainadaptor.h
new file mode 100644
index 0000000..06a513c
--- /dev/null
+++ b/examples/imapresource/domainadaptor.h
@@ -0,0 +1,38 @@
1/*
2 * Copyright (C) 2015 Christian Mollekopf <chrigi_1@fastmail.fm>
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the
16 * Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 */
19#pragma once
20
21#include <common/domainadaptor.h>
22#include "mail_generated.h"
23#include "folder_generated.h"
24#include "dummy_generated.h"
25
26class ImapMailAdaptorFactory : public DomainTypeAdaptorFactory<Sink::ApplicationDomain::Mail, Sink::ApplicationDomain::Buffer::Dummy, Sink::ApplicationDomain::Buffer::DummyBuilder>
27{
28public:
29 ImapMailAdaptorFactory();
30 virtual ~ImapMailAdaptorFactory() {};
31};
32
33class ImapFolderAdaptorFactory : public DomainTypeAdaptorFactory<Sink::ApplicationDomain::Folder, Sink::ApplicationDomain::Buffer::Dummy, Sink::ApplicationDomain::Buffer::DummyBuilder>
34{
35public:
36 ImapFolderAdaptorFactory();
37 virtual ~ImapFolderAdaptorFactory() {};
38};
diff --git a/examples/imapresource/facade.cpp b/examples/imapresource/facade.cpp
new file mode 100644
index 0000000..d338b01
--- /dev/null
+++ b/examples/imapresource/facade.cpp
@@ -0,0 +1,44 @@
1/*
2 * Copyright (C) 2014 Christian Mollekopf <chrigi_1@fastmail.fm>
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the
16 * Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 */
19
20#include "facade.h"
21
22#include <QDir>
23#include <QFileInfo>
24
25#include "domainadaptor.h"
26#include "queryrunner.h"
27
28ImapResourceMailFacade::ImapResourceMailFacade(const QByteArray &instanceIdentifier)
29 : Sink::GenericFacade<Sink::ApplicationDomain::Mail>(instanceIdentifier, QSharedPointer<ImapMailAdaptorFactory>::create())
30{
31}
32
33ImapResourceMailFacade::~ImapResourceMailFacade()
34{
35}
36
37ImapResourceFolderFacade::ImapResourceFolderFacade(const QByteArray &instanceIdentifier)
38 : Sink::GenericFacade<Sink::ApplicationDomain::Folder>(instanceIdentifier, QSharedPointer<ImapFolderAdaptorFactory>::create())
39{
40}
41
42ImapResourceFolderFacade::~ImapResourceFolderFacade()
43{
44}
diff --git a/examples/imapresource/facade.h b/examples/imapresource/facade.h
new file mode 100644
index 0000000..479ad96
--- /dev/null
+++ b/examples/imapresource/facade.h
@@ -0,0 +1,36 @@
1/*
2 * Copyright (C) 2014 Christian Mollekopf <chrigi_1@fastmail.fm>
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the
16 * Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 */
19
20#pragma once
21
22#include "common/facade.h"
23
24class ImapResourceMailFacade : public Sink::GenericFacade<Sink::ApplicationDomain::Mail>
25{
26public:
27 ImapResourceMailFacade(const QByteArray &instanceIdentifier);
28 virtual ~ImapResourceMailFacade();
29};
30
31class ImapResourceFolderFacade : public Sink::GenericFacade<Sink::ApplicationDomain::Folder>
32{
33public:
34 ImapResourceFolderFacade(const QByteArray &instanceIdentifier);
35 virtual ~ImapResourceFolderFacade();
36};
diff --git a/examples/imapresource/imapresource.cpp b/examples/imapresource/imapresource.cpp
new file mode 100644
index 0000000..baa88b9
--- /dev/null
+++ b/examples/imapresource/imapresource.cpp
@@ -0,0 +1,277 @@
1/*
2 * Copyright (C) 2015 Christian Mollekopf <chrigi_1@fastmail.fm>
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the
16 * Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 */
19
20#include "imapresource.h"
21#include "facade.h"
22#include "entitybuffer.h"
23#include "pipeline.h"
24#include "mail_generated.h"
25#include "createentity_generated.h"
26#include "modifyentity_generated.h"
27#include "deleteentity_generated.h"
28#include "domainadaptor.h"
29#include "resourceconfig.h"
30#include "commands.h"
31#include "index.h"
32#include "log.h"
33#include "domain/mail.h"
34#include "definitions.h"
35#include "facadefactory.h"
36#include "indexupdater.h"
37#include "inspection.h"
38#include <QDate>
39#include <QUuid>
40#include <QDir>
41#include <QDirIterator>
42
43#include "imapserverproxy.h"
44
45//This is the resources entity type, and not the domain type
46#define ENTITY_TYPE_MAIL "mail"
47#define ENTITY_TYPE_FOLDER "folder"
48
49#undef DEBUG_AREA
50#define DEBUG_AREA "resource.imap"
51
52
53ImapResource::ImapResource(const QByteArray &instanceIdentifier, const QSharedPointer<Sink::Pipeline> &pipeline)
54 : Sink::GenericResource(instanceIdentifier, pipeline),
55 mMailAdaptorFactory(QSharedPointer<ImapMailAdaptorFactory>::create()),
56 mFolderAdaptorFactory(QSharedPointer<ImapFolderAdaptorFactory>::create())
57{
58 auto config = ResourceConfig::getConfiguration(instanceIdentifier);
59 mServer = config.value("server").toString();
60 mPort = config.value("port").toInt();
61
62 // auto folderUpdater = new FolderUpdater(QByteArray());
63 addType(ENTITY_TYPE_MAIL, mMailAdaptorFactory,
64 QVector<Sink::Preprocessor*>() << new DefaultIndexUpdater<Sink::ApplicationDomain::Mail>);
65 addType(ENTITY_TYPE_FOLDER, mFolderAdaptorFactory,
66 QVector<Sink::Preprocessor*>() << new DefaultIndexUpdater<Sink::ApplicationDomain::Folder>);
67}
68
69QByteArray ImapResource::createFolder(const QString &folderPath, const QByteArray &icon, Sink::Storage::Transaction &transaction, Sink::Storage::Transaction &synchronizationTransaction)
70{
71 auto remoteId = folderPath.toUtf8();
72 auto bufferType = ENTITY_TYPE_FOLDER;
73 Sink::ApplicationDomain::Folder folder;
74 folder.setProperty("name", folderPath.split('/').last());
75 folder.setProperty("icon", icon);
76
77 // if (!md.isRoot()) {
78 // folder.setProperty("parent", resolveRemoteId(ENTITY_TYPE_FOLDER, md.parent().path().toUtf8(), synchronizationTransaction));
79 // }
80 createOrModify(transaction, synchronizationTransaction, *mFolderAdaptorFactory, bufferType, remoteId, folder);
81 return remoteId;
82}
83
84void ImapResource::synchronizeFolders(const QStringList &folderList, Sink::Storage::Transaction &transaction, Sink::Storage::Transaction &synchronizationTransaction)
85{
86 const QByteArray bufferType = ENTITY_TYPE_FOLDER;
87 Trace() << "Found folders " << folderList;
88
89 scanForRemovals(transaction, synchronizationTransaction, bufferType,
90 [&bufferType, &transaction](const std::function<void(const QByteArray &)> &callback) {
91 //TODO Instead of iterating over all entries in the database, which can also pick up the same item multiple times,
92 //we should rather iterate over an index that contains every uid exactly once. The remoteId index would be such an index,
93 //but we currently fail to iterate over all entries in an index it seems.
94 // auto remoteIds = synchronizationTransaction.openDatabase("rid.mapping." + bufferType, std::function<void(const Sink::Storage::Error &)>(), true);
95 auto mainDatabase = Sink::Storage::mainDatabase(transaction, bufferType);
96 mainDatabase.scan("", [&](const QByteArray &key, const QByteArray &) {
97 callback(key);
98 return true;
99 });
100 },
101 [&folderList](const QByteArray &remoteId) -> bool {
102 return folderList.contains(remoteId);
103 }
104 );
105
106 for (const auto folderPath : folderList) {
107 createFolder(folderPath, "folder", transaction, synchronizationTransaction);
108 }
109}
110
111void ImapResource::synchronizeMails(Sink::Storage::Transaction &transaction, Sink::Storage::Transaction &synchronizationTransaction, const QString &path, const QVector<Message> &messages)
112{
113 auto time = QSharedPointer<QTime>::create();
114 time->start();
115 const QByteArray bufferType = ENTITY_TYPE_MAIL;
116
117
118 Trace() << "Importing new mail.";
119
120 // Trace() << "Looking into " << listingPath;
121
122 const auto folderLocalId = resolveRemoteId(ENTITY_TYPE_FOLDER, path.toUtf8(), synchronizationTransaction);
123
124 //This is not a full listing
125 // auto property = "folder";
126 // scanForRemovals(transaction, synchronizationTransaction, bufferType,
127 // [&](const std::function<void(const QByteArray &)> &callback) {
128 // Index index(bufferType + ".index." + property, transaction);
129 // index.lookup(folderLocalId, [&](const QByteArray &sinkId) {
130 // callback(sinkId);
131 // },
132 // [&](const Index::Error &error) {
133 // Warning() << "Error in index: " << error.message << property;
134 // });
135 // },
136 // [](const QByteArray &remoteId) -> bool {
137 // return QFile(remoteId).exists();
138 // }
139 // );
140
141 mSynchronizerQueue.startTransaction();
142 int count = 0;
143 for (const auto &message : messages) {
144 count++;
145 const auto remoteId = path.toUtf8() + "/" + QByteArray::number(message.uid);
146
147 Trace() << "Found a mail " << remoteId << message.msg->subject(true)->asUnicodeString() << message.flags;
148
149 Sink::ApplicationDomain::Mail mail;
150 mail.setFolder(folderLocalId);
151 //FIXME this should come from the mime message, extracted in the pipeline
152 mail.setExtractedSubject(message.msg->subject(true)->asUnicodeString());
153
154 auto filePath = Sink::resourceStorageLocation(mResourceInstanceIdentifier) + "/" + remoteId;
155 QDir().mkpath(Sink::resourceStorageLocation(mResourceInstanceIdentifier) + "/" + path.toUtf8());
156 QFile file(filePath);
157 if (!file.open(QIODevice::WriteOnly)) {
158 Warning() << "Failed to open file for writing: " << file.errorString();
159 }
160 const auto content = message.msg->encodedContent();
161 file.write(content);
162 mail.setMimeMessagePath(filePath);
163 //FIXME Not sure if these are the actual flags
164 mail.setUnread(message.flags.contains("\\SEEN"));
165 mail.setImportant(message.flags.contains("\\FLAGGED"));
166
167 createOrModify(transaction, synchronizationTransaction, *mMailAdaptorFactory, bufferType, remoteId, mail);
168 }
169 mSynchronizerQueue.commit();
170 const auto elapsed = time->elapsed();
171 Log() << "Synchronized " << count << " mails in " << path << Sink::Log::TraceTime(elapsed) << " " << elapsed/qMax(count, 1) << " [ms/mail]";
172}
173
174KAsync::Job<void> ImapResource::synchronizeWithSource(Sink::Storage &mainStore, Sink::Storage &synchronizationStore)
175{
176 Log() << " Synchronizing";
177 return KAsync::start<void>([this, &mainStore, &synchronizationStore](KAsync::Future<void> future) {
178 ImapServerProxy imap(mServer, mPort);
179 QStringList folderList;
180 // QList<KAsync::Future<void>> waitCondition;
181 auto folderFuture = imap.fetchFolders([this, &imap, &mainStore, &synchronizationStore, &folderList](const QStringList &folders) {
182 auto transaction = mainStore.createTransaction(Sink::Storage::ReadOnly);
183 auto syncTransaction = synchronizationStore.createTransaction(Sink::Storage::ReadWrite);
184 synchronizeFolders(folders, transaction, syncTransaction);
185 transaction.commit();
186 syncTransaction.commit();
187 folderList << folders;
188
189 });
190 folderFuture.waitForFinished();
191 if (folderFuture.errorCode()) {
192 future.setError(1, "Folder list sync failed");
193 return;
194 }
195
196 for (const auto &folder : folderList) {
197 // auto transaction = mainStore.createTransaction(Sink::Storage::ReadOnly);
198 // auto syncTransaction = synchronizationStore.createTransaction(Sink::Storage::ReadOnly);
199
200 //TODO load entity to read sync settings should we have some (if the folder is existing already)
201 //Note that this will not work if we change any of those settings in the pipeline
202 //
203 // auto mainDatabase = Sink::Storage::mainDatabase(transaction, ENTITY_TYPE_FOLDER);
204 // const auto sinkId = resolveRemoteId(ENTITY_TYPE_FOLDER, folder.toUtf8(), syncTransaction);
205 // const auto found = mainDatabase.contains(sinkId);
206 // if (found) {
207 // if (auto current = getLatest(mainDatabase, sinkId, mFolderAdaptorFactory)) {
208 //
209 // }
210 // }
211
212 // transaction.commit();
213 // syncTransaction.commit();
214
215 auto messagesFuture = imap.fetchMessages(folder, [this, &mainStore, &synchronizationStore, folder](const QVector<Message> &messages) {
216 auto transaction = mainStore.createTransaction(Sink::Storage::ReadOnly);
217 auto syncTransaction = synchronizationStore.createTransaction(Sink::Storage::ReadWrite);
218 Trace() << "Synchronizing mails" << folder;
219 synchronizeMails(transaction, syncTransaction, folder, messages);
220 transaction.commit();
221 syncTransaction.commit();
222 });
223 messagesFuture.waitForFinished();
224 if (messagesFuture.errorCode()) {
225 future.setError(1, "Folder sync failed: " + folder);
226 return;
227 }
228 }
229
230
231 // auto transaction = mainStore.createTransaction(Sink::Storage::ReadWrite);
232 // auto mainDatabase = Sink::Storage::mainDatabase(transaction, ENTITY_TYPE_FOLDER);
233 // mainDatabase.scan("", [&](const QByteArray &key, const QByteArray &data) {
234 // return true;
235 // });
236 //TODO now fetch all folders and iterate over them and synchronize each one
237
238 Log() << "Done Synchronizing";
239 future.setFinished();
240 });
241}
242
243KAsync::Job<void> ImapResource::replay(Sink::Storage &synchronizationStore, const QByteArray &type, const QByteArray &key, const QByteArray &value)
244{
245 //TODO implement
246 return KAsync::null<void>();
247}
248
249void ImapResource::removeFromDisk(const QByteArray &instanceIdentifier)
250{
251 GenericResource::removeFromDisk(instanceIdentifier);
252 Sink::Storage(Sink::storageLocation(), instanceIdentifier + ".synchronization", Sink::Storage::ReadWrite).removeFromDisk();
253}
254
255KAsync::Job<void> ImapResource::inspect(int inspectionType, const QByteArray &inspectionId, const QByteArray &domainType, const QByteArray &entityId, const QByteArray &property, const QVariant &expectedValue)
256{
257 //TODO
258 return KAsync::null<void>();
259}
260
261ImapResourceFactory::ImapResourceFactory(QObject *parent)
262 : Sink::ResourceFactory(parent)
263{
264
265}
266
267Sink::Resource *ImapResourceFactory::createResource(const QByteArray &instanceIdentifier)
268{
269 return new ImapResource(instanceIdentifier);
270}
271
272void ImapResourceFactory::registerFacades(Sink::FacadeFactory &factory)
273{
274 factory.registerFacade<Sink::ApplicationDomain::Mail, ImapResourceMailFacade>(PLUGIN_NAME);
275 factory.registerFacade<Sink::ApplicationDomain::Folder, ImapResourceFolderFacade>(PLUGIN_NAME);
276}
277
diff --git a/examples/imapresource/imapresource.h b/examples/imapresource/imapresource.h
new file mode 100644
index 0000000..6fe15dd
--- /dev/null
+++ b/examples/imapresource/imapresource.h
@@ -0,0 +1,71 @@
1/*
2 * Copyright (C) 2015 Christian Mollekopf <chrigi_1@fastmail.fm>
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the
16 * Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 */
19
20#pragma once
21
22#include "common/genericresource.h"
23
24#include <Async/Async>
25
26#include <flatbuffers/flatbuffers.h>
27
28//TODO: a little ugly to have this in two places, once here and once in Q_PLUGIN_METADATA
29#define PLUGIN_NAME "org.kde.imap"
30
31class ImapMailAdaptorFactory;
32class ImapFolderAdaptorFactory;
33struct Message;
34
35/**
36 * An imap resource.
37 */
38class ImapResource : public Sink::GenericResource
39{
40public:
41 ImapResource(const QByteArray &instanceIdentifier, const QSharedPointer<Sink::Pipeline> &pipeline = QSharedPointer<Sink::Pipeline>());
42 KAsync::Job<void> synchronizeWithSource(Sink::Storage &mainStore, Sink::Storage &synchronizationStore) Q_DECL_OVERRIDE;
43 KAsync::Job<void> inspect(int inspectionType, const QByteArray &inspectionId, const QByteArray &domainType, const QByteArray &entityId, const QByteArray &property, const QVariant &expectedValue) Q_DECL_OVERRIDE;
44 static void removeFromDisk(const QByteArray &instanceIdentifier);
45private:
46 KAsync::Job<void> replay(Sink::Storage &synchronizationStore, const QByteArray &type, const QByteArray &key, const QByteArray &value) Q_DECL_OVERRIDE;
47
48 QByteArray createFolder(const QString &folderPath, const QByteArray &icon, Sink::Storage::Transaction &transaction, Sink::Storage::Transaction &synchronizationTransaction);
49 void synchronizeFolders(const QStringList &folderList, Sink::Storage::Transaction &transaction, Sink::Storage::Transaction &synchronizationTransaction);
50 void synchronizeMails(Sink::Storage::Transaction &transaction, Sink::Storage::Transaction &synchronizationTransaction, const QString &path, const QVector<Message> &messages);
51
52 QSharedPointer<ImapMailAdaptorFactory> mMailAdaptorFactory;
53 QSharedPointer<ImapFolderAdaptorFactory> mFolderAdaptorFactory;
54private:
55 QString mServer;
56 int mPort;
57};
58
59class ImapResourceFactory : public Sink::ResourceFactory
60{
61 Q_OBJECT
62 Q_PLUGIN_METADATA(IID "org.kde.imap")
63 Q_INTERFACES(Sink::ResourceFactory)
64
65public:
66 ImapResourceFactory(QObject *parent = 0);
67
68 Sink::Resource *createResource(const QByteArray &instanceIdentifier) Q_DECL_OVERRIDE;
69 void registerFacades(Sink::FacadeFactory &factory) Q_DECL_OVERRIDE;
70};
71
diff --git a/examples/imapresource/imapserverproxy.cpp b/examples/imapresource/imapserverproxy.cpp
new file mode 100644
index 0000000..9630096
--- /dev/null
+++ b/examples/imapresource/imapserverproxy.cpp
@@ -0,0 +1,209 @@
1/*
2 * Copyright (C) 2015 Christian Mollekopf <chrigi_1@fastmail.fm>
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the
16 * Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 */
19#include "imapserverproxy.h"
20
21#include <QDir>
22#include <QFile>
23#include <KIMAP/KIMAP/LoginJob>
24#include <KIMAP/KIMAP/SelectJob>
25#include <KIMAP/KIMAP/AppendJob>
26
27#include <KIMAP/KIMAP/SessionUiProxy>
28#include <KCoreAddons/KJob>
29
30#include "log.h"
31
32static KAsync::Job<void> runJob(KJob *job)
33{
34 return KAsync::start<void>([job](KAsync::Future<void> &future) {
35 QObject::connect(job, &KJob::result, job, [&future](KJob *job) {
36 if (job->error()) {
37 Warning() << "Job failed: " << job->errorString();
38 future.setError(job->error(), job->errorString());
39 } else {
40 future.setFinished();
41 }
42 });
43 job->start();
44 });
45}
46
47class SessionUiProxy : public KIMAP::SessionUiProxy {
48 public:
49 bool ignoreSslError( const KSslErrorUiData &errorData ) {
50 return true;
51 }
52};
53
54ImapServerProxy::ImapServerProxy(const QString &serverUrl, int port) : mSession(new KIMAP::Session(serverUrl, port))
55{
56 mSession->setUiProxy(SessionUiProxy::Ptr(new SessionUiProxy));
57 mSession->setTimeout(10);
58}
59
60KAsync::Job<void> ImapServerProxy::login(const QString &username, const QString &password)
61{
62 if (mSession->state() == KIMAP::Session::State::Authenticated || mSession->state() == KIMAP::Session::State::Selected) {
63 return KAsync::null<void>();
64 }
65 auto loginJob = new KIMAP::LoginJob(mSession);
66 loginJob->setUserName(username);
67 loginJob->setPassword(password);
68 loginJob->setAuthenticationMode(KIMAP::LoginJob::Plain);
69 loginJob->setEncryptionMode(KIMAP::LoginJob::EncryptionMode::AnySslVersion);
70 return runJob(loginJob);
71}
72
73KAsync::Job<void> ImapServerProxy::select(const QString &mailbox)
74{
75 if (mSession->state() == KIMAP::Session::State::Disconnected) {
76 return KAsync::error<void>(1, "Not connected");
77 }
78 auto select = new KIMAP::SelectJob(mSession);
79 select->setMailBox(mailbox);
80 // select->setCondstoreEnabled(serverSupportsCondstore());
81 return runJob(select);
82}
83
84KAsync::Job<void> ImapServerProxy::append(const QString &mailbox, const QByteArray &content, const QList<QByteArray> &flags, const QDateTime &internalDate)
85{
86 if (mSession->state() == KIMAP::Session::State::Disconnected) {
87 return KAsync::error<void>(1, "Not connected");
88 }
89 auto append = new KIMAP::AppendJob(mSession);
90 append->setMailBox(mailbox);
91 append->setContent(content);
92 append->setFlags(flags);
93 append->setInternalDate(internalDate);
94 return runJob(append);
95}
96
97KAsync::Job<void> ImapServerProxy::fetch(const KIMAP::ImapSet &set, KIMAP::FetchJob::FetchScope scope, FetchCallback callback)
98{
99 if (mSession->state() == KIMAP::Session::State::Disconnected) {
100 return KAsync::error<void>(1, "Not connected");
101 }
102 auto fetch = new KIMAP::FetchJob(mSession);
103 fetch->setSequenceSet(set);
104 fetch->setUidBased(true);
105 fetch->setScope(scope);
106 QObject::connect(fetch, static_cast<void(KIMAP::FetchJob::*)(const QString &,
107 const QMap<qint64,qint64> &,
108 const QMap<qint64,qint64> &,
109 const QMap<qint64,KIMAP::MessageAttribute> &,
110 const QMap<qint64,KIMAP::MessageFlags> &,
111 const QMap<qint64,KIMAP::MessagePtr> &)>(&KIMAP::FetchJob::headersReceived),
112 callback);
113 return runJob(fetch);
114}
115
116KAsync::Job<QList<qint64>> ImapServerProxy::fetchHeaders(const QString &mailbox)
117{
118 auto list = QSharedPointer<QList<qint64>>::create();
119 KIMAP::FetchJob::FetchScope scope;
120 scope.parts.clear();
121 scope.mode = KIMAP::FetchJob::FetchScope::Headers;
122
123 //Fetch headers of all messages
124 return fetch(KIMAP::ImapSet(1, 0), scope,
125 [list](const QString &mailbox,
126 const QMap<qint64,qint64> &uids,
127 const QMap<qint64,qint64> &sizes,
128 const QMap<qint64,KIMAP::MessageAttribute> &attrs,
129 const QMap<qint64,KIMAP::MessageFlags> &flags,
130 const QMap<qint64,KIMAP::MessagePtr> &messages) {
131 Trace() << "Received " << uids.size() << " headers from " << mailbox;
132 Trace() << uids.size() << sizes.size() << attrs.size() << flags.size() << messages.size();
133
134 //TODO based on the data available here, figure out which messages to actually fetch
135 //(we only fetched headers and structure so far)
136 //We could i.e. build chunks to fetch based on the size
137
138 for (const auto &id : uids.keys()) {
139 list->append(uids.value(id));
140 }
141 })
142 .then<QList<qint64>>([list](){
143 return *list;
144 });
145}
146
147KAsync::Job<void> ImapServerProxy::list(KIMAP::ListJob::Option option, const std::function<void(const QList<KIMAP::MailBoxDescriptor> &mailboxes,const QList<QList<QByteArray> > &flags)> &callback)
148{
149 auto listJob = new KIMAP::ListJob(mSession);
150 listJob->setOption(option);
151 // listJob->setQueriedNamespaces(serverNamespaces());
152 QObject::connect(listJob, &KIMAP::ListJob::mailBoxesReceived,
153 listJob, callback);
154 return runJob(listJob);
155}
156
157KAsync::Future<void> ImapServerProxy::fetchFolders(std::function<void(const QStringList &)> callback)
158{
159 Trace() << "Fetching folders";
160 auto job = login("doe", "doe").then<void>(list(KIMAP::ListJob::IncludeUnsubscribed, [callback](const QList<KIMAP::MailBoxDescriptor> &mailboxes, const QList<QList<QByteArray> > &flags){
161 QStringList list;
162 for (const auto &mailbox : mailboxes) {
163 Trace() << "Found mailbox: " << mailbox.name;
164 list << mailbox.name;
165 }
166 callback(list);
167 }),
168 [](int errorCode, const QString &errorString) {
169 Warning() << "Failed to list folders: " << errorCode << errorString;
170 });
171 return job.exec();
172}
173
174KAsync::Future<void> ImapServerProxy::fetchMessages(const QString &folder, std::function<void(const QVector<Message> &)> callback)
175{
176 auto job = login("doe", "doe").then<void>(select(folder)).then<void, KAsync::Job<void>>([this, callback, folder]() -> KAsync::Job<void> {
177 return fetchHeaders(folder).then<void, KAsync::Job<void>, QList<qint64>>([this, callback](const QList<qint64> &uidsToFetch){
178 Trace() << "Uids to fetch: " << uidsToFetch;
179 if (uidsToFetch.isEmpty()) {
180 Trace() << "Nothing to fetch";
181 return KAsync::null<void>();
182 }
183 KIMAP::FetchJob::FetchScope scope;
184 scope.parts.clear();
185 scope.mode = KIMAP::FetchJob::FetchScope::Full;
186
187 KIMAP::ImapSet set;
188 set.add(uidsToFetch.toVector());
189 return fetch(set, scope,
190 [callback](const QString &mailbox,
191 const QMap<qint64,qint64> &uids,
192 const QMap<qint64,qint64> &sizes,
193 const QMap<qint64,KIMAP::MessageAttribute> &attrs,
194 const QMap<qint64,KIMAP::MessageFlags> &flags,
195 const QMap<qint64,KIMAP::MessagePtr> &messages) {
196 Trace() << "Received " << uids.size() << " messages from " << mailbox;
197 Trace() << uids.size() << sizes.size() << attrs.size() << flags.size() << messages.size();
198
199 QVector<Message> list;
200 for (const auto &id : uids.keys()) {
201 list << Message{uids.value(id), sizes.value(id), attrs.value(id), flags.value(id), messages.value(id)};
202 }
203 callback(list);
204 });
205 });
206
207 });
208 return job.exec();
209}
diff --git a/examples/imapresource/imapserverproxy.h b/examples/imapresource/imapserverproxy.h
new file mode 100644
index 0000000..2beb28c
--- /dev/null
+++ b/examples/imapresource/imapserverproxy.h
@@ -0,0 +1,62 @@
1/*
2 * Copyright (C) 2015 Christian Mollekopf <chrigi_1@fastmail.fm>
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the
16 * Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 */
19
20#pragma once
21
22#include <Async/Async>
23
24#include <KMime/KMime/KMimeMessage>
25#include <KIMAP/KIMAP/ListJob>
26#include <KIMAP/KIMAP/Session>
27#include <KIMAP/KIMAP/FetchJob>
28
29struct Message {
30 qint64 uid;
31 qint64 size;
32 QPair<QByteArray, QVariant> attributes;
33 QList<QByteArray> flags;
34 KMime::Message::Ptr msg;
35};
36
37class ImapServerProxy {
38 KIMAP::Session *mSession;
39public:
40 ImapServerProxy(const QString &serverUrl, int port);
41
42 //Standard IMAP calls
43 KAsync::Job<void> login(const QString &username, const QString &password);
44 KAsync::Job<void> select(const QString &mailbox);
45 KAsync::Job<void> append(const QString &mailbox, const QByteArray &content, const QList<QByteArray> &flags = QList<QByteArray>(), const QDateTime &internalDate = QDateTime());
46
47 typedef std::function<void(const QString &,
48 const QMap<qint64,qint64> &,
49 const QMap<qint64,qint64> &,
50 const QMap<qint64,KIMAP::MessageAttribute> &,
51 const QMap<qint64,KIMAP::MessageFlags> &,
52 const QMap<qint64,KIMAP::MessagePtr> &)> FetchCallback;
53
54 KAsync::Job<void> fetch(const KIMAP::ImapSet &set, KIMAP::FetchJob::FetchScope scope, FetchCallback callback);
55 KAsync::Job<void> list(KIMAP::ListJob::Option option, const std::function<void(const QList<KIMAP::MailBoxDescriptor> &mailboxes,const QList<QList<QByteArray> > &flags)> &callback);
56
57 //Composed calls that do login etc.
58 KAsync::Job<QList<qint64>> fetchHeaders(const QString &mailbox);
59
60 KAsync::Future<void> fetchFolders(std::function<void(const QStringList &)> callback);
61 KAsync::Future<void> fetchMessages(const QString &folder, std::function<void(const QVector<Message> &)> callback);
62};
diff --git a/examples/imapresource/tests/CMakeLists.txt b/examples/imapresource/tests/CMakeLists.txt
new file mode 100644
index 0000000..cdd5fcb
--- /dev/null
+++ b/examples/imapresource/tests/CMakeLists.txt
@@ -0,0 +1,21 @@
1set(CMAKE_AUTOMOC ON)
2include_directories(
3 ${CMAKE_CURRENT_BINARY_DIR}
4 )
5
6macro(auto_tests)
7 foreach(_testname ${ARGN})
8 add_executable(${_testname} ${_testname}.cpp)
9 # generate_flatbuffers(${_testname} calendar)
10 add_test(${_testname} ${_testname})
11 qt5_use_modules(${_testname} Core Test Concurrent)
12 target_link_libraries(${_testname} sink libhawd)
13 endforeach(_testname)
14endmacro(auto_tests)
15
16auto_tests (
17 imapresourcetest
18)
19target_link_libraries(imapresourcetest sink_resource_imap)
20
21install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/resetmailbox.sh DESTINATION bin PERMISSIONS OWNER_EXECUTE OWNER_WRITE OWNER_READ GROUP_EXECUTE GROUP_READ)
diff --git a/examples/imapresource/tests/imapresourcetest.cpp b/examples/imapresource/tests/imapresourcetest.cpp
new file mode 100644
index 0000000..27d7d6d
--- /dev/null
+++ b/examples/imapresource/tests/imapresourcetest.cpp
@@ -0,0 +1,162 @@
1#include <QtTest>
2
3#include <QString>
4#include <KMime/Message>
5
6// #include "imapresource/imapresource.h"
7#include "store.h"
8#include "resourcecontrol.h"
9#include "commands.h"
10#include "entitybuffer.h"
11#include "resourceconfig.h"
12#include "modelresult.h"
13#include "pipeline.h"
14#include "log.h"
15#include "test.h"
16#include "../imapresource.h"
17#include "../imapserverproxy.h"
18
19#define ASYNCCOMPARE(actual, expected) \
20do {\
21 if (!QTest::qCompare(actual, expected, #actual, #expected, __FILE__, __LINE__))\
22 return KAsync::error<void>(1, "Comparison failed.");\
23} while (0)
24
25#define ASYNCVERIFY(statement) \
26do {\
27 if (!QTest::qVerify((statement), #statement, "", __FILE__, __LINE__))\
28 return KAsync::error<void>(1, "Verify failed.");\
29} while (0)
30
31#define VERIFYEXEC(statement) \
32do {\
33 auto result = statement.exec(); \
34 result.waitForFinished(); \
35 if (!QTest::qVerify(!result.errorCode(), #statement, "", __FILE__, __LINE__))\
36 return;\
37} while (0)
38
39using namespace Sink;
40using namespace Sink::ApplicationDomain;
41
42/**
43 * Test of complete system using the imap resource.
44 *
45 * This test requires the imap resource installed.
46 */
47class ImapResourceTest : public QObject
48{
49 Q_OBJECT
50
51 QTemporaryDir tempDir;
52 QString targetPath;
53private slots:
54 void initTestCase()
55 {
56
57 //FIXME initTest only works for the current process,
58 //we also have to start resources in test-mode
59 // Sink::Test::initTest();
60 Sink::Log::setDebugOutputLevel(Sink::Log::Trace);
61 ::ImapResource::removeFromDisk("org.kde.imap.instance1");
62 system("resetmailbox.sh");
63 // auto resource = ApplicationDomain::ImapResource::create("account1");
64 Sink::ApplicationDomain::SinkResource resource;
65 resource.setProperty("identifier", "org.kde.imap.instance1");
66 resource.setProperty("type", "org.kde.imap");
67 resource.setProperty("server", "localhost");
68 resource.setProperty("port", 993);
69 Sink::Store::create(resource).exec().waitForFinished();
70 }
71
72 void cleanup()
73 {
74 Sink::ResourceControl::shutdown(QByteArray("org.kde.imap.instance1")).exec().waitForFinished();
75 ::ImapResource::removeFromDisk("org.kde.imap.instance1");
76 }
77
78 void init()
79 {
80 qDebug();
81 qDebug() << "-----------------------------------------";
82 qDebug();
83 Sink::ResourceControl::start(QByteArray("org.kde.imap.instance1")).exec().waitForFinished();
84 }
85
86 void testListFolders()
87 {
88 Sink::Query query;
89 query.resources << "org.kde.imap.instance1";
90 query.request<Folder::Name>();
91
92 // Ensure all local data is processed
93 VERIFYEXEC(Store::synchronize(query));
94 ResourceControl::flushMessageQueue(query.resources).exec().waitForFinished();
95
96 auto job = Store::fetchAll<Folder>(query).then<void, QList<Folder::Ptr>>([](const QList<Folder::Ptr> &folders) {
97 QCOMPARE(folders.size(), 2);
98 QStringList names;
99 for (const auto &folder : folders) {
100 names << folder->getName();
101 }
102 QVERIFY(names.contains("INBOX"));
103 QVERIFY(names.contains("INBOX.test"));
104 });
105 VERIFYEXEC(job);
106 }
107
108 void testListMails()
109 {
110 Sink::Query query;
111 query.resources << "org.kde.imap.instance1";
112 query.request<Mail::Subject>().request<Mail::MimeMessage>();
113
114 // Ensure all local data is processed
115 VERIFYEXEC(Store::synchronize(query));
116 ResourceControl::flushMessageQueue(query.resources).exec().waitForFinished();
117
118 auto job = Store::fetchAll<Mail>(query).then<void, QList<Mail::Ptr>>([](const QList<Mail::Ptr> &mails) {
119 QCOMPARE(mails.size(), 1);
120 QVERIFY(mails.first()->getSubject().startsWith(QString("[Nepomuk] Jenkins build is still unstable")));
121 const auto data = mails.first()->getMimeMessage();
122 QVERIFY(!data.isEmpty());
123
124 KMime::Message m;
125 m.setContent(data);
126 m.parse();
127 QCOMPARE(mails.first()->getSubject(), m.subject(true)->asUnicodeString());
128 });
129 VERIFYEXEC(job);
130 }
131
132 void testFetchNewMessages()
133 {
134 Sink::Query query;
135 query.resources << "org.kde.imap.instance1";
136 query.request<Mail::Subject>().request<Mail::MimeMessage>();
137
138 // Ensure all local data is processed
139 VERIFYEXEC(Store::synchronize(query));
140 ResourceControl::flushMessageQueue(query.resources).exec().waitForFinished();
141
142 ImapServerProxy imap("localhost", 993);
143 imap.login("doe", "doe").exec().waitForFinished();
144
145 auto msg = KMime::Message::Ptr::create();
146 msg->subject(true)->fromUnicodeString("Foobar", "utf8");
147 msg->assemble();
148
149 VERIFYEXEC(imap.append("INBOX.test", msg->encodedContent(true)));
150
151 Store::synchronize(query).exec().waitForFinished();
152 ResourceControl::flushMessageQueue(query.resources).exec().waitForFinished();
153
154 auto job = Store::fetchAll<Mail>(query).then<void, QList<Mail::Ptr>>([](const QList<Mail::Ptr> &mails) {
155 QCOMPARE(mails.size(), 2);
156 });
157 VERIFYEXEC(job);
158 }
159};
160
161QTEST_MAIN(ImapResourceTest)
162#include "imapresourcetest.moc"
diff --git a/examples/imapresource/tests/resetmailbox.sh b/examples/imapresource/tests/resetmailbox.sh
new file mode 100644
index 0000000..966115a
--- /dev/null
+++ b/examples/imapresource/tests/resetmailbox.sh
@@ -0,0 +1,10 @@
1#!/bin/bash
2
3sudo echo "dm user.doe.test" | cyradm --auth PLAIN -u cyrus -w admin localhost
4sudo echo "cm user.doe.test" | cyradm --auth PLAIN -u cyrus -w admin localhost
5sudo echo "sam user.doe cyrus c" | cyradm --auth PLAIN -u cyrus -w admin localhost
6sudo echo "sam user.doe.test cyrus c" | cyradm --auth PLAIN -u cyrus -w admin localhost
7# sudo rm -R /var/spool/imap/d/user/doe/*
8sudo cp /work/source/Sink/tests/data/maildir1/cur/1365777830.R28.localhost.localdomain\:2\,S /var/spool/imap/d/user/doe/test/1.
9sudo chown cyrus:mail /var/spool/imap/d/user/doe/test/1.
10sudo /usr/lib/cyrus-imapd/reconstruct "user.doe.test"