IMAP (RFC 3501): LOGIN through UID, previously all stubs except CAPABILITY/LOGOUT/a partial APPEND. Multi-folder support with per-folder UID/flag indexing (new MailFolder class, backed by .uidvalidity/.uidnext/ .imapindex sidecar files), ENVELOPE support (new MessageHeaders class: header parsing, address-list parsing, IMAP quoting), and shared cursor-based argument parsing (new IMAPArgs class: sequence sets, flag lists, dates, folder-name normalization, and path-traversal protection via isValidFolderName - a bare ".." folder name would otherwise resolve into a sibling user's mailbox directory). POP3 (RFC 1939): USER through QUIT, previously only CAPA. Added UIDL. POP3 handlers use cursor-based parsing throughout rather than indexed access - ZString's copy constructor doesn't copy its `list` field, and POP3's dispatch passes the request by value, so indexed access into a copy was silently reading nothing. SMTP: DMARC evaluation added to the DATA-terminator path (DKIM verification + alignment/policy, Authentication-Results header, conditional rejection matching RFC 7489 6.3's p=reject + sampled pct=). AUTH LOGIN now handles RFC 4954's optional inline initial response (e.g. Python smtplib's "AUTH LOGIN <base64>" in one line) instead of only the exact three-line form. Outbound relay now forces IPv4 (AF_INET) instead of AF_UNSPEC - this server's IPv4 has a correct PTR record and SPF coverage, its IPv6 doesn't, and Gmail was rejecting outbound mail with 550 5.7.25 as a result. Delivery pipeline: local delivery now runs the message through SpamAssassin (new SpamAssassinFilter class, speaking spamd's PROCESS protocol directly over a raw socket - fails open/delivers unscanned if spamd is unreachable) instead of a plain hardlink, since scanning can tag/modify content. BARANTMailConfig: extracted config loading into its own class. compile: (see individual file changes - build script adjustments to match the above). Deliberately excluded from this commit: barantmail.log/ barantmail-service.log (operational logs, not source - barantmail.log in particular was the runaway 55GB/12.9GB file from a busy-loop bug fixed separately in ServerCore) and barantmail.cfg (this server's own live deployment config, matching how APIServer's equivalent config also stays untracked).
300 lines
10 KiB
C++
300 lines
10 KiB
C++
#include "MailFolder.h"
|
|
#include <algorithm>
|
|
#include <ctime>
|
|
#include <fstream>
|
|
#include <iomanip>
|
|
#include <sstream>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
#include <filesystem>
|
|
|
|
namespace mail {
|
|
|
|
namespace {
|
|
|
|
std::string joinFlags(const std::vector<std::string> &flags) {
|
|
std::string out;
|
|
for (size_t i = 0; i < flags.size(); ++i) {
|
|
if (i > 0)
|
|
out += ",";
|
|
out += flags[i];
|
|
}
|
|
return out;
|
|
}
|
|
|
|
std::vector<std::string> splitFlags(const std::string &blob) {
|
|
std::vector<std::string> 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<std::string> &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;
|
|
}
|
|
|
|
}
|
|
|
|
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<std::string> 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);
|
|
for (const Message &message : mMessages)
|
|
out << message.fileName << " " << message.uid << " " << (long long)message.internalDate
|
|
<< " " << joinFlags(message.flags) << "\n";
|
|
}
|
|
|
|
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<std::string> &flags) {
|
|
Message *message = find(uid);
|
|
if (!message)
|
|
return;
|
|
message->flags = flags;
|
|
save();
|
|
}
|
|
|
|
void MailFolder::addFlags(uint32_t uid, const std::vector<std::string> &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<std::string> &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<std::string> &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<uint32_t> MailFolder::expunge() {
|
|
std::vector<Message> toDelete;
|
|
for (const Message &message : mMessages)
|
|
if (hasFlag(message.flags, "\\Deleted"))
|
|
toDelete.push_back(message);
|
|
|
|
std::vector<uint32_t> 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;
|
|
}
|
|
|
|
}
|