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
54using namespace Akonadi;
55using namespace Akonadi::Server;
56
57FetchHelper::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
66enum PartQueryColumns {
67 PartQueryPimIdColumn,
68 PartQueryTypeNamespaceColumn,
69 PartQueryTypeNameColumn,
70 PartQueryDataColumn,
71 PartQueryExternalColumn,
72 PartQueryVersionColumn
73};
74
75QSqlQuery 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
128QSqlQuery 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
177enum FlagQueryColumns {
178 FlagQueryIdColumn,
179 FlagQueryNameColumn
180};
181
182QSqlQuery 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
203enum TagQueryColumns {
204 TagQueryItemIdColumn,
205 TagQueryTagIdColumn,
206};
207
208QSqlQuery 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
230enum VRefQueryColumns {
231 VRefQueryCollectionIdColumn,
232 VRefQueryItemIdColumn
233};
234
235QSqlQuery 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
255bool 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
301QByteArray 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
318bool 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
596bool 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
605void 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
619void 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
643QStack<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
665QVariant FetchHelper::extractQueryResult(const QSqlQuery &query, FetchHelper::ItemQueryColumns column) const
666{
667 Q_ASSERT(mItemQueryColumnMap[column] >= 0);
668 return query.value(mItemQueryColumnMap[column]);
669}
670