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

KMBox Library

  • kmbox
mbox.cpp
1 /*
2  Copyright (c) 1996-1998 Stefan Taferner <taferner@kde.org>
3  Copyright (c) 2009 Bertjan Broeksema <broeksema@kde.org>
4 
5  This library is free software; you can redistribute it and/or modify it
6  under the terms of the GNU Library General Public License as published by
7  the Free Software Foundation; either version 2 of the License, or (at your
8  option) any later version.
9 
10  This library is distributed in the hope that it will be useful, but WITHOUT
11  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12  FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
13  License for more details.
14 
15  You should have received a copy of the GNU Library General Public License
16  along with this library; see the file COPYING.LIB. If not, write to the
17  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18  02110-1301, USA.
19 
20  NOTE: Most of the code inside here is an slightly adjusted version of
21  kdepim/kmail/kmfoldermbox.cpp. This is why I added a line for Stefan Taferner.
22 
23  Bertjan Broeksema, april 2009
24 */
25 
26 #include "mbox.h"
27 #include "mbox_p.h"
28 #include "mboxentry_p.h"
29 
30 #include <KDebug>
31 #include <KStandardDirs>
32 #include <KUrl>
33 
34 #include <QtCore/QBuffer>
35 #include <QtCore/QProcess>
36 
37 using namespace KMBox;
38 
40 
41 MBox::MBox()
42  : d( new MBoxPrivate( this ) )
43 {
44  // Set some sane defaults
45  d->mFileLocked = false;
46  d->mLockType = None;
47 
48  d->mUnlockTimer.setInterval( 0 );
49  d->mUnlockTimer.setSingleShot( true );
50 }
51 
52 MBox::~MBox()
53 {
54  if ( d->mFileLocked ) {
55  unlock();
56  }
57 
58  d->close();
59 
60  delete d;
61 }
62 
63 // Appended entries works as follows: When an mbox file is loaded from disk,
64 // d->mInitialMboxFileSize is set to the file size at that moment. New entries
65 // are stored in memory (d->mAppendedEntries). The initial file size and the size
66 // of the buffer determine the offset for the next message to append.
67 MBoxEntry MBox::appendMessage( const KMime::Message::Ptr &entry )
68 {
69  // It doesn't make sense to add entries when we don't have an reference file.
70  Q_ASSERT( !d->mMboxFile.fileName().isEmpty() );
71 
72  const QByteArray rawEntry = MBoxPrivate::escapeFrom( entry->encodedContent() );
73 
74  if ( rawEntry.size() <= 0 ) {
75  kDebug() << "Message added to folder `" << d->mMboxFile.fileName()
76  << "' contains no data. Ignoring it.";
77  return MBoxEntry();
78  }
79 
80  int nextOffset = d->mAppendedEntries.size(); // Offset of the appended message
81 
82  // Make sure the byte array is large enough to check for an end character.
83  // Then check if the required newlines are there.
84  if ( nextOffset < 1 && d->mMboxFile.size() > 0 ) { // Empty, add one empty line
85  d->mAppendedEntries.append( "\n" );
86  ++nextOffset;
87  } else if ( nextOffset == 1 && d->mAppendedEntries.at( 0 ) != '\n' ) {
88  // This should actually not happen, but catch it anyway.
89  if ( d->mMboxFile.size() < 0 ) {
90  d->mAppendedEntries.append( "\n" );
91  ++nextOffset;
92  }
93  } else if ( nextOffset >= 2 ) {
94  if ( d->mAppendedEntries.at( nextOffset - 1 ) != '\n' ) {
95  if ( d->mAppendedEntries.at( nextOffset ) != '\n' ) {
96  d->mAppendedEntries.append( "\n\n" );
97  nextOffset += 2;
98  } else {
99  d->mAppendedEntries.append( "\n" );
100  ++nextOffset;
101  }
102  }
103  }
104 
105  const QByteArray separator = MBoxPrivate::mboxMessageSeparator( rawEntry );
106  d->mAppendedEntries.append( separator );
107  d->mAppendedEntries.append( rawEntry );
108  if ( rawEntry[rawEntry.size() - 1] != '\n' ) {
109  d->mAppendedEntries.append( "\n\n" );
110  } else {
111  d->mAppendedEntries.append( "\n" );
112  }
113 
114  MBoxEntry resultEntry;
115  resultEntry.d->mOffset = d->mInitialMboxFileSize + nextOffset;
116  resultEntry.d->mMessageSize = rawEntry.size();
117  resultEntry.d->mSeparatorSize = separator.size();
118  d->mEntries << resultEntry;
119 
120  return resultEntry;
121 }
122 
123 MBoxEntry::List MBox::entries( const MBoxEntry::List &deletedEntries ) const
124 {
125  if ( deletedEntries.isEmpty() ) {
126  // fast path
127  return d->mEntries;
128  }
129 
130  MBoxEntry::List result;
131 
132  foreach ( const MBoxEntry &entry, d->mEntries ) {
133  if ( !deletedEntries.contains( entry ) ) {
134  result << entry;
135  }
136  }
137 
138  return result;
139 }
140 
141 QString MBox::fileName() const
142 {
143  return d->mMboxFile.fileName();
144 }
145 
146 bool MBox::load( const QString &fileName )
147 {
148  if ( d->mFileLocked ) {
149  return false;
150  }
151 
152  d->initLoad( fileName );
153 
154  if ( !lock() ) {
155  kDebug() << "Failed to lock";
156  return false;
157  }
158 
159  QByteArray line;
160  QByteArray prevSeparator;
161  quint64 offs = 0; // The offset of the next message to read.
162 
163  while ( !d->mMboxFile.atEnd() ) {
164  quint64 pos = d->mMboxFile.pos();
165 
166  line = d->mMboxFile.readLine();
167 
168  // if atEnd, use mail only if there was a separator line at all,
169  // otherwise it's not a valid mbox
170  if ( d->isMBoxSeparator( line ) ||
171  ( d->mMboxFile.atEnd() && ( prevSeparator.size() != 0 ) ) ) {
172 
173  // if we are the at the file end, update pos to not forget the last line
174  if ( d->mMboxFile.atEnd() ) {
175  pos = d->mMboxFile.pos();
176  }
177 
178  // Found the separator or at end of file, the message starts at offs
179  quint64 msgSize = pos - offs;
180 
181  if ( pos > 0 ) {
182  // This is not the separator of the first mail in the file. If pos == 0
183  // than we matched the separator of the first mail in the file.
184  MBoxEntry entry;
185  entry.d->mOffset = offs;
186  entry.d->mSeparatorSize = prevSeparator.size();
187  entry.d->mMessageSize = msgSize - 1;
188 
189  // Don't add the separator size and the newline up to the message size.
190  entry.d->mMessageSize -= prevSeparator.size() + 1;
191 
192  d->mEntries << entry;
193  }
194 
195  if ( d->isMBoxSeparator( line ) ) {
196  prevSeparator = line;
197  }
198 
199  offs += msgSize; // Mark the beginning of the next message.
200  }
201  }
202 
203  // FIXME: What if unlock fails?
204  // if no separator was found, the file is still valid if it is empty
205  return unlock() && ( ( prevSeparator.size() != 0 ) || ( d->mMboxFile.size() == 0 ) );
206 }
207 
208 bool MBox::lock()
209 {
210  if ( d->mMboxFile.fileName().isEmpty() ) {
211  return false; // We cannot lock if there is no file loaded.
212  }
213 
214  // We can't load another file when the mbox currently is locked so if d->mFileLocked
215  // is true atm just return true.
216  if ( locked() ) {
217  return true;
218  }
219 
220  if ( d->mLockType == None ) {
221  d->mFileLocked = true;
222  if ( d->open() ) {
223  d->startTimerIfNeeded();
224  return true;
225  }
226 
227  d->mFileLocked = false;
228  return false;
229  }
230 
231  QStringList args;
232  int rc = 0;
233 
234  switch ( d->mLockType ) {
235  case ProcmailLockfile:
236  args << QLatin1String( "-l20" ) << QLatin1String( "-r5" );
237  if ( !d->mLockFileName.isEmpty() ) {
238  args << QString::fromLocal8Bit( QFile::encodeName( d->mLockFileName ) );
239  } else {
240  args << QString::fromLocal8Bit( QFile::encodeName( d->mMboxFile.fileName() +
241  QLatin1String( ".lock" ) ) );
242  }
243 
244  rc = QProcess::execute( QLatin1String( "lockfile" ), args );
245  if ( rc != 0 ) {
246  kDebug() << "lockfile -l20 -r5 " << d->mMboxFile.fileName()
247  << ": Failed (" << rc << ") switching to read only mode";
248  d->mReadOnly = true; // In case the MBox object was created read/write we
249  // set it to read only when locking failed.
250  } else {
251  d->mFileLocked = true;
252  }
253  break;
254 
255  case MuttDotlock:
256  args << QString::fromLocal8Bit( QFile::encodeName( d->mMboxFile.fileName() ) );
257  rc = QProcess::execute( QLatin1String( "mutt_dotlock" ), args );
258 
259  if ( rc != 0 ) {
260  kDebug() << "mutt_dotlock " << d->mMboxFile.fileName()
261  << ": Failed (" << rc << ") switching to read only mode";
262  d->mReadOnly = true; // In case the MBox object was created read/write we
263  // set it to read only when locking failed.
264  } else {
265  d->mFileLocked = true;
266  }
267  break;
268 
269  case MuttDotlockPrivileged:
270  args << QLatin1String( "-p" )
271  << QString::fromLocal8Bit( QFile::encodeName( d->mMboxFile.fileName() ) );
272  rc = QProcess::execute( QLatin1String( "mutt_dotlock" ), args );
273 
274  if ( rc != 0 ) {
275  kDebug() << "mutt_dotlock -p " << d->mMboxFile.fileName() << ":"
276  << ": Failed (" << rc << ") switching to read only mode";
277  d->mReadOnly = true;
278  } else {
279  d->mFileLocked = true;
280  }
281  break;
282 
283  case None:
284  d->mFileLocked = true;
285  break;
286  default:
287  break;
288  }
289 
290  if ( d->mFileLocked ) {
291  if ( !d->open() ) {
292  const bool unlocked = unlock();
293  Q_ASSERT( unlocked ); // If this fails we're in trouble.
294  Q_UNUSED( unlocked );
295  }
296  }
297 
298  d->startTimerIfNeeded();
299  return d->mFileLocked;
300 }
301 
302 bool MBox::locked() const
303 {
304  return d->mFileLocked;
305 }
306 
307 static bool lessThanByOffset( const MBoxEntry &left, const MBoxEntry &right )
308 {
309  return left.messageOffset() < right.messageOffset();
310 }
311 
312 bool MBox::purge( const MBoxEntry::List &deletedEntries, QList<MBoxEntry::Pair> *movedEntries )
313 {
314  if ( d->mMboxFile.fileName().isEmpty() ) {
315  return false; // No file loaded yet.
316  }
317 
318  if ( deletedEntries.isEmpty() ) {
319  return true; // Nothing to do.
320  }
321 
322  if ( !lock() ) {
323  return false;
324  }
325 
326  foreach ( const MBoxEntry &entry, deletedEntries ) {
327  d->mMboxFile.seek( entry.messageOffset() );
328  const QByteArray line = d->mMboxFile.readLine();
329 
330  if ( !d->isMBoxSeparator( line ) ) {
331  qDebug() << "Found invalid separator at:" << entry.messageOffset();
332  unlock();
333  return false; // The file is messed up or the index is incorrect.
334  }
335  }
336 
337  // All entries are deleted, so just resize the file to a size of 0.
338  if ( deletedEntries.size() == d->mEntries.size() ) {
339  d->mEntries.clear();
340  d->mMboxFile.resize( 0 );
341  kDebug() << "Purge comleted successfully, unlocking the file.";
342  return unlock();
343  }
344 
345  qSort( d->mEntries.begin(), d->mEntries.end(), lessThanByOffset );
346  quint64 writeOffset = 0;
347  bool writeOffSetInitialized = false;
348  MBoxEntry::List resultingEntryList;
349  QList<MBoxEntry::Pair> tmpMovedEntries;
350 
351  quint64 origFileSize = d->mMboxFile.size();
352 
353  QListIterator<MBoxEntry> i( d->mEntries );
354  while ( i.hasNext() ) {
355  MBoxEntry entry = i.next();
356 
357  if ( deletedEntries.contains( entry ) && !writeOffSetInitialized ) {
358  writeOffset = entry.messageOffset();
359  writeOffSetInitialized = true;
360  } else if ( writeOffSetInitialized &&
361  writeOffset < entry.messageOffset() &&
362  !deletedEntries.contains( entry ) ) {
363  // The current message doesn't have to be deleted, but must be moved.
364  // First determine the size of the entry that must be moved.
365  quint64 entrySize = 0;
366  if ( i.hasNext() ) {
367  entrySize = i.next().messageOffset() - entry.messageOffset();
368  i.previous(); // Go back to make sure that we also handle the next entry.
369  } else {
370  entrySize = origFileSize - entry.messageOffset();
371  }
372 
373  Q_ASSERT( entrySize > 0 ); // MBox entries really cannot have a size <= 0;
374 
375  // we map the whole area of the file starting at the writeOffset up to the
376  // message that have to be moved into memory. This includes eventually the
377  // messages that are the deleted between the first deleted message
378  // encountered and the message that has to be moved.
379  quint64 mapSize = entry.messageOffset() + entrySize - writeOffset;
380 
381  // Now map writeOffSet + mapSize into mem.
382  uchar *memArea = d->mMboxFile.map( writeOffset, mapSize );
383 
384  // Now read the entry that must be moved to writeOffset.
385  quint64 startOffset = entry.messageOffset() - writeOffset;
386  memmove( memArea, memArea + startOffset, entrySize );
387 
388  d->mMboxFile.unmap( memArea );
389 
390  MBoxEntry resultEntry;
391  resultEntry.d->mOffset = writeOffset;
392  resultEntry.d->mSeparatorSize = entry.separatorSize();
393  resultEntry.d->mMessageSize = entry.messageSize();
394 
395  resultingEntryList << resultEntry;
396  tmpMovedEntries << MBoxEntry::Pair( MBoxEntry( entry.messageOffset() ),
397  MBoxEntry( resultEntry.messageOffset() ) );
398  writeOffset += entrySize;
399  } else if ( !deletedEntries.contains( entry ) ) {
400  // Unmoved and not deleted entry, can only ocure before the first deleted
401  // entry.
402  Q_ASSERT( !writeOffSetInitialized );
403  resultingEntryList << entry;
404  }
405  }
406 
407  // Chop off remaining entry bits.
408  d->mMboxFile.resize( writeOffset );
409  d->mEntries = resultingEntryList;
410 
411  kDebug() << "Purge comleted successfully, unlocking the file.";
412  if ( movedEntries ) {
413  *movedEntries = tmpMovedEntries;
414  }
415  return unlock(); // FIXME: What if this fails? It will return false but the
416  // file has changed.
417 }
418 
419 QByteArray MBox::readRawMessage( const MBoxEntry &entry )
420 {
421  const bool wasLocked = locked();
422  if ( !wasLocked ) {
423  if ( !lock() ) {
424  return QByteArray();
425  }
426  }
427 
428  // TODO: Add error handling in case locking failed.
429 
430  quint64 offset = entry.messageOffset();
431 
432  Q_ASSERT( d->mFileLocked );
433  Q_ASSERT( d->mMboxFile.isOpen() );
434  Q_ASSERT( ( d->mInitialMboxFileSize + d->mAppendedEntries.size() ) > offset );
435 
436  QByteArray message;
437 
438  if ( offset < d->mInitialMboxFileSize ) {
439  d->mMboxFile.seek( offset );
440 
441  QByteArray line = d->mMboxFile.readLine();
442 
443  if ( !d->isMBoxSeparator( line ) ) {
444  kDebug() << "[MBox::readEntry] Invalid entry at:" << offset;
445  if ( !wasLocked ) {
446  unlock();
447  }
448  return QByteArray(); // The file is messed up or the index is incorrect.
449  }
450 
451  line = d->mMboxFile.readLine();
452  while ( !d->isMBoxSeparator( line ) ) {
453  message += line;
454  if ( d->mMboxFile.atEnd() ) {
455  break;
456  }
457  line = d->mMboxFile.readLine();
458  }
459  } else {
460  offset -= d->mInitialMboxFileSize;
461  if ( offset > static_cast<quint64>( d->mAppendedEntries.size() ) ) {
462  if ( !wasLocked ) {
463  unlock();
464  }
465  return QByteArray();
466  }
467 
468  QBuffer buffer( &( d->mAppendedEntries ) );
469  buffer.open( QIODevice::ReadOnly );
470  buffer.seek( offset );
471 
472  QByteArray line = buffer.readLine();
473 
474  if ( !d->isMBoxSeparator( line ) ) {
475  kDebug() << "[MBox::readEntry] Invalid appended entry at:" << offset;
476  if ( !wasLocked ) {
477  unlock();
478  }
479  return QByteArray(); // The file is messed up or the index is incorrect.
480  }
481 
482  line = buffer.readLine();
483  while ( !d->isMBoxSeparator( line ) && !buffer.atEnd() ) {
484  message += line;
485  line = buffer.readLine();
486  }
487  }
488 
489  // Remove te last '\n' added by writeEntry.
490  if ( message.endsWith( '\n' ) ) {
491  message.chop( 1 );
492  }
493 
494  MBoxPrivate::unescapeFrom( message.data(), message.size() );
495 
496  if ( !wasLocked ) {
497  if ( !d->startTimerIfNeeded() ) {
498  const bool unlocked = unlock();
499  Q_ASSERT( unlocked );
500  Q_UNUSED( unlocked );
501  }
502  }
503 
504  return message;
505 }
506 
507 KMime::Message *MBox::readMessage( const MBoxEntry &entry )
508 {
509  const QByteArray message = readRawMessage( entry );
510  if ( message.isEmpty() ) {
511  return 0;
512  }
513 
514  KMime::Message *mail = new KMime::Message();
515  mail->setContent( KMime::CRLFtoLF( message ) );
516  mail->parse();
517 
518  return mail;
519 }
520 
521 QByteArray MBox::readMessageHeaders( const MBoxEntry &entry )
522 {
523  const bool wasLocked = d->mFileLocked;
524  if ( !wasLocked ) {
525  lock();
526  }
527 
528  const quint64 offset = entry.messageOffset();
529 
530  Q_ASSERT( d->mFileLocked );
531  Q_ASSERT( d->mMboxFile.isOpen() );
532  Q_ASSERT( ( d->mInitialMboxFileSize + d->mAppendedEntries.size() ) > offset );
533 
534  QByteArray headers;
535  if ( offset < d->mInitialMboxFileSize ) {
536  d->mMboxFile.seek( offset );
537  QByteArray line = d->mMboxFile.readLine();
538 
539  while ( line[0] != '\n' && !d->mMboxFile.atEnd() ) {
540  headers += line;
541  line = d->mMboxFile.readLine();
542  }
543  } else {
544  QBuffer buffer( &( d->mAppendedEntries ) );
545  buffer.open( QIODevice::ReadOnly );
546  buffer.seek( offset - d->mInitialMboxFileSize );
547  QByteArray line = buffer.readLine();
548 
549  while ( line[0] != '\n' && !buffer.atEnd() ) {
550  headers += line;
551  line = buffer.readLine();
552  }
553  }
554 
555  if ( !wasLocked ) {
556  unlock();
557  }
558 
559  return headers;
560 }
561 
562 bool MBox::save( const QString &fileName )
563 {
564  if ( !fileName.isEmpty() && KUrl( fileName ).toLocalFile() != d->mMboxFile.fileName() ) {
565  if ( !d->mMboxFile.copy( fileName ) ) {
566  return false;
567  }
568 
569  if ( d->mAppendedEntries.size() == 0 ) {
570  return true; // Nothing to do
571  }
572 
573  QFile otherFile( fileName );
574  Q_ASSERT( otherFile.exists() );
575  if ( !otherFile.open( QIODevice::ReadWrite ) ) {
576  return false;
577  }
578 
579  otherFile.seek( d->mMboxFile.size() );
580  otherFile.write( d->mAppendedEntries );
581 
582  // Don't clear mAppendedEntries and don't update mInitialFileSize. These
583  // are still valid for the original file.
584  return true;
585  }
586 
587  if ( d->mAppendedEntries.size() == 0 ) {
588  return true; // Nothing to do.
589  }
590 
591  if ( !lock() ) {
592  return false;
593  }
594 
595  Q_ASSERT( d->mMboxFile.isOpen() );
596 
597  d->mMboxFile.seek( d->mMboxFile.size() );
598  d->mMboxFile.write( d->mAppendedEntries );
599  d->mAppendedEntries.clear();
600  d->mInitialMboxFileSize = d->mMboxFile.size();
601 
602  return unlock();
603 }
604 
605 bool MBox::setLockType( LockType ltype )
606 {
607  if ( d->mFileLocked ) {
608  kDebug() << "File is currently locked.";
609  return false; // Don't change the method if the file is currently locked.
610  }
611 
612  switch ( ltype ) {
613  case ProcmailLockfile:
614  if ( KStandardDirs::findExe( QLatin1String( "lockfile" ) ).isEmpty() ) {
615  kDebug() << "Could not find the lockfile executable";
616  return false;
617  }
618  break;
619  case MuttDotlock: // fall through
620  case MuttDotlockPrivileged:
621  if ( KStandardDirs::findExe( QLatin1String( "mutt_dotlock" ) ).isEmpty() ) {
622  kDebug() << "Could not find the mutt_dotlock executable";
623  return false;
624  }
625  break;
626  default:
627  break; // We assume fcntl available and lock_none doesn't need a check.
628  }
629 
630  d->mLockType = ltype;
631  return true;
632 }
633 
634 void MBox::setLockFile( const QString &lockFile )
635 {
636  d->mLockFileName = lockFile;
637 }
638 
639 void MBox::setUnlockTimeout( int msec )
640 {
641  d->mUnlockTimer.setInterval( msec );
642 }
643 
644 bool MBox::unlock()
645 {
646  if ( d->mLockType == None && !d->mFileLocked ) {
647  d->mFileLocked = false;
648  d->mMboxFile.close();
649  return true;
650  }
651 
652  int rc = 0;
653  QStringList args;
654 
655  switch ( d->mLockType ) {
656  case ProcmailLockfile:
657  // QFile::remove returns true on succes so negate the result.
658  if ( !d->mLockFileName.isEmpty() ) {
659  rc = !QFile( d->mLockFileName ).remove();
660  } else {
661  rc = !QFile( d->mMboxFile.fileName() + QLatin1String( ".lock" ) ).remove();
662  }
663  break;
664 
665  case MuttDotlock:
666  args << QLatin1String( "-u" )
667  << QString::fromLocal8Bit( QFile::encodeName( d->mMboxFile.fileName() ) );
668  rc = QProcess::execute( QLatin1String( "mutt_dotlock" ), args );
669  break;
670 
671  case MuttDotlockPrivileged:
672  args << QLatin1String( "-u" ) << QLatin1String( "-p" )
673  << QString::fromLocal8Bit( QFile::encodeName( d->mMboxFile.fileName() ) );
674  rc = QProcess::execute( QLatin1String( "mutt_dotlock" ), args );
675  break;
676 
677  case None: // Fall through.
678  default:
679  break;
680  }
681 
682  if ( rc == 0 ) { // Unlocking succeeded
683  d->mFileLocked = false;
684  }
685 
686  d->mMboxFile.close();
687 
688  return !d->mFileLocked;
689 }
This file is part of the KDE documentation.
Documentation copyright © 1996-2013 The KDE developers.
Generated on Sat Jul 13 2013 01:25:39 by doxygen 1.8.3.1 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.

KMBox Library

Skip menu "KMBox Library"
  • Main Page
  • Alphabetical List
  • Class List
  • Class Members
  • File List
  • 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