1/***************************************************************************
2 * Copyright (C) 2007 by Robert Zwerus <arzie@dds.nl> *
3 * *
4 * This program is free software; you can redistribute it and/or modify *
5 * it under the terms of the GNU Library General Public License as *
6 * published by the Free Software Foundation; either version 2 of the *
7 * License, or (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 Library General Public *
15 * License 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 "akappend.h"
21
22#include "libs/imapparser_p.h"
23#include "imapstreamparser.h"
24
25#include "append.h"
26#include "response.h"
27#include "handlerhelper.h"
28
29#include "akonadi.h"
30#include "connection.h"
31#include "preprocessormanager.h"
32#include "storage/datastore.h"
33#include "storage/entity.h"
34#include "storage/transaction.h"
35#include "storage/parttypehelper.h"
36#include "storage/dbconfig.h"
37#include "storage/partstreamer.h"
38#include "storage/parthelper.h"
39#include "libs/protocol_p.h"
40
41#include <QtCore/QDebug>
42
43using namespace Akonadi;
44using namespace Akonadi::Server;
45
46AkAppend::AkAppend()
47 : Handler()
48{
49}
50
51AkAppend::~AkAppend()
52{
53}
54
55QByteArray AkAppend::parseFlag(const QByteArray &flag) const
56{
57 const int pos1 = flag.indexOf('[');
58 const int pos2 = flag.lastIndexOf(']');
59 return flag.mid(pos1 + 1, pos2 - pos1 - 1);
60}
61
62bool AkAppend::buildPimItem(PimItem &item, Collection &col,
63 ChangedAttributes &itemFlags,
64 ChangedAttributes &itemTagsRID,
65 ChangedAttributes &itemTagsGID)
66{
67 // Arguments: mailbox name
68 // OPTIONAL flag parenthesized list
69 // OPTIONAL date/time string
70 // (partname literal)+
71 //
72 // Syntax:
73 // x-akappend = "X-AKAPPEND" SP mailbox SP size [SP flag-list] [SP date-time] SP (partname SP literal)+
74 const QByteArray mailbox = m_streamParser->readString();
75
76 const qint64 size = m_streamParser->readNumber();
77 // parse optional flag parenthesized list
78 // Syntax:
79 // flag-list = "(" [flag *(SP flag)] ")"
80 // flag = "\ANSWERED" / "\FLAGGED" / "\DELETED" / "\SEEN" /
81 // "\DRAFT" / flag-keyword / flag-extension
82 // ; Does not include "\Recent"
83 // flag-extension = "\" atom
84 // flag-keyword = atom
85 QList<QByteArray> flags;
86 if (m_streamParser->hasList()) {
87 flags = m_streamParser->readParenthesizedList();
88 }
89
90 // parse optional date/time string
91 QDateTime dateTime;
92 if (m_streamParser->hasDateTime()) {
93 dateTime = m_streamParser->readDateTime().toUTC();
94 // FIXME Should we return an error if m_dateTime is invalid?
95 } else {
96 // if date/time is not given then it will be set to the current date/time
97 // converted to UTC.
98 dateTime = QDateTime::currentDateTime().toUTC();
99 }
100
101 Response response;
102
103 col = HandlerHelper::collectionFromIdOrName(mailbox);
104 if (!col.isValid()) {
105 throw HandlerException(QByteArray("Unknown collection for '") + mailbox + QByteArray("'."));
106 }
107 if (col.isVirtual()) {
108 throw HandlerException("Cannot append item into virtual collection");
109 }
110
111 QByteArray mt;
112 QString remote_id;
113 QString remote_revision;
114 QString gid;
115 Q_FOREACH (const QByteArray &flag, flags) {
116 if (flag.startsWith(AKONADI_FLAG_MIMETYPE)) {
117 mt = parseFlag(flag);
118 } else if (flag.startsWith(AKONADI_FLAG_REMOTEID)) {
119 remote_id = QString::fromUtf8(parseFlag(flag));
120 } else if (flag.startsWith(AKONADI_FLAG_REMOTEREVISION)) {
121 remote_revision = QString::fromUtf8(parseFlag(flag));
122 } else if (flag.startsWith(AKONADI_FLAG_GID)) {
123 gid = QString::fromUtf8(parseFlag(flag));
124 } else if (flag.startsWith("+" AKONADI_FLAG_TAG)) {
125 itemTagsGID.incremental = true;
126 itemTagsGID.added.append(parseFlag(flag));
127 } else if (flag.startsWith("-" AKONADI_FLAG_TAG)) {
128 itemTagsGID.incremental = true;
129 itemTagsGID.removed.append(parseFlag(flag));
130 } else if (flag.startsWith(AKONADI_FLAG_TAG)) {
131 itemTagsGID.incremental = false;
132 itemTagsGID.added.append(parseFlag(flag));
133 } else if (flag.startsWith("+" AKONADI_FLAG_RTAG)) {
134 itemTagsRID.incremental = true;
135 itemTagsRID.added.append(parseFlag(flag));
136 } else if (flag.startsWith("-" AKONADI_FLAG_RTAG)) {
137 itemTagsRID.incremental = true;
138 itemTagsRID.removed.append(parseFlag(flag));
139 } else if (flag.startsWith(AKONADI_FLAG_RTAG)) {
140 itemTagsRID.incremental = false;
141 itemTagsRID.added.append(parseFlag(flag));
142 } else if (flag.startsWith('+')) {
143 itemFlags.incremental = true;
144 itemFlags.added.append(flag.mid(1));
145 } else if (flag.startsWith('-')) {
146 itemFlags.incremental = true;
147 itemFlags.removed.append(flag.mid(1));
148 } else {
149 itemFlags.incremental = false;
150 itemFlags.added.append(flag);
151 }
152 }
153 // standard imap does not know this attribute, so that's mail
154 if (mt.isEmpty()) {
155 mt = "message/rfc822";
156 }
157 MimeType mimeType = MimeType::retrieveByName(QString::fromLatin1(mt));
158 if (!mimeType.isValid()) {
159 MimeType m(QString::fromLatin1(mt));
160 if (!m.insert()) {
161 return failureResponse(QByteArray("Unable to create mimetype '") + mt + QByteArray("'."));
162 }
163 mimeType = m;
164 }
165
166 item.setRev(0);
167 item.setSize(size);
168 item.setMimeTypeId(mimeType.id());
169 item.setCollectionId(col.id());
170 if (dateTime.isValid()) {
171 item.setDatetime(dateTime);
172 }
173 if (remote_id.isEmpty()) {
174 // from application
175 item.setDirty(true);
176 } else {
177 // from resource
178 item.setRemoteId(remote_id);
179 item.setDirty(false);
180 }
181 item.setRemoteRevision(remote_revision);
182 item.setGid(gid);
183 item.setAtime(QDateTime::currentDateTime());
184
185 return true;
186}
187
188// This is used for clients that don't support item streaming
189bool AkAppend::readParts(PimItem &pimItem)
190{
191
192 // parse part specification
193 QVector<QPair<QByteArray, QPair<qint64, int> > > partSpecs;
194 QByteArray partName = "";
195 qint64 partSize = -1;
196 qint64 partSizes = 0;
197 bool ok = false;
198
199 qint64 realSize = pimItem.size();
200
201 const QList<QByteArray> list = m_streamParser->readParenthesizedList();
202 Q_FOREACH (const QByteArray &item, list) {
203 if (partName.isEmpty() && partSize == -1) {
204 partName = item;
205 continue;
206 }
207 if (item.startsWith(':')) {
208 int pos = 1;
209 ImapParser::parseNumber(item, partSize, &ok, pos);
210 if (!ok) {
211 partSize = 0;
212 }
213
214 int version = 0;
215 QByteArray plainPartName;
216 ImapParser::splitVersionedKey(partName, plainPartName, version);
217
218 partSpecs.append(qMakePair(plainPartName, qMakePair(partSize, version)));
219 partName = "";
220 partSizes += partSize;
221 partSize = -1;
222 }
223 }
224
225 realSize = qMax(partSizes, realSize);
226
227 const QByteArray allParts = m_streamParser->readString();
228
229 // chop up literal data in parts
230 int pos = 0; // traverse through part data now
231 QPair<QByteArray, QPair<qint64, int> > partSpec;
232 Q_FOREACH (partSpec, partSpecs) {
233 // wrap data into a part
234 Part part;
235 part.setPimItemId(pimItem.id());
236 part.setPartType(PartTypeHelper::fromFqName(partSpec.first));
237 part.setData(allParts.mid(pos, partSpec.second.first));
238 if (partSpec.second.second != 0) {
239 part.setVersion(partSpec.second.second);
240 }
241 part.setDatasize(partSpec.second.first);
242
243 if (!PartHelper::insert(&part)) {
244 return failureResponse("Unable to append item part");
245 }
246
247 pos += partSpec.second.first;
248 }
249
250 if (realSize != pimItem.size()) {
251 pimItem.setSize(realSize);
252 pimItem.update();
253 }
254
255 return true;
256}
257
258bool AkAppend::insertItem(PimItem &item, const Collection &parentCol,
259 const QVector<QByteArray> &itemFlags,
260 const QVector<QByteArray> &itemTagsRID,
261 const QVector<QByteArray> &itemTagsGID)
262{
263 if (!item.insert()) {
264 return failureResponse("Failed to append item");
265 }
266
267 // set message flags
268 // This will hit an entry in cache inserted there in buildPimItem()
269 const Flag::List flagList = HandlerHelper::resolveFlags(itemFlags);
270 bool flagsChanged = false;
271 if (!DataStore::self()->appendItemsFlags(PimItem::List() << item, flagList, &flagsChanged, false, parentCol, true)) {
272 return failureResponse("Unable to append item flags.");
273 }
274
275 Tag::List tagList;
276 if (!itemTagsGID.isEmpty()) {
277 tagList << HandlerHelper::resolveTagsByGID(itemTagsGID);
278 }
279 if (!itemTagsRID.isEmpty()) {
280 tagList << HandlerHelper::resolveTagsByRID(itemTagsRID, connection()->context());
281 }
282 bool tagsChanged;
283 if (!DataStore::self()->appendItemsTags(PimItem::List() << item, tagList, &tagsChanged, false, parentCol, true)) {
284 return failureResponse("Unable to append item tags.");
285 }
286
287 // Handle individual parts
288 qint64 partSizes = 0;
289 if (connection()->capabilities().akAppendStreaming()) {
290 QByteArray partName /* unused */;
291 qint64 partSize;
292 m_streamParser->beginList();
293 PartStreamer streamer(connection(), m_streamParser, item, this);
294 connect(&streamer, SIGNAL(responseAvailable(Akonadi::Server::Response)),
295 this, SIGNAL(responseAvailable(Akonadi::Server::Response)));
296 while (!m_streamParser->atListEnd()) {
297 QByteArray command = m_streamParser->readString();
298 if (command.isEmpty()) {
299 throw HandlerException("Syntax error");
300 }
301
302 if (!streamer.stream(command, false, partName, partSize)) {
303 throw HandlerException(streamer.error());
304 }
305
306 partSizes += partSize;
307 }
308
309 // TODO: Try to avoid this addition query
310 if (partSizes > item.size()) {
311 item.setSize(partSizes);
312 item.update();
313 }
314 } else {
315 if (!readParts(item)) {
316 return false;
317 }
318 }
319
320 // Preprocessing
321 if (PreprocessorManager::instance()->isActive()) {
322 Part hiddenAttribute;
323 hiddenAttribute.setPimItemId(item.id());
324 hiddenAttribute.setPartType(PartTypeHelper::fromFqName(QString::fromLatin1(AKONADI_ATTRIBUTE_HIDDEN)));
325 hiddenAttribute.setData(QByteArray());
326 hiddenAttribute.setDatasize(0);
327 // TODO: Handle errors? Technically, this is not a critical issue as no data are lost
328 PartHelper::insert(&hiddenAttribute);
329 }
330
331 return true;
332}
333
334bool AkAppend::notify(const PimItem &item, const Collection &collection)
335{
336 DataStore::self()->notificationCollector()->itemAdded(item, collection);
337
338 if (PreprocessorManager::instance()->isActive()) {
339 // enqueue the item for preprocessing
340 PreprocessorManager::instance()->beginHandleItem(item, DataStore::self());
341 }
342 return true;
343}
344
345bool AkAppend::sendResponse(const QByteArray &responseStr, const PimItem &item)
346{
347 // Date time is always stored in UTC time zone by the server.
348 const QString datetime = QLocale::c().toString(item.datetime(), QLatin1String("dd-MMM-yyyy hh:mm:ss +0000"));
349
350 Response response;
351 response.setTag(tag());
352 response.setUserDefined();
353 response.setString("[UIDNEXT " + QByteArray::number(item.id()) + " DATETIME " + ImapParser::quote(datetime.toUtf8()) + ']');
354 Q_EMIT responseAvailable(response);
355
356 response.setSuccess();
357 response.setString(responseStr);
358 Q_EMIT responseAvailable(response);
359 return true;
360}
361
362bool AkAppend::parseStream()
363{
364 // FIXME: The streaming/reading of all item parts can hold the transaction for
365 // unnecessary long time -> should we wrap the PimItem into one transaction
366 // and try to insert Parts independently? In case we fail to insert a part,
367 // it's not a problem as it can be re-fetched at any time, except for attributes.
368 DataStore *db = DataStore::self();
369 Transaction transaction(db);
370
371 ChangedAttributes itemFlags, itemTagsRID, itemTagsGID;
372 Collection parentCol;
373 PimItem item;
374 if (!buildPimItem(item, parentCol, itemFlags, itemTagsRID, itemTagsGID)) {
375 return false;
376 }
377
378 if (itemFlags.incremental) {
379 throw HandlerException("Incremental flags changes are not allowed in AK-APPEND");
380 }
381 if (itemTagsRID.incremental || itemTagsRID.incremental) {
382 throw HandlerException("Incremental tags changes are not allowed in AK-APPEND");
383 }
384
385 if (!insertItem(item, parentCol, itemFlags.added, itemTagsRID.added, itemTagsGID.added)) {
386 return false;
387 }
388
389 // All SQL is done, let's commit!
390 if (!transaction.commit()) {
391 return failureResponse("Failed to commit transaction");
392 }
393
394 notify(item, parentCol);
395 return sendResponse("Append completed", item);
396}
397