summaryrefslogtreecommitdiff
path: root/src/maildir-scan.cc
diff options
context:
space:
mode:
Diffstat (limited to 'src/maildir-scan.cc')
-rw-r--r--src/maildir-scan.cc476
1 files changed, 476 insertions, 0 deletions
diff --git a/src/maildir-scan.cc b/src/maildir-scan.cc
new file mode 100644
index 0000000..4628d5b
--- /dev/null
+++ b/src/maildir-scan.cc
@@ -0,0 +1,476 @@
+/** --------------------------------------------------------------------
+ * @file maildir-scan.cc
+ * @brief Implementation of the Maildir class.
+ * @author Andreas Aardal Hanssen
+ * @date 2002-2005
+ * ----------------------------------------------------------------- **/
+#include <fcntl.h>
+#include <dirent.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <errno.h>
+
+#include "iodevice.h"
+#include "iofactory.h"
+#include "maildir.h"
+
+using namespace Binc;
+using namespace ::std;
+
+Lock::Lock(const string &path)
+{
+ lock = (path == "" ? "." : path) + "/bincimap-scan-lock";
+
+ int lockfd = -1;
+ while ((lockfd = ::open(lock.c_str(),
+ O_CREAT | O_WRONLY | O_EXCL, 0666)) == -1) {
+ if (errno != EEXIST) {
+ bincWarning << "unable to lock mailbox: " << lock
+ << ", " << string(strerror(errno)) << endl;
+ return;
+ }
+
+ struct stat mystat;
+ bincWarning << "waiting for mailbox lock " << lock << "." << endl;
+ if (lstat(lock.c_str(), &mystat) == 0) {
+ if ((time(0) - mystat.st_ctime) > 300) {
+ if (unlink(lock.c_str()) == 0) continue;
+ else bincWarning << "failed to force mailbox lock: " << lock
+ << ", " << string(strerror(errno)) << endl;
+ }
+ } else {
+ if (errno != ENOENT) {
+ string err = "invalid lock " + lock + ": "
+ + strerror(errno);
+ bincWarning << err << endl;
+ return;
+ }
+ }
+
+ // sleep one second.
+ sleep(1);
+ }
+
+ close(lockfd);
+}
+
+Lock::~Lock()
+{
+ // remove the lock
+ if (unlink(lock.c_str()) != 0)
+ bincWarning << "failed to unlock mailbox: " << lock << ", "
+ << strerror(errno) << endl;
+}
+
+//------------------------------------------------------------------------
+// scan the maildir. update flags, find messages in new/ and move them
+// to cur, setting the recent flag in memory only. check for expunged
+// messages. give newly arrived messages uids.
+//------------------------------------------------------------------------
+Maildir::ScanResult Maildir::scan(bool forceScan)
+{
+ const string newpath = path + "/new/";
+ const string curpath = path + "/cur/";
+ const string cachepath = path + "/bincimap-cache";
+
+ // check wether or not we need to bother scanning the folder.
+ if (firstscan || forceScan) {
+ struct stat oldstat;
+ if (stat(newpath.c_str(), &oldstat) != 0) {
+ setLastError("Invalid Mailbox, " + newpath + ": "
+ + string(strerror(errno)));
+ return PermanentError;
+ }
+
+ old_new_st_mtime = oldstat.st_mtime;
+ old_new_st_ctime = oldstat.st_ctime;
+
+ if (stat(curpath.c_str(), &oldstat) != 0) {
+ setLastError("Invalid Mailbox, " + curpath + ": "
+ + string(strerror(errno)));
+ return PermanentError;
+ }
+
+ old_cur_st_mtime = oldstat.st_mtime;
+ old_cur_st_ctime = oldstat.st_ctime;
+
+ if (stat(cachepath.c_str(), &oldstat) == 0) {
+ old_bincimap_cache_st_mtime = oldstat.st_mtime;
+ old_bincimap_cache_st_ctime = oldstat.st_ctime;
+ } else {
+ old_bincimap_cache_st_mtime = 0;
+ old_bincimap_cache_st_ctime = 0;
+ }
+ } else {
+ struct stat oldcurstat;
+ struct stat oldnewstat;
+ struct stat oldbincimapcachestat;
+ if (stat(newpath.c_str(), &oldnewstat) != 0) {
+ setLastError("Invalid Mailbox, " + newpath + ": "
+ + string(strerror(errno)));
+ return PermanentError;
+ }
+
+ if (stat(curpath.c_str(), &oldcurstat) != 0) {
+ setLastError("Invalid Mailbox, " + curpath + ": "
+ + string(strerror(errno)));
+ return PermanentError;
+ }
+
+ if (stat(cachepath.c_str(), &oldbincimapcachestat) != 0) {
+ oldbincimapcachestat.st_ctime = 0;
+ oldbincimapcachestat.st_mtime = 0;
+ }
+
+ if (oldnewstat.st_mtime == old_new_st_mtime
+ && oldnewstat.st_ctime == old_new_st_ctime
+ && oldcurstat.st_mtime == old_cur_st_mtime
+ && oldcurstat.st_ctime == old_cur_st_ctime
+ && oldbincimapcachestat.st_mtime == old_bincimap_cache_st_mtime
+ && oldbincimapcachestat.st_ctime == old_bincimap_cache_st_ctime) {
+ return Success;
+ }
+
+ old_bincimap_cache_st_mtime = oldbincimapcachestat.st_mtime;
+ old_bincimap_cache_st_ctime = oldbincimapcachestat.st_ctime;
+ old_cur_st_mtime = oldcurstat.st_mtime;
+ old_cur_st_ctime = oldcurstat.st_ctime;
+ old_new_st_mtime = oldnewstat.st_mtime;
+ old_new_st_ctime = oldnewstat.st_ctime;
+ }
+
+ // lock the directory as we are scanning. this prevents race
+ // conditions with uid delegation
+ Lock lock(path);
+
+ // Read the cache file if it's there. It holds important information
+ // about the state of the depository, and serves to communicate
+ // changes to the depot across Binc IMAP instances that can not be
+ // communicated via the depot itself.
+ switch (readCache()) {
+ case NoCache:
+ case Error:
+ // An error with reading the cache files when it's not the first
+ // time we scan the depot is treated as an error.
+ if (!firstscan && !readOnly) {
+ old_cur_st_mtime = (time_t) 0;
+ old_cur_st_ctime = (time_t) 0;
+ old_new_st_mtime = (time_t) 0;
+ old_new_st_ctime = (time_t) 0;
+ return TemporaryError;
+ }
+ mailboxchanged = true;
+ break;
+ default:
+ break;
+ }
+
+ // open new/ directory
+ DIR *pdir = opendir(newpath.c_str());
+ if (pdir == 0) {
+ string reason = "failed to open \"" + newpath + "\" (";
+ reason += strerror(errno);
+ reason += ")";
+ setLastError(reason);
+
+ return PermanentError;
+ }
+
+ // scan all entries
+ struct dirent *pdirent;
+ while ((pdirent = readdir(pdir)) != 0) {
+ // "Unless you're writing messages to a maildir, the format of a
+ // unique name is none of your business. A unique name can be
+ // anything that doesn't contain a colon (or slash) and doesn't
+ // start with a dot. Do not try to extract information from unique
+ // names." - The Maildir spec from cr.yp.to
+ string filename = pdirent->d_name;
+ if (filename[0] == '.'
+ || filename.find(':') != string::npos
+ || filename.find('/') != string::npos)
+ continue;
+
+ string fullfilename = newpath + filename;
+
+ // We need to find the timestamp of the message in order to
+ // determine whether or not it's safe to move the message in from
+ // new/. qmail's default message file naming algorithm forces us
+ // to never move messages out of new/ that are less than one
+ // second old.
+ struct stat mystat;
+ if (stat(fullfilename.c_str(), &mystat) != 0) {
+ if (errno == ENOENT) {
+ // prevent looping due to stale symlinks
+ if (lstat(fullfilename.c_str(), &mystat) == 0) {
+ bincWarning << "dangling symlink: " << fullfilename << endl;
+ continue;
+ }
+
+ // a rare race between readdir and stat force us to restart the scan.
+ closedir(pdir);
+
+ if ((pdir = opendir(newpath.c_str())) == 0) {
+ string reason = "Warning: opendir(\"" + newpath + "\") == 0 (";
+ reason += strerror(errno);
+ reason += ")";
+ setLastError(reason);
+ return PermanentError;
+ }
+ } else
+ bincWarning << "junk in Maildir: \"" << fullfilename << "\": "
+ << strerror(errno);
+
+ continue;
+ }
+
+ // this is important. do not move messages from new/ that are not
+ // at least one second old or messages may disappear. this
+ // introduces a special case: we can not cache the old st_ctime
+ // and st_mtime. the next time the mailbox is scanned, it must not
+ // simply be skipped. :-)
+
+ vector<MaildirMessage>::const_iterator newIt = newMessages.begin();
+ bool ours = false;
+ for (; newIt != newMessages.end(); ++newIt) {
+ if ((filename == (*newIt).getUnique())
+ && ((*newIt).getInternalFlags() & MaildirMessage::Committed)) {
+ ours = true;
+ break;
+ }
+ }
+
+ if (!ours && ::time(0) <= mystat.st_mtime) {
+ old_cur_st_mtime = (time_t) 0;
+ old_cur_st_ctime = (time_t) 0;
+ old_new_st_mtime = (time_t) 0;
+ old_new_st_ctime = (time_t) 0;
+ continue;
+ }
+
+ // move files from new/ to cur/
+ string newName = curpath + pdirent->d_name;
+ if (rename((newpath + pdirent->d_name).c_str(),
+ (newName + ":2,").c_str()) != 0) {
+ bincWarning << "error moving messages from new to cur: skipping "
+ << newpath
+ << pdirent->d_name << ": " << strerror(errno) << endl;
+ continue;
+ }
+ }
+
+ closedir(pdir);
+
+ // Now, assume all known messages were expunged and have them prove
+ // otherwise.
+ {
+ Mailbox::iterator i = begin(SequenceSet::all(),
+ INCLUDE_EXPUNGED | SQNR_MODE);
+ for (; i != end(); ++i)
+ (*i).setExpunged();
+ }
+
+ // Then, scan cur
+ // open directory
+
+ if ((pdir = opendir(curpath.c_str())) == 0) {
+ string reason = "Maildir::scan::opendir(\"" + curpath + "\") == 0 (";
+ reason += strerror(errno);
+ reason += ")";
+
+ setLastError(reason);
+ return PermanentError;
+ }
+
+ // erase all old maps between fixed filenames and actual file names.
+ // we'll get a new list now, which will be more up to date.
+ index.clearFileNames();
+
+ // this is to sort recent messages by internaldate
+ multimap<unsigned int, MaildirMessage> tempMessageMap;
+
+ // scan all entries
+ while ((pdirent = readdir(pdir)) != 0) {
+ string filename = pdirent->d_name;
+ if (filename[0] == '.') continue;
+
+ string uniquename;
+ string standard;
+ string::size_type pos;
+ if ((pos = filename.find(':')) != string::npos) {
+ uniquename = filename.substr(0, pos);
+ string tmp = filename.substr(pos);
+ if ((pos = tmp.find("2,")) != string::npos)
+ standard = tmp.substr(pos + 2);
+ } else
+ uniquename = filename;
+
+ unsigned char mflags = Message::F_NONE;
+ for (string::const_iterator i = standard.begin();
+ i != standard.end(); ++i) {
+ switch (*i) {
+ case 'R': mflags |= Message::F_ANSWERED; break;
+ case 'S': mflags |= Message::F_SEEN; break;
+ case 'T': mflags |= Message::F_DELETED; break;
+ case 'D': mflags |= Message::F_DRAFT; break;
+ case 'F': mflags |= Message::F_FLAGGED; break;
+ case 'P': mflags |= Message::F_PASSED; break;
+ default: break;
+ }
+ }
+
+ struct stat mystat;
+ MaildirMessage *message = get(uniquename);
+ if (!message || message->getInternalDate() == 0) {
+ string fullfilename = curpath + filename;
+ if (stat(fullfilename.c_str(), &mystat) != 0) {
+ if (errno == ENOENT) {
+ // prevent looping due to stale symlinks
+ if (lstat(fullfilename.c_str(), &mystat) == 0) {
+ bincWarning << "dangling symlink: " << fullfilename << endl;
+ continue;
+ }
+ // a rare race between readdir and stat force us to restart
+ // the scan.
+ index.clearFileNames();
+
+ closedir(pdir);
+
+ if ((pdir = opendir(newpath.c_str())) == 0) {
+ string reason = "Warning: opendir(\"" + newpath + "\") == 0 (";
+ reason += strerror(errno);
+ reason += ")";
+ setLastError(reason);
+ return PermanentError;
+ }
+ }
+
+ continue;
+ }
+
+ mailboxchanged = true;
+ }
+
+ index.insert(uniquename, 0, filename);
+
+ // If we have this message in memory already..
+ if (message) {
+ if (message->getInternalDate() == 0) {
+ mailboxchanged = true;
+ message->setInternalDate(mystat.st_mtime);
+ }
+
+ // then confirm that this message was not expunged
+ message->setUnExpunged();
+
+ // update the flags with what new flags we found in the filename,
+ // but keep the \Recent flag regardless.
+ if (mflags != (message->getStdFlags() & ~Message::F_RECENT)) {
+ int oldflags = message->getStdFlags();
+ message->resetStdFlags();
+ message->setStdFlag(mflags | (oldflags & Message::F_RECENT));
+ }
+
+ continue;
+ }
+
+ // Wait with delegating UIDs until all entries have been
+ // read. Only then can we sort by internaldate and delegate new
+ // UIDs.
+ MaildirMessage m(*this);
+ m.setUID(0);
+ m.setSize(0);
+ m.setInternalDate(mystat.st_mtime);
+ m.setStdFlag((mflags | Message::F_RECENT) & ~Message::F_EXPUNGED);
+ m.setUnique(uniquename);
+ tempMessageMap.insert(make_pair((unsigned int) mystat.st_mtime, m));
+
+ mailboxchanged = true;
+ }
+
+ closedir(pdir);
+
+ // Recent messages are added, ordered by internaldate.
+ {
+ int readonlyuidnext = uidnext;
+ multimap<unsigned int, MaildirMessage>::iterator i = tempMessageMap.begin();
+ while (i != tempMessageMap.end()) {
+ i->second.setUID(readOnly ? readonlyuidnext++ : uidnext++);
+ multimap<unsigned int, MaildirMessage>::iterator itmp = i;
+ ++itmp;
+ add(i->second);
+ tempMessageMap.erase(i);
+ i = itmp;
+ mailboxchanged = true;
+ }
+ }
+
+ tempMessageMap.clear();
+
+ // Messages that existed in the cache that we read, but did not
+ // exist in the Maildir, are removed from the messages list.
+ Mailbox::iterator jj = begin(SequenceSet::all(), INCLUDE_EXPUNGED | SQNR_MODE);
+ while (jj != end()) {
+ MaildirMessage &message = (MaildirMessage &)*jj;
+
+ if (message.isExpunged()) {
+ mailboxchanged = true;
+ if (message.getInternalFlags() & MaildirMessage::JustArrived) {
+ jj.erase();
+ continue;
+ }
+ } else if (message.getInternalFlags() & MaildirMessage::JustArrived) {
+ message.clearInternalFlag(MaildirMessage::JustArrived);
+ }
+
+ ++jj;
+ }
+
+ // Special case: The first time we scan is in SELECT. All flags
+ // changes for new messages will then appear to be recent, and
+ // to avoid this to be sent to the client as a pending update,
+ // we explicitly unset the "flagsChanged" flag in all messages.
+ if (firstscan) {
+ unsigned int lastuid = 0;
+
+ Mailbox::iterator ii
+ = begin(SequenceSet::all(), INCLUDE_EXPUNGED | SQNR_MODE);
+ for (; ii != end(); ++ii) {
+ MaildirMessage &message = (MaildirMessage &)*ii;
+ message.clearInternalFlag(MaildirMessage::JustArrived);
+
+ if (lastuid < message.getUID())
+ lastuid = message.getUID();
+ else {
+ bincWarning << "UID values are not strictly ascending in this"
+ " mailbox: " << path << ". This is usually caused by "
+ << "access from a broken accessor. Bumping UIDVALIDITY."
+ << endl;
+
+ setLastError("An error occurred while scanning the mailbox. "
+ "Please contact your system administrator.");
+
+ if (!readOnly) {
+ bumpUidValidity(path);
+ old_cur_st_mtime = (time_t) 0;
+ old_cur_st_ctime = (time_t) 0;
+ old_new_st_mtime = (time_t) 0;
+ old_new_st_ctime = (time_t) 0;
+ return TemporaryError;
+ } else {
+ return PermanentError;
+ }
+ }
+
+ message.setFlagsUnchanged();
+ }
+ }
+
+ if (mailboxchanged && !readOnly) {
+ if (!writeCache()) return PermanentError;
+ mailboxchanged = false;
+ }
+
+ firstscan = false;
+ newMessages.clear();
+ return Success;
+}