[Kde-pim] [kdepim] console: New command line tool: kdepim/console/calendarjanitor.

Sergio Martins iamsergio at gmail.com
Sat Aug 17 19:46:01 BST 2013


Git commit b36122a8743cc48d37e5f0384ad6bccc9308c478 by Sergio Martins.
Committed on 14/08/2013 at 21:17.
Pushed by smartins into branch 'master'.

New command line tool: kdepim/console/calendarjanitor.

Scans all your calendar data for buggy incidences.
Also prints some useful statistics like total inline attachment size.

In --fix mode, duplicate incidences ( with same payload ) will be deleted,
incidences with empty summary and description (and no attachments) are deleted
If invalid, DTSTART gets the value of DTEND, or current date if DTEND is invalid too.

A very common use case is to fix recurring to-dos that have a DTDUE but no DTSTART,
RFC doesn't allow this.

Scan with:
$ calendarjanitor

Scan and fix incidences:
$ calendarjanitor --fix

Backup your data first:
$ calendarjanitor --backup

CCMAIL: kde-pim at kde.org
**********************************************************************
Processing collection Sergio Martins calendar (id=567) ...
Checking for incidences with empty summary and description...          [OK]
Checking for incidences with empty UID...                              [OK]
Checking for events with invalid DTSTART...                            [OK]
Checking for recurring to-dos with invalid DTSTART...                  [OK]
Checking for journal with invalid DTSTART...                           [OK]
Checking for duplicate UIDs...                                         [OK]
Gathering statistics...

Events                                            : 516
Todos                                             : 52
Journal                                           : 175
Passed events and to-dos (>365 days)              : 431
Old incidences with alarms                        : 259
Inline attachments                                : 2
Total size of inline attachments (KB)             : 61
**********************************************************************

M  +2    -0    console/CMakeLists.txt
A  +40   -0    console/calendarjanitor/CMakeLists.txt
A  +341  -0    console/calendarjanitor/COPYING
A  +2    -0    console/calendarjanitor/Messages.sh
A  +151  -0    console/calendarjanitor/backuper.cpp     [License: GPL (v2+) (+Qt exception)]
A  +65   -0    console/calendarjanitor/backuper.h     [License: GPL (v2+) (+Qt exception)]
A  +542  -0    console/calendarjanitor/calendarjanitor.cpp     [License: GPL (v2+) (+Qt exception)]
A  +103  -0    console/calendarjanitor/calendarjanitor.h     [License: GPL (v2+) (+Qt exception)]
A  +70   -0    console/calendarjanitor/collectionloader.cpp     [License: GPL (v2+) (+Qt exception)]
A  +50   -0    console/calendarjanitor/collectionloader.h     [License: GPL (v2+) (+Qt exception)]
A  +177  -0    console/calendarjanitor/main.cpp     [License: GPL (v2+) (+Qt exception)]
A  +52   -0    console/calendarjanitor/options.cpp     [License: GPL (v2+) (+Qt exception)]
A  +71   -0    console/calendarjanitor/options.h     [License: GPL (v2+) (+Qt exception)]

http://commits.kde.org/kdepim/b36122a8743cc48d37e5f0384ad6bccc9308c478

diff --git a/console/CMakeLists.txt b/console/CMakeLists.txt
index 7e767d7..05da374 100644
--- a/console/CMakeLists.txt
+++ b/console/CMakeLists.txt
@@ -1,5 +1,7 @@
 project(console)
 
+add_subdirectory(calendarjanitor)
+
 if (KDEPIMLIBS_KRESOURCES_LIBS)
   add_subdirectory(konsolekalendar)
   add_subdirectory(kabcclient)
diff --git a/console/calendarjanitor/CMakeLists.txt b/console/calendarjanitor/CMakeLists.txt
new file mode 100644
index 0000000..f23fcc4
--- /dev/null
+++ b/console/calendarjanitor/CMakeLists.txt
@@ -0,0 +1,40 @@
+project(calendarjanitor)
+
+add_definitions(-DKDE_DEFAULT_DEBUG_AREA=5860)
+
+########### next target ###############
+
+set(calendarjanitor_SRCS
+    backuper.cpp
+    calendarjanitor.cpp
+    collectionloader.cpp
+    main.cpp
+    options.cpp)
+
+include_directories(
+  ${CMAKE_CURRENT_SOURCE_DIR}/interfaces
+  ${CMAKE_CURRENT_BINARY_DIR}
+  ${CMAKE_SOURCE_DIR}/calendarsupport
+  ${CMAKE_BINARY_DIR}/calendarsupport
+
+
+  ${AKONADI_INCLUDE_DIR}
+  ${Boost_INCLUDE_DIRS}
+  ${QT_INCLUDES}
+  ${ZLIB_INCLUDE_DIRS}
+)
+   
+
+kde4_add_app_icon(calendarjanitor_SRCS "${KDE4_ICON_DIR}/oxygen/*/apps/office-calendar.png")
+
+kde4_add_executable(calendarjanitor NOGUI ${calendarjanitor_SRCS})
+
+target_link_libraries(calendarjanitor
+                      ${KDE4_KDECORE_LIBS}
+                      ${KDEPIMLIBS_KCALUTILS_LIBS}
+                      ${KDEPIMLIBS_KCALCORE_LIBS}
+                      kdepim
+                      calendarsupport
+                      akonadi-calendar)
+
+install(TARGETS calendarjanitor  ${INSTALL_TARGETS_DEFAULT_ARGS})
diff --git a/console/calendarjanitor/COPYING b/console/calendarjanitor/COPYING
new file mode 100644
index 0000000..54754ab
--- /dev/null
+++ b/console/calendarjanitor/COPYING
@@ -0,0 +1,341 @@
+		    GNU GENERAL PUBLIC LICENSE
+		       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+               51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+			    Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+		    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+			    NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+		     END OF TERMS AND CONDITIONS
+
+
+	    How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) 19yy  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) 19yy name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Library General
+Public License instead of this License.
diff --git a/console/calendarjanitor/Messages.sh b/console/calendarjanitor/Messages.sh
new file mode 100644
index 0000000..504219a
--- /dev/null
+++ b/console/calendarjanitor/Messages.sh
@@ -0,0 +1,2 @@
+#! /bin/sh
+$XGETTEXT *.cpp -o $podir/calendarjanitor.pot
diff --git a/console/calendarjanitor/backuper.cpp b/console/calendarjanitor/backuper.cpp
new file mode 100644
index 0000000..5430f76
--- /dev/null
+++ b/console/calendarjanitor/backuper.cpp
@@ -0,0 +1,151 @@
+/*
+  Copyright (c) 2013 Sérgio Martins <iamsergio at gmail.com>
+
+  This program is free software; you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation; either version 2 of the License, or
+  (at your option) any later version.
+
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along
+  with this program; if not, write to the Free Software Foundation, Inc.,
+  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+  As a special exception, permission is given to link this program
+  with any edition of Qt, and distribute the resulting executable,
+  without including the source code for Qt in the source distribution.
+*/
+
+#include "backuper.h"
+
+#include <calendarsupport/utils.h>
+
+#include <KCalCore/Incidence>
+#include <KCalCore/FileStorage>
+
+#include <Akonadi/CollectionFetchJob>
+#include <Akonadi/CollectionFetchScope>
+#include <Akonadi/ItemFetchJob>
+#include <Akonadi/ItemFetchScope>
+
+#include <KLocale>
+#include <KJob>
+
+#include <QMetaObject>
+#include <QDebug>
+#include <QCoreApplication>
+
+static void print(const QString &message)
+{
+    QTextStream out(stdout);
+    out << message << "\n";
+}
+
+void Backuper::emitFinished(bool success, const QString &message)
+{
+    if (success) {
+        print(QLatin1Char('\n') + i18n("Backup was successful. %1 incidences were saved.", m_calendar->incidences().count()));
+    } else {
+        print(message);
+    }
+
+    m_calendar.clear();
+
+    emit finished(success, message);
+    qApp->exit(success ? 0 : -1); // TODO: If we move this class to kdepimlibs, remove this
+}
+
+Backuper::Backuper(QObject *parent) : QObject(parent), m_backupInProgress(false)
+{
+}
+
+void Backuper::backup(const QString &filename, const QList<Akonadi::Entity::Id> &collectionIds)
+{
+    if (filename.isEmpty()) {
+        emitFinished(false, i18n("File is empty."));
+        return;
+    }
+
+    if (m_backupInProgress) {
+        emitFinished(false, i18n("A backup is already in progress."));
+        return;
+    }
+    print(i18n("Backing up your calendar data..."));
+    m_calendar = KCalCore::MemoryCalendar::Ptr(new KCalCore::MemoryCalendar(KDateTime::LocalZone));
+    m_requestedCollectionIds = collectionIds;
+    m_backupInProgress = true;
+    m_filename = filename;
+
+    Akonadi::CollectionFetchJob *job = new Akonadi::CollectionFetchJob(Akonadi::Collection::root(),
+                                                                       Akonadi::CollectionFetchJob::Recursive);
+
+    job->fetchScope().setContentMimeTypes(KCalCore::Incidence::mimeTypes());
+    connect(job, SIGNAL(result(KJob*)), SLOT(onCollectionsFetched(KJob *)));
+    job->start();
+}
+
+void Backuper::onCollectionsFetched(KJob *job)
+{
+    if (job->error() == 0) {
+        QSet<QString> mimeTypeSet = KCalCore::Incidence::mimeTypes().toSet();
+        Akonadi::CollectionFetchJob *cfj = qobject_cast<Akonadi::CollectionFetchJob*>(job);
+        foreach(const Akonadi::Collection &collection, cfj->collections()) {
+            if (!m_requestedCollectionIds.isEmpty() && !m_requestedCollectionIds.contains(collection.id()))
+                continue;
+            if (!mimeTypeSet.intersect(collection.contentMimeTypes().toSet()).isEmpty()) {
+                m_collections << collection;
+                loadCollection(collection);
+            }
+        }
+
+        if (m_collections.isEmpty()) {
+            emitFinished(false, i18n("No data to backup."));
+        }
+    } else {
+        kError() << job->errorString();
+        m_backupInProgress = false;
+        emitFinished(false, job->errorString());
+    }
+}
+
+void Backuper::loadCollection(const Akonadi::Collection &collection)
+{
+    print(i18n("Processing collection %1 (id=%2)...", collection.displayName(), collection.id()));
+    Akonadi::ItemFetchJob *ifj = new Akonadi::ItemFetchJob(collection, this);
+    ifj->setProperty("collectionId", collection.id());
+    ifj->fetchScope().fetchFullPayload(true);
+    connect(ifj, SIGNAL(result(KJob*)), SLOT(onCollectionLoaded(KJob*)));
+    m_pendingCollections << collection.id();
+}
+
+void Backuper::onCollectionLoaded(KJob *job)
+{
+    if (job->error()) {
+        m_backupInProgress = false;
+        m_calendar.clear();
+        emitFinished(false, job->errorString());
+    } else {
+        Akonadi::ItemFetchJob *ifj = qobject_cast<Akonadi::ItemFetchJob *>(job);
+        Akonadi::Collection::Id id = ifj->property("collectionId").toInt();
+        Q_ASSERT(id != -1);
+        Akonadi::Item::List items = ifj->items();
+        m_pendingCollections.removeAll(id);
+
+        foreach (const Akonadi::Item &item, items) {
+            KCalCore::Incidence::Ptr incidence = CalendarSupport::incidence(item);
+            Q_ASSERT(incidence);
+            m_calendar->addIncidence(incidence);
+        }
+
+        if (m_pendingCollections.isEmpty()) { // We're done
+            KCalCore::FileStorage storage(m_calendar, m_filename);
+            bool success = storage.save();
+            QString message = success ? QString() : i18n("An error ocurred");
+            emitFinished(success, message);
+        }
+    }
+}
diff --git a/console/calendarjanitor/backuper.h b/console/calendarjanitor/backuper.h
new file mode 100644
index 0000000..d20797d
--- /dev/null
+++ b/console/calendarjanitor/backuper.h
@@ -0,0 +1,65 @@
+/*
+  Copyright (c) 2013 Sérgio Martins <iamsergio at gmail.com>
+
+  This program is free software; you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation; either version 2 of the License, or
+  (at your option) any later version.
+
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along
+  with this program; if not, write to the Free Software Foundation, Inc.,
+  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+  As a special exception, permission is given to link this program
+  with any edition of Qt, and distribute the resulting executable,
+  without including the source code for Qt in the source distribution.
+*/
+
+#ifndef BACKUPER_H
+#define BACKUPER_H
+
+#include "options.h"
+
+#include <KCalCore/MemoryCalendar>
+#include <Akonadi/Collection>
+
+#include <QObject>
+#include <QList>
+
+class KJob;
+
+class Backuper : public QObject
+{
+    Q_OBJECT
+public:
+    explicit Backuper(QObject *parent = 0);
+    void backup(const QString &filename, const QList<Akonadi::Collection::Id> &collectionIds);
+
+Q_SIGNALS:
+    void finished(bool success, const QString &errorMessage);
+
+private Q_SLOTS:
+    void onCollectionsFetched(KJob *);
+    void onCollectionLoaded(KJob *);
+
+private:
+    void loadCollection(const Akonadi::Collection &collection);
+    void emitFinished(bool success, const QString &message);
+
+    QList<Akonadi::Collection::Id> m_requestedCollectionIds;
+    QList<Akonadi::Collection::Id> m_pendingCollections;
+
+    Akonadi::Collection::List m_collections;
+    QString m_filename;
+    KCalCore::MemoryCalendar::Ptr m_calendar;
+
+    bool m_backupInProgress;
+
+};
+
+#endif // BACKUPER_H
diff --git a/console/calendarjanitor/calendarjanitor.cpp b/console/calendarjanitor/calendarjanitor.cpp
new file mode 100644
index 0000000..b13701f
--- /dev/null
+++ b/console/calendarjanitor/calendarjanitor.cpp
@@ -0,0 +1,542 @@
+/*
+  Copyright (c) 2013 Sérgio Martins <iamsergio at gmail.com>
+
+  This program is free software; you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation; either version 2 of the License, or
+  (at your option) any later version.
+
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along
+  with this program; if not, write to the Free Software Foundation, Inc.,
+  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+  As a special exception, permission is given to link this program
+  with any edition of Qt, and distribute the resulting executable,
+  without including the source code for Qt in the source distribution.
+*/
+
+#include "calendarjanitor.h"
+#include "collectionloader.h"
+
+#include <calendarsupport/utils.h>
+
+#include <Akonadi/ItemFetchJob>
+#include <Akonadi/ItemFetchScope>
+
+#include <KCalCore/Attachment>
+#include <KCalCore/Alarm>
+#include <KCalCore/Event>
+#include <KCalCore/Todo>
+#include <KCalCore/Journal>
+
+#include <KLocale>
+#include <KDateTime>
+
+#include <QList>
+#include <QString>
+#include <QTextStream>
+#include <QCoreApplication>
+
+#define TEXT_WIDTH 70
+
+static void print(const QString &message, bool newline = true)
+{
+    QTextStream out(stdout);
+    out << message;
+    if (newline)
+        out << "\n";
+}
+
+static void bailOut()
+{
+    print(i18n("Bailing out. Fix your akonadi setup first. These kind of errors should not happen."));
+    qApp->exit(-1);
+}
+
+static bool collectionIsReadOnly(const Akonadi::Collection &collection)
+{
+    return !(collection.rights() & Akonadi::Collection::CanChangeItem) ||
+           !(collection.rights() & Akonadi::Collection::CanDeleteItem);
+}
+
+CalendarJanitor::CalendarJanitor(const Options &options, QObject *parent) : QObject(parent)
+                                                                          , m_collectionLoader(new CollectionLoader(this))
+                                                                          , m_options(options)
+                                                                          , m_currentSanityCheck(Options::CheckNone)
+                                                                          , m_pendingModifications(0)
+                                                                          , m_pendingDeletions(0)
+{
+    m_changer = new Akonadi::IncidenceChanger(this);
+    m_changer->setShowDialogsOnError(false);
+    connect(m_changer, SIGNAL(modifyFinished(int,Akonadi::Item,Akonadi::IncidenceChanger::ResultCode,QString)),
+            SLOT(onModifyFinished(int,Akonadi::Item,Akonadi::IncidenceChanger::ResultCode,QString)));
+    connect(m_changer, SIGNAL(deleteFinished(int,QVector<Akonadi::Item::Id>,Akonadi::IncidenceChanger::ResultCode,QString)),
+            SLOT(onDeleteFinished(int,QVector<Akonadi::Item::Id>,Akonadi::IncidenceChanger::ResultCode,QString)));
+    connect(m_collectionLoader, SIGNAL(loaded(bool)), SLOT(onCollectionsFetched(bool)));
+}
+
+void CalendarJanitor::start()
+{
+    m_collectionLoader->load();
+}
+
+void CalendarJanitor::onCollectionsFetched(bool success)
+{
+    if (!success) {
+        emit finished(false);
+        qApp->exit(-1);
+        return;
+    }
+
+    foreach (const Akonadi::Collection &collection, m_collectionLoader->collections()) {
+        if (m_options.testCollection(collection.id()))
+            m_collectionsToProcess << collection;
+    }
+
+    if (m_collectionsToProcess.isEmpty()) {
+        print(i18n("There are no collection to process!"));
+        qApp->exit((-1));
+        return;
+    } else {
+        processNextCollection();
+    }
+}
+
+void CalendarJanitor::onItemsFetched(KJob *job)
+{
+    Akonadi::ItemFetchJob *ifj = qobject_cast<Akonadi::ItemFetchJob *>(job);
+    Q_ASSERT(ifj);
+    m_itemsToProcess = ifj->items();
+    if (m_itemsToProcess.isEmpty()) {
+        print(i18n("Collection is empty, ignoring it."));
+    } else {
+        m_incidenceMap.clear();
+        foreach (const Akonadi::Item &item, m_itemsToProcess) {
+            KCalCore::Incidence::Ptr incidence = CalendarSupport::incidence(item);
+            Q_ASSERT(incidence);
+            m_incidenceMap.insert(incidence->instanceIdentifier(), incidence);
+            m_incidenceToItem.insert(incidence, item);
+        }
+        runNextTest();
+    }
+}
+
+void CalendarJanitor::onModifyFinished(int changeId, const Akonadi::Item &item,
+                                       Akonadi::IncidenceChanger::ResultCode resultCode, const QString &errorMessage)
+{
+    Q_UNUSED(changeId);
+    if (resultCode != Akonadi::IncidenceChanger::ResultCodeSuccess) {
+        print(i18n("Error while modifying incidence: %1!", errorMessage));
+        bailOut();
+        return;
+    }
+    print(i18n("Fixed item %1", item.id()));
+    m_pendingModifications--;
+    if (m_pendingModifications == 0) {
+        runNextTest();
+    }
+}
+
+void CalendarJanitor::onDeleteFinished(int changeId, const QVector<Akonadi::Entity::Id> &items,
+                                       Akonadi::IncidenceChanger::ResultCode resultCode, const QString &errorMessage)
+{
+    Q_UNUSED(changeId);
+    if (resultCode != Akonadi::IncidenceChanger::ResultCodeSuccess) {
+        print(i18n("Error while deleting incidence: %1!", errorMessage));
+        bailOut();
+        return;
+    }
+    print(i18n("Deleted item %1", items.first()));
+    m_pendingDeletions--;
+    if (m_pendingDeletions == 0) {
+        runNextTest();
+    }
+}
+
+void CalendarJanitor::processNextCollection()
+{
+    m_itemsToProcess.clear();
+    m_currentSanityCheck = Options::CheckNone;
+
+    if (m_collectionsToProcess.isEmpty()) {
+        print(QLatin1Char('\n') + QString().leftJustified(TEXT_WIDTH, QLatin1Char('*')));
+        emit finished(true);
+        qApp->exit(0);
+        return;
+    }
+
+    m_currentCollection = m_collectionsToProcess.takeFirst();
+    print(QLatin1Char('\n') + QString().leftJustified(TEXT_WIDTH, QLatin1Char('*')));
+    print(i18n("Processing collection %1 (id=%2) ...", m_currentCollection.displayName(), m_currentCollection.id()));
+
+    if (collectionIsReadOnly(m_currentCollection) && m_options.action() == Options::ActionScanAndFix) {
+        print(i18n("Collection is read only, disabling fix mode."));
+    }
+
+    Akonadi::ItemFetchJob *ifj = new Akonadi::ItemFetchJob(m_currentCollection, this);
+    ifj->fetchScope().fetchFullPayload(true);
+    connect(ifj, SIGNAL(result(KJob*)), SLOT(onItemsFetched(KJob*)));
+}
+
+void CalendarJanitor::runNextTest()
+{
+    int currentType = static_cast<int>(m_currentSanityCheck);
+    m_currentSanityCheck = static_cast<Options::SanityCheck>(currentType+1);
+    switch(m_currentSanityCheck) {
+    case Options::CheckEmptySummary:
+        sanityCheck1();
+        break;
+    case Options::CheckEmptyUid:
+        sanityCheck2();
+        break;
+    case Options::CheckEventDates:
+        sanityCheck3();
+        break;
+    case Options::CheckTodoDates:
+        sanityCheck4();
+        break;
+    case Options::CheckJournalDates:
+        sanityCheck5();
+        break;
+    case Options::CheckOrphans:
+        //sanityCheck6(); // Disabled for now
+        runNextTest();
+        break;
+    case Options::CheckDuplicateUIDs:
+        sanityCheck7();
+        break;
+    case Options::CheckStats:
+        sanityCheck8();
+        break;
+    case Options::CheckCount:
+        processNextCollection();
+        break;
+    default:
+        Q_ASSERT(false);
+    }
+}
+
+void CalendarJanitor::sanityCheck1()
+{
+    beginTest(i18n("Checking for incidences with empty summary and description..."));
+
+    foreach (const Akonadi::Item &item, m_itemsToProcess) {
+        KCalCore::Incidence::Ptr incidence = CalendarSupport::incidence(item);
+        if (incidence->summary().isEmpty() && incidence->description().isEmpty()
+            && incidence->attachments().isEmpty()) {
+            printFound(item);
+            deleteIncidence(item);
+        }
+    }
+
+    endTest();
+}
+
+void CalendarJanitor::sanityCheck2()
+{
+    beginTest(i18n("Checking for incidences with empty UID..."));
+
+    foreach (const Akonadi::Item &item, m_itemsToProcess) {
+        KCalCore::Incidence::Ptr incidence = CalendarSupport::incidence(item);
+        if (incidence->uid().isEmpty()) {
+            printFound(item);
+            if (m_fixingEnabled) {
+                incidence->recreate();
+                m_pendingModifications++;
+                m_changer->modifyIncidence(item);
+            }
+        }
+    }
+
+    endTest();
+}
+
+void CalendarJanitor::sanityCheck3()
+{
+    beginTest(i18n("Checking for events with invalid DTSTART..."));
+    foreach (const Akonadi::Item &item, m_itemsToProcess) {
+        KCalCore::Incidence::Ptr incidence = CalendarSupport::incidence(item);
+        KCalCore::Event::Ptr event = incidence.dynamicCast<KCalCore::Event>();
+        if (!event)
+            continue;
+
+        KDateTime start = event->dtStart();
+        KDateTime end   = event->dtEnd();
+
+        bool modify = false;
+        QString message;
+        if (!start.isValid() && end.isValid()) {
+            modify = true;
+            printFound(item);
+            event->setDtStart(end);
+        } else if (!start.isValid() && !end.isValid()) {
+            modify = true;
+            printFound(item);
+            event->setDtStart(KDateTime::currentLocalDateTime());
+            event->setDtEnd(event->dtStart().addSecs(3600));
+        }
+
+        if (modify) {
+            if (m_fixingEnabled) {
+                m_changer->modifyIncidence(item);
+                m_pendingModifications++;
+            }
+        }
+    }
+
+    endTest();
+}
+
+void CalendarJanitor::sanityCheck4()
+{
+    beginTest(i18n("Checking for recurring to-dos with invalid DTSTART..."));
+    foreach (const Akonadi::Item &item, m_itemsToProcess) {
+        KCalCore::Incidence::Ptr incidence = CalendarSupport::incidence(item);
+        KCalCore::Todo::Ptr todo = incidence.dynamicCast<KCalCore::Todo>();
+        if (!todo)
+            continue;
+
+        KDateTime start = todo->dtStart();
+        KDateTime due   = todo->dtDue();
+        bool modify = false;
+        if (todo->recurs() && !start.isValid() && due.isValid()) {
+            modify = true;
+            printFound(item);
+            todo->setDtStart(due);
+        }
+
+        if (todo->recurs() && !start.isValid() && !due.isValid()) {
+            modify = true;
+            printFound(item);
+            todo->setDtStart(KDateTime::currentLocalDateTime());
+        }
+
+        if (modify) {
+            if (m_fixingEnabled) {
+                m_changer->modifyIncidence(item);
+                m_pendingModifications++;
+            }
+        }
+    }
+
+    endTest();
+}
+
+void CalendarJanitor::sanityCheck5()
+{
+    beginTest(i18n("Checking for journal with invalid DTSTART..."));
+    foreach (const Akonadi::Item &item, m_itemsToProcess) {
+        KCalCore::Incidence::Ptr incidence = CalendarSupport::incidence(item);
+        if (incidence->type() != KCalCore::Incidence::TypeJournal)
+            continue;
+
+        if (!incidence->dtStart().isValid()) {
+            printFound(item);
+            incidence->setDtStart(KDateTime::currentLocalDateTime());
+            if (m_fixingEnabled) {
+                m_changer->modifyIncidence(item);
+                m_pendingModifications++;
+            }
+        }
+    }
+    endTest();
+}
+
+void CalendarJanitor::sanityCheck6()
+{
+    beginTest(i18n("Checking for orphans...")); // Incidences without a parent
+
+    foreach (const Akonadi::Item &item, m_itemsToProcess) {
+        KCalCore::Incidence::Ptr incidence = CalendarSupport::incidence(item);
+        const QString parentUid = incidence->relatedTo();
+        if (!parentUid.isEmpty() && !m_incidenceMap.contains(parentUid)) {
+            printFound(item);
+            if (m_fixingEnabled) {
+                incidence->setRelatedTo(QString());
+                m_changer->modifyIncidence(item);
+                m_pendingModifications++;
+            }
+        }
+    }
+
+    endTest();
+}
+
+void CalendarJanitor::sanityCheck7()
+{
+    beginTest(i18n("Checking for duplicate UIDs..."));
+
+    foreach (const Akonadi::Item &item, m_itemsToProcess) {
+        KCalCore::Incidence::Ptr incidence = CalendarSupport::incidence(item);
+        QList<KCalCore::Incidence::Ptr> existingIncidences = m_incidenceMap.values(incidence->instanceIdentifier());
+
+        if (existingIncidences.count() == 1)
+            continue;
+
+        foreach (const KCalCore::Incidence::Ptr &existingIncidence, existingIncidences) {
+            if (existingIncidence != incidence && *incidence == *existingIncidence) {
+                printFound(item);
+                deleteIncidence(item);
+                break;
+            }
+        }
+    }
+
+    foreach (const Akonadi::Item &item, m_itemsToProcess) {
+        KCalCore::Incidence::Ptr incidence = CalendarSupport::incidence(item);
+        QList<KCalCore::Incidence::Ptr> existingIncidences = m_incidenceMap.values(incidence->instanceIdentifier());
+
+        if (existingIncidences.count() == 1)
+            continue;
+
+        for (int i=1; i<existingIncidences.count(); ++i) {
+            printFound(item);
+            if (m_fixingEnabled) {
+                KCalCore::Incidence::Ptr existingIncidence = existingIncidences.at(i);
+                Akonadi::Item item = m_incidenceToItem.value(existingIncidence);
+                Q_ASSERT(item.isValid());
+                if (item.isValid()) {
+                    existingIncidence->recreate();
+                    m_changer->modifyIncidence(item);
+                    m_pendingModifications++;
+                    m_incidenceMap.remove(incidence->instanceIdentifier(), existingIncidence);
+                }
+            }
+        }
+    }
+
+    endTest();
+}
+
+static void printStat(const QString &message, int arg)
+{
+    if (arg > 0) {
+        print(message.leftJustified(50), false);
+        const QString s = QLatin1String(": %1");
+        print(s.arg(arg));
+    }
+}
+
+void CalendarJanitor::sanityCheck8()
+{
+    beginTest(i18n("Gathering statistics..."));
+    print("\n");
+
+    int numOldAlarms = 0;
+    int numAttachments = 0;
+    int totalAttachmentSize = 0;
+    int numOldIncidences = 0;
+    QHash<KCalCore::Incidence::IncidenceType, int> m_counts;
+
+    foreach (const Akonadi::Item &item, m_itemsToProcess) {
+        KCalCore::Incidence::Ptr incidence = CalendarSupport::incidence(item);
+        if (!incidence->attachments().isEmpty()) {
+            foreach (const KCalCore::Attachment::Ptr &attachment, incidence->attachments()) {
+                if (!attachment->isUri()) {
+                    numAttachments++;
+                    totalAttachmentSize += attachment->size();
+                }
+            }
+        }
+
+        m_counts[incidence->type()]++;
+
+        if (incidence->dtStart().isValid() && !incidence->recurs()          &&
+            incidence->dtStart().daysTo(KDateTime::currentDateTime(KDateTime::LocalZone)) > 365 &&
+            incidence->type() != KCalCore::Incidence::TypeJournal) {
+
+            if (!incidence->alarms().isEmpty())
+                numOldAlarms++;
+
+            numOldIncidences++;
+        }
+
+        numAttachments += incidence->attachments().count();
+        numOldAlarms += incidence->alarms().count();
+    }
+
+    printStat(i18n("Events"), m_counts[KCalCore::Incidence::TypeEvent]);
+    printStat(i18n("Todos"), m_counts[KCalCore::Incidence::TypeTodo]);
+    printStat(i18n("Journal"), m_counts[KCalCore::Incidence::TypeJournal]);
+    printStat(i18n("Passed events and to-dos (>365 days)"), numOldIncidences);
+    printStat(i18n("Old incidences with alarms"), numOldAlarms);
+    printStat(i18n("Inline attachments"), numAttachments);
+
+    if (totalAttachmentSize < 1024) {
+        printStat(i18n("Total size of inline attachments (bytes)"), totalAttachmentSize);
+    } else {
+        printStat(i18n("Total size of inline attachments (KB)"), totalAttachmentSize / 1024);
+    }
+
+    endTest(/**print=*/false);
+}
+
+static QString dateString(const KCalCore::Incidence::Ptr &incidence)
+{
+    KDateTime start = incidence->dtStart();
+    KDateTime end = incidence->dateTime(KCalCore::Incidence::RoleEnd);
+    QString str = QLatin1String("DTSTART=") + (start.isValid() ? start.toString() : i18n("invalid")) + QLatin1String("; ");
+
+    if (incidence->type() == KCalCore::Incidence::TypeJournal) {
+        return str;
+    }
+
+    str += QLatin1String("\n        ");
+
+    if (incidence->type() == KCalCore::Incidence::TypeTodo)
+        str += QLatin1String("DTDUE=");
+    else if (incidence->type() == KCalCore::Incidence::TypeEvent)
+        str += QLatin1String("DTEND=");
+
+    str+= (start.isValid() ? end.toString() : i18n("invalid")) + QLatin1String("; ");
+
+    if (incidence->recurs())
+        str += i18n("recurrent");
+
+    return str;
+}
+
+void CalendarJanitor::printFound(const Akonadi::Item &item)
+{
+    KCalCore::Incidence::Ptr incidence = CalendarSupport::incidence(item);
+    m_numDamaged++;
+    if (m_numDamaged == 1)
+        print(QLatin1String(" [!!]"));
+    print(QString("    * ") + i18n("Found buggy item:"));
+    print(QString("        ") + i18n("id=%1; summary=\"%2\"", item.id(), incidence->summary()));
+    print(QString("        ") + dateString(incidence));
+}
+
+void CalendarJanitor::beginTest(const QString &message)
+{
+    m_numDamaged = 0;
+    m_fixingEnabled = m_options.action() == Options::ActionScanAndFix && !collectionIsReadOnly(m_currentCollection);
+    print(message.leftJustified(TEXT_WIDTH), false);
+}
+
+void CalendarJanitor::endTest(bool printEnabled)
+{
+    if (m_numDamaged == 0 && printEnabled) {
+        print(QLatin1String(" [OK]"));
+    }
+
+    if (m_pendingDeletions == 0 && m_pendingModifications == 0) {
+        runNextTest();
+    }
+}
+
+void CalendarJanitor::deleteIncidence(const Akonadi::Item &item)
+{
+    if (m_fixingEnabled && !collectionIsReadOnly(m_currentCollection)) {
+        m_pendingDeletions++;
+        m_changer->deleteIncidence(item);
+        KCalCore::Incidence::Ptr incidence = CalendarSupport::incidence(item);
+        m_incidenceMap.remove(incidence->instanceIdentifier(), incidence);
+        m_incidenceToItem.remove(incidence);
+    }
+}
diff --git a/console/calendarjanitor/calendarjanitor.h b/console/calendarjanitor/calendarjanitor.h
new file mode 100644
index 0000000..5b66732
--- /dev/null
+++ b/console/calendarjanitor/calendarjanitor.h
@@ -0,0 +1,103 @@
+/*
+  Copyright (c) 2013 Sérgio Martins <iamsergio at gmail.com>
+
+  This program is free software; you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation; either version 2 of the License, or
+  (at your option) any later version.
+
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along
+  with this program; if not, write to the Free Software Foundation, Inc.,
+  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+  As a special exception, permission is given to link this program
+  with any edition of Qt, and distribute the resulting executable,
+  without including the source code for Qt in the source distribution.
+*/
+
+#ifndef CALENDARJANITOR_H
+#define CALENDARJANITOR_H
+
+#include "options.h"
+
+#include <KCalCore/Incidence>
+
+#include <Akonadi/Calendar/IncidenceChanger>
+#include <Akonadi/Collection>
+#include <Akonadi/Item>
+#include <QObject>
+#include <QString>
+#include <QMultiMap>
+
+class CollectionLoader;
+
+class KJob;
+
+class CalendarJanitor : public QObject
+{
+    Q_OBJECT
+public:
+    explicit CalendarJanitor(const Options &options, QObject *parent = 0);
+
+    void start();
+
+Q_SIGNALS:
+    void finished(bool success);
+
+private Q_SLOTS:
+    void onCollectionsFetched(bool success);
+    void onItemsFetched(KJob *job);
+    void onModifyFinished(int changeId, const Akonadi::Item &item,
+                          Akonadi::IncidenceChanger::ResultCode resultCode, const QString &errorMessage);
+    void onDeleteFinished(int changeId, const QVector<Akonadi::Item::Id> &,
+                          Akonadi::IncidenceChanger::ResultCode resultCode, const QString &errorMessage);
+
+    void processNextCollection();
+
+    // For each collection we process, we run a bunch of tests on it.
+    void runNextTest();
+
+    void sanityCheck1();
+    void sanityCheck2();
+    void sanityCheck3();
+    void sanityCheck4();
+    void sanityCheck5();
+    void sanityCheck6();
+    void sanityCheck7();
+    void sanityCheck8();
+
+    void printFound(const Akonadi::Item &item);
+
+    void beginTest(const QString &message);
+    void endTest(bool print = true);
+
+    void deleteIncidence(const Akonadi::Item &item);
+
+private:
+    CollectionLoader *m_collectionLoader;
+    Akonadi::Collection::List m_collectionsToProcess;
+    Akonadi::Item::List m_itemsToProcess;
+    Options m_options;
+    Akonadi::IncidenceChanger *m_changer;
+    Akonadi::Collection m_currentCollection;
+    Options::SanityCheck m_currentSanityCheck;
+    int m_pendingModifications;
+    int m_pendingDeletions;
+
+    QList<Akonadi::Item::Id> m_test1Results;
+    QStringList m_test2Results;
+
+    int m_numDamaged;
+    bool m_fixingEnabled;
+
+    QString m_summary; // to print at the end.
+    QMultiMap<QString, KCalCore::Incidence::Ptr> m_incidenceMap;
+    QMap<KCalCore::Incidence::Ptr, Akonadi::Item> m_incidenceToItem;
+};
+
+#endif // CALENDARJANITOR_H
diff --git a/console/calendarjanitor/collectionloader.cpp b/console/calendarjanitor/collectionloader.cpp
new file mode 100644
index 0000000..d0a8c6a
--- /dev/null
+++ b/console/calendarjanitor/collectionloader.cpp
@@ -0,0 +1,70 @@
+/*
+  Copyright (c) 2013 Sérgio Martins <iamsergio at gmail.com>
+
+  This program is free software; you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation; either version 2 of the License, or
+  (at your option) any later version.
+
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along
+  with this program; if not, write to the Free Software Foundation, Inc.,
+  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+  As a special exception, permission is given to link this program
+  with any edition of Qt, and distribute the resulting executable,
+  without including the source code for Qt in the source distribution.
+*/
+
+#include "collectionloader.h"
+
+#include <KCalCore/Incidence>
+
+#include <Akonadi/CollectionFetchJob>
+#include <Akonadi/CollectionFetchScope>
+#include <QString>
+#include <QSet>
+
+
+CollectionLoader::CollectionLoader(QObject *parent) :
+    QObject(parent)
+{
+}
+
+void CollectionLoader::load()
+{
+    Akonadi::CollectionFetchJob *job = new Akonadi::CollectionFetchJob(Akonadi::Collection::root(),
+                                                                       Akonadi::CollectionFetchJob::Recursive);
+
+    job->fetchScope().setContentMimeTypes(KCalCore::Incidence::mimeTypes());
+    connect(job, SIGNAL(result(KJob*)), SLOT(onCollectionsLoaded(KJob *)));
+    job->start();
+}
+
+Akonadi::Collection::List CollectionLoader::collections() const
+{
+    return m_collections;
+}
+
+void CollectionLoader::onCollectionsLoaded(KJob *job)
+{
+    if (job->error() == 0) {
+        QSet<QString> mimeTypeSet = KCalCore::Incidence::mimeTypes().toSet();
+        Akonadi::CollectionFetchJob *cfj = qobject_cast<Akonadi::CollectionFetchJob*>(job);
+        Q_ASSERT(cfj);
+        foreach(const Akonadi::Collection &collection, cfj->collections()) {
+            if (!mimeTypeSet.intersect(collection.contentMimeTypes().toSet()).isEmpty()) {
+                m_collections << collection;
+            }
+        }
+
+        emit loaded(true);
+    } else {
+        kError() << job->errorString();
+        emit loaded(false);
+    }
+}
diff --git a/console/calendarjanitor/collectionloader.h b/console/calendarjanitor/collectionloader.h
new file mode 100644
index 0000000..0807bac
--- /dev/null
+++ b/console/calendarjanitor/collectionloader.h
@@ -0,0 +1,50 @@
+/*
+  Copyright (c) 2013 Sérgio Martins <iamsergio at gmail.com>
+
+  This program is free software; you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation; either version 2 of the License, or
+  (at your option) any later version.
+
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along
+  with this program; if not, write to the Free Software Foundation, Inc.,
+  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+  As a special exception, permission is given to link this program
+  with any edition of Qt, and distribute the resulting executable,
+  without including the source code for Qt in the source distribution.
+*/
+
+#ifndef COLLECTIONLOADER_H
+#define COLLECTIONLOADER_H
+
+#include <Akonadi/Collection>
+
+#include <QObject>
+
+class KJob;
+
+class CollectionLoader : public QObject
+{
+    Q_OBJECT
+public:
+    explicit CollectionLoader(QObject *parent = 0);
+    void load();
+    Akonadi::Collection::List collections() const;
+
+Q_SIGNALS:
+    void loaded(bool succcess);
+
+private Q_SLOTS:
+    void onCollectionsLoaded(KJob*);
+
+private:
+    Akonadi::Collection::List m_collections;
+};
+
+#endif // COLLECTIONLOADER_H
diff --git a/console/calendarjanitor/main.cpp b/console/calendarjanitor/main.cpp
new file mode 100644
index 0000000..e141785
--- /dev/null
+++ b/console/calendarjanitor/main.cpp
@@ -0,0 +1,177 @@
+/*
+  Copyright (c) 2013 Sérgio Martins <iamsergio at gmail.com>
+
+  This program is free software; you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation; either version 2 of the License, or
+  (at your option) any later version.
+
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along
+  with this program; if not, write to the Free Software Foundation, Inc.,
+  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+  As a special exception, permission is given to link this program
+  with any edition of Qt, and distribute the resulting executable,
+  without including the source code for Qt in the source distribution.
+*/
+
+#include "calendarjanitor.h"
+#include "options.h"
+#include "backuper.h"
+
+#include "kdepim-version.h"
+
+#include <KAboutData>
+#include <KLocale>
+#include <KCmdLineArgs>
+#include <KApplication>
+
+#include <QTextStream>
+#include <QString>
+#include <qglobal.h>
+
+#ifdef Q_OS_UNIX
+#    include <sys/types.h>
+#    include <sys/stat.h>
+#    include <fcntl.h>
+#endif
+
+static const char progName[] = "calendarjanitor";
+static const char progDisplay[] = "CalendarJanitor";
+
+static const char progVersion[] = KDEPIM_VERSION;
+static const char progDesc[] = "A command line interface to report and fix errors in your calendar data";
+
+static void print(const QString &message)
+{
+    QTextStream out(stdout);
+    out << message << "\n";
+}
+
+static void printCollectionsUsage()
+{
+    print(i18n("Error while parsing %1", QLatin1String("--collections")));
+    print(i18n("Example usage %1", QLatin1String(": --collections 90,23,40")));
+}
+
+static void silenceStderr()
+{
+#ifdef Q_OS_UNIX
+    if (qgetenv("KDE_CALENDARJANITOR_DEBUG") != "1") {
+        // Disable stderr so we can actually read what's going on
+        int fd = ::open("/dev/null", O_WRONLY);
+        ::dup2(fd, 2);
+        ::close(fd);
+    }
+#endif
+}
+
+int main(int argv, char *argc[])
+{
+    KAboutData aboutData(progName, 0,                 // internal program name
+                         ki18n(progDisplay),          // displayable program name.
+                         progVersion,                 // version string
+                         ki18n(progDesc),             // short program description
+                         KAboutData::License_GPL,     // license type
+                         ki18n("(c) 2013, Sérgio Martins"),
+                         ki18n(0),                    // any free form text
+                         0,                           // program home page address
+                         "bugs.kde.org");
+    aboutData.addAuthor(ki18n("Sérgio Martins"), ki18n("Maintainer"), "iamsergio at gmail.com", 0);
+
+    KCmdLineArgs::init(argv, argc, &aboutData, KCmdLineArgs::CmdLineArgNone);
+
+
+    KCmdLineOptions options;
+    options.add("collections <ids>", ki18n("List of collection ids to scan"));
+    options.add("fix", ki18n("Fix broken incidences"));
+    options.add("backup <output.ics>", ki18n("Backup your calendar"));
+
+    options.add("", ki18n("\nExamples:\n\nScan all collections:\n"
+                          "$ calendarjanitor\n\n"
+                          "Scan and fix all collections:\n"
+                          "$ calendarjanitor --fix\n\n"
+                          "Scan and fix some collections:\n"
+                          "$ calendarjanitor --collections 10,20 --fix\n\n"
+                          "Backup all collections:\n"
+                          "$ calendarjanitor --backup backup.ics\n\n"
+                          "Backup some collections:\n"
+                          "$ calendarjanitor --backup backup.ics --collections 10,20"));
+
+    KCmdLineArgs::addCmdLineOptions(options);
+    KCmdLineArgs *args = KCmdLineArgs::parsedArgs();
+
+    Options janitorOptions;
+
+    if (args->isSet("collections")) {
+        QString option = args->getOption("collections");
+        QStringList collections = option.split(",");
+        QList<Akonadi::Collection::Id> ids;
+        foreach (const QString &collection, collections) {
+            bool ok = false;
+            int num = collection.toInt(&ok);
+            if (ok) {
+                ids << num;
+            } else {
+                printCollectionsUsage();
+                return -1;
+            }
+
+            if (ids.isEmpty()) {
+                printCollectionsUsage();
+                return -1;
+            } else {
+                janitorOptions.setCollections(ids);
+            }
+        }
+    }
+
+    if (args->isSet("fix") && args->isSet("backup")) {
+        print("--fix is incompatible with --backup");
+        return -1;
+    }
+
+    KApplication app(false);
+
+    silenceStderr(); // Switching off mobile phones, movie is about to start
+
+    QString backupFile;
+    if (args->isSet("fix")) {
+        janitorOptions.setAction(Options::ActionScanAndFix);
+        print(i18n("Running in fix mode."));
+    } else if (args->isSet("backup")) {
+        backupFile = args->getOption("backup");
+        if (backupFile.isEmpty()) {
+            print("Please specify a output file.");
+            return -1;
+        }
+        janitorOptions.setAction(Options::ActionBackup);
+    } else {
+        print(i18n("Running in scan only mode."));
+        janitorOptions.setAction(Options::ActionScan);
+    }
+
+    switch(janitorOptions.action()) {
+    case Options::ActionBackup: {
+        Backuper *backuper = new Backuper();
+        backuper->backup(backupFile, janitorOptions.collections());
+        break;
+    }
+    case Options::ActionScan:
+    case Options::ActionScanAndFix: {
+        CalendarJanitor *janitor = new CalendarJanitor(janitorOptions);
+        janitor->start();
+        break;
+    }
+    default:
+        Q_ASSERT(false);
+    }
+
+
+    return app.exec();
+}
diff --git a/console/calendarjanitor/options.cpp b/console/calendarjanitor/options.cpp
new file mode 100644
index 0000000..2c4103e
--- /dev/null
+++ b/console/calendarjanitor/options.cpp
@@ -0,0 +1,52 @@
+/*
+  Copyright (c) 2013 Sérgio Martins <iamsergio at gmail.com>
+
+  This program is free software; you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation; either version 2 of the License, or
+  (at your option) any later version.
+
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along
+  with this program; if not, write to the Free Software Foundation, Inc.,
+  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+  As a special exception, permission is given to link this program
+  with any edition of Qt, and distribute the resulting executable,
+  without including the source code for Qt in the source distribution.
+*/
+
+#include "options.h"
+
+Options::Options() : m_action(ActionNone)
+{
+}
+
+void Options::setAction(Options::Action action)
+{
+    m_action = action;
+}
+
+Options::Action Options::action() const
+{
+    return m_action;
+}
+
+QList<Akonadi::Entity::Id> Options::collections() const
+{
+    return m_collectionIds;
+}
+
+void Options::setCollections(const QList<Akonadi::Collection::Id> &collections)
+{
+    m_collectionIds = collections;
+}
+
+bool Options::testCollection(Akonadi::Entity::Id id) const
+{
+    return m_collectionIds.isEmpty() || m_collectionIds.contains(id);
+}
diff --git a/console/calendarjanitor/options.h b/console/calendarjanitor/options.h
new file mode 100644
index 0000000..150dfa6
--- /dev/null
+++ b/console/calendarjanitor/options.h
@@ -0,0 +1,71 @@
+/*
+  Copyright (c) 2013 Sérgio Martins <iamsergio at gmail.com>
+
+  This program is free software; you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation; either version 2 of the License, or
+  (at your option) any later version.
+
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along
+  with this program; if not, write to the Free Software Foundation, Inc.,
+  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+  As a special exception, permission is given to link this program
+  with any edition of Qt, and distribute the resulting executable,
+  without including the source code for Qt in the source distribution.
+*/
+
+#ifndef OPTIONS_H
+#define OPTIONS_H
+
+#include <Akonadi/Collection>
+#include <QList>
+
+class Options
+{
+public:
+
+    enum SanityCheck {
+        CheckNone,
+        CheckEmptySummary,    // Checks for empty summary and description. In fix mode, it deletes them.
+        CheckEmptyUid,        // Checks for an empty UID. In fix mode, a new UID is assigned.
+        CheckEventDates,      // Check for missing DTSTART or DTEND. New dates will be assigned.
+        CheckTodoDates,       // Check for recurring to-dos without DTSTART. DTDUE will be assigned to DTSTART, or current date if DTDUE is also invalid.
+        CheckJournalDates,    // Check for journals without DTSTART
+        CheckOrphans,         // Check for orphan to-dos. Will be unparented." <disabled for now>
+        CheckDuplicateUIDs,   // Check for duplicated UIDs. Copies will be deleted if the payload is the same. Otherwise a new UID is assigned.
+        CheckStats,           // Gathers some statistics. No fixing is done.
+        CheckCount            // For iteration purposes. Keep at end.
+    };
+
+    enum Action {
+        ActionNone,
+        ActionScan,
+        ActionScanAndFix,
+        ActionBackup
+    };
+
+    Options();
+
+    void setAction(Action);
+    Action action() const;
+
+    /**
+     * List of collections for backup or fix modes.
+     * If empty, all collections will be considered.
+     */
+    QList<Akonadi::Collection::Id> collections() const;
+    void setCollections(const QList<Akonadi::Collection::Id> &);
+    bool testCollection(Akonadi::Collection::Id) const;
+
+private:
+    QList<Akonadi::Collection::Id> m_collectionIds;
+    Action m_action;
+};
+
+#endif // OPTIONS_H
_______________________________________________
KDE PIM mailing list kde-pim at kde.org
https://mail.kde.org/mailman/listinfo/kde-pim
KDE PIM home page at http://pim.kde.org/


More information about the kde-pim mailing list