kmail

backupjob.cpp
00001 /* Copyright 2009 Klarälvdalens Datakonsult AB
00002 
00003    This program is free software; you can redistribute it and/or
00004    modify it under the terms of the GNU General Public License as
00005    published by the Free Software Foundation; either version 2 of
00006    the License or (at your option) version 3 or any later version
00007    accepted by the membership of KDE e.V. (or its successor approved
00008    by the membership of KDE e.V.), which shall act as a proxy
00009    defined in Section 14 of version 3 of the license.
00010 
00011    This program is distributed in the hope that it will be useful,
00012    but WITHOUT ANY WARRANTY; without even the implied warranty of
00013    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00014    GNU General Public License for more details.
00015 
00016    You should have received a copy of the GNU General Public License
00017    along with this program.  If not, see <http://www.gnu.org/licenses/>.
00018 */
00019 #include "backupjob.h"
00020 
00021 #include "kmmsgdict.h"
00022 #include "kmfolder.h"
00023 #include "kmfoldercachedimap.h"
00024 #include "kmfolderdir.h"
00025 #include "folderutil.h"
00026 
00027 #include "progressmanager.h"
00028 
00029 #include "kzip.h"
00030 #include "ktar.h"
00031 #include "kmessagebox.h"
00032 
00033 #include "tqfile.h"
00034 #include "tqfileinfo.h"
00035 #include "tqstringlist.h"
00036 
00037 using namespace KMail;
00038 
00039 BackupJob::BackupJob( TQWidget *parent )
00040   : TQObject( parent ),
00041     mArchiveType( Zip ),
00042     mRootFolder( 0 ),
00043     mArchive( 0 ),
00044     mParentWidget( parent ),
00045     mCurrentFolderOpen( false ),
00046     mArchivedMessages( 0 ),
00047     mArchivedSize( 0 ),
00048     mProgressItem( 0 ),
00049     mAborted( false ),
00050     mDeleteFoldersAfterCompletion( false ),
00051     mCurrentFolder( 0 ),
00052     mCurrentMessage( 0 ),
00053     mCurrentJob( 0 )
00054 {
00055 }
00056 
00057 BackupJob::~BackupJob()
00058 {
00059   mPendingFolders.clear();
00060   if ( mArchive ) {
00061     delete mArchive;
00062     mArchive = 0;
00063   }
00064 }
00065 
00066 void BackupJob::setRootFolder( KMFolder *rootFolder )
00067 {
00068   mRootFolder = rootFolder;
00069 }
00070 
00071 void BackupJob::setSaveLocation( const KURL &savePath )
00072 {
00073   mMailArchivePath = savePath;
00074 }
00075 
00076 void BackupJob::setArchiveType( ArchiveType type )
00077 {
00078   mArchiveType = type;
00079 }
00080 
00081 void BackupJob::setDeleteFoldersAfterCompletion( bool deleteThem )
00082 {
00083   mDeleteFoldersAfterCompletion = deleteThem;
00084 }
00085 
00086 TQString BackupJob::stripRootPath( const TQString &path ) const
00087 {
00088   TQString ret = path;
00089   ret = ret.remove( mRootFolder->path() );
00090   if ( ret.startsWith( "/" ) )
00091     ret = ret.right( ret.length() - 1 );
00092   return ret;
00093 }
00094 
00095 void BackupJob::queueFolders( KMFolder *root )
00096 {
00097   mPendingFolders.append( root );
00098   KMFolderDir *dir = root->child();
00099   if ( dir ) {
00100     for ( KMFolderNode * node = dir->first() ; node ; node = dir->next() ) {
00101       if ( node->isDir() )
00102         continue;
00103       KMFolder *folder = static_cast<KMFolder*>( node );
00104       queueFolders( folder );
00105     }
00106   }
00107 }
00108 
00109 bool BackupJob::hasChildren( KMFolder *folder ) const
00110 {
00111   KMFolderDir *dir = folder->child();
00112   if ( dir ) {
00113     for ( KMFolderNode * node = dir->first() ; node ; node = dir->next() ) {
00114       if ( !node->isDir() )
00115         return true;
00116     }
00117   }
00118   return false;
00119 }
00120 
00121 void BackupJob::cancelJob()
00122 {
00123   abort( i18n( "The operation was canceled by the user." ) );
00124 }
00125 
00126 void BackupJob::abort( const TQString &errorMessage )
00127 {
00128   // We could be called this twice, since killing the current job below will cause the job to fail,
00129   // and that will call abort()
00130   if ( mAborted )
00131     return;
00132 
00133   mAborted = true;
00134   if ( mCurrentFolderOpen && mCurrentFolder ) {
00135     mCurrentFolder->close( "BackupJob" );
00136     mCurrentFolder = 0;
00137   }
00138   if ( mArchive && mArchive->isOpened() ) {
00139     mArchive->close();
00140   }
00141   if ( mCurrentJob ) {
00142     mCurrentJob->kill();
00143     mCurrentJob = 0;
00144   }
00145   if ( mProgressItem ) {
00146     mProgressItem->setComplete();
00147     mProgressItem = 0;
00148     // The progressmanager will delete it
00149   }
00150 
00151   TQString text = i18n( "Failed to archive the folder '%1'." ).arg( mRootFolder->name() );
00152   text += "\n" + errorMessage;
00153   KMessageBox::sorry( mParentWidget, text, i18n( "Archiving failed." ) );
00154   deleteLater();
00155   // Clean up archive file here?
00156 }
00157 
00158 void BackupJob::finish()
00159 {
00160   if ( mArchive->isOpened() ) {
00161     mArchive->close();
00162     if ( !mArchive->closeSucceeded() ) {
00163       abort( i18n( "Unable to finalize the archive file." ) );
00164       return;
00165     }
00166   }
00167 
00168   mProgressItem->setStatus( i18n( "Archiving finished" ) );
00169   mProgressItem->setComplete();
00170   mProgressItem = 0;
00171 
00172   TQFileInfo archiveFileInfo( mMailArchivePath.path() );
00173   TQString text = i18n( "Archiving folder '%1' successfully completed. "
00174                        "The archive was written to the file '%2'." )
00175                    .arg( mRootFolder->name() ).arg( mMailArchivePath.path() );
00176   text += "\n" + i18n( "1 message of size %1 was archived.",
00177                        "%n messages with the total size of %1 were archived.", mArchivedMessages )
00178                    .arg( KIO::convertSize( mArchivedSize ) );
00179   text += "\n" + i18n( "The archive file has a size of %1." )
00180                    .arg( KIO::convertSize( archiveFileInfo.size() ) );
00181   KMessageBox::information( mParentWidget, text, i18n( "Archiving finished." ) );
00182 
00183   if ( mDeleteFoldersAfterCompletion ) {
00184     // Some saftey checks first...
00185     if ( archiveFileInfo.size() > 0 && ( mArchivedSize > 0 || mArchivedMessages == 0 ) ) {
00186       // Sorry for any data loss!
00187       FolderUtil::deleteFolder( mRootFolder, mParentWidget );
00188     }
00189   }
00190 
00191   deleteLater();
00192 }
00193 
00194 void BackupJob::archiveNextMessage()
00195 {
00196   if ( mAborted )
00197     return;
00198 
00199   mCurrentMessage = 0;
00200   if ( mPendingMessages.isEmpty() ) {
00201     kdDebug(5006) << "===> All messages done in folder " << mCurrentFolder->name() << endl;
00202     mCurrentFolder->close( "BackupJob" );
00203     mCurrentFolderOpen = false;
00204     archiveNextFolder();
00205     return;
00206   }
00207 
00208   unsigned long serNum = mPendingMessages.front();
00209   mPendingMessages.pop_front();
00210 
00211   KMFolder *folder;
00212   mMessageIndex = -1;
00213   KMMsgDict::instance()->getLocation( serNum, &folder, &mMessageIndex );
00214   if ( mMessageIndex == -1 ) {
00215     kdWarning(5006) << "Failed to get message location for sernum " << serNum << endl;
00216     abort( i18n( "Unable to retrieve a message for folder '%1'." ).arg( mCurrentFolder->name() ) );
00217     return;
00218   }
00219 
00220   Q_ASSERT( folder == mCurrentFolder );
00221   const KMMsgBase *base = mCurrentFolder->getMsgBase( mMessageIndex );
00222   mUnget = base && !base->isMessage();
00223   KMMessage *message = mCurrentFolder->getMsg( mMessageIndex );
00224   if ( !message ) {
00225     kdWarning(5006) << "Failed to retrieve message with index " << mMessageIndex << endl;
00226     abort( i18n( "Unable to retrieve a message for folder '%1'." ).arg( mCurrentFolder->name() ) );
00227     return;
00228   }
00229 
00230   kdDebug(5006) << "Going to get next message with subject " << message->subject() << ", "
00231                 << mPendingMessages.size() << " messages left in the folder." << endl;
00232 
00233   if ( message->isComplete() ) {
00234     // Use a singleshot timer, or otherwise we risk ending up in a very big recursion
00235     // for folders that have many messages
00236     mCurrentMessage = message;
00237     TQTimer::singleShot( 0, this, TQT_SLOT( processCurrentMessage() ) );
00238   }
00239   else if ( message->parent() ) {
00240     mCurrentJob = message->parent()->createJob( message );
00241     mCurrentJob->setCancellable( false );
00242     connect( mCurrentJob, TQT_SIGNAL( messageRetrieved( KMMessage* ) ),
00243              this, TQT_SLOT( messageRetrieved( KMMessage* ) ) );
00244     connect( mCurrentJob, TQT_SIGNAL( result( KMail::FolderJob* ) ),
00245              this, TQT_SLOT( folderJobFinished( KMail::FolderJob* ) ) );
00246     mCurrentJob->start();
00247   }
00248   else {
00249     kdWarning(5006) << "Message with subject " << mCurrentMessage->subject()
00250                     << " is neither complete nor has a parent!" << endl;
00251     abort( i18n( "Internal error while trying to retrieve a message from folder '%1'." )
00252               .arg( mCurrentFolder->name() ) );
00253   }
00254 
00255   mProgressItem->setProgress( ( mProgressItem->progress() + 5 ) );
00256 }
00257 
00258 static int fileInfoToUnixPermissions( const TQFileInfo &fileInfo )
00259 {
00260   int perm = 0;
00261   if ( fileInfo.permission( TQFileInfo::ExeOther ) ) perm += S_IXOTH;
00262   if ( fileInfo.permission( TQFileInfo::WriteOther ) ) perm += S_IWOTH;
00263   if ( fileInfo.permission( TQFileInfo::ReadOther ) ) perm += S_IROTH;
00264   if ( fileInfo.permission( TQFileInfo::ExeGroup ) ) perm += S_IXGRP;
00265   if ( fileInfo.permission( TQFileInfo::WriteGroup ) ) perm += S_IWGRP;
00266   if ( fileInfo.permission( TQFileInfo::ReadGroup ) ) perm += S_IRGRP;
00267   if ( fileInfo.permission( TQFileInfo::ExeOwner ) ) perm += S_IXUSR;
00268   if ( fileInfo.permission( TQFileInfo::WriteOwner ) ) perm += S_IWUSR;
00269   if ( fileInfo.permission( TQFileInfo::ReadOwner ) ) perm += S_IRUSR;
00270   return perm;
00271 }
00272 
00273 void BackupJob::processCurrentMessage()
00274 {
00275   if ( mAborted )
00276     return;
00277 
00278   if ( mCurrentMessage ) {
00279     kdDebug(5006) << "Processing message with subject " << mCurrentMessage->subject() << endl;
00280     const DwString &messageDWString = mCurrentMessage->asDwString();
00281     const uint messageSize = messageDWString.size();
00282     const char *messageString = mCurrentMessage->asDwString().c_str();
00283     TQString messageName;
00284     TQFileInfo fileInfo;
00285     if ( messageName.isEmpty() ) {
00286       messageName = TQString::number( mCurrentMessage->getMsgSerNum() ); // IMAP doesn't have filenames
00287       if ( mCurrentMessage->storage() ) {
00288         fileInfo.setFile( mCurrentMessage->storage()->location() );
00289         // TODO: what permissions etc to take when there is no storage file?
00290       }
00291     }
00292     else {
00293       // TODO: What if the message is not in the "cur" directory?
00294       fileInfo.setFile( mCurrentFolder->location() + "/cur/" + mCurrentMessage->fileName() );
00295       messageName = mCurrentMessage->fileName();
00296     }
00297 
00298     const TQString fileName = stripRootPath( mCurrentFolder->location() ) +
00299                              "/cur/" + messageName;
00300 
00301     TQString user;
00302     TQString group;
00303     mode_t permissions = 0700;
00304     time_t creationTime = time( 0 );
00305     time_t modificationTime = time( 0 );
00306     time_t accessTime = time( 0 );
00307     if ( !fileInfo.fileName().isEmpty() ) {
00308       user = fileInfo.owner();
00309       group = fileInfo.group();
00310       permissions = fileInfoToUnixPermissions( fileInfo );
00311       creationTime = fileInfo.created().toTime_t();
00312       modificationTime = fileInfo.lastModified().toTime_t();
00313       accessTime = fileInfo.lastRead().toTime_t();
00314     }
00315     else {
00316       kdWarning(5006) << "Unable to find file for message " << fileName << endl;
00317     }
00318 
00319     if ( !mArchive->writeFile( fileName, user, group, messageSize, permissions, accessTime,
00320                                modificationTime, creationTime, messageString ) ) {
00321       abort( i18n( "Failed to write a message into the archive folder '%1'." ).arg( mCurrentFolder->name() ) );
00322       return;
00323     }
00324 
00325     if ( mUnget ) {
00326       Q_ASSERT( mMessageIndex >= 0 );
00327       mCurrentFolder->unGetMsg( mMessageIndex );
00328     }
00329 
00330     mArchivedMessages++;
00331     mArchivedSize += messageSize;
00332   }
00333   else {
00334     // No message? According to ImapJob::slotGetMessageResult(), that means the message is no
00335     // longer on the server. So ignore this one.
00336     kdWarning(5006) << "Unable to download a message for folder " << mCurrentFolder->name() << endl;
00337   }
00338   archiveNextMessage();
00339 }
00340 
00341 void BackupJob::messageRetrieved( KMMessage *message )
00342 {
00343   mCurrentMessage = message;
00344   processCurrentMessage();
00345 }
00346 
00347 void BackupJob::folderJobFinished( KMail::FolderJob *job )
00348 {
00349   if ( mAborted )
00350     return;
00351 
00352   // The job might finish after it has emitted messageRetrieved(), in which case we have already
00353   // started a new job. Don't set the current job to 0 in that case.
00354   if ( job == mCurrentJob ) {
00355     mCurrentJob = 0;
00356   }
00357 
00358   if ( job->error() ) {
00359     if ( mCurrentFolder )
00360       abort( i18n( "Downloading a message in folder '%1' failed." ).arg( mCurrentFolder->name() ) );
00361     else
00362       abort( i18n( "Downloading a message in the current folder failed." ) );
00363   }
00364 }
00365 
00366 bool BackupJob::writeDirHelper( const TQString &directoryPath, const TQString &permissionPath )
00367 {
00368   TQFileInfo fileInfo( permissionPath );
00369   TQString user = fileInfo.owner();
00370   TQString group = fileInfo.group();
00371   mode_t permissions = fileInfoToUnixPermissions( fileInfo );
00372   time_t creationTime = fileInfo.created().toTime_t();
00373   time_t modificationTime = fileInfo.lastModified().toTime_t();
00374   time_t accessTime = fileInfo.lastRead().toTime_t();
00375   return mArchive->writeDir( stripRootPath( directoryPath ), user, group, permissions, accessTime,
00376                              modificationTime, creationTime );
00377 }
00378 
00379 void BackupJob::archiveNextFolder()
00380 {
00381   if ( mAborted )
00382     return;
00383 
00384   if ( mPendingFolders.isEmpty() ) {
00385     finish();
00386     return;
00387   }
00388 
00389   mCurrentFolder = mPendingFolders.take( 0 );
00390   kdDebug(5006) << "===> Archiving next folder: " << mCurrentFolder->name() << endl;
00391   mProgressItem->setStatus( i18n( "Archiving folder %1" ).arg( mCurrentFolder->name() ) );
00392   if ( mCurrentFolder->open( "BackupJob" ) != 0 ) {
00393     abort( i18n( "Unable to open folder '%1'.").arg( mCurrentFolder->name() ) );
00394     return;
00395   }
00396   mCurrentFolderOpen = true;
00397 
00398   const TQString folderName = mCurrentFolder->name();
00399   bool success = true;
00400   if ( hasChildren( mCurrentFolder ) ) {
00401     if ( !writeDirHelper( mCurrentFolder->subdirLocation(), mCurrentFolder->subdirLocation() ) )
00402       success = false;
00403   }
00404   if ( !writeDirHelper( mCurrentFolder->location(), mCurrentFolder->location() ) )
00405     success = false;
00406   if ( !writeDirHelper( mCurrentFolder->location() + "/cur", mCurrentFolder->location() ) )
00407     success = false;
00408   if ( !writeDirHelper( mCurrentFolder->location() + "/new", mCurrentFolder->location() ) )
00409     success = false;
00410   if ( !writeDirHelper( mCurrentFolder->location() + "/tmp", mCurrentFolder->location() ) )
00411     success = false;
00412   if ( !success ) {
00413     abort( i18n( "Unable to create folder structure for folder '%1' within archive file." )
00414               .arg( mCurrentFolder->name() ) );
00415     return;
00416   }
00417 
00418   for ( int i = 0; i < mCurrentFolder->count( false /* no cache */ ); i++ ) {
00419     unsigned long serNum = KMMsgDict::instance()->getMsgSerNum( mCurrentFolder, i );
00420     if ( serNum == 0 ) {
00421       // Uh oh
00422       kdWarning(5006) << "Got serial number zero in " << mCurrentFolder->name()
00423                       << " at index " << i << "!" << endl;
00424       // TODO: handle error in a nicer way. this is _very_ bad
00425       abort( i18n( "Unable to backup messages in folder '%1', the index file is corrupted." )
00426                .arg( mCurrentFolder->name() ) );
00427       return;
00428     }
00429     else
00430       mPendingMessages.append( serNum );
00431   }
00432   archiveNextMessage();
00433 }
00434 
00435 // TODO
00436 // - error handling
00437 // - import
00438 // - connect to progressmanager, especially abort
00439 // - messagebox when finished (?)
00440 // - ui dialog
00441 // - use correct permissions
00442 // - save index and serial number?
00443 // - guarded pointers for folders
00444 // - online IMAP: check mails first, so sernums are up-to-date?
00445 // - "ignore errors"-mode, with summary how many messages couldn't be archived?
00446 // - do something when the user quits KMail while the backup job is running
00447 // - run in a thread?
00448 // - delete source folder after completion. dangerous!!!
00449 //
00450 // BUGS
00451 // - Online IMAP: Test Mails -> Test%20Mails
00452 // - corrupted sernums indices stop backup job
00453 void BackupJob::start()
00454 {
00455   Q_ASSERT( !mMailArchivePath.isEmpty() );
00456   Q_ASSERT( mRootFolder );
00457 
00458   queueFolders( mRootFolder );
00459 
00460   switch ( mArchiveType ) {
00461     case Zip: {
00462       KZip *zip = new KZip( mMailArchivePath.path() );
00463       zip->setCompression( KZip::DeflateCompression );
00464       mArchive = zip;
00465       break;
00466     }
00467     case Tar: {
00468       mArchive = new KTar( mMailArchivePath.path(), "application/x-tar" );
00469       break;
00470     }
00471     case TarGz: {
00472       mArchive = new KTar( mMailArchivePath.path(), "application/x-gzip" );
00473       break;
00474     }
00475     case TarBz2: {
00476       mArchive = new KTar( mMailArchivePath.path(), "application/x-bzip2" );
00477       break;
00478     }
00479   }
00480 
00481   kdDebug(5006) << "Starting backup." << endl;
00482   if ( !mArchive->open( IO_WriteOnly ) ) {
00483     abort( i18n( "Unable to open archive for writing." ) );
00484     return;
00485   }
00486 
00487   mProgressItem = KPIM::ProgressManager::createProgressItem(
00488       "BackupJob",
00489       i18n( "Archiving" ),
00490       TQString(),
00491       true );
00492   mProgressItem->setUsesBusyIndicator( true );
00493   connect( mProgressItem, TQT_SIGNAL(progressItemCanceled(KPIM::ProgressItem*)),
00494            this, TQT_SLOT(cancelJob()) );
00495 
00496   archiveNextFolder();
00497 }
00498 
00499 #include "backupjob.moc"
00500