• Skip to content
  • Skip to link menu
  • KDE API Reference
  • kdepimlibs-4.10.5 API Reference
  • KDE Home
  • Contact Us
 

akonadi

  • akonadi
specialcollectionshelperjobs.cpp
1 /*
2  Copyright (c) 2009 Constantin Berzan <exit3219@gmail.com>
3 
4  This library is free software; you can redistribute it and/or modify it
5  under the terms of the GNU Library General Public License as published by
6  the Free Software Foundation; either version 2 of the License, or (at your
7  option) any later version.
8 
9  This library is distributed in the hope that it will be useful, but WITHOUT
10  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11  FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12  License for more details.
13 
14  You should have received a copy of the GNU Library General Public License
15  along with this library; see the file COPYING.LIB. If not, write to the
16  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17  02110-1301, USA.
18 */
19 
20 #include "specialcollectionshelperjobs_p.h"
21 
22 #include "dbusconnectionpool.h"
23 #include "specialcollectionattribute_p.h"
24 #include "specialcollections.h"
25 #include "servermanager.h"
26 
27 #include <akonadi/agentinstance.h>
28 #include <akonadi/agentinstancecreatejob.h>
29 #include <akonadi/agentmanager.h>
30 #include <akonadi/collectionfetchjob.h>
31 #include <akonadi/collectionfetchscope.h>
32 #include <akonadi/collectionmodifyjob.h>
33 #include <akonadi/entitydisplayattribute.h>
34 #include <akonadi/resourcesynchronizationjob.h>
35 
36 #include <KDebug>
37 #include <KLocalizedString>
38 #include <KStandardDirs>
39 #include <kcoreconfigskeleton.h>
40 
41 #include <QtDBus/QDBusConnectionInterface>
42 #include <QtDBus/QDBusInterface>
43 #include <QtDBus/QDBusServiceWatcher>
44 #include <QtCore/QMetaMethod>
45 #include <QtCore/QTime>
46 #include <QtCore/QTimer>
47 
48 #define LOCK_WAIT_TIMEOUT_SECONDS 10
49 
50 using namespace Akonadi;
51 
52 // convenient methods to get/set the default resource id
53 static void setDefaultResourceId( KCoreConfigSkeleton *settings, const QString &value )
54 {
55  KConfigSkeletonItem *item = settings->findItem( QLatin1String( "DefaultResourceId" ) );
56  Q_ASSERT( item );
57  item->setProperty( value );
58 }
59 
60 static QString defaultResourceId( KCoreConfigSkeleton *settings )
61 {
62  const KConfigSkeletonItem *item = settings->findItem( QLatin1String( "DefaultResourceId" ) );
63  Q_ASSERT( item );
64  return item->property().toString();
65 }
66 
67 static QString dbusServiceName()
68 {
69  QString service = QString::fromLatin1("org.kde.pim.SpecialCollections");
70  if (ServerManager::hasInstanceIdentifier())
71  return service + ServerManager::instanceIdentifier();
72  return service;
73 }
74 
75 static QVariant::Type argumentType( const QMetaObject *mo, const QString &method )
76 {
77  QMetaMethod m;
78  for ( int i = 0; i < mo->methodCount(); ++i ) {
79  const QString signature = QString::fromLatin1( mo->method( i ).signature() );
80  if ( signature.startsWith( method ) )
81  m = mo->method( i );
82  }
83 
84  if ( !m.signature() )
85  return QVariant::Invalid;
86 
87  const QList<QByteArray> argTypes = m.parameterTypes();
88  if ( argTypes.count() != 1 )
89  return QVariant::Invalid;
90 
91  return QVariant::nameToType( argTypes.first() );
92 }
93 
94 // ===================== ResourceScanJob ============================
95 
99 class Akonadi::ResourceScanJob::Private
100 {
101  public:
102  Private( KCoreConfigSkeleton *settings, ResourceScanJob *qq );
103 
104  void fetchResult( KJob *job ); // slot
105 
106  ResourceScanJob *const q;
107 
108  // Input:
109  QString mResourceId;
110  KCoreConfigSkeleton *mSettings;
111 
112  // Output:
113  Collection mRootCollection;
114  Collection::List mSpecialCollections;
115 };
116 
117 ResourceScanJob::Private::Private( KCoreConfigSkeleton *settings, ResourceScanJob *qq )
118  : q( qq ), mSettings( settings )
119 {
120 }
121 
122 void ResourceScanJob::Private::fetchResult( KJob *job )
123 {
124  if ( job->error() ) {
125  kWarning() << job->errorText();
126  return;
127  }
128 
129  CollectionFetchJob *fetchJob = qobject_cast<CollectionFetchJob *>( job );
130  Q_ASSERT( fetchJob );
131 
132  Q_ASSERT( !mRootCollection.isValid() );
133  Q_ASSERT( mSpecialCollections.isEmpty() );
134  foreach ( const Collection &collection, fetchJob->collections() ) {
135  if ( collection.parentCollection() == Collection::root() ) {
136  if ( mRootCollection.isValid() )
137  kWarning() << "Resource has more than one root collection. I don't know what to do.";
138  else
139  mRootCollection = collection;
140  }
141 
142  if ( collection.hasAttribute<SpecialCollectionAttribute>() )
143  mSpecialCollections.append( collection );
144  }
145 
146  kDebug() << "Fetched root collection" << mRootCollection.id()
147  << "and" << mSpecialCollections.count() << "local folders"
148  << "(total" << fetchJob->collections().count() << "collections).";
149 
150  if ( !mRootCollection.isValid() ) {
151  q->setError( Unknown );
152  q->setErrorText( i18n( "Could not fetch root collection of resource %1.", mResourceId ) );
153  q->emitResult();
154  return;
155  }
156 
157  // We are done!
158  q->emitResult();
159 }
160 
161 
162 
163 ResourceScanJob::ResourceScanJob( const QString &resourceId, KCoreConfigSkeleton *settings, QObject *parent )
164  : Job( parent ),
165  d( new Private( settings, this ) )
166 {
167  setResourceId( resourceId );
168 }
169 
170 ResourceScanJob::~ResourceScanJob()
171 {
172  delete d;
173 }
174 
175 QString ResourceScanJob::resourceId() const
176 {
177  return d->mResourceId;
178 }
179 
180 void ResourceScanJob::setResourceId( const QString &resourceId )
181 {
182  d->mResourceId = resourceId;
183 }
184 
185 Akonadi::Collection ResourceScanJob::rootResourceCollection() const
186 {
187  return d->mRootCollection;
188 }
189 
190 Akonadi::Collection::List ResourceScanJob::specialCollections() const
191 {
192  return d->mSpecialCollections;
193 }
194 
195 void ResourceScanJob::doStart()
196 {
197  if ( d->mResourceId.isEmpty() ) {
198  kError() << "No resource ID given.";
199  setError( Job::Unknown );
200  setErrorText( i18n( "No resource ID given." ) );
201  emitResult();
202  return;
203  }
204 
205  CollectionFetchJob *fetchJob = new CollectionFetchJob( Collection::root(),
206  CollectionFetchJob::Recursive, this );
207  fetchJob->fetchScope().setResource( d->mResourceId );
208  fetchJob->fetchScope().setIncludeStatistics( true );
209  connect( fetchJob, SIGNAL(result(KJob*)), this, SLOT(fetchResult(KJob*)) );
210 }
211 
212 
213 // ===================== DefaultResourceJob ============================
214 
218 class Akonadi::DefaultResourceJobPrivate
219 {
220  public:
221  DefaultResourceJobPrivate( KCoreConfigSkeleton *settings, DefaultResourceJob *qq );
222 
223  void tryFetchResource();
224  void resourceCreateResult( KJob *job ); // slot
225  void resourceSyncResult( KJob *job ); // slot
226  void collectionFetchResult( KJob *job ); // slot
227  void collectionModifyResult( KJob *job ); // slot
228 
229  DefaultResourceJob *const q;
230  KCoreConfigSkeleton *mSettings;
231  bool mResourceWasPreexisting;
232  int mPendingModifyJobs;
233  QString mDefaultResourceType;
234  QVariantMap mDefaultResourceOptions;
235  QList<QByteArray> mKnownTypes;
236  QMap<QByteArray, QString> mNameForTypeMap;
237  QMap<QByteArray, QString> mIconForTypeMap;
238 };
239 
240 DefaultResourceJobPrivate::DefaultResourceJobPrivate( KCoreConfigSkeleton *settings, DefaultResourceJob *qq )
241  : q( qq ),
242  mSettings( settings ),
243  mResourceWasPreexisting( true /* for safety, so as not to accidentally delete data */ ),
244  mPendingModifyJobs( 0 )
245 {
246 }
247 
248 void DefaultResourceJobPrivate::tryFetchResource()
249 {
250  // Get the resourceId from config. Another instance might have changed it in the meantime.
251  mSettings->readConfig();
252 
253  const QString resourceId = defaultResourceId( mSettings );
254 
255  kDebug() << "Read defaultResourceId" << resourceId << "from config.";
256 
257  const AgentInstance resource = AgentManager::self()->instance( resourceId );
258  if ( resource.isValid() ) {
259  // The resource exists; scan it.
260  mResourceWasPreexisting = true;
261  kDebug() << "Found resource" << resourceId;
262  q->setResourceId( resourceId );
263 
264  CollectionFetchJob *fetchJob = new CollectionFetchJob( Collection::root(), CollectionFetchJob::Recursive, q );
265  fetchJob->fetchScope().setResource( resourceId );
266  fetchJob->fetchScope().setIncludeStatistics( true );
267  q->connect( fetchJob, SIGNAL(result(KJob*)), q, SLOT(collectionFetchResult(KJob*)) );
268  } else {
269  // Try harder: maybe the default resource has been removed and another one added
270  // without updating the config file, in this case search for a resource
271  // of the same type and the default name
272  const AgentInstance::List resources = AgentManager::self()->instances();
273  foreach ( const AgentInstance &resource, resources ) {
274  if ( resource.type().identifier() == mDefaultResourceType ) {
275  if ( resource.name() == mDefaultResourceOptions.value( QLatin1String( "Name" ) ).toString() ) {
276  // found a matching one...
277  setDefaultResourceId( mSettings, resource.identifier() );
278  mSettings->writeConfig();
279  mResourceWasPreexisting = true;
280  kDebug() << "Found resource" << resource.identifier();
281  q->setResourceId( resource.identifier() );
282  q->ResourceScanJob::doStart();
283  return;
284  }
285  }
286  }
287 
288  // Create the resource.
289  mResourceWasPreexisting = false;
290  kDebug() << "Creating maildir resource.";
291  const AgentType type = AgentManager::self()->type( mDefaultResourceType );
292  AgentInstanceCreateJob *job = new AgentInstanceCreateJob( type, q );
293  QObject::connect( job, SIGNAL(result(KJob*)), q, SLOT(resourceCreateResult(KJob*)) );
294  job->start(); // non-Akonadi::Job
295  }
296 }
297 
298 void DefaultResourceJobPrivate::resourceCreateResult( KJob *job )
299 {
300  if ( job->error() ) {
301  kWarning() << job->errorText();
302  //fail( i18n( "Failed to create the default resource (%1).", job->errorString() ) );
303  q->setError( job->error() );
304  q->setErrorText( job->errorText() );
305  q->emitResult();
306  return;
307  }
308 
309  AgentInstance agent;
310 
311  // Get the resource instance.
312  {
313  AgentInstanceCreateJob *createJob = qobject_cast<AgentInstanceCreateJob*>( job );
314  Q_ASSERT( createJob );
315  agent = createJob->instance();
316  setDefaultResourceId( mSettings, agent.identifier() );
317  kDebug() << "Created maildir resource with id" << defaultResourceId( mSettings );
318  }
319 
320  const QString defaultId = defaultResourceId( mSettings );
321 
322  // Configure the resource.
323  {
324  agent.setName( mDefaultResourceOptions.value( QLatin1String( "Name" ) ).toString() );
325 
326  QDBusInterface conf( QString::fromLatin1( "org.freedesktop.Akonadi.Resource." ) + defaultId,
327  QString::fromLatin1( "/Settings" ), QString() );
328 
329  if ( !conf.isValid() ) {
330  q->setError( -1 );
331  q->setErrorText( i18n( "Invalid resource identifier '%1'", defaultId ) );
332  q->emitResult();
333  return;
334  }
335 
336  QMapIterator<QString, QVariant> it( mDefaultResourceOptions );
337  while ( it.hasNext() ) {
338  it.next();
339 
340  if ( it.key() == QLatin1String( "Name" ) )
341  continue;
342 
343  const QString methodName = QString::fromLatin1( "set%1" ).arg( it.key() );
344  const QVariant::Type argType = argumentType( conf.metaObject(), methodName );
345  if ( argType == QVariant::Invalid ) {
346  q->setError( Job::Unknown );
347  q->setErrorText( i18n( "Failed to configure default resource via D-Bus." ) );
348  q->emitResult();
349  return;
350  }
351 
352  QDBusReply<void> reply = conf.call( methodName, it.value() );
353  if ( !reply.isValid() ) {
354  q->setError( Job::Unknown );
355  q->setErrorText( i18n( "Failed to configure default resource via D-Bus." ) );
356  q->emitResult();
357  return;
358  }
359  }
360 
361  conf.call( QLatin1String( "writeConfig" ) );
362 
363  agent.reconfigure();
364  }
365 
366  // Sync the resource.
367  {
368  ResourceSynchronizationJob *syncJob = new ResourceSynchronizationJob( agent, q );
369  QObject::connect( syncJob, SIGNAL(result(KJob*)), q, SLOT(resourceSyncResult(KJob*)) );
370  syncJob->start(); // non-Akonadi
371  }
372 }
373 
374 void DefaultResourceJobPrivate::resourceSyncResult( KJob *job )
375 {
376  if ( job->error() ) {
377  kWarning() << job->errorText();
378  //fail( i18n( "ResourceSynchronizationJob failed (%1).", job->errorString() ) );
379  return;
380  }
381 
382  // Fetch the collections of the resource.
383  kDebug() << "Fetching maildir collections.";
384  CollectionFetchJob *fetchJob = new CollectionFetchJob( Collection::root(), CollectionFetchJob::Recursive, q );
385  fetchJob->fetchScope().setResource( defaultResourceId( mSettings ) );
386  QObject::connect( fetchJob, SIGNAL(result(KJob*)), q, SLOT(collectionFetchResult(KJob*)) );
387 }
388 
389 void DefaultResourceJobPrivate::collectionFetchResult( KJob *job )
390 {
391  if ( job->error() ) {
392  kWarning() << job->errorText();
393  //fail( i18n( "Failed to fetch the root maildir collection (%1).", job->errorString() ) );
394  return;
395  }
396 
397  CollectionFetchJob *fetchJob = qobject_cast<CollectionFetchJob *>( job );
398  Q_ASSERT( fetchJob );
399 
400  const Collection::List collections = fetchJob->collections();
401  kDebug() << "Fetched" << collections.count() << "collections.";
402 
403  // Find the root maildir collection.
404  Collection::List toRecover;
405  Collection resourceCollection;
406  foreach ( const Collection &collection, collections ) {
407  if ( collection.parentCollection() == Collection::root() ) {
408  resourceCollection = collection;
409  toRecover.append( collection );
410  break;
411  }
412  }
413 
414  if ( !resourceCollection.isValid() ) {
415  q->setError( Job::Unknown );
416  q->setErrorText( i18n( "Failed to fetch the resource collection." ) );
417  q->emitResult();
418  return;
419  }
420 
421  // Find all children of the resource collection.
422  foreach ( const Collection &collection, collections ) {
423  if ( collection.parentCollection() == resourceCollection ) {
424  toRecover.append( collection );
425  }
426  }
427 
428  QHash<QString, QByteArray> typeForName;
429  foreach ( const QByteArray &type, mKnownTypes ) {
430  const QString displayName = mNameForTypeMap.value( type );
431  typeForName[ displayName ] = type;
432  }
433 
434  // These collections have been created by the maildir resource, when it
435  // found the folders on disk. So give them the necessary attributes now.
436  Q_ASSERT( mPendingModifyJobs == 0 );
437  foreach ( Collection collection, toRecover ) { // krazy:exclude=foreach
438 
439  if ( collection.hasAttribute<SpecialCollectionAttribute>() )
440  continue;
441 
442  // Find the type for the collection.
443  QString name = collection.name();
444  if ( collection.hasAttribute<EntityDisplayAttribute>() ) {
445  const QString displayName = collection.attribute<EntityDisplayAttribute>()->displayName();
446  if (!displayName.isEmpty())
447  name = displayName;
448  }
449  const QByteArray type = typeForName.value( name );
450 
451  if ( !type.isEmpty() ) {
452  kDebug() << "Recovering collection" << name;
453  setCollectionAttributes( collection, type, mNameForTypeMap, mIconForTypeMap );
454 
455  CollectionModifyJob *modifyJob = new CollectionModifyJob( collection, q );
456  QObject::connect( modifyJob, SIGNAL(result(KJob*)), q, SLOT(collectionModifyResult(KJob*)) );
457  mPendingModifyJobs++;
458  } else {
459  kDebug() << "Searching for names: " << typeForName.keys();
460  kDebug() << "Unknown collection name" << name << "-- not recovering.";
461  }
462  }
463 
464  if ( mPendingModifyJobs == 0 ) {
465  // Scan the resource.
466  q->setResourceId( defaultResourceId( mSettings ) );
467  q->ResourceScanJob::doStart();
468  }
469 }
470 
471 void DefaultResourceJobPrivate::collectionModifyResult( KJob *job )
472 {
473  if ( job->error() ) {
474  kWarning() << job->errorText();
475  //fail( i18n( "Failed to modify the root maildir collection (%1).", job->errorString() ) );
476  return;
477  }
478 
479  Q_ASSERT( mPendingModifyJobs > 0 );
480  mPendingModifyJobs--;
481  kDebug() << "pendingModifyJobs now" << mPendingModifyJobs;
482  if ( mPendingModifyJobs == 0 ) {
483  // Write the updated config.
484  kDebug() << "Writing defaultResourceId" << defaultResourceId( mSettings ) << "to config.";
485  mSettings->writeConfig();
486 
487  // Scan the resource.
488  q->setResourceId( defaultResourceId( mSettings ) );
489  q->ResourceScanJob::doStart();
490  }
491 }
492 
493 
494 
495 DefaultResourceJob::DefaultResourceJob( KCoreConfigSkeleton *settings, QObject *parent )
496  : ResourceScanJob( QString(), settings, parent ),
497  d( new DefaultResourceJobPrivate( settings, this ) )
498 {
499 }
500 
501 DefaultResourceJob::~DefaultResourceJob()
502 {
503  delete d;
504 }
505 
506 void DefaultResourceJob::setDefaultResourceType( const QString &type )
507 {
508  d->mDefaultResourceType = type;
509 }
510 
511 void DefaultResourceJob::setDefaultResourceOptions( const QVariantMap &options )
512 {
513  d->mDefaultResourceOptions = options;
514 }
515 
516 void DefaultResourceJob::setTypes( const QList<QByteArray> &types )
517 {
518  d->mKnownTypes = types;
519 }
520 
521 void DefaultResourceJob::setNameForTypeMap( const QMap<QByteArray, QString> &map )
522 {
523  d->mNameForTypeMap = map;
524 }
525 
526 void DefaultResourceJob::setIconForTypeMap( const QMap<QByteArray, QString> &map )
527 {
528  d->mIconForTypeMap = map;
529 }
530 
531 void DefaultResourceJob::doStart()
532 {
533  d->tryFetchResource();
534 }
535 
536 void DefaultResourceJob::slotResult( KJob *job )
537 {
538  if ( job->error() ) {
539  kWarning() << job->errorText();
540  // Do some cleanup.
541  if ( !d->mResourceWasPreexisting ) {
542  // We only removed the resource instance if we have created it.
543  // Otherwise we might lose the user's data.
544  const AgentInstance resource = AgentManager::self()->instance( defaultResourceId( d->mSettings ) );
545  kDebug() << "Removing resource" << resource.identifier();
546  AgentManager::self()->removeInstance( resource );
547  }
548  }
549 
550  Job::slotResult( job );
551 }
552 
553 // ===================== GetLockJob ============================
554 
555 class Akonadi::GetLockJob::Private
556 {
557  public:
558  Private( GetLockJob *qq );
559 
560  void doStart(); // slot
561  void serviceOwnerChanged( const QString &name, const QString &oldOwner,
562  const QString &newOwner ); // slot
563  void timeout(); // slot
564 
565  GetLockJob *const q;
566  QTimer *mSafetyTimer;
567 };
568 
569 GetLockJob::Private::Private( GetLockJob *qq )
570  : q( qq ),
571  mSafetyTimer( 0 )
572 {
573 }
574 
575 void GetLockJob::Private::doStart()
576 {
577  // Just doing registerService() and checking its return value is not sufficient,
578  // since we may *already* own the name, and then registerService() returns true.
579 
580  QDBusConnection bus = DBusConnectionPool::threadConnection();
581  const bool alreadyLocked = bus.interface()->isServiceRegistered( dbusServiceName() );
582  const bool gotIt = bus.registerService( dbusServiceName() );
583 
584  if ( gotIt && !alreadyLocked ) {
585  //kDebug() << "Got lock immediately.";
586  q->emitResult();
587  } else {
588  QDBusServiceWatcher *watcher = new QDBusServiceWatcher( dbusServiceName(), DBusConnectionPool::threadConnection(),
589  QDBusServiceWatcher::WatchForOwnerChange, q );
590  //kDebug() << "Waiting for lock.";
591  connect( watcher, SIGNAL(serviceOwnerChanged(QString,QString,QString)),
592  q, SLOT(serviceOwnerChanged(QString,QString,QString)) );
593 
594  mSafetyTimer = new QTimer( q );
595  mSafetyTimer->setSingleShot( true );
596  mSafetyTimer->setInterval( LOCK_WAIT_TIMEOUT_SECONDS * 1000 );
597  mSafetyTimer->start();
598  connect( mSafetyTimer, SIGNAL(timeout()), q, SLOT(timeout()) );
599  }
600 }
601 
602 void GetLockJob::Private::serviceOwnerChanged( const QString&, const QString&, const QString &newOwner )
603 {
604  if ( newOwner.isEmpty() ) {
605  const bool gotIt = DBusConnectionPool::threadConnection().registerService( dbusServiceName() );
606  if ( gotIt ) {
607  mSafetyTimer->stop();
608  q->emitResult();
609  }
610  }
611 }
612 
613 void GetLockJob::Private::timeout()
614 {
615  kWarning() << "Timeout trying to get lock. Check who has acquired the name" << dbusServiceName() << "on DBus, using qdbus or qdbusviewer.";
616  q->setError( Job::Unknown );
617  q->setErrorText( i18n( "Timeout trying to get lock." ) );
618  q->emitResult();
619 }
620 
621 
622 GetLockJob::GetLockJob( QObject *parent )
623  : KJob( parent ),
624  d( new Private( this ) )
625 {
626 }
627 
628 GetLockJob::~GetLockJob()
629 {
630  delete d;
631 }
632 
633 void GetLockJob::start()
634 {
635  QTimer::singleShot( 0, this, SLOT(doStart()) );
636 }
637 
638 void Akonadi::setCollectionAttributes( Akonadi::Collection &collection, const QByteArray &type,
639  const QMap<QByteArray, QString> &nameForType,
640  const QMap<QByteArray, QString> &iconForType )
641 {
642  {
643  EntityDisplayAttribute *attr = new EntityDisplayAttribute;
644  attr->setIconName( iconForType.value( type ) );
645  attr->setDisplayName( nameForType.value( type ) );
646  collection.addAttribute( attr );
647  }
648 
649  {
650  SpecialCollectionAttribute *attr = new SpecialCollectionAttribute;
651  attr->setCollectionType( type );
652  collection.addAttribute( attr );
653  }
654 }
655 
656 bool Akonadi::releaseLock()
657 {
658  return DBusConnectionPool::threadConnection().unregisterService( dbusServiceName() );
659 }
660 
661 #include "moc_specialcollectionshelperjobs_p.cpp"
This file is part of the KDE documentation.
Documentation copyright © 1996-2013 The KDE developers.
Generated on Sat Jul 13 2013 01:27:41 by doxygen 1.8.3.1 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.

akonadi

Skip menu "akonadi"
  • Main Page
  • Namespace List
  • Namespace Members
  • Alphabetical List
  • Class List
  • Class Hierarchy
  • Class Members
  • File List
  • Modules
  • Related Pages

kdepimlibs-4.10.5 API Reference

Skip menu "kdepimlibs-4.10.5 API Reference"
  • akonadi
  •   contact
  •   kmime
  •   socialutils
  • kabc
  • kalarmcal
  • kblog
  • kcal
  • kcalcore
  • kcalutils
  • kholidays
  • kimap
  • kioslave
  •   imap4
  •   mbox
  •   nntp
  • kldap
  • kmbox
  • kmime
  • kontactinterface
  • kpimidentities
  • kpimtextedit
  • kpimutils
  • kresources
  • ktnef
  • kxmlrpcclient
  • mailtransport
  • microblog
  • qgpgme
  • syndication
  •   atom
  •   rdf
  •   rss2
Report problems with this website to our bug tracking system.
Contact the specific authors with questions and comments about the page contents.

KDE® and the K Desktop Environment® logo are registered trademarks of KDE e.V. | Legal