kmail

backupjob.cpp
1/* Copyright 2009 Klarälvdalens Datakonsult AB
2
3 This program is free software; you can redistribute it and/or
4 modify it under the terms of the GNU General Public License as
5 published by the Free Software Foundation; either version 2 of
6 the License or (at your option) version 3 or any later version
7 accepted by the membership of KDE e.V. (or its successor approved
8 by the membership of KDE e.V.), which shall act as a proxy
9 defined in Section 14 of version 3 of the license.
10
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with this program. If not, see <http://www.gnu.org/licenses/>.
18*/
19#include "backupjob.h"
20
21#include "kmmsgdict.h"
22#include "kmfolder.h"
23#include "kmfoldercachedimap.h"
24#include "kmfolderdir.h"
25#include "folderutil.h"
26
27#include "progressmanager.h"
28
29#include "kzip.h"
30#include "ktar.h"
31#include "tdemessagebox.h"
32
33#include "tqfile.h"
34#include "tqfileinfo.h"
35#include "tqstringlist.h"
36
37using namespace KMail;
38
39BackupJob::BackupJob( TQWidget *parent )
40 : TQObject( parent ),
41 mArchiveType( Zip ),
42 mRootFolder( 0 ),
43 mArchive( 0 ),
44 mParentWidget( parent ),
45 mCurrentFolderOpen( false ),
46 mArchivedMessages( 0 ),
47 mArchivedSize( 0 ),
48 mProgressItem( 0 ),
49 mAborted( false ),
50 mDeleteFoldersAfterCompletion( false ),
51 mCurrentFolder( 0 ),
52 mCurrentMessage( 0 ),
53 mCurrentJob( 0 )
54{
55}
56
57BackupJob::~BackupJob()
58{
59 mPendingFolders.clear();
60 if ( mArchive ) {
61 delete mArchive;
62 mArchive = 0;
63 }
64}
65
66void BackupJob::setRootFolder( KMFolder *rootFolder )
67{
68 mRootFolder = rootFolder;
69}
70
71void BackupJob::setSaveLocation( const KURL &savePath )
72{
73 mMailArchivePath = savePath;
74}
75
76void BackupJob::setArchiveType( ArchiveType type )
77{
78 mArchiveType = type;
79}
80
81void BackupJob::setDeleteFoldersAfterCompletion( bool deleteThem )
82{
83 mDeleteFoldersAfterCompletion = deleteThem;
84}
85
86TQString BackupJob::stripRootPath( const TQString &path ) const
87{
88 TQString ret = path;
89 ret = ret.remove( mRootFolder->path() );
90 if ( ret.startsWith( "/" ) )
91 ret = ret.right( ret.length() - 1 );
92 return ret;
93}
94
95void BackupJob::queueFolders( KMFolder *root )
96{
97 mPendingFolders.append( root );
98 KMFolderDir *dir = root->child();
99 if ( dir ) {
100 for ( KMFolderNode * node = dir->first() ; node ; node = dir->next() ) {
101 if ( node->isDir() )
102 continue;
103 KMFolder *folder = static_cast<KMFolder*>( node );
104 queueFolders( folder );
105 }
106 }
107}
108
109bool BackupJob::hasChildren( KMFolder *folder ) const
110{
111 KMFolderDir *dir = folder->child();
112 if ( dir ) {
113 for ( KMFolderNode * node = dir->first() ; node ; node = dir->next() ) {
114 if ( !node->isDir() )
115 return true;
116 }
117 }
118 return false;
119}
120
121void BackupJob::cancelJob()
122{
123 abort( i18n( "The operation was canceled by the user." ) );
124}
125
126void BackupJob::abort( const TQString &errorMessage )
127{
128 // We could be called this twice, since killing the current job below will cause the job to fail,
129 // and that will call abort()
130 if ( mAborted )
131 return;
132
133 mAborted = true;
134 if ( mCurrentFolderOpen && mCurrentFolder ) {
135 mCurrentFolder->close( "BackupJob" );
136 mCurrentFolder = 0;
137 }
138 if ( mArchive && mArchive->isOpened() ) {
139 mArchive->close();
140 }
141 if ( mCurrentJob ) {
142 mCurrentJob->kill();
143 mCurrentJob = 0;
144 }
145 if ( mProgressItem ) {
146 mProgressItem->setComplete();
147 mProgressItem = 0;
148 // The progressmanager will delete it
149 }
150
151 TQString text = i18n( "Failed to archive the folder '%1'." ).arg( mRootFolder->name() );
152 text += "\n" + errorMessage;
153 KMessageBox::sorry( mParentWidget, text, i18n( "Archiving failed." ) );
154 deleteLater();
155 // Clean up archive file here?
156}
157
158void BackupJob::finish()
159{
160 if ( mArchive->isOpened() ) {
161 mArchive->close();
162 if ( !mArchive->closeSucceeded() ) {
163 abort( i18n( "Unable to finalize the archive file." ) );
164 return;
165 }
166 }
167
168 mProgressItem->setStatus( i18n( "Archiving finished" ) );
169 mProgressItem->setComplete();
170 mProgressItem = 0;
171
172 TQFileInfo archiveFileInfo( mMailArchivePath.path() );
173 TQString text = i18n( "Archiving folder '%1' successfully completed. "
174 "The archive was written to the file '%2'." )
175 .arg( mRootFolder->name() ).arg( mMailArchivePath.path() );
176 text += "\n" + i18n( "1 message of size %1 was archived.",
177 "%n messages with the total size of %1 were archived.", mArchivedMessages )
178 .arg( TDEIO::convertSize( mArchivedSize ) );
179 text += "\n" + i18n( "The archive file has a size of %1." )
180 .arg( TDEIO::convertSize( archiveFileInfo.size() ) );
181 KMessageBox::information( mParentWidget, text, i18n( "Archiving finished." ) );
182
183 if ( mDeleteFoldersAfterCompletion ) {
184 // Some saftey checks first...
185 if ( archiveFileInfo.size() > 0 && ( mArchivedSize > 0 || mArchivedMessages == 0 ) ) {
186 // Sorry for any data loss!
187 FolderUtil::deleteFolder( mRootFolder, mParentWidget );
188 }
189 }
190
191 deleteLater();
192}
193
194void BackupJob::archiveNextMessage()
195{
196 if ( mAborted )
197 return;
198
199 mCurrentMessage = 0;
200 if ( mPendingMessages.isEmpty() ) {
201 kdDebug(5006) << "===> All messages done in folder " << mCurrentFolder->name() << endl;
202 mCurrentFolder->close( "BackupJob" );
203 mCurrentFolderOpen = false;
204 archiveNextFolder();
205 return;
206 }
207
208 unsigned long serNum = mPendingMessages.front();
209 mPendingMessages.pop_front();
210
211 KMFolder *folder;
212 mMessageIndex = -1;
213 KMMsgDict::instance()->getLocation( serNum, &folder, &mMessageIndex );
214 if ( mMessageIndex == -1 ) {
215 kdWarning(5006) << "Failed to get message location for sernum " << serNum << endl;
216 abort( i18n( "Unable to retrieve a message for folder '%1'." ).arg( mCurrentFolder->name() ) );
217 return;
218 }
219
220 Q_ASSERT( folder == mCurrentFolder );
221 const KMMsgBase *base = mCurrentFolder->getMsgBase( mMessageIndex );
222 mUnget = base && !base->isMessage();
223 KMMessage *message = mCurrentFolder->getMsg( mMessageIndex );
224 if ( !message ) {
225 kdWarning(5006) << "Failed to retrieve message with index " << mMessageIndex << endl;
226 abort( i18n( "Unable to retrieve a message for folder '%1'." ).arg( mCurrentFolder->name() ) );
227 return;
228 }
229
230 kdDebug(5006) << "Going to get next message with subject " << message->subject() << ", "
231 << mPendingMessages.size() << " messages left in the folder." << endl;
232
233 if ( message->isComplete() ) {
234 // Use a singleshot timer, or otherwise we risk ending up in a very big recursion
235 // for folders that have many messages
236 mCurrentMessage = message;
237 TQTimer::singleShot( 0, this, TQ_SLOT( processCurrentMessage() ) );
238 }
239 else if ( message->parent() ) {
240 mCurrentJob = message->parent()->createJob( message );
241 mCurrentJob->setCancellable( false );
242 connect( mCurrentJob, TQ_SIGNAL( messageRetrieved( KMMessage* ) ),
243 this, TQ_SLOT( messageRetrieved( KMMessage* ) ) );
244 connect( mCurrentJob, TQ_SIGNAL( result( KMail::FolderJob* ) ),
245 this, TQ_SLOT( folderJobFinished( KMail::FolderJob* ) ) );
246 mCurrentJob->start();
247 }
248 else {
249 kdWarning(5006) << "Message with subject " << mCurrentMessage->subject()
250 << " is neither complete nor has a parent!" << endl;
251 abort( i18n( "Internal error while trying to retrieve a message from folder '%1'." )
252 .arg( mCurrentFolder->name() ) );
253 }
254
255 mProgressItem->setProgress( ( mProgressItem->progress() + 5 ) );
256}
257
258static int fileInfoToUnixPermissions( const TQFileInfo &fileInfo )
259{
260 int perm = 0;
261 if ( fileInfo.permission( TQFileInfo::ExeOther ) ) perm += S_IXOTH;
262 if ( fileInfo.permission( TQFileInfo::WriteOther ) ) perm += S_IWOTH;
263 if ( fileInfo.permission( TQFileInfo::ReadOther ) ) perm += S_IROTH;
264 if ( fileInfo.permission( TQFileInfo::ExeGroup ) ) perm += S_IXGRP;
265 if ( fileInfo.permission( TQFileInfo::WriteGroup ) ) perm += S_IWGRP;
266 if ( fileInfo.permission( TQFileInfo::ReadGroup ) ) perm += S_IRGRP;
267 if ( fileInfo.permission( TQFileInfo::ExeOwner ) ) perm += S_IXUSR;
268 if ( fileInfo.permission( TQFileInfo::WriteOwner ) ) perm += S_IWUSR;
269 if ( fileInfo.permission( TQFileInfo::ReadOwner ) ) perm += S_IRUSR;
270 return perm;
271}
272
273void BackupJob::processCurrentMessage()
274{
275 if ( mAborted )
276 return;
277
278 if ( mCurrentMessage ) {
279 kdDebug(5006) << "Processing message with subject " << mCurrentMessage->subject() << endl;
280 const DwString &messageDWString = mCurrentMessage->asDwString();
281 const uint messageSize = messageDWString.size();
282 const char *messageString = mCurrentMessage->asDwString().c_str();
283 TQString messageName;
284 TQFileInfo fileInfo;
285 if ( messageName.isEmpty() ) {
286 messageName = TQString::number( mCurrentMessage->getMsgSerNum() ); // IMAP doesn't have filenames
287 if ( mCurrentMessage->storage() ) {
288 fileInfo.setFile( mCurrentMessage->storage()->location() );
289 // TODO: what permissions etc to take when there is no storage file?
290 }
291 }
292 else {
293 // TODO: What if the message is not in the "cur" directory?
294 fileInfo.setFile( mCurrentFolder->location() + "/cur/" + mCurrentMessage->fileName() );
295 messageName = mCurrentMessage->fileName();
296 }
297
298 const TQString fileName = stripRootPath( mCurrentFolder->location() ) +
299 "/cur/" + messageName;
300
301 TQString user;
302 TQString group;
303 mode_t permissions = 0700;
304 time_t creationTime = time( 0 );
305 time_t modificationTime = time( 0 );
306 time_t accessTime = time( 0 );
307 if ( !fileInfo.fileName().isEmpty() ) {
308 user = fileInfo.owner();
309 group = fileInfo.group();
310 permissions = fileInfoToUnixPermissions( fileInfo );
311 creationTime = fileInfo.created().toTime_t();
312 modificationTime = fileInfo.lastModified().toTime_t();
313 accessTime = fileInfo.lastRead().toTime_t();
314 }
315 else {
316 kdWarning(5006) << "Unable to find file for message " << fileName << endl;
317 }
318
319 if ( !mArchive->writeFile( fileName, user, group, messageSize, permissions, accessTime,
320 modificationTime, creationTime, messageString ) ) {
321 abort( i18n( "Failed to write a message into the archive folder '%1'." ).arg( mCurrentFolder->name() ) );
322 return;
323 }
324
325 if ( mUnget ) {
326 Q_ASSERT( mMessageIndex >= 0 );
327 mCurrentFolder->unGetMsg( mMessageIndex );
328 }
329
330 mArchivedMessages++;
331 mArchivedSize += messageSize;
332 }
333 else {
334 // No message? According to ImapJob::slotGetMessageResult(), that means the message is no
335 // longer on the server. So ignore this one.
336 kdWarning(5006) << "Unable to download a message for folder " << mCurrentFolder->name() << endl;
337 }
338 archiveNextMessage();
339}
340
341void BackupJob::messageRetrieved( KMMessage *message )
342{
343 mCurrentMessage = message;
344 processCurrentMessage();
345}
346
347void BackupJob::folderJobFinished( KMail::FolderJob *job )
348{
349 if ( mAborted )
350 return;
351
352 // The job might finish after it has emitted messageRetrieved(), in which case we have already
353 // started a new job. Don't set the current job to 0 in that case.
354 if ( job == mCurrentJob ) {
355 mCurrentJob = 0;
356 }
357
358 if ( job->error() ) {
359 if ( mCurrentFolder )
360 abort( i18n( "Downloading a message in folder '%1' failed." ).arg( mCurrentFolder->name() ) );
361 else
362 abort( i18n( "Downloading a message in the current folder failed." ) );
363 }
364}
365
366bool BackupJob::writeDirHelper( const TQString &directoryPath, const TQString &permissionPath )
367{
368 TQFileInfo fileInfo( permissionPath );
369 TQString user = fileInfo.owner();
370 TQString group = fileInfo.group();
371 mode_t permissions = fileInfoToUnixPermissions( fileInfo );
372 time_t creationTime = fileInfo.created().toTime_t();
373 time_t modificationTime = fileInfo.lastModified().toTime_t();
374 time_t accessTime = fileInfo.lastRead().toTime_t();
375 return mArchive->writeDir( stripRootPath( directoryPath ), user, group, permissions, accessTime,
376 modificationTime, creationTime );
377}
378
379void BackupJob::archiveNextFolder()
380{
381 if ( mAborted )
382 return;
383
384 if ( mPendingFolders.isEmpty() ) {
385 finish();
386 return;
387 }
388
389 mCurrentFolder = mPendingFolders.take( 0 );
390 kdDebug(5006) << "===> Archiving next folder: " << mCurrentFolder->name() << endl;
391 mProgressItem->setStatus( i18n( "Archiving folder %1" ).arg( mCurrentFolder->name() ) );
392 if ( mCurrentFolder->open( "BackupJob" ) != 0 ) {
393 abort( i18n( "Unable to open folder '%1'.").arg( mCurrentFolder->name() ) );
394 return;
395 }
396 mCurrentFolderOpen = true;
397
398 const TQString folderName = mCurrentFolder->name();
399 bool success = true;
400 if ( hasChildren( mCurrentFolder ) ) {
401 if ( !writeDirHelper( mCurrentFolder->subdirLocation(), mCurrentFolder->subdirLocation() ) )
402 success = false;
403 }
404 if ( !writeDirHelper( mCurrentFolder->location(), mCurrentFolder->location() ) )
405 success = false;
406 if ( !writeDirHelper( mCurrentFolder->location() + "/cur", mCurrentFolder->location() ) )
407 success = false;
408 if ( !writeDirHelper( mCurrentFolder->location() + "/new", mCurrentFolder->location() ) )
409 success = false;
410 if ( !writeDirHelper( mCurrentFolder->location() + "/tmp", mCurrentFolder->location() ) )
411 success = false;
412 if ( !success ) {
413 abort( i18n( "Unable to create folder structure for folder '%1' within archive file." )
414 .arg( mCurrentFolder->name() ) );
415 return;
416 }
417
418 for ( int i = 0; i < mCurrentFolder->count( false /* no cache */ ); i++ ) {
419 unsigned long serNum = KMMsgDict::instance()->getMsgSerNum( mCurrentFolder, i );
420 if ( serNum == 0 ) {
421 // Uh oh
422 kdWarning(5006) << "Got serial number zero in " << mCurrentFolder->name()
423 << " at index " << i << "!" << endl;
424 // TODO: handle error in a nicer way. this is _very_ bad
425 abort( i18n( "Unable to backup messages in folder '%1', the index file is corrupted." )
426 .arg( mCurrentFolder->name() ) );
427 return;
428 }
429 else
430 mPendingMessages.append( serNum );
431 }
432 archiveNextMessage();
433}
434
435// TODO
436// - error handling
437// - import
438// - connect to progressmanager, especially abort
439// - messagebox when finished (?)
440// - ui dialog
441// - use correct permissions
442// - save index and serial number?
443// - guarded pointers for folders
444// - online IMAP: check mails first, so sernums are up-to-date?
445// - "ignore errors"-mode, with summary how many messages couldn't be archived?
446// - do something when the user quits KMail while the backup job is running
447// - run in a thread?
448// - delete source folder after completion. dangerous!!!
449//
450// BUGS
451// - Online IMAP: Test Mails -> Test%20Mails
452// - corrupted sernums indices stop backup job
453void BackupJob::start()
454{
455 Q_ASSERT( !mMailArchivePath.isEmpty() );
456 Q_ASSERT( mRootFolder );
457
458 queueFolders( mRootFolder );
459
460 switch ( mArchiveType ) {
461 case Zip: {
462 KZip *zip = new KZip( mMailArchivePath.path() );
463 zip->setCompression( KZip::DeflateCompression );
464 mArchive = zip;
465 break;
466 }
467 case Tar: {
468 mArchive = new KTar( mMailArchivePath.path(), "application/x-tar" );
469 break;
470 }
471 case TarGz: {
472 mArchive = new KTar( mMailArchivePath.path(), "application/x-gzip" );
473 break;
474 }
475 case TarBz2: {
476 mArchive = new KTar( mMailArchivePath.path(), "application/x-bzip2" );
477 break;
478 }
479 }
480
481 kdDebug(5006) << "Starting backup." << endl;
482 if ( !mArchive->open( IO_WriteOnly ) ) {
483 abort( i18n( "Unable to open archive for writing." ) );
484 return;
485 }
486
487 mProgressItem = KPIM::ProgressManager::createProgressItem(
488 "BackupJob",
489 i18n( "Archiving" ),
490 TQString(),
491 true );
492 mProgressItem->setUsesBusyIndicator( true );
493 connect( mProgressItem, TQ_SIGNAL(progressItemCanceled(KPIM::ProgressItem*)),
494 this, TQ_SLOT(cancelJob()) );
495
496 archiveNextFolder();
497}
498
499#include "backupjob.moc"
500
KMail list that manages the contents of one directory that may contain folders and/or other directori...
Definition: kmfolderdir.h:16
Mail folder.
Definition: kmfolder.h:69
TQString subdirLocation() const
Returns full path to sub directory file.
Definition: kmfolder.cpp:253
KMMsgInfo * unGetMsg(int idx)
Replace KMMessage with KMMsgInfo and delete KMMessage
Definition: kmfolder.cpp:326
void close(const char *owner, bool force=false)
Close folder.
Definition: kmfolder.cpp:489
KMMessage * getMsg(int idx)
Read message at given index.
Definition: kmfolder.cpp:321
const KMMsgBase * getMsgBase(int idx) const
Provides access to the basic message fields that are also stored in the index.
Definition: kmfolder.cpp:360
int count(bool cache=false) const
Number of messages in this folder.
Definition: kmfolder.cpp:445
int open(const char *owner)
Open folder for access.
Definition: kmfolder.cpp:479
TQString location() const
Returns full path to folder file.
Definition: kmfolder.cpp:243
KMFolderDir * child() const
Returns the folder directory associated with this node or 0 if no such directory exists.
Definition: kmfolder.h:157
This is a Mime Message.
Definition: kmmessage.h:68
TQString subject() const
Get or set the 'Subject' header field.
Definition: kmmessage.cpp:2049
const DwString & asDwString() const
Return the entire message contents in the DwString.
Definition: kmmessage.cpp:292
TQString fileName() const
Get/set filename in mail folder.
Definition: kmmessage.h:802
bool isComplete() const
Return true if the complete message is available without referring to the backing store.
Definition: kmmessage.h:867
unsigned long getMsgSerNum(KMFolder *folder, int index) const
Find the message serial number for the message located at index index in folder folder.
Definition: kmmsgdict.cpp:345
void getLocation(unsigned long key, KMFolder **retFolder, int *retIndex) const
Returns the folder the message represented by the serial number key is in and the index in that folde...
Definition: kmmsgdict.cpp:319
static const KMMsgDict * instance()
Access the globally unique MessageDict.
Definition: kmmsgdict.cpp:167
folderdiaquotatab.h
Definition: aboutdata.cpp:40