/** * @file maildir.cc * @brief Implementation of the Maildir class. * @author Andreas Aardal Hanssen * @date 2002-2005 */ #include "maildir.h" #include "convert.h" #include "globals.h" #include "iodevice.h" #include "iofactory.h" #include "maildirmessage.h" #include "pendingupdates.h" #include "session.h" #include "status.h" #include #include #include #include #include #include #include #include #include #include #include #include using namespace Binc; using std::endl; using std::map; using std::string; // 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(&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(const_cast(this), messages.begin(), bset, mod); beginIterator.reposition(); return Mailbox::iterator(beginIterator); } Mailbox::iterator Maildir::end(void) const { endIterator = iterator(const_cast(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 == nullptr) return false; struct dirent *direntp; while ((direntp = readdir(dirp)) != nullptr) { 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 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(nullptr) : readUidValidity); } while (0); } else { s.setUidValidity(time(nullptr)); } // Scan new DIR *dirp = opendir((path + "/new").c_str()); if (dirp == nullptr) return false; struct dirent *direntp; while ((direntp = readdir(dirp)) != nullptr) { 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())) == nullptr) return false; while ((direntp = readdir(dirp)) != nullptr) { 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"; int fd = mkstemp(sname.data()); if (fd == -1) { setLastError("Unable to create safe name."); return nullptr; } MaildirMessage message(*this); message.setFile(fd); message.setSafeName(sname); message.setInternalDate(idate); newMessages.push_back(message); return &newMessages.back(); } bool Maildir::commitNewMessages(const string &mbox) { Session &session = Session::getInstance(); std::vector::iterator i = newMessages.begin(); map 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, nullptr); youngestFile = tv; // Generate Maildir conformant file name BincStream ssid; ssid << (int)tv.tv_sec << "." << "R" << rand() << "M" << (int)tv.tv_usec << "P" << 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 (const auto &i : newMessages) unlink(i.getSafeName().c_str()); for (const auto &[_, second] : committedMessages) { if (unlink(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(second) << ": " << strerror(errno) << endl; } else { bincWarning << "error rollbacking after failed commit to " << toImapString(mbox) << ", failed to unlink " << toImapString(second) << ": " << strerror(errno) << endl; newMessages.clear(); return false; } } } 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, nullptr); while (tv.tv_sec == youngestFile.tv_sec && tv.tv_usec == youngestFile.tv_usec) gettimeofday(&tv, nullptr); for (auto &j : committedMessages) { 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) { // Fixme: Messages that are in committedMessages should be skipped here. for (const auto &i : newMessages) 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(&m); if (!message) return false; string mfilename = message->getFileName(); if (mfilename == "") return false; Maildir *mailbox = dynamic_cast(&desttype); if (!mailbox) return false; for (int attempt = 0; attempt < 1000; ++attempt) { struct timeval tv; gettimeofday(&tv, nullptr); // 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(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 nullptr; unsigned int uid = item->uid; if (messages.find(uid) == messages.end()) return nullptr; 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(std::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::iterator it = idx.find(unique); if (it != idx.end()) idx.erase(it); } MaildirIndexItem *MaildirIndex::find(const string &unique) { map::iterator it = idx.find(unique); if (it != idx.end()) return &it->second; return nullptr; } void MaildirIndex::clear(void) { idx.clear(); } void MaildirIndex::clearUids(void) { for (auto &it : idx) it.second.uid = 0; } void MaildirIndex::clearFileNames(void) { for (auto &it : idx) it.second.fileName = ""; }