diff --git a/akonadi/CMakeLists.txt b/akonadi/CMakeLists.txt index 2a001edbd..bed922a13 100644 --- a/akonadi/CMakeLists.txt +++ b/akonadi/CMakeLists.txt @@ -1,268 +1,268 @@ project(akonadi-kde) add_definitions( -DKDE_DEFAULT_DEBUG_AREA=5250 ) set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${KDE4_ENABLE_EXCEPTIONS}" ) if(CMAKE_COMPILE_GCOV) set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fprofile-arcs -ftest-coverage") endif(CMAKE_COMPILE_GCOV) if (KDE4_BUILD_TESTS) # only with this macro the AKONADI_TESTS_EXPORT macro will do something add_definitions(-DCOMPILING_TESTS) add_subdirectory( tests ) endif (KDE4_BUILD_TESTS) add_definitions( -DQT_NO_CAST_FROM_ASCII ) add_definitions( -DQT_NO_CAST_TO_ASCII ) add_subdirectory( kabc ) add_subdirectory( kmime ) include_directories( ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR} ${QT_QTDBUS_INCLUDE_DIR} ${Boost_INCLUDE_DIR} ${KDE4_INCLUDE_DIR} ${AKONADI_INCLUDE_DIR} ${AKONADI_INCLUDE_DIR}/akonadi/private ) # libakonadi-kde set( akonadikde_LIB_SRC entity.cpp # keep it at top to not break enable-final agentbase.cpp agentfilterproxymodel.cpp agentinstance.cpp agentinstancecreatejob.cpp agentinstancemodel.cpp agentinstancewidget.cpp agentmanager.cpp agenttype.cpp agenttypemodel.cpp agenttypewidget.cpp agenttypedialog.cpp attribute.cpp attributefactory.cpp cachepolicy.cpp cachepolicypage.cpp changerecorder.cpp collection.cpp collectioncopyjob.cpp collectioncreatejob.cpp collectiondeletejob.cpp collectiondialog.cpp collectionfilterproxymodel.cpp collectiongeneralpropertiespage.cpp collectionfetchjob.cpp collectionfetchscope.cpp collectionmodel.cpp collectionmodel_p.cpp collectionmodifyjob.cpp collectionmovejob.cpp collectionpathresolver.cpp collectionpropertiesdialog.cpp collectionpropertiespage.cpp collectionrequester.cpp collectionrightsattribute.cpp collectionselectjob.cpp collectionstatistics.cpp collectionstatisticsdelegate.cpp collectionstatisticsjob.cpp collectionstatisticsmodel.cpp collectionsync.cpp collectionview.cpp control.cpp descendantsproxymodel.cpp + entitycache.cpp entitydisplayattribute.cpp entityhiddenattribute.cpp entitytreemodel.cpp entitytreemodel_p.cpp entityfilterproxymodel.cpp entitytreeviewstatesaver.cpp erroroverlay.cpp exception.cpp - expungejob.cpp favoritecollectionsmodel.cpp firstrun.cpp flatcollectionproxymodel.cpp item.cpp itemcreatejob.cpp itemcopyjob.cpp itemdeletejob.cpp itemfetchjob.cpp itemfetchscope.cpp itemmodel.cpp itemmonitor.cpp itemmovejob.cpp itemserializer.cpp itemserializerplugin.cpp itemmodifyjob.cpp itemsync.cpp itemview.cpp job.cpp linkjob.cpp filteractionjob.cpp mimetypechecker.cpp monitor.cpp monitor_p.cpp pastehelper.cpp preprocessorbase.cpp protocolhelper.cpp resourcebase.cpp resourcescheduler.cpp resourceselectjob.cpp searchcreatejob.cpp selectionproxymodel.cpp selftestdialog.cpp session.cpp servermanager.cpp standardactionmanager.cpp statisticsproxymodel.cpp statisticstooltipproxymodel.cpp subscriptionjob.cpp subscriptionchangeproxymodel.cpp subscriptiondialog.cpp subscriptionmodel.cpp transactionjobs.cpp transactionsequence.cpp transportresourcebase.cpp unlinkjob.cpp # Temporary until ported to Qt-plugin framework pluginloader.cpp ) # DBus interfaces and adaptors set(akonadi_xml ${AKONADI_DBUS_INTERFACES_DIR}/org.freedesktop.Akonadi.NotificationManager.xml) set_source_files_properties(${akonadi_xml} PROPERTIES INCLUDE "notificationmessage_p.h") qt4_add_dbus_interface( akonadikde_LIB_SRC ${akonadi_xml} notificationmanagerinterface ) qt4_add_dbus_interfaces( akonadikde_LIB_SRC ${AKONADI_DBUS_INTERFACES_DIR}/org.freedesktop.Akonadi.AgentManager.xml ) qt4_add_dbus_interfaces( akonadikde_LIB_SRC ${AKONADI_DBUS_INTERFACES_DIR}/org.freedesktop.Akonadi.Tracer.xml ) qt4_add_dbus_adaptor( akonadikde_LIB_SRC ${AKONADI_DBUS_INTERFACES_DIR}/org.freedesktop.Akonadi.Resource.xml resourcebase.h Akonadi::ResourceBase ) qt4_add_dbus_adaptor( akonadikde_LIB_SRC ${AKONADI_DBUS_INTERFACES_DIR}/org.freedesktop.Akonadi.Preprocessor.xml preprocessorbase.h Akonadi::PreprocessorBase ) qt4_add_dbus_adaptor( akonadikde_LIB_SRC ${AKONADI_DBUS_INTERFACES_DIR}/org.freedesktop.Akonadi.Agent.Status.xml agentbase.h Akonadi::AgentBase ) # TODO move this to kdesupport/akonadi/interfaces qt4_add_dbus_adaptor( akonadikde_LIB_SRC interfaces/org.freedesktop.Akonadi.Resource.Transport.xml transportresourcebase_p.h Akonadi::TransportResourceBasePrivate ) # TODO merge with Akonadi.Control in kdesupport/akonadi/interfaces qt4_add_dbus_adaptor( akonadikde_LIB_SRC interfaces/org.freedesktop.Akonadi.Agent.Control.xml agentbase.h Akonadi::AgentBase ) #qt4_add_dbus_adaptor( akonadikde_LIB_SRC ${AKONADI_DBUS_INTERFACES_DIR}/org.freedesktop.Akonadi.Agent.Control.xml agentbase.h Akonadi::AgentBase ) kde4_add_ui_files( akonadikde_LIB_SRC cachepolicypage.ui collectiongeneralpropertiespage.ui subscriptiondialog.ui controlprogressindicator.ui selftestdialog.ui ) kde4_add_library( akonadi-kde SHARED ${akonadikde_LIB_SRC} ) macro_ensure_version( "4.2.0" ${KDE_VERSION} KDE_IS_AT_LEAST_42 ) target_link_libraries( akonadi-kde ${KDE4_SOLID_LIBS} ${QT_QTNETWORK_LIBRARY} ${QT_QTDBUS_LIBRARY} ${QT_QTSQL_LIBRARY} ${KDE4_KDEUI_LIBS} ${KDE4_KIO_LIBS} ${AKONADI_COMMON_LIBRARIES} ) set( AKONADI_KDE_DEPS ${KDE4_KDEUI_LIBS} ${QT_QTDBUS_LIBRARY} ${QT_QTCORE_LIBRARY} ) if(${KDE_IS_AT_LEAST_42}) target_link_libraries( akonadi-kde LINK_INTERFACE_LIBRARIES ${AKONADI_KDE_DEPS}) else(${KDE_IS_AT_LEAST_42}) target_link_libraries( akonadi-kde ${AKONADI_KDE_DEPS}) endif(${KDE_IS_AT_LEAST_42}) set_target_properties( akonadi-kde PROPERTIES VERSION ${GENERIC_LIB_VERSION} SOVERSION ${GENERIC_LIB_SOVERSION} ) install( TARGETS akonadi-kde EXPORT kdepimlibsLibraryTargets ${INSTALL_TARGETS_DEFAULT_ARGS} ) ########### install files ############### install( FILES akonadi_export.h agentbase.h agentfilterproxymodel.h agentinstance.h agentinstancecreatejob.h agentinstancemodel.h agentinstancewidget.h agentmanager.h agenttype.h agenttypemodel.h agenttypewidget.h agenttypedialog.h attribute.h attributefactory.h cachepolicy.h changerecorder.h collection.h collectioncopyjob.h collectioncreatejob.h collectiondeletejob.h collectiondialog.h collectionfilterproxymodel.h collectionfetchjob.h collectionfetchscope.h collectionmodel.h collectionmodifyjob.h collectionpropertiesdialog.h collectionpropertiespage.h collectionrequester.h collectionstatisticsdelegate.h collectionstatisticsmodel.h collectionstatistics.h collectionstatisticsjob.h collectionview.h control.h descendantsproxymodel.h entity.h entitydisplayattribute.h entityhiddenattribute.h entitytreemodel.h entityfilterproxymodel.h entitytreeviewstatesaver.h exception.h favoritecollectionsmodel.h item.h itemcreatejob.h itemcopyjob.h itemdeletejob.h itemfetchjob.h itemfetchscope.h itemmodel.h itemmodifyjob.h itemmonitor.h itemmovejob.h itempayloadinternals_p.h itemserializerplugin.h itemsync.h itemview.h job.h linkjob.h filteractionjob.h mimetypechecker.h monitor.h qtest_akonadi.h preprocessorbase.h resourcebase.h searchcreatejob.h selectionproxymodel.h session.h servermanager.h standardactionmanager.h statisticsproxymodel.h statisticstooltipproxymodel.h transactionjobs.h transactionsequence.h transportresourcebase.h unlinkjob.h DESTINATION ${INCLUDE_INSTALL_DIR}/akonadi COMPONENT Devel ) install( FILES collectionpathresolver_p.h DESTINATION ${INCLUDE_INSTALL_DIR}/akonadi/private COMPONENT Devel ) install( FILES kcfg2dbus.xsl DESTINATION ${DATA_INSTALL_DIR}/akonadi-kde ) diff --git a/akonadi/Mainpage.dox b/akonadi/Mainpage.dox index 027b46eca..b8394cd15 100644 --- a/akonadi/Mainpage.dox +++ b/akonadi/Mainpage.dox @@ -1,145 +1,144 @@ /** @mainpage Akonadi client library \section libakonadi_intro Introduction libakonadi is the client access library for using the Akonadi PIM data server. All processes accessing Akonadi, including those which communicate with a remote server (\ref akonadi_design_agents "agents"), are considered clients. Functionality provided by libakonadi: - \ref libakonadi_objects - \ref libakonadi_collections - \ref libakonadi_pimitems - \ref libakonadi_jobs - \ref libakonadi_monitor - \ref libakonadi_serializer - \ref libakonadi_resource - \ref libakonadi_integration \section libakonadi_objects Akonadi Objects Akonadi works on two basic object types: collections and items. Collections are comparable to folders in a file system and are represented by the class Akonadi::Collection. Every collection has an associated cache policy represented by the class Akonadi::CachePolicy which defines what part of its content is cached for how long. All available ways to work with collections are listed in the \ref libakonadi_collections "Collections" section. Akonadi items are comparable to files in a file system and are represented by the class Akonadi::Item. Each item represents a single PIM object such as a mail or a contact. The actual object it represents is its so-called payload. All available ways to work with items are listed in the \ref libakonadi_pimitems "Items" section. Both, items and collections, are identified by a persistent unique identifier. Also, they can contain arbitrary attributes (derived from Akonadi::Attribute) to attach general or application specific meta data to them. Functionality common to both is provided by their base class Akonadi::Entity. \section libakonadi_collections Collection retrieval and manipulation See \ref akonadi_concepts_collections "Collection Concept" for more information about the concept of collections. A collection is represented by the Akonadi::Collection class. Classes to retrieve information about collections: - Akonadi::CollectionFetchJob - Akonadi::CollectionSelectJob - Akonadi::CollectionStatisticsJob Classes to manipulate collections: - Akonadi::CollectionCreateJob - Akonadi::CollectionCopyJob - Akonadi::CollectionModifyJob - Akonadi::CollectionDeleteJob There is also Akonadi::CollectionModel, which is a self-updating model class which can be used in combination with Akonadi::CollectionView. Akonadi::CollectionFilterProxyModel can be used to limit a displayed collection tree to collections supporting a certain type of PIM items. Akonadi::CollectionPropertiesDialog provides an extensible properties dialog for collections. Often needed KAction for collection operations are provided by Akonadi::StandardActionManager. \section libakonadi_pimitems PIM item retrieval and manipulation PIM items are represented by classes derived from Akonadi::Item. Items can be retrieved using Akonadi::ItemFetchJob. The following classes are provided to manipulate PIM items: - Akonadi::ItemCreateJob - Akonadi::ItemCopyJob - Akonadi::ItemModifyJob - Akonadi::ItemDeleteJob -- Akonadi::ExpungeJob Akonadi::ItemModel provides a self-updating model class which can be used to display the content of a collection. Akonadi::ItemView is the base-class for a corresponding view. Often needed KAction for item operations are provided by Akonadi::StandardActionManager. \section libakonadi_jobs Low-level access to the Akonadi server Accessing the Akonadi server is done using job-classes derived from Akonadi::Job. The communication channel with the server is provided by Akonadi::Session. To use server-side transactions, the following jobs are provided: - Akonadi::TransactionBeginJob - Akonadi::TransactionCommitJob - Akonadi::TransactionRollbackJob There also is Akonadi::TransactionSequence which can be used to automatically group a set of jobs to a single transaction. \section libakonadi_monitor Change notifications The Akonadi::Monitor class allows you to monitor specific resources, collections and PIM items for changes. Akonadi::ChangeRecorder augments this by providing a way to record and replay change notifications. \section libakonadi_serializer PIM item serializer The class Akonadi::ItemSerializer is responsible for converting between the stored (binary) representation of a PIM item and the objects used to handle these items provided by the corresponding libraries (kabc, kcal, etc.). Serializer plugins allow you to add support for new kinds of PIM items to Akonadi. Akonadi::ItemSerializerPlugin can be used as a base class for such a plugin. \section libakonadi_resource Agents and Resources The class Akonadi::AgentBase is the common base class for all agents. It provides commonly needed functionality such as change monitoring and recording. Resources are a special kind of agents. Akonadi::ResourceBase is the base class for them. It provides the necessary interfaces to the server as well as many convenience functions to make implementing a new resource as easy as possible. \section libakonadi_integration Integration in your application Akonadi::Control provides ways to ensure that the Akonadi server is running, to monitor its availability and provide help on server-side errors. A more low-level interface to the Akonadi server is provided by Akonadi::ServerManager. A set of standard actions is provided by Akonadi::StandardActionManager. These provided consistent look and feel across applications. */ /** \defgroup AkonadiMacros Akonadi Macros */ // DOXYGEN_REFERENCES = kdecore kdeui diff --git a/akonadi/collectionfetchjob.cpp b/akonadi/collectionfetchjob.cpp index 972644a0b..1dcbb94c0 100644 --- a/akonadi/collectionfetchjob.cpp +++ b/akonadi/collectionfetchjob.cpp @@ -1,262 +1,262 @@ /* Copyright (c) 2006 - 2007 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "collectionfetchjob.h" #include "imapparser_p.h" #include "job_p.h" #include "protocol_p.h" #include "protocolhelper_p.h" #include "entity_p.h" #include "collectionfetchscope.h" #include #include #include #include using namespace Akonadi; class Akonadi::CollectionFetchJobPrivate : public JobPrivate { public: CollectionFetchJobPrivate( CollectionFetchJob *parent ) : JobPrivate( parent ) { } Q_DECLARE_PUBLIC( CollectionFetchJob ) CollectionFetchJob::Type mType; Collection mBase; Collection::List mBaseList; Collection::List mCollections; CollectionFetchScope mScope; Collection::List mPendingCollections; QTimer *mEmitTimer; void timeout() { Q_Q( CollectionFetchJob ); mEmitTimer->stop(); // in case we are called by result() if ( !mPendingCollections.isEmpty() ) { emit q->collectionsReceived( mPendingCollections ); mPendingCollections.clear(); } } }; CollectionFetchJob::CollectionFetchJob( const Collection &collection, Type type, QObject *parent ) : Job( new CollectionFetchJobPrivate( this ), parent ) { Q_D( CollectionFetchJob ); d->mBase = collection; d->mType = type; d->mEmitTimer = new QTimer( this ); d->mEmitTimer->setSingleShot( true ); d->mEmitTimer->setInterval( 100 ); connect( d->mEmitTimer, SIGNAL(timeout()), this, SLOT(timeout()) ); connect( this, SIGNAL(result(KJob*)), this, SLOT(timeout()) ); } CollectionFetchJob::CollectionFetchJob( const Collection::List & cols, QObject * parent ) : Job( new CollectionFetchJobPrivate( this ), parent ) { Q_D( CollectionFetchJob ); Q_ASSERT( !cols.isEmpty() ); if ( cols.size() == 1 ) { d->mBase = cols.first(); d->mType = CollectionFetchJob::Base; } else { d->mBaseList = cols; } d->mEmitTimer = new QTimer( this ); d->mEmitTimer->setSingleShot( true ); d->mEmitTimer->setInterval( 100 ); connect( d->mEmitTimer, SIGNAL(timeout()), this, SLOT(timeout()) ); connect( this, SIGNAL(result(KJob*)), this, SLOT(timeout()) ); } CollectionFetchJob::~CollectionFetchJob() { } Collection::List CollectionFetchJob::collections() const { Q_D( const CollectionFetchJob ); return d->mCollections; } void CollectionFetchJob::doStart() { Q_D( CollectionFetchJob ); if ( !d->mBaseList.isEmpty() ) { foreach ( const Collection &col, d->mBaseList ) { new CollectionFetchJob( col, CollectionFetchJob::Base, this ); } return; } if ( !d->mBase.isValid() && d->mBase.remoteId().isEmpty() ) { setError( Unknown ); setErrorText( QLatin1String( "Invalid collection given." ) ); emitResult(); return; } QByteArray command = d->newTag(); if ( !d->mBase.isValid() ) command += " " AKONADI_CMD_RID; if ( d->mScope.includeUnubscribed() ) - command += " X-AKLIST "; + command += " LIST "; else - command += " X-AKLSUB "; + command += " LSUB "; if ( d->mBase.isValid() ) command += QByteArray::number( d->mBase.id() ); else command += ImapParser::quote( d->mBase.remoteId().toUtf8() ); command += ' '; switch ( d->mType ) { case Base: command += "0 ("; break; case FirstLevel: command += "1 ("; break; case Recursive: command += "INF ("; break; default: Q_ASSERT( false ); } QList filter; if ( !d->mScope.resource().isEmpty() ) { filter.append( "RESOURCE" ); filter.append( d->mScope.resource().toUtf8() ); } if ( !d->mScope.contentMimeTypes().isEmpty() ) { filter.append( "MIMETYPE" ); QList mts; foreach ( const QString &mt, d->mScope.contentMimeTypes() ) mts.append( mt.toUtf8() ); - filter.append( "(" + ImapParser::join( mts, " " ) + ")" ); + filter.append( '(' + ImapParser::join( mts, " " ) + ')' ); } QList options; if ( d->mScope.includeStatistics() ) { options.append( "STATISTICS" ); options.append( "true" ); } if ( d->mScope.ancestorRetrieval() != CollectionFetchScope::None ) { options.append( "ANCESTORS" ); switch ( d->mScope.ancestorRetrieval() ) { case CollectionFetchScope::None: options.append( "0" ); break; case CollectionFetchScope::Parent: options.append( "1" ); break; case CollectionFetchScope::All: options.append( "INF" ); break; default: Q_ASSERT( false ); } } command += ImapParser::join( filter, " " ) + ") (" + ImapParser::join( options, " " ) + ")\n"; d->writeData( command ); } void CollectionFetchJob::doHandleResponse( const QByteArray & tag, const QByteArray & data ) { Q_D( CollectionFetchJob ); if ( tag == "*" ) { Collection collection; ProtocolHelper::parseCollection( data, collection ); if ( !collection.isValid() ) return; collection.d_ptr->resetChangeLog(); d->mCollections.append( collection ); d->mPendingCollections.append( collection ); if ( !d->mEmitTimer->isActive() ) d->mEmitTimer->start(); return; } kDebug() << "Unhandled server response" << tag << data; } void CollectionFetchJob::setResource(const QString & resource) { Q_D( CollectionFetchJob ); d->mScope.setResource( resource ); } void CollectionFetchJob::slotResult(KJob * job) { Q_D( CollectionFetchJob ); CollectionFetchJob *list = dynamic_cast( job ); Q_ASSERT( job ); d->mCollections += list->collections(); Job::slotResult( job ); if ( !job->error() && !hasSubjobs() ) emitResult(); } void CollectionFetchJob::includeUnsubscribed(bool include) { Q_D( CollectionFetchJob ); d->mScope.setIncludeUnsubscribed( include ); } void CollectionFetchJob::includeStatistics(bool include) { Q_D( CollectionFetchJob ); d->mScope.setIncludeStatistics( include ); } void CollectionFetchJob::setFetchScope( const CollectionFetchScope &scope ) { Q_D( CollectionFetchJob ); d->mScope = scope; } CollectionFetchScope& CollectionFetchJob::fetchScope() { Q_D( CollectionFetchJob ); return d->mScope; } #include "collectionfetchjob.moc" diff --git a/akonadi/collectionsync.cpp b/akonadi/collectionsync.cpp index 7b2de0589..d9155f92d 100644 --- a/akonadi/collectionsync.cpp +++ b/akonadi/collectionsync.cpp @@ -1,535 +1,559 @@ /* Copyright (c) 2007, 2009 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "collectionsync_p.h" #include "collection.h" #include "collectioncreatejob.h" #include "collectiondeletejob.h" #include "collectionfetchjob.h" #include "collectionmodifyjob.h" #include "collectionfetchscope.h" +#include "collectionmovejob.h" #include #include using namespace Akonadi; struct RemoteNode; /** LocalNode is used to build a tree structure of all our locally existing collections. */ struct LocalNode { LocalNode( const Collection &col ) : collection( col ), processed( false ) {} ~LocalNode() { qDeleteAll( childNodes ); qDeleteAll( pendingRemoteNodes ); } Collection collection; QList childNodes; QHash childRidMap; /** When using hierarchical RIDs we attach a list of not yet processable remote nodes to the closest already existing local ancestor node. They will be re-evaluated once a new child node is added. */ QList pendingRemoteNodes; bool processed; }; Q_DECLARE_METATYPE( LocalNode* ) static const char LOCAL_NODE[] = "LocalNode"; /** RemoteNode is used as a container for remote collections which typically don't have a UID set and thus cannot easily be compared or put into maps etc. */ struct RemoteNode { RemoteNode( const Collection &col ) : collection( col ) {} Collection collection; }; Q_DECLARE_METATYPE( RemoteNode* ) static const char REMOTE_NODE[] = "RemoteNode"; /** * @internal */ class CollectionSync::Private { public: Private( CollectionSync *parent ) : q( parent ), pendingJobs( 0 ), + progress( 0 ), incremental( false ), streaming( false ), hierarchicalRIDs( false ), localListDone( false ), deliveryDone( false ) { localRoot = new LocalNode( Collection::root() ); localRoot->processed = true; // never try to delete that one localUidMap.insert( localRoot->collection.id(), localRoot ); if ( !hierarchicalRIDs ) localRidMap.insert( QString(), localRoot ); } ~Private() { delete localRoot; } /** Create a local node from the given local collection and integrate it into the local tree structure. */ LocalNode* createLocalNode( const Collection &col ) { LocalNode *node = new LocalNode( col ); Q_ASSERT( !localUidMap.contains( col.id() ) ); localUidMap.insert( node->collection.id(), node ); if ( !hierarchicalRIDs ) localRidMap.insert( node->collection.remoteId(), node ); // add already existing children if ( localPendingCollections.contains( col.id() ) ) { QList childIds = localPendingCollections.take( col.id() ); foreach ( Collection::Id childId, childIds ) { Q_ASSERT( localUidMap.contains( childId ) ); LocalNode *childNode = localUidMap.value( childId ); node->childNodes.append( childNode ); node->childRidMap.insert( childNode->collection.remoteId(), childNode ); } } // set our parent and add ourselves as child if ( localUidMap.contains( col.parentCollection().id() ) ) { LocalNode* parentNode = localUidMap.value( col.parentCollection().id() ); parentNode->childNodes.append( node ); parentNode->childRidMap.insert( node->collection.remoteId(), node ); } else { localPendingCollections[ col.parentCollection().id() ].append( col.id() ); } return node; } /** Same as createLocalNode() for remote collections. */ void createRemoteNode( const Collection &col ) { if ( col.remoteId().isEmpty() ) { kWarning() << "Collection '" << col.name() << "' does not have a remote identifier - skipping"; return; } RemoteNode *node = new RemoteNode( col ); localRoot->pendingRemoteNodes.append( node ); } /** Create local nodes as we receive the local listing from the Akonadi server. */ void localCollectionsReceived( const Akonadi::Collection::List &localCols ) { foreach ( const Collection &c, localCols ) createLocalNode( c ); } /** Once the local collection listing finished we can continue with the interesting stuff. */ void localCollectionFetchResult( KJob *job ) { if ( job->error() ) return; // handled by the base class // safety check: the local tree has to be connected if ( !localPendingCollections.isEmpty() ) { q->setError( Unknown ); q->setErrorText( QLatin1String( "Inconsistent local collection tree detected! OMG, what have you done to my database?!?!" ) ); q->emitResult(); return; } localListDone = true; execute(); } /** Find the local node that matches the given remote collection, returns 0 if that doesn't exist (yet). */ LocalNode* findMatchingLocalNode( const Collection &collection ) { if ( !hierarchicalRIDs ) { if ( localRidMap.contains( collection.remoteId() ) ) return localRidMap.value( collection.remoteId() ); return 0; } else { if ( collection.id() == Collection::root().id() || collection.remoteId() == Collection::root().remoteId() ) return localRoot; LocalNode *localParent = 0; if ( collection.parentCollection().id() < 0 && collection.parentCollection().remoteId().isEmpty() ) { kWarning() << "Remote collection without valid parent found: " << collection; return 0; } if ( collection.parentCollection().id() == Collection::root().id() || collection.parentCollection().remoteId() == Collection::root().remoteId() ) localParent = localRoot; else localParent = findMatchingLocalNode( collection.parentCollection() ); if ( localParent && localParent->childRidMap.contains( collection.remoteId() ) ) return localParent->childRidMap.value( collection.remoteId() ); return 0; } } /** Find the local node that is the nearest ancestor of the given remote collection (when using hierarchical RIDs only, otherwise it's always the local root node). Never returns 0. */ LocalNode* findBestLocalAncestor( const Collection &collection, bool *exactMatch = 0 ) { if ( !hierarchicalRIDs ) return localRoot; if ( collection.parentCollection().id() < 0 && collection.parentCollection().remoteId().isEmpty() ) { kWarning() << "Remote collection without valid parent found: " << collection; return 0; } if ( collection.parentCollection() == Collection::root() ) { if ( exactMatch ) *exactMatch = true; return localRoot; } bool parentIsExact = false; LocalNode *localParent = findBestLocalAncestor( collection.parentCollection(), &parentIsExact ); if ( !parentIsExact ) { if ( exactMatch ) *exactMatch = false; return localParent; } if ( localParent->childRidMap.contains( collection.remoteId() ) ) { if ( exactMatch ) *exactMatch = true; return localParent->childRidMap.value( collection.remoteId() ); } if ( exactMatch ) *exactMatch = false; return localParent; } /** Checks the pending remote nodes attached to the given local root node to see if any of them can be processed by now. If not, they are moved to the closest ancestor available. */ void processPendingRemoteNodes( LocalNode *localRoot ) { QList pendingRemoteNodes( localRoot->pendingRemoteNodes ); localRoot->pendingRemoteNodes.clear(); QHash > pendingCreations; foreach ( RemoteNode *remoteNode, pendingRemoteNodes ) { // step 1: see if we have a matching local node already LocalNode *localNode = findMatchingLocalNode( remoteNode->collection ); if ( localNode ) { Q_ASSERT( !localNode->processed ); - // TODO: moving when using global RIDs updateLocalCollection( localNode, remoteNode ); continue; } // step 2: check if we have the parent at least, then we can create it localNode = findMatchingLocalNode( remoteNode->collection.parentCollection() ); if ( localNode ) { pendingCreations[localNode].append( remoteNode ); continue; } // step 3: find the best matching ancestor and enqueue it for later processing localNode = findBestLocalAncestor( remoteNode->collection ); if ( !localNode ) { q->setError( Unknown ); q->setErrorText( QLatin1String( "Remote collection without root-terminated ancestor chain provided, fix your resource dude!" ) ); q->emitResult(); return; } localNode->pendingRemoteNodes.append( remoteNode ); } // process the now possible collection creations for ( QHash >::const_iterator it = pendingCreations.constBegin(); it != pendingCreations.constEnd(); ++it ) { createLocalCollections( it.key(), it.value() ); } } /** Performs a local update for the given node pair. */ void updateLocalCollection( LocalNode *localNode, RemoteNode *remoteNode ) { ++pendingJobs; Collection upd( remoteNode->collection ); upd.setId( localNode->collection.id() ); CollectionModifyJob *mod = new CollectionModifyJob( upd, q ); - mod->setProperty( REMOTE_NODE, QVariant::fromValue( remoteNode ) ); connect( mod, SIGNAL(result(KJob*)), q, SLOT(updateLocalCollectionResult(KJob*)) ); + + // detecting moves is only possible with global RIDs + if ( !hierarchicalRIDs ) { + LocalNode *oldParent = localUidMap.value( localNode->collection.parentCollection().id() ); + LocalNode *newParent = findMatchingLocalNode( remoteNode->collection.parentCollection() ); + if ( oldParent != newParent ) { + ++pendingJobs; + CollectionMoveJob *move = new CollectionMoveJob( upd, newParent->collection, q ); + connect( move, SIGNAL(result(KJob*)), q, SLOT(updateLocalCollectionResult(KJob*)) ); + } + } + localNode->processed = true; + delete remoteNode; } void updateLocalCollectionResult( KJob* job ) { --pendingJobs; if ( job->error() ) return; // handled by the base class - RemoteNode* remoteNode = job->property( REMOTE_NODE ).value(); - delete remoteNode; + if ( qobject_cast( job ) ) + ++progress; checkDone(); } /** Creates local folders for the given local parent and remote nodes. @todo group CollectionCreateJobs into a single one once it supports that */ void createLocalCollections( LocalNode* localParent, QList remoteNodes ) { foreach ( RemoteNode *remoteNode, remoteNodes ) { ++pendingJobs; Collection col( remoteNode->collection ); col.setParentCollection( localParent->collection ); CollectionCreateJob *create = new CollectionCreateJob( col, q ); create->setProperty( LOCAL_NODE, QVariant::fromValue( localParent ) ); create->setProperty( REMOTE_NODE, QVariant::fromValue( remoteNode ) ); connect( create, SIGNAL(result(KJob*)), q, SLOT(createLocalCollectionResult(KJob*)) ); } } void createLocalCollectionResult( KJob* job ) { --pendingJobs; if ( job->error() ) return; // handled by the base class const Collection newLocal = static_cast( job )->collection(); LocalNode* localNode = createLocalNode( newLocal ); localNode->processed = true; LocalNode* localParent = job->property( LOCAL_NODE ).value(); Q_ASSERT( localParent->childNodes.contains( localNode ) ); RemoteNode* remoteNode = job->property( REMOTE_NODE ).value(); delete remoteNode; + ++progress; processPendingRemoteNodes( localParent ); if ( !hierarchicalRIDs ) processPendingRemoteNodes( localRoot ); checkDone(); } /** Find all local nodes that are not marked as processed. */ Collection::List findUnprocessedLocalCollections( LocalNode *localNode ) { Collection::List rv; if ( !localNode->processed ) { rv.append( localNode->collection ); return rv; } foreach ( LocalNode *child, localNode->childNodes ) rv.append( findUnprocessedLocalCollections( child ) ); return rv; } /** Deletes unprocessed local nodes, in non-incremental mode. */ void deleteUnprocessedLocalNodes() { if ( incremental ) return; Collection::List cols = findUnprocessedLocalCollections( localRoot ); deleteLocalCollections( cols ); } /** Deletes the given collection list. @todo optimite delete job to support batch operations */ void deleteLocalCollections( const Collection::List &cols ) { + q->setTotalAmount( KJob::Bytes, q->totalAmount( KJob::Bytes ) + cols.size() ); foreach ( const Collection &col, cols ) { ++pendingJobs; CollectionDeleteJob *job = new CollectionDeleteJob( col, q ); connect( job, SIGNAL(result(KJob*)), q, SLOT(deleteLocalCollectionsResult(KJob*)) ); } } void deleteLocalCollectionsResult( KJob *job ) { - if ( job->error() ) - return; // handled by the base class --pendingJobs; + if ( job->error() ) + return; // handled by the base class + ++progress; checkDone(); } /** Process what's currently available. */ void execute() { if ( !localListDone ) return; processPendingRemoteNodes( localRoot ); if ( !incremental && deliveryDone ) deleteUnprocessedLocalNodes(); if ( !hierarchicalRIDs ) { deleteLocalCollections( removedRemoteCollections ); } else { Collection::List localCols; foreach ( const Collection &c, removedRemoteCollections ) { LocalNode *node = findMatchingLocalNode( c ); if ( node ) localCols.append( node->collection ); } deleteLocalCollections( localCols ); } removedRemoteCollections.clear(); checkDone(); } /** Finds pending remote nodes, which at the end of the day should be an empty set. */ QList findPendingRemoteNodes( LocalNode *localNode ) { QList rv; rv.append( localNode->pendingRemoteNodes ); foreach ( LocalNode *child, localNode->childNodes ) rv.append( findPendingRemoteNodes( child ) ); return rv; } /** Are we there yet?? @todo progress reporting */ void checkDone() { + q->setProcessedAmount( KJob::Bytes, progress ); + // still running jobs or not fully delivered local/remote state if ( !deliveryDone || pendingJobs > 0 || !localListDone ) return; // safety check: there must be no pending remote nodes anymore QList orphans = findPendingRemoteNodes( localRoot ); if ( !orphans.isEmpty() ) { q->setError( Unknown ); q->setErrorText( QLatin1String( "Found unresolved orphan collections" ) ); foreach ( RemoteNode* orphan, orphans ) kDebug() << "found orphan collection:" << orphan->collection; + q->emitResult(); + return; } q->commit(); } CollectionSync *q; QString resourceId; int pendingJobs; + int progress; LocalNode* localRoot; QHash localUidMap; QHash localRidMap; // temporary during build-up of the local node tree, must be empty afterwards QHash > localPendingCollections; // removed remote collections in incremental mode Collection::List removedRemoteCollections; bool incremental; bool streaming; bool hierarchicalRIDs; bool localListDone; bool deliveryDone; }; CollectionSync::CollectionSync( const QString &resourceId, QObject *parent ) : TransactionSequence( parent ), d( new Private( this ) ) { d->resourceId = resourceId; + setTotalAmount( KJob::Bytes, 0 ); } CollectionSync::~CollectionSync() { delete d; } void CollectionSync::setRemoteCollections(const Collection::List & remoteCollections) { + setTotalAmount( KJob::Bytes, totalAmount( KJob::Bytes ) + remoteCollections.count() ); foreach ( const Collection &c, remoteCollections ) d->createRemoteNode( c ); if ( !d->streaming ) d->deliveryDone = true; d->execute(); } void CollectionSync::setRemoteCollections(const Collection::List & changedCollections, const Collection::List & removedCollections) { + setTotalAmount( KJob::Bytes, totalAmount( KJob::Bytes ) + changedCollections.count() ); d->incremental = true; foreach ( const Collection &c, changedCollections ) d->createRemoteNode( c ); d->removedRemoteCollections += removedCollections; if ( !d->streaming ) d->deliveryDone = true; d->execute(); } void CollectionSync::doStart() { CollectionFetchJob *job = new CollectionFetchJob( Collection::root(), CollectionFetchJob::Recursive, this ); job->fetchScope().setResource( d->resourceId ); connect( job, SIGNAL(collectionsReceived(Akonadi::Collection::List)), SLOT(localCollectionsReceived(Akonadi::Collection::List)) ); connect( job, SIGNAL(result(KJob*)), SLOT(localCollectionFetchResult(KJob*)) ); } void CollectionSync::setStreamingEnabled( bool streaming ) { d->streaming = streaming; } void CollectionSync::retrievalDone() { d->deliveryDone = true; d->execute(); } void CollectionSync::setHierarchicalRemoteIds( bool hierarchical ) { d->hierarchicalRIDs = hierarchical; } #include "collectionsync_p.moc" diff --git a/akonadi/expungejob.cpp b/akonadi/entitycache.cpp similarity index 62% rename from akonadi/expungejob.cpp rename to akonadi/entitycache.cpp index b5a642ff5..892e325ac 100644 --- a/akonadi/expungejob.cpp +++ b/akonadi/entitycache.cpp @@ -1,49 +1,28 @@ /* - Copyright (c) 2006 Volker Krause + Copyright (c) 2009 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#include "expungejob_p.h" - -#include "job_p.h" +#include "entitycache_p.h" using namespace Akonadi; -class Akonadi::ExpungeJobPrivate : public JobPrivate -{ - public: - ExpungeJobPrivate( ExpungeJob * parent ) - : JobPrivate( parent ) - { - } -}; - -ExpungeJob::ExpungeJob(QObject * parent) - : Job( new ExpungeJobPrivate( this ), parent ) -{ -} - -ExpungeJob::~ExpungeJob() -{ -} - -void ExpungeJob::doStart() +EntityCacheBase::EntityCacheBase( QObject *parent ) : QObject( parent ) { - d_ptr->writeData( d_ptr->newTag() + " EXPUNGE\n" ); } -#include "expungejob_p.moc" +#include "entitycache_p.moc" diff --git a/akonadi/entitycache_p.h b/akonadi/entitycache_p.h new file mode 100644 index 000000000..961168473 --- /dev/null +++ b/akonadi/entitycache_p.h @@ -0,0 +1,213 @@ +/* + Copyright (c) 2009 Volker Krause + + This library is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your + option) any later version. + + This library 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 Library General Public + License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + 02110-1301, USA. +*/ + +#ifndef AKONADI_ENTITYCACHE_P_H +#define AKONADI_ENTITYCACHE_P_H + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +class KJob; + +namespace Akonadi { + +/** + @internal + QObject part of EntityCache. +*/ +class EntityCacheBase : public QObject +{ + Q_OBJECT + public: + explicit EntityCacheBase (QObject * parent = 0); + + signals: + void dataAvailable(); + + private slots: + virtual void fetchResult( KJob* job ) = 0; +}; + +template +struct EntityCacheNode +{ + EntityCacheNode() : pending( false ), invalid( false ) {} + EntityCacheNode( typename T::Id id ) : entity( T( id ) ), pending( true ), invalid( false ) {} + T entity; + bool pending; + bool invalid; +}; + +} + +Q_DECLARE_METATYPE( Akonadi::EntityCacheNode* ) +Q_DECLARE_METATYPE( Akonadi::EntityCacheNode* ) + +namespace Akonadi { + +/** + * @internal + * A in-memory FIFO cache for a small amount of Entity objects. + */ +template +class EntityCache : public EntityCacheBase +{ + public: + explicit EntityCache( int maxCapacity, QObject *parent = 0 ) : + EntityCacheBase( parent ), + mCapacity( maxCapacity ) + {} + + ~EntityCache() + { + qDeleteAll( mCache ); + } + + /** Object is available in the cache and can be retrieved. */ + bool isCached( typename T::Id id ) const + { + EntityCacheNode* node = cacheNodeForId( id ); + return node && !node->pending; + } + + /** Object has been requested but is not yet loaded into the cache or is already available. */ + bool isRequested( typename T::Id id ) const + { + return cacheNodeForId( id ); + } + + T retrieve( typename T::Id id ) const + { + Q_ASSERT( isCached( id ) ); + EntityCacheNode* node = cacheNodeForId( id ); + if ( !node->invalid ) + return node->entity; + return T(); + } + + void invalidate( typename T::Id id ) + { + EntityCacheNode* node = cacheNodeForId( id ); + if ( node ) + node->invalid = true; + } + + /** + Asks the cache to retrieve @p id. @p request is used as + a token to indicate which request has been finished in the + dataAvailable() signal. + */ + void request( typename T::Id id, const FetchScope &scope ) + { + Q_ASSERT( !isRequested( id ) ); + shrinkCache(); + EntityCacheNode *node = new EntityCacheNode( id ); + FetchJob* job = createFetchJob( id ); + job->setFetchScope( scope ); + job->setProperty( "EntityCacheNode", QVariant::fromValue*>( node ) ); + connect( job, SIGNAL(result(KJob*)), SLOT(fetchResult(KJob*)) ); + mCache.enqueue( node ); + } + + private: + EntityCacheNode* cacheNodeForId( typename T::Id id ) const + { + for ( typename QQueue*>::const_iterator it = mCache.constBegin(), endIt = mCache.constEnd(); + it != endIt; ++it ) + { + if ( (*it)->entity.id() == id ) + return *it; + } + return 0; + } + + void fetchResult( KJob* job ) + { + EntityCacheNode *node = job->property( "EntityCacheNode" ).value*>(); + Q_ASSERT( node ); + typename T::Id id = node->entity.id(); + node->pending = false; + extractResult( node, job ); + if ( node->entity.id() != id ) { // make sure we find this node again if something went wrong here... + kWarning() << "Something went very wrong..."; + node->entity.setId( id ); + node->invalid = true; + } + emit dataAvailable(); + } + + void extractResult( EntityCacheNode* node, KJob* job ) const; + + inline FetchJob* createFetchJob( typename T::Id id ) + { + return new FetchJob( T( id ), this ); + } + + /** Tries to reduce the cache size until at least one more object fits in. */ + void shrinkCache() + { + while ( mCache.size() >= mCapacity && !mCache.first()->pending ) + delete mCache.dequeue(); + } + + private: + QQueue*> mCache; + int mCapacity; +}; + +template<> inline void EntityCache::extractResult( EntityCacheNode* node, KJob *job ) const +{ + CollectionFetchJob* fetch = qobject_cast( job ); + Q_ASSERT( fetch ); + if ( fetch->collections().isEmpty() ) + node->entity = Collection(); + else + node->entity = fetch->collections().first(); +} + +template<> inline void EntityCache::extractResult( EntityCacheNode* node, KJob *job ) const +{ + ItemFetchJob* fetch = qobject_cast( job ); + Q_ASSERT( fetch ); + if ( fetch->items().isEmpty() ) + node->entity = Item(); + else + node->entity = fetch->items().first(); +} + +template<> inline CollectionFetchJob* EntityCache::createFetchJob( Collection::Id id ) +{ + return new CollectionFetchJob( Collection( id ), CollectionFetchJob::Base, this ); +} + +typedef EntityCache CollectionCache; +typedef EntityCache ItemCache; + +} + +#endif diff --git a/akonadi/entityhiddenattribute.cpp b/akonadi/entityhiddenattribute.cpp index ea7da259d..6b7868235 100644 --- a/akonadi/entityhiddenattribute.cpp +++ b/akonadi/entityhiddenattribute.cpp @@ -1,61 +1,55 @@ /****************************************************************************** - * - * File : entityhiddenattribute.cpp - * Creation date : Sat 1 Aug 2009 02:07:50 * * Copyright (c) 2009 Szymon Stefanek * * This library is free software; you can redistribute it and/or modify it * under the terms of the GNU Library General Public License as published by * the Free Software Foundation; either version 2 of the License, or (at your * option) any later version. * * This library 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 Library General Public * License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA, 02110-1301, USA. * *****************************************************************************/ #include "entityhiddenattribute.h" #include -namespace Akonadi -{ +using namespace Akonadi; EntityHiddenAttribute::EntityHiddenAttribute() + : d( 0 ) { } EntityHiddenAttribute::~EntityHiddenAttribute() { } QByteArray Akonadi::EntityHiddenAttribute::type() const { return "HIDDEN"; } EntityHiddenAttribute * EntityHiddenAttribute::clone() const { return new EntityHiddenAttribute(); } QByteArray EntityHiddenAttribute::serialized() const { return QByteArray(); } void EntityHiddenAttribute::deserialize( const QByteArray &data ) { Q_ASSERT( data.isEmpty() ); } - -} // namespace Akonadi - diff --git a/akonadi/entityhiddenattribute.h b/akonadi/entityhiddenattribute.h index eb941c8ed..96bcf7e8f 100644 --- a/akonadi/entityhiddenattribute.h +++ b/akonadi/entityhiddenattribute.h @@ -1,85 +1,83 @@ /****************************************************************************** - * - * File : entityhiddenattribute.h - * Creation date : Sat 1 Aug 2009 02:07:50 * * Copyright (c) 2009 Szymon Stefanek * * This library is free software; you can redistribute it and/or modify it * under the terms of the GNU Library General Public License as published by * the Free Software Foundation; either version 2 of the License, or (at your * option) any later version. * * This library 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 Library General Public * License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA, 02110-1301, USA. * *****************************************************************************/ -#ifndef _ENTITYHIDDENATTRIBUTE_H_ -#define _ENTITYHIDDENATTRIBUTE_H_ +#ifndef AKONADI_ENTITYHIDDENATTRIBUTE_H +#define AKONADI_ENTITYHIDDENATTRIBUTE_H #include - namespace Akonadi { /** - * @short Attribute signaling that an entity should be hidden in the UI + * @short Attribute signalling that an entity should be hidden in the UI * - * This class rappresents the attribute of all hidden items. The hidden + * This class represents the attribute of all hidden items. The hidden * items shouldn't be displayed in UI applications (unless in some kind * of "debug" mode). * * @seealso Attribute * * @author Szymon Stefanek * @since 4.4 */ class AKONADI_EXPORT EntityHiddenAttribute : public Attribute { public: - /** * Creates a new entity hidden attribute. */ EntityHiddenAttribute(); /** * Destroys the entity hidden attribute. */ ~EntityHiddenAttribute(); - public: - /** * Reimplemented from Attribute */ QByteArray type() const; /** * Reimplemented from Attribute */ - EntityHiddenAttribute * clone() const; + EntityHiddenAttribute* clone() const; /** * Reimplemented from Attribute */ QByteArray serialized() const; /** * Reimplemented from Attribute */ void deserialize( const QByteArray &data ); -}; // EntityHiddenAttribute + private: + //@cond PRIVATE + class Private; + Private* const d; + //@endcond +}; -} // namespace Akonadi +} -#endif //!_ENTITYHIDDENATTRIBUTE_H_ +#endif diff --git a/akonadi/expungejob_p.h b/akonadi/expungejob_p.h deleted file mode 100644 index 309b5e119..000000000 --- a/akonadi/expungejob_p.h +++ /dev/null @@ -1,59 +0,0 @@ -/* - Copyright (c) 2006 Volker Krause - - This library is free software; you can redistribute it and/or modify it - under the terms of the GNU Library General Public License as published by - the Free Software Foundation; either version 2 of the License, or (at your - option) any later version. - - This library 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 Library General Public - License for more details. - - You should have received a copy of the GNU Library General Public License - along with this library; see the file COPYING.LIB. If not, write to the - Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA - 02110-1301, USA. -*/ - -#ifndef AKONADI_EXPUNGEJOB_P_H -#define AKONADI_EXPUNGEJOB_P_H - -#include "job.h" - -namespace Akonadi { - -class ExpungeJobPrivate; - -/** - * @internal - * - * Permanently removes all items marked for deletion. - */ -class ExpungeJob : public Job -{ - Q_OBJECT - - public: - /** - Creates a new ExpungeJob. - @param parent The parent object. - */ - explicit ExpungeJob( QObject *parent = 0 ); - - /** - Destroys this job. - */ - virtual ~ExpungeJob(); - - protected: - virtual void doStart(); - - private: - Q_DECLARE_PRIVATE( ExpungeJob ) -}; - -} - -#endif diff --git a/akonadi/itemfetchjob.cpp b/akonadi/itemfetchjob.cpp index 5ebdb9ea2..36e6bbe35 100644 --- a/akonadi/itemfetchjob.cpp +++ b/akonadi/itemfetchjob.cpp @@ -1,334 +1,341 @@ /* Copyright (c) 2006 - 2007 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "itemfetchjob.h" #include "attributefactory.h" #include "collection.h" #include "collectionselectjob_p.h" #include "imapparser_p.h" #include "itemfetchscope.h" #include "itemserializer_p.h" #include "itemserializerplugin.h" #include "job_p.h" #include "entity_p.h" #include "protocol_p.h" #include "protocolhelper_p.h" #include #include #include #include #include using namespace Akonadi; class Akonadi::ItemFetchJobPrivate : public JobPrivate { public: ItemFetchJobPrivate( ItemFetchJob *parent ) : JobPrivate( parent ) { } void timeout() { Q_Q( ItemFetchJob ); mEmitTimer->stop(); // in case we are called by result() if ( !mPendingItems.isEmpty() ) { emit q->itemsReceived( mPendingItems ); mPendingItems.clear(); } } void startFetchJob(); void selectDone( KJob * job ); Q_DECLARE_PUBLIC( ItemFetchJob ) Collection mCollection; Item mItem; Item::List mItems; ItemFetchScope mFetchScope; Item::List mPendingItems; // items pending for emitting itemsReceived() QTimer* mEmitTimer; }; void ItemFetchJobPrivate::startFetchJob() { QByteArray command = newTag(); if ( mItem.isValid() ) command += " " AKONADI_CMD_UID " " AKONADI_CMD_ITEMFETCH " " + QByteArray::number( mItem.id() ); else if ( !mItem.remoteId().isEmpty() ) command += " " AKONADI_CMD_RID " " AKONADI_CMD_ITEMFETCH " " + mItem.remoteId().toUtf8(); else command += " " AKONADI_CMD_ITEMFETCH " 1:*"; if ( mFetchScope.fullPayload() ) command += " " AKONADI_PARAM_FULLPAYLOAD; if ( mFetchScope.allAttributes() ) command += " " AKONADI_PARAM_ALLATTRIBUTES; if ( mFetchScope.cacheOnly() ) command += " " AKONADI_PARAM_CACHEONLY; //TODO: detect somehow if server supports external payload attribute command += " " AKONADI_PARAM_EXTERNALPAYLOAD; command += " (UID REMOTEID COLLECTIONID FLAGS SIZE DATETIME"; foreach ( const QByteArray &part, mFetchScope.payloadParts() ) command += ' ' + ProtocolHelper::encodePartIdentifier( ProtocolHelper::PartPayload, part ); foreach ( const QByteArray &part, mFetchScope.attributes() ) command += ' ' + ProtocolHelper::encodePartIdentifier( ProtocolHelper::PartAttribute, part ); command += ")\n"; writeData( command ); } void ItemFetchJobPrivate::selectDone( KJob * job ) { if ( !job->error() ) // the collection is now selected, fetch the message(s) startFetchJob(); } ItemFetchJob::ItemFetchJob( const Collection &collection, QObject * parent ) : Job( new ItemFetchJobPrivate( this ), parent ) { Q_D( ItemFetchJob ); d->mEmitTimer = new QTimer( this ); d->mEmitTimer->setSingleShot( true ); d->mEmitTimer->setInterval( 100 ); connect( d->mEmitTimer, SIGNAL(timeout()), this, SLOT(timeout()) ); connect( this, SIGNAL(result(KJob*)), this, SLOT(timeout()) ); d->mCollection = collection; } ItemFetchJob::ItemFetchJob( const Item & item, QObject * parent) : Job( new ItemFetchJobPrivate( this ), parent ) { Q_D( ItemFetchJob ); d->mEmitTimer = new QTimer( this ); d->mEmitTimer->setSingleShot( true ); d->mEmitTimer->setInterval( 100 ); connect( d->mEmitTimer, SIGNAL(timeout()), this, SLOT(timeout()) ); connect( this, SIGNAL(result(KJob*)), this, SLOT(timeout()) ); d->mCollection = Collection::root(); d->mItem = item; } ItemFetchJob::~ItemFetchJob() { } void ItemFetchJob::doStart() { Q_D( ItemFetchJob ); if ( !d->mItem.isValid() ) { // collection content listing if ( d->mCollection == Collection::root() ) { setErrorText( QLatin1String("Cannot list root collection.") ); setError( Unknown ); emitResult(); } CollectionSelectJob *job = new CollectionSelectJob( d->mCollection, this ); connect( job, SIGNAL(result(KJob*)), SLOT(selectDone(KJob*)) ); addSubjob( job ); } else d->startFetchJob(); } void ItemFetchJob::doHandleResponse( const QByteArray & tag, const QByteArray & data ) { Q_D( ItemFetchJob ); if ( tag == "*" ) { int begin = data.indexOf( "FETCH" ); if ( begin >= 0 ) { // split fetch response into key/value pairs QList fetchResponse; ImapParser::parseParenthesizedList( data, fetchResponse, begin + 6 ); // create a new item object Item::Id uid = -1; int rev = -1; QString rid; QString mimeType; Entity::Id cid = -1; for ( int i = 0; i < fetchResponse.count() - 1; i += 2 ) { const QByteArray key = fetchResponse.value( i ); const QByteArray value = fetchResponse.value( i + 1 ); if ( key == "UID" ) uid = value.toLongLong(); else if ( key == "REV" ) rev = value.toInt(); else if ( key == "REMOTEID" ) { if ( !value.isEmpty() ) rid = QString::fromUtf8( value ); else rid.clear(); } else if ( key == "COLLECTIONID" ) { cid = value.toInt(); } else if ( key == "MIMETYPE" ) mimeType = QString::fromLatin1( value ); } if ( uid < 0 || rev < 0 || mimeType.isEmpty() ) { kWarning() << "Broken fetch response: UID, RID, REV or MIMETYPE missing!"; return; } Item item( uid ); item.setRemoteId( rid ); item.setRevision( rev ); item.setMimeType( mimeType ); item.setStorageCollectionId( cid ); if ( !item.isValid() ) return; // parse fetch response fields for ( int i = 0; i < fetchResponse.count() - 1; i += 2 ) { const QByteArray key = fetchResponse.value( i ); // skip stuff we dealt with already if ( key == "UID" || key == "REV" || key == "REMOTEID" || key == "MIMETYPE" || key == "COLLECTIONID") continue; // flags if ( key == "FLAGS" ) { QList flags; ImapParser::parseParenthesizedList( fetchResponse[i + 1], flags ); foreach ( const QByteArray &flag, flags ) { item.setFlag( flag ); } } else if ( key == "SIZE" ) { const quint64 size = fetchResponse[i + 1].toLongLong(); item.setSize( size ); } else if ( key == "DATETIME" ) { QDateTime datetime; ImapParser::parseDateTime( fetchResponse[i + 1], datetime ); item.setModificationTime( datetime ); } else { int version = 0; QByteArray plainKey( key ); ProtocolHelper::PartNamespace ns; ImapParser::splitVersionedKey( key, plainKey, version ); plainKey = ProtocolHelper::decodePartIdentifier( plainKey, ns ); switch ( ns ) { case ProtocolHelper::PartPayload: { bool isExternal = false; QByteArray fileKey = fetchResponse.value( i + 1 ); if (fileKey == "[FILE]") { isExternal = true; i++; kDebug() << "Payload is external: " << isExternal << " filename: " << fetchResponse.value( i + 1 ); } ItemSerializer::deserialize( item, plainKey, fetchResponse.value( i + 1 ), version, isExternal ); break; } case ProtocolHelper::PartAttribute: { Attribute* attr = AttributeFactory::createAttribute( plainKey ); Q_ASSERT( attr ); if ( fetchResponse.value( i + 1 ) == "[FILE]" ) { ++i; QFile f( QString::fromUtf8( fetchResponse.value( i + 1 ) ) ); if ( f.open( QFile::ReadOnly ) ) attr->deserialize( f.readAll() ); else { kWarning() << "Failed to open attribute file: " << fetchResponse.value( i + 1 ); delete attr; } } else { attr->deserialize( fetchResponse.value( i + 1 ) ); } item.addAttribute( attr ); break; } case ProtocolHelper::PartGlobal: default: kWarning() << "Unknown item part type:" << key; } } } item.d_ptr->resetChangeLog(); d->mItems.append( item ); d->mPendingItems.append( item ); if ( !d->mEmitTimer->isActive() ) d->mEmitTimer->start(); return; } } kDebug() << "Unhandled response: " << tag << data; } Item::List ItemFetchJob::items() const { Q_D( const ItemFetchJob ); return d->mItems; } void ItemFetchJob::setFetchScope( ItemFetchScope &fetchScope ) { Q_D( ItemFetchJob ); d->mFetchScope = fetchScope; } +void ItemFetchJob::setFetchScope( const ItemFetchScope &fetchScope ) +{ + Q_D( ItemFetchJob ); + + d->mFetchScope = fetchScope; +} + ItemFetchScope &ItemFetchJob::fetchScope() { Q_D( ItemFetchJob ); return d->mFetchScope; } Item ItemFetchJob::item() const { Q_D( const ItemFetchJob ); return d->mItem; } Collection ItemFetchJob::collection() const { Q_D( const ItemFetchJob ); return d->mCollection; } void ItemFetchJob::setCollection(const Akonadi::Collection& collection) { Q_D( ItemFetchJob ); d->mCollection = collection; } #include "itemfetchjob.moc" diff --git a/akonadi/itemfetchjob.h b/akonadi/itemfetchjob.h index 01cbd5920..275b85f63 100644 --- a/akonadi/itemfetchjob.h +++ b/akonadi/itemfetchjob.h @@ -1,170 +1,183 @@ /* Copyright (c) 2006 - 2007 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef AKONADI_ITEMFETCHJOB_H #define AKONADI_ITEMFETCHJOB_H #include #include namespace Akonadi { class Collection; class ItemFetchJobPrivate; class ItemFetchScope; /** * @short Job that fetches items from the Akonadi storage. * * This class is used to fetch items from the Akonadi storage. * Which parts of the items (e.g. headers only, attachments or all) * can be specified by the ItemFetchScope. * * Example: * * @code * * // Fetch all items with full payload from the root collection * Akonadi::ItemFetchJob *job = new Akonadi::ItemFetchJob( Akonadi::Collection::root() ); * job->fetchScope().fetchFullPayload(); * * if ( job->exec() ) { * Akonadi::Item::List items = job->items(); * foreach( const Akonadi::Item &item, items ) { * qDebug() << "Item ID:" << item.id(); * } * } else { * qDebug() << "Error occurred"; * } * * @endcode * * @author Volker Krause */ class AKONADI_EXPORT ItemFetchJob : public Job { Q_OBJECT public: /** * Creates a new item fetch job that retrieves all items inside the given collection. * * @param collection The parent collection to fetch all items from. * @param parent The parent object. */ explicit ItemFetchJob( const Collection &collection, QObject *parent = 0 ); /** * Creates a new item fetch job that retrieves the specified item. * If the item has an uid set, this is used to identify the item on the Akonadi * server. If only a remote identifier is available, that one is used. * However, as remote identifier are not necessarily globally unique, you * need to specify the resource and/or collection to search in in that case, * using setCollection() or Akonadi::ResourceSelectJob. * * @param item The item to fetch. * @param parent The parent object. */ explicit ItemFetchJob( const Item &item, QObject *parent = 0 ); /** * Destroys the item fetch job. */ virtual ~ItemFetchJob(); /** * Returns the fetched item. * * @note The items are invalid before the result( KJob* ) * signal has been emitted or if an error occurred. */ Item::List items() const; /** * Sets the item fetch scope. * * The ItemFetchScope controls how much of an item's data is fetched * from the server, e.g. whether to fetch the full item payload or * only meta data. * * @param fetchScope The new scope for item fetch operations. * * @see fetchScope() */ - void setFetchScope( ItemFetchScope &fetchScope ); + void setFetchScope( ItemFetchScope &fetchScope ); // KDE5: remove + + /** + * Sets the item fetch scope. + * + * The ItemFetchScope controls how much of an item's data is fetched + * from the server, e.g. whether to fetch the full item payload or + * only meta data. + * + * @param fetchScope The new scope for item fetch operations. + * + * @see fetchScope() + */ + void setFetchScope( const ItemFetchScope &fetchScope ); /** * Returns the item fetch scope. * * Since this returns a reference it can be used to conveniently modify the * current scope in-place, i.e. by calling a method on the returned reference * without storing it in a local variable. See the ItemFetchScope documentation * for an example. * * @return a reference to the current item fetch scope * * @see setFetchScope() for replacing the current item fetch scope */ ItemFetchScope &fetchScope(); /** * Returns the item passed in on in the constructor or an invalid * item if another constructor, e.g. those with the collection, * was called. */ Item item() const; /** * Returns the collection passed in on in the constructor or set with the * setCollection method. */ Collection collection() const; /** * Specifies the collection the item is in. * This is only required when retrieving an item based on its remote id which might not be * unique globally. * * @see Akonadi::ResourceSelectJob */ void setCollection( const Collection &collection ); Q_SIGNALS: /** * This signal is emitted when the items are fetched completely. * * @param items The fetched items. */ void itemsReceived( const Akonadi::Item::List &items ); protected: virtual void doStart(); virtual void doHandleResponse( const QByteArray &tag, const QByteArray &data ); private: Q_DECLARE_PRIVATE( ItemFetchJob ) //@cond PRIVATE Q_PRIVATE_SLOT( d_func(), void selectDone( KJob* ) ) Q_PRIVATE_SLOT( d_func(), void timeout() ) //@endcond }; } #endif diff --git a/akonadi/monitor.cpp b/akonadi/monitor.cpp index 2d38680c7..1396b4f44 100644 --- a/akonadi/monitor.cpp +++ b/akonadi/monitor.cpp @@ -1,159 +1,170 @@ /* Copyright (c) 2006 - 2007 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "monitor.h" #include "monitor_p.h" #include "itemfetchjob.h" #include "notificationmessage_p.h" #include "session.h" #include #include #include #include #include +#include "collectionfetchscope.h" using namespace Akonadi; #define d d_ptr Monitor::Monitor( QObject *parent ) : QObject( parent ), d_ptr( new MonitorPrivate( this ) ) { d_ptr->connectToNotificationManager(); } //@cond PRIVATE Monitor::Monitor(MonitorPrivate * d, QObject *parent) : QObject( parent ), d_ptr( d ) { } //@endcond Monitor::~Monitor() { delete d; } void Monitor::setCollectionMonitored( const Collection &collection, bool monitored ) { if ( monitored ) d->collections << collection; else d->collections.removeAll( collection ); emit collectionMonitored( collection, monitored ); } void Monitor::setItemMonitored( const Item & item, bool monitored ) { if ( monitored ) d->items.insert( item.id() ); else d->items.remove( item.id() ); emit itemMonitored( item, monitored ); } void Monitor::setResourceMonitored( const QByteArray & resource, bool monitored ) { if ( monitored ) d->resources.insert( resource ); else d->resources.remove( resource ); emit resourceMonitored( resource, monitored ); } void Monitor::setMimeTypeMonitored( const QString & mimetype, bool monitored ) { if ( monitored ) d->mimetypes.insert( mimetype ); else d->mimetypes.remove( mimetype ); emit mimeTypeMonitored( mimetype, monitored ); } void Akonadi::Monitor::setAllMonitored( bool monitored ) { d->monitorAll = monitored; emit allMonitored( monitored ); } void Monitor::ignoreSession(Session * session) { d->sessions << session->sessionId(); connect( session, SIGNAL(destroyed(QObject*)), this, SLOT(slotSessionDestroyed(QObject*)) ); } void Monitor::fetchCollection(bool enable) { d->fetchCollection = enable; } void Monitor::fetchCollectionStatistics(bool enable) { d->fetchCollectionStatistics = enable; } void Monitor::setItemFetchScope( const ItemFetchScope &fetchScope ) { d->mItemFetchScope = fetchScope; } ItemFetchScope &Monitor::itemFetchScope() { return d->mItemFetchScope; } +void Monitor::setCollectionFetchScope( const CollectionFetchScope &fetchScope ) +{ + d->mCollectionFetchScope = fetchScope; +} + +CollectionFetchScope& Monitor::collectionFetchScope() +{ + return d->mCollectionFetchScope; +} + Collection::List Monitor::collectionsMonitored() const { return d->collections; } QList Monitor::itemsMonitored() const { return d->items.toList(); } QStringList Monitor::mimeTypesMonitored() const { return d->mimetypes.toList(); } QList Monitor::resourcesMonitored() const { return d->resources.toList(); } bool Monitor::isAllMonitored() const { return d->monitorAll; } #undef d #include "monitor.moc" diff --git a/akonadi/monitor.h b/akonadi/monitor.h index 70d7311ca..45d045328 100644 --- a/akonadi/monitor.h +++ b/akonadi/monitor.h @@ -1,348 +1,378 @@ /* Copyright (c) 2006 - 2007 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef AKONADI_MONITOR_H #define AKONADI_MONITOR_H #include #include #include namespace Akonadi { +class CollectionFetchScope; class CollectionStatistics; class Item; class ItemFetchScope; class MonitorPrivate; class Session; /** * @short Monitors an item or collection for changes. * * The Monitor emits signals if some of these objects are changed or * removed or new ones are added to the Akonadi storage. * * Optionally, the changed objects can be fetched automatically from the server. - * To enable this, see fetchCollection(), fetchItemMetaData(), fetchItemData(). + * To enable this, see itemFetchScope() and collectionFetchScope(). * * @todo: distinguish between monitoring collection properties and collection content. * @todo: special case for collection content counts changed * * @author Volker Krause */ class AKONADI_EXPORT Monitor : public QObject { Q_OBJECT public: /** * Creates a new monitor. * * @param parent The parent object. */ explicit Monitor( QObject *parent = 0 ); /** * Destroys the monitor. */ virtual ~Monitor(); /** * Sets whether the specified collection shall be monitored for changes. * * @param collection The collection to monitor. * If this collection is Collection::root(), all collections * in the Akonadi storage will be monitored. */ void setCollectionMonitored( const Collection &collection, bool monitored = true ); /** * Sets whether the specified item shall be monitored for changes. * * @param item The item to monitor. */ void setItemMonitored( const Item &item, bool monitored = true ); /** * Sets whether the specified resource shall be monitored for changes. * * @param resource The resource identifier. */ void setResourceMonitored( const QByteArray &resource, bool monitored = true ); /** * Sets whether objects of the specified mime type shall be monitored for changes. * * @param mimetype The mime type to monitor. */ void setMimeTypeMonitored( const QString &mimetype, bool monitored = true ); /** * Sets whether all items shall be monitored. */ void setAllMonitored( bool monitored = true ); /** * Ignores all change notifications caused by the given session. * * @param session The session you want to ignore. */ void ignoreSession( Session *session ); /** * Enables automatic fetching of changed collections from the Akonadi storage. * * @param enable @c true enables automatic fetching, @c false disables automatic fetching. */ void fetchCollection( bool enable ); /** * Enables automatic fetching of changed collection statistics information from * the Akonadi storage. * * @param enable @c true to enables automatic fetching, @c false disables automatic fetching. */ void fetchCollectionStatistics( bool enable ); /** * Sets the item fetch scope. * * Controls how much of an item's data is fetched from the server, e.g. * whether to fetch the full item payload or only meta data. * * @param fetchScope The new scope for item fetch operations. * * @see itemFetchScope() */ void setItemFetchScope( const ItemFetchScope &fetchScope ); /** * Returns the item fetch scope. * * Since this returns a reference it can be used to conveniently modify the * current scope in-place, i.e. by calling a method on the returned reference * without storing it in a local variable. See the ItemFetchScope documentation * for an example. * * @return a reference to the current item fetch scope * * @see setItemFetchScope() for replacing the current item fetch scope */ ItemFetchScope &itemFetchScope(); + /** + * Sets the collection fetch scope. + * + * Controls which collections are monitored and how much of a collection's data + * is fetched from the server. + * + * @param fetchScope The new scope for collection fetch operations. + * + * @see collectionFetchScope() + * @since 4.4 + */ + void setCollectionFetchScope( const CollectionFetchScope &fetchScope ); + + /** + * Returns the collection fetch scope. + * + * Since this returns a reference it can be used to conveniently modify the + * current scope in-place, i.e. by calling a method on the returned reference + * without storing it in a local variable. See the CollectionFetchScope documentation + * for an example. + * + * @return a reference to the current collection fetch scope + * + * @see setCollectionFetchScope() for replacing the current collection fetch scope + * @since 4.4 + */ + CollectionFetchScope &collectionFetchScope(); + + /** * Returns the list of collections being monitored. * * @since 4.3 */ Collection::List collectionsMonitored() const; /** * Returns the set of items being monitored. * * @since 4.3 */ QList itemsMonitored() const; /** * Returns the set of mimetypes being monitored. * * @since 4.3 */ QStringList mimeTypesMonitored() const; /** * Returns the set of identifiers for resources being monitored. * * @since 4.3 */ QList resourcesMonitored() const; /** * Returns true if everything is being monitored. * * @since 4.3 */ bool isAllMonitored() const; Q_SIGNALS: /** * This signal is emitted if a monitored item has changed, e.g. item parts have been modified. * * @param item The changed item. * @param partIdentifiers The identifiers of the item parts that has been changed. */ void itemChanged( const Akonadi::Item &item, const QSet &partIdentifiers ); /** * This signal is emitted if a monitored item has been moved between two collections * * @param item The moved item. * @param collectionSource The collection the item has been moved from. * @param collectionDestination The collection the item has been moved to. */ void itemMoved( const Akonadi::Item &item, const Akonadi::Collection &collectionSource, const Akonadi::Collection &collectionDestination ); /** * This signal is emitted if an item has been added to a monitored collection in the Akonadi storage. * * @param item The new item. * @param collection The collection the item has been added to. */ void itemAdded( const Akonadi::Item &item, const Akonadi::Collection &collection ); /** * This signal is emitted if * - a monitored item has been removed from the Akonadi storage * or * - a item has been removed from a monitored collection. * * @param item The removed item. */ void itemRemoved( const Akonadi::Item &item ); //TODO remove if we are sure nobody uses it any longer void itemRemoved( const Akonadi::Item &item, const Akonadi::Collection &collection ); /** * This signal is emitted if a reference to an item is added to a virtual collection. * @param item The linked item. * @param collection The collection the item is linked to. * * @since 4.2 */ void itemLinked( const Akonadi::Item &item, const Akonadi::Collection &collection ); /** * This signal is emitted if a reference to an item is removed from a virtual collection. * @param item The unlinked item. * @param collection The collection the item is unlinked from. * * @since 4.2 */ void itemUnlinked( const Akonadi::Item &item, const Akonadi::Collection &collection ); /** * This signal is emitted if a new collection has been added to a monitored collection in the Akonadi storage. * * @param collection The new collection. * @param parent The parent collection. */ void collectionAdded( const Akonadi::Collection &collection, const Akonadi::Collection &parent ); /** * This signal is emitted if a monitored collection has been changed (properties or content). * * @param collection The changed collection. */ void collectionChanged( const Akonadi::Collection &collection ); /** * This signals is emitted if a monitored collection has been moved. * * @param collection The moved collection. * @param source The previous parent collection. * @param distination The new parent collection. */ void collectionMoved( const Akonadi::Collection &collection, const Akonadi::Collection &source, const Akonadi::Collection &destination ); /** * This signal is emitted if a monitored collection has been removed from the Akonadi storage. * * @param collection The removed collection. */ void collectionRemoved( const Akonadi::Collection &collection ); /** * This signal is emitted if the statistics information of a monitored collection * has changed. * * @param id The collection identifier of the changed collection. * @param statistics The updated collection statistics, invalid if automatic * fetching of statistics changes is disabled. */ void collectionStatisticsChanged( Akonadi::Collection::Id id, const Akonadi::CollectionStatistics &statistics ); /** * This signal is emitted if the Monitor starts or stops monitoring @p collection explicitly. * @param collection The collection * @param monitored Whether the collection is now being monitored or not. * * @since 4.3 */ void collectionMonitored( const Akonadi::Collection &collection, bool monitored ); /** * This signal is emitted if the Monitor starts or stops monitoring @p item explicitly. * @param item The item * @param monitored Whether the item is now being monitored or not. * * @since 4.3 */ void itemMonitored( const Akonadi::Item &item, bool monitored ); /** * This signal is emitted if the Monitor starts or stops monitoring the resource with the identifier @p identifier explicitly. * @param identifier The identifier of the resource. * @param monitored Whether the resource is now being monitored or not. * * @since 4.3 */ void resourceMonitored( const QByteArray &identifier, bool monitored ); /** * This signal is emitted if the Monitor starts or stops monitoring @p mimeType explicitly. * @param mimeType The mimeType. * @param monitored Whether the mimeType is now being monitored or not. * * @since 4.3 */ void mimeTypeMonitored( const QString &mimeType, bool monitored ); /** * This signal is emitted if the Monitor starts or stops monitoring everything. * @param monitored Whether everything is now being monitored or not. * * @since 4.3 */ void allMonitored( bool monitored ); protected: //@cond PRIVATE MonitorPrivate *d_ptr; explicit Monitor( MonitorPrivate *d, QObject *parent = 0 ); //@endcond private: Q_DECLARE_PRIVATE( Monitor ) //@cond PRIVATE Q_PRIVATE_SLOT( d_ptr, void slotSessionDestroyed( QObject* ) ) Q_PRIVATE_SLOT( d_ptr, void slotStatisticsChangedFinished( KJob* ) ) Q_PRIVATE_SLOT( d_ptr, void slotFlushRecentlyChangedCollections() ) Q_PRIVATE_SLOT( d_ptr, void slotNotify( const Akonadi::NotificationMessage::List& ) ) Q_PRIVATE_SLOT( d_ptr, void slotItemJobFinished( KJob* ) ) Q_PRIVATE_SLOT( d_ptr, void slotCollectionJobFinished( KJob* ) ) //@endcond }; } #endif diff --git a/akonadi/monitor_p.h b/akonadi/monitor_p.h index 9e8a31e23..3dfd9c47a 100644 --- a/akonadi/monitor_p.h +++ b/akonadi/monitor_p.h @@ -1,192 +1,194 @@ /* Copyright (c) 2007 Tobias Koenig This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef AKONADI_MONITOR_P_H #define AKONADI_MONITOR_P_H #include "monitor.h" #include "collection.h" #include "collectionstatisticsjob.h" +#include "collectionfetchscope.h" #include "item.h" #include "itemfetchscope.h" #include "job.h" #include #include "notificationmanagerinterface.h" #include #include #include namespace Akonadi { class Monitor; /** * @internal */ class MonitorPrivate { public: MonitorPrivate( Monitor *parent ); virtual ~MonitorPrivate() {} Monitor *q_ptr; Q_DECLARE_PUBLIC( Monitor ) org::freedesktop::Akonadi::NotificationManager *nm; Collection::List collections; QSet resources; QSet items; QSet mimetypes; bool monitorAll; QList sessions; ItemFetchScope mItemFetchScope; + CollectionFetchScope mCollectionFetchScope; QHash pendingJobs; bool isCollectionMonitored( Collection::Id collection, const QByteArray &resource ) const { if ( monitorAll || isCollectionMonitored( collection ) || resources.contains( resource ) ) return true; return false; } bool isItemMonitored( Item::Id item, Collection::Id collection, Collection::Id collectionDest, const QString &mimetype, const QByteArray &resource ) const { if ( monitorAll || isCollectionMonitored( collection ) || isCollectionMonitored( collectionDest ) ||items.contains( item ) || resources.contains( resource ) || isMimeTypeMonitored( mimetype ) ) return true; return false; } bool isSessionIgnored( const QByteArray &sessionId ) const { return sessions.contains( sessionId ); } bool connectToNotificationManager(); bool acceptNotification( const NotificationMessage &msg ); bool processNotification( const NotificationMessage &msg ); // private slots void slotSessionDestroyed( QObject* ); void slotStatisticsChangedFinished( KJob* ); void slotFlushRecentlyChangedCollections(); virtual void slotNotify( const NotificationMessage::List &msgs ); void slotItemJobFinished( KJob* job ); void slotCollectionJobFinished( KJob *job ); void emitItemNotification( const NotificationMessage &msg, const Item &item = Item(), const Collection &collection = Collection(), const Collection &collectionDest = Collection() ); void emitCollectionNotification( const NotificationMessage &msg, const Collection &col = Collection(), const Collection &par = Collection(), const Collection &dest = Collection() ); bool fetchCollection; bool fetchCollectionStatistics; private: // collections that need a statistics update QSet recentlyChangedCollections; bool isCollectionMonitored( Collection::Id collection ) const { if ( collections.contains( Collection( collection ) ) ) return true; if ( collections.contains( Collection::root() ) ) return true; return false; } bool isMimeTypeMonitored( const QString& mimetype ) const { if ( mimetypes.contains( mimetype ) ) return true; KMimeType::Ptr mimeType = KMimeType::mimeType( mimetype, KMimeType::ResolveAliases ); if ( mimeType.isNull() ) return false; foreach ( const QString &mt, mimetypes ) { if ( mimeType->is( mt ) ) return true; } return false; } void fetchStatistics( Collection::Id colId ) { CollectionStatisticsJob *job = new CollectionStatisticsJob( Collection( colId ), q_ptr ); QObject::connect( job, SIGNAL(result(KJob*)), q_ptr, SLOT(slotStatisticsChangedFinished(KJob*)) ); } void notifyCollectionStatisticsWatchers( Collection::Id collection, const QByteArray &resource ) { if ( isCollectionMonitored( collection, resource ) ) { if (recentlyChangedCollections.empty() ) QTimer::singleShot( 500, q_ptr, SLOT(slotFlushRecentlyChangedCollections()) ); recentlyChangedCollections.insert( collection ); } } }; /** * @internal * * A job which fetches an item and a collection. */ class AKONADI_EXPORT ItemCollectionFetchJob : public Job { Q_OBJECT public: explicit ItemCollectionFetchJob( const Item &item, Collection::Id collectionId, Collection::Id destCollectionId, QObject *parent = 0 ); ~ItemCollectionFetchJob(); Item item() const; Collection collection() const; Collection destCollection() const; void setFetchScope( const ItemFetchScope &fetchScope ); protected: virtual void doStart(); private Q_SLOTS: void collectionJobDone( KJob* job ); void destCollectionJobDone( KJob* job ); void itemJobDone( KJob* job ); private: Item mReferenceItem; Collection::Id mCollectionId; Collection::Id mDestCollectionId; Item mItem; Collection mCollection; Collection mDestCollection; ItemFetchScope mFetchScope; }; } #endif diff --git a/akonadi/protocolhelper.cpp b/akonadi/protocolhelper.cpp index cdd334b0e..29ab112a0 100644 --- a/akonadi/protocolhelper.cpp +++ b/akonadi/protocolhelper.cpp @@ -1,248 +1,248 @@ /* Copyright (c) 2008 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "protocolhelper_p.h" #include "attributefactory.h" #include "collectionstatistics.h" #include "exception.h" #include #include #include #include #include #include #include using namespace Akonadi; int ProtocolHelper::parseCachePolicy(const QByteArray & data, CachePolicy & policy, int start) { QVarLengthArray params; int end = Akonadi::ImapParser::parseParenthesizedList( data, params, start ); for ( int i = 0; i < params.count() - 1; i += 2 ) { const QByteArray key = params[i]; const QByteArray value = params[i + 1]; if ( key == "INHERIT" ) policy.setInheritFromParent( value == "true" ); else if ( key == "INTERVAL" ) policy.setIntervalCheckTime( value.toInt() ); else if ( key == "CACHETIMEOUT" ) policy.setCacheTimeout( value.toInt() ); else if ( key == "SYNCONDEMAND" ) policy.setSyncOnDemand( value == "true" ); else if ( key == "LOCALPARTS" ) { QVarLengthArray tmp; QStringList parts; Akonadi::ImapParser::parseParenthesizedList( value, tmp ); for ( int j=0; j attributes; pos = ImapParser::parseParenthesizedList( data, attributes, pos ); for ( int i = 0; i < attributes.count() - 1; i += 2 ) { const QByteArray key = attributes[i]; const QByteArray value = attributes[i + 1]; if ( key == "NAME" ) { collection.setName( QString::fromUtf8( value ) ); } else if ( key == "REMOTEID" ) { collection.setRemoteId( QString::fromUtf8( value ) ); } else if ( key == "RESOURCE" ) { collection.setResource( QString::fromUtf8( value ) ); } else if ( key == "MIMETYPE" ) { QVarLengthArray ct; ImapParser::parseParenthesizedList( value, ct ); QStringList ct2; for ( int j = 0; j < ct.size(); j++ ) ct2 << QString::fromLatin1( ct[j] ); collection.setContentMimeTypes( ct2 ); } else if ( key == "MESSAGES" ) { CollectionStatistics s = collection.statistics(); s.setCount( value.toLongLong() ); collection.setStatistics( s ); } else if ( key == "UNSEEN" ) { CollectionStatistics s = collection.statistics(); s.setUnreadCount( value.toLongLong() ); collection.setStatistics( s ); } else if ( key == "SIZE" ) { CollectionStatistics s = collection.statistics(); s.setSize( value.toLongLong() ); collection.setStatistics( s ); } else if ( key == "CACHEPOLICY" ) { CachePolicy policy; ProtocolHelper::parseCachePolicy( value, policy ); collection.setCachePolicy( policy ); } else if ( key == "ANCESTORS" ) { QList ancestors; ImapParser::parseParenthesizedList( value, ancestors ); Collection* currentCol = &collection; - foreach ( const QByteArray uidRidPair, ancestors ) { + foreach ( const QByteArray &uidRidPair, ancestors ) { QList parentIds; ImapParser::parseParenthesizedList( uidRidPair, parentIds ); if ( parentIds.size() != 2 ) break; const Collection::Id uid = parentIds.at( 0 ).toLongLong(); const QString rid = QString::fromUtf8( parentIds.at( 1 ) ); if ( uid == Collection::root().id() ) { currentCol->setParentCollection( Collection::root() ); break; } currentCol->parentCollection().setId( uid ); currentCol->parentCollection().setRemoteId( rid ); currentCol = ¤tCol->parentCollection(); } } else { Attribute* attr = AttributeFactory::createAttribute( key ); Q_ASSERT( attr ); attr->deserialize( value ); collection.addAttribute( attr ); } } return pos; } QByteArray ProtocolHelper::attributesToByteArray(const Entity & entity, bool ns ) { QList l; foreach ( const Attribute *attr, entity.attributes() ) { l << encodePartIdentifier( ns ? PartAttribute : PartGlobal, attr->type() ); l << ImapParser::quote( attr->serialized() ); } return ImapParser::join( l, " " ); } QByteArray ProtocolHelper::encodePartIdentifier(PartNamespace ns, const QByteArray & label, int version ) { const QByteArray versionString( version != 0 ? '[' + QByteArray::number( version ) + ']' : "" ); switch ( ns ) { case PartGlobal: return label + versionString; case PartPayload: return "PLD:" + label + versionString; case PartAttribute: return "ATR:" + label + versionString; default: Q_ASSERT( false ); } return QByteArray(); } QByteArray ProtocolHelper::decodePartIdentifier( const QByteArray &data, PartNamespace & ns ) { if ( data.startsWith( "PLD:" ) ) { //krazy:exclude=strings ns = PartPayload; return data.mid( 4 ); } else if ( data.startsWith( "ATR:" ) ) { //krazy:exclude=strings ns = PartAttribute; return data.mid( 4 ); } else { ns = PartGlobal; return data; } } QByteArray ProtocolHelper::itemSetToByteArray( const Item::List &_items, const QByteArray &command ) { if ( _items.isEmpty() ) throw Exception( "No items specified" ); Item::List items( _items ); QByteArray rv; std::sort( items.begin(), items.end(), boost::bind( &Item::id, _1 ) < boost::bind( &Item::id, _2 ) ); if ( items.first().isValid() ) { // all items have a uid set rv += " " AKONADI_CMD_UID " "; rv += command; rv += ' '; QList uids; foreach ( const Item &item, items ) uids << item.id(); ImapSet set; set.add( uids ); rv += set.toImapSequenceSet(); } else { // check if all items have a remote id QList rids; foreach ( const Item &item, items ) { if ( item.remoteId().isEmpty() ) throw Exception( i18n( "No remote identifier specified" ) ); rids << ImapParser::quote( item.remoteId().toUtf8() ); } rv += " " AKONADI_CMD_RID " "; rv += command; rv += " ("; rv += ImapParser::join( rids, " " ); rv += ')'; } return rv; } diff --git a/akonadi/session_p.h b/akonadi/session_p.h index cfed0e56a..0c7653196 100644 --- a/akonadi/session_p.h +++ b/akonadi/session_p.h @@ -1,115 +1,115 @@ /* Copyright (c) 2007 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef AKONADI_SESSION_P_H #define AKONADI_SESSION_P_H #include "session.h" #include "imapparser_p.h" #include #include #include #include class QLocalSocket; namespace Akonadi { /** * @internal */ class SessionPrivate { public: SessionPrivate( Session *parent ) : mParent( parent ), mConnectionSettings( 0 ), protocolVersion( 0 ) { parser = new ImapParser(); } ~SessionPrivate() { delete parser; delete mConnectionSettings; } void startNext(); void reconnect(); void socketDisconnected(); void socketError( QLocalSocket::LocalSocketError error ); void dataReceived(); void doStartNext(); void startJob( Job* job ); void jobDone( KJob* job ); void jobWriteFinished( Akonadi::Job* job ); void jobDestroyed( QObject *job ); bool canPipelineNext(); /** * Creates a new default session for this thread with * the given @p sessionId. The session can be accessed * later by defaultSession(). * * You only need to call this method if you want that the * default session has a special custom id, otherwise a random unique * id is used automatically. */ static void createDefaultSession( const QByteArray &sessionId ); /** Associates the given Job object with this session. */ void addJob( Job* job ); /** Returns the next IMAP tag. */ int nextTag(); /** Sends the given raw data. */ void writeData( const QByteArray &data ); - static int minimumProtocolVersion() { return 17; } + static int minimumProtocolVersion() { return 18; } Session *mParent; QByteArray sessionId; QSettings *mConnectionSettings; QLocalSocket* socket; bool connected; int theNextTag; int protocolVersion; // job management QQueue queue; QQueue pipeline; Job* currentJob; bool jobRunning; // parser stuff ImapParser *parser; }; } #endif diff --git a/akonadi/tests/CMakeLists.txt b/akonadi/tests/CMakeLists.txt index 748128916..9de1ab49d 100644 --- a/akonadi/tests/CMakeLists.txt +++ b/akonadi/tests/CMakeLists.txt @@ -1,126 +1,127 @@ if(${EXECUTABLE_OUTPUT_PATH}) set( PREVIOUS_EXEC_OUTPUT_PATH ${EXECUTABLE_OUTPUT_PATH} ) else(${EXECUTABLE_OUTPUT_PATH}) set( PREVIOUS_EXEC_OUTPUT_PATH . ) endif(${EXECUTABLE_OUTPUT_PATH}) set( EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR} ) set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${KDE4_ENABLE_EXCEPTIONS}" ) include_directories( ${CMAKE_SOURCE_DIR}/akonadi ${CMAKE_CURRENT_SOURCE_DIR}/../ ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR}/../ ${Boost_INCLUDE_DIR} ${AKONADI_INCLUDE_DIR} ) # add testrunner (application for managing a self-contained test # environment) add_subdirectory(testrunner) # add benchmarker add_subdirectory(benchmarker) # convenience macro to add akonadi demo application macro(add_akonadi_demo _source) set(_test ${_source}) get_filename_component(_name ${_source} NAME_WE) kde4_add_executable(${_name} TEST ${_test}) target_link_libraries(${_name} akonadi-kde akonadi-kmime ${QT_QTCORE_LIBRARY} ${QT_QTGUI_LIBRARY} ${KDE4_KDECORE_LIBS} ${KDE4_KDEUI_LIBS}) endmacro(add_akonadi_demo) # convenience macro to add akonadi qtestlib unit-tests macro(add_akonadi_test _source) set(_test ${_source}) get_filename_component(_name ${_source} NAME_WE) kde4_add_unit_test(${_name} TESTNAME akonadi-${_name} ${_test}) target_link_libraries(${_name} akonadi-kde akonadi-kmime ${QT_QTTEST_LIBRARY} ${QT_QTCORE_LIBRARY} ${QT_QTGUI_LIBRARY} ${KDE4_KDECORE_LIBS} ${AKONADI_COMMON_LIBRARIES}) endmacro(add_akonadi_test) # convenience macro to add akonadi testrunner unit-tests macro(add_akonadi_isolated_test _source) set(_test ${_source}) get_filename_component(_name ${_source} NAME_WE) kde4_add_executable(${_name} TEST ${_test}) target_link_libraries(${_name} akonadi-kde akonadi-kmime ${QT_QTTEST_LIBRARY} ${QT_QTCORE_LIBRARY} ${QT_QTGUI_LIBRARY} ${KDE4_KDECORE_LIBS} ${AKONADI_COMMON_LIBRARIES}) # based on kde4_add_unit_test if (WIN32) get_target_property( _loc ${_name} LOCATION ) set(_executable ${_loc}.bat) set(_testrunner ${PREVIOUS_EXEC_OUTPUT_PATH}/akonaditest.bat) else (WIN32) set(_executable ${EXECUTABLE_OUTPUT_PATH}/${_name}) set(_testrunner ${PREVIOUS_EXEC_OUTPUT_PATH}/akonaditest) endif (WIN32) if (UNIX) set(_executable ${_executable}.shell) set(_testrunner ${_testrunner}.shell) endif (UNIX) add_test( akonadi-mysql-db-${_name} ${_testrunner} -c ${CMAKE_CURRENT_SOURCE_DIR}/unittestenv/config-mysql-db.xml ${_executable} ) add_test( akonadi-mysql-fs-${_name} ${_testrunner} -c ${CMAKE_CURRENT_SOURCE_DIR}/unittestenv/config-mysql-fs.xml ${_executable} ) #add_test( akonadi-postgresql-fs-${_name} ${_testrunner} -c ${CMAKE_CURRENT_SOURCE_DIR}/unittestenv/config-postgresql-fs.xml ${_executable} ) #add_test( akonadi-postgresql-fs-${_name} ${_testrunner} -c ${CMAKE_CURRENT_SOURCE_DIR}/unittestenv/config-postgresql-fs.xml ${_executable} ) #add_test( akonadi-sqlite-${_name} ${_testrunner} -c ${CMAKE_CURRENT_SOURCE_DIR}/unittestenv/config-sqlite.xml ${_executable} ) endmacro(add_akonadi_isolated_test) # demo applications add_akonadi_demo(itemdumper.cpp) add_akonadi_demo(subscriber.cpp) add_akonadi_demo(headfetcher.cpp) add_akonadi_demo(agentinstancewidgettest.cpp) add_akonadi_demo(agenttypewidgettest.cpp) add_akonadi_demo(pluginloadertest.cpp) add_akonadi_demo(selftester.cpp) kde4_add_executable( akonadi-firstrun TEST ../firstrun.cpp firstrunner.cpp ) target_link_libraries( akonadi-firstrun akonadi-kde ${KDE4_KDEUI_LIBS} ) # qtestlib unit tests add_akonadi_test(imapparsertest.cpp) add_akonadi_test(imapsettest.cpp) add_akonadi_test(itemhydratest.cpp) add_akonadi_test(itemtest.cpp) add_akonadi_test(itemserializertest.cpp) add_akonadi_test(mimetypecheckertest.cpp) add_akonadi_test(protocolhelpertest.cpp) # qtestlib tests that need non-exported stuff from akonadi-kde kde4_add_unit_test(resourceschedulertest TESTNAME akonadi-resourceschedulertest resourceschedulertest.cpp ../resourcescheduler.cpp) target_link_libraries(resourceschedulertest akonadi-kde ${QT_QTTEST_LIBRARY} ${QT_QTCORE_LIBRARY} ${QT_QTGUI_LIBRARY} ${KDE4_KDECORE_LIBS} ${AKONADI_COMMON_LIBRARIES}) # testrunner tests add_akonadi_isolated_test(testenvironmenttest.cpp) add_akonadi_isolated_test(autoincrementtest.cpp) add_akonadi_isolated_test(attributefactorytest.cpp) add_akonadi_isolated_test(collectionjobtest.cpp) add_akonadi_isolated_test(collectionpathresolvertest.cpp) add_akonadi_isolated_test(collectionattributetest.cpp) add_akonadi_isolated_test(itemfetchtest.cpp) add_akonadi_isolated_test(itemappendtest.cpp) add_akonadi_isolated_test(itemstoretest.cpp) add_akonadi_isolated_test(itemdeletetest.cpp) +add_akonadi_isolated_test(entitycachetest.cpp) add_akonadi_isolated_test(monitortest.cpp) add_akonadi_isolated_test(searchjobtest.cpp) add_akonadi_isolated_test(changerecordertest.cpp) add_akonadi_isolated_test(resourcetest.cpp) add_akonadi_isolated_test(subscriptiontest.cpp) add_akonadi_isolated_test(transactiontest.cpp) add_akonadi_isolated_test(filteractiontest.cpp) add_akonadi_isolated_test(itemcopytest.cpp) add_akonadi_isolated_test(itemmovetest.cpp) add_akonadi_isolated_test(collectioncopytest.cpp) add_akonadi_isolated_test(collectionmovetest.cpp) add_akonadi_isolated_test(collectionsynctest.cpp) add_akonadi_isolated_test(itemsynctest.cpp) add_akonadi_isolated_test(linktest.cpp) add_akonadi_isolated_test(cachetest.cpp) add_akonadi_isolated_test(servermanagertest.cpp) add_akonadi_isolated_test(collectioncreator.cpp) add_akonadi_isolated_test(itembenchmark.cpp) diff --git a/akonadi/tests/entitycachetest.cpp b/akonadi/tests/entitycachetest.cpp new file mode 100644 index 000000000..9b2ee026f --- /dev/null +++ b/akonadi/tests/entitycachetest.cpp @@ -0,0 +1,98 @@ +/* + Copyright (c) 2009 Volker Krause + + This library is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your + option) any later version. + + This library 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 Library General Public + License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + 02110-1301, USA. +*/ + +#include "entitycache.cpp" + +#include +#include + +using namespace Akonadi; + +class EntityCacheTest : public QObject +{ + Q_OBJECT + private: + template + void testCache() + { + EntityCache cache( 2 ); + QSignalSpy spy( &cache, SIGNAL(dataAvailable()) ); + QVERIFY( spy.isValid() ); + + QVERIFY( !cache.isCached( 1 ) ); + QVERIFY( !cache.isRequested( 1 ) ); + + cache.request( 1, FetchScope() ); + QVERIFY( !cache.isCached( 1 ) ); + QVERIFY( cache.isRequested( 1 ) ); + + QTest::qWait( 1000 ); + QCOMPARE( spy.count(), 1 ); + QVERIFY( cache.isCached( 1 ) ); + QVERIFY( cache.isRequested( 1 ) ); + const T e1 = cache.retrieve( 1 ); + QCOMPARE( e1.id(), 1ll ); + + spy.clear(); + cache.request( 2, FetchScope() ); + cache.request( 3, FetchScope() ); + + QVERIFY( !cache.isCached( 1 ) ); + QVERIFY( !cache.isRequested( 1 ) ); + QVERIFY( cache.isRequested( 2 ) ); + QVERIFY( cache.isRequested( 3 ) ); + + cache.invalidate( 2 ); + + QTest::qWait( 1000 ); + QCOMPARE( spy.count(), 2 ); + QVERIFY( cache.isCached( 2 ) ); + QVERIFY( cache.isCached( 3 ) ); + + const T e2 = cache.retrieve( 2 ); + const T e3a = cache.retrieve( 3 ); + QCOMPARE( e3a.id(), 3ll ); + QVERIFY( !e2.isValid() ); + + cache.invalidate( 3 ); + const T e3b = cache.retrieve( 3 ); + QVERIFY( !e3b.isValid() ); + } + + private slots: + void testCacheGeneric_data() + { + QTest::addColumn( "collection" ); + QTest::newRow( "collection" ) << true; + QTest::newRow( "item" ) << false; + } + + void testCacheGeneric() + { + QFETCH( bool, collection ); + if ( collection ) + testCache(); + else + testCache(); + } +}; + +QTEST_AKONADIMAIN( EntityCacheTest, NoGUI ) + +#include "entitycachetest.moc" diff --git a/akonadi/tests/itemappendtest.cpp b/akonadi/tests/itemappendtest.cpp index d2b8b3ef2..280ce351a 100644 --- a/akonadi/tests/itemappendtest.cpp +++ b/akonadi/tests/itemappendtest.cpp @@ -1,238 +1,245 @@ /* Copyright (c) 2006 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "control.h" #include "itemappendtest.h" #include "testattribute.h" #include "test_utils.h" +#include +#include #include #include #include #include #include #include #include using namespace Akonadi; QTEST_AKONADIMAIN( ItemAppendTest, NoGUI ) void ItemAppendTest::initTestCase() { Control::start(); + + // switch all resources offline to reduce interference from them + foreach ( Akonadi::AgentInstance agent, Akonadi::AgentManager::self()->instances() ) + agent.setIsOnline( false ); } void ItemAppendTest::testItemAppend_data() { QTest::addColumn( "remoteId" ); QTest::newRow( "empty" ) << QString(); QTest::newRow( "non empty" ) << QString( "remote-id" ); QTest::newRow( "whitespace" ) << QString( "remote id" ); QTest::newRow( "quotes" ) << QString ( "\"remote\" id" ); + QTest::newRow( "brackets" ) << QString( "[remote id]" ); } void ItemAppendTest::testItemAppend() { const Collection testFolder1( collectionIdFromPath( "res2/space folder" ) ); QVERIFY( testFolder1.isValid() ); QFETCH( QString, remoteId ); Item ref; // for cleanup Item item( -1 ); item.setRemoteId( remoteId ); item.setMimeType( "application/octet-stream" ); item.setFlag( "TestFlag" ); item.setSize( 3456 ); ItemCreateJob *job = new ItemCreateJob( item, Collection( testFolder1 ), this ); AKVERIFYEXEC( job ); ref = job->item(); ItemFetchJob *fjob = new ItemFetchJob( testFolder1, this ); AKVERIFYEXEC( fjob ); QCOMPARE( fjob->items().count(), 1 ); QCOMPARE( fjob->items()[0], ref ); QCOMPARE( fjob->items()[0].remoteId(), remoteId ); QVERIFY( fjob->items()[0].flags().contains( "TestFlag" ) ); qint64 size = 3456; QCOMPARE( fjob->items()[0].size(), size ); ItemDeleteJob *djob = new ItemDeleteJob( ref, this ); AKVERIFYEXEC( djob ); fjob = new ItemFetchJob( testFolder1, this ); AKVERIFYEXEC( fjob ); QVERIFY( fjob->items().isEmpty() ); } void ItemAppendTest::testContent_data() { QTest::addColumn( "data" ); QTest::newRow( "null" ) << QByteArray(); QTest::newRow( "empty" ) << QByteArray( "" ); QTest::newRow( "nullbyte" ) << QByteArray( "\0", 1 ); QTest::newRow( "nullbyte2" ) << QByteArray( "\0X", 2 ); QString utf8string = QString::fromUtf8("äöüß@€µøđ¢©®"); QTest::newRow( "utf8" ) << utf8string.toUtf8(); QTest::newRow( "newlines" ) << QByteArray("\nsome\n\nbreaked\ncontent\n\n"); QByteArray b; QTest::newRow( "big" ) << b.fill( 'a', 1 << 20 ); QTest::newRow( "bignull" ) << b.fill( '\0', 1 << 20 ); QTest::newRow( "bigcr" ) << b.fill( '\r', 1 << 20 ); QTest::newRow( "biglf" ) << b.fill( '\n', 1 << 20 ); } void ItemAppendTest::testContent() { const Collection testFolder1( collectionIdFromPath( "res2/space folder" ) ); QVERIFY( testFolder1.isValid() ); QFETCH( QByteArray, data ); Item item; item.setMimeType( "application/octet-stream" ); item.setPayload( data ); ItemCreateJob* job = new ItemCreateJob( item, testFolder1, this ); AKVERIFYEXEC( job ); Item ref = job->item(); ItemFetchJob *fjob = new ItemFetchJob( testFolder1, this ); fjob->fetchScope().fetchFullPayload(); AKVERIFYEXEC( fjob ); QCOMPARE( fjob->items().count(), 1 ); Item item2 = fjob->items().first(); QCOMPARE( item2.payload(), data ); QEXPECT_FAIL( "null", "Serializer cannot distinguish null vs. empty", Continue ); QCOMPARE( item2.payload().isNull(), data.isNull() ); ItemDeleteJob *djob = new ItemDeleteJob( ref, this ); AKVERIFYEXEC( djob ); } void ItemAppendTest::testNewMimetype() { const Collection col( collectionIdFromPath( "res2/space folder" ) ); QVERIFY( col.isValid() ); Item item; item.setMimeType( "application/new-type" ); ItemCreateJob *job = new ItemCreateJob( item, col, this ); AKVERIFYEXEC( job ); item = job->item(); QVERIFY( item.isValid() ); ItemFetchJob *fetch = new ItemFetchJob( item, this ); AKVERIFYEXEC( fetch ); QCOMPARE( fetch->items().count(), 1 ); QCOMPARE( fetch->items().first().mimeType(), item.mimeType() ); } void ItemAppendTest::testIllegalAppend() { const Collection testFolder1( collectionIdFromPath( "res2/space folder" ) ); QVERIFY( testFolder1.isValid() ); Item item; item.setMimeType( "application/octet-stream" ); // adding item to non-existing collection ItemCreateJob *job = new ItemCreateJob( item, Collection( INT_MAX ), this ); QVERIFY( !job->exec() ); // adding item into a collection which can't handle items of this type const Collection col( collectionIdFromPath( "res1/foo/bla" ) ); QVERIFY( col.isValid() ); job = new ItemCreateJob( item, col, this ); QEXPECT_FAIL( "", "Test not yet implemented in the server.", Continue ); QVERIFY( !job->exec() ); } void ItemAppendTest::testMultipartAppend() { AttributeFactory::registerAttribute(); const Collection testFolder1( collectionIdFromPath( "res2/space folder" ) ); QVERIFY( testFolder1.isValid() ); Item item; item.setMimeType( "application/octet-stream" ); item.setPayload( "body data" ); item.attribute( Item::AddIfMissing )->data = "extra data"; item.setFlag( "TestFlag" ); ItemCreateJob *job = new ItemCreateJob( item, testFolder1, this ); AKVERIFYEXEC( job ); Item ref = job->item(); ItemFetchJob *fjob = new ItemFetchJob( ref, this ); fjob->fetchScope().fetchFullPayload(); fjob->fetchScope().fetchAttribute(); AKVERIFYEXEC( fjob ); QCOMPARE( fjob->items().count(), 1 ); item = fjob->items().first(); QCOMPARE( item.payload(), QByteArray( "body data" ) ); QVERIFY( item.hasAttribute() ); QCOMPARE( item.attribute()->data, QByteArray( "extra data" ) ); QVERIFY( item.flags().contains( "TestFlag" ) ); ItemDeleteJob *djob = new ItemDeleteJob( ref, this ); AKVERIFYEXEC( djob ); } void ItemAppendTest::testItemSize_data() { QTest::addColumn( "item" ); QTest::addColumn( "size" ); Item i( "application/octet-stream" ); i.setPayload( QByteArray( "ABCD" ) ); QTest::newRow( "auto size" ) << i << 4ll; i.setSize( 3 ); QTest::newRow( "too small" ) << i << 4ll; i.setSize( 10 ); QTest::newRow( "too large" ) << i << 10ll; } void ItemAppendTest::testItemSize() { QFETCH( Akonadi::Item, item ); QFETCH( qint64, size ); const Collection col( collectionIdFromPath( "res2/space folder" ) ); QVERIFY( col.isValid() ); ItemCreateJob *create = new ItemCreateJob( item, col, this ); AKVERIFYEXEC( create ); Item newItem = create->item(); ItemFetchJob *fetch = new ItemFetchJob( newItem, this ); AKVERIFYEXEC( fetch ); QCOMPARE( fetch->items().count(), 1 ); QCOMPARE( fetch->items().first().size(), size ); } #include "itemappendtest.moc" diff --git a/akonadi/tests/monitortest.cpp b/akonadi/tests/monitortest.cpp index da918d044..68215d947 100644 --- a/akonadi/tests/monitortest.cpp +++ b/akonadi/tests/monitortest.cpp @@ -1,358 +1,358 @@ /* Copyright (c) 2006 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "monitortest.h" #include "test_utils.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Akonadi; QTEST_AKONADIMAIN( MonitorTest, NoGUI ) static Collection res3; Q_DECLARE_METATYPE(Akonadi::Collection::Id) Q_DECLARE_METATYPE(QSet) void MonitorTest::initTestCase() { Control::start(); res3 = Collection( collectionIdFromPath( "res3" ) ); // switch all resources offline to reduce interference from them foreach ( Akonadi::AgentInstance agent, Akonadi::AgentManager::self()->instances() ) agent.setIsOnline( false ); } void MonitorTest::testMonitor_data() { QTest::addColumn( "fetchCol" ); QTest::newRow( "with collection fetching" ) << true; QTest::newRow( "without collection fetching" ) << false; } void MonitorTest::testMonitor() { QFETCH( bool, fetchCol ); Monitor *monitor = new Monitor( this ); monitor->setCollectionMonitored( Collection::root() ); monitor->fetchCollection( fetchCol ); monitor->itemFetchScope().fetchFullPayload(); // monitor signals qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType >(); QSignalSpy caddspy( monitor, SIGNAL(collectionAdded(Akonadi::Collection,Akonadi::Collection)) ); QSignalSpy cmodspy( monitor, SIGNAL(collectionChanged(const Akonadi::Collection&)) ); QSignalSpy cmvspy( monitor, SIGNAL(collectionMoved(Akonadi::Collection,Akonadi::Collection,Akonadi::Collection)) ); QSignalSpy crmspy( monitor, SIGNAL(collectionRemoved(const Akonadi::Collection&)) ); QSignalSpy cstatspy( monitor, SIGNAL(collectionStatisticsChanged(Akonadi::Collection::Id,Akonadi::CollectionStatistics)) ); QSignalSpy iaddspy( monitor, SIGNAL(itemAdded(const Akonadi::Item&, const Akonadi::Collection&)) ); QSignalSpy imodspy( monitor, SIGNAL(itemChanged(const Akonadi::Item&, const QSet&)) ); QSignalSpy imvspy( monitor, SIGNAL(itemMoved(const Akonadi::Item&, const Akonadi::Collection&, const Akonadi::Collection&)) ); QSignalSpy irmspy( monitor, SIGNAL(itemRemoved(const Akonadi::Item&)) ); QVERIFY( caddspy.isValid() ); QVERIFY( cmodspy.isValid() ); QVERIFY( cmvspy.isValid() ); QVERIFY( crmspy.isValid() ); QVERIFY( cstatspy.isValid() ); QVERIFY( iaddspy.isValid() ); QVERIFY( imodspy.isValid() ); QVERIFY( imvspy.isValid() ); QVERIFY( irmspy.isValid() ); // create a collection Collection monitorCol; monitorCol.setParentCollection( res3 ); monitorCol.setName( "monitor" ); CollectionCreateJob *create = new CollectionCreateJob( monitorCol, this ); QVERIFY( create->exec() ); monitorCol = create->collection(); QVERIFY( monitorCol.isValid() ); QTest::qWait(1000); // make sure the DBus signal has been processed QCOMPARE( caddspy.count(), 1 ); QList arg = caddspy.takeFirst(); Collection col = arg.at(0).value(); QCOMPARE( col, monitorCol ); if ( fetchCol ) QCOMPARE( col.name(), QString("monitor") ); Collection parent = arg.at(1).value(); QCOMPARE( parent, res3 ); QVERIFY( cmodspy.isEmpty() ); QVERIFY( cmvspy.isEmpty() ); QVERIFY( crmspy.isEmpty() ); QVERIFY( cstatspy.isEmpty() ); QVERIFY( iaddspy.isEmpty() ); QVERIFY( imodspy.isEmpty() ); QVERIFY( imvspy.isEmpty() ); QVERIFY( irmspy.isEmpty() ); // add an item Item newItem; newItem.setMimeType( "application/octet-stream" ); ItemCreateJob *append = new ItemCreateJob( newItem, monitorCol, this ); QVERIFY( append->exec() ); Item monitorRef = append->item(); QVERIFY( monitorRef.isValid() ); QTest::qWait(1000); QCOMPARE( cstatspy.count(), 1 ); arg = cstatspy.takeFirst(); QEXPECT_FAIL( "", "Don't know how to handle 'Akonadi::Collection::Id', use qRegisterMetaType to register it. <-- I did this, but it still doesn't work!", Continue ); QCOMPARE( arg.at(0).value(), monitorCol.id() ); /* qRegisterMetaType() registers the type with a name of "qlonglong". Doing qRegisterMetaType( "Akonadi::Collection::Id" ) doesn't help. The problem here is that Akonadi::Collection::Id is a typedef to qlonglong, and qlonglong is already a registered meta type. So the signal spy will give us a QVariant of type Akonadi::Collection::Id, but calling .value() on that variant will in fact end up calling qvariant_cast. From the point of view of QMetaType, Akonadi::Collection::Id and qlonglong are different types, so QVariant can't convert, and returns a default-constructed qlonglong, zero. When connecting to a real slot (without QSignalSpy), this problem is avoided, because the casting is done differently (via a lot of void pointers). The docs say nothing about qRegisterMetaType -ing a typedef, so I'm not sure if this is a bug or not. (cberzan) */ QCOMPARE( iaddspy.count(), 1 ); arg = iaddspy.takeFirst(); Item item = arg.at( 0 ).value(); QCOMPARE( item, monitorRef ); QCOMPARE( item.mimeType(), QString::fromLatin1( "application/octet-stream" ) ); Collection collection = arg.at( 1 ).value(); QCOMPARE( collection.id(), monitorCol.id() ); QVERIFY( caddspy.isEmpty() ); QVERIFY( cmodspy.isEmpty() ); QVERIFY( cmvspy.isEmpty() ); QVERIFY( crmspy.isEmpty() ); QVERIFY( imodspy.isEmpty() ); QVERIFY( imvspy.isEmpty() ); QVERIFY( irmspy.isEmpty() ); // modify an item item.setPayload( "some new content" ); ItemModifyJob *store = new ItemModifyJob( item, this ); QVERIFY( store->exec() ); QTest::qWait(1000); QCOMPARE( cstatspy.count(), 1 ); arg = cstatspy.takeFirst(); QEXPECT_FAIL( "", "Don't know how to handle 'Akonadi::Collection::Id', use qRegisterMetaType to register it. <-- I did this, but it still doesn't work!", Continue ); QCOMPARE( arg.at(0).value(), monitorCol.id() ); QCOMPARE( imodspy.count(), 1 ); arg = imodspy.takeFirst(); item = arg.at( 0 ).value(); QCOMPARE( monitorRef, item ); QCOMPARE( item.payload(), QByteArray( "some new content" ) ); QSet parts = arg.at( 1 ).value >(); QCOMPARE( parts, QSet() << "PLD:RFC822" ); QVERIFY( caddspy.isEmpty() ); QVERIFY( cmodspy.isEmpty() ); QVERIFY( cmvspy.isEmpty() ); QVERIFY( crmspy.isEmpty() ); QVERIFY( iaddspy.isEmpty() ); QVERIFY( imvspy.isEmpty() ); QVERIFY( irmspy.isEmpty() ); // move an item ItemMoveJob *move = new ItemMoveJob( item, res3 ); QVERIFY( move->exec() ); QTest::qWait( 1000 ); QCOMPARE( cstatspy.count(), 1 ); arg = cstatspy.takeFirst(); QEXPECT_FAIL( "", "Don't know how to handle 'Akonadi::Collection::Id', use qRegisterMetaType to register it. <-- I did this, but it still doesn't work!", Continue ); QCOMPARE( arg.at(0).value(), monitorCol.id() ); QCOMPARE( imvspy.count(), 1 ); arg = imvspy.takeFirst(); item = arg.at( 0 ).value(); // the item QCOMPARE( monitorRef, item ); col = arg.at( 1 ).value(); // the source collection QCOMPARE( col.id(), monitorCol.id() ); col = arg.at( 2 ).value(); // the destination collection QCOMPARE( col.id(), res3.id() ); QCOMPARE( cmodspy.count(), 2 ); arg = cmodspy.takeFirst(); Collection col1 = arg.at( 0 ).value(); arg = cmodspy.takeFirst(); Collection col2 = arg.at( 0 ).value(); // source and dest collections, in any order QVERIFY( ( col1.id() == monitorCol.id() && col2.id() == res3.id() ) || ( col2.id() == monitorCol.id() && col1.id() == res3.id() ) ); QVERIFY( caddspy.isEmpty() ); QVERIFY( cmvspy.isEmpty() ); QVERIFY( crmspy.isEmpty() ); QVERIFY( iaddspy.isEmpty() ); QVERIFY( imodspy.isEmpty() ); QVERIFY( irmspy.isEmpty() ); // delete an item ItemDeleteJob *del = new ItemDeleteJob( monitorRef, this ); QVERIFY( del->exec() ); QTest::qWait(1000); QCOMPARE( cstatspy.count(), 1 ); arg = cstatspy.takeFirst(); QEXPECT_FAIL( "", "Don't know how to handle 'Akonadi::Collection::Id', use qRegisterMetaType to register it. <-- I did this, but it still doesn't work!", Continue ); QCOMPARE( arg.at(0).value(), monitorCol.id() ); cmodspy.clear(); QCOMPARE( irmspy.count(), 1 ); arg = irmspy.takeFirst(); Item ref = qvariant_cast( arg.at(0) ); QCOMPARE( monitorRef, ref ); QVERIFY( caddspy.isEmpty() ); QVERIFY( cmodspy.isEmpty() ); QVERIFY( cmvspy.isEmpty() ); QVERIFY( crmspy.isEmpty() ); QVERIFY( iaddspy.isEmpty() ); QVERIFY( imodspy.isEmpty() ); QVERIFY( imvspy.isEmpty() ); imvspy.clear(); // modify a collection monitorCol.setName( "changed name" ); CollectionModifyJob *mod = new CollectionModifyJob( monitorCol, this ); AKVERIFYEXEC( mod ); QTest::qWait(1000); QCOMPARE( cmodspy.count(), 1 ); arg = cmodspy.takeFirst(); col = arg.at(0).value(); QCOMPARE( col, monitorCol ); if ( fetchCol ) QCOMPARE( col.name(), QString("changed name") ); QVERIFY( caddspy.isEmpty() ); QVERIFY( cmvspy.isEmpty() ); QVERIFY( crmspy.isEmpty() ); QVERIFY( cstatspy.isEmpty() ); QVERIFY( iaddspy.isEmpty() ); QVERIFY( imodspy.isEmpty() ); QVERIFY( imvspy.isEmpty() ); QVERIFY( irmspy.isEmpty() ); // move a collection Collection dest = Collection( collectionIdFromPath( "res1/foo" ) ); CollectionMoveJob *cmove = new CollectionMoveJob( monitorCol, dest, this ); AKVERIFYEXEC( cmove ); QTest::qWait( 1000 ); QCOMPARE( cmvspy.count(), 1 ); arg = cmvspy.takeFirst(); col = arg.at( 0 ).value(); QCOMPARE( col, monitorCol ); if ( fetchCol ) QCOMPARE( col.name(), monitorCol.name() ); col = arg.at( 1 ).value(); QCOMPARE( col, res3 ); col = arg.at( 2 ).value(); QCOMPARE( col, dest ); QVERIFY( caddspy.isEmpty() ); QVERIFY( cmodspy.isEmpty() ); QVERIFY( crmspy.isEmpty() ); QVERIFY( cstatspy.isEmpty() ); QVERIFY( iaddspy.isEmpty() ); QVERIFY( imodspy.isEmpty() ); QVERIFY( imvspy.isEmpty() ); QVERIFY( irmspy.isEmpty() ); // delete a collection CollectionDeleteJob *cdel = new CollectionDeleteJob( monitorCol, this ); QVERIFY( cdel->exec() ); QTest::qWait(1000); QCOMPARE( crmspy.count(), 1 ); arg = crmspy.takeFirst(); QCOMPARE( arg.at(0).value().id(), monitorCol.id() ); QVERIFY( caddspy.isEmpty() ); QVERIFY( cmodspy.isEmpty() ); QVERIFY( cmvspy.isEmpty() ); QVERIFY( cstatspy.isEmpty() ); QVERIFY( iaddspy.isEmpty() ); QVERIFY( imodspy.isEmpty() ); QVERIFY( imvspy.isEmpty() ); QVERIFY( irmspy.isEmpty() ); } void MonitorTest::testVirtualCollectionsMonitoring() { Monitor *monitor = new Monitor( this ); monitor->setCollectionMonitored( Collection( 1 ) ); // top-level 'Search' collection QSignalSpy caddspy( monitor, SIGNAL(collectionAdded(Akonadi::Collection,Akonadi::Collection)) ); QVERIFY( caddspy.isValid() ); - SearchCreateJob *job = new SearchCreateJob( "Test search collection", "test-search-query" ); + SearchCreateJob *job = new SearchCreateJob( "Test search collection", "test-search-query", this ); AKVERIFYEXEC( job ); QTest::qWait( 1000 ); QCOMPARE( caddspy.count(), 1 ); } #include "monitortest.moc" diff --git a/kcal/incidenceformatter.cpp b/kcal/incidenceformatter.cpp index f4c423d05..754a99533 100644 --- a/kcal/incidenceformatter.cpp +++ b/kcal/incidenceformatter.cpp @@ -1,2796 +1,2788 @@ /* This file is part of the kcal library. Copyright (c) 2001 Cornelius Schumacher Copyright (c) 2004 Reinhold Kainhofer Copyright (c) 2005 Rafal Rzepecki Copyright (c) 2009 Klarälvdalens Datakonsult AB, a KDAB Group company This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ /** @file This file is part of the API for handling calendar data and provides static functions for formatting Incidences for various purposes. @brief Provides methods to format Incidences in various ways for display purposes. @author Cornelius Schumacher \ @author Reinhold Kainhofer \ */ #include "incidenceformatter.h" #include "attachment.h" #include "event.h" #include "todo.h" #include "journal.h" #include "calendar.h" #include "calendarlocal.h" #include "icalformat.h" #include "freebusy.h" #include "calendarresources.h" #include "kpimutils/email.h" #include "kabc/phonenumber.h" #include "kabc/vcardconverter.h" #include "kabc/stdaddressbook.h" #include #include #include #include #include #include #include #include #include #include #include #include using namespace KCal; /******************************************************************* * Helper functions for the extensive display (event viewer) *******************************************************************/ //@cond PRIVATE static QString eventViewerAddLink( const QString &ref, const QString &text, bool newline = true ) { QString tmpStr( "" + text + "" ); if ( newline ) { tmpStr += '\n'; } return tmpStr; } static QString eventViewerAddTag( const QString &tag, const QString &text ) { int numLineBreaks = text.count( "\n" ); QString str = '<' + tag + '>'; QString tmpText = text; QString tmpStr = str; if( numLineBreaks >= 0 ) { if ( numLineBreaks > 0 ) { int pos = 0; QString tmp; for ( int i = 0; i <= numLineBreaks; ++i ) { pos = tmpText.indexOf( "\n" ); tmp = tmpText.left( pos ); tmpText = tmpText.right( tmpText.length() - pos - 1 ); tmpStr += tmp + "
"; } } else { tmpStr += tmpText; } } tmpStr += "'; return tmpStr; } static QString eventViewerFormatCategories( Incidence *event ) { QString tmpStr; if ( !event->categoriesStr().isEmpty() ) { if ( event->categories().count() == 1 ) { tmpStr = eventViewerAddTag( "h3", i18n( "Category" ) ); } else { tmpStr = eventViewerAddTag( "h3", i18n( "Categories" ) ); } tmpStr += eventViewerAddTag( "p", event->categoriesStr() ); } return tmpStr; } static QString linkPerson( const QString &email, QString name, QString uid, const QString &iconPath ) { // Make the search, if there is an email address to search on, // and either name or uid is missing if ( !email.isEmpty() && ( name.isEmpty() || uid.isEmpty() ) ) { KABC::AddressBook *add_book = KABC::StdAddressBook::self( true ); KABC::Addressee::List addressList = add_book->findByEmail( email ); KABC::Addressee o = ( !addressList.isEmpty() ? addressList.first() : KABC::Addressee() ); if ( !o.isEmpty() && addressList.size() < 2 ) { if ( name.isEmpty() ) { // No name set, so use the one from the addressbook name = o.formattedName(); } uid = o.uid(); } else { // Email not found in the addressbook. Don't make a link uid.clear(); } } // Show the attendee QString tmpString = "
  • "; if ( !uid.isEmpty() ) { // There is a UID, so make a link to the addressbook if ( name.isEmpty() ) { // Use the email address for text tmpString += eventViewerAddLink( "uid:" + uid, email ); } else { tmpString += eventViewerAddLink( "uid:" + uid, name ); } } else { // No UID, just show some text tmpString += ( name.isEmpty() ? email : name ); } tmpString += '\n'; // Make the mailto link if ( !email.isEmpty() && !iconPath.isNull() ) { KUrl mailto; mailto.setProtocol( "mailto" ); mailto.setPath( email ); tmpString += eventViewerAddLink( mailto.url(), "" ); } tmpString += "
  • \n"; return tmpString; } static QString eventViewerFormatAttendees( Incidence *event ) { QString tmpStr; Attendee::List attendees = event->attendees(); if ( attendees.count() ) { KIconLoader *iconLoader = KIconLoader::global(); const QString iconPath = iconLoader->iconPath( "mail-message-new", KIconLoader::Small ); // Add organizer link tmpStr += eventViewerAddTag( "h4", i18n( "Organizer" ) ); tmpStr += "
      "; tmpStr += linkPerson( event->organizer().email(), event->organizer().name(), QString(), iconPath ); tmpStr += "
    "; // Add attendees links tmpStr += eventViewerAddTag( "h4", i18n( "Attendees" ) ); tmpStr += "
      "; Attendee::List::ConstIterator it; for ( it = attendees.constBegin(); it != attendees.constEnd(); ++it ) { Attendee *a = *it; tmpStr += linkPerson( a->email(), a->name(), a->uid(), iconPath ); if ( !a->delegator().isEmpty() ) { tmpStr += i18n( " (delegated by %1)", a->delegator() ); } if ( !a->delegate().isEmpty() ) { tmpStr += i18n( " (delegated to %1)", a->delegate() ); } } tmpStr += "
    "; } return tmpStr; } static QString eventViewerFormatAttachments( Incidence *i ) { QString tmpStr; Attachment::List as = i->attachments(); if ( as.count() > 0 ) { Attachment::List::ConstIterator it; for ( it = as.constBegin(); it != as.constEnd(); ++it ) { if ( (*it)->isUri() ) { tmpStr += eventViewerAddLink( (*it)->uri(), (*it)->label() ); tmpStr += "
    "; } } } return tmpStr; } /* FIXME:This function depends of kaddressbook. Is necessary a new type of event? */ static QString eventViewerFormatBirthday( Event *event ) { if ( !event ) { return QString(); } if ( event->customProperty( "KABC", "BIRTHDAY" ) != "YES" ) { return QString(); } QString uid_1 = event->customProperty( "KABC", "UID-1" ); QString name_1 = event->customProperty( "KABC", "NAME-1" ); QString email_1= event->customProperty( "KABC", "EMAIL-1" ); KIconLoader *iconLoader = KIconLoader::global(); const QString iconPath = iconLoader->iconPath( "mail-message-new", KIconLoader::Small ); //TODO: add a tart icon QString tmpString = "
      "; tmpString += linkPerson( email_1, name_1, uid_1, iconPath ); if ( event->customProperty( "KABC", "ANNIVERSARY" ) == "YES" ) { QString uid_2 = event->customProperty( "KABC", "UID-2" ); QString name_2 = event->customProperty( "KABC", "NAME-2" ); QString email_2= event->customProperty( "KABC", "EMAIL-2" ); tmpString += linkPerson( email_2, name_2, uid_2, iconPath ); } tmpString += "
    "; return tmpString; } static QString eventViewerFormatHeader( Incidence *incidence ) { QString tmpStr = ""; // show icons KIconLoader *iconLoader = KIconLoader::global(); tmpStr += ""; tmpStr += ""; tmpStr += "
    "; // TODO: KDE5. Make the function QString Incidence::getPixmap() so we don't // need downcasting. if ( incidence->type() == "Todo" ) { tmpStr += "( incidence ); if ( !todo->isCompleted() ) { tmpStr += iconLoader->iconPath( "view-calendar-tasks", KIconLoader::Small ); } else { tmpStr += iconLoader->iconPath( "task-complete", KIconLoader::Small ); } tmpStr += "\">"; } if ( incidence->type() == "Event" ) { tmpStr += "iconPath( "view-calendar-day", KIconLoader::Small ) + "\">"; } if ( incidence->type() == "Journal" ) { tmpStr += "iconPath( "view-pim-journal", KIconLoader::Small ) + "\">"; } if ( incidence->isAlarmEnabled() ) { tmpStr += "iconPath( "preferences-desktop-notification-bell", KIconLoader::Small ) + "\">"; } if ( incidence->recurs() ) { tmpStr += "iconPath( "edit-redo", KIconLoader::Small ) + "\">"; } if ( incidence->isReadOnly() ) { tmpStr += "iconPath( "object-locked", KIconLoader::Small ) + "\">"; } tmpStr += "" + eventViewerAddTag( "h2", incidence->richSummary() ) + "
    "; return tmpStr; } static QString eventViewerFormatEvent( Event *event, KDateTime::Spec spec ) { if ( !event ) { return QString(); } QString tmpStr = eventViewerFormatHeader( event ); tmpStr += ""; if ( !event->location().isEmpty() ) { tmpStr += ""; tmpStr += ""; tmpStr += ""; tmpStr += ""; } tmpStr += ""; if ( event->allDay() ) { if ( event->isMultiDay() ) { tmpStr += ""; tmpStr += ""; } else { tmpStr += ""; tmpStr += ""; } } else { if ( event->isMultiDay() ) { tmpStr += ""; tmpStr += ""; } else { tmpStr += ""; if ( event->hasEndDate() && event->dtStart() != event->dtEnd() ) { tmpStr += ""; } else { tmpStr += ""; } tmpStr += ""; tmpStr += ""; tmpStr += ""; } } tmpStr += ""; if ( event->customProperty( "KABC", "BIRTHDAY" ) == "YES" ) { tmpStr += ""; tmpStr += ""; tmpStr += ""; tmpStr += ""; tmpStr += "
    " + i18n( "Location" ) + "" + event->richLocation() + "
    " + i18n( "Time" ) + "" + i18nc( " - ","%1 - %2", IncidenceFormatter::dateToString( event->dtStart(), true, spec ), IncidenceFormatter::dateToString( event->dtEnd(), true, spec ) ) + "" + i18n( "Date" ) + "" + i18nc( "date as string","%1", IncidenceFormatter::dateToString( event->dtStart(), true, spec ) ) + "" + i18n( "Time" ) + "" + i18nc( " - ","%1 - %2", IncidenceFormatter::dateToString( event->dtStart(), true, spec ), IncidenceFormatter::dateToString( event->dtEnd(), true, spec ) ) + "" + i18n( "Time" ) + "" + i18nc( " - ","%1 - %2", IncidenceFormatter::timeToString( event->dtStart(), true, spec ), IncidenceFormatter::timeToString( event->dtEnd(), true, spec ) ) + "" + IncidenceFormatter::timeToString( event->dtStart(), true, spec ) + "
    " + i18n( "Date" ) + "" + i18nc( "date as string","%1", IncidenceFormatter::dateToString( event->dtStart(), true, spec ) ) + "
    " + i18n( "Birthday" ) + "" + eventViewerFormatBirthday( event ) + "
    "; return tmpStr; } if ( !event->description().isEmpty() ) { tmpStr += ""; tmpStr += ""; tmpStr += "" + eventViewerAddTag( "p", event->richDescription() ) + ""; tmpStr += ""; } if ( event->categories().count() > 0 ) { tmpStr += ""; tmpStr += ""; tmpStr += i18np( "1 category", "%1 categories", event->categories().count() ) + ""; tmpStr += "" + event->categoriesStr() + ""; tmpStr += ""; } if ( event->recurs() ) { KDateTime dt = event->recurrence()->getNextDateTime( KDateTime::currentUtcDateTime() ); tmpStr += ""; tmpStr += "" + i18n( "Next Occurrence" )+ ""; tmpStr += "" + ( dt.isValid() ? KGlobal::locale()->formatDateTime( dt.dateTime(), KLocale::ShortDate ) : i18nc( "no date", "none" ) ) + ""; tmpStr += ""; } tmpStr += ""; tmpStr += eventViewerFormatAttendees( event ); tmpStr += ""; int attachmentCount = event->attachments().count(); if ( attachmentCount > 0 ) { tmpStr += ""; tmpStr += ""; tmpStr += i18np( "1 attachment", "%1 attachments", attachmentCount )+ ""; tmpStr += "" + eventViewerFormatAttachments( event ) + ""; tmpStr += ""; } KDateTime kdt = event->created().toTimeSpec( spec ); tmpStr += ""; tmpStr += "

    " + i18n( "Creation date: %1", KGlobal::locale()->formatDateTime( kdt.dateTime(), KLocale::ShortDate ) ) + ""; return tmpStr; } static QString eventViewerFormatTodo( Todo *todo, KDateTime::Spec spec ) { if ( !todo ) { return QString(); } QString tmpStr = eventViewerFormatHeader( todo ); if ( !todo->location().isEmpty() ) { tmpStr += eventViewerAddTag( "b", i18n(" Location: %1", todo->richLocation() ) ); tmpStr += "
    "; } if ( todo->hasDueDate() && todo->dtDue().isValid() ) { tmpStr += i18n( "Due on: %1", IncidenceFormatter::dateTimeToString( todo->dtDue(), todo->allDay(), true, spec ) ); } if ( !todo->description().isEmpty() ) { tmpStr += eventViewerAddTag( "p", todo->richDescription() ); } tmpStr += eventViewerFormatCategories( todo ); if ( todo->priority() > 0 ) { tmpStr += i18n( "

    Priority: %1

    ", todo->priority() ); } else { tmpStr += i18n( "

    Priority: %1

    ", i18n( "Unspecified" ) ); } tmpStr += i18n( "

    %1 % completed

    ", todo->percentComplete() ); if ( todo->recurs() ) { KDateTime dt = todo->recurrence()->getNextDateTime( KDateTime::currentUtcDateTime() ); tmpStr += eventViewerAddTag( "p", "" + i18n( "This is a recurring to-do. The next occurrence will be on %1.", KGlobal::locale()->formatDateTime( dt.dateTime(), KLocale::ShortDate ) ) + "" ); } tmpStr += eventViewerFormatAttendees( todo ); tmpStr += eventViewerFormatAttachments( todo ); KDateTime kdt = todo->created().toTimeSpec( spec ); tmpStr += "

    " + i18n( "Creation date: %1", KGlobal::locale()->formatDateTime( kdt.dateTime(), KLocale::ShortDate ) ) + ""; return tmpStr; } static QString eventViewerFormatJournal( Journal *journal, KDateTime::Spec spec ) { if ( !journal ) { return QString(); } QString tmpStr; if ( !journal->summary().isEmpty() ) { tmpStr+= eventViewerAddTag( "h2", journal->richSummary() ); } tmpStr += eventViewerAddTag( "h3", i18n( "Journal for %1", IncidenceFormatter::dateToString( journal->dtStart(), false, spec ) ) ); if ( !journal->description().isEmpty() ) { tmpStr += eventViewerAddTag( "p", journal->richDescription() ); } return tmpStr; } static QString eventViewerFormatFreeBusy( FreeBusy *fb, KDateTime::Spec spec ) { Q_UNUSED( spec ); if ( !fb ) { return QString(); } QString tmpStr( eventViewerAddTag( "h2", i18n( "Free/Busy information for %1", fb->organizer().fullName() ) ) ); tmpStr += eventViewerAddTag( "h4", i18n( "Busy times in date range %1 - %2:", KGlobal::locale()->formatDate( fb->dtStart().date(), KLocale::ShortDate ), KGlobal::locale()->formatDate( fb->dtEnd().date(), KLocale::ShortDate ) ) ); QList periods = fb->busyPeriods(); QString text = eventViewerAddTag( "em", eventViewerAddTag( "b", i18nc( "tag for busy periods list", "Busy:" ) ) ); QList::iterator it; for ( it = periods.begin(); it != periods.end(); ++it ) { Period per = *it; if ( per.hasDuration() ) { int dur = per.duration().asSeconds(); QString cont; if ( dur >= 3600 ) { cont += i18ncp( "hours part of duration", "1 hour ", "%1 hours ", dur / 3600 ); dur %= 3600; } if ( dur >= 60 ) { cont += i18ncp( "minutes part duration", "1 minute ", "%1 minutes ", dur / 60 ); dur %= 60; } if ( dur > 0 ) { cont += i18ncp( "seconds part of duration", "1 second", "%1 seconds", dur ); } text += i18nc( "startDate for duration", "%1 for %2", KGlobal::locale()->formatDateTime( per.start().dateTime(), KLocale::LongDate ), cont ); text += "
    "; } else { if ( per.start().date() == per.end().date() ) { text += i18nc( "date, fromTime - toTime ", "%1, %2 - %3", KGlobal::locale()->formatDate( per.start().date() ), KGlobal::locale()->formatTime( per.start().time() ), KGlobal::locale()->formatTime( per.end().time() ) ); } else { text += i18nc( "fromDateTime - toDateTime", "%1 - %2", KGlobal::locale()->formatDateTime( per.start().dateTime(), KLocale::LongDate ), KGlobal::locale()->formatDateTime( per.end().dateTime(), KLocale::LongDate ) ); } text += "
    "; } } tmpStr += eventViewerAddTag( "p", text ); return tmpStr; } //@endcond //@cond PRIVATE class KCal::IncidenceFormatter::EventViewerVisitor : public IncidenceBase::Visitor { public: EventViewerVisitor() : mSpec( KDateTime::Spec() ), mResult( "" ) {} bool act( IncidenceBase *incidence, KDateTime::Spec spec=KDateTime::Spec() ) { mSpec = spec; mResult = ""; return incidence->accept( *this ); } QString result() const { return mResult; } protected: bool visit( Event *event ) { mResult = eventViewerFormatEvent( event, mSpec ); return !mResult.isEmpty(); } bool visit( Todo *todo ) { mResult = eventViewerFormatTodo( todo, mSpec ); return !mResult.isEmpty(); } bool visit( Journal *journal ) { mResult = eventViewerFormatJournal( journal, mSpec ); return !mResult.isEmpty(); } bool visit( FreeBusy *fb ) { mResult = eventViewerFormatFreeBusy( fb, mSpec ); return !mResult.isEmpty(); } protected: KDateTime::Spec mSpec; QString mResult; }; //@endcond QString IncidenceFormatter::extensiveDisplayString( IncidenceBase *incidence ) { return extensiveDisplayStr( incidence, KDateTime::Spec() ); } QString IncidenceFormatter::extensiveDisplayStr( IncidenceBase *incidence, KDateTime::Spec spec ) { if ( !incidence ) { return QString(); } EventViewerVisitor v; if ( v.act( incidence, spec ) ) { return v.result(); } else { return QString(); } } /******************************************************************* * Helper functions for the body part formatter of kmail *******************************************************************/ //@cond PRIVATE static QString string2HTML( const QString &str ) { return Qt::convertFromPlainText( str, Qt::WhiteSpaceNormal ); } static QString cleanHtml( const QString &html ) { QRegExp rx( "]*>(.*)", Qt::CaseInsensitive ); rx.indexIn( html ); QString body = rx.cap( 1 ); return Qt::escape( body.remove( QRegExp( "<[^>]*>" ) ).trimmed() ); } static QString eventStartTimeStr( Event *event ) { QString tmp; if ( !event->allDay() ) { tmp = i18nc( "%1: Start Date, %2: Start Time", "%1 %2", IncidenceFormatter::dateToString( event->dtStart(), true, KSystemTimeZones::local() ), IncidenceFormatter::timeToString( event->dtStart(), true, KSystemTimeZones::local() ) ); } else { tmp = i18nc( "%1: Start Date", "%1 (all day)", IncidenceFormatter::dateToString( event->dtStart(), true, KSystemTimeZones::local() ) ); } return tmp; } static QString eventEndTimeStr( Event *event ) { QString tmp; if ( event->hasEndDate() && event->dtEnd().isValid() ) { if ( !event->allDay() ) { tmp = i18nc( "%1: End Date, %2: End Time", "%1 %2", IncidenceFormatter::dateToString( event->dtEnd(), true, KSystemTimeZones::local() ), IncidenceFormatter::timeToString( event->dtEnd(), true, KSystemTimeZones::local() ) ); } else { tmp = i18nc( "%1: End Date", "%1 (all day)", IncidenceFormatter::dateToString( event->dtEnd(), true, KSystemTimeZones::local() ) ); } } return tmp; } static QString invitationRow( const QString &cell1, const QString &cell2 ) { return "" + cell1 + "" + cell2 + "\n"; } static bool iamOrganizer( Incidence *incidence ) { // Check if I'm the organizer for this incidence if ( !incidence ) { return false; } bool iam = false; KEMailSettings settings; QStringList profiles = settings.profiles(); for ( QStringList::Iterator it=profiles.begin(); it != profiles.end(); ++it ) { settings.setProfile( *it ); if ( settings.getSetting( KEMailSettings::EmailAddress ) == incidence->organizer().email() ) { iam = true; break; } } return iam; } static bool iamAttendee( Attendee *attendee ) { // Check if I'm this attendee bool iam = false; KEMailSettings settings; QStringList profiles = settings.profiles(); for ( QStringList::Iterator it=profiles.begin(); it != profiles.end(); ++it ) { settings.setProfile( *it ); if ( settings.getSetting( KEMailSettings::EmailAddress ) == attendee->email() ) { iam = true; break; } } return iam; } static Attendee *findMyAttendee( Incidence *incidence ) { // Return the attendee for the incidence that is probably me Attendee *attendee = 0; if ( !incidence ) { return attendee; } KEMailSettings settings; QStringList profiles = settings.profiles(); for ( QStringList::Iterator it=profiles.begin(); it != profiles.end(); ++it ) { settings.setProfile( *it ); Attendee::List attendees = incidence->attendees(); Attendee::List::ConstIterator it2; for ( it2 = attendees.constBegin(); it2 != attendees.constEnd(); ++it2 ) { Attendee *a = *it2; if ( settings.getSetting( KEMailSettings::EmailAddress ) == a->email() ) { attendee = a; break; } } } return attendee; } static Attendee *findAttendee( Incidence *incidence, const QString &email ) { // Search for an attendee by email address Attendee *attendee = 0; if ( !incidence ) { return attendee; } Attendee::List attendees = incidence->attendees(); Attendee::List::ConstIterator it; for ( it = attendees.constBegin(); it != attendees.constEnd(); ++it ) { Attendee *a = *it; if ( email == a->email() ) { attendee = a; break; } } return attendee; } static bool rsvpRequested( Incidence *incidence ) { if ( !incidence ) { return false; } //use a heuristic to determine if a response is requested. bool rsvp = true; // better send superfluously than not at all Attendee::List attendees = incidence->attendees(); Attendee::List::ConstIterator it; for ( it = attendees.constBegin(); it != attendees.constEnd(); ++it ) { if ( it == attendees.constBegin() ) { rsvp = (*it)->RSVP(); // use what the first one has } else { if ( (*it)->RSVP() != rsvp ) { rsvp = true; // they differ, default break; } } } return rsvp; } static QString rsvpRequestedStr( bool rsvpRequested ) { if ( rsvpRequested ) { return i18n( "Your response is requested" ); } else { return i18n( "A response is not necessary" ); } } static QString invitationPerson( const QString &email, QString name, QString uid ) { // Make the search, if there is an email address to search on, // and either name or uid is missing if ( !email.isEmpty() && ( name.isEmpty() || uid.isEmpty() ) ) { KABC::AddressBook *add_book = KABC::StdAddressBook::self( true ); KABC::Addressee::List addressList = add_book->findByEmail( email ); if ( !addressList.isEmpty() ) { KABC::Addressee o = addressList.first(); if ( !o.isEmpty() && addressList.size() < 2 ) { if ( name.isEmpty() ) { // No name set, so use the one from the addressbook name = o.formattedName(); } uid = o.uid(); } else { // Email not found in the addressbook. Don't make a link uid.clear(); } } } // Show the attendee QString tmpString; if ( !uid.isEmpty() ) { // There is a UID, so make a link to the addressbook if ( name.isEmpty() ) { // Use the email address for text tmpString += eventViewerAddLink( "uid:" + uid, email ); } else { tmpString += eventViewerAddLink( "uid:" + uid, name ); } } else { // No UID, just show some text tmpString += ( name.isEmpty() ? email : name ); } tmpString += '\n'; // Make the mailto link if ( !email.isEmpty() ) { KCal::Person person( name, email ); KUrl mailto; mailto.setProtocol( "mailto" ); mailto.setPath( person.fullName() ); const QString iconPath = KIconLoader::global()->iconPath( "mail-message-new", KIconLoader::Small ); tmpString += eventViewerAddLink( mailto.url(), "" ); } tmpString += '\n'; return tmpString; } static QString invitationsDetailsIncidence( Incidence *incidence, bool noHtmlMode ) { // if description and comment -> use both // if description, but no comment -> use the desc as the comment (and no desc) // if comment, but no description -> use the comment and no description QString html; QString descr; QStringList comments; if ( incidence->comments().isEmpty() ) { if ( !incidence->description().isEmpty() ) { // use description as comments if ( !incidence->descriptionIsRich() ) { comments << string2HTML( incidence->description() ); } else { comments << incidence->richDescription(); if ( noHtmlMode ) { comments[0] = cleanHtml( comments[0] ); } comments[0] = eventViewerAddTag( "p", comments[0] ); } } //else desc and comments are empty } else { // non-empty comments foreach ( const QString &c, incidence->comments() ) { if ( !c.isEmpty() ) { comments += string2HTML( c ); } } if ( !incidence->description().isEmpty() ) { // use description too if ( !incidence->descriptionIsRich() ) { descr = string2HTML( incidence->description() ); } else { descr = incidence->richDescription(); if ( noHtmlMode ) { descr = cleanHtml( descr ); } descr = eventViewerAddTag( "p", descr ); } } } if( !descr.isEmpty() ) { html += "

    "; html += ""; html += ""; html += ""; html += "
    " + eventViewerAddTag( "u", i18n( "Description:" ) ) + "
    " + descr + "
    "; } if ( !comments.isEmpty() ) { html += "

    "; html += ""; html += ""; html += ""; html += "
    " + eventViewerAddTag( "u", i18n( "Comments:" ) ) + "
    "; if ( comments.count() > 1 ) { html += "
      "; for ( int i=0; i < comments.count(); ++i ) { html += "
    • " + comments[i] + "
    • "; } html += "
    "; } else { html += comments[0]; } html += "
    "; } return html; } static QString invitationDetailsEvent( Event *event, bool noHtmlMode, KDateTime::Spec spec ) { // Invitation details are formatted into an HTML table if ( !event ) { return QString(); } QString sSummary = i18n( "Summary unspecified" ); if ( !event->summary().isEmpty() ) { if ( !event->summaryIsRich() ) { sSummary = Qt::escape( event->summary() ); } else { sSummary = event->richSummary(); if ( noHtmlMode ) { sSummary = cleanHtml( sSummary ); } } } QString sLocation = i18n( "Location unspecified" ); if ( !event->location().isEmpty() ) { if ( !event->locationIsRich() ) { sLocation = Qt::escape( event->location() ); } else { sLocation = event->richLocation(); if ( noHtmlMode ) { sLocation = cleanHtml( sLocation ); } } } QString dir = ( QApplication::isRightToLeft() ? "rtl" : "ltr" ); QString html = QString( "

    \n" ).arg( dir ); html += ""; // Invitation summary & location rows html += invitationRow( i18n( "What:" ), sSummary ); html += invitationRow( i18n( "Where:" ), sLocation ); // If a 1 day event if ( event->dtStart().date() == event->dtEnd().date() ) { html += invitationRow( i18n( "Date:" ), IncidenceFormatter::dateToString( event->dtStart(), false, spec ) ); if ( !event->allDay() ) { html += invitationRow( i18n( "Time:" ), IncidenceFormatter::timeToString( event->dtStart(), false, spec ) + " - " + IncidenceFormatter::timeToString( event->dtEnd(), false, spec ) ); } } else { html += invitationRow( i18nc( "starting date", "From:" ), IncidenceFormatter::dateToString( event->dtStart(), false, spec ) ); if ( !event->allDay() ) { html += invitationRow( i18nc( "starting time", "At:" ), IncidenceFormatter::timeToString( event->dtStart(), false, spec ) ); } if ( event->hasEndDate() ) { html += invitationRow( i18nc( "ending date", "To:" ), IncidenceFormatter::dateToString( event->dtEnd(), false, spec ) ); if ( !event->allDay() ) { html += invitationRow( i18nc( "ending time", "At:" ), IncidenceFormatter::timeToString( event->dtEnd(), false, spec ) ); } } else { html += invitationRow( i18nc( "ending date", "To:" ), i18n( "no end date specified" ) ); } } // Invitation Duration Row if ( !event->allDay() && event->hasEndDate() && event->dtEnd().isValid() ) { QString tmp; QTime sDuration( 0, 0, 0 ), t; int secs = event->dtStart().secsTo( event->dtEnd() ); t = sDuration.addSecs( secs ); if ( t.hour() > 0 ) { tmp += i18np( "1 hour ", "%1 hours ", t.hour() ); } if ( t.minute() > 0 ) { tmp += i18np( "1 minute ", "%1 minutes ", t.minute() ); } html += invitationRow( i18n( "Duration:" ), tmp ); } if ( event->recurs() ) { html += invitationRow( i18n( "Recurrence:" ), IncidenceFormatter::recurrenceString( event ) ); } html += "
    \n"; html += invitationsDetailsIncidence( event, noHtmlMode ); return html; } static QString invitationDetailsTodo( Todo *todo, bool noHtmlMode, KDateTime::Spec spec ) { // To-do details are formatted into an HTML table if ( !todo ) { return QString(); } QString sSummary = i18n( "Summary unspecified" ); if ( !todo->summary().isEmpty() ) { if ( !todo->summaryIsRich() ) { sSummary = Qt::escape( todo->summary() ); } else { sSummary = todo->richSummary(); if ( noHtmlMode ) { sSummary = cleanHtml( sSummary ); } } } QString sLocation = i18n( "Location unspecified" ); if ( !todo->location().isEmpty() ) { if ( !todo->locationIsRich() ) { sLocation = Qt::escape( todo->location() ); } else { sLocation = todo->richLocation(); if ( noHtmlMode ) { sLocation = cleanHtml( sLocation ); } } } QString dir = ( QApplication::isRightToLeft() ? "rtl" : "ltr" ); QString html = QString( "
    \n" ).arg( dir ); html += ""; // Invitation summary & location rows html += invitationRow( i18n( "What:" ), sSummary ); html += invitationRow( i18n( "Where:" ), sLocation ); if ( todo->hasStartDate() && todo->dtStart().isValid() ) { html += invitationRow( i18n( "Start Date:" ), IncidenceFormatter::dateToString( todo->dtStart(), false, spec ) ); } if ( todo->hasDueDate() && todo->dtDue().isValid() ) { html += invitationRow( i18n( "Due Date:" ), IncidenceFormatter::dateToString( todo->dtDue(), false, spec ) ); } else { html += invitationRow( i18n( "Due Date:" ), i18nc( "no to-do due date", "None" ) ); } html += "
    \n"; html += invitationsDetailsIncidence( todo, noHtmlMode ); return html; } static QString invitationDetailsJournal( Journal *journal, bool noHtmlMode, KDateTime::Spec spec ) { if ( !journal ) { return QString(); } QString sSummary = i18n( "Summary unspecified" ); QString sDescr = i18n( "Description unspecified" ); if ( ! journal->summary().isEmpty() ) { sSummary = journal->richSummary(); if ( noHtmlMode ) { sSummary = cleanHtml( sSummary ); } } if ( ! journal->description().isEmpty() ) { sDescr = journal->richDescription(); if ( noHtmlMode ) { sDescr = cleanHtml( sDescr ); } } QString html( "\n" ); html += invitationRow( i18n( "Summary:" ), sSummary ); html += invitationRow( i18n( "Date:" ), IncidenceFormatter::dateToString( journal->dtStart(), false, spec ) ); html += invitationRow( i18n( "Description:" ), sDescr ); html += "
    \n"; html += invitationsDetailsIncidence( journal, noHtmlMode ); return html; } static QString invitationDetailsFreeBusy( FreeBusy *fb, bool noHtmlMode, KDateTime::Spec spec ) { Q_UNUSED( noHtmlMode ); if ( !fb ) { return QString(); } QString html( "\n" ); html += invitationRow( i18n( "Person:" ), fb->organizer().fullName() ); html += invitationRow( i18n( "Start date:" ), IncidenceFormatter::dateToString( fb->dtStart(), true, spec ) ); html += invitationRow( i18n( "End date:" ), IncidenceFormatter::dateToString( fb->dtEnd(), true, spec ) ); html += "\n"; html += "\n"; QList periods = fb->busyPeriods(); QList::iterator it; for ( it = periods.begin(); it != periods.end(); ++it ) { Period per = *it; if ( per.hasDuration() ) { int dur = per.duration().asSeconds(); QString cont; if ( dur >= 3600 ) { cont += i18ncp( "hours part of duration", "1 hour ", "%1 hours ", dur / 3600 ); dur %= 3600; } if ( dur >= 60 ) { cont += i18ncp( "minutes part of duration", "1 minute", "%1 minutes ", dur / 60 ); dur %= 60; } if ( dur > 0 ) { cont += i18ncp( "seconds part of duration", "1 second", "%1 seconds", dur ); } html += invitationRow( QString(), i18nc( "startDate for duration", "%1 for %2", KGlobal::locale()->formatDateTime( per.start().dateTime(), KLocale::LongDate ), cont ) ); } else { QString cont; if ( per.start().date() == per.end().date() ) { cont = i18nc( "date, fromTime - toTime ", "%1, %2 - %3", KGlobal::locale()->formatDate( per.start().date() ), KGlobal::locale()->formatTime( per.start().time() ), KGlobal::locale()->formatTime( per.end().time() ) ); } else { cont = i18nc( "fromDateTime - toDateTime", "%1 - %2", KGlobal::locale()->formatDateTime( per.start().dateTime(), KLocale::LongDate ), KGlobal::locale()->formatDateTime( per.end().dateTime(), KLocale::LongDate ) ); } html += invitationRow( QString(), cont ); } } html += "

    Busy periods given in this free/busy object:
    \n"; return html; } static QString invitationHeaderEvent( Event *event, ScheduleMessage *msg ) { if ( !msg || !event ) { return QString(); } switch ( msg->method() ) { case iTIPPublish: return i18n( "This event has been published" ); case iTIPRequest: if ( event->revision() > 0 ) { return i18n( "This invitation has been updated" ); } if ( iamOrganizer( event ) ) { return i18n( "I sent this invitation" ); } else { if ( !event->organizer().fullName().isEmpty() ) { return i18n( "You received an invitation from %1", event->organizer().fullName() ); } else { return i18n( "You received an invitation" ); } } case iTIPRefresh: return i18n( "This invitation was refreshed" ); case iTIPCancel: return i18n( "This invitation has been canceled" ); case iTIPAdd: return i18n( "Addition to the invitation" ); case iTIPReply: { Attendee::List attendees = event->attendees(); if( attendees.count() == 0 ) { kDebug() << "No attendees in the iCal reply!"; return QString(); } if ( attendees.count() != 1 ) { kDebug() << "Warning: attendeecount in the reply should be 1" << "but is" << attendees.count(); } Attendee *attendee = *attendees.begin(); QString attendeeName = attendee->name(); if ( attendeeName.isEmpty() ) { attendeeName = attendee->email(); } if ( attendeeName.isEmpty() ) { attendeeName = i18n( "Sender" ); } QString delegatorName, dummy; KPIMUtils::extractEmailAddressAndName( attendee->delegator(), dummy, delegatorName ); if ( delegatorName.isEmpty() ) { delegatorName = attendee->delegator(); } switch( attendee->status() ) { case Attendee::NeedsAction: return i18n( "%1 indicates this invitation still needs some action", attendeeName ); case Attendee::Accepted: if ( delegatorName.isEmpty() ) { return i18n( "%1 accepts this invitation", attendeeName ); } else { return i18n( "%1 accepts this invitation on behalf of %2", attendeeName, delegatorName ); } case Attendee::Tentative: if ( delegatorName.isEmpty() ) { return i18n( "%1 tentatively accepts this invitation", attendeeName ); } else { return i18n( "%1 tentatively accepts this invitation on behalf of %2", attendeeName, delegatorName ); } case Attendee::Declined: if ( delegatorName.isEmpty() ) { return i18n( "%1 declines this invitation", attendeeName ); } else { return i18n( "%1 declines this invitation on behalf of %2", attendeeName, delegatorName ); } case Attendee::Delegated: { QString delegate, dummy; KPIMUtils::extractEmailAddressAndName( attendee->delegate(), dummy, delegate ); if ( delegate.isEmpty() ) { delegate = attendee->delegate(); } if ( !delegate.isEmpty() ) { return i18n( "%1 has delegated this invitation to %2", attendeeName, delegate ); } else { return i18n( "%1 has delegated this invitation", attendeeName ); } } case Attendee::Completed: return i18n( "This invitation is now completed" ); case Attendee::InProcess: return i18n( "%1 is still processing the invitation", attendeeName ); default: return i18n( "Unknown response to this invitation" ); } break; } case iTIPCounter: return i18n( "Sender makes this counter proposal" ); case iTIPDeclineCounter: return i18n( "Sender declines the counter proposal" ); case iTIPNoMethod: return i18n( "Error: iMIP message with unknown method: '%1'", msg->method() ); } return QString(); } static QString invitationHeaderTodo( Todo *todo, ScheduleMessage *msg ) { if ( !msg || !todo ) { return QString(); } switch ( msg->method() ) { case iTIPPublish: return i18n( "This to-do has been published" ); case iTIPRequest: if ( todo->revision() > 0 ) { return i18n( "This to-do has been updated" ); } else { return i18n( "You have been assigned this to-do" ); } case iTIPRefresh: return i18n( "This to-do was refreshed" ); case iTIPCancel: return i18n( "This to-do was canceled" ); case iTIPAdd: return i18n( "Addition to the to-do" ); case iTIPReply: { Attendee::List attendees = todo->attendees(); if ( attendees.count() == 0 ) { kDebug() << "No attendees in the iCal reply!"; return QString(); } if ( attendees.count() != 1 ) { kDebug() << "Warning: attendeecount in the reply should be 1" << "but is" << attendees.count(); } Attendee *attendee = *attendees.begin(); switch( attendee->status() ) { case Attendee::NeedsAction: return i18n( "Sender indicates this to-do assignment still needs some action" ); case Attendee::Accepted: return i18n( "Sender accepts this to-do" ); case Attendee::Tentative: return i18n( "Sender tentatively accepts this to-do" ); case Attendee::Declined: return i18n( "Sender declines this to-do" ); case Attendee::Delegated: { QString delegate, dummy; KPIMUtils::extractEmailAddressAndName( attendee->delegate(), dummy, delegate ); if ( delegate.isEmpty() ) { delegate = attendee->delegate(); } if ( !delegate.isEmpty() ) { return i18n( "Sender has delegated this request for the to-do to %1", delegate ); } else { return i18n( "Sender has delegated this request for the to-do " ); } } case Attendee::Completed: return i18n( "The request for this to-do is now completed" ); case Attendee::InProcess: return i18n( "Sender is still processing the invitation" ); default: return i18n( "Unknown response to this to-do" ); } break; } case iTIPCounter: return i18n( "Sender makes this counter proposal" ); case iTIPDeclineCounter: return i18n( "Sender declines the counter proposal" ); case iTIPNoMethod: return i18n( "Error: iMIP message with unknown method: '%1'", msg->method() ); } return QString(); } static QString invitationHeaderJournal( Journal *journal, ScheduleMessage *msg ) { // TODO: Several of the methods are not allowed for journals, so remove them. if ( !msg || !journal ) { return QString(); } switch ( msg->method() ) { case iTIPPublish: return i18n( "This journal has been published" ); case iTIPRequest: return i18n( "You have been assigned this journal" ); case iTIPRefresh: return i18n( "This journal was refreshed" ); case iTIPCancel: return i18n( "This journal was canceled" ); case iTIPAdd: return i18n( "Addition to the journal" ); case iTIPReply: { Attendee::List attendees = journal->attendees(); if ( attendees.count() == 0 ) { kDebug() << "No attendees in the iCal reply!"; return QString(); } if( attendees.count() != 1 ) { kDebug() << "Warning: attendeecount in the reply should be 1 " << "but is " << attendees.count(); } Attendee *attendee = *attendees.begin(); switch( attendee->status() ) { case Attendee::NeedsAction: return i18n( "Sender indicates this journal assignment still needs some action" ); case Attendee::Accepted: return i18n( "Sender accepts this journal" ); case Attendee::Tentative: return i18n( "Sender tentatively accepts this journal" ); case Attendee::Declined: return i18n( "Sender declines this journal" ); case Attendee::Delegated: return i18n( "Sender has delegated this request for the journal" ); case Attendee::Completed: return i18n( "The request for this journal is now completed" ); case Attendee::InProcess: return i18n( "Sender is still processing the invitation" ); default: return i18n( "Unknown response to this journal" ); } break; } case iTIPCounter: return i18n( "Sender makes this counter proposal" ); case iTIPDeclineCounter: return i18n( "Sender declines the counter proposal" ); case iTIPNoMethod: return i18n( "Error: iMIP message with unknown method: '%1'", msg->method() ); } return QString(); } static QString invitationHeaderFreeBusy( FreeBusy *fb, ScheduleMessage *msg ) { if ( !msg || !fb ) { return QString(); } switch ( msg->method() ) { case iTIPPublish: return i18n( "This free/busy list has been published" ); case iTIPRequest: return i18n( "The free/busy list has been requested" ); case iTIPRefresh: return i18n( "This free/busy list was refreshed" ); case iTIPCancel: return i18n( "This free/busy list was canceled" ); case iTIPAdd: return i18n( "Addition to the free/busy list" ); case iTIPNoMethod: default: return i18n( "Error: Free/Busy iMIP message with unknown method: '%1'", msg->method() ); } } //@endcond static QString invitationAttendees( Incidence *incidence ) { QString tmpStr; if ( !incidence ) { return tmpStr; } tmpStr += i18n( "Invitation List" ); int count=0; Attendee::List attendees = incidence->attendees(); if ( !attendees.isEmpty() ) { Attendee::List::ConstIterator it; for ( it = attendees.constBegin(); it != attendees.constEnd(); ++it ) { Attendee *a = *it; if ( !iamAttendee( a ) ) { count++; if ( count == 1 ) { tmpStr += ""; } tmpStr += ""; tmpStr += ""; tmpStr += ""; tmpStr += ""; } } } if ( count ) { tmpStr += "
    "; tmpStr += invitationPerson( a->email(), a->name(), QString() ); if ( !a->delegator().isEmpty() ) { tmpStr += i18n( " (delegated by %1)", a->delegator() ); } if ( !a->delegate().isEmpty() ) { tmpStr += i18n( " (delegated to %1)", a->delegate() ); } tmpStr += "" + a->statusStr() + "
    "; } else { tmpStr += "" + i18nc( "no attendees", "None" ) + ""; } return tmpStr; } //@cond PRIVATE class KCal::IncidenceFormatter::ScheduleMessageVisitor : public IncidenceBase::Visitor { public: ScheduleMessageVisitor() : mMessage(0) { mResult = ""; } bool act( IncidenceBase *incidence, ScheduleMessage *msg ) { mMessage = msg; return incidence->accept( *this ); } QString result() const { return mResult; } protected: QString mResult; ScheduleMessage *mMessage; }; class KCal::IncidenceFormatter::InvitationHeaderVisitor : public IncidenceFormatter::ScheduleMessageVisitor { protected: bool visit( Event *event ) { mResult = invitationHeaderEvent( event, mMessage ); return !mResult.isEmpty(); } bool visit( Todo *todo ) { mResult = invitationHeaderTodo( todo, mMessage ); return !mResult.isEmpty(); } bool visit( Journal *journal ) { mResult = invitationHeaderJournal( journal, mMessage ); return !mResult.isEmpty(); } bool visit( FreeBusy *fb ) { mResult = invitationHeaderFreeBusy( fb, mMessage ); return !mResult.isEmpty(); } }; class KCal::IncidenceFormatter::InvitationBodyVisitor : public IncidenceFormatter::ScheduleMessageVisitor { public: InvitationBodyVisitor( bool noHtmlMode, KDateTime::Spec spec ) : ScheduleMessageVisitor(), mNoHtmlMode( noHtmlMode ), mSpec( spec ) {} protected: bool visit( Event *event ) { mResult = invitationDetailsEvent( event, mNoHtmlMode, mSpec ); return !mResult.isEmpty(); } bool visit( Todo *todo ) { mResult = invitationDetailsTodo( todo, mNoHtmlMode, mSpec ); return !mResult.isEmpty(); } bool visit( Journal *journal ) { mResult = invitationDetailsJournal( journal, mNoHtmlMode, mSpec ); return !mResult.isEmpty(); } bool visit( FreeBusy *fb ) { mResult = invitationDetailsFreeBusy( fb, mNoHtmlMode, mSpec ); return !mResult.isEmpty(); } private: bool mNoHtmlMode; KDateTime::Spec mSpec; }; //@endcond QString InvitationFormatterHelper::generateLinkURL( const QString &id ) { return id; } //@cond PRIVATE class IncidenceFormatter::IncidenceCompareVisitor : public IncidenceBase::Visitor { public: IncidenceCompareVisitor() : mExistingIncidence( 0 ) {} bool act( IncidenceBase *incidence, Incidence *existingIncidence ) { if ( !existingIncidence ) { return false; } Incidence *inc = dynamic_cast( incidence ); if ( !inc || !existingIncidence || inc->revision() <= existingIncidence->revision() ) { return false; } mExistingIncidence = existingIncidence; return incidence->accept( *this ); } QString result() const { if ( mChanges.isEmpty() ) { return QString(); } QString html = "
    • "; html += mChanges.join( "
    • " ); html += "
      "; return html; } protected: bool visit( Event *event ) { compareEvents( event, dynamic_cast( mExistingIncidence ) ); compareIncidences( event, mExistingIncidence ); return !mChanges.isEmpty(); } bool visit( Todo *todo ) { compareIncidences( todo, mExistingIncidence ); return !mChanges.isEmpty(); } bool visit( Journal *journal ) { compareIncidences( journal, mExistingIncidence ); return !mChanges.isEmpty(); } bool visit( FreeBusy *fb ) { Q_UNUSED( fb ); return !mChanges.isEmpty(); } private: void compareEvents( Event *newEvent, Event *oldEvent ) { if ( !oldEvent || !newEvent ) { return; } if ( oldEvent->dtStart() != newEvent->dtStart() || oldEvent->allDay() != newEvent->allDay() ) { mChanges += i18n( "The invitation starting time has been changed from %1 to %2", eventStartTimeStr( oldEvent ), eventStartTimeStr( newEvent ) ); } if ( oldEvent->dtEnd() != newEvent->dtEnd() || oldEvent->allDay() != newEvent->allDay() ) { mChanges += i18n( "The invitation ending time has been changed from %1 to %2", eventEndTimeStr( oldEvent ), eventEndTimeStr( newEvent ) ); } } void compareIncidences( Incidence *newInc, Incidence *oldInc ) { if ( !oldInc || !newInc ) { return; } if ( oldInc->summary() != newInc->summary() ) { mChanges += i18n( "The summary has been changed to: \"%1\"", newInc->richSummary() ); } if ( oldInc->location() != newInc->location() ) { mChanges += i18n( "The location has been changed to: \"%1\"", newInc->richLocation() ); } if ( oldInc->description() != newInc->description() ) { mChanges += i18n( "The description has been changed to: \"%1\"", newInc->richDescription() ); } Attendee::List oldAttendees = oldInc->attendees(); Attendee::List newAttendees = newInc->attendees(); for ( Attendee::List::ConstIterator it = newAttendees.constBegin(); it != newAttendees.constEnd(); ++it ) { Attendee *oldAtt = oldInc->attendeeByMail( (*it)->email() ); if ( !oldAtt ) { mChanges += i18n( "Attendee %1 has been added", (*it)->fullName() ); } else { if ( oldAtt->status() != (*it)->status() ) { mChanges += i18n( "The status of attendee %1 has been changed to: %2", (*it)->fullName(), (*it)->statusStr() ); } } } for ( Attendee::List::ConstIterator it = oldAttendees.constBegin(); it != oldAttendees.constEnd(); ++it ) { Attendee *newAtt = newInc->attendeeByMail( (*it)->email() ); if ( !newAtt ) { mChanges += i18n( "Attendee %1 has been removed", (*it)->fullName() ); } } } private: Incidence *mExistingIncidence; QStringList mChanges; }; //@endcond QString InvitationFormatterHelper::makeLink( const QString &id, const QString &text ) { QString res( "%2" ); return res.arg( generateLinkURL( id ) ).arg( text ); return res; } // Check if the given incidence is likely one that we own instead one from // a shared calendar (Kolab-specific) static bool incidenceOwnedByMe( Calendar *calendar, Incidence *incidence ) { CalendarResources *cal = dynamic_cast( calendar ); if ( !cal || !incidence ) { return true; } ResourceCalendar *res = cal->resource( incidence ); if ( !res ) { return true; } const QString subRes = res->subresourceIdentifier( incidence ); if ( !subRes.contains( "/.INBOX.directory/" ) ) { return false; } return true; } Calendar *InvitationFormatterHelper::calendar() const { return 0; } static QString formatICalInvitationHelper( QString invitation, Calendar *mCalendar, InvitationFormatterHelper *helper, bool noHtmlMode, KDateTime::Spec spec ) { if ( invitation.isEmpty() ) { return QString(); } ICalFormat format; // parseScheduleMessage takes the tz from the calendar, // no need to set it manually here for the format! ScheduleMessage *msg = format.parseScheduleMessage( mCalendar, invitation ); if( !msg ) { kDebug() << "Failed to parse the scheduling message"; Q_ASSERT( format.exception() ); kDebug() << format.exception()->message(); return QString(); } IncidenceBase *incBase = msg->event(); incBase->shiftTimes( mCalendar->timeSpec(), KDateTime::Spec::LocalZone() ); // Determine if this incidence is in my calendar (and owned by me) Incidence *existingIncidence = 0; if ( incBase && helper->calendar() ) { existingIncidence = helper->calendar()->incidence( incBase->uid() ); if ( !incidenceOwnedByMe( helper->calendar(), existingIncidence ) ) { existingIncidence = 0; } if ( !existingIncidence ) { const Incidence::List list = helper->calendar()->incidences(); for ( Incidence::List::ConstIterator it = list.begin(), end = list.end(); it != end; ++it ) { if ( (*it)->schedulingID() == incBase->uid() && incidenceOwnedByMe( helper->calendar(), *it ) ) { existingIncidence = *it; break; } } } } // First make the text of the message QString html; html += "
      "; IncidenceFormatter::InvitationHeaderVisitor headerVisitor; // The InvitationHeaderVisitor returns false if the incidence is somehow invalid, or not handled if ( !headerVisitor.act( incBase, msg ) ) { return QString(); } html += eventViewerAddTag( "h3", headerVisitor.result() ); IncidenceFormatter::InvitationBodyVisitor bodyVisitor( noHtmlMode, spec ); if ( !bodyVisitor.act( incBase, msg ) ) { return QString(); } html += bodyVisitor.result(); if ( msg->method() == iTIPRequest ) { // ### Scheduler::Publish/Refresh/Add as well? IncidenceFormatter::IncidenceCompareVisitor compareVisitor; if ( compareVisitor.act( incBase, existingIncidence ) ) { html += i18n( "

      The following changes have been made by the organizer:

      " ); html += compareVisitor.result(); } } Incidence *inc = dynamic_cast( incBase ); // determine if I am the organizer for this invitation bool myInc = iamOrganizer( inc ); // determine if the invitation response has already been recorded bool rsvpRec = false; Attendee *ea = 0; if ( !myInc ) { if ( existingIncidence ) { ea = findMyAttendee( existingIncidence ); } if ( ea && ( ea->status() == Attendee::Accepted || ea->status() == Attendee::Declined ) ) { rsvpRec = true; } } // Print if RSVP needed, not-needed, or response already recorded bool rsvpReq = rsvpRequested( inc ); if ( !myInc ) { html += "
      "; html += ""; - if ( rsvpRec ) { - if ( inc && inc->revision() == 0 ) { - html += i18n( "Your response has already been recorded [%1]", - ea->statusStr() ); - } - if ( inc && inc->revision() > 0 ) { - html += i18n( "Your response to the update has already been recorded [%1]", - ea->statusStr() ); - } + if ( rsvpRec && ( inc && inc->revision() == 0 ) ) { + html += i18n( "Your response has already been recorded [%1]", + ea->statusStr() ); rsvpReq = false; } else if ( msg->method() == iTIPCancel ) { - html += i18n( "Declined the invitation" ); + html += i18n( "This invitation was declined" ); } else if ( msg->method() == iTIPAdd ) { - html += i18n( "Accepted the invitation" ); + html += i18n( "This invitation was accepted" ); } else { html += rsvpRequestedStr( rsvpReq ); } html += "
      "; } // Add groupware links html += "

      "; html += ""; const QString tdOpen = ""; switch ( msg->method() ) { case iTIPPublish: case iTIPRequest: case iTIPRefresh: case iTIPAdd: { - if ( !existingIncidence && !rsvpReq ) { + if ( inc && inc->revision() > 0 && existingIncidence ) { if ( inc->type() == "Todo" ) { html += helper->makeLink( "reply", i18n( "[Record invitation into my to-do list]" ) ); } else { html += helper->makeLink( "reply", i18n( "[Record invitation into my calendar]" ) ); } } if ( !myInc ) { if ( rsvpReq ) { // Accept html += tdOpen; html += helper->makeLink( "accept", i18nc( "accept invitation", "Accept" ) ); html += tdClose; // Accept conditionally html += tdOpen; html += helper->makeLink( "accept_conditionally", i18nc( "Accept invitation conditionally", "Accept cond." ) ); html += tdClose; } if ( rsvpReq ) { // Counter proposal html += tdOpen; html += helper->makeLink( "counter", i18nc( "invitation counter proposal", "Counter proposal" ) ); html += tdClose; } if ( rsvpReq ) { // Decline html += tdOpen; html += helper->makeLink( "decline", i18nc( "decline invitation", "Decline" ) ); html += tdClose; } - if ( !existingIncidence && !rsvpReq ) { + if ( !rsvpRec || ( inc && inc->revision() > 0 ) ) { // Delegate html += tdOpen; html += helper->makeLink( "delegate", i18nc( "delegate inviation to another", "Delegate" ) ); html += tdClose; // Forward html += tdOpen; html += helper->makeLink( "forward", i18nc( "forward request to another", "Forward" ) ); html += tdClose; // Check calendar if ( incBase && incBase->type() == "Event" ) { html += tdOpen; html += helper->makeLink( "check_calendar", i18nc( "look for scheduling conflicts", "Check my calendar" ) ); html += tdClose; } } } break; } case iTIPCancel: // Remove invitation - if ( inc && existingIncidence ) { - html += tdOpen; - if ( inc->type() == "Todo" ) { - html += helper->makeLink( "cancel", - i18n( "Remove invitation from my task list" ) ); - } else { - html += helper->makeLink( "cancel", - i18n( "Remove invitation from my calendar" ) ); - } - html += tdClose; + html += tdOpen; + if ( inc->type() == "Todo" ) { + html += helper->makeLink( "cancel", + i18n( "Remove invitation from my to-do list" ) ); + } else { + html += helper->makeLink( "cancel", + i18n( "Remove invitation from my calendar" ) ); } + html += tdClose; break; case iTIPReply: { // Record invitation response Attendee *a = 0; Attendee *ea = 0; if ( inc ) { a = inc->attendees().first(); if ( a && helper->calendar() ) { ea = findAttendee( existingIncidence, a->email() ); } } if ( ea && ( ea->status() != Attendee::NeedsAction ) && ( ea->status() == a->status() ) ) { html += tdOpen; html += eventViewerAddTag( "i", i18n( "The response has already been recorded" ) ); html += tdClose; } else { if ( inc ) { if ( inc->type() == "Todo" ) { html += helper->makeLink( "reply", i18n( "[Record response into my to-do list]" ) ); } else { html += helper->makeLink( "reply", i18n( "[Record response into my calendar]" ) ); } } } break; } case iTIPCounter: // Counter proposal html += tdOpen; html += helper->makeLink( "accept_counter", i18n( "Accept" ) ); html += tdClose; html += tdOpen; html += helper->makeLink( "decline_counter", i18n( "Decline" ) ); html += tdClose; html += tdOpen; html += helper->makeLink( "check_calendar", i18n( "Check my calendar" ) ); html += tdClose; break; case iTIPDeclineCounter: case iTIPNoMethod: break; } // close the groupware table html += "
      "; const QString tdClose = "
      "; // Add the attendee list if I am the organizer if ( myInc && helper->calendar() ) { html += invitationAttendees( helper->calendar()->incidence( inc->uid() ) ); } // close the top-level html += "

      "; return html; } //@endcond QString IncidenceFormatter::formatICalInvitation( QString invitation, Calendar *mCalendar, InvitationFormatterHelper *helper ) { return formatICalInvitationHelper( invitation, mCalendar, helper, false, KSystemTimeZones::local() ); } QString IncidenceFormatter::formatICalInvitationNoHtml( QString invitation, Calendar *mCalendar, InvitationFormatterHelper *helper ) { return formatICalInvitationHelper( invitation, mCalendar, helper, true, KSystemTimeZones::local() ); } /******************************************************************* * Helper functions for the Incidence tooltips *******************************************************************/ //@cond PRIVATE class KCal::IncidenceFormatter::ToolTipVisitor : public IncidenceBase::Visitor { public: ToolTipVisitor() : mRichText( true ), mSpec( KDateTime::Spec() ), mResult( "" ) {} bool act( IncidenceBase *incidence, bool richText=true, KDateTime::Spec spec=KDateTime::Spec() ) { mRichText = richText; mSpec = spec; mResult = ""; return incidence ? incidence->accept( *this ) : false; } QString result() const { return mResult; } protected: bool visit( Event *event ); bool visit( Todo *todo ); bool visit( Journal *journal ); bool visit( FreeBusy *fb ); QString dateRangeText( Event *event ); QString dateRangeText( Todo *todo ); QString dateRangeText( Journal *journal ); QString dateRangeText( FreeBusy *fb ); QString generateToolTip( Incidence *incidence, QString dtRangeText ); protected: bool mRichText; KDateTime::Spec mSpec; QString mResult; }; QString IncidenceFormatter::ToolTipVisitor::dateRangeText( Event *event ) { //FIXME: support mRichText==false QString ret; QString tmp; if ( event->isMultiDay() ) { tmp = IncidenceFormatter::dateToString( event->dtStart(), true, mSpec ); ret += "
      " + i18nc( "Event start", "From: %1", tmp ); tmp = IncidenceFormatter::dateToString( event->dtEnd(), true, mSpec ); ret += "
      " + i18nc( "Event end","To: %1", tmp ); } else { ret += "
      " + i18n( "Date: %1", IncidenceFormatter::dateToString( event->dtStart(), true, mSpec ) ); if ( !event->allDay() ) { const QString dtStartTime = IncidenceFormatter::timeToString( event->dtStart(), true, mSpec ); const QString dtEndTime = IncidenceFormatter::timeToString( event->dtEnd(), true, mSpec ); if ( dtStartTime == dtEndTime ) { // to prevent 'Time: 17:00 - 17:00' tmp = "
      " + i18nc( "time for event", "Time: %1", dtStartTime ); } else { tmp = "
      " + i18nc( "time range for event", "Time: %1 - %2", dtStartTime, dtEndTime ); } ret += tmp; } } return ret.replace( ' ', " " ); } QString IncidenceFormatter::ToolTipVisitor::dateRangeText( Todo *todo ) { //FIXME: support mRichText==false QString ret; if ( todo->hasStartDate() && todo->dtStart().isValid() ) { // No need to add here. This is separated issue and each line // is very visible on its own. On the other hand... Yes, I like it // italics here :) ret += "
      " + i18n( "Start: %1", IncidenceFormatter::dateToString( todo->dtStart( false ), true, mSpec ) ); } if ( todo->hasDueDate() && todo->dtDue().isValid() ) { ret += "
      " + i18n( "Due: %1", IncidenceFormatter::dateTimeToString( todo->dtDue(), todo->allDay(), true, mSpec ) ); } if ( todo->isCompleted() ) { ret += "
      " + i18n( "Completed: %1", todo->completedStr() ); } else { ret += "
      " + i18nc( "percent complete", "%1 % completed", todo->percentComplete() ); } return ret.replace( ' ', " " ); } QString IncidenceFormatter::ToolTipVisitor::dateRangeText( Journal *journal ) { //FIXME: support mRichText==false QString ret; if ( journal->dtStart().isValid() ) { ret += "
      " + i18n( "Date: %1", IncidenceFormatter::dateToString( journal->dtStart(), false, mSpec ) ); } return ret.replace( ' ', " " ); } QString IncidenceFormatter::ToolTipVisitor::dateRangeText( FreeBusy *fb ) { //FIXME: support mRichText==false QString ret; ret = "
      " + i18n( "Period start: %1", KGlobal::locale()->formatDateTime( fb->dtStart().dateTime() ) ); ret += "
      " + i18n( "Period start: %1", KGlobal::locale()->formatDateTime( fb->dtEnd().dateTime() ) ); return ret.replace( ' ', " " ); } bool IncidenceFormatter::ToolTipVisitor::visit( Event *event ) { mResult = generateToolTip( event, dateRangeText( event ) ); return !mResult.isEmpty(); } bool IncidenceFormatter::ToolTipVisitor::visit( Todo *todo ) { mResult = generateToolTip( todo, dateRangeText( todo ) ); return !mResult.isEmpty(); } bool IncidenceFormatter::ToolTipVisitor::visit( Journal *journal ) { mResult = generateToolTip( journal, dateRangeText( journal ) ); return !mResult.isEmpty(); } bool IncidenceFormatter::ToolTipVisitor::visit( FreeBusy *fb ) { //FIXME: support mRichText==false mResult = "" + i18n( "Free/Busy information for %1", fb->organizer().fullName() ) + ""; mResult += dateRangeText( fb ); mResult += ""; return !mResult.isEmpty(); } QString IncidenceFormatter::ToolTipVisitor::generateToolTip( Incidence *incidence, QString dtRangeText ) { //FIXME: support mRichText==false if ( !incidence ) { return QString(); } QString tmp = ""+ incidence->richSummary() + ""; tmp += dtRangeText; if ( !incidence->location().isEmpty() ) { // Put Location: in italics tmp += "
      " + i18n( "Location: %1", incidence->richLocation() ); } if ( !incidence->description().isEmpty() ) { QString desc( incidence->description() ); if ( !incidence->descriptionIsRich() ) { if ( desc.length() > 120 ) { desc = desc.left( 120 ) + "..."; } desc = Qt::escape( desc ).replace( '\n', "
      " ); } else { // TODO: truncate the description when it's rich text } tmp += "
      ----------
      " + i18n( "Description:" ) + "
      " + desc; } tmp += "
      "; return tmp; } //@endcond QString IncidenceFormatter::toolTipString( IncidenceBase *incidence, bool richText ) { return toolTipStr( incidence, richText, KDateTime::Spec() ); } QString IncidenceFormatter::toolTipStr( IncidenceBase *incidence, bool richText, KDateTime::Spec spec ) { ToolTipVisitor v; if ( v.act( incidence, richText, spec ) ) { return v.result(); } else { return QString(); } } /******************************************************************* * Helper functions for the Incidence tooltips *******************************************************************/ //@cond PRIVATE static QString mailBodyIncidence( Incidence *incidence ) { QString body; if ( !incidence->summary().isEmpty() ) { body += i18n( "Summary: %1\n", incidence->richSummary() ); } if ( !incidence->organizer().isEmpty() ) { body += i18n( "Organizer: %1\n", incidence->organizer().fullName() ); } if ( !incidence->location().isEmpty() ) { body += i18n( "Location: %1\n", incidence->richLocation() ); } return body; } //@endcond //@cond PRIVATE class KCal::IncidenceFormatter::MailBodyVisitor : public IncidenceBase::Visitor { public: MailBodyVisitor() : mSpec( KDateTime::Spec() ), mResult( "" ) {} bool act( IncidenceBase *incidence, KDateTime::Spec spec=KDateTime::Spec() ) { mSpec = spec; mResult = ""; return incidence ? incidence->accept( *this ) : false; } QString result() const { return mResult; } protected: bool visit( Event *event ); bool visit( Todo *todo ); bool visit( Journal *journal ); bool visit( FreeBusy * ) { mResult = i18n( "This is a Free Busy Object" ); return !mResult.isEmpty(); } protected: KDateTime::Spec mSpec; QString mResult; }; bool IncidenceFormatter::MailBodyVisitor::visit( Event *event ) { QString recurrence[]= { i18nc( "no recurrence", "None" ), i18nc( "event recurs by minutes", "Minutely" ), i18nc( "event recurs by hours", "Hourly" ), i18nc( "event recurs by days", "Daily" ), i18nc( "event recurs by weeks", "Weekly" ), i18nc( "event recurs same position (e.g. first monday) each month", "Monthly Same Position" ), i18nc( "event recurs same day each month", "Monthly Same Day" ), i18nc( "event recurs same month each year", "Yearly Same Month" ), i18nc( "event recurs same day each year", "Yearly Same Day" ), i18nc( "event recurs same position (e.g. first monday) each year", "Yearly Same Position" ) }; mResult = mailBodyIncidence( event ); mResult += i18n( "Start Date: %1\n", IncidenceFormatter::dateToString( event->dtStart(), true, mSpec ) ); if ( !event->allDay() ) { mResult += i18n( "Start Time: %1\n", IncidenceFormatter::timeToString( event->dtStart(), true, mSpec ) ); } if ( event->dtStart() != event->dtEnd() ) { mResult += i18n( "End Date: %1\n", IncidenceFormatter::dateToString( event->dtEnd(), true, mSpec ) ); } if ( !event->allDay() ) { mResult += i18n( "End Time: %1\n", IncidenceFormatter::timeToString( event->dtEnd(), true, mSpec ) ); } if ( event->recurs() ) { Recurrence *recur = event->recurrence(); // TODO: Merge these two to one of the form "Recurs every 3 days" mResult += i18n( "Recurs: %1\n", recurrence[ recur->recurrenceType() ] ); mResult += i18n( "Frequency: %1\n", event->recurrence()->frequency() ); if ( recur->duration() > 0 ) { mResult += i18np( "Repeats once", "Repeats %1 times", recur->duration() ); mResult += '\n'; } else { if ( recur->duration() != -1 ) { // TODO_Recurrence: What to do with all-day QString endstr; if ( event->allDay() ) { endstr = KGlobal::locale()->formatDate( recur->endDate() ); } else { endstr = KGlobal::locale()->formatDateTime( recur->endDateTime().dateTime() ); } mResult += i18n( "Repeat until: %1\n", endstr ); } else { mResult += i18n( "Repeats forever\n" ); } } } QString details = event->richDescription(); if ( !details.isEmpty() ) { mResult += i18n( "Details:\n%1\n", details ); } return !mResult.isEmpty(); } bool IncidenceFormatter::MailBodyVisitor::visit( Todo *todo ) { mResult = mailBodyIncidence( todo ); if ( todo->hasStartDate() && todo->dtStart().isValid() ) { mResult += i18n( "Start Date: %1\n", IncidenceFormatter::dateToString( todo->dtStart(false), true, mSpec ) ); if ( !todo->allDay() ) { mResult += i18n( "Start Time: %1\n", IncidenceFormatter::timeToString( todo->dtStart(false), true, mSpec ) ); } } if ( todo->hasDueDate() && todo->dtDue().isValid() ) { mResult += i18n( "Due Date: %1\n", IncidenceFormatter::dateToString( todo->dtDue(), true, mSpec ) ); if ( !todo->allDay() ) { mResult += i18n( "Due Time: %1\n", IncidenceFormatter::timeToString( todo->dtDue(), true, mSpec ) ); } } QString details = todo->richDescription(); if ( !details.isEmpty() ) { mResult += i18n( "Details:\n%1\n", details ); } return !mResult.isEmpty(); } bool IncidenceFormatter::MailBodyVisitor::visit( Journal *journal ) { mResult = mailBodyIncidence( journal ); mResult += i18n( "Date: %1\n", IncidenceFormatter::dateToString( journal->dtStart(), true, mSpec ) ); if ( !journal->allDay() ) { mResult += i18n( "Time: %1\n", IncidenceFormatter::timeToString( journal->dtStart(), true, mSpec ) ); } if ( !journal->description().isEmpty() ) { mResult += i18n( "Text of the journal:\n%1\n", journal->richDescription() ); } return !mResult.isEmpty(); } //@endcond QString IncidenceFormatter::mailBodyString( IncidenceBase *incidence ) { return mailBodyStr( incidence, KDateTime::Spec() ); } QString IncidenceFormatter::mailBodyStr( IncidenceBase *incidence, KDateTime::Spec spec ) { if ( !incidence ) { return QString(); } MailBodyVisitor v; if ( v.act( incidence, spec ) ) { return v.result(); } return QString(); } //@cond PRIVATE static QString recurEnd( Incidence *incidence ) { QString endstr; if ( incidence->allDay() ) { endstr = KGlobal::locale()->formatDate( incidence->recurrence()->endDate() ); } else { endstr = KGlobal::locale()->formatDateTime( incidence->recurrence()->endDateTime() ); } return endstr; } //@endcond QString IncidenceFormatter::recurrenceString( Incidence *incidence ) { if ( !incidence->recurs() ) { return i18n( "No recurrence" ); } QStringList dayList; dayList.append( i18n( "31st Last" ) ); dayList.append( i18n( "30th Last" ) ); dayList.append( i18n( "29th Last" ) ); dayList.append( i18n( "28th Last" ) ); dayList.append( i18n( "27th Last" ) ); dayList.append( i18n( "26th Last" ) ); dayList.append( i18n( "25th Last" ) ); dayList.append( i18n( "24th Last" ) ); dayList.append( i18n( "23rd Last" ) ); dayList.append( i18n( "22nd Last" ) ); dayList.append( i18n( "21st Last" ) ); dayList.append( i18n( "20th Last" ) ); dayList.append( i18n( "19th Last" ) ); dayList.append( i18n( "18th Last" ) ); dayList.append( i18n( "17th Last" ) ); dayList.append( i18n( "16th Last" ) ); dayList.append( i18n( "15th Last" ) ); dayList.append( i18n( "14th Last" ) ); dayList.append( i18n( "13th Last" ) ); dayList.append( i18n( "12th Last" ) ); dayList.append( i18n( "11th Last" ) ); dayList.append( i18n( "10th Last" ) ); dayList.append( i18n( "9th Last" ) ); dayList.append( i18n( "8th Last" ) ); dayList.append( i18n( "7th Last" ) ); dayList.append( i18n( "6th Last" ) ); dayList.append( i18n( "5th Last" ) ); dayList.append( i18n( "4th Last" ) ); dayList.append( i18n( "3rd Last" ) ); dayList.append( i18n( "2nd Last" ) ); dayList.append( i18nc( "last day of the month", "Last" ) ); dayList.append( i18nc( "unknown day of the month", "unknown" ) ); //#31 - zero offset from UI dayList.append( i18n( "1st" ) ); dayList.append( i18n( "2nd" ) ); dayList.append( i18n( "3rd" ) ); dayList.append( i18n( "4th" ) ); dayList.append( i18n( "5th" ) ); dayList.append( i18n( "6th" ) ); dayList.append( i18n( "7th" ) ); dayList.append( i18n( "8th" ) ); dayList.append( i18n( "9th" ) ); dayList.append( i18n( "10th" ) ); dayList.append( i18n( "11th" ) ); dayList.append( i18n( "12th" ) ); dayList.append( i18n( "13th" ) ); dayList.append( i18n( "14th" ) ); dayList.append( i18n( "15th" ) ); dayList.append( i18n( "16th" ) ); dayList.append( i18n( "17th" ) ); dayList.append( i18n( "18th" ) ); dayList.append( i18n( "19th" ) ); dayList.append( i18n( "20th" ) ); dayList.append( i18n( "21st" ) ); dayList.append( i18n( "22nd" ) ); dayList.append( i18n( "23rd" ) ); dayList.append( i18n( "24th" ) ); dayList.append( i18n( "25th" ) ); dayList.append( i18n( "26th" ) ); dayList.append( i18n( "27th" ) ); dayList.append( i18n( "28th" ) ); dayList.append( i18n( "29th" ) ); dayList.append( i18n( "30th" ) ); dayList.append( i18n( "31st" ) ); int weekStart = KGlobal::locale()->weekStartDay(); QString dayNames; QString txt; const KCalendarSystem *calSys = KGlobal::locale()->calendar(); Recurrence *recur = incidence->recurrence(); switch ( recur->recurrenceType() ) { case Recurrence::rNone: return i18n( "No recurrence" ); case Recurrence::rMinutely: if ( recur->duration() != -1 ) { txt = i18np( "Recurs every minute until %2", "Recurs every %1 minutes until %2", recur->frequency(), recurEnd( incidence ) ); if ( recur->duration() > 0 ) { txt += i18nc( "number of occurrences", " (%1 occurrences)", recur->duration() ); } return txt; } return i18np( "Recurs every minute", "Recurs every %1 minutes", recur->frequency() ); case Recurrence::rHourly: if ( recur->duration() != -1 ) { txt = i18np( "Recurs hourly until %2", "Recurs every %1 hours until %2", recur->frequency(), recurEnd( incidence ) ); if ( recur->duration() > 0 ) { txt += i18nc( "number of occurrences", " (%1 occurrences)", recur->duration() ); } return txt; } return i18np( "Recurs hourly", "Recurs every %1 hours", recur->frequency() ); case Recurrence::rDaily: if ( recur->duration() != -1 ) { txt = i18np( "Recurs daily until %2", "Recurs every %1 days until %2", recur->frequency(), recurEnd( incidence ) ); if ( recur->duration() > 0 ) { txt += i18nc( "number of occurrences", " (%1 occurrences)", recur->duration() ); } return txt; } return i18np( "Recurs daily", "Recurs every %1 days", recur->frequency() ); case Recurrence::rWeekly: { bool addSpace = false; for ( int i = 0; i < 7; ++i ) { if ( recur->days().testBit( ( i + weekStart + 6 ) % 7 ) ) { if ( addSpace ) { dayNames.append( i18nc( "separator for list of days", ", " ) ); } dayNames.append( calSys->weekDayName( ( ( i + weekStart + 6 ) % 7 ) + 1, KCalendarSystem::ShortDayName ) ); addSpace = true; } } if ( dayNames.isEmpty() ) { dayNames = i18nc( "Recurs weekly on no days", "no days" ); } if ( recur->duration() != -1 ) { txt = i18ncp( "Recurs weekly on [list of days] until end-date", "Recurs weekly on %2 until %3", "Recurs every %1 weeks on %2 until %3", recur->frequency(), dayNames, recurEnd( incidence ) ); if ( recur->duration() > 0 ) { txt += i18nc( "number of occurrences", " (%1 occurrences)", recur->duration() ); } return txt; } return i18ncp( "Recurs weekly on [list of days]", "Recurs weekly on %2", "Recurs every %1 weeks on %2", recur->frequency(), dayNames ); } case Recurrence::rMonthlyPos: { KCal::RecurrenceRule::WDayPos rule = recur->monthPositions()[0]; if ( recur->duration() != -1 ) { txt = i18ncp( "Recurs every N months on the [2nd|3rd|...]" " weekdayname until end-date", "Recurs every month on the %2 %3 until %4", "Recurs every %1 months on the %2 %3 until %4", recur->frequency(), dayList[rule.pos() + 31], calSys->weekDayName( rule.day(),KCalendarSystem::LongDayName ), recurEnd( incidence ) ); if ( recur->duration() > 0 ) { txt += i18nc( "number of occurrences", " (%1 occurrences)", recur->duration() ); } return txt; } return i18ncp( "Recurs every N months on the [2nd|3rd|...] weekdayname", "Recurs every month on the %2 %3", "Recurs every %1 months on the %2 %3", recur->frequency(), dayList[rule.pos() + 31], calSys->weekDayName( rule.day(), KCalendarSystem::LongDayName ) ); } case Recurrence::rMonthlyDay: { int days = recur->monthDays()[0]; if ( recur->duration() != -1 ) { txt = i18ncp( "Recurs monthly on the [1st|2nd|...] day until end-date", "Recurs monthly on the %2 day until %3", "Recurs every %1 months on the %2 day until %3", recur->frequency(), dayList[days + 31], recurEnd( incidence ) ); if ( recur->duration() > 0 ) { txt += i18nc( "number of occurrences", " (%1 occurrences)", recur->duration() ); } return txt; } return i18ncp( "Recurs monthly on the [1st|2nd|...] day", "Recurs monthly on the %2 day", "Recurs every %1 month on the %2 day", recur->frequency(), dayList[days + 31] ); } case Recurrence::rYearlyMonth: { if ( recur->duration() != -1 ) { txt = i18ncp( "Recurs Every N years on month-name [1st|2nd|...]" " until end-date", "Recurs yearly on %2 %3 until %4", "Recurs every %1 years on %2 %3 until %4", recur->frequency(), calSys->monthName( recur->yearMonths()[0], recur->startDate().year() ), dayList[ recur->yearDates()[0] + 31 ], recurEnd( incidence ) ); if ( recur->duration() > 0 ) { txt += i18nc( "number of occurrences", " (%1 occurrences)", recur->duration() ); } return txt; } if ( !recur->yearDates().isEmpty() ) { return i18ncp( "Recurs Every N years on month-name [1st|2nd|...]", "Recurs yearly on %2 %3", "Recurs every %1 years on %2 %3", recur->frequency(), calSys->monthName( recur->yearMonths()[0], recur->startDate().year() ), dayList[ recur->yearDates()[0] + 31 ] ); } else { if (!recur->yearMonths().isEmpty() ) { return i18nc( "Recurs Every year on month-name [1st|2nd|...]", "Recurs yearly on %1 %2", calSys->monthName( recur->yearMonths()[0], recur->startDate().year() ), dayList[ recur->startDate().day() + 31 ] ); } else { return i18nc( "Recurs Every year on month-name [1st|2nd|...]", "Recurs yearly on %1 %2", calSys->monthName( recur->startDate().month(), recur->startDate().year() ), dayList[ recur->startDate().day() + 31 ] ); } } } case Recurrence::rYearlyDay: if ( recur->duration() != -1 ) { txt = i18ncp( "Recurs every N years on day N until end-date", "Recurs every year on day %2 until %3", "Recurs every %1 years" " on day %2 until %3", recur->frequency(), recur->yearDays()[0], recurEnd( incidence ) ); if ( recur->duration() > 0 ) { txt += i18nc( "number of occurrences", " (%1 occurrences)", recur->duration() ); } return txt; } return i18ncp( "Recurs every N YEAR[S] on day N", "Recurs every year on day %2", "Recurs every %1 years" " on day %2", recur->frequency(), recur->yearDays()[0] ); case Recurrence::rYearlyPos: { KCal::RecurrenceRule::WDayPos rule = recur->yearPositions()[0]; if ( recur->duration() != -1 ) { txt = i18ncp( "Every N years on the [2nd|3rd|...] weekdayname " "of monthname until end-date", "Every year on the %2 %3 of %4 until %5", "Every %1 years on the %2 %3 of %4" " until %5", recur->frequency(), dayList[rule.pos() + 31], calSys->weekDayName( rule.day(), KCalendarSystem::LongDayName ), calSys->monthName( recur->yearMonths()[0], recur->startDate().year() ), recurEnd( incidence ) ); if ( recur->duration() > 0 ) { txt += i18nc( "number of occurrences", " (%1 occurrences)", recur->duration() ); } return txt; } return i18ncp( "Every N years on the [2nd|3rd|...] weekdayname " "of monthname", "Every year on the %2 %3 of %4", "Every %1 years on the %2 %3 of %4", recur->frequency(), dayList[rule.pos() + 31], calSys->weekDayName( rule.day(), KCalendarSystem::LongDayName ), calSys->monthName( recur->yearMonths()[0], recur->startDate().year() ) ); } default: return i18n( "Incidence recurs" ); } } QString IncidenceFormatter::timeToString( const KDateTime &date, bool shortfmt, const KDateTime::Spec &spec ) { if ( spec.isValid() ) { QString timeZone; if ( spec.timeZone() != KSystemTimeZones::local() ) { timeZone = ' ' + spec.timeZone().name(); } return KGlobal::locale()->formatTime( date.toTimeSpec( spec ).time(), !shortfmt ) + timeZone; } else { return KGlobal::locale()->formatTime( date.time(), !shortfmt ); } } QString IncidenceFormatter::dateToString( const KDateTime &date, bool shortfmt, const KDateTime::Spec &spec ) { if ( spec.isValid() ) { QString timeZone; if ( spec.timeZone() != KSystemTimeZones::local() ) { timeZone = ' ' + spec.timeZone().name(); } return KGlobal::locale()->formatDate( date.toTimeSpec( spec ).date(), ( shortfmt ? KLocale::ShortDate : KLocale::LongDate ) ) + timeZone; } else { return KGlobal::locale()->formatDate( date.date(), ( shortfmt ? KLocale::ShortDate : KLocale::LongDate ) ); } } QString IncidenceFormatter::dateTimeToString( const KDateTime &date, bool allDay, bool shortfmt, const KDateTime::Spec &spec ) { if ( allDay ) { return dateToString( date, shortfmt, spec ); } if ( spec.isValid() ) { QString timeZone; if ( spec.timeZone() != KSystemTimeZones::local() ) { timeZone = ' ' + spec.timeZone().name(); } return KGlobal::locale()->formatDateTime( date.toTimeSpec( spec ).dateTime(), ( shortfmt ? KLocale::ShortDate : KLocale::LongDate ) ) + timeZone; } else { return KGlobal::locale()->formatDateTime( date.dateTime(), ( shortfmt ? KLocale::ShortDate : KLocale::LongDate ) ); } } diff --git a/kcal/scheduler.cpp b/kcal/scheduler.cpp index 39f81048c..6659a58ab 100644 --- a/kcal/scheduler.cpp +++ b/kcal/scheduler.cpp @@ -1,597 +1,604 @@ /* This file is part of the kcal library. Copyright (c) 2001,2004 Cornelius Schumacher Copyright (C) 2004 Reinhold Kainhofer This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "scheduler.h" #include "calendar.h" #include "event.h" #include "todo.h" #include "freebusy.h" #include "freebusycache.h" #include "icalformat.h" #include #include #include #include using namespace KCal; //@cond PRIVATE class KCal::ScheduleMessage::Private { public: Private() {} IncidenceBase *mIncidence; iTIPMethod mMethod; Status mStatus; QString mError; }; //@endcond ScheduleMessage::ScheduleMessage( IncidenceBase *incidence, iTIPMethod method, ScheduleMessage::Status status ) : d( new KCal::ScheduleMessage::Private ) { d->mIncidence = incidence; d->mMethod = method; d->mStatus = status; } ScheduleMessage::~ScheduleMessage() { delete d; } IncidenceBase *ScheduleMessage::event() { return d->mIncidence; } iTIPMethod ScheduleMessage::method() { return d->mMethod; } ScheduleMessage::Status ScheduleMessage::status() { return d->mStatus; } QString ScheduleMessage::statusName( ScheduleMessage::Status status ) { switch( status ) { case PublishNew: return i18nc( "@item new message posting", "New Message Publish" ); case PublishUpdate: return i18nc( "@item updated message", "Updated Message Published" ); case Obsolete: return i18nc( "@item obsolete status", "Obsolete" ); case RequestNew: return i18nc( "@item request new message posting", "Request New Message" ); case RequestUpdate: return i18nc( "@item request updated posting", "Request Updated Message" ); default: return i18nc( "@item unknown status", "Unknown Status: %1", status ); } } QString ScheduleMessage::error() { return d->mError; } //@cond PRIVATE struct KCal::Scheduler::Private { Private() : mFreeBusyCache( 0 ) { } FreeBusyCache *mFreeBusyCache; }; //@endcond Scheduler::Scheduler( Calendar *calendar ) : d( new KCal::Scheduler::Private ) { mCalendar = calendar; mFormat = new ICalFormat(); mFormat->setTimeSpec( calendar->timeSpec() ); } Scheduler::~Scheduler() { delete mFormat; delete d; } void Scheduler::setFreeBusyCache( FreeBusyCache *c ) { d->mFreeBusyCache = c; } FreeBusyCache *Scheduler::freeBusyCache() const { return d->mFreeBusyCache; } bool Scheduler::acceptTransaction( IncidenceBase *incidence, iTIPMethod method, ScheduleMessage::Status status ) { return acceptTransaction( incidence, method, status, QString() ); } bool Scheduler::acceptTransaction( IncidenceBase *incidence, iTIPMethod method, ScheduleMessage::Status status, const QString &email ) { kDebug() << "method=" << methodName( method ); switch ( method ) { case iTIPPublish: return acceptPublish( incidence, status, method ); case iTIPRequest: return acceptRequest( incidence, status, email ); case iTIPAdd: return acceptAdd( incidence, status ); case iTIPCancel: return acceptCancel( incidence, status ); case iTIPDeclineCounter: return acceptDeclineCounter( incidence, status ); case iTIPReply: return acceptReply( incidence, status, method ); case iTIPRefresh: return acceptRefresh( incidence, status ); case iTIPCounter: return acceptCounter( incidence, status ); default: break; } deleteTransaction( incidence ); return false; } QString Scheduler::methodName( iTIPMethod method ) { switch ( method ) { case iTIPPublish: return QLatin1String( "Publish" ); case iTIPRequest: return QLatin1String( "Request" ); case iTIPRefresh: return QLatin1String( "Refresh" ); case iTIPCancel: return QLatin1String( "Cancel" ); case iTIPAdd: return QLatin1String( "Add" ); case iTIPReply: return QLatin1String( "Reply" ); case iTIPCounter: return QLatin1String( "Counter" ); case iTIPDeclineCounter: return QLatin1String( "Decline Counter" ); default: return QLatin1String( "Unknown" ); } } QString Scheduler::translatedMethodName( iTIPMethod method ) { switch ( method ) { case iTIPPublish: return i18nc( "@item event, to-do, journal or freebusy posting", "Publish" ); case iTIPRequest: return i18nc( "@item event, to-do or freebusy scheduling requests", "Request" ); case iTIPReply: return i18nc( "@item event, to-do or freebusy reply to request", "Reply" ); case iTIPAdd: return i18nc( "@item event, to-do or journal additional property request", "Add" ); case iTIPCancel: return i18nc( "@item event, to-do or journal cancellation notice", "Cancel" ); case iTIPRefresh: return i18nc( "@item event or to-do description update request", "Refresh" ); case iTIPCounter: return i18nc( "@item event or to-do submit counter proposal", "Counter" ); case iTIPDeclineCounter: return i18nc( "@item event or to-do decline a counter proposal", "Decline Counter" ); default: return i18nc( "@item no method", "Unknown" ); } } bool Scheduler::deleteTransaction(IncidenceBase *) { return true; } bool Scheduler::acceptPublish( IncidenceBase *newIncBase, ScheduleMessage::Status status, iTIPMethod method ) { if( newIncBase->type() == "FreeBusy" ) { return acceptFreeBusy( newIncBase, method ); } bool res = false; kDebug() << "status=" << ScheduleMessage::statusName( status ); Incidence *newInc = static_cast( newIncBase ); Incidence *calInc = mCalendar->incidence( newIncBase->uid() ); switch ( status ) { case ScheduleMessage::Unknown: case ScheduleMessage::PublishNew: case ScheduleMessage::PublishUpdate: res = true; if ( calInc ) { if ( ( newInc->revision() > calInc->revision() ) || ( newInc->revision() == calInc->revision() && newInc->lastModified() > calInc->lastModified() ) ) { mCalendar->deleteIncidence( calInc ); } else { res = false; } } if ( res ) { mCalendar->addIncidence( newInc ); } break; case ScheduleMessage::Obsolete: res = true; break; default: break; } deleteTransaction( newIncBase ); return res; } bool Scheduler::acceptRequest( IncidenceBase *incidence, ScheduleMessage::Status status ) { return acceptRequest( incidence, status, QString() ); } bool Scheduler::acceptRequest( IncidenceBase *incidence, ScheduleMessage::Status status, const QString &email ) { Incidence *inc = static_cast( incidence ); if ( !inc ) { return false; } if ( inc->type() == "FreeBusy" ) { // reply to this request is handled in korganizer's incomingdialog return true; } const Incidence::List existingIncidences = mCalendar->incidencesFromSchedulingID( inc->uid() ); kDebug() << "status=" << ScheduleMessage::statusName( status ) << ": found " << existingIncidences.count() << " incidences with schedulingID " << inc->schedulingID(); Incidence::List::ConstIterator incit = existingIncidences.begin(); for ( ; incit != existingIncidences.end() ; ++incit ) { Incidence *i = *incit; kDebug() << "Considering this found event (" << ( i->isReadOnly() ? "readonly" : "readwrite" ) << ") :" << mFormat->toString( i ); // If it's readonly, we can't possible update it. if ( i->isReadOnly() ) { continue; } if ( i->revision() <= inc->revision() ) { // The new incidence might be an update for the found one bool isUpdate = true; // Code for new invitations: // If you think we could check the value of "status" to be RequestNew: we can't. // It comes from a similar check inside libical, where the event is compared to // other events in the calendar. But if we have another version of the event around // (e.g. shared folder for a group), the status could be RequestNew, Obsolete or Updated. kDebug() << "looking in " << i->uid() << "'s attendees"; // This is supposed to be a new request, not an update - however we want to update // the existing one to handle the "clicking more than once on the invitation" case. // So check the attendee status of the attendee. const KCal::Attendee::List attendees = i->attendees(); KCal::Attendee::List::ConstIterator ait; for ( ait = attendees.begin(); ait != attendees.end(); ++ait ) { if( (*ait)->email() == email && (*ait)->status() == Attendee::NeedsAction ) { // This incidence wasn't created by me - it's probably in a shared folder // and meant for someone else, ignore it. kDebug() << "ignoring " << i->uid() << " since I'm still NeedsAction there"; isUpdate = false; break; } } if ( isUpdate ) { if ( i->revision() == inc->revision() && i->lastModified() > inc->lastModified() ) { // This isn't an update - the found incidence was modified more recently kDebug() << "This isn't an update - the found incidence was modified more recently"; deleteTransaction( i ); return false; } kDebug() << "replacing existing incidence " << i->uid(); mCalendar->deleteIncidence( i ); break; // replacing one is enough } } else { // This isn't an update - the found incidence has a bigger revision number kDebug() << "This isn't an update - the found incidence has a bigger revision number"; deleteTransaction(incidence); return false; } } // Move the uid to be the schedulingID and make a unique UID inc->setSchedulingID( inc->uid() ); inc->setUid( CalFormat::createUniqueId() ); // in case this is an update and we didn't find the to-be-updated incidence, // ask whether we should create a new one, or drop the update if ( existingIncidences.count() > 0 || inc->revision() == 0 || KMessageBox::warningYesNo( 0, i18nc( "@info", "The event, to-do or journal to be updated could not be found. " "Maybe it has already been deleted, or the calendar that " "contains it is disabled. Press 'Store' to create a new " "one or 'Throw away' to discard this update." ), i18nc( "@title", "Discard this update?" ), KGuiItem( i18nc( "@option", "Store" ) ), KGuiItem( i18nc( "@option", "Throw away" ) ) ) == KMessageBox::Yes ) { kDebug() << "Storing new incidence with scheduling uid=" << inc->schedulingID() << " and uid=" << inc->uid(); mCalendar->addIncidence( inc ); } deleteTransaction(incidence); return true; } bool Scheduler::acceptAdd( IncidenceBase *incidence, ScheduleMessage::Status /* status */) { deleteTransaction(incidence); return false; } bool Scheduler::acceptCancel( IncidenceBase *incidence, ScheduleMessage::Status /* status */) { const IncidenceBase *toDelete = mCalendar->incidenceFromSchedulingID( incidence->uid() ); bool ret = true; if ( toDelete ) { if ( toDelete->type() == "Event" ) { Event *event = mCalendar->event( toDelete->uid() ); ret = ( event && mCalendar->deleteEvent( event ) ); } else if ( toDelete->type() == "Todo" ) { Todo *todo = mCalendar->todo( toDelete->uid() ); ret = ( todo && mCalendar->deleteTodo( todo ) ); } + } else { + // only complain if we failed to determine the toDelete incidence + // on non-initial request. + Incidence *inc = static_cast( incidence ); + if ( inc->revision() > 0 ) { + ret = false; + } } if ( !ret ) { KMessageBox::error( 0, i18n( "The event or task to be canceled could not be removed from your calendar. " "Maybe it has already been deleted, or the calendar that " "contains it is disabled." ) ); } deleteTransaction(incidence); return ret; } bool Scheduler::acceptDeclineCounter( IncidenceBase *incidence, ScheduleMessage::Status status ) { Q_UNUSED( status ); deleteTransaction( incidence ); return false; } bool Scheduler::acceptReply( IncidenceBase *incidence, ScheduleMessage::Status status, iTIPMethod method ) { Q_UNUSED( status ); if ( incidence->type() == "FreeBusy" ) { return acceptFreeBusy( incidence, method ); } bool ret = false; Event *ev = mCalendar->event( incidence->uid() ); Todo *to = mCalendar->todo( incidence->uid() ); // try harder to find the correct incidence if ( !ev && !to ) { const Incidence::List list = mCalendar->incidences(); for ( Incidence::List::ConstIterator it=list.constBegin(), end=list.constEnd(); it != end; ++it ) { if ( (*it)->schedulingID() == incidence->uid() ) { ev = dynamic_cast( *it ); to = dynamic_cast( *it ); break; } } } if ( ev || to ) { //get matching attendee in calendar kDebug() << "match found!"; Attendee::List attendeesIn = incidence->attendees(); Attendee::List attendeesEv; Attendee::List attendeesNew; if ( ev ) { attendeesEv = ev->attendees(); } if ( to ) { attendeesEv = to->attendees(); } Attendee::List::ConstIterator inIt; Attendee::List::ConstIterator evIt; for ( inIt = attendeesIn.constBegin(); inIt != attendeesIn.constEnd(); ++inIt ) { Attendee *attIn = *inIt; bool found = false; for ( evIt = attendeesEv.constBegin(); evIt != attendeesEv.constEnd(); ++evIt ) { Attendee *attEv = *evIt; if ( attIn->email().toLower() == attEv->email().toLower() ) { //update attendee-info kDebug() << "update attendee"; attEv->setStatus( attIn->status() ); attEv->setDelegate( attIn->delegate() ); attEv->setDelegator( attIn->delegator() ); ret = true; found = true; } } if ( !found && attIn->status() != Attendee::Declined ) { attendeesNew.append( attIn ); } } bool attendeeAdded = false; for ( Attendee::List::ConstIterator it = attendeesNew.constBegin(); it != attendeesNew.constEnd(); ++it ) { Attendee *attNew = *it; QString msg = i18nc( "@info", "%1 wants to attend %2 but was not invited.", attNew->fullName(), ( ev ? ev->summary() : to->summary() ) ); if ( !attNew->delegator().isEmpty() ) { msg = i18nc( "@info", "%1 wants to attend %2 on behalf of %3.", attNew->fullName(), ( ev ? ev->summary() : to->summary() ), attNew->delegator() ); } if ( KMessageBox::questionYesNo( 0, msg, i18nc( "@title", "Uninvited attendee" ), KGuiItem( i18nc( "@option", "Accept Attendance" ) ), KGuiItem( i18nc( "@option", "Reject Attendance" ) ) ) != KMessageBox::Yes ) { KCal::Incidence *cancel = dynamic_cast( incidence ); if ( cancel ) { cancel->addComment( i18nc( "@info", "The organizer rejected your attendance at this meeting." ) ); } performTransaction( cancel ? cancel : incidence, iTIPCancel, attNew->fullName() ); // ### can't delete cancel here because it is aliased to incidence which // is accessed in the next loop iteration (CID 4232) // delete cancel; continue; } Attendee *a = new Attendee( attNew->name(), attNew->email(), attNew->RSVP(), attNew->status(), attNew->role(), attNew->uid() ); a->setDelegate( attNew->delegate() ); a->setDelegator( attNew->delegator() ); if ( ev ) { ev->addAttendee( a ); } else if ( to ) { to->addAttendee( a ); } ret = true; attendeeAdded = true; } // send update about new participants if ( attendeeAdded ) { if ( ev ) { ev->setRevision( ev->revision() + 1 ); performTransaction( ev, iTIPRequest ); } if ( to ) { to->setRevision( to->revision() + 1 ); performTransaction( to, iTIPRequest ); } } if ( ret ) { // We set at least one of the attendees, so the incidence changed // Note: This should not result in a sequence number bump if ( ev ) { ev->updated(); } else if ( to ) { to->updated(); } } if ( to ) { // for VTODO a REPLY can be used to update the completion status of // a to-do. see RFC2446 3.4.3 Todo *update = dynamic_cast ( incidence ); Q_ASSERT( update ); if ( update && ( to->percentComplete() != update->percentComplete() ) ) { to->setPercentComplete( update->percentComplete() ); to->updated(); } } } else { kError(5800) << "No incidence for scheduling\n"; } if ( ret ) { deleteTransaction( incidence ); } return ret; } bool Scheduler::acceptRefresh( IncidenceBase *incidence, ScheduleMessage::Status status ) { Q_UNUSED( status ); // handled in korganizer's IncomingDialog deleteTransaction( incidence ); return false; } bool Scheduler::acceptCounter( IncidenceBase *incidence, ScheduleMessage::Status status ) { Q_UNUSED( status ); deleteTransaction( incidence ); return false; } bool Scheduler::acceptFreeBusy( IncidenceBase *incidence, iTIPMethod method ) { if ( !d->mFreeBusyCache ) { kError() << "KCal::Scheduler: no FreeBusyCache."; return false; } FreeBusy *freebusy = static_cast(incidence); kDebug() << "freeBusyDirName:" << freeBusyDir(); Person from; if( method == iTIPPublish ) { from = freebusy->organizer(); } if ( ( method == iTIPReply ) && ( freebusy->attendeeCount() == 1 ) ) { Attendee *attendee = freebusy->attendees().first(); from.setName( attendee->name() ); from.setEmail( attendee->email() ); } if ( !d->mFreeBusyCache->saveFreeBusy( freebusy, from ) ) { return false; } deleteTransaction( incidence ); return true; } diff --git a/kioslave/imap4/imap4.cpp b/kioslave/imap4/imap4.cpp index f1414ad21..ca0e42e9a 100644 --- a/kioslave/imap4/imap4.cpp +++ b/kioslave/imap4/imap4.cpp @@ -1,2654 +1,2653 @@ /********************************************************************** * * imap4.cc - IMAP4rev1 KIOSlave * Copyright (C) 2001-2002 Michael Haeckel * Copyright (C) 1999 John Corey * * 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. * * Send comments and bug fixes to jcorey@fruity.ath.cx * *********************************************************************/ /** * @class IMAP4Protocol * @note References: * - RFC 2060 - Internet Message Access Protocol - Version 4rev1 - December 1996 * - RFC 2192 - IMAP URL Scheme - September 1997 * - RFC 1731 - IMAP Authentication Mechanisms - December 1994 * (Discusses KERBEROSv4, GSSAPI, and S/Key) * - RFC 2195 - IMAP/POP AUTHorize Extension for Simple Challenge/Response * - September 1997 (CRAM-MD5 authentication method) * - RFC 2104 - HMAC: Keyed-Hashing for Message Authentication - February 1997 * - RFC 2086 - IMAP4 ACL extension - January 1997 * - http://www.ietf.org/internet-drafts/draft-daboo-imap-annotatemore-05.txt * IMAP ANNOTATEMORE draft - April 2004. * * * Supported URLs: * \verbatim imap://server/ imap://user:pass@server/ imap://user;AUTH=method:pass@server/ imap://server/folder/ * \endverbatim * These URLs cause the following actions (in order): * - Prompt for user/pass, list all folders in home directory * - Uses LOGIN to log in * - Uses AUTHENTICATE to log in * - List messages in folder * * @note API notes: * Not receiving the required write access for a folder means * ERR_CANNOT_OPEN_FOR_WRITING. * ERR_DOES_NOT_EXIST is reserved for folders. */ -#include // for KDE_signal, remove in KDEPIM 4.2 #include "imap4.h" #include #include #include #include #include #include #include #include #include extern "C" { #include } #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "common.h" #include "kdemacros.h" #define IMAP_PROTOCOL "imap" #define IMAP_SSL_PROTOCOL "imaps" const int ImapPort = 143; const int ImapsPort = 993; using namespace KIO; extern "C" { void sigalrm_handler (int); KDE_EXPORT int kdemain (int argc, char **argv); } int kdemain (int argc, char **argv) { kDebug(7116) <<"IMAP4::kdemain"; KComponentData instance ("kio_imap4"); if (argc != 4) { fprintf(stderr, "Usage: kio_imap4 protocol domain-socket1 domain-socket2\n"); ::exit (-1); } if (!initSASL()) ::exit(-1); //set debug handler IMAP4Protocol *slave; if (strcasecmp (argv[1], IMAP_SSL_PROTOCOL) == 0) slave = new IMAP4Protocol (argv[2], argv[3], true); else if (strcasecmp (argv[1], IMAP_PROTOCOL) == 0) slave = new IMAP4Protocol (argv[2], argv[3], false); else abort (); slave->dispatchLoop (); delete slave; sasl_done(); return 0; } void sigchld_handler (int signo) { // A signal handler that calls for example waitpid has to save errno // before and restore it afterwards. // (cf. https://www.securecoding.cert.org/confluence/display/cplusplus/ERR32-CPP.+Do+not+rely+on+indeterminate+values+of+errno) const int save_errno = errno; int pid, status; while (signo == SIGCHLD) { pid = waitpid (-1, &status, WNOHANG); if (pid <= 0) { // Reinstall signal handler, since Linux resets to default after // the signal occurred ( BSD handles it different, but it should do // no harm ). KDE_signal (SIGCHLD, sigchld_handler); break; } } errno = save_errno; } IMAP4Protocol::IMAP4Protocol (const QByteArray & pool, const QByteArray & app, bool isSSL) :TCPSlaveBase ((isSSL ? IMAP_SSL_PROTOCOL : IMAP_PROTOCOL), pool, app, isSSL), imapParser (), mimeIO (), mySSL( isSSL ), relayEnabled( false ), cacheOutput( false ), decodeContent( false ), outputBuffer(&outputCache), outputBufferIndex(0), mProcessedSize( 0 ), readBufferLen( 0 ), mTimeOfLastNoop( QDateTime() ) { readBuffer[0] = 0x00; } IMAP4Protocol::~IMAP4Protocol () { disconnectFromHost(); kDebug(7116) <<"IMAP4: Finishing"; } void IMAP4Protocol::get (const KUrl & _url) { if (!makeLogin()) return; kDebug(7116) <<"IMAP4::get -" << _url.prettyUrl(); QString aBox, aSequence, aType, aSection, aValidity, aDelimiter, aInfo; enum IMAP_TYPE aEnum = parseURL (_url, aBox, aSection, aType, aSequence, aValidity, aDelimiter, aInfo); if (aEnum != ITYPE_ATTACH) mimeType (getMimeType(aEnum)); if (aInfo == "DECODE") decodeContent = true; if (aSequence == "0:0" && getState() == ISTATE_SELECT) { CommandPtr cmd = doCommand (imapCommand::clientNoop()); completeQueue.removeAll(cmd); } if (aSequence.isEmpty ()) { aSequence = "1:*"; } mProcessedSize = 0; CommandPtr cmd; if (!assureBox (aBox, true)) return; #ifdef USE_VALIDITY if (selectInfo.uidValidityAvailable () && !aValidity.isEmpty () && selectInfo.uidValidity () != aValidity.toULong ()) { // this url is stale error (ERR_COULD_NOT_READ, _url.prettyUrl()); return; } else #endif { // The "section" specified by the application can be: // * empty (which means body, size and flags) // * a known keyword, like STRUCTURE, ENVELOPE, HEADER, BODY.PEEK[...] // (in which case the slave has some logic to add the necessary items) // * Otherwise, it specifies the exact data items to request. In this case, all // the logic is in the app. QString aUpper = aSection.toUpper(); if (aUpper.contains("STRUCTURE")) { aSection = "BODYSTRUCTURE"; } else if (aUpper.contains("ENVELOPE")) { aSection = "UID RFC822.SIZE FLAGS ENVELOPE"; if (hasCapability("IMAP4rev1")) { aSection += " BODY.PEEK[HEADER.FIELDS (REFERENCES)]"; } else { // imap4 does not know HEADER.FIELDS aSection += " RFC822.HEADER.LINES (REFERENCES)"; } } else if (aUpper == "HEADER") { aSection = "UID RFC822.HEADER RFC822.SIZE FLAGS"; } else if (aUpper.contains("BODY.PEEK[")) { if (aUpper.contains("BODY.PEEK[]")) { if (!hasCapability("IMAP4rev1")) // imap4 does not know BODY.PEEK[] aSection.replace("BODY.PEEK[]", "RFC822.PEEK"); } aSection.prepend("UID RFC822.SIZE FLAGS "); } else if (aSection.isEmpty()) { aSection = "UID BODY[] RFC822.SIZE FLAGS"; } if (aEnum == ITYPE_BOX || aEnum == ITYPE_DIR_AND_BOX) { // write the digest header cacheOutput = true; outputLine ("Content-Type: multipart/digest; boundary=\"IMAPDIGEST\"\r\n", 55); if (selectInfo.recentAvailable ()) outputLineStr ("X-Recent: " + QString::number(selectInfo.recent ()) + "\r\n"); if (selectInfo.countAvailable ()) outputLineStr ("X-Count: " + QString::number(selectInfo.count ()) + "\r\n"); if (selectInfo.unseenAvailable ()) outputLineStr ("X-Unseen: " + QString::number(selectInfo.unseen ()) + "\r\n"); if (selectInfo.uidValidityAvailable ()) outputLineStr ("X-uidValidity: " + QString::number(selectInfo.uidValidity ()) + "\r\n"); if (selectInfo.uidNextAvailable ()) outputLineStr ("X-UidNext: " + QString::number(selectInfo.uidNext ()) + "\r\n"); if (selectInfo.flagsAvailable ()) outputLineStr ("X-Flags: " + QString::number(selectInfo.flags ()) + "\r\n"); if (selectInfo.permanentFlagsAvailable ()) outputLineStr ("X-PermanentFlags: " + QString::number(selectInfo.permanentFlags ()) + "\r\n"); if (selectInfo.readWriteAvailable ()) { if (selectInfo.readWrite()) { outputLine ("X-Access: Read/Write\r\n", 22); } else { outputLine ("X-Access: Read only\r\n", 21); } } outputLine ("\r\n", 2); flushOutput(QString()); cacheOutput = false; } if (aEnum == ITYPE_MSG || (aEnum == ITYPE_ATTACH && !decodeContent)) relayEnabled = true; // normal mode, relay data if (aSequence != "0:0") { QString contentEncoding; if (aEnum == ITYPE_ATTACH && decodeContent) { // get the MIME header and fill getLastHandled() QString mySection = aSection; mySection.replace(']', ".MIME]"); cmd = sendCommand (imapCommand::clientFetch (aSequence, mySection)); do { while (!parseLoop ()) {} } while (!cmd->isComplete ()); completeQueue.removeAll (cmd); // get the content encoding now because getLastHandled will be cleared if (getLastHandled() && getLastHandled()->getHeader()) contentEncoding = getLastHandled()->getHeader()->getEncoding(); // from here on collect the data // it is send to the client in flushOutput in one go // needed to decode the content cacheOutput = true; } cmd = sendCommand (imapCommand::clientFetch (aSequence, aSection)); int res; aUpper = aSection.toUpper(); do { while (!(res = parseLoop())) {} if (res == -1) break; mailHeader *lastone = 0; imapCache *cache = getLastHandled (); if (cache) lastone = cache->getHeader (); if (cmd && !cmd->isComplete ()) { if ( aUpper.contains("BODYSTRUCTURE") || aUpper.contains("FLAGS") || aUpper.contains("UID") || aUpper.contains("ENVELOPE") || (aUpper.contains("BODY.PEEK[0]") && (aEnum == ITYPE_BOX || aEnum == ITYPE_DIR_AND_BOX))) { if (aEnum == ITYPE_BOX || aEnum == ITYPE_DIR_AND_BOX) { // write the mime header (default is here message/rfc822) outputLine ("--IMAPDIGEST\r\n", 14); cacheOutput = true; if (cache->getUid () != 0) outputLineStr ("X-UID: " + QString::number(cache->getUid ()) + "\r\n"); if (cache->getSize () != 0) outputLineStr ("X-Length: " + QString::number(cache->getSize ()) + "\r\n"); if (!cache->getDate ().isEmpty()) outputLineStr ("X-Date: " + cache->getDate () + "\r\n"); if (cache->getFlags () != 0) outputLineStr ("X-Flags: " + QString::number(cache->getFlags ()) + "\r\n"); } else cacheOutput = true; if ( lastone && !decodeContent ) lastone->outputPart (*this); cacheOutput = false; flushOutput(contentEncoding); } } // if not complete } while (cmd && !cmd->isComplete ()); if (aEnum == ITYPE_BOX || aEnum == ITYPE_DIR_AND_BOX) { // write the end boundary outputLine ("--IMAPDIGEST--\r\n", 16); } completeQueue.removeAll (cmd); } } // just to keep everybody happy when no data arrived data (QByteArray ()); finished (); relayEnabled = false; cacheOutput = false; kDebug(7116) <<"IMAP4::get - finished"; } void IMAP4Protocol::listDir (const KUrl & _url) { kDebug(7116) <<" IMAP4::listDir -" << _url.prettyUrl(); if (_url.path().isEmpty()) { KUrl url = _url; url.setPath("/"); redirection( url ); finished(); return; } QString myBox, mySequence, myLType, mySection, myValidity, myDelimiter, myInfo; // parseURL with caching enum IMAP_TYPE myType = parseURL (_url, myBox, mySection, myLType, mySequence, myValidity, myDelimiter, myInfo, true); if (!makeLogin()) return; if (myType == ITYPE_DIR || myType == ITYPE_DIR_AND_BOX) { QString listStr = myBox; CommandPtr cmd; if (!listStr.isEmpty () && !listStr.endsWith(myDelimiter) && mySection != "FOLDERONLY") listStr += myDelimiter; if (mySection.isEmpty()) { listStr += '%'; } else if (mySection == "COMPLETE") { listStr += '*'; } kDebug(7116) <<"IMAP4Protocol::listDir - listStr=" << listStr; cmd = doCommand (imapCommand::clientList ("", listStr, (myLType == "LSUB" || myLType == "LSUBNOCHECK"))); if (cmd->result () == "OK") { QString mailboxName; UDSEntry entry; KUrl aURL = _url; if ( aURL.path().contains(';') ) aURL.setPath(aURL.path().left(aURL.path().indexOf(';'))); kDebug(7116) <<"IMAP4Protocol::listDir - got" << listResponses.count (); if (myLType == "LSUB") { // fire the same command as LIST to check if the box really exists QList listResponsesSave = listResponses; doCommand (imapCommand::clientList ("", listStr, false)); for (QList< imapList >::Iterator it = listResponsesSave.begin (); it != listResponsesSave.end (); ++it) { bool boxOk = false; for (QList< imapList >::Iterator it2 = listResponses.begin (); it2 != listResponses.end (); ++it2) { if ((*it2).name() == (*it).name()) { boxOk = true; // copy the flags from the LIST-command (*it) = (*it2); break; } } if (boxOk) doListEntry (aURL, myBox, (*it), (mySection != "FOLDERONLY")); else // this folder is dead kDebug(7116) <<"IMAP4Protocol::listDir - suppress" << (*it).name(); } listResponses = listResponsesSave; } else // LIST or LSUBNOCHECK { for (QList< imapList >::Iterator it = listResponses.begin (); it != listResponses.end (); ++it) { doListEntry (aURL, myBox, (*it), (mySection != "FOLDERONLY")); } } entry.clear (); listEntry (entry, true); } else { error (ERR_CANNOT_ENTER_DIRECTORY, _url.prettyUrl()); completeQueue.removeAll (cmd); return; } completeQueue.removeAll (cmd); } if ((myType == ITYPE_BOX || myType == ITYPE_DIR_AND_BOX) && myLType != "LIST" && myLType != "LSUB" && myLType != "LSUBNOCHECK") { KUrl aURL = _url; aURL.setQuery (QString()); const QString encodedUrl = aURL.url(KUrl::LeaveTrailingSlash); // utf-8 if (!_url.query ().isEmpty ()) { QString query = KUrl::fromPercentEncoding (_url.query().toLatin1()); query = query.right (query.length () - 1); if (!query.isEmpty()) { CommandPtr cmd; if (!assureBox (myBox, true)) return; if (!selectInfo.countAvailable() || selectInfo.count()) { cmd = doCommand (imapCommand::clientSearch (query)); if (cmd->result() != "OK") { error(ERR_UNSUPPORTED_ACTION, _url.prettyUrl()); completeQueue.removeAll (cmd); return; } completeQueue.removeAll (cmd); QStringList list = getResults (); int stretch = 0; if (selectInfo.uidNextAvailable ()) stretch = QString::number(selectInfo.uidNext ()).length (); UDSEntry entry; imapCache fake; for (QStringList::ConstIterator it = list.constBegin(); it != list.constEnd(); ++it) { fake.setUid((*it).toULong()); doListEntry (encodedUrl, stretch, &fake); } entry.clear (); listEntry (entry, true); } } } else { if (!assureBox (myBox, true)) return; kDebug(7116) <<"IMAP4: select returned:"; if (selectInfo.recentAvailable ()) kDebug(7116) <<"Recent:" << selectInfo.recent () <<"d"; if (selectInfo.countAvailable ()) kDebug(7116) <<"Count:" << selectInfo.count () <<"d"; if (selectInfo.unseenAvailable ()) kDebug(7116) <<"Unseen:" << selectInfo.unseen () <<"d"; if (selectInfo.uidValidityAvailable ()) kDebug(7116) <<"uidValidity:" << selectInfo.uidValidity () <<"d"; if (selectInfo.flagsAvailable ()) kDebug(7116) <<"Flags:" << selectInfo.flags () <<"d"; if (selectInfo.permanentFlagsAvailable ()) kDebug(7116) <<"PermanentFlags:" << selectInfo.permanentFlags () <<"d"; if (selectInfo.readWriteAvailable ()) kDebug(7116) <<"Access:" << (selectInfo.readWrite ()?"Read/Write" :"Read only"); #ifdef USE_VALIDITY if (selectInfo.uidValidityAvailable () && selectInfo.uidValidity () != myValidity.toULong ()) { //redirect KUrl newUrl = _url; newUrl.setPath ('/' + myBox + ";UIDVALIDITY=" + QString::number(selectInfo.uidValidity ())); kDebug(7116) <<"IMAP4::listDir - redirecting to" << newUrl.prettyUrl(); redirection (newUrl); } else #endif if (selectInfo.count () > 0) { int stretch = 0; if (selectInfo.uidNextAvailable ()) stretch = QString::number(selectInfo.uidNext ()).length (); // kDebug(7116) << selectInfo.uidNext() <<"d used to stretch" << stretch; UDSEntry entry; if (mySequence.isEmpty()) mySequence = "1:*"; bool withSubject = mySection.isEmpty(); if (mySection.isEmpty()) mySection = "UID RFC822.SIZE ENVELOPE"; bool withFlags = mySection.toUpper().contains("FLAGS") ; CommandPtr fetch = sendCommand (imapCommand:: clientFetch (mySequence, mySection)); imapCache *cache; do { while (!parseLoop ()) {} cache = getLastHandled (); if (cache && !fetch->isComplete()) doListEntry (encodedUrl, stretch, cache, withFlags, withSubject); } while (!fetch->isComplete ()); entry.clear (); listEntry (entry, true); } } } if ( !selectInfo.alert().isNull() ) { if ( !myBox.isEmpty() ) { warning( i18n( "Message from %1 while processing '%2': %3", myHost, myBox, selectInfo.alert() ) ); } else { warning( i18n( "Message from %1: %2", myHost, selectInfo.alert() ) ); } selectInfo.setAlert( 0 ); } kDebug(7116) <<"IMAP4Protocol::listDir - Finishing listDir"; finished (); } void IMAP4Protocol::setHost (const QString & _host, quint16 _port, const QString & _user, const QString & _pass) { if (myHost != _host || myPort != _port || myUser != _user || myPass != _pass) { // what's the point of doing 4 string compares to avoid 4 string copies? // DF: I guess to avoid calling closeConnection() unnecessarily. if (!myHost.isEmpty ()) closeConnection (); myHost = _host; if (_port == 0) myPort = (mySSL) ? ImapsPort : ImapPort; else myPort = _port; myUser = _user; myPass = _pass; } } void IMAP4Protocol::parseRelay (const QByteArray & buffer) { if (relayEnabled) { // relay data immediately data( buffer ); mProcessedSize += buffer.size(); processedSize( mProcessedSize ); } else if (cacheOutput) { // collect data if ( !outputBuffer.isOpen() ) { outputBuffer.open(QIODevice::WriteOnly); } outputBuffer.seek( outputBufferIndex ); outputBuffer.write(buffer, buffer.size()); outputBufferIndex += buffer.size(); } } void IMAP4Protocol::parseRelay (ulong len) { if (relayEnabled) totalSize (len); } bool IMAP4Protocol::parseRead(QByteArray & buffer, long len, long relay) { const long int bufLen = 8192; char buf[bufLen]; // FIXME while (buffer.size() < len ) { ssize_t readLen = myRead(buf, qMin(len - buffer.size(), bufLen - 1)); if (readLen == 0) { kDebug(7116) <<"parseRead: readLen == 0 - connection broken"; error (ERR_CONNECTION_BROKEN, myHost); setState(ISTATE_CONNECT); closeConnection(); return false; } if (relay > buffer.size()) { QByteArray relayData; ssize_t relbuf = relay - buffer.size(); int currentRelay = qMin(relbuf, readLen); relayData = QByteArray::fromRawData(buf, currentRelay); parseRelay(relayData); relayData.clear(); } { QBuffer stream( &buffer ); stream.open (QIODevice::WriteOnly); stream.seek (buffer.size ()); stream.write (buf, readLen); stream.close (); } } return (buffer.size() == len); } bool IMAP4Protocol::parseReadLine (QByteArray & buffer, long relay) { if (myHost.isEmpty()) return false; while (true) { ssize_t copyLen = 0; if (readBufferLen > 0) { while (copyLen < readBufferLen && readBuffer[copyLen] != '\n') copyLen++; if (copyLen < readBufferLen) copyLen++; if (relay > 0) { QByteArray relayData; if (copyLen < (ssize_t) relay) relay = copyLen; relayData = QByteArray::fromRawData (readBuffer, relay); parseRelay (relayData); relayData.clear(); // kDebug(7116) <<"relayed :" << relay <<"d"; } // append to buffer { int oldsize = buffer.size(); buffer.resize(oldsize + copyLen); memcpy(buffer.data() + oldsize, readBuffer, copyLen); // kDebug(7116) <<"appended" << copyLen <<"d got now" << buffer.size(); } readBufferLen -= copyLen; if (readBufferLen) memmove(readBuffer, &readBuffer[copyLen], readBufferLen); if (buffer[buffer.size() - 1] == '\n') return true; } if (!isConnected()) { kDebug(7116) <<"parseReadLine - connection broken"; error (ERR_CONNECTION_BROKEN, myHost); setState(ISTATE_CONNECT); closeConnection(); return false; } if (!waitForResponse( responseTimeout() )) { error(ERR_SERVER_TIMEOUT, myHost); setState(ISTATE_CONNECT); closeConnection(); return false; } readBufferLen = read(readBuffer, IMAP_BUFFER - 1); if (readBufferLen == 0) { kDebug(7116) <<"parseReadLine: readBufferLen == 0 - connection broken"; error (ERR_CONNECTION_BROKEN, myHost); setState(ISTATE_CONNECT); closeConnection(); return false; } } } void IMAP4Protocol::setSubURL (const KUrl & _url) { kDebug(7116) <<"IMAP4::setSubURL -" << _url.prettyUrl(); KIO::TCPSlaveBase::setSubUrl (_url); } void IMAP4Protocol::put (const KUrl & _url, int, KIO::JobFlags) { kDebug(7116) <<"IMAP4::put -" << _url.prettyUrl(); // KIO::TCPSlaveBase::put(_url,permissions,flags) QString aBox, aSequence, aLType, aSection, aValidity, aDelimiter, aInfo; enum IMAP_TYPE aType = parseURL (_url, aBox, aSection, aLType, aSequence, aValidity, aDelimiter, aInfo); // see if it is a box if (aType != ITYPE_BOX && aType != ITYPE_DIR_AND_BOX) { if (aBox[aBox.length () - 1] == '/') aBox = aBox.right (aBox.length () - 1); CommandPtr cmd = doCommand (imapCommand::clientCreate (aBox)); if (cmd->result () != "OK") { error (ERR_COULD_NOT_WRITE, _url.prettyUrl()); completeQueue.removeAll (cmd); return; } completeQueue.removeAll (cmd); } else { QList < QByteArray* > bufferList; int length = 0; int result; // Loop until we got 'dataEnd' do { QByteArray *buffer = new QByteArray (); dataReq (); // Request for data result = readData (*buffer); if (result > 0) { bufferList.append (buffer); length += result; } else { delete buffer; } } while (result > 0); if (result != 0) { error (ERR_ABORTED, _url.prettyUrl()); return; } CommandPtr cmd = sendCommand (imapCommand::clientAppend (aBox, aSection, length)); while (!parseLoop ()) {} // see if server is waiting if (!cmd->isComplete () && !getContinuation ().isEmpty ()) { bool sendOk = true; ulong wrote = 0; QByteArray *buffer; QListIterator it(bufferList); // send data to server while (it.hasNext() && sendOk) { buffer = it.next(); sendOk = (write (buffer->data (), buffer->size ()) == (ssize_t) buffer->size ()); wrote += buffer->size (); processedSize(wrote); delete buffer; if (!sendOk) { error (ERR_CONNECTION_BROKEN, myHost); completeQueue.removeAll (cmd); setState(ISTATE_CONNECT); closeConnection(); return; } } parseWriteLine (""); // Wait until cmd is complete, or connection breaks. while (!cmd->isComplete () && getState() != ISTATE_NO) parseLoop (); if ( getState() == ISTATE_NO ) { // TODO KDE4: pass cmd->resultInfo() as third argument. // ERR_CONNECTION_BROKEN expects a host, no way to pass details about the problem. error( ERR_CONNECTION_BROKEN, myHost ); completeQueue.removeAll (cmd); closeConnection(); return; } else if (cmd->result () != "OK") { error( ERR_SLAVE_DEFINED, cmd->resultInfo() ); completeQueue.removeAll (cmd); return; } else { if (hasCapability("UIDPLUS")) { QString uid = cmd->resultInfo(); if ( uid.contains("APPENDUID") ) { uid = uid.section(" ", 2, 2); uid.truncate(uid.length()-1); infoMessage("UID "+uid); } } // MUST reselect to get the new message else if (aBox == getCurrentBox ()) { cmd = doCommand (imapCommand:: clientSelect (aBox, !selectInfo.readWrite ())); completeQueue.removeAll (cmd); } } } else { //error (ERR_COULD_NOT_WRITE, myHost); // Better ship the error message, e.g. "Over Quota" error (ERR_SLAVE_DEFINED, cmd->resultInfo()); completeQueue.removeAll (cmd); return; } completeQueue.removeAll (cmd); } finished (); } void IMAP4Protocol::mkdir (const KUrl & _url, int) { kDebug(7116) <<"IMAP4::mkdir -" << _url.prettyUrl(); QString aBox, aSequence, aLType, aSection, aValidity, aDelimiter, aInfo; parseURL(_url, aBox, aSection, aLType, aSequence, aValidity, aDelimiter, aInfo); kDebug(7116) <<"IMAP4::mkdir - create" << aBox; CommandPtr cmd = doCommand (imapCommand::clientCreate(aBox)); if (cmd->result () != "OK") { kDebug(7116) <<"IMAP4::mkdir -" << cmd->resultInfo(); error (ERR_COULD_NOT_MKDIR, _url.prettyUrl()); completeQueue.removeAll (cmd); return; } completeQueue.removeAll (cmd); // start a new listing to find the type of the folder enum IMAP_TYPE type = parseURL(_url, aBox, aSection, aLType, aSequence, aValidity, aDelimiter, aInfo); if (type == ITYPE_BOX) { bool ask = ( aInfo.contains( "ASKUSER" ) ); if ( ask && messageBox(QuestionYesNo, i18n("The following folder will be created on the server: %1 " "What do you want to store in this folder?", aBox ), i18n("Create Folder"), i18n("&Messages"), i18n("&Subfolders")) == KMessageBox::No ) { cmd = doCommand(imapCommand::clientDelete(aBox)); completeQueue.removeAll (cmd); cmd = doCommand(imapCommand::clientCreate(aBox + aDelimiter)); if (cmd->result () != "OK") { error (ERR_COULD_NOT_MKDIR, _url.prettyUrl()); completeQueue.removeAll (cmd); return; } completeQueue.removeAll (cmd); } } cmd = doCommand(imapCommand::clientSubscribe(aBox)); completeQueue.removeAll(cmd); finished (); } void IMAP4Protocol::copy (const KUrl & src, const KUrl & dest, int, KIO::JobFlags flags) { kDebug(7116) <<"IMAP4::copy - [" << ((flags & KIO::Overwrite) ?"Overwrite" :"NoOverwrite") <<"]" << src.prettyUrl() <<" ->" << dest.prettyUrl(); QString sBox, sSequence, sLType, sSection, sValidity, sDelimiter, sInfo; QString dBox, dSequence, dLType, dSection, dValidity, dDelimiter, dInfo; enum IMAP_TYPE sType = parseURL (src, sBox, sSection, sLType, sSequence, sValidity, sDelimiter, sInfo); enum IMAP_TYPE dType = parseURL (dest, dBox, dSection, dLType, dSequence, dValidity, dDelimiter, dInfo); // see if we have to create anything if (dType != ITYPE_BOX && dType != ITYPE_DIR_AND_BOX) { // this might be konqueror int sub = dBox.indexOf (sBox); // might be moving to upper folder if (sub > 0) { KUrl testDir = dest; QString subDir = dBox.right (dBox.length () - dBox.lastIndexOf ('/')); QString topDir = dBox.left (sub); testDir.setPath ('/' + topDir); dType = parseURL (testDir, topDir, dSection, dLType, dSequence, dValidity, dDelimiter, dInfo); kDebug(7116) <<"IMAP4::copy - checking this destination" << topDir; // see if this is what the user wants if (dType == ITYPE_BOX || dType == ITYPE_DIR_AND_BOX) { kDebug(7116) <<"IMAP4::copy - assuming this destination" << topDir; dBox = topDir; } else { // maybe if we create a new mailbox topDir = '/' + topDir + subDir; testDir.setPath (topDir); kDebug(7116) <<"IMAP4::copy - checking this destination" << topDir; dType = parseURL (testDir, topDir, dSection, dLType, dSequence, dValidity, dDelimiter, dInfo); if (dType != ITYPE_BOX && dType != ITYPE_DIR_AND_BOX) { // ok then we'll create a mailbox CommandPtr cmd = doCommand (imapCommand::clientCreate (topDir)); // on success we'll use it, else we'll just try to create the given dir if (cmd->result () == "OK") { kDebug(7116) <<"IMAP4::copy - assuming this destination" << topDir; dType = ITYPE_BOX; dBox = topDir; } else { completeQueue.removeAll (cmd); cmd = doCommand (imapCommand::clientCreate (dBox)); if (cmd->result () == "OK") dType = ITYPE_BOX; else error (ERR_COULD_NOT_WRITE, dest.prettyUrl()); } completeQueue.removeAll (cmd); } } } } if (sType == ITYPE_MSG || sType == ITYPE_BOX || sType == ITYPE_DIR_AND_BOX) { //select the source box if (!assureBox(sBox, true)) return; kDebug(7116) <<"IMAP4::copy -" << sBox <<" ->" << dBox; //issue copy command CommandPtr cmd = doCommand (imapCommand::clientCopy (dBox, sSequence)); if (cmd->result () != "OK") { kError(5006) <<"IMAP4::copy -" << cmd->resultInfo(); error (ERR_COULD_NOT_WRITE, dest.prettyUrl()); completeQueue.removeAll (cmd); return; } else { if (hasCapability("UIDPLUS")) { QString uid = cmd->resultInfo(); if ( uid.contains("COPYUID") ) { uid = uid.section(" ", 2, 3); uid.truncate(uid.length()-1); infoMessage("UID "+uid); } } } completeQueue.removeAll (cmd); } else { error (ERR_ACCESS_DENIED, src.prettyUrl()); return; } finished (); } void IMAP4Protocol::del (const KUrl & _url, bool isFile) { kDebug(7116) <<"IMAP4::del - [" << (isFile ?"File" :"NoFile") <<"]" << _url.prettyUrl(); QString aBox, aSequence, aLType, aSection, aValidity, aDelimiter, aInfo; enum IMAP_TYPE aType = parseURL (_url, aBox, aSection, aLType, aSequence, aValidity, aDelimiter, aInfo); switch (aType) { case ITYPE_BOX: case ITYPE_DIR_AND_BOX: if (!aSequence.isEmpty ()) { if (aSequence == "*") { if (!assureBox (aBox, false)) return; CommandPtr cmd = doCommand (imapCommand::clientExpunge ()); if (cmd->result () != "OK") { error (ERR_CANNOT_DELETE, _url.prettyUrl()); completeQueue.removeAll (cmd); return; } completeQueue.removeAll (cmd); } else { // if open for read/write if (!assureBox (aBox, false)) return; CommandPtr cmd = doCommand (imapCommand:: clientStore (aSequence, "+FLAGS.SILENT", "\\DELETED")); if (cmd->result () != "OK") { error (ERR_CANNOT_DELETE, _url.prettyUrl()); completeQueue.removeAll (cmd); return; } completeQueue.removeAll (cmd); } } else { if (getCurrentBox() == aBox) { CommandPtr cmd = doCommand(imapCommand::clientClose()); completeQueue.removeAll(cmd); setState(ISTATE_LOGIN); } // We unsubscribe, otherwise we get ghost folders on UW-IMAP CommandPtr cmd = doCommand(imapCommand::clientUnsubscribe(aBox)); completeQueue.removeAll(cmd); cmd = doCommand(imapCommand::clientDelete (aBox)); // If this doesn't work, we try to empty the mailbox first if (cmd->result () != "OK") { completeQueue.removeAll(cmd); if (!assureBox(aBox, false)) return; bool stillOk = true; if (stillOk) { CommandPtr cmd = doCommand( imapCommand::clientStore("1:*", "+FLAGS.SILENT", "\\DELETED")); if (cmd->result () != "OK") stillOk = false; completeQueue.removeAll(cmd); } if (stillOk) { CommandPtr cmd = doCommand(imapCommand::clientClose()); if (cmd->result () != "OK") stillOk = false; completeQueue.removeAll(cmd); setState(ISTATE_LOGIN); } if (stillOk) { CommandPtr cmd = doCommand (imapCommand::clientDelete(aBox)); if (cmd->result () != "OK") stillOk = false; completeQueue.removeAll(cmd); } if (!stillOk) { error (ERR_COULD_NOT_RMDIR, _url.prettyUrl()); return; } } else { completeQueue.removeAll (cmd); } } break; case ITYPE_DIR: { CommandPtr cmd = doCommand (imapCommand::clientDelete (aBox)); if (cmd->result () != "OK") { error (ERR_COULD_NOT_RMDIR, _url.prettyUrl()); completeQueue.removeAll (cmd); return; } completeQueue.removeAll (cmd); } break; case ITYPE_MSG: { // if open for read/write if (!assureBox (aBox, false)) return; CommandPtr cmd = doCommand (imapCommand:: clientStore (aSequence, "+FLAGS.SILENT", "\\DELETED")); if (cmd->result () != "OK") { error (ERR_CANNOT_DELETE, _url.prettyUrl()); completeQueue.removeAll (cmd); return; } completeQueue.removeAll (cmd); } break; case ITYPE_UNKNOWN: case ITYPE_ATTACH: error (ERR_CANNOT_DELETE, _url.prettyUrl()); break; } finished (); } /* * Copy a mail: data = 'C' + srcURL (KUrl) + destURL (KUrl) * Capabilities: data = 'c'. Result shipped in infoMessage() signal * No-op: data = 'N' * Namespace: data = 'n'. Result shipped in infoMessage() signal * The format is: section=namespace=delimiter * Note that the namespace can be empty * Unsubscribe: data = 'U' + URL (KUrl) * Subscribe: data = 'u' + URL (KUrl) * Change the status: data = 'S' + URL (KUrl) + Flags (QCString) * ACL commands: data = 'A' + command + URL (KUrl) + command-dependent args * AnnotateMore commands: data = 'M' + 'G'et/'S'et + URL + entry + command-dependent args * Search: data = 'E' + URL (KUrl) * Quota commands: data = 'Q' + 'R'oot/'G'et/'S'et + URL + entry + command-dependent args * Custom command: data = 'X' + 'N'ormal/'E'xtended + command + command-dependent args */ void IMAP4Protocol::special (const QByteArray & aData) { kDebug(7116) <<"IMAP4Protocol::special"; if (!makeLogin()) return; QDataStream stream( aData ); int tmp; stream >> tmp; switch (tmp) { case 'C': { // copy KUrl src; KUrl dest; stream >> src >> dest; copy(src, dest, 0, false); break; } case 'c': { // capabilities infoMessage(imapCapabilities.join(" ")); finished(); break; } case 'N': { // NOOP CommandPtr cmd = doCommand(imapCommand::clientNoop()); if (cmd->result () != "OK") { kDebug(7116) <<"NOOP did not succeed - connection broken"; completeQueue.removeAll (cmd); error (ERR_CONNECTION_BROKEN, myHost); return; } completeQueue.removeAll (cmd); finished(); break; } case 'n': { // namespace in the form "section=namespace=delimiter" // entries are separated by , infoMessage( imapNamespaces.join(",") ); finished(); break; } case 'U': { // unsubscribe KUrl _url; stream >> _url; QString aBox, aSequence, aLType, aSection, aValidity, aDelimiter, aInfo; parseURL (_url, aBox, aSection, aLType, aSequence, aValidity, aDelimiter, aInfo); CommandPtr cmd = doCommand(imapCommand::clientUnsubscribe(aBox)); if (cmd->result () != "OK") { completeQueue.removeAll (cmd); error(ERR_SLAVE_DEFINED, i18n("Unsubscribe of folder %1 " "failed. The server returned: %2", _url.prettyUrl(), cmd->resultInfo())); return; } completeQueue.removeAll (cmd); finished(); break; } case 'u': { // subscribe KUrl _url; stream >> _url; QString aBox, aSequence, aLType, aSection, aValidity, aDelimiter, aInfo; parseURL (_url, aBox, aSection, aLType, aSequence, aValidity, aDelimiter, aInfo); CommandPtr cmd = doCommand(imapCommand::clientSubscribe(aBox)); if (cmd->result () != "OK") { completeQueue.removeAll (cmd); error(ERR_SLAVE_DEFINED, i18n("Subscribe of folder %1 " "failed. The server returned: %2", _url.prettyUrl(), cmd->resultInfo())); return; } completeQueue.removeAll (cmd); finished(); break; } case 'A': { // acl int cmd; stream >> cmd; if ( hasCapability( "ACL" ) ) { specialACLCommand( cmd, stream ); } else { error( ERR_UNSUPPORTED_ACTION, QString::fromLatin1("ACL") ); } break; } case 'M': { // annotatemore int cmd; stream >> cmd; if ( hasCapability( "ANNOTATEMORE" ) ) { specialAnnotateMoreCommand( cmd, stream ); } else { error( ERR_UNSUPPORTED_ACTION, QString::fromLatin1("ANNOTATEMORE") ); } break; } case 'Q': { // quota int cmd; stream >> cmd; if ( hasCapability( "QUOTA" ) ) { specialQuotaCommand( cmd, stream ); } else { error( ERR_UNSUPPORTED_ACTION, QString::fromLatin1("QUOTA") ); } break; } case 'S': { // status KUrl _url; QByteArray newFlags; stream >> _url >> newFlags; QString aBox, aSequence, aLType, aSection, aValidity, aDelimiter, aInfo; parseURL (_url, aBox, aSection, aLType, aSequence, aValidity, aDelimiter, aInfo); if (!assureBox(aBox, false)) return; // make sure we only touch flags we know QByteArray knownFlags = "\\SEEN \\ANSWERED \\FLAGGED \\DRAFT"; const imapInfo info = getSelected(); if ( info.permanentFlagsAvailable() && (info.permanentFlags() & imapInfo::User) ) { knownFlags += " KMAILFORWARDED KMAILTODO KMAILWATCHED KMAILIGNORED $FORWARDED $TODO $WATCHED $IGNORED"; } CommandPtr cmd = doCommand (imapCommand:: clientStore (aSequence, "-FLAGS.SILENT", knownFlags)); if (cmd->result () != "OK") { completeQueue.removeAll (cmd); error(ERR_SLAVE_DEFINED, i18n("Changing the flags of message %1 " "failed with %2.", _url.prettyUrl(), cmd->result())); return; } completeQueue.removeAll (cmd); if (!newFlags.isEmpty()) { cmd = doCommand (imapCommand:: clientStore (aSequence, "+FLAGS.SILENT", newFlags)); if (cmd->result () != "OK") { completeQueue.removeAll (cmd); error(ERR_SLAVE_DEFINED, i18n("Silent Changing the flags of message %1 " "failed with %2.", _url.prettyUrl(), cmd->result())); return; } completeQueue.removeAll (cmd); } finished(); break; } case 's': { // seen KUrl _url; bool seen; QByteArray newFlags; stream >> _url >> seen; QString aBox, aSequence, aLType, aSection, aValidity, aDelimiter, aInfo; parseURL (_url, aBox, aSection, aLType, aSequence, aValidity, aDelimiter, aInfo); if ( !assureBox(aBox, true) ) // read-only because changing SEEN should be possible even then return; CommandPtr cmd; if ( seen ) cmd = doCommand( imapCommand::clientStore( aSequence, "+FLAGS.SILENT", "\\SEEN" ) ); else cmd = doCommand( imapCommand::clientStore( aSequence, "-FLAGS.SILENT", "\\SEEN" ) ); if (cmd->result () != "OK") { completeQueue.removeAll (cmd); error(ERR_COULD_NOT_WRITE, i18n( "Changing the flags of message %1 failed.", _url.prettyUrl() ) ); return; } completeQueue.removeAll (cmd); finished(); break; } case 'E': { // search specialSearchCommand( stream ); break; } case 'X': { // custom command specialCustomCommand( stream ); break; } default: kWarning(7116) <<"Unknown command in special():" << tmp; error( ERR_UNSUPPORTED_ACTION, QString(QChar(tmp)) ); break; } } void IMAP4Protocol::specialACLCommand( int command, QDataStream& stream ) { // All commands start with the URL to the box KUrl _url; stream >> _url; QString aBox, aSequence, aLType, aSection, aValidity, aDelimiter, aInfo; parseURL (_url, aBox, aSection, aLType, aSequence, aValidity, aDelimiter, aInfo); switch( command ) { case 'S': // SETACL { QString user, acl; stream >> user >> acl; kDebug(7116) <<"SETACL" << aBox << user << acl; CommandPtr cmd = doCommand(imapCommand::clientSetACL(aBox, user, acl)); if (cmd->result () != "OK") { error(ERR_SLAVE_DEFINED, i18n("Setting the Access Control List on folder %1 " "for user %2 failed. The server returned: %3", _url.prettyUrl(), user, cmd->resultInfo())); return; } completeQueue.removeAll (cmd); finished(); break; } case 'D': // DELETEACL { QString user; stream >> user; kDebug(7116) <<"DELETEACL" << aBox << user; CommandPtr cmd = doCommand(imapCommand::clientDeleteACL(aBox, user)); if (cmd->result () != "OK") { error(ERR_SLAVE_DEFINED, i18n("Deleting the Access Control List on folder %1 " "for user %2 failed. The server returned: %3", _url.prettyUrl(), user, cmd->resultInfo())); return; } completeQueue.removeAll (cmd); finished(); break; } case 'G': // GETACL { kDebug(7116) <<"GETACL" << aBox; CommandPtr cmd = doCommand(imapCommand::clientGetACL(aBox)); if (cmd->result () != "OK") { error(ERR_SLAVE_DEFINED, i18n("Retrieving the Access Control List on folder %1 " "failed. The server returned: %2", _url.prettyUrl(), cmd->resultInfo())); return; } // Returning information to the application from a special() command isn't easy. // I'm reusing the infoMessage trick seen above (for capabilities), but this // limits me to a string instead of a stringlist. Using DQUOTE as separator, // because it's forbidden in userids by rfc3501 kDebug(7116) << getResults(); infoMessage(getResults().join( "\"" )); finished(); break; } case 'L': // LISTRIGHTS { // Do we need this one? It basically shows which rights are tied together, but that's all? error( ERR_UNSUPPORTED_ACTION, QString(QChar(command)) ); break; } case 'M': // MYRIGHTS { kDebug(7116) <<"MYRIGHTS" << aBox; CommandPtr cmd = doCommand(imapCommand::clientMyRights(aBox)); if (cmd->result () != "OK") { error(ERR_SLAVE_DEFINED, i18n("Retrieving the Access Control List on folder %1 " "failed. The server returned: %2", _url.prettyUrl(), cmd->resultInfo())); return; } QStringList lst = getResults(); kDebug(7116) <<"myrights results:" << lst; if ( !lst.isEmpty() ) { Q_ASSERT( lst.count() == 1 ); infoMessage( lst.first() ); } finished(); break; } default: kWarning(7116) <<"Unknown special ACL command:" << command; error( ERR_UNSUPPORTED_ACTION, QString(QChar(command)) ); } } void IMAP4Protocol::specialSearchCommand( QDataStream& stream ) { kDebug(7116) <<"IMAP4Protocol::specialSearchCommand"; KUrl _url; stream >> _url; QString aBox, aSequence, aLType, aSection, aValidity, aDelimiter, aInfo; parseURL (_url, aBox, aSection, aLType, aSequence, aValidity, aDelimiter, aInfo); if (!assureBox(aBox, true)) return; CommandPtr cmd = doCommand (imapCommand::clientSearch( aSection )); if (cmd->result () != "OK") { error(ERR_SLAVE_DEFINED, i18n("Searching of folder %1 " "failed. The server returned: %2", aBox, cmd->resultInfo())); return; } completeQueue.removeAll(cmd); QStringList lst = getResults(); kDebug(7116) <<"IMAP4Protocol::specialSearchCommand '" << aSection << "' returns" << lst; infoMessage( lst.join( " " ) ); finished(); } void IMAP4Protocol::specialCustomCommand( QDataStream& stream ) { kDebug(7116) << "IMAP4Protocol::specialCustomCommand" << endl; QString command, arguments; int type; stream >> type; stream >> command >> arguments; /** * In 'normal' mode we send the command with all information in one go * and retrieve the result. */ if ( type == 'N' ) { kDebug(7116) << "IMAP4Protocol::specialCustomCommand: normal mode" << endl; CommandPtr cmd = doCommand (imapCommand::clientCustom( command, arguments )); if (cmd->result () != "OK") { error( ERR_SLAVE_DEFINED, i18n( "Custom command %1:%2 failed. The server returned: %3", command, arguments, cmd->resultInfo() ) ); return; } completeQueue.removeAll(cmd); QStringList lst = getResults(); kDebug(7116) << "IMAP4Protocol::specialCustomCommand '" << command << ":" << arguments << "' returns " << lst << endl; infoMessage( lst.join( " " ) ); finished(); } else /** * In 'extended' mode we send a first header and push the data of the request in * streaming mode. */ if ( type == 'E' ) { kDebug(7116) << "IMAP4Protocol::specialCustomCommand: extended mode" << endl; CommandPtr cmd = sendCommand (imapCommand::clientCustom( command, QString() )); while ( !parseLoop () ) {}; // see if server is waiting if (!cmd->isComplete () && !getContinuation ().isEmpty ()) { const QByteArray buffer = arguments.toUtf8(); // send data to server bool sendOk = (write (buffer.data (), buffer.size ()) == (ssize_t)buffer.size ()); processedSize( buffer.size() ); if ( !sendOk ) { error ( ERR_CONNECTION_BROKEN, myHost ); completeQueue.removeAll ( cmd ); setState(ISTATE_CONNECT); closeConnection(); return; } } parseWriteLine (""); do { while (!parseLoop ()) {}; } while (!cmd->isComplete ()); completeQueue.removeAll (cmd); QStringList lst = getResults(); kDebug(7116) << "IMAP4Protocol::specialCustomCommand: returns " << lst << endl; infoMessage( lst.join( " " ) ); finished (); } } void IMAP4Protocol::specialAnnotateMoreCommand( int command, QDataStream& stream ) { // All commands start with the URL to the box KUrl _url; stream >> _url; QString aBox, aSequence, aLType, aSection, aValidity, aDelimiter, aInfo; parseURL (_url, aBox, aSection, aLType, aSequence, aValidity, aDelimiter, aInfo); switch( command ) { case 'S': // SETANNOTATION { // Params: // KUrl URL of the mailbox // QString entry (should be an actual entry name, no % or *; empty for server entries) // QMap attributes (name and value) QString entry; QMap attributes; stream >> entry >> attributes; kDebug(7116) <<"SETANNOTATION" << aBox << entry << attributes.count() <<" attributes"; CommandPtr cmd = doCommand(imapCommand::clientSetAnnotation(aBox, entry, attributes)); if (cmd->result () != "OK") { error(ERR_SLAVE_DEFINED, i18n("Setting the annotation %1 on folder %2 " " failed. The server returned: %3", entry, _url.prettyUrl(), cmd->resultInfo())); return; } completeQueue.removeAll (cmd); finished(); break; } case 'G': // GETANNOTATION. { // Params: // KUrl URL of the mailbox // QString entry (should be an actual entry name, no % or *; empty for server entries) // QStringList attributes (list of attributes to be retrieved, possibly with % or *) QString entry; QStringList attributeNames; stream >> entry >> attributeNames; kDebug(7116) <<"GETANNOTATION" << aBox << entry << attributeNames; CommandPtr cmd = doCommand(imapCommand::clientGetAnnotation(aBox, entry, attributeNames)); if (cmd->result () != "OK") { error(ERR_SLAVE_DEFINED, i18n("Retrieving the annotation %1 on folder %2 " "failed. The server returned: %3", entry, _url.prettyUrl(), cmd->resultInfo())); return; } // Returning information to the application from a special() command isn't easy. // I'm reusing the infoMessage trick seen above (for capabilities and acls), but this // limits me to a string instead of a stringlist. Let's use \r as separator. kDebug(7116) << getResults(); infoMessage(getResults().join( "\r" )); finished(); break; } default: kWarning(7116) <<"Unknown special annotate command:" << command; error( ERR_UNSUPPORTED_ACTION, QString(QChar(command)) ); } } void IMAP4Protocol::specialQuotaCommand( int command, QDataStream& stream ) { // All commands start with the URL to the box KUrl _url; stream >> _url; QString aBox, aSequence, aLType, aSection, aValidity, aDelimiter, aInfo; parseURL (_url, aBox, aSection, aLType, aSequence, aValidity, aDelimiter, aInfo); switch( command ) { case 'R': // GETQUOTAROOT { kDebug(7116) <<"QUOTAROOT" << aBox; CommandPtr cmd = doCommand(imapCommand::clientGetQuotaroot( aBox ) ); if (cmd->result () != "OK") { error(ERR_SLAVE_DEFINED, i18n("Retrieving the quota root information on folder %1 " "failed. The server returned: %2", _url.prettyUrl(), cmd->resultInfo())); return; } infoMessage(getResults().join( "\r" )); finished(); break; } case 'G': // GETQUOTA { kDebug(7116) <<"GETQUOTA command"; kWarning(7116) <<"UNIMPLEMENTED"; break; } case 'S': // SETQUOTA { kDebug(7116) <<"SETQUOTA command"; kWarning(7116) <<"UNIMPLEMENTED"; break; } default: kWarning(7116) <<"Unknown special quota command:" << command; error( ERR_UNSUPPORTED_ACTION, QString(QChar(command)) ); } } void IMAP4Protocol::rename (const KUrl & src, const KUrl & dest, KIO::JobFlags flags) { kDebug(7116) <<"IMAP4::rename - [" << ((flags & KIO::Overwrite) ?"Overwrite" :"NoOverwrite") <<"]" << src <<" ->" << dest; QString sBox, sSequence, sLType, sSection, sValidity, sDelimiter, sInfo; QString dBox, dSequence, dLType, dSection, dValidity, dDelimiter, dInfo; enum IMAP_TYPE sType = parseURL (src, sBox, sSection, sLType, sSequence, sValidity, sDelimiter, sInfo, false); enum IMAP_TYPE dType = parseURL (dest, dBox, dSection, dLType, dSequence, dValidity, dDelimiter, dInfo, false); if (dType == ITYPE_UNKNOWN) { switch (sType) { case ITYPE_BOX: case ITYPE_DIR: case ITYPE_DIR_AND_BOX: { if (getState() == ISTATE_SELECT && sBox == getCurrentBox()) { kDebug(7116) <<"IMAP4::rename - close" << getCurrentBox(); // mailbox can only be renamed if it is closed CommandPtr cmd = doCommand (imapCommand::clientClose()); bool ok = cmd->result() == "OK"; completeQueue.removeAll(cmd); if (!ok) { error(ERR_CANNOT_RENAME, i18n("Unable to close mailbox.")); return; } setState(ISTATE_LOGIN); } CommandPtr cmd = doCommand (imapCommand::clientRename (sBox, dBox)); if (cmd->result () != "OK") { error (ERR_CANNOT_RENAME, cmd->result ()); completeQueue.removeAll (cmd); return; } completeQueue.removeAll (cmd); } break; case ITYPE_MSG: case ITYPE_ATTACH: case ITYPE_UNKNOWN: error (ERR_CANNOT_RENAME, src.prettyUrl()); break; } } else { error (ERR_CANNOT_RENAME, src.prettyUrl()); return; } finished (); } void IMAP4Protocol::slave_status () { bool connected = (getState() != ISTATE_NO) && isConnected(); kDebug(7116) <<"IMAP4::slave_status" << connected; slaveStatus ( connected ? myHost : QString(), connected ); } void IMAP4Protocol::dispatch (int command, const QByteArray & data) { kDebug(7116) <<"IMAP4::dispatch - command=" << command; KIO::TCPSlaveBase::dispatch (command, data); } void IMAP4Protocol::stat (const KUrl & _url) { kDebug(7116) <<"IMAP4::stat -" << _url.prettyUrl(); QString aBox, aSequence, aLType, aSection, aValidity, aDelimiter, aInfo; // parseURL with caching enum IMAP_TYPE aType = parseURL (_url, aBox, aSection, aLType, aSequence, aValidity, aDelimiter, aInfo, true); UDSEntry entry; entry.insert( UDSEntry::UDS_NAME, aBox); if (!aSection.isEmpty()) { if (getState() == ISTATE_SELECT && aBox == getCurrentBox()) { CommandPtr cmd = doCommand (imapCommand::clientClose()); bool ok = cmd->result() == "OK"; completeQueue.removeAll(cmd); if (!ok) { error(ERR_COULD_NOT_STAT, i18n("Unable to close mailbox.")); return; } setState(ISTATE_LOGIN); } bool ok = false; QString cmdInfo; if (aType == ITYPE_MSG || aType == ITYPE_ATTACH) ok = true; else { CommandPtr cmd = doCommand(imapCommand::clientStatus(aBox, aSection)); ok = cmd->result() == "OK"; cmdInfo = cmd->resultInfo(); completeQueue.removeAll(cmd); } if (!ok) { bool found = false; CommandPtr cmd = doCommand (imapCommand::clientList ("", aBox)); if (cmd->result () == "OK") { for (QList< imapList >::Iterator it = listResponses.begin (); it != listResponses.end (); ++it) { if (aBox == (*it).name ()) found = true; } } completeQueue.removeAll (cmd); if (found) error(ERR_COULD_NOT_STAT, i18n("Unable to get information about folder %1. The server replied: %2", aBox, cmdInfo)); else error(KIO::ERR_DOES_NOT_EXIST, aBox); return; } if ((aSection == "UIDNEXT" && getStatus().uidNextAvailable()) || (aSection == "UNSEEN" && getStatus().unseenAvailable())) { entry.insert( UDSEntry::UDS_SIZE, (aSection == "UIDNEXT") ? getStatus().uidNext() : getStatus().unseen()); } } else if (aType == ITYPE_BOX || aType == ITYPE_DIR_AND_BOX || aType == ITYPE_MSG || aType == ITYPE_ATTACH) { ulong validity = 0; // see if the box is already in select/examine state if (aBox == getCurrentBox ()) validity = selectInfo.uidValidity (); else { // do a status lookup on the box // only do this if the box is not selected // the server might change the validity for new select/examine CommandPtr cmd = doCommand (imapCommand::clientStatus (aBox, "UIDVALIDITY")); completeQueue.removeAll (cmd); validity = getStatus ().uidValidity (); } #ifdef __GNUC__ #warning This is temporary since Dec 2000 and makes most of the below code invalid #endif validity = 0; // temporary if (aType == ITYPE_BOX || aType == ITYPE_DIR_AND_BOX) { // has no or an invalid uidvalidity if (validity > 0 && validity != aValidity.toULong ()) { //redirect KUrl newUrl = _url; newUrl.setPath ('/' + aBox + ";UIDVALIDITY=" + QString::number(validity)); kDebug(7116) <<"IMAP4::stat - redirecting to" << newUrl.prettyUrl(); redirection (newUrl); } } else if (aType == ITYPE_MSG || aType == ITYPE_ATTACH) { //must determine if this message exists //cause konqueror will check this on paste operations // has an invalid uidvalidity // or no messages in box if (validity > 0 && validity != aValidity.toULong ()) { aType = ITYPE_UNKNOWN; kDebug(7116) <<"IMAP4::stat - url has invalid validity [" << validity <<"d]" << _url.prettyUrl(); } } } entry.insert( UDSEntry::UDS_MIME_TYPE,getMimeType (aType)); //kDebug(7116) <<"IMAP4: stat:" << atom.m_str; switch (aType) { case ITYPE_DIR: entry.insert( UDSEntry::UDS_FILE_TYPE, S_IFDIR); break; case ITYPE_BOX: case ITYPE_DIR_AND_BOX: entry.insert(UDSEntry::UDS_FILE_TYPE, S_IFDIR); break; case ITYPE_MSG: case ITYPE_ATTACH: entry.insert(UDSEntry::UDS_FILE_TYPE, S_IFREG); break; case ITYPE_UNKNOWN: error (ERR_DOES_NOT_EXIST, _url.prettyUrl()); break; } statEntry (entry); kDebug(7116) <<"IMAP4::stat - Finishing stat"; finished (); } void IMAP4Protocol::openConnection() { if (makeLogin()) connected(); } void IMAP4Protocol::closeConnection() { if (getState() == ISTATE_NO) return; if (getState() == ISTATE_SELECT && metaData("expunge") == "auto") { CommandPtr cmd = doCommand (imapCommand::clientExpunge()); completeQueue.removeAll (cmd); } if (getState() != ISTATE_CONNECT) { CommandPtr cmd = doCommand (imapCommand::clientLogout()); completeQueue.removeAll (cmd); } disconnectFromHost(); setState(ISTATE_NO); completeQueue.clear(); sentQueue.clear(); lastHandled = 0; currentBox.clear(); readBufferLen = 0; } bool IMAP4Protocol::makeLogin () { if (getState () == ISTATE_LOGIN || getState () == ISTATE_SELECT) return true; kDebug(7116) <<"IMAP4::makeLogin - checking login"; bool alreadyConnected = getState() == ISTATE_CONNECT; kDebug(7116) <<"IMAP4::makeLogin - alreadyConnected" << alreadyConnected; if (alreadyConnected || connectToHost (( mySSL ? IMAP_SSL_PROTOCOL : IMAP_PROTOCOL ), myHost, myPort)) { // fcntl (m_iSock, F_SETFL, (fcntl (m_iSock, F_GETFL) | O_NDELAY)); setState(ISTATE_CONNECT); myAuth = metaData("auth"); myTLS = metaData("tls"); kDebug(7116) <<"myAuth:" << myAuth; CommandPtr cmd; unhandled.clear (); if (!alreadyConnected) while (!parseLoop ()) {} //get greeting QString greeting; if (!unhandled.isEmpty()) greeting = unhandled.first().trimmed(); unhandled.clear (); //get rid of it cmd = doCommand (CommandPtr(new imapCommand ("CAPABILITY", ""))); kDebug(7116) <<"IMAP4: setHost: capability"; for (QStringList::const_iterator it = imapCapabilities.constBegin (); it != imapCapabilities.constEnd (); ++it) { kDebug(7116) <<"'" << (*it) <<"'"; } completeQueue.removeAll (cmd); if (!hasCapability("IMAP4") && !hasCapability("IMAP4rev1")) { error(ERR_COULD_NOT_LOGIN, i18n("The server %1 supports neither " "IMAP4 nor IMAP4rev1.\nIt identified itself with: %2", myHost, greeting)); closeConnection(); return false; } if (metaData("nologin") == "on") return true; if (myTLS == "on" && !hasCapability(QString("STARTTLS"))) { error(ERR_COULD_NOT_LOGIN, i18n("The server does not support TLS.\n" "Disable this security feature to connect unencrypted.")); closeConnection(); return false; } if ((myTLS == "on" /*###|| ( canUseTLS() && myTLS != "off")*/) && hasCapability(QString("STARTTLS"))) { CommandPtr cmd = doCommand (imapCommand::clientStartTLS()); if (cmd->result () == "OK") { completeQueue.removeAll(cmd); if (startSsl()) { kDebug(7116) <<"TLS mode has been enabled."; CommandPtr cmd2 = doCommand (CommandPtr(new imapCommand ("CAPABILITY", ""))); for (QStringList::const_iterator it = imapCapabilities.constBegin (); it != imapCapabilities.constEnd (); ++it) { kDebug(7116) <<"'" << (*it) <<"'"; } completeQueue.removeAll (cmd2); } else { kWarning(7116) <<"TLS mode setup has failed. Aborting."; error (ERR_COULD_NOT_LOGIN, i18n("Starting TLS failed.")); closeConnection(); return false; } } else completeQueue.removeAll(cmd); } if (!myAuth.isEmpty () && myAuth != "*" && !hasCapability (QString ("AUTH=") + myAuth)) { error (ERR_COULD_NOT_LOGIN, i18n("The authentication method %1 is not " "supported by the server.", myAuth)); closeConnection(); return false; } if ( greeting.contains( QRegExp( "Cyrus IMAP4 v2.1" ) ) ) { removeCapability( "ANNOTATEMORE" ); } // starting from Cyrus IMAP 2.3.9, shared seen flags are available QRegExp regExp( "Cyrus\\sIMAP[4]{0,1}\\sv(\\d+)\\.(\\d+)\\.(\\d+)", Qt::CaseInsensitive ); if ( regExp.indexIn( greeting ) >= 0 ) { const int major = regExp.cap( 1 ).toInt(); const int minor = regExp.cap( 2 ).toInt(); const int patch = regExp.cap( 3 ).toInt(); if ( major > 2 || (major == 2 && (minor > 3 || (minor == 3 && patch > 9))) ) { kDebug(7116) << "Cyrus IMAP >= 2.3.9 detected, enabling shared seen flag support"; imapCapabilities.append( "x-kmail-sharedseen" ); } } kDebug(7116) <<"IMAP4::makeLogin - attempting login"; KIO::AuthInfo authInfo; authInfo.username = myUser; authInfo.password = myPass; authInfo.prompt = i18n ("Username and password for your IMAP account:"); kDebug(7116) <<"IMAP4::makeLogin - open_PassDlg said user=" << myUser <<" pass=xx"; QString resultInfo; if (myAuth.isEmpty () || myAuth == "*") { if (myUser.isEmpty () || myPass.isEmpty ()) { if(openPasswordDialog (authInfo)) { myUser = authInfo.username; myPass = authInfo.password; } } if (!clientLogin (myUser, myPass, resultInfo)) error(KIO::ERR_COULD_NOT_AUTHENTICATE, i18n("Unable to login. Probably the " "password is wrong.\nThe server %1 replied:\n%2", myHost, resultInfo)); } else { if (!clientAuthenticate (this, authInfo, myHost, myAuth, mySSL, resultInfo)) error(KIO::ERR_COULD_NOT_AUTHENTICATE, i18n("Unable to authenticate via %1.\n" "The server %2 replied:\n%3", myAuth, myHost, resultInfo)); else { myUser = authInfo.username; myPass = authInfo.password; } } if ( hasCapability("NAMESPACE") ) { // get all namespaces and save the namespace - delimiter association cmd = doCommand( imapCommand::clientNamespace() ); if (cmd->result () == "OK") { kDebug(7116) <<"makeLogin - registered namespaces"; } completeQueue.removeAll (cmd); } // get the default delimiter (empty listing) cmd = doCommand( imapCommand::clientList("", "") ); if (cmd->result () == "OK") { QList< imapList >::Iterator it = listResponses.begin(); if ( it != listResponses.end() ) { namespaceToDelimiter[QString()] = (*it).hierarchyDelimiter(); kDebug(7116) <<"makeLogin - delimiter for empty ns='" << (*it).hierarchyDelimiter() <<"'"; if ( !hasCapability("NAMESPACE") ) { // server does not support namespaces QString nsentry = QString::number( 0 ) + "==" + (*it).hierarchyDelimiter(); imapNamespaces.append( nsentry ); } } } completeQueue.removeAll (cmd); } else { kDebug(7116) <<"makeLogin - NO login"; } return getState() == ISTATE_LOGIN; } void IMAP4Protocol::parseWriteLine (const QString & aStr) { //kDebug(7116) <<"Writing:" << aStr; QByteArray writer = aStr.toUtf8(); int len = writer.length(); // append CRLF if necessary if (len == 0 || (writer[len - 1] != '\n')) { len += 2; writer += "\r\n"; } // write it write(writer.data(), len); } QString IMAP4Protocol::getMimeType (enum IMAP_TYPE aType) { switch (aType) { case ITYPE_DIR: return "inode/directory"; break; case ITYPE_BOX: return "message/digest"; break; case ITYPE_DIR_AND_BOX: return "message/directory"; break; case ITYPE_MSG: return "message/rfc822"; break; // this should be handled by flushOutput case ITYPE_ATTACH: return "application/octet-stream"; break; case ITYPE_UNKNOWN: default: return "unknown/unknown"; } } void IMAP4Protocol::doListEntry (const KUrl & _url, int stretch, imapCache * cache, bool withFlags, bool withSubject) { KUrl aURL = _url; aURL.setQuery (QString()); const QString encodedUrl = aURL.url(KUrl::LeaveTrailingSlash); // utf-8 doListEntry(encodedUrl, stretch, cache, withFlags, withSubject); } void IMAP4Protocol::doListEntry (const QString & encodedUrl, int stretch, imapCache * cache, bool withFlags, bool withSubject) { if (cache) { UDSEntry entry; entry.clear (); const QString uid = QString::number(cache->getUid()); QString tmp = uid; if (stretch > 0) { tmp = "0000000000000000" + uid; tmp = tmp.right (stretch); } if (withSubject) { mailHeader *header = cache->getHeader(); if (header) tmp += ' ' + header->getSubject(); } entry.insert (UDSEntry::UDS_NAME,tmp); tmp = encodedUrl; // utf-8 if (tmp[tmp.length () - 1] != '/') tmp += '/'; tmp += ";UID=" + uid; entry.insert( UDSEntry::UDS_URL, tmp); entry.insert(UDSEntry::UDS_FILE_TYPE,S_IFREG); entry.insert(UDSEntry::UDS_SIZE, cache->getSize()); entry.insert( UDSEntry::UDS_MIME_TYPE, QString::fromLatin1("message/rfc822")); entry.insert(UDSEntry::UDS_USER,myUser); entry.insert( KIO::UDSEntry::UDS_ACCESS, (withFlags) ? cache->getFlags() : S_IRUSR | S_IXUSR | S_IWUSR); listEntry (entry, false); } } void IMAP4Protocol::doListEntry (const KUrl & _url, const QString & myBox, const imapList & item, bool appendPath) { KUrl aURL = _url; aURL.setQuery (QString()); UDSEntry entry; int hdLen = item.hierarchyDelimiter().length(); { // mailboxName will be appended to the path if appendPath is true QString mailboxName = item.name (); // some beautification if ( mailboxName.startsWith(myBox) && mailboxName.length() > myBox.length()) { mailboxName = mailboxName.right (mailboxName.length () - myBox.length ()); } if (mailboxName[0] == '/') mailboxName = mailboxName.right (mailboxName.length () - 1); if (mailboxName.left(hdLen) == item.hierarchyDelimiter()) mailboxName = mailboxName.right(mailboxName.length () - hdLen); if (mailboxName.right(hdLen) == item.hierarchyDelimiter()) mailboxName.truncate(mailboxName.length () - hdLen); QString tmp; if (!item.hierarchyDelimiter().isEmpty() && mailboxName.contains(item.hierarchyDelimiter()) ) tmp = mailboxName.section(item.hierarchyDelimiter(), -1); else tmp = mailboxName; // konqueror will die with an assertion failure otherwise if (tmp.isEmpty ()) tmp = ".."; if (!tmp.isEmpty ()) { entry.insert(UDSEntry::UDS_NAME,tmp); if (!item.noSelect ()) { if (!item.noInferiors ()) { tmp = "message/directory"; } else { tmp = "message/digest"; } entry.insert(UDSEntry::UDS_MIME_TYPE,tmp); mailboxName += '/'; // explicitly set this as a directory for KFileDialog entry.insert(UDSEntry::UDS_FILE_TYPE,S_IFDIR); } else if (!item.noInferiors ()) { entry.insert(UDSEntry::UDS_MIME_TYPE, QString::fromLatin1("inode/directory")); mailboxName += '/'; // explicitly set this as a directory for KFileDialog entry.insert(UDSEntry::UDS_FILE_TYPE,S_IFDIR); } else { entry.insert(UDSEntry::UDS_MIME_TYPE,QString::fromLatin1("unknown/unknown")); } QString path = aURL.path(); if (appendPath) { if (path[path.length() - 1] == '/' && !path.isEmpty() && path != "/") path.truncate(path.length() - 1); if (!path.isEmpty() && path != "/" && path.right(hdLen) != item.hierarchyDelimiter()) { path += item.hierarchyDelimiter(); } path += mailboxName; if (path.toUpper() == "/INBOX/") { // make sure the client can rely on INBOX path = path.toUpper(); } } aURL.setPath(path); tmp = aURL.url(KUrl::LeaveTrailingSlash); // utf-8 entry.insert(UDSEntry::UDS_URL, tmp); entry.insert( UDSEntry::UDS_USER, myUser); entry.insert( UDSEntry::UDS_ACCESS, S_IRUSR | S_IXUSR | S_IWUSR); entry.insert( UDSEntry::UDS_EXTRA,item.attributesAsString()); listEntry (entry, false); } } } enum IMAP_TYPE IMAP4Protocol::parseURL (const KUrl & _url, QString & _box, QString & _section, QString & _type, QString & _uid, QString & _validity, QString & _hierarchyDelimiter, QString & _info, bool cache) { enum IMAP_TYPE retVal; retVal = ITYPE_UNKNOWN; imapParser::parseURL (_url, _box, _section, _type, _uid, _validity, _info); // kDebug(7116) <<"URL: query - '" << KUrl::fromPercentEncoding(_url.query()) <<"'"; // get the delimiter QString myNamespace = namespaceForBox( _box ); kDebug(7116) <<"IMAP4::parseURL - namespace=" << myNamespace; if ( namespaceToDelimiter.contains(myNamespace) ) { _hierarchyDelimiter = namespaceToDelimiter[myNamespace]; kDebug(7116) <<"IMAP4::parseURL - delimiter=" << _hierarchyDelimiter; } if (!_box.isEmpty ()) { kDebug(7116) <<"IMAP4::parseURL - box=" << _box; if (makeLogin ()) { if (getCurrentBox () != _box || _type == "LIST" || _type == "LSUB" || _type == "LSUBNOCHECK") { if ( cache ) { // assume a normal box retVal = ITYPE_DIR_AND_BOX; } else { // start a listing for the box to get the type CommandPtr cmd; cmd = doCommand (imapCommand::clientList ("", _box)); if (cmd->result () == "OK") { for (QList< imapList >::Iterator it = listResponses.begin (); it != listResponses.end (); ++it) { //kDebug(7116) <<"IMAP4::parseURL - checking" << _box <<" to" << (*it).name(); if (_box == (*it).name ()) { if ( !(*it).hierarchyDelimiter().isEmpty() ) _hierarchyDelimiter = (*it).hierarchyDelimiter(); if ((*it).noSelect ()) { retVal = ITYPE_DIR; } else if ((*it).noInferiors ()) { retVal = ITYPE_BOX; } else { retVal = ITYPE_DIR_AND_BOX; } } } // if we got no list response for the box see if it's a prefix if ( retVal == ITYPE_UNKNOWN && namespaceToDelimiter.contains(_box) ) { retVal = ITYPE_DIR; } } else { kDebug(7116) <<"IMAP4::parseURL - got error for" << _box; } completeQueue.removeAll (cmd); } // cache } else // current == box { retVal = ITYPE_BOX; } } else kDebug(7116) <<"IMAP4::parseURL: no login!"; } else // empty box { // the root is just a dir kDebug(7116) <<"IMAP4: parseURL: box [root]"; retVal = ITYPE_DIR; } // see if it is a real sequence or a simple uid if (retVal == ITYPE_BOX || retVal == ITYPE_DIR_AND_BOX) { if (!_uid.isEmpty ()) { if ( !_uid.contains(':') && !_uid.contains(',') && !_uid.contains('*') ) retVal = ITYPE_MSG; } } if (retVal == ITYPE_MSG) { if ( ( _section.contains("BODY.PEEK[", Qt::CaseInsensitive) || _section.contains("BODY[", Qt::CaseInsensitive) ) && !_section.contains(".MIME") && !_section.contains(".HEADER") ) retVal = ITYPE_ATTACH; } if ( _hierarchyDelimiter.isEmpty() && (_type == "LIST" || _type == "LSUB" || _type == "LSUBNOCHECK") ) { // this shouldn't happen but when the delimiter is really empty // we try to reconstruct it from the URL if (!_box.isEmpty()) { int start = _url.path().lastIndexOf(_box); if (start != -1) _hierarchyDelimiter = _url.path().mid(start-1, start); kDebug(7116) <<"IMAP4::parseURL - reconstructed delimiter:" << _hierarchyDelimiter << "from URL" << _url.path(); } if (_hierarchyDelimiter.isEmpty()) _hierarchyDelimiter = "/"; } kDebug(7116) <<"IMAP4::parseURL - return" << retVal; return retVal; } int IMAP4Protocol::outputLine (const QByteArray & _str, int len) { if (len == -1) { len = _str.length(); } if (cacheOutput) { if ( !outputBuffer.isOpen() ) { outputBuffer.open(QIODevice::WriteOnly); } outputBuffer.seek( outputBufferIndex ); outputBuffer.write(_str.data(), len); outputBufferIndex += len; return 0; } QByteArray temp; bool relay = relayEnabled; relayEnabled = true; temp = QByteArray::fromRawData (_str.data (), len); parseRelay (temp); temp.clear(); relayEnabled = relay; return 0; } void IMAP4Protocol::flushOutput(const QString &contentEncoding) { // send out cached data to the application if (outputBufferIndex == 0) return; outputBuffer.close(); outputCache.resize(outputBufferIndex); if (decodeContent) { // get the coding from the MIME header QByteArray decoded; if ( contentEncoding.startsWith("quoted-printable", Qt::CaseInsensitive) ) decoded = KCodecs::quotedPrintableDecode(outputCache); else if ( contentEncoding.startsWith("base64", Qt::CaseInsensitive) ) decoded = QByteArray::fromBase64( outputCache ); else decoded = outputCache; QString mimetype = KMimeType::findByContent( decoded )->name(); kDebug(7116) <<"IMAP4::flushOutput - mimeType" << mimetype; mimeType(mimetype); decodeContent = false; data( decoded ); } else { data( outputCache ); } mProcessedSize += outputBufferIndex; processedSize( mProcessedSize ); outputBufferIndex = 0; outputCache[0] = '\0'; outputBuffer.setBuffer(&outputCache); } ssize_t IMAP4Protocol::myRead(void *data, ssize_t len) { if (readBufferLen) { ssize_t copyLen = (len < readBufferLen) ? len : readBufferLen; memcpy(data, readBuffer, copyLen); readBufferLen -= copyLen; if (readBufferLen) memcpy(readBuffer, &readBuffer[copyLen], readBufferLen); return copyLen; } if (!isConnected()) return 0; waitForResponse( responseTimeout() ); return read((char*)data, len); } bool IMAP4Protocol::assureBox (const QString & aBox, bool readonly) { if (aBox.isEmpty()) return false; CommandPtr cmd; if (aBox != getCurrentBox () || (!getSelected().readWrite() && !readonly)) { // open the box with the appropriate mode kDebug(7116) <<"IMAP4Protocol::assureBox - opening box"; selectInfo = imapInfo(); cmd = doCommand (imapCommand::clientSelect (aBox, readonly)); bool ok = cmd->result() == "OK"; QString cmdInfo = cmd->resultInfo(); completeQueue.removeAll (cmd); if (!ok) { bool found = false; cmd = doCommand (imapCommand::clientList ("", aBox)); if (cmd->result () == "OK") { for (QList< imapList >::Iterator it = listResponses.begin (); it != listResponses.end (); ++it) { if (aBox == (*it).name ()) found = true; } } completeQueue.removeAll (cmd); if (found) { if ( cmdInfo.contains("permission", Qt::CaseInsensitive) ) { // not allowed to enter this folder error(ERR_ACCESS_DENIED, cmdInfo); } else { error(ERR_SLAVE_DEFINED, i18n("Unable to open folder %1. The server replied: %2", aBox, cmdInfo)); } } else { error(KIO::ERR_DOES_NOT_EXIST, aBox); } return false; } } else { // Give the server a chance to deliver updates every ten seconds. // Doing this means a server roundtrip and since assureBox is called // after every mail, we do it with a timeout. kDebug(7116) <<"IMAP4Protocol::assureBox - reusing box"; if ( mTimeOfLastNoop.secsTo( QDateTime::currentDateTime() ) > 10 ) { cmd = doCommand (imapCommand::clientNoop ()); completeQueue.removeAll (cmd); mTimeOfLastNoop = QDateTime::currentDateTime(); kDebug(7116) <<"IMAP4Protocol::assureBox - noop timer fired"; } } // if it is the mode we want if (!getSelected().readWrite() && !readonly) { error(KIO::ERR_CANNOT_OPEN_FOR_WRITING, aBox); return false; } return true; }