kmail Library API Documentation

imapaccountbase.cpp

00001 00024 #ifdef HAVE_CONFIG_H 00025 #include <config.h> 00026 #endif 00027 00028 #include "imapaccountbase.h" 00029 using KMail::SieveConfig; 00030 00031 #include "kmacctmgr.h" 00032 #include "kmfolder.h" 00033 #include "kmbroadcaststatus.h" 00034 #include "kmmainwin.h" 00035 #include "kmfolderimap.h" 00036 #include "kmmainwidget.h" 00037 #include "kmmainwin.h" 00038 #include "kmmsgpart.h" 00039 #include "bodyvisitor.h" 00040 using KMail::BodyVisitor; 00041 #include "imapjob.h" 00042 using KMail::ImapJob; 00043 00044 #include <kdebug.h> 00045 #include <kconfig.h> 00046 #include <klocale.h> 00047 #include <kmessagebox.h> 00048 using KIO::MetaData; 00049 #include <kio/passdlg.h> 00050 using KIO::PasswordDialog; 00051 #include <kio/scheduler.h> 00052 #include <mimelib/bodypart.h> 00053 #include <mimelib/body.h> 00054 #include <mimelib/headers.h> 00055 #include <mimelib/message.h> 00056 //using KIO::Scheduler; // use FQN below 00057 00058 #include <qregexp.h> 00059 00060 namespace KMail { 00061 00062 static const unsigned short int imapDefaultPort = 143; 00063 00064 // 00065 // 00066 // Ctor and Dtor 00067 // 00068 // 00069 00070 ImapAccountBase::ImapAccountBase( KMAcctMgr * parent, const QString & name ) 00071 : NetworkAccount( parent, name ), 00072 mPrefix( "/" ), 00073 mTotal( 0 ), 00074 mCountUnread( 0 ), 00075 mCountLastUnread( 0 ), 00076 mCountRemainChecks( 0 ), 00077 mAutoExpunge( true ), 00078 mHiddenFolders( false ), 00079 mOnlySubscribedFolders( false ), 00080 mLoadOnDemand( true ), 00081 mProgressEnabled( false ), 00082 mIdle( true ), 00083 mErrorDialogIsActive( false ), 00084 mPasswordDialogIsActive( false ), 00085 mCreateInbox( false ) 00086 { 00087 mPort = imapDefaultPort; 00088 mBodyPartList.setAutoDelete(true); 00089 KIO::Scheduler::connect(SIGNAL(slaveError(KIO::Slave *, int, const QString &)), 00090 this, SLOT(slotSchedulerSlaveError(KIO::Slave *, int, const QString &))); 00091 KIO::Scheduler::connect(SIGNAL(slaveConnected(KIO::Slave *)), 00092 this, SLOT(slotSchedulerSlaveConnected(KIO::Slave *))); 00093 } 00094 00095 ImapAccountBase::~ImapAccountBase() { 00096 kdWarning( mSlave, 5006 ) 00097 << "slave should have been destroyed by subclass!" << endl; 00098 } 00099 00100 void ImapAccountBase::init() { 00101 mPrefix = '/'; 00102 mAutoExpunge = true; 00103 mHiddenFolders = false; 00104 mOnlySubscribedFolders = false; 00105 mLoadOnDemand = true; 00106 mProgressEnabled = false; 00107 } 00108 00109 void ImapAccountBase::pseudoAssign( const KMAccount * a ) { 00110 NetworkAccount::pseudoAssign( a ); 00111 00112 const ImapAccountBase * i = dynamic_cast<const ImapAccountBase*>( a ); 00113 if ( !i ) return; 00114 00115 setPrefix( i->prefix() ); 00116 setAutoExpunge( i->autoExpunge() ); 00117 setHiddenFolders( i->hiddenFolders() ); 00118 setOnlySubscribedFolders( i->onlySubscribedFolders() ); 00119 setLoadOnDemand( i->loadOnDemand() ); 00120 } 00121 00122 unsigned short int ImapAccountBase::defaultPort() const { 00123 return imapDefaultPort; 00124 } 00125 00126 QString ImapAccountBase::protocol() const { 00127 return useSSL() ? "imaps" : "imap"; 00128 } 00129 00130 // 00131 // 00132 // Getters and Setters 00133 // 00134 // 00135 00136 void ImapAccountBase::setPrefix( const QString & prefix ) { 00137 mPrefix = prefix; 00138 mPrefix.remove( QRegExp( "[%*\"]" ) ); 00139 if ( mPrefix.isEmpty() || mPrefix[0] != '/' ) 00140 mPrefix.prepend( '/' ); 00141 if ( mPrefix[ mPrefix.length() - 1 ] != '/' ) 00142 mPrefix += '/'; 00143 #if 1 00144 setPrefixHook(); // ### needed while KMFolderCachedImap exists 00145 #else 00146 if ( mFolder ) mFolder->setImapPath( mPrefix ); 00147 #endif 00148 } 00149 00150 void ImapAccountBase::setAutoExpunge( bool expunge ) { 00151 mAutoExpunge = expunge; 00152 } 00153 00154 void ImapAccountBase::setHiddenFolders( bool show ) { 00155 mHiddenFolders = show; 00156 } 00157 00158 void ImapAccountBase::setOnlySubscribedFolders( bool show ) { 00159 mOnlySubscribedFolders = show; 00160 } 00161 00162 void ImapAccountBase::setLoadOnDemand( bool load ) { 00163 mLoadOnDemand = load; 00164 } 00165 00166 // 00167 // 00168 // read/write config 00169 // 00170 // 00171 00172 void ImapAccountBase::readConfig( /*const*/ KConfig/*Base*/ & config ) { 00173 NetworkAccount::readConfig( config ); 00174 00175 setPrefix( config.readEntry( "prefix", "/" ) ); 00176 setAutoExpunge( config.readBoolEntry( "auto-expunge", false ) ); 00177 setHiddenFolders( config.readBoolEntry( "hidden-folders", false ) ); 00178 setOnlySubscribedFolders( config.readBoolEntry( "subscribed-folders", false ) ); 00179 setLoadOnDemand( config.readBoolEntry( "loadondemand", false ) ); 00180 } 00181 00182 void ImapAccountBase::writeConfig( KConfig/*Base*/ & config ) /*const*/ { 00183 NetworkAccount::writeConfig( config ); 00184 00185 config.writeEntry( "prefix", prefix() ); 00186 config.writeEntry( "auto-expunge", autoExpunge() ); 00187 config.writeEntry( "hidden-folders", hiddenFolders() ); 00188 config.writeEntry( "subscribed-folders", onlySubscribedFolders() ); 00189 config.writeEntry( "loadondemand", loadOnDemand() ); 00190 } 00191 00192 // 00193 // 00194 // Network processing 00195 // 00196 // 00197 00198 MetaData ImapAccountBase::slaveConfig() const { 00199 MetaData m = NetworkAccount::slaveConfig(); 00200 00201 m.insert( "auth", auth() ); 00202 if ( autoExpunge() ) 00203 m.insert( "expunge", "auto" ); 00204 00205 return m; 00206 } 00207 00208 ImapAccountBase::ConnectionState ImapAccountBase::makeConnection() { 00209 if ( mSlave ) return Connected; 00210 00211 if ( mPasswordDialogIsActive ) return Connecting; 00212 if( mAskAgain || passwd().isEmpty() || login().isEmpty() ) { 00213 QString log = login(); 00214 QString pass = passwd(); 00215 // We init "store" to true to indicate that we want to have the 00216 // "keep password" checkbox. Then, we set [Passwords]Keep to 00217 // storePasswd(), so that the checkbox in the dialog will be 00218 // init'ed correctly: 00219 bool store = true; 00220 KConfigGroup passwords( KGlobal::config(), "Passwords" ); 00221 passwords.writeEntry( "Keep", storePasswd() ); 00222 QString msg = i18n("You need to supply a username and a password to " 00223 "access this mailbox."); 00224 mPasswordDialogIsActive = true; 00225 if ( PasswordDialog::getNameAndPassword( log, pass, &store, msg, false, 00226 QString::null, name(), 00227 i18n("Account:") ) 00228 != QDialog::Accepted ) { 00229 checkDone(false, 0); 00230 mPasswordDialogIsActive = false; 00231 return Error; 00232 } 00233 mPasswordDialogIsActive = false; 00234 // The user has been given the chance to change login and 00235 // password, so copy both from the dialog: 00236 setPasswd( pass, store ); 00237 setLogin( log ); 00238 mAskAgain = false; // ### taken from kmacctexppop 00239 } 00240 00241 mSlave = KIO::Scheduler::getConnectedSlave( getUrl(), slaveConfig() ); 00242 if ( !mSlave ) { 00243 KMessageBox::error(0, i18n("Could not start process for %1.") 00244 .arg( getUrl().protocol() ) ); 00245 return Error; 00246 } 00247 00248 return Connecting; 00249 } 00250 00251 void ImapAccountBase::postProcessNewMail( KMFolder * folder ) { 00252 00253 disconnect( folder, SIGNAL(numUnreadMsgsChanged(KMFolder*)), 00254 this, SLOT(postProcessNewMail(KMFolder*)) ); 00255 00256 mCountRemainChecks--; 00257 00258 // count the unread messages 00259 mCountUnread += folder->countUnread(); 00260 if (mCountRemainChecks == 0) 00261 { 00262 // all checks are done 00263 KMBroadcastStatus::instance()->setStatusMsgTransmissionCompleted( 00264 name(), mCountUnread ); 00265 if (mCountUnread > 0 && mCountUnread > mCountLastUnread) { 00266 checkDone(true, mCountUnread); 00267 mCountLastUnread = mCountUnread; 00268 } else { 00269 checkDone(false, 0); 00270 } 00271 setCheckingMail(false); 00272 mCountUnread = 0; 00273 } 00274 } 00275 00276 //----------------------------------------------------------------------------- 00277 void ImapAccountBase::displayProgress() 00278 { 00279 if (mProgressEnabled == mapJobData.isEmpty()) 00280 { 00281 mProgressEnabled = !mapJobData.isEmpty(); 00282 KMBroadcastStatus::instance()->setStatusProgressEnable( "I" + mName, 00283 mProgressEnabled ); 00284 } 00285 mIdle = FALSE; 00286 if (mapJobData.isEmpty()) 00287 mIdleTimer.start(15000); 00288 else 00289 mIdleTimer.stop(); 00290 int total = 0, done = 0; 00291 for (QMap<KIO::Job*, jobData>::Iterator it = mapJobData.begin(); 00292 it != mapJobData.end(); ++it) 00293 { 00294 total += (*it).total; 00295 done += (*it).done; 00296 } 00297 if (total == 0) 00298 { 00299 mTotal = 0; 00300 return; 00301 } 00302 if (total > mTotal) mTotal = total; 00303 done += mTotal - total; 00304 KMBroadcastStatus::instance()->setStatusProgressPercent( "I" + mName, 00305 100*done / mTotal ); 00306 } 00307 00308 //----------------------------------------------------------------------------- 00309 void ImapAccountBase::listDirectory(QString path, bool onlySubscribed, 00310 bool secondStep, KMFolder* parent, bool reset) 00311 { 00312 if (makeConnection() == Error) 00313 return; 00314 // create jobData 00315 jobData jd; 00316 jd.total = 1; jd.done = 0; 00317 // reset for a new listing 00318 if (reset) 00319 mHasInbox = false; 00320 // this inboxonly switch is only needed when you set the INBOX as prefix 00321 jd.inboxOnly = !secondStep && prefix() != "/" 00322 && path == prefix() && !mHasInbox; 00323 jd.onlySubscribed = onlySubscribed; 00324 if (parent) jd.parent = parent; 00325 if (!secondStep) mCreateInbox = FALSE; 00326 // make the URL 00327 KURL url = getUrl(); 00328 url.setPath(((jd.inboxOnly) ? QString("/") : path) 00329 + ";TYPE=" + ((onlySubscribed) ? "LSUB" : "LIST")); 00330 mSubfolderNames.clear(); 00331 mSubfolderPaths.clear(); 00332 mSubfolderMimeTypes.clear(); 00333 // and go 00334 KIO::SimpleJob *job = KIO::listDir(url, FALSE); 00335 KIO::Scheduler::assignJobToSlave(mSlave, job); 00336 insertJob(job, jd); 00337 connect(job, SIGNAL(result(KIO::Job *)), 00338 this, SLOT(slotListResult(KIO::Job *))); 00339 connect(job, SIGNAL(entries(KIO::Job *, const KIO::UDSEntryList &)), 00340 this, SLOT(slotListEntries(KIO::Job *, const KIO::UDSEntryList &))); 00341 } 00342 00343 //----------------------------------------------------------------------------- 00344 void ImapAccountBase::slotListEntries(KIO::Job * job, const KIO::UDSEntryList & uds) 00345 { 00346 JobIterator it = findJob( job ); 00347 if ( it == jobsEnd() ) return; 00348 QString name; 00349 KURL url; 00350 QString mimeType; 00351 for (KIO::UDSEntryList::ConstIterator udsIt = uds.begin(); 00352 udsIt != uds.end(); udsIt++) 00353 { 00354 mimeType = QString::null; 00355 for (KIO::UDSEntry::ConstIterator eIt = (*udsIt).begin(); 00356 eIt != (*udsIt).end(); eIt++) 00357 { 00358 // get the needed information 00359 if ((*eIt).m_uds == KIO::UDS_NAME) 00360 name = (*eIt).m_str; 00361 else if ((*eIt).m_uds == KIO::UDS_URL) 00362 url = KURL((*eIt).m_str, 106); // utf-8 00363 else if ((*eIt).m_uds == KIO::UDS_MIME_TYPE) 00364 mimeType = (*eIt).m_str; 00365 } 00366 if ((mimeType == "inode/directory" || mimeType == "message/digest" 00367 || mimeType == "message/directory") 00368 && name != ".." && (hiddenFolders() || name.at(0) != '.') 00369 && (!(*it).inboxOnly || name.upper() == "INBOX")) 00370 { 00371 if (((*it).inboxOnly || 00372 url.path() == "/INBOX/") && name.upper() == "INBOX" && 00373 !mHasInbox) 00374 { 00375 // our INBOX 00376 mCreateInbox = TRUE; 00377 } 00378 00379 // Some servers send _lots_ of duplicates 00380 if (mSubfolderNames.findIndex(name) == -1) 00381 { 00382 mSubfolderNames.append(name); 00383 mSubfolderPaths.append(url.path()); 00384 mSubfolderMimeTypes.append(mimeType); 00385 } 00386 } 00387 } 00388 } 00389 00390 //----------------------------------------------------------------------------- 00391 void ImapAccountBase::slotListResult(KIO::Job * job) 00392 { 00393 JobIterator it = findJob( job ); 00394 if ( it == jobsEnd() ) return; 00395 if (job->error()) 00396 { 00397 slotSlaveError( mSlave, job->error(), 00398 job->errorText() ); 00399 } 00400 if (!job->error()) 00401 { 00402 // transport the information, include the jobData 00403 emit receivedFolders(mSubfolderNames, mSubfolderPaths, 00404 mSubfolderMimeTypes, *it); 00405 } 00406 if (mSlave) removeJob(job); 00407 mSubfolderNames.clear(); 00408 mSubfolderPaths.clear(); 00409 mSubfolderMimeTypes.clear(); 00410 } 00411 00412 //----------------------------------------------------------------------------- 00413 void ImapAccountBase::changeSubscription( bool subscribe, QString imapPath ) 00414 { 00415 // change the subscription of the folder 00416 KURL url = getUrl(); 00417 url.setPath(imapPath); 00418 00419 QByteArray packedArgs; 00420 QDataStream stream( packedArgs, IO_WriteOnly); 00421 00422 if (subscribe) 00423 stream << (int) 'u' << url; 00424 else 00425 stream << (int) 'U' << url; 00426 00427 // create the KIO-job 00428 if (makeConnection() != Connected) 00429 return; 00430 KIO::SimpleJob *job = KIO::special(url, packedArgs, FALSE); 00431 KIO::Scheduler::assignJobToSlave(mSlave, job); 00432 jobData jd; 00433 jd.total = 1; jd.done = 0; jd.parent = NULL; 00434 // a bit of a hack to save one slot 00435 if (subscribe) jd.onlySubscribed = true; 00436 else jd.onlySubscribed = false; 00437 insertJob(job, jd); 00438 00439 connect(job, SIGNAL(result(KIO::Job *)), 00440 SLOT(slotSubscriptionResult(KIO::Job *))); 00441 } 00442 00443 //----------------------------------------------------------------------------- 00444 void ImapAccountBase::slotSubscriptionResult( KIO::Job * job ) 00445 { 00446 // result of a subscription-job 00447 JobIterator it = findJob( job ); 00448 if ( it == jobsEnd() ) return; 00449 if (job->error()) 00450 { 00451 slotSlaveError( mSlave, job->error(), 00452 job->errorText() ); 00453 } else { 00454 emit subscriptionChanged( 00455 static_cast<KIO::SimpleJob*>(job)->url().path(), (*it).onlySubscribed ); 00456 } 00457 if (mSlave) removeJob(job); 00458 } 00459 00460 //----------------------------------------------------------------------------- 00461 void ImapAccountBase::slotSchedulerSlaveError(KIO::Slave *aSlave, int errorCode, 00462 const QString &errorMsg) 00463 { 00464 if (aSlave != mSlave) return; 00465 slotSlaveError( aSlave, errorCode, errorMsg ); 00466 emit connectionResult( errorCode ); 00467 } 00468 00469 //----------------------------------------------------------------------------- 00470 void ImapAccountBase::slotSchedulerSlaveConnected(KIO::Slave *aSlave) 00471 { 00472 if (aSlave != mSlave) return; 00473 emit connectionResult( 0 ); // success 00474 } 00475 00476 //----------------------------------------------------------------------------- 00477 void ImapAccountBase::slotSlaveError(KIO::Slave *aSlave, int errorCode, 00478 const QString &errorMsg) 00479 { 00480 if (aSlave != mSlave) return; 00481 if (errorCode == KIO::ERR_SLAVE_DIED) slaveDied(); 00482 if (errorCode == KIO::ERR_COULD_NOT_LOGIN && !mStorePasswd) mAskAgain = TRUE; 00483 killAllJobs(); 00484 // check if we still display an error 00485 if ( !mErrorDialogIsActive ) 00486 { 00487 mErrorDialogIsActive = true; 00488 KMessageBox::messageBox(kmkernel->mainWin(), KMessageBox::Error, 00489 KIO::buildErrorString(errorCode, errorMsg), 00490 i18n("Error")); 00491 mErrorDialogIsActive = false; 00492 } else 00493 kdDebug(5006) << "suppressing error:" << errorMsg << endl; 00494 } 00495 00496 //----------------------------------------------------------------------------- 00497 QString ImapAccountBase::jobData::htmlURL() const 00498 { 00499 KURL u( url ); 00500 return u.htmlURL(); 00501 } 00502 00503 //----------------------------------------------------------------------------- 00504 void ImapAccountBase::processNewMailSingleFolder(KMFolder* folder) 00505 { 00506 mFoldersQueuedForChecking.append(folder); 00507 if (checkingMail()) 00508 { 00509 disconnect (this, SIGNAL(finishedCheck(bool)), 00510 this, SLOT(slotCheckQueuedFolders())); 00511 connect (this, SIGNAL(finishedCheck(bool)), 00512 this, SLOT(slotCheckQueuedFolders())); 00513 } else { 00514 slotCheckQueuedFolders(); 00515 } 00516 } 00517 00518 //----------------------------------------------------------------------------- 00519 void ImapAccountBase::slotCheckQueuedFolders() 00520 { 00521 disconnect (this, SIGNAL(finishedCheck(bool)), 00522 this, SLOT(slotCheckQueuedFolders())); 00523 00524 QValueList<QGuardedPtr<KMFolder> > mSaveList = mMailCheckFolders; 00525 mMailCheckFolders = mFoldersQueuedForChecking; 00526 kmkernel->acctMgr()->singleCheckMail(this, true); 00527 mMailCheckFolders = mSaveList; 00528 mFoldersQueuedForChecking.clear(); 00529 } 00530 00531 //----------------------------------------------------------------------------- 00532 void ImapAccountBase::handleBodyStructure( QDataStream & stream, KMMessage * msg, 00533 const AttachmentStrategy *as ) 00534 { 00535 mBodyPartList.clear(); 00536 mCurrentMsg = msg; 00537 // make the parts and fill the mBodyPartList 00538 constructParts( stream, 1, 0, 0, msg->asDwMessage() ); 00539 if ( mBodyPartList.count() == 1 ) // we directly set the body later 00540 msg->deleteBodyParts(); 00541 00542 if ( !as ) 00543 { 00544 kdWarning(5006) << "ImapAccountBase::handleBodyStructure - found no attachment strategy!" << endl; 00545 return; 00546 } 00547 // check the size, if the message is smaller than 5KB then load it in one go 00548 if ( msg->msgLength() < 5000 ) 00549 { 00550 FolderJob *job = msg->parent()->createJob( 00551 msg, FolderJob::tGetMessage, 0, "TEXT" ); 00552 job->start(); 00553 return; 00554 } 00555 00556 // download parts according to attachmentstrategy 00557 BodyVisitor *visitor = BodyVisitorFactory::getVisitor( as ); 00558 visitor->visit( mBodyPartList ); 00559 QPtrList<KMMessagePart> parts = visitor->partsToLoad(); 00560 QPtrListIterator<KMMessagePart> it( parts ); 00561 KMMessagePart *part; 00562 while ( (part = it.current()) != 0 ) 00563 { 00564 ++it; 00565 kdDebug(5006) << "ImapAccountBase::handleBodyStructure - load " << part->partSpecifier() 00566 << " (" << part->originalContentTypeStr() << ")" << endl; 00567 if ( part->loadHeaders() ) 00568 { 00569 kdDebug(5006) << "load HEADER" << endl; 00570 FolderJob *job = msg->parent()->createJob( 00571 msg, FolderJob::tGetMessage, 0, part->partSpecifier()+".MIME" ); 00572 job->start(); 00573 } 00574 if ( part->loadPart() ) 00575 { 00576 kdDebug(5006) << "load Part" << endl; 00577 FolderJob *job = msg->parent()->createJob( 00578 msg, FolderJob::tGetMessage, 0, part->partSpecifier() ); 00579 job->start(); 00580 } 00581 } 00582 delete visitor; 00583 } 00584 00585 //----------------------------------------------------------------------------- 00586 void ImapAccountBase::constructParts( QDataStream & stream, int count, KMMessagePart* parentKMPart, 00587 DwBodyPart * parent, const DwMessage * dwmsg ) 00588 { 00589 int children; 00590 for (int i = 0; i < count; i++) 00591 { 00592 stream >> children; 00593 KMMessagePart* part = new KMMessagePart( stream ); 00594 part->setParent( parentKMPart ); 00595 mBodyPartList.append( part ); 00596 kdDebug(5006) << "ImapAccountBase::constructParts - created id " << part->partSpecifier() 00597 << " of type " << part->originalContentTypeStr() << endl; 00598 DwBodyPart *dwpart = mCurrentMsg->createDWBodyPart( part ); 00599 dwpart->Parse(); // also creates an encapsulated DwMessage if necessary 00600 00601 // kdDebug(5006) << "constructed dwpart " << dwpart << ",dwmsg " << dwmsg << ",parent " << parent 00602 // << ",dwparts msg " << dwpart->Body().Message() << endl; 00603 00604 if ( parent ) 00605 { 00606 // add to parent body 00607 parent->Body().AddBodyPart( dwpart ); 00608 } else if ( part->partSpecifier() != "0" && 00609 !part->partSpecifier().endsWith(".HEADER") ) 00610 { 00611 // add to message 00612 dwmsg->Body().AddBodyPart( dwpart ); 00613 } else 00614 dwpart = 0; 00615 00616 if ( !parentKMPart ) 00617 parentKMPart = part; 00618 00619 if (children > 0) 00620 { 00621 DwBodyPart* newparent = dwpart; 00622 const DwMessage* newmsg = dwmsg; 00623 if ( part->originalContentTypeStr() == "MESSAGE/RFC822" && 00624 dwpart->Body().Message() ) 00625 { 00626 // set the encapsulated message as new parent message 00627 newparent = 0; 00628 newmsg = dwpart->Body().Message(); 00629 } 00630 KMMessagePart* newParentKMPart = part; 00631 if ( part->partSpecifier().endsWith(".HEADER") ) // we don't want headers as parent 00632 newParentKMPart = parentKMPart; 00633 00634 constructParts( stream, children, newParentKMPart, newparent, newmsg ); 00635 } 00636 } 00637 } 00638 00639 } // namespace KMail 00640 00641 #include "imapaccountbase.moc"
KDE Logo
This file is part of the documentation for kmail Library Version 3.2.2.
Documentation copyright © 1996-2004 the KDE developers.
Generated on Wed Jul 28 23:57:58 2004 by doxygen 1.3.7 written by Dimitri van Heesch, © 1997-2003