#include "MailFolder.h" #include "Log.h" #include #include #include #include #include #include #include #include namespace mail { namespace { std::string joinFlags(const std::vector &flags) { std::string out; for (size_t i = 0; i < flags.size(); ++i) { if (i > 0) out += ","; out += flags[i]; } return out; } std::vector splitFlags(const std::string &blob) { std::vector flags; std::istringstream stream(blob); std::string part; while (std::getline(stream, part, ',')) if (!part.empty()) flags.push_back(part); return flags; } bool hasFlag(const std::vector &flags, const std::string &flag) { return std::find(flags.begin(), flags.end(), flag) != flags.end(); } uint32_t readCounterFile(const std::string &path, uint32_t defaultValue) { std::ifstream in(path); uint32_t value = 0; if (in >> value) return value; return defaultValue; } void writeCounterFile(const std::string &path, uint32_t value) { std::ofstream out(path, std::ios::trunc); out << value; // ofstream doesn't throw on a failed open (e.g. a permissions // problem, a full/read-only filesystem) - confirmed live: two // pre-existing mailbox index files were root-owned mode 644 // while the mail service runs as a non-root user, so every // counter/flag persist attempt failed here completely silently // for weeks. Logging it is the only way this becomes visible // instead of just quietly not happening. if (!out) coreutils::Log(coreutils::LOG_EXCEPT) << "MailFolder: failed to write " << path << " - check ownership/permissions."; } } bool MailFolder::load(std::string folderPath) { path = folderPath; struct stat folderStat; if (stat(path.c_str(), &folderStat) != 0 || !S_ISDIR(folderStat.st_mode)) return false; // UIDVALIDITY is generated once and never changes for the life of // this folder directory - reusing the folder's own creation time // is a fine source of a value that's stable, unique enough in // practice, and never zero. mUidValidity = readCounterFile(path + "/.uidvalidity", 0); if (mUidValidity == 0) { mUidValidity = (uint32_t)time(nullptr); writeCounterFile(path + "/.uidvalidity", mUidValidity); } mUidNext = readCounterFile(path + "/.uidnext", 1); if (mUidNext < 1) mUidNext = 1; mMessages.clear(); loadIndexFile(); reconcileWithDisk(); save(); return true; } void MailFolder::loadIndexFile() { std::ifstream in(path + "/.imapindex"); if (!in) return; std::string line; while (std::getline(in, line)) { if (line.empty()) continue; std::istringstream stream(line); Message message; std::string uidStr, dateStr, flagsBlob; stream >> message.fileName >> uidStr >> dateStr; std::getline(stream, flagsBlob); // The flags blob (if any) is whatever's left after the third // field, minus the single separating space istream>> already // consumed up to. if (!flagsBlob.empty() && flagsBlob[0] == ' ') flagsBlob.erase(0, 1); message.uid = (uint32_t)std::stoul(uidStr); message.internalDate = (time_t)std::stoll(dateStr); message.flags = splitFlags(flagsBlob); mMessages.push_back(message); } } void MailFolder::reconcileWithDisk() { std::vector onDisk; if (std::filesystem::is_directory(path)) { for (const auto &entry : std::filesystem::directory_iterator(path)) { if (!entry.is_regular_file()) continue; std::string name = entry.path().filename().string(); if (!name.empty() && name[0] != '.') onDisk.push_back(name); } } // Drop indexed messages no longer present on disk (expunged some // other way than MailFolder::expunge(), or the file simply never // made it - either way, nothing to show for it). mMessages.erase(std::remove_if(mMessages.begin(), mMessages.end(), [&](const Message &m) { return std::find(onDisk.begin(), onDisk.end(), m.fileName) == onDisk.end(); }), mMessages.end()); // Anything on disk not yet indexed is newly delivered mail (or a // folder being indexed for the very first time) - assign it a UID // now and mark it \Recent for this one load() only. for (const std::string &name : onDisk) { bool indexed = std::any_of(mMessages.begin(), mMessages.end(), [&](const Message &m) { return m.fileName == name; }); if (indexed) continue; Message message; message.fileName = name; message.uid = mUidNext++; message.recent = true; struct stat fileStat; if (stat((path + "/" + name).c_str(), &fileStat) == 0) { message.size = (size_t)fileStat.st_size; message.internalDate = fileStat.st_mtime; } else { message.internalDate = time(nullptr); } mMessages.push_back(message); } // Fill in size for every message (including ones that were // already indexed - size isn't persisted, it's cheap to stat). for (Message &message : mMessages) { struct stat fileStat; if (stat((path + "/" + message.fileName).c_str(), &fileStat) == 0) message.size = (size_t)fileStat.st_size; } std::sort(mMessages.begin(), mMessages.end(), [](const Message &a, const Message &b) { return a.uid < b.uid; }); } void MailFolder::save() { writeCounterFile(path + "/.uidvalidity", mUidValidity); writeCounterFile(path + "/.uidnext", mUidNext); std::ofstream out(path + "/.imapindex", std::ios::trunc); if (!out) { coreutils::Log(coreutils::LOG_EXCEPT) << "MailFolder: failed to open " << path << "/.imapindex for writing - flag/message changes will not persist. Check ownership/permissions."; return; } for (const Message &message : mMessages) out << message.fileName << " " << message.uid << " " << (long long)message.internalDate << " " << joinFlags(message.flags) << "\n"; if (!out) coreutils::Log(coreutils::LOG_EXCEPT) << "MailFolder: write to " << path << "/.imapindex failed partway through - index may be incomplete."; } uint32_t MailFolder::seq(uint32_t uid) const { for (size_t i = 0; i < mMessages.size(); ++i) if (mMessages[i].uid == uid) return (uint32_t)(i + 1); return 0; } uint32_t MailFolder::uidForSeq(uint32_t seqNum) const { if (seqNum < 1 || seqNum > mMessages.size()) return 0; return mMessages[seqNum - 1].uid; } MailFolder::Message *MailFolder::find(uint32_t uid) { for (Message &message : mMessages) if (message.uid == uid) return &message; return nullptr; } size_t MailFolder::recentCount() const { return (size_t)std::count_if(mMessages.begin(), mMessages.end(), [](const Message &m) { return m.recent; }); } size_t MailFolder::unseenCount() const { return (size_t)std::count_if(mMessages.begin(), mMessages.end(), [](const Message &m) { return !hasFlag(m.flags, "\\Seen"); }); } uint32_t MailFolder::firstUnseenSeq() const { for (size_t i = 0; i < mMessages.size(); ++i) if (!hasFlag(mMessages[i].flags, "\\Seen")) return (uint32_t)(i + 1); return 0; } std::string MailFolder::readMessage(uint32_t uid) { Message *message = find(uid); if (!message) return ""; std::ifstream in(path + "/" + message->fileName, std::ios::binary); std::ostringstream out; out << in.rdbuf(); return out.str(); } void MailFolder::setFlags(uint32_t uid, const std::vector &flags) { Message *message = find(uid); if (!message) return; message->flags = flags; save(); } void MailFolder::addFlags(uint32_t uid, const std::vector &flags) { Message *message = find(uid); if (!message) return; for (const std::string &flag : flags) if (!hasFlag(message->flags, flag)) message->flags.push_back(flag); save(); } void MailFolder::removeFlags(uint32_t uid, const std::vector &flags) { Message *message = find(uid); if (!message) return; message->flags.erase(std::remove_if(message->flags.begin(), message->flags.end(), [&](const std::string &f) { return hasFlag(flags, f); }), message->flags.end()); save(); } void MailFolder::deleteMessage(uint32_t uid) { Message *message = find(uid); if (!message) return; unlink((path + "/" + message->fileName).c_str()); mMessages.erase(std::remove_if(mMessages.begin(), mMessages.end(), [&](const Message &m) { return m.uid == uid; }), mMessages.end()); save(); } std::string MailFolder::generateFileName() { struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); std::stringstream ss; ss << ts.tv_sec << "." << std::setfill('0') << std::setw(9) << ts.tv_nsec; return ss.str(); } uint32_t MailFolder::append(const std::string &data, const std::vector &flags, time_t internalDate) { std::string fileName = generateFileName(); std::ofstream out(path + "/" + fileName, std::ios::binary | std::ios::trunc); out << data; out.close(); Message message; message.fileName = fileName; message.uid = mUidNext++; message.flags = flags; message.size = data.size(); message.internalDate = internalDate != 0 ? internalDate : time(nullptr); mMessages.push_back(message); std::sort(mMessages.begin(), mMessages.end(), [](const Message &a, const Message &b) { return a.uid < b.uid; }); save(); return message.uid; } std::vector MailFolder::expunge() { std::vector toDelete; for (const Message &message : mMessages) if (hasFlag(message.flags, "\\Deleted")) toDelete.push_back(message); std::vector adjustedSeqs; for (size_t i = 0; i < toDelete.size(); ++i) { uint32_t originalSeq = seq(toDelete[i].uid); adjustedSeqs.push_back(originalSeq - (uint32_t)i); unlink((path + "/" + toDelete[i].fileName).c_str()); } mMessages.erase(std::remove_if(mMessages.begin(), mMessages.end(), [&](const Message &m) { return hasFlag(m.flags, "\\Deleted"); }), mMessages.end()); save(); return adjustedSeqs; } }