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

Skip menu "kdepimlibs-4.9.4 API Reference"
  • akonadi
  •   contact
  •   kmime
  • kabc
  • kalarmcal
  • kblog
  • kcal
  • kcalcore
  • kcalutils
  • kholidays
  • kimap
  • kioslave
  •   imap4
  •   mbox
  •   nntp
  • kldap
  • kmbox
  • kmime
  • kontactinterface
  • kpimidentities
  • kpimtextedit
  •   richtextbuilders
  • 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