/* -*- Mode: c++; -*- */
/* --------------------------------------------------------------------
* Filename:
* maildir-scan.cc
*
* Description:
* Implementation of the Maildir class.
*
* Authors:
* Andreas Aardal Hanssen <andreas-binc curly bincimap spot org>
*
* Bugs:
*
* ChangeLog:
*
* --------------------------------------------------------------------
* Copyright 2002-2005 Andreas Aardal Hanssen
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Street #330, Boston, MA 02111-1307, USA.
* --------------------------------------------------------------------
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <fcntl.h>
#include <dirent.h>
#include <sys/stat.h>
#include <unistd.h>
#include "io.h"
#include "maildir.h"
#include "storage.h"
using namespace Binc;
using namespace ::std;
namespace {
//----------------------------------------------------------------------
class Lock {
string lock;
public:
//--
Lock(const string &path)
{
IO &logger = IOFactory::getInstance().get(2);
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) {
logger << "unable to lock mailbox: " << lock
<< ", " << string(strerror(errno)) << endl;
return;
}
struct stat mystat;
if (lstat(lock.c_str(), &mystat) == 0) {
if ((time(0) - mystat.st_ctime) > 300) {
if (unlink(lock.c_str()) == 0) {
logger << "5 minute old lock detected: " << lock
<< ", lock deleted." << endl;
continue;
} else {
logger << "failed to force mailbox lock: " << lock
<< ", " << string(strerror(errno)) << endl;
}
}
} else {
if (errno != ENOENT) {
string err = "invalid lock " + lock + ": "
+ strerror(errno);
logger << err << endl;
return;
}
}
// sleep one second.
sleep(1);
}
close(lockfd);
}
//--
~Lock()
{
IO &logger = IOFactory::getInstance().get(2);
// remove the lock
if (unlink(lock.c_str()) != 0)
logger << "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)
{
IO &logger = IOFactory::getInstance().get(2);
const string newpath = path + "/new/";
const string curpath = path + "/cur/";
const string uidvalfilename = path + "/bincimap-uidvalidity";
const string cachefilename = 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;
} else {
struct stat oldcurstat;
struct stat oldnewstat;
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 (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)
return Success;
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;
}
uidnextchanged = true;
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) {
logger << "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
logger << "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) {
logger << "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;
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) {
logger << "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 in the proper order.
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. Also delegate
// UIDs.
{
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;
uidnextchanged = true;
}
}
tempMessageMap.clear();
// Some messages may have been detected by another server, and then
// written to the cache, and then expunged again without us
// accessing the cache nor the maildir. We need to remove those from
// our internal message list.
Mailbox::iterator jj = begin(SequenceSet::all(), INCLUDE_EXPUNGED | SQNR_MODE);
while (jj != end()) {
MaildirMessage &message = (MaildirMessage &)*jj;
if (message.isExpunged()) {
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 {
logger << "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;
}
if (uidnextchanged && !readOnly) {
Storage uidvalfile(uidvalfilename, Storage::WriteOnly);
uidvalfile.put("depot", "_uidvalidity", toString(uidvalidity));
uidvalfile.put("depot", "_uidnext", toString(uidnext));
uidvalfile.put("depot", "_version", UIDVALFILEVERSION);
if (!uidvalfile.commit()) {
setLastError("Unable to save cache file.");
return PermanentError;
}
uidnextchanged = false;
}
firstscan = false;
newMessages.clear();
return Success;
}
syntax highlighted by Code2HTML, v. 0.9.1