Files
BARANTMail/MailFileSystem.cpp
T
Brad Arant e8b2a79907 Add authenticated domain/mailbox/alias/password admin console
Port 1027 used to be an unauthenticated console (see ServerCore's
ConsoleServer/ConsoleSession fix) that also exposed the raw SMTP
command set (AUTH/DATA/EHLO/EXPN/HELO/...) to anyone who could reach
it. It's now started only when barantmail.cfg has a real "admin"
section configured (BARANTMailConfig::hasAdmin/getAdminPort/
getAdminUser/getAdminPassword), matching the same optional-listener
pattern already used for smtpTLS/imap/pop3, and registers four real
commands instead of the SMTP command table:

  domain add|remove|list
  mailbox add|remove|list
  alias add|remove|list
  password reset

MailFileSystem gained the actual domain/mailbox/alias/password CRUD
these commands call (createDomain/removeDomain/listDomains,
createMailbox/removeMailbox/listMailboxes, createAlias/removeAlias/
listAliases, setPassword), matching its existing on-disk layout: a
domain is a directory, a mailbox is a directory with Inbox/Spam/Trash
and a .password file, an alias is a plain file listing target
mailboxes one per line.

getAliasMailboxes() was previously an unconditional stub that always
returned empty (confirmed via grep to have zero callers anywhere),
so an alias could be "created" but could never actually receive mail.
It's now wired for real: SMTPServer::inCreate() checks
ifAliasExists()/getAliasMailboxes() during local delivery and copies
the same spam-scanned message to every target mailbox (extracted into
a new deliverToMailbox() helper so the alias and single-mailbox
delivery paths share it).
2026-09-06 04:08:57 +00:00

328 lines
12 KiB
C++

#include "MailFileSystem.h"
# include "INotify.h"
# include "Log.h"
# include "File.h"
# include <filesystem>
# include <cerrno>
namespace mail {
MailFileSystem::MailFileSystem(coreutils::ZString &mailPath) : mailPath(mailPath) {
queuePath << mailPath << "/.queue/";
sendLogPath << mailPath << "/.sendlog/";
// Harmless if it already exists (EEXIST is ignored, same as
// every other error here - a failed mkdir just means the later
// File open in SendMail::deliver() throws instead, which does
// not affect whether the message itself is delivered).
mkdir(sendLogPath.c_str(), 0770);
}
bool MailFileSystem::ifMailBoxExists(coreutils::ZString &mailbox) {
coreutils::Log(coreutils::LOG_DEBUG_2) << "if exists mailbox: " << mailbox << ".";
if(stat(getMailBoxPath(mailbox).c_str(), &statbuf) != -1)
if(S_ISDIR(statbuf.st_mode))
return true;
coreutils::Log(coreutils::LOG_DEBUG_2) << "doesnt exist mailbox: " << mailbox << ".";
return false;
}
bool MailFileSystem::ifAliasExists(coreutils::ZString &alias) {
if(stat(getMailBoxPath(alias).c_str(), &statbuf) != -1)
if(S_ISREG(statbuf.st_mode))
return true;
return false;
}
coreutils::MString MailFileSystem::getMailBoxPath(coreutils::ZString &mailbox) {
mailbox.split("@", 1);
// A mailbox string with no "@" splits to a single-element list -
// mailbox[1] would then be an out-of-bounds std::vector access
// (ZString::operator[] doesn't bounds-check), not even a
// catchable exception. Previously unreachable with a safe input
// in practice; now reachable directly from a client-supplied IMAP
// LOGIN/POP3 USER username, so a malformed one (a bare "barant"
// instead of "barant@barant.com", an easy real typo) needs to
// fail cleanly here rather than read adjacent memory.
if (mailbox.getList().size() < 2)
return coreutils::MString();
coreutils::MString path;
path << mailPath << "/" << mailbox[1] << "/" << mailbox[0];
return path;
}
std::vector<coreutils::MString> MailFileSystem::getAliasMailboxes(coreutils::ZString &alias) {
// Previously an unconditional stub - always returned empty,
// confirmed by grep to have zero callers anywhere until this
// change wired real aliasing into SMTPServer::inCreate() and the
// admin console. The file format is this class's own choice
// (createAlias() below writes it): one target mailbox per line.
std::vector<coreutils::MString> returnValue;
coreutils::MString path = getMailBoxPath(alias);
try {
coreutils::File aliasFile(path, O_RDONLY);
aliasFile.read();
coreutils::ZString contents = aliasFile.asZString();
contents.split("\n");
for (auto &line : contents.getList()) {
coreutils::MString target(line);
if (!target.str().empty())
returnValue.push_back(target);
}
} catch (...) {
// Not a real alias (or unreadable) - caller sees an empty
// target list, same as before this method did anything.
}
return returnValue;
}
coreutils::ZString& MailFileSystem::getMailPath() {
return mailPath;
}
coreutils::ZString& MailFileSystem::getQueuePath() {
return queuePath;
}
coreutils::ZString& MailFileSystem::getSendLogPath() {
return sendLogPath;
}
bool MailFileSystem::checkPassword(coreutils::ZString &userName, coreutils::ZString &password) {
// File's constructor throws if the mailbox or its .password file
// doesn't exist - an unknown username needs to come back as "not
// authenticated", not an uncaught exception. Previously (as
// __SMTP_AUTH::authLogin(), before this logic moved here) this
// was only saved from crashing the whole process by
// CommandList::processRequest()'s own unrelated catch-all, which
// treated the exception as "could not be parsed" and silently
// dropped the reply entirely - a client attempting AUTH LOGIN (or
// now IMAP LOGIN, POP3 USER/PASS) against a nonexistent user got
// no response at all rather than a clean authentication failure.
try {
coreutils::MString secretPath;
secretPath << getMailBoxPath(userName) << "/.password";
coreutils::File secret(secretPath);
secret.read();
return password == secret.asZString();
} catch (...) {
return false;
}
}
coreutils::MString MailFileSystem::getFolderPath(coreutils::ZString &mailbox, std::string folder) {
coreutils::MString path;
path << getMailBoxPath(mailbox) << "/" << folder;
return path;
}
bool MailFileSystem::ifFolderExists(coreutils::ZString &mailbox, std::string folder) {
struct stat folderStat;
return stat(getFolderPath(mailbox, folder).c_str(), &folderStat) != -1 && S_ISDIR(folderStat.st_mode);
}
std::vector<coreutils::MString> MailFileSystem::listFolders(coreutils::ZString &mailbox) {
std::vector<coreutils::MString> folders;
std::string root = getMailBoxPath(mailbox).str();
if (!std::filesystem::is_directory(root))
return folders;
for (const auto &entry : std::filesystem::directory_iterator(root)) {
std::string name = entry.path().filename().string();
if (!name.empty() && name[0] != '.' && entry.is_directory())
folders.push_back(coreutils::MString(name));
}
return folders;
}
bool MailFileSystem::createDomain(coreutils::ZString &domain, std::string &error) {
coreutils::MString path;
path << mailPath << "/" << domain;
if (mkdir(path.c_str(), 0770) == -1 && errno != EEXIST) {
error = "could not create domain directory";
return false;
}
return true;
}
bool MailFileSystem::removeDomain(coreutils::ZString &domain, std::string &error) {
coreutils::MString path;
path << mailPath << "/" << domain;
if (!std::filesystem::is_directory(path.str())) {
error = "domain does not exist";
return false;
}
// Refuse rather than cascade-delete every mailbox/alias under a
// domain silently - the admin has to clear those first, same
// spirit as removeMailbox() not being implicitly called from here.
for (const auto &entry : std::filesystem::directory_iterator(path.str())) {
std::string name = entry.path().filename().string();
if (!name.empty() && name[0] != '.') {
error = "domain still has mailboxes or aliases - remove those first";
return false;
}
}
std::error_code ec;
std::filesystem::remove_all(path.str(), ec);
if (ec) {
error = "could not remove domain directory";
return false;
}
return true;
}
std::vector<coreutils::MString> MailFileSystem::listDomains() {
std::vector<coreutils::MString> domains;
std::string root = mailPath.str();
if (!std::filesystem::is_directory(root))
return domains;
for (const auto &entry : std::filesystem::directory_iterator(root)) {
std::string name = entry.path().filename().string();
if (!name.empty() && name[0] != '.' && entry.is_directory())
domains.push_back(coreutils::MString(name));
}
return domains;
}
bool MailFileSystem::createMailbox(coreutils::ZString &mailbox, coreutils::ZString &password, std::string &error) {
mailbox.split("@", 1);
if (mailbox.getList().size() < 2) {
error = "mailbox must be in the form user@domain";
return false;
}
coreutils::MString domainPath;
domainPath << mailPath << "/" << mailbox[1];
if (!std::filesystem::is_directory(domainPath.str())) {
error = "domain does not exist: " + mailbox[1].str();
return false;
}
coreutils::MString path = getMailBoxPath(mailbox);
if (std::filesystem::exists(path.str())) {
error = "a mailbox or alias already exists at that address";
return false;
}
if (mkdir(path.c_str(), 0770) == -1) {
error = "could not create mailbox directory";
return false;
}
for (std::string folder : {"Inbox", "Spam", "Trash"}) {
coreutils::MString folderPath;
folderPath << path << "/" << folder;
mkdir(folderPath.c_str(), 0770);
}
coreutils::MString passwordPath;
passwordPath << path << "/.password";
try {
coreutils::File secret(passwordPath, O_CREAT | O_WRONLY | O_TRUNC, 0660);
secret.write(password.str());
} catch (...) {
error = "could not write password file";
return false;
}
return true;
}
bool MailFileSystem::removeMailbox(coreutils::ZString &mailbox, std::string &error) {
if (!ifMailBoxExists(mailbox)) {
error = "mailbox does not exist";
return false;
}
coreutils::MString path = getMailBoxPath(mailbox);
std::error_code ec;
std::filesystem::remove_all(path.str(), ec);
if (ec) {
error = "could not remove mailbox";
return false;
}
return true;
}
std::vector<coreutils::MString> MailFileSystem::listMailboxes(coreutils::ZString &domain) {
std::vector<coreutils::MString> mailboxes;
coreutils::MString root;
root << mailPath << "/" << domain;
if (!std::filesystem::is_directory(root.str()))
return mailboxes;
for (const auto &entry : std::filesystem::directory_iterator(root.str())) {
std::string name = entry.path().filename().string();
if (!name.empty() && name[0] != '.' && entry.is_directory())
mailboxes.push_back(coreutils::MString(name));
}
return mailboxes;
}
bool MailFileSystem::createAlias(coreutils::ZString &alias, coreutils::ZString &targets, std::string &error) {
if (ifMailBoxExists(alias)) {
error = "a mailbox already exists at that address";
return false;
}
coreutils::MString path = getMailBoxPath(alias);
if (path.str().empty()) {
error = "alias must be in the form user@domain";
return false;
}
// Accept either comma- or whitespace-separated targets from the
// console command line - normalized here to one per line, which
// is this class's own on-disk format (see getAliasMailboxes()).
targets.split(",");
coreutils::MString contents;
for (auto &target : targets.getList()) {
coreutils::MString trimmed(target);
if (!trimmed.str().empty())
contents << trimmed << "\n";
}
try {
coreutils::File aliasFile(path, O_CREAT | O_WRONLY | O_TRUNC, 0660);
aliasFile.write(contents.str());
} catch (...) {
error = "could not write alias file";
return false;
}
return true;
}
bool MailFileSystem::removeAlias(coreutils::ZString &alias, std::string &error) {
if (!ifAliasExists(alias)) {
error = "alias does not exist";
return false;
}
coreutils::MString path = getMailBoxPath(alias);
if (unlink(path.c_str()) == -1) {
error = "could not remove alias file";
return false;
}
return true;
}
std::vector<coreutils::MString> MailFileSystem::listAliases(coreutils::ZString &domain) {
std::vector<coreutils::MString> aliases;
coreutils::MString root;
root << mailPath << "/" << domain;
if (!std::filesystem::is_directory(root.str()))
return aliases;
for (const auto &entry : std::filesystem::directory_iterator(root.str())) {
std::string name = entry.path().filename().string();
if (!name.empty() && name[0] != '.' && entry.is_regular_file())
aliases.push_back(coreutils::MString(name));
}
return aliases;
}
bool MailFileSystem::setPassword(coreutils::ZString &mailbox, coreutils::ZString &newPassword, std::string &error) {
if (!ifMailBoxExists(mailbox)) {
error = "mailbox does not exist";
return false;
}
coreutils::MString path;
path << getMailBoxPath(mailbox) << "/.password";
try {
coreutils::File secret(path, O_CREAT | O_WRONLY | O_TRUNC, 0660);
secret.write(newPassword.str());
} catch (...) {
error = "could not write password file";
return false;
}
return true;
}
}