1 | /*************************************************************************** |
2 | * Copyright (C) 2006-2009 by Tobias Koenig <tokoe@kde.org> * |
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 "fetchhelper.h" |
21 | |
22 | #include "akdebug.h" |
23 | #include "akdbus.h" |
24 | #include "akonadi.h" |
25 | #include "connection.h" |
26 | #include "handler.h" |
27 | #include "handlerhelper.h" |
28 | #include "imapstreamparser.h" |
29 | #include "libs/imapparser_p.h" |
30 | #include "libs/protocol_p.h" |
31 | #include "response.h" |
32 | #include "storage/selectquerybuilder.h" |
33 | #include "storage/itemqueryhelper.h" |
34 | #include "storage/itemretrievalmanager.h" |
35 | #include "storage/itemretrievalrequest.h" |
36 | #include "storage/parthelper.h" |
37 | #include <storage/parttypehelper.h> |
38 | #include "storage/transaction.h" |
39 | #include "utils.h" |
40 | #include "intervalcheck.h" |
41 | #include "agentmanagerinterface.h" |
42 | #include "dbusconnectionpool.h" |
43 | #include "tagfetchhelper.h" |
44 | |
45 | #include <QtCore/QLocale> |
46 | #include <QtCore/QStringList> |
47 | #include <QtCore/QUuid> |
48 | #include <QtCore/QVariant> |
49 | #include <QtCore/QDateTime> |
50 | #include <QtSql/QSqlDatabase> |
51 | #include <QtSql/QSqlError> |
52 | #include <QtSql/QSqlQuery> |
53 | |
54 | using namespace Akonadi; |
55 | using namespace Akonadi::Server; |
56 | |
57 | FetchHelper::FetchHelper(Connection *connection, const Scope &scope, const FetchScope &fetchScope) |
58 | : mStreamParser(0) |
59 | , mConnection(connection) |
60 | , mScope(scope) |
61 | , mFetchScope(fetchScope) |
62 | { |
63 | std::fill(mItemQueryColumnMap, mItemQueryColumnMap + ItemQueryColumnCount, -1); |
64 | } |
65 | |
66 | enum PartQueryColumns { |
67 | PartQueryPimIdColumn, |
68 | PartQueryTypeNamespaceColumn, |
69 | PartQueryTypeNameColumn, |
70 | PartQueryDataColumn, |
71 | PartQueryExternalColumn, |
72 | PartQueryVersionColumn |
73 | }; |
74 | |
75 | QSqlQuery FetchHelper::buildPartQuery(const QVector<QByteArray> &partList, bool allPayload, bool allAttrs) |
76 | { |
77 | ///TODO: merge with ItemQuery |
78 | QueryBuilder partQuery(PimItem::tableName()); |
79 | if (!partList.isEmpty() || allPayload || allAttrs) { |
80 | partQuery.addJoin(QueryBuilder::InnerJoin, Part::tableName(), PimItem::idFullColumnName(), Part::pimItemIdFullColumnName()); |
81 | partQuery.addJoin(QueryBuilder::InnerJoin, PartType::tableName(), Part::partTypeIdFullColumnName(), PartType::idFullColumnName()); |
82 | partQuery.addColumn(PimItem::idFullColumnName()); |
83 | partQuery.addColumn(PartType::nsFullColumnName()); |
84 | partQuery.addColumn(PartType::nameFullColumnName()); |
85 | partQuery.addColumn(Part::dataFullColumnName()); |
86 | partQuery.addColumn(Part::externalFullColumnName()); |
87 | |
88 | partQuery.addColumn(Part::versionFullColumnName()); |
89 | |
90 | partQuery.addSortColumn(PimItem::idFullColumnName(), Query::Descending); |
91 | |
92 | Query::Condition cond(Query::Or); |
93 | if (!partList.isEmpty()) { |
94 | QStringList partNameList; |
95 | Q_FOREACH (const QByteArray &b, partList) { |
96 | if (b.startsWith("PLD" ) || b.startsWith("ATR" )) { |
97 | partNameList.push_back(QString::fromLatin1(b)); |
98 | } |
99 | } |
100 | if (!partNameList.isEmpty()) { |
101 | cond.addCondition(PartTypeHelper::conditionFromFqNames(partNameList)); |
102 | } |
103 | } |
104 | |
105 | if (allPayload) { |
106 | cond.addValueCondition(PartType::nsFullColumnName(), Query::Equals, QLatin1String("PLD" )); |
107 | } |
108 | if (allAttrs) { |
109 | cond.addValueCondition(PartType::nsFullColumnName(), Query::Equals, QLatin1String("ATR" )); |
110 | } |
111 | |
112 | if (!cond.isEmpty()) { |
113 | partQuery.addCondition(cond); |
114 | } |
115 | |
116 | ItemQueryHelper::scopeToQuery(mScope, mConnection->context(), partQuery); |
117 | |
118 | if (!partQuery.exec()) { |
119 | throw HandlerException("Unable to list item parts" ); |
120 | } |
121 | |
122 | partQuery.query().next(); |
123 | } |
124 | |
125 | return partQuery.query(); |
126 | } |
127 | |
128 | QSqlQuery FetchHelper::buildItemQuery() |
129 | { |
130 | QueryBuilder itemQuery(PimItem::tableName()); |
131 | |
132 | itemQuery.addJoin(QueryBuilder::InnerJoin, MimeType::tableName(), |
133 | PimItem::mimeTypeIdFullColumnName(), MimeType::idFullColumnName()); |
134 | |
135 | int column = 0; |
136 | #define ADD_COLUMN(colName, colId) { itemQuery.addColumn( colName ); mItemQueryColumnMap[colId] = column++; } |
137 | ADD_COLUMN(PimItem::idFullColumnName(), ItemQueryPimItemIdColumn); |
138 | if (mFetchScope.remoteIdRequested()) { |
139 | ADD_COLUMN(PimItem::remoteIdFullColumnName(), ItemQueryPimItemRidColumn) |
140 | } |
141 | ADD_COLUMN(MimeType::nameFullColumnName(), ItemQueryMimeTypeColumn) |
142 | ADD_COLUMN(PimItem::revFullColumnName(), ItemQueryRevColumn) |
143 | if (mFetchScope.remoteRevisionRequested()) { |
144 | ADD_COLUMN(PimItem::remoteRevisionFullColumnName(), ItemQueryRemoteRevisionColumn) |
145 | } |
146 | if (mFetchScope.sizeRequested()) { |
147 | ADD_COLUMN(PimItem::sizeFullColumnName(), ItemQuerySizeColumn) |
148 | } |
149 | if (mFetchScope.mTimeRequested()) { |
150 | ADD_COLUMN(PimItem::datetimeFullColumnName(), ItemQueryDatetimeColumn) |
151 | } |
152 | ADD_COLUMN(PimItem::collectionIdFullColumnName(), ItemQueryCollectionIdColumn) |
153 | if (mFetchScope.gidRequested()) { |
154 | ADD_COLUMN(PimItem::gidFullColumnName(), ItemQueryPimItemGidColumn) |
155 | } |
156 | #undef ADD_COLUMN |
157 | |
158 | itemQuery.addSortColumn(PimItem::idFullColumnName(), Query::Descending); |
159 | |
160 | if (mScope.scope() != Scope::Invalid) { |
161 | ItemQueryHelper::scopeToQuery(mScope, mConnection->context(), itemQuery); |
162 | } |
163 | |
164 | if (mFetchScope.changedSince().isValid()) { |
165 | itemQuery.addValueCondition(PimItem::datetimeFullColumnName(), Query::GreaterOrEqual, mFetchScope.changedSince().toUTC()); |
166 | } |
167 | |
168 | if (!itemQuery.exec()) { |
169 | throw HandlerException("Unable to list items" ); |
170 | } |
171 | |
172 | itemQuery.query().next(); |
173 | |
174 | return itemQuery.query(); |
175 | } |
176 | |
177 | enum FlagQueryColumns { |
178 | FlagQueryIdColumn, |
179 | FlagQueryNameColumn |
180 | }; |
181 | |
182 | QSqlQuery FetchHelper::buildFlagQuery() |
183 | { |
184 | QueryBuilder flagQuery(PimItem::tableName()); |
185 | flagQuery.addJoin(QueryBuilder::InnerJoin, PimItemFlagRelation::tableName(), |
186 | PimItem::idFullColumnName(), PimItemFlagRelation::leftFullColumnName()); |
187 | flagQuery.addJoin(QueryBuilder::InnerJoin, Flag::tableName(), |
188 | Flag::idFullColumnName(), PimItemFlagRelation::rightFullColumnName()); |
189 | flagQuery.addColumn(PimItem::idFullColumnName()); |
190 | flagQuery.addColumn(Flag::nameFullColumnName()); |
191 | ItemQueryHelper::scopeToQuery(mScope, mConnection->context(), flagQuery); |
192 | flagQuery.addSortColumn(PimItem::idFullColumnName(), Query::Descending); |
193 | |
194 | if (!flagQuery.exec()) { |
195 | throw HandlerException("Unable to retrieve item flags" ); |
196 | } |
197 | |
198 | flagQuery.query().next(); |
199 | |
200 | return flagQuery.query(); |
201 | } |
202 | |
203 | enum TagQueryColumns { |
204 | TagQueryItemIdColumn, |
205 | TagQueryTagIdColumn, |
206 | }; |
207 | |
208 | QSqlQuery FetchHelper::buildTagQuery() |
209 | { |
210 | QueryBuilder tagQuery(PimItem::tableName()); |
211 | tagQuery.addJoin(QueryBuilder::InnerJoin, PimItemTagRelation::tableName(), |
212 | PimItem::idFullColumnName(), PimItemTagRelation::leftFullColumnName()); |
213 | tagQuery.addJoin(QueryBuilder::InnerJoin, Tag::tableName(), |
214 | Tag::idFullColumnName(), PimItemTagRelation::rightFullColumnName()); |
215 | tagQuery.addColumn(PimItem::idFullColumnName()); |
216 | tagQuery.addColumn(Tag::idFullColumnName()); |
217 | |
218 | ItemQueryHelper::scopeToQuery(mScope, mConnection->context(), tagQuery); |
219 | tagQuery.addSortColumn(PimItem::idFullColumnName(), Query::Descending); |
220 | |
221 | if (!tagQuery.exec()) { |
222 | throw HandlerException("Unable to retrieve item tags" ); |
223 | } |
224 | |
225 | tagQuery.query().next(); |
226 | |
227 | return tagQuery.query(); |
228 | } |
229 | |
230 | enum VRefQueryColumns { |
231 | VRefQueryCollectionIdColumn, |
232 | VRefQueryItemIdColumn |
233 | }; |
234 | |
235 | QSqlQuery FetchHelper::buildVRefQuery() |
236 | { |
237 | QueryBuilder vRefQuery(PimItem::tableName()); |
238 | vRefQuery.addJoin(QueryBuilder::LeftJoin, CollectionPimItemRelation::tableName(), |
239 | CollectionPimItemRelation::rightFullColumnName(), |
240 | PimItem::idFullColumnName()); |
241 | vRefQuery.addColumn(CollectionPimItemRelation::leftFullColumnName()); |
242 | vRefQuery.addColumn(CollectionPimItemRelation::rightFullColumnName()); |
243 | ItemQueryHelper::scopeToQuery(mScope, mConnection->context(), vRefQuery); |
244 | vRefQuery.addSortColumn(PimItem::idFullColumnName(), Query::Descending); |
245 | |
246 | if (!vRefQuery.exec()) { |
247 | throw HandlerException("Unable to retrieve virtual references" ); |
248 | } |
249 | |
250 | vRefQuery.query().next(); |
251 | |
252 | return vRefQuery.query(); |
253 | } |
254 | |
255 | bool FetchHelper::isScopeLocal(const Scope &scope) |
256 | { |
257 | // The only agent allowed to override local scope is the Baloo Indexer |
258 | if (!mConnection->sessionId().startsWith("akonadi_baloo_indexer" )) { |
259 | return false; |
260 | } |
261 | |
262 | // Get list of all resources that own all items in the scope |
263 | QueryBuilder qb(PimItem::tableName(), QueryBuilder::Select); |
264 | qb.setDistinct(true); |
265 | qb.addColumn(Resource::nameFullColumnName()); |
266 | qb.addJoin(QueryBuilder::LeftJoin, Collection::tableName(), |
267 | PimItem::collectionIdFullColumnName(), Collection::idFullColumnName()); |
268 | qb.addJoin(QueryBuilder::LeftJoin, Resource::tableName(), |
269 | Collection::resourceIdFullColumnName(), Resource::idFullColumnName()); |
270 | ItemQueryHelper::scopeToQuery(scope, mConnection->context(), qb); |
271 | if (mConnection->context()->resource().isValid()) { |
272 | qb.addValueCondition(Resource::nameFullColumnName(), Query::NotEquals, |
273 | mConnection->context()->resource().name()); |
274 | } |
275 | |
276 | if (!qb.exec()) { |
277 | throw HandlerException("Failed to query database" ); |
278 | return false; |
279 | } |
280 | |
281 | // If there is more than one resource, i.e. this is a fetch from multiple |
282 | // collections, then don't bother and just return FALSE. This case is aimed |
283 | // specifically on Baloo, which fetches items from each collection independently, |
284 | // so it will pass this check. |
285 | QSqlQuery query = qb.query(); |
286 | if (query.size() != 1) { |
287 | return false; |
288 | } |
289 | |
290 | query.next(); |
291 | const QString resourceName = query.value(0).toString(); |
292 | |
293 | org::freedesktop::Akonadi::AgentManager manager(AkDBus::serviceName(AkDBus::Control), |
294 | QLatin1String("/AgentManager" ), |
295 | DBusConnectionPool::threadConnection()); |
296 | const QString typeIdentifier = manager.agentInstanceType(resourceName); |
297 | const QVariantMap properties = manager.agentCustomProperties(typeIdentifier); |
298 | return properties.value(QLatin1String("HasLocalStorage" ), false).toBool(); |
299 | } |
300 | |
301 | QByteArray FetchHelper::tagsToByteArray(const Tag::List &tags) |
302 | { |
303 | QByteArray b; |
304 | QList<QByteArray> attributes; |
305 | b += "(" ; |
306 | Q_FOREACH (const Tag &tag, tags) { |
307 | b += "(" + TagFetchHelper::tagToByteArray(tag.id(), |
308 | tag.gid().toLatin1(), |
309 | tag.parentId(), |
310 | tag.tagType().name().toLatin1(), |
311 | QByteArray(), |
312 | TagFetchHelper::fetchTagAttributes(tag.id())) + ") " ; |
313 | } |
314 | b += ")" ; |
315 | return b; |
316 | } |
317 | |
318 | bool FetchHelper::fetchItems(const QByteArray &responseIdentifier) |
319 | { |
320 | // retrieve missing parts |
321 | // HACK: isScopeLocal() is a workaround for resources that have cache expiration |
322 | // because when the cache expires, Baloo is not able to content of the items. So |
323 | // we allow fetch of items that belong to local resources (like maildir) to ignore |
324 | // cacheOnly and retrieve missing parts from the resource. However ItemRetriever |
325 | // is painfully slow with many items and is generally designed to fetch a few |
326 | // messages, not all of them. In the long term, we need a better way to do this. |
327 | if (!mFetchScope.cacheOnly() || isScopeLocal(mScope)) { |
328 | // trigger a collection sync if configured to do so |
329 | triggerOnDemandFetch(); |
330 | |
331 | // Prepare for a call to ItemRetriever::exec(); |
332 | // From a resource perspective the only parts that can be fetched are payloads. |
333 | ItemRetriever retriever(mConnection); |
334 | retriever.setScope(mScope); |
335 | retriever.setRetrieveParts(mFetchScope.requestedPayloads()); |
336 | retriever.setRetrieveFullPayload(mFetchScope.fullPayload()); |
337 | retriever.setChangedSince(mFetchScope.changedSince()); |
338 | if (!retriever.exec() && !mFetchScope.ignoreErrors()) { // There we go, retrieve the missing parts from the resource. |
339 | if (mConnection->context()->resource().isValid()) { |
340 | throw HandlerException(QString::fromLatin1("Unable to fetch item from backend (collection %1, resource %2) : %3" ) |
341 | .arg(mConnection->context()->collectionId()) |
342 | .arg(mConnection->context()->resource().id()) |
343 | .arg(QString::fromLatin1(retriever.lastError()))); |
344 | } else { |
345 | throw HandlerException(QString::fromLatin1("Unable to fetch item from backend (collection %1) : %2" ) |
346 | .arg(mConnection->context()->collectionId()) |
347 | .arg(QString::fromLatin1(retriever.lastError()))); |
348 | } |
349 | } |
350 | } |
351 | |
352 | QSqlQuery itemQuery = buildItemQuery(); |
353 | |
354 | // error if query did not find any item and scope is not listing items but |
355 | // a request for a specific item |
356 | if (!itemQuery.isValid()) { |
357 | if (mFetchScope.ignoreErrors()) { |
358 | return true; |
359 | } |
360 | switch (mScope.scope()) { |
361 | case Scope::Uid: // fall through |
362 | case Scope::Rid: // fall through |
363 | case Scope::HierarchicalRid: // fall through |
364 | case Scope::Gid: |
365 | throw HandlerException("Item query returned empty result set" ); |
366 | break; |
367 | default: |
368 | break; |
369 | } |
370 | } |
371 | // build part query if needed |
372 | QSqlQuery partQuery; |
373 | if (!mFetchScope.requestedParts().isEmpty() || mFetchScope.fullPayload() || mFetchScope.allAttributes()) { |
374 | partQuery = buildPartQuery(mFetchScope.requestedParts(), mFetchScope.fullPayload(), mFetchScope.allAttributes()); |
375 | } |
376 | |
377 | // build flag query if needed |
378 | QSqlQuery flagQuery; |
379 | if (mFetchScope.flagsRequested()) { |
380 | flagQuery = buildFlagQuery(); |
381 | } |
382 | |
383 | // build tag query if needed |
384 | QSqlQuery tagQuery; |
385 | if (mFetchScope.tagsRequested()) { |
386 | tagQuery = buildTagQuery(); |
387 | } |
388 | |
389 | QSqlQuery vRefQuery; |
390 | if (mFetchScope.virtualReferencesRequested()) { |
391 | vRefQuery = buildVRefQuery(); |
392 | } |
393 | |
394 | // build responses |
395 | Response response; |
396 | response.setUntagged(); |
397 | while (itemQuery.isValid()) { |
398 | const qint64 pimItemId = extractQueryResult(itemQuery, ItemQueryPimItemIdColumn).toLongLong(); |
399 | const int pimItemRev = extractQueryResult(itemQuery, ItemQueryRevColumn).toInt(); |
400 | |
401 | QList<QByteArray> attributes; |
402 | attributes.append(AKONADI_PARAM_UID " " + QByteArray::number(pimItemId)); |
403 | attributes.append(AKONADI_PARAM_REVISION " " + QByteArray::number(pimItemRev)); |
404 | if (mFetchScope.remoteIdRequested()) { |
405 | attributes.append(AKONADI_PARAM_REMOTEID " " + ImapParser::quote(Utils::variantToByteArray(extractQueryResult(itemQuery, ItemQueryPimItemRidColumn)))); |
406 | } |
407 | attributes.append(AKONADI_PARAM_MIMETYPE " " + ImapParser::quote(Utils::variantToByteArray(extractQueryResult(itemQuery, ItemQueryMimeTypeColumn)))); |
408 | Collection::Id parentCollectionId = extractQueryResult(itemQuery, ItemQueryCollectionIdColumn).toLongLong(); |
409 | attributes.append(AKONADI_PARAM_COLLECTIONID " " + QByteArray::number(parentCollectionId)); |
410 | |
411 | if (mFetchScope.sizeRequested()) { |
412 | const qint64 pimItemSize = extractQueryResult(itemQuery, ItemQuerySizeColumn).toLongLong(); |
413 | attributes.append(AKONADI_PARAM_SIZE " " + QByteArray::number(pimItemSize)); |
414 | } |
415 | if (mFetchScope.mTimeRequested()) { |
416 | const QDateTime pimItemDatetime = extractQueryResult(itemQuery, ItemQueryDatetimeColumn).toDateTime(); |
417 | // Date time is always stored in UTC time zone by the server. |
418 | QString datetime = QLocale::c().toString(pimItemDatetime, QLatin1String("dd-MMM-yyyy hh:mm:ss +0000" )); |
419 | attributes.append(AKONADI_PARAM_MTIME " " + ImapParser::quote(datetime.toUtf8())); |
420 | } |
421 | if (mFetchScope.remoteRevisionRequested()) { |
422 | const QByteArray rrev = Utils::variantToByteArray(extractQueryResult(itemQuery, ItemQueryRemoteRevisionColumn)); |
423 | if (!rrev.isEmpty()) { |
424 | attributes.append(AKONADI_PARAM_REMOTEREVISION " " + ImapParser::quote(rrev)); |
425 | } |
426 | } |
427 | if (mFetchScope.gidRequested()) { |
428 | const QByteArray gid = Utils::variantToByteArray(extractQueryResult(itemQuery, ItemQueryPimItemGidColumn)); |
429 | if (!gid.isEmpty()) { |
430 | attributes.append(AKONADI_PARAM_GID " " + ImapParser::quote(gid)); |
431 | } |
432 | } |
433 | |
434 | if (mFetchScope.flagsRequested()) { |
435 | QList<QByteArray> flags; |
436 | while (flagQuery.isValid()) { |
437 | const qint64 id = flagQuery.value(FlagQueryIdColumn).toLongLong(); |
438 | if (id > pimItemId) { |
439 | flagQuery.next(); |
440 | continue; |
441 | } else if (id < pimItemId) { |
442 | break; |
443 | } |
444 | flags << Utils::variantToByteArray(flagQuery.value(FlagQueryNameColumn)); |
445 | flagQuery.next(); |
446 | } |
447 | attributes.append(AKONADI_PARAM_FLAGS " (" + ImapParser::join(flags, " " ) + ')'); |
448 | } |
449 | |
450 | if (mFetchScope.tagsRequested()) { |
451 | ImapSet tags; |
452 | QVector<qint64> tagIds; |
453 | //We don't take the fetch scope into account yet. It's either id only or the full tag. |
454 | const bool fullTagsRequested = !mFetchScope.tagFetchScope().isEmpty(); |
455 | while (tagQuery.isValid()) { |
456 | const qint64 id = tagQuery.value(TagQueryItemIdColumn).toLongLong(); |
457 | if (id > pimItemId) { |
458 | tagQuery.next(); |
459 | continue; |
460 | } else if (id < pimItemId) { |
461 | break; |
462 | } |
463 | const qint64 tagId = tagQuery.value(TagQueryTagIdColumn).toLongLong(); |
464 | tags.add(QVector<qint64>() << tagId); |
465 | |
466 | tagIds << tagId; |
467 | tagQuery.next(); |
468 | } |
469 | if (!fullTagsRequested) { |
470 | if (!tags.isEmpty()) { |
471 | attributes.append(AKONADI_PARAM_TAGS " " + tags.toImapSequenceSet()); |
472 | } |
473 | } else { |
474 | Tag::List tagList; |
475 | Q_FOREACH (qint64 t, tagIds) { |
476 | tagList << Tag::retrieveById(t); |
477 | } |
478 | attributes.append(AKONADI_PARAM_TAGS " " + tagsToByteArray(tagList)); |
479 | } |
480 | } |
481 | |
482 | if (mFetchScope.virtualReferencesRequested()) { |
483 | ImapSet cols; |
484 | while (vRefQuery.isValid()) { |
485 | const qint64 id = vRefQuery.value(VRefQueryItemIdColumn).toLongLong(); |
486 | if (id > pimItemId) { |
487 | vRefQuery.next(); |
488 | continue; |
489 | } else if (id < pimItemId) { |
490 | break; |
491 | } |
492 | const qint64 collectionId = vRefQuery.value(VRefQueryCollectionIdColumn).toLongLong(); |
493 | cols.add(QVector<qint64>() << collectionId); |
494 | vRefQuery.next(); |
495 | } |
496 | if (!cols.isEmpty()) { |
497 | attributes.append(AKONADI_PARAM_VIRTREF " " + cols.toImapSequenceSet()); |
498 | } |
499 | } |
500 | |
501 | if (mFetchScope.ancestorDepth() > 0) { |
502 | attributes.append(HandlerHelper::ancestorsToByteArray(mFetchScope.ancestorDepth(), ancestorsForItem(parentCollectionId))); |
503 | } |
504 | |
505 | bool skipItem = false; |
506 | |
507 | QList<QByteArray> cachedParts; |
508 | |
509 | while (partQuery.isValid()) { |
510 | const qint64 id = partQuery.value(PartQueryPimIdColumn).toLongLong(); |
511 | if (id > pimItemId) { |
512 | partQuery.next(); |
513 | continue; |
514 | } else if (id < pimItemId) { |
515 | break; |
516 | } |
517 | const QByteArray partName = Utils::variantToByteArray(partQuery.value(PartQueryTypeNamespaceColumn)) + ':' + |
518 | Utils::variantToByteArray(partQuery.value(PartQueryTypeNameColumn)); |
519 | QByteArray part = partName; |
520 | QByteArray data = Utils::variantToByteArray(partQuery.value(PartQueryDataColumn)); |
521 | |
522 | if (mFetchScope.checkCachedPayloadPartsOnly()) { |
523 | if (!data.isEmpty()) { |
524 | cachedParts << part; |
525 | } |
526 | partQuery.next(); |
527 | } else { |
528 | if (mFetchScope.ignoreErrors() && data.isEmpty()) { |
529 | //We wanted the payload, couldn't get it, and are ignoring errors. Skip the item. |
530 | //This is not an error though, it's fine to have empty payload parts (to denote existing but not cached parts) |
531 | //akDebug() << "item" << id << "has an empty payload part in parttable for part" << partName; |
532 | skipItem = true; |
533 | break; |
534 | } |
535 | const bool partIsExternal = partQuery.value(PartQueryExternalColumn).toBool(); |
536 | if (!mFetchScope.externalPayloadSupported() && partIsExternal) { //external payload not supported by the client, translate the data |
537 | data = PartHelper::translateData(data, partIsExternal); |
538 | } |
539 | int version = partQuery.value(PartQueryVersionColumn).toInt(); |
540 | if (version != 0) { // '0' is the default, so don't send it |
541 | part += '[' + QByteArray::number(version) + ']'; |
542 | } |
543 | if (mFetchScope.externalPayloadSupported() && partIsExternal) { // external data and this is supported by the client |
544 | part += " [FILE] " ; |
545 | } |
546 | if (data.isNull()) { |
547 | part += " NIL" ; |
548 | } else if (data.isEmpty()) { |
549 | part += " \"\"" ; |
550 | } else { |
551 | if (partIsExternal) { |
552 | if (!mConnection->capabilities().noPayloadPath()) { |
553 | data = PartHelper::resolveAbsolutePath(data).toLocal8Bit(); |
554 | } |
555 | } |
556 | |
557 | part += " {" + QByteArray::number(data.length()) + "}\r\n" ; |
558 | part += data; |
559 | } |
560 | |
561 | if (mFetchScope.requestedParts().contains(partName) || mFetchScope.fullPayload() || mFetchScope.allAttributes()) { |
562 | attributes << part; |
563 | } |
564 | |
565 | partQuery.next(); |
566 | } |
567 | } |
568 | |
569 | if (skipItem) { |
570 | itemQuery.next(); |
571 | continue; |
572 | } |
573 | |
574 | if (mFetchScope.checkCachedPayloadPartsOnly()) { |
575 | attributes.append(AKONADI_PARAM_CACHEDPARTS " (" + ImapParser::join(cachedParts, " " ) + ')'); |
576 | } |
577 | |
578 | // IMAP protocol violation: should actually be the sequence number |
579 | QByteArray attr = QByteArray::number(pimItemId) + ' ' + responseIdentifier + " (" ; |
580 | attr += ImapParser::join(attributes, " " ) + ')'; |
581 | response.setUntagged(); |
582 | response.setString(attr); |
583 | Q_EMIT responseAvailable(response); |
584 | |
585 | itemQuery.next(); |
586 | } |
587 | |
588 | // update atime (only if the payload was actually requested, otherwise a simple resource sync prevents cache clearing) |
589 | if (needsAccessTimeUpdate(mFetchScope.requestedParts()) || mFetchScope.fullPayload()) { |
590 | updateItemAccessTime(); |
591 | } |
592 | |
593 | return true; |
594 | } |
595 | |
596 | bool FetchHelper::needsAccessTimeUpdate(const QVector<QByteArray> &parts) |
597 | { |
598 | // TODO technically we should compare the part list with the cache policy of |
599 | // the parent collection of the retrieved items, but that's kinda expensive |
600 | // Only updating the atime if the full payload was requested is a good |
601 | // approximation though. |
602 | return parts.contains(AKONADI_PARAM_PLD_RFC822); |
603 | } |
604 | |
605 | void FetchHelper::updateItemAccessTime() |
606 | { |
607 | Transaction transaction(mConnection->storageBackend()); |
608 | QueryBuilder qb(PimItem::tableName(), QueryBuilder::Update); |
609 | qb.setColumnValue(PimItem::atimeColumn(), QDateTime::currentDateTime()); |
610 | ItemQueryHelper::scopeToQuery(mScope, mConnection->context(), qb); |
611 | |
612 | if (!qb.exec()) { |
613 | qWarning() << "Unable to update item access time" ; |
614 | } else { |
615 | transaction.commit(); |
616 | } |
617 | } |
618 | |
619 | void FetchHelper::triggerOnDemandFetch() |
620 | { |
621 | if (mScope.scope() != Scope::None || mConnection->context()->collectionId() <= 0 || mFetchScope.cacheOnly()) { |
622 | return; |
623 | } |
624 | |
625 | Collection collection = mConnection->context()->collection(); |
626 | |
627 | // HACK: don't trigger on-demand syncing if the resource is the one triggering it |
628 | if (mConnection->sessionId() == collection.resource().name().toLatin1()) { |
629 | return; |
630 | } |
631 | |
632 | DataStore *store = mConnection->storageBackend(); |
633 | store->activeCachePolicy(collection); |
634 | if (!collection.cachePolicySyncOnDemand()) { |
635 | return; |
636 | } |
637 | |
638 | if (AkonadiServer::instance()->intervalChecker()) { |
639 | AkonadiServer::instance()->intervalChecker()->requestCollectionSync(collection); |
640 | } |
641 | } |
642 | |
643 | QStack<Collection> FetchHelper::ancestorsForItem(Collection::Id parentColId) |
644 | { |
645 | if (mFetchScope.ancestorDepth() <= 0 || parentColId == 0) { |
646 | return QStack<Collection>(); |
647 | } |
648 | if (mAncestorCache.contains(parentColId)) { |
649 | return mAncestorCache.value(parentColId); |
650 | } |
651 | |
652 | QStack<Collection> ancestors; |
653 | Collection col = Collection::retrieveById(parentColId); |
654 | for (int i = 0; i < mFetchScope.ancestorDepth(); ++i) { |
655 | if (!col.isValid()) { |
656 | break; |
657 | } |
658 | ancestors.prepend(col); |
659 | col = col.parent(); |
660 | } |
661 | mAncestorCache.insert(parentColId, ancestors); |
662 | return ancestors; |
663 | } |
664 | |
665 | QVariant FetchHelper::extractQueryResult(const QSqlQuery &query, FetchHelper::ItemQueryColumns column) const |
666 | { |
667 | Q_ASSERT(mItemQueryColumnMap[column] >= 0); |
668 | return query.value(mItemQueryColumnMap[column]); |
669 | } |
670 | |