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 | |
43 | using namespace Akonadi; |
44 | using namespace Akonadi::Server; |
45 | |
46 | AkAppend::AkAppend() |
47 | : Handler() |
48 | { |
49 | } |
50 | |
51 | AkAppend::~AkAppend() |
52 | { |
53 | } |
54 | |
55 | QByteArray 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 | |
62 | bool 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 |
189 | bool 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 | |
258 | bool 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 | |
334 | bool 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 | |
345 | bool 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 | |
362 | bool 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 | |