bincimap

Log | Files | Refs | LICENSE

maildir-scan.cc (15855B)


      1 /* -*- Mode: c++; -*- */
      2 /*  --------------------------------------------------------------------
      3  *  Filename:
      4  *    maildir-scan.cc
      5  *  
      6  *  Description:
      7  *    Implementation of the Maildir class.
      8  *
      9  *  Authors:
     10  *    Andreas Aardal Hanssen <andreas-binc curly bincimap spot org>
     11  *
     12  *  Bugs:
     13  *
     14  *  ChangeLog:
     15  *
     16  *  --------------------------------------------------------------------
     17  *  Copyright 2002-2005 Andreas Aardal Hanssen
     18  *
     19  *  This program is free software; you can redistribute it and/or modify
     20  *  it under the terms of the GNU General Public License as published by
     21  *  the Free Software Foundation; either version 2 of the License, or
     22  *  (at your option) any later version.
     23  *
     24  *  This program is distributed in the hope that it will be useful,
     25  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
     26  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     27  *  GNU General Public License for more details.
     28  *
     29  *  You should have received a copy of the GNU General Public License
     30  *  along with this program; if not, write to the Free Software
     31  *  Foundation, Inc., 59 Temple Street #330, Boston, MA 02111-1307, USA.
     32  *  --------------------------------------------------------------------
     33  */
     34 #ifdef HAVE_CONFIG_H
     35 #include <config.h>
     36 #endif
     37 
     38 #include <fcntl.h>
     39 #include <dirent.h>
     40 #include <sys/stat.h>
     41 #include <unistd.h>
     42 
     43 #include "io.h"
     44 #include "maildir.h"
     45 #include "storage.h"
     46 
     47 using namespace Binc;
     48 using namespace ::std;
     49 
     50 namespace {
     51 
     52   //----------------------------------------------------------------------
     53   class Lock {
     54     string lock;
     55 
     56   public:
     57 
     58     //--
     59     Lock(const string &path)
     60     {
     61       IO &logger = IOFactory::getInstance().get(2);
     62 
     63       lock = (path == "" ? "." : path) + "/bincimap-scan-lock";
     64 
     65       int lockfd = -1;
     66       while ((lockfd = ::open(lock.c_str(),
     67 			      O_CREAT | O_WRONLY | O_EXCL, 0666)) == -1) {
     68 	if (errno != EEXIST) {
     69 	  logger << "unable to lock mailbox: " << lock
     70 		 << ", " << string(strerror(errno)) << endl;
     71 	  return;
     72 	}
     73 				 
     74 	struct stat mystat;
     75 	if (lstat(lock.c_str(), &mystat) == 0) {
     76 	  if ((time(0) - mystat.st_ctime) > 300) {
     77 	    if (unlink(lock.c_str()) == 0) {
     78               logger << "5 minute old lock detected: " << lock
     79                      << ", lock deleted." << endl;
     80               continue;
     81 	    } else {
     82               logger << "failed to force mailbox lock: " << lock
     83                      << ", " << string(strerror(errno)) << endl;
     84             }
     85 	  }
     86 	} else {
     87 	  if (errno != ENOENT) {
     88 	    string err = "invalid lock " + lock + ": "
     89 	      + strerror(errno);
     90 	    logger << err << endl;
     91 	    return;
     92 	  }
     93 	}
     94 	
     95 	// sleep one second.
     96 	sleep(1);
     97       }
     98 
     99       close(lockfd);
    100     }
    101 
    102     //--
    103     ~Lock()
    104     {
    105       IO &logger = IOFactory::getInstance().get(2);
    106 
    107       // remove the lock
    108       if (unlink(lock.c_str()) != 0)
    109 	logger << "failed to unlock mailbox: " << lock << ", "
    110 	       << strerror(errno) << endl;
    111     }
    112   };
    113 }
    114 
    115 //------------------------------------------------------------------------
    116 // scan the maildir. update flags, find messages in new/ and move them
    117 // to cur, setting the recent flag in memory only. check for expunged
    118 // messages. give newly arrived messages uids.
    119 //------------------------------------------------------------------------
    120 Maildir::ScanResult Maildir::scan(bool forceScan)
    121 {
    122   IO &logger = IOFactory::getInstance().get(2);
    123 
    124   const string newpath = path + "/new/";
    125   const string curpath = path + "/cur/";
    126   const string uidvalfilename = path + "/bincimap-uidvalidity";
    127   const string cachefilename = path + "/bincimap-cache";
    128 
    129   // check wether or not we need to bother scanning the folder.
    130   if (firstscan || forceScan) {
    131     struct stat oldstat;
    132     if (stat(newpath.c_str(), &oldstat) != 0) {
    133       setLastError("Invalid Mailbox, " + newpath + ": "
    134 		   + string(strerror(errno)));
    135       return PermanentError;
    136     }
    137 
    138     old_new_st_mtime = oldstat.st_mtime;
    139     old_new_st_ctime = oldstat.st_ctime;
    140 
    141     if (stat(curpath.c_str(), &oldstat) != 0) {
    142       setLastError("Invalid Mailbox, " + curpath + ": "
    143 		   + string(strerror(errno)));
    144       return PermanentError;
    145     }
    146 
    147     old_cur_st_mtime = oldstat.st_mtime;
    148     old_cur_st_ctime = oldstat.st_ctime;
    149 
    150   } else {
    151     struct stat oldcurstat;
    152     struct stat oldnewstat;
    153     if (stat(newpath.c_str(), &oldnewstat) != 0) {
    154       setLastError("Invalid Mailbox, " + newpath + ": "
    155 		   + string(strerror(errno)));
    156       return PermanentError;
    157     }
    158 
    159     if (stat(curpath.c_str(), &oldcurstat) != 0) {
    160       setLastError("Invalid Mailbox, " + curpath + ": "
    161 		   + string(strerror(errno)));
    162       return PermanentError;
    163     }
    164 
    165     if (oldnewstat.st_mtime == old_new_st_mtime
    166 	&& oldnewstat.st_ctime == old_new_st_ctime
    167 	&& oldcurstat.st_mtime == old_cur_st_mtime
    168 	&& oldcurstat.st_ctime == old_cur_st_ctime)
    169       return Success;
    170 
    171     old_cur_st_mtime = oldcurstat.st_mtime;
    172     old_cur_st_ctime = oldcurstat.st_ctime;
    173     old_new_st_mtime = oldnewstat.st_mtime;
    174     old_new_st_ctime = oldnewstat.st_ctime;
    175   }
    176 
    177   // lock the directory as we are scanning. this prevents race
    178   // conditions with uid delegation
    179   Lock lock(path);
    180 
    181   // Read the cache file if it's there. It holds important information
    182   // about the state of the depository, and serves to communicate
    183   // changes to the depot across Binc IMAP instances that can not be
    184   // communicated via the depot itself.
    185   switch (readCache()) {
    186   case NoCache:
    187   case Error:
    188     // An error with reading the cache files when it's not the first
    189     // time we scan the depot is treated as an error.
    190     if (!firstscan && !readOnly) {
    191       old_cur_st_mtime = (time_t) 0;
    192       old_cur_st_ctime = (time_t) 0;
    193       old_new_st_mtime = (time_t) 0;
    194       old_new_st_ctime = (time_t) 0;
    195       return TemporaryError;
    196     }
    197 
    198     uidnextchanged = true;
    199     mailboxchanged = true;
    200     break;
    201   default:
    202     break;
    203   }
    204 
    205   // open new/ directory
    206   DIR *pdir = opendir(newpath.c_str());
    207   if (pdir == 0) {
    208     string reason = "failed to open \"" + newpath + "\" (";
    209     reason += strerror(errno);
    210     reason += ")";
    211     setLastError(reason);
    212 
    213     return PermanentError;
    214   }
    215 
    216   // scan all entries
    217   struct dirent *pdirent;
    218   while ((pdirent = readdir(pdir)) != 0) {
    219     // "Unless you're writing messages to a maildir, the format of a
    220     // unique name is none of your business. A unique name can be
    221     // anything that doesn't contain a colon (or slash) and doesn't
    222     // start with a dot. Do not try to extract information from unique
    223     // names." - The Maildir spec from cr.yp.to
    224     string filename = pdirent->d_name;
    225     if (filename[0] == '.'
    226 	|| filename.find(':') != string::npos
    227 	|| filename.find('/') != string::npos)
    228       continue;
    229 
    230     string fullfilename = newpath + filename;
    231 
    232     // We need to find the timestamp of the message in order to
    233     // determine whether or not it's safe to move the message in from
    234     // new/. qmail's default message file naming algorithm forces us
    235     // to never move messages out of new/ that are less than one
    236     // second old.
    237     struct stat mystat;
    238     if (stat(fullfilename.c_str(), &mystat) != 0) {
    239       if (errno == ENOENT) {
    240         // prevent looping due to stale symlinks
    241         if (lstat(fullfilename.c_str(), &mystat) == 0) { 
    242           logger << "dangling symlink: " << fullfilename << endl; 
    243           continue;
    244         }
    245 
    246 	// a rare race between readdir and stat force us to restart
    247 	// the scan.
    248 	closedir(pdir);
    249 	
    250 	if ((pdir = opendir(newpath.c_str())) == 0) {
    251 	  string reason = "Warning: opendir(\"" + newpath + "\") == 0 (";
    252 	  reason += strerror(errno);
    253 	  reason += ")";
    254 	  setLastError(reason);
    255 	  
    256 	  return PermanentError;
    257 	}
    258       } else
    259 	logger << "junk in Maildir: \"" << fullfilename << "\": "
    260 	       << strerror(errno);
    261 
    262       continue;
    263     }
    264 
    265     // this is important. do not move messages from new/ that are not
    266     // at least one second old or messages may disappear. this
    267     // introduces a special case: we can not cache the old st_ctime
    268     // and st_mtime. the next time the mailbox is scanned, it must not
    269     // simply be skipped. :-)
    270 
    271     vector<MaildirMessage>::const_iterator newIt = newMessages.begin();
    272     bool ours = false;
    273     for (; newIt != newMessages.end(); ++newIt) {
    274       if ((filename == (*newIt).getUnique())
    275 	  && ((*newIt).getInternalFlags() & MaildirMessage::Committed)) {
    276 	ours = true;
    277 	break;
    278       }
    279     }
    280     
    281     if (!ours && ::time(0) <= mystat.st_mtime) {
    282       old_cur_st_mtime = (time_t) 0;
    283       old_cur_st_ctime = (time_t) 0;
    284       old_new_st_mtime = (time_t) 0;
    285       old_new_st_ctime = (time_t) 0;
    286       continue;
    287     }
    288 
    289     // move files from new/ to cur/
    290     string newName = curpath + pdirent->d_name;
    291     if (rename((newpath + pdirent->d_name).c_str(), 
    292 	       (newName + ":2,").c_str()) != 0) {
    293       logger << "error moving messages from"
    294 	" new to cur: skipping " << newpath 
    295 	     << pdirent->d_name << ": " << strerror(errno) << endl;
    296       continue;
    297     }
    298   }
    299 
    300   closedir(pdir);
    301 
    302   // Now, assume all known messages were expunged and have them prove
    303   // otherwise.
    304   {
    305     Mailbox::iterator i = begin(SequenceSet::all(), INCLUDE_EXPUNGED | SQNR_MODE);
    306     for (; i != end(); ++i)
    307       (*i).setExpunged();
    308   }
    309 
    310   // Then, scan cur
    311   // open directory
    312 
    313   if ((pdir = opendir(curpath.c_str())) == 0) {
    314     string reason = "Maildir::scan::opendir(\"" + curpath + "\") == 0 (";
    315     reason += strerror(errno);
    316     reason += ")";
    317 
    318     setLastError(reason);
    319     return PermanentError;
    320   }
    321 
    322   // erase all old maps between fixed filenames and actual file names.
    323   // we'll get a new list now, which will be more up to date.
    324   index.clearFileNames();
    325 
    326   // this is to sort recent messages by internaldate
    327   multimap<unsigned int, MaildirMessage> tempMessageMap;
    328 
    329   // scan all entries
    330   while ((pdirent = readdir(pdir)) != 0) {
    331     string filename = pdirent->d_name;
    332     if (filename[0] == '.')
    333       continue;
    334 
    335     string uniquename;
    336     string standard;
    337     string::size_type pos;
    338     if ((pos = filename.find(':')) != string::npos) {
    339       uniquename = filename.substr(0, pos);
    340 
    341       string tmp = filename.substr(pos);
    342       if ((pos = tmp.find("2,")) != string::npos)
    343 	standard = tmp.substr(pos + 2);
    344 
    345     } else
    346       uniquename = filename;
    347 
    348     unsigned char mflags = Message::F_NONE;
    349     for (string::const_iterator i = standard.begin();
    350 	 i != standard.end(); ++i) {
    351       switch (*i) {
    352       case 'R': mflags |= Message::F_ANSWERED; break;
    353       case 'S': mflags |= Message::F_SEEN; break;
    354       case 'T': mflags |= Message::F_DELETED; break;
    355       case 'D': mflags |= Message::F_DRAFT; break;
    356       case 'F': mflags |= Message::F_FLAGGED; break;
    357       default: break;
    358       }
    359     }
    360 
    361     struct stat mystat;
    362     MaildirMessage *message = get(uniquename);
    363     if (!message || message->getInternalDate() == 0) {
    364       string fullfilename = curpath + filename;
    365       if (stat(fullfilename.c_str(), &mystat) != 0) {
    366 	if (errno == ENOENT) {
    367           // prevent looping due to stale symlinks
    368           if (lstat(fullfilename.c_str(), &mystat) == 0) { 
    369             logger << "dangling symlink: " << fullfilename << endl; 
    370             continue;
    371           }
    372 	  // a rare race between readdir and stat force us to restart
    373 	  // the scan.
    374 	  index.clearFileNames();
    375 	  
    376 	  closedir(pdir);
    377 	  
    378 	  if ((pdir = opendir(newpath.c_str())) == 0) {
    379 	    string reason = "Warning: opendir(\"" + newpath + "\") == 0 (";
    380 	    reason += strerror(errno);
    381 	    reason += ")";
    382 	    setLastError(reason);
    383 	    
    384 	    return PermanentError;
    385 	  }
    386 	}
    387 	
    388 	continue;
    389       }
    390 
    391       mailboxchanged = true;
    392     }
    393 
    394     index.insert(uniquename, 0, filename);
    395   
    396     // If we have this message in memory already..
    397     if (message) {
    398 
    399       if (message->getInternalDate() == 0) {
    400 	mailboxchanged = true;
    401 	message->setInternalDate(mystat.st_mtime);
    402       }
    403 
    404       // then confirm that this message was not expunged
    405       message->setUnExpunged();
    406 
    407       // update the flags with what new flags we found in the filename,
    408       // but keep the \Recent flag regardless.
    409       if (mflags != (message->getStdFlags() & ~Message::F_RECENT)) {
    410 	int oldflags = message->getStdFlags();
    411 	message->resetStdFlags();
    412 	message->setStdFlag(mflags | (oldflags & Message::F_RECENT));
    413       }
    414 
    415       continue;
    416     }
    417 
    418     // Wait with delegating UIDs until all entries have been
    419     // read. Only then can we sort by internaldate and delegate new
    420     // UIDs in the proper order.
    421     MaildirMessage m(*this);
    422     m.setUID(0);
    423     m.setSize(0);
    424     m.setInternalDate(mystat.st_mtime);
    425     m.setStdFlag((mflags | Message::F_RECENT) & ~Message::F_EXPUNGED);
    426     m.setUnique(uniquename);
    427     tempMessageMap.insert(make_pair((unsigned int) mystat.st_mtime, m));
    428 
    429     mailboxchanged = true;
    430   }
    431 
    432   closedir(pdir);
    433 
    434   // Recent messages are added, ordered by internaldate. Also delegate
    435   // UIDs.
    436   {
    437     int readonlyuidnext = uidnext;
    438     multimap<unsigned int, MaildirMessage>::iterator i = tempMessageMap.begin();
    439     while (i != tempMessageMap.end()) {
    440       i->second.setUID(readOnly ? readonlyuidnext++ : uidnext++);
    441       multimap<unsigned int, MaildirMessage>::iterator itmp = i;
    442       ++itmp;
    443       add(i->second);
    444       tempMessageMap.erase(i);
    445       i = itmp;
    446       uidnextchanged = true;
    447     }
    448   }
    449 
    450   tempMessageMap.clear();
    451 
    452   // Some messages may have been detected by another server, and then
    453   // written to the cache, and then expunged again without us
    454   // accessing the cache nor the maildir. We need to remove those from
    455   // our internal message list.
    456   Mailbox::iterator jj = begin(SequenceSet::all(), INCLUDE_EXPUNGED | SQNR_MODE);
    457   while (jj != end()) {
    458     MaildirMessage &message = (MaildirMessage &)*jj;
    459 
    460     if (message.isExpunged()) {
    461       if (message.getInternalFlags() & MaildirMessage::JustArrived) {
    462 	jj.erase();
    463 	continue;
    464       }
    465     } else if (message.getInternalFlags() & MaildirMessage::JustArrived) {
    466       message.clearInternalFlag(MaildirMessage::JustArrived);
    467     }
    468 
    469     ++jj;
    470   }
    471 
    472   // Special case: The first time we scan is in SELECT. All flags
    473   // changes for new messages will then appear to be recent, and
    474   // to avoid this to be sent to the client as a pending update,
    475   // we explicitly unset the "flagsChanged" flag in all messages.
    476   if (firstscan) {
    477     unsigned int lastuid = 0;
    478 
    479     Mailbox::iterator ii
    480       = begin(SequenceSet::all(), INCLUDE_EXPUNGED | SQNR_MODE);
    481     for (; ii != end(); ++ii) {
    482       MaildirMessage &message = (MaildirMessage &)*ii;
    483       message.clearInternalFlag(MaildirMessage::JustArrived);
    484 
    485       if (lastuid < message.getUID())
    486 	lastuid = message.getUID();
    487       else {
    488 	logger << "UID values are not strictly ascending in this"
    489 	  " mailbox: " << path << ". This is usually caused by "
    490 	       << "access from a broken accessor. Bumping UIDVALIDITY." 
    491 	       << endl;
    492 
    493 	setLastError("An error occurred while scanning the mailbox. "
    494 		     "Please contact your system administrator.");
    495 
    496 	if (!readOnly) {
    497 	  bumpUidValidity(path);
    498 
    499 	  old_cur_st_mtime = (time_t) 0;
    500 	  old_cur_st_ctime = (time_t) 0;
    501 	  old_new_st_mtime = (time_t) 0;
    502 	  old_new_st_ctime = (time_t) 0;
    503 	  return TemporaryError;
    504 	} else {
    505       	  return PermanentError;
    506 	}
    507       }
    508       
    509       message.setFlagsUnchanged();
    510     }
    511   }
    512 
    513   if (mailboxchanged && !readOnly) {
    514     if (!writeCache())
    515       return PermanentError;
    516 
    517     mailboxchanged = false;
    518   }
    519 
    520   if (uidnextchanged && !readOnly) {
    521     Storage uidvalfile(uidvalfilename, Storage::WriteOnly);
    522     uidvalfile.put("depot", "_uidvalidity", toString(uidvalidity));
    523     uidvalfile.put("depot", "_uidnext", toString(uidnext));
    524     uidvalfile.put("depot", "_version", UIDVALFILEVERSION);
    525     if (!uidvalfile.commit()) {
    526       setLastError("Unable to save cache file.");
    527       return PermanentError;
    528     }
    529 
    530     uidnextchanged = false;
    531   }
    532 
    533   firstscan = false;
    534   newMessages.clear();
    535   return Success;
    536 }