diff options
Diffstat (limited to 'examples/maildirresource/maildirresource.cpp')
-rw-r--r-- | examples/maildirresource/maildirresource.cpp | 284 |
1 files changed, 284 insertions, 0 deletions
diff --git a/examples/maildirresource/maildirresource.cpp b/examples/maildirresource/maildirresource.cpp new file mode 100644 index 0000000..f9cc2a4 --- /dev/null +++ b/examples/maildirresource/maildirresource.cpp | |||
@@ -0,0 +1,284 @@ | |||
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 "maildirresource.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 "domainadaptor.h" | ||
27 | #include "resourceconfig.h" | ||
28 | #include "commands.h" | ||
29 | #include "index.h" | ||
30 | #include "log.h" | ||
31 | #include "domain/mail.h" | ||
32 | #include "definitions.h" | ||
33 | #include "facadefactory.h" | ||
34 | #include "indexupdater.h" | ||
35 | #include "libmaildir/maildir.h" | ||
36 | #include <QDate> | ||
37 | #include <QUuid> | ||
38 | #include <QDir> | ||
39 | #include <QDirIterator> | ||
40 | #include <KMime/KMime/KMimeMessage> | ||
41 | |||
42 | //This is the resources entity type, and not the domain type | ||
43 | #define ENTITY_TYPE_MAIL "mail" | ||
44 | #define ENTITY_TYPE_FOLDER "folder" | ||
45 | |||
46 | MaildirResource::MaildirResource(const QByteArray &instanceIdentifier, const QSharedPointer<Akonadi2::Pipeline> &pipeline) | ||
47 | : Akonadi2::GenericResource(instanceIdentifier, pipeline) | ||
48 | { | ||
49 | addType(ENTITY_TYPE_MAIL, QSharedPointer<MaildirMailAdaptorFactory>::create(), | ||
50 | QVector<Akonadi2::Preprocessor*>() << new DefaultIndexUpdater<Akonadi2::ApplicationDomain::Mail>); | ||
51 | addType(ENTITY_TYPE_MAIL, QSharedPointer<MaildirMailAdaptorFactory>::create(), | ||
52 | QVector<Akonadi2::Preprocessor*>() << new DefaultIndexUpdater<Akonadi2::ApplicationDomain::Folder>); | ||
53 | auto config = ResourceConfig::getConfiguration(instanceIdentifier); | ||
54 | mMaildirPath = config.value("path").toString(); | ||
55 | } | ||
56 | |||
57 | QString MaildirResource::resolveRemoteId(const QByteArray &bufferType, const QString &remoteId, Akonadi2::Storage::Transaction &transaction) | ||
58 | { | ||
59 | //Lookup local id for remote id, or insert a new pair otherwise | ||
60 | auto remoteIdWithType = bufferType + remoteId.toUtf8(); | ||
61 | QByteArray akonadiId = Index("rid.mapping", transaction).lookup(remoteIdWithType); | ||
62 | if (akonadiId.isEmpty()) { | ||
63 | akonadiId = QUuid::createUuid().toString().toUtf8(); | ||
64 | Index("rid.mapping", transaction).add(remoteIdWithType, akonadiId); | ||
65 | } | ||
66 | return akonadiId; | ||
67 | } | ||
68 | |||
69 | static QStringList listRecursive( const QString &root, const KPIM::Maildir &dir ) | ||
70 | { | ||
71 | QStringList list; | ||
72 | foreach (const QString &sub, dir.subFolderList()) { | ||
73 | const KPIM::Maildir md = dir.subFolder(sub); | ||
74 | if (!md.isValid()) { | ||
75 | continue; | ||
76 | } | ||
77 | QString path = root + QDir::separator() + sub; | ||
78 | list << path; | ||
79 | list += listRecursive(path, md ); | ||
80 | } | ||
81 | return list; | ||
82 | } | ||
83 | |||
84 | QStringList MaildirResource::listAvailableFolders() | ||
85 | { | ||
86 | KPIM::Maildir dir(mMaildirPath, true); | ||
87 | if (!dir.isValid()) { | ||
88 | return QStringList(); | ||
89 | } | ||
90 | QStringList folderList; | ||
91 | folderList << mMaildirPath; | ||
92 | folderList += listRecursive(mMaildirPath, dir); | ||
93 | return folderList; | ||
94 | } | ||
95 | |||
96 | void MaildirResource::synchronizeFolders(Akonadi2::Storage::Transaction &transaction) | ||
97 | { | ||
98 | const QString bufferType = ENTITY_TYPE_FOLDER; | ||
99 | QStringList folderList = listAvailableFolders(); | ||
100 | Trace() << "Found folders " << folderList; | ||
101 | |||
102 | Akonadi2::Storage store(Akonadi2::storageLocation(), mResourceInstanceIdentifier + ".synchronization", Akonadi2::Storage::ReadWrite); | ||
103 | auto synchronizationTransaction = store.createTransaction(Akonadi2::Storage::ReadWrite); | ||
104 | Index ridMapping("rid.mapping", synchronizationTransaction); | ||
105 | for (const auto folder : folderList) { | ||
106 | const auto remoteId = folder.toUtf8(); | ||
107 | auto akonadiId = resolveRemoteId(bufferType.toUtf8(), remoteId, synchronizationTransaction); | ||
108 | |||
109 | bool found = false; | ||
110 | transaction.openDatabase(bufferType.toUtf8() + ".main").scan(akonadiId.toUtf8(), [&found](const QByteArray &, const QByteArray &) -> bool { | ||
111 | found = true; | ||
112 | return false; | ||
113 | }, [this](const Akonadi2::Storage::Error &error) { | ||
114 | }, true); | ||
115 | |||
116 | if (!found) { //A new entity | ||
117 | m_fbb.Clear(); | ||
118 | |||
119 | KPIM::Maildir md(folder); | ||
120 | |||
121 | flatbuffers::FlatBufferBuilder entityFbb; | ||
122 | auto name = m_fbb.CreateString(md.name().toStdString()); | ||
123 | auto icon = m_fbb.CreateString("folder"); | ||
124 | flatbuffers::Offset<flatbuffers::String> parent; | ||
125 | |||
126 | if (!md.isRoot()) { | ||
127 | auto akonadiId = resolveRemoteId(ENTITY_TYPE_FOLDER, md.parent().path(), transaction); | ||
128 | parent = m_fbb.CreateString(akonadiId.toStdString()); | ||
129 | } | ||
130 | |||
131 | auto builder = Akonadi2::ApplicationDomain::Buffer::FolderBuilder(m_fbb); | ||
132 | builder.add_name(name); | ||
133 | if (!md.isRoot()) { | ||
134 | builder.add_parent(parent); | ||
135 | } | ||
136 | builder.add_icon(icon); | ||
137 | auto buffer = builder.Finish(); | ||
138 | Akonadi2::ApplicationDomain::Buffer::FinishFolderBuffer(m_fbb, buffer); | ||
139 | Akonadi2::EntityBuffer::assembleEntityBuffer(entityFbb, 0, 0, 0, 0, m_fbb.GetBufferPointer(), m_fbb.GetSize()); | ||
140 | |||
141 | flatbuffers::FlatBufferBuilder fbb; | ||
142 | //This is the resource type and not the domain type | ||
143 | auto entityId = fbb.CreateString(akonadiId.toStdString()); | ||
144 | auto type = fbb.CreateString(bufferType.toStdString()); | ||
145 | auto delta = Akonadi2::EntityBuffer::appendAsVector(fbb, entityFbb.GetBufferPointer(), entityFbb.GetSize()); | ||
146 | auto location = Akonadi2::Commands::CreateCreateEntity(fbb, entityId, type, delta); | ||
147 | Akonadi2::Commands::FinishCreateEntityBuffer(fbb, location); | ||
148 | |||
149 | Trace() << "Found a new entity: " << remoteId; | ||
150 | enqueueCommand(mSynchronizerQueue, Akonadi2::Commands::CreateEntityCommand, QByteArray::fromRawData(reinterpret_cast<char const *>(fbb.GetBufferPointer()), fbb.GetSize())); | ||
151 | } else { //modification | ||
152 | Trace() << "Found a modified entity: " << remoteId; | ||
153 | //TODO diff and create modification if necessary | ||
154 | } | ||
155 | } | ||
156 | //TODO find items to remove | ||
157 | } | ||
158 | |||
159 | void MaildirResource::synchronizeMails(Akonadi2::Storage::Transaction &transaction, const QString &path) | ||
160 | { | ||
161 | Trace() << "Synchronizing mails" << path; | ||
162 | const QString bufferType = ENTITY_TYPE_MAIL; | ||
163 | |||
164 | KPIM::Maildir maildir(path, true); | ||
165 | if (!maildir.isValid()) { | ||
166 | Warning() << "Failed to sync folder " << maildir.lastError(); | ||
167 | return; | ||
168 | } | ||
169 | |||
170 | auto listingPath = maildir.pathToCurrent(); | ||
171 | auto entryIterator = QSharedPointer<QDirIterator>::create(listingPath, QDir::Files); | ||
172 | Trace() << "Looking into " << maildir.pathToNew(); | ||
173 | |||
174 | QFileInfo entryInfo; | ||
175 | |||
176 | Akonadi2::Storage store(Akonadi2::storageLocation(), mResourceInstanceIdentifier + ".synchronization", Akonadi2::Storage::ReadWrite); | ||
177 | auto synchronizationTransaction = store.createTransaction(Akonadi2::Storage::ReadWrite); | ||
178 | Index ridMapping("rid.mapping", synchronizationTransaction); | ||
179 | |||
180 | while (entryIterator->hasNext()) { | ||
181 | QString filePath = entryIterator->next(); | ||
182 | QString fileName = entryIterator->fileName(); | ||
183 | |||
184 | const auto remoteId = fileName.toUtf8(); | ||
185 | auto akonadiId = resolveRemoteId(bufferType.toUtf8(), remoteId, synchronizationTransaction); | ||
186 | |||
187 | bool found = false; | ||
188 | transaction.openDatabase(bufferType.toUtf8() + ".main").scan(akonadiId.toUtf8(), [&found](const QByteArray &, const QByteArray &) -> bool { | ||
189 | found = true; | ||
190 | return false; | ||
191 | }, [this](const Akonadi2::Storage::Error &error) { | ||
192 | }, true); | ||
193 | |||
194 | if (!found) { //A new entity | ||
195 | m_fbb.Clear(); | ||
196 | |||
197 | KMime::Message *msg = new KMime::Message; | ||
198 | auto filepath = listingPath + QDir::separator() + fileName; | ||
199 | msg->setHead(KMime::CRLFtoLF(maildir.readEntryHeadersFromFile(filepath))); | ||
200 | msg->parse(); | ||
201 | |||
202 | const auto flags = maildir.readEntryFlags(fileName); | ||
203 | |||
204 | Trace() << "Found a mail " << filePath << fileName << msg->subject(true)->asUnicodeString(); | ||
205 | flatbuffers::FlatBufferBuilder entityFbb; | ||
206 | auto subject = m_fbb.CreateString(msg->subject(true)->asUnicodeString().toStdString()); | ||
207 | auto sender = m_fbb.CreateString(msg->from(true)->asUnicodeString().toStdString()); | ||
208 | auto senderName = m_fbb.CreateString(msg->from(true)->asUnicodeString().toStdString()); | ||
209 | auto date = m_fbb.CreateString(msg->date(true)->dateTime().toString().toStdString()); | ||
210 | auto folder = m_fbb.CreateString(resolveRemoteId(ENTITY_TYPE_FOLDER, path, transaction).toStdString()); | ||
211 | auto mimeMessage = m_fbb.CreateString(filepath.toStdString()); | ||
212 | |||
213 | auto builder = Akonadi2::ApplicationDomain::Buffer::MailBuilder(m_fbb); | ||
214 | builder.add_subject(subject); | ||
215 | builder.add_sender(sender); | ||
216 | builder.add_senderName(senderName); | ||
217 | builder.add_unread(!(flags & KPIM::Maildir::Seen)); | ||
218 | builder.add_important(flags & KPIM::Maildir::Flagged); | ||
219 | builder.add_date(date); | ||
220 | builder.add_folder(folder); | ||
221 | builder.add_mimeMessage(mimeMessage); | ||
222 | auto buffer = builder.Finish(); | ||
223 | Akonadi2::ApplicationDomain::Buffer::FinishMailBuffer(m_fbb, buffer); | ||
224 | Akonadi2::EntityBuffer::assembleEntityBuffer(entityFbb, 0, 0, 0, 0, m_fbb.GetBufferPointer(), m_fbb.GetSize()); | ||
225 | |||
226 | flatbuffers::FlatBufferBuilder fbb; | ||
227 | //This is the resource type and not the domain type | ||
228 | auto entityId = fbb.CreateString(akonadiId.toStdString()); | ||
229 | auto type = fbb.CreateString(bufferType.toStdString()); | ||
230 | auto delta = Akonadi2::EntityBuffer::appendAsVector(fbb, entityFbb.GetBufferPointer(), entityFbb.GetSize()); | ||
231 | auto location = Akonadi2::Commands::CreateCreateEntity(fbb, entityId, type, delta); | ||
232 | Akonadi2::Commands::FinishCreateEntityBuffer(fbb, location); | ||
233 | |||
234 | Trace() << "Found a new entity: " << remoteId; | ||
235 | enqueueCommand(mSynchronizerQueue, Akonadi2::Commands::CreateEntityCommand, QByteArray::fromRawData(reinterpret_cast<char const *>(fbb.GetBufferPointer()), fbb.GetSize())); | ||
236 | } else { //modification | ||
237 | Trace() << "Found a modified entity: " << remoteId; | ||
238 | //TODO diff and create modification if necessary | ||
239 | } | ||
240 | } | ||
241 | //TODO find items to remove | ||
242 | } | ||
243 | |||
244 | KAsync::Job<void> MaildirResource::synchronizeWithSource() | ||
245 | { | ||
246 | Log() << " Synchronizing"; | ||
247 | return KAsync::start<void>([this]() { | ||
248 | auto transaction = Akonadi2::Storage(Akonadi2::storageLocation(), mResourceInstanceIdentifier, Akonadi2::Storage::ReadOnly).createTransaction(Akonadi2::Storage::ReadOnly); | ||
249 | synchronizeFolders(transaction); | ||
250 | for (const auto &folder : listAvailableFolders()) { | ||
251 | synchronizeMails(transaction, folder); | ||
252 | } | ||
253 | }); | ||
254 | } | ||
255 | |||
256 | KAsync::Job<void> MaildirResource::replay(const QByteArray &type, const QByteArray &key, const QByteArray &value) | ||
257 | { | ||
258 | Trace() << "Replaying " << key; | ||
259 | return KAsync::null<void>(); | ||
260 | } | ||
261 | |||
262 | void MaildirResource::removeFromDisk(const QByteArray &instanceIdentifier) | ||
263 | { | ||
264 | GenericResource::removeFromDisk(instanceIdentifier); | ||
265 | Akonadi2::Storage(Akonadi2::storageLocation(), instanceIdentifier + ".synchronization", Akonadi2::Storage::ReadWrite).removeFromDisk(); | ||
266 | } | ||
267 | |||
268 | MaildirResourceFactory::MaildirResourceFactory(QObject *parent) | ||
269 | : Akonadi2::ResourceFactory(parent) | ||
270 | { | ||
271 | |||
272 | } | ||
273 | |||
274 | Akonadi2::Resource *MaildirResourceFactory::createResource(const QByteArray &instanceIdentifier) | ||
275 | { | ||
276 | return new MaildirResource(instanceIdentifier); | ||
277 | } | ||
278 | |||
279 | void MaildirResourceFactory::registerFacades(Akonadi2::FacadeFactory &factory) | ||
280 | { | ||
281 | factory.registerFacade<Akonadi2::ApplicationDomain::Mail, MaildirResourceMailFacade>(PLUGIN_NAME); | ||
282 | factory.registerFacade<Akonadi2::ApplicationDomain::Folder, MaildirResourceFolderFacade>(PLUGIN_NAME); | ||
283 | } | ||
284 | |||