diff options
Diffstat (limited to 'src/maildir.cc')
-rw-r--r-- | src/maildir.cc | 839 |
1 files changed, 839 insertions, 0 deletions
diff --git a/src/maildir.cc b/src/maildir.cc new file mode 100644 index 0000000..11f472c --- /dev/null +++ b/src/maildir.cc @@ -0,0 +1,839 @@ +/** -------------------------------------------------------------------- + * @file maildir.cc + * @brief Implementation of the Maildir class. + * @author Andreas Aardal Hanssen + * @date 2002-2005 + * ----------------------------------------------------------------- **/ +#include <iostream> +#include <iomanip> +#include <algorithm> + +#include <ctype.h> +#include <dirent.h> +#include <errno.h> +#include <fcntl.h> +#include <stdio.h> +#include <sys/stat.h> +#include <sys/time.h> +#include <sys/types.h> +#include <unistd.h> + +#include "convert.h" +#include "iodevice.h" +#include "iofactory.h" +#include "maildir.h" +#include "maildirmessage.h" +#include "pendingupdates.h" +#include "session.h" +#include "status.h" +#include "globals.h" + +using namespace ::std; +using namespace Binc; + +// Used to generate the unique names for Maildir delivery +static int numDeliveries = 0; + +//------------------------------------------------------------------------ +Maildir::iterator::iterator(void) +{ +} + +//------------------------------------------------------------------------ +Maildir::iterator::iterator(Maildir *home, + MessageMap::iterator it, + const SequenceSet &_bset, + unsigned int _mod) + : BaseIterator(1), mailbox(home), bset(_bset), mod(_mod), i(it) +{ + uidmax = home->getMaxUid(); + sqnrmax = home->getMaxSqnr(); +} + +//------------------------------------------------------------------------ +Maildir::iterator::iterator(const iterator ©) + : BaseIterator(copy.sqnr), mailbox(copy.mailbox), + bset(copy.bset), mod(copy.mod), i(copy.i), uidmax(copy.uidmax), + sqnrmax(copy.sqnrmax) +{ +} + +//------------------------------------------------------------------------ +Maildir::iterator &Maildir::iterator::operator =(const iterator ©) +{ + sqnr = copy.sqnr; + mailbox = copy.mailbox; + bset = copy.bset; + mod = copy.mod; + i = copy.i; + uidmax = copy.uidmax; + sqnrmax = copy.sqnrmax; + return *this; +} + +//------------------------------------------------------------------------ +Maildir::iterator::~iterator(void) +{ +} + +//------------------------------------------------------------------------ +MaildirMessage &Maildir::iterator::curMessage(void) +{ + return i->second; +} + +//------------------------------------------------------------------------ +Message &Maildir::iterator::operator *(void) +{ + return curMessage(); +} + +//------------------------------------------------------------------------ +void Maildir::iterator::operator ++(void) +{ + ++i; + ++sqnr; + reposition(); +} + +//------------------------------------------------------------------------ +bool Maildir::iterator::operator ==(const BaseIterator &a) const +{ + const iterator *b = dynamic_cast<const iterator *>(&a); + return b ? (i == b->i) : false; +} + +//------------------------------------------------------------------------ +bool Maildir::iterator::operator !=(const BaseIterator &a) const +{ + return !((*this) == a); +} + +//------------------------------------------------------------------------ +void Maildir::iterator::reposition(void) +{ + for (;;) { + if (i == mailbox->messages.end()) + break; + + Message &message = curMessage(); + if ((mod & SKIP_EXPUNGED) && message.isExpunged()) { + ++i; + continue; + } + + bool inset = false; + if (mod & SQNR_MODE) { + if (bset.isInSet(sqnr) || (!bset.isLimited() && sqnr == sqnrmax)) + inset = true; + } else { + if (bset.isInSet(message.getUID()) || (!bset.isLimited() && message.getUID() == uidmax)) + inset = true; + } + + if (!inset) { + ++i; + if (!message.isExpunged()) + ++sqnr; + continue; + } + + break; + } +} + +//------------------------------------------------------------------------ +Mailbox::iterator Maildir::begin(const SequenceSet &bset, + unsigned int mod) const +{ + beginIterator = iterator((Maildir *)this, messages.begin(), bset, mod); + beginIterator.reposition(); + + return Mailbox::iterator(beginIterator); +} + +//------------------------------------------------------------------------ +Mailbox::iterator Maildir::end(void) const +{ + endIterator = iterator((Maildir *)this, messages.end(), + endIterator.bset, endIterator.mod); + return Mailbox::iterator(endIterator); +} + +//------------------------------------------------------------------------ +void Maildir::iterator::erase(void) +{ + MessageMap::iterator iter = i; + ++iter; + + MaildirMessageCache::getInstance().removeStatus(&curMessage()); + mailbox->index.remove(i->second.getUnique()); + mailbox->messages.erase(i); + + i = iter; + reposition(); +} + +//------------------------------------------------------------------------ +Maildir::Maildir(void) : Mailbox() +{ + firstscan = true; + cacheRead = false; + uidvalidity = 0; + uidnext = 1; + selected = false; + oldrecent = 0; + oldexists = 0; +} + +//------------------------------------------------------------------------ +Maildir::~Maildir(void) +{ +} + +//------------------------------------------------------------------------ +void Maildir::setPath(const string &path_in) +{ + path = path_in; +} + +//------------------------------------------------------------------------ +bool Maildir::getUpdates(bool doscan, unsigned int type, + PendingUpdates &updates, bool forceScan) +{ + if (doscan && scan(forceScan) != Success) + return false; + + unsigned int exists = 0; + unsigned int recent = 0; + bool displayExists = false; + + // count messages, find recent + if (!readOnly && (type & PendingUpdates::EXPUNGE)) { + Mailbox::iterator i = begin(SequenceSet::all(), + INCLUDE_EXPUNGED | SQNR_MODE); + + while (i != end()) { + Message &message = *i; + + if (message.isExpunged()) { + updates.addExpunged(i.getSqnr()); + i.erase(); + mailboxchanged = true; + displayExists = true; + } else + ++i; + } + } + + Mailbox::iterator i = begin(SequenceSet::all(), + INCLUDE_EXPUNGED | SQNR_MODE); + for (; i != end(); ++i) { + Message & message = *i; + // at this point, there is a message that is not expunged + ++exists; + if (message.getStdFlags() & Message::F_RECENT) ++recent; + } + + if (displayExists || exists != oldexists) + updates.setExists(oldexists = exists); + + if (recent != oldrecent) + updates.setRecent(oldrecent = recent); + + if (type & PendingUpdates::FLAGS) { + Mailbox::iterator i = begin(SequenceSet::all(), SQNR_MODE); + for (; i != end(); ++i) { + Message &message = *i; + if (message.hasFlagsChanged()) { + int flags = message.getStdFlags(); + + updates.addFlagUpdates(i.getSqnr(), message.getUID(), flags, + message.getCustomFlags()); + + message.setFlagsUnchanged(); + } + } + } + + return true; +} + +//------------------------------------------------------------------------ +bool Maildir::isMailbox(const std::string &s_in) const +{ + struct stat mystat; + + return ((stat((s_in + "/cur").c_str(), &mystat) == 0 + && S_ISDIR(mystat.st_mode)) + && (stat((s_in + "/new").c_str(), &mystat) == 0 + && S_ISDIR(mystat.st_mode)) + && (stat((s_in + "/tmp").c_str(), &mystat) == 0 + && S_ISDIR(mystat.st_mode))); +} + +//------------------------------------------------------------------------ +const std::string Maildir::getTypeName(void) const +{ + return "Maildir"; +} + +//------------------------------------------------------------------------ +void Maildir::bumpUidValidity(const string &s_in) const +{ + unlink((s_in + "/bincimap-uidvalidity").c_str()); + unlink((s_in + "/bincimap-cache").c_str()); +} + +//------------------------------------------------------------------------ +bool Maildir::isMarked(const std::string &s_in) const +{ + DIR *dirp = opendir((s_in + "/new").c_str()); + if (dirp == 0) return false; + + struct dirent *direntp; + while ((direntp = readdir(dirp)) != 0) { + string s = direntp->d_name; + if (s[0] != '.' + && s.find('/') == string::npos + && s.find(':') == string::npos) { + closedir(dirp); + return true; + } + } + + closedir(dirp); + return false; +} + +//------------------------------------------------------------------------ +unsigned int Maildir::getStatusID(const string &path) const +{ + unsigned int statusid = 0; + struct stat mystat; + if (stat((path + "/new/").c_str(), &mystat) == 0) + statusid = mystat.st_ctime; + + if (stat((path + "/cur/").c_str(), &mystat) == 0) + statusid += mystat.st_ctime; + + if (stat((path + "/bincimap-cache").c_str(), &mystat) == 0) + statusid += mystat.st_ctime; + + return statusid; +} + +//------------------------------------------------------------------------ +bool Maildir::getStatus(const string &path, Status &s) const +{ + unsigned int messages = 0; + unsigned int unseen = 0; + unsigned int recent = 0; + + unsigned int readUidValidity; + unsigned int readUidNext = 1; + map<string, bool> mincache; + + const string cachefilename = path + "/bincimap-cache"; + FILE *fp = fopen(cachefilename.c_str(), "r"); + if (fp) do { + char inputBuffer[512]; + if (!fgets(inputBuffer, sizeof(inputBuffer), fp)) { + fclose(fp); + return false; + } + + // terminate the buffer + inputBuffer[sizeof(inputBuffer) - 1] = '\0'; + + char cacheFileVersionBuffer[512]; + + if (sscanf(inputBuffer, "%s %u %u", cacheFileVersionBuffer, + &readUidValidity, &readUidNext) != 3 + || strcmp(cacheFileVersionBuffer, BINC_CACHE) != 0) { + fclose(fp); + readUidValidity = 0; + readUidNext = 1; + mincache.clear(); + break; + } + + unsigned int readUID; + unsigned int readSize; + unsigned int readInternalDate; + char readUnique[512]; + while (fgets(inputBuffer, sizeof(inputBuffer), fp)) { + inputBuffer[sizeof(inputBuffer) - 1] = '\0'; + if (sscanf(inputBuffer, "%u %u %u %s", &readUID, + &readInternalDate, &readSize, readUnique) != 4) { + fclose(fp); + readUidValidity = 0; + readUidNext = 1; + mincache.clear(); + break; + } + + mincache[readUnique] = true; + } + + fclose(fp); + + s.setUidValidity(readUidValidity < 1 ? time(0) : readUidValidity); + } while (0); else { + s.setUidValidity(time(0)); + } + + // Scan new + DIR *dirp = opendir((path + "/new").c_str()); + if (dirp == 0) return false; + + struct dirent *direntp; + while ((direntp = readdir(dirp)) != 0) { + const string filename = direntp->d_name; + if (filename[0] == '.' + || filename.find(':') != string::npos + || filename.find('/') != string::npos) + continue; + + ++recent; + ++readUidNext; + ++unseen; + ++messages; + } + + closedir(dirp); + + // Scan cur + if ((dirp = opendir((path + "/cur").c_str())) == 0) + return false; + + while ((direntp = readdir(dirp)) != 0) { + const string dname = direntp->d_name; + if (dname[0] == '.') + continue; + + ++messages; + + // Add to unseen if it doesn't have the seen flag or if it has no + // flags. + const string::size_type pos = dname.find(':'); + if (pos != string::npos) { + if (mincache.find(dname.substr(0, pos)) == mincache.end()) { + ++recent; + ++readUidNext; + } + + if (dname.substr(pos).find('S') == string::npos) + ++unseen; + } else { + if (mincache.find(dname) == mincache.end()) { + ++recent; + ++readUidNext; + } + + ++unseen; + } + } + + closedir(dirp); + + s.setRecent(recent); + s.setMessages(messages); + s.setUnseen(unseen); + s.setUidNext(uidnext); + + return true; +} + +//------------------------------------------------------------------------ +unsigned int Maildir::getMaxUid(void) const +{ + MessageMap::const_iterator i = messages.end(); + if (i == messages.begin()) + return 0; + + --i; + for (;;) { + const MaildirMessage &message = i->second; + if (message.isExpunged()) { + if (i == messages.begin()) + return 0; + --i; + } else { + return message.getUID(); + } + } + + return 0; +} + +//------------------------------------------------------------------------ +unsigned int Maildir::getMaxSqnr(void) const +{ + int sqnr = messages.size(); + MessageMap::const_iterator i = messages.end(); + if (i == messages.begin()) + return 0; + + --i; + for (;;) { + const MaildirMessage &message = i->second; + if (message.isExpunged()) { + if (i == messages.begin()) + return 0; + --sqnr; + --i; + } else { + return sqnr; + } + } + + return 0; +} + +//------------------------------------------------------------------------ +unsigned int Maildir::getUidValidity(void) const +{ + return uidvalidity; +} + +//------------------------------------------------------------------------ +unsigned int Maildir::getUidNext(void) const +{ + return uidnext; +} + +//------------------------------------------------------------------------ +Message *Maildir::createMessage(const string &mbox, time_t idate) +{ + string sname = mbox + "/tmp/bincimap-XXXXXX"; + char *safename = strdup(sname.c_str()); + + int fd = mkstemp(safename); + if (fd == -1) { + setLastError("Unable to create safe name."); + return 0; + } + + string safenameStr = safename; + delete[] safename; + + MaildirMessage message(*this); + + message.setFile(fd); + message.setSafeName(safenameStr); + message.setInternalDate(idate); + + newMessages.push_back(message); + vector<MaildirMessage>::iterator i = newMessages.end(); + --i; + return &(*i); +} + +//------------------------------------------------------------------------ +bool Maildir::commitNewMessages(const string &mbox) +{ + Session &session = Session::getInstance(); + + vector<MaildirMessage>::iterator i = newMessages.begin(); + map<MaildirMessage *, string> committedMessages; + + struct timeval youngestFile = {0, 0}; + + bool abort = false; + while (!abort && i != newMessages.end()) { + MaildirMessage &m = *i; + + if (m.getInternalFlags() & MaildirMessage::Committed) { + ++i; + continue; + } + + m.setInternalFlag(MaildirMessage::Committed); + + string safeName = m.getSafeName(); + + for (int attempt = 0; !abort && attempt < 1000; ++attempt) { + struct timeval tv; + gettimeofday(&tv, 0); + youngestFile = tv; + + // Generate Maildir conformant file name + BincStream ssid; + ssid << (int) tv.tv_sec << "." + << "R" << (int) rand() + << "M" << (int) tv.tv_usec + << "P" << (int) session.getPid() + << "Q" << numDeliveries++ + << "." << session.getEnv("TCPLOCALHOST"); + + BincStream ss; + ss << mbox << "/new/" << ssid.str(); + + string fileName = ss.str(); + + if (link(safeName.c_str(), fileName.c_str()) == 0) { + unlink(safeName.c_str()); + m.setInternalDate(tv.tv_sec); + m.setUnique(ssid.str()); + m.setUID(0); + committedMessages[&m] = fileName; + break; + } + + if (errno == EEXIST) + continue; + + bincWarning << "link(" << toImapString(safeName) << ", " + << toImapString(fileName) << ") failed: " + << strerror(errno) << endl; + + session.setResponseCode("TRYCREATE"); + session.setLastError("failed, internal error."); + abort = true; + break; + } + + ++i; + } + + // abort means to make an attempt to restore the mailbox to + // its original state. + if (abort) { + // Fixme: Messages that are in committedMessages should be skipped + // here. + for (i = newMessages.begin(); i != newMessages.end(); ++i) + unlink((*i).getSafeName().c_str()); + + map<MaildirMessage *, string>::const_iterator j + = committedMessages.begin(); + while (j != committedMessages.end()) { + if (unlink(j->second.c_str()) != 0) { + if (errno == ENOENT) { + // FIXME: The message was probably moves away from new/ by + // another IMAP session. + bincWarning << "error rollbacking after failed commit to " + << toImapString(mbox) << ", failed to unlink " + << toImapString(j->second) + << ": " << strerror(errno) << endl; + } else { + bincWarning << "error rollbacking after failed commit to " + << toImapString(mbox) << ", failed to unlink " + << toImapString(j->second) + << ": " << strerror(errno) << endl; + newMessages.clear(); + return false; + } + } + + ++j; + } + + newMessages.clear(); + return false; + } + + // cover the extremely unlikely event that another concurrent + // Maildir accessor has just made a file with the same timestamp and + // random number by spinning until the timestamp has changed before + // moving the message into cur. + struct timeval tv; + gettimeofday(&tv, 0); + while (tv.tv_sec == youngestFile.tv_sec + && tv.tv_usec == youngestFile.tv_usec) { + gettimeofday(&tv, 0); + } + + map<MaildirMessage *, string>::const_iterator j + = committedMessages.begin(); + for (;j != committedMessages.end(); ++j) { + string basename = j->second.substr(j->second.rfind('/') + 1); + + int flags = j->first->getStdFlags(); + string flagStr; + if (flags & Message::F_DRAFT) flagStr += "D"; + if (flags & Message::F_FLAGGED) flagStr += "F"; + if (flags & Message::F_ANSWERED) flagStr += "R"; + if (flags & Message::F_SEEN) flagStr += "S"; + if (flags & Message::F_DELETED) flagStr += "T"; + + string dest = mbox + "/cur/" + basename + ":2," + flagStr; + if (rename(j->second.c_str(), dest.c_str()) == 0) + continue; + + if (errno != ENOENT) + bincWarning << "when setting flags on: " << j->second + << ": " << strerror(errno) << endl; + } + + committedMessages.clear(); + return true; +} + +//------------------------------------------------------------------------ +bool Maildir::rollBackNewMessages(void) +{ + vector<MaildirMessage>::const_iterator i = newMessages.begin(); + // Fixme: Messages that are in committedMessages should be skipped + // here. + for (; i != newMessages.end(); ++i) + unlink((*i).getSafeName().c_str()); + + newMessages.clear(); + return true; +} + +//------------------------------------------------------------------------ +bool Maildir::fastCopy(Message &m, Mailbox &desttype, + const std::string &destname) +{ + // At this point, fastCopy is broken because the creation time is + // equal for the two clones. The new clone must have a new creation + // time. Symlinks are a possibility, but they break if other Maildir + // accessors rename mailboxes. + // return false; + + Session &session = Session::getInstance(); + + MaildirMessage *message = dynamic_cast<MaildirMessage *>(&m); + if (!message) + return false; + + string mfilename = message->getFileName(); + if (mfilename == "") + return false; + + Maildir *mailbox = dynamic_cast<Maildir *>(&desttype); + if (!mailbox) + return false; + + for (int attempt = 0; attempt < 1000; ++attempt) { + + struct timeval tv; + gettimeofday(&tv, 0); + + // Generate Maildir conformant file name + BincStream ssid; + ssid << (int) tv.tv_sec << "." + << "R" << (int) rand() + << "M" << (int) tv.tv_usec + << "P" << (int) session.getPid() + << "Q" << numDeliveries++ + << "." << session.getEnv("TCPLOCALHOST"); + + BincStream ss; + ss << destname << "/tmp/" << ssid.str(); + + string fileName = ss.str(); + + if (link((path + "/cur/" + mfilename).c_str(), fileName.c_str()) == 0) { + MaildirMessage newmess = *message; + newmess.setSafeName(fileName); + newmess.setUnique(ssid.str()); + newmess.setInternalDate((time_t) tv.tv_sec); + newmess.setUID(0); + newMessages.push_back(newmess); + break; + } + + if (errno == EEXIST) + continue; + + bincWarning << "Warning: link(" + << toImapString(path + "/cur/" + mfilename) + << ", " << toImapString(fileName) << ") failed: " + << strerror(errno) << endl; + + session.setResponseCode("TRYCREATE"); + session.setLastError("failed, internal error."); + return false; + } + + return true; +} + +//------------------------------------------------------------------------ +MaildirMessage *Maildir::get(const std::string &id) +{ + MaildirIndexItem *item = index.find(id); + if (!item) + return 0; + + unsigned int uid = item->uid; + if (messages.find(uid) == messages.end()) + return 0; + + return &messages.find(uid)->second; +} + +//------------------------------------------------------------------------ +void Maildir::add(MaildirMessage &m) +{ + MessageMap::iterator it = messages.find(m.getUID()); + if (it != messages.end()) + messages.erase(it); + messages.insert(make_pair(m.getUID(), m)); + index.insert(m.getUnique(), m.getUID()); +} + +//------------------------------------------------------------------------ +unsigned int MaildirIndex::getSize(void) const +{ + return idx.size(); +} + +//------------------------------------------------------------------------ +void MaildirIndex::insert(const string &unique, unsigned int uid, + const string &fileName) +{ + if (idx.find(unique) == idx.end()) { + MaildirIndexItem item; + item.uid = uid; + item.fileName = fileName; + idx[unique] = item; + } else { + MaildirIndexItem &item = idx[unique]; + if (uid != 0) item.uid = uid; + if (fileName != "") item.fileName = fileName; + } +} + +//------------------------------------------------------------------------ +void MaildirIndex::remove(const string &unique) +{ + map<string, MaildirIndexItem>::iterator it = idx.find(unique); + if (it != idx.end()) + idx.erase(it); +} + +//------------------------------------------------------------------------ +MaildirIndexItem *MaildirIndex::find(const string &unique) +{ + map<string, MaildirIndexItem>::iterator it = idx.find(unique); + if (it != idx.end()) + return &it->second; + + return 0; +} + +//------------------------------------------------------------------------ +void MaildirIndex::clear(void) +{ + idx.clear(); +} + +//------------------------------------------------------------------------ +void MaildirIndex::clearUids(void) +{ + map<string, MaildirIndexItem>::iterator it = idx.begin(); + for (; it != idx.end(); ++it) + it->second.uid = 0; +} + +//------------------------------------------------------------------------ +void MaildirIndex::clearFileNames(void) +{ + map<string, MaildirIndexItem>::iterator it = idx.begin(); + for (; it != idx.end(); ++it) + it->second.fileName = ""; +} |